claude-code-session-manager 0.39.4 → 0.40.1
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-BSRtwtPP.js → TiptapBody-DWdlsmg9.js} +1 -1
- package/dist/assets/index-BbI9slj8.js +3198 -0
- package/dist/assets/{index-DyLm7GM_.css → index-BxVBtmjA.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +14 -13
- package/src/main/__tests__/prdCreate.test.cjs +54 -15
- package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +118 -0
- package/src/main/__tests__/transcriptsUsageFor.test.cjs +89 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +119 -0
- package/src/main/config.cjs +17 -0
- package/src/main/index.cjs +24 -6
- package/src/main/ipcSchemas.cjs +35 -0
- package/src/main/lib/__tests__/instanceLock.test.cjs +87 -0
- package/src/main/lib/__tests__/projectBriefCore.test.cjs +142 -0
- package/src/main/lib/epicMint.cjs +1 -1
- package/src/main/lib/instanceLock.cjs +100 -0
- package/src/main/lib/prdCreate.cjs +19 -6
- package/src/main/lib/prdLocations.cjs +28 -0
- package/src/main/lib/projectBriefCore.cjs +176 -0
- package/src/main/lib/runClaudeP.cjs +52 -0
- package/src/main/lib/schedulerBatch.cjs +36 -4
- package/src/main/memoryAggregate.cjs +2 -40
- package/src/main/projectBrief.cjs +313 -0
- package/src/main/pty.cjs +9 -0
- package/src/main/scheduler/prdParser.cjs +28 -4
- package/src/main/scheduler.cjs +94 -5
- package/src/main/templates/PRD_AUTHORING.md +9 -5
- package/src/main/transcripts.cjs +73 -0
- package/src/preload/api.d.ts +80 -3
- package/src/preload/index.cjs +8 -0
- package/dist/assets/index-XGsBHqYu.js +0 -3180
package/src/main/transcripts.cjs
CHANGED
|
@@ -262,6 +262,77 @@ function getBuffer(tabId) {
|
|
|
262
262
|
return sub ? sub.buffer.slice() : [];
|
|
263
263
|
}
|
|
264
264
|
|
|
265
|
+
// Cap on the transcript file we'll fully re-read for a token-usage summary —
|
|
266
|
+
// protects against OOM on a pathologically large transcript. Sessions over
|
|
267
|
+
// this size report null (omitted by the renderer) rather than a partial sum.
|
|
268
|
+
const MAX_USAGE_FILE_BYTES = 64 * 1024 * 1024;
|
|
269
|
+
|
|
270
|
+
/** Map<filePath, { mtimeMs, size, usage: {inputTokens, outputTokens} }> */
|
|
271
|
+
const usageCache = new Map();
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* Sum `usage` events out of one session's JSONL transcript — same
|
|
275
|
+
* classifyLine()/field-name resolution live.ts's ingest uses for its running
|
|
276
|
+
* per-tab totals (input_tokens/output_tokens, snake_case on the wire).
|
|
277
|
+
* Cached by file mtime so repeat calls for an unchanged transcript are a
|
|
278
|
+
* single fs.stat.
|
|
279
|
+
*/
|
|
280
|
+
async function usageForOne(filePath) {
|
|
281
|
+
const stat = await fsp.stat(filePath).catch(() => null);
|
|
282
|
+
if (!stat) return null;
|
|
283
|
+
const cached = usageCache.get(filePath);
|
|
284
|
+
if (cached && cached.mtimeMs === stat.mtimeMs && cached.size === stat.size) {
|
|
285
|
+
return cached.usage;
|
|
286
|
+
}
|
|
287
|
+
if (stat.size > MAX_USAGE_FILE_BYTES) {
|
|
288
|
+
logs.writeLine({
|
|
289
|
+
level: 'warn',
|
|
290
|
+
scope: 'transcripts',
|
|
291
|
+
message: 'usageForOne: transcript too large, skipping',
|
|
292
|
+
meta: { filePath, size: stat.size },
|
|
293
|
+
});
|
|
294
|
+
return null;
|
|
295
|
+
}
|
|
296
|
+
const text = await fsp.readFile(filePath, 'utf8').catch(() => null);
|
|
297
|
+
if (text === null) return null;
|
|
298
|
+
let inputTokens = 0;
|
|
299
|
+
let outputTokens = 0;
|
|
300
|
+
for (const line of text.split('\n')) {
|
|
301
|
+
if (!line) continue;
|
|
302
|
+
let obj;
|
|
303
|
+
try {
|
|
304
|
+
obj = JSON.parse(line);
|
|
305
|
+
} catch {
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
const ev = classifyLine(obj);
|
|
309
|
+
if (!ev || ev.kind !== 'usage') continue;
|
|
310
|
+
const u = ev.data || {};
|
|
311
|
+
inputTokens += u.input_tokens ?? u.inputTokens ?? 0;
|
|
312
|
+
outputTokens += u.output_tokens ?? u.outputTokens ?? 0;
|
|
313
|
+
}
|
|
314
|
+
const usage = { inputTokens, outputTokens };
|
|
315
|
+
usageCache.set(filePath, { mtimeMs: stat.mtimeMs, size: stat.size, usage });
|
|
316
|
+
return usage;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Batched token-usage lookup for the Epics workspace — one IPC call per
|
|
321
|
+
* visible set of Epics rather than a per-row round trip. Returns a map keyed
|
|
322
|
+
* by sessionId; a session with no transcript file yet (or one over the size
|
|
323
|
+
* cap) maps to null so callers can omit it rather than showing "0".
|
|
324
|
+
*/
|
|
325
|
+
async function usageFor(cwd, sessionIds) {
|
|
326
|
+
const out = {};
|
|
327
|
+
await Promise.all(
|
|
328
|
+
sessionIds.map(async (sessionId) => {
|
|
329
|
+
const filePath = transcriptPath(cwd, sessionId);
|
|
330
|
+
out[sessionId] = await usageForOne(filePath);
|
|
331
|
+
}),
|
|
332
|
+
);
|
|
333
|
+
return out;
|
|
334
|
+
}
|
|
335
|
+
|
|
265
336
|
function closeAll() {
|
|
266
337
|
for (const sub of subs.values()) sub.watcher?.close().catch(() => {});
|
|
267
338
|
subs.clear();
|
|
@@ -282,6 +353,7 @@ function registerTranscriptHandlers() {
|
|
|
282
353
|
}));
|
|
283
354
|
ipcMain.handle('transcript:buffer', v(s.transcriptTabId, ({ tabId }) => getBuffer(tabId)));
|
|
284
355
|
ipcMain.handle('transcript:path', v(s.transcriptPath, ({ cwd, sessionUuid }) => transcriptPath(cwd, sessionUuid)));
|
|
356
|
+
ipcMain.handle('transcript:usageFor', v(s.transcriptUsageFor, ({ cwd, sessionIds }) => usageFor(cwd, sessionIds)));
|
|
285
357
|
}
|
|
286
358
|
|
|
287
359
|
module.exports = {
|
|
@@ -293,4 +365,5 @@ module.exports = {
|
|
|
293
365
|
encodeCwd,
|
|
294
366
|
transcriptPath,
|
|
295
367
|
classifyLine,
|
|
368
|
+
usageFor,
|
|
296
369
|
};
|
package/src/preload/api.d.ts
CHANGED
|
@@ -398,16 +398,22 @@ export interface ScheduleJob {
|
|
|
398
398
|
*/
|
|
399
399
|
verifierVerdict?: string;
|
|
400
400
|
/** Per-job values carried in queue.json for dependency checking. */
|
|
401
|
+
/** Explicit cross-PRD ordering (PRD 832): slugs that must complete before
|
|
402
|
+
* this job is eligible. Replaces the retired shared-NN-parallel convention. */
|
|
401
403
|
dependsOn?: string[];
|
|
404
|
+
/** The originating claude session — the Epic's claudeSessionId, resolved
|
|
405
|
+
* from active-index.json at ingest (PRD 832). An Epic IS a tagged session. */
|
|
406
|
+
originSessionId?: string | null;
|
|
402
407
|
/** Originating tab id (PRD frontmatter `sourceTabId`), refreshed from the
|
|
403
408
|
* PRD file on every reconcile. When the PRD was authored from inside a
|
|
404
409
|
* PromptSessionConversation, this equals that PromptSession's own id
|
|
405
410
|
* (its chat key) — lets the Scheduler UI trace a job back to the
|
|
406
411
|
* PromptSession that spawned it. */
|
|
407
412
|
sourceTabId?: string | null;
|
|
408
|
-
/** Originating prompt id (PRD frontmatter `sourcePromptId`) — the
|
|
409
|
-
*
|
|
410
|
-
* for referential tracing even when the tab/session no
|
|
413
|
+
/** Originating prompt id (PRD frontmatter `sourcePromptId`) — the
|
|
414
|
+
* PromptSession (Epic) id this PRD was dispatched from. Kept alongside
|
|
415
|
+
* `sourceTabId` for referential tracing even when the tab/session no
|
|
416
|
+
* longer resolves. */
|
|
411
417
|
sourcePromptId?: string | null;
|
|
412
418
|
}
|
|
413
419
|
|
|
@@ -450,6 +456,9 @@ export interface PrdListItem {
|
|
|
450
456
|
cwd: string;
|
|
451
457
|
estimateMinutes: number | null;
|
|
452
458
|
mtimeMs: number;
|
|
459
|
+
/** PRD frontmatter `sourcePromptId` — the PromptSession (Epic) id this PRD
|
|
460
|
+
* was dispatched from, if any. */
|
|
461
|
+
sourcePromptId?: string | null;
|
|
453
462
|
}
|
|
454
463
|
|
|
455
464
|
export interface SupervisorConfig {
|
|
@@ -843,6 +852,58 @@ export interface MemoryStaleResult {
|
|
|
843
852
|
error: string | null;
|
|
844
853
|
}
|
|
845
854
|
|
|
855
|
+
// ────────────────────────────────────────────── Project Brief (PRD 837)
|
|
856
|
+
// Persisted at <cwd>/session-manager-operations/project-brief/brief.json.
|
|
857
|
+
export type ProjectBriefPinnableBlock = 'what' | 'conventions';
|
|
858
|
+
|
|
859
|
+
export interface ProjectBriefArea {
|
|
860
|
+
name: string;
|
|
861
|
+
files: number;
|
|
862
|
+
note: string;
|
|
863
|
+
epic: string | null;
|
|
864
|
+
heat: number;
|
|
865
|
+
}
|
|
866
|
+
|
|
867
|
+
export interface ProjectBriefScopeEntry {
|
|
868
|
+
when: string;
|
|
869
|
+
kind: 'added' | 'narrowed' | 'decided';
|
|
870
|
+
text: string;
|
|
871
|
+
src: string;
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
export interface ProjectBrief {
|
|
875
|
+
version: number;
|
|
876
|
+
synthesizedAt: string;
|
|
877
|
+
model: string;
|
|
878
|
+
purpose: string;
|
|
879
|
+
what: string[];
|
|
880
|
+
areas: ProjectBriefArea[];
|
|
881
|
+
scope: ProjectBriefScopeEntry[];
|
|
882
|
+
conventions: string[];
|
|
883
|
+
pins: { what: boolean; conventions: boolean };
|
|
884
|
+
pinned: { what: string[] | null; conventions: string[] | null };
|
|
885
|
+
}
|
|
886
|
+
|
|
887
|
+
export interface ProjectBriefSource {
|
|
888
|
+
label: string;
|
|
889
|
+
detail: string;
|
|
890
|
+
mtimeMs: number | null;
|
|
891
|
+
drift: boolean;
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
export interface ProjectBriefGetResult {
|
|
895
|
+
brief: ProjectBrief | null;
|
|
896
|
+
sources: ProjectBriefSource[];
|
|
897
|
+
}
|
|
898
|
+
|
|
899
|
+
export type ProjectBriefRefreshResult =
|
|
900
|
+
| { ok: true; brief: ProjectBrief }
|
|
901
|
+
| { ok: false; error: string };
|
|
902
|
+
|
|
903
|
+
export type ProjectBriefSetPinResult =
|
|
904
|
+
| { ok: true; brief: ProjectBrief }
|
|
905
|
+
| { ok: false; error: string };
|
|
906
|
+
|
|
846
907
|
// ────────────────────────────────────────────── Per-subagent memory
|
|
847
908
|
// Stored at ~/.claude/session-manager/agent-memory/<agentId>.json. Keyed by
|
|
848
909
|
// agent name (the .md filename in ~/.claude/agents/), not by workspace cwd.
|
|
@@ -1092,6 +1153,10 @@ export interface SessionManagerAPI {
|
|
|
1092
1153
|
write: (payload: { tabId: string; data: string }) => void;
|
|
1093
1154
|
resize: (payload: { tabId: string; cols: number; rows: number }) => void;
|
|
1094
1155
|
kill: (tabId: string) => void;
|
|
1156
|
+
/** Subset of `tabIds` (session keys) that currently have a live PTY —
|
|
1157
|
+
* used by the Epics workspace to reconcile Terminal-mode attachment
|
|
1158
|
+
* after a renderer reload (PRD 833 C1). */
|
|
1159
|
+
alive: (tabIds: string[]) => Promise<string[]>;
|
|
1095
1160
|
onData: (tabId: string, handler: (data: string) => void) => () => void;
|
|
1096
1161
|
onExit: (tabId: string, handler: (info: PtyExit) => void) => () => void;
|
|
1097
1162
|
onWriteError: (handler: (ev: WriteErrorEvent) => void) => () => void;
|
|
@@ -1161,6 +1226,10 @@ export interface SessionManagerAPI {
|
|
|
1161
1226
|
closeTab: (tabId: string) => Promise<{ ok: boolean }>;
|
|
1162
1227
|
buffer: (tabId: string) => Promise<TranscriptEvent[]>;
|
|
1163
1228
|
pathFor: (cwd: string, sessionUuid: string) => Promise<string>;
|
|
1229
|
+
/** Batched token-usage totals, one map entry per requested sessionId. A
|
|
1230
|
+
* session with no transcript file yet (or one over the main-process size
|
|
1231
|
+
* cap) maps to null. */
|
|
1232
|
+
usageFor: (cwd: string, sessionIds: string[]) => Promise<Record<string, { inputTokens: number; outputTokens: number } | null>>;
|
|
1164
1233
|
onEvent: (tabId: string, handler: (ev: TranscriptEvent) => void) => () => void;
|
|
1165
1234
|
};
|
|
1166
1235
|
sessions: {
|
|
@@ -1363,6 +1432,14 @@ export interface SessionManagerAPI {
|
|
|
1363
1432
|
/** Deterministic, zero-LLM-cost staleness report. `cwd` (optional) scopes the dead-repo-ref check. */
|
|
1364
1433
|
stale: (workspace?: string, cwd?: string) => Promise<MemoryStaleResult>;
|
|
1365
1434
|
};
|
|
1435
|
+
projectBrief: {
|
|
1436
|
+
/** Read brief.json (or null if none yet) plus cheaply-computed source/drift metadata. Never fires an LLM call. */
|
|
1437
|
+
get: (cwd: string) => Promise<ProjectBriefGetResult>;
|
|
1438
|
+
/** Cost-gated headless synthesis. Acquires a machine session slot first; returns `{ok:false}` if none free or a refresh is already running for this cwd. */
|
|
1439
|
+
refresh: (cwd: string) => Promise<ProjectBriefRefreshResult>;
|
|
1440
|
+
/** Pin/unpin a synthesized block ('what' | 'conventions'), freezing or clearing its frozen copy. */
|
|
1441
|
+
setPin: (cwd: string, block: ProjectBriefPinnableBlock, pinned: boolean) => Promise<ProjectBriefSetPinResult>;
|
|
1442
|
+
};
|
|
1366
1443
|
agentMemory: {
|
|
1367
1444
|
/** List all memory entries for one subagent. Sorted newest first. */
|
|
1368
1445
|
list: (agentId: string) => Promise<AgentMemoryListResult>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -36,6 +36,7 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
36
36
|
write: (payload) => ipcRenderer.send('pty:write', payload),
|
|
37
37
|
resize: (payload) => ipcRenderer.send('pty:resize', payload),
|
|
38
38
|
kill: (tabId) => ipcRenderer.send('pty:kill', tabId),
|
|
39
|
+
alive: (tabIds) => ipcRenderer.invoke('pty:alive', { tabIds }),
|
|
39
40
|
onData: (tabId, handler) => {
|
|
40
41
|
const channel = `pty:data:${tabId}`;
|
|
41
42
|
const listener = (_e, data) => handler(data);
|
|
@@ -121,6 +122,8 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
121
122
|
buffer: (tabId) => ipcRenderer.invoke('transcript:buffer', { tabId }),
|
|
122
123
|
pathFor: (cwd, sessionUuid) =>
|
|
123
124
|
ipcRenderer.invoke('transcript:path', { cwd, sessionUuid }),
|
|
125
|
+
usageFor: (cwd, sessionIds) =>
|
|
126
|
+
ipcRenderer.invoke('transcript:usageFor', { cwd, sessionIds }),
|
|
124
127
|
onEvent: (tabId, handler) => {
|
|
125
128
|
const channel = `transcript:event:${tabId}`;
|
|
126
129
|
const listener = (_e, ev) => handler(ev);
|
|
@@ -313,6 +316,11 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
313
316
|
stale: (workspace, cwd) =>
|
|
314
317
|
ipcRenderer.invoke('memory:stale', { ...(workspace ? { workspace } : {}), ...(cwd ? { cwd } : {}) }),
|
|
315
318
|
},
|
|
319
|
+
projectBrief: {
|
|
320
|
+
get: (cwd) => ipcRenderer.invoke('project-brief:get', { cwd }),
|
|
321
|
+
refresh: (cwd) => ipcRenderer.invoke('project-brief:refresh', { cwd }),
|
|
322
|
+
setPin: (cwd, block, pinned) => ipcRenderer.invoke('project-brief:set-pin', { cwd, block, pinned }),
|
|
323
|
+
},
|
|
316
324
|
agentMemory: {
|
|
317
325
|
list: (agentId) => ipcRenderer.invoke('agent-memory:list', { agentId }),
|
|
318
326
|
get: (agentId, entryId) => ipcRenderer.invoke('agent-memory:get', { agentId, entryId }),
|