create-claude-cabinet 0.51.0 → 0.53.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/README.md +35 -28
  2. package/lib/cli.js +2 -2
  3. package/lib/modules.js +5 -4
  4. package/package.json +1 -1
  5. package/templates/CLAUDE.md +196 -31
  6. package/templates/EXTENSIONS.md +7 -0
  7. package/templates/README.md +3 -3
  8. package/templates/briefing/_briefing-template.md +1 -1
  9. package/templates/briefing/_briefing-work-tracking-template.md +3 -3
  10. package/templates/cabinet/_cabinet-member-template.md +10 -7
  11. package/templates/cabinet/memory-lifecycle-contract.md +16 -0
  12. package/templates/cabinet/pib-db-access.md +1 -1
  13. package/templates/cabinet/pib-db-triggers.md +20 -16
  14. package/templates/cabinet/watchtower-contracts.md +321 -10
  15. package/templates/engagement/pib-db-patches/pib-db-mcp-server.mjs +2 -2
  16. package/templates/memory/patterns/_pattern-template.md +1 -1
  17. package/templates/mux/bin/mux +5 -0
  18. package/templates/mux/config/mux-server.py +7 -5
  19. package/templates/rules/enforcement-pipeline.md +2 -2
  20. package/templates/rules/memory-capture.md +2 -2
  21. package/templates/scripts/__tests__/absorption-no-prune.test.mjs +89 -0
  22. package/templates/scripts/__tests__/assessment-recalibration.test.mjs +0 -2
  23. package/templates/scripts/__tests__/batch-disposition.test.mjs +7 -9
  24. package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
  25. package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
  26. package/templates/scripts/__tests__/draft-surfacing.test.mjs +46 -162
  27. package/templates/scripts/__tests__/extraction-classification.test.mjs +306 -0
  28. package/templates/scripts/__tests__/fixtures/read-pass-2026-07-14.json +325 -0
  29. package/templates/scripts/__tests__/fold-similarity.test.mjs +290 -0
  30. package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
  31. package/templates/scripts/__tests__/hold-verb.test.mjs +149 -0
  32. package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
  33. package/templates/scripts/__tests__/reach1-inbox-relations.test.mjs +274 -0
  34. package/templates/scripts/__tests__/reach2-significance.test.mjs +300 -0
  35. package/templates/scripts/__tests__/read-pass-fixture.test.mjs +71 -0
  36. package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
  37. package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
  38. package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
  39. package/templates/scripts/__tests__/ring3-outage-backfill.test.mjs +156 -0
  40. package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
  41. package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
  42. package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
  43. package/templates/scripts/memory-consolidation-scan.mjs +149 -0
  44. package/templates/scripts/pib-db-mcp-server.mjs +2 -2
  45. package/templates/scripts/skill-validator.sh +7 -4
  46. package/templates/scripts/watchtower-build-context.mjs +33 -1
  47. package/templates/scripts/watchtower-lib.mjs +225 -6
  48. package/templates/scripts/watchtower-queue.mjs +245 -59
  49. package/templates/scripts/watchtower-ring1.mjs +194 -2
  50. package/templates/scripts/watchtower-ring2.mjs +488 -190
  51. package/templates/scripts/watchtower-ring3-close.mjs +994 -115
  52. package/templates/scripts/watchtower-routines.mjs +107 -1
  53. package/templates/skills/audit/SKILL.md +5 -5
  54. package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
  55. package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
  56. package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
  57. package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
  58. package/templates/skills/cabinet-debugger/SKILL.md +36 -0
  59. package/templates/skills/cabinet-deployment/SKILL.md +22 -0
  60. package/templates/skills/cabinet-elegance/SKILL.md +12 -0
  61. package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
  62. package/templates/skills/cabinet-historian/SKILL.md +33 -0
  63. package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
  64. package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
  65. package/templates/skills/cabinet-qa/SKILL.md +63 -0
  66. package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
  67. package/templates/skills/cabinet-system-advocate/SKILL.md +2 -2
  68. package/templates/skills/cabinet-workflow-cop/SKILL.md +65 -3
  69. package/templates/skills/cc-feedback/SKILL.md +4 -4
  70. package/templates/skills/cc-remember/SKILL.md +4 -4
  71. package/templates/skills/close/SKILL.md +63 -9
  72. package/templates/skills/debrief/SKILL.md +14 -8
  73. package/templates/skills/debrief/phases/qa-handoff-sweep.md +12 -9
  74. package/templates/skills/dx-feedback/SKILL.md +1 -1
  75. package/templates/skills/inbox/SKILL.md +85 -16
  76. package/templates/skills/memory/SKILL.md +1 -1
  77. package/templates/skills/onboard/SKILL.md +42 -33
  78. package/templates/skills/onboard/phases/detect-state.md +3 -2
  79. package/templates/skills/onboard/phases/generate-briefing.md +3 -2
  80. package/templates/skills/onboard/phases/generate-session-loop.md +70 -37
  81. package/templates/skills/onboard/phases/interview.md +17 -12
  82. package/templates/skills/onboard/phases/modularity-menu.md +33 -19
  83. package/templates/skills/onboard/phases/options.md +3 -1
  84. package/templates/skills/onboard/phases/post-onboard-audit.md +17 -11
  85. package/templates/skills/onboard/phases/summary.md +15 -12
  86. package/templates/skills/onboard/phases/work-tracking.md +14 -8
  87. package/templates/skills/orient/SKILL.md +7 -5
  88. package/templates/skills/pulse/SKILL.md +23 -18
  89. package/templates/skills/seed/phases/build-member.md +5 -3
  90. package/templates/skills/session-handoff/SKILL.md +72 -45
  91. package/templates/skills/spring-clean/SKILL.md +15 -13
  92. package/templates/skills/triage-audit/SKILL.md +3 -3
