create-claude-cabinet 0.48.0 → 0.49.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 (48) hide show
  1. package/lib/CLAUDE.md +22 -0
  2. package/lib/cli.js +100 -5
  3. package/lib/metadata.js +20 -0
  4. package/package.json +1 -1
  5. package/templates/CLAUDE.md +212 -12
  6. package/templates/cabinet/watchtower-contracts.md +85 -0
  7. package/templates/cabinet/worktree-invocation-contract.md +35 -0
  8. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +53 -9
  9. package/templates/mux/__tests__/mux-fail-loud.fixture.sh +4 -5
  10. package/templates/mux/__tests__/worktree-lifecycle.fixture.sh +214 -0
  11. package/templates/mux/__tests__/worktree-lifecycle.test.mjs +44 -0
  12. package/templates/mux/__tests__/worktree-status-clean.fixture.sh +245 -0
  13. package/templates/mux/__tests__/worktree-status-clean.test.mjs +89 -0
  14. package/templates/mux/bin/mux +44 -28
  15. package/templates/mux/config/worktree-cleanup.sh +57 -12
  16. package/templates/mux/config/worktree-dirty-check.sh +72 -2
  17. package/templates/mux/config/worktree-session-health.sh +401 -17
  18. package/templates/scripts/__tests__/assessment-recalibration.test.mjs +387 -0
  19. package/templates/scripts/__tests__/batch-disposition.test.mjs +19 -0
  20. package/templates/scripts/__tests__/branch-diverged-reconcile.test.mjs +374 -0
  21. package/templates/scripts/__tests__/completion-review-reconcile.test.mjs +173 -0
  22. package/templates/scripts/__tests__/cross-ring-reader.test.mjs +12 -4
  23. package/templates/scripts/__tests__/detector-registry.test.mjs +152 -0
  24. package/templates/scripts/__tests__/draft-surfacing.test.mjs +433 -0
  25. package/templates/scripts/__tests__/generated-state-classification.test.mjs +228 -0
  26. package/templates/scripts/__tests__/mirror-parity.test.mjs +54 -0
  27. package/templates/scripts/__tests__/resolve-project-slug.test.mjs +186 -0
  28. package/templates/scripts/__tests__/ring3-attribution.test.mjs +267 -0
  29. package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +176 -0
  30. package/templates/scripts/__tests__/ring3-coverage-debt.test.mjs +377 -0
  31. package/templates/scripts/__tests__/ring3-last-session-pointer.test.mjs +80 -0
  32. package/templates/scripts/audit-coherence-check.mjs +6 -2
  33. package/templates/scripts/load-triage-history.js +23 -2
  34. package/templates/scripts/merge-findings.js +9 -2
  35. package/templates/scripts/pib-db-lib.mjs +55 -7
  36. package/templates/scripts/watchtower-cross-ring-reader.mjs +20 -3
  37. package/templates/scripts/watchtower-inbox-assessment.mjs +186 -35
  38. package/templates/scripts/watchtower-lib.mjs +268 -5
  39. package/templates/scripts/watchtower-queue.mjs +197 -2
  40. package/templates/scripts/watchtower-ring1.mjs +490 -69
  41. package/templates/scripts/watchtower-ring2.mjs +175 -3
  42. package/templates/scripts/watchtower-ring3-close.mjs +519 -69
  43. package/templates/skills/audit/SKILL.md +6 -3
  44. package/templates/skills/cabinet-anthropic-insider/SKILL.md +29 -8
  45. package/templates/skills/cabinet-process-therapist/SKILL.md +45 -3
  46. package/templates/skills/inbox/SKILL.md +29 -1
  47. package/templates/skills/session-handoff/SKILL.md +6 -8
  48. package/templates/watchtower/config.json.template +2 -1
package/lib/CLAUDE.md CHANGED
@@ -33,6 +33,16 @@ No build step; CommonJS.
33
33
  it, `expandFrozenClosure`, and the hoisted top-level `manifestPath`
34
34
  are exported + unit-tested. `generateAgentWrappers()` (see root
35
35
  § Generated artifacts) also lives here.
36
+ Also home of `seedAdvisoriesGitignore()` (act:c008862c,
37
+ grp:wt-noise-immunity), run in the install body: on FRESH installs
38
+ only (no prior `.ccrc.json`) it seeds a `.gitignore` entry for
39
+ `.claude/cabinet/advisories-state.json` (machine-written runtime
40
+ state — tracked, it churns every commit and reads as authored work in
41
+ mux worktrees) unless some rule already ignores it (`git
42
+ check-ignore`); existing installs are left untouched — untracking a
43
+ live consumer repo is operator-gated — but a TRACKED copy is reported
44
+ loudly so the consumer walk surfaces it. No-git and dry-run aware,
45
+ injectable exec, exported for tests.
36
46
  - `lib/modules.js` — the `MODULES` manifest: every module → the template
37
47
  paths it copies into `.claude/` (architecture-0005; extracted from
38
48
  `cli.js` in act:e82baead). `cli.js` requires + re-exports it for
@@ -90,6 +100,18 @@ No build step; CommonJS.
90
100
  (act:c1013967): the `.ccrc.json` `frozenModules` accessors for the
91
101
  freeze-module mechanism (a non-array reads as empty — shape-drift
92
102
  defensive; `create()` preserves the field via `...existing`).
