@sema-agent/core 7.27.0 → 7.28.1

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.
Files changed (35) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/dist/agents/builtin-agents.d.ts +6 -15
  3. package/dist/agents/cascade.d.ts +29 -33
  4. package/dist/agents/observer.d.ts +18 -20
  5. package/dist/agents/subagent.d.ts +58 -70
  6. package/dist/agents/tool-filter.d.ts +10 -12
  7. package/dist/agents/verify.d.ts +44 -49
  8. package/dist/brain/input-too-long.d.ts +10 -12
  9. package/dist/brain/media-degrade.d.ts +4 -5
  10. package/dist/brain/reasoning.d.ts +10 -12
  11. package/dist/brain/repetition.d.ts +7 -9
  12. package/dist/brain/request-params.d.ts +20 -25
  13. package/dist/brain/route-adjudicator.d.ts +4 -4
  14. package/dist/brain/route-conformance.d.ts +2 -2
  15. package/dist/brain/status-sink.d.ts +12 -14
  16. package/dist/brain/terminal-cause.d.ts +12 -15
  17. package/dist/brain/timeout.d.ts +17 -19
  18. package/dist/core/memory-engine/consolidation.d.ts +11 -15
  19. package/dist/core/memory-engine/engine.d.ts +45 -54
  20. package/dist/core/memory-engine/tools.d.ts +17 -23
  21. package/dist/core/permission-rule-model.js +5 -3
  22. package/dist/shell/lex.d.ts +279 -0
  23. package/dist/shell/lex.js +1253 -0
  24. package/dist/tools/fs/bash-program-position.d.ts +2 -4
  25. package/dist/tools/fs/bash-program-position.js +2 -1
  26. package/dist/tools/fs/bash-readonly-classifier.d.ts +2 -2
  27. package/dist/tools/fs/bash-readonly-classifier.js +1 -1
  28. package/dist/tools/fs/shell-read-check.js +1 -1
  29. package/dist/tools/fs/shell-read-establish.js +11 -1
  30. package/dist/tools/fs/shell-read-plan.d.ts +7 -2
  31. package/dist/tools/fs/shell-read-plan.js +48 -10
  32. package/package.json +2 -2
  33. package/wire-changes/core@7.28.1.json +594 -0
  34. package/dist/tools/fs/bash-lexer.d.ts +0 -152
  35. package/dist/tools/fs/bash-lexer.js +0 -627
