From abaa8425d85443921ea48629d073468f00605989 Mon Sep 17 00:00:00 2001 From: Scout Date: Sat, 26 Sep 2026 23:00:40 -0400 Subject: [PATCH 1/4] SubagentStart token-lanes carrier for non-blind subagents Adds a stdlib, fail-open SubagentStart hook that returns adoption/hooks/claude/token-lanes-block.md as additionalContext to every subagent except agent types starting "blind-", registers it in the Claude settings template beside ai-memory, and installs both files through the existing sha256-checked guard step. docs/token-session-handbook.md "Token lanes carried into subagents" is the source of truth the block copies line for line. Its RTK line is the measured RTK 0.50.0 rule (rtk hook check and the rtk hook claude PreToolUse hook; rewrite_multiline_block, rtk-ai/rtk#3319): && chains and multi-line blocks are rewritten segment by segment; $(...), backticks, <(...), file redirects and heredocs are never rewritten; gh pipelines are not rewritten. Native probe (Claude Code 2.1.283, Agent-tool Haiku children): with the hook the child found and quoted the block's first 60 characters exactly; without it, and for a blind-judge child with it, the child found nothing. Decision record: docs/decisions/2026-09-27-token-lanes-subagent-start.md. Co-Authored-By: Claude Opus 5.5 (1M context) --- adoption/bootstrap.md | 15 +- adoption/hooks/claude/SHA256SUMS | 2 + adoption/hooks/claude/token-lanes-block.md | 11 + .../claude/token-lanes-subagent-start.py | 40 ++++ .../templates/claude.settings.template.json | 10 + .../2026-09-27-token-lanes-subagent-start.md | 199 ++++++++++++++++++ docs/harness-defaults.md | 3 + docs/token-session-handbook.md | 47 +++++ tests/test_install_claude_profile.py | 68 +++++- tests/test_token_lanes_subagent_start.py | 139 ++++++++++++ tools/adoption/install_claude_profile.py | 10 +- 11 files changed, 536 insertions(+), 8 deletions(-) create mode 100644 adoption/hooks/claude/token-lanes-block.md create mode 100644 adoption/hooks/claude/token-lanes-subagent-start.py create mode 100644 docs/decisions/2026-09-27-token-lanes-subagent-start.md create mode 100644 tests/test_token_lanes_subagent_start.py diff --git a/adoption/bootstrap.md b/adoption/bootstrap.md index c06b090d4..10eb16378 100644 --- a/adoption/bootstrap.md +++ b/adoption/bootstrap.md @@ -249,6 +249,12 @@ GitHub-hosted macOS runner; see and `OTEL_LOG_TOOL_DETAILS` to `"1"`, a dated user exception whose Collector filter exports only tool, MCP server, skill and agent names ([tool details](../docs/secret-storage.md#telemetry-and-pasted-values)). + `claude.settings.template.json` also changed after `v2026.09.26.2`: it adds a + `SubagentStart` group that runs `~/.claude/hooks/token-lanes-subagent-start.py`, + installed by the **guard hooks** step of `install_claude_profile.py` below, so + every non-blind subagent receives the token-lanes block + ([decision](../docs/decisions/2026-09-27-token-lanes-subagent-start.md)); while that + file is absent the command exits 0 and adds nothing. `codex.config.template.toml` changed after `v2026.09.26`: it turns the context-mode plugin's own MCP server off and registers context-mode at user scope with no `cwd`, running the pinned npm install's `start.mjs`, so each Codex session's server binds that session's own directory ([recipe](../recipes/README.md#retained-context-mode)), and its `headroom` entry adds `HF_HUB_OFFLINE` and `TRANSFORMERS_OFFLINE`; `project.codex.config.template.toml` changed after `v2026.09.26` in its comments only. The recipe's project-scoped alternative changed after `v2026.09.26.2`: it adds `default_tools_approval_mode = "approve"` and a `CLAUDE_PROJECT_DIR` equal to its project directory, as upstream `start.mjs` sets, so a project entry keeps Codex tool approvals and the server-side project `Bash(...)` denies ([recipe](../recipes/README.md#retained-context-mode)). The rendered `codex.config.toml` keeps the source host's `trusted_hash` @@ -311,7 +317,14 @@ GitHub-hosted macOS runner; see to `~/.claude/hooks/effort-default-guard.py` and the secret guard [`scripts/hooks/secret_path_guard.py`](../scripts/hooks/secret_path_guard.py) to `~/.claude/hooks/secret_path_guard.py` (the secret guard and its - settings entries were added after `v2026.09.24.1`), refusing to install either unless + settings entries were added after `v2026.09.24.1`). This step **changed after `v2026.09.26.2`**: + it also copies [`adoption/hooks/claude/token-lanes-subagent-start.py`](hooks/claude/token-lanes-subagent-start.py) + and its sibling [`adoption/hooks/claude/token-lanes-block.md`](hooks/claude/token-lanes-block.md) + into `~/.claude/hooks/` (both files added after `v2026.09.26.2`). + The hook supplies token-lane guidance before each non-blind subagent's first prompt + through the [SubagentStart context contract](https://code.claude.com/docs/en/hooks#subagentstart); + `blind-*` roles receive no context from this hook. + It refuses to install any file unless every sha256 matches [`adoption/hooks/claude/SHA256SUMS`](hooks/claude/SHA256SUMS) (paths relative to that file); skipped per file if the installed copy already matches. diff --git a/adoption/hooks/claude/SHA256SUMS b/adoption/hooks/claude/SHA256SUMS index f2465f3a8..d08e4e140 100644 --- a/adoption/hooks/claude/SHA256SUMS +++ b/adoption/hooks/claude/SHA256SUMS @@ -1,2 +1,4 @@ df3248659c191d8800dfd64a8c7d415fc13652cebee08c9437bf579862c2e561 effort-default-guard.py ed5556f0cb4942859cf9e8e2e6f709de4dbc5b96163fc6ef65fcd0dcab8da153 ../../../scripts/hooks/secret_path_guard.py +8b9a60fa425900930c610ba7e3e0f8a0fa1457048a39c79e3136fb5073e7269f token-lanes-block.md +7a9228e41235771336189287944d81d998795b025a9a038d022beb21a19ce870 token-lanes-subagent-start.py diff --git a/adoption/hooks/claude/token-lanes-block.md b/adoption/hooks/claude/token-lanes-block.md new file mode 100644 index 000000000..75ff95c0b --- /dev/null +++ b/adoption/hooks/claude/token-lanes-block.md @@ -0,0 +1,11 @@ +TOKEN LANES (source: docs/token-session-handbook.md, "Token lanes carried into subagents"). +- Load deferred token tools in ONE ToolSearch call before first use: "select:mcp__plugin_context-mode_context-mode__ctx_execute,mcp__plugin_context-mode_context-mode__ctx_batch_execute,mcp__plugin_context-mode_context-mode__ctx_search,mcp__serena__find_symbol,mcp__serena__find_referencing_symbols"; add jcodemunch/codebase-memory/socraticode/qmd/ai-memory/headroom names to that same call when this task needs them. +- For output over ~5 KB, use ctx_batch_execute / ctx_execute / ctx_search; print derived answers, keep failures and original-output recovery. The context-mode server is bound to the main checkout: explicitly cd to the actual working directory. Open authorized scratch files from Python inside ctx_execute; ctx_execute_file enforces project boundaries (#852). Never bypass permissions. +- Automatic RTK: a Bash call containing $(...), backticks, <(...), a redirect to a file or a heredoc is never rewritten. && chains and multi-line blocks are rewritten segment by segment. In a pipeline RTK rewrites a supported producer feeding cat, head or tail (git diff | head -n 5) or a final grep/rg stage, but not gh (gh pr view 1 | head -n 5 is not rewritten). Run heavy reads as their own call. Never directly prefix rtk before `git show REV:path`, `diff`, `git branch`, `git log`, `jq`, `cd`, or `find` on a possibly-missing path. Use native commands or `rtk proxy ` for exact bytes/exit status; process large results in ctx_execute. +- Use Serena find_symbol / find_referencing_symbols for exact symbols and references; jcodemunch route(task, repo?, execute?), menu(query?), order(action, args) on indexed repos; socraticode codebase_search(query, projectPath) with explicit projectPath for conceptual questions. Open original source before judging or editing. +- Use codebase-memory trace_path (include_evidence=true adds resolver class and confidence; this parameter is on trace_path, not search_graph) or search_graph for symbol/reference queries; treat edges below confidence 0.5 as unverified candidates, and verify exact caller lists with Serena find_referencing_symbols. +- For the indexed catalog, use qmd query (us-equities-foundation / us-equities-catalog), then get. Use ai-memory memory_query with workspace/project from .ai-memory.toml as historical evidence only, never authority. +- Use TOON for uniform arrays of flat records (same keys in every item); keep compact JSON for nested or non-uniform data, where TOON can be larger (upstream README). +- For large selected text, use headroom_compress, then headroom_retrieve for recovery; require fidelity and count the recovery cost. +- Use one lane per artifact; never stack compressors or claim token savings. At the end of your return, list the token tools used and why. +- Before claiming a task done, follow the installed verification-before-completion skill: real command output before any success claim. Research upstream first with the installed search-first skill before writing custom code. Source: already installed: skillOverrides in adoption/templates/claude.settings.template.json. diff --git a/adoption/hooks/claude/token-lanes-subagent-start.py b/adoption/hooks/claude/token-lanes-subagent-start.py new file mode 100644 index 000000000..2cf66da72 --- /dev/null +++ b/adoption/hooks/claude/token-lanes-subagent-start.py @@ -0,0 +1,40 @@ +#!/usr/bin/env python3 +"""Inject sibling token-lanes text for non-blind subagents; fail open on errors. + +SubagentStart supplies agent_type: https://code.claude.com/docs/en/hooks#subagentstart +Fail-open/output pattern: adoption/hooks/claude/effort-default-guard.py. +""" + +import json +import os +import sys + + +def main(): + try: + data = json.load(sys.stdin) + except Exception: + return + agent_type = data.get("agent_type") + if isinstance(agent_type, str) and agent_type.startswith("blind-"): + return + path = os.path.join(os.path.dirname(os.path.abspath(__file__)), "token-lanes-block.md") + with open(path, encoding="utf-8") as source: + block = source.read() + if not block: + return + output = {"hookSpecificOutput": { + "hookEventName": "SubagentStart", "additionalContext": block, + }} + # Unbuffered output leaves no failing flush to change the exit status at shutdown. + encoded = (json.dumps(output) + "\n").encode("utf-8") + while encoded: + encoded = encoded[os.write(1, encoded):] + + +if __name__ == "__main__": + try: + main() + except Exception: + pass + sys.exit(0) diff --git a/adoption/templates/claude.settings.template.json b/adoption/templates/claude.settings.template.json index fa02b626b..3faac7b5a 100644 --- a/adoption/templates/claude.settings.template.json +++ b/adoption/templates/claude.settings.template.json @@ -197,6 +197,16 @@ "command": "${ECO_ROOT}/tools/ai-memory-2.4.1/ai-memory --data-dir ${HOME}/.local/share/ai-memory hook --event subagent-start --agent claude-code --server-url http://${AI_MEMORY_URL}" } ] + }, + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "python3 \"${HOME}/.claude/hooks/token-lanes-subagent-start.py\" 2>/dev/null || true", + "timeout": 5 + } + ] } ] }, diff --git a/docs/decisions/2026-09-27-token-lanes-subagent-start.md b/docs/decisions/2026-09-27-token-lanes-subagent-start.md new file mode 100644 index 000000000..28b8923ec --- /dev/null +++ b/docs/decisions/2026-09-27-token-lanes-subagent-start.md @@ -0,0 +1,199 @@ +# Decision: carry token lanes through SubagentStart (2026-09-27) + +**Decided by:** unit `token-lanes-subagent-start`, branch +`claude/token-lanes-subagent-carrier-20260927`, implementing the user's +SubagentStart build task for Ultracode workflow children. +The UTC date was read with `date -u +%F`. + +**Scope:** the portable Claude hook and sibling text, the existing profile +installer's `HOOKS` map, one additional settings-template hook group, the +handbook, bootstrap instructions and local subprocess/integration tests. The +implementation follows the official [SubagentStart JSON contract](https://code.claude.com/docs/en/hooks#subagentstart) +and this repository's [effort guard fail-open/output pattern](../../adoption/hooks/claude/effort-default-guard.py). +No host installation or service change is part of this unit. + +## Evidence + +**WP1 scope.** The [retained baseline README](../../evidence/artifacts/child-lane-baseline-20260926/README.md) +and [`claude-lanes.json` `groups.by_spawn`](../../evidence/artifacts/child-lane-baseline-20260926/claude-lanes.json) +cover 639 children: 597 through Workflow and 42 through the Agent tool. Workflow +children received the block in 0/597 prompts and 0/597 SubagentStart contexts; +Agent-tool children received it in 42/42 prompts and 0/42 SubagentStart contexts. +All 30 `subagent_start` aggregates in that JSON have `additional_context: 0`. +The brief also reports "only 22 of 1,010 Ultracode workflow children" received +guidance; those totals are not supported by these retained files and are not +treated as the verified WP1 denominator. + +**Upstream gap.** Context-mode 1.0.169's [hook manifest at +`589d8214d56740a28b5f7bf63167743d586b0b40`](https://github.com/mksglu/context-mode/blob/589d8214d56740a28b5f7bf63167743d586b0b40/hooks/hooks.json) +registers no SubagentStart hook. Its [child prompt-rewrite route](https://github.com/mksglu/context-mode/blob/589d8214d56740a28b5f7bf63167743d586b0b40/hooks/core/routing.mjs#L892) +uses `PreToolUse:Agent`; [SessionStart also injects routing context](https://github.com/mksglu/context-mode/blob/589d8214d56740a28b5f7bf63167743d586b0b40/hooks/sessionstart.mjs#L166). +Therefore "only via PreToolUse:Agent" would overstate the source. Both cited +issues were independently read: [#64](https://github.com/mksglu/context-mode/issues/64) +concerns obsolete Task matching missing Agent calls, while [#233](https://github.com/mksglu/context-mode/issues/233) +concerns user-facing commands reaching subagents in 1.0.75 and describes +SessionStart injection; it is not evidence that every other injection path is absent. + +Read-only inspection of both installed plugin metadata files returned 1.0.169; +the installed Claude `hooks/hooks.json` registered `PreToolUse:Agent` and no +SubagentStart event. Its `hooks/pretooluse.mjs` delegates to +`hooks/core/routing.mjs`, whose Agent branch builds the child block with +`includeCommands: false` and the ToolSearch bootstrap. The +[v1.0.169 release notes](https://github.com/mksglu/context-mode/releases/tag/v1.0.169) +describe accounting changes, not a new child carrier. Installed `claude --version` +returned `2.1.283 (Claude Code)`; the [upstream changelog at `7779afb1`](https://github.com/anthropics/claude-code/blob/7779afb12e3635f46f56ec823979d68350ae000b/CHANGELOG.md) +records SubagentStart's introduction at 2.0.43, command-type-only SubagentStart +hooks at 2.1.142, and at 2.1.265 a fix that keeps SubagentStart context in a +resumed subagent's prompt prefix. These source/version checks are not a native +child execution; the probe under Limitations is. + +**Native event contract.** The [official hooks reference](https://code.claude.com/docs/en/hooks#subagentstart) +names `agent_type` in the input and uses the agent name for matching; an omitted +matcher or `"*"` matches every occurrence, and the template's `""` matcher matched +in the native probe under Limitations. `hookSpecificOutput.additionalContext` is +added to the subagent's context before its first prompt. When a later hook run +finds the earlier copy still in context, Claude Code keeps that copy and the prompt +cache; after auto-compaction discards it, the next run's context is injected again. +For a plugin-shipped agent, `agent_type` is the plugin-scoped name, such as +`my-plugin:reviewer`. + +The [context-output section](https://code.claude.com/docs/en/hooks#add-context-for-claude) +specifies that values over 10,000 characters go to a session file and Claude Code +"passes Claude the file path with a preview of up to the first 2,000 characters instead." +This is an inline-output threshold, not a hard rejection limit or a guarantee +that the subagent reads the file. The carrier has a stricter local budget of +3,500 bytes, checked by the [text contract test](../../tests/test_token_lanes_subagent_start.py). + +**Research and adoption.** The existing upstream plugin and its published hook +manifest were reviewed before selecting this small adapter. The supplied skill +catalog exposes `search-first`, TDD and `verification-before-completion`; no +separate skill-discovery tool was exposed, and no package installation is needed. +Already installed: [`skillOverrides` in `adoption/templates/claude.settings.template.json`](../../adoption/templates/claude.settings.template.json) +sets `verification-before-completion` to `on` and `search-first` to `name-only`, +not both to `on`. The [handbook subsection](../token-session-handbook.md#token-lanes-carried-into-subagents) +cites the RTK 0.50.0 source and rewrite PR, TOON README, jCodeMunch's three-verb front door, +codebase-memory schemas and other selected tool sources for each routing rule. + +Jcodemunch and ToolSearch were not exposed in the builder sandbox, so its +`route`/`menu`/`order` descriptions were read in upstream `server.py` at the +pinned v1.108.319 (`8f7b34ab`, lines 341-343). The codebase-memory evidence +parameter was checked against the connected MCP schema and upstream `mcp.c` at +the pinned v0.11.0, where `include_evidence` (L557) sits in the `trace_path` +schema (from L532), not in `search_graph` (L483). +The [adopted skills manifest](../../adoption/skills/manifest.json) and its +[pinned find-skills reference](https://github.com/vercel-labs/skills/blob/7407f3893ad4dceab546ac002c3ef806e4000c73/skills/find-skills/SKILL.md) +provided discovery; no registry install was needed. + +### Anti-pattern log + +| Proven mistake | Correction and source | +| --- | --- | +| Passing `include_evidence` to `search_graph` | The exposed schema puts it on `trace_path`. Discover nodes first, then request path evidence. | +| Attributing a five-record TOON threshold to upstream | Its README describes uniform shared-field records but specifies no five-record minimum. Five is a local eligibility rule, not a measured break-even. | +| Claiming RTK never rewrites multiple lines | RTK 0.50.0 rewrites a multi-line block per line (`rewrite_multiline_block`, rtk-ai/rtk#3319); `rtk hook check` and the `rtk hook claude` hook both returned `rtk git status` and `rtk git diff --stat` for those two lines. The earlier negative came from a block of `echo` lines, which RTK never rewrites. The block states the measured rule. | + +## Decision + +Ship [`token-lanes-subagent-start.py`](../../adoption/hooks/claude/token-lanes-subagent-start.py) +and its [sibling block](../../adoption/hooks/claude/token-lanes-block.md) as the +default routing carrier for every non-blind subagent, including Workflow and +Agent-tool children. The [settings template](../../adoption/templates/claude.settings.template.json) +adds its own empty-matcher group alongside ai-memory; exclusion belongs inside +the hook. All `blind-*` roles receive zero bytes from this carrier, including +`blind-lane-reviewer`, `blind-judge` and `blind-adjudicator`. The +[blind judge leak rules](../../adoption/agents/claude/blind-judge.md) and +[blind comparison protocol](../../blueprints/blind-catalog-convergence/README.md) +require that boundary: tool identities and routing text would contaminate a +sealed comparison. This gate covers this hook, not unrelated installed hooks. + +Keep the [existing installer](../../tools/adoption/install_claude_profile.py) +generic over `HOOKS`; both files are checksum-pinned, installed together and +skipped when byte-identical. The hook resolves the block beside itself, remains +silent for unavailable/empty/unreadable text or malformed input, and exits zero. +Its output contract follows the [official JSON example](https://code.claude.com/docs/en/hooks#subagentstart); +its exception and unbuffered-write handling follow the existing effort guard. + +Use the [handbook](../token-session-handbook.md#token-lanes-carried-into-subagents) +as the text's source of truth. The local tests require its sentences and distinctive +key tool phrases to match the block, exercise the script through subprocesses, +and verify installation plus repeated merging against an existing ai-memory +entry. The merge behavior is defined by [`merge_hooks`](../../tools/adoption/apply_claude_settings.py): +commands are deduplicated across each event, and unseen commands join the first +group with the same matcher. This is local integration evidence, not an upstream +test suite or a new native model run. + +## Alternatives considered + +- **Per-prompt blocks.** WP1 observed 42/42 Agent-tool prompts carrying guidance + and 0/597 Workflow prompts doing so. They remain possible, but leave delivery + dependent on each prompt author. Source: the retained WP1 README and JSON above. +- **PR-B role preloads.** The brief's role-specific skill preload proposal is + complementary: selected named roles can receive their core skills, while the + carrier addresses a per-child routing instruction across non-blind roles. + The distinction follows the [native subagent skill-preload contract](https://code.claude.com/docs/en/sub-agents#preload-skills-into-subagents) + and the SubagentStart event contract, not measured acceptance of PR-B here. + This in-flight change was not inspected; ad-hoc Workflow stages without an + agent definition remain outside named-role preload coverage. +- **Context-mode's PreToolUse:Agent route.** Its pinned routing source handles + Agent prompt input. In this WP1 measurement, that path delivered no block to + Workflow children. The separate SessionStart path prevents claiming this is + the plugin's only injection capability. Sources: the pinned routing and + SessionStart files and WP1 above. +- **PreToolUse on Workflow.** By the [PreToolUse contract](https://code.claude.com/docs/en/hooks#pretooluse), + that hook runs at a tool invocation. Inference: matching a Workflow launch + targets the launch request, not each child created inside it; it could inspect + or modify supported launch input but would need further prompt wiring to + reach each child. SubagentStart is the documented per-subagent injection point. + +## Overturn conditions + +- Reassess this adapter when an upstream context-mode release supplies a + SubagentStart carrier with an equivalent blind-role exclusion and routing + contract; remove duplicate delivery after qualifying that release. Compare + against the pinned manifest cited above. +- Repeat WP1's lane-usage sweep after native acceptance, preserving spawn path + and task mix. Define gain as higher verified context delivery and a higher + share of eligible children using the task-appropriate lanes, without lower + output quality. If delivery works but there is no lane shift, reconsider the + default; count complete provider use separately and make no savings claim + from presence or tool counts alone. The baseline is the retained WP1 JSON. + +## Limitations + +- WP1 is a single-host observation, not a controlled comparison across hosts + or a causal savings experiment; see its retained README's scope. +- This unit's subprocess and temp-HOME checks are local integration checks. +- **Native probe (Claude Code 2.1.283, 2026-09-27 02:37Z to 02:53Z).** Each run was + one `claude -p` session in this checkout (main model Opus 5.5) that launched one + Haiku 4.5 subagent through the Agent tool. The positive arm added a settings file + whose `SubagentStart` group (matcher `""`) ran this branch's hook; the negative + arm used the host's live settings, which have no such hook. A first question, + whether a block whose first line starts with `TOKEN LANES` was present, returned + `NO` in both arms, although the positive subagent's transcript held the block as + a `SubagentStart` `hook_additional_context` attachment, so that question did not + discriminate. The likely reason is that Claude Code frames attached context + (`contextRendering: announced`); the child's reasoning was redacted, so that cause + is not observed. A + second question (find text containing the phrase, other than the task, and quote + the 60 characters that begin with it) returned `NOT FOUND` for the negative + `general-purpose` child and `FOUND` with the block's exact first 60 characters + for the positive one, and `NOT FOUND` for a `blind-judge` child with the hook + enabled. The transcripts agree: one `hook_additional_context` attachment in the + positive `general-purpose` child, none in the other two. This is one native + operation per arm on Agent-tool children. It does not cover Workflow children, + which WP1 shows also reach SubagentStart, nor lane use, compliance or savings. +- The blind gate matches `agent_type` values starting `blind-`, which is how + [`install_claude_profile.py`](../../tools/adoption/install_claude_profile.py) + installs the blind agents (user agents). A plugin-shipped blind agent would + report a plugin-scoped `agent_type` and would receive the block. +- The block is 3,159 bytes and starts literally `TOKEN LANES`, within the 3,500-byte + budget and below the documented 10,000-character + inline-output threshold. A size check cannot prove model compliance. Source: + the official context-output reference and local text test above. +- The new files are absent at the pinned `v2026.09.26.2` release; the + [bootstrap guard step](../../adoption/bootstrap.md) marks both their addition + and the changed hook directory. A new release is a separate step. + +This record claims no after measurement, token savings or live installation. +A native Workflow-child run remains open; text delivery alone does not prove +tool access, compliance or improved outcomes. diff --git a/docs/harness-defaults.md b/docs/harness-defaults.md index c26dcb8f9..688e05578 100644 --- a/docs/harness-defaults.md +++ b/docs/harness-defaults.md @@ -105,3 +105,6 @@ When a claim or action proves wrong, correct it where it was relayed and record | 2026-09-26 | Relaying a worker's absence claim without an upstream check | A docs subagent said Claude Code had no native advisor, the coordinator relayed it, and the user then ran `/advisor`; the upstream CHANGELOG and CLI reference document it (see the check order above) | Steps 1 and 2 of the check order before any absence claim, else "not found in" the sources checked; relay worker answers only with the upstream citation | Top rule steps 2 and 3 in `examples/claude-native/CLAUDE.md`, pinned by `tests/test_install_claude_profile.py` | | 2026-09-26 | Assuming an instruction file delivers what it references | The global Codex `AGENTS.md` held one `@/RTK.md` line, the form `rtk init -g --codex` writes (`codex_rtk_md_ref`, `src/hooks/init.rs` at rtk `v0.50.0`). Codex 0.157.1 passes that line to the model literally (`codex debug prompt-input` read-back; `codex-rs/core/src/agents_md.rs` at `rust-v0.157.1` has no import handling), so the RTK text is not in Codex's instructions; `evidence/artifacts/token-e2e-codex-20260926/README.md` names that reference as RTK's Codex channel | Read back what the model receives (`codex debug prompt-input`; `/memory` or `/context` in Claude Code) before counting an instruction channel, and inline text for Codex, which does not expand `@` imports | This log | | 2026-09-26 | Summarizing evidence more strongly than its source | In this log, the unpublished-commit row named `scripts/host_receipts.py validate` and a fabricated-SHA test as enforcement, although the validator accepts a local-only commit in a worktree that shares the object store, and the web-search row dropped its source's qualifiers (a local stand-in provider, OpenAI parity by source reading, an inferred sweep mode); a cross-family review caught both | Carry the source's evidence class and qualifiers into each summary, and name a check as enforcement only after seeing it fail on the stated failure or a reproduction of it | This section's rule text; `UpstreamVerificationSectionTests` checks only the log's shape | +| 2026-09-27 | Testing a rewrite rule with input the rewriter never handles | A builder's stop report concluded that RTK 0.50.0 does not rewrite multi-line Bash blocks after `rtk hook check` returned no rewrite for a block of `echo` lines; an earlier carrier draft told subagents to use single-line calls, and this unit's review brief called the multi-line rule false. `echo` has no RTK rule, while `git status` then `git diff --stat` on two lines is rewritten per line through both `rtk hook check` and the `rtk hook claude` PreToolUse hook | Test a rewrite rule with commands the rewriter supports, through the hook processor the client runs; the measured rule and its sources ([`rewrite_multiline_block`](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L951-L1013), [rtk-ai/rtk#3319](https://github.com/rtk-ai/rtk/pull/3319)) are in the [handbook](token-session-handbook.md#token-lanes-carried-into-subagents) | `TokenLanesTextTests.test_block_fits_budget_and_matches_handbook` pins the measured wording; it checks text, not RTK behavior | +| 2026-09-27 | Guessing hook-field aliases | Falling back from missing or empty `agent_type` to `agentType` suppressed injection contrary to this task's contract; the subprocess contract test failed on that fallback | Read only the documented [SubagentStart `agent_type`](https://code.claude.com/docs/en/hooks#subagentstart); missing and non-string values are non-blind for this carrier | `TokenLanesHookTests.test_only_documented_agent_type_controls_blind_gate`, observed failing on the alias implementation | +| 2026-09-27 | Treating a build brief's source assertions as verified facts | The brief called both skills `on`, although [skillOverrides](../adoption/templates/claude.settings.template.json) has `search-first: name-only`, and called context-mode injection exclusively Agent-based, although [its pinned SessionStart hook](https://github.com/mksglu/context-mode/blob/589d8214d56740a28b5f7bf63167743d586b0b40/hooks/sessionstart.mjs#L166) also injects context | Keep the source's exact scope and distinguish the brief-reported 22/1,010 from the retained WP1 sample of 639 children; the [carrier decision](decisions/2026-09-27-token-lanes-subagent-start.md#evidence) records the source checks | This log; no mechanical check of those source assertions | diff --git a/docs/token-session-handbook.md b/docs/token-session-handbook.md index 9f37f6f13..1a10a7153 100644 --- a/docs/token-session-handbook.md +++ b/docs/token-session-handbook.md @@ -192,6 +192,53 @@ Checked on 2026-09-26 against Context Mode 1.0.169 at the reviewed revision `6f0 - **No settings, and no purge workaround.** The event cap, the eviction order and the retention period are constants in the code, and the snapshot has no size limit. `CONTEXT_MODE_DIR` is the one storage setting that both the hooks and the server honor; `CONTEXT_MODE_SESSION_SUFFIX` changes only the per-project session file names (the database, the events file and the cleanup flag, `hooks/session-helpers.mjs:78-92, 391-427`). Do not set `CONTEXT_MODE_DATA_DIR` (upstream #649; not in the README): the server honors it but the Claude Code hooks do not, so it splits the store. `ctx_purge` permanently deletes a session's or the project's records; upstream reserves it for an explicit user request and advises against purging to free capacity (`src/server.ts:4465`). - **`ctx_stats` figures.** `ctx_stats` renders upstream estimates for this server's session and for retained history. Quote any of its figures as an upstream-rendered figure: upstream defines the Without/kept-out bytes as measured diverted output (`src/session/analytics.ts:1025-1034, 2173-2180`), but on Claude Code they also include `read-redirected` rows that book a large file's full size for a Read that context-mode only advised against and did not block (`hooks/core/routing.mjs:848-862`; `hooks/posttooluse.mjs:94-141`; upstream #950, comment 5412624311). Such a figure is not verified avoidance and not provider usage, so derive no ratios from it; savings claims use exact artifact comparisons and native provider counters ([counter limits](token-practice.md#why-the-context-mode-lifetime-dollar-line-can-be-small)). +### Token lanes carried into subagents + +The following sentences are the source of truth for the portable +[`token-lanes-block.md`](../adoption/hooks/claude/token-lanes-block.md) carrier. +Tool contracts cite upstream; selection thresholds and accounting rules are local +policy, as recorded in the [carrier decision](decisions/2026-09-27-token-lanes-subagent-start.md). + +Load deferred token tools in ONE ToolSearch call before first use: "select:mcp__plugin_context-mode_context-mode__ctx_execute,mcp__plugin_context-mode_context-mode__ctx_batch_execute,mcp__plugin_context-mode_context-mode__ctx_search,mcp__serena__find_symbol,mcp__serena__find_referencing_symbols"; add jcodemunch/codebase-memory/socraticode/qmd/ai-memory/headroom names to that same call when this task needs them. + +Sources: [Claude MCP tool search](https://code.claude.com/docs/en/mcp#scale-with-mcp-tool-search) and the [context-mode 1.0.169 Agent bootstrap](https://github.com/mksglu/context-mode/blob/589d8214d56740a28b5f7bf63167743d586b0b40/hooks/core/routing.mjs#L892). Select names exposed by the active client. + +For output over ~5 KB, use ctx_batch_execute / ctx_execute / ctx_search; print derived answers, keep failures and original-output recovery. The context-mode server is bound to the main checkout: explicitly cd to the actual working directory. Open authorized scratch files from Python inside ctx_execute; ctx_execute_file enforces project boundaries (#852). Never bypass permissions. + +Sources: [context-mode 1.0.169 execution tools and file boundary](https://github.com/mksglu/context-mode/blob/v1.0.169/README.md#security), [#852](https://github.com/mksglu/context-mode/issues/852), and the [worker scope in this decision](decisions/2026-09-27-token-lanes-subagent-start.md). The ~5 KB trigger and main-checkout binding describe this carrier's worker policy, not a universal server default. Python opens are for paths the task already authorizes; a refused read requires checking the documented permissions, not switching tools to evade them. + +Automatic RTK: a Bash call containing $(...), backticks, <(...), a redirect to a file or a heredoc is never rewritten. && chains and multi-line blocks are rewritten segment by segment. In a pipeline RTK rewrites a supported producer feeding cat, head or tail (git diff | head -n 5) or a final grep/rg stage, but not gh (gh pr view 1 | head -n 5 is not rewritten). Run heavy reads as their own call. Never directly prefix rtk before `git show REV:path`, `diff`, `git branch`, `git log`, `jq`, `cd`, or `find` on a possibly-missing path. Use native commands or `rtk proxy ` for exact bytes/exit status; process large results in ctx_execute. + +Sources: RTK 0.50.0, tag commit `1d87b8e719ce0a50c223cd93ca64dd16921f9aec`. [`src/hooks/decision.rs` L86-88](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/hooks/decision.rs#L86-L88) leaves a call containing any of those constructs unrewritten; [`rewrite_multiline_block`, `src/discover/registry.rs` L951-1013](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L951-L1013), added by [rtk-ai/rtk#3319](https://github.com/rtk-ai/rtk/pull/3319) (commit [`fc8054eb`](https://github.com/rtk-ai/rtk/commit/fc8054eb0b357d32cb0457094d142de52c3db0e7)), rewrites each line; [L1087-1345](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L1087-L1345) rewrite pipeline stages and `&&`, `||` and `;` segments. Observed on 2026-09-27 (02:28Z to 02:46Z) with `rtk hook check`, and for the multi-line, `&&` and `gh` cases also through the `rtk hook claude` PreToolUse hook that the Claude template runs: `git status` and `git diff --stat` on two lines came back as `rtk git status` and `rtk git diff --stat`; `cd /tmp` then `git status` as `cd /tmp` and `rtk git status`; `git diff | head -n 5` as `rtk git diff | head -n 5`. A block with `$(...)` on one line, a heredoc and `gh pr view 1 | head -n 5` came back unchanged. A block of `echo` lines is never rewritten because `echo` has no RTK rule, so it cannot test multi-line handling. The named exclusions are local routing policy, with this catalog's [exactness exceptions and retained RTK checks](../recipes/README.md#native-context-mode-and-hooks). + +Use Serena find_symbol / find_referencing_symbols for exact symbols and references; jcodemunch route(task, repo?, execute?), menu(query?), order(action, args) on indexed repos; socraticode codebase_search(query, projectPath) with explicit projectPath for conceptual questions. Open original source before judging or editing. + +Sources: [Serena symbol tools](https://github.com/oraios/serena/blob/c6fbd1c5932df2494ffa0020af5a9fbe80b82143/src/serena/tools/symbol_tools.py), [jCodeMunch's three-verb front door and MCP instructions](https://github.com/jgravelle/jcodemunch-mcp/blob/8f7b34abe16fb459e0bf1c04747d584216dfe32e/src/jcodemunch_mcp/server.py), and [SocratiCode](https://github.com/giancarloerra/SocratiCode/tree/2218f25153d0f3f4a76ee240a5643dbc873e80be). `projectPath` is optional in the connected `codebase_search` schema; this policy makes it explicit. + +Use codebase-memory trace_path (include_evidence=true adds resolver class and confidence; this parameter is on trace_path, not search_graph) or search_graph for symbol/reference queries; treat edges below confidence 0.5 as unverified candidates, and verify exact caller lists with Serena find_referencing_symbols. + +Source: [codebase-memory 0.11.0 MCP tool schemas](https://github.com/DeusData/codebase-memory-mcp/blob/v0.11.0/src/mcp/mcp.c#L557), also checked against the connected `trace_path` and `search_graph` schemas. The 0.5 treatment is a local verification rule, not a discard filter or an upstream recall guarantee. + +For the indexed catalog, use qmd query (us-equities-foundation / us-equities-catalog), then get. Use ai-memory memory_query with workspace/project from .ai-memory.toml as historical evidence only, never authority. + +Sources: [QMD MCP query/get](https://github.com/tobi/qmd/tree/v2.8.3), [this catalog's scoped collections](../catalogs/us-equities/native-workflows.md), and [ai-memory's project-scoped retrieval](https://github.com/akitaonrails/ai-memory/tree/433a19f3d54dea287571b1423591db2a89965fa9). The historical-evidence boundary is this catalog's interpretation policy. + +Use TOON for uniform arrays of flat records (same keys in every item); keep compact JSON for nested or non-uniform data, where TOON can be larger (upstream README). + +Sources: [TOON 4.1.1 README](https://github.com/toon-format/toon/blob/v4.1.1/packages/toon/README.md#when-not-to-use-toon) and [tabular encoder](https://github.com/toon-format/toon/blob/v4.1.1/packages/toon/src/encode/tabular.ts). The encoder accepts a non-empty uniform array; there is no five-record minimum. + +For large selected text, use headroom_compress, then headroom_retrieve for recovery; require fidelity and count the recovery cost. + +Source: [Headroom 0.37.0 MCP tools](https://github.com/headroomlabs-ai/headroom/tree/v0.37.0). Recovery and fidelity accounting follow the [selected-artifact practice](token-practice.md). + +Use one lane per artifact; never stack compressors or claim token savings. At the end of your return, list the token tools used and why. + +This is this catalog's [one-artifact accounting policy](token-practice.md), also expressed in the reusable prompt above; tool-native estimates are not a measured provider saving. + +Before claiming a task done, follow the installed verification-before-completion skill: real command output before any success claim. Research upstream first with the installed search-first skill before writing custom code. Source: already installed: skillOverrides in adoption/templates/claude.settings.template.json. + +Already installed: [`skillOverrides` in `adoption/templates/claude.settings.template.json`](../adoption/templates/claude.settings.template.json) sets `verification-before-completion` to `on` and `search-first` to `name-only`. Both are adopted; this hook adds no skill dependency and changes neither setting. + ## Choose the new-PC profile and install once Clone [the canonical repository](https://github.com/seathatflowsinourveins/native-agent-stack), select a reviewed revision, and record `git rev-parse HEAD`. Read [adoption](../adoption/README.md), its [manifest](../adoption/manifest.json) and [update protocol](../adoption/update.md). Linux/WSL2 x86_64 is the accepted portability target; other hosts need matching assets and their own checks. diff --git a/tests/test_install_claude_profile.py b/tests/test_install_claude_profile.py index 89c17b686..6d58725f7 100644 --- a/tests/test_install_claude_profile.py +++ b/tests/test_install_claude_profile.py @@ -9,6 +9,7 @@ import io import json +import os import re import string import subprocess @@ -39,6 +40,34 @@ def template_server_names() -> list[str]: class GuardInstallTests(unittest.TestCase): + def test_token_lanes_assets_cli_dry_run_and_temp_home_install(self): + names = ("token-lanes-block.md", "token-lanes-subagent-start.py") + with tempfile.TemporaryDirectory() as tmp: + home = Path(tmp) + command = [sys.executable, str(ROOT / "tools/adoption/install_claude_profile.py"), + "--only", "guard"] + env = {**os.environ, "HOME": tmp} + planned = subprocess.run(command + ["--dry-run"], env=env, cwd=ROOT, + capture_output=True, text=True, timeout=30) + self.assertEqual(planned.returncode, 0, planned.stderr) + self.assertFalse((home / ".claude").exists()) + for name in names: + self.assertIn(f"would install {home / '.claude/hooks' / name}", planned.stdout) + installed = subprocess.run(command, env=env, cwd=ROOT, + capture_output=True, text=True, timeout=30) + self.assertEqual(installed.returncode, 0, installed.stderr) + for name in names: + dest = home / ".claude/hooks" / name + self.assertIn(f"installed {dest}", installed.stdout) + self.assertEqual(dest.read_bytes(), (ROOT / "adoption/hooks/claude" / name).read_bytes()) + # The installed script resolves the installed block, even from a different cwd. + injected = subprocess.run([sys.executable, str(home / ".claude/hooks" / names[1])], + input='{"agent_type":"general-purpose"}', env=env, cwd=home, + capture_output=True, text=True, timeout=30) + self.assertEqual(injected.returncode, 0, injected.stderr) + self.assertEqual(json.loads(injected.stdout)["hookSpecificOutput"]["additionalContext"], + (home / ".claude/hooks" / names[0]).read_text(encoding="utf-8")) + def test_installs_when_absent(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) @@ -68,15 +97,44 @@ class SecretGuardProfileTests(unittest.TestCase): TEMPLATE = ROOT / "adoption" / "templates" / "claude.settings.template.json" - def test_install_guards_installs_both_hooks_pinned_by_sha256sums(self): + def test_install_guards_installs_all_hooks_pinned_by_sha256sums(self): with tempfile.TemporaryDirectory() as tmp: home = Path(tmp) results = icp.install_guards(home, dry_run=False) - self.assertEqual(results, {"effort-default-guard.py": "installed", "secret_path_guard.py": "installed"}) - dest = home / ".claude" / "hooks" / "secret_path_guard.py" - self.assertEqual(dest.read_bytes(), icp.SECRET_GUARD_SRC.read_bytes()) + self.assertEqual(results, {name: "installed" for name in icp.HOOKS}) + for name, source in icp.HOOKS.items(): + self.assertEqual((home / ".claude" / "hooks" / name).read_bytes(), source.read_bytes()) self.assertEqual(icp.install_guards(home, dry_run=False), - {"effort-default-guard.py": "skipped", "secret_path_guard.py": "skipped"}) + {name: "skipped" for name in icp.HOOKS}) + + def test_token_lanes_template_merges_once_alongside_existing_ai_memory(self): + import apply_claude_settings as acs + with tempfile.TemporaryDirectory() as tmp: + template = json.loads(string.Template(self.TEMPLATE.read_text()).safe_substitute(HOME=tmp)) + incoming = template["hooks"]["SubagentStart"] + memory = next(hook for group in incoming for hook in group["hooks"] + if "--event subagent-start" in hook["command"]) + # The current live shape already carries ai-memory under the empty matcher. + base = {"hooks": {"SubagentStart": [{"matcher": "", "hooks": [memory]}]}} + wanted = f'python3 "{tmp}/.claude/hooks/token-lanes-subagent-start.py" 2>/dev/null || true' + own_groups = [group for group in incoming + if any(acs.command_key(hook["command"]) == acs.command_key(wanted) + for hook in group["hooks"])] + self.assertEqual(len(own_groups), 1) + self.assertEqual(own_groups[0], {"matcher": "", "hooks": [ + {"type": "command", "command": wanted, "timeout": 5}]}) + for initial in ({}, base): + merged = initial + for application in (1, 2): + previous = merged + merged = acs.merge_settings(merged, template) + keys = [acs.command_key(hook["command"]) + for group in merged["hooks"]["SubagentStart"] for hook in group["hooks"]] + with self.subTest(existing=bool(initial), application=application): + self.assertEqual(keys.count(acs.command_key(wanted)), 1) + self.assertEqual(keys.count(acs.command_key(memory["command"])), 1) + if application == 2: + self.assertEqual(merged, previous) def test_sha256sums_verifies_like_sha256sum_c(self): entries = icp.sha256sums_entries() diff --git a/tests/test_token_lanes_subagent_start.py b/tests/test_token_lanes_subagent_start.py new file mode 100644 index 000000000..84856ddf3 --- /dev/null +++ b/tests/test_token_lanes_subagent_start.py @@ -0,0 +1,139 @@ +"""Subprocess contract for the token-lanes carrier and its portable text. + +Hook JSON reference: https://code.claude.com/docs/en/hooks#subagentstart +Blind-lane boundary: adoption/agents/claude/blind-judge.md. +""" + +import json +import os +import shutil +import subprocess +import sys +import tempfile +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +HOOK = ROOT / "adoption/hooks/claude/token-lanes-subagent-start.py" +BLOCK = HOOK.with_name("token-lanes-block.md") +HANDBOOK = ROOT / "docs/token-session-handbook.md" +KEY_PHRASES = ( + "ToolSearch", "ctx_batch_execute", "rtk", "find_referencing_symbols", + "codebase-memory", "jcodemunch", "TOON", "headroom", + "verification-before-completion", "search-first", +) +CORRECTED_PHRASES = ( + # RTK 0.50.0, measured with `rtk hook check` and the `rtk hook claude` hook path. + "&& chains and multi-line blocks are rewritten segment by segment", + "a redirect to a file or a heredoc is never rewritten", + "but not gh (gh pr view 1 | head -n 5 is not rewritten)", + "uniform arrays of flat records (same keys in every item)", + "this parameter is on trace_path, not search_graph", + "treat edges below confidence 0.5 as unverified candidates", +) + + +class TokenLanesHookTests(unittest.TestCase): + def run_hook(self, stdin, script=HOOK, home=None): + env = os.environ.copy() + if home is not None: + env["HOME"] = str(home) + result = subprocess.run([sys.executable, str(script)], input=stdin, + capture_output=True, text=True, timeout=10, + cwd=ROOT, env=env) + self.assertEqual(result.returncode, 0, result.stderr) + self.assertEqual(result.stderr, "") + return result.stdout + + def test_blind_roles_receive_zero_injection(self): + for role in ("blind-lane-reviewer", "blind-judge", "blind-adjudicator", "blind-anything"): + with self.subTest(agent_type=role): + self.assertEqual(self.run_hook(json.dumps({"agent_type": role})), "") + + def test_only_documented_agent_type_controls_blind_gate(self): + for payload in ({"agentType": "blind-judge"}, + {"agent_type": "", "agentType": "blind-judge"}, + {"agent_type": None, "agentType": "blind-judge"}, + {"agent_type": "general-purpose", "agentType": "blind-judge"}): + self.assertIn("TOKEN LANES", json.loads(self.run_hook(json.dumps(payload)))[ + "hookSpecificOutput"]["additionalContext"]) + + def test_non_blind_roles_receive_one_exact_context_object(self): + payloads = [{"agent_type": role} for role in + ("general-purpose", "source-scout", "workflow-subagent", "future-role")] + payloads += [{}] + [{"agent_type": value} for value in + ("", None, 7, False, [], ["blind-judge"], {"name": "blind-judge"})] + for payload in payloads: + with self.subTest(payload=payload): + stdout = self.run_hook(json.dumps(payload)) + self.assertEqual(len(stdout.splitlines()), 1) + self.assertLess(len(stdout), 10_000) + output = json.loads(stdout) + self.assertEqual(output, {"hookSpecificOutput": { + "hookEventName": "SubagentStart", + "additionalContext": BLOCK.read_text(encoding="utf-8"), + }}) + context = output["hookSpecificOutput"]["additionalContext"] + self.assertTrue(context.startswith("TOKEN LANES")) + for phrase in KEY_PHRASES: + self.assertIn(phrase, context) + + def test_malformed_stdin_is_silent(self): + for stdin in ("not JSON", "", "{", "[]", "null", '"string"', "42"): + with self.subTest(stdin=stdin): + self.assertEqual(self.run_hook(stdin), "") + + def test_missing_sibling_block_is_silent(self): + with tempfile.TemporaryDirectory() as tmp: + script = Path(tmp) / HOOK.name + shutil.copy2(HOOK, script) + self.assertEqual(self.run_hook('{"agent_type":"general-purpose"}', script, Path(tmp)), "") + + def test_script_uses_its_own_sibling_verbatim(self): + with tempfile.TemporaryDirectory() as tmp: + script = Path(tmp) / HOOK.name + shutil.copy2(HOOK, script) + expected = "TOKEN LANES\nSibling fixture, including its final newline.\n" + script.with_name(BLOCK.name).write_text(expected, encoding="utf-8") + result = json.loads(self.run_hook("{}", script, Path(tmp))) + self.assertEqual(result["hookSpecificOutput"]["additionalContext"], expected) + + def test_empty_or_unreadable_sibling_block_is_silent(self): + for content in (b"", b"\xff"): + with self.subTest(content=content), tempfile.TemporaryDirectory() as tmp: + script = Path(tmp) / HOOK.name + shutil.copy2(HOOK, script) + script.with_name(BLOCK.name).write_bytes(content) + self.assertEqual(self.run_hook("{}", script, Path(tmp)), "") + with tempfile.TemporaryDirectory() as tmp: + script = Path(tmp) / HOOK.name + shutil.copy2(HOOK, script) + script.with_name(BLOCK.name).mkdir() + self.assertEqual(self.run_hook("{}", script, Path(tmp)), "") + + +class TokenLanesTextTests(unittest.TestCase): + def test_block_fits_budget_and_matches_handbook(self): + block = BLOCK.read_text(encoding="utf-8") + handbook = HANDBOOK.read_text(encoding="utf-8") + self.assertTrue(block.startswith("TOKEN LANES")) + self.assertLessEqual(len(block), 3_500) + for marker in ("/" + "home/", "/" + "tmp/claude-1000", "/" + "Users/", "/" + "mnt/c/"): + self.assertNotIn(marker, block) + for phrase in KEY_PHRASES: + with self.subTest(phrase=phrase): + self.assertIn(phrase, block) + self.assertIn(phrase, handbook) + for phrase in CORRECTED_PHRASES: + with self.subTest(correction=phrase): + self.assertIn(phrase, block) + self.assertIn(phrase, handbook) + # Every guidance line is copied verbatim from the source of truth. + for line in block.splitlines(): + if line.startswith("- "): + with self.subTest(line=line): + self.assertIn(line[2:], handbook) + + +if __name__ == "__main__": + unittest.main() diff --git a/tools/adoption/install_claude_profile.py b/tools/adoption/install_claude_profile.py index b7c89c217..de772e66f 100644 --- a/tools/adoption/install_claude_profile.py +++ b/tools/adoption/install_claude_profile.py @@ -4,7 +4,9 @@ guard -- sha256-checked copies of the user-scope hooks to ~/.claude/hooks/: adoption/hooks/claude/effort-default-guard.py (effort self-heal) and scripts/hooks/secret_path_guard.py (PreToolUse Bash secret guard; - the same file the project .claude/settings.json runs) + the same file the project .claude/settings.json runs), plus + token-lanes-subagent-start.py and its sibling token-lanes-block.md + from adoption/hooks/claude/ (non-blind child context) agents -- verbatim copies of adoption/agents/claude/*.md to ~/.claude/agents/ mcp -- `claude mcp add --scope user` for each server named in adoption/mcp/claude-user.json after rendering its ${HOME} and @@ -39,11 +41,15 @@ ROOT = Path(__file__).resolve().parents[2] GUARD_SRC = ROOT / "adoption" / "hooks" / "claude" / "effort-default-guard.py" SECRET_GUARD_SRC = ROOT / "scripts" / "hooks" / "secret_path_guard.py" +TOKEN_LANES_BLOCK_SRC = ROOT / "adoption" / "hooks" / "claude" / "token-lanes-block.md" +TOKEN_LANES_HOOK_SRC = ROOT / "adoption" / "hooks" / "claude" / "token-lanes-subagent-start.py" SHA256SUMS = ROOT / "adoption" / "hooks" / "claude" / "SHA256SUMS" -# Installed name under ~/.claude/hooks/ -> checked-in source. The settings template runs both. +# Installed name under ~/.claude/hooks/ -> checked-in source; includes the carrier's sibling block. HOOKS = { "effort-default-guard.py": GUARD_SRC, "secret_path_guard.py": SECRET_GUARD_SRC, + "token-lanes-block.md": TOKEN_LANES_BLOCK_SRC, + "token-lanes-subagent-start.py": TOKEN_LANES_HOOK_SRC, } AGENTS_SRC_DIR = ROOT / "adoption" / "agents" / "claude" MCP_TEMPLATE = ROOT / "adoption" / "mcp" / "claude-user.json" From 9a445b78e3b6fdf5ad2f3b07f59fb115594fec1b Mon Sep 17 00:00:00 2001 From: Scout Date: Sat, 26 Sep 2026 23:31:44 -0400 Subject: [PATCH 2/4] Native probe and RTK hook-check receipt for the token-lanes carrier evidence/artifacts/token-lanes-subagent-start-20260927/ retains, sanitized, the five native probe runs (Claude Code 2.1.283, Agent-tool Haiku children: without the hook, with it, and a blind-judge child with it, plus the failed v1 question kept as a failed design) and 17 RTK 0.50.0 inputs through both `rtk hook check` and the `rtk hook claude` PreToolUse processor. Raw CLI output and child transcripts stay private and are identified by SHA-256 and counts. Evidence class local_integration; not native_proven for Workflow children, which have not run with the hook. The handbook's RTK sources paragraph and the decision record now cite the receipt instead of unretained observations. Co-Authored-By: Claude Opus 5.5 (1M context) --- .../2026-09-27-token-lanes-subagent-start.md | 2 + docs/token-session-handbook.md | 2 +- .../README.md | 99 +++ .../receipt.json | 678 ++++++++++++++++++ 4 files changed, 780 insertions(+), 1 deletion(-) create mode 100644 evidence/artifacts/token-lanes-subagent-start-20260927/README.md create mode 100644 evidence/artifacts/token-lanes-subagent-start-20260927/receipt.json diff --git a/docs/decisions/2026-09-27-token-lanes-subagent-start.md b/docs/decisions/2026-09-27-token-lanes-subagent-start.md index 28b8923ec..6a741cd33 100644 --- a/docs/decisions/2026-09-27-token-lanes-subagent-start.md +++ b/docs/decisions/2026-09-27-token-lanes-subagent-start.md @@ -182,6 +182,8 @@ test suite or a new native model run. positive `general-purpose` child, none in the other two. This is one native operation per arm on Agent-tool children. It does not cover Workflow children, which WP1 shows also reach SubagentStart, nor lane use, compliance or savings. + The [carrier receipt](../../evidence/artifacts/token-lanes-subagent-start-20260927/README.md) + retains the prompts, replies, transcript counts, usage and the RTK hook checks. - The blind gate matches `agent_type` values starting `blind-`, which is how [`install_claude_profile.py`](../../tools/adoption/install_claude_profile.py) installs the blind agents (user agents). A plugin-shipped blind agent would diff --git a/docs/token-session-handbook.md b/docs/token-session-handbook.md index 1a10a7153..396f98b08 100644 --- a/docs/token-session-handbook.md +++ b/docs/token-session-handbook.md @@ -209,7 +209,7 @@ Sources: [context-mode 1.0.169 execution tools and file boundary](https://github Automatic RTK: a Bash call containing $(...), backticks, <(...), a redirect to a file or a heredoc is never rewritten. && chains and multi-line blocks are rewritten segment by segment. In a pipeline RTK rewrites a supported producer feeding cat, head or tail (git diff | head -n 5) or a final grep/rg stage, but not gh (gh pr view 1 | head -n 5 is not rewritten). Run heavy reads as their own call. Never directly prefix rtk before `git show REV:path`, `diff`, `git branch`, `git log`, `jq`, `cd`, or `find` on a possibly-missing path. Use native commands or `rtk proxy ` for exact bytes/exit status; process large results in ctx_execute. -Sources: RTK 0.50.0, tag commit `1d87b8e719ce0a50c223cd93ca64dd16921f9aec`. [`src/hooks/decision.rs` L86-88](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/hooks/decision.rs#L86-L88) leaves a call containing any of those constructs unrewritten; [`rewrite_multiline_block`, `src/discover/registry.rs` L951-1013](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L951-L1013), added by [rtk-ai/rtk#3319](https://github.com/rtk-ai/rtk/pull/3319) (commit [`fc8054eb`](https://github.com/rtk-ai/rtk/commit/fc8054eb0b357d32cb0457094d142de52c3db0e7)), rewrites each line; [L1087-1345](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L1087-L1345) rewrite pipeline stages and `&&`, `||` and `;` segments. Observed on 2026-09-27 (02:28Z to 02:46Z) with `rtk hook check`, and for the multi-line, `&&` and `gh` cases also through the `rtk hook claude` PreToolUse hook that the Claude template runs: `git status` and `git diff --stat` on two lines came back as `rtk git status` and `rtk git diff --stat`; `cd /tmp` then `git status` as `cd /tmp` and `rtk git status`; `git diff | head -n 5` as `rtk git diff | head -n 5`. A block with `$(...)` on one line, a heredoc and `gh pr view 1 | head -n 5` came back unchanged. A block of `echo` lines is never rewritten because `echo` has no RTK rule, so it cannot test multi-line handling. The named exclusions are local routing policy, with this catalog's [exactness exceptions and retained RTK checks](../recipes/README.md#native-context-mode-and-hooks). +Sources: RTK 0.50.0, tag commit `1d87b8e719ce0a50c223cd93ca64dd16921f9aec`. [`src/hooks/decision.rs` L86-88](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/hooks/decision.rs#L86-L88) leaves a call containing any of those constructs unrewritten; [`rewrite_multiline_block`, `src/discover/registry.rs` L951-1013](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L951-L1013), added by [rtk-ai/rtk#3319](https://github.com/rtk-ai/rtk/pull/3319) (commit [`fc8054eb`](https://github.com/rtk-ai/rtk/commit/fc8054eb0b357d32cb0457094d142de52c3db0e7)), rewrites each line; [L1087-1345](https://github.com/rtk-ai/rtk/blob/1d87b8e719ce0a50c223cd93ca64dd16921f9aec/src/discover/registry.rs#L1087-L1345) rewrite pipeline stages and `&&`, `||` and `;` segments. The [carrier receipt](../evidence/artifacts/token-lanes-subagent-start-20260927/README.md) retains 17 inputs run on 2026-09-27 through both `rtk hook check` and the `rtk hook claude` PreToolUse hook that the Claude template runs, with identical results: `git status` and `git diff --stat` on two lines came back as `rtk git status` and `rtk git diff --stat`; `cd /tmp` then `git status` as `cd /tmp` and `rtk git status`; `git diff | head -n 5` as `rtk git diff | head -n 5`. A block with `$(...)` on one line, a heredoc and `gh pr view 1 | head -n 5` came back unchanged. A block of `echo` lines is never rewritten because `echo` has no RTK rule, so it cannot test multi-line handling. The named exclusions are local routing policy, with this catalog's [exactness exceptions and retained RTK checks](../recipes/README.md#native-context-mode-and-hooks). Use Serena find_symbol / find_referencing_symbols for exact symbols and references; jcodemunch route(task, repo?, execute?), menu(query?), order(action, args) on indexed repos; socraticode codebase_search(query, projectPath) with explicit projectPath for conceptual questions. Open original source before judging or editing. diff --git a/evidence/artifacts/token-lanes-subagent-start-20260927/README.md b/evidence/artifacts/token-lanes-subagent-start-20260927/README.md new file mode 100644 index 000000000..3f5979fb3 --- /dev/null +++ b/evidence/artifacts/token-lanes-subagent-start-20260927/README.md @@ -0,0 +1,99 @@ +# SubagentStart token-lanes carrier: native probe and RTK hook checks (2026-09-27) + +- Host: `nativestack-5975wx-20260925` (the WSL2 workstation). Claude Code 2.1.283, RTK 0.50.0. +- Probe runs: 02:37Z to 02:53Z UTC. Retained RTK capture: 03:28:14Z to 03:28:15Z. +- Change under test: the [carrier decision](../../../docs/decisions/2026-09-27-token-lanes-subagent-start.md). + The probe ran the hook and block files before they were committed; their SHA-256 values in + [`receipt.json`](receipt.json) `files_under_test` identify the exact bytes, which the change commits unchanged. +- Machine-readable record: [`receipt.json`](receipt.json). + +**Evidence class: `local_integration`.** Each run is a native Claude Code operation on this host through this +repository's integration, for subagents launched with the Agent tool. It is not `native_proven` for Workflow +children, because no Workflow subagent ran. It measures no lane use, compliance or token savings. + +## Question + +Does the `SubagentStart` hook put the token-lanes block into a non-blind subagent's context and keep it out of a +blind one's? How does RTK 0.50.0's Claude hook rewrite each Bash shape the block describes? + +## Method + +Each probe run was one `claude -p --output-format json` session (stdin `/dev/null`, timeout 300 s) in this +repository's main checkout. Its main session (Opus 5.5) launched one Haiku 4.5 subagent through the Agent tool and +relayed the reply verbatim. Positive runs added a settings file whose `SubagentStart` group (matcher `""`, +timeout 5) runs `python3 "/adoption/hooks/claude/token-lanes-subagent-start.py"`. Negative runs used only +the host's live settings, whose `SubagentStart` group runs ai-memory and not this hook. Each child's transcript was +then read directly for the block's first-line marker `TOKEN LANES (source:` and for its `SubagentStart` entries. + +The v2 question given to the child, verbatim: + +> Without calling any tool, look through everything already in your context for this turn other than this task +> message itself, including any system reminders or hook-provided context. Is there any text there that contains +> the phrase TOKEN LANES? Answer FOUND or NOT FOUND on the first line. If FOUND, on the second line quote exactly +> the 60 characters that begin with TOKEN LANES, character for character, in quotes. + +The blind control used the same prompt with `subagent_type "blind-judge"`. `receipt.json` has both prompts in full. + +## Results + +| Run (start, UTC) | Hook | Child | Child's reply | Child transcript | +| --- | --- | --- | --- | --- | +| v2 negative (02:50:39Z) | none | `general-purpose` | `NOT FOUND` | no marker; only ai-memory's SubagentStart hook ran | +| v2 positive (02:50:57Z) | token-lanes | `general-purpose` | `FOUND` / `"TOKEN LANES (source: docs/token-session-handbook.md, "Token "` | one `hook_additional_context` attachment from SubagentStart; the token-lanes hook exited 0 with 3,258 bytes of output | +| v2 blind control (02:52:25Z) | token-lanes | `blind-judge` | `NOT FOUND` | no marker; no entry for the token-lanes hook | +| v1 negative (02:37:40Z) | none | `general-purpose` | `NO` | no marker | +| v1 positive (02:49:08Z) | token-lanes | `general-purpose` | `NO` | block attached, as in v2 positive | + +The v2-positive quote equals the committed block's first 60 characters exactly. Recomputing the hook's output +from the committed files gives the same 3,258 bytes the v2-positive transcript recorded. All five runs exited 0. + +Provider usage for all five runs, as the CLI's `modelUsage` reported it: Opus 5.5 input 20, output 2,344, cache +read 263,466, cache creation 153,558; Haiku 4.5 input 50, output 4,290, cache read 14,666, cache creation 70,203; +`costUSD` $1.44 in total, including the failed v1 design. + +### RTK 0.50.0 hook checks + +Every input ran through `rtk hook check -- ` and through `rtk hook claude` with a Claude PreToolUse Bash +payload on stdin. That is the processor the Claude settings template runs. Neither executes the input. Both entry +points agreed on every case: + +| Input | Result | +| --- | --- | +| `git status && git log -n 3` | `rtk git status && rtk git log -n 3` | +| `git status` / `git diff --stat` (two lines) | `rtk git status` / `rtk git diff --stat` | +| `git status` / `echo done` | `rtk git status` / `echo done` | +| `cd /tmp` / `git status` | `cd /tmp` / `rtk git status` | +| `git diff \| head -n 5` | `rtk git diff \| head -n 5` | +| `git log -n 20 \| grep fix` | `git log -n 20 \| rtk grep fix` | +| `gh pr view 1 \| head -n 5`, `echo $(git status)`, backticks, `<(...)`, `> /tmp/x.txt`, a heredoc, a two-line block with `$(...)` on one line, `echo a` / `echo b` | not rewritten (`rtk hook check` exit 1, `No rewrite for: ...`; no `updatedInput`) | + +`receipt.json` lists all 17 inputs with both outputs verbatim. A block of `echo` lines is never rewritten because +`echo` has no RTK rule, so it cannot test multi-line handling. The same cases were first observed between 02:28Z +and 02:46Z; the 03:28Z capture is the retained one. + +## Retained failure + +Probe v1 asked whether a block whose *first line starts with* `TOKEN LANES` was present. Both arms answered `NO`, +although the v1-positive child transcript held the block, so v1 did not discriminate. The likely cause is that +Claude Code frames attached context (the child's prompt snapshot records `contextRendering: announced`). The +child's reasoning was redacted, so that cause is not observed. v2 replaced the question. + +## Limits + +- One native operation per arm for Agent-tool subagents, on one host and client version. No Workflow child has + run with the hook; the [WP1 baseline](../child-lane-baseline-20260926/README.md) recorded SubagentStart for 465 + `workflow-subagent` children. +- The blind control shows that no block reached the `blind-judge` child. It does not show that the hook ran and + skipped: a hook with empty output leaves no entry in the transcript. The skip logic is covered by + `tests/test_token_lanes_subagent_start.py`. +- A plugin-scoped blind agent type such as `plugin:blind-judge` does not start with `blind-` and would receive the + block. +- The RTK results hold for RTK 0.50.0 with this host's user configuration. + +## Sanitization + +Raw CLI output and child transcripts stay private on the host, because they carry session, agent and tool-use +identifiers and local paths. `receipt.json` `raw_files_private` identifies each one by SHA-256, byte count and +event or line count. Absolute scratch and worktree paths are written as `` and ``. Hook +commands inside transcripts are reduced to the hook's name. No transcript text is published beyond each child's +reply. diff --git a/evidence/artifacts/token-lanes-subagent-start-20260927/receipt.json b/evidence/artifacts/token-lanes-subagent-start-20260927/receipt.json new file mode 100644 index 000000000..164a0bbb3 --- /dev/null +++ b/evidence/artifacts/token-lanes-subagent-start-20260927/receipt.json @@ -0,0 +1,678 @@ +{ + "schema_version": 1, + "kind": "token_lanes_subagent_start_probe", + "host_id": "nativestack-5975wx-20260925", + "evidence_class": "local_integration", + "evidence_class_note": "A native Claude Code operation on this host through this repository's integration: the SubagentStart hook, run by Claude Code 2.1.283, for Agent-tool subagents. It is not native_proven for Workflow children, because no Workflow subagent ran, and it measures no lane use, compliance or savings.", + "question": "Does the SubagentStart token-lanes hook put the block into a non-blind subagent's context, and keep it out of a blind one's; and how does RTK 0.50.0's Claude hook rewrite each Bash shape the block describes?", + "catalog_base_revision": "34c56340e636ad3d3daeb8482d48d1d8f0b135ae", + "files_under_test": { + "note": "The probe ran the carrier's working files before they were committed; these digests identify the exact bytes, which this change commits unchanged. hook_stdout_bytes is the hook's output for a non-blind subagent recomputed from the committed files; the v2-positive child transcript recorded the same byte count.", + "files": [ + { + "path": "adoption/hooks/claude/token-lanes-subagent-start.py", + "sha256": "7a9228e41235771336189287944d81d998795b025a9a038d022beb21a19ce870", + "bytes": 1162 + }, + { + "path": "adoption/hooks/claude/token-lanes-block.md", + "sha256": "8b9a60fa425900930c610ba7e3e0f8a0fa1457048a39c79e3136fb5073e7269f", + "bytes": 3159 + } + ], + "hook_stdout_bytes": 3258 + }, + "probe": { + "method": "One `claude -p` session per run in this repository's main checkout. The main session launched one Haiku subagent through the Agent tool and relayed its reply verbatim. Positive runs added the settings file below; negative runs used only the host's live settings, whose SubagentStart group runs ai-memory and not this hook. The child transcript was then inspected directly for the block's first-line marker and the SubagentStart hook entries.", + "argv": [ + "timeout", + "300", + "claude", + "-p", + "--output-format", + "json", + "[--settings /probe-settings.json]", + "" + ], + "stdin": "/dev/null", + "cwd": "
", + "settings_file": { + "hooks": { + "SubagentStart": [ + { + "matcher": "", + "hooks": [ + { + "type": "command", + "command": "python3 \"/adoption/hooks/claude/token-lanes-subagent-start.py\"", + "timeout": 5 + } + ] + } + ] + } + }, + "settings_note": "The template runs the same script with `2>/dev/null || true` appended; the probe settings do not.", + "prompts": { + "v1": "Use the Agent tool exactly once to launch one subagent with subagent_type \"general-purpose\" and model \"haiku\". Do not use any other tool yourself, and do not answer from your own context. Give that subagent exactly this task, verbatim, and tell it to use no tools:\n\n\"Without calling any tool, look only at the context and instructions already present in your own prompt for this turn. Does that context contain a block of text whose first line starts with the exact words TOKEN LANES? Answer with a single line: YES or NO. If YES, add a second line quoting the first 60 characters of that block exactly, character for character, in quotes.\"\n\nAfter the subagent replies, return its two lines back to me verbatim and nothing else.", + "v2": "Use the Agent tool exactly once to launch one subagent with subagent_type \"general-purpose\" and model \"haiku\". Do not use any other tool yourself, and do not answer from your own context. Give that subagent exactly this task, verbatim, and tell it to use no tools:\n\n\"Without calling any tool, look through everything already in your context for this turn other than this task message itself, including any system reminders or hook-provided context. Is there any text there that contains the phrase TOKEN LANES? Answer FOUND or NOT FOUND on the first line. If FOUND, on the second line quote exactly the 60 characters that begin with TOKEN LANES, character for character, in quotes.\"\n\nAfter the subagent replies, return its reply back to me verbatim and nothing else." + }, + "prompt_v2_blind": "prompt v2 with subagent_type \"general-purpose\" replaced by \"blind-judge\"", + "transcript_marker": "TOKEN LANES (source:", + "runs": [ + { + "id": "v1-negative", + "design": "v1", + "arm": "negative", + "hook_configured": false, + "child_subagent_type": "general-purpose", + "start": "2026-09-27T02:37:40Z", + "end": "2026-09-27T02:38:02Z", + "exit": 0, + "event_timestamps_within_window": true, + "claude_code_version": "2.1.283", + "main_model": "claude-opus-5-5[1m]", + "num_turns": 2, + "is_error": false, + "duration_ms": 18821, + "returned_result": "NO", + "child_transcript": { + "entries_with_block_marker": 0, + "marker_entry_kinds": [], + "subagentstart_additional_context_attachments": 0, + "subagentstart_hook_success_entries": [ + { + "hook": "ai-memory", + "exit": 0, + "stdout_bytes": 3 + } + ], + "context_rendering": "announced" + }, + "usage": { + "claude-opus-5-5[1m]": { + "inputTokens": 4, + "outputTokens": 469, + "cacheReadInputTokens": 51726, + "cacheCreationInputTokens": 32413, + "costUSD": 0.279045 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 10, + "outputTokens": 974, + "cacheReadInputTokens": 0, + "cacheCreationInputTokens": 19633, + "costUSD": 0.029421 + } + } + }, + { + "id": "v1-positive", + "design": "v1", + "arm": "positive", + "hook_configured": true, + "child_subagent_type": "general-purpose", + "start": "2026-09-27T02:49:08Z", + "end": "2026-09-27T02:49:28Z", + "exit": 0, + "event_timestamps_within_window": true, + "claude_code_version": "2.1.283", + "main_model": "claude-opus-5-5[1m]", + "num_turns": 2, + "is_error": false, + "duration_ms": 17632, + "returned_result": "NO", + "child_transcript": { + "entries_with_block_marker": 2, + "marker_entry_kinds": [ + "attachment:hook_additional_context", + "attachment:hook_success" + ], + "subagentstart_additional_context_attachments": 1, + "subagentstart_hook_success_entries": [ + { + "hook": "ai-memory", + "exit": 0, + "stdout_bytes": 3 + }, + { + "hook": "token-lanes", + "exit": 0, + "stdout_bytes": 3258 + } + ], + "context_rendering": "announced" + }, + "usage": { + "claude-opus-5-5[1m]": { + "inputTokens": 4, + "outputTokens": 423, + "cacheReadInputTokens": 52922, + "cacheCreationInputTokens": 30229, + "costUSD": 0.260892 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 10, + "outputTokens": 1046, + "cacheReadInputTokens": 0, + "cacheCreationInputTokens": 20452, + "costUSD": 0.030805 + } + } + }, + { + "id": "v2-negative", + "design": "v2", + "arm": "negative", + "hook_configured": false, + "child_subagent_type": "general-purpose", + "start": "2026-09-27T02:50:39Z", + "end": "2026-09-27T02:50:57Z", + "exit": 0, + "event_timestamps_within_window": true, + "claude_code_version": "2.1.283", + "main_model": "claude-opus-5-5[1m]", + "num_turns": 2, + "is_error": false, + "duration_ms": 15008, + "returned_result": "NOT FOUND", + "child_transcript": { + "entries_with_block_marker": 0, + "marker_entry_kinds": [], + "subagentstart_additional_context_attachments": 0, + "subagentstart_hook_success_entries": [ + { + "hook": "ai-memory", + "exit": 0, + "stdout_bytes": 3 + } + ], + "context_rendering": "announced" + }, + "usage": { + "claude-opus-5-5[1m]": { + "inputTokens": 4, + "outputTokens": 455, + "cacheReadInputTokens": 52940, + "cacheCreationInputTokens": 30278, + "costUSD": 0.261928 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 10, + "outputTokens": 610, + "cacheReadInputTokens": 7333, + "cacheCreationInputTokens": 12302, + "costUSD": 0.019171 + } + } + }, + { + "id": "v2-positive", + "design": "v2", + "arm": "positive", + "hook_configured": true, + "child_subagent_type": "general-purpose", + "start": "2026-09-27T02:50:57Z", + "end": "2026-09-27T02:51:16Z", + "exit": 0, + "event_timestamps_within_window": true, + "claude_code_version": "2.1.283", + "main_model": "claude-opus-5-5[1m]", + "num_turns": 2, + "is_error": false, + "duration_ms": 16744, + "returned_result": "FOUND\n\"TOKEN LANES (source: docs/token-session-handbook.md, \"Token \"", + "child_transcript": { + "entries_with_block_marker": 3, + "marker_entry_kinds": [ + "assistant message", + "attachment:hook_additional_context", + "attachment:hook_success" + ], + "subagentstart_additional_context_attachments": 1, + "subagentstart_hook_success_entries": [ + { + "hook": "ai-memory", + "exit": 0, + "stdout_bytes": 3 + }, + { + "hook": "token-lanes", + "exit": 0, + "stdout_bytes": 3258 + } + ], + "context_rendering": "announced" + }, + "usage": { + "claude-opus-5-5[1m]": { + "inputTokens": 4, + "outputTokens": 488, + "cacheReadInputTokens": 52938, + "cacheCreationInputTokens": 30306, + "costUSD": 0.262812 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 10, + "outputTokens": 1091, + "cacheReadInputTokens": 7333, + "cacheCreationInputTokens": 13127, + "costUSD": 0.022607 + } + }, + "quote_matches_block_first_60_chars": true + }, + { + "id": "v2-blind-control", + "design": "v2-blind", + "arm": "positive", + "hook_configured": true, + "child_subagent_type": "blind-judge", + "start": "2026-09-27T02:52:25Z", + "end": "2026-09-27T02:52:43Z", + "exit": 0, + "event_timestamps_within_window": true, + "claude_code_version": "2.1.283", + "main_model": "claude-opus-5-5[1m]", + "num_turns": 2, + "is_error": false, + "duration_ms": 15424, + "returned_result": "NOT FOUND", + "child_transcript": { + "entries_with_block_marker": 0, + "marker_entry_kinds": [], + "subagentstart_additional_context_attachments": 0, + "subagentstart_hook_success_entries": [ + { + "hook": "ai-memory", + "exit": 0, + "stdout_bytes": 3 + } + ], + "context_rendering": "announced" + }, + "usage": { + "claude-opus-5-5[1m]": { + "inputTokens": 4, + "outputTokens": 509, + "cacheReadInputTokens": 52940, + "cacheCreationInputTokens": 30332, + "costUSD": 0.26344 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 10, + "outputTokens": 569, + "cacheReadInputTokens": 0, + "cacheCreationInputTokens": 4689, + "costUSD": 0.008716 + } + } + } + ], + "usage_total": { + "claude-opus-5-5[1m]": { + "inputTokens": 20, + "outputTokens": 2344, + "cacheReadInputTokens": 263466, + "cacheCreationInputTokens": 153558, + "costUSD": 1.328117 + }, + "claude-haiku-4-5-20251001": { + "inputTokens": 50, + "outputTokens": 4290, + "cacheReadInputTokens": 14666, + "cacheCreationInputTokens": 70203, + "costUSD": 0.11072 + } + }, + "cost_usd_total_cli_reported": 1.4388, + "cost_note": "costUSD as reported by the CLI's modelUsage, for all five runs including the failed v1 design." + }, + "rtk_hook_check": { + "rtk_version": "rtk 0.50.0", + "window_utc": { + "start": "2026-09-27T03:28:14Z", + "end": "2026-09-27T03:28:15Z" + }, + "method": "Each input through `rtk hook check -- ` (dry run) and through `rtk hook claude` with a Claude PreToolUse Bash payload on stdin (cwd /tmp, payload cwd /tmp), the processor the Claude settings template runs. Neither executes the input. The same cases were first observed between 02:28Z and 02:46Z; this run is the retained one.", + "cases": [ + { + "label": "single supported command", + "input": "git status", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status" + } + }, + { + "label": "and-chain", + "input": "git status && git log -n 3", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status && rtk git log -n 3\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status && rtk git log -n 3" + } + }, + { + "label": "multi-line, two supported lines", + "input": "git status\ngit log -n 3", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status\nrtk git log -n 3\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status\nrtk git log -n 3" + } + }, + { + "label": "multi-line, two supported lines", + "input": "git status\ngit diff --stat", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status\nrtk git diff --stat\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status\nrtk git diff --stat" + } + }, + { + "label": "multi-line, supported then echo", + "input": "git status\necho done", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status\necho done\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status\necho done" + } + }, + { + "label": "multi-line, cd then supported", + "input": "cd /tmp\ngit status", + "rtk_hook_check": { + "exit": 0, + "stdout": "cd /tmp\nrtk git status\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "cd /tmp\nrtk git status" + } + }, + { + "label": "multi-line, three supported lines", + "input": "git status\ngit diff\nls -la", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git status\nrtk git diff\nrtk ls -la\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git status\nrtk git diff\nrtk ls -la" + } + }, + { + "label": "pipeline, producer feeding head", + "input": "git diff | head -n 5", + "rtk_hook_check": { + "exit": 0, + "stdout": "rtk git diff | head -n 5\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "rtk git diff | head -n 5" + } + }, + { + "label": "pipeline, final grep stage", + "input": "git log -n 20 | grep fix", + "rtk_hook_check": { + "exit": 0, + "stdout": "git log -n 20 | rtk grep fix\n", + "stderr": "" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": "git log -n 20 | rtk grep fix" + } + }, + { + "label": "pipeline, gh producer", + "input": "gh pr view 1 | head -n 5", + "rtk_hook_check": { + "exit": 1, + "stdout": "", + "stderr": "No rewrite for: gh pr view 1 | head -n 5\n" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": null + } + }, + { + "label": "command substitution", + "input": "echo $(git status)", + "rtk_hook_check": { + "exit": 1, + "stdout": "", + "stderr": "No rewrite for: echo $(git status)\n" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": null + } + }, + { + "label": "backticks", + "input": "echo `git status`", + "rtk_hook_check": { + "exit": 1, + "stdout": "", + "stderr": "No rewrite for: echo `git status`\n" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": null + } + }, + { + "label": "process substitution", + "input": "diff <(git status) <(git status)", + "rtk_hook_check": { + "exit": 1, + "stdout": "", + "stderr": "No rewrite for: diff <(git status) <(git status)\n" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": null + } + }, + { + "label": "redirect to a file", + "input": "git status > /tmp/x.txt", + "rtk_hook_check": { + "exit": 1, + "stdout": "", + "stderr": "No rewrite for: git status > /tmp/x.txt\n" + }, + "rtk_hook_claude": { + "exit": 0, + "updated_command": null + } + }, + { + "label": "heredoc", + "input": "cat < and ; the working directory is described.", + "Hook commands inside transcripts are reduced to the hook's name (token-lanes or ai-memory).", + "No raw transcript text is published beyond each child's returned reply and the block's first 60 characters." + ], + "claim_boundary": "One native operation per arm for Agent-tool subagents on one host and client version. It shows SubagentStart delivery of the committed block to a general-purpose child and its absence for a blind-judge child and without the hook. It does not cover Workflow children, other hosts, plugin-scoped agent types, lane use, compliance or token savings." +} From ab8faeeb694cfa3438ab2292f22a3d6a9e625201 Mon Sep 17 00:00:00 2001 From: Scout Date: Sat, 26 Sep 2026 23:53:32 -0400 Subject: [PATCH 3/4] Count the carrier block's budget in UTF-8 bytes (cross-family review) The GPT-6 review of #378 at 85dd27cd found that the text test checked len(block), a character count, although the decision record states a 3,500-byte budget: a 3,280-character, 3,520-byte block would pass. The test now measures the file's bytes through fits_budget() and adds that case as a regression test. Controls: a character-counting fits_budget fails the regression test, and a block padded to 3,560 bytes (3,360 characters) fails the byte check but passed the old character check. The committed block is ASCII, 3,159 bytes. Co-Authored-By: Claude Opus 5.5 (1M context) --- tests/test_token_lanes_subagent_start.py | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/test_token_lanes_subagent_start.py b/tests/test_token_lanes_subagent_start.py index 84856ddf3..d043fc31a 100644 --- a/tests/test_token_lanes_subagent_start.py +++ b/tests/test_token_lanes_subagent_start.py @@ -17,6 +17,7 @@ HOOK = ROOT / "adoption/hooks/claude/token-lanes-subagent-start.py" BLOCK = HOOK.with_name("token-lanes-block.md") HANDBOOK = ROOT / "docs/token-session-handbook.md" +BUDGET_BYTES = 3_500 # the carrier's local budget, in UTF-8 bytes (docs/decisions/2026-09-27-token-lanes-subagent-start.md) KEY_PHRASES = ( "ToolSearch", "ctx_batch_execute", "rtk", "find_referencing_symbols", "codebase-memory", "jcodemunch", "TOON", "headroom", @@ -33,6 +34,10 @@ ) +def fits_budget(raw: bytes) -> bool: + return len(raw) <= BUDGET_BYTES + + class TokenLanesHookTests(unittest.TestCase): def run_hook(self, stdin, script=HOOK, home=None): env = os.environ.copy() @@ -113,11 +118,19 @@ def test_empty_or_unreadable_sibling_block_is_silent(self): class TokenLanesTextTests(unittest.TestCase): + def test_budget_counts_utf8_bytes_not_characters(self): + # 3,280 characters but 3,520 UTF-8 bytes: a character count would accept it. + text = "é" * 240 + "x" * 3_040 + self.assertEqual((len(text), len(text.encode("utf-8"))), (3_280, 3_520)) + self.assertFalse(fits_budget(text.encode("utf-8"))) + self.assertTrue(fits_budget(b"x" * BUDGET_BYTES)) + def test_block_fits_budget_and_matches_handbook(self): - block = BLOCK.read_text(encoding="utf-8") + raw = BLOCK.read_bytes() + block = raw.decode("utf-8") handbook = HANDBOOK.read_text(encoding="utf-8") self.assertTrue(block.startswith("TOKEN LANES")) - self.assertLessEqual(len(block), 3_500) + self.assertTrue(fits_budget(raw), f"{len(raw)} bytes") for marker in ("/" + "home/", "/" + "tmp/claude-1000", "/" + "Users/", "/" + "mnt/c/"): self.assertNotIn(marker, block) for phrase in KEY_PHRASES: From 7d533896ba101624b93a725b3fd4a3c442dd4dae Mon Sep 17 00:00:00 2001 From: Scout Date: Sun, 27 Sep 2026 01:54:04 -0400 Subject: [PATCH 4/4] Register the carrier's files on main 1aa97765 (after #366 and #380) Replayed onto main: the bootstrap.md note merges after #366's OTEL tool-details sentence, manifests/evidence.json is re-registered, and the matrix and grand list are regenerated. validate FAILS=0; 438 related tests OK. Co-Authored-By: Claude Opus 5.5 (1M context) --- manifests/evidence.json | 59 ++++++++++++++++++++++++++++++++--------- 1 file changed, 47 insertions(+), 12 deletions(-) diff --git a/manifests/evidence.json b/manifests/evidence.json index 3bbbe7c44..3b79e86e3 100644 --- a/manifests/evidence.json +++ b/manifests/evidence.json @@ -3537,8 +3537,8 @@ }, { "path": "adoption/bootstrap.md", - "sha256": "eb3c9ecc5fd7d25df5f8f93d845c32238a0a2bab2bb5572aef2eb3cc27ebca72", - "bytes": 41767 + "sha256": "65cd3dcbcf23109bb2aa2d19b30de4fd03c5b306b2d8659f477c6de53b55d8c5", + "bytes": 42814 }, { "path": "adoption/credential-inventory.json", @@ -3552,8 +3552,18 @@ }, { "path": "adoption/hooks/claude/SHA256SUMS", - "sha256": "94b9c17fc579d0b2b7850fad8a6687854f7de7ff00a2732489c2a9500d5fb7de", - "bytes": 200 + "sha256": "dd640276e81077e115713d03d9b4d389654a3d4708fdfd95fc7cf6b137067f0e", + "bytes": 383 + }, + { + "path": "adoption/hooks/claude/token-lanes-block.md", + "sha256": "8b9a60fa425900930c610ba7e3e0f8a0fa1457048a39c79e3136fb5073e7269f", + "bytes": 3159 + }, + { + "path": "adoption/hooks/claude/token-lanes-subagent-start.py", + "sha256": "7a9228e41235771336189287944d81d998795b025a9a038d022beb21a19ce870", + "bytes": 1162 }, { "path": "adoption/host-receipt.schema.json", @@ -3697,8 +3707,8 @@ }, { "path": "adoption/templates/claude.settings.template.json", - "sha256": "7203ca7f223e2bb28d1e6bce314a647e7bf609b1112a8c9f2dd1f69dbab496a4", - "bytes": 9132 + "sha256": "71fd95769a18bf367addd8e5c705948c84bef950347507ec54e83917edd6afd9", + "bytes": 9391 }, { "path": "adoption/templates/codex.config.template.toml", @@ -13676,6 +13686,11 @@ "sha256": "517441575c63c19ff252f6fb53eca2e09a5c910e565aabde45da221e6922247f", "bytes": 7782 }, + { + "path": "docs/decisions/2026-09-27-token-lanes-subagent-start.md", + "sha256": "9afeaefcb0915e8de59e6f897702d991da310e82cce233044d20b4d2668493d0", + "bytes": 14486 + }, { "path": "docs/direct-results.md", "sha256": "12a511fee11f22c90b9b622701d5485be9c8247167102b5768d694244f939a5b", @@ -13808,8 +13823,8 @@ }, { "path": "docs/harness-defaults.md", - "sha256": "30c6a0769637d7bde5d7f066aa557b10fd6c571a4bc47ac95ac206d6d660bea9", - "bytes": 29153 + "sha256": "b299b913601e7498a1c540b3c25596a7135e39e8b1dce6b604225d77589deae7", + "bytes": 31510 }, { "path": "docs/harness-rules-convergence-20260922.md", @@ -14048,8 +14063,8 @@ }, { "path": "docs/token-session-handbook.md", - "sha256": "18069130d801ecc9b770b8d1a06be5c1c70f0697cf985495df92b8a195172894", - "bytes": 58662 + "sha256": "45b4c6f9c1d576dad3f4d479c681b354b2590de5c01816b86124635bde492c9f", + "bytes": 66836 }, { "path": "docs/ultracode-token-routing-20260921.md", @@ -35566,6 +35581,16 @@ "sha256": "f186f4a192efafb3182185093abf87a7bb7d5b157bfe0d783dfe5df83f38c9f8", "bytes": 147636 }, + { + "path": "evidence/artifacts/token-lanes-subagent-start-20260927/README.md", + "sha256": "42bc254fd84b254520f06d9cb9bbd6460bb8ef301b50bff893467470e0c36834", + "bytes": 6655 + }, + { + "path": "evidence/artifacts/token-lanes-subagent-start-20260927/receipt.json", + "sha256": "c575243c1efc5886e1f77895606959016bec808493f5dad3b300b506c2d0038a", + "bytes": 24371 + }, { "path": "evidence/artifacts/token-workflow-hardening-20260926/README.md", "sha256": "9a7cffe2d1b60650995a8de94507b8b7aa38e373b69e547728d937f300677a31", @@ -38718,8 +38743,8 @@ }, { "path": "tests/test_install_claude_profile.py", - "sha256": "9a712defd2f8bd10ef68753cfa39e654b9e5fcbdcc3e3ea1c1d2fa3363329d9f", - "bytes": 44586 + "sha256": "19a55aa2fedfe799507a82c9f8c7cfe7940e425f53bf6afa790f09ad64b4a18c", + "bytes": 48391 }, { "path": "tests/test_install_skills.py", @@ -39051,6 +39076,11 @@ "sha256": "a4e3e5a6f40a7f3b4e679b21dc56f443a27ccdd9f097423bdf64d9e41eef19f8", "bytes": 6838 }, + { + "path": "tests/test_token_lanes_subagent_start.py", + "sha256": "3d16aa8faaba13d38e7d853d1afdb6e4ff6fde8d8dccb6bab85785a2a90f7a78", + "bytes": 7147 + }, { "path": "tests/test_trading_gates.py", "sha256": "10b1ce5b0cec4d3e68d792c83a9ad2b4fcbb70863305b0496961650deb34214f", @@ -39131,6 +39161,11 @@ "sha256": "b567a3ce64d66f0798bd5fc73cf78d8a12074d583177c204d5465050873258b1", "bytes": 8209 }, + { + "path": "tools/adoption/install_claude_profile.py", + "sha256": "653059217d6e23ef5ea2b6311808f4d67e4d565c6cfdfe73dfe2f2104d8cbed9", + "bytes": 14105 + }, { "path": "tools/adoption/install_skills.py", "sha256": "e2660484c3c094f27c1a4780945ec0a65572ae974139ceeb092939b36e94bf8a",