103
+ Also exposes `readMcpOptOut` (act:b770c323): the `.ccrc.json`
104
+ `mcpOptOut` accessor — the CC-shipped MCP server keys this consumer
105
+ declines (flow's `no-pib-db.md` is the live specimen; its validator
106
+ requires the pib-db registration absent while the engagement→
107
+ work-tracking closure keeps shipping the files). Same defensive shape
108
+ as `readFrozenModules`; the key survives reinstalls via `create()`'s
109
+ `...existing` spread, so a consumer sets it once by hand. Honored by
110
+ `mergeMcpServers` in `lib/cli.js` (opted-out keys are never added and
111
+ are REMOVED if present — self-heal toward declared intent; removal is
112
+ scoped to keys CC ships, so hand-added servers are never touched;
113
+ nothing-to-contribute installs don't rewrite `.mcp.json`). Tests in
114
+ `test/mcp-opt-out/`.
93
115
  `normalize()` guards shape drift (non-object top level reads as
94
116
  'no metadata').
95
117
  - `lib/settings-merge.js` — merges CC hooks into `.claude/settings.json`.
package/lib/cli.js CHANGED
@@ -5,7 +5,7 @@ const os = require('os');
5
5
  const crypto = require('crypto');
6
6
  const { copyTemplates, recordSkip, isProjectOwnedSeed, classifyFileOwnership } = require('./copy');
7
7
  const { mergeSettings, healUserSettings, mergeWatchtowerHooks, mergeMuxHooks, mergeBashCompressHooks, mergeCcDevHooks, mergeSkillOverrides } = require('./settings-merge');
8
- const { create: createMetadata, read: readMetadata, readFrozenModules, setFrozenModules } = require('./metadata');
8
+ const { create: createMetadata, read: readMetadata, readFrozenModules, setFrozenModules, readMcpOptOut } = require('./metadata');
9
9
  const { setupDb, shouldSkipInit, readBundledSchemaVersion } = require('./db-setup');
10
10
  const { setupVerifyRuntime } = require('./verify-setup');
11
11
  const { setupSiteAuditRuntime } = require('./site-audit-setup');
@@ -551,6 +551,35 @@ function computeRemovals({ existingManifest, allManifest, selectedModules, templ
551
551
  return { toRemove, kept };
552
552
  }
553
553
 
554
+ // Merge CC-shipped MCP server entries into a consumer's .mcp.json mcpServers
555
+ // object, honoring the .ccrc.json `mcpOptOut` list (act:b770c323). Pure and
556
+ // mutating-in-place on `existing` (the caller serializes the parent object):
557
+ // - incoming keys NOT opted out are set/overwritten (the prior Object.assign
558
+ // behavior) and reported in `added`;
559
+ // - incoming keys the consumer opted out are never added (`skipped`);
560
+ // - an opted-out key already present in `existing` is DELETED (`removed`) —
561
+ // self-heal toward the declared intent, but ONLY for keys CC itself ships
562
+ // (in `incoming`), so a consumer's hand-added servers are never touched.
563
+ function mergeMcpServers(existing, incoming = {}, optOut = []) {
564
+ const optedOut = new Set(optOut);
565
+ const added = [];
566
+ const removed = [];
567
+ const skipped = [];
568
+ for (const [key, value] of Object.entries(incoming)) {
569
+ if (optedOut.has(key)) {
570
+ skipped.push(key);
571
+ if (Object.prototype.hasOwnProperty.call(existing, key)) {
572
+ delete existing[key];
573
+ removed.push(key);
574
+ }
575
+ continue;
576
+ }
577
+ existing[key] = value;
578
+ added.push(key);
579
+ }
580
+ return { added, removed, skipped };
581
+ }
582
+
554
583
  // Signals that a directory contains a real project (not just empty)
555
584
  const PROJECT_SIGNALS = [
556
585
  'package.json', 'Cargo.toml', 'requirements.txt', 'pyproject.toml',
@@ -690,6 +719,42 @@ function printHelp() {
690
719
  `);
691
720
  }
692
721
 
722
+ // --- advisories-state gitignore seeding (act:c008862c) ---
723
+ // .claude/cabinet/advisories-state.json is machine-written runtime state
724
+ // (the advisory pass rewrites it), so tracked it churns every commit and
725
+ // reads as authored work in mux worktrees. FRESH installs (no prior
726
+ // .ccrc.json) seed a .gitignore entry so it never enters git; existing
727
+ // installs are left untouched — untracking a live consumer repo is
728
+ // operator-gated — but a TRACKED copy is reported so the consumer walk
729
+ // surfaces it. Pure and injectable for tests.
730
+ const ADVISORIES_STATE_PATH = '.claude/cabinet/advisories-state.json';
731
+ function seedAdvisoriesGitignore(projectDir, { fresh, dryRun, exec } = {}) {
732
+ const run = exec || ((cmd) => require('child_process').execSync(cmd, { cwd: projectDir, stdio: 'pipe' }));
733
+ try {
734
+ run(`git ls-files --error-unmatch ${ADVISORIES_STATE_PATH}`);
735
+ return { status: 'tracked-reported' };
736
+ } catch { /* not tracked — fall through */ }
737
+ if (!fresh) return { status: 'skipped-existing-install' };
738
+ try {
739
+ run('git rev-parse --git-dir');
740
+ } catch {
741
+ return { status: 'skipped-no-git' };
742
+ }
743
+ try {
744
+ // Already ignored by ANY rule (exact line or a covering glob like this
745
+ // repo's `.claude/cabinet/*`) — nothing to add.
746
+ run(`git check-ignore -q ${ADVISORIES_STATE_PATH}`);
747
+ return { status: 'already-present' };
748
+ } catch { /* not ignored — seed below */ }
749
+ const giPath = path.join(projectDir, '.gitignore');
750
+ let content = '';
751
+ try { content = fs.readFileSync(giPath, 'utf8'); } catch { /* no .gitignore yet */ }
752
+ if (dryRun) return { status: 'would-seed' };
753
+ const nl = content && !content.endsWith('\n') ? '\n' : '';
754
+ fs.appendFileSync(giPath, `${nl}# machine-written advisory-dismissal state (CC)\n${ADVISORIES_STATE_PATH}\n`);
755
+ return { status: 'seeded' };
756
+ }
757
+
693
758
  async function run() {
694
759
  const flags = parseArgs(process.argv);
695
760
 
@@ -1406,9 +1471,22 @@ async function run() {
1406
1471
  existing = {};
1407
1472
  }
1408
1473
  if (!existing.mcpServers) existing.mcpServers = {};
1409
- Object.assign(existing.mcpServers, mcpConfig.mcpServers);
1410
- fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
1411
- console.log(` 🔌 Merged pib-db MCP server into .mcp.json`);
1474
+ const optOut = readMcpOptOut(projectDir);
1475
+ const { added, removed, skipped } = mergeMcpServers(existing.mcpServers, mcpConfig.mcpServers, optOut);
1476
+ // Only write when this merge actually contributed or healed something —
1477
+ // an all-opted-out install must not reformat a consumer's .mcp.json.
1478
+ if (added.length > 0 || removed.length > 0) {
1479
+ fs.writeFileSync(mcpJsonPath, JSON.stringify(existing, null, 2) + '\n');
1480
+ }
1481
+ if (added.length > 0) {
1482
+ console.log(` 🔌 Merged ${added.join(', ')} MCP server into .mcp.json`);
1483
+ }
1484
+ if (skipped.length > 0) {
1485
+ console.log(` ⏭ MCP registration declined by .ccrc.json mcpOptOut: ${skipped.join(', ')}`);
1486
+ }
1487
+ if (removed.length > 0) {
1488
+ console.log(` 🧹 Removed opted-out MCP entr${removed.length === 1 ? 'y' : 'ies'} from .mcp.json: ${removed.join(', ')}`);
1489
+ }
1412
1490
  }
1413
1491
  } catch (err) {
1414
1492
  console.log(` ⚠ MCP config merge failed: ${err.message}`);
@@ -1632,6 +1710,23 @@ async function run() {
1632
1710
  }
1633
1711
  }
1634
1712
 
1713
+ // --- Seed advisories-state gitignore (fresh installs only; act:c008862c) ---
1714
+ try {
1715
+ const seedResult = seedAdvisoriesGitignore(projectDir, {
1716
+ fresh: dirState !== 'existing-install',
1717
+ dryRun: !!flags.dryRun,
1718
+ });
1719
+ if (seedResult.status === 'seeded') {
1720
+ console.log(' 🙈 Gitignored .claude/cabinet/advisories-state.json (machine-written state)');
1721
+ } else if (seedResult.status === 'would-seed') {
1722
+ console.log(' [dry run] Would gitignore .claude/cabinet/advisories-state.json');
1723
+ } else if (seedResult.status === 'tracked-reported') {
1724
+ console.log(' ℹ .claude/cabinet/advisories-state.json is TRACKED in this repo — left untouched. Untracking (git rm --cached) is an operator call; tracked, it churns every commit.');
1725
+ }
1726
+ } catch (err) {
1727
+ console.log(` ⚠ advisories-state gitignore seeding skipped: ${err.message}`);
1728
+ }
1729
+
1635
1730
  // --- Write metadata ---
1636
1731
  if (!flags.dryRun) {
1637
1732
  createMetadata(projectDir, {
@@ -1725,4 +1820,4 @@ async function run() {
1725
1820
  console.log('');
1726
1821
  }
1727
1822
 
1728
- module.exports = { run, MODULES, generateAgentWrappers, generateSkillIndex, expandFrozenClosure, computeRemovals, manifestPath };
1823
+ module.exports = { run, MODULES, generateAgentWrappers, generateSkillIndex, expandFrozenClosure, computeRemovals, mergeMcpServers, manifestPath, seedAdvisoriesGitignore };
package/lib/metadata.js CHANGED
@@ -111,6 +111,25 @@ function setFrozenModules(projectDir, mods) {
111
111
  return normalized;
112
112
  }
113
113
 
114
+ /**
115
+ * Read the project's MCP opt-out list from `.ccrc.json` (`mcpOptOut`) — the
116
+ * CC-shipped MCP server keys this consumer declines (act:b770c323; the live
117
+ * specimen is flow's no-pib-db.md: engagement→work-tracking closure ships the
118
+ * pib-db files, but the consumer's own validator requires the MCP registration
119
+ * absent). Honored by the `.mcp.json` merge in lib/cli.js: opted-out keys are
120
+ * never added and, when present from a prior install, are removed (self-heal
121
+ * toward the declared intent). Same defensive shape as `readFrozenModules`:
122
+ * non-array reads as "no opt-outs"; a malformed `.ccrc.json` still throws in
123
+ * `read()` and halts the install. The key survives reinstalls via `create()`'s
124
+ * `...existing` spread — no threading required.
125
+ */
126
+ function readMcpOptOut(projectDir) {
127
+ const m = read(projectDir);
128
+ const raw = m && m.mcpOptOut;
129
+ if (!Array.isArray(raw)) return [];
130
+ return [...new Set(raw.filter((x) => typeof x === 'string'))];
131
+ }
132
+
114
133
  module.exports = {
115
134
  read,
116
135
  write,
@@ -120,4 +139,5 @@ module.exports = {
120
139
  METADATA_FILE,
121
140
  readFrozenModules,
122
141
  setFrozenModules,
142
+ readMcpOptOut,
123
143
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-cabinet",
3
- "version": "0.48.0",
3
+ "version": "0.49.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"
@@ -15,15 +15,58 @@ consumes the registry stays in cli.js).
15
15
  ## mux config (`templates/mux/config/`)
16
16
 
17
17
  - `worktree-session-health.sh` — self-healing worktree health check (4
18
- checks, auto-fix, cache + tmux indicator).
18
+ checks, auto-fix, cache + tmux indicator); the single home for the
19
+ worktree LANE LIFECYCLE (act:dc80380c): PROVISION (a per-project
20
+ `.mux-worktree-provision` file declares gitignored env files copied
21
+ main → worktree at creation and freshness-checked on later health
22
+ runs — a locally-modified worktree copy is never overwritten; reports
23
+ name paths/hashes/mtimes only, never file CONTENTS), `--preflight`
24
+ (pre-spawn source check: every declared provision file exists in MAIN
25
+ and every declared time-boxed `check:` command passes; exit 1 gates
26
+ parallel lane spawns — surfaced as `mux worktree preflight`), and
27
+ `--reap` (removal-time docker container reap: exact
28
+ compose-project-label match on the worktree dir name, containers only
29
+ NEVER volumes, always exits 0 — a failed reap is loud but never blocks
30
+ removal; every removal path delegates here). Also owns the worktree
31
+ MERGE-OR-LOSE noise immunity (act:c008862c): `rebuild_worktree_excludes`
32
+ makes per-worktree excludes LIVE via `extensions.worktreeConfig` + a
33
+ worktree-scoped `core.excludesFile` (git never consults a linked
34
+ worktree's `worktrees/<name>/info/exclude` — the old rules were
35
+ silently dead), copying the user's own excludesFile patterns in FIRST
36
+ so the authored-record negations win, then hiding untracked identity
37
+ symlinks (root-anchored; only while actually a symlink into main) and
38
+ machine-written generated state (names kept in step with
39
+ `GENERATED_STATE_PATTERNS` in watchtower-lib and `GENERATED_STATE_RE`
40
+ in worktree-dirty-check.sh — the merge tail diffs the lists);
41
+ `hide_identity_symlink_typechange` sets git's skip-worktree bit on
42
+ TRACKED `.mcp.json`/`.claudeignore` mux symlinks (typechange churn is
43
+ exclude-immune) — granted only while the symlink resolves into main,
44
+ cleared whenever a real file returns.
19
45
  - `worktree-health-popup.sh` — tmux popup for health details (triggered by
20
46
  double-clicking a `·wt` tab).
21
47
  - `worktree-dirty-check.sh` — single shared worktree dirty-detection
22
48
  (fail-DIRTY on any error, `--untracked-files=all`; the authored-record
23
49
  set is derived from the worktree's index — any top-level `.claude/`
24
50
  entry holding tracked files — with the static plans/methodology pair as
25
- fallback floor); delegated to by both `bin/mux` and the pane-exited
26
- `worktree-cleanup.sh`; sandboxed fixture suite in `templates/mux/__tests__/`,
51
+ fallback floor). act:c008862c taught it the noise/work boundary:
52
+ untracked identity symlinks (`?? .mcp.json`/`.claudeignore`/`pib.db`)
53
+ are churn ONLY while the path is a live symlink — a real file there is
54
+ real work — and a skip-worktree'd path that no longer holds a symlink
55
+ counts DIRTY (a real edit may hide behind the bit; a clean-verdict
56
+ deletion must never race the health heal); untracked generated state
57
+ (`GENERATED_STATE_RE`: advisories-state.json, checklist-stats.json,
58
+ `.claude/verification/`, `e2e/.verify-progress.jsonl` — the shell twin
59
+ of watchtower-lib's `GENERATED_STATE_PATTERNS`, list-diffed at the
60
+ program merge tail) is subtracted from BOTH counting passes, so a
61
+ fully-merged worktree no longer reads "MERGE OR LOSE" over runtime
62
+ residue while a tracked-and-modified copy still counts dirty.
63
+ Delegated to by both `bin/mux` and the pane-exited
64
+ `worktree-cleanup.sh` — whose dirty-close inbox filing mirrors ring1's
65
+ earned-urgency register split (`urgent` only with unmerged commits,
66
+ `normal` for uncommitted-only, `evidence.unmerged` carried) and whose
67
+ clean-close removal reaps lane containers via `--reap`, routing an
68
+ incomplete reap to the desk's sticky notes, never silence;
69
+ sandboxed fixture suite in `templates/mux/__tests__/`,
27
70
  which also holds the `mux-fail-loud` suite (bin/mux fail-loud worktree
28
71
  creation + resume reuse, run against a private tmux server), the
29
72
  `claude-carveout` suite (tracked `.claude/` = authored: never frozen with
@@ -161,7 +204,22 @@ consumes the registry stays in cli.js).
161
204
  - `watchtower-contracts.md` — shared contracts for all watchtower components
162
205
  (atomic writes, no-index, session-ID join, schema versioning, project-state
163
206
  section ownership — Ring 3 owns `## Last Session` once authored — and
164
- deferred schemas).
207
+ deferred schemas). Also home of "Detector Symmetry" (act:4d11fd53,
208
+ grp:wt-noise-immunity): every ring-filed inbox category declares its
209
+ mechanical retraction condition + filing exclusions, or an explicit
210
+ exemption — encoded in `INBOX_DETECTOR_REGISTRY` (watchtower-lib) and
211
+ enforced by a structural filing-site test; machine retractions resolve
212
+ through `autoReconcileItem` (`resolution_type: 'auto-reconciled'` +
213
+ `evidence.actor`, excluded from engagement buckets); reconcilers fail
214
+ toward KEEPING (retract only on positive evidence — fid-not-found
215
+ keeps); the long-lived branch exclusion list
216
+ (`defaults.long_lived_branches`, staging/production exact-match +
217
+ `backup/*` glob, consumer-overridable) gates FILING only, never the
218
+ attention surfaces; and the standing-debt aggregation contract
219
+ (act:e888dd63) — a standing condition files ONE item per (project,
220
+ debt-class), APPENDS evidence with `first_seen` = oldest and a
221
+ session-count urgency escalation, and post-resolution recurrence files
222
+ FRESH, never a refile loop.
165
223
 
166
224
  ## Workflows (`templates/workflows/`)
167
225
 
@@ -249,7 +307,39 @@ consumes the registry stays in cli.js).
249
307
  `checkMemoryIntegrity` delegates to it (the old filename-substring scan
250
308
  predated both mechanisms and flagged 203 false orphans while the validator
251
309
  passed green). Tests in `__tests__/bsql-loader-verify.test.mjs` +
252
- `__tests__/memory-reachability.test.mjs`.
310
+ `__tests__/memory-reachability.test.mjs`. Lane A of
311
+ grp:wt-noise-immunity added the reconciliation substrate:
312
+ `autoReconcileItem` — the ONE resolution path for machine (ring)
313
+ retractions: resolves through the real queue with `resolution_type:
314
+ 'auto-reconciled'`, then stamps `evidence.actor` + caller evidence
315
+ naming WHY (action fid / branch); returns null when the item is no
316
+ longer pending (a human got there first); a failed evidence stamp is
317
+ logged and tolerated — the typed resolution already landed atomically.
318
+ `INBOX_DETECTOR_REGISTRY` — the detector-symmetry ledger mapping every
319
+ ring-filed inbox category to `{reconciler}` or `{exempt}` (exactly
320
+ one), enforced by the structural filing-site test
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).
325
+ `GENERATED_STATE_PATTERNS`/`generatedStateIsDisposable` — the JS SSOT
326
+ for machine-written generated state (UNTRACKED `??` lines only:
327
+ `.claude/verification/`, advisories-state.json, checklist-stats.json,
328
+ `e2e/.verify-progress.jsonl` — a TRACKED file of the same name is
329
+ authored by definition), folded into `claudeChurnIsDisposable` for
330
+ ring1's `countRealUncommitted` and ring3's Phase 2a dirty filter; the
331
+ mux shell twin (`GENERATED_STATE_RE` in worktree-dirty-check.sh) is
332
+ list-diffed at the program merge tail (act:c008862c). And
333
+ `resolveProjectFromTranscriptSlug` — the slug-parsing fallback beside
334
+ `resolveProjectIdentity` for CLEANED-UP mux worktrees (act:29001b07,
335
+ filesystem resolution has nothing left to resolve): decodes the
336
+ transcript dir slug's `--mux-worktrees-` tail through mux's own
337
+ `~/.config/mux/projects.json` (longest match on a `-` boundary;
338
+ same-length tie = ambiguous → null), hands the MAIN-checkout path to
339
+ the canonical resolver, and returns null on EVERY failure — the caller
340
+ files `project_unresolved: true`, never a cwd guess. The export NAME
341
+ is a cross-lane contract: ring3-close feature-detects it on its
342
+ namespace import.
253
343
  - `watchtower-ring3-close.mjs` — Ring 3 session-close transcript processing:
