Skip to content

fix(bin): record the harness-declared session pid as the lock identity - #2

Open
tomglenn wants to merge 11 commits into
mainfrom
fix/session-lock-declared-session-pid
Open

tomglenn wants to merge 11 commits into
mainfrom
fix/session-lock-declared-session-pid

Conversation

@tomglenn

Copy link
Copy Markdown
Owner

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:

  • The ONLY authorised code change is the one-line SC1090 annotation. Do not touch bin/fm-session-lock-lib.sh, bin/fm-lock.sh, or any other file's logic.
  • The nine commits already on this branch are reviewed and must not be rewritten. No force-push, no history rewrite, no merge.
  • Do not open a new PR and do not change PR 3122's base. Remotes are already correct and must not be reconfigured: origin is the fork tomglenn/firstmate and is the push target; upstream is kunchenguid/firstmate and is never pushed to.
  • Budget is firm at one validation round. Deliberately out of scope: any change touching another file, any new standing rule, guideline, abstraction or subsystem, and any documentation beyond what this annotation itself requires. Findings in those areas should not be treated as defects in this change.

What Changed

  • bin/fm-session-lock-lib.sh gains fm_harness_declared_session_pid(), and fm_harness_ancestry_pid() now prefers the harness's own CLAUDE_PID declaration 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.sh adds 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 real bin/fm-lock.sh, Stop auto-arm, and turn-end guard; lib_eval now clears CLAUDE_PID unless a case sets it, and one variable-path source is annotated shellcheck source=/dev/null.
  • Adds the opt-in tests/fm-session-lock-declaration-live-e2e.test.sh drift guard (print-mode and --bg sessions launched with a deliberately wrong CLAUDE_PID), registers it in bin/fm-test-run.sh with a bin/fm-session-lock-lib.sh family mapping, and records the measurement and prose updates in docs/verification/supervision.md, docs/scripts.md, docs/sessionstart-nudge.md, docs/watcher-continuity.md, and docs/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)

==============================================================
  BEFORE the fix (base commit 9ce69ac library)
  session-lock lib: /tmp/fm-session-lock-lib.base.sh
==============================================================

-- the real process chain the background session ran inside ---
   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
   exit=0

-- state/.lock recorded at session start ---------------------
   79598  <- the LAUNCHING session's pid (wrong identity)

-- $ fm-lock.sh status         (after the daemon above exited) 
   lock: held by live harness pid 79598

-- $ fm-claude-stop-autoarm.sh (turn end, Stop hook) ---------
   exit=0   (2 = this session claimed its home and rewoke supervision)
   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.
   ●  arm supervision
   ●━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
   exit=2   (0 = turn may end, 2 = turn blocked as blind)

==============================================================
  AFTER the fix (this branch, c71005b)
  session-lock lib: /Users/tom/.no-mistakes/worktrees/7f843bcc4249/01M116395BW2CA3DG95N20XMDF/bin/fm-session-lock-lib.sh
==============================================================

-- the real process chain the background session ran inside ---
   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
   exit=0

-- 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.
   stale: demo-win actionable
   Run bin/fm-wake-drain.sh first, handle the wake, then run its exact WAKE_ACK_REQUIRED --ack-through command. Until that post-handling acknowledgement, interruption leaves the wake durable for idempotent re-handling. This Stop hook owns watcher continuity: when the handling turn ends, the next needed cycle arms automatically - do NOT run bin/fm-watch-arm.sh after an ordinary wake.
   exit=2   (2 = this session claimed its home and rewoke supervision)
   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

#!/usr/bin/env bash
# Manual end-to-end demo of the branch's user-visible behaviour, run against a
# chosen copy of bin/fm-session-lock-lib.sh so the same real scripts can be
# exercised before and after the fix.
#
# Shape: a real three-level harness chain built from an executable named
# "claude" - launching session -> session-hosting daemon -> background session.
# The background session runs the REAL bin/fm-lock.sh at session start with the
# harness declaration (CLAUDE_PID) naming its own process, the daemon above it
# then exits, and the session finishes its turn through the REAL Stop auto-arm
# and the REAL turn-end guard. Everything printed below is what those commands
# print to an operator.
#
# usage: session-lock-declared-pid-demo.sh <repo-root> <lib-to-use> <label>
set -u
ROOT=$1 LIB_SRC=$2 LABEL=$3

HOME_DIR=$(mktemp -d "${TMPDIR:-/tmp}/fm-lock-demo.XXXXXX")
cleanup() { rm -rf "$HOME_DIR"; }
trap cleanup EXIT

mkdir -p "$HOME_DIR/state" "$HOME_DIR/bin" "$HOME_DIR/fakebin"
git init -q "$HOME_DIR"
git -C "$HOME_DIR" -c user.name=demo -c user.email=demo@example.invalid commit -q --allow-empty -m init
: > "$HOME_DIR/AGENTS.md"
: > "$HOME_DIR/state/task.meta"

