create-claude-cabinet 0.48.0 → 0.50.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 (62) 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 +270 -37
  6. package/templates/cabinet/watchtower-contracts.md +85 -0
  7. package/templates/cabinet/worktree-invocation-contract.md +52 -10
  8. package/templates/engagement/__tests__/transport-token.test.mjs +103 -0
  9. package/templates/engagement/engagement-transport.mjs +35 -1
  10. package/templates/engagement/pib-db-patches/pib-db-lib.mjs +53 -9
  11. package/templates/mux/__tests__/mux-fail-loud.fixture.sh +4 -5
  12. package/templates/mux/__tests__/worktree-lifecycle.fixture.sh +214 -0
  13. package/templates/mux/__tests__/worktree-lifecycle.test.mjs +44 -0
  14. package/templates/mux/__tests__/worktree-status-clean.fixture.sh +245 -0
  15. package/templates/mux/__tests__/worktree-status-clean.test.mjs +89 -0
  16. package/templates/mux/bin/mux +44 -28
  17. package/templates/mux/config/worktree-cleanup.sh +57 -12
  18. package/templates/mux/config/worktree-dirty-check.sh +72 -2
  19. package/templates/mux/config/worktree-session-health.sh +401 -17
  20. package/templates/scripts/__tests__/assessment-recalibration.test.mjs +387 -0
  21. package/templates/scripts/__tests__/batch-disposition.test.mjs +21 -1
  22. package/templates/scripts/__tests__/branch-diverged-reconcile.test.mjs +374 -0
  23. package/templates/scripts/__tests__/completion-review-reconcile.test.mjs +173 -0
  24. package/templates/scripts/__tests__/cross-ring-reader.test.mjs +12 -4
  25. package/templates/scripts/__tests__/detector-registry.test.mjs +152 -0
  26. package/templates/scripts/__tests__/draft-surfacing.test.mjs +433 -0
  27. package/templates/scripts/__tests__/generated-state-classification.test.mjs +228 -0
  28. package/templates/scripts/__tests__/mirror-parity.test.mjs +54 -0
  29. package/templates/scripts/__tests__/qa-handoff-aging.e2e.test.mjs +3 -0
  30. package/templates/scripts/__tests__/qa-handoff-gate.test.mjs +8 -1
  31. package/templates/scripts/__tests__/qa-handoff-merge-state.test.mjs +69 -20
  32. package/templates/scripts/__tests__/qa-handoff-merged-by-construction.test.mjs +310 -0
  33. package/templates/scripts/__tests__/resolve-cli.test.mjs +6 -1
  34. package/templates/scripts/__tests__/resolve-project-slug.test.mjs +186 -0
  35. package/templates/scripts/__tests__/ring3-attribution.test.mjs +267 -0
  36. package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +176 -0
  37. package/templates/scripts/__tests__/ring3-coverage-debt.test.mjs +377 -0
  38. package/templates/scripts/__tests__/ring3-last-session-pointer.test.mjs +80 -0
  39. package/templates/scripts/__tests__/routine-dispatch.test.mjs +2 -0
  40. package/templates/scripts/__tests__/session-ack.test.mjs +61 -0
  41. package/templates/scripts/audit-coherence-check.mjs +6 -2
  42. package/templates/scripts/load-triage-history.js +23 -2
  43. package/templates/scripts/merge-findings.js +9 -2
  44. package/templates/scripts/pib-db-lib.mjs +55 -7
  45. package/templates/scripts/watchtower-build-context.mjs +24 -2
  46. package/templates/scripts/watchtower-cross-ring-reader.mjs +20 -3
  47. package/templates/scripts/watchtower-inbox-assessment.mjs +186 -35
  48. package/templates/scripts/watchtower-lib.mjs +268 -5
  49. package/templates/scripts/watchtower-queue.mjs +469 -4
  50. package/templates/scripts/watchtower-ring1.mjs +490 -69
  51. package/templates/scripts/watchtower-ring2.mjs +175 -3
  52. package/templates/scripts/watchtower-ring3-close.mjs +519 -69
  53. package/templates/skills/audit/SKILL.md +6 -3
  54. package/templates/skills/cabinet-anthropic-insider/SKILL.md +29 -8
  55. package/templates/skills/cabinet-process-therapist/SKILL.md +45 -3
  56. package/templates/skills/collab-client/SKILL.md +13 -7
  57. package/templates/skills/collab-consultant/SKILL.md +9 -4
  58. package/templates/skills/execute/SKILL.md +60 -33
  59. package/templates/skills/inbox/SKILL.md +29 -1
  60. package/templates/skills/qa-handoff/SKILL.md +112 -48
  61. package/templates/skills/session-handoff/SKILL.md +6 -8
  62. 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.50.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 —
