claude-code-session-manager 0.38.5 → 0.38.7
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-D1ufJtTb.js → TiptapBody-C74mBmMU.js} +1 -1
- package/dist/assets/index-BGTU12rl.css +32 -0
- package/dist/assets/index-DPSmbyU_.js +3178 -0
- package/dist/index.html +2 -2
- package/package.json +2 -1
- package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +35 -9
- package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +16 -7
- package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +8 -5
- package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +7 -4
- package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +11 -2
- package/src/main/__tests__/prdCreate.test.cjs +55 -1
- package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +65 -0
- package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +43 -0
- package/src/main/index.cjs +10 -10
- package/src/main/ipcSchemas.cjs +17 -16
- package/src/main/layoutStore.cjs +58 -0
- package/src/main/lib/prdCreate.cjs +66 -43
- package/src/main/pty.cjs +0 -5
- package/src/main/queueOps.cjs +58 -0
- package/src/main/scheduler.cjs +48 -3
- package/src/main/transcripts.cjs +2 -15
- package/src/preload/api.d.ts +39 -116
- package/src/preload/index.cjs +6 -27
- package/dist/assets/index-CZrfXQpq.css +0 -32
- package/dist/assets/index-DwjviHVo.js +0 -3501
- package/src/main/hives.cjs +0 -287
- package/src/main/superagent.cjs +0 -212
- package/src/main/usageMatrix.cjs +0 -340
|
@@ -48,7 +48,7 @@ function deriveSlugFromTitle(title) {
|
|
|
48
48
|
function buildPrdBody(input) {
|
|
49
49
|
const {
|
|
50
50
|
title, cwd, estimateMinutes, goal, acceptanceCriteria,
|
|
51
|
-
implementationNotes, outOfScope, sourcePromptId, sourceTabId,
|
|
51
|
+
implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag,
|
|
52
52
|
} = input;
|
|
53
53
|
|
|
54
54
|
// No `parallelGroup` frontmatter key by convention (SKILL.md) — the NN-
|
|
@@ -61,6 +61,9 @@ function buildPrdBody(input) {
|
|
|
61
61
|
// Optional, additive: the tab that queued this PRD, read back by
|
|
62
62
|
// scheduler.cjs at job completion (PRD 761) to route a status prompt.
|
|
63
63
|
if (sourceTabId) fmLines.push(`sourceTabId: ${sourceTabId}`);
|
|
64
|
+
// Optional, additive: the user-selected Feature/Bug tag (PRD 774) carried
|
|
65
|
+
// through from the originating PromptTicket — deterministic, never LLM-classified.
|
|
66
|
+
if (tag) fmLines.push(`tag: ${tag}`);
|
|
64
67
|
fmLines.push('---', '');
|
|
65
68
|
|
|
66
69
|
const acLines = acceptanceCriteria.map((line) => `- [ ] ${line}`).join('\n');
|
|
@@ -85,6 +88,63 @@ function buildPrdBody(input) {
|
|
|
85
88
|
return `${fmLines.join('\n')}${bodyLines.join('\n')}`;
|
|
86
89
|
}
|
|
87
90
|
|
|
91
|
+
/**
|
|
92
|
+
* Core create-PRD orchestration: cwd validation, slug derivation, NN
|
|
93
|
+
* allocation, existing-file collision check, body build, write. Shared by
|
|
94
|
+
* both the admin HTTP route (registerAdminRoute, used by the MCP tool) and
|
|
95
|
+
* the renderer-facing IPC handler (chat:create-prd, index.cjs) so the
|
|
96
|
+
* validation/write logic lives in exactly one place (API-reuse standard).
|
|
97
|
+
* `input` must already be schema-validated by the caller (schemas.schedulerCreatePrd).
|
|
98
|
+
* `remote` is scheduler.cjs's remote object (allocateParallelGroup/readPrd/writePrd).
|
|
99
|
+
*
|
|
100
|
+
* Returns `{ ok: true, nn, filename }` on success, or `{ ok: false, status, error }`
|
|
101
|
+
* on failure — `status` is the HTTP status code the caller should map errors to
|
|
102
|
+
* (400/409/500), left to the caller since only the HTTP route needs it.
|
|
103
|
+
*/
|
|
104
|
+
async function createPrd(input, remote) {
|
|
105
|
+
// cwd is untrusted: this is the first *creating* mutation on a
|
|
106
|
+
// token-authed API whose product is "a command that will later run
|
|
107
|
+
// with --dangerously-skip-permissions in a chosen cwd". Route it
|
|
108
|
+
// through config.cjs's validatePath (allowedRoots = home dir) —
|
|
109
|
+
// same boundary every other fs-touching IPC handler uses — never a
|
|
110
|
+
// bespoke check here.
|
|
111
|
+
try {
|
|
112
|
+
config.validatePath(expandHome(input.cwd));
|
|
113
|
+
} catch (e) {
|
|
114
|
+
return { ok: false, status: 400, error: `cwd rejected: ${e?.message ?? 'outside allowed roots'}` };
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const slug = input.slug || deriveSlugFromTitle(input.title);
|
|
118
|
+
if (!slug || !PRD_CREATE_SLUG_RE.test(slug)) {
|
|
119
|
+
return { ok: false, status: 400, error: 'could not derive a valid kebab-case slug from title; supply "slug" explicitly' };
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// NN allocation is delegated to allocateParallelGroup() (PRD 548) via
|
|
123
|
+
// the injected remote — never re-derived here — unless the caller
|
|
124
|
+
// opted into an existing group explicitly.
|
|
125
|
+
const nn = input.parallelGroup ?? await remote.allocateParallelGroup();
|
|
126
|
+
const filenameSlug = `${nn}-${slug}`;
|
|
127
|
+
|
|
128
|
+
// An explicit `parallelGroup` bypasses allocateParallelGroup()'s
|
|
129
|
+
// collision-proof reservation, so re-check for an existing file at
|
|
130
|
+
// this exact destination before writing — remote.writePrd itself has
|
|
131
|
+
// no existence guard (by design, it doubles as the edit-in-place
|
|
132
|
+
// path for Scheduler UI PRD edits), so "create" must not silently
|
|
133
|
+
// clobber an existing job's PRD.
|
|
134
|
+
const existing = await remote.readPrd(filenameSlug);
|
|
135
|
+
if (existing?.ok) {
|
|
136
|
+
return { ok: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const body = buildPrdBody(input);
|
|
140
|
+
const writeResult = await remote.writePrd(filenameSlug, body);
|
|
141
|
+
if (!writeResult?.ok) {
|
|
142
|
+
return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return { ok: true, nn, filename: `${filenameSlug}.md` };
|
|
146
|
+
}
|
|
147
|
+
|
|
88
148
|
/**
|
|
89
149
|
* Registers the create-prd admin HTTP route (PRD 689 — moved verbatim out of
|
|
90
150
|
* the former standalone admin HTTP server module's handleRequest, no behavior
|
|
@@ -112,51 +172,13 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
112
172
|
return;
|
|
113
173
|
}
|
|
114
174
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
// through config.cjs's validatePath (allowedRoots = home dir) —
|
|
119
|
-
// same boundary every other fs-touching IPC handler uses — never a
|
|
120
|
-
// bespoke check here.
|
|
121
|
-
try {
|
|
122
|
-
config.validatePath(expandHome(input.cwd));
|
|
123
|
-
} catch (e) {
|
|
124
|
-
sendJson(res, 400, { ok: false, error: `cwd rejected: ${e?.message ?? 'outside allowed roots'}` });
|
|
125
|
-
return;
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const slug = input.slug || deriveSlugFromTitle(input.title);
|
|
129
|
-
if (!slug || !PRD_CREATE_SLUG_RE.test(slug)) {
|
|
130
|
-
sendJson(res, 400, { ok: false, error: 'could not derive a valid kebab-case slug from title; supply "slug" explicitly' });
|
|
131
|
-
return;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// NN allocation is delegated to allocateParallelGroup() (PRD 548) via
|
|
135
|
-
// the injected remote — never re-derived here — unless the caller
|
|
136
|
-
// opted into an existing group explicitly.
|
|
137
|
-
const nn = input.parallelGroup ?? await remote.allocateParallelGroup();
|
|
138
|
-
const filenameSlug = `${nn}-${slug}`;
|
|
139
|
-
|
|
140
|
-
// An explicit `parallelGroup` bypasses allocateParallelGroup()'s
|
|
141
|
-
// collision-proof reservation, so re-check for an existing file at
|
|
142
|
-
// this exact destination before writing — remote.writePrd itself has
|
|
143
|
-
// no existence guard (by design, it doubles as the edit-in-place
|
|
144
|
-
// path for Scheduler UI PRD edits), so "create" must not silently
|
|
145
|
-
// clobber an existing job's PRD.
|
|
146
|
-
const existing = await remote.readPrd(filenameSlug);
|
|
147
|
-
if (existing?.ok) {
|
|
148
|
-
sendJson(res, 409, { ok: false, error: `PRD already exists: ${filenameSlug}.md` });
|
|
149
|
-
return;
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
const body = buildPrdBody(input);
|
|
153
|
-
const writeResult = await remote.writePrd(filenameSlug, body);
|
|
154
|
-
if (!writeResult?.ok) {
|
|
155
|
-
sendJson(res, 500, { ok: false, error: writeResult?.error ?? 'write failed' });
|
|
175
|
+
const result = await createPrd(input, remote);
|
|
176
|
+
if (!result.ok) {
|
|
177
|
+
sendJson(res, result.status, { ok: false, error: result.error });
|
|
156
178
|
return;
|
|
157
179
|
}
|
|
158
180
|
|
|
159
|
-
sendJson(res, 200, { nn, filename:
|
|
181
|
+
sendJson(res, 200, { nn: result.nn, filename: result.filename, status: 'queued' });
|
|
160
182
|
});
|
|
161
183
|
}
|
|
162
184
|
|
|
@@ -166,5 +188,6 @@ module.exports = {
|
|
|
166
188
|
readStandards,
|
|
167
189
|
deriveSlugFromTitle,
|
|
168
190
|
buildPrdBody,
|
|
191
|
+
createPrd,
|
|
169
192
|
registerAdminRoute,
|
|
170
193
|
};
|
package/src/main/pty.cjs
CHANGED
|
@@ -226,9 +226,6 @@ class PtyManager {
|
|
|
226
226
|
}
|
|
227
227
|
this.sessions.delete(tabId);
|
|
228
228
|
}
|
|
229
|
-
// Always drop superagent run state — a run can be started before the pty
|
|
230
|
-
// finishes spawning, so clean up regardless of whether a session existed.
|
|
231
|
-
try { require('./superagent.cjs').dropTab(tabId); } catch { /* superagent not loaded (e2e) */ }
|
|
232
229
|
}
|
|
233
230
|
|
|
234
231
|
killAll() {
|
|
@@ -245,8 +242,6 @@ function registerPtyHandlers() {
|
|
|
245
242
|
ipcMain.on('pty:resize', (_e, payload) => { try { manager.resize(s.ptyResize.parse(payload)); } catch { /* ignore */ } });
|
|
246
243
|
ipcMain.on('pty:kill', (_e, tabId) => {
|
|
247
244
|
if (typeof tabId !== 'string') return;
|
|
248
|
-
// manager.kill() already drops superagent run state for tabId — see kill()
|
|
249
|
-
// above. Do not duplicate that call here.
|
|
250
245
|
manager.kill(tabId);
|
|
251
246
|
});
|
|
252
247
|
}
|
package/src/main/queueOps.cjs
CHANGED
|
@@ -83,6 +83,50 @@ const LINE_RULES = [
|
|
|
83
83
|
},
|
|
84
84
|
];
|
|
85
85
|
|
|
86
|
+
// Interactive/GUI-rendering-as-headless-AC patterns (research: PRDs 776/779,
|
|
87
|
+
// the 2026-07-30 exit143 incident — a claude -p executor hits a tool-use
|
|
88
|
+
// rejection on these and hangs until the scheduler SIGTERMs it, even when
|
|
89
|
+
// the step works fine run interactively). Keyed off AC-checkbox context
|
|
90
|
+
// specifically (a "- [ ] " line via INTERACTIVE_AC_CHECKBOX_RE), not any
|
|
91
|
+
// occurrence anywhere in the file — a PRD's Out-of-scope/Implementation-notes
|
|
92
|
+
// prose explicitly telling the executor NOT to do this (e.g. "NOTE: do NOT
|
|
93
|
+
// add a `playwright test ... under xvfb` acceptance criterion here", as in
|
|
94
|
+
// PRDs 780/787/788) is plain prose, not a checkbox line, so it must not
|
|
95
|
+
// trigger a false positive.
|
|
96
|
+
const INTERACTIVE_AC_CHECKBOX_RE = /^\s*-\s*\[[ xX]\]/;
|
|
97
|
+
const INTERACTIVE_AC_PATTERNS = [
|
|
98
|
+
{
|
|
99
|
+
id: 'interactive-xvfb',
|
|
100
|
+
re: /\bxvfb-run\b/i,
|
|
101
|
+
label: 'xvfb-run as a headless AC — cannot run under claude -p (see PRD_AUTHORING.md / 776+779 incidents)',
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
id: 'interactive-electron-launch',
|
|
105
|
+
re: /electron\.launch\b/i,
|
|
106
|
+
label: 'playwright electron.launch as a headless AC — interactive tool-use rejection hangs the executor',
|
|
107
|
+
},
|
|
108
|
+
{
|
|
109
|
+
id: 'interactive-playwright-test',
|
|
110
|
+
re: /\bplaywright\s+test\b/i,
|
|
111
|
+
label: '"playwright test" as a headless AC — GUI-rendering e2e cannot run headlessly',
|
|
112
|
+
},
|
|
113
|
+
{
|
|
114
|
+
id: 'interactive-launch-app',
|
|
115
|
+
re: /\blaunch(?:ing)?\s+the\s+app\b/i,
|
|
116
|
+
label: '"launch the app" as a headless AC — interactive/GUI step',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
id: 'interactive-click-through',
|
|
120
|
+
re: /\bclick(?:ing)?\s+through\b/i,
|
|
121
|
+
label: '"click through" as a headless AC — interactive/GUI step',
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
id: 'interactive-screenshot',
|
|
125
|
+
re: /\bscreenshot/i,
|
|
126
|
+
label: 'screenshot capture as a headless AC — interactive/GUI-rendering step',
|
|
127
|
+
},
|
|
128
|
+
];
|
|
129
|
+
|
|
86
130
|
const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
|
|
87
131
|
|
|
88
132
|
/**
|
|
@@ -165,6 +209,20 @@ function lintParsed(slug, raw) {
|
|
|
165
209
|
});
|
|
166
210
|
}
|
|
167
211
|
}
|
|
212
|
+
// Interactive-AC scan — only on checkbox-format AC lines, see the
|
|
213
|
+
// pattern-list comment above for why prose mentions must not match.
|
|
214
|
+
if (INTERACTIVE_AC_CHECKBOX_RE.test(line)) {
|
|
215
|
+
for (const rule of INTERACTIVE_AC_PATTERNS) {
|
|
216
|
+
if (rule.re.test(line)) {
|
|
217
|
+
findings.push({
|
|
218
|
+
rule: rule.id,
|
|
219
|
+
line: fmLineCount + i + 1,
|
|
220
|
+
snippet: `${rule.label} — ${line.trim().slice(0, 80)}`,
|
|
221
|
+
severity: 'warn',
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
168
226
|
}
|
|
169
227
|
|
|
170
228
|
return { slug, findings };
|
package/src/main/scheduler.cjs
CHANGED
|
@@ -264,6 +264,27 @@ async function computeCommittedDuringRun(cwd, headBefore, headAfter, startedAt,
|
|
|
264
264
|
return committedInWindow(cwd, startedAt, untilIso);
|
|
265
265
|
}
|
|
266
266
|
|
|
267
|
+
/**
|
|
268
|
+
* Override for a SIGTERM'd (143) run when a commit landed in its window.
|
|
269
|
+
* Exit 143 alone doesn't prove the deliverable is missing — the 776/779
|
|
270
|
+
* incidents (2026-07-30) both died on a headless-incompatible interactive
|
|
271
|
+
* step (xvfb Electron screenshot / playwright electron.launch) AFTER their
|
|
272
|
+
* commit had already landed, and sat `failed` forever with a real deliverable
|
|
273
|
+
* on disk. A landed commit doesn't prove every AC line passed though (the
|
|
274
|
+
* step it died on is still unverified), so this routes to `needs_review`,
|
|
275
|
+
* never silently promotes to `completed`. Returns null for every other exit
|
|
276
|
+
* code, or for a 143 with no commit found in window — those fall through to
|
|
277
|
+
* the existing `failed` path unchanged. Pure/no I/O: the commit-window scan
|
|
278
|
+
* (committedInWindow) happens at the call site, not here.
|
|
279
|
+
*/
|
|
280
|
+
function classifySigtermWithCommit(exitCode, commitFoundInWindow) {
|
|
281
|
+
if (exitCode !== 143 || !commitFoundInWindow) return null;
|
|
282
|
+
return {
|
|
283
|
+
status: 'needs_review',
|
|
284
|
+
reason: 'SIGTERM after a commit landed — verify AC before treating as done',
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
|
|
267
288
|
const ROOT = path.join(os.homedir(), '.claude', 'session-manager', 'scheduled-plans');
|
|
268
289
|
const PRDS_DIR = path.join(ROOT, 'prds');
|
|
269
290
|
const RUNS_DIR = path.join(ROOT, 'runs');
|
|
@@ -1994,6 +2015,23 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
1994
2015
|
}
|
|
1995
2016
|
}
|
|
1996
2017
|
|
|
2018
|
+
// SIGTERM commit check: reuse the same commit-window scan the exit=0
|
|
2019
|
+
// guard uses above (one commit-detection path, not two) to see whether a
|
|
2020
|
+
// 143 (SIGTERM) run still landed a deliverable before it died. Scoped
|
|
2021
|
+
// narrowly to exit 143 — never applied to other non-zero exit codes or to
|
|
2022
|
+
// rateLimited (already handled separately, above).
|
|
2023
|
+
let sigtermCommitFound = false;
|
|
2024
|
+
if (res.exitCode === 143 && !res.rateLimited) {
|
|
2025
|
+
const guardHeadAtSigterm = await gitHead(guardCwd);
|
|
2026
|
+
sigtermCommitFound = await computeCommittedDuringRun(
|
|
2027
|
+
guardCwd,
|
|
2028
|
+
guardHeadBefore,
|
|
2029
|
+
guardHeadAtSigterm,
|
|
2030
|
+
job.startedAt,
|
|
2031
|
+
new Date().toISOString(),
|
|
2032
|
+
);
|
|
2033
|
+
}
|
|
2034
|
+
|
|
1997
2035
|
let actuallyFailed = false;
|
|
1998
2036
|
let failedJobSnapshot = null;
|
|
1999
2037
|
let needsInvestigationNow = false;
|
|
@@ -2009,7 +2047,14 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
2009
2047
|
} else {
|
|
2010
2048
|
// Determine effective status, applying the verifier verdict for exit=0 runs.
|
|
2011
2049
|
let effectiveStatus;
|
|
2012
|
-
|
|
2050
|
+
let sigtermOverrideReason = null;
|
|
2051
|
+
const sigtermOverride = res.exitCode !== 0
|
|
2052
|
+
? classifySigtermWithCommit(res.exitCode, sigtermCommitFound)
|
|
2053
|
+
: null;
|
|
2054
|
+
if (sigtermOverride) {
|
|
2055
|
+
effectiveStatus = sigtermOverride.status;
|
|
2056
|
+
sigtermOverrideReason = sigtermOverride.reason;
|
|
2057
|
+
} else if (res.exitCode !== 0) {
|
|
2013
2058
|
effectiveStatus = 'failed';
|
|
2014
2059
|
} else if (
|
|
2015
2060
|
!verifyResult
|
|
@@ -2033,7 +2078,7 @@ async function spawnJob(job, runId, runDir, defaultCwd) {
|
|
|
2033
2078
|
s.jobs[i2].finishedAt = new Date().toISOString();
|
|
2034
2079
|
s.jobs[i2].exitCode = res.exitCode;
|
|
2035
2080
|
s.jobs[i2].error = effectiveStatus === 'needs_review'
|
|
2036
|
-
? (verifyResult?.reason ?? null)
|
|
2081
|
+
? (verifyResult?.reason ?? sigtermOverrideReason ?? null)
|
|
2037
2082
|
: (res.error || null);
|
|
2038
2083
|
// Persist the verifier's verdict string so the renderer can show it.
|
|
2039
2084
|
if (verifyResult?.verdict && verifyResult.verdict !== 'clean') {
|
|
@@ -3454,4 +3499,4 @@ function registerAdminRoutes(adminHttp, remoteObj = remote) {
|
|
|
3454
3499
|
});
|
|
3455
3500
|
}
|
|
3456
3501
|
|
|
3457
|
-
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, allocateParallelGroup, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, isEligibleForImmediateAutoFix, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, computeCommittedDuringRun, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH, forceTickOutcome, applyPauseCleared, detectNetworkErrorInLog, detectRateLimitInLog, classifyFailureOutcome, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, feedbackSweepDue, FEEDBACK_SWEEP_TICK_INTERVAL, sweepFeedback, registerAdminRoutes, notifyOriginatingTab, isNotifiableTerminalStatus };
|
|
3502
|
+
module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, 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, TRANSIENT_RETRY_CAP, buildScheduleStatePayload, partitionBootOrphans, applyOrphanOutcome, BOOT_ORPHAN_KILL_GRACE_MS, feedbackSweepDue, FEEDBACK_SWEEP_TICK_INTERVAL, sweepFeedback, registerAdminRoutes, notifyOriginatingTab, isNotifiableTerminalStatus };
|
package/src/main/transcripts.cjs
CHANGED
|
@@ -25,7 +25,6 @@ const os = require('node:os');
|
|
|
25
25
|
const chokidar = require('chokidar');
|
|
26
26
|
const otel = require('./otel.cjs');
|
|
27
27
|
const logs = require('./logs.cjs');
|
|
28
|
-
const usageMatrix = require('./usageMatrix.cjs');
|
|
29
28
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
30
29
|
|
|
31
30
|
let window = null;
|
|
@@ -103,7 +102,7 @@ async function readDelta(sub) {
|
|
|
103
102
|
}
|
|
104
103
|
}
|
|
105
104
|
|
|
106
|
-
async function doFlush(sub, { emit = true
|
|
105
|
+
async function doFlush(sub, { emit = true } = {}) {
|
|
107
106
|
const lines = await readDelta(sub);
|
|
108
107
|
for (const line of lines) {
|
|
109
108
|
let obj;
|
|
@@ -117,15 +116,6 @@ async function doFlush(sub, { emit = true, replay = false } = {}) {
|
|
|
117
116
|
// Ring buffer (cap at 500 entries to bound memory).
|
|
118
117
|
sub.buffer.push(ev);
|
|
119
118
|
if (sub.buffer.length > 500) sub.buffer.shift();
|
|
120
|
-
// Feed the AgOps aggregator on every event — both replay and live, so
|
|
121
|
-
// freshly-attached tabs land with full history reflected in the matrix.
|
|
122
|
-
usageMatrix.recordEvent({
|
|
123
|
-
tabId: sub.tabId,
|
|
124
|
-
cwd: sub.cwd,
|
|
125
|
-
sessionUuid: sub.sessionUuid,
|
|
126
|
-
ev,
|
|
127
|
-
replay,
|
|
128
|
-
});
|
|
129
119
|
if (emit) sendIfAlive(window, `transcript:event:${sub.tabId}`, ev);
|
|
130
120
|
// Mirror to OTEL — no-op when disabled. We emit on the initial drain too
|
|
131
121
|
// so backfilled transcripts show up in the trace store.
|
|
@@ -178,7 +168,6 @@ function _closeSub(tabId) {
|
|
|
178
168
|
if (!sub) return;
|
|
179
169
|
sub.watcher?.close().catch(() => {});
|
|
180
170
|
subs.delete(tabId);
|
|
181
|
-
usageMatrix.removeTab(tabId);
|
|
182
171
|
const i = lruReleased.indexOf(tabId);
|
|
183
172
|
if (i !== -1) lruReleased.splice(i, 1);
|
|
184
173
|
}
|
|
@@ -247,9 +236,8 @@ async function subscribe({ tabId, cwd, sessionUuid }) {
|
|
|
247
236
|
// during this initial drain — the renderer drains sub.buffer via
|
|
248
237
|
// `transcript:buffer` after `transcript:subscribe` resolves. Emitting here
|
|
249
238
|
// would race the renderer's onEvent listener registration and drop events.
|
|
250
|
-
// replay:true prevents historical usage events from entering the 5-min window.
|
|
251
239
|
if (fs.existsSync(filePath)) {
|
|
252
|
-
await doFlush(sub, { emit: false
|
|
240
|
+
await doFlush(sub, { emit: false });
|
|
253
241
|
}
|
|
254
242
|
const watcher = chokidar.watch(filePath, {
|
|
255
243
|
ignoreInitial: false,
|
|
@@ -277,7 +265,6 @@ function getBuffer(tabId) {
|
|
|
277
265
|
function closeAll() {
|
|
278
266
|
for (const sub of subs.values()) sub.watcher?.close().catch(() => {});
|
|
279
267
|
subs.clear();
|
|
280
|
-
usageMatrix.closeAll();
|
|
281
268
|
}
|
|
282
269
|
|
|
283
270
|
function registerTranscriptHandlers() {
|
package/src/preload/api.d.ts
CHANGED
|
@@ -176,6 +176,14 @@ export interface LoadedSessions {
|
|
|
176
176
|
freshStart?: boolean;
|
|
177
177
|
}
|
|
178
178
|
|
|
179
|
+
export interface LayoutEnvelope {
|
|
180
|
+
version: number;
|
|
181
|
+
/** Panel ids present in `dockview` at save time — pruned against the live registry on hydrate. */
|
|
182
|
+
panels: string[];
|
|
183
|
+
/** Opaque dockview `SerializedDockview` blob (from `api.toJSON()`/for `api.fromJSON()`). */
|
|
184
|
+
dockview: Record<string, unknown>;
|
|
185
|
+
}
|
|
186
|
+
|
|
179
187
|
export interface UsageWindow {
|
|
180
188
|
utilization: number;
|
|
181
189
|
resets_at: string | null;
|
|
@@ -228,54 +236,6 @@ export interface McpStatusResult {
|
|
|
228
236
|
checkedAt: number;
|
|
229
237
|
}
|
|
230
238
|
|
|
231
|
-
// ── Usage matrix (AgOps dashboard, main-side aggregator)
|
|
232
|
-
export type UsageMatrixState = 'idle' | 'executing' | 'plan' | 'background';
|
|
233
|
-
export type UsageMatrixIntensity = 'idle' | 'low' | 'medium' | 'critical';
|
|
234
|
-
export type UsageMatrixCacheState = 'warm' | 'expiring' | 'cold';
|
|
235
|
-
|
|
236
|
-
export interface UsageMatrixAlert {
|
|
237
|
-
level: 'info' | 'warn';
|
|
238
|
-
code:
|
|
239
|
-
| 'geometric-growth'
|
|
240
|
-
| 'long-session'
|
|
241
|
-
| 'cache-cold-start'
|
|
242
|
-
| 'cache-cold'
|
|
243
|
-
| 'subagent-fanout';
|
|
244
|
-
message: string;
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
export interface UsageMatrixTab {
|
|
248
|
-
tabId: string;
|
|
249
|
-
workspace: string;
|
|
250
|
-
cwd: string;
|
|
251
|
-
state: UsageMatrixState;
|
|
252
|
-
turns: number;
|
|
253
|
-
lastEventAt: number;
|
|
254
|
-
lastAssistantAt: number;
|
|
255
|
-
cacheAgeMs: number | null;
|
|
256
|
-
cacheState: UsageMatrixCacheState;
|
|
257
|
-
tokensPerMin: number;
|
|
258
|
-
intensity: UsageMatrixIntensity;
|
|
259
|
-
avgTokensPerTurn: number;
|
|
260
|
-
cumulativeUsage: { input: number; output: number; cacheRead: number; cacheCreate: number };
|
|
261
|
-
subagentsActive: number;
|
|
262
|
-
subagentsSpawned: number;
|
|
263
|
-
geometricGrowth: boolean;
|
|
264
|
-
alerts: UsageMatrixAlert[];
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
export interface UsageMatrixSnapshot {
|
|
268
|
-
generatedAt: number;
|
|
269
|
-
tabs: UsageMatrixTab[];
|
|
270
|
-
totals: {
|
|
271
|
-
activeSessions: number;
|
|
272
|
-
subagentsActive: number;
|
|
273
|
-
subagentsSpawned: number;
|
|
274
|
-
tokensTotal: number;
|
|
275
|
-
combinedTokensPerMin: number;
|
|
276
|
-
};
|
|
277
|
-
}
|
|
278
|
-
|
|
279
239
|
export interface VoiceHotkeyConfig {
|
|
280
240
|
accelerator: string;
|
|
281
241
|
mode: 'hold' | 'toggle';
|
|
@@ -691,19 +651,6 @@ export interface RepoAnalyzeResult {
|
|
|
691
651
|
}
|
|
692
652
|
export interface RepoAnalyzeError { ok: false; error: string }
|
|
693
653
|
|
|
694
|
-
export interface RecipeStep { agentName: string; note?: string }
|
|
695
|
-
export interface Recipe {
|
|
696
|
-
slug: string;
|
|
697
|
-
name: string;
|
|
698
|
-
description: string;
|
|
699
|
-
brief?: string;
|
|
700
|
-
steps: RecipeStep[];
|
|
701
|
-
}
|
|
702
|
-
// Channel keeps legacy "hives" name; concept is now "recipe".
|
|
703
|
-
export interface HiveListResult { hives: Recipe[]; error: string | null }
|
|
704
|
-
export interface HiveGetResult { hive: Recipe | null; error: string | null }
|
|
705
|
-
export interface HiveMutationResult { ok: boolean; error: string | null }
|
|
706
|
-
|
|
707
654
|
export interface WatcherInfo {
|
|
708
655
|
watcherId: string;
|
|
709
656
|
tabId: string;
|
|
@@ -956,40 +903,6 @@ export interface PluginInstallProgressEvent {
|
|
|
956
903
|
line: string;
|
|
957
904
|
}
|
|
958
905
|
|
|
959
|
-
// ────────────────────────────────────────────── SuperAgent
|
|
960
|
-
// "Boss" run that dispatches specialist subagents on the active tab's claude
|
|
961
|
-
// session. Renderer-driven progress — main only owns lifecycle + prompt write.
|
|
962
|
-
|
|
963
|
-
export type SuperAgentDepth = 'quick' | 'standard' | 'deep';
|
|
964
|
-
export type SuperAgentStatus = 'idle' | 'running' | 'done' | 'error';
|
|
965
|
-
|
|
966
|
-
export interface SuperAgentRunState {
|
|
967
|
-
status: SuperAgentStatus;
|
|
968
|
-
prompt: string;
|
|
969
|
-
specialistCount: number;
|
|
970
|
-
depth: SuperAgentDepth;
|
|
971
|
-
startedAt: number | null;
|
|
972
|
-
finishedAt: number | null;
|
|
973
|
-
error?: string;
|
|
974
|
-
}
|
|
975
|
-
|
|
976
|
-
export interface SuperAgentStartArgs {
|
|
977
|
-
tabId: string;
|
|
978
|
-
prompt: string;
|
|
979
|
-
specialistCount: number;
|
|
980
|
-
depth: SuperAgentDepth;
|
|
981
|
-
}
|
|
982
|
-
|
|
983
|
-
export interface SuperAgentStartResult {
|
|
984
|
-
ok: boolean;
|
|
985
|
-
error?: string;
|
|
986
|
-
}
|
|
987
|
-
|
|
988
|
-
export interface SuperAgentStateChangedEvent {
|
|
989
|
-
tabId: string;
|
|
990
|
-
state: SuperAgentRunState | null;
|
|
991
|
-
}
|
|
992
|
-
|
|
993
906
|
// ────────────────────────────────────────────── Web Remote (PRD 08)
|
|
994
907
|
|
|
995
908
|
export interface WebRemoteDevice {
|
|
@@ -1046,6 +959,28 @@ export interface ChatRunPayload {
|
|
|
1046
959
|
promptId?: string;
|
|
1047
960
|
}
|
|
1048
961
|
|
|
962
|
+
export interface ChatCreatePrdPayload {
|
|
963
|
+
title: string;
|
|
964
|
+
cwd: string;
|
|
965
|
+
estimateMinutes: number;
|
|
966
|
+
goal: string;
|
|
967
|
+
acceptanceCriteria: string[];
|
|
968
|
+
implementationNotes: string;
|
|
969
|
+
outOfScope?: string[];
|
|
970
|
+
slug?: string;
|
|
971
|
+
parallelGroup?: number;
|
|
972
|
+
/** Originating PromptTicket.id (PRD 748) — traces the PRD back to the prompt that spawned it. */
|
|
973
|
+
sourcePromptId?: string;
|
|
974
|
+
/** Originating tab id — used at job completion to route a status prompt back into the tab. */
|
|
975
|
+
sourceTabId?: string;
|
|
976
|
+
/** User-selected Feature/Bug tag (PRD 774) carried from the originating PromptTicket. */
|
|
977
|
+
tag?: 'feature' | 'bug';
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
export type ChatCreatePrdResult =
|
|
981
|
+
| { ok: true; nn: number; filename: string }
|
|
982
|
+
| { ok: false; status: number; error: string };
|
|
983
|
+
|
|
1049
984
|
export interface ChatRunQueuedEvent {
|
|
1050
985
|
tabId: string;
|
|
1051
986
|
sessionId: string;
|
|
@@ -1213,16 +1148,16 @@ export interface SessionManagerAPI {
|
|
|
1213
1148
|
load: () => Promise<LoadedSessions>;
|
|
1214
1149
|
save: (payload: LoadedSessions) => Promise<{ ok: boolean }>;
|
|
1215
1150
|
};
|
|
1151
|
+
layout: {
|
|
1152
|
+
load: () => Promise<LayoutEnvelope | null>;
|
|
1153
|
+
save: (payload: LayoutEnvelope) => Promise<{ ok: boolean }>;
|
|
1154
|
+
};
|
|
1216
1155
|
billing: {
|
|
1217
1156
|
fetch: () => Promise<BillingFetchResult>;
|
|
1218
1157
|
};
|
|
1219
1158
|
mcp: {
|
|
1220
1159
|
status: () => Promise<McpStatusResult>;
|
|
1221
1160
|
};
|
|
1222
|
-
usageMatrix: {
|
|
1223
|
-
snapshot: () => Promise<UsageMatrixSnapshot>;
|
|
1224
|
-
onTick: (handler: (snap: UsageMatrixSnapshot) => void) => () => void;
|
|
1225
|
-
};
|
|
1226
1161
|
logs: {
|
|
1227
1162
|
write: (scope: string, level: 'debug' | 'info' | 'warn' | 'error', message: string, meta?: unknown) => void;
|
|
1228
1163
|
dir: () => Promise<string>;
|
|
@@ -1315,12 +1250,6 @@ export interface SessionManagerAPI {
|
|
|
1315
1250
|
repo: {
|
|
1316
1251
|
analyze: (cwd: string) => Promise<RepoAnalyzeResult | RepoAnalyzeError>;
|
|
1317
1252
|
};
|
|
1318
|
-
hives: {
|
|
1319
|
-
list: () => Promise<HiveListResult>;
|
|
1320
|
-
get: (slug: string) => Promise<HiveGetResult>;
|
|
1321
|
-
save: (slug: string, hive: Recipe) => Promise<HiveMutationResult>;
|
|
1322
|
-
delete: (slug: string) => Promise<HiveMutationResult>;
|
|
1323
|
-
};
|
|
1324
1253
|
history: {
|
|
1325
1254
|
aggregate: (req?: HistoryAggregateRequest) => Promise<HistoryAggregateResult>;
|
|
1326
1255
|
scanProjects: () => Promise<SessionScanResult>;
|
|
@@ -1436,17 +1365,6 @@ export interface SessionManagerAPI {
|
|
|
1436
1365
|
* renderer needs per-row badges without a separate git call per file. */
|
|
1437
1366
|
fileStatus: (cwd: string) => Promise<GitFileStatusMap>;
|
|
1438
1367
|
};
|
|
1439
|
-
superagent: {
|
|
1440
|
-
/** Start a SuperAgent boss run — writes a structured prompt to the tab's
|
|
1441
|
-
* PTY asking Claude to pick + dispatch specialists. Single run per tab. */
|
|
1442
|
-
start: (args: SuperAgentStartArgs) => Promise<SuperAgentStartResult>;
|
|
1443
|
-
/** Current run state for tab, or null if no run has been started. */
|
|
1444
|
-
status: (tabId: string) => Promise<SuperAgentRunState | null>;
|
|
1445
|
-
/** Mark the run as done. Does not interrupt Claude — the user can stop
|
|
1446
|
-
* the in-PTY work via Ctrl-C; this only flips the renderer indicator. */
|
|
1447
|
-
stop: (tabId: string) => Promise<{ ok: boolean }>;
|
|
1448
|
-
onStateChanged: (handler: (ev: SuperAgentStateChangedEvent) => void) => () => void;
|
|
1449
|
-
};
|
|
1450
1368
|
webRemote: {
|
|
1451
1369
|
getStatus: () => Promise<WebRemoteStatus>;
|
|
1452
1370
|
enable: () => Promise<WebRemoteMutationResult>;
|
|
@@ -1490,6 +1408,11 @@ export interface SessionManagerAPI {
|
|
|
1490
1408
|
* chatRunner) or 'develop' (dispatch to /develop for PRD decomposition).
|
|
1491
1409
|
* A single bounded `claude -p` call, never a scheduler job. */
|
|
1492
1410
|
classifyTicket: (payload: { text: string }) => Promise<'inline' | 'develop'>;
|
|
1411
|
+
/** Author a mechanical draft PRD from a queued PromptTicket classified
|
|
1412
|
+
* 'develop' — an in-process call to prdCreate.createPrd, never an HTTP
|
|
1413
|
+
* round-trip. The draft is meant to be reviewed/edited in the Scheduler
|
|
1414
|
+
* tab's PRD editor before it runs. */
|
|
1415
|
+
createPrd: (payload: ChatCreatePrdPayload) => Promise<ChatCreatePrdResult>;
|
|
1493
1416
|
};
|
|
1494
1417
|
exchanges: {
|
|
1495
1418
|
/** Durable per-exchange log entries for a project, newest-first (max 100 by default). */
|
package/src/preload/index.cjs
CHANGED
|
@@ -132,20 +132,16 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
132
132
|
load: () => ipcRenderer.invoke('sessions:load'),
|
|
133
133
|
save: (payload) => ipcRenderer.invoke('sessions:save', payload),
|
|
134
134
|
},
|
|
135
|
+
layout: {
|
|
136
|
+
load: () => ipcRenderer.invoke('layout:load'),
|
|
137
|
+
save: (payload) => ipcRenderer.invoke('layout:save', payload),
|
|
138
|
+
},
|
|
135
139
|
billing: {
|
|
136
140
|
fetch: () => ipcRenderer.invoke('billing:fetch'),
|
|
137
141
|
},
|
|
138
142
|
mcp: {
|
|
139
143
|
status: () => ipcRenderer.invoke('mcp:status'),
|
|
140
144
|
},
|
|
141
|
-
usageMatrix: {
|
|
142
|
-
snapshot: () => ipcRenderer.invoke('usage:matrix:snapshot'),
|
|
143
|
-
onTick: (handler) => {
|
|
144
|
-
const listener = (_e, payload) => handler(payload);
|
|
145
|
-
ipcRenderer.on('usage:matrix:tick', listener);
|
|
146
|
-
return () => ipcRenderer.removeListener('usage:matrix:tick', listener);
|
|
147
|
-
},
|
|
148
|
-
},
|
|
149
145
|
logs: {
|
|
150
146
|
write: (scope, level, message, meta) =>
|
|
151
147
|
ipcRenderer.send('log:write', { scope, level, message, meta }),
|
|
@@ -248,12 +244,6 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
248
244
|
repo: {
|
|
249
245
|
analyze: (cwd) => ipcRenderer.invoke('repo:analyze', { cwd }),
|
|
250
246
|
},
|
|
251
|
-
hives: {
|
|
252
|
-
list: () => ipcRenderer.invoke('hives:list'),
|
|
253
|
-
get: (slug) => ipcRenderer.invoke('hives:get', { slug }),
|
|
254
|
-
save: (slug, hive) => ipcRenderer.invoke('hives:save', { slug, hive }),
|
|
255
|
-
delete: (slug) => ipcRenderer.invoke('hives:delete', { slug }),
|
|
256
|
-
},
|
|
257
247
|
schedule: {
|
|
258
248
|
state: () => ipcRenderer.invoke('schedule:state'),
|
|
259
249
|
setConfig: (partial) => ipcRenderer.invoke('schedule:set-config', partial),
|
|
@@ -338,19 +328,6 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
338
328
|
status: (cwd) => ipcRenderer.invoke('git:status', { cwd }),
|
|
339
329
|
fileStatus: (cwd) => ipcRenderer.invoke('git:file-status', { cwd }),
|
|
340
330
|
},
|
|
341
|
-
superagent: {
|
|
342
|
-
/** Start a SuperAgent boss run on a tab — writes a structured prompt to
|
|
343
|
-
* the PTY asking Claude to pick + dispatch specialists. Single live run
|
|
344
|
-
* per tab; starting again on a running tab terminates the prior one. */
|
|
345
|
-
start: (payload) => ipcRenderer.invoke('superagent:start', payload),
|
|
346
|
-
status: (tabId) => ipcRenderer.invoke('superagent:status', { tabId }),
|
|
347
|
-
stop: (tabId) => ipcRenderer.invoke('superagent:stop', { tabId }),
|
|
348
|
-
onStateChanged: (handler) => {
|
|
349
|
-
const listener = (_e, payload) => handler(payload);
|
|
350
|
-
ipcRenderer.on('superagent:state-changed', listener);
|
|
351
|
-
return () => ipcRenderer.removeListener('superagent:state-changed', listener);
|
|
352
|
-
},
|
|
353
|
-
},
|
|
354
331
|
webRemote: {
|
|
355
332
|
/** Current connection state (no token values). */
|
|
356
333
|
getStatus: () => ipcRenderer.invoke('webRemote:get-status'),
|
|
@@ -445,6 +422,8 @@ contextBridge.exposeInMainWorld('api', {
|
|
|
445
422
|
},
|
|
446
423
|
/** Classify a queued PromptTicket's text as 'inline' or 'develop'. */
|
|
447
424
|
classifyTicket: (payload) => ipcRenderer.invoke('chat:classify-ticket', payload),
|
|
425
|
+
/** Author a mechanical draft PRD from a queued PromptTicket classified 'develop'. */
|
|
426
|
+
createPrd: (payload) => ipcRenderer.invoke('chat:create-prd', payload),
|
|
448
427
|
},
|
|
449
428
|
exchanges: {
|
|
450
429
|
/** List exchanges for a project (durable chat-run log), newest-first.
|