create-claude-cabinet 0.52.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 (38) hide show
  1. package/lib/cli.js +2 -2
  2. package/package.json +1 -1
  3. package/templates/CLAUDE.md +164 -20
  4. package/templates/cabinet/watchtower-contracts.md +202 -10
  5. package/templates/mux/bin/mux +5 -0
  6. package/templates/mux/config/mux-server.py +7 -5
  7. package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
  8. package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
  9. package/templates/scripts/__tests__/extraction-classification.test.mjs +87 -0
  10. package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
  11. package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
  12. package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
  13. package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
  14. package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
  15. package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
  16. package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
  17. package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
  18. package/templates/scripts/skill-validator.sh +7 -4
  19. package/templates/scripts/watchtower-lib.mjs +58 -5
  20. package/templates/scripts/watchtower-queue.mjs +54 -7
  21. package/templates/scripts/watchtower-ring1.mjs +74 -0
  22. package/templates/scripts/watchtower-ring2.mjs +442 -81
  23. package/templates/scripts/watchtower-ring3-close.mjs +271 -82
  24. package/templates/scripts/watchtower-routines.mjs +107 -1
  25. package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
  26. package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
  27. package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
  28. package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
  29. package/templates/skills/cabinet-debugger/SKILL.md +36 -0
  30. package/templates/skills/cabinet-deployment/SKILL.md +22 -0
  31. package/templates/skills/cabinet-elegance/SKILL.md +12 -0
  32. package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
  33. package/templates/skills/cabinet-historian/SKILL.md +33 -0
  34. package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
  35. package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
  36. package/templates/skills/cabinet-qa/SKILL.md +63 -0
  37. package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
  38. package/templates/skills/cabinet-workflow-cop/SKILL.md +60 -0
