@treeseed/dsh-chapters 0.1.24 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. package/README.md +2 -1
  2. package/cordis.patch.yml +49 -1
  3. package/docs/architecture.md +1 -496
  4. package/docs/backlog.md +234 -4
  5. package/docs/contract.md +127 -1
  6. package/docs/development.md +248 -12
  7. package/docs/host-compaction-seam.md +108 -0
  8. package/docs/knowledge-repo.md +532 -16
  9. package/docs/project-state.md +323 -0
  10. package/docs/verify.md +266 -0
  11. package/lib/archive.d.ts +49 -1
  12. package/lib/archive.js +56 -2
  13. package/lib/archive.js.map +1 -1
  14. package/lib/arrival.js +24 -10
  15. package/lib/arrival.js.map +1 -1
  16. package/lib/attach.d.ts +46 -0
  17. package/lib/attach.js +120 -0
  18. package/lib/attach.js.map +1 -0
  19. package/lib/auto-save.d.ts +74 -0
  20. package/lib/auto-save.js +175 -26
  21. package/lib/auto-save.js.map +1 -1
  22. package/lib/client.js +413 -11
  23. package/lib/client.js.map +1 -1
  24. package/lib/commands.d.ts +8 -1
  25. package/lib/commands.js +135 -7
  26. package/lib/commands.js.map +1 -1
  27. package/lib/compose.js +25 -6
  28. package/lib/compose.js.map +1 -1
  29. package/lib/continue-core.d.ts +24 -1
  30. package/lib/continue-core.js +41 -9
  31. package/lib/continue-core.js.map +1 -1
  32. package/lib/diag.d.ts +55 -0
  33. package/lib/diag.js +186 -0
  34. package/lib/diag.js.map +1 -0
  35. package/lib/engine-core.d.ts +53 -1
  36. package/lib/engine-core.js +64 -7
  37. package/lib/engine-core.js.map +1 -1
  38. package/lib/engine.d.ts +99 -0
  39. package/lib/engine.js +488 -36
  40. package/lib/engine.js.map +1 -1
  41. package/lib/enrich-queue.js +28 -2
  42. package/lib/enrich-queue.js.map +1 -1
  43. package/lib/enrich-wire.d.ts +92 -2
  44. package/lib/enrich-wire.js +135 -17
  45. package/lib/enrich-wire.js.map +1 -1
  46. package/lib/enrich.d.ts +27 -2
  47. package/lib/enrich.js +114 -18
  48. package/lib/enrich.js.map +1 -1
  49. package/lib/index.d.ts +52 -0
  50. package/lib/index.js +182 -2
  51. package/lib/index.js.map +1 -1
  52. package/lib/indexing.d.ts +7 -0
  53. package/lib/indexing.js.map +1 -1
  54. package/lib/injections.d.ts +1 -0
  55. package/lib/injections.js +38 -0
  56. package/lib/injections.js.map +1 -1
  57. package/lib/last-resort.d.ts +24 -0
  58. package/lib/last-resort.js +27 -0
  59. package/lib/last-resort.js.map +1 -0
  60. package/lib/prompt.d.ts +130 -0
  61. package/lib/prompt.js +255 -0
  62. package/lib/prompt.js.map +1 -0
  63. package/lib/registry.d.ts +39 -1
  64. package/lib/registry.js +76 -9
  65. package/lib/registry.js.map +1 -1
  66. package/lib/render.d.ts +31 -3
  67. package/lib/render.js +75 -3
  68. package/lib/render.js.map +1 -1
  69. package/lib/repair-run.d.ts +49 -0
  70. package/lib/repair-run.js +423 -0
  71. package/lib/repair-run.js.map +1 -0
  72. package/lib/repair.d.ts +200 -0
  73. package/lib/repair.js +354 -0
  74. package/lib/repair.js.map +1 -0
  75. package/lib/rules-commands.d.ts +22 -0
  76. package/lib/rules-commands.js +177 -20
  77. package/lib/rules-commands.js.map +1 -1
  78. package/lib/rules.d.ts +184 -7
  79. package/lib/rules.js +312 -27
  80. package/lib/rules.js.map +1 -1
  81. package/lib/save-core.d.ts +31 -1
  82. package/lib/save-core.js +153 -17
  83. package/lib/save-core.js.map +1 -1
  84. package/lib/session-rules.d.ts +222 -0
  85. package/lib/session-rules.js +454 -0
  86. package/lib/session-rules.js.map +1 -0
  87. package/lib/store.d.ts +22 -1
  88. package/lib/store.js +94 -17
  89. package/lib/store.js.map +1 -1
  90. package/lib/sync.js +13 -1
  91. package/lib/sync.js.map +1 -1
  92. package/lib/tools.d.ts +14 -0
  93. package/lib/tools.js +321 -32
  94. package/lib/tools.js.map +1 -1
  95. package/lib/types.d.ts +12 -1
  96. package/lib/types.js.map +1 -1
  97. package/lib/version.d.ts +9 -0
  98. package/lib/version.js +10 -0
  99. package/lib/version.js.map +1 -0
  100. package/lib/warnings.d.ts +10 -0
  101. package/lib/warnings.js +18 -0
  102. package/lib/warnings.js.map +1 -0
  103. package/package.json +9 -3