@@ -62,7 +62,7 @@ every session:
62
62
  When deferring an item that waits on a specific identifiable
63
63
  condition — a dependency landing, a stack decision finalizing, a
64
64
  referenced file appearing — use `pib_defer_with_trigger` instead of
65
- plain `status='deferred'`. Orient re-evaluates each trigger every
65
+ plain `status='deferred'`. The session loop re-evaluates each trigger every
66
66
  session and surfaces items whose conditions have become true. See
67
67
  [pib-db-triggers.md](pib-db-triggers.md) for the full convention:
68
68
  result vocabulary, cascade semantics for projects, migration
@@ -1,7 +1,7 @@
1
1
  # pib-db Deferred Triggers
2
2
 
3
3
  How to defer work items with structured return conditions and how
4
- orient re-evaluates them each session.
4
+ the session loop re-evaluates them each session.
5
5
 
6
6
  ## Purpose
7
7
 
@@ -13,8 +13,8 @@ or a `status='deferred'` row and sits there forever. Nobody re-reads
13
13
 
14
14
  Deferred triggers turn those items into structurally-encoded work:
15
15
  each one carries a natural-language condition describing what would
16
- have to change to reactivate it. Orient surfaces them every session
17
- and evaluates the condition against the current session's context.
16
+ have to change to reactivate it. The session loop surfaces them every
17
+ session and evaluates the condition against the current session's context.
18
18
  The item sits in the queue, not on a reminder list.
19
19
 
20
20
  ## Schema
@@ -110,29 +110,33 @@ The cascade:
110
110
  trigger is the single source of truth.
111
111
 
112
112
  When the parent reopens (user flips status back to `active`, or
113
- orient surfaces it as `triggered` and the user accepts):
113
+ the session loop surfaces it as `triggered` and the user accepts):
114
114
 
115
115
  - Children remain `deferred`.
116
116
  - The user decides which children to reopen. Reopening everything
117
117
  automatically often resurrects stale subgoals that the deferral
118
118
  period should have retired.
119
119
 
120
- ## Orient integration
120
+ ## Session-loop integration
121
121
 
122
- The `deferred-check.md` phase fires after `work-scan.md`, before
123
- `auto-maintenance.md`. Every session:
122
+ Under watchtower (the default loop), Ring 1 surfaces deferred/triggered
123
+ items in the project state file's Standing Issues, and the session — or
124
+ `/briefing` — re-evaluates each trigger against current context. On the
125
+ legacy loop, the `deferred-check.md` orient phase does this, firing after
126
+ `work-scan.md`, before `auto-maintenance.md`.
124
127
 
125
- 1. Orient calls `pib_list_triggered` (or CLI fallback).
126
- 2. For each item, evaluates the trigger text against the current
127
- session's context (new dependencies installed? a referenced file
128
- now exists? a stack decision was finalized?).
129
- 3. Records each evaluation via `pib_mark_trigger_checked`. Prefer
128
+ Either way, every session:
129
+
130
+ 1. Call `pib_list_triggered` (or CLI fallback).
131
+ 2. For each item, evaluate the trigger text against the current session's
132
+ context (new dependencies installed? a referenced file now exists? a
133
+ stack decision was finalized?).
134
+ 3. Record each evaluation via `pib_mark_trigger_checked`. Prefer
130
135
  `needs-info` over guessing `triggered`.
131
- 4. Items that evaluate to `triggered` appear in the briefing's
132
- **Attention Items** section. Orient does not auto-reopen — the
133
- user decides.
136
+ 4. Items that evaluate to `triggered` surface as **Attention Items**. The
137
+ session loop does not auto-reopen — the user decides.
134
138
 
135
- Cost control: cap the phase at 30 seconds. If more than 10 items are
139
+ Cost control: cap the evaluation at 30 seconds. If more than 10 items are
136
140
  triggered, evaluate only the 5 least-recently-checked.
137
141
 
138
142
  ## Migration guarantees
@@ -208,6 +208,81 @@ mux descriptor; routine items resolve normally (no QA gate, normal
208
208
  expiry — expiry also clears the descriptor). A pending routine item
209
209
  blocks refiring of the same routine until resolved or stale.
210
210
 
