Conversation
A daemon-hosted background session sits several harness-named hops below the session that launched it, with no non-harness process in between, so the whole chain reads as one contiguous harness run. Resolving this session's identity as the outermost pid of that run therefore records the LAUNCHING session's pid. When the daemon above the session later exits, that recorded pid stops being an ancestor, and because it still names a live harness the Stop-owned auto-arm reads it as a competing session and stays inert for the rest of the session. These cases build that real three-level process shape and drive the real bin/fm-lock.sh, the real Stop auto-arm, and the real turn-end guard through it, alongside the negative cases that must keep behaving as they do today: a live launching session that genuinely owns the home, a demonstrably dead owner, away mode, and an inherited session declaration.
Under a session-hosting daemon, two different arrangements produce an identical process chain: an async hook or tool call run for session X inside a daemon-hosted worker, and a background session launched BY X through that same daemon. No process-table fact separates them, so resolving identity as the outermost pid of the contiguous harness run records X in both - which is right for the worker and wrong for the background session. Prefer the harness's own declaration of which process is this session, accepted only when it names a live harness process inside this contiguous run. Membership is what makes an untrustworthy value harmless: a pid inherited from an unrelated session is not in this ancestry and is ignored, and a dead one cannot be recorded as a live owner. Everything else falls back to the outermost pid unchanged, which is what every harness that declares nothing keeps using. The self-ownership predicate is deliberately untouched. It already accepts the recorded pid anywhere in the ancestry; the defect was that the pid it was given only belonged to the session transiently.
The writer now depends on a vendor-controlled surface: if Claude Code stopped setting CLAUDE_PID, or started letting an inherited value through, identity would silently fall back to the launching session's pid and a background session's supervision would go inert mid-session again. A stub agent cannot see that. The guard launches a real session with a deliberately wrong CLAUDE_PID planted in its environment and asserts the value reported inside that session's own hook is its own live process instead, so it proves the declaration is authoritative rather than merely present. It self-skips without the opt-in, refuses a pass that checked nothing, and fails naming the harness and version. Also records the dated result and re-selects the families a change to the identity library must re-run.
A print-mode session runs its hook directly beneath itself, so it cannot show what the writer would have recorded without the declaration. A real background agent can: its hook sits two harness-named hops below the shared session-hosting daemon, and the outermost pid of that chain is the daemon itself - a process every background session in the home shares and which outlives any one of them. The guard now exercises both shapes, refuses to pass if the background chain turned out to be a single hop (which would prove nothing about the case the fix exists for), reports the pid the old resolution would have recorded, and stops the agent it started.
The live session-declaration guard printed the resolved `claude` executable path alongside its version, so the measurement recorded in docs/verification/supervision.md carried an absolute home directory. The version is the identifying fact for that evidence, so the note now prints only the version and the recorded block matches what a real run emits. The guard still resolves and requires an executable `claude`, so an absent harness continues to fail loudly instead of verifying nothing.
The declared-pid guard sources the session-lock library through a variable, which ShellCheck cannot follow. Annotate that one site with the same source=/dev/null directive the rest of the suite uses for non-constant sources, so bin/fm-lint.sh is clean again.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Intent
Make the two failing checks on the EXISTING upstream pull request kunchenguid#3122 pass, and nothing else. That PR is open against kunchenguid/firstmate main from the fork tomglenn/firstmate, branch fix/session-lock-declared-session-pid. 12 of 14 checks pass; the maintainer reviewed it favourably and will not merge only because of two failures.
Failure 1 - Lint: a single SC1090 warning at tests/fm-session-lock-ancestry.test.sh line 737, where the declared-pid guard sources the session-lock library through the $LIB variable, which ShellCheck cannot follow. The fix had to match the annotation convention the suite already uses for non-constant sources rather than inventing a new form or blanket-disabling the check. A repo-relative 'shellcheck source=bin/fm-session-lock-lib.sh' directive was tried first and rejected because making ShellCheck follow the library cascades into three new SC2031 findings about $dir being modified in a subshell; 'shellcheck source=/dev/null', the convention used across the rest of tests/, leaves bin/fm-lint.sh completely clean. That was verified locally with CI's pinned ShellCheck 0.11.0 and actionlint 1.7.12 installed via bin/fm-install-shellcheck.sh and bin/fm-install-actionlint.sh: bin/fm-lint.sh exits 0, and tests/fm-session-lock-ancestry.test.sh still passes.
Failure 2 - 'PR must be raised via no-mistakes': the PR body carries no no-mistakes-pipeline-attestation:v1 stamp matching its head, because an earlier run on this branch was deliberately cancelled before its push step. The maintainer's instruction is to complete one no-mistakes run all the way through its own push so the structured attestation stamp lands on kunchenguid/firstmate PR 3122.
Hard constraints the user set, which a reviewer reading only the diff would not know:
What Changed
bin/fm-session-lock-lib.shgainsfm_harness_declared_session_pid(), andfm_harness_ancestry_pid()now prefers the harness's ownCLAUDE_PIDdeclaration over the outermost pid of the contiguous harness run, accepting it only when it is numeric, live, and a member of that run; otherwise the outermost-pid fallback is unchanged. This stops a daemon-hosted background session from recording the launching session's pid, which made it stop recognizing its own lock once the daemon between them exited.tests/fm-session-lock-ancestry.test.shadds a declaration-guard unit case (live in-ancestry, out-of-ancestry, dead, and non-numeric declarations) plus a real three-level launcher/daemon/session fixture driving the realbin/fm-lock.sh, Stop auto-arm, and turn-end guard;lib_evalnow clearsCLAUDE_PIDunless a case sets it, and one variable-path source is annotatedshellcheck source=/dev/null.tests/fm-session-lock-declaration-live-e2e.test.shdrift guard (print-mode and--bgsessions launched with a deliberately wrongCLAUDE_PID), registers it inbin/fm-test-run.shwith abin/fm-session-lock-lib.shfamily mapping, and records the measurement and prose updates indocs/verification/supervision.md,docs/scripts.md,docs/sessionstart-nudge.md,docs/watcher-continuity.md, anddocs/verification/runtime-backends.md.Risk Assessment
✅ Low: This run's change is a single ShellCheck annotation matching the suite's existing convention, and the underlying branch logic traces correctly with portable regression coverage that reproduces the original failure, leaving only a self-correcting CI shard-balance hint.
Testing
I ran the smallest relevant automated check, the portable session-lock ancestry guard (13/13 cases pass), which is also the suite that executes the exact line the top commit annotates, so the comment cannot have broken the guard it sits inside. Because a ShellCheck comment proves nothing on its own, I also did a manual end-to-end verification of the behavior the branch exists for: a real three-level harness process chain (launching session, session-hosting daemon, background session) driving the real fm-lock.sh, Stop auto-arm, and turn-end guard, captured once with the base commit's library and once with the branch's. The transcript shows the user-visible difference plainly - before, the lock records the launching session's pid, supervision never arms, and the guard prints TURN WOULD END BLIND and blocks with exit 2; after, the lock records the background session's own declared pid, the auto-arm claims the home and rewakes supervision, and the guard stands down silently with exit 0. No screenshot applies: the affected surface is a CLI and hook output, so the operator-facing transcript is the real end-user surface. I did not verify the SC1090 clearance itself or the pipeline attestation stamp - running ShellCheck is a lint-phase action and the attestation is owned by the push phase. Everything I exercised passed and the worktree is clean.
Evidence: Before/after CLI transcript: background-session lock identity, auto-arm, and turn-end guard
Source: Before/after CLI transcript: background-session lock identity, auto-arm, and turn-end guard
BEFORE the fix (base commit 9ce69ac library) launching session (claude) pid 79598 -> session-hosting daemon (claude) pid 79603 -> background session (claude) pid 79605 [declared CLAUDE_PID=79605] -- $ fm-lock.sh (background session start) ---- lock acquired: harness pid 79598 -- state/.lock recorded at session start --------------------- 79598 <- the LAUNCHING session's pid (wrong identity) -- $ fm-claude-stop-autoarm.sh (turn end, Stop hook) --------- exit=0 supervision armed: NO - this session is unsupervised -- $ fm-turnend-guard.sh --claude (turn end, blind-turn guard) ● TURN WOULD END BLIND - SUPERVISION IS OFF ● 1 task(s) in flight, but no live watcher holds this home lock (last beat: never). ● The Stop-owned auto-arm did not claim this home either, so recovery is NOT already under way. exit=2 (0 = turn may end, 2 = turn blocked as blind) AFTER the fix (this branch, c71005b) launching session (claude) pid 80120 -> session-hosting daemon (claude) pid 80125 -> background session (claude) pid 80127 [declared CLAUDE_PID=80127] -- $ fm-lock.sh (background session start) ---- lock acquired: harness pid 80127 -- state/.lock recorded at session start --------------------- 80127 <- this background session's own pid (correct) -- $ fm-lock.sh status (after the daemon above exited) lock: held by live harness pid 80127 -- $ fm-claude-stop-autoarm.sh (turn end, Stop hook) --------- firstmate watcher wake - one supervision event needs a handling turn now. exit=2 supervision armed: yes -- $ fm-turnend-guard.sh --claude (turn end, blind-turn guard) (no banner: the guard stood down) exit=0 (0 = turn may end, 2 = turn blocked as blind)Evidence: Reproducible demo harness used to produce the transcript
Source: Reproducible demo harness used to produce the transcript
Evidence: Targeted suite run covering the annotated guard line
Source: Targeted suite run covering the annotated guard line
ok - session-lock: a declaration outside this session's ancestry is ignored for the prior identity FM_TEST_END tests/fm-session-lock-ancestry.test.sh exit=0 duration_ms=17324 gate_skip=false FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0Pipeline
Updates from git push no-mistakes
✅ **intent** - passed
✅ No issues found.
✅ **Rebase** - passed
✅ No issues found.
bin/fm-test-run.sh:469- The portable-serial balance hint for tests/fm-session-lock-ancestry.test.sh is still 1205 ms, but this branch added five real-process end-to-end cases to that suite (three-level launcher/daemon/session trees with 0.05s settle loops, plus the outsider process), so its true duration is now far higher. bin/fm-test-run.sh documents these as balance hints only — the shard partition stays complete and disjoint — so the effect is CI shard skew until the next timing-artifact refresh described in docs/fm-test-portable-shards.md, not lost coverage. Noting it rather than proposing a change, since refreshing the table is out of this run's authorised scope.✅ **Test** - passed
✅ No issues found.
bin/fm-test-run.sh tests/fm-session-lock-ancestry.test.sh- all 13 cases pass (exit=0), includingtest_bg_session_ignores_a_declaration_outside_its_own_ancestry, the case whose body executes the newly annotated( . "$LIB" && fm_harness_pid_alive ... )line at tests/fm-session-lock-ancestry.test.sh:737Manual end-to-end demo (/Users/tom/.no-mistakes/evidence/01M116395BW2CA3DG95N20XMDF/session-lock-declared-pid-demo.sh <root> <lib> <label>) run twice against the same real scripts: once with the base commit'sbin/fm-session-lock-lib.sh(git show 9ce69ac:bin/fm-session-lock-lib.sh) and once with the branch's, building a real launching-session -> session-hosting-daemon -> background-session chain from an executable namedclaudeWithin that demo: realbin/fm-lock.shat session start, realbin/fm-lock.sh statusafter the daemon above the session exited, realbin/fm-claude-stop-autoarm.shStop hook, and realbin/fm-turnend-guard.sh --claudeat turn end, plus the persistedstate/.lockcontents compared against the launcher/daemon/session pidsgit diff 9ce69ac..c71005b -- tests/fm-session-lock-ancestry.test.shreviewed to confirm the top commit adds only the# shellcheck source=/dev/nullcomment line and touches no other filegit status --porcelainafter testing - worktree clean, no transient artifacts left behind🔧 **Document** - 1 issue found → auto-fixed ✅
tests/fm-session-lock-declaration-live-e2e.test.sh:25- The new opt-in guard's header tells readers to run it "before trusting refreshed evidence in docs/verification/runtime-backends.md", but this guard's evidence and its run command live in docs/verification/supervision.md (lines 302-317), and the line this same branch added to runtime-backends.md explicitly disclaims that ownership ("supervision.md owns that evidence and its own opt-in drift guard"). The pointer sends a maintainer refreshing post-upgrade evidence to the wrong document. Left unfixed deliberately: the user intent puts "any documentation beyond what this annotation itself requires" and any change touching another file out of scope, and states findings in those areas are not defects in this change. Suggested follow-up: change that one pointer to docs/verification/supervision.md in a later commit.🔧 Fix: point declaration guard header at supervision.md
✅ Re-checked - no issues remain.
✅ **Push** - passed
✅ No issues found.