@@ -286,6 +286,29 @@ Honest quality bar: strong for coding sessions (files and commands leave loud si
286
286
  degrading to time/size splitting for pure-prose conversations — never worse than today's
287
287
  behavior, never blocking.
288
288
 
289
+ **Amendment 2026-10-22 (#61, item 6) — a delegate's report to its parent IS conversation.** The
290
+ injection screen (H1) drops host-injected `user/message` events, and it was right to drop the project's
291
+ instruction files. But the same rule was also dropping two shapes the harness produces *about this
292
+ session's own work*:
293
+
294
+ | source (measured on 0.2.x logs) | content | was | now |
295
+ |---|---|---|---|
296
+ | `{ kind: 'agent-message', form: 'relay' }` | "Agent <id> sent a message: Both Starlight documentation files have been written…" | screened | **archived** |
297
+ | `{ kind: 'subagent-settled', form: 'notice' }` | "Background subagent <id> finished and will do no further work…" | screened | **archived** |
298
+ | `{ kind: 'agent-instructions', form: 'instructions' }` | a project's workspace instruction file | screened | **still screened** |
299
+
300
+ Measured cost of the old rule in the first `chapters`-preset project: **44 markers, 248,150 chars ≈ 62K
301
+ est tokens** of delegated results thrown away, including an 18,253-char spike report — so the work that
302
+ crossed the delegation edge was lost through **both** doors built to capture it (the parent's archive
303
+ dropped the report; the child's settlement had already archived its own bytes, so the parent's chapter
304
+ recorded a delegation with no outcome).
305
+
306
+ Still named by **producer AND form**, exactly like the `plan-mode` precedent it joins — the list cannot
307
+ widen by accident into the general plugin-source exemption, and an instruction file arriving with some
308
+ other form still screens. `isHumanAuthored` stays strict on purpose: the topic-signature layer keeps
309
+ ignoring these, because the r28 lesson (injected text moves chapter boundaries) is unchanged, and a
310
+ 2 KB report is exactly the kind of text that would drag a signature toward `docs/…` filenames.
311
+
289
312
  ### 4.3 The completeness guard
290
313
 
291
314
  Chapters include **only collections with a `turn/end`** inside the span. If a compaction
@@ -294,6 +317,33 @@ cannot produce one, §4.1), the composition clamps to the last completed collect
294
317
  TOC notes the clamp. The unfinished collection stays in the live context, per the user
295
318
  requirement, and composes at the next archive.
296
319
 
320
+ **Amendment 2026-10-22 (#59, measured in the first real `chapters`-preset project).** This guard is
321
+ one-directional: it keeps unfinished work OUT, and says nothing about what may silently fail to come
322
+ IN. Composing from collections alone means a completed turn that never got a signature vanishes — not
323
+ truncated, not deferred, **absent from every chapter and from every index**, with no warning. That is
324
+ what happened: `session-2edf9694` had 32 `turn/end` events and 31 collections, and the 203-event span
325
+ between chapters 27 and 28 — a complete turn including the maintainer's "I consider it foundational to
326
+ the project" hosting brief and its implementation — exists in no chapter in any tree, and the three
327
+ verbatim phrases from it match zero files in the store.
328
+
329
+ The guard is therefore **bidirectional** now:
330
+
331
+ 1. Nothing unfinished is archived (the rule as written above).
332
+ 2. **Nothing finished is dropped.** The composed ranges must cover `[spanStart..spanEnd]`
333
+ contiguously; an unsigned but *completed* span is absorbed into its neighbouring chapter (a
334
+ wider range, never a re-titled one), and every door asserts the coverage before writing,
335
+ refusing with the exact seqs rather than proceeding. The claim a chapter makes is
336
+ "everything in this range is here", so a hole is a false statement, not a small one.
337
+ 3. A door may not treat "no collection covered it" as "incomplete turn": completeness is a
338
+ `turn/end` fact, read from the log, not inferred from the presence of a signature — a lost
339
+ signature is a registry defect (see docs/architecture.md § The row is shared), and the note the
340
+ composer used to emit claimed the opposite while dropping completed work.
341
+
342
+ `coverage()` (`src/archive.ts`) existed for exactly this and was called in one door, for a warning
343
+ that got counted and thrown away. The same rule now covers model-submitted ranges:
344
+ `validateRanges` refuses a **gap**, not only an overlap — AGENTS.md already said "a gap silently
345
+ drops it"; the check did not exist.
346
+
297
347
  ### 4.4 Consequence: composition is archive-time-only
298
348
 
299
349
  Because chapters are append-only (invariant 3), the merge threshold must be right *when the
@@ -415,7 +465,33 @@ search, and sync. Frontmatter: `kind: rule`, plus `category`, `status`, and prov
415
465
  plugin's modularity is what makes this nearly free — the chapter code paths are parameterized
416
466
  by `kind`, not duplicated.
417
467
 
418
- ### 7.2 Lifecycle: proposed → core, approved in-session
468
+ **Two scopes, one store, one format** (Amendment 2026-10-20, §15). Session rules are *not* a
469
+ second subsystem: they are `kind: rule` files in the same `rules/<harness>/` tree, with the same
470
+ write-once immutability, the same `list` surface, the same search index. What differs is **how
471
+ they get authored and how they become active**:
472
+
473
+ | Tier | Authored by | Activation | Transport | Carried by |
474
+ |---|---|---|---|---|
475
+ | **project** | `/chapters-rule add`, `chapters_rule_propose` (agent) | human `approve` → per-machine curation fact (§7.2) | synced to the pool | continue **and** fork |
476
+ | **session** | `ADD RULE:` marker (deterministic), enrichment (optional) | **derived** (§7.5) — no fact, no approval | local only; never staged (§7.5) | continue only — dropped by fork (§7.6) |
477
+
478
+ Extra frontmatter on every rule file: `scope: project | session` (absent ⇒ `project`, so every
479
+ pre-existing record keeps working byte-identically), `session: <sessionId>` (session tier only),
480
+ `derivedFrom: human | marker | model`, `pinned: true` (§7.4). Same `category`, `status`,
481
+ `sha256`, provenance chain as before.
482
+
483
+ **Why the split is safe.** A rule that only ever governs one machine's session tree is not shared
484
+ state, so §11.3's blast-radius argument does not apply to it — and the per-machine approval
485
+ boundary of §7.2 is left exactly as the 2026-09-20 amendment set it. Cross-checkout carry of
486
+ *project* rules needed no new mechanism: `projectKey` is UUIDv5 over the canonicalized remote
487
+ (§2.3) and `harnessId` defaults to the hostname, so several checkouts of one repository already
488
+ resolve to one rule namespace on one machine. §7.8 is about making the cases where that identity
489
+ *diverges* loud, not about building propagation.
490
+
491
+ ### 7.2 Lifecycle: proposed → core, approved in-session *(the project tier)*
492
+
493
+ This section governs `scope: project` — the shared, team-visible tier. The session tier's
494
+ activation is derived and needs no approval (§7.5).
419
495
 
420
496
  - **Proposal:** a user command (`/chapters-rule add <category> <text>`) or an agent's async
421
497
  derivation from a completed session (P3, always `status: proposed`, provenance-tagged with
@@ -439,14 +515,35 @@ by `kind`, not duplicated.
439
515
  in the loop is the one who types `approve`; the git diff of the rule file is the audit
440
516
  trail. This is the trust boundary for shared state (§11.3).
441
517
 
442
- ### 7.3 The rules section of the continuation notice
443
-
444
- The notice carries (all O(1), §1 invariant 6):
445
-
446
- - **Core rules:** the `status: core` set, rendered verbatim, **size-capped** by the same
447
- refuse-don't-truncate rule — if core rules exceed their budget, the notice names the
448
- overflow with numbers and the user curates (`revoke`, or split a category). Never silent
449
- clipping.
518
+ ### 7.3 The rules block: what a rebuilt head shows
519
+
520
+ One builder renders the rules block (`buildRulesSection`), and **both** places a head is rebuilt
521
+ use it — the continuation notice and the in-place compaction checkpoint (§7.6). The block is O(1)
522
+ in corpus size (§1 invariant 6) and, on an empty or unlinked corpus, renders **zero bytes**
523
+ (`kind: 'none'` → nothing at the call site): load-bearing for the tape corpus, which contains no
524
+ rules block anywhere (§13 P3).
525
+
526
+ Contents (all O(1), §1 invariant 6):
527
+
528
+ - **Active rules, verbatim,** selected by the three classes of §7.4 — pinned, then
529
+ relevance-selected, then index-only. Project tier and session tier render under their own
530
+ headers so the model can tell whose order it is reading.
531
+ - **A withheld line — mandatory.** When the caps drop anything, the block says so with numbers:
532
+ `9 project rules active, 4 shown — /chapters-rule list`. Selection by relevance is legitimate
533
+ (§7.4); *silent* selection is the exact failure this feature exists to remove, because a rule
534
+ the human believes is active and is not, is worse than a rule that was never captured.
535
+ - **The token ceiling stays refuse-don't-truncate:** if the selected set exceeds
536
+ `coreRulesBudgetTokens`, the builder returns `refusal` with per-rule token numbers, the caller
537
+ omits the block rather than clipping (§1.2), and the refusal is reported — never swallowed.
538
+ Omission stays non-blocking: rules are additive, and a broken mirror or an overflowing ruleset
539
+ must not stop a continuation or a compaction.
540
+ - **Precedence line** (§7.7): session rules refine and add; they never override a project rule.
541
+ - **Per-rule warrant** (§7.5, amended 2026-10-21): a session rule renders with how it was
542
+ established — `you said this` (marker), `stated by you, mined from ch.N`, or `inferred from ch.N
543
+ — nobody said it`. Inference is allowed to govern, but never anonymously.
544
+ - **The speak-up line**, present only when a block carries session rules: the agent names by id
545
+ which of them shaped its reply, or says none did. A project-only block keeps its exact pre-#50
546
+ bytes (verify.md R0 pins that), so nothing is spent where nothing is inferred.
450
547
  - **Rules category index:** category names + one-line essence + paths, with the instruction
451
548
  *load a rule category when this session's topics overlap a category* — i.e., the agent
452
549
  reads the full category from the repo when (and only when) relevant.
@@ -458,11 +555,197 @@ The notice carries (all O(1), §1 invariant 6):
458
555
  **message count** — not token count, since tokenization is not comparable across models —
459
556
  and parent/fork links) unchanged.
460
557
 
461
- ### 7.4 Later: topic-matched auto-inclusion
558
+ ### 7.4 Topic-matched selection (raised from the old P3b sketch)
559
+
560
+ The record deferred "topic-matched auto-inclusion" because always-render-everything does not scale
561
+ against an O(1) head. With automatic capture the corpus of *active* rules now grows on its own, so
562
+ selection moves from garnish to requirement, and it is built here on machinery that already existed
563
+ purely for pull-based search.
564
+
565
+ Three render classes, in this order, under `projectRulesDisplayCap` / `sessionRulesDisplayCap`
566
+ (count) and `coreRulesBudgetTokens` (tokens):
567
+
568
+ 1. **Pinned** (`pinned: true`, human-set via `/chapters-rule pin`) — always rendered, first.
569
+ Ground rules are *old* by definition, so any scheme that ranks by recency or topicality evicts
570
+ them first; the pinned class is the floor that keeps that from being a surprise.
571
+ 2. **Relevance-selected** — `rules.selectRules`, the ONE selector for what a head shows:
572
+ pinned first, then topicality against a term bag, then the pre-#50 category→id order. The term
573
+ bag is the archived span's stored turn signatures for a rebuilt head and the task query for
574
+ the `chapters_rules` door — both paths share the selector, so the head and the door can never
575
+ disagree about what applies. `search.rankEntries` stays the *corpus* scorer (§8): ranking an
576
+ unbounded archive against a query and selecting a bounded set for one head are different jobs,
577
+ and this round kept one implementation of each rather than bending a rebuilt head to read index
578
+ shards. What is forbidden is a **third** rule-picker living in a door or a checkpoint — a
579
+ source-shape row fails if either stops calling the shared builder.
580
+ 3. **Index-only** — everything else: category + one-line essence + path, loadable. The corpus
581
+ grows without bound; the head does not.
582
+
583
+ `rulesRelevance: false` falls back to the pre-existing deterministic order (category, then id),
584
+ which is what the tapes were distilled against.
585
+
586
+ **Determinism is a requirement, not a preference.** Selection must be a pure function of (rule
587
+ files, stored topic signatures, caps, query) — no wall-clock timestamps, no map-iteration or
588
+ filesystem-readdir order — or two machines disagree about what the agent was told *and* every
589
+ replay prefix becomes unstable. Ranked-by-recency was rejected partly for this reason: recency
590
+ reads the clock.
591
+
592
+ ### 7.5 Session rules: capture and derived activation
593
+
594
+ Two capture paths, deliberately different speeds (this is §6.2's single-slot constraint applied to
595
+ rules, not a new policy):
596
+
597
+ - **Marker — deterministic, instant, zero model tokens (§1 invariant 1).** A line in a
598
+ *human-authored* message beginning `ADD RULE:` (§12 `rulesMarkerPrefix`) becomes an active
599
+ session rule at the existing `turn/end` listener — the same one the topic signatures collect on
600
+ (r35j). It is not a new archive door (§12 of `docs/architecture.md`: five moments, extended in
601
+ place, never copied). The rule body is the human's own words, verbatim.
602
+ - **Enrichment — idle-batched, `rulesEnrichment` default ON.** The §6.1 pass carries a
603
+ `rules: [{ text, category, quote }]` field through that pass's existing ladder: validation
604
+ before commit, one retry, deterministic fallback, provenance chain, and the body-hash guard
605
+ (mining writes its own rule files; it never touches chapter prose). `rules` is **optional in the
606
+ output contract**, so every pre-existing stub, fake and tape still validates, and a malformed
607
+ `rules` value cannot cost a chapter its annotation.
608
+ **What decides whether a mined rule governs is POLICY, not evidence** (Amendment 2026-10-21).
609
+ `rulesAutoActivate` defaults ON: a rule the model inferred from context applies to the session
610
+ tree the moment it is written. With it off, every mined rule is written `reviewOnly: true` and
611
+ stays inert until this machine's `approve` fact says core — activation still rides a curation
612
+ fact, so the write-once file is never rewritten (§1.3).
613
+ **The quote check survives, demoted to a label.** Matching the chapter's human bytes verbatim
614
+ classifies a rule `stated`; not matching classifies it `inferred`; the label is rendered *beside
615
+ the rule text, with the chapter it came from*, and it drives nothing. Assistant prose is not
616
+ admissible even as a label — a "quote" found only in the model's own restatement is `inferred`.
617
+ Why it stopped being a gate is the amendment's subject: requiring a human to have already said it
618
+ makes the tier redundant with the deterministic marker, and the premise of the tier is that the
619
+ human may not know the rule yet.
620
+ Measured before shipping, on three real chapters at the session model (§13 P3c, verify.md R5): 9
621
+ rules captured, 6 stated / 3 inferred, every inference confined to the chapter that had no human
622
+ text at all, and the model's self-declared `basis` less accurate than the check. It also exposed a
623
+ cost fact worth a decision: the hosted route counts reasoning tokens, so mining inside the
624
+ annotation call shares a budget sized for annotation (`ENRICH_MAX_TOKENS`) — `rulesModel` is the
625
+ clean bound, and an unexamined default is not.
626
+
627
+ Three properties worth stating: mining runs **before** the hash-guarded annotation write, so a
628
+ chapter whose enrichment is refused still contributes its standing orders; mining may use a
629
+ **dedicated model** (`rulesModel` / `/chapters-rule model`), which costs one extra bounded call
630
+ per chapter and, when set, replaces rather than supplements the annotation pass's answer; and the
631
+ agent must **name the session rules it applied** in its reply (§7.3) — which is where a wrong
632
+ inference becomes visible to the only party who can revoke it. That is the §1.4 division — model proposes, plugin commits — with the activation gate
633
+ moved to machine-local, where a wrong guess costs one session and not a team's corpus.
634
+
635
+ **Activation is derived, never a curation fact.** A session rule is active iff `scope: session` ∧
636
+ its `session` is on the current session's ancestor path ∧ it is not revoked. No
637
+ `rule-status` entry is written for it: `edits/<harness>/curation.jsonl` travels in the pool, and
638
+ machine-local activation volume does not belong in a file reserved for human status decisions —
639
+ nor should it, since another machine reads only its own harness directory and would see nothing.
640
+ `revoke` on a session rule writes the one exception: a local fact, because the store is write-once
641
+ (§1.3) and a delete would be a rewrite.
642
+
643
+ **Transport exclusion happens at one point.** `planStoreToRepo` is the single store→repo path
644
+ mapper (`rules/<harness>/*.md → rules/<projectKey>/<harness>/*.md`), so it is the single place that
645
+ skips `scope: session`. No provider verb, no sync-loop edit, no second filter — proven over the
646
+ git integration *and* the TreeDX stub, because §5's exit criterion is transport-grade or it is
647
+ nothing. Session bytes therefore never become another machine's `proposed` noise, which is what
648
+ keeps §7.2's "stop re-reviewing a growing list" goal intact for teammates.
649
+
650
+ **Loss is expected and re-derivable.** The store root is gitignored inside a re-clonable directory
651
+ (`docs/architecture.md § Storage`), so deleting a checkout destroys its session rules with
652
+ everything else in it. The marker half re-derives deterministically by replaying the parser over
653
+ the durable log (invariant 1 is what makes that possible); the enrichment half re-derives by
654
+ re-running the pass. The correct description of that event is "the ruleset was rebuilt", never
655
+ "sessions were corrupted".
656
+
657
+ ### 7.6 Carriage: where rules reach the model
658
+
659
+ Rules are delivered at the moments the head is rebuilt, plus on demand — **not** per request:
660
+
661
+ | Door | What renders | Mechanism |
662
+ |---|---|---|
663
+ | Continuation notice | project + session | `rulesSection` at the four door call sites (tools) — already shipped for project |
664
+ | Fork / branch notice | project only | the same `intent` axis that already decides plan bytes (§9–13 of `docs/plans-carriage.md`): `branch` ⇒ zero session bytes |
665
+ | In-place checkpoint | project + session | `rulesCarry`, the third lead section in `planSummarize`, beside `planCarry` and `plot` — both the composed and legacy paths |
666
+ | On demand | assembled set | `chapters_rules` tool (§8): verbatim text within budget + the withheld line |
667
+
668
+ **Why not "bundled with every request".** Two measured reasons, not stylistic. (a) Invariant 3:
669
+ the system prompt is process-global and `systemPrompt` is excluded from our injection list on
670
+ purpose — a per-session block cannot ride it, and the append-as-section precedent (`dsh-session-fork`)
671
+ was reviewed and rejected here. (b) Cache: on llama.cpp, the target hardware, a head-position
672
+ replacement reuses **nothing** (E3, r23/r28: `cacheReuse 0`), so a ruleset that churns mid-session
673
+ is a cache-wipe machine. Between rebuilds the human's original bytes are still live in the surface
674
+ anyway — they only genuinely die when the surface is replaced, and that is where they are now
675
+ re-stated. Plans arrived at the same conclusion for the same reason.
676
+
677
+ Empty set ⇒ the checkpoint carries zero rule bytes, which is what protects every already-committed
678
+ plot-carrying checkpoint (`src/engine-core.ts` records exactly that hazard).
679
+
680
+ ### 7.7 Precedence and conflicts
681
+
682
+ Two live tiers means two sources of "must", and a small model degrades badly on a contradiction,
683
+ so precedence is stated in the block rather than left to the reader:
684
+
685
+ - **Project rules are the team's order. Session rules refine and add; they never override.**
686
+ - A session rule that conflicts with an active project rule is *rendered and flagged*, not
687
+ resolved silently — the human's options are to promote a fix to the project rule or revoke the
688
+ session rule.
689
+ - **`/chapters-rule conflicts`** spends one bounded `ctx.llm` stream call over the assembled set,
690
+ reusing the conversation's own system prompt as the cache-friendly prefix (`docs/contract.md`),
691
+ and returns a **report** of contradictory pairs with rule ids. Like `elicitedPlot`, this is an
692
+ explicit, config-gated exception to zero-inference. It is never a mutation: rules are write-once
693
+ (§1.3) and the only sanctioned change is a curation fact. Enrichment may flag the same pairs in
694
+ its own pass; neither path edits a rule.
695
+ - Command output never enters the context window (§7.2 verified), so reviewing conflicts costs a
696
+ human no session tokens.
697
+
698
+ ### 7.8 Identity must fail loudly
699
+
700
+ The design goal "project rules carry across my checkouts of this repo" holds because identity is
701
+ remote-derived (§2.3). Where it does *not* hold, silence is the bug — ungoverned must never look
702
+ like governed:
703
+
704
+ | Divergence | Consequence | What the plugin must say |
705
+ |---|---|---|
706
+ | No readable remote ⇒ `path-derived` key | that checkout has a **private** rule namespace | `0 project rules — this checkout resolves project <key> from path <cwd>; link it with /chapters-link` |
707
+ | No `origin`, another remote listed first | key derives from the fork ⇒ a forked pool | report the remote string actually used |
708
+ | Linked via TreeDX rather than git | key is the **pool location** (`treedx/<host>/<repo>`), not the workspace remote | report `projectKeySource` so git/treedx keyed checkouts are visibly different projects |
709
+ | Fresh checkout, unlinked or not yet synced | no project ⇒ no rules at all | same explicit zero line, plus `/chapters-status` sync state |
710
+
711
+ `projectKeySource` (`override | remote | path-derived`) rides **`/chapters-status`** as an
712
+ `Identity:` line — naming the source AND where it came from (remote string / override / literal
713
+ path) — and the divergence branch is its own warning: `⚠ THIS CHECKOUT RESOLVES A DIFFERENT
714
+ PROJECT KEY … rules and chapters approved elsewhere will NOT appear here`. `/chapters-link`'s view
715
+ reports the same fact. The continuation head carries the zero-rules statement **only when the
716
+ session tier is governing that head**: bolting an "unlinked" line onto every unlinked workspace's
717
+ notice would spend real bytes on a warning most sessions do not need, while a head that already
718
+ carries local rules is exactly where a silent zero gets misread as "the team approved nothing".
719
+ `/chapters-rule active` prints the tier counts on demand, and `chapters_rules` names a missing
720
+ project in its note. A session-scoped archive depends on none of this — which is why an unlinked
721
+ checkout still gets its own rules.
722
+
723
+ ### 7.9 Command surface, and what the agent is told
724
+
725
+ **Principle: the machinery is invisible; the query is encouraged.** Capture, activation, budgeting
726
+ and carriage happen without the agent doing anything — no tool it must remember to call, no
727
+ command it must issue, no bookkeeping it can get wrong. What it *is* told, in every rebuilt head,
728
+ is to **look the rules up before starting a task**: `chapters_rules` with the task description,
729
+ named in the notice's `## Finding things` block (§8). Everything else about rules reaches it as
730
+ prose it simply reads.
731
+
732
+ | Surface | Tier | Notes |
733
+ |---|---|---|
734
+ | `/chapters-rule add <category> <text>` | project | unchanged — the only human door that writes shared rule bytes |
735
+ | `/chapters-rule list [--scope project\|session] [--proposed] [--category <c>]` | both | table: id, scope, status, category, one-line text, provenance, source seq |
736
+ | `/chapters-rule active` | both | **exactly what is or will be rendered**, with the withheld count and why — the command that makes §7.3's honesty checkable |
737
+ | `/chapters-rule approve / revoke <id>` | project (approve) · both (revoke) | per-machine curation facts (§7.2, §15 2026-09-20) |
738
+ | `/chapters-rule pin / unpin <id>` | both | the §7.4 always-shown class; **human-set only** — an auto-pinned rule is an ungoverned always-shown rule |
739
+ | `/chapters-rule conflicts [--model <pin>]` | both | report-only (§7.7) |
740
+ | `chapters_rule_propose` (tool) | project | agent-facing, stays `proposed` and machine-inert until a human approves — unchanged contract, do not widen |
741
+ | `chapters_rules` (tool) | both | the assembly door: verbatim selected set within budget + the withheld line |
742
+
743
+ Encouragement lives in the **notice**, deliberately: notice prose costs nothing against the tape
744
+ corpus (measured 2026-10-11 — no committed tape prefix contains a seeded notice), while a persona
745
+ or tool-description edit forces a re-record of every affected project (§13). A new tool or tool
746
+ parameter does change the header, so this feature's schema edits are sequenced into **one**
747
+ re-record rather than scattered across several.
462
748
 
463
- When a continuation is created, the plugin can compute topic overlap between the archived
464
- span's topics and rule categories and inline matching *category-level* rules within the same
465
- budget. P3b; gated by the same cap-and-refuse rule.
466
749
 
467
750
  ## 8. Search: `chapters_search`
468
751
 
@@ -491,6 +774,24 @@ chapters_search { query: string, maxTokens?: number, topic?: string, crossProjec
491
774
  opens (chapters in `.dsh-chapters/`, shards in `.dsh-knowledge/index/`). The search tool
492
775
  adds a scorer and a formatter; it adds no new way to read.
493
776
 
777
+ **`chapters_rules` — the head's selector, assembled (§7.4, §7.9).** One door whose output is *the
778
+ text of the rules that apply to this task*, verbatim, within budget, plus the per-tier counts it
779
+ withheld. It calls `renderRulesBlock` — the renderer the continuation notice and the compaction
780
+ checkpoint also use — and nothing else; a rule-picker living in a door or a checkpoint is forbidden
781
+ by the same argument that made `exit_plan_mode` and `chapters_continue` share one builder. §7.4
782
+ keeps `rankEntries` (corpus ranking) and `selectRules` (head selection) as one implementation of
783
+ each genuinely different job.
784
+
785
+ - **It reads both corpora, which is where the trap is.** Project rules come from the pool mirror
786
+ under `rules/<projectKey>/<harness>/`; session rules live in the **workspace store** under
787
+ `rules/<harness>/` and are never staged (§7.5), so they carry no `projectKey` path prefix. The
788
+ `projectKey` prefix filter above must therefore not be the only scope for the session tier, or
789
+ session rules become invisible to the very door whose job is to find them — a silent zero, the
790
+ failure class §7.8 exists to eliminate. Scoping for session rules is by the ancestor-path
791
+ activation predicate instead, never by path prefix.
792
+ - **It is the encouraged step, not a required one.** Capture and carriage never depend on it
793
+ (§7.9); the notice names it in `## Finding things` as the thing to run before starting a task.
794
+
494
795
  ## 9. Redaction: minimal, deterministic, marked
495
796
 
496
797
  User position, adopted: **credentials only, deterministic patterns only, everything else
@@ -527,7 +828,10 @@ ever** (§3.3), zero conflicts by construction:
527
828
 
528
829
  Fact types: `topic-alias` (merge/rename), `topic-pin`, `group` (sibling chapters that
529
830
  belong together — the semantic grouping that composition cannot do alone), `weight`,
530
- `note`. Authors are `plugin` or `user` (user entries via commands, P3). This is the
831
+ `note`, and the two rule facts (§7.2, §7.4): `rule-status` — per-machine approve/revoke,
832
+ the 2026-09-20 amendment's whole mechanism — and `rule-pin`, the always-rendered class,
833
+ which rides a fact for the same reason status does: a rule file is write-once and the
834
+ append-only rule has no exceptions. Authors are `plugin` or `user` (user entries via commands, P3). This is the
531
835
  "improve over time" layer: curation accumulates as facts, and the derived index reflects
532
836
  them — performance improves without anyone hand-editing, and every fact is inspectable and
533
837
  revertable in git.
@@ -572,6 +876,16 @@ needs a human gate:
572
876
  git-visible curation entries (§6.3).
573
877
  - **Rules:** high — they are prompts by another name, shared across machines. **Human
574
878
  `approve` is mandatory** (§7.2). A proposed rule that is never approved affects nothing.
879
+ - **Session rules** (§15 amendment 2026-10-20) are the deliberate exception, and the exception is
880
+ *narrowed by scope, not by gate*: machine-local, never staged to the pool (§7.5), permanently
881
+ active for one session tree, and visible/reversible with one command (`active`, `revoke`).
882
+ Blast radius is that tree. What keeps it honest is structural, not evidentiary (§7.5, amended
883
+ 2026-10-21): machine-local scope and never staged, a per-chapter cap of 3 plus per-tier display
884
+ caps, the warrant rendered beside every rule with its chapter, the agent naming which rules it
885
+ applied, one-command `active`/`revoke` review, `rulesAutoActivate` for anyone who wants
886
+ review-before-trust, and a session rule never overriding a project rule (§7.7). What is *not*
887
+ permitted is a session rule migrating into the shared tier on its own — promotion is a human act,
888
+ exactly as approval is.
575
889
 
576
890
  Git history + provenance makes every tier auditable and every mistake revertible.
577
891
 
@@ -591,9 +905,19 @@ No hardcoded tunables (existing hard rule). New config, all with defaults docume
591
905
  | `toolResultArtifactTokens` | arrival-time artifacting floor (architecture.md amendment 2026-09-19): tool results at/above this many estimated tokens are stored as artifacts and reference-stubbed BEFORE the next request composes — the blob never enters any prompt | 8000 |
592
906
  | `elicitedPlot` | when a compaction region holds no model-authored `PLOT:` note, spend ONE bounded stream call (≤8K chars in, ≤220 tokens out) to mint the working plot carried into the checkpoint; the approved, config-killed exception to zero-inference | true |
593
907
  | `enrichment.enabled` / `.model` | per-harness enrichment toggle + model pin (built flat: `enrichmentEnabled`, `enrichmentModel`) | enabled, conversation model |
908
+ | `enrichmentMaxTokens` | One auxiliary completion budget (annotation+mining, conflicts). Must cover **reasoning** tokens, not just the visible answer — measured mining usage 1.1K–3.2K per chapter. The route measured here ignores `max_tokens`, so this is headroom for strict providers, not a live limit. No call site keeps a literal | 4000 |
594
909
  | `enrichment.trigger` / `.idleMs` / `.batchCap` | P2 queue policy seam (built: `enrichmentTrigger/enrichmentIdleMs/enrichmentBatchCap`); realm owns auto, host plane is manual-only; `/chapters-enrich model` overrides durably | both / 60000 / 5 |
595
910
  | `vocabulary.*` | built flat: `vocabApply` (false = **shadow**: candidates reported in the sync status line, nothing written), `vocabCoMin` (3), `vocabOverlapMin` (0.5); applied merges land as §10.1 `topic-alias` facts under `edits/<harness>/`; `topics/vocabulary.json` derived | shadow / 3 / 0.5 |
596
911
  | `coreRulesBudgetTokens` | P3 §7.3 cap on the notice's core-rules block; overflow refuses with per-rule numbers | 1200 |
912
+ | `sessionRulesEnabled` | §7.5: the deterministic `ADD RULE:` capture path (zero model tokens) | true |
913
+ | `rulesMarkerPrefix` | §7.5 capture marker; matched at line start of **human-authored** messages only | `ADD RULE:` |
914
+ | `rulesEnrichment` | §7.5 model-derived capture: rides the §6.2 idle batch, never per-turn | true |
915
+ | `rulesAutoActivate` | §7.5 (amended 2026-10-21): may a mined rule govern on arrival? It **may**, by default — inferring what nobody said is the tier's purpose. Off ⇒ mined rules are `reviewOnly` and inert until `approve` | true |
916
+ | `rulesModel` | §7.5: `'provider/model'` for a dedicated mining call — one extra bounded call per chapter, replacing the annotation pass's answer. `''` ⇒ mine inside the enrichment call, at no extra inference cost. Runtime override `/chapters-rule model` (settings key `rules.model`) | `''` |
917
+ | `rulesMinedPerChapter` | §7.5 cap on mined rules per chapter — the cap counts rules **written**, not candidates examined, so a duplicate cannot eat a chapter's budget | 3 |
918
+ | `projectRulesDisplayCap` / `sessionRulesDisplayCap` | §7.4 per-tier count caps on rules **rendered** into a rebuilt head; anything dropped is announced by count, never silently | 12 / 12 |
919
+ | `rulesRelevance` | §7.4 relevance selection on; off ⇒ deterministic category→id order (the order tapes were distilled against) | true |
920
+ | — | §7.7 `/chapters-rule conflicts` has **no key of its own**: one bounded stream call on the conversation route the session already has (so it rides that route's cached prefix), report-only. No route yet ⇒ the verb refuses honestly. A gate of its own was rejected: a human types the verb, so `elicitedPlot`'s reason for a switch does not apply | — |
597
921
  | `rulesCoreBonus` | P3 §8 query-time rank bonus for rules effective-core on this machine (never baked into shared shards) | 0.15 |
598
922
  | `redaction.patterns` | additive to the built-in list (§9) | built-ins |
599
923
  | `search.defaultMaxTokens` | default pack size for `chapters_search` | small, config |
@@ -632,7 +956,8 @@ e2e project replays the whole ladder through the model tape. CLOSED 2026-09-20:
632
956
  Rules-as-chapters (`kind: rule`); the in-session command family `/chapters-rule
633
957
  list|add|approve|revoke` (§7.2 — command output verified context-free); core-rules section +
634
958
  category index in the notice (§7.3); agent-side rule proposals (async, always `proposed`);
635
- `/chapters-topic` escape hatch; P3b topic-matched auto-inclusion (§7.4).
959
+ `/chapters-topic` escape hatch; topic-matched selection (§7.4 — deferred as P3b in this draft,
960
+ built later under P3c, not by design but by sequencing).
636
961
  *Exit criterion: a rule proposed by a machine affects no other machine until a human types
637
962
  `approve`; the core set overflows by refusing with numbers, never clipping.*
638
963
 
@@ -645,7 +970,38 @@ attempted even at copied===0 so fact-only passes travel — the change that made
645
970
  shareable) + scorer (category 1.5, rule bodies, per-machine core bonus at query time).
646
971
  Exit-criterion tests: unit ('machine B sees nothing core'), transport-grade (three machines
647
972
  over a real pool), e2e (`rules` project: UI add → approve → fork → the child's own log
648
- carries the rule verbatim). §7.4 auto-inclusion remains P3b, unbuilt by design.
973
+ carries the rule verbatim). §7.4 selection was left unbuilt in that round by design — the corpus
974
+ of active rules did not yet grow on its own.
975
+
976
+ **P3c — Two-tier rules (issue #50): BUILT, both capture speeds (2026-10-20).** §7.1's `scope`/`session`/
977
+ `derivedFrom`/`pinned` frontmatter (absent `scope` ⇒ `project`, so every pre-existing rule record
978
+ keeps working byte-identically); `ADD RULE:` capture at the existing `turn/end` listener;
979
+ `rulesEnrichment` capture (§7.5's second speed; governed on arrival by default per
980
+ Amendment 2026-10-21, the quote demoted to a rendered warrant); session-scope exclusion in
981
+ `planStoreToRepo`; the three render classes and per-tier caps in `buildRulesSection`; the third
982
+ checkpoint lead section; `intent` filtering at the four door sites; `chapters_rules`; the
983
+ `active`/`pin`/`conflicts` verbs; and §7.8's loud-identity lines.
984
+ *Exit criteria (each must be a red row before it is a fact):* an in-place compaction renders active
985
+ rules verbatim into the checkpoint; `chapters_continue` carries project **+ session** while
986
+ `chapters_fork` carries project only; `ADD RULE:` yields an active rule at **zero** model tokens;
987
+ no session rule file appears in the pool over **either** provider; an unlinked or `path-derived`
988
+ checkout says it rendered zero rules; the empty set still renders byte-identical ABSENT; selection
989
+ is byte-stable across two machines with the same inputs.
990
+ Build status, honestly split (each ✅ is a red-then-green row, not an intention):
991
+ ✅ `scope`/`session`/`derivedFrom`/`pinned` frontmatter with absent-means-`project` byte-compat;
992
+ ✅ tier-aware `buildRulesSection` with pinned→relevance→category/id selection, per-tier caps and the
993
+ announced withheld count; ✅ `ADD RULE:` capture at the existing `turn/end` listener (zero model
994
+ tokens); ✅ the one `planStoreToRepo` exclusion over **both** providers; ✅ `rulesCarry` as the third
995
+ checkpoint lead section on both paths; ✅ `intent` filtering at the doors through one
996
+ `renderRulesBlock`, with a source-shape row forbidding a door-local picker; ✅ `chapters_rules`;
997
+ ✅ `active`/`pin`/`unpin`/`--scope`/`conflicts` and revoke-on-session; ✅ §7.8 identity lines at
998
+ `/chapters-status` and `/chapters-link`. ❌ the §6.1 `rules: []` enrichment field and its quote gate.
999
+ ❌ the browser plane: `npm run test:e2e:replay` **refuses by name** on the pinned host (it predates
1000
+ preset declaration rows — `tests/e2e/boot.ts:130`), so the `rules` journey and the R-rows over a
1001
+ live model are unverified, and the new tool schema is a standing re-record owed the moment
1002
+ `dev/dsh-host-lock` moves to 0.2.x. Sequencing then stays as planned: **one** whole-project
1003
+ re-record, never a series of per-spec ones (the 2026-09-23 lesson: a tape records the context it was
1004
+ distilled in).
649
1005
 
650
1006
  ## 14. Open risks (honest register)
651
1007
 
@@ -659,6 +1015,10 @@ carries the rule verbatim). §7.4 auto-inclusion remains P3b, unbuilt by design.
659
1015
  | `mergeThreshold` mis-tune at archive time (composition is archive-time-only, §4.4) | signatures are stored with provenance; tune for future archives, inspect past ones |
660
1016
  | TreeDX service down or token expired (Amendment 2026-09-21, transport) | same §5.3 degradation (offline mirror, deferred push, auth re-link surfaced with numbers); no corpus loss |
661
1017
  | TreeDX corpus pull is per-file (`paths/list` + `files/read`): N+1 requests at large pools | acceptable at per-project corpus sizes (the record's own scaling premise); snapshot/`context` APIs are the Phase-F optimization |
1018
+ | P3c: a model-captured rule encodes a **mis-generalization** permanently for the session tree ("never run two GPU suites at once" → "never run tests concurrently") | narrowed by the verbatim-quote activation gate (§7.5), by scope (local, never the shared tier), and by one-command visibility (`active`, `revoke`); the residual risk is a wrong rule you did not look at — which is why the count is capped and the withheld line is mandatory |
1019
+ | P3c: relevance selection **ranks out the load-bearing rule**, reintroducing "lost in the noise" inside the fix for it | the pinned class is never subject to selection (§7.4); the withheld count renders even when nothing is selected |
1020
+ | P3c: a new tool/param changes every replay prefix | one whole-project re-record, sequenced deliberately; notice prose stays free (measured 2026-10-11) |
1021
+ | P3c: per-checkout identity divergence silently forks a project's rule namespace (§7.8) | stated, not assumed: `projectKeySource` reported in the zero-rules line, `active`, and `/chapters-status` |
662
1022
 
663
1023
  ## 15. Amendments
664
1024
 
@@ -792,6 +1152,162 @@ kernel's own exported predicates (`KNOWN_SESSION_EVENT_TYPES`, `isSurfaceEligibl
792
1152
  `isAppendSurfaceEvent`) rather than reimplemented, so a kernel change surfaces as a red row instead of a
793
1153
  child that silently stops being gated.
794
1154
 
1155
+ ### Amendment 2026-10-20 — rules become two tiers: project (shared, approved) and session (local, derived)
1156
+
1157
+ Human directive, in the round that planned it: *"I end up giving the AI agent a bunch of rules
1158
+ embedded in my requests… these rules just get lost in the noise."* Auditing where they actually get
1159
+ lost found one concrete hole, not a vague one: the in-place checkpoint composes
1160
+ `leadSections = [planSection, plotSection]` (`src/engine-core.ts`). **A plan survives a compaction
1161
+ and a plot survives a compaction; a rule does not.** That, plus a rule set that only ever reaches a
1162
+ child through the continuation notice with no relevance selection, is the whole motivation.
1163
+
1164
+ Six decisions, each narrowing a temptation the discussion produced:
1165
+
1166
+ 1. **Two tiers, one store** (§7.1). Session rules are `kind: rule` files in the same tree with
1167
+ `scope: session` — same numbering, same write-once immutability, same `list`, same index. Absent
1168
+ `scope` means `project`, so every rule record on disk before this amendment keeps working with
1169
+ unchanged bytes. There is no second rules subsystem to drift.
1170
+ 2. **The global tier was not built, because it already existed.** The request was "carry across my
1171
+ checkouts of this repository", and `projectKey` is UUIDv5 over the *canonicalized remote* (§2.3)
1172
+ with `harnessId` defaulting to the hostname — so several checkouts of one repo already resolve to
1173
+ one rule namespace on one machine. The work became making the four ways that identity silently
1174
+ diverges say so (§7.8) instead of inventing a third namespace with a much larger blast radius.
1175
+ 3. **Session activation is derived, never a curation fact** (§7.5). Active iff `scope: session` ∧
1176
+ its session is on the current ancestor path ∧ not revoked. `edits/<harness>/curation.jsonl` is
1177
+ reserved for human status decisions *and it travels in the pool* — machine-local activation volume
1178
+ does not belong there. The 2026-09-20 amendment (approval is a per-machine fact, never a file
1179
+ rewrite) is **not** relaxed: it still governs every byte of the shared tier.
1180
+ 4. **Model-captured rules may auto-activate, and only on a verbatim quote** (§7.5, behind
1181
+ `rulesEnrichment`, default off — *decided and specified; the P3c build shipped the marker path
1182
+ first, so this half is still ahead of the code*). The rejected framing was "the model writes its own governing
1183
+ rules with no human gate"; the accepted one is that the machine-local, one-command-reversible,
1184
+ never-staged scope makes the blast radius one session tree — so the gate moves from *who clicks*
1185
+ to *what the rule is derived from*. An inferred rule with no human sentence behind it waits in
1186
+ `list --proposed`. And capture stays off the critical path entirely: the `ADD RULE:` marker is
1187
+ deterministic and costs zero model tokens (invariant 1), while the model pass rides §6.2's idle
1188
+ batch, because the local server is single-slot and per-turn work queues against the user's own
1189
+ turns.
1190
+ 5. **Rules are carried at the moments the head is rebuilt, never injected per request** (§7.6).
1191
+ Two measured reasons, not taste: invariant 3 (the system prompt is process-global and
1192
+ `systemPrompt` is excluded from our injections on purpose), and the llama.cpp number from
1193
+ E3/r23-r28 — a head-position replacement reuses **nothing** (`cacheReuse 0`), so a ruleset that
1194
+ churns mid-session is a cache-wipe machine on exactly the target hardware. Between rebuilds the
1195
+ human's own bytes are still in the surface; they die only when the surface is replaced, which is
1196
+ where they are now re-stated. `chapters_continue` carries both tiers, `chapters_fork` carries
1197
+ project only — the *same* `intent` axis that already decides plan bytes (`docs/plans-carriage.md`
1198
+ §§ 9–13), not a new distinction.
1199
+ 6. **§7.4 selection is raised from "P3b later" to required, with a floor.** Automatic capture means
1200
+ the active set now grows without a human noticing, so always-render-everything stops being
1201
+ affordable: pinned (human-set, always) → relevance-selected (the existing `search.rankEntries`,
1202
+ never a second ranker) → index-only. Per-tier count caps are **announced**, because selection
1203
+ that can silently drop the load-bearing rule would rebuild the original bug inside the fix —
1204
+ and a cap that *truncates* would violate §1.2, so the token ceiling still refuses with numbers
1205
+ and the count ceiling selects with a visible count. Selection must be a pure function of stored
1206
+ inputs (no clock, no readdir order) or two machines disagree about what the agent was told and
1207
+ the tape corpus cannot be stable.
1208
+
1209
+ Also recorded as accepted consequences: session-scope exclusion happens at the one store→repo mapper
1210
+ (`planStoreToRepo`) and is proven over **both** providers; a session rule never overrides a project
1211
+ rule, and conflicts are reported (`/chapters-rule conflicts`, one bounded stream call in the
1212
+ `elicitedPlot` family) rather than resolved (§7.7); losing a checkout loses its session rules, which
1213
+ is *re-derivation*, not corruption (§7.5, and the storage warning in `AGENTS.md`); and the agent-
1214
+ facing surface stays minimal by decision — capture and carriage are invisible to the model, and the
1215
+ one thing it is told to do is **look the rules up before starting a task** (§7.9), because a tool an
1216
+ instruction never names does not exist to a small model.
1217
+
1218
+ Specification: §7.1 and §7.3–§7.9. Build status and exit criteria: §13 P3c (specified, not yet
1219
+ built at the time of this amendment — this entry is the decision record, not a claim of behavior).
1220
+
1221
+ ### Amendment 2026-10-21 (b) — rules reach agents, and agents reach rules (#52)
1222
+
1223
+ Two user decisions in the same review round, both about the gap between "the rule exists" and
1224
+ "the agent in front of the work is told about it".
1225
+
1226
+ **1. The preset persona is the instruction channel.** Nothing told a brand-new session, or a delegated
1227
+ subagent, to use `chapters_rules` / `chapters_search`: a plugin must not contribute a system-prompt
1228
+ section (§1.3 / invariant 3), and the continuation notice only exists for continuation children. The
1229
+ sanctioned per-composition channel is the preset's OWN persona row — `@deepseek-ai/dsh-persona`,
1230
+ prefix/suffix — the mechanism already carrying the `PLOT:` discipline. The shipped suffix now adds a
1231
+ short "Chapters memory and rules" section: run `chapters_rules` with the task description before a
1232
+ task and NAME the rules you followed (or say none); for earlier work, decisions, or bug history run
1233
+ `chapters_search` FIRST instead of re-reading files or asking the human; `chapters_artifact` holds the
1234
+ oversized bytes; and a delegated child is covered by the same rules and the same archive. It is
1235
+ per-preset, so a session on `standard` sees none of it. Two couplings are pinned by behaviour, not
1236
+ comment: the suffix must remain matched by the plot template-echo guard (else every session inherits
1237
+ its own instruction as a "plot"), and no new prompt variable may appear (`{{model}}` only — an
1238
+ unregistered variable throws at assembly and kills every turn of every chapters session).
1239
+
1240
+ **2. Rule ancestry crosses the delegation edge.** The registry is the sole authority on continuation
1241
+ lineage and deliberately ignores the kernel header's `parentSession` — correct for numbering and the
1242
+ TOC, and it left a whole session shape with ZERO rules: a delegated subagent was an unrelated root by
1243
+ that view. So `session-rules.ruleAncestorSet` walks BOTH edges (registry `parentSession` ∪ header
1244
+ `parentSession`) transitively, READ-ONLY, writing nothing, and still only UPWARD — a child's
1245
+ inferences never govern the session that delegated them. Bounded (64 expansions) and loud when the
1246
+ cap bites (`lineage walk hit its cap…` inside the block, `LINEAGE TRUNCATED:` in `/chapters-rule
1247
+ active`); an unreadable header contributes no edge rather than breaking a turn. One walk, one builder:
1248
+ every door, the checkpoint and the surface call `ruleAncestorSet`, and an anti-drift row fails if a
1249
+ plane computes ancestry by hand again — hand-built ancestry is precisely how the gap appeared.
1250
+
1251
+ Consequence, recorded before anyone is surprised by it: this changes a PRODUCT PROMPT (the persona
1252
+ suffix) and adds a tool, so the browser-plane tapes owe one whole-project re-distill when the host
1253
+ lock moves to 0.2.x — one re-record pays both debts. Until that runs, no claim about browser-plane
1254
+ rule carriage is verified.
1255
+
1256
+ ### Amendment 2026-10-21 — a mined rule is governed by POLICY, not by proof that a human said it
1257
+
1258
+ Human directive, one day after the previous entry and against the feature as built: *"I don't like
1259
+ your approach of making mined rules governing if it appears verbatim in my own words… we need to
1260
+ assume that the human may not know, which is why they may be using AI in the first place."* The
1261
+ verbatim-quote gate built under §7.5 is **removed as a condition on activation**.
1262
+
1263
+ What was wrong was a category error, and it is worth stating plainly because it is the kind of
1264
+ error this project keeps making: the gate confused **where a rule came from** with **whether it is
1265
+ allowed to govern**. Requiring a human sentence behind every mined rule meant the model could only
1266
+ ever re-state what had already been said — which is exactly what the deterministic `ADD RULE:`
1267
+ marker does better, because there you know precisely what you got. A tier that can only confirm the
1268
+ known is not inference; it is a paraphrase of the marker with extra latency. And the honest failure
1269
+ mode it was built to prevent — the model inventing a rule the human never sanctioned — is the same
1270
+ failure mode it was built to *cause*: the useful case is the standing order nobody has articulated
1271
+ yet, and the value of asking a model to look is precisely that it may see one.
1272
+
1273
+ **What replaces it.** Four things, all structural — and the reason all four are needed is that
1274
+ evidence is no longer one of them:
1275
+
1276
+ 1. `rulesAutoActivate` (default **on**) is the single switch. Off ⇒ every mined rule is written
1277
+ `reviewOnly: true` and stays inert until this machine approves it. Review-before-trust is a
1278
+ posture a team can choose; it is no longer the default posture.
1279
+ 2. The quote check survives, **demoted to a label**: `stated` (a human sentence backs it) vs
1280
+ `inferred` (the model judged it, nobody said it). It renders beside the rule text *with the
1281
+ chapter it came from*, in the head, so no inferred rule can govern anonymously. Assistant prose
1282
+ stays inadmissible even as a label — a "quote" found only in the model's restatement is
1283
+ `inferred`, not `stated`.
1284
+ 3. The agent must **name by id the session rules that shaped its reply** (§7.3's speak-up line),
1285
+ or say none did. This is the mechanism the human asked for instead of a gate, and it is better:
1286
+ a wrong inference becomes visible at the moment it acts, to the only party who can revoke it,
1287
+ rather than being prevented in advance by a test the model can satisfy by quoting badly.
1288
+ 4. The caps stay: 3 mined rules per chapter (counting rules **written**), per-tier display caps
1289
+ with the withheld count announced, machine-local scope, never staged to the pool, `revoke` one
1290
+ command away.
1291
+
1292
+ Two things deliberately **not** relaxed: a session rule never overrides a project rule (§7.7), and
1293
+ a session rule never becomes a project rule without a human (`add`) — promotion is still a human
1294
+ act. The tier's warrant for governing itself got broader; its warrant for governing *anyone else*
1295
+ did not change at all.
1296
+
1297
+ **Honest limits, recorded at the time of writing.** No live model has mined a real chapter yet (the
1298
+ dev box's GPU driver was unreachable), so how often a 4B produces a *correct* inference — versus a
1299
+ confident wrong one that now governs by default — is unmeasured. The default is a bet on the
1300
+ visibility mechanisms above, and `docs/verify.md` R5 marks the quality half unverified. Anyone
1301
+ revisiting this should look at the ratio of `stated` to `inferred` on real archives first; if
1302
+ inferences are wrong more often than a human catches them in replies, the fix is
1303
+ `rulesAutoActivate: false`, not a new gate.
1304
+
1305
+ Implementation: `src/rules.ts` (`basis`/`chapter`/`reviewOnly`, `sessionRuleActive`),
1306
+ `src/session-rules.ts` (`captureMinedRules` policy + classification), `src/enrich-wire.ts`
1307
+ (`rulesAutoActivate`, `rulesModel`, dedicated mining call), `src/rules-commands.ts` (`model` verb,
1308
+ `active` posture). Reversal pinned by a test row, not just described: *"THE REVERSAL, PINNED: an
1309
+ inferred rule governs on arrival — no human said it."*
1310
+
795
1311
  ---
796
1312
 
797
1313
  *Cross-references: invariants and the four invariants — `AGENTS.md`; budget math —