for f in fm-claude-stop-autoarm.sh fm-primary-scope-lib.sh fm-supervision-lib.sh \
         fm-wake-lib.sh fm-cursor-lib.sh fm-hook-host-lib.sh fm-lock.sh fm-turnend-guard.sh; do
  cp "$ROOT/bin/$f" "$HOME_DIR/bin/$f"
done
cp "$LIB_SRC" "$HOME_DIR/bin/fm-session-lock-lib.sh"
chmod +x "$HOME_DIR/bin/fm-lock.sh" "$HOME_DIR/bin/fm-claude-stop-autoarm.sh" "$HOME_DIR/bin/fm-turnend-guard.sh"

cat > "$HOME_DIR/bin/fm-watch-arm.sh" <<'SH'
#!/usr/bin/env bash
echo "$$" >> "$FM_HOME/state/arm-ran"
printf 'watcher: started pid=%s (beacon fresh)\n' "$$"
printf 'stale: demo-win actionable\n'
exit 0
SH
cat > "$HOME_DIR/bin/fm-supervision-instructions.sh" <<'SH'
#!/usr/bin/env bash
printf 'arm supervision\n'
SH
cat > "$HOME_DIR/bin/fm-watch.sh" <<'SH'
#!/usr/bin/env bash
exit 0
SH
chmod +x "$HOME_DIR/bin/fm-watch-arm.sh" "$HOME_DIR/bin/fm-supervision-instructions.sh" "$HOME_DIR/bin/fm-watch.sh"

ln -s /bin/bash "$HOME_DIR/fakebin/claude"
CLAUDE_BIN="$HOME_DIR/fakebin/claude"

cat > "$HOME_DIR/launcher.sh" <<'SH'
#!/usr/bin/env bash
i=0
while [ "$i" -lt 400 ] && [ "$(ps -o ppid= -p $$ 2>/dev/null | tr -d ' ')" != 1 ]; do
  sleep 0.05; i=$((i + 1))
done
printf '%s\n' "$$" > "$FM_HOME/state/launcher-pid"
"$FM_CLAUDE_BIN" "$FM_HOME/daemon.sh" &
i=0
while [ "$i" -lt 900 ] && [ ! -e "$FM_HOME/state/finished" ]; do
  sleep 0.05; i=$((i + 1))
done
SH

cat > "$HOME_DIR/daemon.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$$" > "$FM_HOME/state/daemon-pid"
"$FM_CLAUDE_BIN" "$FM_HOME/session.sh" &
i=0
while [ "$i" -lt 600 ] && [ ! -e "$FM_HOME/state/lock-done" ]; do
  sleep 0.05; i=$((i + 1))
done
exit 0
SH

cat > "$HOME_DIR/session.sh" <<'SH'
#!/usr/bin/env bash
printf '%s\n' "$$" > "$FM_HOME/state/session-pid"
# What Claude Code exports into every process it spawns for a session.
export CLAUDE_PID=$$
"$FM_HOME/bin/fm-lock.sh" > "$FM_HOME/state/session-lock.out" 2>&1
printf '%s\n' "$?" > "$FM_HOME/state/session-lock.rc"
cp "$FM_HOME/state/.lock" "$FM_HOME/state/lock-after-start" 2>/dev/null
: > "$FM_HOME/state/lock-done"
# Wait until the session-hosting daemon above has gone, as it does mid-session.
i=0
while [ "$i" -lt 600 ] && [ "$(ps -o ppid= -p $$ 2>/dev/null | tr -d ' ')" != 1 ]; do
  sleep 0.05; i=$((i + 1))
done
"$FM_HOME/bin/fm-lock.sh" status > "$FM_HOME/state/status.out" 2>&1
"$FM_HOME/bin/fm-claude-stop-autoarm.sh" </dev/null > "$FM_HOME/state/hook.out" 2>&1
printf '%s\n' "$?" > "$FM_HOME/state/hook.rc"
printf '%s' '{"session_id":"demo","stop_hook_active":false}' \
  | "$FM_HOME/bin/fm-turnend-guard.sh" --claude > "$FM_HOME/state/guard.out" 2>&1
printf '%s\n' "$?" > "$FM_HOME/state/guard.rc"
: > "$FM_HOME/state/finished"
SH
chmod +x "$HOME_DIR/launcher.sh" "$HOME_DIR/daemon.sh" "$HOME_DIR/session.sh"

env FM_HOME="$HOME_DIR" FM_CLAUDE_BIN="$CLAUDE_BIN" \
  bash -c '"$0" "$1" &' "$CLAUDE_BIN" "$HOME_DIR/launcher.sh"
i=0
while [ "$i" -lt 900 ] && [ ! -e "$HOME_DIR/state/finished" ]; do
  sleep 0.05; i=$((i + 1))
done
[ -e "$HOME_DIR/state/finished" ] || { echo "demo tree never finished"; exit 1; }

r() { tr -d '[:space:]' < "$HOME_DIR/state/$1" 2>/dev/null; }
launcher=$(r launcher-pid); daemon=$(r daemon-pid); session=$(r session-pid); recorded=$(r lock-after-start)

