feat(autonomy): deterministic lane-stop gate + operator STOP notification - #969
Conversation
…tion Ship #535 members 3 and 4 (the ratified leave-week safety set); members 1, 2, and 5 remain deferred and out of scope. Member 3 — deterministic lane-stop gate. autonomy's first hook, a `Stop` event handler (hooks/lane-stop-gate.sh) that structurally intercepts every stop attempt of an opted-in lane and runs a deterministic completion self-check, converting "a lane that stops itself before its goal is met is a bug" from a prompt admonition into a mechanism. Unless completion is explicitly signaled — the exact sentinel token (default LANE-STOP-OK, standalone-matched) in the agent's final message, or a configured marker file — the first stop is blocked with a re-injected self-check (decision:"block" + reason). This counters the fabricated-context-percentage premature-stop failure (#576/#577): the reason states a self-estimated "~50% context" is not a completion condition. It is the settings-scoped, cross-session sibling of /goal's session-only condition (#481); a shell hook cannot re-run the /goal evaluator model, so completion is checked deterministically. Default OFF (a Stop-blocker must never engage for an interactive session), fail-open on unreadable stdin / missing jq / a non-Stop event (a SubagentStop never trips it), and bounded against runaway by the stop_hook_active one-block guard plus Claude Code's consecutive-block cap. Member 4 — operator notification. When a lane stops anyway after the one structural nudge, the gate treats it as a down/stuck lane, allows the stop (never wedges it), and alerts the operator via a self-contained hooks/lane-notify.sh (OS toast + terminal bell/OSC 9). Reach is local-machine only (no remote/Slack primitive exists). It reimplements rather than sources desktop-notification: a Stop hook's stdout is parsed for decision/reason and cannot use the terminalSequence field that plugin's Notification hook relies on, and cache-isolated plugins cannot source each other. No separate repeated-failure counter — a lane that keeps stopping re-fires this notification, and StopFailure API-error telemetry is already owned by claude-ops. Adds six userConfig options, the shared hooks/hook-utils.sh copy, README docs, and black-box tests (25 cases). shellcheck/shfmt/markdownlint clean; silent-skip, cross-plugin-drift, changelog-parity, and plugin-manifest gates pass locally. ## Related Refs #535 (members 3+4 per the ratified 2026-07-20T02:17:15Z triage-routing decision — members 1/2/5 remain deferred, not in scope). Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 5m 40s —— View job PR Review:
|
|
Claude finished @kyle-sexton's task in 5m 20s —— View job Security Review — PR #969
Reviewed files changed in this PR. No GitHub Actions files are in scope. No SQL, template injection, or supply-chain concerns found. Three findings follow, ordered by severity. IMPORTANT — Gate bypass: sentinel revealed in the block reasonConfidence: HIGH The block reason string includes the exact sentinel token the agent must emit to pass the gate on a future attempt: An agent that reads its block reason (the documented purpose of a
This collapses the gate from "require deliberate standalone token" into "repeat the token we just gave you." The PR description calls this "residual risk," but revealing the token in the denial message makes the false-negative scenario trivial rather than residual — any agent that processes its block output will naturally exercise this path. Mitigation options (not required for this review, raising for awareness):
SUGGESTION —
|
|
Orchestrator review pass (rule 10 full-diff-read + post-green review). Reviewed the full diff personally (Stop-hook gate = high blast radius). Logic review: fail-open paths correct (empty stdin, missing jq, non-Stop event all allow-and-exit), default-OFF verified, one-nudge-then-notify bounded correctly against One real finding, fix requested before flipping ready: README.md's example lane-launcher invocation is not valid Claude Code CLI syntax: Verified against https://code.claude.com/docs/en/plugins-reference.md: The correct per-session-override mechanism per the same doc page is Please fix the README example to a verified-working invocation (test it empirically if the docs are ambiguous on the exact |
The README showed `claude --config lane_stop_gate_enabled=true ...`, which is invalid: `--config <key=value>` is an option of `claude plugin install` (persisting to pluginConfigs in user settings), not a flag on the session-launch command — a launcher copying it would fail. Replace it with the correct per-lane, session-scoped mechanism: `--settings` carrying a `pluginConfigs` override (inline JSON, applies to one session only, no persistence). Verified against the official docs (plugins-reference.md line "pluginConfigs[<plugin-id>].options ... reads it back from ... the --settings flag", cli-reference.md "--settings ... Path to a settings JSON file or an inline JSON string") and empirically against this machine's live settings.json, whose existing `pluginConfigs` entry is keyed by the marketplace-qualified id `source-control@melodic-software` — confirming the key is `autonomy@melodic-software`, not the bare name. Also note that the persistent `--config` install path would enable the gate for every session, defeating the default-OFF design. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 2m 38s —— View job PR Review —
|
|
Claude finished @kyle-sexton's task in 4m 26s —— View job Security Review — PR #969
This review covers the full diff (both commits on this branch). The latest commit ( IMPORTANT — Gate bypass: sentinel disclosed in the block reasonConfidence: HIGH REASON="...declare completion by emitting the exact token ${SENTINEL} on its own line..."The block reason interpolates the literal sentinel value and delivers it to the agent as the
This collapses the gate's semantic intent from "require deliberate completion signaling" into "repeat the token we handed you." The standalone-only match (the ERE boundary check) still holds technically, but it provides no meaningful lift: an agent cannot accidentally emit the sentinel, but one that reads the denial message will naturally do so deliberately. The block reason is operator-authored copy (not agent-supplied), so there is no injection surface here — this is a logic-level flaw only. Mitigation options:
SUGGESTION — Unvalidated CWD used to construct marker-file pathConfidence: MEDIUM
CWD=$(printf '%s' "$INPUT" | jq -r '.cwd // ""' 2>/dev/null | tr -d '\r')
[[ -n "$CWD" ]] && MARKER="${CWD%/}/$MARKER"
Impact is limited to false-negative (gate bypassed, lane stops early) — not privilege escalation. The absolute-path branch of the
SUGGESTION —
|
…on example The activation example literally named this org's marketplace (`autonomy@<org>`), which the autonomy plugin's portability contract forbids (validate-plugin-contracts.mjs: the plugin must stay org/fleet-agnostic — binding-seam owns org-specific instance data; cf. #531). plugin-gate caught it. Replace the literal with a `<marketplace>` placeholder and state the key format explicitly (`<plugin-name>@<marketplace-name>`). The verified fact — that the pluginConfigs key IS marketplace-qualified, not the bare plugin name — is preserved; only this org's specific marketplace name is removed. Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Claude finished @kyle-sexton's task in 1m 44s —— View job PR Review —
|
|
Claude finished @kyle-sexton's task in 2m 58s —— View job Security Review — PR #969 (HEAD
|
|
Full CI green (24/24 checks) after both fix rounds. Flipping to ready. Summary for the record: rule-10 full-diff-read found the core logic (Stop-hook gate, notification, tests) correct with no DEFER-FORBIDDEN-class issue. Post-green review surfaced one doc-accuracy defect (invalid |
|
Claude finished @kyle-sexton's task in 3m 6s —— View job Final Review —
|
|
Claude finished @kyle-sexton's task in 3m 19s —— View job Security Review — PR #969 (HEAD
|
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 73035c6b02
ℹ️ 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".
The completion-token detection matched the sentinel as a standalone word *anywhere* in the final message, so a turn that merely mentioned or negated the token inline (e.g. "I should not emit LANE-STOP-OK yet") set SIGNALED=1 and authorized the stop — a false positive in the very mechanism the gate exists to make reliable. The block reason already instructs the agent to emit the token "on its own line"; detection now enforces that same contract (the token alone on a line, surrounding whitespace allowed), so inline discussion of the token can no longer bypass the gate. Erring toward a missed genuine signal over a spurious one is the correct bias here: a missed signal is bounded (one nudge, then allow + notify), whereas a spurious match defeats the gate outright. Tests: the sentinel-present cases (3, 11) now place the token on its own line — a contract tightening, not a weakening-to-pass. Adds Case 13 (inline mention/negation → blocked) and Case 14 (token alone on an indented line amid other lines → allowed).
|
Claude finished @kyle-sexton's task in 2m 37s —— View job PR Review —
|
|
Claude finished @kyle-sexton's task in 3m 16s —— View job Security Review — PR #969 (HEAD
|
|
Classification of the HEAD (
This pass fixed the one core-purpose correctness finding (#1 — the gate firing on a mention of its own token). Findings #2–#5 are a genuine design tradeoff (#2) plus defense-in-depth/doc follow-ups (#3–#5) with no |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a6335f4ac1
ℹ️ 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".
|
🔒 merge-drive lane claiming this PR for on-branch fix work this cycle (amendment-round: 16, worker tier). Will fix clear branch-owned CI/bot findings, defer non-DEFER-FORBIDDEN items with reply+resolve, push, then re-check the merge gate. |
… match
The `lane_stop_gate_sentinel` description still described the pre-a6335f4a
word-boundary behavior ("Match is standalone-token only") after detection was
tightened to require the token alone on its own line, leaving the one surface a
configuring operator actually reads contradicting README, CHANGELOG, and the
block reason the gate emits.
|
Claude finished @kyle-sexton's task in 1m 28s —— View job PR Review —
|
| # | Finding | Prior rating | Status |
|---|---|---|---|
| 1 | Sentinel disclosed in block reason (L121) |
IMPORTANT (design tradeoff) | Unchanged — deferred, accepted |
| 2 | Unvalidated CWD in relative marker-path (L86–87) |
MEDIUM/SUGGESTION | Unchanged — unaddressed |
| 3 | git -C "$CWD" without absolute-path guard (L108) |
LOW | Unchanged — unaddressed |
| 4 | Empty sentinel behavior undocumented in userConfig |
Low | Unchanged — still open |
| 5 | Stale "standalone-token only" description | New defect from a6335f4 |
✓ Fixed by e64b988 |
Finding #1 remains an accepted design tradeoff per the orchestrator's ruling — the a6335f4 line-level match raises the bar meaningfully (inline mention no longer bypasses the gate). Findings #2 and #3 are defense-in-depth hardening that has been consistently deferred. Finding #4 is the one item this commit was positioned to close but did not.
Summary
e64b988 correctly resolves the stale description defect introduced by a6335f4. The description wording now accurately reflects the line-level match semantics and matches the README/CHANGELOG language. ✓
The empty-sentinel documentation gap (finding #4) remains the one low-effort open item — a one-sentence addition to the description that keeps the lane_stop_gate_sentinel and lane_stop_gate_marker parallel notes consistent. All other findings are unchanged from the prior pass.
| Branch
|
Claude finished @kyle-sexton's task in 3m 11s —— View job Security Review — PR #969 (HEAD
|
|
Classification of the HEAD (
Everything else in the security pass landed under "Items reviewed and found acceptable" (OSC 9 / AppleScript / Not merge-eligible this cycle regardless: the branch is behind |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: e64b98823b
ℹ️ 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".
main's #903 updated the shared git parser in lib/hook-utils.sh; the autonomy copy added by this branch predated it, tripping hook-utils-sync and cross-plugin-source-drift on the merge ref. Re-run of scripts/sync-hook-utils.sh. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017P1vVA8iViUTfQWjA9tgZG
Two review findings on the lane-stop gate, both addressed: Sentinel match via here-string (fix). Under pipefail, `printf | grep -q` loses an early match when the final message overruns the pipe buffer: grep exits at the match, printf takes SIGPIPE (rc 141), the pipeline reads false, and a genuinely completed lane gets blocked. Reproduced with the sentinel followed by ~120KB of text. A here-string has no pipeline, so an early match can never be lost. Regression cases: a ~120KB message with an early sentinel must be allowed, and a control without the sentinel must still block (proving the long payload really reaches the gate rather than fail-opening). Evaluated-outcome telemetry (feat). The gate now emits one fire-and-forget hook-telemetry envelope per evaluated outcome when HOOK_TELEMETRY_SINK is set: blocked/nudged for the one structural nudge, ok/completion-signaled (signal: sentinel|marker) for a legitimate stop, ok/stopped-after-nudge for the down-lane path that fires the operator alert — making premature lane stops measurable and the local alert correlatable fleet-wide. Default-off and fail-open exits stay silent (pre-evaluation, and interactive noise otherwise). The data payload is a closed fixed vocabulary published at docs/conventions/hook-telemetry/data/lane-stop-gate.schema.json; it never carries the sentinel token value, marker path, cwd, or branch, so the envelope cannot leak the completion token or lane paths into the sink. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017P1vVA8iViUTfQWjA9tgZG
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ac5766708b
ℹ️ 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".
Version collision: main shipped autonomy 0.10.0 (routines determinism-rule vocabulary change) while this branch also used 0.10.0 for the lane-stop gate. Composed by moving this branch's entries to a new 0.11.0 section above main's 0.10.0 and bumping plugin.json to 0.11.0. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012SV88ZgGDKdUKEPJaJtDpG
…he marker on use Addresses the two live Codex review threads: P1 — the shipped standing-lane flow never received the gate: the claude-ops lane launcher built only `claude --bg -n … [--model] [--effort]` and no integration set lane_stop_gate_enabled. The lane config now takes an optional per-lane `settings` JSON object passed verbatim as `--settings` (session-only, verified against the current CLI reference), validated as an object at preflight; the gate opt-in via a pluginConfigs override is the documented motivating example. claude-ops bumped to 0.20.0. P2 — a marker left by a prior completed run in the same checkout satisfied the existence check and authorized every stop of a later run. The marker is now consumed (deleted) when it authorizes a stop — one marker, one stop; test coverage asserts consumption and that the consumed path no longer authorizes the next stop. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012SV88ZgGDKdUKEPJaJtDpG
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Claude encountered an error after 2s —— View job I'll analyze this and get back to you. |
|
Warning Automated review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
|
Warning Automated security review did not complete — this is an infrastructure failure, not a review verdict. Treat any Claude comment on this PR (including a placeholder like "I'll analyze this and get back to you") as incomplete, not "no findings."
Re-running the job, or pushing a new commit, will retry the review. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 83b75b0711
ℹ️ 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".
…uests (#1781) ## Why A review that lands **after** a merge has nowhere to go: - the ruleset's `required_review_thread_resolution` is a **merge-time predicate** that already passed; - the babysit lane works *open* PRs, and a merged PR leaves that queue; - nothing on a merged PR surfaces its open threads — GitHub shows the merge, not the findings. Six findings — one **P1** — posted **46 seconds after #1720 merged** and sat unread for a day. They surfaced only because a later session happened to audit that merge batch. Nothing was bypassed; the gate was satisfied *because the threads did not yet exist*. The morning brief is the right home: read-only, unattended, and already where attention signals land. ## What it does Compares each unresolved thread's **first-comment timestamp** against the PR's `mergedAt`, and reports only threads the gate could never have seen. A thread that predates the merge was visible to the gate — that is an ordinary unresolved thread, not this failure mode, and it stays out. - **One line per PR, at that PR's worst severity, with a finding count.** Several findings on one PR are one thing to go look at; repeating the title per thread buries every other PR. Collapsing on the *worst* severity means a P0 sitting beside advisory findings can never be softened. - **Severity survives to the operator** — a stranded P1 must not read like a P3. - **`--stranded-days`** (default 3) — wide enough to cover slow bot review *and* an operator-absent weekend. ## It fails loud, not clear A GraphQL error document is well-formed JSON that simply carries no `data`. The extraction would yield an empty list and render **"every merged PR in the window is clear"** — an all-clear asserted from an answer never received, which is the same fail-open shape this section exists to catch. This is not hypothetical: a rate-limit error did exactly that during development. An API error now says explicitly that it is *not* an all-clear, and prints the message. Covered by a regression case. ## This is a standing leak, not a one-off Its **first live run** against this repository immediately surfaced four more stranded findings on other merged PRs — including a **P1 on #1694** (merged `05:04:45Z`, finding posted `05:05:20Z`, 35 seconds later) recording that a shipped `autonomy` cell **never reached installations**. ## Verification - `morning-brief.test.sh`: **30 → 63 cases, 0 failures.** - The **negative** cases carry the weight — a pre-merge thread, an already-resolved post-merge thread, and a merge outside the window must all stay silent, or the section is noise rather than signal. Plus: collapse-does-not-soften-severity, highest-severity-first, window-widening, and the API-error case above. - The fixture mirrors the real #1720 shape, including the 46-second gap. - `shellcheck -x` on script and test — clean. One `SC2016` is declared, not blanket-suppressed: the `$owner`/`$name`/`$endCursor` in the GraphQL query are server-side variables bound by `-F` and **must** reach the server unexpanded. - `node scripts/validate-plugin-contracts.mjs` — 43 setup skills, 2153 files, pass. - `npx markdownlint-cli2` on both changed markdown files — 0 errors. ### Live run — posted in full in the comments below A live run on the current branch found **44 merged PRs carrying post-merge findings in a five-day window: 0 P0, 10 P1, 34 P2.** Among the P1s: **#1503**, a guardrail-bypass fix whose own review landed unread, and **#1322** with 5 findings. Read the **second** comment for the authoritative figures — the first was produced by the pre-review severity logic and reported a false P0, which review then caught. No truncation warning fired, so the read is complete. The five-day window filter was spot-checked against `mergedAt` (a PR numbered #969 in a 5-day window looks wrong until you check: it merged `2026-07-25`, 4.2 days before the run). This is a far larger leak than the six findings that exposed it. ## Related Closes #1777 Refs #1720 Refs #1759 --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…sent Two stranded findings from #969, re-verified against origin/main. lane-stop-gate.sh: the completion marker's one-shot authorization was latched solely by deleting the file, which lives in the watched checkout. An `rm` the OS refuses left a file that still satisfied `[[ -f ]]` on a later, unrelated lane run. Consumption is now recorded in the plugin's own data directory, derived from the hook's install path under the documented plugins/cache anchor; the delete is the tidy-up rather than the latch. lane-launcher.sh: both field readers used jq's `//` alternative, which fires on every falsy value rather than on absence. `"settings": false` yielded empty, reached bash as "", skipped the guarded type check, and launched the lane with `--settings` silently omitted. Presence is now tested with `has`, and the scalar fields are typed once at config time. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FVoZoMYXqf8ZVbQYixPVPW
…sent (#1851) No linked issue ## Summary Fixes two of the three defects re-verified in #1784 — the two stranded #969 findings. Both were a value that is *present but falsy* being read as *absent*. **#1784 is deliberately NOT closed by this PR.** Its P1 (the lane-stop gate reading its enable flag off channel B) is untouched — see "Not addressed" below. ## Fix **`plugins/autonomy/hooks/lane-stop-gate.sh` (0.11.7 → 0.11.8).** The completion marker's one-shot authorization was latched solely by deleting the file, and the marker lives in the watched checkout — a directory the hook is not guaranteed to be able to write. An `rm` the OS refused left a file that still satisfied `[[ -f "$MARKER" ]]` on the next run: exactly the cross-run bypass that consuming the marker exists to close. The surrounding comment asserted "the next run must not rely on that stale file" while nothing enforced it. Consumption is now recorded in the plugin's own persistent data directory — the marker path plus the consumed file's identity (mtime and size) — and the deletion is the tidy-up rather than the latch. A marker recorded as consumed is not a signal however long it survives on disk; a marker later recreated has a new identity, so the stale record is dropped and the fresh marker authorizes normally. Recreation recovery is **best-effort by design**, and the second commit scopes the claim to what the identity read actually resolves. Both portable `stat` dialects report whole-second mtime, so a marker recreated at the same size within the same second — an empty `touch`-style marker being the realistic case — is indistinguishable from the consumed one and stays latched until the second turns over. Sub-second (`%.9Y`) and inode spellings would narrow that window but are GNU-only, and this identity feeds a **gate**: the coarse read costs a stop delayed by under a second, while a wrong "recreated" verdict costs the unearned second authorization the ledger exists to prevent. Delay is the correct failure direction, so the portable spelling stands and the comment and CHANGELOG say so rather than implying recovery is guaranteed. A host where neither dialect reports an identity holds the record for the same reason. The data directory is derived from the hook's **own install path** (the `plugins/cache` anchor Claude Code documents), falling back to `CLAUDE_PLUGIN_DATA` only for a `--plugin-dir` install carrying no such anchor: the script's own location is not something a watched repository can redirect, whereas `CLAUDE_PLUGIN_DATA` is an env value a repo `settings.json` `env` block reaches. Where no data directory can be written, the deletion remains the only latch — the behavior that predates this ledger. **`plugins/claude-ops/skills/lanes/scripts/lane-launcher.sh` (0.24.4 → 0.24.5).** Both field readers used jq's `//` alternative operator, which fires on every FALSY value rather than on absence. A lane configured `"settings": false` yielded `empty`, reached bash as `""`, and — because `validate_launch_inputs` guards its "settings must be a JSON object" check on `[[ -n "$settings" ]]` — that type check never ran at all: the lane launched with `--settings` silently omitted, no error, nothing for the operator to see. `lane_json_field` now tests presence with `has`, so `false` reaches the type check and the lane is skipped with the error already written for it. The scalar reader had the same collapse for `name`/`model`/`effort`/`prompt` (a mistyped `"effort": false` launched a lane with no effort), so those fields are typed once at config time and a non-string value is a config error alongside the existing duplicate-name and path-traversal checks. An explicit `null` stays the JSON spelling of "no value" and remains equivalent to an absent field in both readers. ## Test plan Red-to-green proven for both defects by running the new cases against the **pre-fix** sources checked out from `origin/main` into a staged copy, then against the fixed sources. | Suite | Pre-fix (`origin/main` source) | Post-fix | |---|---|---| | `plugins/autonomy/hooks/lane-stop-gate.test.sh` | **1 fail** — `a surviving consumed marker wrongly authorized a later run` | **0 fail** (37 cases) | | `plugins/claude-ops/skills/lanes/scripts/lane-launcher.test.sh` | **10 fail** — the 8 `boolean .name/.model/.effort/.prompt` cases plus `settings:false reaches the type check`, `settings:false lane not launched`, `settings:false surfaces a non-zero exit` | **0 fail** (144 cases) | Case counts differ across the two columns for the gate suite because cases 21b and 22–23 were added *after* that red run; the red run is the marker regression alone. Case 21b pins the same-second/same-size recreation boundary described above, and reports which side of the second it landed on rather than asserting a timing race — so a future finer-grained identity has to move that case deliberately. **A defect the new coverage found.** The first draft of the marker ledger tested only the `CLAUDE_PLUGIN_DATA` fallback — never the install-path derivation the tamper-resistance claim actually rests on. Cases 22–23 stage the hook under a synthetic `<root>/plugins/cache/<marketplace>/<name>/<version>/hooks/` tree with an unrelated `CLAUDE_PLUGIN_DATA` present, and they failed: `gate_data_dir` appended `/plugins` twice, writing to `<root>/plugins/plugins/data/<id>` instead of the documented `<root>/plugins/data/<id>`. The layout was checked against a real install on this machine (`~/.claude/plugins/cache/melodic-software/autonomy/<ver>` alongside `~/.claude/plugins/data/autonomy-melodic-software`) and the path is corrected here. Gates run locally against `origin/main` as base before pushing, all green (and re-confirmed by the full CI run on this PR): - `scripts/check-changelog-parity.sh --check`, `--check-bump origin/main`, `--check-order` - `scripts/check-changed-skills.sh origin/main` - `scripts/check-silent-skips.sh` - `scripts/check-shell-portability.sh origin/main` - `shellcheck --rcfile=.shellcheckrc` over all four changed shell files - `markdownlint-cli2 --config .markdownlint-cli2.jsonc` over the two CHANGELOGs and `config.md` - Sibling suites unaffected and still green: `lane-notify`, `machine-behavior`, `restart-consumer`, `telemetry-upsert` ## Not addressed **#1784's P1 — the lane-stop gate reads its enable flag off channel B — is not fixed here, and the issue stays open for it.** `lane-stop-gate.sh` still reads `CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ENABLED` from bare env, which `docs/conventions/hook-config-delivery/README.md` rule 3 forbids for a safety-critical optional-with-default toggle (fact 4: an unconfigured key lets a repo's own `settings.json` `env` block populate it freely). That fix is a channel-F reader, and the only shipped exemplar is Python (`plugins/disk-hygiene/lib/killswitch_config.py`); a bash equivalent needs the `${CLAUDE_PLUGIN_ROOT}` anchor, the fixed-path managed-settings table plus `managed-settings.d/` drop-ins, an explicit fail direction, and an entry in the convention's Adopters table. That is its own design and its own security review, not a rider on this one. Acceptance criterion 1 of #1784 therefore remains unmet; criterion 5 is only partly reachable — two of the three #969 threads become resolvable when this lands, the channel-B thread does not. #1784 remains the tracker for both. ## Related Refs #1784 Refs #969 Refs #1777 Refs #1781 --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
What
Ships the two cheap, additive hooks the operator ratified as the leave-week safety set for the
"operational resilience" umbrella (#535): member 3 (deterministic lane-stop gate) and member 4
(operator notification on a genuine lane stop). Both land inside the
autonomyplugin — its firsthooks/directory — and touch nothing else.Member 3 — deterministic lane-stop gate (
hooks/lane-stop-gate.sh,Stopevent)"A lane that stops itself before its goal is met is a bug" was only a prompt admonition. This hook
fires on every stop attempt of an opted-in lane and structurally intercepts it:
/goalevaluatormodel) — either the exact sentinel token (default
LANE-STOP-OK, matched standalone-only) inthe agent's final message, or the existence of a configured marker file. This is the
settings-scoped, cross-session sibling of
/goal's session-only completion condition (feat: /goal prompt-crafting skill — docs-conformant goal shape + mechanical length check #481); itsown docs name "a prompt-based Stop hook" as exactly this alternative.
(
decision:"block"+ reason). The reason directly counters the fabricated-context-percentagepremature-stop failure (loop skill / babysit-prs: rule 15's '~50% context indicator' has no real instrument behind it — self-reported estimates are fabricated guesses, not measurements #576/work-items(work): rule-20 context-estimate is a guess, not a measurement — caused a premature session stop (30% actual vs 55% claimed) #577): a self-estimated "~50% context", a turn count, or a vague sense
of "enough" is explicitly stated to not be completion.
Safety properties: default OFF (a Stop-blocking hook must never engage for an interactive
session); fail-open on unreadable stdin, missing
jq, or a non-Stopevent (aSubagentStopnever trips it); bounded against runaway by the
stop_hook_activeone-block guard plus Claude Code'sown consecutive-block cap. Scope: catches a graceful self-stop only — a closed laptop, a
killed process, or
/loopexpiry emit noStopevent and are members 1/2's job.Member 4 — operator notification (
hooks/lane-notify.sh)Fired by the gate at the genuine-stop juncture. OS-native toast (macOS/Linux) + best-effort terminal
bell + OSC 9, local machine only (no remote/Slack/push primitive exists in the marketplace yet).
It reimplements rather than sources
desktop-notificationbecause aStophook's stdout is parsedfor
decision/reasonand cannot use theterminalSequencefield that plugin'sNotificationhookrelies on (a genuinely different emission path: direct
/dev/tty), and because cache-isolated pluginscannot source each other at runtime. No separate repeated-failure counter was built — a lane that
keeps stopping simply re-fires this notification, and
StopFailureAPI-error telemetry is alreadyowned by
claude-ops(avoids that plugin's in-flight PRs entirely).Files
plugins/autonomy/hooks/{lane-stop-gate.sh, lane-notify.sh, hooks.json, hook-utils.sh}(new)plugins/autonomy/hooks/{lane-stop-gate.test.sh, lane-notify.test.sh}(new, 25 black-box cases)plugins/autonomy/.claude-plugin/plugin.json(0.9.0 → 0.10.0, sixuserConfigoptions)plugins/autonomy/{CHANGELOG.md, README.md}Verification (local)
shellcheck (0.11.0) + shfmt (v3.13.1) + markdownlint clean; both test suites pass (15 + 10);
sync-hook-utils --check(13 copies),check-silent-skips,check-cross-plugin-source-drift,check-changelog-parity(--check+--check-bump), andvalidate-pluginsall pass.For the reviewer — please double-check
to emit the sentinel / touch the marker gets exactly one blocked turn, then is allowed to stop with
a notification. Bounded and non-fatal, and only for opted-in lanes. Acceptable?
hallucinating completion. Mitigated by requiring a deliberate standalone token (much higher bar
than a passive stop) and by the marker-file alternative; residual risk documented.
stop_hook_activereset semantics decide whether the gate re-arms for another nudge on a laternatural stop (per-attempt reset) or nudges only once per session (latched). The code and safety
bound are correct under either reading; the docs are worded to not over-claim a per-cluster reset.
If you know the authoritative semantics, confirm the wording.
StopvsSubagentStopfiring: the hook early-allows unlesshook_event_name == "Stop", so aTask-tool worker finishing can never trip the lane gate regardless of how the platform routes the
registration.
Issue linkage
No linked issue — this PR partially addresses the umbrella #535 (members 3+4 only) and closes nothing.
The umbrella stays open with members 1/2/5 deferred; see Related.
Related
Refs #535 (members 3+4 per the ratified 2026-07-20T02:17:15Z triage-routing decision — members 1/2/5
remain deferred, not in scope).