254
344
  the knowledge-extraction lenses + the dedup that gates whether an
255
345
  extraction is filed. The **recall-fix program** (audit act:7d6a4b3b;
@@ -275,7 +365,36 @@ consumes the registry stays in cli.js).
275
365
  suppression sites appends a `recordSuppression` ledger line. Tests in
276
366
  `__tests__/ring3-dedup`, `ring3-memory-titles`, `ring3-novelty-rescue`,
277
367
  `ring3-recent-slice`, `ring3-chunk-merge`, `suppression-ledger`,
278
- `ring3-close-lenses`.
368
+ `ring3-close-lenses`. Lane B of grp:wt-noise-immunity rebuilt the
369
+ filing quality. Attribution (act:29001b07): BOTH entry points — live
370
+ session close and manual reprocess — share ONE `resolveSessionProject`
371
+ call that attributes the session by its TRANSCRIPT SLUG, never the
372
+ runner's cwd (302 of 510 reprocess-recovered extractions were
373
+ confidently misattributed to the runner's project before this): cwd is
374
+ trusted only when it AGREES with the transcript's project-dir slug
375
+ (`transcriptSlugFromPath`/`normalizeSlugKey`); a distrusted cwd falls
376
+ to the lib's `resolveProjectFromTranscriptSlug` (feature-detected by
377
+ name on the namespace import) and then to `project_unresolved: true` —
378
+ 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
388
+ STANDING-DEBT aggregation (act:e888dd63): ONE pending item per
389
+ (project, debt-class — `COVERAGE_DEBT_CLASS`) whose evidence APPENDS
390
+ per session (session-id union, so a reprocess replay can't inflate
391
+ counts; per-path counts; `first_seen` = oldest evidence date;
392
+ `filed_at` bumped so the expiry clock measures the LAST recurrence),
393
+ urgency escalating with session count (`coverageDebtUrgency`: normal
394
+ at 5, urgent at 15); resolving closes the accumulated view and a
395
+ persisting debt refiles FRESH next session. The `## Last Session`
396
+ block names its authoritative per-session record (`appendRecordPointer`
397
+ → `<slug>/sessions/<date>-<sessionId>.md`; act:8c076580).
279
398
  - `watchtower-queue.mjs` — inbox queue CRUD library (atomic temp+rename,
280
399
  no-index convention); also home of the staff-QA recipient gate
