@sema-agent/core 7.14.0 → 7.16.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 +59 -0
- package/dist/core/ask-origin.d.ts +2 -2
- package/dist/core/checkpoint-store.d.ts +14 -15
- package/dist/core/governance-codes.js +2 -0
- package/dist/core/hooks.d.ts +4 -3
- package/dist/core/memory-engine/delegation-settlement.d.ts +37 -12
- package/dist/core/memory-engine/delegation-settlement.js +128 -39
- package/dist/core/memory-engine/engine.d.ts +9 -8
- package/dist/core/memory-engine/engine.js +115 -120
- package/dist/core/memory-engine/file-backend.d.ts +17 -5
- package/dist/core/memory-engine/file-backend.js +78 -34
- package/dist/core/memory-engine/index.d.ts +2 -2
- package/dist/core/memory-engine/index.js +2 -2
- package/dist/core/memory-engine/layout.d.ts +152 -54
- package/dist/core/memory-engine/layout.js +441 -104
- package/dist/core/memory-engine/types.d.ts +8 -3
- package/dist/core/memory-engine/types.js +5 -0
- package/dist/core/runner/abort-race.d.ts +3 -3
- package/dist/core/runner/active-skill-scope.d.ts +2 -2
- package/dist/core/runner/assemble-result.d.ts +29 -29
- package/dist/core/runner/checkpoint-scope.d.ts +4 -4
- package/dist/core/runner/clock-and-limits.d.ts +3 -3
- package/dist/core/runner/compaction-call-options.d.ts +11 -11
- package/dist/core/runner/compaction-knobs.d.ts +4 -4
- package/dist/core/runner/compaction-seams.d.ts +5 -5
- package/dist/core/runner/contracts.d.ts +224 -226
- package/dist/core/runner/decide-continuation.d.ts +6 -6
- package/dist/core/runner/denial-limit-arms.d.ts +8 -8
- package/dist/core/runner/derived-route-fallback.d.ts +2 -2
- package/dist/core/runner/gate-exit.d.ts +6 -6
- package/dist/core/runner/git-status-frame.d.ts +7 -7
- package/dist/core/runner/inherited-ask-grants.d.ts +6 -6
- package/dist/core/runner/memory-capture-optout.d.ts +1 -1
- package/dist/core/runner/memory-consolidation-driver.d.ts +4 -4
- package/dist/core/runner/memory-consolidation.d.ts +6 -6
- package/dist/core/runner/park-commit.d.ts +6 -6
- package/dist/core/runner/permission-rule-lanes.d.ts +22 -20
- package/dist/core/runner/permission-rule-lanes.js +1 -1
- package/dist/core/runner/prepare-acquire-reconcile.d.ts +4 -4
- package/dist/core/runner/prepare-ask-lane.d.ts +3 -3
- package/dist/core/runner/prepare-boundary-parks.d.ts +3 -3
- package/dist/core/runner/prepare-caps-and-workflow.d.ts +5 -5
- package/dist/core/runner/prepare-config-doors.d.ts +21 -21
- package/dist/core/runner/prepare-context-lane.d.ts +6 -6
- package/dist/core/runner/prepare-defer-classify.d.ts +3 -3
- package/dist/core/runner/prepare-defer-classify.js +12 -10
- package/dist/core/runner/prepare-delegation-surface.d.ts +1 -1
- package/dist/core/runner/prepare-file-history.d.ts +2 -2
- package/dist/core/runner/prepare-gate-stations.d.ts +8 -9
- package/dist/core/runner/prepare-gate-stations.js +2 -2
- package/dist/core/runner/prepare-hands-readface.d.ts +41 -41
- package/dist/core/runner/prepare-hands-readface.js +27 -29
- package/dist/core/runner/prepare-inherited-gate.d.ts +6 -6
- package/dist/core/runner/prepare-listings.d.ts +3 -3
- package/dist/core/runner/prepare-lsp.d.ts +3 -3
- package/dist/core/runner/prepare-memory-engine-session.d.ts +5 -5
- package/dist/core/runner/prepare-memory.js +6 -7
- package/dist/core/runner/prepare-offload-wrappers.d.ts +2 -2
- package/dist/core/runner/prepare-park-ask.d.ts +11 -12
- package/dist/core/runner/prepare-park-ask.js +3 -3
- package/dist/core/runner/prepare-policy-chain.d.ts +6 -6
- package/dist/core/runner/prepare-policy-chain.js +2 -2
- package/dist/core/runner/prepare-prompt-inputs.d.ts +5 -5
- package/dist/core/runner/prepare-protocol-tools.d.ts +1 -1
- package/dist/core/runner/prepare-question-face.d.ts +4 -4
- package/dist/core/runner/prepare-run-refs.d.ts +11 -11
- package/dist/core/runner/prepare-safety-scan.d.ts +1 -1
- package/dist/core/runner/prepare-suspend-saga.d.ts +4 -4
- package/dist/core/runner/prepare-task.d.ts +12 -12
- package/dist/core/runner/prepare-task.js +5 -7
- package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +2 -3
- package/dist/core/runner/prepare-wiring-manifest.d.ts +4 -4
- package/dist/core/runner/prepare-workspace-restore.d.ts +1 -1
- package/dist/core/runner/remote-env-retry.d.ts +3 -3
- package/dist/core/runner/resume-admission.d.ts +1 -1
- package/dist/core/runner/resume-claim.d.ts +1 -1
- package/dist/core/runner/resume-internals-and-config.d.ts +1 -1
- package/dist/core/runner/resume-policy-outcome.d.ts +1 -1
- package/dist/core/runner/resume-preflight.d.ts +1 -1
- package/dist/core/runner/resume-review-outcome.d.ts +1 -1
- package/dist/core/runner/run-brain-sinks.d.ts +1 -1
- package/dist/core/runner/run-clock-and-content.d.ts +4 -4
- package/dist/core/runner/run-compaction-machinery.d.ts +3 -3
- package/dist/core/runner/run-git-lane.d.ts +1 -1
- package/dist/core/runner/run-harness-handlers.d.ts +2 -2
- package/dist/core/runner/run-identity-wiring.d.ts +4 -4
- package/dist/core/runner/run-leg.d.ts +3 -3
- package/dist/core/runner/run-notification-lane.d.ts +2 -2
- package/dist/core/runner/run-reasoning-seat.d.ts +2 -2
- package/dist/core/runner/run-settle-and-teardown.d.ts +4 -4
- package/dist/core/runner/run-telemetry-and-budget-seats.d.ts +2 -2
- package/dist/core/runner/run-terminal-adoption.d.ts +5 -5
- package/dist/core/runner/runtask.d.ts +14 -14
- package/dist/core/runner/session-file-state-replay.d.ts +3 -3
- package/dist/core/runner/session-rule-policy.d.ts +2 -2
- package/dist/core/runner/steer-admission.d.ts +2 -2
- package/dist/core/runner/stream-halt-verbs.d.ts +1 -1
- package/dist/core/runner/stream-reap.d.ts +1 -1
- package/dist/core/runner/synthetic-tools.d.ts +8 -8
- package/dist/core/runner/teardown-bounded.d.ts +2 -2
- package/dist/core/runner/tool-defer-gate.d.ts +86 -0
- package/dist/core/runner/tool-defer-gate.js +57 -0
- package/dist/core/runner/tool-disclosure.d.ts +33 -69
- package/dist/core/runner/tool-disclosure.js +0 -43
- package/dist/core/runner/tool-end-body.d.ts +3 -3
- package/dist/core/runner/tool-face-overlay.d.ts +2 -2
- package/dist/core/runner/tool-output-projection.d.ts +2 -2
- package/dist/core/runner/turn-attachments.d.ts +53 -53
- package/dist/core/runner/usage-accounting.d.ts +4 -4
- package/dist/core/runner/workspace-path.d.ts +1 -1
- package/dist/core/runner-deps.d.ts +16 -9
- package/dist/core/task-spec.d.ts +8 -2
- package/dist/core/tool-spec.d.ts +7 -4
- package/dist/core/types.d.ts +1 -1
- package/dist/engine/llm/types.d.ts +1 -1
- package/dist/index.d.ts +4 -4
- package/dist/index.js +3 -3
- package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -40
- package/dist/tools/fs/encoding.d.ts +31 -40
- package/dist/tools/fs/fs-bash.d.ts +76 -35
- package/dist/tools/fs/fs-bash.js +86 -29
- package/dist/tools/fs/fs-pdf.d.ts +8 -8
- package/dist/tools/fs/fs-search-tools.d.ts +4 -4
- package/dist/tools/fs/fs-shared.d.ts +144 -158
- package/dist/tools/fs/fs-write.d.ts +8 -8
- package/dist/tools/fs/gh-rate-limit.d.ts +13 -14
- package/dist/tools/fs/index.d.ts +74 -78
- package/dist/tools/fs/notebook.d.ts +24 -25
- package/dist/tools/fs/pdf.d.ts +3 -3
- package/dist/tools/fs/read-deny.d.ts +22 -22
- package/dist/tools/fs/read-face.d.ts +21 -24
- package/dist/tools/fs/repo-map.d.ts +8 -8
- package/dist/tools/fs/safety.d.ts +116 -129
- package/dist/tools/fs/search.d.ts +63 -71
- package/package.json +5 -2
- package/test/export-surface.snapshot.json +9 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,64 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 7.16.0 — 2026-09-12
|
|
4
|
+
|
|
5
|
+
### BREAKING — memory directory layout: every scope's home is `<memoryDir>/<label>`, the memory root is nobody's home (#699; design/699; @server @cli @test)
|
|
6
|
+
- **Before.** The first write scope ever used for a memory dir claimed the ROOT itself (loose `*.md` + `MEMORY.md` at `<memoryDir>/`) and every later scope nested beneath it (`<memoryDir>/<label>/`). Ten rules hung off that: a durable root claim, an empty-label ⇔ root-scope registry invariant, three-arm home derivation, a `""` exception in both collision arms, three copies of "exclude the sibling scope dirs when scanning the root", a `skipRoot`/`excludeTopDirs` chmod special case, a runner fence arm refusing a write root that contains another scope, the erase sweep treating the root as an index home, read-only sessions reading the root's `MEMORY.md`, and six sidecars keyed by physical paths.
|
|
7
|
+
- **After (five rules).** ① A scope's home is `join(memoryDir, label)` with `label = scopeDirName(scope)` (the registered label is the truth once written); the memory dir itself is never a home. ② The registry is `scopes.json = { v: 2, scopes, rootOwner?, migrating? }`; every label matches `SCOPE_DIR_LABEL_RE = /^[A-Za-z0-9_][A-Za-z0-9._-]*$/` (the closure of `scopeDirName`'s outputs; pinned by a 5k-input property); the four read shapes `absent | legacy | v2 | migrating` are a closed set (`SCOPE_REGISTRY_SHAPES`, disposition table `REGISTRY_SHAPE_DERIVES_PATHS`, registered in `docs/CLOSED-SETS.md`) — `legacy` and `migrating` are refused fail-closed by `registerScope` / `scopeDirFor` / `registeredScopes` until the adoption has run. ③ A home must be a REAL directory or absent (`lstat` at registration and at every mount; a link or a file at the home path refuses). ④ The two collision arms (exact + case-fold) are unchanged minus the `""` exception. ⑤ Every persistent sidecar key that names a memory file is the ledger form `(scope, rel)` — projection debts, instruction holds, session-account membership, the write-scan fuse — so a home move never re-keys anything (`index-revs.json` and `MEMORY.md` link targets stay model-facing physical coordinates, by design).
|
|
8
|
+
- **Migration (one-shot, two-phase, durable; `adoptRootScopeLayout` in `layout.ts`).** Runs before the first registry-derived path of every mount (`MemoryEngine.materialize` → the backend's `adoptRootScopeLayout` face under its transaction lock, plus the copy-out projection tree directly) and at the head of every backend public face (`ensureLayout`, non-blocking try-acquire; the locked faces run it under their lock; a view object runs it on every call). Phase 0 converts the registry (`rootScope` → `rootOwner`, the owner gets its label) and enumerates the root's loose content on EVERY call; content with an owner (`rootOwner ?? this mount's write scope / the batch's first add scope`) passes four zero-write pre-checks (control plane inside the root; the home a link/file or a non-empty directory with no move in progress; any loose name already present in the home) or refuses with `ControlPlaneCorruptError` (memory-less + loud at the runner). Commit A writes the `migrating` marker; phase 1 moves each entry (`lstat` destination, `rename`, source-gone = already moved) and removes `.casefold-probe-*` residue; phase 2 clears `index-revs.json`; commit B drops the marker. No sidecar key and no index text is rewritten (cross-scope index lines heal under the existing rebuild law: orphan cleared, mechanical row re-minted, model wording lost once). A registry lock heartbeat (`utimes` + strict ownership re-proof, plus the backend's transaction-lock heartbeat) runs before every action; a lost lock throws immediately and the next holder resumes from the marker. Content with NO owner on record (a read-only mount over a registry-less root) is left in place, disclosed once, never served. Two disclosure codes: `memory.layout_migrated`, `memory.layout_unowned_root_content` (incident sink).
|
|
9
|
+
- **BREAKING (package surface).** `MemorySessionHandle.writableRoot` is `string | null` — `null` iff there is no write face (`writeScope === null`); it is never `memoryDir` any more. New `writableRootOf(handle)` (throws on `null`) for call sites that established a write face. `scanEntryFiles(dir, { exclude })` — the `exclude` option is removed. `MemoryEngine.readonlyDirNamesUnderRoot` is REMOVED, together with the runner arm that was its only consumer: the set it answered — another scope's directory lying under the writable root — is empty by construction now that homes never nest. Internal bucket: `claimRootScope` / `rootScopeOf` removed.
|
|
10
|
+
- **BREAKING (on disk; downstream may read or seed these).** `scopes.json` form (above). `projection-debts.json` / `holds.json` / `session-accounts.json` are `version: 2` with `(scope, rel)` keys (`HoldRow.relPath` → `HoldRow.rel`, home-relative; `SessionAccountRow.unattributed` is `Array<{ scope, rel }>` plus `unattributable?: string[]`); each converts once, in its own lock, on the first read that finds the old form (idempotent by field name + version); a row the registry cannot attribute is KEPT (`unresolved` / `unattributable`) and never served / marks the whole session unattributed — never dropped. `scan-fuse.json` is `{ v: 2, counts }` keyed by `scanFuseKey(scope, rel)`; the absolute-path form is dropped (fail-open reset). The user-visible path of every entry and index gains one level (`memory/x.md` → `memory/<label>/x.md`; server's default scope `local` ⇒ `memory/local/`).
|
|
11
|
+
- **Narrowing (read-only sessions).** An all-read-only session (`writableRoot === null`) never reads a `MEMORY.md` from disk in either provenance mode — its index is the mechanical rebuild off committed headers (links computed against the memory dir as a virtual base). The model-worded index the root owner's read-only sessions used to get by accident is no longer served to them; write sessions are unchanged.
|
|
12
|
+
- **Retired special cases.** Root claim at materialize and at `applyPatches`; `siblingScopeDirNames`, `excludedSubdirNames`, the five `isRoot` scan branches, `chmodScopeTree`'s `skipRoot` / `excludeTopDirs`; the erase sweep's memory-root index candidate (its failure text names `<scope homes>`); the harvest/materialize `exclude` sets. `gateWrite` on a read-only handle judges the whole plane (`memoryDir`) and refuses every path under it with `read_only_layering`.
|
|
13
|
+
- **Runner (the fence).** `prepare-memory.ts` consumes `writableRootOf(writeHandle)` at the admission seat and takes an explicit `root !== null` branch in the two write-gate legs. The admission's sibling-scope refusal is gone: `foreignUnderRoot` keeps only the control-plane arm (a directory advertised to the model must not contain the engine's own metadata), and `writeChannelOpen` keeps all four conjuncts. Consequence — the deployment the old arm refused (the write scope owning the root with any other scope registered: a single-user server with an org layer, or any `defaultScopes` extra) is admitted and taught again, and it is not a hole: the admitted directory is that scope's home, so a sibling scope's file, the homes' shared parent and a scope another process registers mid-session all stay outside the fence (`path_not_in_root`).
|
|
14
|
+
- **Operations.** Stop every process sharing a memory dir before upgrading; an older engine re-occupying the root after the move leaves a non-empty home beside loose root content, which the next mount refuses loudly (no silent merge). Interruption safety is process-level (`atomicWriteFileSync` does not fsync directories); every step resumes from the registry marker and the sidecar row forms.
|
|
15
|
+
- **Review rounds (adversarial, three; all findings reproduced by reading, adopted unless noted).** Round 1 — five: a registry commit fenced only before its journal write (a holder stolen from past its commit point could overwrite the stealer's main file / remove its journal) → strict ownership re-proved before each of the three steps; the v1 sidecar key conversions were lazy (a scope registered after the move, named like an old topic directory, would re-read a v1 key's first segment) → converted inside the adoption; a v1 hold row's slug was not shape-checked (a crafted row could derive an escaping seat) → the rel-path shape law on `slug`; registry accumulators over `{}` dropped a `__proto__` scope → null-prototype accumulators; a v1 root owner whose prospective home already stood non-empty was silently re-read under new slugs → refused before conversion. Round 2 — three: conversion ran after the commit that cleared the durable mark → conversion now precedes every registry commit (the v1 registry / the marker stays until every sidecar is in the ledger form); the strict journal roll-forward replayed unfenced → fenced before its replay write and its journal removal; a home spelled in another letter case on a folding volume was enumerated as loose content and moved into itself → recognized by directory identity, self-move refused. Round 3 — three: the strict-ledger commit (holds / session accounts / projection debts) had the round-1 gap → fenced like the registry commit; the marker persisted the root verbatim (a relative spelling resumed from another cwd would read as finished) → absolute; **declared, not chased**: the window between a fence and the atomic writer's publishing rename is inherent to the mkdir-lock design (three rounds on the same root — the lock family — stop here by rule).
|
|
16
|
+
- Pins: `test/memory-layout-migration.test.ts` (39 — P1 registry shapes + the 5k label property; P2 exclusivity + physical form; P3 mainline on the real load path; P4 idempotence + three breakpoints + a foreign-tree marker; P5 four pre-checks; P6 heartbeat + stolen lock; P7 journal-before-adoption; P9 read-only index both modes; P10 owner resolution ①–④; P11 chmod; P12/P15 erase sweep; P14 tripwire; P16 read-only gate; P17 key conversions + unattributable rows; the review-round seams: conversion-before-registration, the escaping v1 hold slug, the `__proto__` scope, the occupied v1 home, the stolen-lock commit refusal, pre-check 4 on the v2 path, the heartbeat refusing at the very next action, the backend face's replay-then-adopt order, conversion-before-commit on both branches, the folded home spelling, the absolute marker root); re-pinned `memory-engine` / `memory-scope-enumeration` / `memory-txn-lock` registry pins and the design/336 hold/account fixtures. Re-pinned end-to-end through the Runner: `test/backlog691-advertised-writable-dir.test.ts` (P8 — two scopes over one memory dir, the write scope the first writer: home admitted and taught, own home read/write both ways, sibling + mid-session third scope + shared parent refused, no refused/skipped line). Wire rows: `docs/wire-consumption/unreleased-699.md`.
|
|
17
|
+
|
|
18
|
+
- Docs/gate (#710) · new `gate:docs-tool-names` fails on a RETIRED tool name (`report_blocked`, `run_workflow`, `web_fetch`, `submit_output`, `present_plan`, `tool_search`, `MultiEdit`, `bash_readonly`, …) surviving in `docs/**/*.md` outside a small historical-record whitelist; one-time rename sweep over `ARCHITECTURE.md`, `GATE-OUTCOME-STATIONS.md`, `INTEGRATION-CORE.md`, `PROMPT-ASSEMBLY-EXECUTION-PLAN.md`, `PROMPT-ASSEMBLY-HANDOFF.md`, `PROMPT-SECTION-MAP.md`, `PROMPT-SENTENCE-EVIDENCE-INDEX.md`, `ROADMAP.md`, `SERVICE-INTEGRATION-GUIDE.md`, `sdk/02`, `sdk/07`, `sdk/09` to their live names, plus one factual fix (`sdk/09-security-and-policies.md`: `handsReadOnly` mounts `Bash` in its read-only posture, not a separate `bash_readonly` tool). No behaviour change.
|
|
19
|
+
|
|
20
|
+
### Contract — `Model.cost` is optional; `isModelPriced` is exported (#718; @server @cli)
|
|
21
|
+
- **Member-level type change (package surface).** `Model.cost` is now `cost?: {…}`. The runtime already read it as optional everywhere — `modelCostToPricing(undefined)` yields the all-zero table, `selectModel` sorts a model without `cost` as `Infinity`, every brain adapter reads `c?.input ?? 0`, and the RB-368 predicate `isModelPriced` takes `cost?: unknown` — only the type claimed it was required, so a host without a price table had to fabricate one or cast. Absent ⇒ UNPRICED; a free model declares four `0` rates; the two are now distinguishable at the type as they already were at runtime. Readers of `model.cost.<rate>` on a core-typed `Model` must guard for absence (compile-time; no core read and no server/cli `src` read does so unguarded).
|
|
22
|
+
- **Exported.** `isModelPriced(model, pricing)` (tier `advanced`) — the one RB-368 predicate ("does a price table EXIST": an id-keyed `RunnerDeps.pricing` entry or the model's own `cost`) that a host previously had to re-implement beside `computeCostMicroUsd` / `modelCostToPricing`.
|
|
23
|
+
- Pins: `test/backlog718-model-cost-optional.test.ts` (the literal without `cost` typechecks; predicate three-way; root export identity); `test/export-surface.snapshot.json`.
|
|
24
|
+
|
|
25
|
+
### Tooling (no behaviour change)
|
|
26
|
+
- Docs (#704 ② pilot sweep · `src/tools/fs`, comments only — zero behaviour face): every comment in the bucket now states a contract or an invariant; the ticket ids (`RB-NNN`/`#NNN`/`design/NNN`), channel coordinates, review-round attributions, batch names and deprecated-corpus byte offsets are gone, and a defect retelling is rewritten as the rule that holds. CC parity/divergence assertions, fail-closed reasons, refusal orderings and safety consequences are kept verbatim in substance. Bucket reading: comment lines 7334→7147, ratio 0.4835→0.4771, process-word hits 863→9 (the 9 remaining are English false positives — `Math.round`, `round trip`). Four bucket ratchets banked (`comment-ratio`, `internal-wording` 46→3, `line-anchor` 15→6, `legacy-residue` 35→33) plus one now-unique `contract-sentences` baseline row removed. Narrative archived to the internal repo.
|
|
27
|
+
- Docs (#704 ② sweep · `src/core/runner`, comments only — zero behaviour face, 36 batch commits): a mechanical two-tier label stripper (leading ticket-cluster removal + capitalize-restart; pure-citation-parenthetical removal) over all 111 files in the bucket, followed by a hand fix where the stripper's capitalization exposed a pre-existing cross-file exact-duplicate sentence (`gate:contract-sentences`) between `prepare-boundary-parks.ts` and `prepare-park-ask.ts` — the shorter copy is now a pointer to the fuller one. Every comment keeps its contract/invariant content, fail-closed reasoning and CC-anchor sentence verbatim; only the leading `design/NNN`/`RB-NNN`/`#NNN`/`[NNNN]`/`codex …` coordinate cluster and pure-citation parentheticals are gone. Bucket reading: comment lines 23666→23667 (+1, the dedup-fix wrap), total lines 47238→47239, ratio 0.5010→0.50101, process-word hits 3048→1340 (56% down; the residue is dominated by domain vocabulary the lexicon false-positives on — `review`/`round` inside real identifiers like `dry_run_review`/`plan_review`/`Math.round` — plus mid-sentence citations the leading-label/pure-citation matchers deliberately do not touch). Two bucket ratchets banked (`comment-ratio` processHitsMax 3048→1340, ratioMax 0.501→0.5011; `internal-wording` 174→121); `legacy-residue` (104) and `line-anchor` (13) unchanged for this bucket. No internal-archive file — every removed span was a short ticket/citation label, not a relocatable narrative paragraph.
|
|
28
|
+
- Docs (#704 ② sweep, second pass · `src/core/runner`, comments only — zero behaviour face, ~30 batch commits, hand-edited): the first pass's leading-label/pure-citation matchers were conservative on mid-sentence and multi-line citations (design/RB/#/[NNNN]/codex-round/rescan clusters woven into the middle of a contract sentence); this pass read every one of the 1204 process-word hits the first pass left (excluding `prepare-memory.ts`, car A/#699's file) by hand, file by file, and either compressed the citation out of the sentence (keeping the contract clause) or confirmed the hit as genuine domain vocabulary the lexicon false-positives on (`review`/`plan_review`/`human-review`/`dry-run review`/`code-review` — real gate-kind and CC-tool names, not review-process narrative; `round` inside `model round`/`classifier round`/`round cap`/`corrective round` — real bounded-iteration counts, not review-round citations). `design`/`RB`/`#`/`codex`/`rescan` citations are now at ZERO across the whole bucket (one `[1995]①` kept verbatim — a quoted test-name string, not a citation). Bucket reading (excluding `prepare-memory.ts`, unchanged by this car): process-word hits 1204→149 (88% down on the touched files); whole-bucket total (incl. `prepare-memory.ts`'s 109, task-0 census regex false-positive fix applied first) 1313→258. Two bucket ratchets banked (`comment-ratio` processHitsMax 1340→258; `internal-wording` 121→91) plus one `contract-sentences` baseline shrink (a 14-file and a 2-file duplicate row both resolved; one new byte-identical duplicate the citation-stripping introduced was reworded, not admitted). `comment-census.mjs`'s `PROCESS_WORD_RE` gained two false-positive exclusions (`Math.round(...)`/`round trip` no longer match `round`; identifier-embedded `review` spellings were already excluded by the existing word boundary, now pinned) — every bucket's baseline in `test/comment-ratio-ratchet.test.ts` re-measured and banked down to match (lower-only). Two structural-pin test landmarks in `prepare-wiring-manifest.ts` were reverted verbatim after breaking `test/design385-slice2b-agents.test.ts`'s exact-substring search (test files are off-limits; the source comment kept its citation instead). No internal-archive file — every removed span was a citation label or a short compressible clause, not a relocatable narrative paragraph.
|
|
29
|
+
- Added · `npm run car:gate -- '<word list>' [--base <sha>] [--full] [--log-dir <dir>]` (`scripts/car-gate.mjs`, #705): runs a dispatch order's door-family as one command — `tsc --noEmit`, a derived vitest suite (content-grepped `test/*.test.ts` ∪ the eleven door-family pins ∪ `test/gate-all-roster.test.ts`; hub names in the word list auto-stripped and announced), and `verify-layering`; `--full` adds `npm run build && node scripts/gate-all.mjs --with-dist --rc`. Any step non-zero makes the whole run non-zero and every step still runs. Pins: `test/car-gate.test.ts`.
|
|
30
|
+
- Added · `npm run gate:comment-ratio` (`scripts/comment-census.mjs`, #704, gate-landing only — no src sweep in this car): a per-directory-bucket ratchet on `src/**/*.ts`'s comment-line ratio and process-word (round/rescan/codex/design-ticket/battle-narrative) hit count, sibling to `internal-wording-gate`/`legacy-residue-gate`. One-way (ratchet-down-must-bank), a HARD cap (0.25 ratio / 0 hits) for a bucket absent from the table, wired into `gate:all`'s static tier. Pins: `test/comment-ratio-ratchet.test.ts`.
|
|
31
|
+
|
|
32
|
+
## 7.15.0 — 2026-09-11
|
|
33
|
+
|
|
34
|
+
### Internal — abstraction unification pass over the 7.15.0 merges (no behaviour change; exports +1 type)
|
|
35
|
+
- Added · `ShellGateDoctrine` (`"off" | "classify" | "always"`) is exported from the package root — the one alias behind `TaskSpec.shellGate`, `RiskDescriptor.shellGateDoctrine` and the inherited-gate seat (nine inline spellings retired; the type domain is unchanged).
|
|
36
|
+
- Internal · the shell seat's two provenance bits became one membership set (`shellGatedTools`) read by the park mint, the gate station and the rule-offer factory; the two shell probe seats read the command through one single-read helper and the deny judge's voice through one predicate (`gate:single-mint` row `denyJudgeSpoke`); `tool-defer-gate.ts` joins the runner's machinery stratum. Pins: `test/backlog706-shell-seat-single-spelling.test.ts` (7), the Monitor twin of the durable-descriptor pin in `test/backlog696-…`.
|
|
37
|
+
- Not changed (reviewed, kept) · the sandbox-boundary predicate keeps `toolName.includes("__")` — the separator, not the protocol table's ownership, is the containment fact (a `__`-bearing name mounted after the scan-time reservation is unowned yet uncontained); the containment abstraction (mount-time execution provenance) is ticketed.
|
|
38
|
+
|
|
39
|
+
### Narrowing — the read boundary is judged on every shell call under every `shellGate` doctrine (#696; P1 design; CC 2.1.250 form: the read boundary is a path-layer property judged on every Bash call, the permission mode disposes only of the residual; sema keeps its deny table — the registered divergence — and its opt-out operand scan; @server @cli @test)
|
|
40
|
+
- **Before.** The read boundary (built-in deny table + roots containment) rode the `shellGate` doctrine's probe: `shellGate` absent / `"off"` mounted NO probe on Bash, so `cat .claude/settings.json` / `cat ~/.ssh/id_rsa` ran with zero asks and the bytes reached the session record while `Read` of the same path refused typed. Two read faces of one process gave two answers about one path, and the shell face's judgment hung on a switch unrelated to the boundary. Three rules: the structured faces always judge; the shell face judges only under `"classify"`; `"always"` absorbs the question into its blanket ask.
|
|
41
|
+
- **After (two rules).** ① Every read face judges the read boundary — structured faces refuse (`read_path_denied`), the shell face gets a MANDATED ask (neither a stored allow rule nor the read-only arm retires it; a person's yes releases the read). ② `shellGate` disposes only of the RESIDUAL shell risk: `"off"` (the default) installs no shape classifier — writes, egress and unknown commands go to the ordinary policy/hook chain; `"classify"` auto-allows a provably read-only in-boundary command and asks for everything else; `"always"` asks for every command. Mechanically: the shell seat (`prepare-hands-readface.ts`, `installShellSeat`) puts Bash and Monitor on the `"maybe"` tier with a probe under EVERY doctrine — `bashReadBoundaryProbe` (new named seat, `src/tools/fs/fs-bash.ts`; export +1) under `"off"`, `bashReversibilityProbe` (= boundary ∘ shape classifier) under `"classify"`; under `"always"` the tier is the mandate and no probe runs. The boundary mandate is one shared function on both seats (`readBoundaryMandate`: deny-listed operand / recursive walk under the deny judge / out-of-root operand). A read face's refusal is ATTRIBUTED (`attributeRefusal`): the face reports one sentence, so the same face is asked again with the deny judge removed and containment opened, and once more with no boundary — a changed or lifted sentence means the boundary objected (mandated), an unchanged sentence that no boundary lifts means the grammar refused (the doctrine's residual), anything else means the walk could not read an operand (a plain fail-closed ask). A loop body reading a deny-listed in-root file — or `cd`-ing into a deny-listed directory — is mandated on both seats (under `"classify"` it was an un-mandated, rule-clearable ask), and a body that also carries a glob does not lose the declared objection. A `~`-rooted operand stays UNRESOLVABLE to the walk (no home is wired into the shell read boundary — deliberately: `NodeExecutionEnv.homeDir` is the host's `os.homedir()` even under a `shellEnv.HOME` override, so resolving `~` against it would vouch a read of a directory the shell never expands to; the adapter's declaration is the contract to fix first, P1 candidate below): the seat answers a plain ask for it, exactly as the classify seat always did — see the next paragraph for what the gate then does with that ask.
|
|
42
|
+
- **BREAKING (behaviour narrowing, one lane).** Under `shellGate` absent / `"off"`, a Bash or Monitor command that reads a deny-listed path (`cat .claude/settings.json`, `cat ~/.ssh/id_rsa`, `head`/`sed -n`/`wc`/`grep` of one), a path outside the session's roots, or a RECURSIVE form whose walk reaches into the root (`grep -r … .`, `find .`, `ls -R`, `du .` — the walk reads the deny-listed files inside the root; the same mandated ask `"classify"` already gave, with cause `shell.recursive_read_unbounded`) goes from ZERO asks to ONE mandated ask (rule-clearable: no; auto-mode classifier: no — the ask is structural). Affected lanes: `permissionMode: "bypassPermissions"` through the shell (server's `shellGateForMode` writes `"off"` for that mode only) and a direct integration that states no `permissionMode` / no `shellGate`. NOT affected: the shell's ordinary lanes — server's `shellGateForMode` writes `"classify"` for every other mode and the cli always passes a mode, so those lanes already asked for exactly these commands. An in-root non-listed read, a no-operand command (`echo`, `pwd`) and every non-reader (`rm`, `curl`, `git`, `npm`) keep zero asks under `"off"`.
|
|
43
|
+
- BREAKING (consequence on the same lane, disclosed, not fixed here — ticket below) · under `shellGate` absent / `"off"`, Bash and Monitor now sit on the `"maybe"` tier, and `gate-lanes.ts` derives a park's safety axis from the STATIC tier: a POLICY or hook ask on Bash (`npm run build` under a `defaultAction:"ask"` policy) that the boundary seat vouched now parks as the non-budgetable `irreversible_ask` kind with `safety.irreversible: true` and the `shell` axis on its descriptor, where it parked as a budgetable `human` ask before (`"classify"` has always parked it this way). A budget resolver auto-approves fewer asks on this lane. The fix (the safety axis read off THIS call's seat judgment rather than the static tier) touches `gate-lanes.ts` and the classify lane and is its own ticket.
|
|
44
|
+
- BREAKING · `RiskDescriptor.shellGateDoctrine` widens from `"classify" | "always"` to the whole `TaskSpec.shellGate` vocabulary `"off" | "classify" | "always"` (a shell-gated durable park under the `"off"` doctrine now persists `"off"` where it used to omit the key); a consumer that pinned the two-member set re-pins.
|
|
45
|
+
- Wire-observable (additive, per-key rows in `docs/wire-consumption/unreleased-696.md`): under `"off"` the tool roster's Bash/Monitor rows carry `irreversibility: "maybe"` + `reversibilityProbe: true`; a Bash/Monitor ask under `"off"` carries `riskAxes.irreversible: true` (the seat tier's reading, as under `"classify"`), grades as the `shell` axis (severity 3, never `irreversible` 4), attributes `tightenedBy: "shell_gate"` / deny `source: "shellGate"`, and its card's `ruleOffersAbsence` reads `"mandated"` / `"shadowed"`.
|
|
46
|
+
- Disclosure text · the `shell-gate-off` config note (phase `config`, classification `shell-gate-off`) now reads "the read boundary is still judged on every command (a deny-listed or out-of-root read asks; the ask is mandated), but no residual shell-risk classifier is installed …" — the previous "no shell safety-axis fold applies" would be false.
|
|
47
|
+
- **Not changed (measured, disclosed — and the headline example's `~` spelling is IN this list).** (i) An operand the walk cannot RESOLVE or DECIDE — `cat ~/.ssh/id_rsa` with no home declared, `cat .s?/x`, `cat *.log` (an unexpanded glob) — gets a plain, un-mandated ask from BOTH probe seats, and the gate's read-only shell arm (7.11.0, a reader of command NAMES: `cat` is read-only) then CLEARS that ask: measured end-to-end at the base commit under `"classify"` and after this change under both doctrines, ZERO asks, the command runs. The `~` spelling of the ticket's own example therefore still runs unasked; the absolute spelling (`cat /Users/<u>/.ssh/id_rsa`) is resolved, declared and mandated. This is baseline behaviour (RB-474's "undecided ⇒ ask" and the `~` fail-closed have been cleared by that arm since 7.11.0), pinned here as a tripwire; the candidate fix — the unread arms joining the mandated family under the `probe_unanswered` doctrine ("the boundary is unknown, which no standing yes may stand in for") — makes every glob read ask un-clearably on the shell's ordinary lanes and is a ruling, not this car's. (ii) The boundary seat's reach is the read faces' reach and nothing wider: a read — deny-listed OR out-of-root — hidden in a shape the faces refuse whole (`cat .claude/settings.json | base64`, `cat /etc/passwd | base64` — an unlisted segment; `echo $(cat x)` — a substitution; `cat $DIR/x` — a variable operand; `env cat ~/.ssh/id_rsa` — a launcher; `/bin/cat /etc/passwd` — a path-prefixed program) is not seen under `"off"` and runs with zero asks; `"classify"` asks for every one of those shapes (as before — mandated where its text-level containment gate recognizes the reader, `env cat` / `/bin/cat` / the piped out-of-root read). The same residual reaches TWO shapes that are plain listed readers: `tail -f <path>` (follow mode) and a special file (`cat /proc/1/environ`) — the compound face refuses them BEFORE it walks their operands, so no structural finding exists for the boundary seat to read: `tail -f /etc/passwd` and `tail -f .claude/settings.json` run with zero asks under `"off"` (Monitor's own idiom is `tail -f`), while `"classify"` mandates them through the text gate. Fix: the compound face keeps walking the listed readers' operands on an availability / special-file refusal and surfaces `readDenied` / `outOfRootRead` beside the `reason` (a scan-form change in `bash-readonly-classifier.ts`; `reason` and the classify verdict unchanged; the second form of #714 — a judge refusing before it walks the operands leaves no finding for a downstream seat to read). The boundary seat deliberately does NOT consult that text-level gate (`classifyOutOfRootReadGate`): it re-scans the command under conservative assumptions that may ADD a mandate to an existing ask but would ORIGINATE false asks on non-readers (`env rm /tmp/cache`, `env echo /etc/passwd`, `pushd . && echo hello` — measured, all zero asks under `"off"`). Closing the in-shape gap is a classifier scan-form change (ticket #663, the two read-only classifiers' unification). `run_in_background` is not the boundary's question (WHEN, not WHERE): a backgrounded in-root read runs under `"off"`; a backgrounded deny-listed read is mandated under both doctrines. The containment judgment is LEXICAL against the CANONICAL roots (the classifier is pure, no I/O — its standing contract): an absolute operand spelled through a symlink alias of the root (macOS `/var/…` for `/private/var/…`, `/tmp/…` for `/private/tmp/…`) reads as out-of-root and asks (mandated) — pre-existing under `"classify"`, now reachable in the `"off"` lane too; two Monitor test fixtures that watched a `tail -f /var/folders/…` path under a canonical root were re-spelled through `realpathSync` (the convention the read-face fixtures already follow). A relative operand resolves against the live tracked cwd and is unaffected.
|
|
48
|
+
- **Review rounds (adversarial, three).** Round 1 — five findings, all reproduced: three same-root (the boundary-only seat consumed the text-level containment gate, which mis-resolved a loop body's `cd`, originated asks on launchers, and let a `cd`-dependent refusal read as grammar) → adopted as one change (structural members + answer-comparison attribution); one ruled out of this car (the adapter's `homeDir` ignores a `shellEnv.HOME` override — the seat therefore takes no home; #712); one ledgered (policy asks park as `irreversible_ask` under `"off"` — static-tier safety axis; #713). Round 2 — two findings: the attribution's remaining blind spot (a deny-listed read masked by a glob in the same loop body; an unmodelled `cd -` after an out-of-root read) → adopted (attribute by the face's sentence, not by presence); the early-refusal residual (`tail -f`, `/proc/…`) → disclosed, #714 second form. Round 3 (narrow, the attribution alone) — approve, no findings.
|
|
49
|
+
- Pins: `test/backlog696-read-boundary-any-doctrine.test.ts` (28 — on the real load path Runner prepare → NodeExecutionEnv → gate: G-a/G-b default-doctrine mandated asks with a clean record and `ruleOffersAbsence` `"shadowed"` under a covering rule (G-e), G-c/G-d/`touch` zero asks, tier-off and open-face controls (G-f), `"always"` asks (G-g), `"classify"` ≡ default on G-a/b/c/e (G-h), Monitor pair, the disclosure text, an unattended default-doctrine run parking durably with `shellGateDoctrine: "off"` on the persisted descriptor; the boundary seat's three answers at the unit face incl. the poll-loop attribution, the deny-plus-glob body, `cd -`, the `~`-without-home form and the two early-refusal tripwires). `test/backlog685-agent-config-read-deny.test.ts` "declared scope" pin flipped (off ⇒ one mandated ask, rule does not clear, control zero asks). Mutations (10, each red ≥1 in the pin set): restore the classify-only probe guard; read the walk's unresolvable refusal as a shape refusal; attribute a refusal by presence alone instead of by the face's sentence; let the text-level containment gate originate asks on the boundary seat; vouch the undecided arm; drop the declared mandate; un-attribute the poll-loop deny; skip the Monitor seat; restore the `!== "off"` doctrine filter on the descriptor.
|
|
50
|
+
|
|
51
|
+
### Behaviour — deferred tool disclosure is one threshold gate over the engine's candidates; the caller's `deferTools` list stays literal (#695; ruling 「效果与 CC 一致;更差则对齐」; CC 2.1.250 `eDn`/`XD` form for the gate and its measure; @server @cli @test)
|
|
52
|
+
- **Before.** Every remote-protocol tool (MCP and A2A), every `ToolSpec.defer: true` and every self-declared built-in (the shared-memory pair) was deferred UNCONDITIONALLY — on a two-tool MCP server the first request already carried placeholders plus an injected `ToolSearch`. `deferMode: "auto"` was a second, additive arm: it swept the caller's undeclared tools in when the whole inline face (pinned schemas included) exceeded 10% of the context window. Measured on three roster shapes (264 live runs, deepseek-v4-flash), the unconditional arm cost 16–20% more total input than inlining the same tools; CC keeps the identical mechanism behind a 10% threshold measured on the deferrable set.
|
|
53
|
+
- **After (two rules).** ① The caller's list is literal: `TaskSpec.deferTools` names defer on every request and are not measured; `alwaysLoad` (`ToolSpec.alwaysLoad`, `TaskSpec.alwaysLoadTools`, the server's `_meta["anthropic/alwaysLoad"]`) pins inline and is judged first. ② The engine's CANDIDATES — `ToolSpec.defer: true`, every remote-protocol name, self-declared built-ins, and under `deferMode: "auto"` every caller spec — sit behind ONE gate, all or nothing: deferred iff their inlined size (name + description + schema, chars ÷ 3) reaches `floor(contextWindow × 10%)` tokens; below that they ship inline in full and no `ToolSearch` is mounted. Only the candidates are measured — the caller's list, pins and non-candidates weigh nothing (CC `XD()` filter-then-count). `"auto"` is a candidate-WIDENING knob, not a second gate; engine built-ins the deployment did not name are candidates under no setting; a `deferMode` outside `"auto"`/absent refuses `config.defer_mode_invalid` (loud, no fallback). The comparison is `≥` (CC `p >= E`); a zero window is a zero threshold.
|
|
54
|
+
- BREAKING · **the default first request changes shape**: remote-protocol tools, `defer: true` tools and the shared-memory pair ship INLINE (full schema, no `ToolSearch`) whenever their combined inlined size is under 10% of the model's context window — previously they were placeholders on every request. A consumer that pinned "an MCP tool is always a placeholder" / "`ToolSearch` is present whenever an MCP server is configured" re-pins on the threshold (or names the tool in `deferTools`, which is unchanged). `deferMode: "auto"` deployments: the trip measure is now the candidate set only — a face heavy in PINNED or `deferTools` bytes no longer trips the gate.
|
|
55
|
+
- **Rules 8 → 2.** Gone: the four unconditional sources as separate rules, the auto candidate sweep as an extra arm, the inline-face measure, the `window > 0` guard, the strict `>` trip. Left: the literal list (①) and the one gate (②). The gate lives in its own module, `src/core/runner/tool-defer-gate.ts` (`deferralGate` → `{ explicit, gated, gatedChars, thresholdChars, open }`, `classifyDeferred`, `DEFER_THRESHOLD_FRACTION`, `DEFER_MODE_INVALID_CODE`); `tool-disclosure.ts` keeps the placeholder / `ToolSearch` mechanics.
|
|
56
|
+
- **Estimator note (honest reading).** The engine measures chars ÷ 3 (the mid-stream budget constant), not a tokenizer: on the measured `std` roster the 12 declared tools read 11 595 chars ≈ 3 865 estimated tokens against a 3 276-token threshold (32k window) and the gate OPENS, where the live API count of the same tools (2 842 tokens) would keep it shut; `big` (64k) and `fat` (128k) windows stay shut either way. CC's own char fallback (2.5 chars/token) would open too. Pinned as measured in `test/backlog695-defer-gate-alignment.test.ts`.
|
|
57
|
+
- **Review round 1 (adversarial; one REAL finding adopted, one confirmed-and-already-in-hand).** ① The materialized MCP roster also carries the engine's own global resource helpers (`ListMcpResourcesTool` / `ReadMcpResourceTool` / `ReadMcpResourceDirTool`); read as "every name on `mcp.tools`" they entered the remote-candidate set — measured, and deferred when the gate opened (before this change they were deferred unconditionally with the rest). Remote now means a name the protocol table minted (`protocolOf(name)` — `mcp__…` / `a2a__…`), so the helpers are what they are: engine built-ins, never candidates, never measured (pinned on a resource-capable stdio server, gate shut and gate open). ② Ten placeholder-mechanics pins outside the derived set relied on unconditional deferral — re-pinned with the ballast candidate (below).
|
|
58
|
+
- **Review round 2 (adversarial; two REAL test-precondition findings adopted).** ① The registry-exhaustion pin (CC207 P1-4, `tool-disclosure-integration.test.ts`) had gained the ballast as a third placeholder the script never activated, so "every deferred tool activated" was no longer reached — the two tools now defer through the literal `deferTools` list, exhaustion is reached in two selects. ② The RB-414 stale-schema pin (`rb422-deferred-directcall-hardening.test.ts`) had fallen off the placeholder path (a one-tool remote roster sits under the threshold, so the direct-call resolver under test was never exercised) — the remote tool is now on the literal list and its placeholder is asserted before the refresh and immediately before the direct call. Round 2 confirmed the round-1 helper-provenance fix and the gate invariants.
|
|
59
|
+
- **Fail-open reverse check — what still defers below the threshold?** Only the caller's own `deferTools` list (by design: an intentional deferral is not a size question; the shell relies on it to keep `Workflow` out of the model's view). Nothing else: no candidate, no built-in, no undeclared tool.
|
|
60
|
+
- Pins: `test/backlog695-defer-gate-alignment.test.ts` (the three-shape roster: default gate readings per shape, auto widening per shape, auto below its widened threshold, the face-wide-measure mutation guard; real load path with a stdio MCP server: below-threshold inline + no ToolSearch, `deferTools` literal beside an inline sibling, small-window placeholders + `alwaysLoadTools` pin); `test/tool-disclosure.test.ts` (sources, literal list disjoint from candidates, auto widening + measure, candidate-only measure, `≥` boundary both sides, bad value, zero window, invariants, RB-400-a); `test/runner-support-coverage.test.ts`; `test/rb422-deferred-directcall-hardening.test.ts` DD-4 (a pin's bytes never open the gate; small-window control); `test/public-knob-liveness.test.ts` (auto through the Runner). Re-pinned to the gate (small window opens it): `mcp.test.ts` RB-400-a e2e, `shared-memory-mount.test.ts`, `memory-tools-mount.test.ts`, `prepare-defer-classify-transcript.test.ts` (plus the roomy-window inline control), `tool-materialize-static.test.ts`, `policy-name-audit.test.ts`, `deferred-materialize-constrained-decoding.test.ts`, `backlog126-definetool-axis-carry.test.ts`.
|
|
61
|
+
|
|
3
62
|
## 7.14.0 — 2026-09-11
|
|
4
63
|
|
|
5
64
|
### Narrowing — the agent's own key-bearing settings files are refused on the structured read faces by default (#685; P0; ruling B: the session record stays verbatim, the fix is on the READ face; sema form — CC 2.1.250 has no read-face deny table, the table itself is the registered divergence; @server @cli @test)
|
|
@@ -234,8 +234,8 @@ export declare function classifierMayAnswer(origin: AskOrigin): boolean;
|
|
|
234
234
|
* · `egress` — the tool's own external-write mark, judged next: it is the tool's declaration even
|
|
235
235
|
* when the coarse doctrine also installed a shell tier on the same seat;
|
|
236
236
|
* · `shellGated` + tier `"always"` — the operator's per-call confirmation doctrine
|
|
237
|
-
* (`"operator_always"`); the
|
|
238
|
-
* lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
|
|
237
|
+
* (`"operator_always"`); the shell seat installs `"maybe"` under the off and classify doctrines,
|
|
238
|
+
* and THOSE asks stay the rule lane's home turf (`undefined`) — that is the don't-ask-again main case, and the per-call member
|
|
239
239
|
* above is deliberately the ONLY thing that carves a mandate out of it;
|
|
240
240
|
* · a tool's OWN `"always"`/`"maybe"` irreversibility tier without the doctrine (`"tool_marks"`).
|
|
241
241
|
*
|
|
@@ -8,7 +8,7 @@ import type { RepairBundle } from "../agents/repair-loop.js";
|
|
|
8
8
|
import type { WorkspaceHandle } from "./remote-env.js";
|
|
9
9
|
import type { ConsolidationNote } from "./runner/memory-consolidation.js";
|
|
10
10
|
import type { SessionPermissionRules } from "./session-policy-store.js";
|
|
11
|
-
import type { NestedUsageAccum, TaskResult } from "./types.js";
|
|
11
|
+
import type { NestedUsageAccum, ShellGateDoctrine, TaskResult } from "./types.js";
|
|
12
12
|
import type { ActiveWorktreeSession } from "../internal/harness-types.js";
|
|
13
13
|
/**
|
|
14
14
|
* design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
|
|
@@ -120,10 +120,9 @@ export interface RiskDescriptor {
|
|
|
120
120
|
/** ToolEmu-style severity tier 1..5 (5 = most severe). The inbox sorts DESC by this. Deterministic — the
|
|
121
121
|
* pure {@link riskSeverity} of {@link axes}. The ORDERING is what matters (the inbox's triage key). */
|
|
122
122
|
severity: 1 | 2 | 3 | 4 | 5;
|
|
123
|
-
/** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
* `human` ask. Self-contained so the inbox needn't cross-ref `safetyAxis`. */
|
|
123
|
+
/** Which safety axes tripped — for `irreversible_ask`, derived from the D-2 {@link SafetyAxis} (+ the shell
|
|
124
|
+
* seat). `shell` marks a tighten by the engine's shell seat (read-boundary judge and/or `shellGate` doctrine,
|
|
125
|
+
* NO explicit per-tool egress/irreversible mark). Empty `{}` for a plain budgetable `human` ask. */
|
|
127
126
|
axes: {
|
|
128
127
|
egress?: boolean;
|
|
129
128
|
irreversible?: boolean;
|
|
@@ -131,12 +130,12 @@ export interface RiskDescriptor {
|
|
|
131
130
|
};
|
|
132
131
|
/** The tool whose call is gated (mirrors the gate's `toolName`). */
|
|
133
132
|
toolName: string;
|
|
134
|
-
/**
|
|
135
|
-
*
|
|
136
|
-
*
|
|
137
|
-
*
|
|
138
|
-
* answer that in the field. */
|
|
139
|
-
shellGateDoctrine?:
|
|
133
|
+
/** The run's resolved shellGate doctrine, present ONLY when this ask was minted through the engine's shell
|
|
134
|
+
* seat (coarse `always`; a `classify` probe that declined to auto-allow; the read-boundary judge asking under
|
|
135
|
+
* `off` — the boundary is judged under every doctrine, so the vocabulary is `TaskSpec.shellGate`'s whole).
|
|
136
|
+
* Absent for per-tool safety-axis / plain policy asks. Answers "which doctrine was live when this gate
|
|
137
|
+
* fired" from the persisted row alone — three artifact layers could not answer that in the field. */
|
|
138
|
+
shellGateDoctrine?: ShellGateDoctrine;
|
|
140
139
|
/** A REDACTED, length-capped ONE-LINE summary of the call (the command for a shell gate; a brief key-arg
|
|
141
140
|
* digest otherwise) for the inbox preview. NEUTRALIZED via
|
|
142
141
|
* {@link import("./permission-rule-model.js").renderUntrustedCommandText} + length-capped (it is
|
|
@@ -406,11 +405,11 @@ export declare function buildRiskDescriptor(input: {
|
|
|
406
405
|
args: unknown;
|
|
407
406
|
/** The D-2 {@link SafetyAxis} threaded to the mint (egress/irreversible), or `undefined` for a plain ask. */
|
|
408
407
|
safety?: SafetyAxis;
|
|
409
|
-
/** True ONLY when this is a
|
|
410
|
-
* mark) — drives the `shell` axis + severity
|
|
408
|
+
/** True ONLY when this is a shell call whose tier is the engine's shell seat's (read-boundary judge and/or
|
|
409
|
+
* `shellGate` doctrine, no explicit per-tool egress/irreversible mark) — drives the `shell` axis + severity 3. */
|
|
411
410
|
shellGated?: boolean;
|
|
412
411
|
/** The resolved doctrine to persist when `shellGated` (see {@link RiskDescriptor.shellGateDoctrine}). */
|
|
413
|
-
shellGateDoctrine?:
|
|
412
|
+
shellGateDoctrine?: ShellGateDoctrine;
|
|
414
413
|
/** #144: the matched-but-outranked persisted rule to persist (see {@link RiskDescriptor.shadowedRule}). */
|
|
415
414
|
shadowedRule?: string;
|
|
416
415
|
/** backlog #239: the reversibility probe's cause for not clearing this call (see
|
|
@@ -1110,7 +1109,7 @@ export interface CheckpointState {
|
|
|
1110
1109
|
rev: number;
|
|
1111
1110
|
rules: SessionPermissionRules;
|
|
1112
1111
|
}>;
|
|
1113
|
-
shellGate?:
|
|
1112
|
+
shellGate?: ShellGateDoctrine;
|
|
1114
1113
|
/** The chain's AUTO-MODE INTENT at suspend (data half, same law as `shellGate`): `true` when the
|
|
1115
1114
|
* suspended leg was an auto-mode task — its own seat, the bit its live chain carried, or the bit
|
|
1116
1115
|
* an earlier suspend of the same chain recorded (carried forward across a re-suspend). A resume
|
|
@@ -14,6 +14,8 @@ export const NON_GOVERNANCE_MEMORY_CODES = new Set([
|
|
|
14
14
|
"memory.announce_failed",
|
|
15
15
|
"memory.partition_adopt_failed",
|
|
16
16
|
"memory.partition_split",
|
|
17
|
+
"memory.layout_migrated",
|
|
18
|
+
"memory.layout_unowned_root_content",
|
|
17
19
|
"memory.tail",
|
|
18
20
|
"memory.pollution_mark_failed",
|
|
19
21
|
"memory.session_polluted",
|
package/dist/core/hooks.d.ts
CHANGED
|
@@ -1326,9 +1326,10 @@ export interface ToolGateInput {
|
|
|
1326
1326
|
*/
|
|
1327
1327
|
onHookError?: (err: unknown) => void;
|
|
1328
1328
|
/**
|
|
1329
|
-
* design/134 R5: true when this tool's `irreversibility` tier was installed by the
|
|
1330
|
-
* `TaskSpec.shellGate` doctrine
|
|
1331
|
-
*
|
|
1329
|
+
* design/134 R5: true when this tool's `irreversibility` tier was installed by the ENGINE'S SHELL SEAT
|
|
1330
|
+
* (the read-boundary judge under every doctrine, plus the `TaskSpec.shellGate` doctrine's residual-risk
|
|
1331
|
+
* classifier — design/80 D-E; the seat's `shellGatedTools` membership), not an explicit per-tool mark —
|
|
1332
|
+
* attributes a tighten-deny to `source:"shellGate"` instead of `"safety"`.
|
|
1332
1333
|
*/
|
|
1333
1334
|
shellGated?: boolean;
|
|
1334
1335
|
/**
|
|
@@ -144,32 +144,54 @@ export declare function replayExternalSettlementEffects(controlDir: string, inpu
|
|
|
144
144
|
sessionId: string;
|
|
145
145
|
}>;
|
|
146
146
|
export declare const SESSION_ACCOUNTS_FILE = "session-accounts.json";
|
|
147
|
+
/** One member of the sticky set: the ledger form `(scope, rel)` — `rel` relative to the scope's
|
|
148
|
+
* home, `/`-separated. Location-free, so a home move never re-keys the membership. */
|
|
149
|
+
export interface UnattributedMember {
|
|
150
|
+
scope: string;
|
|
151
|
+
rel: string;
|
|
152
|
+
}
|
|
147
153
|
export interface SessionAccountRow {
|
|
148
154
|
sessionId: string;
|
|
149
155
|
openedAt: number;
|
|
150
|
-
/** The STICKY unattributed set
|
|
151
|
-
*
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
unattributed:
|
|
156
|
+
/** The STICKY unattributed set, frozen at materialize (§3.6 归属判据): pre-existing, uncommitted
|
|
157
|
+
* files seen while a FOREIGN dangling open row stood. Membership never shrinks on later writes
|
|
158
|
+
* (the sticky law); it clears only when the row is re-opened with a fresh classification (the
|
|
159
|
+
* residue was adjudicated by an intervening harvest). */
|
|
160
|
+
unattributed: UnattributedMember[];
|
|
161
|
+
/** The v1 members (memory-dir-relative paths) the registry could not attribute at the one-time
|
|
162
|
+
* conversion. Kept, never dropped; while any stands, EVERY membership question about
|
|
163
|
+
* this session answers "unattributed" (over-marking is the safe direction). */
|
|
164
|
+
unattributable?: string[];
|
|
155
165
|
closedAt?: number;
|
|
156
166
|
/** r7-3 — the host valve closed this row WITHOUT adjudication: it stops dangling but keeps
|
|
157
167
|
* triggering the residue arm until the next FULL-domain harvest closes an account normally
|
|
158
168
|
* (an unadjudicated close must not launder the residue window it covered). */
|
|
159
169
|
unadjudicated?: true;
|
|
160
170
|
}
|
|
171
|
+
/** The one-time v1 key-form conversions of the three engine sidecars, in one call (each in its own
|
|
172
|
+
* lock, each idempotent). The layout adoption runs this before any later scope registration can
|
|
173
|
+
* change what a v1 key's first segment names. */
|
|
174
|
+
export declare function convertV1SidecarKeys(controlDir: string): void;
|
|
161
175
|
/** The dangling-trigger read: open rows (and unadjudicated closes) belonging to OTHER sessions.
|
|
162
176
|
* These are what arm the unattributed residue classification at a session's materialize. */
|
|
163
177
|
export declare function foreignDanglingSessionAccounts(controlDir: string, selfSessionId: string): SessionAccountRow[];
|
|
178
|
+
/** The membership question the harvest asks per file: is `(scope, rel)` in the session's sticky
|
|
179
|
+
* set? A session carrying members the conversion could not attribute answers yes for every seat. */
|
|
180
|
+
export interface SessionUnattributedSet {
|
|
181
|
+
has(scope: string, rel: string): boolean;
|
|
182
|
+
/** True when no seat can answer yes (no members, none unattributable). */
|
|
183
|
+
readonly empty: boolean;
|
|
184
|
+
}
|
|
185
|
+
export declare function emptyUnattributedSet(): SessionUnattributedSet;
|
|
164
186
|
/** Open (or re-open, on a same-session resume) the session's account row, persisting the sticky
|
|
165
187
|
* unattributed classification frozen at THIS materialize. */
|
|
166
188
|
export declare function openSessionAccount(controlDir: string, input: {
|
|
167
189
|
sessionId: string;
|
|
168
190
|
now: () => number;
|
|
169
|
-
unattributed: readonly
|
|
191
|
+
unattributed: readonly UnattributedMember[];
|
|
170
192
|
}): void;
|
|
171
193
|
/** The current session's sticky unattributed set (the harvest's residue-arm input). */
|
|
172
|
-
export declare function sessionUnattributedSet(controlDir: string, sessionId: string):
|
|
194
|
+
export declare function sessionUnattributedSet(controlDir: string, sessionId: string): SessionUnattributedSet;
|
|
173
195
|
/** Close the session's account row — called ONLY after a FULL-domain harvest (zero BUDGET-deferred
|
|
174
196
|
* files, i.e. `HarvestReport.degraded` absent): a partial harvest's close would launder the deferred
|
|
175
197
|
* residue window (§3.6 序则②). A normal full close also clears every standing `unadjudicated` flag
|
|
@@ -209,8 +231,10 @@ export interface HoldRow {
|
|
|
209
231
|
op: "add" | "update";
|
|
210
232
|
/** CAS anchor at capture time (update form): release refuses to blind-write over a later edit. */
|
|
211
233
|
baseRev?: string;
|
|
212
|
-
/**
|
|
213
|
-
|
|
234
|
+
/** The plane seat the file was removed from, in the ledger form: relative to the write scope's
|
|
235
|
+
* home (`<memoryDir>/<label>`), `/`-separated — `${slug}.md`. Location-free, so a home move never
|
|
236
|
+
* re-keys a hold. */
|
|
237
|
+
rel: string;
|
|
214
238
|
/** The entry slug (path relative to the scope dir, sans `.md`) — the release leg's projection
|
|
215
239
|
* identity and its filename-scan input. */
|
|
216
240
|
slug: string;
|
|
@@ -263,7 +287,8 @@ export declare function noReplaceRestore(stagingPath: string, destPath: string):
|
|
|
263
287
|
*/
|
|
264
288
|
export declare function openInstructionHold(controlDir: string, input: {
|
|
265
289
|
sessionId: string;
|
|
266
|
-
|
|
290
|
+
/** The seat in the ledger form (relative to the write scope's home). */
|
|
291
|
+
rel: string;
|
|
267
292
|
absPath: string;
|
|
268
293
|
content: string;
|
|
269
294
|
entryId?: string;
|
|
@@ -290,11 +315,11 @@ export declare function openInstructionHold(controlDir: string, input: {
|
|
|
290
315
|
* (the file stays on the plane and walks the ordinary/§3.6 arms; never "capture whatever is
|
|
291
316
|
* there now" — that would hold a third writer's bytes under the first writer's account);
|
|
292
317
|
* - `capturing` + custody absent + plane file gone ⇒ `capture_lost` (nothing to hold).
|
|
293
|
-
* - `held` + plane file REAPPEARED at the
|
|
318
|
+
* - `held` + plane file REAPPEARED at the seat ⇒ left alone (a fresh write is a fresh file —
|
|
294
319
|
* the next harvest judges it on its own; the hold's custody bytes stay the hold's).
|
|
295
320
|
*/
|
|
296
321
|
export declare function reconcileHolds(controlDir: string, input: {
|
|
297
|
-
|
|
322
|
+
homeOf: (scope: string) => string;
|
|
298
323
|
now: () => number;
|
|
299
324
|
}): void;
|
|
300
325
|
/** Dispose one held row: custody bytes move to the quarantine directory (host-auditable), the row
|