claude-code-session-manager 0.51.0 → 0.53.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-DOVjN6Gc.js → TiptapBody-BYG18cfk.js} +1 -1
- package/dist/assets/{index-BXzDjHNE.css → index-BoOiJJOr.css} +1 -1
- package/dist/assets/index-D1v4VU6k.js +3218 -0
- package/dist/index.html +2 -2
- package/package.json +4 -1
- package/src/main/__tests__/epicMint.test.cjs +110 -0
- package/src/main/__tests__/prdCreate.test.cjs +6 -0
- package/src/main/__tests__/prdLocationsArchived.test.cjs +152 -0
- package/src/main/__tests__/prdMigration.test.cjs +5 -0
- package/src/main/__tests__/scheduler-find-prd-dir.test.cjs +38 -5
- package/src/main/__tests__/scheduler-prd-missing-skip.test.cjs +89 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +103 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +8 -0
- package/src/main/index.cjs +9 -0
- package/src/main/ipcSchemas.cjs +29 -1
- package/src/main/lib/epicMint.cjs +46 -12
- package/src/main/lib/prdLocations.cjs +40 -2
- package/src/main/projectPages.cjs +65 -0
- package/src/main/scheduler.cjs +148 -19
- package/src/preload/api.d.ts +37 -2
- package/src/preload/index.cjs +6 -0
- package/dist/assets/index-DPY5vv68.js +0 -3218
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -313,7 +313,7 @@ const schedulerCreatePrd = z.object({
|
|
|
313
313
|
originClaudeSessionId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
314
314
|
// User-selected Feature/Bug tag (PRD 774) carried from the originating
|
|
315
315
|
// PromptTicket — deterministic, never LLM-classified.
|
|
316
|
-
tag: z.enum(['feature', 'bug', 'discussion', 'build']).optional(),
|
|
316
|
+
tag: z.enum(['feature', 'bug', 'discussion', 'build', 'project-home-builder']).optional(),
|
|
317
317
|
});
|
|
318
318
|
|
|
319
319
|
// Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
|
|
@@ -446,6 +446,13 @@ const PROJECT_BRIEF_BLOCK = z.enum(['what', 'conventions']);
|
|
|
446
446
|
const projectBriefCwd = z.object({
|
|
447
447
|
cwd: z.string().min(1).max(4096),
|
|
448
448
|
}).strict();
|
|
449
|
+
|
|
450
|
+
// ──────────────────────────────────────────── Project Pages (PRD 932)
|
|
451
|
+
// Same validation split as projectBriefCwd above: real path validation is
|
|
452
|
+
// config.cjs's validatePath at first fs access in projectPages.cjs.
|
|
453
|
+
const projectPagesCwd = z.object({
|
|
454
|
+
cwd: z.string().min(1).max(4096),
|
|
455
|
+
}).strict();
|
|
449
456
|
const projectBriefSetPin = z.object({
|
|
450
457
|
cwd: z.string().min(1).max(4096),
|
|
451
458
|
block: PROJECT_BRIEF_BLOCK,
|
|
@@ -486,6 +493,25 @@ const promptSessionTranscriptRead = z.object({
|
|
|
486
493
|
limit: z.number().int().positive().max(10_000).optional(),
|
|
487
494
|
}).strict();
|
|
488
495
|
|
|
496
|
+
// ──────────────────────────────────────────── Audit log (renderer Epic
|
|
497
|
+
// lifecycle trace, PRD 940). Kind allowlist is enforced HERE, main-side — the
|
|
498
|
+
// renderer cannot write arbitrary kinds through this channel, only the six
|
|
499
|
+
// Epic lifecycle transitions it emits from promptSessions.ts.
|
|
500
|
+
const AUDIT_LOG_KIND = z.enum([
|
|
501
|
+
'epic_create',
|
|
502
|
+
'epic_approve',
|
|
503
|
+
'epic_complete',
|
|
504
|
+
'epic_delete',
|
|
505
|
+
'epic_resume',
|
|
506
|
+
'epic_duplicate',
|
|
507
|
+
]);
|
|
508
|
+
const auditLogAppend = z.object({
|
|
509
|
+
kind: AUDIT_LOG_KIND,
|
|
510
|
+
cwd: z.string().min(1).max(4096),
|
|
511
|
+
epicId: z.string().min(1).max(256),
|
|
512
|
+
source: z.string().min(1).max(256),
|
|
513
|
+
}).strict();
|
|
514
|
+
|
|
489
515
|
// ──────────────────────────────────────────── Per-subagent memory
|
|
490
516
|
// Distinct from the workspace-scoped Memory tool: agentMemory is keyed by
|
|
491
517
|
// subagent name (the .md filename in ~/.claude/agents/, e.g. "code-reviewer"),
|
|
@@ -877,10 +903,12 @@ module.exports = {
|
|
|
877
903
|
memoryAggregate,
|
|
878
904
|
memoryStale,
|
|
879
905
|
projectBriefCwd,
|
|
906
|
+
projectPagesCwd,
|
|
880
907
|
projectBriefSetPin,
|
|
881
908
|
projectBriefUpdate,
|
|
882
909
|
promptSessionTranscriptAppend,
|
|
883
910
|
promptSessionTranscriptRead,
|
|
911
|
+
auditLogAppend,
|
|
884
912
|
agentMemoryList,
|
|
885
913
|
agentMemoryGet,
|
|
886
914
|
agentMemorySet,
|
|
@@ -182,9 +182,13 @@ function withPathLock(lockPath, task) {
|
|
|
182
182
|
*
|
|
183
183
|
* `status` defaults to 'proposed' — a fail-safe default so any caller that
|
|
184
184
|
* forgets to pass it files an Epic that waits for human approval rather than
|
|
185
|
-
* one that starts running immediately.
|
|
186
|
-
*
|
|
187
|
-
*
|
|
185
|
+
* one that starts running immediately. Every Epic is BORN 'proposed' — the
|
|
186
|
+
* mint branch below ignores/rejects any other requested status; it is
|
|
187
|
+
* fail-closed, mirroring opsOwnership.cjs's assertOpsWrite. Activation
|
|
188
|
+
* ('proposed' → 'active') happens exactly once, entirely in the renderer
|
|
189
|
+
* store's `approveProposed` (`state/promptSessions.ts`) — that code path
|
|
190
|
+
* never calls ensureEpic(). Joining an already-'active' Epic (this function's
|
|
191
|
+
* join branches, above the mint branch) remains legal and unchanged.
|
|
188
192
|
*
|
|
189
193
|
* `mintIfMissing` defaults to true for the small set of callers that are
|
|
190
194
|
* themselves the human-intent gate (propose-epic, the RCA hook, the feedback
|
|
@@ -208,11 +212,27 @@ function ensureEpic(cwd, { goalText, tag, reuseByGoal = false, epicId: explicitE
|
|
|
208
212
|
const index = readActiveIndex(cwd);
|
|
209
213
|
|
|
210
214
|
// A dispatch that already knows its Epic (sourcePromptId frontmatter from
|
|
211
|
-
// an Epic-conversation dispatch) joins it rather than minting a sibling
|
|
215
|
+
// an Epic-conversation dispatch) joins it rather than minting a sibling —
|
|
216
|
+
// but only while that Epic is still open. Joining unconditionally here
|
|
217
|
+
// let a stale/hallucinated sourcePromptId silently attach a PRD (and its
|
|
218
|
+
// follow-on events/chat activity) to an unrelated or even completed
|
|
219
|
+
// Epic — the "this session ran again without my knowledge, and it was
|
|
220
|
+
// really another Epic's prompt" cross-contamination bug. Mirrors
|
|
221
|
+
// findJoinableEpicInIndex()'s preferEpicId open-check (line ~124).
|
|
212
222
|
if (explicitEpicId && hasOwn(index.sessions, explicitEpicId)) {
|
|
213
|
-
const
|
|
214
|
-
|
|
215
|
-
|
|
223
|
+
const preferred = index.sessions[explicitEpicId];
|
|
224
|
+
if (preferred && (preferred.status === 'proposed' || preferred.status === 'active')) {
|
|
225
|
+
const prdDir = resolveEpicPrdWriteDir(cwd, explicitEpicId);
|
|
226
|
+
fs.mkdirSync(prdDir, { recursive: true });
|
|
227
|
+
return { epicId: explicitEpicId, prdDir, created: false };
|
|
228
|
+
}
|
|
229
|
+
console.warn(`[epicMint] ensureEpic: explicit epicId ${explicitEpicId} exists but is not open (status=${preferred?.status ?? 'unknown'}) — refusing to join, falling through`);
|
|
230
|
+
appendAuditEvent('epic_mint_refused', {
|
|
231
|
+
cwd,
|
|
232
|
+
epicId: explicitEpicId,
|
|
233
|
+
status,
|
|
234
|
+
reason: `explicit epicId exists but is not open (status=${preferred?.status ?? 'unknown'}) — refusing to join, falling through to mint`,
|
|
235
|
+
});
|
|
216
236
|
}
|
|
217
237
|
|
|
218
238
|
if (reuseByGoal) {
|
|
@@ -241,11 +261,11 @@ function ensureEpic(cwd, { goalText, tag, reuseByGoal = false, epicId: explicitE
|
|
|
241
261
|
}
|
|
242
262
|
|
|
243
263
|
if (!mintIfMissing) {
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
+ '
|
|
247
|
-
|
|
248
|
-
);
|
|
264
|
+
const reason = 'no existing Epic found and mintIfMissing is false — a new Epic can only be created by '
|
|
265
|
+
+ 'explicit human intent (New Epic UI, or /propose-epic + Approve & start), never implicitly by a '
|
|
266
|
+
+ 'PRD-authoring path';
|
|
267
|
+
appendAuditEvent('epic_mint_refused', { cwd, epicId: explicitEpicId ?? null, status, reason });
|
|
268
|
+
throw new Error(`ensureEpic: ${reason} (epicId=${explicitEpicId ?? 'none'})`);
|
|
249
269
|
}
|
|
250
270
|
|
|
251
271
|
if (!forceNewEpic) {
|
|
@@ -257,6 +277,20 @@ function ensureEpic(cwd, { goalText, tag, reuseByGoal = false, epicId: explicitE
|
|
|
257
277
|
}
|
|
258
278
|
}
|
|
259
279
|
|
|
280
|
+
// BORN-PROPOSED LAW (fail-closed, mirrors opsOwnership.cjs's
|
|
281
|
+
// assertOpsWrite): a mint always writes 'proposed', regardless of what
|
|
282
|
+
// status the caller requested. A caller explicitly asking to mint
|
|
283
|
+
// 'active' is refused outright rather than silently downgraded — that
|
|
284
|
+
// shape (mint + already-active) should never occur, so it is treated as
|
|
285
|
+
// a bug in the caller, not a normal fallback path.
|
|
286
|
+
if (status !== 'proposed') {
|
|
287
|
+
const reason = `ensureEpic: refusing to mint a new Epic with status '${status}' — every Epic is born `
|
|
288
|
+
+ "'proposed' (CLAUDE.md domain model); activation happens only via the renderer store's "
|
|
289
|
+
+ 'approveProposed, never through ensureEpic()';
|
|
290
|
+
appendAuditEvent('epic_mint_refused', { cwd, epicId: explicitEpicId ?? null, status, reason });
|
|
291
|
+
throw new Error(reason);
|
|
292
|
+
}
|
|
293
|
+
|
|
260
294
|
const epicId = `${slugify(goalText)}-${crypto.randomUUID().slice(0, 8)}`;
|
|
261
295
|
const now = new Date().toISOString();
|
|
262
296
|
const session = {
|
|
@@ -145,6 +145,39 @@ function resolvePrdsDirs(maxAgeMin, opts) {
|
|
|
145
145
|
return dirs;
|
|
146
146
|
}
|
|
147
147
|
|
|
148
|
+
/**
|
|
149
|
+
* resolveArchivedPrdsDirs(maxAgeMin?, opts?) → string[]
|
|
150
|
+
*
|
|
151
|
+
* The `prds-archived/` counterpart of resolvePrdsDirs — every archive dir
|
|
152
|
+
* (the flat per-project sibling plus each Epic's own sibling, per
|
|
153
|
+
* listArchivedPrdDirs) that exists on disk, across every historical AND
|
|
154
|
+
* currently-active project. Same "not recency-filtered" rationale as
|
|
155
|
+
* resolvePrdsDirs: a quiet project's completed-PRD history is still real
|
|
156
|
+
* history, not something list-prds should stop counting.
|
|
157
|
+
*/
|
|
158
|
+
function resolveArchivedPrdsDirs(maxAgeMin, opts) {
|
|
159
|
+
const dirs = [];
|
|
160
|
+
const seen = new Set();
|
|
161
|
+
const add = (dir) => {
|
|
162
|
+
if (seen.has(dir)) return;
|
|
163
|
+
seen.add(dir);
|
|
164
|
+
dirs.push(dir);
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
for (const cwd of allProjectCwds(opts)) {
|
|
168
|
+
for (const dir of listArchivedPrdDirs(cwd)) {
|
|
169
|
+
if (fs.existsSync(dir)) add(dir);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
for (const cwd of activeProjectCwds(maxAgeMin, opts)) {
|
|
173
|
+
for (const dir of listArchivedPrdDirs(cwd)) {
|
|
174
|
+
if (fs.existsSync(dir)) add(dir);
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return dirs;
|
|
179
|
+
}
|
|
180
|
+
|
|
148
181
|
/**
|
|
149
182
|
* deriveEpicIdFromPrdPath(filePath) → epicId | null
|
|
150
183
|
*
|
|
@@ -160,7 +193,11 @@ function resolvePrdsDirs(maxAgeMin, opts) {
|
|
|
160
193
|
function deriveEpicIdFromPrdPath(filePath) {
|
|
161
194
|
if (!filePath || typeof filePath !== 'string') return null;
|
|
162
195
|
const prdsDir = path.dirname(filePath);
|
|
163
|
-
|
|
196
|
+
// Accept both an Epic's live `prds/` dir and its sibling `prds-archived/`
|
|
197
|
+
// dir — archived PRDs still belong to the Epic they were dispatched from,
|
|
198
|
+
// and callers (parsePrdRaw, list-prds) need epicId for archived files too.
|
|
199
|
+
const dirName = path.basename(prdsDir);
|
|
200
|
+
if (dirName !== 'prds' && dirName !== 'prds-archived') return null;
|
|
164
201
|
const epicDir = path.dirname(prdsDir);
|
|
165
202
|
const epicId = path.basename(epicDir);
|
|
166
203
|
if (!epicId || epicId === '.' || epicId === path.sep) return null;
|
|
@@ -168,13 +205,14 @@ function deriveEpicIdFromPrdPath(filePath) {
|
|
|
168
205
|
for (let i = 0; i < EPICS_SUBPATH.length; i++) projectCwd = path.dirname(projectCwd);
|
|
169
206
|
let epicsRoot;
|
|
170
207
|
try { epicsRoot = resolveEpicsRoot(projectCwd); } catch { return null; }
|
|
171
|
-
if (path.join(epicsRoot, epicId,
|
|
208
|
+
if (path.join(epicsRoot, epicId, dirName) !== prdsDir) return null;
|
|
172
209
|
return epicId;
|
|
173
210
|
}
|
|
174
211
|
|
|
175
212
|
module.exports = {
|
|
176
213
|
resolvePrdWriteDir,
|
|
177
214
|
resolvePrdsDirs,
|
|
215
|
+
resolveArchivedPrdsDirs,
|
|
178
216
|
resolveEpicsRoot,
|
|
179
217
|
resolveEpicPrdWriteDir,
|
|
180
218
|
listEpicPrdDirs,
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* projectPages.cjs — read-only backend for Project Home's Project Pages
|
|
5
|
+
* display (PRD 932). Reads what the `project-home-builder` Epic's own
|
|
6
|
+
* session writes directly with its Write tool
|
|
7
|
+
* (`session-manager-operations/project-pages/output/*.html` + manifest.json)
|
|
8
|
+
* — this module never writes anything, matching the corrected "not an
|
|
9
|
+
* OWNERS namespace" storage/ownership note in
|
|
10
|
+
* session-manager-operations/architecture/project-pages-pipeline.md and
|
|
11
|
+
* session-manager-operations/project-pages/README.md. Structure mirrors
|
|
12
|
+
* projectBrief.cjs's `get()`: validatePath first, config.cjs read helpers,
|
|
13
|
+
* one ipcMain.handle registered from index.cjs's registerXHandlers() pattern.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { ipcMain } = require('electron');
|
|
17
|
+
const path = require('node:path');
|
|
18
|
+
const config = require('./config.cjs');
|
|
19
|
+
|
|
20
|
+
function outputDir(cwd) {
|
|
21
|
+
return path.join(cwd, 'session-manager-operations', 'project-pages', 'output');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const LENSES = ['marketing', 'feature', 'architecture'];
|
|
25
|
+
|
|
26
|
+
async function get({ cwd }) {
|
|
27
|
+
const realCwd = config.validatePath(cwd);
|
|
28
|
+
const dir = outputDir(realCwd);
|
|
29
|
+
const manifestResult = await config.readJson(path.join(dir, 'manifest.json'));
|
|
30
|
+
if (!manifestResult.exists || !manifestResult.data || manifestResult.parseError) {
|
|
31
|
+
return { output: null };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const htmlResults = await Promise.all(
|
|
35
|
+
LENSES.map((lens) => config.readText(path.join(dir, `${lens}.html`))),
|
|
36
|
+
);
|
|
37
|
+
if (htmlResults.some((r) => !r.exists)) {
|
|
38
|
+
return { output: null };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const [marketing, feature, architecture] = htmlResults;
|
|
42
|
+
const generatedAt = typeof manifestResult.data.generatedAt === 'string' ? manifestResult.data.generatedAt : null;
|
|
43
|
+
if (!generatedAt) {
|
|
44
|
+
return { output: null };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return {
|
|
48
|
+
output: {
|
|
49
|
+
marketing: marketing.text,
|
|
50
|
+
feature: feature.text,
|
|
51
|
+
architecture: architecture.text,
|
|
52
|
+
generatedAt,
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function registerProjectPagesIpc() {
|
|
58
|
+
const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
|
|
59
|
+
ipcMain.handle('project-pages:get', v(s.projectPagesCwd, get));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
registerProjectPagesIpc,
|
|
64
|
+
get,
|
|
65
|
+
};
|
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);
|
|
@@ -536,22 +565,33 @@ async function archivedTwinExists(job) {
|
|
|
536
565
|
}
|
|
537
566
|
|
|
538
567
|
/**
|
|
539
|
-
* Build the non-failure result + run meta for a job whose PRD
|
|
540
|
-
*
|
|
541
|
-
*
|
|
542
|
-
*
|
|
568
|
+
* Build the non-failure result + run meta for a queued job whose PRD source
|
|
569
|
+
* is gone. Shared by both PRD-read failure exits in executeJob so the
|
|
570
|
+
* stale-skip logic isn't duplicated. Two reasons, both exitCode: 0 (no RCA
|
|
571
|
+
* feedback item, since neither is a real work failure):
|
|
572
|
+
* - 'prd-archived': the source was archived — archivedTwinExists found it
|
|
573
|
+
* under a prds-archived/ dir. The work already shipped.
|
|
574
|
+
* - 'prd-missing': the source is gone everywhere (ENOENT on every
|
|
575
|
+
* candidate dir, no archived twin either) — most commonly a PRD that
|
|
576
|
+
* existed at enqueue time and was deleted before dispatch (e.g. a test
|
|
577
|
+
* fixture that leaked into the live queue and was cleaned up by its own
|
|
578
|
+
* `finally` block). Retire the row rather than fail it.
|
|
543
579
|
*/
|
|
544
|
-
function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath) {
|
|
545
|
-
const
|
|
546
|
-
|
|
580
|
+
function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, reason = 'prd-archived') {
|
|
581
|
+
const msg = reason === 'prd-missing'
|
|
582
|
+
? 'PRD source no longer exists on disk — retiring stale queue entry'
|
|
583
|
+
: (() => {
|
|
584
|
+
const archivedTwin = archivedPrdPathForJob(job);
|
|
585
|
+
return `PRD already archived (${archivedTwin}) — work shipped; retiring stale queue entry`;
|
|
586
|
+
})();
|
|
547
587
|
safeLog(`[scheduler] ${msg}\n`);
|
|
548
588
|
closeFd();
|
|
549
589
|
const finishedAt = Date.now();
|
|
550
590
|
config.writeJsonSync(metaPath, {
|
|
551
|
-
slug: job.slug, cwd, sessionId, exitCode: 0, skipped:
|
|
591
|
+
slug: job.slug, cwd, sessionId, exitCode: 0, skipped: reason,
|
|
552
592
|
note: msg, startedAt, finishedAt, durationMs: 0,
|
|
553
593
|
});
|
|
554
|
-
return { exitCode: 0, durationMs: 0, skipped:
|
|
594
|
+
return { exitCode: 0, durationMs: 0, skipped: reason, note: msg, sessionId };
|
|
555
595
|
}
|
|
556
596
|
|
|
557
597
|
/**
|
|
@@ -2003,16 +2043,34 @@ async function executeJob(job, runDir, defaultCwd, onPid) {
|
|
|
2003
2043
|
prompt = parsed.body + FINISH_PROTOCOL;
|
|
2004
2044
|
prdPath = fallbackPath;
|
|
2005
2045
|
} catch (e2) {
|
|
2046
|
+
// Found the dir a moment ago but the read still failed. Case A: the
|
|
2047
|
+
// source has since been archived — stale-skip as usual. Case B: it
|
|
2048
|
+
// was deleted out from under us in the window between findPrdDir and
|
|
2049
|
+
// parsePrd (ENOENT) with no archived twin — also a stale row, not a
|
|
2050
|
+
// real failure. Anything else (malformed/unreadable PRD) is a real
|
|
2051
|
+
// failure and keeps exitCode: -1.
|
|
2006
2052
|
if (await archivedTwinExists(job)) {
|
|
2007
|
-
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath);
|
|
2053
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
2054
|
+
}
|
|
2055
|
+
if (e2 && e2.code === 'ENOENT') {
|
|
2056
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
2008
2057
|
}
|
|
2009
2058
|
safeLog(`[scheduler] failed to read PRD: ${e2?.message}\n`);
|
|
2010
2059
|
closeFd();
|
|
2011
2060
|
return { exitCode: -1, durationMs: 0, error: e2?.message };
|
|
2012
2061
|
}
|
|
2013
2062
|
} else {
|
|
2063
|
+
// No candidate dir has the slug at all. Case A: it was archived —
|
|
2064
|
+
// stale-skip. Case B: the read error is ENOENT (the common case: a
|
|
2065
|
+
// source that existed at enqueue time and is gone by dispatch, e.g. a
|
|
2066
|
+
// leaked test fixture) with no archived twin — retire as stale rather
|
|
2067
|
+
// than a hard failure. Anything else (a real read/parse error) still
|
|
2068
|
+
// fails the job.
|
|
2014
2069
|
if (await archivedTwinExists(job)) {
|
|
2015
|
-
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath);
|
|
2070
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
|
|
2071
|
+
}
|
|
2072
|
+
if (e && e.code === 'ENOENT') {
|
|
2073
|
+
return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
|
|
2016
2074
|
}
|
|
2017
2075
|
safeLog(`[scheduler] failed to read PRD: ${e?.message}\n`);
|
|
2018
2076
|
closeFd();
|
|
@@ -2458,6 +2516,16 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2458
2516
|
return { deferred: false };
|
|
2459
2517
|
}
|
|
2460
2518
|
|
|
2519
|
+
// Mark the job 'investigating' so the Queue UI shows an active status for
|
|
2520
|
+
// the whole probe duration — previously this left the job's persisted
|
|
2521
|
+
// status frozen at 'failed'/'needs_review' the entire time, which read as
|
|
2522
|
+
// "nothing is happening" even though an Opus process was actively running.
|
|
2523
|
+
await mutate((s) => {
|
|
2524
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2525
|
+
if (j) j.status = 'investigating';
|
|
2526
|
+
});
|
|
2527
|
+
await broadcast({ flush: true });
|
|
2528
|
+
|
|
2461
2529
|
const claudeBin = resolveClaudeBin();
|
|
2462
2530
|
const childEnv = cleanChildEnv({ PATH: pathWithUserBins() }); // Homebrew/user bins for macOS
|
|
2463
2531
|
|
|
@@ -2495,6 +2563,14 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2495
2563
|
watchdogs: [deadmanWatchdog],
|
|
2496
2564
|
onExit({ exitCode, error, spawnFailed, safeLog: sl }) {
|
|
2497
2565
|
releaseSlot();
|
|
2566
|
+
// Restore the pre-investigation status now that the probe has exited —
|
|
2567
|
+
// 'investigating' must never be the job's resting state.
|
|
2568
|
+
mutate((s) => {
|
|
2569
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2570
|
+
if (j && j.status === 'investigating') j.status = failedJob.status || 'failed';
|
|
2571
|
+
})
|
|
2572
|
+
.then(() => broadcast({ flush: true }))
|
|
2573
|
+
.catch(() => {});
|
|
2498
2574
|
if (error) {
|
|
2499
2575
|
const errMsg = spawnFailed
|
|
2500
2576
|
? `investigation spawn failed: ${error?.message ?? String(error)}`
|
|
@@ -2541,8 +2617,15 @@ async function spawnInvestigation(failedJob, runDir) {
|
|
|
2541
2617
|
return { deferred: false };
|
|
2542
2618
|
} catch (e) {
|
|
2543
2619
|
// A synchronous throw before onExit is wired (e.g. resolveClaudeBin not found,
|
|
2544
|
-
// openLog failure, spawn setup) must not strand the reserved slot
|
|
2620
|
+
// openLog failure, spawn setup) must not strand the reserved slot, and must
|
|
2621
|
+
// not strand the job on the transient 'investigating' status either.
|
|
2545
2622
|
releaseSlot();
|
|
2623
|
+
mutate((s) => {
|
|
2624
|
+
const j = s.jobs.find((x) => x.slug === failedJob.slug);
|
|
2625
|
+
if (j && j.status === 'investigating') j.status = failedJob.status || 'failed';
|
|
2626
|
+
})
|
|
2627
|
+
.then(() => broadcast({ flush: true }))
|
|
2628
|
+
.catch(() => {});
|
|
2546
2629
|
throw e;
|
|
2547
2630
|
}
|
|
2548
2631
|
}
|
|
@@ -3167,7 +3250,11 @@ async function maybeLaunchWhenAvailable(state) {
|
|
|
3167
3250
|
*/
|
|
3168
3251
|
async function reapDeadRunningJobs() {
|
|
3169
3252
|
try {
|
|
3170
|
-
|
|
3253
|
+
// Do NOT gate on runningSet: spawnJob()'s finally block unconditionally
|
|
3254
|
+
// deletes a job's slug from runningSet even when the preceding completion
|
|
3255
|
+
// mutate() threw and was swallowed, leaving queue.json stuck at
|
|
3256
|
+
// status:"running" with no slug left in runningSet to trigger reconciliation.
|
|
3257
|
+
// queue.json is the source of truth for which jobs are actually running.
|
|
3171
3258
|
const state = await readQueue();
|
|
3172
3259
|
const dead = [];
|
|
3173
3260
|
for (const j of state.jobs) {
|
|
@@ -3925,7 +4012,9 @@ function registerScheduleHandlers() {
|
|
|
3925
4012
|
ipcMain.handle('schedule:list-prds', async () => {
|
|
3926
4013
|
ensureDirs();
|
|
3927
4014
|
const out = [];
|
|
3928
|
-
|
|
4015
|
+
const seenSlugs = new Set();
|
|
4016
|
+
|
|
4017
|
+
async function readDirInto(dir, { archived }) {
|
|
3929
4018
|
let entries;
|
|
3930
4019
|
try {
|
|
3931
4020
|
entries = await fsp.readdir(dir);
|
|
@@ -3933,15 +4022,21 @@ function registerScheduleHandlers() {
|
|
|
3933
4022
|
if (e?.code !== 'ENOENT') {
|
|
3934
4023
|
logs.writeLine({ level: 'warn', scope: 'scheduler', message: 'list-prds: readdir failed', meta: { dir, error: e?.message } });
|
|
3935
4024
|
}
|
|
3936
|
-
|
|
4025
|
+
return;
|
|
3937
4026
|
}
|
|
3938
4027
|
for (const name of entries) {
|
|
3939
4028
|
if (!name.endsWith('.md') || name.startsWith('.')) continue;
|
|
3940
4029
|
const filePath = path.join(dir, name);
|
|
3941
4030
|
try {
|
|
3942
4031
|
const parsed = await parsePrd(filePath);
|
|
4032
|
+
// A slug can't be both live and archived at once, but a duplicate
|
|
4033
|
+
// slug found in two archive dirs (shouldn't happen — archiving is
|
|
4034
|
+
// a single rename — but is cheap to guard) is skipped rather than
|
|
4035
|
+
// double-counted.
|
|
4036
|
+
if (seenSlugs.has(parsed.slug)) continue;
|
|
4037
|
+
seenSlugs.add(parsed.slug);
|
|
3943
4038
|
const stat = await fsp.stat(filePath);
|
|
3944
|
-
|
|
4039
|
+
const entry = {
|
|
3945
4040
|
slug: parsed.slug,
|
|
3946
4041
|
parallelGroup: parsed.parallelGroup,
|
|
3947
4042
|
title: parsed.title,
|
|
@@ -3950,12 +4045,46 @@ function registerScheduleHandlers() {
|
|
|
3950
4045
|
sourcePromptId: parsed.sourcePromptId,
|
|
3951
4046
|
epicId: parsed.epicId ?? null,
|
|
3952
4047
|
mtimeMs: stat.mtimeMs,
|
|
3953
|
-
|
|
4048
|
+
archived,
|
|
4049
|
+
};
|
|
4050
|
+
out.push(entry);
|
|
3954
4051
|
} catch (e) {
|
|
3955
4052
|
logs.writeLine({ level: 'warn', scope: 'scheduler', message: 'list-prds: skipping unparseable file', meta: { name, error: e?.message } });
|
|
3956
4053
|
}
|
|
3957
4054
|
}
|
|
3958
4055
|
}
|
|
4056
|
+
|
|
4057
|
+
// Live PRDs first, so an archived duplicate (shouldn't exist, but a
|
|
4058
|
+
// stale rename copy is possible) never shadows the still-runnable live
|
|
4059
|
+
// entry.
|
|
4060
|
+
for (const dir of candidatePrdsDirs()) {
|
|
4061
|
+
await readDirInto(dir, { archived: false });
|
|
4062
|
+
}
|
|
4063
|
+
|
|
4064
|
+
const archivedStart = out.length;
|
|
4065
|
+
for (const dir of candidateArchivedPrdsDirs()) {
|
|
4066
|
+
await readDirInto(dir, { archived: true });
|
|
4067
|
+
}
|
|
4068
|
+
|
|
4069
|
+
// Archived PRDs need a status: archiveCompletedPrd (scheduler.cjs) only
|
|
4070
|
+
// ever archives a job whose effective status is 'completed' — a 'failed'
|
|
4071
|
+
// job's PRD source stays in the live prds/ dir (still visible/countable
|
|
4072
|
+
// there already). Still resolve the real job status defensively (live
|
|
4073
|
+
// queue row, falling back to history.jsonl) rather than hard-coding
|
|
4074
|
+
// 'completed', so this stays correct if that archiving invariant ever
|
|
4075
|
+
// changes.
|
|
4076
|
+
if (out.length > archivedStart) {
|
|
4077
|
+
const [state, histBySlug] = await Promise.all([
|
|
4078
|
+
readQueue(),
|
|
4079
|
+
queueHistory.historyTerminalBySlug().catch(() => new Map()),
|
|
4080
|
+
]);
|
|
4081
|
+
const liveStatusBySlug = new Map(state.jobs.map((j) => [j.slug, j.status]));
|
|
4082
|
+
for (let i = archivedStart; i < out.length; i++) {
|
|
4083
|
+
const entry = out[i];
|
|
4084
|
+
entry.archivedStatus = resolveArchivedPrdStatus(entry.slug, liveStatusBySlug, histBySlug);
|
|
4085
|
+
}
|
|
4086
|
+
}
|
|
4087
|
+
|
|
3959
4088
|
out.sort((a, b) => a.slug.localeCompare(b.slug, undefined, { numeric: true }));
|
|
3960
4089
|
return out;
|
|
3961
4090
|
});
|
|
@@ -4380,4 +4509,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
4380
4509
|
});
|
|
4381
4510
|
}
|
|
4382
4511
|
|
|
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 };
|
|
4512
|
+
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, executeJob, prdArchivedSkipResult };
|
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: (
|
|
@@ -1530,6 +1556,15 @@ export interface SessionManagerAPI {
|
|
|
1530
1556
|
/** Read back an Epic's full-text turns (optionally capped to the last `limit`). Skips corrupt lines rather than throwing. */
|
|
1531
1557
|
read: (cwd: string, epicId: string, limit?: number) => Promise<{ turns: PromptSessionTranscriptTurn[] }>;
|
|
1532
1558
|
};
|
|
1559
|
+
auditLog: {
|
|
1560
|
+
/** Append one Epic-lifecycle audit event to ~/.claude/session-manager/audit-log.jsonl.
|
|
1561
|
+
* Kind allowlist is enforced main-side (ipcSchemas.cjs) — this channel is
|
|
1562
|
+
* append-only, no read/list IPC exists. */
|
|
1563
|
+
append: (
|
|
1564
|
+
kind: 'epic_create' | 'epic_approve' | 'epic_complete' | 'epic_delete' | 'epic_resume' | 'epic_duplicate',
|
|
1565
|
+
fields: { cwd: string; epicId: string; source: string },
|
|
1566
|
+
) => Promise<{ ok: boolean }>;
|
|
1567
|
+
};
|
|
1533
1568
|
agentMemory: {
|
|
1534
1569
|
/** List all memory entries for one subagent. Sorted newest first. */
|
|
1535
1570
|
list: (agentId: string) => Promise<AgentMemoryListResult>;
|
package/src/preload/index.cjs
CHANGED
|
@@ -330,12 +330,18 @@ 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 }),
|
|
336
339
|
read: (cwd, epicId, limit) =>
|
|
337
340
|
ipcRenderer.invoke('promptSessionTranscript:read', { cwd, epicId, ...(limit ? { limit } : {}) }),
|
|
338
341
|
},
|
|
342
|
+
auditLog: {
|
|
343
|
+
append: (kind, fields) => ipcRenderer.invoke('auditLog:append', { kind, ...fields }),
|
|
344
|
+
},
|
|
339
345
|
agentMemory: {
|
|
340
346
|
list: (agentId) => ipcRenderer.invoke('agent-memory:list', { agentId }),
|
|
341
347
|
get: (agentId, entryId) => ipcRenderer.invoke('agent-memory:get', { agentId, entryId }),
|