281
400
  (2026-06-11): every terminal exit on a `qa-handoff` item is structural —
@@ -328,6 +447,25 @@ consumes the registry stays in cli.js).
328
447
  drain. Fail-loud exit policy: 2 = malformed invocation, 1 = can't-proceed
329
448
  (item left pending), 0 = resolved; never leaks the absolute queue path.
330
449
  qa-drain's Step 2 is the consumer; 23 tests in `__tests__/resolve-cli.test.mjs`.
450
+ Also home of the surfacing-intelligence apparatus (act:00051dca,
451
+ grp:wt-noise-immunity): `extractCitedActFids` (regex-validated `act:`
452
+ fids from draft text, first-appearance order, capped at 50 — the
453
+ matches are the ONLY values that ever reach a pib-db query, bound as
454
+ parameters); the fold recipe — `foldTokens` (lowercase alnum runs,
455
+ length ≥3, stopworded) + `unigramJaccard` with
456
+ `FOLD_SIMILARITY_THRESHOLD = 0.22` and the `FOLD_MIN_TOKENS = 3`
457
+ short-text floor (calibrated 2026-07-12 against the live pairs a full
458
+ 510-draft human read surfaced), `proposeFolds` annotating BOTH sides
459
+ of a pair (reciprocity is the self-validating criterion); and
460
+ `annotateItemEvidence` — the additive evidence-only write path
461
+ (fresh read from disk, PENDING-only fence, deep-equal skip so
462
+ idempotent re-sweeps never rewrite). `isHighConfidenceSignoff` DEMOTES
463
+ on `evidence.freshness.overtaken` or a non-empty
464
+ `evidence.possible_duplicate_of` — the item drops out of batch
465
+ sign-off into individual review, never auto-dismissed. Every
466
+ caller-supplied id → path join routes through `assertLegalItemId`
467
+ (`itemPath` id-shape guard — persisted annotation id arrays flow back
468
+ in as lookups).
331
469
  - `watchtower-routines.mjs` — routine dispatch engine (act:c2a55c08):