package/CHANGELOG.md CHANGED
@@ -1,5 +1,32 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.28.1 — 2026-09-22
4
+
5
+ > **7.28.0 was never published.** Its first `npm publish` was cut off mid-upload by a pipe on the publisher's side; the registry keeps a *staged* half-upload under that number and refuses every later publish of it (`E409 Cannot publish over previously staged version`), and `npm unpublish` does not clear a staged version. This release is the same tree under the next patch number — tag `v7.28.1`; no `v7.28.0` tag or package exists.
6
+
7
+ ### One shell lexer mints word FACTS; the thirteen ANSI-C letters fold; an unterminated quote is a parse failure (#994 S0; behaviour WIDENING ×1 and NARROWING ×1; +0 exports; @cli @test)
8
+
9
+ - **What changed.** The read faces' lexer moved from `src/tools/fs/bash-lexer.ts` to `src/shell/lex.ts` (a layer-0 module that imports nothing) and grew the structure every later slice reads: each word carries `pieces` (literal / expansion, each with its source span and the quote that holds it), `quotedSpans`, `expansions` (parameter, command, process, arithmetic, tilde, brace, glob), an `assignment` fact (`name`, `op`, `subscript`, `value`, `bare`, `form`, `settlesWholeName`), an `unresolved` reason from a closed set, and `atCommandPosition`. `raw` / `folded` / `wordsOf` / the connectors are unchanged views, pinned byte-identical to the previous lexer over a 91-entry corpus except the deltas below (`test/backlog994-s0-lexer-baseline.test.ts`). No reader migrates in this slice; the position reader, the classifier and the read station read the same two spellings they read before.
10
+ - **WIDENING (ANSI-C).** `$'…'` bodies carrying one of the thirteen single-letter escapes (`\a \b \e \E \f \n \r \t \v \\ \' \" \?`) now fold to the shell's byte instead of refusing the word. The table is admitted by a differential test against the real shells (`test/backlog994-s0-lexer-real-shells.test.ts`: every bash found on the machine, plus `sh`; a machine with no bash or no `sh` FAILS that test rather than skipping). Every other escape — numeric, `\x`, `\u`/`\U`, `\c` — stays `unresolved` (its bytes differ between bash 3.2 and 5.x, or NUL truncates the region), and the refusing sentence is unchanged. Who is hurt: nobody — the thirteen values are fixed on every target shell.
11
+ - **NARROWING (unterminated quotes).** A command with an unclosed `'`, `"` or `$'` is a parse failure in every mode, refused with a sentence, where the strict faces used to re-read it quote-blind (more words, more segments, each vetted). Bash runs nothing on such a command. Consumers: the classify face refuses it (`splitShellCompoundSegments` returns the sentence), the read station's plan is `unreadable` (a card on the bash seats, a refusal headless), and the refused-command whitelist no longer vouches for it. Who is hurt: a deployment whose approvals relied on a quote-blind reading of a malformed command auto-passing as read-only — that reading vouched for text the shell would reject.
12
+ - **Rule import: a persisted-rule body with an unbalanced `$'…'` is refused as `invalid.unbalanced_quotes`, where 7.27.0 refused it as `invalid.not_simple_command`** (`src/core/permission-rule-model.ts`; `RuleRejectCode` gains no member — the code moves within the closed set to the one its `'` / `"` siblings already use, so an import report groups the three spellings together). Consumer flip for a report keyed on the old code (measured on both trees: G5).
13
+ - **An unresolved word refuses ITS reading, not the whole text (merged-tree adversarial round).** The read plan (`planShellReads`) now lexes to facts and keeps every segment beside a word it cannot settle (a compound array value, a subscript, an off-table `$'…'` escape), carrying the refusal sentence as the plan's `unreadable` floor; the fold raises that floor to `unestablishable` and a deny hit anywhere in the text still outranks it (`deny > ask`). Collapsing to an empty plan had let `A=(1 2); cat <deny row>` land on an approvable ask where `cat <deny row>` alone is refused. A PARSE failure is unchanged (no words, bash runs nothing). Pin: `test/backlog994-s0-deny-outranks-unresolved.test.ts`. Glob and brace carving is one walk over the pieces instead of a rebuild per character (a word of 8000 `*` took 1.6 s, now 13 ms; `test/backlog994-s0-lexer-baseline.test.ts` holds the byte-identical reading).
14
+ - **A word the lexer could not settle hands the judge no literal, and a lost working directory keeps the deny floor (round 2).** Every candidate minted from a refused-view unresolved word — a read operand, a `cd` target — is `unresolvable`; an unprovable move loses the base as before, and a relative read after it is ALSO judged for deny rows only against the bases the text was certainly in when it lost track (`ReadCandidate.floorBases`; the declared cwd when nothing had moved — `cd sub && cd $'\\x2e' && cat <row under sub>` is refused through the `sub` floor) (`cd $'\x2e' && cat <deny row>` — bash: `cd .` — had moved the base to a fictitious `/work/\x2e` and walked past the deny; it is refused on both full-shell seats now, and the same text without a deny row stays a mandated ask). Unmatched `{` / `[` no longer rescan the suffix: brace pairs are matched in one forward walk and `]` positions in one backward walk (32 000 unmatched braces: 3.3 s → 37 ms).
15
+ - **Facts minted, verdicts unchanged (until S1).** A subscripted assignment at command position pairs its brackets across blanks and operators as bash does (`A[1 + 1]=v cat f` is ONE word); the position reader still reads the word as an unknown program (a card), and a subscript that bash reads as arithmetic across an operator, a quoted `]`, or a pairing the two bash versions disagree on (after a leading redirection) is `unresolved`. A compound value `NAME=(…)` at command position — or as an operand of `alias`/`declare`/`eval`/`export`/`let`/`local`/`readonly`/`typeset` — is one word marked `unresolved` rather than a `(` group. `$"…"` carries `unresolved: "locale_string"` while the refused-reading view still folds it (refusing it today would turn a deny hit into a card).
16
+ - **Dialect.** The lexer declares `SHELL_LEX_DIALECT = "bash"`; `bashOnlyConstructsOf(lex)` lists the constructs a POSIX `sh` reads differently, for the execution environment's `sh` fallback to degrade on (the reader that consumes it lands in S1).
17
+ - **Retired.** The "any backslash in `$'…'` is unreadable" special case (folded into `unresolved`), and the quote-blind re-run (`scan`'s `quotesLiteral`). `SHELL_CONTROL_PREFIXES` has one home (the lexer; the position reader re-exports it).
18
+
19
+ ### The verification debt: four binary pins have a real-green reading, the embedder pin is in the gate, and five comment ceilings come back down (#970; no behaviour change; +0 exports; @server @test)
20
+
21
+ - **Four pins that could only ever skip now have a measured green, in the lane that produces the ratchet's own report.** On the gate container (`scripts/remote-gate.sh`, node 24.21.0): ripgrep 13.0.0 — `fs-search-honesty` 156 passed / 0 skipped, including the three real-`rg` legs the baseline had been carrying; sharp 0.35.3 — the two live-sharp legs pass (their `sharp-absent` twin is the far half of a paired conditional and still skips, by design); a real Postgres with pgvector — `pg-live` 4 + `pg-stores` 1, all green, which is the first reading for the two things pg-mem cannot emulate (true `<=>` cosine ranking, real MVCC contention); real language servers over stdio — `lsp-live-servers` 6 passed / 0 failed with typescript-language-server 6.0.0 and pyright 1.1.414 installed into the lane (the five languages with no server installed skip, as the file's own gate intends).
22
+ - **The skip ratchet names its lane and loses five entries.** `test/skip-baseline.json` gained a `note` block (which machine this baseline is judged on, which binaries that lane carries, why an env-gated entry is a deliberate opt-in rather than lost coverage), and the rg ×3 / sharp ×2 entries are gone — they run in that lane, so a machine without those binaries now goes red, which is what a ratchet is for. The LSP and Postgres entries stay: they are opt-in on `LSP_LIVE` / `PG_LIVE` + `PG_URL`, and removing them would make the whole ratchet depend on those services being up. `verify-skip-baseline.mjs --update` / `--merge` used to rewrite the file as `{skipped}` alone, which would have deleted that note one run later; both legs now carry over every non-`skipped` key.
23
+ - **A required-green pin must be PRESENT and PASSED on a full run, not merely "not skipped".** `gate:skips` now passes `--full` to `scripts/verify-skip-baseline.mjs`; on a declared full run every `requiredGreen` entry has to appear in the report as `passed` — a file that was renamed, glob-missed or crashed at collection used to read as green through the skip-only check (main-session receipt counterexample). Scoped runs (no `--full`) keep absence as scope. The baseline `note` now states ONE lane: the gate container is the only lane the full suite runs in on the release path, and the list is re-minted from that lane's report at RC. Six pins spawn the gate on synthetic reports (`test/skip-gate-required-green.test.ts`).
24
+ - **The full-run check runs before the rewrite legs, and the live gate judges its registry on its own (merged-tree adversarial round).** `--full --update` / `--full --merge` refuse when a required pin is absent, skipped or failed (a rewrite must not bank the loss); `run-live-gate.mjs` fails by name when an ENABLED conditional file is missing from the tree and announces a disabled one whether or not the file exists (a renamed file used to vanish without a line). Pins: `test/skip-gate-required-green.test.ts` (+2), `test/live-gate-conditional-registry.test.ts`.
25
+ - **The live gate's verdict is per target (round 2).** `scripts/live-gate-verdict.mjs`: every selected file must be present in the JSON report with at least one passed case and nothing skipped or failed — a selected file vitest never collected used to pass on the aggregate counts. Pin: `test/live-gate-verdict.test.ts`.
26
+ - **The real-embedder pin enters the release gate by name.** `test/memory-embed-live.test.ts` → `test/live-memory-embed.test.ts`: `run-live-gate.mjs` selects the live surface by glob over `test/live-*.test.ts`, so the only real-embedder coverage in the tree had been structurally outside it. An embeddings endpoint is not the standing chat provider, so the file carries an admission condition rather than a flat exclusion — in the default surface when `EMB_LIVE_BASE_URL` is set (live surface 47 → 49 assertions), and when it is not, the gate prints the omission by name instead of silently globbing past it. **Measured while wiring it:** against an endpoint refusing every connection the SECOND pin passed green — its query shared the word `particles` with the note it expected to rank first, so the lexical path answered it alone. The query now shares no term with either body and the leg asserts two rows and a real cosine ordering (refused-endpoint reading: 1 failed / 1 passed before, 2 failed after).
27
+ - **Five comment-ratio ceilings come back off the 7.24.2 raises** (`src` 0.3003/264 → 0.3002/263, `src/examples` 0.1661/38 → 0.1653/37, `src/brain` 0.4227/227 → 0.4210/219, `src/core/memory-engine` 0.3672/954 → 0.3663/932, `src/agents` 0.4385/1056 → 0.4371/1038), by removing narrative rather than contract, and every one is banked in the commit that earned it. `docs/RELEASE-TIERS.md` gains the X-before fixed checklist (the full `gate-all --with-dist --rc` roster, the ratchet/bookkeeping pins, the silent-arm gate and the three failopen files) — gates whose only other chance to be red is after the tag.
28
+ - **Still owed, by their owners.** @server: the Pg/TiDB dialects across the 39-cell memory kit have no real-green reading yet; the lane and the DSN wiring above are the same ones that leg needs. @test: the two memory black-box fences pinned at `core@5.23.0` still exit 2 (SKIP), and the cross-session personal-recall e2e was last exercised on 2026-08-16.
29
+
3
30
  ## 7.27.0 — 2026-09-22
