Skip to content

feat(fable-5): route the context-budget menu to the operator and seed check I23 - #2031

Merged
kyle-sexton merged 4 commits into
mainfrom
feat/fable5-followups
Aug 8, 2026
Merged

feat(fable-5): route the context-budget menu to the operator and seed check I23#2031
kyle-sexton merged 4 commits into
mainfrom
feat/fable5-followups

Conversation

@kyle-sexton

Copy link
Copy Markdown
Contributor

Closes #2009
Closes #2011

Summary

Two follow-ups from the Fable 5 prompting-guide alignment audit (#2000) turned out to share a single
open decision, so they land together rather than answering it twice and risking two different
answers. #2011's option 2 was "move the continuation trigger to the measured mechanism — which is
what context-guard already is", and #2009 asked whether that mechanism is itself a finding. If
context-guard's injection stayed an exit menu, #2011's option 2 would have moved triggers into a
surface check I23 flags. Net zero. So #2009 is answered first and #2011's policy falls out of it.

The decision: three signals license a surface to route into a handoff, a fork, or a new session
— the user's own report, an instrument that measures the window, and visible decay in the model's
own output. A self-estimated budget is none of the three. The third signal stays the model's to
read, because decay shows up in the output and never in a budget number.

Fix

context-guard — the zone-crossing report splits by audience (0.4.90.5.0).

The hook injected one block into model context naming the zone and then enumerating four
continuation options — continue, /clear, handoff-then-/clear, /compact — plus the
/session-flow:workflow router. The menu now renders on systemMessage, the operator channel,
whose whole content is a human's choice to make. additionalContext carries the zone determination
plus the counter-steer: this is a measurement and not a decay signal, so do not volunteer to end the
session, summarize, hand off, or trim work on the strength of the reading. The dumb zone keeps its
extra clause, restated as the model-independent fact it always was — compaction distance is short,
so write expensive conclusions to a durable note as they stabilize.

systemMessage is a universal hook output field documented as "Warning message shown to the user",
and additionalContext is the field that reaches model context; both are available on
PostToolBatch and UserPromptSubmit
(https://code.claude.com/docs/en/hooks, fetched this session).

Two details worth calling out:

session-flow (0.20.00.21.0) and implementation (0.11.00.12.0) lose their
self-estimated-budget triggers.

handoff's description listed "context is heavy", and its body repeated it as "Mid-task, context
heavy (check /context output or user report)". A description is resident in context by default, so
that trigger was live in every session with the plugin installed. implement's "Mid-phase handoff
is still appropriate when context is heavy" is the same shape. Both stay model-invocable and neither
loses a capability beyond the exact behavior the guide counsels against — only the budget clause is
gone. handoff's "Fork beats compaction" section keeps its window-position threshold and gains a
one-line anchor saying what it always meant: it picks between two continuation mechanisms and never
licenses the continuation itself.

The considered alternative — flipping continuation skills to disable-model-invocation: true — was
refused. It also suppresses the description from context entirely
(https://code.claude.com/docs/en/skills, fetched this session), so it forfeits every model-side
invocation the skill has, including the ones a user asks for in the words the description exists to
match, to remove one clause.

claude-config — I23 gains a pre-scan pattern (0.24.00.25.0, catalog 1.18.0
1.19.0).

The row shipped unseeded, waiting on calibration. What it was actually waiting on was the policy
above, not a regex: the blast-radius argument (a continuation skill can barely be model-invocable
without naming a context trigger somewhere) was an argument about a population whose disposition was
unsettled. Under the licensing rule its members are true positives, not noise. Two supporting
clauses ship with the row — residency (description vs body) is a severity input rather than an
admission test, and remediation moves the trigger rather than withdrawing the skill.

The pattern marks budget phrasing alone and never the stop/summarize/hand-off verb it licenses,
because the trigger and the action routinely sit in different sentences; counter-steer text,
documents about the pattern, and operator-facing budgets therefore match too, on the same advisory
over-production contract the I8 families already carry. It is deliberately not anchored to the bare
term "context window".

Verification

  • bash plugins/context-guard/hooks/zone-crossing-inject.test.sh20/20 pass, including two
    new cases asserting the channel split: the model channel carries the counter-steer and none of
    /compact, /clear, /session-flow:workflow, /session-flow:handoff; the operator channel
    carries the menu.
  • bash plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.test.sh
    56/56 pass, including new cases 13 and 14: six I23 positives, a licensed trigger naming no
    budget that must not match, the counter-steer that must match (advisory over-production), and two
    negatives proving the pattern is not anchored to bare "context window".
  • Blast radius measured, not asserted: the seeded pattern yields 20 rows across 10 files over
    the marketplace's 193 SKILL.md surfaces. Four are clear false positives the model lane discards
    (claude-ops's "remaining tokens are lane names" is an argument-parsing homograph;
    testing's Playwright CLI-vs-MCP token budget is an operator cost figure; claude-memory and
    playbooks:boris name the topic in a routing table). That rate is the candidate set the pre-scan
    contract is for, not a corpus.
  • scripts/validate-plugins.sh — all plugin manifests and the catalog validate.
  • scripts/check-changelog-parity.sh --check, --check-order, and --check-bump origin/main — all
    four version bumps carry matching ## [<version>] entries; 72 changelogs read newest-first.
  • scripts/check-silent-skips.sh, scripts/check-shell-portability.sh origin/main, shellcheck on
    all four changed shell files — clean.
  • markdownlint-cli2 over the nine changed markdown files — 0 issues.
  • node scripts/generate-catalog.mjs re-run after the manifest description change.

Related

Explicitly unverified

#2009 carried a behavioural claim it marked unverified, and this PR does not upgrade it. No
transcript was produced showing a model volunteering an unprompted stop because of the old
injection in a context-guard-only install. The fix is made on the shape finding, which is
present in the text whether or not a model has demonstrably acted on it.

… check I23

Two follow-ups from the Fable 5 prompting-guide alignment audit share one
decision, so they land together: whether a continuation trigger may be
self-judged from the remaining window at all.

It may not. Three signals license a surface to route into a handoff, a fork,
or a new session — the user's own report, an instrument that measures the
window, and visible decay in the model's own output. A self-estimated budget
is none of the three.

context-guard now splits its zone-crossing report by audience. The four-option
continuation menu renders to the operator on systemMessage, whose whole content
is a human's choice; the model's channel carries the zone determination plus
the counter-steer that a zone word is a measurement and not a decay signal. An
exit menu injected into model context manufactures the model's own initiative
to stop — the measurement decides only when to ask, while the model still
decides whether to stop — which is the shape check I23 detects. The
counter-steer is stated inline rather than delegated to the playbooks doctrine
that also carries it, because the two plugins are independently installable.

session-flow's handoff and implementation's implement lose their
self-estimated-budget triggers on the same rule. Both stay model-invocable;
only the budget clause is gone.

With the policy settled, I23's pre-scan pattern is seedable and is seeded. It
marks budget phrasing alone, never the verb it licenses, and is not anchored to
the bare term "context window" — 20 rows across the marketplace's 193 skills.

Closes #2009
Closes #2011

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNmc2T3y81efZTaDZHerWo
@cursor

cursor Bot commented Aug 8, 2026

Copy link
Copy Markdown

Bugbot is not enabled for your account, so this pull request was not reviewed.

Enable Bugbot in the Cursor dashboard to get automatic reviews on future PRs.

@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: 4986665344

ℹ️ 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 plugins/context-guard/hooks/zone-crossing-inject.sh
# Conflicts:
#	plugins/claude-config/CHANGELOG.md
#	plugins/claude-config/skills/audit-instructions/SKILL.md
#	plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.sh
#	plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.test.sh
@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 4m 25s —— View job


Code Review

  • Gather context (PR body, diff, review comments)
  • Review context-guard hook + test changes
  • Review claude-config I23 pattern + scan script changes
  • Review session-flow / implementation SKILL.md changes
  • Check CHANGELOG/manifest version bumps for parity
  • Evaluate existing Codex review comment (systemMessage channel usage)
  • Post inline findings and final summary

Summary

Reviewed the diff against origin/main (via gh pr diff 2031, since this sandbox's origin/main ref was stale/shallow). The core policy change — three signals license a continuation (user report, measuring instrument, visible output decay), a self-estimated budget is none of them — is applied consistently across context-guard, session-flow, implementation, and the new claude-config I23 pattern. Version bumps and CHANGELOG entries line up 1:1 across all four touched plugins.

I concur with the existing Codex review comment on zone-crossing-inject.sh:157: routing the continuation menu through systemMessage conflicts with this repo's own documented convention. docs/conventions/hook-observability/README.md scopes systemMessage to exactly two cases — a missing-runtime-prerequisite skip, and an unrequested content mutation — and is explicit that "legitimate advisory findings… belong on additionalContext only," calling over-applying systemMessage to advisory findings "itself a conformance defect (redundant user noise…)" (README.md:79-82, 129-132). The hook's own header still self-describes this report as advisory-only (zone-crossing-inject.sh:32), which is the exact category the convention carves out of systemMessage. Given this PR is otherwise carefully aligned with the repo's own catalogs and doctrine, this looks like a real gap between the new behavior and an existing, explicit house rule rather than a judgment call — worth resolving before merge, either by using a different signal for the operator-facing menu or by explicitly amending the convention doc alongside this change.

I posted one additional inline finding building on that same line: in a headless/non-interactive session (batch claude -p runs, CI-triggered sessions, unattended lane workers), nobody reads systemMessage, so the continuation menu is emitted into a channel with no consumer while the model is simultaneously told the operator "owns that choice" and not to act on the reading itself. That may be an accepted tradeoff of the design, but it's worth a deliberate answer rather than a silent gap — see the inline comment on zone-crossing-inject.sh:155.

Minor, non-blocking: the PR description's parenthetical for claude-config says "(0.24.00.25.0, catalog 1.18.01.19.0)", but the actual diff bumps plugin.json 0.25.00.26.0 and criteria.md 1.19.01.20.0 (matching the CHANGELOG's own "catalog 1.20.0" reference) — main had already advanced claude-config to 0.25.0/catalog 1.19.0 via #2017 before this branch was current. The shipped changelog/version numbers are internally consistent; only the PR body prose is stale.

I23 pattern / instruction-scan.sh and its tests: traced the new I23_ERE alternation against all eight lines of Case 13 and both lines of Case 14 by hand — the pattern's coverage (budget phrasing, /context output, window-position thirds/quarters/halves, "approaching the context limit") and its intentional non-anchoring to bare "context window" match every assert_contains/assert_not_contains in the new test cases. Logic looks sound.

Verification note: this review lane's sandbox declined to run shell scripts (bash <script>.sh required approval that wasn't grantable non-interactively), so I could not independently execute zone-crossing-inject.test.sh (claimed 20/20), instruction-scan.test.sh (claimed 56/56), or the changelog-parity/shellcheck/markdownlint checks the PR body describes. Those results are author-claimed and unverified by me — the regex/logic trace above is a substitute for, not equivalent to, running the tests. If you want independent execution, this lane's --allowedTools would need script execution added.

No issues found in the session-flow/implementation SKILL.md trigger-clause removals — straightforward, consistent with the stated policy, and narrowly scoped to the budget clause as claimed.

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 44s —— View job


Security Review — Complete

  • Gather context (PR diff, changed files)
  • Review hook script changes (context-guard/hooks/zone-crossing-inject.sh)
  • Review shell script changes (claude-config instruction-scan.sh)
  • Review skill/markdown/config changes for injection or trust-boundary issues
  • Post findings

Reviewed the full diff (gh pr diff 2031, 18 files, commit 4986665) against the security lane's scope: injection surfaces, unsafe handling of untrusted data crossing a trust boundary, authorization/access-control gaps, and dangerous automation patterns. Findings below.

No security vulnerabilities found in this PR.

Reasoning, so the "no findings" isn't a rubber stamp:

  • plugins/context-guard/hooks/zone-crossing-inject.sh — the substantive shell change. It now builds two hook-output strings (guidance for additionalContext, operator for systemMessage) instead of one, and both flow through hook::emit_channels, which JSON-escapes via hook::json_escape (hook-utils.sh:38-49, unchanged by this PR) before embedding them — so no unescaped-JSON injection into the hook's stdout contract.
    • The only variable data interpolated into either string is prev_label/zone_label. zone_label is either the literal dumb (evidence-degraded: …) string or $zone, which is gated to exactly smart/acceptable/dumb before this point (zone-crossing-inject.sh:87) — every other value (including unknown) exits early. prev_label comes from $last, read from a per-session state file through tr -cd '[:lower:]' | head -c 16 (zone-crossing-inject.sh:104, unchanged), so it's bounded to ≤16 lowercase characters even in a worst-case corrupted state file. Neither value can carry attacker-controlled prose, shell metacharacters, or unescaped quotes into the JSON payload or into either audience's channel.
    • $SESSION (the one field sourced from the untrusted stdin payload) is validated against ^[A-Za-z0-9_-]+$ (zone-crossing-inject.sh:71) before any use, including the state-file path — this line is unchanged by the PR, and the new code never re-interpolates $SESSION into either emitted string.
    • The operator-channel content is static template text (fixed continuation-menu prose); nothing user- or model-supplied reaches systemMessage.
  • plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.sh — the new I23_ERE variable is a fixed, hardcoded extended-regex literal (budget-phrasing alternation), passed straight to grep -niE "$I23_ERE" "$file" the same way the pre-existing I8_A_ERE/I8_B_ERE/etc. patterns are. No user input is spliced into the pattern, so there's no regex-injection or command-injection surface here, and it's advisory tooling that only reads files the operator already passed as arguments.
  • Markdown/prose-only changes (SKILL.md, README.md, CHANGELOG.md, reference/criteria.md, docs/CATALOG.md) and the four plugin.json version/description bumps carry no executable surface, no permission-grant changes, and no config/settings changes.
  • No GitHub Actions workflow files, .mcp.json, or permissions:/settings surfaces are touched by this PR, so the pull_request_target/workflow_run/script-injection/permission-widening/supply-chain checks this lane covers don't apply here — nothing to defer to zizmor either, since there's no workflow diff for it to have already covered.

One non-security note for completeness: Codex's review comment on zone-crossing-inject.sh:157 (routing the continuation menu through systemMessage) is a repo-convention/observability-taxonomy question (per docs/conventions/hook-observability/README.md), not a security issue — out of scope for this lane, and I'm not re-litigating it here.

Comment thread plugins/context-guard/hooks/zone-crossing-inject.sh
@github-actions

github-actions Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude has reviewed this PR 3 times. The lane skips further automatic reviews after 5; deleting this comment resets the count.

…rigger

The residency clause and the remediation that rests on it turn on one fact from
the Skills page — that disable-model-invocation: true also suppresses the
description from context. The row's existing Verified/Recheck trailer is scoped
to the prompting guide, so that second source carried a verification date and no
trigger. It now names the change that would invalidate it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNmc2T3y81efZTaDZHerWo
@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 19s —— View job


Code Review

  • Gather context (PR body, diff, prior review comments)
  • Identify what changed since the last review pass (commit 891e763)
  • Review new/changed files against CLAUDE.md and repo conventions
  • Post inline findings and final summary

Summary

This lane already reviewed the PR at commit 4986665 (see the prior "Claude finished" comment above). Since then the branch merged origin/main (commit 7496545, pulling in unrelated, already-on-main changes from .github/recurring-schedule.json and plugins/playbooks) and added one new commit, 891e763:

docs(claude-config): stamp I23's second source with its own recheck trigger

That's the only genuinely new content in this PR versus the last review — confirmed via git diff 4986665..891e763 --stat and cross-checked against the PR's file list (relative to origin/main), which shows no files outside the previously-reviewed 18.

The new commit (criteria.md:1368-1375) adds a "Second-source recheck trigger" clause to I23's residency bullet, naming what would invalidate the disable-model-invocation: true-suppresses-description claim (the Skills page's invocation-control table changing which fields keep a description resident). I traced this against:

  • The catalog's own stated policy for recheck triggers (criteria.md:13-22: "a claim, its basis, an as-of date, and a recheck trigger naming an observable event") — this addition satisfies that shape and closes a real gap: the row's existing Verified 2026-08-08 / Recheck trigger pair (criteria.md:1395-1403) is scoped to the Fable 5 prompting-guide source only, so the Skills-page citation two bullets earlier previously carried a verification date with no trigger of its own — an inconsistency with every other multi-source row in the file.
  • No version bump accompanies it, and none is required: plugin.json is already at 0.26.0 / catalog 1.20.0 from the prior commit in this same PR, and scripts/check-changelog-parity.sh --check-bump only gates a version change lacking a new changelog entry — a same-version content refinement isn't in its scope.
  • No script or test references "recheck trigger" text (instruction-scan.sh/.test.sh pattern-match I23_ERE against skill files, not criteria.md prose), so this is a documentation-only change with no executable surface to break.

I found no defect in this addition — it's small, internally consistent with the row it amends, and consistent with the file-wide recheck-trigger convention. No new inline comments filed.

Still open from the prior pass (unaddressed by this commit, since it only touches criteria.md): the Codex comment and this lane's own prior finding on zone-crossing-inject.sh:155-157 — routing the continuation menu through systemMessage versus docs/conventions/hook-observability/README.md's scoping of that channel to missing-prerequisite skips and unrequested mutations. That's a substantive, pre-existing finding, not something introduced by this commit; I'm not re-filing it, just flagging it's still unresolved as of 891e763.

Not independently verified this pass (sandbox declined non-interactive script execution again): instruction-scan.test.sh, zone-crossing-inject.test.sh, and the changelog-parity/shellcheck checks. Nothing in this commit's diff touches those scripts, so there's no new reason to doubt the previously-reported 20/20 and 56/56 results, but I did not re-run them myself.
— branch feat/fable5-followups

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 1m 15s —— View job


Security Review — Complete

  • Gather context (PR diff, changed files)
  • Review hook script changes (context-guard/hooks/zone-crossing-inject.sh)
  • Review shell script changes (claude-config instruction-scan.sh)
  • Review skill/markdown/config changes for injection or trust-boundary issues
  • Post findings

Reviewed the full diff at HEAD 891e763 (gh pr diff 2031, 18 files) against this lane's scope: injection surfaces, unsafe handling of untrusted data crossing a trust boundary, authorization/access-control gaps, and dangerous automation patterns. The one commit added since the prior security pass on this PR (891e763, "stamp I23's second source with its own recheck trigger") is a 4-line prose-only change to criteria.md with no executable surface.

No security vulnerabilities found in this PR.

Reasoning:

  • plugins/context-guard/hooks/zone-crossing-inject.sh (the substantive change) — now builds two strings, guidance (→ additionalContext) and operator (→ systemMessage), both passed through hook::emit_channels, which JSON-escapes each via hook::json_escape (hook-utils.sh:38-49, unchanged by this PR) before embedding into the emitted JSON — no unescaped-JSON injection into stdout.
    • The only variable data interpolated into either string is prev_label/zone_label. zone_label is either a fixed literal (dumb (evidence-degraded: …)) or $zone, gated to exactly smart/acceptable/dumb before use (zone-crossing-inject.sh:87, unchanged). prev_label comes from $last, read from a per-session state file through tr -cd '[:lower:]' | head -c 16 (zone-crossing-inject.sh:104, unchanged), bounding it to ≤16 lowercase characters even from a corrupted state file. Neither value can carry shell metacharacters or unescaped quotes into either channel.
    • $SESSION (the untrusted stdin field) is validated against ^[A-Za-z0-9_-]+$ (zone-crossing-inject.sh:71, unchanged) before any use, including the state-file path, and is never re-interpolated into either emitted string.
    • The new operator (systemMessage) content is static template prose — nothing user- or model-supplied reaches it.
  • plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.shI23_ERE is a fixed, hardcoded extended-regex literal, passed to grep -niE "$I23_ERE" "$file" the same way the pre-existing I8_*/I10/I27 patterns are. No user input is spliced into the pattern; it's advisory tooling reading operator-supplied file arguments — no regex- or command-injection surface.
  • Markdown/prose-only changes (SKILL.md, README.md, CHANGELOG.md, reference/criteria.md, docs/CATALOG.md) and the four plugin.json version/description bumps carry no executable surface, no permission-grant changes, and no config/settings changes.
  • No GitHub Actions workflow files, .mcp.json, or permissions:/settings surfaces are touched, so pull_request_target/workflow_run/script-injection/permission-widening/supply-chain checks don't apply — nothing to defer to zizmor either, since there's no workflow diff for it to cover.

This confirms the same conclusion as the prior security pass on this PR (commit 4986665) — the intervening commit didn't change the analysis.

…vention its predicate

Two independent reviewers landed on the same line from different angles, and
the findings turned out to be separate defects rather than one.

The model channel said "The operator has been shown the continuation options and
owns that choice." A hook cannot know that. systemMessage is documented only as
a message shown to the user, and nothing upstream describes its behavior in
non-interactive runs, so the claim is unknowable in every mode rather than only
headless ones. It now states ownership without delivery: continuation is the
operator's call, and nothing is being asked of the model. A regression assertion
rejects "has been shown" so the sentence cannot creep back.

The routing finding is real against the hook-observability convention's text but
not against its reasoning. That convention excludes advisory findings from
systemMessage because misrouting AGENT-ACTIONABLE content to the user channel is
the harm — and check I23 forbids the model to act on a continuation menu, so the
antecedent is false here. The exclusion now names its own predicate, who can act,
and admits a carve-out on three conditions together: the payload is a choice whose
only legitimate actor is the human, the model channel separately carries the
determination the model does need, and the emission is keyed to a state
transition. The convention also now forbids any model-channel text from asserting
operator presence, fleet-wide.

The carve-out is written as a property rather than as a reference to I23, so a
consumer without that plugin still has a rule that means something. It creates an
exception rather than codifying practice — every other systemMessage site in the
fleet is a prerequisite skip or a formatter's content-mutation notice — and the
conformance section says so.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NNmc2T3y81efZTaDZHerWo
@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 2m 22s —— View job


Code Review

  • Gather context (PR body, diff, prior review comments)
  • Identify what changed since the last review pass (commit 891e763)
  • Review new commit b79a2ff against CLAUDE.md and repo conventions
  • Post inline findings and final summary

Summary

This lane already reviewed the PR through commit 891e763. The only new content since then is commit b79a2ff — "fix(context-guard): stop asserting operator receipt, and give the convention its predicate" — which directly resolves the two open review threads from the prior passes:

  1. Codex's finding that routing the continuation menu through systemMessage conflicted with docs/conventions/hook-observability/README.md's scoping of that channel to prerequisite-skip/content-mutation cases. This commit takes the option Codex itself offered — amend the convention — adding a three-condition carve-out (README.md:87-99): the payload must be a choice only a human can legitimately act on, the model channel must separately carry what the model needs, and emission must be keyed to a state transition rather than every invocation. I checked all three against zone-crossing-inject.sh's actual behavior and they hold: the continuation menu is human-only-actionable, additionalContext still carries the zone determination + counter-steer independently, and the hook only fires on a worsening zone crossing (((new_rank > last_rank)) gate, unchanged). The Conformance section is updated to name this hook as the sole admitted site rather than a precedent to follow.

  2. My own prior finding (review comment) about headless sessions where nobody reads systemMessage. Rather than changing the channel split, this commit removes the one claim that made the old behavior actually wrong: the model channel previously asserted "The operator has been shown the continuation options and owns that choice" — a delivery claim the hook cannot know in any mode (interactive or headless), since nothing in the documented hook contract reports operator presence. It's now "Continuation is the operator's call, and nothing here is being asked of you" — ownership without a delivery claim (zone-crossing-inject.sh:157). The new README "Honest limit" / "Delivery may never be asserted" language explicitly names the headless case as an accepted tradeoff ("Emitting to an unread operator channel is harmless; telling the model a human holds the choice when none does is not") rather than leaving it a silent gap — which is what I'd asked for.

A regression test is added (zone-crossing-inject.test.sh:90-96) asserting the model channel claims ownership ("operator's call") without claiming delivery ("has been shown" / "shown to") — correctly locks the fix against a future rewording. I searched the full tree for the retired "has been shown" phrasing and confirmed no stale copies remain outside that negative-assertion test. plugins/context-guard/.claude-plugin/plugin.json stays at 0.5.0 (no re-bump) with the new content appended under the existing ## [0.5.0] CHANGELOG entry — consistent with how 891e763 handled the same-PR claude-config addendum, and correct since the plugin hasn't shipped yet.

No new correctness issues found in b79a2ff; no inline comments filed against it. Both threads still visible above (from Codex and this lane) are now addressed by this commit, not just discussed — worth a maintainer glance to confirm the carve-out's wording is acceptable before resolving them.

Not independently verified this pass: sandbox again declined non-interactive script execution (bash <script>.sh, shellcheck <file> all required approval that wasn't grantable), so I could not re-run zone-crossing-inject.test.sh or shellcheck myself. The reasoning above is a manual trace against the actual diff, not a substitute for running the suite — the PR body's claimed 20/20 and shellcheck-clean results remain author-claimed and unverified by me.
— branch feat/fable5-followups

@claude

claude Bot commented Aug 8, 2026

Copy link
Copy Markdown

Claude finished @kyle-sexton's task in 52s —— View job


Security Review — Complete

  • Gather context (PR diff at HEAD b79a2ff, what changed since last security pass at 891e763)
  • Review new commit for security issues
  • Re-confirm prior findings still hold across full diff
  • Post final review

No security vulnerabilities found in this PR.

What's new since the last security pass on this PR

The last security review on this PR covered up through commit 891e763. One commit has been added since: b79a2ff — "fix(context-guard): stop asserting operator receipt, and give the convention its predicate." It touches 4 files, all non-executable:

  • plugins/context-guard/hooks/zone-crossing-inject.sh — a comment-header addition plus a rewording of the static guidance string literal (removes the claim "the operator has been shown the continuation options" and replaces it with "continuation is the operator's call, and nothing here is being asked of you"). No variable interpolation was added or changed, no new data flow, no new external input touches this string — it's the same fixed template text through the same hook::emit_channelshook::json_escape path reviewed previously.
  • plugins/context-guard/hooks/zone-crossing-inject.test.sh — adds one regression assertion (model channel must claim ownership without claiming delivery). Test-only, no executable surface beyond the test harness itself.
  • docs/conventions/hook-observability/README.md — governance/convention prose, amending the systemMessage scoping rule to name a three-condition carve-out and forbid any model-channel text from asserting operator presence. Documentation only.
  • plugins/context-guard/CHANGELOG.md — changelog entry describing the above. Documentation only.

This commit is a direct, substantive response to the observability-channel concern raised in the prior review round (the Codex comment and this lane's own earlier finding on zone-crossing-inject.sh) — it doesn't introduce new code paths, new input sources, or new interpolation, so it doesn't change the security analysis already given for this PR.

Confirmation of prior analysis (unchanged)

  • zone-crossing-inject.sh's two emitted strings (guidanceadditionalContext, operatorsystemMessage) both flow through hook::json_escape before embedding in JSON — no unescaped-JSON injection.
  • The only variable data interpolated into either string remains prev_label/zone_label, both bounded/validated before use (zone gated to a fixed enum; prev_label derived from a state file via tr -cd '[:lower:]' | head -c 16) — no attacker-controlled prose reaches either channel.
  • $SESSION is validated against ^[A-Za-z0-9_-]+$ before any use and is never re-interpolated into either emitted string.
  • plugins/claude-config/skills/audit-instructions/scripts/instruction-scan.sh's I23_ERE remains a fixed regex literal with no user input spliced in.
  • No GitHub Actions workflow files, .mcp.json, or permissions:/settings surfaces are touched anywhere in this PR.

@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Both findings addressed in b79a2ff — and they are two defects, not one

Adjudicated by two independent verifiers with my reasoning withheld. They converged on the shape and split on the remedy's form; I took the narrower one.

The Claude finding is real, and worse than reported. zone-crossing-inject.sh:148 said "The operator has been shown the continuation options and owns that choice." That asserts a fact the hook cannot know in every mode, not only headless onessystemMessage is documented solely as "Warning message shown to the user", with nothing upstream describing non-interactive behavior. It now reads "Continuation is the operator's call, and nothing here is being asked of you": ownership without delivery. The following sentence already carried headless correctly on its own — with no operator asking, the model keeps working, and its third licensing signal (visible decay in its own output) is the one I23 sanctions. A regression assertion now rejects has been shown so the sentence cannot creep back on a rewording pass.

The Codex finding is real against the convention's text but not against its reasoning. docs/conventions/hook-observability/README.md:79-82 excludes advisory findings from systemMessage because the harm is misrouting agent-actionable content to the user channel — "a finding to Claude for it to act on". Under I23 the model is forbidden to act on a continuation menu, so that antecedent is false here. The rule reached the hook; the reasoning did not.

So the convention gives, by amendment on its own principle:

  • Its advisory-findings bullet now names its predicate — who can act — and admits a carve-out on three conditions together, never a judgment call: the payload is a choice whose only legitimate actor is the human; the model channel separately carries the determination the model does need; and the emission is keyed to a state transition.
  • A new fleet-wide prohibition: model-channel text may say a choice is the operator's, never that the operator has seen it.
  • The stale section-2 heading is corrected, and the conformance section carries the new test.

Two things stated rather than left for a reviewer to find:

  • This creates an exception; it does not codify practice. Verified by enumeration — every other systemMessage site in the fleet is a prerequisite skip or a formatter's content-mutation notice (bash-format.sh:273, markdown-format.sh:992, typos-format.sh:457). context-guard is the only one admitted this way, and the conformance section says a second one should re-read the conditions rather than follow the precedent.
  • An honest limit. The docs state additionalContext is inserted into the conversation and saved to the transcript, and say no such thing about systemMessage; that the latter stays out of model context is inferred from the asymmetry, not stated. That inference is recorded in the convention, with the consequence if it is ever falsified: drop the payload, do not re-route it.

The carve-out is phrased as a property rather than as a reference to I23, so a consumer without claude-config installed still has a rule that means something.

Ownership checked before editing: docs/conventions/ is absent from standards' sync-manifest.yml (which manages only docs/adr/*), so this file is locally owned.

One correction to a claim made during adjudication, since it nearly became a constraint in the convention: this hook does not fire at most twice per session. The gate is ((new_rank > last_rank)) with the zone persisted on every observation including improvements, so a relapse re-fires — unbounded in count if the zone oscillates. The convention's condition 3 says "keyed to a state transition, not to every invocation", which remains true and is the actual discriminator. Whether to add a per-session cap is a separate calibration question, not part of this decision.

@kyle-sexton
kyle-sexton merged commit c973b61 into main Aug 8, 2026
33 checks passed
@kyle-sexton
kyle-sexton deleted the feat/fable5-followups branch August 8, 2026 23:15
kyle-sexton added a commit that referenced this pull request Aug 8, 2026
…ontract (#2032)

Closes #2010

## Summary

The third follow-up from the Fable 5 prompting-guide alignment audit
(#2000). The issue named the
real remainder plainly: nothing shipped the autonomous-pipeline reminder
as a reusable artifact for
a consumer's *own* pipeline — it existed only hand-authored inline in
two of this repository's three
lane launch prompts, which is a launch surface for these lanes and not
something anyone else can
use. The issue left one shape to decide first: whether a pointer plus a
locally-authored equivalent
earns its place, or whether the doctrine surface already suffices.

**It earns its place.** `autonomy` is the consumer-facing
autonomous-pipeline seam an adopting org
installs and binds; `playbooks:fable-5` is a separately installable
doctrine plugin with no
dependency wiring between them. An org that installs `autonomy` to run a
governed pipeline should
not have to also install and arm a second plugin to learn that its
pipeline must not end a turn on
unexecuted intent. This is the same composition reasoning applied to
`context-guard` in #2031.

## Fix

**New: `plugins/autonomy/reference/autonomous-pipeline-reminder.md`**
(`0.12.3` → `0.13.0`).

The file states the two stopping failures a pipeline cannot recover from
— a turn ending on
unexecuted intent, and a turn stopping to ask permission nobody is there
to give — notes that both
are *stopping* failures rather than *doing* failures (which is why
review catches them late: the
artifact looks reasonable and only the absent effect gives it away),
then gives the paste-ready
clause set:

- proceed without asking on anything reversible that follows from the
original request;
- pause only for a destructive or irreversible action, one that leaves
the working environment, a
  real change of scope, or input only the launcher can supply;
- ask once and never re-ask what is already settled — offering
follow-ups once done is a report,
  not a request;
- read the final paragraph back before ending a turn, and if it
describes an action rather than
  reporting one, do it now with tool calls;
- an enumeration of the shapes that are work orders to act on rather
than messages to end on;
- end the turn only on completion or a genuine block, and say what the
block is.

The companion checkpoint instruction the source guide asks to be paired
with this reminder is folded
into the pause clause, so a consumer pastes one block instead of
noticing a cross-reference and
assembling two.

**Locally authored, not reproduced.** Copying the upstream text would
have violated this
repository's own rule against hand-copying upstream content — which is
exactly why the issue was
filed rather than fixed in #2000. The wording is this repository's own;
the citation, the exact
section, and the recheck trigger are recorded in the plugin README.

**Two boundaries ship with it**, because an artifact that reads as
universally applicable gets
applied where it does damage:

- **An attended lane deliberately does not carry it.** "Recommend, then
wait for my direction" is
the opposite posture, and pasting the block into one converts a working
human-in-the-loop review
into an agent acting on its own recommendations. So the two-of-three
split the issue observed is
the contract, not an inconsistency — this repository's attended-queue
prompt opens with "I am
  present. Recommend, then wait for my direction before mutating."
- **The `lane-stop-gate` hook mechanizes exactly one clause.** It
performs no content classification
beyond its literal sentinel check, so it cannot tell a blocked-on-user
stop from a lazy one; both
get the same single nudge. That over-blocking stays benign — a genuinely
blocked lane costs one
wasted nudge and then stops with the operator alerted — but it is
over-blocking, not
classification. The scope is now stated in the gate's **own header**, so
a reader of the hook does
  not infer coverage it does not have.

**Also changed:** `docs/conventions/loop-lane/README.md` now points at
the reference for the clause
set rather than leaving it implicit in the launch prompts, keeping only
the two boundaries that are
lane-topology facts rather than reminder content.

## Verification

- `scripts/validate-plugins.sh` — passes. This caught a real contract
violation on the first run:
`autonomy reference/ contracts must use surface classes, never vendor
names`
(`scripts/validate-plugin-contracts.mjs:236`). The provenance citation
moved to the plugin
`README.md`, which is the surface that may name a vendor, and
`reference/` points at it. Grepped
  the new file for every banned token — clean.
- `python3 scripts/check-contract-clause-coverage.py` — passes; 4
canonical surfaces, 14 tagged
  restatements, 10 surfaces that point rather than restate.
- `scripts/check-changelog-parity.sh --check-bump origin/main` and
`--check-order` — the version
  bump carries its `## [0.13.0]` entry; 72 changelogs read newest-first.
- `shellcheck plugins/autonomy/hooks/lane-stop-gate.sh` — clean. The
hook edit is comment-only; no
  executable line changed.
- `markdownlint-cli2` over the four changed/added markdown files — 0
issues.
- `node scripts/generate-catalog.mjs` — catalog already in sync (no
manifest description change).
- **Clause coverage checked against the source, not from recall.** The
guide page was fetched this
session and its "Rare cases of early stopping" snippet read verbatim;
the local wording was then
revised to cover two clauses an earlier draft had missed (a plan or list
of remaining steps, and
an analysis standing in place of acting on it) and to add the
scope-change pause condition from
  the companion checkpoint instruction.

## Related

- Refs #2000 — the alignment PR that filed this follow-up.
- Refs #2031 — the sibling PR closing #2009 and #2011. No file overlap;
kept separate because it
  turns on a different decision.
-
<https://platform.claude.com/docs/en/build-with-claude/prompt-engineering/prompting-claude-fable-5>
— fetched this session for the "Rare cases of early stopping" clause set
and the "Strong
  instruction following" checkpoint instruction it pairs with.

### Scope note

The issue judged the gate's partial coverage low priority rather than a
defect, and this PR does not
change that judgement or the gate's behavior. It closes the
documentation half — the reusable
artifact, and an honest statement of what the mechanism covers — and
leaves the gate's classification
limits as they are, now stated rather than implicit.

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
@kyle-sexton

Copy link
Copy Markdown
Contributor Author

Post-merge note: an independent fresh-context calibration study of the I23 pre-scan question ran in parallel with this PR (neither knew of the other) and corroborates its direction. Highlights worth keeping with the record:

  • Empirical variance: ~29 distinct surface forms across 26 attested rows in 11 files (4 literal percentage forms, 10 non-numeric budget phrasings, 4 window-position phrasings, 7 quality-degradation phrasings) — confirming the original "calibrate before seeding" premise.
  • The discriminator that actually separates the classes in this tree is whether an instrument is named (e.g. "check /context output" / zone words / used_percentage), not description-vs-body location.
  • A conservative three-family seed (bare-percentage N% used, heavy-context+directive, quality-degradation+directive) matches 9 class-(a) rows across 3+ files with zero hits on invocation-trigger or measured-mechanism rows; the one whole-repo false positive is a quoted-doc shape already covered by I8-b's fence pattern. Window-position and "context budget" phrasings are fixture-grade (single-file) and per the study should NOT be seeded — I8-d/I8-e style declines.
  • Self-reference caveat: an I23 criteria row quoting these phrasings becomes its own hit; it needs the "document about the pattern" fence I8-b/I8-e carry.
  • Git-blame gradient: every self-estimating instruction predates the measured-mechanism router (2026-07-29); the remaining unmigrated class-(a) rows are in session-flow retro/handoff bodies and knowledge course-digest/book-distill.

Full study is in the work-stream session record; surfacing here so the next I23 calibration round starts from data, not re-derivation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant