@mmerterden/multi-agent-pipeline 16.20.0 → 16.21.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -16,6 +16,30 @@ Internal file-layout changes that don't affect the slash-command surface are sti
16
16
 
17
17
  ## [Unreleased]
18
18
 
19
+ ## [16.21.0] - 2026-09-08
20
+
21
+ ### Added
22
+
23
+ - **A read-size gate, and a cheap worker to send an oversized read to.** `offload-ref.sh` took half the machinery bill in 15.10.0 - the build log, the diff, the test output become a pointer plus a tail. The other half was never looked at, because nothing could look at it: only a `PreToolUse` hook sees a tool call before it runs. A phase that reads six 900-line files pays for 5,400 lines at that phase's own rung, and the part it needed was a handful of symbols.
24
+
25
+ `check-read-size.sh` (+ `check-read-size.py`, the decision core) is the third hook in `install/templates/claude-hooks.json` and the first on the READ side. `bulk-read.sh` is where a blocked read goes instead: the file reaches a haiku-rung worker over the `bulk-reader` persona, and what comes back is a structured summary with LINE NUMBERS, so the caller's next step is a bounded `Read(offset:limit:)` around the region that matters - which is cheap, exact, and passes the gate. The full text is parked under `.multi-agent/refs/`, the same place and the same promise `offload-ref.sh` makes.
26
+
27
+ **`observe` is a real mode, not a debug flag.** It decides and logs, blocks nothing, and it is what `bulkRead.mode` should be set to first: a gate switched straight to `enforce` has no baseline to be compared against, and "we cut tokens" becomes a claim nothing can check. The saving is measured from `metrics.jsonl` (`read.observed` / `read.blocked` / `bulk_read.delegated`), not asserted here - this entry deliberately quotes no percentage.
28
+
29
+ **Phase 3 is exempt, and that is load-bearing.** Claude Code's `Edit` requires the same file to have been `Read` first, so a gate that blocks reads while code is being changed blocks the change. So does any file the run itself has touched (`phases[n].files[]`). The gate is for the phases that read to UNDERSTAND. A draft of the decision core read `run.phase` and `dev.editSet` - neither of which `agent-state.schema.json` declares - so the exemption could never have engaged and development would have been blocked by a hook whose message read like advice; `smoke-bulk-read.sh` now asserts the field names against the schema so it cannot come back quietly.
30
+
31
+ **A ceiling, because delegation is not free either** (`bulkRead.maxBytes`, default 1 MB). Past some size the worker's own input bill approaches the read it replaced, and a file large enough to strain its window comes back truncated - a partial summary presented as a whole one is the one thing this feature must never produce. Over the ceiling the caller is told to narrow first (grep, then a bounded read). The check runs BEFORE the file is hashed or parked, so the guard is not theatre.
32
+
33
+ **The command parser is per-tool, because the flags are not shared.** `cat -n` NUMBERS LINES rather than taking a count, and reading its next token as one swallowed the filename, left no operand, and let `cat -n <bigfile>` through the gate entirely. `sed -n '1,900p'` looked bounded because it carried a flag, when the range in its script IS the bound. An unparseable count reads as UNBOUNDED, never as 1. A `tail -f` is a stream and passes, because blocking it would be a false positive with no cheap alternative to offer.
34
+
35
+ Off by default (`bulkRead.mode: "off"`), so merging the hook block is not itself a behaviour change. The worker degrades rather than inventing: no CLI, no auth, a timeout, or a non-JSON answer prints why and tells the caller to do a bounded read. A fabricated summary of a file nobody read is the one outcome worse than paying full price for the file.
36
+
37
+ ### Changed
38
+
39
+ - **`multi-agent:setup` Step 8 and the picker-contract stop miscounting the hooks.** Both said two gates ship, and the setup step called the secret scan "the only deterministic gate that is OS-enforceable as a hook" - untrue since `agent-guard.sh` landed. Three ship now, and both places say so, along with which side of the run each one inspects.
40
+ - **`smoke-gate-hooks.sh` covers all three gates** rather than two, and asserts the shipped defaults are safe (the read gate off, phase 3 exempt) - a read gate that shipped switched on would have been a breaking change disguised as a merge.
41
+
42
+
19
43
  ## [16.20.0] - 2026-09-07
20
44
 
21
45
  A comparison against `msitarzewski/agency-agents`, a 150k-star persona catalog, found nothing to copy wholesale and three things to fix: the personas were never registered, the circuit-breaker was prose, and a review finding had no identity from one round to the next. This release gives findings that identity and builds the loop controls, the handoff records and the evidence rules on top of it.
package/README.md CHANGED
@@ -213,7 +213,7 @@ The pipeline runs natively on **Claude Code**, **Copilot CLI** and **Codex CLI**
213
213
 
214
214
  | Tool | Flag | What it installs |
215
215
  |---|---|---|
216
- | Claude Code | `--claude` (default) | slash commands + skills + agents + `PreToolUse` secret-scan hook |
216
+ | Claude Code | `--claude` (default) | slash commands + skills + agents + three `PreToolUse` hooks (secret scan, agent-guard, read-size gate) |
217
217
  | Copilot CLI | `--copilot` | instructions + 55 sub-command skills + scripts |
218
218
  | Codex CLI | `--codex` | one router skill + 55 specs as refs + 8 agent TOML + `AGENTS.md` block + `codex mcp add` |
219
219
 
package/README.tr.md CHANGED
@@ -213,7 +213,7 @@ Pipeline **Claude Code**, **Copilot CLI** ve **Codex CLI** üzerinde native çal
213
213
 
214
214
  | Araç | Bayrak | Ne kurar |
215
215
  |---|---|---|
216
- | Claude Code | `--claude` (varsayılan) | slash komutları + skill'ler + agent'lar + `PreToolUse` secret-scan hook'u |
216
+ | Claude Code | `--claude` (varsayılan) | slash komutları + skill'ler + agent'lar + üç `PreToolUse` hook'u (secret scan, agent-guard, okuma-boyutu geçidi) |
217
217
  | Copilot CLI | `--copilot` | talimatlar + 55 alt-komut skill'i + script'ler |
218
218
  | Codex CLI | `--codex` | bir router skill + ref olarak 55 spec + 8 agent TOML + `AGENTS.md` bloğu + `codex mcp add` |
219
219
 
package/docs/features.md CHANGED
@@ -222,6 +222,7 @@ Phase 3 treats the issue-tracker status update as a required step with a post-mu
222
222
  ## Safety & Hygiene
223
223
 
224
224
  - **Pre-Commit Secret Detection** (12 patterns): `PreToolUse` hook scans staged files for API keys/tokens, AWS access keys, private keys, `.env` files, service account JSON. Commit **blocked** if found.
225
+ - **Read-Size Gate** (opt-in, `prefs.global.bulkRead.mode`): a `PreToolUse` hook inspects `Read` and the shell commands that read a file whole. In `observe` it only logs what it would have caught - the baseline you measure before routing anything. In `enforce` a file over `minLines` (default 350) is blocked and delegated to a haiku-rung worker (`bulk-read.sh`), which returns a line-numbered summary so the follow-up is a bounded `Read(offset:limit:)` instead of the whole file; the full text is parked under `.multi-agent/refs/`. The development phase and any file the run has already touched are exempt, because Claude Code's `Edit` requires its own `Read` first.
225
226
  - **Build Queue**: All `xcodebuild` calls acquire a lock. Each worktree uses own `-derivedDataPath`. Stale locks auto-clean after 15 min. Non-Xcode builds don't need the lock.
226
227
  - **Context Management**: `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE=65` - compaction at 65% usage (prevents degradation in 8-phase sessions).
227
228
  - **3-Iteration Hard Kill**: Any retry loop stops after 3 attempts, then pauses for user. No infinite loops.
@@ -1,5 +1,5 @@
1
1
  {
2
- "_readme": "Recommended Claude Code hooks for multi-agent-pipeline. Merge the `hooks` object into your ~/.claude/settings.json to make these deterministic, OS-enforced PreToolUse gates real (exit 2 blocks the tool call) rather than prompt-level hopes. Two gates ship here: (1) a staged-diff secret scan on git commit (pre-commit-check.sh); (2) an agent-guard on git commit + git push (agent-guard.sh) that blocks AI/assistant attribution in commit messages and force-push to a protected branch (main/master/develop). Both scripts are self-contained, fail-open on internal error, never execute the inspected command, and need no run-specific arguments, which is why they are naturally PreToolUse hooks. The other deterministic gates (evidence, consensus, intent, learnings) take run-specific arguments and are phase-enforced by the pipeline instead. multi-agent:setup offers to merge this block.",
2
+ "_readme": "Recommended Claude Code hooks for multi-agent-pipeline. Merge the `hooks` object into your ~/.claude/settings.json to make these deterministic, OS-enforced PreToolUse gates real (exit 2 blocks the tool call) rather than prompt-level hopes. Three gates ship here: (1) a staged-diff secret scan on git commit (pre-commit-check.sh); (2) an agent-guard on git commit + git push (agent-guard.sh) that blocks AI/assistant attribution in commit messages and force-push to a protected branch (main/master/develop); (3) a read-size gate on Read and Bash (check-read-size.sh), which inspects Read plus the shell commands that read a file whole (cat/head/tail/sed) and returns immediately for everything else, which routes an oversized read to a cheap worker instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `prefs.global.bulkRead.mode` is set to observe or enforce - so merging this block changes nothing until you opt in. All three are self-contained, fail-open on internal error, never execute the inspected command, and need no run-specific arguments, which is why they are naturally PreToolUse hooks. The other deterministic gates (evidence, consensus, intent, learnings) take run-specific arguments and are phase-enforced by the pipeline instead. multi-agent:setup offers to merge this block.",
3
3
  "hooks": {
4
4
  "PreToolUse": [
5
5
  {
@@ -29,6 +29,17 @@
29
29
  "statusMessage": "Checking push safety..."
30
30
  }
31
31
  ]
32
+ },
33
+ {
34
+ "matcher": "Read|Bash",
35
+ "hooks": [
36
+ {
37
+ "type": "command",
38
+ "command": "bash $HOME/.claude/scripts/check-read-size.sh",
39
+ "timeout": 10,
40
+ "statusMessage": "Checking read size..."
41
+ }
42
+ ]
32
43
  }
33
44
  ]