4
31
 
5
32
  ### Every JSONL read-back is form-judged, and every store that tolerates damage names the loss (#1013; behaviour NARROWING; +0 exports; @server @cli @client-core @test)
@@ -3,21 +3,12 @@ import type { AgentDefinition } from "../core/types.js";
3
3
  * F1 (design/135 批次3 / PARITY-SPOT-AGENT-TYPES-2026-07-10) — the CC 2.1.198 BUILT-IN `Explore` and
4
4
  * `Plan` agent types, ported to sema's AgentDefinition roster shape.
5
5
  *
6
- * 198 锚 (亲核 2026-07-10, drift 调研: 198→204→206 逐字零漂移):
7
- * - Explore definition `qme` — pretty.js:409186-409198 (`agentType:"Explore"`, whenToUse=nbm,
8
- * whenToUseLean=rbm, `disallowedTools:[is,NR,s0,xa,Ic,FR]` = Agent/Artifact/ExitPlanMode/Edit/Write/
9
- * NotebookEdit, `model:"inherit"`, `omitClaudeMd:!0`, getSystemPrompt=tbm).
10
- * - Explore whenToUse `nbm` / whenToUseLean `rbm` — pretty.js:409173-409174 (verbatim below).
11
- * - Explore system prompt `tbm()` — pretty.js:409122-409160 (verbatim below, unix+Glob/Grep arm).
12
- * - Plan definition `Xor` — pretty.js:486943-486953 (`agentType:"Plan"`, single whenToUse, same six
13
- * disallowedTools, `tools: qme.tools` = undefined, `model:"inherit"`, `omitClaudeMd:!0`).
14
- * - Plan system prompt `kBm()` — pretty.js:486874-486930 (verbatim below, unix arm).
15
- * - Registry `HCe` — pretty.js:487221-487240: Explore+Plan ship behind `iAt()` (487127-487131:
16
- * env kill-switch `CLAUDE_CODE_DISABLE_EXPLORE_PLAN_AGENTS` + `tengu_slate_ibis` gate DEFAULT ON)
17
- * ⇒ GA default-on in 198 (and byte-stable through 204/206 — CC-DRIFT-198-206-AGENT-TYPES 锚 1-7).
18
- * SDK entrypoints ship them too (only `claude-code-guide` is non-SDK); the SDK-form opt-out is
19
- * `CLAUDE_AGENT_SDK_DISABLE_BUILTIN_AGENTS` (487221) → sema's deployment-config analog is
20
- * `SubagentToolOptions.builtinAgents: false` (config-driven, not env-driven).
6
+ * Both definitions, their `whenToUse` lines and their system prompts are ported VERBATIM from the
7
+ * reference implementation's own (unix + Glob/Grep arm), including the six denied tools
8
+ * (Agent/Artifact/ExitPlanMode/Edit/Write/NotebookEdit) and `model:"inherit"`. Upstream they ship
9
+ * default-on behind an env kill-switch; sema's analog is the deployment-config
10
+ * `SubagentToolOptions.builtinAgents: false` rather than an env variable. Per-symbol source
11
+ * coordinates and the drift re-checks live in the parity register, not here.
21
12
  *
22
13
  * sema adaptations (deliberate, each recorded):
23
14
  * - De-branding (prompt-sovereignty / SUBAGENT_PROMPT precedent): CC's persona first sentences name the