@@ -297,28 +416,45 @@ consumes the registry stays in cli.js).
297
416
  required, and a structural refusal of GATED_CATEGORIES items (today:
298
417
  qa-handoff) — gated items never batch, each exits only through its own
299
418
  gate; `routine` is dispatched but NOT gated, so stale routines are legal
300
- batch fodder (18-test hermetic suite in `__tests__/batch-disposition.test.mjs`).
419
+ batch fodder (19-test hermetic suite in `__tests__/batch-disposition.test.mjs`).
301
420
  Inbox bulk triage splits sign-off from judgment via `isHighConfidenceSignoff`/
302
421
  `partitionForBatchSignoff` — high-confidence pre-drafted extractions
303
422
  auto-group into one batch sign-off while judgment items stay per-item
304
423
  (act:7f1e4d91). qa-handoff items carry a first-class `evidence.merge_state`
305
424
  (act:971570c4): `'merged'` (default — filed after the worktree branch
306
- merged) or `'merge-pending'` (filed before the merge, when it's gated on an
307
- in-flight deploy). `normalizeMergeState` + `QA_MERGE_STATES` are exported;
308
- `createItem` rejects a present-but-illegal token at the boundary (a typo
309
- can't read as `'merged'`) and canonicalizes a legal one (additive — absent
310
- stays absent, readers default to `'merged'`). A `createItem`
425
+ merged) or `'merge-pending'` (filed before the merge, ONLY when it's gated
426
+ on something external). `normalizeMergeState` + `QA_MERGE_STATES` are
427
+ exported; `createItem` rejects a present-but-illegal token at the boundary
428
+ (a typo can't read as `'merged'`) and canonicalizes a legal one (additive —
429
+ absent stays absent, readers default to `'merged'`). A `createItem`
311
430
  MERGE-COHERENCE guard (act:e859b3a3) also rejects a `'merged'` handoff
312
431
  whose `merged_into`/`merged_commit` free-texts a not-yet-merged marker
