@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.
Files changed (195) hide show
  1. package/LICENSE +14 -0
  2. package/README.md +76 -2
  3. package/cordis.patch.yml +363 -0
  4. package/docs/architecture.md +692 -0
  5. package/docs/autorun-guide.md +248 -0
  6. package/docs/backlog.md +2022 -0
  7. package/docs/contract.md +938 -0
  8. package/docs/development.md +990 -0
  9. package/docs/host-compaction-seam.md +225 -0
  10. package/docs/host-protected-head-ask.md +80 -0
  11. package/docs/knowledge-repo.md +1332 -0
  12. package/docs/plans-carriage.md +571 -0
  13. package/docs/project-state.md +642 -0
  14. package/docs/provider.md +98 -0
  15. package/docs/superpowers/plans/2026-10-17-chapters-save.md +198 -0
  16. package/docs/superpowers/plans/2026-11-02-chapter-removal.md +151 -0
  17. package/docs/superpowers/specs/2026-11-02-chapter-removal-design.md +115 -0
  18. package/docs/verify.md +561 -0
  19. package/lib/archive.d.ts +161 -0
  20. package/lib/archive.js +229 -0
  21. package/lib/archive.js.map +1 -0
  22. package/lib/arrival.d.ts +23 -0
  23. package/lib/arrival.js +125 -0
  24. package/lib/arrival.js.map +1 -0
  25. package/lib/artifact-query.d.ts +62 -0
  26. package/lib/artifact-query.js +187 -0
  27. package/lib/artifact-query.js.map +1 -0
  28. package/lib/attach.d.ts +46 -0
  29. package/lib/attach.js +120 -0
  30. package/lib/attach.js.map +1 -0
  31. package/lib/auto-save.d.ts +145 -0
  32. package/lib/auto-save.js +334 -0
  33. package/lib/auto-save.js.map +1 -0
  34. package/lib/aux-message.d.ts +17 -0
  35. package/lib/aux-message.js +20 -0
  36. package/lib/aux-message.js.map +1 -0
  37. package/lib/client.js +1419 -0
  38. package/lib/client.js.map +1 -0
  39. package/lib/codegraph.d.ts +76 -0
  40. package/lib/codegraph.js +190 -0
  41. package/lib/codegraph.js.map +1 -0
  42. package/lib/commands.d.ts +52 -0
  43. package/lib/commands.js +946 -0
  44. package/lib/commands.js.map +1 -0
  45. package/lib/compose.d.ts +30 -0
  46. package/lib/compose.js +185 -0
  47. package/lib/compose.js.map +1 -0
  48. package/lib/continue-core.d.ts +245 -0
  49. package/lib/continue-core.js +405 -0
  50. package/lib/continue-core.js.map +1 -0
  51. package/lib/diag.d.ts +55 -0
  52. package/lib/diag.js +186 -0
  53. package/lib/diag.js.map +1 -0
  54. package/lib/engine-core.d.ts +301 -0
  55. package/lib/engine-core.js +531 -0
  56. package/lib/engine-core.js.map +1 -0
  57. package/lib/engine-note.d.ts +35 -0
  58. package/lib/engine-note.js +32 -0
  59. package/lib/engine-note.js.map +1 -0
  60. package/lib/engine.d.ts +329 -0
  61. package/lib/engine.js +1179 -0
  62. package/lib/engine.js.map +1 -0
  63. package/lib/enrich-queue.d.ts +66 -0
  64. package/lib/enrich-queue.js +126 -0
  65. package/lib/enrich-queue.js.map +1 -0
  66. package/lib/enrich-store.d.ts +31 -0
  67. package/lib/enrich-store.js +103 -0
  68. package/lib/enrich-store.js.map +1 -0
  69. package/lib/enrich-wire.d.ts +163 -0
  70. package/lib/enrich-wire.js +353 -0
  71. package/lib/enrich-wire.js.map +1 -0
  72. package/lib/enrich.d.ts +102 -0
  73. package/lib/enrich.js +267 -0
  74. package/lib/enrich.js.map +1 -0
  75. package/lib/gitops.d.ts +107 -0
  76. package/lib/gitops.js +539 -0
  77. package/lib/gitops.js.map +1 -0
  78. package/lib/index.d.ts +241 -0
  79. package/lib/index.js +1162 -0
  80. package/lib/index.js.map +1 -0
  81. package/lib/indexing.d.ts +98 -0
  82. package/lib/indexing.js +223 -0
  83. package/lib/indexing.js.map +1 -0
  84. package/lib/injections.d.ts +90 -0
  85. package/lib/injections.js +165 -0
  86. package/lib/injections.js.map +1 -0
  87. package/lib/last-resort.d.ts +24 -0
  88. package/lib/last-resort.js +27 -0
  89. package/lib/last-resort.js.map +1 -0
  90. package/lib/log-shape.d.ts +38 -0
  91. package/lib/log-shape.js +93 -0
  92. package/lib/log-shape.js.map +1 -0
  93. package/lib/models-command.d.ts +74 -0
  94. package/lib/models-command.js +100 -0
  95. package/lib/models-command.js.map +1 -0
  96. package/lib/notice.d.ts +74 -0
  97. package/lib/notice.js +92 -0
  98. package/lib/notice.js.map +1 -0
  99. package/lib/plans.d.ts +154 -0
  100. package/lib/plans.js +315 -0
  101. package/lib/plans.js.map +1 -0
  102. package/lib/preset.d.ts +186 -0
  103. package/lib/preset.js +212 -0
  104. package/lib/preset.js.map +1 -0
  105. package/lib/prompt-keys.d.ts +7 -0
  106. package/lib/prompt-keys.js +8 -0
  107. package/lib/prompt-keys.js.map +1 -0
  108. package/lib/prompt-loop.d.ts +73 -0
  109. package/lib/prompt-loop.js +147 -0
  110. package/lib/prompt-loop.js.map +1 -0
  111. package/lib/prompt-push.d.ts +47 -0
  112. package/lib/prompt-push.js +115 -0
  113. package/lib/prompt-push.js.map +1 -0
  114. package/lib/prompt-runner.d.ts +48 -0
  115. package/lib/prompt-runner.js +90 -0
  116. package/lib/prompt-runner.js.map +1 -0
  117. package/lib/prompt-state.d.ts +142 -0
  118. package/lib/prompt-state.js +101 -0
  119. package/lib/prompt-state.js.map +1 -0
  120. package/lib/prompt.d.ts +270 -0
  121. package/lib/prompt.js +408 -0
  122. package/lib/prompt.js.map +1 -0
  123. package/lib/provider.d.ts +44 -0
  124. package/lib/provider.js +66 -0
  125. package/lib/provider.js.map +1 -0
  126. package/lib/redact.d.ts +24 -0
  127. package/lib/redact.js +85 -0
  128. package/lib/redact.js.map +1 -0
  129. package/lib/registry.d.ts +187 -0
  130. package/lib/registry.js +235 -0
  131. package/lib/registry.js.map +1 -0
  132. package/lib/removal.d.ts +159 -0
  133. package/lib/removal.js +556 -0
  134. package/lib/removal.js.map +1 -0
  135. package/lib/render.d.ts +97 -0
  136. package/lib/render.js +530 -0
  137. package/lib/render.js.map +1 -0
  138. package/lib/repair-run.d.ts +76 -0
  139. package/lib/repair-run.js +506 -0
  140. package/lib/repair-run.js.map +1 -0
  141. package/lib/repair.d.ts +208 -0
  142. package/lib/repair.js +357 -0
  143. package/lib/repair.js.map +1 -0
  144. package/lib/repo.d.ts +78 -0
  145. package/lib/repo.js +184 -0
  146. package/lib/repo.js.map +1 -0
  147. package/lib/rules-commands.d.ts +46 -0
  148. package/lib/rules-commands.js +379 -0
  149. package/lib/rules-commands.js.map +1 -0
  150. package/lib/rules.d.ts +267 -0
  151. package/lib/rules.js +478 -0
  152. package/lib/rules.js.map +1 -0
  153. package/lib/save-core.d.ts +72 -0
  154. package/lib/save-core.js +220 -0
  155. package/lib/save-core.js.map +1 -0
  156. package/lib/search.d.ts +112 -0
  157. package/lib/search.js +282 -0
  158. package/lib/search.js.map +1 -0
  159. package/lib/session-rules.d.ts +222 -0
  160. package/lib/session-rules.js +454 -0
  161. package/lib/session-rules.js.map +1 -0
  162. package/lib/signature.d.ts +45 -0
  163. package/lib/signature.js +199 -0
  164. package/lib/signature.js.map +1 -0
  165. package/lib/store.d.ts +156 -0
  166. package/lib/store.js +368 -0
  167. package/lib/store.js.map +1 -0
  168. package/lib/sync.d.ts +220 -0
  169. package/lib/sync.js +810 -0
  170. package/lib/sync.js.map +1 -0
  171. package/lib/tools.d.ts +113 -0
  172. package/lib/tools.js +1090 -0
  173. package/lib/tools.js.map +1 -0
  174. package/lib/treedx/client.d.ts +57 -0
  175. package/lib/treedx/client.js +82 -0
  176. package/lib/treedx/client.js.map +1 -0
  177. package/lib/treedx/provider.d.ts +39 -0
  178. package/lib/treedx/provider.js +479 -0
  179. package/lib/treedx/provider.js.map +1 -0
  180. package/lib/treedx/state.d.ts +27 -0
  181. package/lib/treedx/state.js +65 -0
  182. package/lib/treedx/state.js.map +1 -0
  183. package/lib/types.d.ts +210 -0
  184. package/lib/types.js +31 -0
  185. package/lib/types.js.map +1 -0
  186. package/lib/version.d.ts +9 -0
  187. package/lib/version.js +10 -0
  188. package/lib/version.js.map +1 -0
  189. package/lib/vocabulary.d.ts +41 -0
  190. package/lib/vocabulary.js +118 -0
  191. package/lib/vocabulary.js.map +1 -0
  192. package/lib/warnings.d.ts +10 -0
  193. package/lib/warnings.js +18 -0
  194. package/lib/warnings.js.map +1 -0
  195. package/package.json +151 -3