echo "=============================================================="
echo "  $LABEL"
echo "  session-lock lib: $LIB_SRC"
echo "=============================================================="
echo
echo "-- the real process chain the background session ran inside ---"
printf '   launching session (claude) pid %s\n     -> session-hosting daemon (claude) pid %s\n       -> background session (claude) pid %s   [declared CLAUDE_PID=%s]\n' \
  "$launcher" "$daemon" "$session" "$session"
echo
echo '-- $ fm-lock.sh                (background session start) ----'
sed 's/^/   /' "$HOME_DIR/state/session-lock.out"
echo "   exit=$(r session-lock.rc)"
echo
echo "-- state/.lock recorded at session start ---------------------"
if [ "$recorded" = "$session" ]; then
  echo "   $recorded  <- this background session's own pid (correct)"
elif [ "$recorded" = "$launcher" ]; then
  echo "   $recorded  <- the LAUNCHING session's pid (wrong identity)"
else
  echo "   $recorded  <- neither this session ($session) nor its launcher ($launcher)"
fi
echo
echo '-- $ fm-lock.sh status         (after the daemon above exited) '
sed 's/^/   /' "$HOME_DIR/state/status.out"
echo
echo '-- $ fm-claude-stop-autoarm.sh (turn end, Stop hook) ---------'
sed 's/^/   /' "$HOME_DIR/state/hook.out"
echo "   exit=$(r hook.rc)   (2 = this session claimed its home and rewoke supervision)"
[ -e "$HOME_DIR/state/arm-ran" ] \
  && echo "   supervision armed: yes" || echo "   supervision armed: NO - this session is unsupervised"
echo
echo '-- $ fm-turnend-guard.sh --claude   (turn end, blind-turn guard)'
if [ -s "$HOME_DIR/state/guard.out" ]; then sed 's/^/   /' "$HOME_DIR/state/guard.out"; else echo "   (no banner: the guard stood down)"; fi
echo "   exit=$(r guard.rc)   (0 = turn may end, 2 = turn blocked as blind)"
echo
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=0

FM_TEST_BEGIN 2026-08-27T08:50:41Z tests/fm-session-lock-ancestry.test.sh family=watcher-wake-lock expected_gate_skip=none
ok - session-lock: a version-named Claude Code session is identified from its install path and argv[0]
ok - session-lock: ordinary script paths under a harness directory are not harness processes
ok - session-lock: ownership stops at the first non-harness gap above the contiguous run
ok - session-lock: a live version-named session holding the lock is not mistaken for a stale owner
ok - session-lock: a declared session pid is used only while it is live and inside this ancestry
ok - session-lock e2e: a version-named session claims the home and arms supervision
ok - session-lock e2e: a session parented by a harness-named daemon claims the home and arms supervision
ok - session-lock e2e: a version-named session under a harness-named daemon keeps its own lock
ok - session-lock: a background session records its own identity and keeps claiming its home
ok - session-lock: a background session never claims a home a live launching session owns
ok - session-lock: a background session still reclaims a genuinely dead owner
ok - session-lock: away mode still owns supervision for a background session
ok - session-lock: a declaration outside this session's ancestry is ignored for the prior identity
FM_TEST_END 2026-08-27T08:50:59Z tests/fm-session-lock-ancestry.test.sh exit=0 duration_ms=17324 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=17519
FM_TEST_SUMMARY_FAMILY family=watcher-wake-lock count=1 duration_ms=17324 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-session-lock-ancestry.test.sh duration_ms=17324

Pipeline

Updates from git push no-mistakes

✅ **intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

⚠️ **Review** - 1 info
  • ℹ️ 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), including test_bg_session_ignores_a_declaration_outside_its_own_ancestry, the case whose body executes the newly annotated ( . &#34;$LIB&#34; &amp;&amp; fm_harness_pid_alive ... ) line at tests/fm-session-lock-ancestry.test.sh:737
  • Manual end-to-end demo (/Users/tom/.no-mistakes/evidence/01M116395BW2CA3DG95N20XMDF/session-lock-declared-pid-demo.sh &lt;root&gt; &lt;lib&gt; &lt;label&gt;) run twice against the same real scripts: once with the base commit's bin/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 named claude
  • Within that demo: real bin/fm-lock.sh at session start, real bin/fm-lock.sh status after the daemon above the session exited, real bin/fm-claude-stop-autoarm.sh Stop hook, and real bin/fm-turnend-guard.sh --claude at turn end, plus the persisted state/.lock contents compared against the launcher/daemon/session pids
  • git diff 9ce69ac..c71005b -- tests/fm-session-lock-ancestry.test.sh reviewed to confirm the top commit adds only the # shellcheck source=/dev/null comment line and touches no other file
  • git status --porcelain after 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.

⚠️ **Lint** - 1 warning
  • ⚠️ linter found issues (exit code 1)
✅ **Push** - passed

✅ No issues found.

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.
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.

1 participant