34
45
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mmerterden/multi-agent-pipeline",
3
- "version": "16.20.0",
3
+ "version": "16.21.0",
4
4
  "description": "8-phase AI development pipeline with full orchestration on Claude Code, Copilot CLI and Codex CLI. Analysis, planning, TDD, CLI-aware parallel review with consensus surfacing + Fable triage, default-FAIL evidence gates, secret + intent guards, per-phase cost ledger, persistent learnings memory, wiki generation, commit automation. Token-preserving uninstall.",
5
5
  "type": "module",
6
6
  "main": "index.js",
@@ -0,0 +1,57 @@
1
+ ---
2
+ name: bulk-reader
3
+ description: "Reads ONE large file and returns a structured, line-numbered summary so the full text never enters the caller's context. Dispatched by bulk-read.sh when check-read-size.sh blocks a whole-file read. Haiku by default; a delegated read costs a fraction of a cent."
4
+ model: haiku
5
+ preferredModel: haiku
6
+ modelRationale: "Reading a file and reporting what is in it is extraction, not judgement - the task has a single source, a fixed output shape, and no reasoning chain. Haiku is the right rung and the whole point: the saving is the difference between this rung and the caller's. A worker that reasons is the wrong tool here, and the contract below forbids it explicitly, because a cheap rung's opinion about code is worth less than nothing."
7
+ ---
8
+
9
+ # Bulk Reader
10
+
11
+ You are given ONE file and ONE question. You return ONE JSON object and nothing
12
+ else: no prose before it, no markdown fence around it, no commentary after it.
13
+
14
+ The file arrives with its lines numbered. Those numbers are the file's own, so a
15
+ number you report is a number the caller can open directly.
16
+
17
+ ## Rules
18
+
19
+ - **Every claim carries the line numbers it comes from.** A claim without them is
20
+ not usable - the caller cannot open it, cannot check it, and ends up reading
21
+ the file itself, having now paid for it twice. If you cannot cite it, do not
22
+ claim it.
23
+ - **You describe what IS in the file.** You do not review it, do not judge its
24
+ quality, do not propose changes, and do not name defects. Judgement about code
25
+ is the caller's; you are here so the caller has something to judge.
26
+ - **You never guess.** If the question cannot be answered from this file, say
27
+ exactly that in `answer` and return an empty `regions`. A confident wrong
28
+ summary is the one outcome worse than the caller paying full price for the
29
+ file, because nothing downstream can tell it is wrong.
30
+ - **`regions` are where a reader should look next**, most important first, at
31
+ most 8. Each one is a span worth opening on its own - not the whole file
32
+ restated as one region.
33
+ - If you did not see the whole file, set `truncated: true`. Do not summarize a
34
+ part as though it were the whole.
35
+
36
+ ## Output Format
37
+
38
+ ```json
39
+ {
40
+ "answer": "<direct answer to the question, or why this file cannot answer it>",
41
+ "summary": "<what this file is and does, 3-6 sentences>",
42
+ "symbols": [{"name": "<declaration>", "kind": "type|func|var|extension|other", "line": 42}],
43
+ "regions": [{"why": "<what a reader finds here>", "start": 120, "end": 180}],
44
+ "truncated": false
45
+ }
46
+ ```
47
+
48
+ Contract: `pipeline/schemas/bulk-read-output.schema.json`.
49
+
50
+ ## What this agent does NOT do
51
+
52
+ - Does NOT review, rate, or critique the code it reads.
53
+ - Does NOT read a second file, follow an import, or look anything up.
54
+ - Does NOT answer from prior knowledge of a framework - only from this file.
55
+ - Does NOT edit anything. It has no write path by design: a summary has no
56
+ reliable basis for an edit, which is why the caller comes back with a bounded
57
+ read before changing a line.
@@ -813,13 +813,15 @@ To set up multi-agent on a new machine:
813
813
 
814
814
  All tokens are optional in the sense that every service can be answered with Skip - but the ASKING is not optional: the Step 3 sequential loop still walks every missing service one by one (token → author → host). Phase 0 re-asks at runtime only for tokens the user skipped here.
815
815
 
816
- ### Step 8 - Enforcement hook (optional, Claude Code)
816
+ ### Step 8 - Enforcement hooks (optional, Claude Code)
817
817
 
818
- Offer to make the secret scan a HARD pre-commit gate (a non-zero exit blocks the commit) instead of an advisory step. The recommended block ships at `install/templates/claude-hooks.json`.
818
+ Offer to make the three hookable gates HARD (a non-zero exit blocks the tool call). The block ships at `install/templates/claude-hooks.json`: secret scan, agent-guard, read-size gate.
819
+
820
+ - Ask (picker): "Install the pipeline's PreToolUse gates into `~/.claude/settings.json`?" Default Yes.
821
+ - On Yes, deep-merge the template's `hooks.PreToolUse` (preserve existing hooks; never duplicate a matcher already calling the same script).
822
+ - Say what the merge does NOT cover: only these three need no run-specific arguments, so only these three are hookable; the rest are phase-enforced.
823
+ - Say what it does not turn on: the read-size gate is inert until `prefs.global.bulkRead.mode` is set. Recommend `observe` first. Why, and the Phase 3 exemption: `$HOME/.claude/multi-agent-refs/picker-contract.md`.
819
824
 
820
- - Ask (picker): "Install the pre-commit secret-scan hook into `~/.claude/settings.json`?" Default Yes.
821
- - On Yes, deep-merge the template's `hooks.PreToolUse` into the user's `settings.json` (preserve any existing hooks; do not duplicate a matcher that already calls `pre-commit-check.sh`).
822
- - Honest note to show: this is the only deterministic gate that is OS-enforceable as a hook (it needs no run-specific arguments). The evidence / consensus / intent / learnings gates are invoked by the pipeline phases with per-run arguments, so they are enforced by the phase contract + the installed gate scripts, not by a hook.
823
825
  ### Step 9 - Default stack plugin enablement
824
826
 
825
827
  Stack skills ship as versioned plugins in the `{owner}/multi-agent-plugins` marketplace. On first setup, wire the stack so the pipeline works out of the box.
@@ -81,6 +81,6 @@ In autopilot, `ask_choice` resolves to `default` (or the safe first option) with
81
81
 
82
82
  ## Deterministic gates note
83
83
 
84
- Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Two ship, both needing no run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop). Both are self-contained, fail-open on internal error, and never execute the inspected command. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
84
+ Claude Code's `PreToolUse` exit-2 hooks are the HARD blocking gates. Three ship, none needing run-specific arguments so they are naturally hookable: (1) `pre-commit-check.sh` scans the staged diff on every `git commit` and blocks on a detected secret; (2) `agent-guard.sh` runs on `git commit` + `git push` and blocks AI/assistant attribution in a commit message and force-push to a protected branch (main/master/develop); (3) `check-read-size.sh` runs on `Read` and on the shell commands that read a file whole, and routes an oversized read to a cheap worker (`bulk-read.sh`) instead of the caller's own rung. The first two inspect what a run WRITES; the third inspects what it pays to READ, and it is inert until `bulkRead.mode` is set to `observe` or `enforce`, so merging the block changes nothing until the user opts in. Its `observe` mode blocks nothing and only logs, which is how the baseline is measured before anything is routed. All three are self-contained, fail-open on internal error, and never execute the inspected command. The recommended hook block ships at `install/templates/claude-hooks.json`; `multi-agent:setup` offers to merge it into `~/.claude/settings.json`. The other deterministic gates (evidence, consensus, intent, learnings) are invoked by the pipeline phases with per-run arguments (a build-log path, the triage JSON, the free-text input), so they are phase-enforced by contract, not OS-hookable.
85
85
 
86
86
  Copilot CLI has no `PreToolUse` equivalent, so the secret scan there is workflow-enforced (run as a phase step, not OS-blocked) plus a CI smoke-gate step.
@@ -0,0 +1,52 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema#",
3
+ "$id": "https://example.com/pipeline/bulk-read-output.schema.json",
4
+ "title": "Bulk Read Output",
5
+ "description": "Schema for the bulk-reader worker's answer. Source: pipeline/agents/bulk-reader.md, invoked by pipeline/scripts/bulk-read.sh when check-read-size.sh blocks a whole-file read. Every positional field is a LINE NUMBER in the file as given to the worker, because a summary the caller cannot open is a summary the caller will pay to replace.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["summary"],
9
+ "properties": {
10
+ "answer": {
11
+ "type": "string",
12
+ "description": "Direct answer to the question the caller asked, or a plain statement that this file cannot answer it. Never a guess - an unanswerable question is reported, not filled in."
13
+ },
14
+ "summary": {
15
+ "type": "string",
16
+ "description": "What the file is and does, 3-6 sentences. Descriptive only: no review, no judgement, no proposed change."
17
+ },
18
+ "symbols": {
19
+ "type": "array",
20
+ "description": "Top-level declarations the file contains, in file order.",
21
+ "items": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "required": ["name", "line"],
25
+ "properties": {
26
+ "name": { "type": "string" },
27
+ "kind": { "type": "string", "enum": ["type", "func", "var", "extension", "other"] },
28
+ "line": { "type": "integer", "minimum": 1 }
29
+ }
30
+ }
31
+ },
32
+ "regions": {
33
+ "type": "array",
34
+ "maxItems": 8,
35
+ "description": "Spans worth opening next, most important first. The intended use is a bounded Read(offset:limit:) over one of these, which is both cheaper than the whole file and exact - and which the read gate allows through.",
36
+ "items": {
37
+ "type": "object",
38
+ "additionalProperties": false,
39
+ "required": ["start", "end"],
40
+ "properties": {
41
+ "why": { "type": "string", "description": "What a reader finds in this span." },
42
+ "start": { "type": "integer", "minimum": 1 },
43
+ "end": { "type": "integer", "minimum": 1 }
44
+ }
45
+ }
46
+ },
47
+ "truncated": {
48
+ "type": "boolean",
49
+ "description": "True when the worker did not see the whole file. A partial read summarized as a whole one is the failure this flag exists to make visible."
50
+ }
51
+ }
52
+ }
@@ -365,7 +365,7 @@
365
365
  },
