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
|
@@ -158,6 +158,97 @@ function checkOpsWrite(absPath, writer) {
|
|
|
158
158
|
function assertOpsWrite(absPath, writer) {
|
|
159
159
|
const verdict = checkOpsWrite(absPath, writer);
|
|
160
160
|
if (!verdict.ok) throw new Error(verdict.error);
|
|
161
|
+
// Ownership is one question; WHERE the ops root lives is another. A writer
|
|
162
|
+
// that bypasses the path helpers below and builds its own absolute path can
|
|
163
|
+
// still aim at an ephemeral project root (a worktree, os.tmpdir()) — refuse
|
|
164
|
+
// that here too so the single-writer gate is also the last line against the
|
|
165
|
+
// worktree-cwd hazard (PRD 1082; incidents 2026-08-30, 2026-09-01).
|
|
166
|
+
const { inOps } = parseOpsPath(absPath);
|
|
167
|
+
if (inOps) {
|
|
168
|
+
const segs = absPath.split(path.sep);
|
|
169
|
+
const idx = segs.lastIndexOf(OPS_ROOT_DIR);
|
|
170
|
+
const projectRoot = segs.slice(0, idx).join(path.sep) || path.sep;
|
|
171
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
172
|
+
if (isEphemeralCwd(projectRoot)) {
|
|
173
|
+
const err = new Error(
|
|
174
|
+
`refusing to write ${OPS_ROOT_DIR}/ state under an ephemeral project root `
|
|
175
|
+
+ `(tmpdir or linked git worktree): "${projectRoot}" — see lib/opsOwnership.cjs resolveProjectRoot`,
|
|
176
|
+
);
|
|
177
|
+
err.ephemeral = true;
|
|
178
|
+
throw err;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
// ─── THE ONE ops-root resolver (PRD 1082) ─────────────────────────────────
|
|
184
|
+
//
|
|
185
|
+
// Every namespace used to compute its own path with a bare
|
|
186
|
+
// `path.join(cwd, 'session-manager-operations', ...)`, each one independently
|
|
187
|
+
// trusting its `cwd`. The worktree / ops-internal cwd hazard therefore had to
|
|
188
|
+
// be fixed one call site at a time (activeSessions 2026-08-30, PRDs 1073,
|
|
189
|
+
// 1074, 1081). This is the single choke point every reader and writer of the
|
|
190
|
+
// ops root goes through instead — normalize what can be normalized, refuse
|
|
191
|
+
// what cannot — so the next stray cwd is caught by construction, not by
|
|
192
|
+
// incident. `scripts/ops-sweep.cjs` lints that no other file in src/main/
|
|
193
|
+
// spells the ops-root literal.
|
|
194
|
+
//
|
|
195
|
+
// Normalization is `activeSessions.projectRootOf`: an ops-internal path is
|
|
196
|
+
// truncated to the project above it, a linked git worktree is mapped to its
|
|
197
|
+
// main tree via the `.git` file's `gitdir:` pointer, and a subdirectory of a
|
|
198
|
+
// git repo resolves to that repo's root. A plain directory with no enclosing
|
|
199
|
+
// `.git` is returned unchanged (byte-identical to the old join). A nested
|
|
200
|
+
// project that IS its own git repo (e.g. Apple/01-Shapes-Foundation) keeps its
|
|
201
|
+
// own root — the walk stops at the first `.git` it meets.
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* resolveProjectRoot(cwd, { opsInternal }) → absolute project root.
|
|
205
|
+
*
|
|
206
|
+
* Throws on a missing/non-string/relative cwd (a caller that cannot name an
|
|
207
|
+
* absolute project has no business touching that project's state), and
|
|
208
|
+
* throws a tagged `err.ephemeral = true` when the normalized root is still
|
|
209
|
+
* inside os.tmpdir() or is a linked worktree root (ephemeralCwd.cjs).
|
|
210
|
+
*
|
|
211
|
+
* `opsInternal` — 'normalize' (default): a cwd inside session-manager-
|
|
212
|
+
* operations/ is truncated up to its project, the reader-friendly behaviour
|
|
213
|
+
* activeSessions already has. 'refuse': throw instead — the fail-closed
|
|
214
|
+
* posture a WRITER wants (queueStore.projectStateDir), since silently
|
|
215
|
+
* redirecting a write target is riskier than refusing it.
|
|
216
|
+
*/
|
|
217
|
+
function resolveProjectRoot(cwd, { opsInternal = 'normalize' } = {}) {
|
|
218
|
+
if (!cwd || typeof cwd !== 'string') throw new Error('resolveProjectRoot: cwd is required');
|
|
219
|
+
if (!path.isAbsolute(cwd)) {
|
|
220
|
+
throw new Error(`resolveProjectRoot: cwd must be an absolute path, got "${cwd}"`);
|
|
221
|
+
}
|
|
222
|
+
if (opsInternal === 'refuse' && cwd.split(path.sep).includes(OPS_ROOT_DIR)) {
|
|
223
|
+
throw new Error(
|
|
224
|
+
`resolveProjectRoot: cwd must be a project root, not a path inside ${OPS_ROOT_DIR}/, got "${cwd}"`,
|
|
225
|
+
);
|
|
226
|
+
}
|
|
227
|
+
// Lazy: activeSessions → gitWorktree → (lazily) config.cjs → this module.
|
|
228
|
+
const { projectRootOf } = require('../../../scripts/lib/activeSessions.cjs');
|
|
229
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
230
|
+
const root = projectRootOf(cwd);
|
|
231
|
+
if (isEphemeralCwd(root)) {
|
|
232
|
+
const err = new Error(
|
|
233
|
+
`resolveProjectRoot: refusing ephemeral cwd (tmpdir or linked git worktree), got "${cwd}"`,
|
|
234
|
+
);
|
|
235
|
+
err.ephemeral = true;
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
238
|
+
return root;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/** resolveOpsRoot(cwd, opts) → `<projectRoot>/session-manager-operations`. */
|
|
242
|
+
function resolveOpsRoot(cwd, opts) {
|
|
243
|
+
return path.join(resolveProjectRoot(cwd, opts), OPS_ROOT_DIR);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
/**
|
|
247
|
+
* opsPath(cwd, ...segments) → `<projectRoot>/session-manager-operations/<segments>`.
|
|
248
|
+
* The only sanctioned way to build a path inside a project's ops root.
|
|
249
|
+
*/
|
|
250
|
+
function opsPath(cwd, ...segments) {
|
|
251
|
+
return path.join(resolveOpsRoot(cwd), ...segments);
|
|
161
252
|
}
|
|
162
253
|
|
|
163
254
|
module.exports = {
|
|
@@ -167,4 +258,7 @@ module.exports = {
|
|
|
167
258
|
parseOpsPath,
|
|
168
259
|
checkOpsWrite,
|
|
169
260
|
assertOpsWrite,
|
|
261
|
+
resolveProjectRoot,
|
|
262
|
+
resolveOpsRoot,
|
|
263
|
+
opsPath,
|
|
170
264
|
};
|
|
@@ -29,6 +29,9 @@ const { schemas } = require('../ipcSchemas.cjs');
|
|
|
29
29
|
const { readBody, sendJson } = require('./localAdminHttp.cjs');
|
|
30
30
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
31
31
|
const queueOps = require('../queueOps.cjs');
|
|
32
|
+
const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./mcpToolCatalog.cjs');
|
|
33
|
+
const { checkDelegationReadiness } = require('./delegationReadiness.cjs');
|
|
34
|
+
const { resolveProjectContext } = require('./projectRootResolve.cjs');
|
|
32
35
|
|
|
33
36
|
function parseJsonBody(raw) {
|
|
34
37
|
try {
|
|
@@ -45,6 +48,41 @@ function parseJsonBody(raw) {
|
|
|
45
48
|
* scheduler.cjs's own registerAdminRoute(s) pattern.
|
|
46
49
|
*/
|
|
47
50
|
function registerAdminRoute(adminHttp, remote) {
|
|
51
|
+
// GET /admin/mcp/catalog — read-only surface for mcpToolCatalog.cjs, the
|
|
52
|
+
// single source of truth scheduler-mcp-server.cjs's tool descriptions are
|
|
53
|
+
// now composed from (PRD: agent-facing help + Home-tab surface). No
|
|
54
|
+
// `remote` dependency; kept in this file only because it registers
|
|
55
|
+
// "alongside the existing PRD admin routes".
|
|
56
|
+
adminHttp.registerRoute('GET', '/admin/mcp/catalog', async (req, res) => {
|
|
57
|
+
sendJson(res, 200, { ok: true, tools: MCP_TOOL_CATALOG, recipes: MCP_RECIPES });
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
// GET /admin/mcp/readiness?cwd=... — session_manager_help's live half: the
|
|
61
|
+
// catalog above is static and always answerable in-process, but "is this
|
|
62
|
+
// MCP server actually wired up for this project" needs the same four
|
|
63
|
+
// checks delegationReadiness.cjs already runs for the New Epic readiness
|
|
64
|
+
// banner. `cwd` is a required query param (this server has no notion of
|
|
65
|
+
// "the" project — one admin API instance serves every open tab/project).
|
|
66
|
+
adminHttp.registerRoute('GET', '/admin/mcp/readiness', async (req, res, query) => {
|
|
67
|
+
let input;
|
|
68
|
+
try {
|
|
69
|
+
input = schemas.delegationReadinessCwd.parse(Object.fromEntries(query ?? []));
|
|
70
|
+
} catch (e) {
|
|
71
|
+
sendJson(res, 400, { ok: false, error: 'invalid query', details: e?.issues ?? e?.message });
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
// Normalize a worktree/ops-internal cwd to its real project root first —
|
|
75
|
+
// session_manager_help called from inside an Epic's worktree must report
|
|
76
|
+
// THAT project's readiness, not a worktree checkout's own (nonexistent)
|
|
77
|
+
// config state. See projectRootResolve.cjs's header.
|
|
78
|
+
const resolved = resolveProjectContext({ cwd: input.cwd });
|
|
79
|
+
const result = await checkDelegationReadiness({ cwd: resolved.cwd || input.cwd });
|
|
80
|
+
// `ok: true` means "the route answered"; `ready`/`checks` carry the
|
|
81
|
+
// actual delegation-readiness verdict — kept distinct so a caller can't
|
|
82
|
+
// mistake "readiness is false" for "the request itself failed".
|
|
83
|
+
sendJson(res, 200, { ok: true, ready: result.ok, checks: result.checks });
|
|
84
|
+
});
|
|
85
|
+
|
|
48
86
|
// GET /admin/scheduler/prds — paginated PRD listing (PRD: 353KB/120s
|
|
49
87
|
// fix). Sort order is stable and explicit: slug ascending via
|
|
50
88
|
// String.localeCompare(..., { numeric: true }) (listPrdsInternal in
|
|
@@ -121,7 +159,7 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
121
159
|
sendJson(res, 400, { ok: false, error: 'invalid payload', details: e?.issues ?? e?.message });
|
|
122
160
|
return;
|
|
123
161
|
}
|
|
124
|
-
const result = await queueOps.archiveMany(input.slugs);
|
|
162
|
+
const result = await queueOps.archiveMany(input.slugs, input.cwd);
|
|
125
163
|
// Audit only the slugs that actually archived — archiveMany's `results`
|
|
126
164
|
// array carries the real per-slug outcome; logging every requested slug
|
|
127
165
|
// regardless of whether it succeeded would misrepresent a partial-failure
|
|
@@ -147,9 +185,11 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
147
185
|
sendJson(res, 400, { ok: false, error: 'invalid payload', details: e?.issues ?? e?.message });
|
|
148
186
|
return;
|
|
149
187
|
}
|
|
150
|
-
const result = await remote.cancelJob(input.slug);
|
|
188
|
+
const result = await remote.cancelJob(input.slug, { cwd: input.cwd });
|
|
151
189
|
if (!result.ok) {
|
|
152
|
-
const status =
|
|
190
|
+
const status = result.error === 'invalid slug' ? 400
|
|
191
|
+
: /unknown slug|not found/i.test(result.error ?? '') ? 404
|
|
192
|
+
: 409;
|
|
153
193
|
sendJson(res, status, result);
|
|
154
194
|
return;
|
|
155
195
|
}
|
|
@@ -25,6 +25,7 @@ const { expandHome } = require('./expandHome.cjs');
|
|
|
25
25
|
const { readBody, sendJson } = require('./localAdminHttp.cjs');
|
|
26
26
|
const { readActiveIndex } = require('./epicMint.cjs');
|
|
27
27
|
const { appendAuditEvent } = require('./auditLog.cjs');
|
|
28
|
+
const { resolveProjectContext } = require('./projectRootResolve.cjs');
|
|
28
29
|
|
|
29
30
|
const STANDARDS_PATH = path.join(
|
|
30
31
|
__dirname, '..', '..', '..',
|
|
@@ -138,17 +139,41 @@ function resolveSourcePromptIdFromClaudeSession(cwd, claudeSessionId) {
|
|
|
138
139
|
* (400/409/500), left to the caller since only the HTTP route needs it.
|
|
139
140
|
*/
|
|
140
141
|
async function createPrd(input, remote) {
|
|
141
|
-
// cwd is untrusted
|
|
142
|
-
//
|
|
143
|
-
//
|
|
144
|
-
//
|
|
145
|
-
//
|
|
146
|
-
//
|
|
147
|
-
//
|
|
148
|
-
//
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
const
|
|
142
|
+
// cwd is untrusted AND, since this PRD, optional — a caller inside an
|
|
143
|
+
// Epic's git worktree naturally passes its worktree pwd (or omits cwd
|
|
144
|
+
// entirely and relies on originClaudeSessionId/sourcePromptId). Resolve the
|
|
145
|
+
// real project + owning Epic server-side BEFORE any validation: a worktree
|
|
146
|
+
// cwd currently PASSES config.validatePath and then reads the WRONG
|
|
147
|
+
// active-index.json (a git-tracked snapshot frozen at branch time), which
|
|
148
|
+
// is why "the Epic got lost" reports keep recurring even though the Epic
|
|
149
|
+
// never left main's index. See projectRootResolve.cjs's header.
|
|
150
|
+
const rawCwd = input.cwd ? expandHome(input.cwd) : null;
|
|
151
|
+
const originProjectRoot = input.originProjectRoot ? expandHome(input.originProjectRoot) : null;
|
|
152
|
+
const resolved = resolveProjectContext({
|
|
153
|
+
cwd: rawCwd,
|
|
154
|
+
originClaudeSessionId: input.originClaudeSessionId,
|
|
155
|
+
// Only ask the resolver to hunt for the project BY Epic id when no cwd
|
|
156
|
+
// (nor a trusted SM_PROJECT_ROOT hint) was supplied at all — an explicit
|
|
157
|
+
// cwd that merely needs worktree/ops-path normalization is already fixed
|
|
158
|
+
// by projectRootOf above, so the common already-correct-cwd call (cwd +
|
|
159
|
+
// sourcePromptId both supplied) never pays for a cross-project scan.
|
|
160
|
+
epicId: (rawCwd || originProjectRoot) ? undefined : input.sourcePromptId,
|
|
161
|
+
originProjectRoot,
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (!resolved.cwd) {
|
|
165
|
+
return {
|
|
166
|
+
ok: false,
|
|
167
|
+
status: 400,
|
|
168
|
+
error: 'cwd is required — pass cwd explicitly, or originClaudeSessionId/sourcePromptId that resolves '
|
|
169
|
+
+ 'to an existing, already-approved Epic (create/approve an Epic first)',
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// A resolved Epic's own project cwd always wins over the supplied/hinted
|
|
174
|
+
// one — see resolveProjectContext's header. `cwd` below is that final,
|
|
175
|
+
// already-normalized (worktree -> main tree) value.
|
|
176
|
+
const cwd = resolved.cwd;
|
|
152
177
|
let realCwd;
|
|
153
178
|
try {
|
|
154
179
|
realCwd = config.validatePath(cwd);
|
|
@@ -164,9 +189,16 @@ async function createPrd(input, remote) {
|
|
|
164
189
|
config.addAllowedRoot(realCwd);
|
|
165
190
|
input = { ...input, cwd };
|
|
166
191
|
|
|
167
|
-
if (
|
|
168
|
-
|
|
169
|
-
|
|
192
|
+
if (resolved.epicId) {
|
|
193
|
+
input = { ...input, sourcePromptId: resolved.epicId };
|
|
194
|
+
} else if (!input.sourcePromptId && input.originClaudeSessionId) {
|
|
195
|
+
// Fallback kept for the narrow case a caller passes originClaudeSessionId
|
|
196
|
+
// but resolveProjectContext (which also scans cross-project) somehow
|
|
197
|
+
// missed it — resolveSourcePromptIdFromClaudeSession is the single-cwd
|
|
198
|
+
// primitive resolveProjectContext itself is built from, so this can only
|
|
199
|
+
// ever be a no-op in practice; kept for defense-in-depth, not duplicated logic.
|
|
200
|
+
const fallback = resolveSourcePromptIdFromClaudeSession(cwd, input.originClaudeSessionId);
|
|
201
|
+
if (fallback) input = { ...input, sourcePromptId: fallback };
|
|
170
202
|
}
|
|
171
203
|
|
|
172
204
|
const slug = input.slug || deriveSlugFromTitle(input.title);
|
|
@@ -19,8 +19,14 @@ const fs = require('node:fs');
|
|
|
19
19
|
const path = require('node:path');
|
|
20
20
|
const { activeProjectCwds, allProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
|
|
21
21
|
|
|
22
|
-
const
|
|
23
|
-
|
|
22
|
+
const { opsPath, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
23
|
+
|
|
24
|
+
// Segments UNDER the ops root; the ops root itself is resolved by
|
|
25
|
+
// opsOwnership.opsPath (PRD 1082) — never joined onto a raw cwd here.
|
|
26
|
+
const PRD_SUBPATH = ['scheduler', 'prds'];
|
|
27
|
+
const EPICS_SUBPATH = ['scheduler', 'epics'];
|
|
28
|
+
// Segment count from a project root to an epic's own dir: <ops-root> + EPICS_SUBPATH.
|
|
29
|
+
const EPICS_DEPTH_FROM_PROJECT = 1 + EPICS_SUBPATH.length;
|
|
24
30
|
|
|
25
31
|
/**
|
|
26
32
|
* resolveEpicsRoot(cwd) → `<cwd>/session-manager-operations/scheduler/epics`
|
|
@@ -30,7 +36,7 @@ function resolveEpicsRoot(cwd) {
|
|
|
30
36
|
if (!cwd || typeof cwd !== 'string') {
|
|
31
37
|
throw new Error('resolveEpicsRoot: cwd is required');
|
|
32
38
|
}
|
|
33
|
-
return
|
|
39
|
+
return opsPath(cwd, ...EPICS_SUBPATH);
|
|
34
40
|
}
|
|
35
41
|
|
|
36
42
|
/**
|
|
@@ -69,7 +75,8 @@ function listEpicPrdDirs(cwd) {
|
|
|
69
75
|
* its Epic (the layout every new PRD uses) is still found.
|
|
70
76
|
*/
|
|
71
77
|
function listArchivedPrdDirs(cwd) {
|
|
72
|
-
|
|
78
|
+
let dirs;
|
|
79
|
+
try { dirs = [opsPath(cwd, ...PRD_SUBPATH, '..', 'prds-archived')]; } catch { return []; }
|
|
73
80
|
let root;
|
|
74
81
|
try { root = resolveEpicsRoot(cwd); } catch { return dirs; }
|
|
75
82
|
let entries;
|
|
@@ -91,7 +98,7 @@ function resolvePrdWriteDir(cwd) {
|
|
|
91
98
|
if (!cwd || typeof cwd !== 'string') {
|
|
92
99
|
throw new Error('resolvePrdWriteDir: cwd is required');
|
|
93
100
|
}
|
|
94
|
-
return
|
|
101
|
+
return opsPath(cwd, ...PRD_SUBPATH);
|
|
95
102
|
}
|
|
96
103
|
|
|
97
104
|
/**
|
|
@@ -202,7 +209,7 @@ function deriveEpicIdFromPrdPath(filePath) {
|
|
|
202
209
|
const epicId = path.basename(epicDir);
|
|
203
210
|
if (!epicId || epicId === '.' || epicId === path.sep) return null;
|
|
204
211
|
let projectCwd = path.dirname(epicDir); // epicsRoot (.../scheduler/epics)
|
|
205
|
-
for (let i = 0; i <
|
|
212
|
+
for (let i = 0; i < EPICS_DEPTH_FROM_PROJECT; i++) projectCwd = path.dirname(projectCwd);
|
|
206
213
|
let epicsRoot;
|
|
207
214
|
try { epicsRoot = resolveEpicsRoot(projectCwd); } catch { return null; }
|
|
208
215
|
if (path.join(epicsRoot, epicId, dirName) !== prdsDir) return null;
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* projectRootResolve.cjs — server-side project/Epic resolution for every
|
|
5
|
+
* cwd-accepting ops-root entry point (PRD: worktree-cwd Epic-lookup hazard).
|
|
6
|
+
*
|
|
7
|
+
* Problem this closes. An agent running inside an Epic's git worktree has
|
|
8
|
+
* `pwd` = `/tmp/session-manager-epic-worktrees/<hash>/<epicId>` and naturally
|
|
9
|
+
* passes that as `cwd` to `scheduler_create_prd` / `feedback_open_session`.
|
|
10
|
+
* `active-index.json` (the Epic registry) is git-tracked, so the worktree's
|
|
11
|
+
* copy is a snapshot frozen at branch time — it usually does NOT contain the
|
|
12
|
+
* very Epic the agent is running inside. Verified live 2026-09-01 on
|
|
13
|
+
* starry-night-ships: main's index had 5 rows including the running Epic; the
|
|
14
|
+
* worktree's copy had 1 row and did not contain it, so a naive per-cwd lookup
|
|
15
|
+
* returns null and the write is refused with "create/approve an Epic first" —
|
|
16
|
+
* reading to the human as "the Epic got lost", when it never did.
|
|
17
|
+
*
|
|
18
|
+
* `resolveProjectContext` fixes this at the API boundary, not by trying to
|
|
19
|
+
* keep every cwd-accepting caller worktree-aware individually:
|
|
20
|
+
* 1. Normalizes any supplied cwd through `activeSessions.projectRootOf`
|
|
21
|
+
* (worktree -> main tree, ops-internal path -> project root).
|
|
22
|
+
* 2. When an `originClaudeSessionId` (or explicit `epicId`) is known,
|
|
23
|
+
* searches for the owning Epic first in the normalized cwd's own
|
|
24
|
+
* active-index, then across every project this machine has ever opened
|
|
25
|
+
* (`activeSessions.allProjectCwds`) — because the Epic's OWN project may
|
|
26
|
+
* not even be the cwd the agent guessed (e.g. a worktree whose gitdir
|
|
27
|
+
* pointer is stale/unresolvable, or a cross-project mixup).
|
|
28
|
+
* 3. A resolved Epic's own project cwd (`epicCwd`) always wins over the
|
|
29
|
+
* supplied/hinted cwd when they differ — the Epic is the ground truth.
|
|
30
|
+
*
|
|
31
|
+
* Pure, synchronous, fs-only — no Electron deps — so it can be required from
|
|
32
|
+
* the standalone scheduler-mcp-server.cjs process and from every ops-root
|
|
33
|
+
* path helper a sibling PRD (ops-root-single-resolver) folds in later.
|
|
34
|
+
*/
|
|
35
|
+
|
|
36
|
+
const path = require('node:path');
|
|
37
|
+
const { projectRootOf, allProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
|
|
38
|
+
const { readActiveIndex } = require('./epicMint.cjs');
|
|
39
|
+
const { isEphemeralCwd } = require('./ephemeralCwd.cjs');
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* resolveProjectContext({ cwd, originClaudeSessionId, epicId, originProjectRoot }, deps?)
|
|
43
|
+
* → { cwd, epicId, epicCwd, source }
|
|
44
|
+
*
|
|
45
|
+
* `cwd` — the caller-supplied cwd (may be inside a worktree/ops-internal path,
|
|
46
|
+
* or absent entirely when the caller is relying purely on Epic resolution).
|
|
47
|
+
* `originClaudeSessionId` — the calling session's own claude session id
|
|
48
|
+
* (chatRunner.cjs's SM_CHAT_SESSION_ID / pty.cjs's tabId) — matched against
|
|
49
|
+
* each Epic's `claudeSessionId`.
|
|
50
|
+
* `epicId` — an explicit already-known Epic id (e.g. a caller-supplied
|
|
51
|
+
* `sourcePromptId`) — matched against each Epic's `id`. Either identifier
|
|
52
|
+
* (or both) may be supplied; a match on either counts.
|
|
53
|
+
* `originProjectRoot` — a trusted hint (e.g. SM_PROJECT_ROOT env, stamped by
|
|
54
|
+
* the process that actually knows the real project root) — normalized the
|
|
55
|
+
* same way as `cwd` and preferred over it when both are given, but still
|
|
56
|
+
* loses to a resolved Epic's own `epicCwd`.
|
|
57
|
+
*
|
|
58
|
+
* `source` records which path produced the returned `cwd`/`epicId`:
|
|
59
|
+
* 'cwd' — no Epic identifier given, or none resolved; cwd is
|
|
60
|
+
* just the normalized supplied/hinted value.
|
|
61
|
+
* 'epic-index' — the Epic was found in the (single) normalized
|
|
62
|
+
* base cwd's own active-index.
|
|
63
|
+
* 'cross-project-scan' — the Epic was found only by scanning other known
|
|
64
|
+
* projects (or a duplicate id required tie-breaking).
|
|
65
|
+
*
|
|
66
|
+
* Never throws: an unresolvable cwd/hint/Epic id just yields a smaller
|
|
67
|
+
* result, never an error — refusal is the caller's decision, not this
|
|
68
|
+
* resolver's.
|
|
69
|
+
*/
|
|
70
|
+
function resolveProjectContext({ cwd, originClaudeSessionId, epicId: wantedEpicId, originProjectRoot } = {}, deps = {}) {
|
|
71
|
+
const projectRootOfFn = deps.projectRootOf || projectRootOf;
|
|
72
|
+
const allProjectCwdsFn = deps.allProjectCwds || allProjectCwds;
|
|
73
|
+
const readActiveIndexFn = deps.readActiveIndex || readActiveIndex;
|
|
74
|
+
const isEphemeralCwdFn = deps.isEphemeralCwd || isEphemeralCwd;
|
|
75
|
+
const warn = deps.warn || console.warn;
|
|
76
|
+
|
|
77
|
+
let baseCwd = null;
|
|
78
|
+
if (cwd && typeof cwd === 'string') {
|
|
79
|
+
baseCwd = projectRootOfFn(path.resolve(cwd));
|
|
80
|
+
}
|
|
81
|
+
if (originProjectRoot && typeof originProjectRoot === 'string') {
|
|
82
|
+
const hintRoot = projectRootOfFn(path.resolve(originProjectRoot));
|
|
83
|
+
// A trusted hint outranks the agent-supplied cwd, but a resolved Epic
|
|
84
|
+
// (below) still outranks both.
|
|
85
|
+
if (hintRoot) baseCwd = hintRoot;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const result = { cwd: baseCwd, epicId: null, epicCwd: null, source: 'cwd' };
|
|
89
|
+
|
|
90
|
+
if (!originClaudeSessionId && !wantedEpicId) return result;
|
|
91
|
+
|
|
92
|
+
const candidates = [];
|
|
93
|
+
if (baseCwd) candidates.push(baseCwd);
|
|
94
|
+
for (const p of allProjectCwdsFn()) {
|
|
95
|
+
if (!candidates.includes(p)) candidates.push(p);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const matches = [];
|
|
99
|
+
for (const projectCwd of candidates) {
|
|
100
|
+
const { sessions } = readActiveIndexFn(projectCwd);
|
|
101
|
+
for (const session of Object.values(sessions)) {
|
|
102
|
+
if (!session) continue;
|
|
103
|
+
const idHit = wantedEpicId && session.id === wantedEpicId;
|
|
104
|
+
const claudeHit = originClaudeSessionId && session.claudeSessionId === originClaudeSessionId;
|
|
105
|
+
if (idHit || claudeHit) {
|
|
106
|
+
matches.push({ projectCwd, epicId: session.id, status: session.status ?? null });
|
|
107
|
+
break;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (matches.length === 0) return result;
|
|
113
|
+
|
|
114
|
+
let chosen = matches[0];
|
|
115
|
+
if (matches.length > 1) {
|
|
116
|
+
const score = (m) => (isEphemeralCwdFn(m.projectCwd) ? 0 : 2) + (m.status === 'active' ? 1 : 0);
|
|
117
|
+
chosen = matches.reduce((best, m) => (score(m) > score(best) ? m : best), matches[0]);
|
|
118
|
+
warn(
|
|
119
|
+
`[projectRootResolve] Epic id ${chosen.epicId} matched in multiple projects: `
|
|
120
|
+
+ `${matches.map((m) => m.projectCwd).join(', ')} — choosing ${chosen.projectCwd} `
|
|
121
|
+
+ '(prefers a non-worktree/tmp project with status=active)',
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
result.epicId = chosen.epicId;
|
|
126
|
+
result.epicCwd = chosen.projectCwd;
|
|
127
|
+
result.cwd = chosen.projectCwd;
|
|
128
|
+
result.source = matches.length > 1
|
|
129
|
+
? 'cross-project-scan'
|
|
130
|
+
: (chosen.projectCwd === baseCwd ? 'epic-index' : 'cross-project-scan');
|
|
131
|
+
return result;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
module.exports = { resolveProjectContext };
|
|
@@ -79,6 +79,13 @@ const PromptSessionSchema = z.object({
|
|
|
79
79
|
// Passthrough only as of PRD 1032 — nothing populates this yet; the next
|
|
80
80
|
// PRD in this chain wires Epic worktree minting.
|
|
81
81
|
worktree: EpicWorktreeSchema.optional(),
|
|
82
|
+
// Status-mirror fields (epicStatusMirror.cjs) written onto prompt-sessions/
|
|
83
|
+
// <id>.json alongside the active-index.json row — never onto the index row
|
|
84
|
+
// itself, so a plain PromptSession read from active-index.json normally
|
|
85
|
+
// omits them. Optional here only so activeIndexRebuild.cjs can validate a
|
|
86
|
+
// mirrored file's reconstructed row against this same schema.
|
|
87
|
+
archivedAt: z.string().nullable().optional(),
|
|
88
|
+
indexedAt: z.string().optional(),
|
|
82
89
|
});
|
|
83
90
|
|
|
84
91
|
/**
|
|
@@ -33,12 +33,13 @@ const fsp = require('node:fs/promises');
|
|
|
33
33
|
const path = require('node:path');
|
|
34
34
|
const os = require('node:os');
|
|
35
35
|
const { allProjectCwds, activeProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
|
|
36
|
-
const { assertOpsWrite } = require('./opsOwnership.cjs');
|
|
36
|
+
const { assertOpsWrite, resolveOpsRoot, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
|
|
37
37
|
const { ScheduleJobSchema } = require('./scheduleJobSchema.cjs');
|
|
38
38
|
|
|
39
39
|
const MACHINE_STATE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduler-machine.json');
|
|
40
40
|
const LEGACY_QUEUE_PATH = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans', 'queue.json');
|
|
41
|
-
const
|
|
41
|
+
const OPS_DIRNAME = OPS_ROOT_DIR;
|
|
42
|
+
const STATE_SUBPATH = ['scheduler', 'state'];
|
|
42
43
|
|
|
43
44
|
/**
|
|
44
45
|
* A project's `session-manager-operations/scheduler/state/` directory.
|
|
@@ -60,7 +61,43 @@ function projectStateDir(cwd) {
|
|
|
60
61
|
if (!path.isAbsolute(cwd)) {
|
|
61
62
|
throw new Error(`projectStateDir: cwd must be an absolute path, got "${cwd}"`);
|
|
62
63
|
}
|
|
63
|
-
|
|
64
|
+
// ...and it must not itself sit INSIDE an ops root. Absolute-and-existing
|
|
65
|
+
// was not enough: a transcript records an agent's `cd` into a PRD folder,
|
|
66
|
+
// activeSessions hands that subdirectory over as a project, and this join
|
|
67
|
+
// materializes a second ops root under the first — the doubled
|
|
68
|
+
// `.../prds/session-manager-operations/scheduler/state/queue.json` stubs
|
|
69
|
+
// found in starry-night-ships on 2026-08-30 (14 of them, 8 days' worth).
|
|
70
|
+
// activeSessions.projectRootOf normalizes such a cwd away upstream; this is
|
|
71
|
+
// the fail-closed backstop for every other caller. A queue file below an
|
|
72
|
+
// ops root is always a bug, never a project.
|
|
73
|
+
if (cwd.split(path.sep).includes(OPS_DIRNAME)) {
|
|
74
|
+
throw new Error(
|
|
75
|
+
`projectStateDir: cwd must be a project root, not a path inside ${OPS_DIRNAME}/, got "${cwd}"`,
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
// ...and it must not be an EPHEMERAL cwd (inside os.tmpdir(), or a linked
|
|
79
|
+
// git worktree root) — a worktree is torn down when its Epic/job ends, so
|
|
80
|
+
// any state written there is silently destroyed (verified live 2026-09-01
|
|
81
|
+
// as a scheduler shard rewritten every reconcile pass inside a since-deleted
|
|
82
|
+
// epic worktree). That check, and the worktree → main-tree normalization,
|
|
83
|
+
// live in opsOwnership.resolveOpsRoot (PRD 1082) — the one resolver every
|
|
84
|
+
// ops-root reader/writer goes through. `opsInternal: 'refuse'` keeps this
|
|
85
|
+
// WRITER fail-closed on an ops-internal cwd (the guard above shapes the
|
|
86
|
+
// message; the resolver is the backstop).
|
|
87
|
+
let opsRoot;
|
|
88
|
+
try {
|
|
89
|
+
opsRoot = resolveOpsRoot(cwd, { opsInternal: 'refuse' });
|
|
90
|
+
} catch (e) {
|
|
91
|
+
if (e?.ephemeral) {
|
|
92
|
+
const err = new Error(
|
|
93
|
+
`projectStateDir: refusing ephemeral cwd (tmpdir or linked git worktree), got "${cwd}"`,
|
|
94
|
+
);
|
|
95
|
+
err.ephemeral = true;
|
|
96
|
+
throw err;
|
|
97
|
+
}
|
|
98
|
+
throw e;
|
|
99
|
+
}
|
|
100
|
+
return path.join(opsRoot, ...STATE_SUBPATH);
|
|
64
101
|
}
|
|
65
102
|
|
|
66
103
|
function projectQueuePath(cwd) {
|
|
@@ -71,6 +108,26 @@ function projectHistoryPath(cwd) {
|
|
|
71
108
|
return path.join(projectStateDir(cwd), 'history.jsonl');
|
|
72
109
|
}
|
|
73
110
|
|
|
111
|
+
/**
|
|
112
|
+
* queuePathOrSkip(cwd, context) → the shard path, or null when `cwd` is not a
|
|
113
|
+
* usable project root. projectStateDir fails closed on a relative or
|
|
114
|
+
* ops-internal cwd; a single such job row (or a stale cached cwd) must not
|
|
115
|
+
* abort a whole read or write of every OTHER project's queue, so callers that
|
|
116
|
+
* loop over many cwds log-and-skip instead of throwing.
|
|
117
|
+
*/
|
|
118
|
+
function queuePathOrSkip(cwd, context) {
|
|
119
|
+
try {
|
|
120
|
+
return projectQueuePath(cwd);
|
|
121
|
+
} catch (e) {
|
|
122
|
+
if (e?.ephemeral) {
|
|
123
|
+
console.warn(`[queueStore] ${context}: refusing ephemeral cwd "${cwd}" — ${e.message}`);
|
|
124
|
+
} else {
|
|
125
|
+
console.error(`[queueStore] ${context}: skipping invalid project cwd — ${e?.message}`);
|
|
126
|
+
}
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
74
131
|
function writeJsonAtomicSync(file, value) {
|
|
75
132
|
assertOpsWrite(file, 'scheduler');
|
|
76
133
|
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
@@ -183,7 +240,8 @@ function readMergedSync(opts) {
|
|
|
183
240
|
}
|
|
184
241
|
}
|
|
185
242
|
for (const cwd of stateCwds(opts)) {
|
|
186
|
-
const file =
|
|
243
|
+
const file = queuePathOrSkip(cwd, 'read');
|
|
244
|
+
if (!file) continue;
|
|
187
245
|
try {
|
|
188
246
|
const { jobs, invalid } = shapeJobs(fs.readFileSync(file, 'utf8'), file);
|
|
189
247
|
out.jobs.push(...jobs);
|
|
@@ -212,7 +270,8 @@ async function readMerged(opts) {
|
|
|
212
270
|
}
|
|
213
271
|
}
|
|
214
272
|
for (const cwd of stateCwds(opts)) {
|
|
215
|
-
const file =
|
|
273
|
+
const file = queuePathOrSkip(cwd, 'read');
|
|
274
|
+
if (!file) continue;
|
|
216
275
|
try {
|
|
217
276
|
const { jobs, invalid } = shapeJobs(await fsp.readFile(file, 'utf8'), file);
|
|
218
277
|
out.jobs.push(...jobs);
|
|
@@ -261,12 +320,14 @@ async function writeSplit(state, defaultCwd) {
|
|
|
261
320
|
byCwd.get(cwd).push(job);
|
|
262
321
|
}
|
|
263
322
|
for (const [cwd, jobs] of byCwd) {
|
|
323
|
+
const file = queuePathOrSkip(cwd, 'writeSplit');
|
|
324
|
+
if (!file) continue;
|
|
264
325
|
try {
|
|
265
|
-
await writeJsonAtomic(
|
|
326
|
+
await writeJsonAtomic(file, { jobs });
|
|
266
327
|
} catch (e) {
|
|
267
328
|
// A single unwritable project (deleted repo dir, permissions) must not
|
|
268
329
|
// lose every other project's write.
|
|
269
|
-
console.error(`[queueStore] failed to write ${
|
|
330
|
+
console.error(`[queueStore] failed to write ${file}: ${e?.message}`);
|
|
270
331
|
}
|
|
271
332
|
}
|
|
272
333
|
bustCwdCache();
|
|
@@ -316,7 +377,8 @@ async function migrateLegacyGlobalQueue(defaultCwd) {
|
|
|
316
377
|
}
|
|
317
378
|
let moved = 0;
|
|
318
379
|
for (const [cwd, jobs] of byCwd) {
|
|
319
|
-
const file =
|
|
380
|
+
const file = queuePathOrSkip(cwd, 'legacy split');
|
|
381
|
+
if (!file) continue;
|
|
320
382
|
let existing = [];
|
|
321
383
|
try { existing = shapeJobs(await fsp.readFile(file, 'utf8'), file).jobs; } catch { /* fresh shard */ }
|
|
322
384
|
const have = new Set(existing.map((j) => j.slug));
|
|
@@ -74,7 +74,7 @@ const PREVENTION_HINTS = {
|
|
|
74
74
|
[FAILURE_CLASSES.ALREADY_SHIPPED]:
|
|
75
75
|
"This run found its acceptance criteria already satisfied by a prior commit and correctly made no change, so no commit landed and the verifier returned `pass_no_commit`. This is a stale re-run, not an execution failure — the PRD's `.md` was never moved out of `session-manager-operations/scheduler/prds/` after the work shipped. Archive the PRD into `session-manager-operations/scheduler/prds-archived/` instead of re-queuing or re-running it.",
|
|
76
76
|
[FAILURE_CLASSES.SELF_QUEUE]:
|
|
77
|
-
"This run either invoked /develop or /process-feedback from inside its own headless execution, or backgrounded a long-running command and called ScheduleWakeup to check back later —
|
|
77
|
+
"This run either invoked /develop or /process-feedback from inside its own headless execution, or backgrounded a long-running command (e.g. a verification/test run) and called Monitor, TaskOutput, or ScheduleWakeup to check back later — all are the 'you ARE the executor — never re-queue or self-schedule' anti-pattern. A headless PRD run must perform its own acceptance criteria directly and has no next turn to resume it (standards.md → Execution discipline).",
|
|
78
78
|
[FAILURE_CLASSES.STUCK_LOOP]:
|
|
79
79
|
'Bound every command with `timeout <N> <cmd>` — never leave an unbounded `until`/`while true`/`sleep` poll in a PRD body (PRD_AUTHORING.md loop-hang guidance).',
|
|
80
80
|
[FAILURE_CLASSES.POST_AC_OVERRUN]:
|