313
- (`PENDING`/`unmerged`/…) — unmerged work can't be filed as merged at the
314
- boundary, the honest state is `'merge-pending'`; pure string check, fenced
315
- to qa-handoff, and the drain's Step 0 does the live git-ancestry
316
- verification of the sha. The recipient gate is unchanged: a merge-pending
317
- handoff still resolves only through a stamped `qa_verdict` cited against
318
- the post-merge commit. The skill defers stage-2 dispatch and the pickup
319
- prompt says "merge then QA" when merge-pending. 32-test hermetic suite in
320
- `templates/scripts/__tests__/qa-handoff-gate.test.mjs` + 12-test
321
- `qa-handoff-merge-state.test.mjs`. Also home of the operator RESOLVE CLI
432
+ (`PENDING`/`unmerged`/…) — pure string check, fenced to qa-handoff. On top
433
+ of it, the MERGED-BY-CONSTRUCTION guards (act:3d1ac2b7) make the merge
434
+ claim verified, not asserted — a two-layer defense with the drain's Step 0
435
+ live re-check: a `'merged'` handoff requires a sha-shaped `merged_commit`
436
+ (omission was the trivial bypass) which `verifyMergedAncestry` checks
437
+ against git (queue-local origin/HEAD→main/master ref resolution, no fetch,
438
+ execFile args, 5s timeout; REJECT only on positive refutation, file with a
439
+ visible `evidence.ancestry_verified: 'unverifiable'` stamp when git can't
440
+ answer); a `'merge-pending'` handoff requires a non-empty
441
+ `evidence.merge_gate` naming the external blocker (rejection messages
442
+ teach the merge-first close-out, self-correcting older skill text); and
443
+ the ONE legal merge-pending→merged flip is `markHandoffMerged` — same
444
+ verification, writes the deferred stage-2 dispatch descriptor directly
445
+ into the mux queue dir (fs-only, desk token shape-guarded, in-flight
446
+ aware), records `evidence.stage2_dispatch` so a failed dispatch retries
447
+ by state — while `annotateItemEvidence` REFUSES `merge_state` patches on
448
+ qa-handoffs (the side door is closed). The recipient gate is unchanged: a
449
+ merge-pending handoff resolves only AFTER `markHandoffMerged` records the
450
+ verified merge, then through a stamped `qa_verdict` cited against the
451
+ post-merge commit. The skill defers stage-2 dispatch and the pickup
452
+ prompt says "merge then QA" when merge-pending (`buildQaPickupPrompt` is
453
+ the library sibling of the skill's merged-path prompt — pared to the gate
454
+ essentials, not byte-parity). 32-test hermetic suite in
455
+ `templates/scripts/__tests__/qa-handoff-gate.test.mjs` + 20-test
456
+ `qa-handoff-merge-state.test.mjs` + 19-test
457
+ `qa-handoff-merged-by-construction.test.mjs` (real temp git repos). Also home of the operator RESOLVE CLI
322
458
  (act:00030e1d): `resolve <id> --verdict-file <path.json>` (`parseResolveArgs`
323
459
  + `runResolveCli` + an isMain guard) reads the FULL `resolveItem` params
324
460
  object `{resolution, resolution_type, resolution_notes, qa_verdict}` from a
@@ -328,6 +464,25 @@ consumes the registry stays in cli.js).
328
464
  drain. Fail-loud exit policy: 2 = malformed invocation, 1 = can't-proceed
329
465
  (item left pending), 0 = resolved; never leaks the absolute queue path.
330
466
  qa-drain's Step 2 is the consumer; 23 tests in `__tests__/resolve-cli.test.mjs`.
467
+ Also home of the surfacing-intelligence apparatus (act:00051dca,
468
+ grp:wt-noise-immunity): `extractCitedActFids` (regex-validated `act:`
469
+ fids from draft text, first-appearance order, capped at 50 — the
470
+ matches are the ONLY values that ever reach a pib-db query, bound as
471
+ parameters); the fold recipe — `foldTokens` (lowercase alnum runs,
472
+ length ≥3, stopworded) + `unigramJaccard` with
473
+ `FOLD_SIMILARITY_THRESHOLD = 0.22` and the `FOLD_MIN_TOKENS = 3`
474
+ short-text floor (calibrated 2026-07-12 against the live pairs a full
475
+ 510-draft human read surfaced), `proposeFolds` annotating BOTH sides
476
+ of a pair (reciprocity is the self-validating criterion); and
477
+ `annotateItemEvidence` — the additive evidence-only write path
478
+ (fresh read from disk, PENDING-only fence, deep-equal skip so
479
+ idempotent re-sweeps never rewrite). `isHighConfidenceSignoff` DEMOTES
480
+ on `evidence.freshness.overtaken` or a non-empty
481
+ `evidence.possible_duplicate_of` — the item drops out of batch
482
+ sign-off into individual review, never auto-dismissed. Every
483
+ caller-supplied id → path join routes through `assertLegalItemId`
484
+ (`itemPath` id-shape guard — persisted annotation id arrays flow back
485
+ in as lookups).
331
486
  - `watchtower-routines.mjs` — routine dispatch engine (act:c2a55c08):
332
487
  projects declare interactive routines in watchtower config (`{name,
333
488
  trigger, script}`; triggers: time-of-day, interval, path-nonempty,
@@ -370,8 +525,17 @@ consumes the registry stays in cli.js).
370
525
  (git-dir == git-common-dir; worktree windows stay silent) and built from
371
526
  the shared `buildPickupPrompt` so the resolve-on-run wording is