@@ -161,48 +161,44 @@ export declare function runCascade(runner: RunnerSelfSeat, spec: TaskSpec, confi
161
161
  *
162
162
  * WHY THE SEAT EXISTS: `runCascade` returns a `TaskResult` and DRAINS each rung's event stream
163
163
  * internally, so the host never holds a `TaskStream` for any rung. Without this parameter a
164
- * deployment can observe nothing a rung did — tool beats, forwarded child progress, the resolved
165
- * workspace — only the per-rung {@link CascadeAttempt} audit rows and the final result. Wiring an
164
+ * deployment observes nothing a rung did — tool beats, forwarded child progress, the resolved
165
+ * workspace — only the per-rung {@link CascadeAttempt} audit rows and the final result. An
166
166
  * observation sink here (`onActivity`, `onForwardEvent`, `onWorkspaceResolved`) is the only way in.
167
167
  *
168
- * ATTRIBUTION: rungs run SEQUENTIALLY and each is dispatched with the SAME object, so one sink
169
- * receives the rungs' beats CONCATENATED in ladder order. `ToolActivity` carries no rung identity of
170
- * its own — the frames of two rungs running the same tool are indistinguishable in content, so the
171
- * boundary a host keys on is {@link CascadeConfig.onRung}, which fires only after its own rung's
172
- * beats have been delivered (the sole rung that ends without one is a durable pause, which returns
173
- * immediately). Two honesty notes: SUBAGENT events forwarded through `onForwardEvent` do carry their
174
- * own `sourceTaskId`, and a BACKGROUND child outlives the rung that spawned it — its frames can
175
- * arrive after that rung's `onRung`, i.e. inside the next rung's segment.
168
+ * ATTRIBUTION: rungs run SEQUENTIALLY with the SAME object, so one sink receives the rungs' beats
169
+ * CONCATENATED in ladder order. `ToolActivity` carries no rung identity — two rungs running the same
170
+ * tool are indistinguishable — so the boundary a host keys on is {@link CascadeConfig.onRung}, which
171
+ * fires only after its own rung's beats have been delivered (the sole rung ending without one is a
172
+ * durable pause, which returns immediately). Two honesty notes: SUBAGENT events forwarded through
173
+ * `onForwardEvent` carry their own `sourceTaskId`, and a BACKGROUND child outlives the rung that
174
+ * spawned it — its frames can arrive inside the next rung's segment.
176
175
  *
177
- * FAILURE-ISOLATED since #248, by the same primitive as {@link CascadeConfig.onRung}: engine-invoked
178
- * through a run-scoped safe notifier — a throwing `onActivity` is contained (first failure per site
179
- * disclosed via `console.warn`, later ones counted) and no longer faults the rung. Before #248 the
180
- * fault AMPLIFIED: the faulted rung was rejected by the gate, the cascade escalated, and a
181
- * non-idempotent tool the rung had ALREADY executed ran AGAIN on the next rung — the MEASURED
182
- * HAZARD pin that recorded that now pins containment.
176
+ * FAILURE-ISOLATED, by the same primitive as {@link CascadeConfig.onRung}: engine-invoked through a
177
+ * run-scoped safe notifier — a throwing `onActivity` is contained (first failure per site disclosed
178
+ * via `console.warn`, later ones counted) and never faults the rung. Uncontained, the fault
179
+ * AMPLIFIES: the faulted rung is rejected by the gate, the cascade escalates, and a non-idempotent
180
+ * tool the rung had ALREADY executed runs AGAIN on the next rung — the MEASURED HAZARD pin holds
181
+ * containment.
183
182
  *
184
183
  * FORWARDED WHOLE, NOT FILTERED: every field rides every rung, including the ref-valued ones
185
184
  * (`peerSelfRef`, `ownOrgAdmissionRef`) whose own contract is one ref per RUN — wiring those here
186
- * makes the ladder's rungs share one carrier. The seat is meant for observation sinks; anything else
187
- * is a choice the caller is making explicitly, not a default this layer applies.
185
+ * makes the ladder's rungs share one carrier. The seat is meant for observation sinks; anything
186
+ * else is a choice the caller is making explicitly, not a default this layer applies.
188
187
  *
189
188
  * `isolation: "worktree"` in particular rides through here, where the verification gate REFUSES it —
190
- * an asymmetry with a reason. That field mints a NEW workspace per RUN. The gate's runs are one
189
+ * an asymmetry with a reason. That field mints a NEW workspace per RUN: the gate's runs are one
191
190
  * artifact judged and repaired by several legs, so separate workspaces would have the verifier
192
- * grading files the implementation never wrote to; a ladder's rungs are INDEPENDENT COLD RE-RUNS of
193
- * the same task (this function's own contract), so one workspace per rung is the coherent reading —
194
- * each attempt gets its own tree. What a host must then know, stated without promises this function
195
- * cannot keep: the returned result is ONE rung's, its work lives in THAT rung's workspace, and no
196
- * field of the result names it — `onWorkspaceResolved` is the only place the path appears, once per
197
- * rung in ladder order. Whether the path is still readable afterwards is the FACTORY's contract, not
198
- * this function's: each rung's factory-minted env is torn down at the end of that rung's run, and
199
- * what teardown does to the workspace is the factory's business.
191
+ * grading files the implementation never wrote to, while a ladder's rungs are INDEPENDENT COLD
192
+ * RE-RUNS, so one workspace per rung is the coherent reading. What a host must then know: the
193
+ * returned result is ONE rung's, its work lives in THAT rung's workspace, and no field of the result
194
+ * names it — `onWorkspaceResolved` is the only place the path appears, once per rung in ladder
195
+ * order. Whether it is still readable afterwards is the FACTORY's contract: each rung's
196
+ * factory-minted env is torn down at the end of that rung's run.
200
197
  *
201
- * Absent ⇒ every rung is dispatched exactly as before (the call site passes `undefined`, which is
202
- * what an omitted optional argument already was) — with ONE design/380 O1② placement delta either
203
- * way: when neither `placementRoot` nor `rootSessionId` is present, rungs after the first are
204
- * dispatched with a fresh shallow copy carrying `placementRoot` = the first rung's resolved
205
- * sessionId (the ladder's placement fixed point); every other field still rides by the same
206
- * reference, and an internals object that already carries a fixed point is forwarded verbatim.
198
+ * Absent ⇒ every rung is dispatched with `undefined`, with ONE design/380 O1② placement delta either
199
+ * way: when neither `placementRoot` nor `rootSessionId` is present, rungs after the first get a
200
+ * fresh shallow copy carrying `placementRoot` = the first rung's resolved sessionId (the ladder's
201
+ * placement fixed point); every other field still rides by the same reference, and an internals
202
+ * object that already carries a fixed point is forwarded verbatim.
207
203
  */
208
204
  internals?: RunInternals): Promise<CascadeRunResult>;