332
470
  projects declare interactive routines in watchtower config (`{name,
333
471
  trigger, script}`; triggers: time-of-day, interval, path-nonempty,
@@ -416,7 +554,9 @@ consumes the registry stays in cli.js).
416
554
  configured project, Ring 1's parsed state-file sections + git-attention
417
555
  facts, Ring 3's recent sessions + thread cursor timelines, and the inbox's
418
556
  pending counts + RESOLUTION HISTORY (`resolution_mix` — engaged vs
419
- discarded vs untyped, the never-before-consulted signal). The script
557
+ discarded vs machine vs untyped; `machine` = `MACHINE_TYPES`
558
+ auto-reconciled ring retractions, excluded from `ENGAGED_TYPES` so cron
559
+ activity never reads as operator engagement). The script
420
560
  GATHERS; the consumer synthesizes — no convergence scoring
421
561
  (pattern-intelligence-first). Read-only projection (no write path; hermetic
422
562
  fs-snapshot test), a sibling of `watchtower-narrative-corpus.mjs`, touches
@@ -475,7 +615,25 @@ consumes the registry stays in cli.js).
475
615
  drill-down) / `--window <N>` — answer-first, verbs + counts, NEVER
476
616
  decimals. Consumers: /inbox Step 2a (answer-first verdict before the
477
617
  walk/bulk/summary choice, healthy projects suppressed) and /briefing Step