372
527
  single-sourced; qa-handoff items are excluded by the `category: 'routine'`
373
- filter (operator-initiated by design). Exports `renderMissedRoutineSection`/
374
- `isMainCheckout` for tests.
528
+ filter (operator-initiated by design). Also OPENS every injection with a
529
+ session-start acknowledgment directive (act:058d00f0,
530
+ `renderSessionAckSection`, `PRIORITY_NEVER`): a short instruction telling
531
+ the session to begin its first reply with ONE plain-English line
532
+ confirming the context loaded — positive confirmation the hook chain
533
+ works (the injection is invisible in the terminal, so a silent session
534
+ was indistinguishable from a broken hook). Prompt-layer convention only;
535
+ a frontier-model warning still precedes it, resume re-injections skip it;
536
+ tests in `__tests__/session-ack.test.mjs`. Exports
537
+ `renderMissedRoutineSection`/`renderSessionAckSection`/`isMainCheckout`
538
+ for tests.
375
539
  - `watchtower-snapshot.mjs` — mid-session awareness polling (act:a8057f87):
376
540
  the sibling primitive behind `/catch-up`. The SessionStart hook captures a
377
541
  per-session BASELINE snapshot (keyed by the stdin `.session_id`, written to
@@ -416,7 +580,9 @@ consumes the registry stays in cli.js).
416
580
  configured project, Ring 1's parsed state-file sections + git-attention
417
581
  facts, Ring 3's recent sessions + thread cursor timelines, and the inbox's
418
582
  pending counts + RESOLUTION HISTORY (`resolution_mix` — engaged vs
419
- discarded vs untyped, the never-before-consulted signal). The script
583
+ discarded vs machine vs untyped; `machine` = `MACHINE_TYPES`
584
+ auto-reconciled ring retractions, excluded from `ENGAGED_TYPES` so cron
585
+ activity never reads as operator engagement). The script
420
586
  GATHERS; the consumer synthesizes — no convergence scoring
421
587
  (pattern-intelligence-first). Read-only projection (no write path; hermetic
422
588
  fs-snapshot test), a sibling of `watchtower-narrative-corpus.mjs`, touches
@@ -475,7 +641,25 @@ consumes the registry stays in cli.js).
475
641
  drill-down) / `--window <N>` — answer-first, verbs + counts, NEVER
476
642
  decimals. Consumers: /inbox Step 2a (answer-first verdict before the
477
643
  walk/bulk/summary choice, healthy projects suppressed) and /briefing Step
478
- 3b (the "backlog rot" weighable candidate). 12 hermetic tests in
644
+ 3b (the "backlog rot" weighable candidate). act:ab4927eb
645
+ (grp:wt-noise-immunity) sharpened the verdict:
646
+ `deriveAssessmentDetail` returns `{state, state_reason}` and two
647
+ ABSOLUTE not-consumed arms override a healthy keep ratio — a pile ≥50
648
+ pending (`deep-pile`) or an oldest tail ≥21d on a pile ≥10
649
+ (`old-tail`) can never read `keeping` (the 2026-07-12 morning
650
+ regression: 884 pending / 29d oldest reported all-keeping); rendered
651
+ wordings are state_reason-honest. `worked_recent` and the keep-ratio
652
+ event window EXCLUDE machine acts (`bucketResolution` 'machine' /
653
+ `MACHINE_TYPES`, re-exported from the cross-ring reader; surfaced as a
654
+ `machine_resolutions` count) so cron retractions can't suppress the
655
+ not-consumed verdict. Backlog rot handles AGGREGATED standing-debt
656
+ items: age = `evidence.first_seen` (falls back to filed_at) and weight
657
+ = the accumulated session count (`evidence.session_ids` — the live
658
+ producer spelling — /`session_count`/`sessions`), so one 49-session
659
+ debt can trip `MIN_PILE_FOR_ROT` alone; raw item counts ride beside
660
+ the weighted ones. `assessRingsAlive` also reads Ring 2's
661
+ `draft-annotations-health.json` sidecar (its named reader) —
662
+ informational only, never flips `all_ok`. 12 hermetic tests in
479
663
  `__tests__/inbox-assessment.test.mjs`.