211
+ **Moment expiry for `time-of-day` routines (act:ea23b3a5).** A daily
212
+ routine's value is bound to its day: a morning briefing delivered
213
+ tomorrow is not a briefing. `stale_after_hours` only supersedes at the
214
+ NEXT due firing, which for a daily routine is 24 hours later, so a
215
+ missed firing held a queue slot and a lit desk badge all day. Ring 1's
216
+ tick now supersedes a pending `time-of-day` item once the calendar day
217
+ it was FILED on has ended.
218
+
219
+ End of day, not a fixed number of hours, and the difference is
220
+ measured rather than tasteful: across six weeks the portfolio's one
221
+ time-of-day routine had ten real pickups — eight within four hours and
222
+ two after, at 4.4h and 8.6h. A four-hour window would have destroyed two
223
+ genuine pickups to clear one stale item. End-of-day preserves all ten,
224
+ needs no tuning constant, and matches what a daily routine means to a
225
+ person: you can still want your morning briefing at 4pm; you do not want
226
+ it tomorrow.
227
+
228
+ Three scoping rules, each load-bearing:
229
+
230
+ - **Ring 1's portfolio tick only.** Ring 3 also calls `runRoutinePass`,
231
+ with a single-project `session-close` event. An unscoped sweep there
232
+ would have every session close in any project superseding every OTHER
233
+ project's routine items — and one portfolio sweep per replayed session
234
+ during a `--reprocess-failed` drain.
235
+ - **Classify from the ITEM, not from config.** `dispatchRoutine`
236
+ persists `evidence.trigger` at filing, so a routine whose declaration
237
+ was later removed still classifies. A config lookup would find nothing
238
+ and leave it pending forever.
239
+ - **An unparseable `filed_at` abstains, loudly.** The `stale_after_hours`
240
+ path treats the same input as stale (NaN comparisons fall through to
241
+ supersede); this one refuses and logs. Two mechanisms disagreeing
242
+ silently is worse than one abstaining out loud.
243
+
244
+ Consequences, named rather than discovered later: for a `time-of-day`
245
+ trigger the moment sweep pre-empts the `stale_after_hours` supersede
246
+ (same outcome, more honest reason), a missed briefing is no longer
247
+ re-delivered by the SessionStart missed-routine section on a LATER day,
248
+ and the sweep has no in-flight awareness — a routine picked up by `mux
249
+ qa drain` very close to midnight could be superseded mid-run, which
250
+ end-of-day narrows to minutes where a fixed-hour window would have
251
+ opened it to hours. `interval`, `path-nonempty`, and `session-close`
252
+ routines have no moment and are untouched.
253
+
254
+ ## Upstream-Friction Actionability (Phase 2g)
255
+
256
+ A friction report with no party who could act on it is not a report.
257
+ Five of the 2026-07-26 dismissals were observations like "Claude made
258
+ iterative browser tweaks without writing to file" — true, and addressed
259
+ to nobody, aging in the queue for weeks.
260
+
261
+ Phase 2g now classifies each finding into three arms (act:ea23b3a5),
262
+ copying Phase 2f's verification-gate shape (which files a methodology
263
+ capture only when the claimed artifact verifiably exists on disk, after
264
+ 11 of 11 unverified captures were dismissed over three weeks):
265
+
266
+ 1. **A named route** — `evidence.route.type` from the CLOSED vocabulary
267
+ `cc-feedback | project-tracker | config-change`, plus a non-empty
268
+ `detail` naming who acts and how ⇒ files `upstream-friction`, with the
269
+ route rendered into the summary so the operator sees it without
270
+ opening the evidence.
271
+ 2. **Durable, no route** — a platform constraint or environment quirk
272
+ nobody can fix, whose WORKAROUND is worth remembering ⇒ files
273
+ `knowledge-extraction`. This arm is not theory: of the five friction
274
+ items the operator KEPT, four exited as `captured-to-memory`. A
275
+ two-arm gate would have discarded the useful half of the lens.
276
+ 3. **Neither** — a transient incident or a general observation about
277
+ Claude's behavior ⇒ files nothing, and is COUNTED. All three counts
278
+ are logged every run; silence would be indistinguishable from a lens
279
+ that never ran, and the drop count is the number this change exists to
280
+ move.
281
+
282
+ An invented route type or an empty `detail` falls to arm 3. The
283
+ vocabulary is closed precisely so "someone should fix this" cannot
284
+ become an escape hatch.
285
+
211
286
  ## Session Advisor Pass (re-homed standing advisors)
212
287
 
