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.
Files changed (32) hide show
  1. package/dist/assets/{TiptapBody-BSRtwtPP.js → TiptapBody-DWdlsmg9.js} +1 -1
  2. package/dist/assets/index-BbI9slj8.js +3198 -0
  3. package/dist/assets/{index-DyLm7GM_.css → index-BxVBtmjA.css} +1 -1
  4. package/dist/index.html +2 -2
  5. package/package.json +1 -1
  6. package/plugins/session-manager-dev/skills/develop/SKILL.md +14 -13
  7. package/src/main/__tests__/prdCreate.test.cjs +54 -15
  8. package/src/main/__tests__/prdSourcePromptIdBackfill.test.cjs +118 -0
  9. package/src/main/__tests__/transcriptsUsageFor.test.cjs +89 -0
  10. package/src/main/__tests__/uniquePrdNumbers.test.cjs +119 -0
  11. package/src/main/config.cjs +17 -0
  12. package/src/main/index.cjs +24 -6
  13. package/src/main/ipcSchemas.cjs +35 -0
  14. package/src/main/lib/__tests__/instanceLock.test.cjs +87 -0
  15. package/src/main/lib/__tests__/projectBriefCore.test.cjs +142 -0
  16. package/src/main/lib/epicMint.cjs +1 -1
  17. package/src/main/lib/instanceLock.cjs +100 -0
  18. package/src/main/lib/prdCreate.cjs +19 -6
  19. package/src/main/lib/prdLocations.cjs +28 -0
  20. package/src/main/lib/projectBriefCore.cjs +176 -0
  21. package/src/main/lib/runClaudeP.cjs +52 -0
  22. package/src/main/lib/schedulerBatch.cjs +36 -4
  23. package/src/main/memoryAggregate.cjs +2 -40
  24. package/src/main/projectBrief.cjs +313 -0
  25. package/src/main/pty.cjs +9 -0
  26. package/src/main/scheduler/prdParser.cjs +28 -4
  27. package/src/main/scheduler.cjs +94 -5
  28. package/src/main/templates/PRD_AUTHORING.md +9 -5
  29. package/src/main/transcripts.cjs +73 -0
  30. package/src/preload/api.d.ts +80 -3
  31. package/src/preload/index.cjs +8 -0
  32. package/dist/assets/index-XGsBHqYu.js +0 -3180
@@ -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
  };
@@ -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 chain-root
409
- * PromptTicket id this PRD was dispatched from. Kept alongside `sourceTabId`
410
- * for referential tracing even when the tab/session no longer resolves. */
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>;
@@ -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 }),