@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/provider.md
ADDED
|
@@ -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.
|