478
- 3b (the "backlog rot" weighable candidate). 12 hermetic tests in
618
+ 3b (the "backlog rot" weighable candidate). act:ab4927eb
619
+ (grp:wt-noise-immunity) sharpened the verdict:
620
+ `deriveAssessmentDetail` returns `{state, state_reason}` and two
621
+ ABSOLUTE not-consumed arms override a healthy keep ratio — a pile ≥50
622
+ pending (`deep-pile`) or an oldest tail ≥21d on a pile ≥10
623
+ (`old-tail`) can never read `keeping` (the 2026-07-12 morning
624
+ regression: 884 pending / 29d oldest reported all-keeping); rendered
625
+ wordings are state_reason-honest. `worked_recent` and the keep-ratio
626
+ event window EXCLUDE machine acts (`bucketResolution` 'machine' /
627
+ `MACHINE_TYPES`, re-exported from the cross-ring reader; surfaced as a
628
+ `machine_resolutions` count) so cron retractions can't suppress the
629
+ not-consumed verdict. Backlog rot handles AGGREGATED standing-debt
630
+ items: age = `evidence.first_seen` (falls back to filed_at) and weight
631
+ = the accumulated session count (`evidence.session_ids` — the live
632
+ producer spelling — /`session_count`/`sessions`), so one 49-session
633
+ debt can trip `MIN_PILE_FOR_ROT` alone; raw item counts ride beside
634
+ the weighted ones. `assessRingsAlive` also reads Ring 2's
635
+ `draft-annotations-health.json` sidecar (its named reader) —
636
+ informational only, never flips `all_ok`. 12 hermetic tests in
479
637
  `__tests__/inbox-assessment.test.mjs`.
480
638
  - `watchtower-migrate-keys.mjs` — one-time migration re-keying inbox items,
481
639
  thread memberships, and state dirs filed under pre-resolver
@@ -723,13 +881,48 @@ retained for the human-readable line only. Compares against `origin/<main>`
723
881
  structured `state/git-attention.json` sidecar that
724
882
  `watchtower-build-context.mjs` re-verifies against live git before relaying
725
883
  any banner — so a stale "MERGE OR LOSE" line can't survive a merge that
