claude-code-session-manager 0.75.2 → 0.76.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/dist/assets/{AgentLibrary-s1jRGOji.js → AgentLibrary-CBx9l4zN.js} +1 -1
- package/dist/assets/{DataModel-DNefcWbk.js → DataModel-Bf0EIE_t.js} +1 -1
- package/dist/assets/{History-4sMk80yI.js → History-CpdtWhC8.js} +1 -1
- package/dist/assets/{Hooks-CNUVwH5P.js → Hooks-DyUbMDmg.js} +1 -1
- package/dist/assets/{HostBilko-BhkyvIhw.js → HostBilko-By-wIpry.js} +1 -1
- package/dist/assets/{Library-BpockanA.js → Library-CQmo4QVC.js} +1 -1
- package/dist/assets/{ListDetail-Cre7h3U0.js → ListDetail-BQMd6NOm.js} +1 -1
- package/dist/assets/{MarkdownEditor-x-FzvaXm.js → MarkdownEditor-DEp43FXX.js} +1 -1
- package/dist/assets/{McpServers-CXAcQLMB.js → McpServers-CLarzwqA.js} +1 -1
- package/dist/assets/{Memory-CGBv1Rco.js → Memory-B0sCdIy1.js} +1 -1
- package/dist/assets/{Panel-czEs21N2.js → Panel-BhWPVOCD.js} +1 -1
- package/dist/assets/{Permissions-rGkLOpqI.js → Permissions-Ddlq8T_O.js} +1 -1
- package/dist/assets/{Plugins-BTmB_vgU.js → Plugins-D2oA_2Jl.js} +2 -2
- package/dist/assets/{ProvenanceBadge-dmCivDq5.js → ProvenanceBadge-DgAgavUM.js} +1 -1
- package/dist/assets/{SaveBar-D80TNkAx.js → SaveBar-Qvc4Ek-H.js} +1 -1
- package/dist/assets/{Scheduler-C6wHSZP7.js → Scheduler-BmYJvNzK.js} +1 -1
- package/dist/assets/{ScopeSwitcher-DsaiYTgl.js → ScopeSwitcher-C_zWEtIl.js} +1 -1
- package/dist/assets/{Settings-m2fyU8Q5.js → Settings-2Vx3X5SI.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-yS-lgBFU.js → SkillReferenceGraph-BDEUjlTQ.js} +1 -1
- package/dist/assets/{Skills-DtQgCs9-.js → Skills-Cmrz_LeN.js} +1 -1
- package/dist/assets/{SystemPrompt-CLuVdoMA.js → SystemPrompt-DVA1eYDP.js} +1 -1
- package/dist/assets/{TagLibrary-DQXoI752.js → TagLibrary-DYJGAKZu.js} +1 -1
- package/dist/assets/{TiptapBody-BGKVXrvq.js → TiptapBody-DmPc3amD.js} +1 -1
- package/dist/assets/{Toggle-RFnEV71m.js → Toggle-zfd5LJkK.js} +1 -1
- package/dist/assets/{index-Ddrw7J7O.js → index-B_4PNh9T.js} +676 -676
- package/dist/assets/{index-CMLnzdZC.css → index-DIjnPkRN.css} +1 -1
- package/dist/assets/{settingsSchema-LwuJXKD1.js → settingsSchema-B9es6fdA.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/lib/activeSessions.cjs +146 -4
- package/scripts/scheduler-mcp-server.cjs +154 -95
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +105 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdCreate.test.cjs +109 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +15 -3
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +60 -1
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +22 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +56 -4
- package/src/main/ipcSchemas.cjs +18 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +63 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +241 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +1 -1
- package/src/main/lib/__tests__/gitWorktree.test.cjs +14 -2
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +2 -2
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +101 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +202 -0
- package/src/main/lib/__tests__/opsRootNestedWrite.test.cjs +51 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +112 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +33 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/delegationReadiness.cjs +408 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/gitWorktree.cjs +56 -2
- package/src/main/lib/jobWorktree.cjs +1 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +285 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +94 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdCreate.cjs +46 -14
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueStore.cjs +70 -8
- package/src/main/lib/rcaReport.cjs +1 -1
- package/src/main/lib/reaperHelpers.cjs +47 -1
- package/src/main/lib/schedulerBatch.cjs +171 -29
- package/src/main/lib/schedulerConfig.cjs +80 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +2 -1
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/scheduler.cjs +346 -49
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/preload/api.d.ts +69 -1
- package/src/preload/index.cjs +2 -0
|
@@ -8,17 +8,29 @@
|
|
|
8
8
|
* implements inline instead. See scripts/install-scheduler-mcp-user-scope.sh's
|
|
9
9
|
* header for the incident this repeat-guards against (PRD 1024-1030).
|
|
10
10
|
*
|
|
11
|
-
* This module only computes the
|
|
12
|
-
*
|
|
11
|
+
* This module only computes the checks below; surfacing them in the UI or
|
|
12
|
+
* the Epic's grounding line is a dependent PRD (delegation-readiness-ui).
|
|
13
|
+
* A registration key existing in ~/.claude.json is necessary but NOT
|
|
14
|
+
* sufficient — scheduler-mcp-live actually spawns the registered server and
|
|
15
|
+
* confirms it answers tools/list, closing the gap where a moved repo, a
|
|
16
|
+
* missing `node`, or a syntax error in the server showed GREEN forever.
|
|
13
17
|
*/
|
|
14
18
|
|
|
15
19
|
const fs = require('node:fs');
|
|
16
20
|
const path = require('node:path');
|
|
17
21
|
const os = require('node:os');
|
|
22
|
+
const { spawn } = require('node:child_process');
|
|
23
|
+
const { writeJson, addAllowedRoot } = require('../config.cjs');
|
|
24
|
+
const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
|
|
18
25
|
|
|
19
26
|
const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
|
|
20
27
|
const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
|
|
21
28
|
const SCHEDULER_MCP_SERVER_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'scheduler-mcp-server.cjs');
|
|
29
|
+
const PRD_WRITE_GUARD_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'hooks', 'guard-prd-writes.cjs');
|
|
30
|
+
const PRD_WRITE_GUARD_MATCHER = 'Write|Edit|NotebookEdit';
|
|
31
|
+
const LIVE_PROBE_TIMEOUT_MS = 10_000;
|
|
32
|
+
const LIVE_PROBE_TTL_MS = 60_000;
|
|
33
|
+
const REQUIRED_LIVE_TOOLS = ['scheduler_create_prd', 'session_manager_help'];
|
|
22
34
|
|
|
23
35
|
/** Read + JSON.parse a file, returning `fallback` on any read/parse failure. */
|
|
24
36
|
function readJsonSafe(absPath, fallback) {
|
|
@@ -29,27 +41,257 @@ function readJsonSafe(absPath, fallback) {
|
|
|
29
41
|
}
|
|
30
42
|
}
|
|
31
43
|
|
|
44
|
+
const SCHEDULER_MCP_FIX_CMD = `claude mcp add ${SCHEDULER_MCP_NAME} --scope user -- node ${SCHEDULER_MCP_SERVER_SCRIPT}`;
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* `ok` must mean "an agent in this project can actually reach a working
|
|
48
|
+
* server", not "some entry named session-manager-scheduler exists somewhere".
|
|
49
|
+
* A user-scope entry whose script path is relative or points at a file that
|
|
50
|
+
* no longer exists (repo moved, worktree deleted) is exactly as dead as no
|
|
51
|
+
* entry at all — the difference is it shows GREEN under a substring check,
|
|
52
|
+
* which is the incident this PRD closes.
|
|
53
|
+
*/
|
|
32
54
|
function checkSchedulerMcp({ cwd, homeDir }) {
|
|
33
55
|
const userConfig = readJsonSafe(path.join(homeDir, '.claude.json'), null);
|
|
34
|
-
const
|
|
56
|
+
const userEntry = userConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
35
57
|
|
|
36
58
|
const projectConfig = readJsonSafe(path.join(cwd, '.mcp.json'), null);
|
|
37
59
|
const projectHas = !!projectConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
38
60
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
61
|
+
if (userEntry) {
|
|
62
|
+
const args = Array.isArray(userEntry.args) ? userEntry.args : [];
|
|
63
|
+
const scriptArg = args[args.length - 1];
|
|
64
|
+
const isAbsolute = typeof scriptArg === 'string' && path.isAbsolute(scriptArg);
|
|
65
|
+
const exists = isAbsolute && fs.existsSync(scriptArg);
|
|
66
|
+
if (!isAbsolute || !exists) {
|
|
67
|
+
return {
|
|
68
|
+
id: 'scheduler-mcp',
|
|
69
|
+
label: 'Scheduler MCP server registered',
|
|
70
|
+
ok: false,
|
|
71
|
+
detail: !isAbsolute
|
|
72
|
+
? `user-scope ${SCHEDULER_MCP_NAME} registration points at a non-absolute script path (${scriptArg})`
|
|
73
|
+
: `user-scope ${SCHEDULER_MCP_NAME} registration points at a script that no longer exists (${scriptArg})`,
|
|
74
|
+
fix: SCHEDULER_MCP_FIX_CMD,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
id: 'scheduler-mcp',
|
|
79
|
+
label: 'Scheduler MCP server registered',
|
|
80
|
+
ok: true,
|
|
81
|
+
detail: `${SCHEDULER_MCP_NAME} registered at user scope (~/.claude.json)`,
|
|
82
|
+
fix: null,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (projectHas) {
|
|
87
|
+
return {
|
|
88
|
+
id: 'scheduler-mcp',
|
|
89
|
+
label: 'Scheduler MCP server registered',
|
|
90
|
+
ok: true,
|
|
91
|
+
detail: `${SCHEDULER_MCP_NAME} registered at project scope (${cwd}/.mcp.json)`,
|
|
92
|
+
fix: null,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
44
95
|
|
|
45
96
|
return {
|
|
46
97
|
id: 'scheduler-mcp',
|
|
47
98
|
label: 'Scheduler MCP server registered',
|
|
48
|
-
ok,
|
|
49
|
-
detail
|
|
50
|
-
fix:
|
|
99
|
+
ok: false,
|
|
100
|
+
detail: `no ${SCHEDULER_MCP_NAME} entry in ~/.claude.json or ${cwd}/.mcp.json`,
|
|
101
|
+
fix: SCHEDULER_MCP_FIX_CMD,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* A project-scope `.mcp.json` entry is never wrong by itself, but once a
|
|
107
|
+
* user-scope entry also exists it is pure risk: `.mcp.json` commonly carries
|
|
108
|
+
* a RELATIVE script path (this repo's own copy did, until this PRD), and a
|
|
109
|
+
* relative path resolves against the process's cwd — which is the worktree
|
|
110
|
+
* root for an Epic, not this repo root — silently shadowing the working
|
|
111
|
+
* user-scope registration with a broken one. Reported as a warning, never a
|
|
112
|
+
* failure: enabling/disabling project-scope servers is the human's call
|
|
113
|
+
* (enableAllProjectMcpServers), not something this probe decides.
|
|
114
|
+
*/
|
|
115
|
+
function checkSchedulerMcpProjectDuplicate({ cwd, homeDir }) {
|
|
116
|
+
const userConfig = readJsonSafe(path.join(homeDir, '.claude.json'), null);
|
|
117
|
+
const userHas = !!userConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
118
|
+
|
|
119
|
+
const projectConfig = readJsonSafe(path.join(cwd, '.mcp.json'), null);
|
|
120
|
+
const projectHas = !!projectConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
121
|
+
|
|
122
|
+
const warn = userHas && projectHas;
|
|
123
|
+
return {
|
|
124
|
+
id: 'scheduler-mcp-project-duplicate',
|
|
125
|
+
label: 'No project-scope duplicate of the scheduler MCP',
|
|
126
|
+
ok: true,
|
|
127
|
+
warn,
|
|
128
|
+
detail: warn
|
|
129
|
+
? `${cwd}/.mcp.json ALSO registers ${SCHEDULER_MCP_NAME} — user scope is canonical; this project entry can shadow it (e.g. via a relative script path resolved against a worktree cwd) instead of adding coverage`
|
|
130
|
+
: `no project-scope duplicate of ${SCHEDULER_MCP_NAME} in ${cwd}/.mcp.json`,
|
|
131
|
+
fix: warn
|
|
132
|
+
? `Remove the ${SCHEDULER_MCP_NAME} entry from ${cwd}/.mcp.json — the user-scope registration already covers every project`
|
|
133
|
+
: null,
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Cache key: a fresh registration signature (command+args+env) always misses. */
|
|
138
|
+
function liveProbeSignature({ command, args, env }) {
|
|
139
|
+
return JSON.stringify({ command, args, env: env || {} });
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const liveProbeCache = new Map(); // signature -> { result: Promise, expiresAt }
|
|
143
|
+
|
|
144
|
+
/** Test-only: forces the next probe for every signature to actually spawn. */
|
|
145
|
+
function clearLiveProbeCache() {
|
|
146
|
+
liveProbeCache.clear();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Spawns the registered command+args with the registered env (via
|
|
151
|
+
* cleanChildEnv/pathWithUserBins — the same env the scheduler gives
|
|
152
|
+
* `claude -p`, so this fails the way a real session would fail, not the way
|
|
153
|
+
* the Electron process would), sends an initialize + tools/list JSON-RPC
|
|
154
|
+
* pair over stdin, and resolves once a tools/list result naming every entry
|
|
155
|
+
* in REQUIRED_LIVE_TOOLS arrives, or the 10s bound is hit. Never rejects,
|
|
156
|
+
* never leaves the child running — every path clears the timer and kills it.
|
|
157
|
+
*/
|
|
158
|
+
function probeSchedulerMcpLive({ command, args, env }) {
|
|
159
|
+
return new Promise((resolve) => {
|
|
160
|
+
let child;
|
|
161
|
+
try {
|
|
162
|
+
child = spawn(command, args, {
|
|
163
|
+
cwd: os.homedir(),
|
|
164
|
+
env: cleanChildEnv({ PATH: pathWithUserBins(), ...(env || {}) }),
|
|
165
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
166
|
+
});
|
|
167
|
+
} catch (err) {
|
|
168
|
+
resolve({ ok: false, detail: err?.message ?? String(err) });
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
let settled = false;
|
|
173
|
+
let stdoutBuf = '';
|
|
174
|
+
let firstStderrLine = null;
|
|
175
|
+
|
|
176
|
+
const finish = (result) => {
|
|
177
|
+
if (settled) return;
|
|
178
|
+
settled = true;
|
|
179
|
+
clearTimeout(timer);
|
|
180
|
+
try { child.kill('SIGKILL'); } catch { /* already gone */ }
|
|
181
|
+
resolve(result);
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
const timer = setTimeout(() => finish({ ok: false, detail: 'timeout' }), LIVE_PROBE_TIMEOUT_MS);
|
|
185
|
+
|
|
186
|
+
child.on('error', (err) => finish({ ok: false, detail: err?.message ?? String(err) }));
|
|
187
|
+
|
|
188
|
+
child.stderr?.on('data', (chunk) => {
|
|
189
|
+
if (firstStderrLine != null) return;
|
|
190
|
+
const line = chunk.toString('utf8').split('\n').find((l) => l.trim());
|
|
191
|
+
if (line) firstStderrLine = line.trim();
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
child.stdout?.on('data', (chunk) => {
|
|
195
|
+
stdoutBuf += chunk.toString('utf8');
|
|
196
|
+
let idx;
|
|
197
|
+
while ((idx = stdoutBuf.indexOf('\n')) !== -1) {
|
|
198
|
+
const line = stdoutBuf.slice(0, idx).trim();
|
|
199
|
+
stdoutBuf = stdoutBuf.slice(idx + 1);
|
|
200
|
+
if (!line) continue;
|
|
201
|
+
let msg;
|
|
202
|
+
try { msg = JSON.parse(line); } catch { continue; }
|
|
203
|
+
if (msg && msg.id === 2) {
|
|
204
|
+
const tools = Array.isArray(msg.result?.tools) ? msg.result.tools.map((t) => t?.name) : null;
|
|
205
|
+
if (!tools) {
|
|
206
|
+
finish({ ok: false, detail: msg.error?.message ? `tools/list error: ${msg.error.message}` : 'tools/list returned no tools array' });
|
|
207
|
+
return;
|
|
208
|
+
}
|
|
209
|
+
const missing = REQUIRED_LIVE_TOOLS.filter((t) => !tools.includes(t));
|
|
210
|
+
if (missing.length > 0) {
|
|
211
|
+
finish({ ok: false, detail: `tools/list did not include: ${missing.join(', ')}` });
|
|
212
|
+
} else {
|
|
213
|
+
finish({ ok: true, detail: `tools/list returned ${tools.length} tool(s) including ${REQUIRED_LIVE_TOOLS.join(', ')}` });
|
|
214
|
+
}
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
child.on('close', () => {
|
|
221
|
+
finish({ ok: false, detail: firstStderrLine || 'process exited before answering tools/list' });
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
const initialize = {
|
|
226
|
+
jsonrpc: '2.0',
|
|
227
|
+
id: 1,
|
|
228
|
+
method: 'initialize',
|
|
229
|
+
params: { protocolVersion: '2024-11-05', capabilities: {}, clientInfo: { name: 'sm-readiness', version: '1' } },
|
|
230
|
+
};
|
|
231
|
+
const toolsList = { jsonrpc: '2.0', id: 2, method: 'tools/list', params: {} };
|
|
232
|
+
child.stdin.write(`${JSON.stringify(initialize)}\n`);
|
|
233
|
+
child.stdin.write(`${JSON.stringify(toolsList)}\n`);
|
|
234
|
+
} catch (err) {
|
|
235
|
+
finish({ ok: false, detail: err?.message ?? String(err) });
|
|
236
|
+
}
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
function probeSchedulerMcpLiveMemoized({ command, args, env }) {
|
|
241
|
+
const sig = liveProbeSignature({ command, args, env });
|
|
242
|
+
const now = Date.now();
|
|
243
|
+
const cached = liveProbeCache.get(sig);
|
|
244
|
+
if (cached && cached.expiresAt > now) return cached.result;
|
|
245
|
+
|
|
246
|
+
const result = probeSchedulerMcpLive({ command, args, env });
|
|
247
|
+
liveProbeCache.set(sig, { result, expiresAt: now + LIVE_PROBE_TTL_MS });
|
|
248
|
+
return result;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Answers "does the server process actually answer", which
|
|
253
|
+
* checkSchedulerMcp's static-config check cannot — a registration can point
|
|
254
|
+
* at a script that spawns and dies on a syntax error, or at `node` when
|
|
255
|
+
* `node` isn't on PATH for a real session's env, and both show GREEN there.
|
|
256
|
+
* Skipped (not failed) when the registration check already failed, so a
|
|
257
|
+
* missing entry produces one red row, not two.
|
|
258
|
+
*/
|
|
259
|
+
async function checkSchedulerMcpLive({ cwd, homeDir, schedulerMcpCheck }) {
|
|
260
|
+
const base = { id: 'scheduler-mcp-live', label: 'Scheduler MCP server answers tools/list' };
|
|
261
|
+
|
|
262
|
+
if (!schedulerMcpCheck.ok) {
|
|
263
|
+
return { ...base, ok: true, skipped: true, detail: 'skipped — scheduler-mcp registration check already failed', fix: null };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ONLY ever spawn the USER-scope entry. A project-scope `.mcp.json` is
|
|
267
|
+
// attacker-controllable (any repo a human points Session Manager at) and
|
|
268
|
+
// Claude Code deliberately gates running it behind the human's own
|
|
269
|
+
// enabledMcpjsonServers/enableAllProjectMcpServers approval — a gate this
|
|
270
|
+
// probe has no way to check. checkDelegationReadiness runs unattended
|
|
271
|
+
// (New Epic banner on mount, session_manager_help), so falling back to a
|
|
272
|
+
// project-scope command here would let a malicious repo's .mcp.json get
|
|
273
|
+
// executed the moment its cwd is merely probed, bypassing that approval
|
|
274
|
+
// entirely. Auto-approving project-scope servers is explicitly out of
|
|
275
|
+
// scope for this probe — see this PRD's "Out of scope".
|
|
276
|
+
const userConfig = readJsonSafe(path.join(homeDir, '.claude.json'), null);
|
|
277
|
+
const entry = userConfig?.mcpServers?.[SCHEDULER_MCP_NAME];
|
|
278
|
+
|
|
279
|
+
if (!entry?.command) {
|
|
280
|
+
return { ...base, ok: true, skipped: true, detail: 'skipped — no user-scope registration to safely run (a project-scope-only .mcp.json entry is not auto-executed)', fix: null };
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
const command = entry.command;
|
|
284
|
+
const args = Array.isArray(entry.args) ? entry.args : [];
|
|
285
|
+
const env = entry.env && typeof entry.env === 'object' ? entry.env : {};
|
|
286
|
+
|
|
287
|
+
const result = await probeSchedulerMcpLiveMemoized({ command, args, env });
|
|
288
|
+
return {
|
|
289
|
+
...base,
|
|
290
|
+
ok: result.ok,
|
|
291
|
+
detail: result.detail,
|
|
292
|
+
fix: result.ok
|
|
51
293
|
? null
|
|
52
|
-
: `
|
|
294
|
+
: `Run \`${command} ${args.join(' ')}\` by hand in a fresh shell to see the startup error directly, or \`claude mcp get ${SCHEDULER_MCP_NAME}\``,
|
|
53
295
|
};
|
|
54
296
|
}
|
|
55
297
|
|
|
@@ -98,39 +340,172 @@ function checkAgentPersonas({ cwd, homeDir }) {
|
|
|
98
340
|
};
|
|
99
341
|
}
|
|
100
342
|
|
|
343
|
+
/**
|
|
344
|
+
* Pull the guard script's path out of a hook `command` string.
|
|
345
|
+
*
|
|
346
|
+
* Commands are `node <script>` (optionally quoted). We only care about the
|
|
347
|
+
* token naming guard-prd-writes — anything else is not this hook.
|
|
348
|
+
*/
|
|
349
|
+
function extractGuardScriptPath(command) {
|
|
350
|
+
const match = String(command).match(/(?:"([^"]*guard-prd-writes\.cjs)"|'([^']*guard-prd-writes\.cjs)'|(\S*guard-prd-writes\.cjs))/);
|
|
351
|
+
if (!match) return null;
|
|
352
|
+
return match[1] || match[2] || match[3] || null;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* `ok` must mean "a hook that will actually run", not "the settings file
|
|
357
|
+
* mentions guard-prd-writes somewhere".
|
|
358
|
+
*
|
|
359
|
+
* The failure mode this guards against: a PreToolUse command that exits
|
|
360
|
+
* non-zero WITHOUT exit code 2 is a non-blocking error, so a hook pointing at
|
|
361
|
+
* a script that doesn't exist in this project silently guards nothing — while
|
|
362
|
+
* a substring match would flip this check green. That is strictly worse than
|
|
363
|
+
* an honest red. So we resolve the command's script path (relative resolves
|
|
364
|
+
* against the project cwd, which is what the harness runs the hook with) and
|
|
365
|
+
* require the file to exist on disk.
|
|
366
|
+
*/
|
|
101
367
|
function checkPrdWriteGuard({ cwd }) {
|
|
102
368
|
const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
|
|
103
369
|
const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
)
|
|
370
|
+
|
|
371
|
+
let mentioned = false;
|
|
372
|
+
let resolvedScript = null;
|
|
373
|
+
for (const matcher of preToolUse) {
|
|
374
|
+
if (!Array.isArray(matcher?.hooks)) continue;
|
|
375
|
+
for (const h of matcher.hooks) {
|
|
376
|
+
if (typeof h?.command !== 'string' || !h.command.includes('guard-prd-writes')) continue;
|
|
377
|
+
mentioned = true;
|
|
378
|
+
const raw = extractGuardScriptPath(h.command);
|
|
379
|
+
if (!raw) continue;
|
|
380
|
+
const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
|
|
381
|
+
if (fs.existsSync(abs)) { resolvedScript = abs; break; }
|
|
382
|
+
}
|
|
383
|
+
if (resolvedScript) break;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
const ok = !!resolvedScript;
|
|
387
|
+
let detail;
|
|
388
|
+
if (ok) detail = `guard-prd-writes PreToolUse hook found in ${cwd}/.claude/settings.json, resolving to ${resolvedScript}`;
|
|
389
|
+
else if (mentioned) detail = `guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json names a script that does not exist — it would silently guard nothing`;
|
|
390
|
+
else detail = `no guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json`;
|
|
108
391
|
|
|
109
392
|
return {
|
|
110
393
|
id: 'prd-write-guard',
|
|
111
394
|
label: 'PRD-write guard hook installed',
|
|
112
395
|
ok,
|
|
113
|
-
detail
|
|
114
|
-
? `guard-prd-writes PreToolUse hook found in ${cwd}/.claude/settings.json`
|
|
115
|
-
: `no guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json`,
|
|
396
|
+
detail,
|
|
116
397
|
fix: ok
|
|
117
398
|
? null
|
|
118
|
-
: `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node
|
|
399
|
+
: `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node ${PRD_WRITE_GUARD_SCRIPT}`,
|
|
400
|
+
// The one check with a sanctioned one-press install (installPrdWriteGuard).
|
|
401
|
+
fixAction: ok ? null : 'install-prd-write-guard',
|
|
119
402
|
};
|
|
120
403
|
}
|
|
121
404
|
|
|
405
|
+
|
|
122
406
|
/**
|
|
123
|
-
*
|
|
407
|
+
* Install the PRD-write guard into `<cwd>/.claude/settings.json`.
|
|
408
|
+
*
|
|
409
|
+
* ── The standardized approach is REFERENCE, not vendor ────────────────────
|
|
410
|
+
* The hook command points at session-manager's own copy of
|
|
411
|
+
* scripts/hooks/guard-prd-writes.cjs by ABSOLUTE path. We deliberately do NOT
|
|
412
|
+
* copy the script into the adopting repo:
|
|
413
|
+
*
|
|
414
|
+
* - Vendoring drifts. It already did, with exactly one adopter
|
|
415
|
+
* (social-signals-trader's copy is missing the 26-line adoption header).
|
|
416
|
+
* - The thing being guarded is `session-manager-operations/scheduler/` —
|
|
417
|
+
* Session Manager's own territory in someone else's repo. The enforcement
|
|
418
|
+
* logic belongs with the owner, not forked into every consumer.
|
|
419
|
+
* - Adopting repos need no Node toolchain of their own; `node` plus this
|
|
420
|
+
* absolute path is the entire dependency.
|
|
421
|
+
*
|
|
422
|
+
* The liability of reference — a silently-no-op hook if the script ever moves
|
|
423
|
+
* — is closed by checkPrdWriteGuard() above, which resolves the command's
|
|
424
|
+
* script path and fails the check when the file is gone.
|
|
425
|
+
*
|
|
426
|
+
* Merges into any existing hooks block rather than clobbering it: other
|
|
427
|
+
* PreToolUse matchers, other hooks under the same matcher, and every unrelated
|
|
428
|
+
* settings key are preserved. Idempotent — a healthy guard is a no-op, and a
|
|
429
|
+
* guard entry pointing at a missing script is repaired in place.
|
|
430
|
+
*/
|
|
431
|
+
async function installPrdWriteGuard({ cwd }) {
|
|
432
|
+
const settingsPath = path.join(cwd, '.claude', 'settings.json');
|
|
433
|
+
// config.cjs's validateWrite only permits `<registered project root>/.claude/**`,
|
|
434
|
+
// and a project the user is merely *pointing* the New Epic dialog at may
|
|
435
|
+
// never have spawned a pty — same cold-boot hazard config.cjs:73-81 already
|
|
436
|
+
// documents for PromptSession archive writes. `cwd` here IS a project root.
|
|
437
|
+
addAllowedRoot(cwd);
|
|
438
|
+
|
|
439
|
+
const existing = fs.existsSync(settingsPath) ? readJsonSafe(settingsPath, undefined) : {};
|
|
440
|
+
if (existing === undefined) {
|
|
441
|
+
// Unparseable settings — refuse rather than silently discarding whatever
|
|
442
|
+
// the human has in there.
|
|
443
|
+
return { ok: false, action: 'error', error: `${settingsPath} is not valid JSON — fix it by hand before installing the guard` };
|
|
444
|
+
}
|
|
445
|
+
const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
|
|
446
|
+
|
|
447
|
+
const command = `node ${PRD_WRITE_GUARD_SCRIPT}`;
|
|
448
|
+
const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
|
|
449
|
+
const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
|
|
450
|
+
|
|
451
|
+
// Already healthy? Nothing to do.
|
|
452
|
+
if (checkPrdWriteGuard({ cwd }).ok) {
|
|
453
|
+
return { ok: true, action: 'already-installed', settingsPath, command };
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Repair a broken entry in place, wherever it sits, before appending.
|
|
457
|
+
let repaired = false;
|
|
458
|
+
for (const matcher of preToolUse) {
|
|
459
|
+
if (!Array.isArray(matcher?.hooks)) continue;
|
|
460
|
+
for (const h of matcher.hooks) {
|
|
461
|
+
if (typeof h?.command === 'string' && h.command.includes('guard-prd-writes')) {
|
|
462
|
+
h.command = command;
|
|
463
|
+
h.type = 'command';
|
|
464
|
+
repaired = true;
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
if (!repaired) {
|
|
470
|
+
const target = preToolUse.find((m) => m?.matcher === PRD_WRITE_GUARD_MATCHER);
|
|
471
|
+
if (target) {
|
|
472
|
+
target.hooks = Array.isArray(target.hooks) ? target.hooks : [];
|
|
473
|
+
target.hooks.push({ type: 'command', command });
|
|
474
|
+
} else {
|
|
475
|
+
preToolUse.push({ matcher: PRD_WRITE_GUARD_MATCHER, hooks: [{ type: 'command', command }] });
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
const next = { ...settings, hooks: { ...hooks, PreToolUse: preToolUse } };
|
|
480
|
+
await writeJson(settingsPath, next);
|
|
481
|
+
|
|
482
|
+
return { ok: true, action: repaired ? 'repaired' : 'installed', settingsPath, command };
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
/**
|
|
486
|
+
* Runs all six delegation-readiness checks for `cwd`. Every filesystem read
|
|
124
487
|
* is wrapped (readJsonSafe / try-catch) so a missing or unparseable file
|
|
125
|
-
* yields ok:false with a detail, never a thrown exception.
|
|
488
|
+
* yields ok:false with a detail, never a thrown exception. Async because
|
|
489
|
+
* scheduler-mcp-live genuinely spawns a process and waits on it (bounded by
|
|
490
|
+
* LIVE_PROBE_TIMEOUT_MS, and memoized per registration signature so repeat
|
|
491
|
+
* callers — the New Epic banner, session_manager_help — don't each pay for
|
|
492
|
+
* a fresh spawn).
|
|
126
493
|
*/
|
|
127
|
-
function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
|
|
494
|
+
async function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
|
|
495
|
+
const schedulerMcpCheck = checkSchedulerMcp({ cwd, homeDir });
|
|
496
|
+
const schedulerMcpLiveCheck = await checkSchedulerMcpLive({ cwd, homeDir, schedulerMcpCheck });
|
|
497
|
+
|
|
498
|
+
// `fixAction: null` / `warn: false` are the defaults — only a check with a
|
|
499
|
+
// sanctioned one-press installer overrides fixAction (today: prd-write-guard),
|
|
500
|
+
// and only scheduler-mcp-project-duplicate ever sets warn.
|
|
128
501
|
const checks = [
|
|
129
|
-
|
|
502
|
+
schedulerMcpCheck,
|
|
503
|
+
schedulerMcpLiveCheck,
|
|
504
|
+
checkSchedulerMcpProjectDuplicate({ cwd, homeDir }),
|
|
130
505
|
checkDevPlugin({ homeDir }),
|
|
131
506
|
checkAgentPersonas({ cwd, homeDir }),
|
|
132
507
|
checkPrdWriteGuard({ cwd }),
|
|
133
|
-
];
|
|
508
|
+
].map((c) => ({ fixAction: null, warn: false, ...c }));
|
|
134
509
|
|
|
135
510
|
return {
|
|
136
511
|
ok: checks.every((c) => c.ok),
|
|
@@ -138,4 +513,11 @@ function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
|
|
|
138
513
|
};
|
|
139
514
|
}
|
|
140
515
|
|
|
141
|
-
module.exports = {
|
|
516
|
+
module.exports = {
|
|
517
|
+
checkDelegationReadiness,
|
|
518
|
+
installPrdWriteGuard,
|
|
519
|
+
probeSchedulerMcpLive,
|
|
520
|
+
clearLiveProbeCache,
|
|
521
|
+
PRD_WRITE_GUARD_SCRIPT,
|
|
522
|
+
PRD_WRITE_GUARD_MATCHER,
|
|
523
|
+
};
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ephemeralCwd.cjs — fail-closed predicate for "this cwd must never receive a
|
|
5
|
+
* written ops-state tree."
|
|
6
|
+
*
|
|
7
|
+
* Verified live 2026-09-01: a worktree cwd slipped past project-discovery and
|
|
8
|
+
* queueStore.writeSplit materialized a per-project scheduler shard INSIDE an
|
|
9
|
+
* ephemeral epic/job worktree
|
|
10
|
+
* (/tmp/session-manager-epic-worktrees/<id>/<epic>/session-manager-operations/
|
|
11
|
+
* scheduler/state/queue.json, rewritten every reconcile pass) plus a stray
|
|
12
|
+
* /tmp/session-manager-operations/logs/ tree. A worktree is torn down when its
|
|
13
|
+
* Epic/job ends, so any state written there is silently destroyed — and while
|
|
14
|
+
* it exists it duplicate-answers "does this project have jobs" for a cwd that
|
|
15
|
+
* isn't really a distinct project.
|
|
16
|
+
*
|
|
17
|
+
* Three independent refusal conditions, any one is enough:
|
|
18
|
+
* - cwd IS os.tmpdir() itself (exact match — the bare-root case behind the
|
|
19
|
+
* stray /tmp/session-manager-operations/logs/ tree).
|
|
20
|
+
* - cwd resolves inside one of the two MANAGED worktree roots
|
|
21
|
+
* (gitWorktree.cjs's KIND_CONFIG job/epic roots under os.tmpdir()) — a
|
|
22
|
+
* prefix match here is safe because nothing but managed worktree
|
|
23
|
+
* checkouts is ever created under these two roots (same reasoning
|
|
24
|
+
* activeSessions.cjs's TMP_DROP_ROOTS already relies on).
|
|
25
|
+
* - cwd IS a linked git worktree's own root anywhere on disk —
|
|
26
|
+
* worktreeMainRootOf(cwd) resolves to a DIFFERENT path than cwd itself.
|
|
27
|
+
* (When cwd is itself an ordinary main-tree repo root, worktreeMainRootOf
|
|
28
|
+
* (cwd) resolves back to cwd unchanged, which is not a redirect and must
|
|
29
|
+
* not be refused.)
|
|
30
|
+
*
|
|
31
|
+
* Deliberately NOT a blanket "anywhere under os.tmpdir()" prefix match: this
|
|
32
|
+
* codebase's own scheduler test suite routinely mkdtemp()s a fake project cwd
|
|
33
|
+
* under os.tmpdir() (documented convention — see e.g.
|
|
34
|
+
* scheduler-clear-queue-history.test.cjs's header comment), a legitimate,
|
|
35
|
+
* unrelated use that a blanket match would wrongly refuse.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
const os = require('node:os');
|
|
39
|
+
const path = require('node:path');
|
|
40
|
+
const { worktreeMainRootOf } = require('../../../scripts/lib/activeSessions.cjs');
|
|
41
|
+
const { KIND_CONFIG } = require('./gitWorktree.cjs');
|
|
42
|
+
|
|
43
|
+
const TMPDIR = path.resolve(os.tmpdir());
|
|
44
|
+
const MANAGED_WORKTREE_ROOTS = [KIND_CONFIG.job.root, KIND_CONFIG.epic.root].map((p) => path.resolve(p));
|
|
45
|
+
|
|
46
|
+
function isExactlyTmpdir(absCwd) {
|
|
47
|
+
return absCwd === TMPDIR;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isUnderManagedWorktreeRoot(absCwd) {
|
|
51
|
+
return MANAGED_WORKTREE_ROOTS.some((root) => {
|
|
52
|
+
const rel = path.relative(root, absCwd);
|
|
53
|
+
return rel === '' || (!rel.startsWith('..') && !path.isAbsolute(rel));
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isLinkedWorktreeRoot(absCwd) {
|
|
58
|
+
const mainRoot = worktreeMainRootOf(absCwd);
|
|
59
|
+
return Boolean(mainRoot) && path.resolve(mainRoot) !== absCwd;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* isEphemeralCwd(cwd) → true when `cwd` must never have ops state written
|
|
64
|
+
* into it (tmpdir-resident, or a linked git worktree root). Pure, synchronous,
|
|
65
|
+
* never throws — an unresolvable or non-absolute cwd is treated as NOT
|
|
66
|
+
* ephemeral so this predicate only ever adds refusals, never masks the
|
|
67
|
+
* existing "cwd must be absolute" checks callers already perform.
|
|
68
|
+
*/
|
|
69
|
+
function isEphemeralCwd(cwd) {
|
|
70
|
+
if (!cwd || typeof cwd !== 'string' || !path.isAbsolute(cwd)) return false;
|
|
71
|
+
const absCwd = path.resolve(cwd);
|
|
72
|
+
if (isExactlyTmpdir(absCwd)) return true;
|
|
73
|
+
if (isUnderManagedWorktreeRoot(absCwd)) return true;
|
|
74
|
+
if (isLinkedWorktreeRoot(absCwd)) return true;
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = { isEphemeralCwd };
|
|
@@ -40,7 +40,8 @@ const APPLICATION_SOURCE_PREFIXES = ['src/', 'scripts/', 'plugins/', 'bin/'];
|
|
|
40
40
|
function countPrdsQueued(cwd, epicId, deps = {}) {
|
|
41
41
|
const readdirSync = deps.readdirSync || fs.readdirSync;
|
|
42
42
|
try {
|
|
43
|
-
const
|
|
43
|
+
const { opsPath } = require('./opsOwnership.cjs');
|
|
44
|
+
const dir = opsPath(cwd, 'scheduler', 'epics', epicId, 'prds');
|
|
44
45
|
return readdirSync(dir).filter((name) => name.endsWith('.md')).length;
|
|
45
46
|
} catch {
|
|
46
47
|
return 0;
|
|
@@ -34,6 +34,7 @@ const crypto = require('node:crypto');
|
|
|
34
34
|
const { resolveEpicPrdWriteDir } = require('./prdLocations.cjs');
|
|
35
35
|
const { assertOpsWrite } = require('./opsOwnership.cjs');
|
|
36
36
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
37
|
+
const { mirrorEpicStatus, removeEpicMirror } = require('./epicStatusMirror.cjs');
|
|
37
38
|
// Required as the module object (not destructured) so a test can
|
|
38
39
|
// monkeypatch promptSessionSchema.assertValidPromptSession in place to
|
|
39
40
|
// simulate a corrupted construction — the real construction below is
|
|
@@ -74,7 +75,10 @@ const MINT_AUTHORITIES = Object.freeze([
|
|
|
74
75
|
]);
|
|
75
76
|
|
|
76
77
|
function activeIndexPath(cwd) {
|
|
77
|
-
|
|
78
|
+
// Lazy: opsOwnership → ephemeralCwd → activeSessions; this module is
|
|
79
|
+
// required early by several of those neighbours.
|
|
80
|
+
const { opsPath } = require('./opsOwnership.cjs');
|
|
81
|
+
return opsPath(cwd, 'prompt-sessions', 'active-index.json');
|
|
78
82
|
}
|
|
79
83
|
|
|
80
84
|
function readActiveIndex(cwd) {
|
|
@@ -362,6 +366,13 @@ function ensureEpic(cwd, { goalText, tag, epicId: explicitEpicId, status = 'prop
|
|
|
362
366
|
index.sessions[epicId] = session;
|
|
363
367
|
index.events[epicId] = [firstEvent];
|
|
364
368
|
writeActiveIndex(cwd, index);
|
|
369
|
+
// Status mirror (see epicStatusMirror.cjs's header) — every Epic is born
|
|
370
|
+
// 'proposed', so this is the FIRST mirror write for this id, inside the
|
|
371
|
+
// same withPathLock task the index write above just ran in. Carries the
|
|
372
|
+
// full validated `session` object (not just status) so a later rebuild
|
|
373
|
+
// (activeIndexRebuild.cjs) can reconstruct a complete row from this file
|
|
374
|
+
// alone.
|
|
375
|
+
mirrorEpicStatus(cwd, epicId, { session, status });
|
|
365
376
|
|
|
366
377
|
// Every mint is logged — the trace-back point for "who created this Epic"
|
|
367
378
|
// (see auditLog.cjs).
|
|
@@ -416,6 +427,10 @@ function removeEpic(cwd, epicId) {
|
|
|
416
427
|
delete index.sessions[epicId];
|
|
417
428
|
delete index.events[epicId];
|
|
418
429
|
writeActiveIndex(cwd, index);
|
|
430
|
+
// Undo the mirror written by ensureEpic's mint branch too — otherwise
|
|
431
|
+
// the rolled-back id would linger as an orphan_file (a mirrored status
|
|
432
|
+
// with no index row and no tombstone) in health.cjs's epic_index check.
|
|
433
|
+
removeEpicMirror(cwd, epicId);
|
|
419
434
|
return true;
|
|
420
435
|
});
|
|
421
436
|
}
|
|
@@ -429,6 +444,7 @@ module.exports = {
|
|
|
429
444
|
removeEpic,
|
|
430
445
|
activeIndexPath,
|
|
431
446
|
readActiveIndex,
|
|
447
|
+
writeActiveIndex,
|
|
432
448
|
personaFileExists,
|
|
433
449
|
resolvePersonaPaths,
|
|
434
450
|
// Exported so lib/activeIndexMerge.cjs's renderer-facing merge IPC handler
|