@@ -0,0 +1,98 @@
1
+ # Knowledge Transport Providers
2
+
3
+ The sync layer's transport is pluggable (governing record: `knowledge-repo.md` §3.2 extended by
4
+ Amendment 2026-09-21). One project record picks one provider; nothing else knows the difference.
5
+ This file is the contract: what a provider must do, which invariants it inherits, and how each
6
+ backend fills them.
7
+
8
+ ## The seam
9
+
10
+ `runSync`'s transport surface is exactly six verbs — a provider is those, plus identity:
11
+
12
+ ```ts
13
+ interface SyncProvider extends GitDriver { // src/provider.ts
14
+ readonly kind: 'git' | 'treedx'
15
+ describe(project: ProjectRecord): string // one honest status line
16
+ ensureClone(dir, remote, opts?) // make the mirror exist and reflect the remote
17
+ initLocal(dir, remote?, opts?) // offline mirror (§5.3)
18
+ removeMirror(dir) // destroy transport (the store is truth, §3.1)
19
+ stageAllAndCommit(dir, message, author) // LOCAL diff workdir-vs-known-baseline
20
+ pullFastForward(dir, remote, opts?) // converge; failure classes diverged|network|auth
21
+ push(dir, remote, opts?) // ship the staged delta
22
+ }
23
+ ```
24
+
25
+ Rules a provider must honor (all inherited from the record, restated at the seam):
26
+
27
+ 1. **The mirror directory exists in every mode.** The workspace store is the read path's truth
28
+ (§3.1); the mirror under `.dsh-knowledge/` is materialized content — search (`loadCorpus`),
29
+ rules (`collectMirrorRules`), the index build, and the notice layers consume it unchanged.
30
+ A provider that skips materialization breaks all of them.
31
+ 2. **Every failure is a value.** `{ok:false, code}` — the machine-readable classes drive
32
+ runSync's branching: `network` (degrade to local-only), `diverged` (rebuild the mirror from
33
+ the store and re-push), `rejected` (pull-and-retry once), `origin-mismatch` (rebuild; never
34
+ sync into the wrong pool), `auth` (surface the re-link instruction with the detail). Providers
35
+ branch on status + code, **never on prose** (r35).
36
+ 3. **ff-only, append-only.** Divergence is never merge-resolved; it is rebuilt
37
+ (§5.2). A provider never rewrites remote content.
38
+ 4. **Nothing throws into the conversation path**, and no token bytes appear in any `detail`
39
+ string (§2.1).
40
+ 5. **Provider selection is scheme-first, policy-strict.** `treedx+<url>/<repo>` → TreeDX;
41
+ anything else → git. `knowledgeProvider: 'git'|'treedx'` forces one and REFUSES a
42
+ contradiction rather than falling back — a wrong pool is a data-privacy failure, not a
43
+ config convenience (`providerKindFor`).
44
+
45
+ ## Backend truth table
46
+
47
+ | Verb | git (`gitops.ts`) | TreeDX (`treedx/provider.ts`) |
48
+ |---|---|---|
49
+ | ensureClone | clone (or adopt existing repo + origin sentinel) | resolve repo in the catalog; fetch refs + `paths/list` (no extension filter — the mirror must be faithful) + `files/read`/`blobs/read`; materialize; write `.treedx-state.json` |
50
+ | initLocal | `git init` + origin | state file with `offline: true` (publish/commit/index/search keep working) |
51
+ | stageAllAndCommit | workdir-vs-HEAD content diff → git commit objects | workdir-vs-baseline diff → staged path list in the state file (baseline advances at stage time: restaging identical bytes says 'nothing to commit', the git parity) |
52
+ | pullFastForward | fetch, ancestor checks, ff; non-ff ⇒ `diverged` | head compare vs state. Unmoved ⇒ up to date. Moved, nothing staged ⇒ refetch (ff). Moved with staged work ⇒ `diverged` — a merge could leave derived files (index/, topics/) built against a stale corpus; the rebuild re-derives |
53
+ | push | git push (rejected ⇒ pull-retry) | workspace-create (`refs/heads/main`, one writable lease per branch) → write each staged path (UTF-8 File API; binary via Blob API) → commit. 409 with head unmoved ⇒ `rejected` (lease), bounded retry; 409 after the head moved ⇒ `diverged`; 401 ⇒ `auth` |
54
+ | offline recovery | remote returns → pull diverged → rebuild | `state.offline` + reachable ⇒ `diverged` (same rebuild-from-store path) |
55
+ | origin drift | `.git/DSH-ORIGIN` sentinel | `state.origin` vs the record's remote ⇒ `origin-mismatch` |
56
+
57
+ ## State: `.treedx-state.json` (the TreeDX mirror's `.git/`)
58
+
59
+ `{origin, head, baseline: {path → sha256}, staged: {message, author, paths} | null, offline, repoId?}`
60
+ — lives inside the mirror, starts with a dot so it never walks into any commit, and is the
61
+ provider's memory across verbs (provider instances stay stateless per call, safe to share).
62
+
63
+ ## Identity and credentials (§2.1/§2.3, one rule for both providers)
64
+
65
+ `projectKey = UUIDv5(NAMESPACE_OID, "treedx/" + canonicalize(baseUrl/repoName))` for TreeDX;
66
+ the unchanged `canonicalize(remote)` for git. The `treedx/` namespace keeps a service whose URL
67
+ happens to canonicalize like a git remote from stealing another's pool. Credentials ride the
68
+ same DSH-home store (`credentials/<projectKey>`, 0600); for TreeDX the token is ALWAYS required
69
+ (no loopback exemption: TreeDX answers 401, and a service URL without auth is a config bug,
70
+ not a local file).
71
+
72
+ ## What a provider may NOT touch
73
+
74
+ The workspace store, the registry domain, chapter rendering, redaction, the injection screen
75
+ (`src/injections.ts`), composition, budgets, notice assembly, and the rules-approval gate. The
76
+ approval (§15 amendment: per-machine curation fact) is the trust boundary shared state passes
77
+ through a human on the local machine — a server (TreeDX included, federation included) cannot
78
+ pre-approve anything, because the fact file lives under `edits/<harness-id>/` and the effective
79
+ status is computed locally.
80
+
81
+ ## TreeDX API surface consumed (spikes/treedx/FINDINGS.md carries the pinning)
82
+
83
+ `GET /health`, `POST /auth/dev-token` (dev), `GET|POST /repos`, `GET /repos/:id/refs`,
84
+ `POST /repos/:id/paths/list`, `POST /repos/:id/files/read`, `POST /repos/:id/blobs/read`,
85
+ `POST /repos/:id/workspaces`, `PUT|DELETE /workspaces/:id/files`, `POST /workspaces/:id/blobs/write`,
86
+ `POST /workspaces/:id/commit`. Server-side search/graph/context (`files/search`, `graph/*`,
87
+ `context/build`) and federation are Phase F: the plugin codes to one ingress URL and federation
88
+ needs no client change.
89
+
90
+ ## Failure semantics summary (§5.3, per backend)
91
+
92
+ | Condition | git | TreeDX |
93
+ |---|---|---|
94
+ | remote down | offline mirror init; push deferred | offline state; push deferred; pull probes recovery → diverged → rebuild |
95
+ | credential bad/revoked | 401 as network class + detail | `auth` class: status says "re-link the token with /chapters-link", conversation untouched |
96
+ | repo missing | GitHub creates on first push for scoped tokens (config'd) | link command creates it; sync-time absence ⇒ `not_found`, degraded with the name in the detail |
97
+ | oversized file | n/a (packs stream) | File API 413 ⇒ refuse-with-numbers detail (§1.2), never truncate; >1 MiB belongs in multipart (Phase F) |
98
+ | lease busy (two machines at once) | push rejected → pull → retry | create/commit 409 → bounded provider retry → `rejected` → runSync's pull-and-retry |
@@ -0,0 +1,198 @@
1
+ # Implementation Plan: `/chapters-save` Command
2
+
3
+ **Issue:** #11
4
+ **Date:** 2026-10-17
5
+ **Author:** Agent (subagent)
6
+ **Status:** ✅ IMPLEMENTED — shipped as a6c840d (#11 → PR #12), hardened by 75ca0ff (#16 → PR #17,
7
+ fire-and-forget enrichment after the timeout bug). `runSave` lives in `src/tools.ts` (`saveCommand`,
8
+ registered beside fork/continue); tests in `tests/unit/save-command.test.ts`; behavior documented on
9
+ the site at `site/src/content/docs/reference/commands.md` § `/chapters-save`. Kept as the design
10
+ record for that command — read it with the shipped code, not as an open to-do.
11
+
12
+ ## Goal
13
+
14
+ Implement `/chapters-save` — a host command that archives the current session into
15
+ searchable chapters **without** creating a child session or triggering compaction.
16
+ The parent session remains fully functional; archived content becomes searchable
17
+ via `chapters_search` from any session in the workspace.
18
+
19
+ ## Success Criteria
20
+
21
+ - `/chapters-save` archives completed turns into verbatim chapter files on disk
22
+ - Session state is updated with new chapter records (search index picks them up)
23
+ - Enrichment is triggered after archiving (if configured)
24
+ - Idempotent: respects the watermark, re-running saves only new content
25
+ - No child session is created
26
+ - No compaction is triggered
27
+ - Returns clear success/error messages
28
+
29
+ ## Architecture Decision
30
+
31
+ `/chapters-save` is the **side-effect-only half** of the deterministic door. It reuses
32
+ the same archive pipeline as `/chapters-fork` and `/chapters-continue`:
33
+
34
+ ```
35
+ composeChapters → renderChapter → writeArchive → commitChapters → scheduler.schedule
36
+ ```
37
+
38
+ But stops before `agents.create`. The key insight: `runContinue` in `src/continue-core.ts`
39
+ bundles rendering + writing + child creation into one function. We need to extract the
40
+ render+write portion into a reusable function, then call it from both `runContinue` and
41
+ the new `runSave`.
42
+
43
+ ### Why NOT reuse `runContinue` directly
44
+
45
+ `runContinue` calls `finishChild` which does budget preflight, notice assembly, and
46
+ `ports.createChild` — all of which are irrelevant for save. Extracting the archive-only
47
+ portion avoids coupling save to child-creation logic and keeps the atomicity contract
48
+ clean.
49
+
50
+ ## File Structure
51
+
52
+ | File | Change | Responsibility |
53
+ |------|--------|---------------|
54
+ | `src/continue-core.ts` | Add `runSave()` function | Pure archive-without-child orchestration |
55
+ | `src/tools.ts` | Add `saveCommand` to `buildChaptersTools` return | Host command definition + handler |
56
+ | `src/tools.ts` | Register `saveCommand` in `registerChaptersTools` | Wire into host command service |
57
+ | `tests/unit/save-command.test.ts` | New test file | Unit tests for the save command handler |
58
+ | `site/src/content/docs/reference/commands.md` | Already documented | No change needed |
59
+
60
+ ## Detailed Tasks
61
+
62
+ ### Task 1: Extract `runSave` in `src/continue-core.ts`
63
+
64
+ Add a new exported function that performs steps 1-3 of `runContinue` (validate, render,
65
+ write, commit) but skips step 4-5 (notice, budget, create child).
66
+
67
+ ```typescript
68
+ // src/continue-core.ts — new export
69
+
70
+ export interface SaveArgs {
71
+ callerSessionId: string
72
+ chapters: ChapterRange[]
73
+ toolResultOverrides: ToolResultOverride[]
74
+ }
75
+
76
+ export interface SaveConfig extends RenderConfig {
77
+ artifactStoreRoot: string
78
+ }
79
+
80
+ export interface SaveResult {
81
+ ok: boolean
82
+ chapters?: Array<{ number: number; path: string; startSeq: number; endSeq: number }>
83
+ warnings?: string[]
84
+ reason?: string
85
+ }
86
+
87
+ export async function runSave(
88
+ ports: Pick<ContinuePorts, 'readCallerEvents' | 'getState' | 'putState' | 'fs'>,
89
+ args: SaveArgs,
90
+ config: SaveConfig,
91
+ ): Promise<SaveResult>
92
+ ```
93
+
94
+ **Implementation notes:**
95
+ - Reuse `validateRanges`, `renderChapter`, `writeArchive`, `appendChapters` from existing imports
96
+ - Use `attemptKey(args.callerSessionId, ceiling)` for idempotent number allocation
97
+ - Call `ports.putState` with `appendChapters(state, wrote.records)` to commit records
98
+ - Do NOT call `ports.createChild`, `assembleNotice`, `preflight`, or `linkChild`
99
+ - Return `{ ok: true, chapters, warnings }` on success
100
+
101
+ ### Task 2: Add `saveCommand` in `src/tools.ts`
102
+
103
+ Inside `buildChaptersTools`, after the `continueCommand` definition (~line 529), add:
104
+
105
+ ```typescript
106
+ const saveCommand: HostCommand = {
107
+ name: 'chapters-save',
108
+ description: 'Archive completed turns into searchable chapters without creating a child session. The parent stays fully functional.',
109
+ input: { hint: '[title]' },
110
+ async handler(invocation): Promise<CommandResult> {
111
+ // 1. Get ports (same as deterministicCommand)
112
+ // 2. Read events, find anchor (last turn/end)
113
+ // 3. Check watermark — if nothing new, return success with "nothing to save"
114
+ // 4. Compose/derive ranges (same logic as deterministicCommand lines 485-494)
115
+ // 5. Call runSave(ports, { callerSessionId, chapters, toolResultOverrides }, config)
116
+ // 6. On success: trigger scheduler.schedule(cwd, 'archive:save')
117
+ // 7. Return success text with chapter count and seq range
118
+ }
119
+ }
120
+ ```
121
+
122
+ **Key differences from `deterministicCommand`:**
123
+ - No `intent`, no `handoffNote`, no `callerPreset`, no `rulesSection`, no `projectLine`
124
+ - No `runContinue` / `runFork` call — uses `runSave` instead
125
+ - No child session ID in the return message
126
+ - Simpler error handling (no Refusal/budget concerns)
127
+
128
+ Update the return value of `buildChaptersTools` (line 734):
129
+
130
+ ```typescript
131
+ return {
132
+ segment: chaptersSegment,
133
+ chaptersContinue, chaptersFork, chaptersSearch, chaptersArtifact, chaptersRulePropose,
134
+ forkCommand, continueCommand, saveCommand // ← add saveCommand
135
+ }
136
+ ```
137
+
138
+ ### Task 3: Register `saveCommand` in `registerChaptersTools`
139
+
140
+ In `registerChaptersTools` (line 737), add `saveCommand` to the command registration loop:
141
+
142
+ ```typescript
143
+ for (const def of [built.forkCommand, built.continueCommand, built.saveCommand]) {
144
+ const disposer = commands.register(def)
145
+ if (typeof disposer === 'function') commandDisposers.push(disposer as () => void)
146
+ }
147
+ ```
148
+
149
+ This is a one-line change to the existing loop — the disposal mechanism already handles N commands.
150
+
151
+ ### Task 4: Write tests in `tests/unit/save-command.test.ts`
152
+
153
+ Follow the pattern from `tests/unit/fork-command.test.ts`:
154
+
155
+ ```typescript
156
+ // Test cases:
157
+ // 1. Saves completed turns into chapter files on disk
158
+ // 2. Updates session state with chapter records
159
+ // 3. Respects watermark — second call with no new turns returns "nothing to save"
160
+ // 4. Returns error when no completed turn exists
161
+ // 5. Triggers scheduler.schedule after successful save
162
+ // 6. Does NOT create a child session (verify agents.create is never called)
163
+ // 7. Handles title argument (optional prefix)
164
+ ```
165
+
166
+ **Test infrastructure:** Same `world()` helper pattern as fork-command tests. Stub `ctx`
167
+ with `agents.create` that throws if called (proving no child is created). Real temp-dir
168
+ fs so chapter files materialize.
169
+
170
+ ### Task 5: Verify documentation
171
+
172
+ `site/src/content/docs/reference/commands.md` lines 77-94 already document `/chapters-save`.
173
+ Verify the documented behavior matches the implementation. No doc changes expected.
174
+
175
+ ## Dependencies Between Tasks
176
+
177
+ ```
178
+ Task 1 (runSave) → Task 2 (saveCommand) → Task 3 (registration) → Task 4 (tests)
179
+ Task 5 (docs verification) can run in parallel with Task 4
180
+ ```
181
+
182
+ ## Risk Assessment
183
+
184
+ | Risk | Mitigation |
185
+ |------|-----------|
186
+ | `runSave` duplicates logic from `runContinue` | Extract shared render+write into a private helper if duplication exceeds ~20 lines |
187
+ | Scheduler not triggered | Mirror the exact `config.scheduler?.schedule(cwd, 'archive:save')` pattern from fork/continue |
188
+ | Search index doesn't pick up new chapters | `commitChapters` via `appendChapters` updates the registry; search reads from registry |
189
+ | Watermark check wrong | Use identical logic to `deterministicCommand` lines 473-480 |
190
+
191
+ ## Verification
192
+
193
+ After implementation:
194
+ 1. `npm test` — all existing tests still pass (no regressions)
195
+ 2. `npm run build` — compiles cleanly
196
+ 3. Manual: run `/chapters-save` in a live session, verify chapter files appear in `.dsh-chapters/`
197
+ 4. Manual: run `chapters_search` from another session, verify archived content is findable
198
+ 5. Manual: run `/chapters-save` again immediately, verify "nothing to save" response
@@ -0,0 +1,151 @@
1
+ # Chapter Removal (`/chapters-remove`) Implementation Plan
2
+
3
+ > **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
+
5
+ **Goal:** Operator command that actually deletes bad chapters (file, orphaned artifacts, plan docs, mirror copy) from a project store, leaving a `removed` stub so the watermark holds and no derived surface re-cites the chapter; journalled, re-derivable undo. Ships as 0.2.17.
6
+
7
+ **Architecture:** Registry widening (`status: 'superseded' | 'removed'` through the one `isActiveChapter` predicate) + a pure `src/removal.ts` core (resolve → plan → journal → unlink → mutate-stub → force sync) wired into `src/commands.ts` beside `chapters-status`. Sync gains clone-side unlinking and a rehydrate guard; dangling citations are reported and logged; restore re-renders the span through the save core repair already uses.
8
+
9
+ **Tech Stack:** TypeScript, zod (domain records), Node ≥ 24 test loop, host slash-command plane (`CommandResult`), git provider (`stageAllAndCommit`).
10
+
11
+ **Spec:** `docs/superpowers/specs/2026-11-02-chapter-removal-design.md` — read it first; the plan argues from it.
12
+
13
+ ## Global Constraints
14
+
15
+ - Durable log append-only (Invariant 1); registry writes only via `store.mutate(sessionId, fn)`; never put a snapshot (#59).
16
+ - Stored paths: mint with `toStoredPath`, compare with `sameStoredPath` — never `===` (#69).
17
+ - One predicate per semantic decision: all active-chapter filtering goes through `isActiveChapter`; no second status list may appear (#59 rule; enrich-wire.ts:195 gets swept).
18
+ - Never commit an archive that rendered nothing (`stats.messages === 0` over `events > 0` is a refusal, before any number is spent).
19
+ - Report caps are config rows; withheld lines are named, never silent (#117). Diagnostics and warnings must reach a human or a file (#61).
20
+ - No model-visible prompt bytes change ⇒ no e2e corpus/tape re-record; the acceptance job's nine model-free legs must stay green untouched.
21
+ - Commits/PR bodies through files (`git commit -F`, `--body-file`), never quoted args with backticks.
22
+ - Version bump = `package.json` + `src/version.ts` + lock root together; `tests/unit/loudness.test.ts` pins `PLUGIN_VERSION === pkg.version`.
23
+ - Both typechecks green: `npm run typecheck` and `npm run typecheck:client`.
24
+ - Node ≥ 24 for test runs; on older node every suite fails misleadingly — check `node --version` first.
25
+
26
+ ## Review Focus
27
+
28
+ (Each line gets its pinning test in the named task.)
29
+
30
+ 1. Shared content-addressed artifact still referenced by a surviving chapter → must NOT be deleted (Task 3).
31
+ 2. Removing the **tail** chapter → watermark still `max(endSeq)` over stubs; next auto-save archives only seqs above the removed span, never re-captures it (Task 2).
32
+ 3. Sync pass running between unlink and push → `rehydrateStoreFromMirror` does not resurrect removed names (Task 4).
33
+ 4. `find` result pool path and a registry path naming the same chapter → resolve to one record; ambiguous or cross-project ids refuse with candidates (Task 3).
34
+ 5. `apply` interrupted (journal write fails, or first unlink succeeds and a second throws) → nothing applied without a journal; partial apply is listed by bytes and named in output and journal (Task 3).
35
+
36
+ ---
37
+
38
+ ### Task 0: Issue, branch, docs commit
39
+
40
+ - [ ] Issue #255 exists; branch `feature/255-chapter-removal` from latest `origin/staging`.
41
+ - [ ] Commit the spec (this plan's source doc) + this plan file: `docs: chapter-removal design record and implementation plan (#255)` via `git commit -F`.
42
+
43
+ ### Task 1: Documentation FIRST
44
+
45
+ **Files:**
46
+ - Modify: `AGENTS.md` (Hard Rules: the append-only bullet gains the sanctioned removal class; What-This-Is/Storage tables stay truthful)
47
+ - Modify: `docs/architecture.md` (integrity/storage section), `docs/knowledge-repo.md` (amendment: deletions in the transport layer + follow-up tombstone note), `docs/verify.md` (manual procedures), one new numbered site page in `site/src/content/docs/` (§29) + cross-link from the repair page; README doorway line.
48
+
49
+ - [ ] Write the guidance amendment exactly as the spec's last section frames it: bad bytes leave the memory; the log stays; the stub keeps the watermark; the journal makes reversal re-derivable; removal is an operator door, not an eighth archive moment.
50
+ - [ ] Site page documents the verbs, identifiers, what is deleted at each layer, the dangling/missing-access log, restore semantics, and the single-machine v1 bar with the multi-machine follow-up named.
51
+ - [ ] `docs/verify.md`: rows for each acceptance criterion with the command or fixture that proves it.
52
+ - [ ] Commit `docs: chapter-removal guidance amendment and operator manual (#255)`; check the issue's docs checkbox.
53
+
54
+ ### Task 2: Registry widening — one predicate
55
+
56
+ **Files:**
57
+ - Modify: `src/archive.ts` (`ChapterRecord`, ~:76), `src/registry.ts` (`isActiveChapter`, ~:213), `src/store.ts` (zod `chapterRecordSchema`), `src/enrich-wire.ts` (~:195 filter → predicate), `src/render.ts` if the stub needs a shape note.
58
+ - Test: `tests/unit/registry.test.ts` (new rows), plus touched rows in `tests/unit/continue.test.ts` / enrichment suites.
59
+
60
+ **Interfaces:**
61
+ - Produces: `isActiveChapter(c: { status?: 'superseded' | 'removed' }): boolean` (excludes both); `ChapterRecord.status?: 'superseded' | 'removed'`; `ChapterRecord.removedAt?: string`.
62
+
63
+ - [ ] **Step 1 (failing tests):** (a) zod round-trips a record with `status:'removed'` + `removedAt`; (b) `buildIndex`/`collectEntries` exclude a `removed` stub and keep `superseded` excluded; (c) watermark math over `[{endSeq:50,status:'removed'}]` yields 50 — assert at the three reduce sites' shared helper or via a small pure function `archiveWatermark(records)` (already exists in continue-core.ts:447 — reuse; extend it, do not duplicate); (d) enrichment candidate scan excludes `removed` (replaces the inline string compare at enrich-wire.ts:195).
64
+ - [ ] **Step 2:** run, watch fail.
65
+ - [ ] **Step 3:** implement the widening; sweep `enrich-wire.ts:195` to `isActiveChapter`; add `status` to zod as `z.enum(['superseded', 'removed']).optional()` and `removedAt` as `z.string().datetime().optional()` (Optional-with-default precedent: old rows parse unchanged).
66
+ - [ ] **Step 4:** run green; `npm run typecheck`.
67
+ - [ ] **Step 5:** commit `feat: registry status 'removed' behind the one active-chapter predicate (#255)`.
68
+
69
+ ### Task 3: Removal core — `src/removal.ts`
70
+
71
+ **Files:**
72
+ - Create: `src/removal.ts`; Test: `tests/unit/removal.test.ts` (+ fixture helpers copied from repair test patterns).
73
+
74
+ **Interfaces (produced for Tasks 4–7):**
75
+ ```ts
76
+ export interface RemovalPorts { fs: () => FsLike; store: RegistryStore; storeRoot: string; cwd: string; now: () => string }
77
+ export type RemovalId = { sessionId: string; number: number }
78
+ export function resolveRemovalId(raw: string, rows: Iterable<[string, SessionState]>, cwd: string): { sessionId: string; record: ChapterRecord } | { refusal: string }
79
+ export function planRemoval(plan: { target: { sessionId: string; record: ChapterRecord }[]; rows: Iterable<[string, SessionState]>; fs: ...; storeRoot: string; cwd: string }): { removals: RemovalEntry[]; dangling: DanglingRef[]; refused: string[] }
80
+ export function applyRemoval(ports: RemovalPorts, plan: ReturnType<typeof planRemoval>): Promise<{ journalId: string; removed: string[]; problems: string[] }>
81
+ export function scanDangling(fs, roots: string[], needles: string[]): DanglingRef[]
82
+ export function appendMissingAccess(storeRoot: string, lines: string[]): void
83
+ ```
84
+
85
+ - [ ] **Step 1 (failing unit tests)** — names and assertions:
86
+ - `resolveRemovalId accepts registry path / pool path / "<sessionId>:<number>" / bare number against the display index`
87
+ - `resolveRemovalId refuses unknown id naming near candidates` ; `refuses ambiguous pool path hitting two sessions` ; `refuses a path under a different store root (cross-project)`
88
+ - `planRemoval keeps an artifact blob still named by an ACTIVE record; deletes one named only by removed/none` (two records sharing sha, one active)
89
+ - `planRemoval includes planDocs + plan artifactPath files in the deletion set`
90
+ - `planRemoval dangling scan reports a surviving chapter whose body cites "007-evil.md"`
91
+ - `applyRemoval writes the journal BEFORE the first unlink (throw on journal write ⇒ zero unlinks, record unmutated)`
92
+ - `applyRemoval partial failure lists what went and journals it (second unlink throws)`
93
+ - `applyRemoval mutates through store.mutate and leaves a stub: number/startSeq/endSeq/path/sha256/artifacts kept, status 'removed', removedAt set`
94
+ - [ ] **Step 2:** run to fail.
95
+ - [ ] **Step 3:** implement. Notes for choices the tests don't fix: journal lives at `<storeRoot>/<rootSession>/removals/<journalId>.json` with the `repair-run.ts:196-247` write-ahead pattern (same `journalId` format); needles for the dangling scan = removed filename basename + registry path (match via `sameStoredPath` for path arms, substring for body-citation arms); store-root scoping = `resolveStoreRoot`-adjacent path prefix check.
96
+ - [ ] **Step 4:** green; commit `feat: removal core — resolve/plan/journal/apply with orphan-artifact math (#255)`.
97
+
98
+ ### Task 4: Sync correctness
99
+
100
+ **Files:**
101
+ - Modify: `src/sync.ts` (`rehydrateStoreFromMirror` ~:488 + its caller in `runSync`; possibly a `removedNames` set threaded from registry rows read by the sync pass), `src/removal.ts` (clone-side unlink in `applyRemoval` behind a `cloneDir?: string` port; force-sync trigger).
102
+ - Test: `tests/integration/sync.test.ts` new rows + `tests/unit/removal.test.ts` clone-arm rows.
103
+
104
+ - [ ] **Step 1 (failing):** (a) given a mirror clone containing `chapters/<key>/<s>/007-x.md` and a store stub naming it removed, `rehydrateStoreFromMirror` restores zero files; (b) after clone unlink, `buildIndexInClone` output no longer contains the removed entry; (c) stale topic shard cleanup on the `changed=true` branch (read the current behavior first — if shards for vanished topics already get removed, pin it with a test instead of writing code).
105
+ - [ ] **Step 2:** run to fail.
106
+ - [ ] **Step 3:** implement guard + unlink arms; keep the copy-only invariant of `publishIntoClone` (deletions are done by the removal, not the publisher — the doc comment in sync.ts must say so).
107
+ - [ ] **Step 4:** green (integration sync suites on Node ≥ 24 — check `node --version`); commit `feat: deletions publish to the pool and the mirror never rehydrates them (#255)`.
108
+
109
+ ### Task 5: Missing-access log
110
+
111
+ **Files:**
112
+ - Modify: `src/removal.ts` (apply writes dangling lines to `<storeRoot>/removals/missing-access.log`), `src/continue-core.ts`/`src/prompt.ts` where `verifyChapter` mints `"<path>: missing since archived"` — emit a structured dead-citation line there too (grep both call sites; the delivery must not depend on the caller reading `warnings`).
113
+ - Test: `tests/unit/removal.test.ts` append rows + `tests/integration/` TOC-build row.
114
+
115
+ - [ ] **Step 1 (failing):** `removal appends dangling report lines "ts | removed=<path> | referrer=<path> | how=cited-in-body"` ; `collectEntries marks a chapter missing and appends a dead-citation line naming the citing session`.
116
+ - [ ] **Step 2:** run to fail. **Step 3:** implement; confirm the #61 diagnostics sink already receives the warning text (it does for doors — verify, don't assume). **Step 4:** green; commit `feat: missing-access log for removed-chapter citations (#255)`.
117
+
118
+ ### Task 6: `/chapters-remove` command wiring
119
+
120
+ **Files:**
121
+ - Modify: `src/commands.ts` (new command object; shape = `statusCommand` :271-…), `src/index.ts` (registration + config plumbing; report cap row e.g. `removalReportMaxLines` defaulting to `repairReportMaxLines`'s value), `src/diag.ts` if a status line helper is wanted.
122
+ - Test: `tests/integration/commands-removal.test.ts` (fixture store: 3 sessions, 5 chapters, one linked project with a throwaway loopback remote; port-keyed temp homes per rig rules).
123
+
124
+ - [ ] **Step 1 (failing integration):** verbs — `list` prints flat ancestor index with canonical ids + statuses; `find` returns pool-scoped hits; `dry-run` prints deletion set + dangling report + `NOTHING WRITTEN`; `apply` journals and reports counts; `report` tails the log with the cap and withheld count named; unknown verb/args refuse without touching state; no-session/no-cwd invocation refuses cleanly.
125
+ - [ ] **Step 2–4:** implement the handler against the core; green; commit `feat: /chapters-remove operator command (#255)`.
126
+ - [ ] `/chapters-status` addition (one line, printed in EVERY state incl. unlinked): `Removals (this project): N removed stub(s); missing-access log: <path or 'empty'>`. Test it renders in the unlinked case.
127
+
128
+ ### Task 7: Restore = re-derivation
129
+
130
+ **Files:**
131
+ - Modify: `src/removal.ts` (`restoreRemoval(ports, journalId)`), reuse the explicit-span seam `src/save-core.ts` exposes to repair (`spanOverride`, see save-core ~:70/:189 and `repair-run.ts` usage).
132
+ - Test: `tests/integration/removal-restore.test.ts` with a synthetic durable log (deterministic bulk per rig rules — no model).
133
+
134
+ - [ ] **Step 1 (failing):** `restore re-renders the journaled span at a NEW number, chapter bytes sha-match the journal's recorded sha256`; `restore of a span whose render yields zero messages refuses, stub untouched`; `restore is journaled (second journal entry, or appended to the same file)`; `restore after supersede-by-repair of the same span refuses and says why`.
135
+ - [ ] **Step 2–4:** implement, green; commit `feat: /chapters-remove restore re-derives a removed chapter from the durable log (#255)`.
136
+
137
+ ### Task 8: Full deterministic verification — run ONCE
138
+
139
+ - [ ] Confirm `node --version` ≥ 24 (if not, find the pinned runtime — the local-llm/dev-platform docs name the rig; do not trust red suites on 22.x).
140
+ - [ ] `npm run typecheck && npm run typecheck:client && npm test` — all green.
141
+ - [ ] `git diff origin/staging -- tests/e2e` shows **no** changes to model-visible prompts/tool descriptions ⇒ no re-record; the CI acceptance job must run its nine model-free legs untouched as proof.
142
+ - [ ] `node scripts/check-env-parity.mjs` — note drift for the maintainer's install step (do not force the live home).
143
+
144
+ ### Task 9: PR → staging → RC (stop at the maintainer's gate)
145
+
146
+ - [ ] `git fetch origin staging && git merge origin/staging` into the branch; resolve; push.
147
+ - [ ] Check every acceptance box in #255 with evidence (test names / CI run ids); post the final progress comment.
148
+ - [ ] PR to `staging` per template via `--body-file`, title `feat: /chapters-remove — spot-delete contaminated chapters (#255)`, `Closes #255`.
149
+ - [ ] CI green ⇒ squash-merge to staging (agents may).
150
+ - [ ] Bump `0.2.17` (three files together) `chore: release 0.2.17 — /chapters-remove (#255)`; open RC PR `staging → main` (`Release candidate: 0.2.17`, `Changes since 0.2.16`).
151
+ - [ ] **Do not merge or tag.** File the cross-machine-tombstone follow-up issue; link it in the RC body and the design doc. Hand the maintainer: parity script output + a note that BOTH of their harnesses need a restart to load the new copy.
@@ -0,0 +1,115 @@
1
+ # Chapter Removal — design record (issue #255)
2
+
3
+ Date: 2026-11-02. Decided with the maintainer across three brainstorming rounds in the session
4
+ that archived this design; approved plan at `docs/superpowers/plans/2026-11-02-chapter-removal.md`.
5
+
6
+ ## Intent and the why
7
+
8
+ Operators mistype prompts meant for another project into a project session. When that session
9
+ archives, the contaminated turns become chapters that flow through every derived surface from that
10
+ point on — TOCs, index shards, search, enrichment. The maintainer asked for the ability to see the
11
+ active chapters of a session and **spot-delete** the bad ones, removing the chapter *everywhere it
12
+ matters*: file, metadata, artifacts, and references.
13
+
14
+ The bar the maintainer set, in their words: removal must be *actual* — "I want the bad chapters
15
+ actually removed" — and the project guidance must accept that **removal can be good when the data
16
+ is bad**. What matters is the generated TOCs from that point on; git history of the pool and other
17
+ chapters that happen to cite the removed file are acceptable residue. An open question the
18
+ maintainer raised themselves: a log of missing-chapter access from other chapters, so they can
19
+ decide whether those referrers need to stay too.
20
+
21
+ ## Goals
22
+
23
+ 1. `/chapters-remove` — operator-only command: `list` (active TOC view with canonical ids),
24
+ `find <query>` (project pool-mirror search), `[dry-run] <ids…>`, `apply <ids…>`, `report`,
25
+ `restore <journalId>`.
26
+ 2. `apply` deletes the chapter file, its **orphaned** artifacts and plan docs, from the store AND
27
+ the pool mirror clone (so the deletion publishes and the derived index regenerates clean), and
28
+ flips the registry record to a `status: 'removed'` stub.
29
+ 3. No TOC, search result, or enrichment pass cites a removed chapter from that moment on —
30
+ through the ONE predicate `isActiveChapter` (no second filter lists).
31
+ 4. The archive watermark never rewinds: a removed chapter's span is never silently re-captured
32
+ by an auto-archive moment.
33
+ 5. Every removal is journaled; `restore` re-derives the chapter from the durable log through the
34
+ same save core (a NEW number — identity is `(number, sha256)`).
35
+ 6. Dangling citations of removed chapters are reported at removal time and logged thereafter
36
+ (`removals/missing-access.log`), per the #61 rule that a detected failure is a delivered one.
37
+
38
+ ## Non-goals
39
+
40
+ - Not a model-facing tool. The human made the mistake; the human pulls the trigger. This also
41
+ keeps every model-visible prompt byte unchanged — no corpus/tape re-record (rig rule).
42
+ - Not scrubbing git history, already-seeded child-session TOCs, or other projects' stores.
43
+ Invariant 1 (append-only durable log) is never bent.
44
+ - Not cross-machine tombstones: v1 guarantees correctness on the removing machine and on clones
45
+ pulled after the deletion commit. A second machine holding a local copy can re-publish it; the
46
+ follow-up is a removal fact carried in the per-session collections JSONL (which already syncs),
47
+ consumed by `planStoreToRepo`. Filed as follow-up at PR time.
48
+
49
+ ## Anti-goals (what would make this a failure)
50
+
51
+ - A removed chapter reappearing — via watermark rewind (re-archived span), via mirror rehydration,
52
+ or via a stale put resurrecting the record. Each has a named test.
53
+ - Deleting bytes that a *surviving* chapter still references (shared content-addressed blobs).
54
+ - A destructive apply that cannot be explained or reversed: no journal written before the first
55
+ unlink ⇒ refuse.
56
+
57
+ ## Constraints (from AGENTS.md, load-bearing)
58
+
59
+ - Append-only durable log; registry mutation only through `store.mutate(sessionId, fn)`.
60
+ - Stored paths are one dialect: `sameStoredPath(a, b, cwd)`, never `===` (#69).
61
+ - One predicate per semantic decision (#59) — this feature extends `isActiveChapter`, it must not
62
+ fork it; the hand-rolled `!== 'superseded'` in `src/enrich-wire.ts:195` is swept into the
63
+ predicate as part of this change.
64
+ - Never bound reports loosely: caps are config rows, withheld counts are named (#117/#61).
65
+ - Three schema systems, never mixed: zod for domain records (`ChapterRecord`), plain specs for
66
+ tool params, Schemastery for `Config`.
67
+ - Version bumps touch `package.json`, `src/version.ts`, and the lock root together
68
+ (`loudness.test.ts` pins it); commit/PR bodies via files, never quoted args.
69
+
70
+ ## The mechanics, as decided
71
+
72
+ **Why the stub survives deletion.** The watermark is `max(chapter.endSeq)` over all records in the
73
+ author session's row (`src/save-core.ts:187`, `src/tools.ts:559`, `src/continue-core.ts:279`).
74
+ Deleting the record outright rewinds the watermark and the next archive moment re-captures the
75
+ poisoned span — the exact failure the feature exists to prevent. So the record keeps `number`,
76
+ `startSeq`/`endSeq`, `path`, `sha256`, the artifact manifest (for orphan forensics and restore
77
+ re-counting), title, plus `status: 'removed'` and `removedAt`. The *bytes* are gone; the *span book
78
+ keeping* is not. `nextChapterNumber` never rewinds: numbers are never re-spent.
79
+
80
+ **Orphan math.** Artifacts are content-addressed and deduplicated across the subtree, so a blob is
81
+ deleted only when no *active* record in any session tree under the project's store root names it
82
+ (chapter `artifacts`, `plans[].artifactPath`, `planDocs[]`). `removed` stubs do not hold blobs
83
+ open — their references are exactly what the operator judged bad.
84
+
85
+ **Publish + rehydrate.** `publishIntoClone` only copies new files; a store-side deletion never
86
+ reaches the pool by itself, so `apply` unlinks the clone-relative paths directly and forces a sync
87
+ pass, letting `stageAllAndCommit` record `git rm` deletions. Independently,
88
+ `rehydrateStoreFromMirror` (which copies *absent* files back) gains a guard: names belonging to
89
+ `removed` stubs are never restored — closing the window between unlink and push, and protecting a
90
+ future `git checkout` of an old pool commit.
91
+
92
+ **Identifiers.** Canonical id is the registry `path` (workspace-relative, the #69 dialect).
93
+ `find` results (`chapters/<projectKey>/<session>/<file>` pool dialect) map through the
94
+ `<session>/<file>` tail; a TOC display ordinal from `list` is accepted alongside `list` output but
95
+ re-checked against the flat index at apply time. Unknown/ambiguous ids refuse with near candidates;
96
+ an id resolving outside this project's store root refuses outright.
97
+
98
+ **Dangling / missing-access.** At removal, scan every active chapter file (store and clone) plus
99
+ registry `plans` titles for citations of the removed filename; report + append to
100
+ `<store-root>/removals/missing-access.log`. Ongoing: where `collectEntries` mints
101
+ `"<path>: missing since archived"` warnings, also append a structured dead-citation line. The
102
+ report verb reads the log tail with the same cap discipline as repair reports.
103
+
104
+ **Restore.** Re-render the journaled span through the same save-core path repair already uses for
105
+ explicit-span re-derivation, commit as a NEW chapter number, flip the stub's tombstone into the
106
+ new record's lineage (`repairs: <n>`-style provenance), and journal the restore. A restore whose
107
+ render yields zero messages is refused (never-commit-an-empty-archive rule), not faked.
108
+
109
+ ## Guidance amendment (docs-first deliverable)
110
+
111
+ AGENTS.md's Hard Rule currently says the store is append-only "with no exceptions" and damage is
112
+ "superseded, not repaired in place". It gains the sanctioned operator class beside #60 repair:
113
+ **removal of bad data** — bytes leave the store, the log stays, the stub keeps the watermark, and
114
+ the journal makes reversal re-derivable. The seven archive moments are untouched: removal is an
115
+ operator door, not a moment.