726
- already happened; act:6f36cbe2, act:a136b362), Ring 2 (Claude intelligence,
884
+ already happened; act:6f36cbe2, act:a136b362. Since grp:wt-noise-immunity
885
+ Ring 1's tick also RETRACTS what the rings filed — every machine
886
+ retraction routes through `autoReconcileItem` (watchtower-lib;
887
+ `'auto-reconciled'` + `evidence.actor`, excluded from engagement buckets
888
+ portfolio-wide): `autoResolveBranchDivergedItems` retracts
889
+ branch-diverged items whose branch merged, is verifiably gone
890
+ (membership in a successful branch listing — a failed git call retracts
891
+ nothing), or sits on `defaults.long_lived_branches` (staging/production
892
+ exact + `backup/*` glob) — an exclusion that gates FILING ONLY, so an
893
+ excluded branch stays visible on all four attention surfaces (attention
894
+ line, git-attention sidecar, summary, Standing Issues; act:98649971);
895
+ `autoReconcileCompletionReviews` resolves completion-review items whose
896
+ action fid is verifiably CLOSED in its own project's pib-db (opened
897
+ read-only; fid-not-found KEEPS the item — act:9eebbac4);
898
+ worktree-unmerged filing splits an earned-urgency register
899
+ (act:c008862c): `urgent` only with real unmerged commits, `normal` for
900
+ uncommitted-files-only (`evidence.unmerged`), with REGISTER-AWARE dedup
901
+ — a register change supersedes + refiles under the current register, and
902
+ escalation is damped (a soft→urgent flip needs corroborating commits
903
+ ahead, since `hasUnmergedContent` fails toward true on git blips);
904
+ `countRealUncommitted` excludes generated runtime state via the
905
+ watchtower-lib SSOT. CP3 hardening: untracked filenames pass as execFile
906
+ args (never shell-interpolated), every branch-name path gates on
907
+ `isSafeRefName`, and the detector census fails loudly on a non-literal
908
+ `category:` inside a filing owner), Ring 2 (Claude intelligence,
727
909
  fast 5-min + slow 30-min tiers; the slow tier also runs `recallCanary`
728
910
  (act:6354a9db) — reads + prunes `state/suppression-ledger.jsonl`, writes
729
911
  `state/recall-canary.json`, an over-suppression-only trend detector gated on
730
912
  `defaults.recall_canary`; `buildRecallCanary`/`readLedgerRecords`/`withinDays`
731
- exported), Ring 3 (session close, transcript processing; fires session-close
732
- routines as Phase 2j2; runs the standing session advisors as Phase 2m — see
913
+ exported and `runDraftAnnotationSweep` (act:00051dca; gated on
914
+ `defaults.draft_annotations`, default ON): annotates pending
915
+ knowledge-extraction drafts with freshness (cited `act:` fids positively
916
+ CLOSED in the item's own project pib-db — parameter-bound lookups via
917
+ `openPibDb`, now `timeout: 5000` for lock parity with Ring 3) and fold
918
+ proposals (the watchtower-queue recipe), demoting them out of batch
919
+ sign-off, never dismissing; unreadable dbs and out-of-config drafts are
920
+ counted LOUDLY (`skipped_projects`/`unscanned_items`) and every run
921
+ writes the positive-confirmation sidecar
922
+ `state/draft-annotations-health.json`, so "no annotations" is
923
+ distinguishable from "never ran"), Ring 3 (session close, transcript
924
+ processing; fires session-close routines as Phase 2j2; runs the standing
925
+ session advisors as Phase 2m — see
733
926
  below; runs three session-close extraction lenses (act:4ff2cfb3) — Phase 2n
734
927
  raised-but-unhandled (loose ends the session raised but neither did nor filed
735
928
  → inbox category `raised-unhandled`), Phase 2o skill-candidate (a manual
@@ -874,7 +1067,14 @@ shared fail-DIRTY `worktree-dirty-check.sh`; carve-out-aware `mux worktree
874
1067
  refresh` preserves uncommitted edits to ANY tracked `.claude/` file — tracked
875
1068
  = authored, only gitignored infra is disposable, classification
876
1069
  single-sourced in `worktree-session-health.sh` and guarded by the
877
- `claude-carveout` test suite), trail logging, sticky notes, global DX
1070
+ `claude-carveout` test suite), worktree lane lifecycle (act:dc80380c:
1071
+ per-project `.mux-worktree-provision` env-file provisioning at spawn,
1072
+ `mux worktree preflight` pre-spawn source check, docker container reap
1073
+ on every removal path — all single-sourced in
1074
+ `worktree-session-health.sh`), live per-worktree excludes + skip-worktree
1075
+ identity-churn suppression so a fully-merged worktree never reads
1076
+ "MERGE OR LOSE" over mux/runtime residue (act:c008862c), trail logging,
1077
+ sticky notes, global DX
878
1078
  captures, portal color-switching, durable tmux bindings (`mux.tmux.conf`
879
1079
  sourced via `~/.tmux.conf`), clipboard copy with hard-wrap removal
880
1080
  (`unwrap-copy.py`), screenshot-to-clipboard launchd watcher
@@ -336,6 +336,91 @@ each owning its tier. Flag: `defaults.recall_canary` (opt-out).
336
336
  extraction never proposed (M2 under-extraction). The canary states
337
337
  this blindness in its own output.
338
338
 