213
288
  The standing session advisors (historian, system-advocate,
@@ -336,6 +411,125 @@ each owning its tier. Flag: `defaults.recall_canary` (opt-out).
336
411
  extraction never proposed (M2 under-extraction). The canary states
337
412
  this blindness in its own output.
338
413
 
414
+ ## Reach 1 — Pending-Relation Proposals (act:5182beda, N3 of grp:retro-remeaning)
415
+
416
+ The first shippable instance of **retrospective temporality** (`act:ef5a3842`,
417
+ MANIFESTO principle 4): new understanding reaching BACK to recolor what came
418
+ before, at the earliest and safest reach — the still-pending inbox, where
419
+ nothing is believed yet. Every specimen that motivated the program had its
420
+ counterpart sitting pending at the moment the second one was written; Phase
421
+ 2d's extraction call now sees that pending pile and can propose a relation.
422
+
423
+ **Mechanism.** `selectNearbyPendingItems` (ring3-close.mjs) shortlists the
424
+ pending inbox drafts most relevant to THIS session's transcript (same
425
+ relevance-scoring recipe as `selectNearbyMemoryTitles`, a SEPARATE corpus
426
+ with its OWN cap, `PENDING_RELATION_CAP` — never a shared budget with
427
+ memory's, or a large pending pile would evict memory titles and degrade the
428
+ M1b novelty gate). The extraction prompt's `PENDING IN INBOX` block is
429
+ injected as its OWN section, distinct from `ALREADY SAVED TO MEMORY`. For
430
+ each item, the model may set `relates_to` (the exact pending title) and
431
+ `relation` (`pairs_with` | `answers` | `contradicts`) — fields the model
432
+ outputs ALONGSIDE, never IN, `covered_by`. This separation is load-bearing:
433
+ `covered_by` stays scoped to memory titles and feeds `modelRescues`/
434
+ `isDuplicate` exactly as before; `relates_to`/`relation` are never read by
435
+ either — naming a pending title in `covered_by` instead would flip the
436
+ rescue gate into a suppressor (a filed-today item wrongly read as "covered
437
+ by," and suppressed, because it topically resembles a merely-PENDING draft
438
+ that isn't believed yet — the exact failure the Meta question/answer
439
+ specimen would have suffered).
440
+
441
+ **Filing.** A relation proposal is its own inbox item, category
442
+ `pending-relation`, filed via `fileRelationProposal` AFTER the newly-
443
+ extracted item's own `createItem` call succeeds (its id isn't known until
444
+ then). Deduped by `evidence.relation_key` (sorted ids + the relation verb) —
445
+ one pending item per (pair, verb), so the same relation proposed from either
446
+ side, or in a later session, never refiles. The proposal never changes
447
+ anything about the item that prompted it: no suppression, no home change, no
448
+ urgency change — it is strictly additive.
449
+
450
+ **The M5 suppression ledger is blind to this channel.** `recordSuppression`
451
+ (the Suppression Ledger above) only sees LEXICAL suppressions — the
452
+ deterministic `isDuplicate` corpus matches. The model's real per-item
453
+ inclusion/omission decision happens INSIDE the extraction call, in a channel
454
+ the ledger cannot observe at all: if the model chose to silently omit an
455
+ item from its output array (rather than including it with `relation: none`),
456
+ no suppression record is ever written, because nothing was suppressed BY A
457
+ CORPUS — it was never proposed. This is why the prompt carries an explicit,
458
+ load-bearing instruction that a pending item is grounds for a RELATION only,
459
+ never grounds to omit or demote anything — the instruction is the entire
460
+ defense, because the suppression ledger cannot audit it.
461
+
462
+ **Retraction.** Registered in `INBOX_DETECTOR_REGISTRY` with a reconciler:
463
+ `autoReconcilePendingRelations` (ring1.mjs, one global pass per tick — purely
464
+ queue-internal, no per-project pib-db access, unlike `completion-review`'s).
465
+ Retracts via `autoReconcileItem` when EITHER cited side (`evidence.from_id`
466
+ / `evidence.to_id`) leaves pending — a relation between two open questions
467
+ no longer applies once one resolves. Fails toward KEEPING: an id that no
468
+ longer resolves via `getItem` (deleted/migrated) is ambiguous, not evidence
469
+ the relation resolved.
470
+
471
+ ## Reach 2 — Significance-Change Proposals (act:bcb7edd4, L1 of grp:retro-remeaning)
472
+
473
+ The second reach of the retrospective-temporality motion (`act:ef5a3842`,
474
+ MANIFESTO principle 4), extending Reach 1's inbox-range reach-back to the
475
+ recently-absorbed thread trail. When Ring 3 closes a session, the new
476
+ understanding it captured reaches BACK into the prior cursor history of the
477
+ threads THIS session touched, and may PROPOSE that an earlier entry now
478
+ means something different: `reframes` (significance change — what looked
479
+ like friction was the seed of the architecture; the class a contradiction
480
+ scan is structurally blind to), `answers` (a real earlier uncertainty is
481
+ settled), or `contradicts`. Category: `significance-change`.
482
+
483
+ **Mechanism.** Inside Phase 2b2, AFTER the cursor appends land: the prior
484
+ trail of each earned thread is snapshotted from disk BEFORE this session's
485
+ entry is appended (last `PRIOR_HISTORY_CAP` entries), and one focused
486
+ follow-up model call (`significanceReachBack`) sees the new understanding
487
+ beside each prior trail. It fires only when at least one earned thread has
488
+ prior history — a routine or first-touch session costs nothing. It cannot
489
+ ride the capture call the way Reach 1 rode extraction: the reach-back
490
+ corpus (which threads matter) IS that call's output. EVENT-DRIVEN by
491
+ construction — never a periodic sweep (the shape the cabinet killed).
492
+
493
+ **Proposals are validated, then durably deduped.** A proposal must cite an
494
+ ACTUAL prior entry — session_id + date checked against the on-disk snapshot
495
+ (`validateRecolorProposals`); a hallucinated anchor drops, logged, never
496
+ filed. Filing (`fileSignificanceProposal`) dedups by
497
+ `evidence.relation_key` (`thread|prior_session|new_session|verb`) across
498
+ ALL queue statuses with NO time window: a dismissed or expired judgment
499
+ about a static corpus never re-nags — not after `RESOLUTION_CORPUS_DAYS`,
500
+ not ever — and a reprocess replay of the same session produces the same key
501
+ and files nothing. (The same unwindowed-exclusion discipline was backported
502
+ to Reach 1's `fileRelationProposal`, whose original pending-only dedup
503
+ left a post-dismissal refile hole.)
504
+
505
+ **The gate is real, not ceremonial.** Both relational categories
506
+ (`significance-change`, `pending-relation`) are in `GATED_CATEGORIES`:
507
+ `applyBatch` structurally refuses them, so each reinterpretation is
508
+ confirmed or dismissed individually — a rubber-stamped batch of re-meanings
509
+ is how a system gaslights its operator. On CONFIRM, /inbox applies the
510
+ recolor via `appendThreadRecolor` (watchtower-lib): an entry in the
511
+ thread's additive `recolor_log` array. The recolor ANNOTATES history — it
512
+ never rewrites `cursor_history`, and no ring ever writes `recolor_log`;
513
+ the rings propose, only the operator's confirmation applies.
514
+
515
+ **Retraction.** `autoReconcileSignificanceItems` (ring1, per tick):
516
+ retracts when the cited thread file is POSITIVELY gone from
517
+ `state/threads/` (dir readable, file absent — a recolor of deleted history
518
+ is moot); keeps on any read error, and keeps EVERYTHING when the dir is
519
+ unreadable (a mount hiccup must not mass-retract).
520
+
521
+ **The honest metric that gates Reach 3.** Every real-work close appends one
522
+ line to `state/significance-ledger.jsonl` (`recordSignificanceEvent`,
523
+ fail-open, same discipline as the suppression ledger): session, threads
524
+ touched, threads with history, entries shown, proposals filed.
525
+ `readSignificanceRate` (watchtower-lib) combines the ledger (denominator,
526
+ replay-deduped by session_id) with the queue's per-category dispositions
527
+ (numerator) into `{sessions, proposals_per_session, keep_ratio, …}` — with
528
+ null ratios when a denominator is zero, never fabricated 0s. This is the
529
+ L2 gate's named reader: Reach 3 (`act:a8b372a7`) is built only when an
530
+ observed significance-change rate on real data warrants it — replacing the
531
+ contradiction-density measurement the plan retired as the wrong instrument.
532
+
339
533
  ## Detector Symmetry (no detector without a retraction path)
340
534
 
341
535
  The 2026-07-12 inbox drain (893→194 pending) hit the same disease four
@@ -404,16 +598,133 @@ branch-diverged item for it — the branch stays visible on the attention
404
598
  line / git-attention sidecar / summary / Standing Issues (the exclusion
405
599
  gates filing ONLY), and the consumer overrides the list in config.
406
600
 
407
- **Standing-debt aggregation (coverage-warning first):** a detector
408
- whose finding is a STANDING condition (recurs while a debt persists)
409
- files ONE item per (project, debt-class) and APPENDS evidence to the
410
- pending item (path union + per-path counts + session count), carrying
411
- `evidence.first_seen` (oldest evidence date) so aging reads the debt's
412
- true age, with an urgency escalation at a session-count threshold.
413
- Resolving the item closes the accumulated view; if the debt persists,
414
- the next tick files FRESH — post-disposition recurrence is a new
415
- signal, not a refile loop (act:e888dd63; 49 identical coverage-warning
416
- items for one month-old debt was the live specimen).
601
+ **Standing-debt aggregation (coverage-warning, memory-budget):** a
602
+ detector whose finding is a STANDING condition (recurs while a debt
603
+ persists) files ONE item per (project, debt-class) and APPENDS evidence
604
+ to the pending item, carrying `evidence.first_seen` (oldest evidence
605
+ date) so aging reads the debt's true age. Resolving the item closes the
606
+ accumulated view; if the debt persists, the next tick files FRESH —
607
+ post-disposition recurrence is a new signal, not a refile loop
608
+ (act:e888dd63; 49 identical coverage-warning items for one month-old
609
+ debt was the live specimen).
610
+
611
+ Three rules the second adopter (memory-budget, act:ea23b3a5) added, each
612
+ learned from a live failure mode:
613
+
614
+ 1. **The occurrence key must be the operator's unit of time, not the
615
+ ring's.** coverage-warning keys on the session; memory-budget keys on
616
+ the CALENDAR DAY, because Ring 2 slow runs ~48 times a day. A per-run
617
+ key makes the replay guard dead code, grows the key list by ~17,500
618
+ entries a year inside one JSON file, renders a count no human can
619
+ read, and — because the backlog-rot reader weighs standing debt by
620
+ that count — poisons the `keeping` / `losing` / `not-consumed` verdict
621
+ `/inbox` and `/briefing` lead with.
622
+ 2. **Reuse an evidence spelling the readers already know.** The rot
623
+ weigher (`itemSessionWeight`, watchtower-inbox-assessment) accepts
624
+ `session_count` / `session_ids` / `sessions` and silently weighs
625
+ everything else as 1. A new adopter that invents a fifth spelling
626
+ reintroduces the blind spot the fourth was added to fix. memory-budget
627
+ writes `session_count`.
628
+ 3. **Prefer `annotateItemEvidence` over a direct item write, and do not
629
+ bump `filed_at` without a reason.** The append gets a fresh read, a
630
+ pending fence checked at write time, and a deep-equal skip for free.
631
+ Bumping `filed_at` has three side effects worth choosing
632
+ deliberately: it strips an `[AGING]` prefix the escalation pass added
633
+ (and prevents its return), it re-escalates urgency the decay pass
634
+ de-escalated, and — since `listPending` sorts by `filed_at` — it pins
635
+ the item to the top of every pending list permanently. coverage-warning
636
+ bumps on purpose (its expiry clock should measure the last
637
+ recurrence); memory-budget does not.
638
+
639
+ **What does NOT aggregate.** `doc-drift` carries a per-claim,
640
+ mutually-exclusive fix-doc / fix-code decision, so N claims folded into
641
+ one item pose a question the operator cannot answer, and any reconciler
642
+ over the aggregate could only fire when every claim cleared.
643
+ `raised-unhandled` is free text: recognizing "this loose end restates
644
+ that one" needs a similarity matcher, and `proposeFolds`'
645
+ `overlapCoefficient` at `FOLD_SIMILARITY_THRESHOLD = 0.22` folds two
646
+ three-token titles that share ONE word — calibrated for advisory
647
+ retrieval, not for a destructive merge. Both instead route through Ring
648
+ 2's `runDraftAnnotationSweep`, which annotates both sides with
649
+ `possible_duplicate_of` and demotes them out of batch sign-off. **A
650
+ lexical matcher may propose; it may never silently suppress.**
651
+
652
+ ## Category Expiry
653
+
654
+ Every pending item ages out on a per-category policy. The table and its
655
+ helpers live in `watchtower-queue.mjs` (`expiryDaysFor`,
656
+ `categoryNeverExpires`, `CATEGORY_EXPIRE_DAYS`) — read the constants
657
+ there for the numbers; mirroring them into this document would be a
658
+ count claim that drifts, in a repo whose Ring 4 files `doc-drift` items
659
+ for exactly that.
660
+
661
+ **There are TWO expiry engines and both must delegate.** Ring 2's
662
+ `escalateQueueItems` runs on the 5-minute cron; `runExpiry` is called by
663
+ `/inbox` and `/briefing`. Before act:ea23b3a5 each hardcoded 30 days and
664
+ inlined its own qa-handoff carve-out, so a per-category policy set in one
665
+ would be silently violated by whichever engine reached an item first.
666
+
667
+ **The qa-handoff exemption is structural, not a preference.**
668
+ `expireItem` THROWS on a qa-handoff by the recipient-gate contract, so
669
+ `categoryNeverExpires` lives beside that throw: returning anything but
670
+ null for it would crash the caller's loop. One fact, one place.
671
+
672
+ **A shortened expiry can open a refile loop.** completion-review expires
673
+ at 14 days, which equals `COMPLETION_REVIEW_DEDUP_DAYS`. An item
674
+ therefore leaves the dedup window at exactly the moment it expires — so
675
+ Phase 2c's dedup corpus must include `expired` and `superseded`, or the
676
+ fid is in no corpus at all and the next session refiles it forever. That
677
+ is Detector Symmetry §2's named failure, reached from the other
678
+ direction: **when you shorten a category's expiry, check what its filing
679
+ site dedups against.**
680
+
681
+ ## Queue Items Require a Decision; Ambient State Describes Conditions
682
+
683
+ A queue item asks the operator to decide something. A per-project state
684
+ file describes a condition. Filing a condition as an item spends the
685
+ operator's scarcest resource — a triage slot — on something they can
686
+ only acknowledge.
687
+
688
+ **The axis is who is judging, not whether the value is recomputable.**
689
+ Recomputability is the wrong test and would contradict a rule this
690
+ system already ships: derivable and perishable facts (act:471dd701,
691
+ act:a69c21f8) are recomputable *by definition*, and they are **always
692
+ filed** — routed to their derivation or the session record, never
693
+ dropped — because that routing is a per-item MODEL judgment, and
694
+ dropping one would be silent model auto-suppression. A deterministic
695
+ ring measurement is different in kind: the operator routes it once, by
696
+ category, and the routing holds.
697
+
698
+ **The functional test: does waiting cost something irreversible?**
699
+ `worktree-unmerged` is the calibrating counter-example. Ring 1
700
+ recomputes it from git every five minutes, so by a recomputability rule
701
+ it would be pure ambient state — and it must stay in the queue, because
702
+ the worktree gets cleaned up and the work is gone. A recomputable
703
+ condition with decay belongs in the queue; a recomputable condition that
704
+ is stable belongs in ambient state.
705
+
706
+ **The calibrating pair (act:ea23b3a5).** The cabinet-roster review
707
+ produces three signals. `uncovered-tech` names a specific per-project
708
+ gap and proposes adding a cabinet seat — a decision, so it stays a queue
709
+ item. `dormant-skill` and `stale-briefing` are measurements, so they
710
+ moved to `state/roster-review.json` and render in Ring 1's Standing
711
+ Issues.
712
+
713
+ Three obligations come with moving something to ambient state:
714
+
715
+ - **Fix the measurement first.** The dormant-skill count was a saturated
716
+ display cap: every project ever measured reported exactly "12 dead",
717
+ because the title was built from an array already sliced to
718
+ `ROSTER_DORMANT_LIST_CAP`. Moving that into seven state files would
719
+ have given a constant the authority of a fact.
720
+ - **Render on change, or give it a firing threshold.** Ambient state has
721
+ no dismiss verb — the queue item at least had one. A number that never
722
+ moves in a file read at every briefing is furniture by the third read.
723
+ The precedent is the recall canary, which Ring 1 renders only on alert.
724
+ - **Retire what is already filed.** The filing site was usually the only
725
+ code that ever touched those items. Removing it without a one-shot
726
+ supersede leaves them displaying a stale count for weeks beside a live
727
+ ambient line saying something different — dual existence.
417
728
 
418
729
  **Known limitation (named, not hidden):** reconcilers run per
419
730
  configured project — items whose project was deleted or de-registered
@@ -176,7 +176,7 @@ const TOOLS = [
176
176
  },
177
177
  {
178
178
  name: 'pib_defer_with_trigger',
179
- description: 'Defer an action or project with a free-text trigger condition describing what would reactivate it. Orient re-evaluates triggers each session. Use this INSTEAD of pib_update_action with status=deferred when the deferral is waiting for a specific condition.',
179
+ description: 'Defer an action or project with a free-text trigger condition describing what would reactivate it. Each session re-evaluates triggers and surfaces those whose condition has fired. Use this INSTEAD of pib_update_action with status=deferred when the deferral is waiting for a specific condition.',
180
180
  inputSchema: {
181
181
  type: 'object',
182
182
  properties: {
@@ -199,7 +199,7 @@ const TOOLS = [
199
199
  },
200
200
  {
201
201
  name: 'pib_mark_trigger_checked',
202
- description: 'Record that a trigger was evaluated. Use after orient\'s deferred-check phase evaluates triggers against session context. Result must be one of: triggered | still-waiting | needs-info | condition-obsolete.',
202
+ description: 'Record that a trigger was evaluated. Use after a session evaluates triggers against session context. Result must be one of: triggered | still-waiting | needs-info | condition-obsolete.',
203
203
  inputSchema: {
204
204
  type: 'object',
205
205
  properties: {
@@ -35,7 +35,7 @@ Descriptive slug. Convention: `pattern-{domain}-{topic}` or just
35
35
  `pattern-{topic}`.
36
36
 
37
37
  ### `description`
38
- One-line summary. Used by orient/debrief to decide whether to load
38
+ One-line summary. Used by the session loop to decide whether to load
39
39
  the full pattern. Be specific — "deployment issues" is too vague;
40
40
  "silent deployment failures and stale build log output" is useful.
41
41
 
@@ -28,6 +28,11 @@ in_tmux() { [[ -n "${TMUX:-}" ]]; }
28
28
 
29
29
  slugify() {
30
30
  local input="$*"
31
+ # Flatten to one line FIRST — sed/cut below process input line-by-line,
32
+ # so a multi-paragraph prompt would otherwise yield a multi-line "slug"
33
+ # (one line per input line) that mangles or empties out every caller's
34
+ # branch/directory name instead of collapsing to a single slug.
35
+ input="${input//$'\n'/ }"
31
36
  # If prompt has "name. rest", use only the name part
32
37
  if [[ "$input" == *". "* ]]; then
33
38
  input="${input%%". "*}"
@@ -2,6 +2,7 @@
2
2
  """MCP server for mux — lets Claude manage desks, windows, and notes."""
3
3
 
4
4
  import json
5
+ import shlex
5
6
  import subprocess
6
7
  import sys
7
8
  import os
@@ -192,11 +193,12 @@ def handle_tool(name, args):
192
193
  elif name == "mux_new":
193
194
  prompt = args.get("prompt", "")
194
195
  if prompt:
195
- out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} new "{prompt}" 2>&1')
196
- return out or f"New window '{prompt}' created."
196
+ out, _, rc = run(f'{os.path.expanduser("~/.local/bin/mux")} new {shlex.quote(prompt)} 2>&1')
197
197
  else:
198
- out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} new 2>&1')
199
- return out or "New window created."
198
+ out, _, rc = run(f'{os.path.expanduser("~/.local/bin/mux")} new 2>&1')
199
+ if rc != 0:
200
+ return f"Failed to open new window: {out or '(no output — check tmux/mux state)'}"
201
+ return out or (f"New window '{prompt}' created." if prompt else "New window created.")
200
202
 
201
203
  elif name == "mux_note_add":
202
204
  text = args["text"]
@@ -236,7 +238,7 @@ def handle_tool(name, args):
236
238
  elif name == "mux_status":
237
239
  project = args.get("project", "")
238
240
  if project:
239
- out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status "{project}" 2>&1')
241
+ out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status {shlex.quote(project)} 2>&1')
240
242
  else:
241
243
  out, _, _ = run(f'{os.path.expanduser("~/.local/bin/mux")} status 2>&1')
242
244
  return out or "No desks open."
@@ -107,10 +107,10 @@ promotion candidates (specific rules ready for promotion).
107
107
 
108
108
  | Step | Owner | When |
109
109
  |------|-------|------|
110
- | Capture + Classify | /debrief | Every session |
110
+ | Capture + Classify | watchtower Ring 3 (or /debrief) | Every session |
111
111
  | Pattern analysis | Periodic review | As feedback accumulates |
112
112
  | Promotion checkpoint | Review cadence | When patterns mature |
113
- | Enforcement health | /orient or /pulse | Regular check |
113
+ | Enforcement health | /briefing or /pulse | Regular check |
114
114
 
115
115
  ## Audit Findings as Promotion Input
116
116
 
@@ -17,12 +17,12 @@ write it to memory without asking whether to save it.
17
17
  `/cc-remember` ensures:
18
18
  - The new memory lands at `~/.claude/projects/<slug>/memory/<slug>.md`
19
19
  - `MEMORY.md` gets a new entry in the "Curated entries (hand-authored)"
20
- section, so next session's `/orient` can find the memory
20
+ section, so the next session can find the memory
21
21
  - Filename collisions are handled (suffix with date if needed)
22
22
  - The write is atomic (temp + rename — safe under concurrent sessions)
23
23
 
24
24
  Direct flat-`.md` writes work but skip the indexing step. The memory
25
- becomes invisible to `/orient` until you manually update `MEMORY.md`.
25
+ becomes invisible to the next session until you manually update `MEMORY.md`.
26
26
  The `memory-index-guard` PostToolUse hook will flag this when it
27
27
  happens, but it's better to avoid the issue: **use `/cc-remember`**.
28
28
 
@@ -0,0 +1,89 @@
1
+ // absorbThreadSessions (act:6f93a1f1, N4 of grp:retro-remeaning) — the
2
+ // age-only session-record pruning gate (ABSORPTION_SESSION_AGE_DAYS = 90)
3
+ // is RETIRED. Structural contract under test:
4
+ // A1: an active thread's session record survives a slow-tier run, even when
5
+ // the session is far older than the old 90-day cutoff (the deleted
6
+ // constant no longer exists to prune it).
7
+ // A2: dormant-marking is UNCHANGED — a thread untouched for 30+ days still
8
+ // flips to 'dormant'.
9
+ // A3: the retired constant is gone from the module's exports/source.
10
+ //
11
+ // Hermetic per qa-handoff-gate.test.mjs: WATCHTOWER_DIR points at a temp dir,
12
+ // set BEFORE the dynamic import. No Anthropic calls — absorbThreadSessions
13
+ // touches only the filesystem.
14
+
15
+ import { test } from 'node:test';
16
+ import assert from 'node:assert/strict';
17
+ import { mkdtempSync, mkdirSync, writeFileSync, readFileSync, existsSync, rmSync } from 'node:fs';
18
+ import { join } from 'node:path';
19
+ import { tmpdir } from 'node:os';
20
+
21
+ const root = mkdtempSync(join(tmpdir(), 'absorption-no-prune-'));
22
+ process.env.WATCHTOWER_DIR = root;
23
+
24
+ const ring2 = await import('../watchtower-ring2.mjs');
25
+ const { absorbThreadSessions, DORMANT_THREAD_AGE_DAYS } = ring2;
26
+
27
+ test.after(() => rmSync(root, { recursive: true, force: true }));
28
+
29
+ function isoDaysAgo(days) {
30
+ return new Date(Date.now() - days * 86400000).toISOString();
31
+ }
32
+
33
+ test('A1: an active thread carrying a >90-day-old session record survives a slow-tier run', async () => {
34
+ const threadsDir = join(root, 'state', 'threads');
35
+ mkdirSync(threadsDir, { recursive: true });
36
+
37
+ const thread = {
38
+ schema_version: 2,
39
+ thread: 'fixture-thread',
40
+ display_name: 'Fixture thread',
41
+ cursor_history: [{ date: isoDaysAgo(2), session_id: 'sess-recent', cursor: { what: 'x', where_left_off: 'y', open_questions: [] } }],
42
+ // Fixture must pin active + recent last_updated, or this hits the
43
+ // dormant branch and the survival assertion is vacuous.
44
+ sessions: [{ id: 'sess-old', project: 'fixture-proj', date: isoDaysAgo(200) }],
45
+ related_fids: [],
46
+ last_updated: isoDaysAgo(2),
47
+ status: 'active',
48
+ };
49
+ writeFileSync(join(threadsDir, 'fixture-thread.json'), JSON.stringify(thread, null, 2));
50
+
51
+ const sessionsDir = join(root, 'state', 'projects', 'fixture-proj', 'sessions');
52
+ mkdirSync(sessionsDir, { recursive: true });
53
+ const sessionFilePath = join(sessionsDir, `${isoDaysAgo(200).slice(0, 10)}-sess-old.md`);
54
+ writeFileSync(sessionFilePath, '# Session sess-old\n\nDate: ' + isoDaysAgo(200) + '\n\n- did a thing\n');
55
+
56
+ await absorbThreadSessions();
57
+
58
+ assert.ok(existsSync(sessionFilePath), 'the 200-day-old session record must survive — age-only pruning is retired');
59
+ const stored = JSON.parse(readFileSync(join(threadsDir, 'fixture-thread.json'), 'utf8'));
60
+ assert.equal(stored.status, 'active', 'a recently-updated thread must stay active, not be pruned into dormant');
61
+ });
62
+
63
+ test('A2: dormant-marking is unchanged — a thread untouched 30+ days flips to dormant', async () => {
64
+ const threadsDir = join(root, 'state', 'threads');
65
+ mkdirSync(threadsDir, { recursive: true });
66
+
67
+ const thread = {
68
+ schema_version: 2,
69
+ thread: 'stale-thread',
70
+ display_name: 'Stale thread',
71
+ cursor_history: [],
72
+ sessions: [],
73
+ related_fids: [],
74
+ last_updated: isoDaysAgo(DORMANT_THREAD_AGE_DAYS + 5),
75
+ status: 'active',
76
+ };
77
+ writeFileSync(join(threadsDir, 'stale-thread.json'), JSON.stringify(thread, null, 2));
78
+
79
+ await absorbThreadSessions();
80
+
81
+ const stored = JSON.parse(readFileSync(join(threadsDir, 'stale-thread.json'), 'utf8'));
82
+ assert.equal(stored.status, 'dormant');
83
+ });
84
+
85
+ test('A3: the retired ABSORPTION_SESSION_AGE_DAYS constant is gone', () => {
86
+ assert.equal(ring2.ABSORPTION_SESSION_AGE_DAYS, undefined);
87
+ const src = readFileSync(new URL('../watchtower-ring2.mjs', import.meta.url), 'utf8');
88
+ assert.ok(!src.includes('ABSORPTION_SESSION_AGE_DAYS'));
89
+ });
@@ -373,9 +373,7 @@ test('R8: the draft-sweep sidecar has a reader — assessRingsAlive surfaces its
373
373
  last_run: daysAgo(2),
374
374
  projects_scanned: 3,
375
375
  items_scanned: 41,
376
- freshness_annotated: 2,
377
376
  fold_annotated: 4,
378
- skipped_projects: [],
379
377
  unscanned_items: 0,
380
378
  errors: 0,
381
379
  }));