480
664
  - `watchtower-migrate-keys.mjs` — one-time migration re-keying inbox items,
481
665
  thread memberships, and state dirs filed under pre-resolver
@@ -723,13 +907,48 @@ retained for the human-readable line only. Compares against `origin/<main>`
723
907
  structured `state/git-attention.json` sidecar that
724
908
  `watchtower-build-context.mjs` re-verifies against live git before relaying
725
909
  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,
910
+ already happened; act:6f36cbe2, act:a136b362. Since grp:wt-noise-immunity
911
+ Ring 1's tick also RETRACTS what the rings filed — every machine
912
+ retraction routes through `autoReconcileItem` (watchtower-lib;
913
+ `'auto-reconciled'` + `evidence.actor`, excluded from engagement buckets
914
+ portfolio-wide): `autoResolveBranchDivergedItems` retracts
915
+ branch-diverged items whose branch merged, is verifiably gone
916
+ (membership in a successful branch listing — a failed git call retracts
917
+ nothing), or sits on `defaults.long_lived_branches` (staging/production
918
+ exact + `backup/*` glob) — an exclusion that gates FILING ONLY, so an
919
+ excluded branch stays visible on all four attention surfaces (attention
920
+ line, git-attention sidecar, summary, Standing Issues; act:98649971);
921
+ `autoReconcileCompletionReviews` resolves completion-review items whose
922
+ action fid is verifiably CLOSED in its own project's pib-db (opened
923
+ read-only; fid-not-found KEEPS the item — act:9eebbac4);
924
+ worktree-unmerged filing splits an earned-urgency register
925
+ (act:c008862c): `urgent` only with real unmerged commits, `normal` for
926
+ uncommitted-files-only (`evidence.unmerged`), with REGISTER-AWARE dedup
927
+ — a register change supersedes + refiles under the current register, and
928
+ escalation is damped (a soft→urgent flip needs corroborating commits
929
+ ahead, since `hasUnmergedContent` fails toward true on git blips);
930
+ `countRealUncommitted` excludes generated runtime state via the
931
+ watchtower-lib SSOT. CP3 hardening: untracked filenames pass as execFile
932
+ args (never shell-interpolated), every branch-name path gates on
933
+ `isSafeRefName`, and the detector census fails loudly on a non-literal
934
+ `category:` inside a filing owner), Ring 2 (Claude intelligence,
727
935
  fast 5-min + slow 30-min tiers; the slow tier also runs `recallCanary`
728
936
  (act:6354a9db) — reads + prunes `state/suppression-ledger.jsonl`, writes
729
937
  `state/recall-canary.json`, an over-suppression-only trend detector gated on
730
938
  `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
939
+ exported and `runDraftAnnotationSweep` (act:00051dca; gated on
940
+ `defaults.draft_annotations`, default ON): annotates pending
941
+ knowledge-extraction drafts with freshness (cited `act:` fids positively
942
+ CLOSED in the item's own project pib-db — parameter-bound lookups via
943
+ `openPibDb`, now `timeout: 5000` for lock parity with Ring 3) and fold
944
+ proposals (the watchtower-queue recipe), demoting them out of batch
945
+ sign-off, never dismissing; unreadable dbs and out-of-config drafts are
946
+ counted LOUDLY (`skipped_projects`/`unscanned_items`) and every run
947
+ writes the positive-confirmation sidecar
948
+ `state/draft-annotations-health.json`, so "no annotations" is
949
+ distinguishable from "never ran"), Ring 3 (session close, transcript
950
+ processing; fires session-close routines as Phase 2j2; runs the standing
951
+ session advisors as Phase 2m — see
733
952
  below; runs three session-close extraction lenses (act:4ff2cfb3) — Phase 2n
734
953
  raised-but-unhandled (loose ends the session raised but neither did nor filed
735
954
  → inbox category `raised-unhandled`), Phase 2o skill-candidate (a manual
@@ -805,7 +1024,9 @@ step — then push it to the desk's main window via `mux qa dispatch`; usually
805
1024
  filed right after the merge — an unmerged named branch now PROMPTS merge-now
806
1025
  (Claude merges → `merged`) vs keep-gated, never a silent `merge-pending`
807
1026
  default (act:350e7373); merge-pending is opt-in, filed BEFORE the merge only
808
- when the merge is gated, in which case stage-2 dispatch defers — see the
1027
+ when the merge is gated on a named external blocker (`evidence.merge_gate`,
1028
+ required at the boundary; act:3d1ac2b7), in which case stage-2 dispatch
1029
+ defers until `markHandoffMerged` records the verified flip — see the
809
1030
  watchtower-queue `merge_state` entry above; stages 1–2 of the QA-handoff
810
1031
  protocol, `.claude/plans/qa-handoff-protocol.md`). Stage 3 — the drain — is
811
1032
  enforced by the staff-QA recipient gate in `watchtower-queue.mjs`: qa-handoff
@@ -874,7 +1095,14 @@ shared fail-DIRTY `worktree-dirty-check.sh`; carve-out-aware `mux worktree
874
1095
  refresh` preserves uncommitted edits to ANY tracked `.claude/` file — tracked
875
1096
  = authored, only gitignored infra is disposable, classification
876
1097
  single-sourced in `worktree-session-health.sh` and guarded by the
877
- `claude-carveout` test suite), trail logging, sticky notes, global DX
1098
+ `claude-carveout` test suite), worktree lane lifecycle (act:dc80380c:
1099
+ per-project `.mux-worktree-provision` env-file provisioning at spawn,
1100
+ `mux worktree preflight` pre-spawn source check, docker container reap
1101
+ on every removal path — all single-sourced in
1102
+ `worktree-session-health.sh`), live per-worktree excludes + skip-worktree
1103
+ identity-churn suppression so a fully-merged worktree never reads
1104
+ "MERGE OR LOSE" over mux/runtime residue (act:c008862c), trail logging,
1105
+ sticky notes, global DX
878
1106
  captures, portal color-switching, durable tmux bindings (`mux.tmux.conf`
879
1107
  sourced via `~/.tmux.conf`), clipboard copy with hard-wrap removal
880
1108
  (`unwrap-copy.py`), screenshot-to-clipboard launchd watcher
@@ -950,10 +1178,15 @@ MAIN checkout for breadcrumbs/report, so worktree invocations land artifacts
950
1178
  durably). `checkpoint-protocol.md` documents three CP modes (Interactive,
951
1179
  Advisory, Full) and is the shared contract between `/execute` and
952
1180
  `/execute-group`. `/execute` is worktree-aware (act:1067adee): its Step 0
953
- reads the worktree-invocation contract and syncs main *into* the worktree (it
954
- never merges OUT the worktree→main bridge is `/qa-handoff`); breadcrumb
955
- writes resolve to `$MAIN/.claude/verification/`, closing the lost-on-cleanup
956
- gap (act:d4767af6); and its Step 8 worktree close-out files a `merge-pending`
957
- qa-handoff and marks the action done at commit, degrading loudly (never
958
- silently) when watchtower isn't installed. On the main checkout all of this
959
- no-ops.
1181
+ reads the worktree-invocation contract and syncs main *into* the worktree
1182
+ (the merge OUT happens once, at close-out); breadcrumb writes resolve to
1183
+ `$MAIN/.claude/verification/`, closing the lost-on-cleanup gap
1184
+ (act:d4767af6); and its Step 8 worktree close-out is MERGE-FIRST
1185
+ (act:3d1ac2b7): re-sync + re-gate, merge + push via `/qa-handoff`'s
1186
+ merge-now recipe (no prompt merge-now is the default; every failure HALTs
1187
+ loudly, the action stays open), then file a `merged` qa-handoff that
1188
+ dispatches unconditionally. Done is marked only after merge + filing
1189
+ succeed. `merge-pending` is reserved for an explicitly gated merge
1190
+ (`evidence.merge_gate` required). Without watchtower the close-out still
1191
+ merges, then degrades loudly (never silently) — the no-handoff note names
1192
+ what to QA manually. On the main checkout all of this no-ops.