Skip to content

merge: sync upstream/main into fork main - #2

Merged
chipssss merged 10 commits into
mainfrom
fm/fm-upstream-sync
Aug 8, 2026
Merged

chipssss merged 10 commits into
mainfrom
fm/fm-upstream-sync

Conversation

@chipssss

@chipssss chipssss commented Aug 8, 2026

Copy link
Copy Markdown

What

Merges upstream/main (kunchenguid/firstmate, 833a9a2) into this fork's main as a true merge commit, not a rebase - our main is published.

Divergence, as actually measured

The task brief assumed 9 local custom commits on our side.
The repository says otherwise: the merge base is 8398d31, which is already in upstream, so this fork carries exactly one local commit ahead of upstream.
Upstream carries the expected 9.

git rev-list --count upstream/main..HEAD -> 1
git rev-list --count HEAD..upstream/main -> 9

Local commit preserved:

Upstream commits taken in full:

Commit Title
4b6b89d fix: move network checks off the session-start blocking path (kunchenguid#1860)
d8bb074 fix(procevent): apply remote replies during capture (kunchenguid#1831)
073cb30 feat(skills): port internal stow curation disciplines to the public skill (kunchenguid#1841)
70aeba8 chore(bootstrap): raise lavish-axi version floor to 0.1.46 (kunchenguid#1865)
06b33aa fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (kunchenguid#1917)
60eb534 feat(startup-network): record per-step elapsed times for the deferred stage (kunchenguid#1918)
167ff42 feat(stow): cascade the internal /stow to every registered secondmate (kunchenguid#1928)
be32879 fix(remote-job): stop workers abandoned by a pruned code root (kunchenguid#1927)
833a9a2 feat(bin): lint only the changed shard locally, full lint in CI (kunchenguid#1925)

Conflicts

There were no textual conflicts.
Three files that both sides touched auto-merged on non-overlapping regions.
Each was inspected after the merge rather than trusted, because a clean auto-merge can still be semantically wrong:

File Both sides touched Verification Outcome
bin/fm-test-run.sh Ours added fm-worktree-env-sync.test.sh to the backend-dispatch family and bin/fm-worktree-env-sync.sh to the changed-path family map; upstream edited other family lists in 4b6b89d, 167ff42, be32879 Both local hunks confirmed present at lines 196 and 932 Both sides kept, no adaptation needed
docs/configuration.md Ours added the "Worktree environment synchronization" section; upstream edited neighbouring sections in 4b6b89d, d8bb074, 167ff42 Section confirmed present Both sides kept
docs/scripts.md Ours added the fm-worktree-env-sync.sh row; upstream added rows for fm-startup-network.sh, fm-timing-lib.sh, fm-stow-cascade.sh, fm-remote-job-reap-orphans.sh Our row confirmed present; fm-doc-audience-check.sh passes Both sides kept

bin/fm-spawn.sh carries our env-sync call and upstream did not touch it, so it merged untouched.

Adaptation checked and found unnecessary

Upstream 833a9a2 changed bin/fm-lint.sh to lint only the changed shard locally.
Its canonical file set is still the glob bin/*.sh bin/backends/*.sh tests/*.sh, so our bin/fm-worktree-env-sync.sh is picked up automatically with no registration.

Our test is absent from portable_serial_weight_hints() in bin/fm-test-run.sh.
That list is documented in-file as balance hints only - the shard partition stays complete and disjoint regardless - so this costs shard balance, not coverage, and needs no change here.

Verification

Lint, full canonical set, pinned ShellCheck 0.11.0:

CI=true bin/fm-lint.sh
fm-lint.sh: ShellCheck 0.11.0 (pinned 0.11.0)   # exit 0

Tests run - the customization's own tests, the tests guarding the three auto-merged files, and every upstream-touched test file:

Test Result
fm-worktree-env-sync pass
fm-spawn-worktree-settle pass
fm-lint pass
fm-test-run pass
fm-test-isolation-proof pass
fm-documentation-audiences pass
fm-doc-audience-check.sh pass, surfaces=65 local_links=214
fm-bootstrap pass
fm-startup-network pass
fm-stow-cascade pass
fm-remote-job-orphan-reap pass
fm-remote-job pass
fm-remote-reply pass
fm-remote-backlog-handoff pass
fm-turnend-guard pass
fm-procevent pass
fm-pending-reply pass
fm-wake-drain-open-decisions pass
fm-wake-drain-open-decisions-cursor pass
fm-session-start 1 failure, pre-existing upstream

The one failure

not ok - successful Herdr husk recovery should stay non-actionable (unexpected: 'SECONDMATE_LIVENESS:')

This is not a merge regression.
The identical assertion fails on a detached checkout of upstream/main in the same environment, so it arrives with the upstream commits rather than from this merge.
It is left as-is: fixing an upstream defect is outside this sync's scope.

Not done, deliberately

  • No rebase of published history, and no force push.
  • No changes under data/, state/, config/, or projects/ - all gitignored and untouched by the merge.
  • The no-mistakes pipeline was skipped at the captain's explicit instruction during this task; lint and the test evidence above were run directly instead.

kunchenguid and others added 10 commits August 6, 2026 20:08
…guid#1860)

* perf(session-start): run every network check off the blocking path

The session-start digest runs on a session-open hook that blocks session
initialization, and every external-network call it made was individually
unbounded: `gh auth status`, secondmate liveness, secondmate convergence,
pending remote handoff delivery, and the fleet-sync fetch. One unreachable
remote secondmate could consume the whole FM_SESSION_START_TIMEOUT and
truncate the digest, so a slow network could cost the work queue itself.
Measured against a host hanging 25s per SSH connection, that startup took
1m18s.

The digest is now composed from local reads alone. bin/fm-startup-network.sh
runs the same checks concurrently in a bounded detached worker and the digest
harvests whatever finished, without ever waiting. Same fixture: 0.84s.

Nothing is dropped. fm-bootstrap.sh stays the single owner of every sweep and
still runs all of them, through a new FM_BOOTSTRAP_NETWORK phase split whose
`skip` and `only` halves are a partition of the unsplit run. Deferral is safe
because the sweeps are idempotent detectors, the result is durable and always
surfaces (inline, or as a `check: startup-network` wake), and the worker
re-verifies that the fleet lock still names the session that asked before it
mutates anything. While the worker is still running the digest names exactly
what is unconfirmed rather than implying it passed.

A relaunch performed by the deferred pass is now always reported, because the
digest that printed the superseded endpoint record is already out.

Also collapses the duplicate tasks-axi compatibility probe: the verdict is
computed once and handed to the bootstrap child for one process hop, then
consumed so it never reaches a spawned agent's environment. 10 tasks-axi
invocations per startup become 7.

Verified on Claude Code 2.1.222 that a worker detached by the session-open
hook survives the hook returning, the one vendor behavior this design needs
and no portable test can see.

Re-landed on current main, superseding PR kunchenguid#1845, which was cut from a
pre-kunchenguid#1842 base. The digest's section numbering in AGENTS.md section 3 now
states the emission order directly - supervision block and its read-once
contract, fleet state, network checks, then context - which keeps kunchenguid#1826's
fleet-state-before-context ordering. The old-bin test shim keeps main's
git-archive baseline from kunchenguid#1851, which already subsumes this branch's reason
for widening that shim.

* docs(verification): re-measure the deferred startup stage on the current base

Re-runs the unreachable-remote latency fixture against default-branch tip
8398d31 rather than the now-historical 345de4e, and records the sweep-result
comparison the deferral's safety argument rests on: the deferred worker's
published report is byte-identical to the three sweep lines the blocking
baseline printed, with the unreachable route preserved in both.

* no-mistakes(review): Fail deferred startup when report publication fails

* no-mistakes(document): Document deferred startup network behavior accurately
* fix(procevent): apply a captured adapter result in code, not by instruction

A remote secondmate's reply was captured and announced, but never applied.
Nothing dispatched the reply adapter's `handle` on a `procevent remote-reply`
wake, and the handling instruction named only the generic acknowledgement, so
the wake was retired while everything it carried was dropped: the reply never
reached the secondmate's local status mirror, the request it answered kept
escalating as a missed report, and the relay - whose registration each capture
retires, and which only that same handling re-arms - was left dead until the
next session start armed it again.

Applying such a result carries no judgement, so it belongs in code. After
publishing, the runner now calls
`bin/fm-procevent-<adapter>.sh autohandle <source-id> <sequence> <result-file>`
and lets the adapter apply and acknowledge its own result, through the same kind
of seam that already owns the terminal verdict. It runs strictly after terminal
retirement, because a handling adapter re-arms its own next source and retiring
afterwards would drop that fresh registration. An adapter with no such command,
or one whose pass does not complete, leaves the result unacknowledged and
therefore still announced, so a handler receives it exactly as before.

Resolving the request was not enough on its own either. An escalation opens a
durable keyed decision in the parent status log, and nothing ever closed it, so
a request the remote had answered kept surfacing in every later open-decisions
fold. The pending-reply library now owns both ends of that decision: it opens
one under a per-request key rather than the shared default key, and closes it
once the record resolves, appending the closing line only while that exact
decision is still open in the fold so it can neither double-close nor clear an
unrelated decision that has since taken the same key.

The handling instruction still routes a wake to its adapter, now as the
idempotent confirmation of what the runner already did rather than as the
guarantee.

Verified end to end in a throwaway isolated home driving the real armed source,
blocking delta reader, runner, and wake queue, with the handler doing only the
generic acknowledgement and no part of the ingest stubbed: before, seven failed
observations reproducing the incident; after, none. Each half is independently
load-bearing - without the runner change the reply never reaches the mirror,
without the escalation close the settled request still surfaces as an open
decision.

* no-mistakes(review): Prevent legacy reply closure from masking decisions

* no-mistakes(review): Serialize pending reply resolution and escalation closure

* no-mistakes(review): Serialize pending reply escalation with resolution

* no-mistakes(review): Clarify guarded legacy escalation closure behavior

* no-mistakes(review): Guard legacy closure and reserve pending reply keys

* no-mistakes(review): Match pending reply escalations by construction

* no-mistakes(document): Document automatic remote reply resolution

* no-mistakes(lint): Fix unused concurrent escalation loop variable

* no-mistakes(lint): Fix unused concurrent resolution loop binding

* no-mistakes(review): Version fold cache and gate autohandle on publication

* no-mistakes(document): Clarify remote reply relay documentation
…kill (kunchenguid#1841)

Bring the public installer-facing stow skill up to the internal skill's
current curation behavior while keeping it fully standalone:

- Replace the total-capture thesis with the compact-operating-map framing.
- Add read-the-destination-before-writing with the inspect-then-update
  triad (supersedes what, one-sentence rewrite, delete stale now).
- Add the concrete prune list together with its unique-fact guard, as an
  accuracy discipline with no size-budget machinery.
- Curate every memory file the pass has open, not only the routed one.
- Add the standing-decisions sweep category.
- Add the stronger-owner pointer-over-copy test before filing.
- Add tool-agnostic task-note discipline (inspect, classify, considered
  replacement body, never blind-append) and blocked-on recording.
- Give .stow-notes.md a closed set of three exits.
- Forbid storing, creating, or editing a skill as a stow destination.
- Report per-file action verbs in the completion receipt.
- Consolidate the repeated local-vs-external and .gitignore prose and fix
  the second-person voice slip, so the file does not grow (11334 -> 11276
  bytes).
…nchenguid#1917)

* fix(hooks): keep tracked Claude entries inert under grok 1.0.0 hooks

Grok loads Claude-compatible settings, so the tracked `.claude/settings.json`
hook entries also fire under Grok. They were meant to be inert there, guarded
by `[ -z "${GROK_AGENT:-}" ] || exit 0`. That guard silently stopped working.

Verified from the live process environment of a wedged grok 1.0.0 Stop hook on
2026-08-07: a grok 1.0.0 HOOK process carries GROK_HOOK_EVENT, GROK_HOOK_NAME,
GROK_SESSION_ID, and GROK_WORKSPACE_ROOT, but no GROK_AGENT. The observed hook
process was labelled `GROK_HOOK_NAME=project/settings:stop[0].hooks[1]`, which
is the Claude-only auto-arm entry.

Consequence: Grok ran `bin/fm-claude-stop-autoarm.sh` synchronously. Grok has
no `asyncRewake`, so it waited on the foregrounded watcher for that entry's
declared 28800-second timeout and the Grok turn never ended - the operator saw
an infinite "Responding".

Widen the guard to `[ -z "${GROK_AGENT:-}${GROK_HOOK_EVENT:-}" ] || exit 0` on
the five entries that have a `.grok/hooks/` counterpart: both Stop entries, the
SessionStart entry, and the two PreToolUse Bash entries.

Two deliberate limits:

- The guard is NOT widened to GROK_SESSION_ID. Grok injects it into every child
  process, so it can survive into a Claude session that Grok launched and would
  silently disable Claude's own watcher continuity. GROK_HOOK_EVENT is
  per-hook-invocation and does not leak that way.
- `bin/fm-subagent-pretool-check.sh` stays unguarded on purpose. It is the one
  tracked entry with no `.grok/hooks/` counterpart, so guarding it would remove
  the guard from Grok entirely rather than deduplicate it. The new test asserts
  it stays unguarded so the exception cannot be closed silently, and
  docs/subagent-guard.md is honest that the coverage it leaves is partial.

`bin/fm-harness.sh` corrects a comment that presented GROK_AGENT as reliably
present; it is a fast path only, and the ancestry walk is what actually
guarantees grok identification.

tests/fm-turnend-guard.test.sh adds test_tracked_claude_entries_inert_under_grok,
which runs every tracked entry under a real grok 1.0.0 hook environment, a
legacy GROK_AGENT environment, and a native Claude environment.

* no-mistakes(document): docs: sync grok hook-marker guard facts to owners

* no-mistakes(review): docs: state grok guard criterion by event coverage
… stage (kunchenguid#1918)

The deferred network stage published one aggregate started/finished pair, so
a run that took a minute could not be attributed to a phase, a host, or a
clone without re-running it by hand under manual tracing.

Add bin/fm-timing-lib.sh as the single owner of elapsed-time records, and
bracket each network owner with one: the gh auth probe, the secondmate
liveness sweep, secondmate convergence, pending handoff delivery, and the
project clone refresh, plus one record per secondmate for the remote-touching
steps (id and host) and one per project clone. Each record carries a start
offset from one shared origin, so the artifact reads as a timeline.

The stage publishes them beside its report as state/.startup-network.timings,
for a timed-out or failed run too, where the partial record is the answer.
Only the on-demand `report` command prints them: `harvest` composes the
session-start digest, so its output, the wake cadence, and every other part
of a normal session start are unchanged.

Recording is inert unless a run asks for it, so nothing else that sources
these scripts pays for it. Details are identities only - a detail carrying
whitespace is refused rather than cleaned up, which is what keeps a command
line, an environment dump, or a captured error out of the file.

Split two per-item loop bodies into their own functions so each iteration can
be timed; every `continue` became a `return 0` with the same meaning, and the
sweeps still run directly, in the same order, returning the same results.
…kunchenguid#1928)

* feat(stow): cascade the internal /stow to every registered secondmate

Invoked in a primary home, /stow now sweeps every registered secondmate
after the primary's own required pass, enforcing the same startup-memory
threshold in each home against that home's own allowance rather than a
fleet total.

bin/fm-stow-cascade.sh owns the mechanical inputs: it enumerates each
registered secondmate exactly once from data/secondmates.md, reports that
home's own budget accounting, and resolves how the sweep reaches it. A
live agent sweeps its own home so its uncaptured session knowledge is
captured too; a local home without one is curated in place; a remote home
without one is accounted read-only and deferred, because there is no
generic remote write path for a home's own memory files. Every host-
crossing step and each home's accounting runs under one hard bound, so a
slow or unreachable home reports an exception and the sweep continues.

Nothing changes until /stow is invoked: no new notification, digest
section, or background work. The public skills/stow skill is untouched.

* no-mistakes(review): fix(stow): extend cascade --help range to include full exit-code contract
…nguid#1927)

29 fm-remote-job-worker.sh processes were found running at ppid 1, 1-2 days
old, each still polling and appending to a log inside a no-mistakes gate
worktree that had already been returned.

Three things combined to make that possible:

- The recorded worker.pid is the serving child, not the restart supervisor
  above it, so a teardown that stops that one pid only makes the supervisor
  respawn. The Linux start path also left the worker tree in the launching
  command's process group, so there was no group to signal instead.
- Neither the serving loop nor the supervisor ever rechecked whether its
  configured FM_ROOT still existed, so a worker launched from a worktree
  outlived that worktree indefinitely.
- The supervisor restarted a failing child with a fixed 0.1s delay and no
  bound, which is what grew the logs (~66MB/day measured).

The Linux start path now puts the worker tree in its own process group, and
fm_remote_job_stop_worker_tree signals that whole group - refusing any group
whose leader is not itself a worker, so a worker from an older build or from
launchd's own session is still stopped safely as a single process. The worker
stops itself once its code root stops being a Firstmate checkout, confirmed
across a grace window so an ordinary transient cannot stop a healthy worker.
The supervisor backs off and gives up rather than restarting forever.

bin/fm-remote-job-reap-orphans.sh is the belt-and-suspenders sweep for workers
already orphaned that way, wired into fm-teardown.sh. Its reap condition is
exactly "the code root named in the worker's own command line is gone", which
is why the account's healthy LaunchAgent worker and every live remote
secondmate worker are never candidates.

The two suites that leaked these in the first place now stop the worker tree
rather than the recorded pid alone.
…henguid#1925)

* fix(bin): lint only the changed shard locally, full lint in CI

Two ships hitting fm-lint.sh at once could spike CPU to 190% and load
to 8.58 on a captain's Mac, even though each run finishes quickly.
fm-lint.sh now defaults to linting only the canonical-set files
changed since the merge-base with origin/main (including uncommitted
edits) on an ordinary local branch, using plain local git with no
network calls. It still lints the full canonical set in CI
(GITHUB_ACTIONS=true or CI=true), on the main branch, or whenever no
merge-base can be found, so CI coverage never depends on a local diff.
Explicit paths keep bypassing this selection entirely.

* no-mistakes: apply CI fixes
Brings in the 9 upstream commits since 8398d31 and keeps this fork's local
worktree env-sync customization intact.

Upstream commits merged:
- 4b6b89d fix: move network checks off the session-start blocking path (kunchenguid#1860)
- d8bb074 fix(procevent): apply remote replies during capture (kunchenguid#1831)
- 073cb30 feat(skills): port internal stow curation disciplines to the public skill (kunchenguid#1841)
- 70aeba8 chore(bootstrap): raise lavish-axi version floor to 0.1.46 (kunchenguid#1865)
- 06b33aa fix(bin): keep tracked Claude hook entries inert under grok 1.0.0 (kunchenguid#1917)
- 60eb534 feat(startup-network): record per-step elapsed times for the deferred stage (kunchenguid#1918)
- 167ff42 feat(stow): cascade the internal /stow to every registered secondmate (kunchenguid#1928)
- be32879 fix(remote-job): stop workers abandoned by a pruned code root (kunchenguid#1927)
- 833a9a2 feat(bin): lint only the changed shard locally, full lint in CI (kunchenguid#1925)

Local commit preserved:
- a6f0924 feat: sync configured env into task worktrees (#1)

No textual conflicts. Three files auto-merged on adjacent regions
(bin/fm-test-run.sh, docs/configuration.md, docs/scripts.md) and each local
hunk was verified present after the merge.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: a73c1b214c

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread bin/fm-test-run.sh
bin/fm-backlog-handoff.sh|bin/fm-backlog-receive.sh|bin/fm-procevent-remote-reply.sh|\
bin/fm-config-inherit-lib.sh|bin/fm-config-push.sh|bin/fm-shared*)
bin/fm-config-inherit-lib.sh|bin/fm-config-push.sh|bin/fm-shared*|\
bin/fm-stow-cascade.sh)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Map the public stow skill into changed-test selection

Captain, this commit changes skills/stow/SKILL.md, but the changed-path selector only adds bin/fm-stow-cascade.sh; running bin/fm-test-run.sh --list --changed --base 87f0399e^ exits with no changed-test mapping for source path: skills/stow/SKILL.md before selecting any tests. Add an explicit family mapping or intentional ignore rule for the public skill so the documented changed-test workflow remains usable on this commit.

Useful? React with 👍 / 👎.

@chipssss
chipssss merged commit 66c2fd1 into main Aug 8, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants