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.
- package/lib/cli.js +2 -2
- package/package.json +1 -1
- package/templates/CLAUDE.md +164 -20
- package/templates/cabinet/watchtower-contracts.md +202 -10
- package/templates/mux/bin/mux +5 -0
- package/templates/mux/config/mux-server.py +7 -5
- package/templates/scripts/__tests__/category-expiry.test.mjs +130 -0
- package/templates/scripts/__tests__/detector-registry.test.mjs +41 -7
- package/templates/scripts/__tests__/extraction-classification.test.mjs +87 -0
- package/templates/scripts/__tests__/friction-actionability.test.mjs +180 -0
- package/templates/scripts/__tests__/memory-budget-debt.test.mjs +264 -0
- package/templates/scripts/__tests__/ring2-pattern-gate.test.mjs +315 -0
- package/templates/scripts/__tests__/ring2-roster-review.test.mjs +75 -16
- package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +167 -105
- package/templates/scripts/__tests__/roster-ambient-render.test.mjs +132 -0
- package/templates/scripts/__tests__/routine-dispatch.test.mjs +29 -1
- package/templates/scripts/__tests__/routine-moment-expiry.test.mjs +192 -0
- package/templates/scripts/skill-validator.sh +7 -4
- package/templates/scripts/watchtower-lib.mjs +58 -5
- package/templates/scripts/watchtower-queue.mjs +54 -7
- package/templates/scripts/watchtower-ring1.mjs +74 -0
- package/templates/scripts/watchtower-ring2.mjs +442 -81
- package/templates/scripts/watchtower-ring3-close.mjs +271 -82
- package/templates/scripts/watchtower-routines.mjs +107 -1
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +35 -2
- package/templates/skills/cabinet-anti-confirmation/SKILL.md +24 -0
- package/templates/skills/cabinet-cc-health/SKILL.md +22 -0
- package/templates/skills/cabinet-data-integrity/SKILL.md +19 -0
- package/templates/skills/cabinet-debugger/SKILL.md +36 -0
- package/templates/skills/cabinet-deployment/SKILL.md +22 -0
- package/templates/skills/cabinet-elegance/SKILL.md +12 -0
- package/templates/skills/cabinet-goal-alignment/SKILL.md +11 -0
- package/templates/skills/cabinet-historian/SKILL.md +33 -0
- package/templates/skills/cabinet-organized-mind/SKILL.md +11 -0
- package/templates/skills/cabinet-process-therapist/SKILL.md +132 -6
- package/templates/skills/cabinet-qa/SKILL.md +63 -0
- package/templates/skills/cabinet-roster-check/SKILL.md +12 -0
- 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.
|
|
1836
|
-
console.log(' 3.
|
|
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
package/templates/CLAUDE.md
CHANGED
|
@@ -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
|
|
323
|
-
source scan) and fails on an unregistered
|
|
324
|
-
category value inside a filing owner
|
|
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`
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
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.
|
|
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).
|
|
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;
|
|
963
|
-
|
|
964
|
-
|
|
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"
|
|
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
|
|
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
|
|
527
|
-
whose finding is a STANDING condition (recurs while a debt
|
|
528
|
-
files ONE item per (project, debt-class) and APPENDS evidence
|
|
529
|
-
pending item
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
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
|
package/templates/mux/bin/mux
CHANGED
|
@@ -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, _,
|
|
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, _,
|
|
199
|
-
|
|
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
|
|
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."
|