@@ -1,16 +1,16 @@
1
1
  /**
2
2
  * Observer agents — CC 2.1.206 parity (逐字锚: docs/CC206-OBSERVER-ANCHORS-2026-07-11.md).
3
3
  *
4
- * A definition may declare `observer: "<agent name>"`: whenever that agent runs, the named agent
5
- * type is auto-spawned in the background, PAIRED with it. The observer receives read-only activity
6
- * digests of the observed run (assistant text / tool calls / tool results / turn boundaries) and
7
- * may push a report back through the {@link createObserverReportTool | ObserverReport} tool — the
8
- * expected steady state is silence. One-way by construction: the observed agent never replies, and
9
- * SendMessage is refused in BOTH directions (sender-is-observer and target-is-observer).
4
+ * A definition may declare `observer: "<agent name>"`: whenever that agent runs, the named agent type
5
+ * is auto-spawned in the background, PAIRED with it. The observer receives read-only activity digests
6
+ * of the observed run (assistant text / tool calls / tool results / turn boundaries) and may push a
7
+ * report back through the {@link createObserverReportTool | ObserverReport} tool — the expected steady
8
+ * state is silence. One-way by construction: the observed agent never replies, and SendMessage is
9
+ * refused in BOTH directions.
10
10
  *
11
- * This module is the anchor-faithful half (digest model / renderers / envelope / postamble /
12
- * framing prompt / declaration resolver / pairing state machine / ObserverReport tool). The spawn
13
- * wiring (event tap on the delegation lanes + auto-spawn + delivery) lives in `subagent.ts`.
11
+ * This module is the anchor-faithful half (digest model / renderers / envelope / postamble / framing
12
+ * prompt / declaration resolver / pairing state machine / ObserverReport tool); the spawn wiring
13
+ * (event tap on the delegation lanes + auto-spawn + delivery) lives in `subagent.ts`.
14
14
  *
15
15
  * CC anchor coordinates cited per symbol below refer to `claude-cli-2.1.206.pretty.js`.
16
16
  */