339
+ ## Detector Symmetry (no detector without a retraction path)
340
+
341
+ The 2026-07-12 inbox drain (893→194 pending) hit the same disease four
342
+ times in four detectors: branch-diverged never retracted (35/39 stale),
343
+ completion-review never reconciled (28/66 moot), worktree-unmerged
344
+ retracted only on branch deletion, and dismissed items refiled on the
345
+ next tick because dedup checks only PENDING items (the `backup/*` loop
346
+ refiled within hours, twice in one day). `autoResolveWorktreeItems`'
347
+ own comment states the principle: dedup suppresses refiling but nothing
348
+ retracts the original — **stale alarms train the operator to ignore
349
+ real ones**. This contract encodes the rule so detector #5 cannot ship
350
+ with the same bug (grp:wt-noise-immunity, act:4d11fd53).
351
+
352
+ **Every detector that FILES an inbox item must declare, at ship time:**
353
+
354
+ 1. **Its retraction condition** — the mechanical, per-tick check that
355
+ the filed condition no longer holds (branch merged/gone, action
356
+ closed, …). Retraction is truth-checking, not judgment: it runs in
357
+ Ring 1's tick beside the other reconcilers.
358
+ 2. **Its filing exclusions** — states that must never file, checked at
359
+ the FILING site (not just retraction) so a human dismissal cannot
360
+ reopen the loop: dedup consults only pending items, so an excluded
361
+ state that files at all refiles after every dismissal, forever.
362
+ 3. **Or an explicit exemption** — a detector with no possible
363
+ mechanical retraction (pure knowledge extraction, advisory
364
+ observations, session-historical facts) declares that explicitly in
365
+ the registry, with the rationale.
366
+
367
+ **The registry and its enforcement:** `INBOX_DETECTOR_REGISTRY` in
368
+ `watchtower-lib.mjs` maps every ring-filed category to
369
+ `{ reconciler }` or `{ exempt }` (exactly one). A structural test
370
+ (`templates/scripts/__tests__/detector-registry.test.mjs`) parses the
371
+ `category:` literals at `createItem` filing sites across ring1, ring2,
372
+ and ring3-close (read-only source scan, house precedent:
373
+ suppression-ledger's wiring assertion) and fails the suite when a
374
+ filing site's category is missing from the registry — a new category
375
+ without a declared reconciler or exemption breaks the build, not the
376
+ operator's trust.
377
+
378
+ **Machine resolutions are machine acts, not engagement.** Every
379
+ auto-retraction resolves through `autoReconcileItem` (watchtower-lib):
380
+ `resolution_type: 'auto-reconciled'` + `evidence.actor: 'ring1'` + a
381
+ note naming the evidence (action fid / branch). The cross-ring reader's
382
+ engagement bucketing and the inbox assessment's `worked_recent` EXCLUDE
383
+ this type — cron sweeps must never read as operator engagement or
384
+ suppress the not-consumed verdict. A semantically-wrong close (e.g. an
385
+ action closed via the ungated `pib_update_action --status done` path)
386
+ stays traceable to its mechanical trigger through the stamp.
387
+
388
+ **Fail-toward-keeping.** A reconciler retracts only on POSITIVE
389
+ evidence that the condition cleared: branch absence is concluded only
390
+ from membership in a successful branch listing (a failed git call
391
+ retracts nothing); an action-fid lookup retracts only on a FOUND,
392
+ non-deleted, closed row — fid-not-found keeps the item, which is
393
+ load-bearing for flow (a readable-but-vestigial pib.db; the real
394
+ tracker is flow.db) and for misattributed items whose fid lives in a
395
+ different project's db. Foreign pib.dbs are opened READ-ONLY behind an
396
+ existsSync guard; reconciler writes land only in the watchtower queue.
397
+
398
+ **Branch exclusion list:** `defaults.long_lived_branches` (watchtower
399
+ config), default `["staging", "production", "backup/*"]`. Entries
400
+ without `*` are EXACT names (`staging-fix` files normally); `*` is a
401
+ glob. **Consumer-default risk, documented:** a consumer whose real work
402
+ branch is literally named `staging` or `production` will never get a
403
+ branch-diverged item for it — the branch stays visible on the attention
404
+ line / git-attention sidecar / summary / Standing Issues (the exclusion
405
+ gates filing ONLY), and the consumer overrides the list in config.
406
+
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).
417
+
418
+ **Known limitation (named, not hidden):** reconcilers run per
419
+ configured project — items whose project was deleted or de-registered
420
+ are never visited and age pending until a human disposes them (the
421
+ inbox's aging surfaces them). Auto-resolving on "project gone" would
422
+ guess; the honest state is pending.
423
+
339
424
  ## Deferred Schemas
340
425
 
341
426
  The following are specified in project notes but NOT formalized until
@@ -97,6 +97,41 @@ correct in both places — no on-main special case. One rule, both
97
97
  directions: worktree-invisible `.claude/` state is always addressed
98
98
  through `$MAIN`, whether you are writing it (§3) or reading it (here).
99
99
 
100
+ ## 5. Worktree environment lifecycle (mux projects)
101
+
102
+ Mux owns the worktree environment end to end; sessions should not
103
+ hand-repair it. Three verbs, all single-sourced in
104
+ `~/.config/mux/worktree-session-health.sh`:
105
+
106
+ - **Provision** — a project that declares gitignored env files in
107
+ `.mux-worktree-provision` (project root; one path per line) gets them
108
+ copied from main at worktree creation and freshness-checked on later
109
+ health runs. A locally-modified worktree copy is never overwritten.
110
+ - **Preflight** — `mux worktree preflight [project]` verifies main's
111
+ declared env files exist and every declared `check:` command passes.
112
+ Run it BEFORE spawning parallel worktree lanes; non-zero exit means
113
+ do not spawn yet. Probes should verify identity/freshness, not bare
114
+ liveness — a leftover stack answers a bare port probe. Trust note:
115
+ `check:` lines execute as shell from the project root (same trust
116
+ class as npm scripts and CC hooks) — don't run preflight inside a
117
+ clone you don't trust.
118
+ - **Reap** — every worktree removal path stops the lane's docker
119
+ containers (exact compose-project-label match on the worktree dir
120
+ name; containers only, volumes kept). Assumes the lane's compose
121
+ stack was started from the worktree ROOT (compose's default project
122
+ name = directory basename); a stack started with `-p`/
123
+ `COMPOSE_PROJECT_NAME` or from a subdirectory is not reaped. Nothing
124
+ for a session to do; noted so cleanup surprises are recognized as
125
+ reaping, not data loss.
126
+
127
+ One symptom worth recognizing from a worktree: if `git merge main`
128
+ aborts citing "local changes to .mcp.json" while `git status` shows
129
+ clean, that is the mux identity symlink held under skip-worktree (main
130
+ changed the file since the worktree was cut — pre-existing behavior
131
+ either way). Recovery: `git update-index --no-skip-worktree .mcp.json
132
+ && git checkout -- .mcp.json && git merge main`; the next health run
133
+ re-symlinks and re-hides.
134
+
100
135
  ## What this contract does NOT cover
101
136
 
102
137
  Caller-specific orchestration stays in the caller's own Step 0, never