claude-code-session-manager 0.50.0 → 0.52.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-Dpf1sF5d.js → TiptapBody-CQtxxn1C.js} +1 -1
- package/dist/assets/{index-DMb9iPd9.css → index-CKP60_3d.css} +1 -1
- package/dist/assets/index-hNso_ykJ.js +3218 -0
- package/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/main/__tests__/prdLocationsArchived.test.cjs +152 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +103 -0
- package/src/main/index.cjs +2 -0
- package/src/main/ipcSchemas.cjs +9 -1
- package/src/main/lib/epicMint.cjs +14 -4
- package/src/main/lib/prdLocations.cjs +40 -2
- package/src/main/projectPages.cjs +65 -0
- package/src/main/scheduler.cjs +108 -8
- package/src/preload/api.d.ts +28 -2
- package/src/preload/index.cjs +3 -0
- package/dist/assets/index-DLWoGvIt.js +0 -3218
package/src/main/scheduler.cjs
CHANGED
|
@@ -85,7 +85,7 @@ const queueOps = require('./queueOps.cjs');
|
|
|
85
85
|
// Plain Node module, no Electron dependency; queuePath/prdsDir defaults already
|
|
86
86
|
// match ROOT/QUEUE_PATH below since both resolve the same ~/.claude/session-manager
|
|
87
87
|
// home-dir layout.
|
|
88
|
-
const { resolvePrdsDirs, resolvePrdWriteDir, listEpicPrdDirs, listArchivedPrdDirs } = require('./lib/prdLocations.cjs');
|
|
88
|
+
const { resolvePrdsDirs, resolveArchivedPrdsDirs, resolvePrdWriteDir, listEpicPrdDirs, listArchivedPrdDirs } = require('./lib/prdLocations.cjs');
|
|
89
89
|
const { ensureEpic, appendPrdCreatedEvent, readActiveIndex } = require('./lib/epicMint.cjs');
|
|
90
90
|
|
|
91
91
|
// ---------- origin session resolution (PRD 832) ----------
|
|
@@ -486,6 +486,35 @@ function candidatePrdsDirs() {
|
|
|
486
486
|
return [PRDS_DIR, ...resolvePrdsDirs()];
|
|
487
487
|
}
|
|
488
488
|
|
|
489
|
+
/**
|
|
490
|
+
* Every `prds-archived/` dir across every project (the sibling-of-source
|
|
491
|
+
* layout archiveCompletedPrd writes into — see that function's comment).
|
|
492
|
+
* Used only by list-prds so the PRDs/Runs tabs can show an Epic's REAL
|
|
493
|
+
* historical PRD count, not just its currently-pending one. Deliberately
|
|
494
|
+
* excludes the flat legacy PRDS_ARCHIVE_DIR (manual `schedule:archive-prd`
|
|
495
|
+
* timestamped-subdir layout) — that path is for a different, unused-by-
|
|
496
|
+
* any-renderer manual archive flow, not Epic-scoped completion archiving.
|
|
497
|
+
*/
|
|
498
|
+
function candidateArchivedPrdsDirs() {
|
|
499
|
+
return resolveArchivedPrdsDirs();
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
/**
|
|
503
|
+
* Resolve an archived PRD's real terminal outcome for `schedule:list-prds`:
|
|
504
|
+
* its live queue.json row (if the job hasn't aged out yet), else its
|
|
505
|
+
* history.jsonl row, else 'completed' — archiveCompletedPrd only ever
|
|
506
|
+
* archives a job whose effective status is 'completed' (a 'failed' job's
|
|
507
|
+
* PRD source stays in the live prds/ dir), so 'completed' is a safe
|
|
508
|
+
* default, not a guess; the live/history lookups are defensive in case
|
|
509
|
+
* that archiving invariant ever changes. Exported as a pure function (no
|
|
510
|
+
* IPC/fs) so this fallback chain is unit-testable without an Electron
|
|
511
|
+
* harness.
|
|
512
|
+
*/
|
|
513
|
+
function resolveArchivedPrdStatus(slug, liveStatusBySlug, histBySlug) {
|
|
514
|
+
const resolved = liveStatusBySlug.get(slug) ?? histBySlug.get(slug)?.status ?? 'completed';
|
|
515
|
+
return resolved === 'failed' ? 'failed' : 'completed';
|
|
516
|
+
}
|
|
517
|
+
|
|
489
518
|
/** The PRD-source directory for a given job cwd (falls back to DEFAULT_PROJECT_CWD). */
|
|
490
519
|
function prdDirForCwd(cwd) {
|
|
491
520
|
return resolvePrdWriteDir(cwd || DEFAULT_PROJECT_CWD);
|
|
@@ -2458,6 +2487,16 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2458
2487
|
return { deferred: false };
|
|
2459
2488
|
}
|
|
2460
2489
|
|
|
2490
|
+
// Mark the job 'investigating' so the Queue UI shows an active status for
|
|
2491
|
+
// the whole probe duration — previously this left the job's persisted
|
|
2492
|
+
// status frozen at 'failed'/'needs_review' the entire time, which read as
|
|
2493
|
+
// "nothing is happening" even though an Opus process was actively running.
|
|
2494
|
+
await mutate((s) => {
|
|
2495
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2496
|
+
if (j) j.status = 'investigating';
|
|
2497
|
+
});
|
|
2498
|
+
await broadcast({ flush: true });
|
|
2499
|
+
|
|
2461
2500
|
const claudeBin = resolveClaudeBin();
|
|
2462
2501
|
const childEnv = cleanChildEnv({ PATH: pathWithUserBins() }); // Homebrew/user bins for macOS
|
|
2463
2502
|
|
|
@@ -2495,6 +2534,14 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2495
2534
|
watchdogs: [deadmanWatchdog],
|
|
2496
2535
|
onExit({ exitCode, error, spawnFailed, safeLog: sl }) {
|
|
2497
2536
|
releaseSlot();
|
|
2537
|
+
// Restore the pre-investigation status now that the probe has exited —
|
|
2538
|
+
// 'investigating' must never be the job's resting state.
|
|
2539
|
+
mutate((s) => {
|
|
2540
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2541
|
+
if (j && j.status === 'investigating') j.status = failedJob.status || 'failed';
|
|
2542
|
+
})
|
|
2543
|
+
.then(() => broadcast({ flush: true }))
|
|
2544
|
+
.catch(() => {});
|
|
2498
2545
|
if (error) {
|
|
2499
2546
|
const errMsg = spawnFailed
|
|
2500
2547
|
? `investigation spawn failed: ${error?.message ?? String(error)}`
|
|
@@ -2541,8 +2588,15 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2541
2588
|
return { deferred: false };
|
|
2542
2589
|
} catch (e) {
|
|
2543
2590
|
// A synchronous throw before onExit is wired (e.g. resolveClaudeBin not found,
|
|
2544
|
-
// openLog failure, spawn setup) must not strand the reserved slot
|
|
2591
|
+
// openLog failure, spawn setup) must not strand the reserved slot, and must
|
|
2592
|
+
// not strand the job on the transient 'investigating' status either.
|
|
2545
2593
|
releaseSlot();
|
|
2594
|
+
mutate((s) => {
|
|
2595
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2596
|
+
if (j && j.status === 'investigating') j.status = failedJob.status || 'failed';
|
|
2597
|
+
})
|
|
2598
|
+
.then(() => broadcast({ flush: true }))
|
|
2599
|
+
.catch(() => {});
|
|
2546
2600
|
throw e;
|
|
2547
2601
|
}
|
|
2548
2602
|
}
|
|
@@ -3167,7 +3221,11 @@ async function maybeLaunchWhenAvailable(state) {
|
|
|
3167
3221
|
*/
|
|
3168
3222
|
async function reapDeadRunningJobs() {
|
|
3169
3223
|
try {
|
|
3170
|
-
|
|
3224
|
+
// Do NOT gate on runningSet: spawnJob()'s finally block unconditionally
|
|
3225
|
+
// deletes a job's slug from runningSet even when the preceding completion
|
|
3226
|
+
// mutate() threw and was swallowed, leaving queue.json stuck at
|
|
3227
|
+
// status:"running" with no slug left in runningSet to trigger reconciliation.
|
|
3228
|
+
// queue.json is the source of truth for which jobs are actually running.
|
|
3171
3229
|
const state = await readQueue();
|
|
3172
3230
|
const dead = [];
|
|
3173
3231
|
for (const j of state.jobs) {
|
|
@@ -3925,7 +3983,9 @@ function registerScheduleHandlers() {
|
|
|
3925
3983
|
ipcMain.handle('schedule:list-prds', async () => {
|
|
3926
3984
|
ensureDirs();
|
|
3927
3985
|
const out = [];
|
|
3928
|
-
|
|
3986
|
+
const seenSlugs = new Set();
|
|
3987
|
+
|
|
3988
|
+
async function readDirInto(dir, { archived }) {
|
|
3929
3989
|
let entries;
|
|
3930
3990
|
try {
|
|
3931
3991
|
entries = await fsp.readdir(dir);
|
|
@@ -3933,15 +3993,21 @@ function registerScheduleHandlers() {
|
|
|
3933
3993
|
if (e?.code !== 'ENOENT') {
|
|
3934
3994
|
logs.writeLine({ level: 'warn', scope: 'scheduler', message: 'list-prds: readdir failed', meta: { dir, error: e?.message } });
|
|
3935
3995
|
}
|
|
3936
|
-
|
|
3996
|
+
return;
|
|
3937
3997
|
}
|
|
3938
3998
|
for (const name of entries) {
|
|
3939
3999
|
if (!name.endsWith('.md') || name.startsWith('.')) continue;
|
|
3940
4000
|
const filePath = path.join(dir, name);
|
|
3941
4001
|
try {
|
|
3942
4002
|
const parsed = await parsePrd(filePath);
|
|
4003
|
+
// A slug can't be both live and archived at once, but a duplicate
|
|
4004
|
+
// slug found in two archive dirs (shouldn't happen — archiving is
|
|
4005
|
+
// a single rename — but is cheap to guard) is skipped rather than
|
|
4006
|
+
// double-counted.
|
|
4007
|
+
if (seenSlugs.has(parsed.slug)) continue;
|
|
4008
|
+
seenSlugs.add(parsed.slug);
|
|
3943
4009
|
const stat = await fsp.stat(filePath);
|
|
3944
|
-
|
|
4010
|
+
const entry = {
|
|
3945
4011
|
slug: parsed.slug,
|
|
3946
4012
|
parallelGroup: parsed.parallelGroup,
|
|
3947
4013
|
title: parsed.title,
|
|
@@ -3950,12 +4016,46 @@ function registerScheduleHandlers() {
|
|
|
3950
4016
|
sourcePromptId: parsed.sourcePromptId,
|
|
3951
4017
|
epicId: parsed.epicId ?? null,
|
|
3952
4018
|
mtimeMs: stat.mtimeMs,
|
|
3953
|
-
|
|
4019
|
+
archived,
|
|
4020
|
+
};
|
|
4021
|
+
out.push(entry);
|
|
3954
4022
|
} catch (e) {
|
|
3955
4023
|
logs.writeLine({ level: 'warn', scope: 'scheduler', message: 'list-prds: skipping unparseable file', meta: { name, error: e?.message } });
|
|
3956
4024
|
}
|
|
3957
4025
|
}
|
|
3958
4026
|
}
|
|
4027
|
+
|
|
4028
|
+
// Live PRDs first, so an archived duplicate (shouldn't exist, but a
|
|
4029
|
+
// stale rename copy is possible) never shadows the still-runnable live
|
|
4030
|
+
// entry.
|
|
4031
|
+
for (const dir of candidatePrdsDirs()) {
|
|
4032
|
+
await readDirInto(dir, { archived: false });
|
|
4033
|
+
}
|
|
4034
|
+
|
|
4035
|
+
const archivedStart = out.length;
|
|
4036
|
+
for (const dir of candidateArchivedPrdsDirs()) {
|
|
4037
|
+
await readDirInto(dir, { archived: true });
|
|
4038
|
+
}
|
|
4039
|
+
|
|
4040
|
+
// Archived PRDs need a status: archiveCompletedPrd (scheduler.cjs) only
|
|
4041
|
+
// ever archives a job whose effective status is 'completed' — a 'failed'
|
|
4042
|
+
// job's PRD source stays in the live prds/ dir (still visible/countable
|
|
4043
|
+
// there already). Still resolve the real job status defensively (live
|
|
4044
|
+
// queue row, falling back to history.jsonl) rather than hard-coding
|
|
4045
|
+
// 'completed', so this stays correct if that archiving invariant ever
|
|
4046
|
+
// changes.
|
|
4047
|
+
if (out.length > archivedStart) {
|
|
4048
|
+
const [state, histBySlug] = await Promise.all([
|
|
4049
|
+
readQueue(),
|
|
4050
|
+
queueHistory.historyTerminalBySlug().catch(() => new Map()),
|
|
4051
|
+
]);
|
|
4052
|
+
const liveStatusBySlug = new Map(state.jobs.map((j) => [j.slug, j.status]));
|
|
4053
|
+
for (let i = archivedStart; i < out.length; i++) {
|
|
4054
|
+
const entry = out[i];
|
|
4055
|
+
entry.archivedStatus = resolveArchivedPrdStatus(entry.slug, liveStatusBySlug, histBySlug);
|
|
4056
|
+
}
|
|
4057
|
+
}
|
|
4058
|
+
|
|
3959
4059
|
out.sort((a, b) => a.slug.localeCompare(b.slug, undefined, { numeric: true }));
|
|
3960
4060
|
return out;
|
|
3961
4061
|
});
|
|
@@ -4380,4 +4480,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
4380
4480
|
});
|
|
4381
4481
|
}
|
|
4382
4482
|
|
|
4383
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields };
|
|
4483
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, writeQueue, reconcile, reconcileSourcePromptId, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, classifySigtermWithCommit, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, commitGuardVerdict, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, registerAdminRoutes, notifyOriginatingTab, isNotifiableTerminalStatus, extractResultTextFromLog, candidatePrdsDirs, candidateArchivedPrdsDirs, resolveArchivedPrdStatus, prdDirForCwd, prdPathForJob, archivedPrdPathForJob, archivedTwinExists, findPrdDir, runPrdMigration, shouldSkipInvestigationForCleanRun, archiveCompletedPrd, retireCompletedSlugs, SCHEDULER_BOOTED_AT, SCHEDULER_CODE_SHA, resetJobFields };
|
package/src/preload/api.d.ts
CHANGED
|
@@ -385,7 +385,7 @@ export interface ScheduleConfig {
|
|
|
385
385
|
schemaVersion: 1;
|
|
386
386
|
}
|
|
387
387
|
|
|
388
|
-
export type ScheduleJobStatus = 'pending' | 'running' | 'completed' | 'failed' | 'needs_review';
|
|
388
|
+
export type ScheduleJobStatus = 'pending' | 'running' | 'investigating' | 'completed' | 'failed' | 'needs_review';
|
|
389
389
|
|
|
390
390
|
export interface ScheduleJobRuntime {
|
|
391
391
|
pid: number;
|
|
@@ -490,6 +490,15 @@ export interface PrdListItem {
|
|
|
490
490
|
sourcePromptId?: string | null;
|
|
491
491
|
/** Owning Epic id, derived from the PRD's directory. See ScheduleJob.epicId. */
|
|
492
492
|
epicId?: string | null;
|
|
493
|
+
/** True when this PRD's source .md was found in a `prds-archived/` dir
|
|
494
|
+
* (its scheduler job already ran to completion) rather than the live
|
|
495
|
+
* `prds/` dir. Archived PRDs have no matching queue.json job row (the
|
|
496
|
+
* row may have aged out into history.jsonl), so consumers must read
|
|
497
|
+
* `archivedStatus` instead of joining on a job to know the outcome. */
|
|
498
|
+
archived?: boolean;
|
|
499
|
+
/** Only present when `archived` is true — the resolved terminal outcome
|
|
500
|
+
* ('completed' vs 'failed') of the job that produced this archived PRD. */
|
|
501
|
+
archivedStatus?: 'completed' | 'failed';
|
|
493
502
|
}
|
|
494
503
|
|
|
495
504
|
export interface SupervisorConfig {
|
|
@@ -959,6 +968,19 @@ export type ProjectBriefUpdateResult =
|
|
|
959
968
|
| { ok: true; brief: ProjectBrief }
|
|
960
969
|
| { ok: false; error: string };
|
|
961
970
|
|
|
971
|
+
// ────────────────────────────────────────────── Project Pages (PRD 929-932)
|
|
972
|
+
export interface ProjectPagesOutput {
|
|
973
|
+
marketing: string;
|
|
974
|
+
feature: string;
|
|
975
|
+
architecture: string;
|
|
976
|
+
generatedAt: string;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
export interface ProjectPagesGetResult {
|
|
980
|
+
/** null when no manifest/output files exist yet — the empty-state signal. */
|
|
981
|
+
output: ProjectPagesOutput | null;
|
|
982
|
+
}
|
|
983
|
+
|
|
962
984
|
// ────────────────────────────────────────────── Per-subagent memory
|
|
963
985
|
// Stored at ~/.claude/session-manager/agent-memory/<agentId>.json. Keyed by
|
|
964
986
|
// agent name (the .md filename in ~/.claude/agents/), not by workspace cwd.
|
|
@@ -1109,7 +1131,7 @@ export interface ChatCreatePrdPayload {
|
|
|
1109
1131
|
/** Originating tab id — used at job completion to route a status prompt back into the tab. */
|
|
1110
1132
|
sourceTabId?: string;
|
|
1111
1133
|
/** User-selected Feature/Bug tag (PRD 774) carried from the originating PromptTicket. */
|
|
1112
|
-
tag?: 'feature' | 'bug' | 'discussion' | 'build';
|
|
1134
|
+
tag?: 'feature' | 'bug' | 'discussion' | 'build' | 'project-home-builder';
|
|
1113
1135
|
}
|
|
1114
1136
|
|
|
1115
1137
|
export type ChatCreatePrdResult =
|
|
@@ -1520,6 +1542,10 @@ export interface SessionManagerAPI {
|
|
|
1520
1542
|
/** Hand-edit brief.json in place — no LLM cost. Edited pinnable blocks are auto-pinned so the next refresh preserves them. */
|
|
1521
1543
|
update: (cwd: string, patch: ProjectBriefPatch) => Promise<ProjectBriefUpdateResult>;
|
|
1522
1544
|
};
|
|
1545
|
+
projectPages: {
|
|
1546
|
+
/** Read output/*.html + manifest.json (or `{output: null}` if none exist yet). Never fires an LLM call. */
|
|
1547
|
+
get: (cwd: string) => Promise<ProjectPagesGetResult>;
|
|
1548
|
+
};
|
|
1523
1549
|
promptSessionTranscript: {
|
|
1524
1550
|
/** Append one full-text turn to an Epic's durable JSONL transcript. Best-effort — resolves `{ok:false}` rather than throwing on failure. */
|
|
1525
1551
|
append: (
|
package/src/preload/index.cjs
CHANGED
|
@@ -330,6 +330,9 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
330
330
|
setPin: (cwd, block, pinned) => ipcRenderer.invoke('project-brief:set-pin', { cwd, block, pinned }),
|
|
331
331
|
update: (cwd, patch) => ipcRenderer.invoke('project-brief:update', { cwd, patch }),
|
|
332
332
|
},
|
|
333
|
+
projectPages: {
|
|
334
|
+
get: (cwd) => ipcRenderer.invoke('project-pages:get', { cwd }),
|
|
335
|
+
},
|
|
333
336
|
promptSessionTranscript: {
|
|
334
337
|
append: (cwd, epicId, turn) =>
|
|
335
338
|
ipcRenderer.invoke('promptSessionTranscript:append', { cwd, epicId, ...turn }),
|