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.
- package/lib/CLAUDE.md +22 -0
- package/lib/cli.js +100 -5
- package/lib/metadata.js +20 -0
- package/package.json +1 -1
- package/templates/CLAUDE.md +212 -12
- package/templates/cabinet/watchtower-contracts.md +85 -0
- package/templates/cabinet/worktree-invocation-contract.md +35 -0
- package/templates/engagement/pib-db-patches/pib-db-lib.mjs +53 -9
- package/templates/mux/__tests__/mux-fail-loud.fixture.sh +4 -5
- package/templates/mux/__tests__/worktree-lifecycle.fixture.sh +214 -0
- package/templates/mux/__tests__/worktree-lifecycle.test.mjs +44 -0
- package/templates/mux/__tests__/worktree-status-clean.fixture.sh +245 -0
- package/templates/mux/__tests__/worktree-status-clean.test.mjs +89 -0
- package/templates/mux/bin/mux +44 -28
- package/templates/mux/config/worktree-cleanup.sh +57 -12
- package/templates/mux/config/worktree-dirty-check.sh +72 -2
- package/templates/mux/config/worktree-session-health.sh +401 -17
- package/templates/scripts/__tests__/assessment-recalibration.test.mjs +387 -0
- package/templates/scripts/__tests__/batch-disposition.test.mjs +19 -0
- package/templates/scripts/__tests__/branch-diverged-reconcile.test.mjs +374 -0
- package/templates/scripts/__tests__/completion-review-reconcile.test.mjs +173 -0
- package/templates/scripts/__tests__/cross-ring-reader.test.mjs +12 -4
- package/templates/scripts/__tests__/detector-registry.test.mjs +152 -0
- package/templates/scripts/__tests__/draft-surfacing.test.mjs +433 -0
- package/templates/scripts/__tests__/generated-state-classification.test.mjs +228 -0
- package/templates/scripts/__tests__/mirror-parity.test.mjs +54 -0
- package/templates/scripts/__tests__/resolve-project-slug.test.mjs +186 -0
- package/templates/scripts/__tests__/ring3-attribution.test.mjs +267 -0
- package/templates/scripts/__tests__/ring3-completion-filter.test.mjs +176 -0
- package/templates/scripts/__tests__/ring3-coverage-debt.test.mjs +377 -0
- package/templates/scripts/__tests__/ring3-last-session-pointer.test.mjs +80 -0
- package/templates/scripts/audit-coherence-check.mjs +6 -2
- package/templates/scripts/load-triage-history.js +23 -2
- package/templates/scripts/merge-findings.js +9 -2
- package/templates/scripts/pib-db-lib.mjs +55 -7
- package/templates/scripts/watchtower-cross-ring-reader.mjs +20 -3
- package/templates/scripts/watchtower-inbox-assessment.mjs +186 -35
- package/templates/scripts/watchtower-lib.mjs +268 -5
- package/templates/scripts/watchtower-queue.mjs +197 -2
- package/templates/scripts/watchtower-ring1.mjs +490 -69
- package/templates/scripts/watchtower-ring2.mjs +175 -3
- package/templates/scripts/watchtower-ring3-close.mjs +519 -69
- package/templates/skills/audit/SKILL.md +6 -3
- package/templates/skills/cabinet-anthropic-insider/SKILL.md +29 -8
- package/templates/skills/cabinet-process-therapist/SKILL.md +45 -3
- package/templates/skills/inbox/SKILL.md +29 -1
- package/templates/skills/session-handoff/SKILL.md +6 -8
- 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
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
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
package/templates/CLAUDE.md
CHANGED
|
@@ -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)
|
|
26
|
-
|
|
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
|
|
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).
|
|
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
|
|
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
|
|
732
|
-
|
|
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),
|
|
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
|