claude-code-session-manager 0.42.0 → 0.44.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/{TiptapBody-7JlgGW3C.js → TiptapBody-CmDMR4ip.js} +1 -1
- package/dist/assets/{index-D4ga1iZ5.js → index-BGEZVIeo.js} +837 -833
- package/dist/index.html +1 -1
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +32 -10
- package/scripts/lib/watchdogHelpers.cjs +1 -0
- package/scripts/mint-epic.cjs +26 -13
- package/scripts/propose-epic.cjs +1 -0
- package/src/main/__tests__/chat-dead-channels.test.cjs +63 -0
- package/src/main/__tests__/epicMint.test.cjs +132 -1
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +81 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +41 -14
- package/src/main/ipcSchemas.cjs +15 -7
- package/src/main/lib/__tests__/sessionSlots.test.cjs +59 -15
- package/src/main/lib/epicMint.cjs +139 -12
- package/src/main/lib/rcaFeedbackHook.cjs +23 -2
- package/src/main/lib/sessionSlots.cjs +74 -9
- package/src/main/runVerify.cjs +33 -6
- package/src/main/scheduler.cjs +33 -88
- package/src/main/templates/PRD_AUTHORING.md +5 -1
- package/src/main/webRemote.cjs +0 -32
- package/src/preload/api.d.ts +10 -2
- package/src/preload/index.cjs +1 -0
package/src/main/scheduler.cjs
CHANGED
|
@@ -86,7 +86,7 @@ const queueOps = require('./queueOps.cjs');
|
|
|
86
86
|
// match ROOT/QUEUE_PATH below since both resolve the same ~/.claude/session-manager
|
|
87
87
|
// home-dir layout.
|
|
88
88
|
const { resolvePrdsDirs, resolvePrdWriteDir, listEpicPrdDirs, listArchivedPrdDirs } = require('./lib/prdLocations.cjs');
|
|
89
|
-
const { ensureEpic, appendPrdCreatedEvent,
|
|
89
|
+
const { ensureEpic, appendPrdCreatedEvent, readActiveIndex } = require('./lib/epicMint.cjs');
|
|
90
90
|
|
|
91
91
|
// ---------- origin session resolution (PRD 832) ----------
|
|
92
92
|
// An Epic IS a tagged claude session — job rows carry the originating
|
|
@@ -3710,6 +3710,15 @@ function registerScheduleHandlers() {
|
|
|
3710
3710
|
// configuration tab.
|
|
3711
3711
|
ipcMain.handle('schedule:session-slots', () => sessionSlots.snapshot());
|
|
3712
3712
|
|
|
3713
|
+
// Home-tab control for the same pool: user-set cap in [0, 10], default 5.
|
|
3714
|
+
// 0 pauses new claude -p launches machine-wide without killing anything
|
|
3715
|
+
// already running. SM_SESSION_SLOTS (if set) still overrides this at read
|
|
3716
|
+
// time — sessionSlots.snapshot().envOverride tells the UI to disable itself.
|
|
3717
|
+
ipcMain.handle('schedule:set-session-slots', validated(schemas.setSessionSlotsSchema, async (data) => {
|
|
3718
|
+
sessionSlots.setCap(data.cap);
|
|
3719
|
+
return sessionSlots.snapshot();
|
|
3720
|
+
}));
|
|
3721
|
+
|
|
3713
3722
|
ipcMain.handle('schedule:health', async () => {
|
|
3714
3723
|
const state = await readQueue();
|
|
3715
3724
|
const runningJobs = [];
|
|
@@ -4178,13 +4187,6 @@ async function init() {
|
|
|
4178
4187
|
|
|
4179
4188
|
// remote — callable from webRemote.cjs without going through IPC.
|
|
4180
4189
|
const remote = {
|
|
4181
|
-
async getState() {
|
|
4182
|
-
const state = await readQueue();
|
|
4183
|
-
await reconcile(state);
|
|
4184
|
-
await writeQueue(state);
|
|
4185
|
-
return buildScheduleStatePayload(state, { withPaths: true });
|
|
4186
|
-
},
|
|
4187
|
-
|
|
4188
4190
|
// `cwd` is optional: prdCreate.cjs's create flow passes the target
|
|
4189
4191
|
// project's cwd explicitly (the file may not exist yet, so there's
|
|
4190
4192
|
// nothing for findPrdDir to search for); the renderer's slug-only IPC
|
|
@@ -4220,33 +4222,12 @@ const remote = {
|
|
|
4220
4222
|
}
|
|
4221
4223
|
},
|
|
4222
4224
|
|
|
4223
|
-
async readLog(slug, runId) {
|
|
4224
|
-
const logPath = path.resolve(path.join(RUNS_DIR, runId, `${slug}.log`));
|
|
4225
|
-
if (!logPath.startsWith(RUNS_DIR + path.sep)) {
|
|
4226
|
-
return { ok: false, error: 'invalid slug or runId' };
|
|
4227
|
-
}
|
|
4228
|
-
try {
|
|
4229
|
-
// realpath resolves symlinks; re-check boundary to block a rogue agent job
|
|
4230
|
-
// that places a symlink inside RUNS_DIR pointing outside the safe root.
|
|
4231
|
-
const real = await fsp.realpath(logPath);
|
|
4232
|
-
if (!real.startsWith(RUNS_DIR + path.sep)) {
|
|
4233
|
-
return { ok: false, error: 'invalid slug or runId' };
|
|
4234
|
-
}
|
|
4235
|
-
const text = await fsp.readFile(real, 'utf8');
|
|
4236
|
-
return { ok: true, text };
|
|
4237
|
-
} catch (e) {
|
|
4238
|
-
return { ok: false, error: e?.message };
|
|
4239
|
-
}
|
|
4240
|
-
},
|
|
4241
|
-
|
|
4242
4225
|
// `cwd` optional — see readPrd's comment above; prdCreate.cjs's create
|
|
4243
4226
|
// flow supplies it (the destination project dir for a brand-new file that
|
|
4244
4227
|
// doesn't exist yet, so findPrdDir would return nothing to write into).
|
|
4245
4228
|
async writePrd(slug, body, cwd) {
|
|
4246
4229
|
let dir;
|
|
4247
4230
|
let epicTrace = null;
|
|
4248
|
-
let epicCreated = false;
|
|
4249
|
-
let epicId = null;
|
|
4250
4231
|
if (cwd) {
|
|
4251
4232
|
// Edit-in-place if this slug already lives anywhere under this project
|
|
4252
4233
|
// (legacy flat dir or any Epic's prds/); otherwise this is a CREATE,
|
|
@@ -4258,29 +4239,37 @@ const remote = {
|
|
|
4258
4239
|
if (candidate && fs.existsSync(candidate)) { dir = d; break; }
|
|
4259
4240
|
}
|
|
4260
4241
|
if (!dir) {
|
|
4242
|
+
// Every PRD must join an EXISTING, already-human-approved Epic —
|
|
4243
|
+
// mintIfMissing:false means this never conjures a new one. Only
|
|
4244
|
+
// Epics born from explicit human intent (New Epic UI, or
|
|
4245
|
+
// /propose-epic + Approve & start) may write PRDs; this write path
|
|
4246
|
+
// is not itself a human-intent gate, so it must not become one by
|
|
4247
|
+
// accident.
|
|
4248
|
+
const { fm } = splitFrontmatter(body);
|
|
4261
4249
|
try {
|
|
4262
|
-
const { fm } = splitFrontmatter(body);
|
|
4263
4250
|
const epic = await ensureEpic(cwd, {
|
|
4264
|
-
goalText: fm.title || slug,
|
|
4265
|
-
tag: fm.tag,
|
|
4266
|
-
// An Epic-conversation dispatch already has its Epic — join it.
|
|
4267
4251
|
// fm.sourcePromptId must be an existing Epic's promptSessionId
|
|
4268
4252
|
// (== its active-index.json sessions key), NOT a
|
|
4269
4253
|
// PromptTicket.id — epicMint.cjs's ensureEpic looks it up via
|
|
4270
4254
|
// `index.sessions[explicitEpicId]` (see epicMint.cjs ~line 73),
|
|
4271
4255
|
// a literal-equality join. Any other id (e.g. a PromptTicket.id)
|
|
4272
|
-
// simply won't match and
|
|
4256
|
+
// simply won't match and this call throws below.
|
|
4273
4257
|
epicId: fm.sourcePromptId,
|
|
4258
|
+
mintIfMissing: false,
|
|
4259
|
+
// Join-only call (mintIfMissing:false, explicit epicId): the
|
|
4260
|
+
// explicitEpicId branch in ensureEpic returns before `source` is
|
|
4261
|
+
// ever read, so this is a no-op today — kept for symmetry/audit
|
|
4262
|
+
// trail if that join path ever grows a source-touch (PRD 902/905).
|
|
4263
|
+
source: { producer: 'scheduler-dispatch', prdSlug: slug },
|
|
4274
4264
|
});
|
|
4275
4265
|
dir = epic.prdDir;
|
|
4276
4266
|
epicTrace = epic.epicId;
|
|
4277
|
-
epicCreated = epic.created === true;
|
|
4278
|
-
epicId = epic.epicId;
|
|
4279
4267
|
} catch (e) {
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4268
|
+
return {
|
|
4269
|
+
ok: false,
|
|
4270
|
+
error: `no existing Epic to join (sourcePromptId=${fm.sourcePromptId ?? 'none'}): ${e?.message ?? 'unknown error'}. `
|
|
4271
|
+
+ 'Create the Epic first via the New Epic UI or /propose-epic + Approve & start, then pass its id as sourcePromptId.',
|
|
4272
|
+
};
|
|
4284
4273
|
}
|
|
4285
4274
|
}
|
|
4286
4275
|
await fsp.mkdir(dir, { recursive: true });
|
|
@@ -4289,30 +4278,11 @@ const remote = {
|
|
|
4289
4278
|
if (dir === PRDS_DIR) ensureDirs();
|
|
4290
4279
|
}
|
|
4291
4280
|
|
|
4292
|
-
//
|
|
4293
|
-
//
|
|
4294
|
-
//
|
|
4295
|
-
// PRD 851: also drop the Epic's active-index.json entry (sessions/events)
|
|
4296
|
-
// so an orphaned seed-prompt-only Epic doesn't linger in the Epics nav.
|
|
4297
|
-
// Gated on epicCreated (never fires when ensureEpic joined an existing
|
|
4298
|
-
// Epic) so a pre-existing Epic's history is never touched.
|
|
4299
|
-
const cleanupEmptyMintedEpic = async () => {
|
|
4300
|
-
if (!epicCreated || !epicId) return;
|
|
4301
|
-
try {
|
|
4302
|
-
const entries = await fsp.readdir(dir);
|
|
4303
|
-
if (entries.length === 0) {
|
|
4304
|
-
await fsp.rmdir(dir);
|
|
4305
|
-
const epicRootDir = path.dirname(dir);
|
|
4306
|
-
const epicRootEntries = await fsp.readdir(epicRootDir);
|
|
4307
|
-
if (epicRootEntries.length === 0) await fsp.rmdir(epicRootDir);
|
|
4308
|
-
}
|
|
4309
|
-
} catch { /* best-effort only */ }
|
|
4310
|
-
try { removeEpic(cwd, epicId); } catch { /* best-effort only */ }
|
|
4311
|
-
};
|
|
4312
|
-
|
|
4281
|
+
// writePrd only ever JOINS an existing Epic now (mintIfMissing:false
|
|
4282
|
+
// above) — it can never mint one, so there is no orphaned-mint case left
|
|
4283
|
+
// to roll back here (contrast the old PRD 825/851 cleanup, removed).
|
|
4313
4284
|
const resolved = safeSlugPathIn(dir, slug);
|
|
4314
4285
|
if (!resolved) {
|
|
4315
|
-
await cleanupEmptyMintedEpic();
|
|
4316
4286
|
return { ok: false, error: 'invalid slug' };
|
|
4317
4287
|
}
|
|
4318
4288
|
try {
|
|
@@ -4324,23 +4294,20 @@ const remote = {
|
|
|
4324
4294
|
// symlink.
|
|
4325
4295
|
const realParent = await fsp.realpath(path.dirname(resolved));
|
|
4326
4296
|
if (realParent !== dir && !realParent.startsWith(dir + path.sep)) {
|
|
4327
|
-
await cleanupEmptyMintedEpic();
|
|
4328
4297
|
return { ok: false, error: 'invalid slug' };
|
|
4329
4298
|
}
|
|
4330
4299
|
const existing = await fsp.lstat(resolved).catch(() => null);
|
|
4331
4300
|
if (existing && existing.isSymbolicLink()) {
|
|
4332
|
-
await cleanupEmptyMintedEpic();
|
|
4333
4301
|
return { ok: false, error: 'invalid slug' };
|
|
4334
4302
|
}
|
|
4335
4303
|
await config.writeTextAtomic(resolved, body, { writer: 'scheduler' });
|
|
4336
4304
|
const stat = await fsp.stat(resolved);
|
|
4337
4305
|
if (epicTrace) {
|
|
4338
|
-
// Best-effort: record the dispatch on the
|
|
4306
|
+
// Best-effort: record the dispatch on the Epic's event chain.
|
|
4339
4307
|
try { await appendPrdCreatedEvent(cwd, epicTrace, slug); } catch { /* trace only */ }
|
|
4340
4308
|
}
|
|
4341
4309
|
return { ok: true, bytesWritten: stat.size };
|
|
4342
4310
|
} catch (e) {
|
|
4343
|
-
await cleanupEmptyMintedEpic();
|
|
4344
4311
|
return { ok: false, error: e?.message ?? 'write failed' };
|
|
4345
4312
|
}
|
|
4346
4313
|
},
|
|
@@ -4378,28 +4345,6 @@ const remote = {
|
|
|
4378
4345
|
// reuses the same allocator the file-based /develop authoring path relies
|
|
4379
4346
|
// on implicitly, rather than re-deriving NN here.
|
|
4380
4347
|
allocateParallelGroup,
|
|
4381
|
-
|
|
4382
|
-
async runNow() {
|
|
4383
|
-
await clearPause('run-now');
|
|
4384
|
-
runDueJobs().catch((e) => logs.writeLine({
|
|
4385
|
-
level: 'error', scope: 'scheduler',
|
|
4386
|
-
message: 'runDueJobs error (remote:run-now)', meta: { error: e?.message },
|
|
4387
|
-
}));
|
|
4388
|
-
return { ok: true };
|
|
4389
|
-
},
|
|
4390
|
-
|
|
4391
|
-
async setConfig(partial) {
|
|
4392
|
-
const cfg = await mutate((state) => {
|
|
4393
|
-
const { supervisor: supPartial, ...rest } = partial;
|
|
4394
|
-
state.config = { ...state.config, ...rest };
|
|
4395
|
-
if (supPartial !== undefined) {
|
|
4396
|
-
state.config.supervisor = { ...(state.config.supervisor ?? {}), ...supPartial };
|
|
4397
|
-
}
|
|
4398
|
-
return state.config;
|
|
4399
|
-
});
|
|
4400
|
-
await rescheduleTimer();
|
|
4401
|
-
return { ok: true, config: cfg };
|
|
4402
|
-
},
|
|
4403
4348
|
};
|
|
4404
4349
|
|
|
4405
4350
|
// Registers the two job-management admin HTTP routes (PRD 689 — moved
|
|
@@ -392,7 +392,11 @@ the user may or may not have open.
|
|
|
392
392
|
|
|
393
393
|
### Fallback: writing the PRD file directly
|
|
394
394
|
|
|
395
|
-
When the app is not running, first
|
|
395
|
+
When the app is not running, first join the EXISTING, already-human-approved Epic you're already
|
|
396
|
+
working inside — `node <session-manager-repo>/scripts/mint-epic.cjs <cwd> <epic-id>`; its last
|
|
397
|
+
stdout line is the prds dir. This only joins; it never creates an Epic, and errors out if
|
|
398
|
+
`<epic-id>` doesn't already exist — get a human to create/approve the Epic first (New Epic UI, or
|
|
399
|
+
`/propose-epic` + Approve & start) if it doesn't. Then write `<NN>-<slug>.md` by hand into that
|
|
396
400
|
`<cwd>/session-manager-operations/scheduler/epics/<epic-id>/prds/` dir (the flat `scheduler/prds/` is RETIRED and auto-archived unexecuted at boot), add `sourcePromptId: <epic-id>` to the frontmatter so the job keeps its Epic linkage, following the frontmatter rules in §6 and the
|
|
397
401
|
body conventions the rest of this guide describes (`# Goal`, `# Acceptance criteria`,
|
|
398
402
|
`# Implementation notes`, `## Engineering standards` inlined verbatim — see `/develop`'s output
|
package/src/main/webRemote.cjs
CHANGED
|
@@ -1054,7 +1054,6 @@ function getDispatchMap() {
|
|
|
1054
1054
|
|
|
1055
1055
|
const { manager: ptyManager } = require('./pty.cjs');
|
|
1056
1056
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
1057
|
-
const scheduler = require('./scheduler.cjs');
|
|
1058
1057
|
const { remote: histRemote } = require('./historyAggregator.cjs');
|
|
1059
1058
|
const chatRunner = require('./chatRunner.cjs');
|
|
1060
1059
|
|
|
@@ -1095,37 +1094,6 @@ function getDispatchMap() {
|
|
|
1095
1094
|
return { ok: true };
|
|
1096
1095
|
},
|
|
1097
1096
|
|
|
1098
|
-
'cmd:schedule:state': async () =>
|
|
1099
|
-
scheduler.remote.getState(),
|
|
1100
|
-
|
|
1101
|
-
'cmd:schedule:read-prd': async (payload) => {
|
|
1102
|
-
const parsed = schemas.scheduleSlug.parse(payload);
|
|
1103
|
-
return scheduler.remote.readPrd(parsed.slug);
|
|
1104
|
-
},
|
|
1105
|
-
|
|
1106
|
-
'cmd:schedule:read-log': async (payload) => {
|
|
1107
|
-
const parsed = schemas.scheduleReadLog.parse(payload);
|
|
1108
|
-
return scheduler.remote.readLog(parsed.slug, parsed.runId);
|
|
1109
|
-
},
|
|
1110
|
-
|
|
1111
|
-
'cmd:schedule:write-prd': async (payload) => {
|
|
1112
|
-
const parsed = schemas.scheduleWritePrd.parse(payload);
|
|
1113
|
-
return scheduler.remote.writePrd(parsed.slug, parsed.body);
|
|
1114
|
-
},
|
|
1115
|
-
|
|
1116
|
-
'cmd:schedule:reset-job': async (payload) => {
|
|
1117
|
-
const parsed = schemas.scheduleSlug.parse(payload);
|
|
1118
|
-
return scheduler.remote.resetJob(parsed.slug);
|
|
1119
|
-
},
|
|
1120
|
-
|
|
1121
|
-
'cmd:schedule:run-now': async () =>
|
|
1122
|
-
scheduler.remote.runNow(),
|
|
1123
|
-
|
|
1124
|
-
'cmd:schedule:set-config': async (payload) => {
|
|
1125
|
-
const parsed = schemas.setConfigSchema.default({}).parse(payload ?? {});
|
|
1126
|
-
return scheduler.remote.setConfig(parsed);
|
|
1127
|
-
},
|
|
1128
|
-
|
|
1129
1097
|
'cmd:history:aggregate': async (payload) => {
|
|
1130
1098
|
const parsed = schemas.historyAggregate.parse(payload);
|
|
1131
1099
|
return histRemote.aggregate(parsed);
|
package/src/preload/api.d.ts
CHANGED
|
@@ -1390,8 +1390,16 @@ export interface SessionManagerAPI {
|
|
|
1390
1390
|
};
|
|
1391
1391
|
schedule: {
|
|
1392
1392
|
state: () => Promise<ScheduleStateSnapshot>;
|
|
1393
|
-
/** Machine-wide claude -p slot pool (sessionSlots.cjs): total/inUse/holders. */
|
|
1394
|
-
sessionSlots: () => Promise<{
|
|
1393
|
+
/** Machine-wide claude -p slot pool (sessionSlots.cjs): total/inUse/holders + [min,max]/default/envOverride. */
|
|
1394
|
+
sessionSlots: () => Promise<{
|
|
1395
|
+
total: number; inUse: number; holders: { owner: string; at: string }[];
|
|
1396
|
+
min: number; max: number; default: number; envOverride: boolean;
|
|
1397
|
+
}>;
|
|
1398
|
+
/** Set the slot pool's user cap (Home tab), [0, 10]; 0 pauses new launches. Ignored if SM_SESSION_SLOTS is set. */
|
|
1399
|
+
setSessionSlots: (cap: number) => Promise<{
|
|
1400
|
+
total: number; inUse: number; holders: { owner: string; at: string }[];
|
|
1401
|
+
min: number; max: number; default: number; envOverride: boolean;
|
|
1402
|
+
}>;
|
|
1395
1403
|
setConfig: (partial: Partial<ScheduleConfig & { supervisor?: Partial<SupervisorConfig> }>) => Promise<{ ok: boolean; config: ScheduleConfig }>;
|
|
1396
1404
|
resetJob: (slug: string) => Promise<{ ok: boolean; error?: string }>;
|
|
1397
1405
|
runNow: () => Promise<{ ok: boolean }>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -254,6 +254,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
254
254
|
schedule: {
|
|
255
255
|
state: () => ipcRenderer.invoke('schedule:state'),
|
|
256
256
|
sessionSlots: () => ipcRenderer.invoke('schedule:session-slots'),
|
|
257
|
+
setSessionSlots: (cap) => ipcRenderer.invoke('schedule:set-session-slots', { cap }),
|
|
257
258
|
setConfig: (partial) => ipcRenderer.invoke('schedule:set-config', partial),
|
|
258
259
|
resetJob: (slug) => ipcRenderer.invoke('schedule:reset-job', { slug }),
|
|
259
260
|
runNow: () => ipcRenderer.invoke('schedule:run-now'),
|