@fractalsynapse/dsh-chapters 0.0.0-stage → 0.2.21
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/LICENSE +14 -0
- package/README.md +76 -2
- package/cordis.patch.yml +363 -0
- package/docs/architecture.md +692 -0
- package/docs/autorun-guide.md +248 -0
- package/docs/backlog.md +2022 -0
- package/docs/contract.md +938 -0
- package/docs/development.md +990 -0
- package/docs/host-compaction-seam.md +225 -0
- package/docs/host-protected-head-ask.md +80 -0
- package/docs/knowledge-repo.md +1332 -0
- package/docs/plans-carriage.md +571 -0
- package/docs/project-state.md +642 -0
- package/docs/provider.md +98 -0
- package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
- package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
- package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
- package/docs/verify.md +561 -0
- package/lib/archive.d.ts +161 -0
- package/lib/archive.js +229 -0
- package/lib/archive.js.map +1 -0
- package/lib/arrival.d.ts +23 -0
- package/lib/arrival.js +125 -0
- package/lib/arrival.js.map +1 -0
- package/lib/artifact-query.d.ts +62 -0
- package/lib/artifact-query.js +187 -0
- package/lib/artifact-query.js.map +1 -0
- package/lib/attach.d.ts +46 -0
- package/lib/attach.js +120 -0
- package/lib/attach.js.map +1 -0
- package/lib/auto-save.d.ts +145 -0
- package/lib/auto-save.js +334 -0
- package/lib/auto-save.js.map +1 -0
- package/lib/aux-message.d.ts +17 -0
- package/lib/aux-message.js +20 -0
- package/lib/aux-message.js.map +1 -0
- package/lib/client.js +1419 -0
- package/lib/client.js.map +1 -0
- package/lib/codegraph.d.ts +76 -0
- package/lib/codegraph.js +190 -0
- package/lib/codegraph.js.map +1 -0
- package/lib/commands.d.ts +52 -0
- package/lib/commands.js +946 -0
- package/lib/commands.js.map +1 -0
- package/lib/compose.d.ts +30 -0
- package/lib/compose.js +185 -0
- package/lib/compose.js.map +1 -0
- package/lib/continue-core.d.ts +245 -0
- package/lib/continue-core.js +405 -0
- package/lib/continue-core.js.map +1 -0
- package/lib/diag.d.ts +55 -0
- package/lib/diag.js +186 -0
- package/lib/diag.js.map +1 -0
- package/lib/engine-core.d.ts +301 -0
- package/lib/engine-core.js +531 -0
- package/lib/engine-core.js.map +1 -0
- package/lib/engine-note.d.ts +35 -0
- package/lib/engine-note.js +32 -0
- package/lib/engine-note.js.map +1 -0
- package/lib/engine.d.ts +329 -0
- package/lib/engine.js +1179 -0
- package/lib/engine.js.map +1 -0
- package/lib/enrich-queue.d.ts +66 -0
- package/lib/enrich-queue.js +126 -0
- package/lib/enrich-queue.js.map +1 -0
- package/lib/enrich-store.d.ts +31 -0
- package/lib/enrich-store.js +103 -0
- package/lib/enrich-store.js.map +1 -0
- package/lib/enrich-wire.d.ts +163 -0
- package/lib/enrich-wire.js +353 -0
- package/lib/enrich-wire.js.map +1 -0
- package/lib/enrich.d.ts +102 -0
- package/lib/enrich.js +267 -0
- package/lib/enrich.js.map +1 -0
- package/lib/gitops.d.ts +107 -0
- package/lib/gitops.js +539 -0
- package/lib/gitops.js.map +1 -0
- package/lib/index.d.ts +241 -0
- package/lib/index.js +1162 -0
- package/lib/index.js.map +1 -0
- package/lib/indexing.d.ts +98 -0
- package/lib/indexing.js +223 -0
- package/lib/indexing.js.map +1 -0
- package/lib/injections.d.ts +90 -0
- package/lib/injections.js +165 -0
- package/lib/injections.js.map +1 -0
- package/lib/last-resort.d.ts +24 -0
- package/lib/last-resort.js +27 -0
- package/lib/last-resort.js.map +1 -0
- package/lib/log-shape.d.ts +38 -0
- package/lib/log-shape.js +93 -0
- package/lib/log-shape.js.map +1 -0
- package/lib/models-command.d.ts +74 -0
- package/lib/models-command.js +100 -0
- package/lib/models-command.js.map +1 -0
- package/lib/notice.d.ts +74 -0
- package/lib/notice.js +92 -0
- package/lib/notice.js.map +1 -0
- package/lib/plans.d.ts +154 -0
- package/lib/plans.js +315 -0
- package/lib/plans.js.map +1 -0
- package/lib/preset.d.ts +186 -0
- package/lib/preset.js +212 -0
- package/lib/preset.js.map +1 -0
- package/lib/prompt-keys.d.ts +7 -0
- package/lib/prompt-keys.js +8 -0
- package/lib/prompt-keys.js.map +1 -0
- package/lib/prompt-loop.d.ts +73 -0
- package/lib/prompt-loop.js +147 -0
- package/lib/prompt-loop.js.map +1 -0
- package/lib/prompt-push.d.ts +47 -0
- package/lib/prompt-push.js +115 -0
- package/lib/prompt-push.js.map +1 -0
- package/lib/prompt-runner.d.ts +48 -0
- package/lib/prompt-runner.js +90 -0
- package/lib/prompt-runner.js.map +1 -0
- package/lib/prompt-state.d.ts +142 -0
- package/lib/prompt-state.js +101 -0
- package/lib/prompt-state.js.map +1 -0
- package/lib/prompt.d.ts +270 -0
- package/lib/prompt.js +408 -0
- package/lib/prompt.js.map +1 -0
- package/lib/provider.d.ts +44 -0
- package/lib/provider.js +66 -0
- package/lib/provider.js.map +1 -0
- package/lib/redact.d.ts +24 -0
- package/lib/redact.js +85 -0
- package/lib/redact.js.map +1 -0
- package/lib/registry.d.ts +187 -0
- package/lib/registry.js +235 -0
- package/lib/registry.js.map +1 -0
- package/lib/removal.d.ts +159 -0
- package/lib/removal.js +556 -0
- package/lib/removal.js.map +1 -0
- package/lib/render.d.ts +97 -0
- package/lib/render.js +530 -0
- package/lib/render.js.map +1 -0
- package/lib/repair-run.d.ts +76 -0
- package/lib/repair-run.js +506 -0
- package/lib/repair-run.js.map +1 -0
- package/lib/repair.d.ts +208 -0
- package/lib/repair.js +357 -0
- package/lib/repair.js.map +1 -0
- package/lib/repo.d.ts +78 -0
- package/lib/repo.js +184 -0
- package/lib/repo.js.map +1 -0
- package/lib/rules-commands.d.ts +46 -0
- package/lib/rules-commands.js +379 -0
- package/lib/rules-commands.js.map +1 -0
- package/lib/rules.d.ts +267 -0
- package/lib/rules.js +478 -0
- package/lib/rules.js.map +1 -0
- package/lib/save-core.d.ts +72 -0
- package/lib/save-core.js +220 -0
- package/lib/save-core.js.map +1 -0
- package/lib/search.d.ts +112 -0
- package/lib/search.js +282 -0
- package/lib/search.js.map +1 -0
- package/lib/session-rules.d.ts +222 -0
- package/lib/session-rules.js +454 -0
- package/lib/session-rules.js.map +1 -0
- package/lib/signature.d.ts +45 -0
- package/lib/signature.js +199 -0
- package/lib/signature.js.map +1 -0
- package/lib/store.d.ts +156 -0
- package/lib/store.js +368 -0
- package/lib/store.js.map +1 -0
- package/lib/sync.d.ts +220 -0
- package/lib/sync.js +810 -0
- package/lib/sync.js.map +1 -0
- package/lib/tools.d.ts +113 -0
- package/lib/tools.js +1090 -0
- package/lib/tools.js.map +1 -0
- package/lib/treedx/client.d.ts +57 -0
- package/lib/treedx/client.js +82 -0
- package/lib/treedx/client.js.map +1 -0
- package/lib/treedx/provider.d.ts +39 -0
- package/lib/treedx/provider.js +479 -0
- package/lib/treedx/provider.js.map +1 -0
- package/lib/treedx/state.d.ts +27 -0
- package/lib/treedx/state.js +65 -0
- package/lib/treedx/state.js.map +1 -0
- package/lib/types.d.ts +210 -0
- package/lib/types.js +31 -0
- package/lib/types.js.map +1 -0
- package/lib/version.d.ts +9 -0
- package/lib/version.js +10 -0
- package/lib/version.js.map +1 -0
- package/lib/vocabulary.d.ts +41 -0
- package/lib/vocabulary.js +118 -0
- package/lib/vocabulary.js.map +1 -0
- package/lib/warnings.d.ts +10 -0
- package/lib/warnings.js +18 -0
- package/lib/warnings.js.map +1 -0
- package/package.json +151 -3
package/docs/verify.md
ADDED
|
@@ -0,0 +1,561 @@
|
|
|
1
|
+
# Verification
|
|
2
|
+
|
|
3
|
+
The checklist in `AGENTS.md` is short because it only lists what matters. This is how to actually
|
|
4
|
+
perform each check, with the numbers to record.
|
|
5
|
+
|
|
6
|
+
## Test Strategy First
|
|
7
|
+
|
|
8
|
+
Decide the harness in Phase 0, before Phase 1 code. Both reference plugins test their own way:
|
|
9
|
+
`dsh-compact` uses vitest (`vitest.config.ts`, `test/`, `e2e/`), `dsh-session-fork` uses `bun test`
|
|
10
|
+
with `tests/`.
|
|
11
|
+
|
|
12
|
+
The point of choosing early: several core guarantees are *pure functions* and must be unit-tested, not
|
|
13
|
+
hand-verified on a live profile every time.
|
|
14
|
+
|
|
15
|
+
| Unit-testable without a running harness | Why it must be automated |
|
|
16
|
+
|---|---|
|
|
17
|
+
| Range validation (ascending, non-overlapping, coverage ≤ ceiling) | A silent gap drops archived history forever |
|
|
18
|
+
| Chapter renderer: event slice → Markdown, including fence-escaping and artifact deferral | Deterministic input/output; the only cheap way to catch a renderer truncating at an embedded ```` ``` ```` |
|
|
19
|
+
| `anchoredBoundaryOf` → archive ceiling | Vendored kernel logic; drift is invisible until a chain is corrupted |
|
|
20
|
+
| TOC assembly from a fixture registry (flat, chronological, back-links) | Proves the DAG flattening without a live session |
|
|
21
|
+
| Preflight budget math and refusal message | The "never truncate" guarantee is one `if` away from being lost |
|
|
22
|
+
|
|
23
|
+
Kernel-dependent behaviour (durable create, sidebar listing, cache behaviour) cannot be unit-tested and
|
|
24
|
+
needs the live procedure below.
|
|
25
|
+
|
|
26
|
+
## Live Procedure
|
|
27
|
+
|
|
28
|
+
Install for every step, after building:
|
|
29
|
+
|
|
30
|
+
```bash
|
|
31
|
+
tsc -p tsconfig.json
|
|
32
|
+
dsh plugin --profile web add "link:$(pwd)"
|
|
33
|
+
# restart: Ctrl-C, then `dsh web`, then refresh the page
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
A restart recomposes every session's header, which resets cache state profile-wide. **All cache
|
|
37
|
+
measurements are only meaningful within one process generation** — record the generation (restart) with
|
|
38
|
+
the numbers, or the measurement is unreproducible.
|
|
39
|
+
|
|
40
|
+
### The Gate Checks
|
|
41
|
+
|
|
42
|
+
**G1 — The child is smaller than the parent.** *The* regression test for the seeding mistake.
|
|
43
|
+
Record the first-request `usage` of both. Expected: child ≈ system prompt + TOC + note, and **materially
|
|
44
|
+
below** the parent's. A child near the parent's size means it was seeded from the log — invariant 2
|
|
45
|
+
violated. Every other check can pass while this one fails silently, so check it first.
|
|
46
|
+
|
|
47
|
+
**G2 — The parent's cache is undisturbed.** Continue the parent; record `usage.cacheReadTokens` on the
|
|
48
|
+
step before the continuation and the step after. Expected: both high, no drop.
|
|
49
|
+
|
|
50
|
+
**G3 — The child builds a stable cache.** Send two messages in the child; `cacheReadTokens` should be
|
|
51
|
+
near-zero on the first and high on the second.
|
|
52
|
+
|
|
53
|
+
Steps 4, 7, 8 of the `AGENTS.md` list are G1–G3. They are the whole point of the design.
|
|
54
|
+
|
|
55
|
+
### Bound a mechanism by its invariants, never by a tally (#143)
|
|
56
|
+
|
|
57
|
+
A live leg that waits on a model-driven turn must assert what the mechanism **promises**, not what one run happened to
|
|
58
|
+
count. Measured proof, 2026-10-28, one commit and one realm (`heavy`, 32K window, `thresholdRatio: 0.45`, nothing
|
|
59
|
+
arrival-deferred): two live runs produced **8** and **38** `compaction/summary` events. The assertion was `passes ≤ 30`,
|
|
60
|
+
so the leg could go red with nothing broken — a red test that carries no information is worse than no test, because it
|
|
61
|
+
teaches the next reader to ignore it.
|
|
62
|
+
|
|
63
|
+
**Assert instead:**
|
|
64
|
+
|
|
65
|
+
1. **Progress per step** — every pass shadows at least one node. A zero-removal pass is the infinite loop any ceiling
|
|
66
|
+
exists to catch, and it is the actual bug class.
|
|
67
|
+
2. **Monotonicity** — no node is shadowed twice; the shadowed set strictly grows. This is what rules out relief
|
|
68
|
+
circling instead of converging.
|
|
69
|
+
3. **Post-condition** — relief leaves the surface under the realm's trigger. Read it from the engine's own decision
|
|
70
|
+
note (`surface=… tok`, `window=…`, `ratio=…`, `shadowed=`, `shadowedTok=`, `nodes=`), which is durable bytes written
|
|
71
|
+
**during** the run, not a parser's guess afterwards.
|
|
72
|
+
|
|
73
|
+
A count may stay as **runaway protection**, commented as such (`PASS_CEILING = 60`, ≈2× the worst observed) and never
|
|
74
|
+
cited as the claim. Two related traps this surfaced: (a) a row whose assertion lives in a branch the harness never
|
|
75
|
+
reaches passes while asserting nothing — the unit plane cannot produce a real commit, because the base engine returns a
|
|
76
|
+
result only for a live `Session`, so pin such claims in the leg or on a pure function that is genuinely called; and
|
|
77
|
+
(b) a field printed from a config key the realm does not carry reads `?` forever — measure the shape you *read*
|
|
78
|
+
(#153's `window=?`).
|
|
79
|
+
|
|
80
|
+
### Correctness Checks
|
|
81
|
+
|
|
82
|
+
| # | Check | Procedure | Passes when |
|
|
83
|
+
|---|---|---|---|
|
|
84
|
+
| 1 | Installs | `dsh plugin --profile web add "link:$(pwd)"` | no error; note that `add` alone does not activate |
|
|
85
|
+
| 2 | Tools listed | restart, open an agent session | `chapters_segment` and `chapters_continue` in the tool list |
|
|
86
|
+
| 3 | Files written | run a continuation on a 2-topic conversation | chapter files exist under `<store-root>/<root>/chapters/` |
|
|
87
|
+
| 4 | **G1** | see above | child first request materially smaller |
|
|
88
|
+
| 5 | TOC is the first message | open the child | its entire content is the TOC notice; nothing inherited |
|
|
89
|
+
| 6 | Durable and listed | restart `dsh web` | child still in the sidebar, still resumable |
|
|
90
|
+
| 7 | **G2** | see above | parent `cacheReadTokens` unchanged |
|
|
91
|
+
| 8 | **G3** | see above | child cache builds on turn 2 |
|
|
92
|
+
| 9 | Reachability | in the child, ask it to `read` a bullet path | returns real archived text. A **reachability** test, not file-existence — pass `ls` first to see the difference |
|
|
93
|
+
| 10 | Artifact deferral | archive a turn with a large tool result; mark it `inline: false`, then `read` the reference path | artifact written and path resolves; identical results stored once; **and** the same result is still written when inlined (deferral is a copy decision, not a data decision) |
|
|
94
|
+
| 11 | Cumulative TOC | a second continuation | first fork's chapters still listed, by ancestry walk |
|
|
95
|
+
| 12 | Sibling branches | continue an ancestor twice | two correct, non-colliding TOCs, each listing only its own ancestor path |
|
|
96
|
+
| 13 | Registry survives restart | restart, continue again | ancestry and next number resume; no reset to `001` |
|
|
97
|
+
| 14 | Atomicity | kill `dsh web` mid-continuation, restart, retry the same call | no orphan citation, no duplicate numbers, no TOC pointing at a missing file |
|
|
98
|
+
| 15 | Range validation | hand it overlapping ranges and a deliberate gap | refused with the offending range named — not silently archived wrong |
|
|
99
|
+
| 16 | Preflight refusal | force a note large enough to blow the budget | refused **before** any session is created, with token numbers; **no truncation** |
|
|
100
|
+
| 17 | Tamper detection | edit a chapter body on disk, then continue | `⚠ modified since archived` surfaced from the stored hash, not silent trust |
|
|
101
|
+
| 18 | `ctx.fs` absent | run in a configuration without the fs provider | clean refusal with an actionable message, not a crash |
|
|
102
|
+
| 19 | Turn boundary | attempt a continuation with an open turn | ceiling lands on a completed `turn/end`; the in-flight exchange is carried by the handoff note, not by a mid-turn cut |
|
|
103
|
+
| 20 | First-turn behaviour | open a fresh child, wait | confirm whether it acknowledges its own TOC (open question 2). If it does, the notice needs a stay-silent instruction |
|
|
104
|
+
|
|
105
|
+
### Evidence Map (as of probe round 26/26b, 2026-09-17)
|
|
106
|
+
|
|
107
|
+
Checks 1-19 were executed mechanically on the `.dshdev2` scratch profile
|
|
108
|
+
(`spikes/probe/lib/round26*.js`, `results-26*.json`): 18 of 20 PASS with durable evidence — including
|
|
109
|
+
[15] overlap refusals naming the range, [16] budget refusals carrying numbers, [17] a tampered chapter
|
|
110
|
+
surfacing `⚠ modified since archived` inside a real child's notice, [14] an idempotent retry reusing
|
|
111
|
+
numbers `[1, 2]` with no duplicate chapters, [13] registry state read back across a process kill, [6]
|
|
112
|
+
continuation children AND fork siblings resuming across boots, [19]/[20] turn-boundary refusal and
|
|
113
|
+
creation-schedules-no-turn. [7][8][9] (cache gates) point at r11/r22/r23 evidence; [10] boot-level rests
|
|
114
|
+
on its L0 tests plus the citation-rule test; [18] is N/A because the store uses node:fs by design.
|
|
115
|
+
**Genuinely human-only:** sidebar/visual resume in a browser, `/compact` observed in a live UI, the
|
|
116
|
+
readability judgment of TOC prose and chapter bodies — plus optionally following one real 40KB tool
|
|
117
|
+
result into an artifact.
|
|
118
|
+
|
|
119
|
+
## Knowledge Layer — P1 Checklist (record §13)
|
|
120
|
+
|
|
121
|
+
| # | Check | Passes when | Evidence |
|
|
122
|
+
|---|---|---|---|
|
|
123
|
+
| K1 | Project identity | same repo URL, different spellings → one key; non-git cwd degrades honestly; local-path upstreams key off the resolved path | unit `repo.test.ts` (10) + `sync-local-upstream.test.ts` |
|
|
124
|
+
| K2 | Redaction at the chokepoint | a secret-bearing transcript never lands in a chapter or artifact, same secret → same marker | unit `redact.test.ts` + render chokepoint test; FINDINGS-era |
|
|
125
|
+
| K3 | Turn signatures, live | a completed real turn yields a signature with paths/terms from HUMAN content only | **live probe r35j (K5b)** + L0 `engine-adapter.test.ts` (real Session shape) |
|
|
126
|
+
| K4 | Topic-sequential composition | same-topic adjacent turns merge into one chapter; topic changes split; size cap splits | **live r36: merge proven (score 0.171 < τ, shared primary)**; L0 compose/anchor tests |
|
|
127
|
+
| K5 | Index | shards deterministic; incremental manifest gates commits; rebuild-from-empty == incremental | unit `indexing.test.ts` (7) |
|
|
128
|
+
| K6 | Search | topic>title>summary ranking, budget packing with total-vs-shown honesty, project scoping | unit `search.test.ts`; live r35j (K7: finds through a LOCAL-ONLY mirror) |
|
|
129
|
+
| K7 | Commands context-free | `/chapters-link` (4 modes: view / path / git-URL+token / `treedx+`URL+token), `/chapters-status` render as flow nodes; the model's context is unchanged | durable: `command/run`+`command/done` lifecycle events, no model/usage around them (e2e plane); r35j K2/K8; treedx modes: `link-treedx.test.ts` |
|
|
130
|
+
| K8 | Sync loop, success path | two machines over a REAL git smart-HTTP remote: publish → remote carries layout+index → clone sees it → search crosses | integration `sync-remote.test.ts` + **e2e `knowledge.spec.ts` — suite green 2026-09-18 (4/4, twice — also after the credential-move + three-mode link)**: browser composer link pushes to a live git-http-backend remote, status reaches 'synced', the child TOC carries the Project line + (N msgs), the debounced push lands `collections/`, and machine B searches A's chapters |
|
|
131
|
+
| K9 | Sync loop, degradation | remote down ⇒ local-only mode (mirror+index current, status names it); recovery rebuilds and pushes | `sync-remote.test.ts` degradation arc; **live r35j: link to unreachable remote, search still works, status 'local-only'** |
|
|
132
|
+
| K10 | Triggers fire | push scheduled after each archive (debounced); pull on first turn + pre-fork | **proven by K8's e2e run**: `collections/` reached the remote with no manual sync between fork and poll; L0 `scheduler.test.ts` + first-turn tests |
|
|
133
|
+
| K11 | Notice v2 | `Project:` line + per-chapter `(N msgs)`; legacy records never render a fake zero | r35j (K6, K6b); `notice.test.ts` |
|
|
134
|
+
|
|
135
|
+
Human-only rows (unchanged): sidebar visual resume, `/compact` observed in a live UI, TOC/chapter
|
|
136
|
+
readability judgment.
|
|
137
|
+
|
|
138
|
+
## Knowledge Transport Providers (record §15, Amendments 2026-09-21)
|
|
139
|
+
|
|
140
|
+
| # | Check | Passes when | Evidence |
|
|
141
|
+
|---|---|---|---|
|
|
142
|
+
| T1 | Seam preserves git behavior | provider selection defaults 'git' for every pre-existing record; all K1–K10 evidence still green after the refactor | full suite (276→285) with zero git-mode changes; `provider.test.ts` selection + migration rows |
|
|
143
|
+
| T2 | TreeDX publish/clone/search | machine A syncs (create→workspace→commit), B clones via the provider and `chapters_search` finds A's chapter through the materialized mirror | `treedx-sync.test.ts` (transport grade, stub) + **`treedx-live.test.ts` PROVEN against the real dev-auth container 2026-09-21** (born-head clone, publish→clone→search crossing, bad-token auth detail, head advance/lease reuse, binary blob round-trip) |
|
|
144
|
+
| T3 | Divergence & lease classes | moved head + staged work ⇒ `diverged` ⇒ rebuild-from-store republishes BOTH contributions; busy lease ⇒ bounded retry ⇒ `rejected` ⇒ pull-and-retry; failed push CLOSES its workspace (leases survive ttl otherwise — measured) | `treedx-provider.test.ts` (all three), `treedx-sync.test.ts` (rebuild converge) |
|
|
145
|
+
| T4 | Degradation with numbers | service down / token bad ⇒ `local-only` (offline mirror still publishes, commits, indexes, searches); `auth` detail carries the re-link instruction, never the token | `treedx-sync.test.ts` offline row; `treedx-provider.test.ts` auth row (token-sentinel absence) |
|
|
146
|
+
| T5 | Oversized refusal | a UTF-8 file above the service limit ⇒ refuse with the number, never truncate | `treedx-provider.test.ts` 1 MiB-413 row |
|
|
147
|
+
| T6 | Identity namespaces | `treedx+host/repo` key ≠ a git remote's key for the same canonical string (one pool per transport); stable across spellings (`/api/v1`, trailing slash, case) | `provider.test.ts` identity rows |
|
|
148
|
+
| T7 | Commands surface | `/chapters-link treedx+…` resolve-or-creates the managed repo, stores the token 0600 under the DSH home, records kind+repoId; view/status print provider + head | `link-treedx.test.ts` (5) |
|
|
149
|
+
| T8 | Dead-mirror self-heal | a pre-existing non-empty mirror dir (stale test residue, moved tree, abandoned pool) moves aside as `.stale-<ts>` and the clone proceeds — the old hard refusal made every later sync of an affected workspace a permanent deadlock (the r39 e2e class) | unit `sync.test.ts` self-heal row; `treedx-provider.test.ts` clone row (both providers); e2e boots now run dirty-free |
|
|
150
|
+
| T9 | Body-hash anchor round-trips | the frontmatter anchor hashes EXACTLY what `parseChapterFile` returns as the body (the writer appends `'\n'`; hashing bare `bodyText` made every real chapter look tampered to the enrich guard, which silently refused — host logger). Sanctioned writes preserve body bytes and re-anchor the registry's whole-file hash | unit `enrich-store.test.ts`/`enrich-wire.test.ts`; **the enrich e2e passed 2026-09-23 — the first time the ladder ever wrote through the real UI** |
|
|
151
|
+
| T10 | Host-plane enrichment route | `/chapters-enrich run` resolves a route from the last real tool-bearing session (`noteRoute` persists `enrichment.route`; the host has no live agent) — without it, a fork-only flow could never enrich | `enrich-wire.test.ts` + the passing enrich e2e |
|
|
152
|
+
| T11 | Git-plane auth honesty | a rejected credential (401/403) classifies as `auth`, never `network` — clone/pull/push carry it and the degrade-to-local-only status says `credentials rejected … re-link with /chapters-link`, not the unreachable-ghost (the TreeDX live lesson applied to git) | `git-auth-classification.test.ts` (5) over a REAL smart-HTTP remote with a token gate; classifier unit rows included |
|
|
153
|
+
| T12 | Corpus listing is complete | `paths/list` paginates (limit 100, base64 offset cursor) — `corpusPaths` follows `nextCursor` to the end; a second machine materializes EVERY file past a page boundary | `treedx-sync.test.ts` pagination row (150 files over the paginating stub); the live container (>100 paths) proves it at the real service |
|
|
154
|
+
| T13 | The human entry surface | `/chapters-link` all four modes incl. refusals (no-token non-loopback, junk target), `/chapters-status` renders each fact ONCE (a duplicate-`Provider:` line was caught here), `/chapters-enrich` verbs, legacy workspace-token migration to the DSH home (0600, old file deleted) | `link-git-commands.test.ts` (11), `token-migration.test.ts` (4) |
|
|
155
|
+
| COV | Both coverage ledgers are statement-grade and run | `npm run coverage:ci` — the GATED deterministic ledger (c8 over all unit+integration, `--check-coverage` at 85% statements/branches/functions/lines; enforced in hosted CI, which is GREEN end to end 2026-09-25) — currently **98.11% stmts / 86.44% branch / 93.8% funcs @ 549 tests, zero skips with the TreeDX container up** (the plans-carriage work added 80 rows and RAISED branch coverage from the 85.61% this row used to quote). `npm run coverage:e2e` — seven-project replay under E2E_COVERAGE=1; boots run lib/*.js with REAL source maps (tsconfig `sourceMap`) remapped by c8/v8-to-istanbul; first ledger **80% stmts / 62% branches / 74% funcs** — it corrected the earlier line-range estimate honestly (tools.ts 99% lines → 72% statements). The 2026-09-23 audit then closed most of that gap at the DETERMINISTIC plane: the engine's construction/listeners/degraded-summarize/plot-elicit/finalize-guard paths (`engine-lifecycle.test.ts` — real cordis `Context()`, real base engine, injected llm/tokenMeter fakes: engine.ts 40→91% stmts), every tool wrapper's success/refusal/missing-caller surface (`tools-surface` + `tools-branches` + `tools-deep-branches`), the TreeDX error taxonomy via one-shot stub faults (`treedx-provider-branches`) and parser-shape drift via injected `fetchImpl` (`treedx-parser-branches`), the local-path git transport arms (`gitops-branches`), host-command error arms (`host-command-branches`), and the pure planners (vocabulary/enrich/indexing/search/arrival/archive — `rules-vocab-branches`, `enrich-planner-branches`, `indexing-branches`, `notice-search-rules-branches`, `render-archive-ladder-branches`, `pure-branches`). It also caught one REAL bug on the way: `canonicalizeRemote` carried embedded `user:token@` credentials into the project key (pasting a remote with and without creds forked the pool — now stripped, pinned in `pure-branches.test.ts`). Gotchas pinned in the script header: c8 filters include/exclude BEFORE remapping (`--include=src/**` against lib-path profiles zeroes the report). Neither ledger alone is the claim; the e2e one makes index/render-in-browser paths that no deterministic test can see accountable |
|
|
156
|
+
| H1 | Corpus carries no project instructions | host-injected context (instruction files, runtime snapshot, skill catalog, our own TOC notice — event-level AND `<system-reminder>` spans inside human messages) renders as a countable `⟦omitted:host-injected …⟧` marker, never bytes; conversation survives verbatim and byte-identical | unit `injections.test.ts` (15) + `render.test.ts` rows; integration `corpus-hygiene.test.ts` (archive→sync→remote→pull→search sentinel-absence, and legacy-store-publishes-unchanged anti-migration guard); e2e stays AGENTS-BLIND (tape masking) |
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
## Plans Carriage (docs/plans-carriage.md, implemented 2026-10-09)
|
|
160
|
+
|
|
161
|
+
A session plan is one `tool/call` for `exit_plan_mode` whose `arguments.plan` carries the whole
|
|
162
|
+
markdown, answered by a `tool/result` that carries the human verdict. There is no plan document in
|
|
163
|
+
the log, so EVERY row below is about reading those two events the same way in three places — the
|
|
164
|
+
renderer, the segmentation tool, and the child's notice.
|
|
165
|
+
|
|
166
|
+
| # | Check | Passes when | Evidence |
|
|
167
|
+
|---|---|---|---|
|
|
168
|
+
| P1 | Verdict classification is right for every host shape | approved (`{approved:true}` / the fixed render text), rejected ("keep planning" + feedback), dismissed (takeover text), `failed` (the review never opened: no channel, service reloaded, outside plan mode, no `#` heading), pending (NO result event) — and a `superseded` rule that only fires for a same-title later submission | unit `plans.test.ts` (23) over fixtures copied from `@deepseek-ai/dsh-plan-mode/lib/index.js:230-289`; the pairing primitives are pinned separately in `log-shape.test.ts` (7) |
|
|
169
|
+
| P2 | `pending` is never printable as approval | a plan with no result renders "NO RESULT: the review was never answered"; the notice adds "do NOT implement it", and the word `approved` appears nowhere in that row | unit `plans.test.ts` + `render-plans.test.ts` + `continue-plans.test.ts`; integration notice assertion |
|
|
170
|
+
| P3 | Full plan bytes are retrievable | archive → the artifact file EXISTS on disk → its bytes equal the submitted plan (modulo the § 9 redaction chokepoint) → `chapters_artifact` `toc` lists the plan's own headings with line numbers and `search` reaches a phrase by SHA alone | integration `plans-carriage.test.ts` (4, real `node:fs` + real registry domain); unit `render-plans.test.ts`, `plans-engine.test.ts` (engine path parity: the same block from `buildFinalizedChapter(s)`) |
|
|
171
|
+
| P4 | `plan/mode` is not a silent hole | the event renders as a `mode` line, folds last-value-wins into `planMode:` frontmatter/record, and stays OUT of `unrenderedSeqs` — while an unnamed event type STILL lands there | unit `render-plans.test.ts` + `plans-engine.test.ts`; the pre-existing `render.test.ts` `mystery/event` row is unchanged |
|
|
172
|
+
| P5 | The plans block is MEASURED, never exempted | with the block on, the same archive REFUSES at a budget where the block-off variant passes; refusal carries both numbers | unit `continue-plans.test.ts` (the paired-measurement row); `preflight` untouched (`src/continue-core.ts:147-161`) |
|
|
173
|
+
| P6 | A plan above the ceiling is carried and named | `chapters_segment` returns `livePlans` (seq > `archiveCeiling`) and `modeActive`; the notice prints `NOT ARCHIVED — parent log seq N only`, and promises no artifact for bytes the store does not hold | unit `plans-tools.test.ts` (4) + `continue-plans.test.ts`; integration `plans-carriage.test.ts` (the open-review row) |
|
|
174
|
+
| P7 | Plan bodies obey the injection screen | a `<system-reminder>` span inside a plan is removed (countable marker upstream), while a plan that QUOTES `AGENTS.md` in prose survives verbatim — and credential patterns are redacted in BOTH the printed head and the stored blob | unit `plans.test.ts` + `render-plans.test.ts` (H1 family) |
|
|
175
|
+
| P8 | A plan-less path is byte-identical to pre-feature | no `## Plans in this chapter` block, no `plans:`/`planMode:` frontmatter lines, no `## Plans` in the notice, and `chapters_segment` emits no `planHint` — the whole feature is conditional on there being a plan | unit `render-plans.test.ts`, `continue-plans.test.ts`, `plans-segmentation.test.ts`, `plans-tools.test.ts`; integration `plans-carriage.test.ts` |
|
|
176
|
+
| P9 | Both carriage paths agree | the CONTINUATION path (`chapters_continue`) and the ENGINE path (in-place `summarize`/finalize) render the same block and the same record facts, including a verdict that lands past a composed chapter boundary — which is why `renderChapter` scans the whole event list, not its own range | unit `plans-engine.test.ts` (5) + `render-plans.test.ts`; zod round-trip of `plans`/`planMode` through the registry in `plans-engine.test.ts`, legacy rows gaining nothing |
|
|
177
|
+
| P10 | Artifact citations resolve wherever they were written | a chapter's `artifacts/aa/<sha>.txt` reference resolves whether the blob sits at the STORE ROOT (arrival stubs) or inside the ANCESTRY TREE (`<store>/<root>/artifacts/…`, where `writeArchive` puts deferred results and every plan) — before this fix the second form was unreadable through `chapters_artifact`, which is what "every byte remains retrievable" actually promised | `artifact-query.test.ts` rows + integration `plans-carriage.test.ts` (`resolveArtifactWithMirror` over the real tree) |
|
|
178
|
+
| P11 | The browser plane runs in the chain | `/plan <message>` → a real `exit_plan_mode` call → the host's review card opened and answered by a real CLICK → fork; asserts the submission's bytes are the artifact's bytes (or provably only-redacted), `planMode: inactive` from the fold, a `kind: plan` document on disk, and that the BRANCH child carries **no** `## Active plan` and no call-out while still citing the archive; plus a guard that no `compaction/*` entered the journey | `tests/e2e/plans.spec.ts` = chain project `plans` (13 s replay, no LLM; 3-entry tape). **Scope, learned at cost:** the continuation leg was REMOVED from this journey because a tape freezes the model's tool ARGUMENTS — my scripted upstream derived `chapters_continue` ranges from the ceiling the live segment tool reported while distilling (30 here), and the runner's session lands at 29, so `validateRanges` refused the stale input (correctly). A journey whose recorded request embeds a machine-specific seq cannot be hardware-neutral. Continuation semantics — `## Active plan`, `## Previous plans`, the seeded `plan/mode`, the checkpoint carry — are covered by the deterministic plane (`continue-plans`, `plans-engine`, `plan-documents`, the § 10 kernel probe) and the in-place path by the heavy/arrival/enrich/fanout journeys. Tape provenance: scripted upstream, see `tests/fixtures/model-tape/plans/PROVENANCE.md` |
|
|
179
|
+
|
|
180
|
+
| P23 | **Retired in 0.1.17 with the legacy copy** (support floor 0.2.0): `installChaptersPreset` and `presets/chapters/` are deleted, so there is no installed file set to half-delete. The key is ACCEPTED-and-inert since 0.1.18 — removing it would make an existing profile patch fail to parse, and a bundle that will not mount is worse than a key that does nothing | see P24 |
|
|
181
|
+
| P24 | The preset is a **visible declaration row**, and the row composes on every host in range | `preset-chapters` is inserted by our own `cordis.patch.yml` with the preset definition in its `config`, mounted by `@fractalsynapse/dsh-chapters/preset` (`src/preset.ts`) — the visible Loader entry the host's own presets are, overridable and `disabled`-able from a profile patch, with the registration's lifetime yielded to Cordis (an `apply()`-scoped disposer un-registered it: measured as `register()` succeeding with the roster WITHOUT `chapters`). Rows the host cannot import are dropped and NAMED, because one unresolvable child makes the whole preset `broken` | 15 rows in `preset-declaration.test.ts`, **each verified to fail on the mutation it guards** (ralph re-disabled, an unknown pruner key, `toolFilter` restored, `{{cwd}}` restored, the durable id renamed) — plus real boots of the packed tarball on 0.2.0-rc.2 (`roster: present`, `inventoryRows: 28`, `unusableRows: []`, tool-schedule pruned), 0.2.1-alpha.4 (`registered`, `inventoryRows: 29`, nothing pruned) and 0.1.5-rc.1 (boots; the row says the host cannot register rows) |
|
|
182
|
+
| P21 | The continuation has a **human-pulled door**, and the two doors cannot drift | `/chapters-continue` = `deterministicCommand({intent:'continue'})`, the same builder as `/chapters-fork`; only name/verbs/intent/no-directive prose differ; archive semantics identical (everything since the watermark). CLIENT SIDE, same discipline: the assistant row registers both doors from ONE runner (`src/client/doors.ts` `makeDoorRunner` + a two-row `DOOR_ACTIONS` table) — a composer-only continue left the user with a created child and no affordance | unit `continue-command.test.ts` (7 rows) incl. the **no-drift row**: same events in ⇒ same ranges, titles and plan facts out of both doors; the no-plan case, the refusal naming its own door, and approved ⇒ NOT seeded vs still-planning ⇒ seeded `['user/message','plan/mode']`. Unit `client-doors.test.ts` (7 rows) pins the two client rows (distinct id/order/label/command, the fork label frozen for the specs), that ONE child-id regex parses BOTH door wordings, that both rows fire their own command through the same body, and — by reading the server's own templates — that the reply wording the client parses is still what `deterministicCommand` emits. Browser: project `fork` asserts BOTH buttons render |
|
|
183
|
+
| P22 | The continuation shape is covered **in the browser**, on every push | the leg that a tape could not carry (§ 15) is back, driven by the command — no model request, no frozen `endSeq`; asserted from the child's durable log without switching the UI: `## Active plan`, the operative plan by title with its real verdict, `plan document:` and `chapters_artifact` citations, `## Finding things`, no seeded gate after approval, exactly one synthetic notice (invariant 2) | `tests/e2e/plans.spec.ts` project `plans` — replays in ~14 s with no LLM/GPU, eight-project chain green |
|
|
184
|
+
| P18 | Search answers **without a pool linked** — the findability audit | `buildLocalCorpus` walks the workspace store (chapters, `kind: plan` documents, rules) and answers through the SAME scorer as the mirror door (`rankEntries`), so ranking cannot drift between doors; artifacts are deliberately not indexed (they answer to `chapters_artifact`); the answer names its door ("searched this machine's store, N docs, no pool mirror") so a session never reads *this machine only* as *the whole project*; "nothing matched" (`documents>0`) is distinguished from "nothing here" (`empty`); a pool miss falls back to the local store before answering empty | unit `local-corpus.test.ts` (5) + integration `find-without-a-pool.test.ts` through the real tool wrapper — the empty answer used to be born in the wrapper, not the pure function |
|
|
185
|
+
| P19 | A child is TOLD the lookup verbs | the notice gains a three-line `## Finding things` naming `read` (chapters), `chapters_artifact` (deferred blobs, path or bare sha) and `chapters_search` (everything else), plus the one fact a model cannot infer — that plans are searchable **as plans** (`kind: plan`) and rules arrive from other machines; `retrievalHints` (default on) off ⇒ the notice is byte-identical to the previous shape | unit rows in `find-without-a-pool.test.ts` (on/off goldens); measured that NO committed tape prefix contains a seeded notice, so the change costs no re-record, and the eight-project chain replays green |
|
|
186
|
+
| P20 | Kernel-drift gate on the seeded continuation | the seed depends on two host facts: `plan/mode` is a KNOWN event type and is NOT surface-eligible. Read from the kernel's OWN exported predicates (`KNOWN_SESSION_EVENT_TYPES`, `isSurfaceEligibleType`, `isAppendSurfaceEvent`, `adoptSessionEvent`) rather than reimplemented, so the gate fails on a change of kernel INTENT, not on refactoring. Without it, a host upgrade turns a gated child into an ungated one with every existing test still green | unit `host-seed-contract.test.ts` (4 rows, 0 skips against the pinned host; skips loudly naming the missing package if the host is absent) |
|
|
187
|
+
| P12 | A plan is queryable AS a plan | each submission becomes its own `kind: plan` document in the same tree, so pool transport, mirror rehydrate and the corpus index carry it unchanged; `loadCorpus` keeps the file's own kind instead of flattening it to the directory's; `chapters_search` answers a plan query with the plan | unit `plan-documents.test.ts` (5) + integration row in `plans-carriage.test.ts` (asserts the published path, the indexed kind and the search hit) |
|
|
188
|
+
| P13 | Plan numbering never touches chapter numbering | plans allocate through their own registry counter (`nextPlanNumber` / `reservePlan`, attempt-keyed, idempotent under retry); a chapter number is never consumed, and names come from the registry rather than a directory scan so siblings cannot collide | unit `plan-documents.test.ts` (counters, retry reuse, writer-without-plan-allocation stays additive) |
|
|
189
|
+
| P14 | A directive is never annotated, and superseding never overwrites | enrichment skips any document whose own frontmatter says `kind: plan` (a rename cannot disarm it); a revised plan leaves the earlier document's bytes intact and both stand in the store | unit `enrich-plan-documents.test.ts` (3) + integration evolution row |
|
|
190
|
+
| P15 | Plan mode survives a continuation only when it should | seeded `plan/mode` ONLY for `intent: 'continue'` while the fold says active (measured: kernel accepts the two-event seed, host projection folds it to `active:true`, one-event control does not); never for a branch, never after an approval — because approving a plan EXITS plan mode | unit `continue-plans.test.ts` (four rows: seed shape, approved-no-seed, branch-no-seed, `planSeedMode:false`) against the installed-kernel probe recorded in docs/plans-carriage.md § 10 |
|
|
191
|
+
| P16 | The notice reads the intent, not just the log | continuation → `## Active plan` + `## Previous plans on this path` (marked history); branch → zero bytes, plans live in the cited chapters and as documents; the mode statement is either "the directive above is live" or "this child starts OUTSIDE plan mode", never both | unit `plans.test.ts` (seeded/unseeded/branch rows) |
|
|
192
|
+
| P17 | The one injection-screen exception is named, not open-ended | `kind:'plugin' + plugin:'plan-mode' + form:'notice'` archives verbatim; any other plugin notice, snapshot or instruction shape still screens; `isHumanAuthored` (which drives topic signatures and `(N msgs)`) is unchanged | unit `injections.test.ts` (two rows, incl. wrong-form and wrong-plugin negatives) |
|
|
193
|
+
## Repair (#60) — recovering an archive from the durable log
|
|
194
|
+
|
|
195
|
+
| # | Claim | Procedure | Status 2026-10-22 |
|
|
196
|
+
|---|---|---|---|
|
|
197
|
+
| R1 | The auditor finds the measured damage | `node scripts/chapters-repair.mjs --home <lab> --project <cwd> --cwd-override <lab-tree>` on a COPY of SAIP's live registry + store + 26 session logs | **PASS** — 436 findings: hollow 5, empty-artifact 335, never-archived-span 19, unregistered-file 73, ghost manifest 2, stuck reservation 2. Matches the manual audit exactly. |
|
|
198
|
+
| R2 | A repair re-derives lost knowledge | apply, then grep for three phrases that previously matched 0 files | **PASS** — span 5265-5467 (the "foundational to the project" hosting brief) is now in `chapters/030-hosting-…-plan-1.md`; 719 moves across 3 passes |
|
|
199
|
+
| R3 | Repair converges (idempotent) | run apply again; the third pass must move 0 | **PASS** — pass 3 = 0 moves; audit falls 436 → 3 findings |
|
|
200
|
+
| R4 | What it will NOT do | 3 files remain: two `015-*.md` claiming one number. Re-registering needs a new number = renumbering a chapter → refused, with the reason printed | **PASS** — residue is a human decision, reported as such, not silently "fixed" |
|
|
201
|
+
| R5 | Nothing is deleted or rewritten | after a full apply, the husk files still exist (copied to `.repair-quarantine/`), the registry keeps every record (append-only), and the log's md5 is unchanged | **PASS** |
|
|
202
|
+
| R6 | The run is reversible | `restoreRepair` walks the journal backwards; new files return from quarantine | **PASS** — `tests/integration/repair-apply.test.ts` |
|
|
203
|
+
| R7 | Dry-run is genuinely read-only | registry bytes and directory listing identical before/after | **PASS** — pinned by a row |
|
|
204
|
+
| R8 | LIVE application | `~/.dsh` is read-only to the agent sandbox (EROFS), so SAIP and this repo have NOT been repaired for real | **BLOCKED on the user**: run `node scripts/chapters-repair.mjs --project /home/adrian/Projects/saip --apply all --yes` yourself, or approve the sandbox |
|
|
205
|
+
|
|
206
|
+
Known limitation, stated rather than hidden: an audit through `ctx.fs` cannot list a directory, so it
|
|
207
|
+
reports `blindSpots: ['orphan-file','test-litter']`; the offline script has `readdir` and sees them.
|
|
208
|
+
|
|
209
|
+
## Removal (#255) — deleting bad chapters for real
|
|
210
|
+
|
|
211
|
+
The wrong-project-prompt case: bytes that should not be in this project's memory leave the store and the
|
|
212
|
+
mirror, while the registry stub holds the watermark and the durable log stays complete (invariant 1).
|
|
213
|
+
Every row below was written before the implementation (docs-first), then flipped with the named proof.
|
|
214
|
+
Rows verified 2026-11-02 on the branch `feature/255-chapter-removal`; deterministic suites only — no
|
|
215
|
+
browser or model leg is claimed here (M11 records the tape-stability check).
|
|
216
|
+
|
|
217
|
+
| # | Claim | Procedure | Status 2026-11-02 |
|
|
218
|
+
|---|---|---|---|
|
|
219
|
+
| M1 | A `removed` stub holds the watermark | archive-tail after removing the newest chapter: the next save starts ABOVE the removed `endSeq`, never re-capturing it | **PASS** — `tests/unit/registry.test.ts` ('archiveWatermark counts removed stubs'); the three reduce sites read ALL records by construction, no tombstone filter added anywhere |
|
|
220
|
+
| M2 | The one predicate excludes it everywhere | TOC (`collectEntries`), index build, enrichment candidates, plan rows all omit `removed`; grep finds no second status list | **PASS** — `registry.test.ts` rows + gate `tests/unit/one-predicate.test.ts` (compares against tombstones outside registry.ts fail the build); `enrich-wire.ts` swept to the predicate, old inline shape now pinned absent in `repair.test.ts` |
|
|
221
|
+
| M3 | Shared blobs survive | two records name one artifact sha; remove one ⇒ blob on disk, the other chapter still verifies | **PASS** — `removal.test.ts` orphan-math row + `commands-removal.test.ts` apply row (shared blob present after apply) |
|
|
222
|
+
| M4 | Orphaned blobs and plan docs die with the chapter | deletion set lists file + artifacts + planDocs; bytes match the dry-run exactly | **PASS** — `removal.test.ts` (owned blob + planDoc in set), `commands-removal.test.ts` (dry-run set == apply set) |
|
|
223
|
+
| M5 | Journal precedes deletion | fail the journal write ⇒ zero unlinks, record unmutated; partial unlink ⇒ journal names exactly what went | **PASS** — `removal.test.ts` rows 'a journal that cannot be written' (file-where-dir-goes ⇒ zero unlinks) and 'failing second unlink is journalled as partial' (directory-at-blob-path ⇒ status partial, problems in journal) |
|
|
224
|
+
| M6 | The mirror publishes the deletion and never rehydrates it | clone paths unlinked before sync pass; commit shows `git rm`; `rehydrateStoreFromMirror` restores 0 for stub-owned names | **PASS** — guard rows in `tests/integration/sync.test.ts`; clone unlink in `removal.test.ts`/`commands-removal.test.ts`. The `git rm` line itself follows from `stageAllAndCommit`'s workdir-vs-HEAD diff (established #60 behavior); no new live-push leg was added in v1 |
|
|
225
|
+
| M7 | Index and search go clean | `buildIndexInClone` shard + `chapters_search` after removal omit the chapter; TOC of a NEW continuation omits it | **PASS** — `sync.test.ts` dead-topic-shard sweep row (+ the pre-existing slug-vs-filename cleanup bug it exposed, fixed and pinned); `buildIndex` stub row; search reads corpus files, which are gone |
|
|
226
|
+
| M8 | Dangling citations are reported and logged | removal output prints referrers; `<tree>/removals/missing-access.log` gains the lines; a later dead-citation miss appends too | **PASS** — dangling rows in `removal.test.ts` + `commands-removal.test.ts` (dry-run prints referrer list; report reads ledger); structured relay pinned in `continue.test.ts` (`onIntegrity` modified + missing arms), plane append wired in tools.ts ports |
|
|
227
|
+
| M9 | Restore re-derives byte-identically, under a NEW number | journal → restore: file renders from the log, sha matches the journal's, old number stays spent; a zero-message span refuses | **PASS** — `tests/integration/removal-restore.test.ts` (5 rows: identity, unrecoverable, idempotent, renders-nothing, ledger) |
|
|
228
|
+
| M10 | Ids cannot cross projects or guess | unknown id ⇒ near candidates; ambiguous pool path ⇒ refusal; foreign store-root path ⇒ refusal; dry-run is default | **PASS** — `removal.test.ts` resolve rows (bare basename across two sessions refuses; pool path disambiguates) + `commands-removal.test.ts` foreign-tree and unknown-verb refusals; NOTHING-WRITTEN asserted byte-intact |
|
|
229
|
+
| M11 | No recorded request byte changed | token-level anchor diff + the acceptance job replaying the UNTOUCHED corpus | **PASS with a correction, stated honestly**: the prompt-byte anchor DID move (`710e0052829e2b50 → ffbcad76bc71008c`) — token diff names exactly one added literal, the `/chapters-remove` command `description`, human catalog text. Tape prefixes serialize messages only: no recorded request carries it (grep across the corpus: zero), and the acceptance job replayed **9/9 legs green on this commit** (run 41561341047). Per docs/development.md rule 1 the stamp was re-taken on that green replay (`lib=bb2ddb1808e3f568`). Zero re-records; the earlier "no model-visible byte" wording in this row was wrong before measurement and is superseded by it. |
|
|
230
|
+
|
|
231
|
+
## Two-Tier Rules — SPEC (record §7.1, §7.3–§7.9; issue #50)
|
|
232
|
+
|
|
233
|
+
Rows **R1–R10 verified 2026-10-20** except **R-e2e**, which this host cannot run (see the last row
|
|
234
|
+
and §13 P3c). Each row below was written docs-first and then proven to fail on the mutation it
|
|
235
|
+
guards — the mutation runs are named per row so a future reader can repeat them. The P3 rows above
|
|
236
|
+
stay the shipped-behavior record; these extend it with the session tier.
|
|
237
|
+
|
|
238
|
+
| # | Claim | Status | How it is witnessed (and the mutation that proves it) |
|
|
239
|
+
|---|---|---|---|
|
|
240
|
+
| R0 | The empty tier still renders **zero bytes**, and the pre-#50 option shape renders **byte-identically** to the previous builder | ✅ verified | The golden literal was captured by running **HEAD's** `buildRulesSection` over the same fixture and diffing it against the new one (text-equal, kind-equal, only the non-rendered `withheld` field added). On the checkpoint path an empty set asserts neither header nor announcement appears |
|
|
241
|
+
| R1 | A rule survives an in-place compaction | ✅ verified | `rules-checkpoint.test.ts` drives the **REAL realm engine** through `summarize()` and reads the text that would be prefilled next: project rule + session rule verbatim, project before session (positional precedence); a parent's rule rides the child's checkpoint (R1b — the tier binds the TREE); over budget the omission is announced. Mutation: dropping `rulesSection` from `leadSections` breaks 3 of 4 rows |
|
|
242
|
+
| R2 | `chapters_continue` carries both tiers; `chapters_fork` carries project and **zero session bytes** | ✅ verified | `rules-doors.test.ts` asserts the asymmetry through the shared renderer, and a **source-shape row** fails if `tools.ts` or `engine.ts` calls `buildRulesSection` directly — two rule-pickers would drift apart invisibly (the same argument that pins `rankEntries` and `doors.ts`) |
|
|
243
|
+
| R3 | `ADD RULE:` is deterministic and free | ✅ verified | `session-rules.test.ts` (8 rows): line-start-only, minimum length, dedupe; the two injection screens (`isHumanAuthored`, `stripReminderSpans`) each with a positive and a negative; assistant-restatement never captures (the amplification path); idempotent across repeated turn/ends; numbers honor registry **and** disk; §9 credential screen; listener containment (disabled / missing cwd / throwing store). The module holds no `ctx.llm` reference at all |
|
|
244
|
+
| R4 | Session rules never reach the shared pool, on **either** provider | ✅ verified | `rules-transport.test.ts`: a real git pool (A stages project only, B pulls and never sees the session file, A still renders it locally — exclusion ≠ invisibility) and the TreeDX stub's commit tree. Mutation: deleting the single `isSessionRuleFile` skip reddens both |
|
|
245
|
+
| R5 | Mined rules are governed by **policy**, not by proof that a human said them, and each one carries its **warrant** (§7.5, Amendment 2026-10-21) | ✅ verified | `rules-mining.test.ts` (20 rows): `rules` is OPTIONAL so the old three-field answer still validates; bounds (≤3 **written** per chapter, sub-minimum dropped, a garbage `rules` value cannot cost the annotation); the quote check is a LABEL — `stated` when the chapter's human bytes contain it, `inferred` when they don't, and BOTH govern under the default `rulesAutoActivate: true`; the row named "THE REVERSAL, PINNED" asserts exactly that, and the rendered head shows `inferred from ch.N — nobody said it` beside the text with the agent required to name the rules it applied; `autoActivate: false` writes `reviewOnly: true` and renders NOTHING until `approve` (a curation fact — the write-once file is byte-compared unchanged); assistant prose is inadmissible even as a label; ladder rows prove human-bytes-only evidence, mining before the hash-guarded write, `rulesEnrichment: false` logging the drop, an unwired plane WARNING, a throwing sink not losing the annotation, and `rulesModel` making a second call on its own route whose answer replaces the annotation pass's; `/chapters-rule active` prints the posture and the inferred count. Mutations: predicate ignoring `reviewOnly` → 2 rows; `autoActivate` ignored → 2; `rulesModel` ignored → 2; basis classification collapsed → 1 |
|
|
246
|
+
| R6 | Selection is deterministic and announced | ✅ verified | Shuffled input order yields byte-identical selection; two identical runs render identical text; the withheld line appears exactly when `shown < active`, and its ABSENCE is asserted when no cap is set. `active`/`chapters_rules` return the counts even when nothing was withheld, so "you are seeing all of it" is a fact |
|
|
247
|
+
| R7 | Nothing was silently clipped anywhere the budget bites | ✅ verified | Token ceiling still returns `refusal` with per-rule numbers and now counts **both** tiers; the checkpoint announces the omission instead of squeezing; the door path still refuses the continuation with numbers (§1.2) |
|
|
248
|
+
| R8 | Ungoverned never looks like governed | ✅ verified | `/chapters-status` prints an `Identity:` line with the divergence warning — and this repo's own non-git test workspace hit that branch during verification, which is the honest evidence the warning is not theoretical; `chapters_rules` answers "no linked project" separately from "nothing approved on this machine" separately from "nothing captured this session"; the head states the zero-project tier only when session rules are actually governing it |
|
|
249
|
+
| R10 | Every chapters session — fresh, continued, or DELEGATED — is told to use `chapters_rules` and `chapters_search` | persona row parsed from the shipped patch: the suffix names both tools, names delegation, keeps the `PLOT:` discipline, adds no unregistered prompt variable, and remains matched by the plot template-echo guard. Verified by mutation — dropping `chapters_rules` from the suffix reddens the row |
|
|
250
|
+
| R11 | Rule ancestry crosses the delegation edge, and every door uses the ONE walk | `tests/unit/rules-lineage.test.ts` (8 rows: header-only parent, transitive mixed edges, one-way direction, cycle termination, the 64-expansion cap with its loud flag, unknown session, throwing reader, anti-drift). Verified by mutation: disabling the header edge reddens 4 rows |
|
|
251
|
+
| R9 | A conflict is reported, never resolved | ✅ verified | `conflicts` wrote no file (directory listing asserted unchanged), the prompt explicitly forbids rewriting, and with no conversation route the verb refuses rather than half-answering |
|
|
252
|
+
| R10 | Losing a checkout is re-derivation, not corruption | 🟡 partial | `parseRuleMarkers` is deterministic over message text and capture dedupes against disk, so a replay fills gaps without duplicating — but the **end-to-end wipe-and-replay row is not written**. The mining half is now re-derivable by re-running the ladder, and capture dedupes against disk, so a replay fills gaps rather than duplicating. |
|
|
253
|
+
| R-e2e | The browser plane: a live model, the `rules` journey, the new tool in the header | ❌ cannot run on this host | `npm run test:e2e:replay` **refuses by name** (`tests/e2e/boot.ts:130`: the pinned host predates preset declaration rows). The re-record this feature owes is queued with the `dev/dsh-host-lock` move in `docs/backlog.md` — stated, not skipped silently |
|
|
254
|
+
|
|
255
|
+
**First live witness (2026-10-21, `npm run mining:probe`).** Three real archived chapters of this repo,
|
|
256
|
+
mined by `qwen3.8-flash` — the model that powers live sessions here — through the shipped code paths
|
|
257
|
+
(`buildMineInput` → `parseMinedRules` → `quoteIsHuman` → `captureMinedRules`), local :8080 down so the
|
|
258
|
+
token-plan hosted route was used. Nine rules captured (the 3-per-chapter cap bound every chapter),
|
|
259
|
+
**6 stated / 3 inferred**, and all three inferences came from the one chapter with ZERO human bytes —
|
|
260
|
+
a subagent chapter — which is exactly what the classification should do. The model's own `basis` claim
|
|
261
|
+
was the weaker signal: chapters 2 and 3 each declared only 1 stated rule, while the plugin's check
|
|
262
|
+
found 3/3 quotes genuinely present in the human text. So the label is earned from evidence, not from
|
|
263
|
+
the model's say-so, and the head rendered all nine in 479 tokens with nothing withheld at a cap of 12.
|
|
264
|
+
On quality: none of the nine was wrong or unsafe; the three inferences were arguably the most
|
|
265
|
+
transferable ("verify decisive architectural claims in shipped code before treating them as settled").
|
|
266
|
+
n = 3 chapters on one repo's own history is not proof, but it is no longer nothing.
|
|
267
|
+
|
|
268
|
+
**A cost finding, stated twice because the first version was wrong.** The probe reported completion
|
|
269
|
+
usage of 969–3237 tokens against a 700-token request. The inference I drew — that the annotation
|
|
270
|
+
ceiling was truncating mining — was **wrong**: a direct test of the same route with a 64-token cap
|
|
271
|
+
still produced 113 tokens with `finish_reason: stop`, and usage itemizes
|
|
272
|
+
`completion_tokens_details.reasoning_tokens`. `max_tokens` is advisory on this provider, not
|
|
273
|
+
enforced, and reasoning counts toward completion usage. So nothing was truncating; what was true is
|
|
274
|
+
that the ceiling had no business being a literal at three call sites (900/900/600) sized for
|
|
275
|
+
pre-mining answers. It is now `enrichmentMaxTokens`, default **4000** — above the observed 3.2K —
|
|
276
|
+
because a *strict* provider would enforce it, and one that itemizes reasoning deserves a budget that
|
|
277
|
+
covers the thinking. The lesson stays worth recording: a usage number is not evidence about a
|
|
278
|
+
mechanism until the mechanism is tested directly.
|
|
279
|
+
|
|
280
|
+
**Inference QUALITY had no witness when the posture was set; the first data is below and it is thin.** The mechanisms are proven against fixtures; nothing here can say how often a small model infers a *correct* standing order, now that inferences govern by default. That is the open risk of the 2026-10-21 decision, and the measured first step is cheap: point the enrichment route at a reachable model, run `/chapters-enrich run` over real archives, then read the `stated` vs `inferred` ratio in `/chapters-rule list --scope session`. If inferences are wrong more often than a human catches them in replies, the answer is `rulesAutoActivate: false` — not a new evidence gate (the amendment records why the gate was the wrong instrument). The box's GPU driver was unreachable when this landed (`nvidia-smi` cannot talk to it), so no vLLM boot and no real mining pass ran.
|
|
281
|
+
|
|
282
|
+
**Two environmental facts, learned the hard way on 2026-10-20 and worth repeating.** (1) The
|
|
283
|
+
deterministic suites need **Node ≥ 24** (`package.json` engines); on Node 22 every test file fails
|
|
284
|
+
with `ERR_NO_TYPESCRIPT`/unknown-extension, which looks exactly like a broken change and is not one.
|
|
285
|
+
(2) Under `npm run coverage:ci`'s parallel load the real-HTTP-git files
|
|
286
|
+
(`git-auth-classification`, `sync-remote`) intermittently failed with an EPIPE blamed on their
|
|
287
|
+
`before` hook. It was **recorded as a box-level flake and left alone — which was the wrong call**: the
|
|
288
|
+
"prove it predates my diff" step was right, and stopping there was not. The cause was real and is now
|
|
289
|
+
fixed in the rig: the test git server wrote each request body into a CGI child's `stdin` with **no
|
|
290
|
+
error listener**, so a child that had already exited raised an async EPIPE with no handler, which the
|
|
291
|
+
runner reported as uncaughtException after the test ended. Guards now sit on the child's three
|
|
292
|
+
streams and on both request/response objects ([tests/integration/http-git-server.ts](http-git-server.ts));
|
|
293
|
+
three consecutive combined runs since then are 800 pass / 0 fail. Lesson for the next agent, in the
|
|
294
|
+
repo's own style: a pre-existing failure is still a failure.
|
|
295
|
+
Ledger on this branch: **93.16% statements / 85.53% branches / 90.01% functions** — the gate holds.
|
|
296
|
+
|
|
297
|
+
## The Cache Metric, Before You Trust a Number
|
|
298
|
+
|
|
299
|
+
`usage.inputTokens` is the **uncached delta** and `usage.cacheReadTokens` is the cached prefix, so:
|
|
300
|
+
|
|
301
|
+
```
|
|
302
|
+
totalPrompt = inputTokens + cacheReadTokens
|
|
303
|
+
hitRate = cacheReadTokens / totalPrompt // NOT cacheRead / input
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
Dividing by the delta yields nonsense like 1721% — which these notes actually did before it was caught.
|
|
307
|
+
For "the parent's cache is undisturbed" (G2), assert **`cacheReadTokens` does not collapse**. Do *not*
|
|
308
|
+
assert that hit percentage stays flat: it falls innocently every time the prompt grows past a cache-block
|
|
309
|
+
boundary while the prefix is fully retained. Measured steady state on this harness: 396,800 cached + 822
|
|
310
|
+
uncached, **99.79%**.
|
|
311
|
+
|
|
312
|
+
## Engine-Path Checks (Phase 0b delta)
|
|
313
|
+
|
|
314
|
+
For the `ChaptersCompactionEngine` path (in-place compaction, no new session). Rounds 12/13/17 already
|
|
315
|
+
proved the *mechanism* offline (durable records, zero usage, realm dispatch — see
|
|
316
|
+
[spikes/probe/FINDINGS.md](../spikes/probe/FINDINGS.md) § Phase 0b); these are the product-level checks
|
|
317
|
+
that still need a running chapter engine:
|
|
318
|
+
|
|
319
|
+
| # | Check | Passes when |
|
|
320
|
+
|---|---|---|
|
|
321
|
+
| E1 | Zero-inference summarization | the `compaction/summary` event carries **no `usage` field** and no provider call appears in the round (proved with a stub at r12/r13; must re-prove with the real TOC `summarize()`) |
|
|
322
|
+
| E2 | Archive honesty after automatic compaction | every `shadowedSeq` of a landed compaction resolves (via durable log + `sourceEventSeqs`) into a written chapter file whose hash matches the registry |
|
|
323
|
+
| E3 | Header cache across a compaction — **CLOSED by r23 (measured, per-machine)** | cloud: `cacheReadTokens` held at **7,424 on both post-replacement turns** while uncached refill fell 13,658 → ~7,000. Local llama.cpp (r28-era probes): reuses NOTHING across a head replacement (0 cached) but steady-state turns cost only the new content. **Never quote one machine's number for the other.** |
|
|
324
|
+
| E2E-TAPE | Acceptance without the GPU | `test:e2e:replay` serves every model turn from `var/model-tape/<project>` (isolated per boot) (proxy smoke: exact + prefix hit, loud 503 miss with journal). Usage fields ride the tape, so the token meter and compaction thresholds fire at identical steps under replay. Tapes are committed test DATA under `tests/fixtures/model-tape/` (the rest of `var/` stays ignored) — CI replays them via `scripts/ci-replay.sh` with no local model; regenerate via `test:e2e:record` (local GPU) when PRODUCT prompts change. The local model remains the distiller and ultimate witness. STATUS 2026-09-23: pipeline current (escape/whitespace classes, volatile rules incl. HOSTPORT-BLIND + derived-count collapses for `(N est tokens)` and host-injected char counts, tool bytes matched out); SEVEN-project replay chain (suite, fanout, heavy, arrival, enrich, rules, fork). Doc edits are ALWAYS free (user decision 2026-10-01, no exceptions): the tapes stand in for ANOTHER project — injected doc bytes are masked from matching, and compaction summaries that paraphrase doc prose ride the tape as recorded model output, so no doc edit ever requires a re-record. The e2e is the integration witness that the deterministic layers cannot replace — it found two real product bugs (the enrich body-hash anchor mismatch and the missing host-plane route) that had been invisible behind the host logger |
|
|
325
|
+
| E9 | P2 enrichment + vocabulary, two-machine grade | `node --test tests/integration/two-machine-enrich.test.ts` + `--project=enrich` tape replay | **green 2026-09-20** — enriched frontmatter/provenance cross the pool byte-exact; B honors A’s aliases and dedupes its own vocab pass; caught+fixed r37 (modifications invisible to commits). Four-project replay chain green, 0 misses |
|
|
326
|
+
| CACHE | **Cache hit rates rise over time; nothing poisons the prefix silently** | Three nets, all born of the 2026-09-25 machine deep-dive (user report: "hit rates FALL over time"). The decoded truth — timeline first, because the first attribution got it wrong twice: a parent session's provider cacheRead rode 11,386→29,656 (hit .997) for seven warm requests, then COLLAPSED to a pinned 9,613 at request seq 78 — BEFORE any compaction event (those land at seq 105). Cause: KV-CAPACITY EVICTION — parent + three fan-out children + a live agent session sharing one 65,536-token llama.cpp pool (total_slots 1 per /props); prefix lookup kept doing exactly its job, returning the shared 9,613-token header for every divergent chain. The engine's pressure compaction then fired CORRECTLY at the meter's number (~66,700 ≥ 0.9×64,000) — the meter is provider baseline + 4-chars/token estimate of content added since the last request, so its trigger point is invisible in provider usage numbers (29,656 looked early; it was not). After the children quiesced the parent RE-WARMED (req 121: read 21,969, hit .97). **Deterministic net 1**: `tape-prefix-stability.test.ts` — every mid-chain tape request must extend its predecessor byte-for-byte (only the two documented replacement points may break a chain); message mutation mid-session — the general cache-poison class — fails this ledger with the offending entry; all seven projects pass. **Deterministic net 2**: `context-hygiene.test.ts` — no context-window literal may ship in src/ or presets/, no preset may pin a window key: the threshold math (floor(configured window × ratio)) can only follow adapter config. **Physical net** (opt-in, live): `npm run test:e2e:cache` serial — asserts the warm curve (no collapse <50% of best cacheRead, ≥50% of prompt growth landing cached, final hit ≥0.90, no compaction in-window; full ledger on failure; first run: 0%→99.7% monotonic). `E2E_CACHE_CONCURRENCY=N` adds N-1 live tenant chains firing per round — VALIDATED at CONCUR=3 (17.6 min): the probe chain still finished hit 0.997 with zero tenant errors, while the interleaved tenants showed the textbook capacity signature the user's log had (cacheRead pinned at 9,617, hit ~0.50 when their turns alternated on the single slot, recovering to 0.998 the moment they got two contiguous turns). Proves the collapse is capacity/interleaving, not the plugin: the same server, same slot, a chain left alone still warms to .997; pass measures this server holding N warm chains, pinned-header/queue-starvation measures the capacity cliff; run it while your own agent sessions are few, and read the ledger SHAPE (collapse-and-stay vs flat-pin vs dip-and-recover) before believing any single attribution | Host-capacity epilogue (2026-09-26, measured on the dev box): single-slot llama.cpp pins interleaved chains to the shared-prefix LCP (9,617 tokens in every experiment); unified KV (--kv-unified, auto slots) crashed b10920 on hybrid-GDN models (Qwen3-Next class; llama.cpp #23210/#21401) — RETESTED WINNING on b11176 (2026-09-27): contention probe CONCUR=3 passed, every chain at 0.997-0.998 with tails restoring one turn after a collision, zero crashes; keep auto slots (1), never force N_PARALLEL>1, re-run scripts/prefix-swap-test.mjs + the contention probe after any image upgrade — one trap measured: LLAMA_ARG_CTX_CHECKPOINTS=0 looks like a saving (upstream invalidates them for the recurrent layers) but on b10920 it deletes ALL cross-request prefix reuse (cached_tokens flat 0); keep the default 32 |
|
|
327
|
+
| E2E | The fork BUTTON, browser journey | **green (2026-09-19, 7 specs / three projects — suite@0.75-of-32K + heavy@0.5-floor-off + arrival@0.9-floor-500 — on port-keyed throwaway homes)**: specs CREATE their sessions through the UI ('New session' + one real Local-model turn; turn-complete = the session's OWN `turn/end` in its own log — the global registry count false-passed twice and was retired). Fixture-free by design after the R29-era rows aged out of the 90-session sidebar. The suite also caught the 'Internal Testing Notice' focus trap and the re-link origin bug (see FINDINGS). |
|
|
328
|
+
| E10 | P3 rules lifecycle (record §7) | unit (status resolution, budget refusal, golden absent-notice bytes) + transport (three machines over a real pool) + e2e `rules` project replay | proposed everywhere by default; per-machine core facts; verbatim in the child's OWN log after UI approve; overflow refuses with per-rule numbers — **green 2026-09-20 at unit/transport grade** (three machines over a real pool; exit criterion covered); the `rules` browser scenario parked out of the default chain pending the r39 diagnosis — evidence-preservation first |
|
|
329
|
+
| E2E-EDGE | Oversized turn handled gracefully | **green in the heavy project (captures 5.3–13.2m; run 11: 5 crossings, 27.8m; 6.0m suite pass)**: one browser turn reads 700KB range-by-range and crosses the threshold 5×: every mid-turn compaction commits (provider `dsh-chapters`, zero usage), every shadowed seq is chapter-covered, the pre-compaction marker survives in the transcript, the turn completes, and the follow-up turn answers — fork row reappears after settle (observed). The model's plan erosion under heavy trims is the fork's motivation; FINDINGS records it. |
|
|
330
|
+
| E4 | Pruner composition | with `tool-result-pruner` mounted in the realm, a chapter covering a pruned result still renders the ORIGINAL text (follow `sourceEventSeqs` backwards), plus the model's inline override where requested |
|
|
331
|
+
| E5 | Shrink-floor refusal is loud | a region too small to yield a smaller framed TOC fails with `ManualCompactionError code='summary'` and changes no surface (r12 behavior; keep as regression) |
|
|
332
|
+
| E6 | Automatic timing inherits the host's |
|
|
333
|
+
| E7 | Arrival-time artifacting (architecture amendment) | **green 2026-09-19 (9.3m)**: 373KB book / one read → exactly 1 arrival prune at the tail; model answers buried token + chapter count via 5 `chapters_artifact` calls; ZERO compaction events at production 0.9; blob fingerprint in no `request/*` event; cache table in FINDINGS shows prefix intact (cacheRead monotonically 15.7K→24.5K) |
|
|
334
|
+
| E8 | Plot carriage across in-place checkpoints | forward path live (persona discipline — suiteB oversized run carries `PLOT:` blocks through 7 compactions); render→extract round trip pinned in unit (`carried plot survives the checkpoint round-trip`); elicited fallback unit-pinned (capped input/output, degrades to null) — fires only when the agent writes no note | compaction lands at `agent/pre-step`, bracketed `compaction/start.turn` = open turn number (r13/r17 `turn: 2`); never mid-request, never mid-step |
|
|
335
|
+
|
|
336
|
+
## Door 5 — Subagent Settlement (#47; read contract corrected by #58)
|
|
337
|
+
|
|
338
|
+
The settlement door is the only archive that runs with nobody watching, so its checks are about **payload**,
|
|
339
|
+
not about plumbing. Every row here was verified to fail on the mutation it guards.
|
|
340
|
+
|
|
341
|
+
| # | Check | How to verify |
|
|
342
|
+
|---|---|---|
|
|
343
|
+
| S1 | The durable read is payload-bearing | `tests/unit/host-durable-read.test.ts` reads the **host's own published types** and asserts `SessionEventRecord` has no `data` key while `readSession`/`snapshotSessionEvent` yields one. This is a kernel-drift gate: a host rename must turn it red, not re-create husks. Cross-check by hand: `zstd -dc ~/.dsh/sessions/<slug>/<id>/session.v4.jsonl.zstd \| head -20` for a settled child — the real log has `data.name`, `data.arguments`, `data.message.content`; a hollow chapter has none of them. |
|
|
344
|
+
| S2 | Both arms, both modes | A **one-shot** child settles with a live handle (`snapshotEvents()`); a **continuable** child is disposed *before* `subagent/end` (`dsh-subagent/lib/index.js:1240-1250`) so only the durable arm runs. Integration rows cover each arm × each mode, and one covers **depth 2** (a continuation child that delegates — `d57b4710`'s parent is `ch-dc3be3ab` in the SAIP field data). |
|
|
345
|
+
| S3 | No archive that rendered nothing | Feed the real `data`-less record shape to `saveSession` and assert: skipped `payloadless`, **no file written**, **no chapter number reserved**, `nextChapterNumber` unchanged. Then the converse pair: the same span read through `readSession` archives with `stats.messages > 0`. |
|
|
346
|
+
| S4 | The watermark is never poisoned | After a S3 refusal, a later settle of the same child still archives (i.e. `max(endSeq)` did not advance). This is the property that makes the defect recoverable instead of permanent. |
|
|
347
|
+
| S5 | Warnings survive the door | `writeArchive` emits `N event(s) rendered no output` for a low-content chapter; assert door 5 logs it (via the injected `warn`/sink), not returns-and-drops. |
|
|
348
|
+
| S6 | Lineage recorded, tree NOT re-homed | After a settle the child's own row/frontmatter carry `subagent` + `parentSession` from the durable header, and the child keeps its OWN tree: `rootSession` is unchanged and its chapter paths still resolve. **AMENDED 2026-10-22 (#58):** this row originally demanded `linkChild` onto the parent's path. Measured against the code, that buys no reach (`buildIndex` walks ancestors only) and re-homes files already written under the child's root, so the door does not link; the row now pins the absence, and `subagent-auto-save.test.ts` ("lineage by design") holds both halves. |
|
|
349
|
+
| S7 | Fanout containment | A 10-child fanout schedules sync 10× and runs enrichment **0** times (`runNow` never per settlement) and no child blocks another (per-child promise chains). |
|
|
350
|
+
| S8 | Field census (human, one-time) | `SELECT` the store: no chapter may have `messages === 0` with `events > 0`. The recovery half of this defect is #60; measure **before/after counts** there, and quote them as a repair result, never as a passing test. |
|
|
351
|
+
|
|
352
|
+
**Anti-pattern that created this section:** a stub that returns *your* type instead of the host's.
|
|
353
|
+
`tests/integration/subagent-auto-save.test.ts` once faked `listEvents: async () => durable` with full
|
|
354
|
+
events and passed against a host verb that returns metadata. If a test proves a door works while the
|
|
355
|
+
production arm returns a different shape, the test is the bug.
|
|
356
|
+
|
|
357
|
+
## Bug Reports
|
|
358
|
+
|
|
359
|
+
Cache and context behaviour are the plugin's core promise, so a report must include:
|
|
360
|
+
|
|
361
|
+
- parent session id, child session id, root session id, store directory, archive ceiling
|
|
362
|
+
- chapter paths written, and any artifact references
|
|
363
|
+
- `usage.cacheReadTokens` for the parent's steps immediately before and after, plus the child's first two
|
|
364
|
+
steps — **and the process generation (which restart) each was taken in**
|
|
365
|
+
- **first-request token counts for both sessions** — the single most diagnostic number, since a silently
|
|
366
|
+
re-seeded child is the most damaging regression
|
|
367
|
+
- the ranges supplied and the refusal text, if a preflight rejected the attempt
|
|
368
|
+
|
|
369
|
+
## The first real-remote push (manual, one-time per project)
|
|
370
|
+
|
|
371
|
+
The sync loop is integration-tested against a fake driver that models the git
|
|
372
|
+
semantics it relies on; the wire protocol itself is isomorphic-git's (https only
|
|
373
|
+
— isomorphic-git 1.42 registers no `file://` transport, verified against its
|
|
374
|
+
transport registry). The first push against a real remote is therefore a
|
|
375
|
+
**manual, one-time step per project**:
|
|
376
|
+
|
|
377
|
+
1. Create a **private** empty repo (record §2.1: per-project, user-created) and a
|
|
378
|
+
scoped token for it (fine-grained PAT limited to that repo).
|
|
379
|
+
2. In a Chapters session: `/chapters-link https://… <token>`.
|
|
380
|
+
The token is stored 0600 at `.dsh-chapters/.git-auth/<projectKey>`.
|
|
381
|
+
3. The command reports the sync steps; on `push rejected`, re-run
|
|
382
|
+
`/chapters-link` (pull-retry) or wait for the next archive event.
|
|
383
|
+
|
|
384
|
+
If isomorphic-git ever chokes on a specific forge's smart-HTTP, the documented
|
|
385
|
+
fallback is a CLI driver (the `GitDriver` seam in `src/gitops.ts` exists for
|
|
386
|
+
exactly this swap) — the loop's logic is driver-agnostic by design.
|
|
387
|
+
|
|
388
|
+
## Prompt recommendations (#71) — SPEC + verification rows
|
|
389
|
+
|
|
390
|
+
Interface: site §28 (`concepts/prompt-recommendations`); host facts in `docs/contract.md`
|
|
391
|
+
§ Composer Ghost Layer. Off by default — **the effective model string non-empty IS the
|
|
392
|
+
enable**; there is deliberately no second boolean.
|
|
393
|
+
|
|
394
|
+
Deterministic rows (each must be proven to fail on the mutation it guards):
|
|
395
|
+
|
|
396
|
+
| Row | Pins |
|
|
397
|
+
|---|---|
|
|
398
|
+
| enable derivation | `effectiveModel = setting('prompt.model') ?? config.promptModel`; empty or unset ⇒ OFF, and a stored empty override beats a non-empty config (`off` verb) |
|
|
399
|
+
| input assembly | chapter summaries first, then the live tail; total ≤ `promptContextChars`; host-injected spans defused BEFORE the cap; deterministic order (same input ⇒ same bytes) |
|
|
400
|
+
| answer validation | trim → reject empty → reject > `promptMaxChars` → ONE retry with the cap restated → refuse with the measured length. Never truncate |
|
|
401
|
+
| cache & watermark | one entry per session keyed by the last completed `turn/end` seq; fresh hit ⇒ zero llm calls; new turn ⇒ recompute on next ask; in-flight dedup shares one promise |
|
|
402
|
+
| command arms | `model` show/set/clear texts, `off`, `suggest` refusals (off / no route / not computable / model failure) each name their remedy |
|
|
403
|
+
| status line | `/chapters-status` prints ON with the model + source, or OFF — both states, linked store or not |
|
|
404
|
+
| client pure logic | `shouldShowGhost` matrix: enabled ∧ !running ∧ draft==='' ∧ hasText ∧ !suppressed/boundary; accept ⇒ setDraft; typing ⇒ hidden; Esc ⇒ suppressed until the boundary moves |
|
|
405
|
+
| client anti-dup | `src/client/index.ts` contains no `commands.execute` for the ghost either — the wire body lives in `src/client/prompt-ghost.ts` ONE time, mirroring the door-runner rule (tests/unit/client-doors.test.ts pattern) |
|
|
406
|
+
| honesty | a suggestion never reaches a chapter file, the notice, the pool, or any model request: an archived session that used the feature contains zero recommendation bytes |
|
|
407
|
+
|
|
408
|
+
Manual browser procedure (browser-plane claim stays **gated by #56** — the corpus must be
|
|
409
|
+
re-anchored before replay can vouch for it):
|
|
410
|
+
|
|
411
|
+
1. `/chapters-prompt model local/qwen3.8-flash` (or a cloud route) ⇒ expect "…enables the feature".
|
|
412
|
+
2. Finish a turn, idle with an empty draft ⇒ the ghost line appears dimmed inside the
|
|
413
|
+
composer card, labeled with the model; the host placeholder is HIDDEN beneath it (draft
|
|
414
|
+
empty ⇒ host placeholder visible — our layer renders over it; if both are readable at
|
|
415
|
+
once, that is a styling bug).
|
|
416
|
+
3. Type one character ⇒ ghost gone instantly; delete it ⇒ ghost back (while not accepted).
|
|
417
|
+
4. Click into the editor, press Right-arrow ⇒ suggestion lands as real draft text, cursor
|
|
418
|
+
at its end; nothing sends. Enter sends as a normal human message — then verify in the
|
|
419
|
+
log that the message is plain human-authored.
|
|
420
|
+
5. Esc on a fresh ghost ⇒ hidden for this turn; send any turn ⇒ boundary moves, a new offer
|
|
421
|
+
may come.
|
|
422
|
+
6. `/chapters-prompt off` ⇒ ghost never reappears in this or later sessions, `suggest`
|
|
423
|
+
refuses; `/chapters-status` says OFF.
|
|
424
|
+
7. Kill the model route (wrong provider) ⇒ ghost hidden, refusal only in command output, no banner.
|
|
425
|
+
|
|
426
|
+
**Distilled, observed, AND replay-verified 2026-10-22** (`prompt-suggestion.spec.ts`, project `prompt`,
|
|
427
|
+
port 41739): the scripted-upstream record run PASSED in a real browser on host 0.2.0-rc.2 — chip visible
|
|
428
|
+
with the suggestion, hidden by one typed character, re-shown on an emptied draft, Escape dismissed
|
|
429
|
+
boundary 1, the next boundary re-armed, ArrowRight accepted into the draft via the host's own
|
|
430
|
+
`InputActions.setDraft`, Enter sent it, and the log held the human message with zero persona bytes.
|
|
431
|
+
A true `E2E_MODEL=replay` run then passed with ALL SIX requests served from tape and zero misses
|
|
432
|
+
(corpus gate lifted for that check ALONE and restored byte-identical — the committed gate still
|
|
433
|
+
refuses the chain until #56 re-anchors the other eight; `tests/fixtures/model-tape/prompt/PROVENANCE.md`). The distill ALSO caught a rig bug worth remembering: `localModelUp` answered "no upstream" while a
|
|
434
|
+
healthy stub ran on the `E2E_UPSTREAM` port — the guard probed :8080 for any LOCAL base, the same
|
|
435
|
+
silent-skip family #54 fixed for remotes. A ROW had pinned that behavior ("loopback is answered on
|
|
436
|
+
:8080, not by E2E_UPSTREAM") — and the row was measured WRONG in use, twice, by this very journey
|
|
437
|
+
refusing to run against a live stub. Per the house rule (change obsolete behavior with its tests):
|
|
438
|
+
the row now pins probe-the-actual-base for local upstreams, plus a dead-local-port NO arm, with the
|
|
439
|
+
reason in its comment. The guard lying by REFUSING is the same disease as lying by nodding.
|
|
440
|
+
|
|
441
|
+
## Release gate — field checks, because green tests shipped a broken product twice
|
|
442
|
+
|
|
443
|
+
Every item below is the shape of a defect that reached `main` or was found in the field (SAIP,
|
|
444
|
+
issues #58–#61, #77–#82) **while the suite was green**. They are not more unit tests: several of
|
|
445
|
+
this week's bugs had rows passing correctly around them. The gate is that a human or an agent
|
|
446
|
+
observes the affordance working on a real project, and that every new instrument is shown to be
|
|
447
|
+
capable of failing.
|
|
448
|
+
|
|
449
|
+
Run on a scratch clone of a real project (not this repo), at the pinned host and pinned window.
|
|
450
|
+
|
|
451
|
+
1. **A continuation opens, not just a continuation created.** Fork (button or `chapters_continue`),
|
|
452
|
+
then require: the app's current session IS the child (read `dsh.sessions.current`, or the door's
|
|
453
|
+
own note — it now refuses to claim a switch it cannot verify), the child is in the sidebar, and
|
|
454
|
+
the state survives a reload. *Why: #77 — three evenings of "the client refuses to catalogue
|
|
455
|
+
plugin-created children" were a door calling `sessions.open`, a verb that does not exist
|
|
456
|
+
(`docs/contract.md` § Client Sessions Service). Green throughout, because no test read the id.*
|
|
457
|
+
2. **The realm is mounted, not declared.** One session header must read `agentPreset: "chapters"`,
|
|
458
|
+
and the sink must show at least one `compactIfNeeded entered` line. *Why: the preset pin sat on
|
|
459
|
+
a retired row id (`agent-presets` → `agent-preset-registry`) and every acceptance boot ran the
|
|
460
|
+
standard realm for two days — zero `compaction/*` events in 15 logs, while leg verdicts looked
|
|
461
|
+
like product failures (#56, #78). "Declared" was asserted; "invoked" never was.*
|
|
462
|
+
3. **Context relief actually happens, with numbers.** Force one oversized read at the pinned
|
|
463
|
+
window; require `compaction/*` events (or an arrival stub pair) in the durable log, then that the
|
|
464
|
+
NEXT turn answers a plain question. *Why: #78's decline is now reported with
|
|
465
|
+
`surface=/trigger=/window=/ratio=`, and a session that survives but answers a stale transcript
|
|
466
|
+
token is exactly what `oversized-turn.spec` caught — after #81, the wrong answer had been the rig
|
|
467
|
+
sending the instruction twice.*
|
|
468
|
+
4. **A command reaches the human.** Run `/chapters-status` and check BOTH the durable
|
|
469
|
+
`command/done` AND the screen; today the screen half fails and that is #80, not a flake. A
|
|
470
|
+
delivered-but-invisible result is a feature the operator cannot use (#61a, #67).
|
|
471
|
+
5. **A disabled feature is quiet in the durable log.** On a boot without `promptModel`,
|
|
472
|
+
`countGhostCommandRuns(log)` must be ≤ 1 (one capability probe per mount). *Why: #79 measured 4–26
|
|
473
|
+
`chapters-prompt` command pairs per session — a "once per mount" effect depending on `[props]`,
|
|
474
|
+
writing into the transcript this plugin archives, with the OFF test passing because it only
|
|
475
|
+
looked at the chip.*
|
|
476
|
+
6. **The rig's pins reach the realm.** The boot's own `cordis.patch.yml` must carry this project's
|
|
477
|
+
`thresholdRatio` and arrival floor on the `chapters-engine` row. *Why: `Pins` were collected and
|
|
478
|
+
applied to nothing for a whole release cycle (leg headers claimed 0.5 while realm defaults
|
|
479
|
+
ran).*
|
|
480
|
+
7. **The corpus is one behavior.** `CORPUS_HOST`'s `host` equals the pin, and every project's
|
|
481
|
+
`recordedAgainst` names the same code generation. If any leg was recorded before a rig-behavior
|
|
482
|
+
change (send timing, window enforcement), re-record it: a mixed corpus passes for the wrong
|
|
483
|
+
reason (#56, #81, #82).
|
|
484
|
+
8. **A boot may read only tracked state.** List every file the e2e home is seeded from and prove each is
|
|
485
|
+
in `git ls-files` — a gitignored path in that chain makes the corpus a recording of one laptop.
|
|
486
|
+
*Why: #89, three layers deep. First the matcher folded no workspace root (CI's `/home/runner/…` vs
|
|
487
|
+
the recorded `/home/adrian/…`); then `spawn('dsh')` took PATH and got a **linked dev build** whose
|
|
488
|
+
version string matched the pin; and even with the pinned tarball booted by absolute path, CI sent a
|
|
489
|
+
2-message first request against a 6-message tape — because `boot.ts` copied
|
|
490
|
+
`.dshdev-local/settings.yaml` (gitignored) with the provider route and `contextWindow: 32000`. Each
|
|
491
|
+
layer looked like the end of the story; the tape diff (`E2E_TAPE_DIFF` / `E2E_MISS_DUMP`) is what
|
|
492
|
+
said otherwise every time. Reproduce across machines before believing a green local chain.*
|
|
493
|
+
|
|
494
|
+
9. **Instrumentation must be shown to be capable of failing.** For every gate this week added, name
|
|
495
|
+
the mutation that reddens it — the hygiene guard, the roster predicate, the report helper, the
|
|
496
|
+
window rows and the bounded-scroll row all have one. *Why: three bugs this week lived in code
|
|
497
|
+
written to observe the fourth — `\bpending\b` missing an escaped newline, `ids.size > 0`
|
|
498
|
+
disabling every #77 candidate, a helper resolving logs against an import-time home so a gate
|
|
499
|
+
could never pass, and an unbounded `scrollIntoViewIfNeeded` that hung a leg for 17 minutes. Each
|
|
500
|
+
looked like a product failure. **An unexercised assertion is a rumor of an assertion.**
|
|
501
|
+
|
|
502
|
+
## Refusal output is grouped by class, with the remainder counted (#101)
|
|
503
|
+
|
|
504
|
+
`/chapters-repair` prints **one line per refusal class per session** via `renderRefusalGroups`
|
|
505
|
+
(`src/repair-run.ts`) — the count, up to three named examples, and `N more of the same class` when items
|
|
506
|
+
were withheld. Un-grouped, the command printed 21 identical `test-litter` refusals across 23 sessions:
|
|
507
|
+
483 repeated lines that made 372 actionable findings unreadable in the maintainer's run, while the offline
|
|
508
|
+
runner had already been aggregating. Aggregation is presentation only — every item survives in
|
|
509
|
+
`groupRefusals`'s output, and a class with a single refusal prints plainly, with no `×1` ceremony.
|
|
510
|
+
|
|
511
|
+
If you add a refusal class, its line must carry the class name before the first `': '`: that is the grouping
|
|
512
|
+
key, and a class that does not follow it will not aggregate (it will not be hidden, just alone).
|
|
513
|
+
|
|
514
|
+
## One unreadable session must not stop a repair (#101)
|
|
515
|
+
|
|
516
|
+
The kernel refuses to open a session log it considers invalid — measured 2026-10-24 when
|
|
517
|
+
`/chapters-repair dry-run` died with `stored session "ch-dc3be3ab…" is corrupt: …` while that log
|
|
518
|
+
decoded cleanly offline (2,924 lines, zero unparseable). The command's per-session read was
|
|
519
|
+
unprotected, so ONE rejected session aborted the whole audit and NOTHING was repaired, including the
|
|
520
|
+
48 rows that were readable. The offline runner already tolerated this; the in-host command did not.
|
|
521
|
+
|
|
522
|
+
The rule, for both planes: a session whose durable log cannot be read is reported by id, with the
|
|
523
|
+
kernel's reason, and skipped. The run still finishes, the output still says which sessions were not
|
|
524
|
+
repaired, and the exit is not a silent success — `chapters-repair` must never let one poison item
|
|
525
|
+
cancel the rest (the same rule that governs the finalize queue and the registry row).
|
|
526
|
+
|
|
527
|
+
## Bringing a store up to spec, at any time (`/chapters-repair`, offline runner)
|
|
528
|
+
|
|
529
|
+
The repair runner is **safe to run whenever**, from any machine, with no host and no model:
|
|
530
|
+
|
|
531
|
+
```bash
|
|
532
|
+
node scripts/chapters-repair.mjs --project <cwd> # audit only — writes nothing
|
|
533
|
+
node scripts/chapters-repair.mjs --project <cwd> --apply all --yes --until-clean
|
|
534
|
+
node scripts/chapters-repair.mjs --list # what the registry holds
|
|
535
|
+
node scripts/chapters-repair.mjs --restore <journal-id> --session <id> --yes # walk one run backwards
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
What it fixes, and why it can be run twice: a single pass does NOT always reach the fixed point, measured
|
|
539
|
+
2026-10-24 on SAIP (1,180 moves → 130 more → 0). Rows that share a `rootSession` tree each need their own
|
|
540
|
+
registration, and a registration can expose a class the next pass handles. `--until-clean` repeats until a
|
|
541
|
+
pass applies 0 moves (max 6), so one command is the whole job. Each pass backs the registry up beside
|
|
542
|
+
itself, writes atomically, and journals every file move under `<tree>/repairs/*.json`. Nothing is ever
|
|
543
|
+
deleted: quarantine is a COPY, and the loser of a twin keeps its bytes.
|
|
544
|
+
|
|
545
|
+
**A nonzero refusal count is the expected terminal state, not a failure.** Some findings refuse on purpose,
|
|
546
|
+
because fixing them would silently demote knowledge or rewrite an archive:
|
|
547
|
+
|
|
548
|
+
- `unregistered-file: number N is already carried by another record — a lost-update twin` (#59). Two files
|
|
549
|
+
claiming one number; choosing a winner is a human call.
|
|
550
|
+
- `test-litter: the store holds a second root …` — directories another rig wrote into this store. Inspect
|
|
551
|
+
before removing; repair will not guess.
|
|
552
|
+
- `empty-artifact: chapter K cites the 0-byte blob …` — the fingerprint of a real historical defect: results
|
|
553
|
+
deferred before the arrival shape was read correctly (fixed on this release). The chapter bodies are
|
|
554
|
+
intact; the blobs never captured content, and repair cannot invent it.
|
|
555
|
+
- `tampered-file` — report-only by design: a record hash that disagrees with the file means the operator
|
|
556
|
+
decides which is truth.
|
|
557
|
+
|
|
558
|
+
Field numbers from 2026-10-24 (both stores, 0.2.0 installed): SAIP **1,441 registrations across 3 passes**,
|
|
559
|
+
terminal state 3 refused (#59 twins); this repo **1,097 moves in one pass**, terminal state 634 refused, of
|
|
560
|
+
which 483 `test-litter` and 143 `empty-artifact`. Registry backups sit at
|
|
561
|
+
`$DSH_HOME/storages/dsh_chapters.json.pre-repair-*.bak` for either run to be undone.
|