package/lib/cli.js CHANGED
@@ -1832,8 +1832,8 @@ async function run() {
1832
1832
  console.log('\n ✅ Cabinet assembled!\n');
1833
1833
  console.log(' Next steps:');
1834
1834
  console.log(' 1. Run /onboard in Claude Code — Claude will interview you and prepare the briefings');
1835
- console.log(' 2. Start each session with /orientget briefed');
1836
- console.log(' 3. End each session with /debrief close the loop for next time');
1835
+ console.log(' 2. Run /watchtower install to start the background rings sessions then open pre-briefed');
1836
+ console.log(' 3. /briefing is your daily surface; end sessions with /close (or /session-handoff)');
1837
1837
  const skippedKeys = Object.keys(skippedModules);
1838
1838
  if (skippedKeys.length > 0) {
1839
1839
  console.log(`\n Skipped modules: ${skippedKeys.join(', ')}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.52.0",
3
+ "version": "0.53.0",
4
4
  "description": "Claude Cabinet — opinionated process scaffolding for Claude Code projects",
5
5
  "bin": {
6
6
  "create-claude-cabinet": "bin/create-claude-cabinet.js"
@@ -319,9 +319,22 @@ consumes the registry stays in cli.js).
319
319
  ring-filed inbox category to `{reconciler}` or `{exempt}` (exactly
320
320
  one), enforced by the structural filing-site test
321
321
  `__tests__/detector-registry.test.mjs`: it scans `category:` literals
322
- at createItem sites across ring1 + ring2 + ring3-close (read-only
323
- source scan) and fails on an unregistered category or a non-literal
324
- category value inside a filing owner (act:4d11fd53).
322
+ at createItem sites across ring1 + ring2 + ring3-close + **ring4 +
323
+ routines** (read-only source scan) and fails on an unregistered
324
+ category or a non-literal category value inside a filing owner
325
+ (act:4d11fd53; act:ea23b3a5 added the last two scripts and their
326
+ `doc-drift` / `routine` entries, closing the gap act:c2f06955 item 5
327
+ named — the contract's "maps every ring-filed category" claim was
328
+ false while ring4 and the routine engine sat outside the scan). Census
329
+ at 2026-07-26: 27 filing sites, 20 distinct categories, 20 registry
330
+ entries. **The census binds a `category:` literal to its NEAREST
331
+ PRECEDING call opener and recognises only `createItem(` and `file(` as
332
+ filing owners** — an injectable filing dep under any other name reads
333
+ as a query filter and its category silently drops out of the census.
334
+ That is not hypothetical: act:ea23b3a5's ring2 memory-budget dep, first
335
+ written as `createItemFn`, dropped `watchtower-health` out of the
336
+ filing set and the suite caught it. Name an injected filing seam
337
+ `file`.
325
338
  `GENERATED_STATE_PATTERNS`/`generatedStateIsDisposable` — the JS SSOT
326
339
  for machine-written generated state (UNTRACKED `??` lines only:
327
340
  `.claude/verification/`, advisories-state.json, checklist-stats.json,
@@ -376,15 +389,43 @@ consumes the registry stays in cli.js).
376
389
  to the lib's `resolveProjectFromTranscriptSlug` (feature-detected by
377
390
  name on the namespace import) and then to `project_unresolved: true` —
378
391
  inbox filing proceeds, but no state-file or thread key is minted from
379
- a raw slug. Phase 2c's `completionReviewEmitGuard` adds the
380
- create-vs-complete filter (act:9eebbac4): an action whose db `created`
381
- date falls inside the session's day window (+1 for past-midnight
382
- spill) with confidence below `high` is not a completion candidate (an
383
- action born this session whose only evidence is its own creation);
384
- mechanical (db row, the same prepared statement as the status
385
- re-check) and FAIL-OPEN in every gap no sessionDate, an unparseable
386
- sessionDate (windowEnd is a required conjunct), a missing `created`
387
- column, or a thrown lookup all emit. coverage-warning is now a
392
+ a raw slug. Phase 2c's `completionReviewEmitGuard` was **rebuilt on
393
+ measurement** (act:ea23b3a5): act:9eebbac4's create-vs-complete
394
+ day-window filter is DELETED and the model's `confidence: 'high'` is
395
+ SUPPRESSED instead. Over all 223 items this category had ever filed,
396
+ each joined against the cited action's OWN project db, `high` scored
397
+ 30% precision (12 acted-on / 28 dismissed-noise) against 60% for
398
+ medium+low (48 / 32) the model rates high when a session *talks
399
+ about* an action (files it, plans it, discusses it), not when it
400
+ quietly finishes one. The deleted filter exempted `high` from its
401
+ born-this-session skip, so it suppressed the BEST cohort (same-day
402
+ creation + medium/low: 74%) and passed the worst (same-day + high:
403
+ 33%); split at its 2026-07-12 ship date the category went **0-for-23**
404
+ — zero acted-on, 19 dismissed as noise. A **calibration fence** sits on
405
+ the check: the result holds for the Phase 2c confidence rubric AS
406
+ WRITTEN, so rewording that rubric requires re-running the measurement.
407
+ The per-fid dedup corpus gained `expired` + `superseded`, load-bearing
408
+ because the category's 14-day expiry equals
409
+ `COMPLETION_REVIEW_DEDUP_DAYS` (without them a fid leaves every corpus
410
+ exactly when its item expires and refiles forever). A
411
+ quoted-completion-evidence check (`verifyCompletionQuote` +
412
+ `decodeTranscriptText`) ships as **INSTRUMENTATION ONLY** — stamped to
413
+ `evidence.quote_verified`, counted per session, gating nothing: the
414
+ haystack is `preprocessTranscript`'s JSON-serialized output, so a
415
+ substring gate rejects ordinary multi-line prose (a newline is the two
416
+ characters `\` + `n`) and reliably ACCEPTS serialized TodoWrite
417
+ arguments; `decodeTranscriptText` drops `tool_use` blocks for exactly
418
+ that reason, and even decoded, containment proves a span EXISTS, never
419
+ that it means the work is done. Phase 2g (upstream friction) gained a
420
+ **three-arm actionability gate** (act:ea23b3a5, Phase 2f's
421
+ verification-gate shape): a named route from the closed vocabulary
422
+ `cc-feedback | project-tracker | config-change` with a non-empty detail
423
+ files `upstream-friction`; a durable no-route platform constraint files
424
+ `knowledge-extraction` (4 of the 5 friction items the operator KEPT
425
+ exited `captured-to-memory`, so a two-arm gate would have discarded the
426
+ useful half of the lens); anything else files nothing and is COUNTED.
427
+ Contract in `watchtower-contracts.md` ("Upstream-Friction
428
+ Actionability"). coverage-warning is now a
388
429
  STANDING-DEBT aggregation (act:e888dd63): ONE pending item per
389
430
  (project, debt-class — `COVERAGE_DEBT_CLASS`) whose evidence APPENDS
390
431
  per session (session-id union, so a reprocess replay can't inflate
@@ -394,7 +435,17 @@ consumes the registry stays in cli.js).
394
435
  at 5, urgent at 15); resolving closes the accumulated view and a
395
436
  persisting debt refiles FRESH next session. The `## Last Session`
396
437
  block names its authoritative per-session record (`appendRecordPointer`
397
- → `<slug>/sessions/<date>-<sessionId>.md`; act:8c076580).
438
+ → `<slug>/sessions/<date>-<sessionId>.md`; act:8c076580). Phase 2d
439
+ classifies non-durable facts AT extraction (act:471dd701 N2 +
440
+ act:a69c21f8): `derivable` (recomputable from a live source → home
441
+ `derivation`, the recompute instruction visible) and its twin
442
+ `perishable` (true-at-a-moment, will become false, NOT recomputable —
443
+ `perishes_when` names what falsifies it → home `session-record`);
444
+ both ALWAYS FILED to acknowledge/dismiss, never memory, never
445
+ dropped; derivable wins when both apply; the type/home vocabulary is
446
+ the exported `KNOWLEDGE_EXTRACTION_TYPES`/`_HOMES` consts
447
+ (watchtower-queue) with createItem boundary checks + a prompt-parity
448
+ test (`__tests__/extraction-classification.test.mjs`).
398
449
  - `watchtower-queue.mjs` — inbox queue CRUD library (atomic temp+rename,
399
450
  no-index convention); also home of the staff-QA recipient gate
400
451
  (2026-06-11): every terminal exit on a `qa-handoff` item is structural —
@@ -405,7 +456,19 @@ consumes the registry stays in cli.js).
405
456
  exempts the category). `emitPatternPromotion` files one deduplicated
406
457
  pattern-promotion inbox item at >=3 sweep instances of a failure class.
407
458
  `listItems()` exposes filtered item listing (project/category/status/since)
408
- for the Ring 3 dedup corpora. Terminal exits on any dispatched-category
459
+ for the Ring 3 dedup corpora. **Category expiry is single-sourced here**
460
+ (act:ea23b3a5): `expiryDaysFor` / `categoryNeverExpires` /
461
+ `CATEGORY_EXPIRE_DAYS` / `DEFAULT_EXPIRE_DAYS`, with
462
+ `categoryNeverExpires` sitting beside `expireItem`'s throw because the
463
+ qa-handoff carve-out is STRUCTURAL (returning anything but null for it
464
+ crashes the caller's loop). There are TWO expiry engines — Ring 2's
465
+ `escalateQueueItems` cron and `runExpiry`, which `/inbox` and
466
+ `/briefing` call — and each previously hardcoded 30 days with its own
467
+ inlined carve-out, so a per-category policy set in one was silently
468
+ violated by whichever reached the item first. Both now delegate.
469
+ completion-review is the first override at 14 days; note that a
470
+ shortened expiry can open a refile loop when it meets its filing site's
471
+ dedup window (see the "Category Expiry" contract). Terminal exits on any dispatched-category
409
472
  item (`qa-handoff`, `routine` — the DISPATCHED_CATEGORIES set; resolve /
410
473
  typed dismiss / supersede / expire) also clear the matching mux
411
474
  dispatch-queue descriptor — queued AND in-flight, across all desk dirs —
@@ -491,6 +554,23 @@ consumes the registry stays in cli.js).
491
554
  descriptor through `mux qa dispatch` — the SAME hardened desk-dispatch path
492
555
  as qa-handoffs, never a fork. Pending-item dedup blocks refiring; stale
493
556
  pending firings (default 24h) are superseded + redispatched.
557
+ `sweepExpiredMoments` (act:ea23b3a5) adds **end-of-day moment expiry for
558
+ `time-of-day` triggers**: Ring 1's tick supersedes a pending item once
559
+ the calendar day it was FILED on has ended, instead of waiting 24h for
560
+ the next firing. End-of-day rather than a fixed window is a measured
561
+ choice — the portfolio's one time-of-day routine had 10 real pickups in
562
+ six weeks, 8 within four hours and 2 after (4.4h, 8.6h), so a 4h window
563
+ would have destroyed two genuine pickups to clear one stale item.
564
+ Scoped to Ring 1's PORTFOLIO tick (Ring 3's single-project
565
+ session-close call would otherwise sweep every other project's items,
566
+ once per replayed session during a reprocess drain), classified from
567
+ `item.evidence.trigger` rather than config (so a de-registered routine
568
+ still classifies instead of rotting), and an unparseable `filed_at`
569
+ ABSTAINS loudly rather than sweeping (the `stale_after_hours` path
570
+ treats the same input as stale — two mechanisms disagreeing silently is
571
+ worse than one abstaining out loud). For time-of-day the moment sweep
572
+ now pre-empts the `stale_after_hours` supersede; interval /
573
+ path-nonempty / session-close have no moment and are untouched.
494
574
  `buildPickupPrompt` (the dispatch pickup-prompt wording, including the
495
575
  resolve-on-run that clears the badge) is exported and reused by the
496
576
  build-context missed-routine re-delivery (act:4b4fa7d9), so the wording is
@@ -911,7 +991,17 @@ summary attention aggregates portfolio-wide pib errors into ONE line (almost
911
991
  always a single system-level fault; Ring 2's `openPibDb` logs instead of
912
992
  silently nulling); the memory-integrity check delegates to
913
993
  `checkMemoryReachability` (watchtower-lib) — reachability, never
914
- filename-substring (act:49cb1c27). Ring 1's worktree-unmerged +
994
+ filename-substring (act:49cb1c27). Standing Issues also renders the
995
+ **cabinet-roster ambient lines** (act:ea23b3a5, `readRosterMetrics` +
996
+ `renderRosterEntries`): dormant-skill and stale-briefing counts stopped
997
+ being `advisor-finding` queue items and became a Ring 2 measurement that
998
+ Ring 1 renders — the same relationship as the recall canary (the
999
+ measurement is Ring 2's, the render is Ring 1's, because Ring 1 owns the
1000
+ per-project state file). Rendered ON CHANGE only and carrying skill
1001
+ NAMES, because ambient state has no dismiss verb and a number that never
1002
+ moves is furniture by the third read; the count itself had to be fixed
1003
+ first, since `ROSTER_DORMANT_LIST_CAP` saturated it and every project
1004
+ ever measured reported exactly "12 dead". Ring 1's worktree-unmerged +
915
1005
  branch-diverged detectors flag on CONTENT, not ahead-count (act:a152cf6c): a
916
1006
  squash-merge leaves a branch a non-ancestor of main with phantom "ahead"
917
1007
  commits but an IDENTICAL tree, so the old ancestry/`aheadCount` gates flagged
@@ -959,11 +1049,60 @@ knowledge-extraction drafts with freshness (cited `act:` fids positively
959
1049
  CLOSED in the item's own project pib-db — parameter-bound lookups via
960
1050
  `openPibDb`, now `timeout: 5000` for lock parity with Ring 3) and fold
961
1051
  proposals (the watchtower-queue recipe), demoting them out of batch
962
- sign-off, never dismissing; unreadable dbs and out-of-config drafts are
963
- counted LOUDLY (`skipped_projects`/`unscanned_items`) and every run
964
- writes the positive-confirmation sidecar
1052
+ sign-off, never dismissing; act:09184ad7 added a SECOND fold corpus —
1053
+ pending pattern-promotion items, grouped per `evidence.target_member`
1054
+ (cross-member similarity is never a duplicate), so near-duplicate
1055
+ pattern variants arrive pre-annotated for the next consolidation pass
1056
+ (`pattern_items_scanned`/`pattern_fold_annotated`/
1057
+ `unscanned_pattern_items` counters); act:ea23b3a5 added a THIRD corpus —
1058
+ pending `raised-unhandled` loose ends, grouped per project
1059
+ (`loose_end_items_scanned`/`loose_end_fold_annotated`). That third corpus
1060
+ is where the spec's "aggregate recurring loose ends into ONE item"
1061
+ landed after measurement refused the merge: `proposeFolds` scores with
1062
+ `overlapCoefficient` (act:421a8ab2 retired `unigramJaccard`), whose
1063
+ `min()` denominator lets two three-token titles sharing ONE word clear
1064
+ `FOLD_SIMILARITY_THRESHOLD = 0.22` — four false folds in fifteen pairs
1065
+ against the six live titles — and the five timelog specimens were
1066
+ resolved `deferred` (real work the operator postponed, across two
1067
+ projects and three date ranges), so merging would have buried which gaps
1068
+ remain. The threshold was calibrated for ADVISORY retrieval, not a
1069
+ destructive merge: **a lexical matcher may propose, never silently
1070
+ suppress.** Unreadable dbs and out-of-config
1071
+ items are counted LOUDLY (`skipped_projects`/`unscanned_items`) and
1072
+ every run writes the positive-confirmation sidecar
965
1073
  `state/draft-annotations-health.json`, so "no annotations" is
966
- distinguishable from "never ran"), Ring 3 (session close, transcript
1074
+ distinguishable from "never ran". The slow tier also owns the
1075
+ **memory-budget standing debt + its retraction** (act:ea23b3a5):
1076
+ `surfacePersistentViolations` files ONE accumulating `watchtower-health`
1077
+ item per project instead of a fresh one every 7 days — keyed by CALENDAR
1078
+ DAY (a per-run key would count ~48 cron ticks a day and make the replay
1079
+ guard dead code), appended via `annotateItemEvidence` so `filed_at` is
1080
+ never bumped, carrying `session_count` because that is the spelling
1081
+ `itemSessionWeight` already reads — and `retractClearedMemoryBudgetItems`
1082
+ retracts on the SAME pass over the `passProjects` `runMemoryHygiene`
1083
+ already computes. Deliberately not in Ring 1: a second memory-health
1084
+ producer is forbidden by name in `watchtower-contracts.md`, and at a
1085
+ 30-minute throttle against ring2's 30-minute cadence it would buy zero
1086
+ freshness. Both selectors match the LEGACY `{violations, streak}` title
1087
+ shape as well as the new `debt_class`, because every item on disk when
1088
+ this shipped carried the former. The cabinet-roster review now files only
1089
+ `uncovered-tech` (a proposed cabinet seat is a decision); `dormant-skill`
1090
+ and `stale-briefing` became ambient metrics in `state/roster-review.json`
1091
+ (`parseDormantSkills` reports TRUE counts — the cap bounds the NAME list
1092
+ only) and `retireRosterFindingItems` supersedes any of the two retired
1093
+ kinds still pending, since the filing site was the only code that ever
1094
+ touched them. The slow tier's `scanAuditPatterns`
1095
+ (the pattern-promotion filing site) now runs the act:09184ad7
1096
+ emission-quality SHAPE GATE — `patternHasRequiredShape` requires the
1097
+ `**Evidence:**` + `**Gap:**` blocks Phase 2e's prompt mandates, rejecting
1098
+ document echoes (wave ledgers, dependency graphs, recipes, transcript
1099
+ fragments — 17/144 items in the 2026-07-26 consolidation) BEFORE the
1100
+ routing Claude call; rejections are counted loudly
1101
+ (`rejected_malformed_total`/`last_scan_rejected` in
1102
+ `state/pattern-detection.json`) and their hashes marked processed, never
1103
+ retried. Calibrated against the consolidation corpus: 120/120 legitimate
1104
+ bodies pass, 17/17 fragments fail; hermetic suite in
1105
+ `__tests__/ring2-pattern-gate.test.mjs`), Ring 3 (session close, transcript
967
1106
  processing; fires session-close routines as Phase 2j2; runs the standing
968
1107
  session advisors as Phase 2m — see
969
1108
  below; runs three session-close extraction lenses (act:4ff2cfb3) — Phase 2n
@@ -971,7 +1110,12 @@ raised-but-unhandled (loose ends the session raised but neither did nor filed
971
1110
  → inbox category `raised-unhandled`), Phase 2o skill-candidate (a manual
972
1111
  procedure repeated by hand → `skill-candidate` inbox item), Phase 2p
973
1112
  checklist-catch (a surfaced change-impact check that caught a real bug →
974
- `checklist-stats.json`); its Phase 2h feedback "flush" was deleted 2026-06-12
1113
+ `checklist-stats.json`); its Phase 2e quality-pattern prompt is the exported
1114
+ `QUALITY_PATTERN_SYSTEM_PROMPT` carrying the act:09184ad7 generation-side
1115
+ exclusions — no-gap positives, transcript-artifact observations, document
1116
+ echoes are never patterns — the prompt half of ring2's mechanical shape
1117
+ gate, guarded by `__tests__/ring2-pattern-gate.test.mjs`; its Phase 2h
1118
+ feedback "flush" was deleted 2026-06-12
975
1119
  — it marked without delivering), Ring 4 (periodic truth reconciliation,
976
1120
  act:36dae795 — stage-1 BUILT, weekly cadence; cron/launchd registration is a
977
1121
  deferred `/watchtower install` follow-up). Ring 4 mechanically compares
@@ -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,
@@ -523,16 +598,133 @@ branch-diverged item for it — the branch stays visible on the attention
523
598
  line / git-attention sidecar / summary / Standing Issues (the exclusion
524
599
  gates filing ONLY), and the consumer overrides the list in config.
525
600
 
526
- **Standing-debt aggregation (coverage-warning first):** a detector
527
- whose finding is a STANDING condition (recurs while a debt persists)
528
- files ONE item per (project, debt-class) and APPENDS evidence to the
529
- pending item (path union + per-path counts + session count), carrying
530
- `evidence.first_seen` (oldest evidence date) so aging reads the debt's
531
- true age, with an urgency escalation at a session-count threshold.
532
- Resolving the item closes the accumulated view; if the debt persists,
533
- the next tick files FRESH — post-disposition recurrence is a new
534
- signal, not a refile loop (act:e888dd63; 49 identical coverage-warning
535
- 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.
536
728
 
537
729
  **Known limitation (named, not hidden):** reconcilers run per
538
730
  configured project — items whose project was deleted or de-registered
@@ -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."