@@ -86,18 +86,16 @@ export declare function renderObserverDigestBatch(pairing: {
86
86
  * CC `OIu` (220 `vrd` @374408) — the observer's opening system framing (delivered as the first
87
87
  * prompt message).
88
88
  *
89
- * RB-306 ① (undeclared divergence, now declared — text unchanged): CC's report-target slot is an
90
- * IDENTITY — `reportTargetTaskId ?? "main"` (@374409), i.e. the observed run's task id, or the
91
- * literal "main" for the main conversation. sema passes the observed agent's NAME
92
- * (`childAgentName`, see subagent.ts's arm) because a delegated child's task id is minted inside
93
- * the delegation and carries no meaning for the observer's own reasoning, while the agent name is
94
- * exactly what the digests are tagged with (`<{name}-activity>`). Deliberate: the slot's job is to
95
- * tell the observer WHERE its report lands, and in sema the name is the addressable form. Note the
96
- * ``?? "main"`` fallback is byte-identical to CC on the no-label leg.
89
+ * DIVERGENCE (declared, text unchanged): CC's report-target slot is an IDENTITY —
90
+ * `reportTargetTaskId ?? "main"` (@374409). sema passes the observed agent's NAME (`childAgentName`,
91
+ * see subagent.ts's arm): a delegated child's task id is minted inside the delegation and means
92
+ * nothing to the observer, while the name is exactly what the digests are tagged with
93
+ * (`<{name}-activity>`) and the addressable form here. The ``?? "main"`` fallback is byte-identical
94
+ * on the no-label leg.
97
95
  *
98
- * Not implemented (declared gap, not a divergence in this function): CC's SECOND framing shape —
99
- * the coordinator/worker chain (`viaWorkerName`, @374419-374426), where an observer watches a
100
- * WORKER but reports to the COORDINATOR. sema has one pairing shape (observer ⇄ observed).
96
+ * Declared gap: CC's SECOND framing shape — the coordinator/worker chain (`viaWorkerName`,
97
+ * @374419-374426), where an observer watches a WORKER but reports to the COORDINATOR. sema has one
98
+ * pairing shape (observer ⇄ observed).
101
99
  */
102
100
  export declare function observerFramingPrompt(args: {
103
101
  observedEnvelopeName: string;
@@ -5,27 +5,25 @@ import { type ExecutionEnv } from "../internal/harness.js";
5
5
  import type { TaskNotificationPayload } from "../core/task-notification.js";
6
6
  import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js";
7
7
  /**
8
- * #505 residual — the two GRANULARITY facts a delegation's terminal `usage` was missing beside
9
- * tokens/turns/cost, closing a real reporting asymmetry rather than adding decoration: the workflow
10
- * lane's completion notification has published `tool_uses` (summed `stats.toolCalls`) and `duration_ms`
11
- * (wall clock) since it was written, and the Agent tool's own SYNC report publishes both in its
12
- * `<usage>` footer — only the delegation TERMINAL faces (the background/fork/revive notification and its
13
- * observer twin) reported neither, so the same child answered "how much work, how long" on one lane and
14
- * refused on the other.
8
+ * The two GRANULARITY facts a delegation's terminal `usage` carries beside tokens/turns/cost. The
9
+ * workflow lane's completion notification publishes `tool_uses` (summed `stats.toolCalls`) and
10
+ * `duration_ms` (wall clock), and the Agent tool's own SYNC report publishes both in its `<usage>`
11
+ * footer, so a delegation TERMINAL face (the background/fork/revive notification and its observer
12
+ * twin) that reported neither would have the same child answering "how much work, how long" on one
13
+ * lane and refusing on the other.
15
14
  *
16
15
  * PROVENANCE is the workflow lane's — `stats.toolCalls`, the RUN's own count — because that is the
17
16
  * quantity the other side of the comparison publishes, and because it is available on every terminal
18
17
  * lane, including the revive lane, which has no parent-side forwarding observer at all.
19
18
  *
20
- * WHAT THIS DOES NOT CLOSE, stated because the honest scope is narrower than "one name, one meaning":
21
- * the Agent tool's SYNC `<usage>` footer keeps its own `tool_uses`, counted by the parent-side
19
+ * WHAT THIS DOES NOT CLOSE, since the honest scope is narrower than "one name, one meaning": the Agent
20
+ * tool's SYNC `<usage>` footer keeps its own `tool_uses`, counted by the parent-side
22
21
  * `toolStatsCounter`, and the two numbers differ in BOTH directions for the same child — that counter
23
- * EXCLUDES the delegation tool's own calls and INCLUDES descendants' (CC's nested fold), while
24
- * `stats.toolCalls` counts the direct child's calls and counts its delegations among them. A child that
25
- * itself delegates therefore reports two different `tool_uses` depending on whether it was run
26
- * synchronously or in the background. That footer is a byte-pinned CC-anchored face; reconciling the
27
- * two is a separate ruling, and until it is made this comment is the place the divergence is written
28
- * down rather than a claim that it is gone.
22
+ * EXCLUDES the delegation tool's own calls and INCLUDES descendants' (the reference implementation's
23
+ * nested fold), while `stats.toolCalls` counts the direct child's calls and counts its delegations
24
+ * among them. A child that itself delegates therefore reports two different `tool_uses` depending on
25
+ * whether it ran synchronously or in the background. That footer is a byte-pinned anchored face;
26
+ * reconciling the two is a separate ruling, and this is where the divergence is written down.
29
27
  *
30
28
  * Knownness follows `delegatedCostField`'s rule exactly: `toolCalls` is optional on the stats face
31
29
  * ("undefined means the gateway reported no usage", and a checkpointed resume folds it per-leg), so the
@@ -39,11 +37,11 @@ import { RETAIN_DEFAULT_TTL_MS, RETAIN_DEFAULT_MAX } from "../config/defaults.js
39
37
  * word is minted on either side.
40
38
  *
41
39
  * Exported at MODULE level (not from the package index — the same posture as `completedAgentCard` and
42
- * `classifySubagentError`, and for the same reason): the ABSENT arm is the point of the knownness rule
43
- * and it is not reachable through a real child. This engine's runner initializes `stats.toolCalls` to a
44
- * number on every path, so only a `TaskResult` from elsewhere — a foreign runner, an older checkpoint —
45
- * can carry the optional-and-missing shape the stats face declares. A pin that only ever sees a live
46
- * child cannot tell "absent" apart from "zero", which is precisely the confusion being prevented.
40
+ * `classifySubagentError`): the ABSENT arm is the point of the knownness rule and is not reachable
41
+ * through a real child. This runner initializes `stats.toolCalls` to a number on every path, so only a
42
+ * `TaskResult` from elsewhere — a foreign runner, an older checkpoint — carries the
43
+ * optional-and-missing shape the stats face declares, and a pin that only sees a live child cannot
44
+ * tell "absent" apart from "zero".
47
45
  */
48
46
  export declare function delegatedGranularityFields(stats: {
49
47
  toolCalls?: number;
@@ -241,17 +239,14 @@ export declare function classifySubagentError(terminal: TerminalCause): {
241
239
  retryable: boolean;
242
240
  } | undefined;
243
241
  /**
244
- * structured-card audit 2026-08-05, item D — the Agent family's **completed** `tool_end.structured`
245
- * card, assembled from an EXPLICIT field whitelist over the child `TaskResult`.
242
+ * The Agent family's **completed** `tool_end.structured` card, assembled from an EXPLICIT field
243
+ * whitelist over the child `TaskResult`.
246
244
  *
247
- * ### What was broken
248
245
  * `TaskEvent.tool_end.structured`'s 双面契约 (core/types.ts) states that a host MUST render a
249
- * delegation's landing report from `structured` and MUST NOT scrape the model-facing text. The
250
- * `async_launched` receipt honored it (`type:"agent"`, on the allowlist); the **completed** delivery
251
- * did not: its `details` was `{...child}` — a bare `TaskResult` spread with no `type` key at all — so
252
- * the card projector's allowlist dropped it every time. The one card the contract names by name was the
253
- * one card that could never arrive, and the only way to get the child's report was the thing the
254
- * contract forbids.
246
+ * delegation's landing report from `structured` and MUST NOT scrape the model-facing text — so this
247
+ * card has to carry `type:"agent"` and survive the card projector's allowlist. A bare `{...child}`
248
+ * spread does not: with no `type` key the projector drops it, and the one card the contract names is
249
+ * the one card that can never arrive.
255
250
  *
256
251
  * ### Reconciling with "never leak the whole child TaskResult"
257
252
  * tool-output-projection.ts's own header calls the subagent `details` its example of what must NOT
@@ -263,37 +258,34 @@ export declare function classifySubagentError(terminal: TerminalCause): {
263
258
  * - **`checkpointToken`** — the resume CAPABILITY ("never log it or put it in a URL", per its own
264
259
  * JSDoc). A durable pause is short-circuited into `durablePauseDelegationReport` well before this
265
260
  * point, but that is NOT what keeps the token off the wire: a child whose orchestrator mapped an
266
- * unexpected `suspended` down to `failed` (`errorCode:"unexpected.suspended"`) reaches this function
267
- * carrying one. Under a spread, registering the completed card would have published it.
261
+ * unexpected `suspended` down to `failed` (`errorCode:"unexpected.suspended"`) arrives here carrying
262
+ * one, and a spread would publish it with the completed card.
268
263
  * - **`checkpointGate`** — the token's other half (who must approve what); useless without the token
269
264
  * and part of the same capability story.
270
265
  *
271
266
  * Everything admitted below is already on the MODEL face of the same tool result (status, result text,
272
- * failure attribution, run totals, the transcript id) or was already an additive key on the previous
273
- * `details` object (`resolvedModel` / `toolStats` / `worktreePath`, CC207-B/206-B). So this widens the
274
- * WIRE by exactly the fields a host needs to render the report, and widens what anyone can read by
275
- * nothing at all. New `TaskResult` fields do NOT join automatically — that is the point of the
276
- * whitelist: admitting one is a deliberate edit here, reviewed against this boundary.
267
+ * failure attribution, run totals, the transcript id) or an additive key on `details`
268
+ * (`resolvedModel` / `toolStats` / `worktreePath`). So the WIRE carries exactly the fields a host
269
+ * needs to render the report, and nothing new becomes readable. New `TaskResult` fields do NOT join
270
+ * automatically — that is the point: admitting one is a deliberate edit here.
277
271
  *
278
272
  * ### What this whitelist actually governs (wider than "the wire")
279
273
  * `details` is the tool result's structured face, so this set is ALSO what the in-process consumers
280
274
  * see: the parent's `afterToolCall` hook, the persisted tool-result message in the parent transcript,
281
275
  * and anything reading `toolResult.details` — not only `tool_end.structured`. Omitting a field here
282
- * therefore withholds it from every one of them, which is why the omission of a machine-actionable
283
- * field (2026-08-04: `retryAfterMs`, the wait hint paired with `errorCode:"usage.window_exhausted"`,
284
- * and `degraded`, the reduced-quality fact) is a real loss rather than a cosmetic one. The capability
285
- * fields (`checkpointToken` / `checkpointGate`) stay excluded on exactly the same reading — the
286
- * whitelist's reach is the reason their exclusion is worth anything.
276
+ * withholds it from all of them, which is why dropping a machine-actionable field (`retryAfterMs`,
277
+ * the wait hint paired with `errorCode:"usage.window_exhausted"`; `degraded`, the reduced-quality
278
+ * fact) is a real loss. The capability fields stay excluded on the same reading — the whitelist's
279
+ * reach is the reason their exclusion is worth anything.
287
280
  *
288
281
  * The card keeps `type:"agent"` (rather than minting a second word): CC models the Agent tool's output
289
282
  * as one union discriminated by `status`, and both sema cards already carry `status` —
290
283
  * `"async_launched"` for the receipt, the child's terminal status here. A consumer branches on it.
291
284
  *
292
285
  * Exported at MODULE level (not from the package index, same as `classifySubagentError`) so the leak
293
- * boundary can be pinned directly against a child result carrying a token — the `unexpected.suspended`
294
- * shape is reachable in production (a child running its own team/verify orchestration) but expensive to
295
- * stage end-to-end, and a pin that only ever sees an ordinary completed child cannot tell this
296
- * whitelist apart from a spread.
286
+ * boundary can be pinned against a child result carrying a token: the `unexpected.suspended` shape is
287
+ * reachable in production but expensive to stage, and a pin that only sees an ordinary completed
288
+ * child cannot tell this whitelist apart from a spread.
297
289
  */
298
290
  export declare function completedAgentCard(child: {
299
291
  taskId: string;
@@ -578,21 +570,19 @@ export interface SubagentToolOptions {
578
570
  * exists at all after the fact.** A SYNC or BACKGROUND child mints its session through
579
571
  * `runner.sessions`, never through the parent leg's store, so a deployment that mounts the Agent
580
572
  * tool on a DIFFERENT runner than the one serving root tasks has split the transcript lane away
581
- * from the session lane. (The FORK lane is the deliberate exception and shows the split is a known
582
- * shape: a fork branches the CALLER's session through `ctx.hostSessionFork` — the host Runner's
583
- * own store — precisely because `runner` may be a split child-execution runner whose store never
584
- * held the source. A fork child therefore follows the HOST store's fate, an ordinary child this
585
- * runner's, and the same deployment can land one on disk and lose the other.) Three shapes for the
586
- * ordinary lanes, all reachable and all silent:
573
+ * from the session lane. (The FORK lane is the deliberate exception: a fork branches the CALLER's
574
+ * session through `ctx.hostSessionFork` — the host Runner's own store — precisely because `runner`
575
+ * may be a split child-execution runner whose store never held the source. A fork child follows the
576
+ * HOST store's fate, an ordinary child this runner's, and one deployment can land one on disk and
577
+ * lose the other.) Three shapes for the ordinary lanes, all reachable and all silent:
587
578
  * - durable store on THIS runner whose `release` PRESERVES history (`evict:"forget"`, the
588
579
  * `FileStorageBackend` default), no {@link SubagentToolOptions.background}.`agentStore` — the
589
580
  * child transcript lands UNPLACED and survives the settle leg's eager release (a cache drop),
590
581
  * so post-hoc forensics work even though nothing can ADDRESS it through an `a*` handle. The
591
- * qualifier is load-bearing, not decorative: durability and release semantics are INDEPENDENT
592
- * declarations, and a store that declares itself durable while deleting on release
593
- * (`new FileStorageBackend({ evict: "delete" })`, or any custom store with the same policy)
594
- * destroys the transcript at settle — the settle leg calls `release`, and there is no placement
595
- * to make the deletion refuse. "Durable" is a statement about restarts, never about release;
582
+ * qualifier is load-bearing: durability and release semantics are INDEPENDENT declarations, and
583
+ * a store declaring itself durable while deleting on release destroys the transcript at settle —
584
+ * the settle leg calls `release`, and no placement makes the deletion refuse. "Durable" is a
585
+ * statement about restarts, never about release;
596
586
  * - durable store + `agentStore` — the placed partition (see the mint gate in the background
597
587
  * lane): addressable across a restart, list-excluded, joint-reaped with its row. This arm is
598
588
  * also the one immune to the evict policy above (the settle leg keeps a confirmed row's
@@ -602,21 +592,19 @@ export interface SubagentToolOptions {
602
592
  * reaches disk whatever the root leg's store is. Nothing in the engine compensates: the
603
593
  * completion notification's recent steps and the in-memory registry are the only readers, and
604
594
  * both die with the process.
605
- * The asymmetry this produces is worth naming, because it reads as a bug from the outside: the
606
- * `run_workflow` tool is mounted by the ENGINE on the run's own runner (prepare-task's
607
- * `runnerSelf`), so a workflow node's child session always follows the ROOT store, while this tool
608
- * is mounted by the DEPLOYMENT and follows whatever runner it was handed. One data root can
609
- * therefore hold every workflow node's transcript and none of the Agent tool's.
610
- * There is deliberately NO manifest field that answers "does a child transcript reach disk here":
595
+ * The asymmetry this produces reads as a bug from the outside: the `run_workflow` tool is mounted by
596
+ * the ENGINE on the run's own runner (prepare-task's `runnerSelf`), so a workflow node's child
597
+ * session always follows the ROOT store, while this tool is mounted by the DEPLOYMENT and follows
598
+ * whatever runner it was handed. One data root can hold every workflow node's transcript and none
599
+ * of the Agent tool's.
600
+ * There is deliberately NO manifest field answering "does a child transcript reach disk here":
611
601
  * `WiringManifest.fleet.subagentTranscripts` is the ADDRESSABILITY tier and folds to `"none"` on
612
- * the absent-row-store arm before it ever looks at a session store, so it cannot separate the
613
- * first shape above from the third (the landing pins carry that as an explicit arm). The facts
614
- * that DO answer it are this runner's store's own declarations — `sessions.durability` and its
615
- * release/evict policy — read together, which is why they are named here rather than folded into
616
- * a word. Upstream anchor (CC 2.1.250): a sub-agent transcript is written unconditionally to
617
- * its own file beside the parent session (`projects/<projectKey>/<sessionId>/subagents/agent-<id>
618
- * .jsonl` + a `.meta.json` sidecar), so "the child conversation is readable afterwards" is a
619
- * property CC never makes conditional on a store tier.
602
+ * the absent-row-store arm before it looks at a session store, so it cannot separate the first
603
+ * shape above from the third. What DOES answer it is this runner's store's own pair of
604
+ * declarations — `sessions.durability` and its release/evict policy — read together. Upstream
605
+ * (CC 2.1.250) writes a sub-agent transcript unconditionally to its own file beside the parent
606
+ * session, so "the child conversation is readable afterwards" is never conditional on a store
607
+ * tier there.
620
608
  */
621
609
  runner: RunnerSelfSeat;
622
610
  /**
@@ -13,14 +13,12 @@ import type { ToolSpec } from "../core/types.js";
13
13
  * newly-added sensitive tool to every agent. Names not present in the pool are ignored (no error) — an
14
14
  * allow/deny list is a filter over what's available, not an assertion that those tools exist.
15
15
  *
16
- * anchor ruling (dual-leg verified, adversarially reviewed): this spawn-time item-level filter IS
17
- * the upstream shape for allow entries — CC's resolveAgentTools partitions unknown names into an
18
- * `invalidTools` bucket nobody consumes at runtime (88 readable source), and a live 2.1.207 probe shows
19
- * the agent stays listed/delegable with the unknown item silently dropped, zero warnings. The
20
- * "rejected at startup" posture this replaced had no verbatim anchor and did not survive verification.
21
- * The asymmetric prepare-time fail-loud for `TaskSpec.agents` DENY entries is a deliberate sema
22
- * extension (no upstream deny-list exists): an allow-typo silently narrows (safe direction), a
23
- * deny-typo silently widens (unacceptable) — each face follows its own failure direction.
16
+ * Anchored (dual-leg verified): this spawn-time item-level filter IS the upstream shape for allow
17
+ * entries — CC's resolveAgentTools partitions unknown names into an `invalidTools` bucket nobody
18
+ * consumes at runtime, and a live 2.1.207 probe shows the agent stays listed/delegable with the
19
+ * unknown item silently dropped, zero warnings. The asymmetric prepare-time fail-loud for
20
+ * `TaskSpec.agents` DENY entries is a deliberate sema extension (no upstream deny-list exists): an
21
+ * allow-typo silently narrows (safe direction), a deny-typo silently widens (unacceptable).
24
22
  *
25
23
  * Note: `"*"` in `allowTools` is the "allow all" sentinel, so a tool literally NAMED `"*"` cannot be
26
24
  * the sole allowed tool via this path (tool names are conventionally identifiers, so this is moot).
@@ -28,10 +26,10 @@ import type { ToolSpec } from "../core/types.js";
28
26
  /**
29
27
  * Membership check for ONE tool name under the same allow/deny semantics as {@link resolveToolSubset}
30
28
  * (canonicalized both sides; `"*"` allow sentinel; deny wins). Used by the delegation tool to decide
31
- * whether a chosen agent DEFINITION may re-delegate: a per-agent allow/deny that excludes the delegation
32
- * tool's own name suppresses the nested delegation tool (CC 2.1.198 parity — Explore/Plan
33
- * `disallowedTools` include Agent, pretty.js:409189/486948; CC filters the Agent tool like any other).
34
- * The TOOL-level allow/deny deliberately keeps its old semantics (delegation governed by `maxDepth`).
29
+ * whether a chosen agent DEFINITION may re-delegate: a per-agent allow/deny that excludes the
30
+ * delegation tool's own name suppresses the nested delegation tool (upstream parity — the built-in
31
+ * Explore/Plan denials include Agent, and the delegation tool is filtered like any other). The
32
+ * TOOL-level allow/deny keeps its own semantics (delegation governed by `maxDepth`).
35
33
  */
36
34
  export declare function toolNameAllowed(name: string, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): boolean;
37
35
  export declare function resolveToolSubset(pool: ReadonlyArray<ToolSpec>, allowTools?: ReadonlyArray<string>, denyTools?: ReadonlyArray<string>): ToolSpec[];