366
366
  "multiRepoIntegrationHosts": {
367
367
  "type": "array",
368
- "description": "v5.6.0+. Learn-once registry of host projects that build together with a multi-repo combo (codegen producer consumer + host integration project). Pipeline checks this registry in Phase 6 before commit; on match, auto-runs the host build. On miss for a ≥2-repo task, prompts the user once and persists the answer. See refs/multi-repo-integration-build.md for the full contract.",
368
+ "description": "v5.6.0+. Learn-once registry of host projects that build together with a multi-repo combo (codegen producer \u2192 consumer + host integration project). Pipeline checks this registry in Phase 6 before commit; on match, auto-runs the host build. On miss for a \u22652-repo task, prompts the user once and persists the answer. See refs/multi-repo-integration-build.md for the full contract.",
369
369
  "items": {
370
370
  "type": "object",
371
371
  "additionalProperties": false,
@@ -653,12 +653,12 @@
653
653
  "reportChannels": {
654
654
  "type": "object",
655
655
  "additionalProperties": false,
656
- "description": "v5.7+ - Phase 7 / /multi-agent:channels kanal seçimi default'ları. Multi-select menüde tick'li gelecek kanallar. Her kanal bağımsız boolean. Autopilot Phase 7'de ALWAYS pauses (30-min timeout) - bu değerler sadece menünün önceden seçili halini belirler.",
656
+ "description": "v5.7+ - Phase 7 / /multi-agent:channels kanal se\u00e7imi default'lar\u0131. Multi-select men\u00fcde tick'li gelecek kanallar. Her kanal ba\u011f\u0131ms\u0131z boolean. Autopilot Phase 7'de ALWAYS pauses (30-min timeout) - bu de\u011ferler sadece men\u00fcn\u00fcn \u00f6nceden se\u00e7ili halini belirler.",
657
657
  "properties": {
658
658
  "pr": {
659
659
  "type": "boolean",
660
660
  "default": true,
661
- "description": "PR description update (replace/append). Default ON - en yaygın kanal."
661
+ "description": "PR description update (replace/append). Default ON - en yayg\u0131n kanal."
662
662
  },
663
663
  "jira": {
664
664
  "type": "boolean",
@@ -668,12 +668,12 @@
668
668
  "confluence": {
669
669
  "type": "boolean",
670
670
  "default": false,
671
- "description": "Confluence page creation. Default OFF - bir kez parent page seçince LRU'dan öner."
671
+ "description": "Confluence page creation. Default OFF - bir kez parent page se\u00e7ince LRU'dan \u00f6ner."
672
672
  },
673
673
  "wiki": {
674
674
  "type": "boolean",
675
675
  "default": false,
676
- "description": "Component wiki pages (Case A scope multi-select). Default OFF - taskType=component + figmaConfig.wiki.enabled gerekli, yoksa menüde greyed out."
676
+ "description": "Component wiki pages (Case A scope multi-select). Default OFF - taskType=component + figmaConfig.wiki.enabled gerekli, yoksa men\u00fcde greyed out."
677
677
  }
678
678
  },
679
679
  "default": {
@@ -686,32 +686,32 @@
686
686
  "reportContent": {
687
687
  "type": "object",
688
688
  "additionalProperties": false,
689
- "description": "v5.7+ - Phase 7 / /multi-agent:channels içerik seçimi default'ları. Multi-select menüde tick'li gelecek content source'ları.",
689
+ "description": "v5.7+ - Phase 7 / /multi-agent:channels i\u00e7erik se\u00e7imi default'lar\u0131. Multi-select men\u00fcde tick'li gelecek content source'lar\u0131.",
690
690
  "properties": {
691
691
  "normalAnalysis": {
692
692
  "type": "boolean",
693
693
  "default": true,
694
- "description": "Phase 1+2+4 pipeline log'undan impact summary + risks + architectural decisions (yüksek seviye). Greyed out post-hoc çağrıda pipeline log yoksa."
694
+ "description": "Phase 1+2+4 pipeline log'undan impact summary + risks + architectural decisions (y\u00fcksek seviye). Greyed out post-hoc \u00e7a\u011fr\u0131da pipeline log yoksa."
695
695
  },
696
696
  "technicalAnalysis": {
697
697
  "type": "boolean",
698
698
  "default": false,
699
- "description": "Changes (değişen dosyalar gruplanıp ne/neden), Architecture (structural decisions), Dependencies (yeni import/framework/paket). PR body'deki 'Technical Details' bölümünün özeti; user'ın kanal seçimi PR içermediği durumlarda (ör. sadece Jira/Confluence) teknik içerik aktarmak istiyorsa devreye girer. Source: Phase 2 planning + Phase 3 dev log + PR diff stat."
699
+ "description": "Changes (de\u011fi\u015fen dosyalar gruplan\u0131p ne/neden), Architecture (structural decisions), Dependencies (yeni import/framework/paket). PR body'deki 'Technical Details' b\u00f6l\u00fcm\u00fcn\u00fcn \u00f6zeti; user'\u0131n kanal se\u00e7imi PR i\u00e7ermedi\u011fi durumlarda (\u00f6r. sadece Jira/Confluence) teknik i\u00e7erik aktarmak istiyorsa devreye girer. Source: Phase 2 planning + Phase 3 dev log + PR diff stat."
700
700
  },
701
701
  "testScenarios": {
702
702
  "type": "boolean",
703
703
  "default": true,
704
- "description": "Precondition / steps / expected tablosu (4-8 satır, user perspective). Pipeline log source."
704
+ "description": "Precondition / steps / expected tablosu (4-8 sat\u0131r, user perspective). Pipeline log source."
705
705
  },
706
706
  "autoDiff": {
707
707
  "type": "boolean",
708
708
  "default": false,
709
- "description": "PR diff'ten auto-generate özet (eski enrich behavior - root cause / solution / changed files / test scenarios). PR linked değilse greyed out."
709
+ "description": "PR diff'ten auto-generate \u00f6zet (eski enrich behavior - root cause / solution / changed files / test scenarios). PR linked de\u011filse greyed out."
710
710
  },
711
711
  "manualNote": {
712
712
  "type": "boolean",
713
713
  "default": false,
714
- "description": "Serbest metin paragraf (--message / --message-file). Her durumda seçilebilir."
714
+ "description": "Serbest metin paragraf (--message / --message-file). Her durumda se\u00e7ilebilir."
715
715
  },
716
716
  "costSummary": {
717
717
  "type": "boolean",
@@ -721,7 +721,7 @@
721
721
  "workSummary": {
722
722
  "type": "boolean",
723
723
  "default": false,
724
- "description": "v7.1.0+ - Executive 'Work Done' summary block. Distills the whole pipeline run into a single-screen section: task + branch + base + PR number, scope delivered (✅/⏳ per Phase 2 task), changed files with +/- counts (capped at 20 rows), review outcome (accepted/deferred/rejected counts + approved flag), and a one-line phase tick strip (0 Init · 1 Analysis · ...). Source: `agent-state.json` + `phase-tracker.json` + `git diff --numstat` between `baseBranch`...HEAD. Consumed by `render-work-summary.sh`. Greyed out if no state file exists for the task. Opt-in - off by default so baseline PR body stays unchanged."
724
+ "description": "v7.1.0+ - Executive 'Work Done' summary block. Distills the whole pipeline run into a single-screen section: task + branch + base + PR number, scope delivered (\u2705/\u23f3 per Phase 2 task), changed files with +/- counts (capped at 20 rows), review outcome (accepted/deferred/rejected counts + approved flag), and a one-line phase tick strip (0 Init \u2705 \u00b7 1 Analysis \u2705 \u00b7 ...). Source: `agent-state.json` + `phase-tracker.json` + `git diff --numstat` between `baseBranch`...HEAD. Consumed by `render-work-summary.sh`. Greyed out if no state file exists for the task. Opt-in - off by default so baseline PR body stays unchanged."
725
725
  }
726
726
  },
727
727
  "default": {
@@ -739,7 +739,7 @@
739
739
  "default": 1800,
740
740
  "minimum": 60,
741
741
  "maximum": 7200,
742
- "description": "v5.7+ - Phase 7'de autopilot always-pause menüsünde kullanıcı cevap vermezse session'ı sonlandırma süresi (saniye). Default 1800 (30 dk). Timeout'ta external delivery aborted, internal capture (agent-log, telemetry, knowledge) yine çalışır, session /multi-agent:resume ile devam ettirilebilir."
742
+ "description": "v5.7+ - Phase 7'de autopilot always-pause men\u00fcs\u00fcnde kullan\u0131c\u0131 cevap vermezse session'\u0131 sonland\u0131rma s\u00fcresi (saniye). Default 1800 (30 dk). Timeout'ta external delivery aborted, internal capture (agent-log, telemetry, knowledge) yine \u00e7al\u0131\u015f\u0131r, session /multi-agent:resume ile devam ettirilebilir."
743
743
  },
744
744
  "wikiScope": {
745
745
  "type": "array",
@@ -748,7 +748,7 @@
748
748
  "enum": ["main", "ios", "screenshots", "index"]
749
749
  },
750
750
  "default": ["main", "ios", "screenshots", "index"],
751
- "description": "v5.7+ - Wiki Case A scope multi-select default'u. Component wiki dispatch'inde hangi artifact'lar yazılacak: main (ana component sayfası), ios (iOS sub-page), screenshots (assets/ klasörü), index (_Sidebar.md + ComponentImplementationStatus.md). Legacy wikiDefault=true [main,ios,screenshots,index] migration; wikiDefault=false [] (empty array = Wiki adapter Case B menüsüne düşer)."
751
+ "description": "v5.7+ - Wiki Case A scope multi-select default'u. Component wiki dispatch'inde hangi artifact'lar yaz\u0131lacak: main (ana component sayfas\u0131), ios (iOS sub-page), screenshots (assets/ klas\u00f6r\u00fc), index (_Sidebar.md + ComponentImplementationStatus.md). Legacy wikiDefault=true \u2192 [main,ios,screenshots,index] migration; wikiDefault=false \u2192 [] (empty array = Wiki adapter Case B men\u00fcs\u00fcne d\u00fc\u015fer)."
752
752
  },
753
753
  "autoJiraFromGithubIssue": {
754
754
  "type": "string",
@@ -831,7 +831,7 @@
831
831
  "reviewDisagreementRound": {
832
832
  "type": "boolean",
833
833
  "default": false,
834
- "description": "v6.1.0+ - Phase 4 Step 2.5 rebuttal round. When reviewers disagree (mixed blocker/approved verdict), each reviewer is re-prompted with the others' opposing arguments for one additional round before triage. Lifts signal quality on ambiguous findings at ~1× Step 2 token cost. Off by default - flip for security-critical or release-branch reviews."
834
+ "description": "v6.1.0+ - Phase 4 Step 2.5 rebuttal round. When reviewers disagree (mixed blocker/approved verdict), each reviewer is re-prompted with the others' opposing arguments for one additional round before triage. Lifts signal quality on ambiguous findings at ~1\u00d7 Step 2 token cost. Off by default - flip for security-critical or release-branch reviews."
835
835
  },
836
836
  "analysisProfiles": {
837
837
  "type": "array",
@@ -1047,7 +1047,7 @@
1047
1047
  "minimum": 0,
1048
1048
  "maximum": 10,
1049
1049
  "default": 6,
1050
- "description": "Clarity threshold. Score threshold proceed silently. Below questions fire. 6 is the borderline 'the what is clear but the how is fuzzy' line."
1050
+ "description": "Clarity threshold. Score \u2265 threshold \u2192 proceed silently. Below \u2192 questions fire. 6 is the borderline 'the what is clear but the how is fuzzy' line."
1051
1051
  },
1052
1052
  "maxQuestions": {
1053
1053
  "type": "integer",
@@ -1215,7 +1215,7 @@
1215
1215
  "minimum": 30,
1216
1216
  "maximum": 86400,
1217
1217
  "default": 300,
1218
- "description": "Polling interval for --watch loop. Clamped to ≥30s to stay polite with GitHub rate limits."
1218
+ "description": "Polling interval for --watch loop. Clamped to \u226530s to stay polite with GitHub rate limits."
1219
1219
  },
1220
1220
  "labelFilter": {
1221
1221
  "type": "string",
@@ -1291,7 +1291,7 @@
1291
1291
  "devCritic": {
1292
1292
  "type": "object",
1293
1293
  "additionalProperties": false,
1294
- "description": "v8.6+ - Phase 3.5 evaluator-optimizer. After the Dev generator's last edit and BEFORE Phase 4 reviewers, dispatch agents/dev-critic.md (Sonnet by default) to run deterministic gates (build/lint/test/secrets) + the platform checklist (rules/*.md). Max 2 critic iterations, then escalate. Catches gate failures and checklist violations that would otherwise burn 2-3 Phase 4 reviewer calls + Opus triage. Off by default - introduces 1× Sonnet call per Dev iteration; flip on for feature work, security-touching paths, or multi-file refactors. Source: Anthropic 'Building Effective Agents' (Dec 2024) evaluator-optimizer pattern.",
1294
+ "description": "v8.6+ - Phase 3.5 evaluator-optimizer. After the Dev generator's last edit and BEFORE Phase 4 reviewers, dispatch agents/dev-critic.md (Sonnet by default) to run deterministic gates (build/lint/test/secrets) + the platform checklist (rules/*.md). Max 2 critic iterations, then escalate. Catches gate failures and checklist violations that would otherwise burn 2-3 Phase 4 reviewer calls + Opus triage. Off by default - introduces 1\u00d7 Sonnet call per Dev iteration; flip on for feature work, security-touching paths, or multi-file refactors. Source: Anthropic 'Building Effective Agents' (Dec 2024) evaluator-optimizer pattern.",
1295
1295
  "properties": {
1296
1296
  "enabled": {
1297
1297
  "type": "boolean",
@@ -1464,6 +1464,61 @@
1464
1464
  "tailLines": 20
1465
1465
  }
1466
1466
  },
1467
+ "bulkRead": {
1468
+ "type": "object",
1469
+ "additionalProperties": false,
1470
+ "description": "v16.21+ - Route a whole-file read that is too big to be worth the caller's rung to a cheap worker instead, and put its line-numbered summary in context rather than the file. The gate is pipeline/scripts/check-read-size.sh (PreToolUse, installed from install/templates/claude-hooks.json); the worker is pipeline/scripts/bulk-read.sh over the bulk-reader persona. Companion to contextOffload, which does the same for tool OUTPUT rather than for reads.",
1471
+ "properties": {
1472
+ "mode": {
1473
+ "type": "string",
1474
+ "enum": ["off", "observe", "enforce"],
1475
+ "default": "off",
1476
+ "description": "off = the hook is a no-op. observe = decide and log every inspected read, block nothing; this is the BASELINE MEASUREMENT, and running it before enforce is what makes a later saving claim checkable. enforce = block a whole-file read over minLines and tell the model to delegate it."
1477
+ },
1478
+ "minLines": {
1479
+ "type": "integer",
1480
+ "minimum": 1,
1481
+ "maximum": 100000,
1482
+ "default": 350,
1483
+ "description": "Files shorter than this are never inspected. A delegated read costs a round trip measured in tens of seconds, so a small file is cheaper read directly - the threshold is where that trade turns over."
1484
+ },
1485
+ "exemptPhases": {
1486
+ "type": "array",
1487
+ "items": {
1488
+ "type": "string"
1489
+ },
1490
+ "default": ["3"],
1491
+ "description": "Phases the gate never blocks in. Phase 3 is the shipped default and removing it breaks development: Claude Code's Edit requires the same file to have been Read first, so a gate that blocks reads while code is being changed blocks the change. The gate is for phases that read to UNDERSTAND."
1492
+ },
1493
+ "model": {
1494
+ "type": "string",
1495
+ "default": "haiku",
1496
+ "description": "Rung the delegated read runs on. The saving IS the gap between this rung and the caller's, so a worker raised to the caller's own rung saves nothing."
1497
+ },
1498
+ "timeoutSeconds": {
1499
+ "type": "integer",
1500
+ "minimum": 5,
1501
+ "maximum": 600,
1502
+ "default": 60,
1503
+ "description": "Wall-clock budget for one delegated read. On expiry the worker degrades and the caller is told to do a bounded read instead - never left waiting, and never handed an invented summary."
1504
+ },
1505
+ "maxBytes": {
1506
+ "type": "integer",
1507
+ "minimum": 1024,
1508
+ "maximum": 20971520,
1509
+ "default": 1048576,
1510
+ "description": "Ceiling past which a delegated read degrades instead of running. Delegation is not free: at some size the worker's own input bill approaches the read it replaced, and a file large enough to strain its window comes back truncated - a partial summary presented as a whole one is the failure mode this feature must never produce. Over the ceiling the caller is told to narrow first (grep, then a bounded read) rather than handed an expensive round trip to a worse answer."
1511
+ }
1512
+ },
1513
+ "default": {
1514
+ "mode": "off",
1515
+ "minLines": 350,
1516
+ "exemptPhases": ["3"],
1517
+ "model": "haiku",
1518
+ "timeoutSeconds": 60,
1519
+ "maxBytes": 1048576
1520
+ }
1521
+ },
1467
1522
  "testGap": {
1468
1523
  "type": "object",
1469
1524
  "additionalProperties": false,
@@ -1506,7 +1561,7 @@
1506
1561
  "autopilotSafetyGate": {
1507
1562
  "type": "boolean",
1508
1563
  "default": true,
1509
- "description": "v7.0.0+ - Phase 2 autopilot safety classifier. Before autopilot mode consumes the user's approval skip, run `classify-plan-safety.mjs` over the approved plan. If the heuristic score 50 (e.g. >15 files touched, or security-path touch, or delete-without-test, or schema migration) inject a ONE-TIME pause asking for explicit manual approval - even in autopilot. Default ON because the risk of skipping this gate is asymmetric: a pause on a high-blast-radius plan costs seconds; a silent auto-merge of a bad one costs hours of rollback. Flip to `false` only for tightly-scoped autopilot workflows (e.g. batch figma component iteration) where the task class is known-safe."
1564
+ "description": "v7.0.0+ - Phase 2 autopilot safety classifier. Before autopilot mode consumes the user's approval skip, run `classify-plan-safety.mjs` over the approved plan. If the heuristic score \u2265 50 (e.g. >15 files touched, or security-path touch, or delete-without-test, or schema migration) inject a ONE-TIME pause asking for explicit manual approval - even in autopilot. Default ON because the risk of skipping this gate is asymmetric: a pause on a high-blast-radius plan costs seconds; a silent auto-merge of a bad one costs hours of rollback. Flip to `false` only for tightly-scoped autopilot workflows (e.g. batch figma component iteration) where the task class is known-safe."
1510
1565
  },
1511
1566
  "dynamicSkillLoading": {
1512
1567
  "type": "boolean",
@@ -0,0 +1,277 @@
1
+ #!/usr/bin/env bash
2
+ # bulk-read.sh - read a large file on the cheap rung and return a summary.
3
+ #
4
+ # The worker behind `check-read-size.sh`. The gate says a file is too big to be
5
+ # worth reading whole at this phase's rung; this is where that read goes instead.
6
+ # The full text reaches a haiku-rung worker, the worker returns a structured
7
+ # summary WITH LINE NUMBERS, and only the summary enters the caller's context.
8
+ #
9
+ # Line numbers are the whole design, not a nicety. A summary without them is a
10
+ # dead end: the caller cannot edit from it, cannot verify it, and ends up reading
11
+ # the file anyway - having now paid twice. With them, the intended next step is a
12
+ # bounded `Read(file, offset:, limit:)` around the region that matters, which is
13
+ # both cheap and exact, and which the gate lets through.
14
+ #
15
+ # Usage:
16
+ # bulk-read.sh --file <path> [--question "<what you need>"] [--phase N]
17
+ # [--model <rung>] [--timeout <seconds>] [--json]
18
+ #
19
+ # Output: a human-readable summary on stdout, plus the ref id that buys the full
20
+ # text back. `--json` prints the worker's raw object instead, for a caller that
21
+ # wants to parse it.
22
+ #
23
+ # Degradation is explicit and always safe. No CLI, no auth, a timeout, or a
24
+ # non-JSON answer -> this prints WHY and tells the caller to fall back to a
25
+ # bounded read. It never invents a summary, and it never silently succeeds: a
26
+ # fabricated summary of a file nobody read is the one outcome worse than paying
27
+ # full price for the file.
28
+ #
29
+ # Exit codes: 0 summary produced · 3 degraded (caller should read it directly)
30
+ # 1 usage error.
31
+
32
+ set -uo pipefail
33
+
34
+ FILE=""
35
+ QUESTION="Summarize this file so a reader can decide which regions to open."
36
+ PHASE="${MULTI_AGENT_PHASE:-0}"
37
+ MODEL=""
38
+ TIMEOUT=""
39
+ AS_JSON=0
40
+
41
+ while [ $# -gt 0 ]; do
42
+ case "$1" in
43
+ --file) FILE="${2:?--file needs a value}"; shift 2 ;;
44
+ --question) QUESTION="${2:?--question needs a value}"; shift 2 ;;
45
+ --phase) PHASE="${2:?--phase needs a value}"; shift 2 ;;
46
+ --model) MODEL="${2:?--model needs a value}"; shift 2 ;;
47
+ --timeout) TIMEOUT="${2:?--timeout needs a value}"; shift 2 ;;
48
+ --json) AS_JSON=1; shift ;;
49
+ -h|--help) sed -n '2,30p' "$0"; exit 0 ;;
50
+ *) echo "bulk-read: unknown argument '$1'" >&2; exit 1 ;;
51
+ esac
52
+ done
53
+
54
+ [ -n "$FILE" ] || { echo "bulk-read: --file is required" >&2; exit 1; }
55
+ [ -f "$FILE" ] || { echo "bulk-read: not a readable file: $FILE" >&2; exit 1; }
56
+
57
+ HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
58
+
59
+ degrade() {
60
+ echo "bulk-read: DEGRADED - $1" >&2
61
+ echo "Read the region you need directly instead: Read('$FILE', offset: <n>, limit: <n>)." >&2
62
+ [ -x "$HERE/log-metric.sh" ] && \
63
+ "$HERE/log-metric.sh" "${MULTI_AGENT_TASK_ID:-unknown}" "$PHASE" bulk_read.degraded \
64
+ reason="$1" >/dev/null 2>&1
65
+ exit 3
66
+ }
67
+
68
+ # Resolve the three prefs this script honours - `bulkRead.model` (which rung the
69
+ # delegated read runs on), `bulkRead.timeoutSeconds` (how long it may take) and
70
+ # `bulkRead.maxBytes` (the ceiling past which delegation stops being a saving).
71
+ # Same search order and the same positive-integer rule as offload-ref.sh: a zero
72
+ # or a string takes the default rather than silently disabling the control.
73
+ PREF_MODEL=""
74
+ PREF_TIMEOUT=""
75
+ PREF_MAX_BYTES=""
76
+ if command -v jq >/dev/null 2>&1; then
77
+ for prefs in \
78
+ "$HOME/.claude/multi-agent-preferences.json" \
79
+ "$HOME/.config/multi-agent-pipeline/multi-agent-preferences.json" \
80
+ "$HOME/.claude/preferences.json" \
81
+ "$HOME/.config/multi-agent-pipeline/preferences.json"
82
+ do
83
+ [ -f "$prefs" ] || continue
84
+ values=$(jq -r '.global.bulkRead // {} | [(.model // ""), (.timeoutSeconds // ""), (.maxBytes // "")] | @tsv' \
85
+ "$prefs" 2>/dev/null) || true
86
+ PREF_MODEL=$(printf '%s' "$values" | cut -f1)
87
+ PREF_TIMEOUT=$(printf '%s' "$values" | cut -f2)
88
+ PREF_MAX_BYTES=$(printf '%s' "$values" | cut -f3)
89
+ break
90
+ done
91
+ fi
92
+ [ -n "$MODEL" ] || MODEL="${PREF_MODEL:-haiku}"
93
+ case "$TIMEOUT" in "" ) TIMEOUT="$PREF_TIMEOUT" ;; esac
94
+ case "$TIMEOUT" in ""|*[!0-9]*|0) TIMEOUT=60 ;; esac
95
+
96
+ command -v claude >/dev/null 2>&1 || degrade "the claude CLI is not on PATH"
97
+
98
+ LINES=$(wc -l < "$FILE" | tr -d ' ')
99
+ BYTES=$(wc -c < "$FILE" | tr -d ' ')
100
+
101
+ # A ceiling, because delegation is not free either. Past some size the worker's
102
+ # own input bill approaches the read it replaced, and a file large enough to
103
+ # strain the worker's window would come back truncated - a partial summary
104
+ # presented as a whole one is exactly what this must never produce. Degrading
105
+ # here hands the caller a cheaper move (grep for the symbol, then read around it)
106
+ # instead of an expensive round trip to a worse answer.
107
+ case "$PREF_MAX_BYTES" in ""|*[!0-9]*|0) MAX_BYTES=1048576 ;; *) MAX_BYTES="$PREF_MAX_BYTES" ;; esac
108
+ if [ "$BYTES" -gt "$MAX_BYTES" ]; then
109
+ echo "bulk-read: DEGRADED - ${FILE} is ${BYTES} bytes, past the ${MAX_BYTES}-byte ceiling" >&2
110
+ echo "At this size the delegated read costs about what reading it would, and risks a truncated" >&2
111
+ echo "summary presented as a whole one. Narrow it first: grep for the symbol you need, then" >&2
112
+ echo "Read('${FILE}', offset: <n>, limit: <n>) around the hit." >&2
113
+ [ -x "$HERE/log-metric.sh" ] && \
114
+ "$HERE/log-metric.sh" "${MULTI_AGENT_TASK_ID:-unknown}" "$PHASE" bulk_read.degraded \
115
+ reason=over-ceiling file_bytes="$BYTES" >/dev/null 2>&1
116
+ exit 3
117
+ fi
118
+
119
+ # Measured and accepted, so park the full text before asking anything: the
120
+ # pointer in the summary is then a promise that is already kept. The caller buys the rest back by reading this file - the same
121
+ # contract offload-ref.sh makes for build logs, and the same directory.
122
+ ROOT=$(git rev-parse --show-toplevel 2>/dev/null || true)
123
+ [ -n "$ROOT" ] || ROOT="$PWD"
124
+ REFS_DIR="$ROOT/.multi-agent/refs"
125
+ NODE_ID=""
126
+ if mkdir -p "$REFS_DIR" 2>/dev/null; then
127
+ GITIGNORE="$ROOT/.multi-agent/.gitignore"
128
+ if [ ! -f "$GITIGNORE" ]; then
129
+ printf '# Local run artefacts - never commit.\nmemory/\nrefs/\n' > "$GITIGNORE"
130
+ elif ! grep -q '^refs/$' "$GITIGNORE" 2>/dev/null; then
131
+ printf 'refs/\n' >> "$GITIGNORE"
132
+ fi
133
+ if command -v shasum >/dev/null 2>&1; then
134
+ DIGEST=$(shasum -a 256 "$FILE" | awk '{print substr($1,1,8)}')
135
+ elif command -v sha256sum >/dev/null 2>&1; then
136
+ DIGEST=$(sha256sum "$FILE" | awk '{print substr($1,1,8)}')
137
+ else
138
+ DIGEST=$(cksum < "$FILE" | awk '{print $1}')
139
+ fi
140
+ SLUG=$(basename "$FILE" | tr '[:upper:]' '[:lower:]' | tr -c 'a-z0-9' '-' | sed 's/-\{1,\}/-/g; s/^-//; s/-$//')
141
+ NODE_ID="p${PHASE}-read-${SLUG:-file}-${DIGEST}"
142
+ cp "$FILE" "$REFS_DIR/${NODE_ID}.txt" 2>/dev/null || NODE_ID=""
143
+ fi
144
+
145
+ PROMPT_FILE="$(mktemp -t bulk-read.XXXXXX)"
146
+ OUT_FILE="$(mktemp -t bulk-read-out.XXXXXX)"
147
+ trap 'rm -f "$PROMPT_FILE" "$OUT_FILE"' EXIT
148
+
149
+ # The worker gets numbered lines because every claim it makes has to be
150
+ # addressable afterwards. `nl -ba` numbers blank lines too, so the numbers match
151
+ # the file's own and an offset computed from them is correct.
152
+ # The worker's contract is the bulk-reader PERSONA, loaded from disk rather than
153
+ # restated here. Two copies of the same prompt is the drift this repo keeps
154
+ # catching elsewhere: the copy that gets edited is never the copy that runs.
155
+ PERSONA=""
156
+ for candidate in \
157
+ "$HOME/.claude/agents/bulk-reader.md" \
158
+ "$HERE/../agents/bulk-reader.md"
159
+ do
160
+ [ -f "$candidate" ] && { PERSONA="$candidate"; break; }
161
+ done
162
+ [ -n "$PERSONA" ] || degrade "the bulk-reader persona is not installed"
163
+
164
+ # The frontmatter is dispatch metadata for the Agent tool, not instruction text;
165
+ # strip it and send the body.
166
+ {
167
+ awk 'BEGIN{fm=0} /^---$/{fm++; next} fm>=2{print}' "$PERSONA"
168
+ printf '\n<question>\n%s\n</question>\n\n' "$QUESTION"
169
+ printf '<file path="%s" lines="%s">\n' "$FILE" "$LINES"
170
+ nl -ba "$FILE"
171
+ printf '\n</file>\n'
172
+ } > "$PROMPT_FILE"
173
+
174
+ START=$(date +%s)
175
+ # `claude -p` reads the prompt from stdin; the model flag names the rung. A
176
+ # non-zero exit, an empty answer and a non-JSON answer are all degradations, and
177
+ # each one names itself rather than falling through to a generic failure.
178
+ # macOS ships no `timeout`; coreutils installs it as `gtimeout`. Naming only the
179
+ # GNU spelling would leave the budget silently unenforced on the platform most of
180
+ # these runs happen on - a pref that does nothing, which is the class
181
+ # smoke-prefs-consumed exists to catch.
182
+ TIMEOUT_BIN=""
183
+ for candidate in timeout gtimeout; do
184
+ command -v "$candidate" >/dev/null 2>&1 && { TIMEOUT_BIN="$candidate"; break; }
185
+ done
186
+ if [ -n "$TIMEOUT_BIN" ]; then
187
+ "$TIMEOUT_BIN" "$TIMEOUT" claude -p --model "$MODEL" < "$PROMPT_FILE" > "$OUT_FILE" 2>/dev/null
188
+ STATUS=$?
189
+ else
190
+ # No timeout binary: the budget cannot be enforced, so say so rather than
191
+ # letting the caller believe timeoutSeconds is holding.
192
+ echo "bulk-read: no timeout binary found; bulkRead.timeoutSeconds is not enforced on this host" >&2
193
+ claude -p --model "$MODEL" < "$PROMPT_FILE" > "$OUT_FILE" 2>/dev/null
194
+ STATUS=$?
195
+ fi
196
+ ELAPSED=$(( $(date +%s) - START ))
197
+
198
+ [ "$STATUS" -eq 124 ] && degrade "the worker did not answer within ${TIMEOUT}s"
199
+ [ "$STATUS" -ne 0 ] && degrade "the worker exited $STATUS"
200
+ [ -s "$OUT_FILE" ] || degrade "the worker returned nothing"
201
+
202
+ RENDER=$(FILE="$FILE" LINES="$LINES" BYTES="$BYTES" NODE_ID="$NODE_ID" \
203
+ MODEL="$MODEL" ELAPSED="$ELAPSED" AS_JSON="$AS_JSON" \
204
+ python3 - "$OUT_FILE" <<'PYEOF'
205
+ import json, os, re, sys
206
+
207
+ raw = open(sys.argv[1], encoding="utf-8", errors="replace").read()
208
+ try:
209
+ data = json.loads(raw)
210
+ except Exception:
211
+ # A worker that wrapped its object in prose or a fence is still usable; a
212
+ # worker that answered in prose is not, and falls through to the degrade path.
213
+ m = re.search(r"\{[\s\S]*\}", raw)
214
+ if not m:
215
+ sys.exit(7)
216
+ try:
217
+ data = json.loads(m.group(0))
218
+ except Exception:
219
+ sys.exit(7)
220
+
221
+ if not isinstance(data, dict) or "summary" not in data:
222
+ sys.exit(7)
223
+
224
+ if os.environ.get("AS_JSON") == "1":
225
+ print(json.dumps(data, indent=2))
226
+ sys.exit(0)
227
+
228
+ out = []
229
+ out.append("bulk-read: %s (%s lines, %s bytes) via %s in %ss"
230
+ % (os.environ["FILE"], os.environ["LINES"], os.environ["BYTES"],
231
+ os.environ["MODEL"], os.environ["ELAPSED"]))
232
+ answer = (data.get("answer") or "").strip()
233
+ if answer:
234
+ out.append("")
235
+ out.append("ANSWER: " + answer)
236
+ out.append("")
237
+ out.append((data.get("summary") or "").strip())
238
+
239
+ symbols = [s for s in (data.get("symbols") or []) if isinstance(s, dict)]
240
+ if symbols:
241
+ out.append("")
242
+ out.append("Symbols:")
243
+ for s in symbols[:40]:
244
+ out.append(" %-6s %s (line %s)" % (s.get("kind", "?"), s.get("name", "?"), s.get("line", "?")))
245
+
246
+ regions = [r for r in (data.get("regions") or []) if isinstance(r, dict)]
247
+ if regions:
248
+ out.append("")
249
+ out.append("Read next (bounded, and this gate allows it):")
250
+ for r in regions[:8]:
251
+ start, end = r.get("start"), r.get("end")
252
+ span = ""
253
+ try:
254
+ span = " Read(offset: %d, limit: %d)" % (int(start), int(end) - int(start) + 1)
255
+ except Exception:
256
+ pass
257
+ out.append(" %s-%s %s%s" % (start, end, r.get("why", ""), span))
258
+
259
+ node = os.environ.get("NODE_ID") or ""
260
+ if node:
261
+ out.append("")
262
+ out.append("Full text: .multi-agent/refs/%s.txt [[ref:%s]]" % (node, node))
263
+ if data.get("truncated"):
264
+ out.append("")
265
+ out.append("NOTE: the worker reports it did not see the whole file.")
266
+ print("\n".join(out))
267
+ PYEOF
268
+ ) || degrade "the worker's answer was not the requested JSON object"
269
+
270
+ printf '%s\n' "$RENDER"
271
+
272
+ [ -x "$HERE/log-metric.sh" ] && \
273
+ "$HERE/log-metric.sh" "${MULTI_AGENT_TASK_ID:-unknown}" "$PHASE" bulk_read.delegated \
274
+ file_lines="$LINES" file_bytes="$BYTES" model="$MODEL" duration_ms="$(( ELAPSED * 1000 ))" \
275
+ >/dev/null 2>&1
276
+
277
+ exit 0
@@ -0,0 +1,335 @@
1
+ #!/usr/bin/env python3
2
+ """check-read-size.py - decision core for the read-size PreToolUse gate.
3
+
4
+ Reads a Claude Code hook payload on stdin and prints ONE line:
5
+
6
+ PASS
7
+ OBSERVE\t<path>\t<lines>\t<why>
8
+ BLOCK\t<path>\t<lines>\t<why>
9
+
10
+ It decides only. The wrapper (check-read-size.sh) owns telemetry, the message
11
+ the model sees, and the exit code. Splitting them is what lets the smoke gate
12
+ drive the decision without a hook harness, and it keeps the blocking path in
13
+ shell where the other two gates already live.
14
+
15
+ Why a gate at all: a phase that reads six 900-line files pays for 5,400 lines of
16
+ Swift at the phase's own rung, and the useful content is a handful of symbols.
17
+ `offload-ref.sh` already caught the other half of that bill - the build log - but
18
+ nothing looked at reads.
19
+
20
+ Why it must not simply block: in Claude Code, `Edit` requires that the SAME file
21
+ was read first. A gate that blocks reads during development stops the pipeline
22
+ from editing anything. So the development phase is exempt by default, and so is
23
+ any file already in the run's edit set. The gate exists for the phases that read
24
+ to UNDERSTAND (analysis, review), not for the one that reads to change.
25
+
26
+ Modes (prefs `global.bulkRead.mode`):
27
+ off nothing is inspected (default)
28
+ observe decide, log, never block - the baseline measurement
29
+ enforce block a read over the threshold
30
+
31
+ Fail-open everywhere: any error prints PASS. A gate that guesses wrong must cost
32
+ a log line, never a tool call.
33
+ """
34
+
35
+ import json
36
+ import os
37
+ import re
38
+ import shlex
39
+ import sys
40
+
41
+ # Shipped defaults for the two prefs read below: `bulkRead.minLines` and
42
+ # `bulkRead.exemptPhases`. A pref that resolves to anything else - absent, zero,
43
+ # a string - takes these rather than silently turning the gate off.
44
+ DEFAULT_MIN_LINES = 350
45
+ DEFAULT_EXEMPT_PHASES = ["3"]
46
+
47
+ PREF_PATHS = [
48
+ "~/.claude/multi-agent-preferences.json",
49
+ "~/.config/multi-agent-pipeline/multi-agent-preferences.json",
50
+ "~/.claude/preferences.json",
51
+ "~/.config/multi-agent-pipeline/preferences.json",
52
+ ]
53
+
54
+ # Commands that read a whole file to stdout. `sed` is here for `sed -n '1,900p'`,
55
+ # which is the spelling an agent reaches for when Read is unavailable.
56
+ READ_COMMANDS = {"cat", "head", "tail", "sed", "bat", "less", "more"}
57
+
58
+ # `1,900p` / `1,$p` / `900p` - a sed print script with a line range.
59
+ SED_RANGE = re.compile(r"^'?(\d+)(?:,(\d+|\$))?p'?$")
60
+
61
+
62
+ def emit(verdict, path="", lines=0, why=""):
63
+ sys.stdout.write("%s\t%s\t%s\t%s\n" % (verdict, path, lines, why))
64
+ sys.exit(0)
65
+
66
+
67
+ def load_prefs():
68
+ for raw in PREF_PATHS:
69
+ p = os.path.expanduser(raw)
70
+ if not os.path.isfile(p):
71
+ continue
72
+ try:
73
+ with open(p, "r", encoding="utf-8") as fh:
74
+ return json.load(fh).get("global", {}).get("bulkRead", {}) or {}
75
+ except Exception:
76
+ return {}
77
+ return {}
78
+
79
+
80
+ def positive_int(value, default):
81
+ """A pref counts only as a positive integer; anything else takes the default.
82
+
83
+ Same rule as offload-ref.sh: a zero or a string must not silently disable
84
+ the threshold, because that reads as "the gate is off" when it is broken.
85
+ """
86
+ try:
87
+ n = int(value)
88
+ except (TypeError, ValueError):
89
+ return default
90
+ return n if n > 0 else default
91
+
92
+
93
+ def find_state():
94
+ """Walk up from cwd for the run's agent-state.json.
95
+
96
+ A hook runs in the worktree where the tool call happens, so the state file
97
+ is at cwd or above it. The env override exists for the smoke gate.
98
+ """
99
+ override = os.environ.get("MULTI_AGENT_STATE")
100
+ if override:
101
+ return override if os.path.isfile(override) else None
102
+ here = os.path.abspath(os.getcwd())
103
+ while True:
104
+ candidate = os.path.join(here, "agent-state.json")
105
+ if os.path.isfile(candidate):
106
+ return candidate
107
+ parent = os.path.dirname(here)
108
+ if parent == here:
109
+ return None
110
+ here = parent
111
+
112
+
113
+ def run_context():
114
+ """(phase, edit_set) for the current run, both best-effort.
115
+
116
+ Both come from agent-state.json's REAL shape, checked against
117
+ pipeline/schemas/agent-state.schema.json: the phase is the top-level
118
+ `currentPhase` integer, and the files a phase has touched are
119
+ `phases["<n>"].files[]` - the same list semantic revert uses. An earlier
120
+ draft of this read `run.phase` and `dev.editSet`, neither of which the
121
+ schema declares, so the exemption could never have engaged from state and
122
+ the gate would have blocked development. smoke-bulk-read.sh asserts the
123
+ field names against the schema now, so that cannot come back quietly.
124
+ """
125
+ phase = os.environ.get("MULTI_AGENT_PHASE", "")
126
+ edit_set = set()
127
+ path = find_state()
128
+ if not path:
129
+ return phase, edit_set
130
+ try:
131
+ with open(path, "r", encoding="utf-8") as fh:
132
+ state = json.load(fh)
133
+ except Exception:
134
+ return phase, edit_set
135
+ if not phase:
136
+ current = state.get("currentPhase")
137
+ phase = str(current) if isinstance(current, int) else ""
138
+ # Every phase's file list counts, not only the current one: a review that
139
+ # re-reads what development just wrote is reading the run's own work.
140
+ phases = state.get("phases")
141
+ if isinstance(phases, dict):
142
+ for entry in phases.values():
143
+ if not isinstance(entry, dict):
144
+ continue
145
+ for f in entry.get("files") or []:
146
+ if isinstance(f, str):
147
+ edit_set.add(os.path.basename(f))
148
+ return phase, edit_set
149
+
150
+
151
+ def count_lines(path):
152
+ """Line count, or -1 when the file cannot be measured.
153
+
154
+ Binary-safe and streamed: the gate must not itself load the payload it
155
+ exists to keep out of memory.
156
+ """
157
+ try:
158
+ total = 0
159
+ with open(path, "rb") as fh:
160
+ while True:
161
+ chunk = fh.read(1 << 20)
162
+ if not chunk:
163
+ break
164
+ total += chunk.count(b"\n")
165
+ return total
166
+ except Exception:
167
+ return -1
168
+
169
+
170
+ def as_count(token):
171
+ """A flag's numeric argument, or a large sentinel when it is not a number.
172
+
173
+ Not-a-number must read as UNBOUNDED, not as 1: treating an unparseable
174
+ count as a tiny one is how a gate lets through exactly the reads it exists
175
+ to catch.
176
+ """
177
+ try:
178
+ n = int(token)
179
+ except (TypeError, ValueError):
180
+ return 1 << 30
181
+ return abs(n)
182
+
183
+
184
+ def looks_like_sed_script(token):
185
+ return bool(SED_RANGE.match(token or ""))
186
+
187
+
188
+ def sed_span(script):
189
+ """Lines a `sed` print script covers, or a large sentinel when unclear."""
190
+ m = SED_RANGE.match(script or "")
191
+ if not m:
192
+ return 1 << 30
193
+ start, end = m.group(1), m.group(2)
194
+ try:
195
+ if end in (None, "", "$"):
196
+ return 1 << 30
197
+ return abs(int(end) - int(start)) + 1
198
+ except (TypeError, ValueError):
199
+ return 1 << 30
200
+
201
+
202
+ def read_target(payload):
203
+ """The file a tool call would read whole, or None.
204
+
205
+ Returns None for every call that is already bounded - a Read with a small
206
+ `limit`, a `head -n 50` - because those are not what this gate is for.
207
+ """
208
+ tool = payload.get("tool_name", "")
209
+ args = payload.get("tool_input", {}) or {}
210
+
211
+ if tool == "Read":
212
+ path = args.get("file_path")
213
+ if not path:
214
+ return None
215
+ limit = args.get("limit")
216
+ if isinstance(limit, int) and limit > 0:
217
+ return (path, limit)
218
+ return (path, None)
219
+
220
+ if tool != "Bash":
221
+ return None
222
+
223
+ command = args.get("command", "")
224
+ if not command:
225
+ return None
226
+ # A compound command is not decided here: splitting on shell operators
227
+ # correctly is the kind of parsing that goes wrong quietly, and a wrong
228
+ # block is worse than a missed one.
229
+ for operator in ("&&", "||", "|", ";", "$(", "`", ">", "<"):
230
+ if operator in command:
231
+ return None
232
+ try:
233
+ tokens = shlex.split(command)
234
+ except ValueError:
235
+ return None
236
+ if not tokens or os.path.basename(tokens[0]) not in READ_COMMANDS:
237
+ return None
238
+
239
+ tool = os.path.basename(tokens[0])
240
+ bound = None
241
+ operands = []
242
+ i = 1
243
+ while i < len(tokens):
244
+ token = tokens[i]
245
+
246
+ if not token.startswith("-") or token == "-":
247
+ if tool == "sed" and not operands and looks_like_sed_script(token):
248
+ # `sed '1,900p' file` - the script, not the file.
249
+ bound = max(bound or 0, sed_span(token))
250
+ else:
251
+ operands.append(token)
252
+ i += 1
253
+ continue
254
+
255
+ # A following-tail stream is not a bulk read at all; blocking it would
256
+ # be a false positive with no cheap alternative to offer.
257
+ if tool == "tail" and token in ("-f", "-F", "--follow"):
258
+ return None
259
+
260
+ # Only head/tail take a separate count operand. `cat -n` NUMBERS LINES -
261
+ # reading its next token as a count swallowed the filename, left no
262
+ # operand, and let `cat -n <bigfile>` through the gate entirely.
263
+ if tool in ("head", "tail") and token in ("-n", "-c"):
264
+ nxt = tokens[i + 1] if i + 1 < len(tokens) else ""
265
+ bound = max(bound or 0, as_count(nxt))
266
+ i += 2
267
+ continue
268
+
269
+ # `sed -n` suppresses default output; the range lives in the script,
270
+ # which arrives as the next operand and is handled above.
271
+ if tool == "head" or tool == "tail":
272
+ attached = token.lstrip("-")
273
+ if attached.isdigit():
274
+ bound = max(bound or 0, as_count(attached))
275
+ elif attached[:1] in ("n", "c") and attached[1:].isdigit():
276
+ bound = max(bound or 0, as_count(attached[1:]))
277
+ i += 1
278
+
279
+ if len(operands) != 1:
280
+ return None
281
+ return (operands[0], bound)
282
+
283
+
284
+ def main():
285
+ try:
286
+ payload = json.load(sys.stdin)
287
+ except Exception:
288
+ emit("PASS", why="unparseable payload")
289
+
290
+ # `bulkRead.mode` decides whether this gate does anything at all.
291
+ prefs = load_prefs()
292
+ mode = str(prefs.get("mode", "off") or "off").lower()
293
+ if mode not in ("observe", "enforce"):
294
+ emit("PASS", why="mode=%s" % mode)
295
+
296
+ target = read_target(payload)
297
+ if target is None:
298
+ emit("PASS", why="not a whole-file read")
299
+ path, bound = target
300
+
301
+ min_lines = positive_int(prefs.get("minLines"), DEFAULT_MIN_LINES)
302
+ if bound is not None and bound < min_lines:
303
+ emit("PASS", path, 0, "bounded read")
304
+
305
+ if not os.path.isfile(path):
306
+ emit("PASS", path, 0, "not a readable file")
307
+
308
+ lines = count_lines(path)
309
+ if lines < 0:
310
+ emit("PASS", path, 0, "unmeasurable")
311
+ if lines < min_lines:
312
+ emit("PASS", path, lines, "under threshold")
313
+
314
+ phase, edit_set = run_context()
315
+ exempt = prefs.get("exemptPhases")
316
+ if not isinstance(exempt, list) or not exempt:
317
+ exempt = DEFAULT_EXEMPT_PHASES
318
+ exempt = [str(x) for x in exempt]
319
+
320
+ # The two exemptions that keep Edit working. Both are stated as PASS with a
321
+ # reason rather than skipped silently, so the telemetry shows how often the
322
+ # gate declined to act and why.
323
+ if phase and phase in exempt:
324
+ emit("PASS", path, lines, "phase %s exempt" % phase)
325
+ if os.path.basename(path) in edit_set:
326
+ emit("PASS", path, lines, "in edit set")
327
+
328
+ emit("OBSERVE" if mode == "observe" else "BLOCK", path, lines, "over threshold")
329
+
330
+
331
+ if __name__ == "__main__":
332
+ try:
333
+ main()
334
+ except Exception:
335
+ emit("PASS", why="internal error")
@@ -0,0 +1,86 @@
1
+ #!/usr/bin/env bash
2
+ # check-read-size.sh - PreToolUse read gate for the multi-agent pipeline.
3
+ #
4
+ # The third deterministic hook, and the first on the READ side. The other two
5
+ # (pre-commit-check, agent-guard) inspect what the run writes; this one inspects
6
+ # what it pays to read.
7
+ #
8
+ # The bill it exists for: a phase that reads six 900-line files pays for 5,400
9
+ # lines at that phase's own rung, and the part it needed was a handful of
10
+ # symbols. `offload-ref.sh` already took the other half of that bill - the build
11
+ # log, the diff, the test output - by parking the payload and printing a pointer.
12
+ # Nothing looked at file reads, because nothing could: only a hook sees a tool
13
+ # call before it runs.
14
+ #
15
+ # Contract (Claude Code PreToolUse hook):
16
+ # - Reads the tool-call JSON on stdin.
17
+ # - Exit 2 -> BLOCK (reason on stderr, shown to the model).
18
+ # - Exit 0 -> allow.
19
+ #
20
+ # Modes - `prefs.global.bulkRead.mode`:
21
+ # off (default) nothing is inspected; the hook is a no-op.
22
+ # observe decide and LOG, never block. This is the measurement: run the
23
+ # pipeline normally and metrics.jsonl carries the read-size
24
+ # distribution, so the saving can be estimated before anything is
25
+ # built on top of it.
26
+ # enforce block a whole-file read over `minLines` and tell the model to
27
+ # delegate it to bulk-read.sh instead.
28
+ #
29
+ # Why observe is a real mode and not a debug flag: the honest order is measure,
30
+ # then route. A gate switched straight to enforce has no baseline to be
31
+ # compared against, and "we cut tokens" becomes a claim nothing can check.
32
+ #
33
+ # Why the development phase is exempt by default: in Claude Code, `Edit` requires
34
+ # the same file to have been read first. A gate that blocks reads in Phase 3
35
+ # blocks editing. The gate is for the phases that read to UNDERSTAND.
36
+ #
37
+ # Safety design (fail-OPEN): any internal error, missing python3, missing helper,
38
+ # empty payload -> exit 0. It never executes the inspected command, never writes
39
+ # to the inspected file, makes no network call, and prints no file content.
40
+
41
+ set -u
42
+
43
+ HERE="$(cd "$(dirname "$0")" 2>/dev/null && pwd || true)"
44
+ HELPER="$HERE/check-read-size.py"
45
+ [ -f "$HELPER" ] || exit 0
46
+ command -v python3 >/dev/null 2>&1 || exit 0
47
+
48
+ PAYLOAD="$(cat 2>/dev/null || true)"
49
+ [ -z "$PAYLOAD" ] && exit 0
50
+
51
+ DECISION="$(printf '%s' "$PAYLOAD" | python3 "$HELPER" 2>/dev/null || true)"
52
+ [ -z "$DECISION" ] && exit 0
53
+
54
+ VERDICT="$(printf '%s' "$DECISION" | cut -f1)"
55
+ FILE="$(printf '%s' "$DECISION" | cut -f2)"
56
+ LINES="$(printf '%s' "$DECISION" | cut -f3)"
57
+ WHY="$(printf '%s' "$DECISION" | cut -f4)"
58
+
59
+ # Telemetry is best-effort and never gates the decision. `log-metric.sh` already
60
+ # tolerates an unwritable directory, so the only guard needed here is the
61
+ # script's own presence.
62
+ note() {
63
+ [ -x "$HERE/log-metric.sh" ] || return 0
64
+ "$HERE/log-metric.sh" "${MULTI_AGENT_TASK_ID:-unknown}" "${MULTI_AGENT_PHASE:-0}" \
65
+ "read.$1" lines="${LINES:-0}" reason="${WHY:-}" >/dev/null 2>&1 || true
66
+ }
67
+
68
+ case "$VERDICT" in
69
+ OBSERVE)
70
+ note observed
71
+ exit 0 ;;
72
+ BLOCK)
73
+ note blocked
74
+ echo "BLOCKED by check-read-size: ${FILE} is ${LINES} lines." >&2
75
+ echo "Reading it whole costs this phase's rung for a file whose useful part is a few symbols." >&2
76
+ echo "" >&2
77
+ echo "Delegate it, then work from what comes back:" >&2
78
+ echo " bash \$HOME/.claude/scripts/bulk-read.sh --file '${FILE}' --question '<what you need from it>'" >&2
79
+ echo "" >&2
80
+ echo "The summary carries LINE NUMBERS, so a follow-up Read with an offset/limit around the" >&2
81
+ echo "region you actually need is the intended next step and passes this gate." >&2
82
+ echo "Full text stays on disk under .multi-agent/refs/ if the summary is not enough." >&2
83
+ exit 2 ;;
84
+ *)
85
+ exit 0 ;;
86
+ esac
@@ -194,6 +194,7 @@ scan_file() {
194
194
  if (s ~ /^[0-9]+$/) next; # pure digits
195
195
  if (s ~ /^[0-9a-f]+$/) next; # lowercase hex (git sha / md5 / sha-*)
196
196
  if (s ~ /^[0-9A-F]+$/) next; # uppercase hex
197
+ if (s ~ /^([A-Z][a-z]+)+[A-Z]*$/) next; # CamelCase words only (label key / type name), never a credential
197
198
  delete freq;
198
199
  for (i = 1; i <= n; i++) { c = substr(s, i, 1); freq[c]++ }
199
200
  H = 0;
@@ -67,6 +67,7 @@ const HOME = process.env.HOME || process.env.USERPROFILE;
67
67
  export const PIPELINE_AGENT_FILES = [
68
68
  "android-architect.md",
69
69
  "backend-architect.md",
70
+ "bulk-reader.md",
70
71
  "code-reviewer.md",
71
72
  "dev-critic.md",
72
73
  "explorer.md",
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: backlog
3
3
  version: 1.0.0
4
- description: "The engineering backlog registry for deferred-but-approved work in this codebase. Every job the user defers ('defer it', 'add it to the list', 'later', 'not now') lands HERE with its full spec, origin, and unblock condition, so deferral never means loss. Load when the user defers work, asks 'what's on the backlog / the list', says 'work the backlog' / 'pick up <item>', or when completing an item. The bundled BACKLOG.md is the registry; entries move deferred -> ready -> in-progress -> done and are never deleted."
4
+ description: "The engineering backlog registry for deferred-but-approved work in this codebase. Every job the user defers ('defer it', 'add it to the list', 'later', 'not now') lands HERE with its full spec, origin, and unblock condition, so deferral never means loss. Load when the user defers work, asks 'what's on the backlog / the list', says 'work the backlog' / 'pick up <item>', or when completing an item. The bundled BACKLOG.md is the registry; entries move deferred -> ready -> in-progress -> done, and the registry is re-audited against live code - a rotted entry deletes, a done record retires once what it taught is re-homed in the skill that owns it. This records WHAT IS OWED; git history records what happened."
5
5
  user-invocable: true
6
- argument-hint: [list | add | pick <id> | done <id>]
6
+ argument-hint: [list | add | pick <id> | done <id> | re-audit]
7
7
  allowed-tools: Read, Grep, Glob, Edit, Write
8
8
  ---
9
9
 
@@ -13,14 +13,17 @@ allowed-tools: Read, Grep, Glob, Edit, Write
13
13
  > **Bundled:** [BACKLOG.md](BACKLOG.md) - the registry. The heart of this skill.
14
14
  > **Iron rule:** moving a job onto this list is a MOVE, never a rewrite - the FULL spec
15
15
  > travels verbatim. A compressed one-liner is a loss; the point of the backlog is that
16
- > deferring costs nothing later.
16
+ > deferring costs nothing later. The registry stays honest in the other direction too:
17
+ > entries are re-audited against live code, and an entry whose named types or premises
18
+ > no longer exist is DELETED, never left readable as live.
17
19
 
18
20
  ## When to load
19
21
 
20
22
  - The user defers work: "defer it", "add it to the list", "later", "not now", "backlog this".
21
23
  - The user asks about it: "what's on the backlog", "what's on the list", "what's owed".
22
24
  - The user works it: "work the backlog", "pick up <id>", "do the next one".
23
- - An item completes: mark it done (never delete).
25
+ - An item completes: mark it done, and retire the record once what it taught has a home.
26
+ - A staleness signal arrives (a rename, a deleted subject, a doubted claim): re-audit.
24
27
 
25
28
  ## The registry contract
26
29
 
@@ -28,7 +31,7 @@ Every entry in `BACKLOG.md` carries, verbatim:
28
31
 
29
32
  - **id** - short stable slug (`kebab-case`), assigned on add.
30
33
  - **title** - one line.
31
- - **state** - `deferred` -> `ready` -> `in-progress` -> `done`. Entries only move forward; nothing is deleted.
34
+ - **state** - `deferred` -> `ready` -> `in-progress` -> `done`. Entries move forward; they leave the registry only by the two rules below (rot, or a re-homed done record), never by being quietly rewritten.
32
35
  - **origin** - where it came from (the task/PR/finding/conversation that spawned it).
33
36
  - **spec** - the FULL description, verbatim from when it was deferred. Not a summary.
34
37
  - **unblock** - the condition that makes it actionable (a dependency, a decision, a date). `deferred` items without an unblock condition are just `ready`.
@@ -42,7 +45,8 @@ Prefer an append-only markdown table or section list in `BACKLOG.md`; never over
42
45
  | `list` | Show entries grouped by state (ready first, then deferred, then in-progress; done collapsed). |
43
46
  | `add` | Capture the deferred job with its full spec + origin + unblock condition. Assign an id. |
44
47
  | `pick <id>` | Move `ready`/`deferred` -> `in-progress`; surface the full spec so work resumes with zero context loss. |
45
- | `done <id>` | Move -> `done`. Keep the entry (it becomes the record of what was owed and delivered). |
48
+ | `done <id>` | Move -> `done` with the evidence (PR/commit + date). The record RETIRES from the registry once what it taught is written into the skill or doc that owns it - git history is the archive of what was owed and delivered. A done entry kept past that point is a second place for the same truth to rot. |
49
+ | `re-audit` | Verify every entry's named types, paths and claims against LIVE code (binary-safe: `LC_ALL=C grep -a`, so a repo with binary blobs does not silently return nothing). A subject that no longer exists deletes its entry; a changed fact restates it. **Numbers move by measurement, never by edit** - a count adjusted to match an impression reads as evidence and is worse than no count. |
46
50
 
47
51
  ## Why keep it whole
48
52