claude-code-session-manager 0.38.5 → 0.38.6

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 (29) hide show
  1. package/dist/assets/{TiptapBody-D1ufJtTb.js → TiptapBody-CTotrCub.js} +1 -1
  2. package/dist/assets/index-BGTU12rl.css +32 -0
  3. package/dist/assets/index-C54zva1x.js +3178 -0
  4. package/dist/index.html +2 -2
  5. package/package.json +2 -1
  6. package/plugins/session-manager-dev/skills/blog-for-project-feature/2-storyboard/SKILL.md +35 -9
  7. package/plugins/session-manager-dev/skills/blog-for-project-feature/3-capture/SKILL.md +16 -7
  8. package/plugins/session-manager-dev/skills/blog-for-project-feature/6-compose/SKILL.md +8 -5
  9. package/plugins/session-manager-dev/skills/blog-for-project-feature/7-review/SKILL.md +7 -4
  10. package/plugins/session-manager-dev/skills/blog-for-project-feature/SKILL.md +1 -1
  11. package/plugins/session-manager-dev/skills/blog-for-project-feature/feature.config.yaml +11 -2
  12. package/src/main/__tests__/prdCreate.test.cjs +55 -1
  13. package/src/main/__tests__/queueOps-interactive-ac-lint.test.cjs +65 -0
  14. package/src/main/__tests__/scheduler-sigterm-commit.test.cjs +43 -0
  15. package/src/main/index.cjs +10 -10
  16. package/src/main/ipcSchemas.cjs +17 -16
  17. package/src/main/layoutStore.cjs +58 -0
  18. package/src/main/lib/prdCreate.cjs +66 -43
  19. package/src/main/pty.cjs +0 -5
  20. package/src/main/queueOps.cjs +58 -0
  21. package/src/main/scheduler.cjs +48 -3
  22. package/src/main/transcripts.cjs +2 -15
  23. package/src/preload/api.d.ts +39 -116
  24. package/src/preload/index.cjs +6 -27
  25. package/dist/assets/index-CZrfXQpq.css +0 -32
  26. package/dist/assets/index-DwjviHVo.js +0 -3501
  27. package/src/main/hives.cjs +0 -287
  28. package/src/main/superagent.cjs +0 -212
  29. 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
- // cwd is untrusted: this is the first *creating* mutation on a
116
- // token-authed API whose product is "a command that will later run
117
- // with --dangerously-skip-permissions in a chosen cwd". Route it
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: `${filenameSlug}.md`, status: 'queued' });
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
  }
@@ -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 };
@@ -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
- if (res.exitCode !== 0) {
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 };
@@ -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, replay = false } = {}) {
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, replay: true });
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() {
@@ -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). */
@@ -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.