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
package/dist/index.html CHANGED
@@ -7,10 +7,10 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-DwjviHVo.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-C54zva1x.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
- <link rel="stylesheet" crossorigin href="./assets/index-CZrfXQpq.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-BGTU12rl.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.38.5",
3
+ "version": "0.38.6",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -98,6 +98,7 @@
98
98
  "@xterm/addon-web-links": "^0.11.0",
99
99
  "@xterm/xterm": "^5.5.0",
100
100
  "chokidar": "^4.0.1",
101
+ "dockview-react": "^6.6.1",
101
102
  "dompurify": "^3.4.8",
102
103
  "electron": "42.1.0",
103
104
  "framer-motion": "^12.38.0",
@@ -12,8 +12,8 @@ Mission/description + feature description + demo surface (routes, run command, s
12
12
  ## Steps
13
13
 
14
14
  1. **Break the feature into aspects**, not just clicks. Each storyboard step should reveal ONE
15
- idea — a screen, a state change, a result — not an arbitrary UI action. 4-8 steps is a good
16
- range; fewer feels thin, more starts diluting the "one aspect at a time" promise.
15
+ idea — a screen, a state change, a result — not an arbitrary UI action. **5-10 steps** ("swipes")
16
+ is the target range; fewer feels thin, more starts diluting the "one aspect at a time" promise.
17
17
 
18
18
  2. **For each step, define:**
19
19
  - `step`: index + short name (e.g. `2-queue-populated`)
@@ -21,18 +21,44 @@ Mission/description + feature description + demo surface (routes, run command, s
21
21
  submit form with Z) — precise enough that `:capture` can execute it without re-deciding
22
22
  - `expected_screen`: what should be visible when the action completes — this is the
23
23
  hypothesis `:capture` verifies against the real app
24
+ - `capture_scope`: `full_page` or `component` (see the composition recipe below) — plus, when
25
+ `component`, which element/region to frame (a CSS selector, ARIA role+name, or plain
26
+ description precise enough for `:capture` to target a single element screenshot instead of
27
+ the whole viewport)
24
28
  - `caption`: one sentence, written for the reader of the final page, naming what changed on
25
29
  screen (not internal mechanics)
26
30
 
27
- 3. **Order for narrative sense**: typically entry point → the feature's core interaction →
28
- the result/outcome it produces. Match `content.demo_interaction` in `feature.config.yaml`
29
- (one-step reveal, not a scroll dump) — so the order IS the reveal sequence.
31
+ 3. **Compose the sequence with this recipe — authoritative, not a suggestion.** A full-page shot
32
+ every step is disorienting (the reader has to re-find the relevant thing each time); an
33
+ all-component sequence loses context (the reader forgets where they are in the app). Mix them
34
+ deliberately in this arc:
35
+ - **Orientation (1 step, `full_page`).** Where the feature lives in the real product — the
36
+ screen the reader would actually land on. Establishes context before zooming in.
37
+ - **Mechanism (2-5 steps, `component`).** The actual control, interaction, or state change,
38
+ cropped/framed to just that component so the reader's eye isn't hunting across a full
39
+ screen for what changed. This is the bulk of the demo — most features are *shown* here.
40
+ - **Variation or depth (0-2 steps, mixed).** Secondary states, options, or edge cases worth
41
+ surfacing. Default to `component`; use `full_page` only if the variation reshapes the whole
42
+ screen (a new page, a full dashboard swap) rather than one region of it.
43
+ - **Outcome (1-2 steps, `full_page`).** The result landing back in the real product, in
44
+ context — proof the feature actually changed something the user will encounter, not just a
45
+ component in isolation.
30
46
 
31
- 4. **Note any setup steps** needed before step 1 is reachable (login, seed data, feature flag)
47
+ Decision rule when a step doesn't obviously fit: default to `component` — it isolates the
48
+ thing being demonstrated and reads more clearly at small size. Only use `full_page` for
49
+ orientation/outcome steps, or when the change genuinely spans the whole viewport (new page,
50
+ layout shift, multi-panel result) and a cropped shot would lose the point.
51
+
52
+ 4. **Order for narrative sense**: entry point → mechanism → variation → outcome, per the recipe
53
+ above. Match `content.demo_interaction` in `feature.config.yaml` (one-step reveal, not a
54
+ scroll dump) — so the order IS the reveal sequence.
55
+
56
+ 5. **Note any setup steps** needed before step 1 is reachable (login, seed data, feature flag)
32
57
  — hand these to `:capture` explicitly so it isn't guessing at prerequisites.
33
58
 
34
59
  ## Output
35
60
 
36
- Hand off to `:capture`: the ordered step list (`step`, `action`, `expected_screen`, `caption`)
37
- plus any setup/prerequisite steps. This is a draft — `:capture` is expected to correct captions
38
- or drop steps that don't match what the real app actually shows.
61
+ Hand off to `:capture`: the ordered step list (`step`, `action`, `expected_screen`,
62
+ `capture_scope`, `caption`) plus any setup/prerequisite steps. This is a draft — `:capture` is
63
+ expected to correct captions, adjust `capture_scope` if the planned framing doesn't hold up
64
+ against the real screen, or drop steps that don't match what the real app actually shows.
@@ -6,8 +6,8 @@ description: Step 3 of blog-for-project-feature — drive the REAL running app a
6
6
  # blog-for-project-feature:capture
7
7
 
8
8
  ## Input
9
- Ordered storyboard steps (`step`, `action`, `expected_screen`, `caption`) + setup steps, from
10
- `:storyboard`.
9
+ Ordered storyboard steps (`step`, `action`, `expected_screen`, `capture_scope`, `caption`) +
10
+ setup steps, from `:storyboard`.
11
11
 
12
12
  ## Steps
13
13
 
@@ -22,12 +22,21 @@ Ordered storyboard steps (`step`, `action`, `expected_screen`, `caption`) + setu
22
22
  3. **For each storyboard step, in order:**
23
23
  - Perform the `action` using `mcp__playwright__browser_navigate` /
24
24
  `browser_click` / `browser_type` / etc.
25
- - Take the screenshot with `mcp__playwright__browser_take_screenshot`.
25
+ - Take the screenshot per the step's `capture_scope`:
26
+ - `full_page`: `mcp__playwright__browser_take_screenshot` for the full viewport/page.
27
+ - `component`: use a `ref`/element target (via `browser_snapshot` to locate the element,
28
+ then an element-scoped `browser_take_screenshot`) framed to the specific control or
29
+ region the step calls out — not the whole viewport. If the described element can't be
30
+ resolved to a single clean region, fall back to `full_page` for that step rather than
31
+ guessing at a crop.
26
32
  - Compare what actually rendered against `expected_screen`. If it matches: keep the step,
27
- correct the caption only if something material differs (a number, a label). If it doesn't
28
- match at all (error state, feature not reachable, requires access you don't have): **drop
29
- this step and record why** — do not screenshot an error page and caption around it, and
30
- never substitute a mockup, wireframe, or hand-drawn image for a missing real one.
33
+ correct the caption only if something material differs (a number, a label). If the planned
34
+ `capture_scope` doesn't hold up (the "component" is actually two disconnected regions, or a
35
+ "full_page" shot is so busy the point gets lost), correct `capture_scope` for that step
36
+ before saving. If the step doesn't match `expected_screen` at all (error state, feature not
37
+ reachable, requires access you don't have): **drop this step and record why** — do not
38
+ screenshot an error page and caption around it, and never substitute a mockup, wireframe, or
39
+ hand-drawn image for a missing real one.
31
40
 
32
41
  4. **Save images** per `feature.config.yaml`'s `images` block (format, max width, compression) —
33
42
  downscale before handing to `:compose`, don't pass raw multi-MB captures through.
@@ -18,15 +18,18 @@ its omission-reason (from `:illustrate`).
18
18
  the `folder_fallback_layout` instead (html + `screenshots/` dir, relative paths).
19
19
 
20
20
  2. **Build the page**, self-contained (inline `<style>`/`<script>`, no external CDN/font/JS
21
- requests — must work fully offline via `file://`), with exactly the three required sections
22
- in order:
21
+ requests — must work fully offline via `file://`), with exactly the three required sections,
22
+ **Click-Through Demo first** — the real screenshots are the most persuasive thing on the page,
23
+ so they lead, above the fold, before any prose:
24
+ - **Click-Through Demo** — the captured steps rendered as a one-step-at-a-time viewer
25
+ (numbered tabs or next/prev buttons — vanilla JS, no framework). Each step's real image
26
+ renders on top, full width within the viewer, with its caption directly below it — image
27
+ first, caption second, never the reverse. Screenshots only — the illustration never appears
28
+ in this section. This section opens the page, immediately above the fold.
23
29
  - **Project Mission & Description** — the finalized text from `:draft-copy`, verbatim
24
30
  - **Feature Description** — the finalized text from `:draft-copy`, verbatim. If a companion
25
31
  illustration exists, place it here as a header/hero image for the section — never inside
26
32
  the Click-Through Demo viewer, so it's never confusable with a real screenshot
27
- - **Click-Through Demo** — the captured steps rendered as a one-step-at-a-time viewer
28
- (numbered tabs or next/prev buttons — vanilla JS, no framework), each step showing its real
29
- image + caption. Screenshots only — the illustration never appears in this section
30
33
  - If `:illustrate` reported an omission, skip the hero image silently (it's an optional
31
34
  companion, not a required section) — do not note the omission on the page itself
32
35
 
@@ -24,13 +24,16 @@ Check every item in `feature.config.yaml`'s `gates` list against the actual file
24
24
  3. Confirm all three required sections are present and non-empty (or explicitly gap-noted per
25
25
  `:compose` step 5, which is an acceptable non-empty state). The companion illustration is
26
26
  optional — its absence is never a gate failure.
27
- 4. Confirm the demo is genuinely click-through — the next/prev or tab controls exist and their
27
+ 4. Confirm the section order matches `feature.config.yaml`'s `content.sections_required` —
28
+ Click-Through Demo leads the page (above the fold, before the prose sections), each visible
29
+ step showing its real image on top and the caption directly below it.
30
+ 5. Confirm the demo is genuinely click-through — the next/prev or tab controls exist and their
28
31
  JS actually switches the visible step (not just a cosmetic active-class flip with all steps
29
32
  rendered stacked underneath).
30
- 5. Confirm no external network calls (grep for `http://`/`https://` in `<script src>`,
33
+ 6. Confirm no external network calls (grep for `http://`/`https://` in `<script src>`,
31
34
  `<link href>`, `fetch(`, `@import` — should be none).
32
- 6. Confirm the size ceiling was respected for the mode chosen.
33
- 7. Spot-check 2-3 sentences in the Mission/Feature sections against `:research`'s notes for
35
+ 7. Confirm the size ceiling was respected for the mode chosen.
36
+ 8. Spot-check 2-3 sentences in the Mission/Feature sections against `:research`'s notes for
34
37
  traceability — flag anything that reads like invented marketing language.
35
38
 
36
39
  ## On failure
@@ -74,7 +74,7 @@ project (+ optional feature name)
74
74
  |---|---|---|---|
75
75
  | 0. `:select` | project (cwd or named) + optional feature hint | one confirmed project + one confirmed feature name + why it's demo-worthy | **STOP** — report if no clear feature can be confirmed (don't guess) |
76
76
  | 1. `:research` | confirmed project + feature | mission/description (project-level), feature description, key files, how it actually works | n/a — always produces notes, flags gaps explicitly rather than inventing claims |
77
- | 2. `:storyboard` | mission + feature notes | ordered list of `{step, action, expected_screen, caption}` — one aspect per step | n/a — storyboard is a draft, `:capture` is allowed to correct it |
77
+ | 2. `:storyboard` | mission + feature notes | ordered list of `{step, action, expected_screen, capture_scope, caption}`, 5-10 steps, a deliberate mix of full-page and component-scoped shots | n/a — storyboard is a draft, `:capture` is allowed to correct it |
78
78
  | 3. `:capture` | storyboard | one real screenshot file per step + any corrected captions | **STOP** on that step — report what's missing; never substitute a placeholder/mockup image |
79
79
  | 4. `:draft-copy` | research notes + captured captions | final Mission & Feature Description prose + one-sentence essence line | n/a — always produces final text, gaps stay explicit |
80
80
  | 5. `:illustrate` | the essence line (text must exist first) | one companion illustration, tagged distinct from screenshots — or an explicit omission note | never blocks — skips cleanly if no image generator is configured, or after one off-brief retry |
@@ -28,6 +28,12 @@ images:
28
28
  compression: "reasonable lossy ok for PNG->WebP if it keeps text legible; never so lossy captions become unreadable"
29
29
  source_of_truth: "mcp__playwright__browser_take_screenshot against a real running instance"
30
30
  placeholder_or_mockup_images: forbidden # a dropped step beats a fabricated one
31
+ capture_scope: "full_page | component, chosen per step in :storyboard and executed in :capture.
32
+ Full-page screenshots are not always the clearest way to show a feature — a control or state
33
+ change is often clearer cropped to just that component. Default to component-scoped shots for
34
+ mechanism/detail steps; reserve full_page for orientation (where the feature lives) and
35
+ outcome (the result landing back in the real product) steps. See :storyboard's composition
36
+ recipe for the authoritative mix."
31
37
 
32
38
  illustration:
33
39
  # The ONE companion image, generated by :illustrate — a distinct image class from the
@@ -49,10 +55,13 @@ illustration:
49
55
 
50
56
  content:
51
57
  sections_required:
58
+ # Order matters: this IS the on-page order, top to bottom.
59
+ - "Click-Through Demo" # LEADS the page — the storyboard steps, one aspect
60
+ # revealed at a time; screenshot on top, caption below
52
61
  - "Project Mission & Description" # 2-4 sentences, from :research, project-level not feature-level
53
62
  - "Feature Description" # what it does, why it matters, 1-2 short paragraphs
54
- - "Click-Through Demo" # the storyboard steps, one aspect revealed at a time
55
- demo_interaction: "one-step-at-a-time reveal (next/prev controls or numbered tabs) — never a single long scroll dump of every screenshot at once"
63
+ demo_interaction: "one-step-at-a-time reveal (next/prev controls or numbered tabs) — never a single long scroll dump of every screenshot at once. Each step: real image on top, caption directly below it."
64
+ demo_step_count: "5-10 steps (a mix of full_page and component-scoped captures — see :storyboard's composition recipe), not a flat full-page-per-step dump"
56
65
  tone: "same plain, non-salesy, show-don't-tell bar as blog-from-git/voice.md bot-tell blocklist — no 'excited to share', no grand-implication filler"
57
66
  caption_rule: "every screenshot has a one-sentence caption naming what changed on screen, not what the code does internally"
58
67
 
@@ -9,7 +9,7 @@
9
9
 
10
10
  'use strict';
11
11
 
12
- import { test, expect } from 'vitest';
12
+ import { test, expect, vi } from 'vitest';
13
13
  const fs = require('node:fs');
14
14
  const fsp = require('node:fs/promises');
15
15
  const os = require('node:os');
@@ -22,6 +22,7 @@ const {
22
22
  STANDARDS_PATH,
23
23
  PRD_CREATE_SLUG_RE,
24
24
  registerAdminRoute,
25
+ createPrd,
25
26
  } = require('../lib/prdCreate.cjs');
26
27
  const { createAdminHttp } = require('../lib/localAdminHttp.cjs');
27
28
  const config = require('../config.cjs');
@@ -338,6 +339,59 @@ test('POST /admin/scheduler/create-prd honors an explicit slug + parallelGroup i
338
339
  }
339
340
  });
340
341
 
342
+ // ──────────────────────────────────────────── createPrd() (chat:create-prd IPC handler, PRD 749 follow-up)
343
+ //
344
+ // createPrd() is the function ipcMain.handle('chat:create-prd', ...) in
345
+ // index.cjs calls directly (in-process, no HTTP) — same function
346
+ // registerAdminRoute's HTTP handler calls above. These tests exercise it
347
+ // directly, mirroring the handler's own call shape: createPrd(input, remote).
348
+
349
+ test('createPrd rejects a cwd outside allowedRoots before any write, returning {ok:false, status:400}', async () => {
350
+ const prdsDir = await mkTmpPrdsDir();
351
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
352
+ const writePrdSpy = vi.fn(remote.writePrd);
353
+ remote.writePrd = writePrdSpy;
354
+
355
+ const result = await createPrd(validCreateBody({ cwd: '/etc/passwd-adjacent-outside-home' }), remote);
356
+
357
+ expect(result.ok).toBe(false);
358
+ expect(result.status).toBe(400);
359
+ expect(writePrdSpy).not.toHaveBeenCalled();
360
+ const entries = await fsp.readdir(prdsDir);
361
+ expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
362
+ });
363
+
364
+ test('createPrd with a valid payload allocates a group, writes through remote.writePrd, and returns {ok:true, nn, filename}', async () => {
365
+ const prdsDir = await mkTmpPrdsDir();
366
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
367
+
368
+ const result = await createPrd(validCreateBody({ sourcePromptId: 'ticket-abc', sourceTabId: 'tab-1' }), remote);
369
+
370
+ expect(result.ok).toBe(true);
371
+ expect(typeof result.nn).toBe('number');
372
+ expect(result.filename).toMatch(/^\d+-add-widget-frobnication\.md$/);
373
+
374
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
375
+ expect(written).toMatch(/^---\n/);
376
+ expect(written).toMatch(/title: Add widget frobnication/);
377
+ expect(written).toMatch(/sourcePromptId: ticket-abc/);
378
+ expect(written).toMatch(/sourceTabId: tab-1/);
379
+ expect(written).toMatch(/# Goal/);
380
+ });
381
+
382
+ test('createPrd returns {ok:false, status:409} without clobbering an existing file at the same destination', async () => {
383
+ const prdsDir = await mkTmpPrdsDir();
384
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
385
+ await fsp.writeFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'ORIGINAL CONTENT\n');
386
+
387
+ const result = await createPrd(validCreateBody({ slug: 'my-explicit-slug', parallelGroup: 777 }), remote);
388
+
389
+ expect(result.ok).toBe(false);
390
+ expect(result.status).toBe(409);
391
+ const stillThere = await fsp.readFile(path.join(prdsDir, '777-my-explicit-slug.md'), 'utf8');
392
+ expect(stillThere).toBe('ORIGINAL CONTENT\n');
393
+ });
394
+
341
395
  test('POST /admin/scheduler/create-prd with sourcePromptId writes it into the created PRD frontmatter', async () => {
342
396
  const prdsDir = await mkTmpPrdsDir();
343
397
  const remote = makeFakeRemoteWithPrdsDir(prdsDir);
@@ -0,0 +1,65 @@
1
+ /**
2
+ * queueOps-interactive-ac-lint.test.cjs — lint for AC checkboxes that
3
+ * instruct an interactive/GUI-rendering step (xvfb-run, playwright test,
4
+ * electron.launch, screenshot, ...) that a headless claude -p executor can
5
+ * never satisfy (see the 776/779 exit143 incidents, 2026-07-30). Must key off
6
+ * AC-checkbox context specifically — a PRD's Out-of-scope/notes prose telling
7
+ * the executor NOT to do this (PRDs 780/787/788's pattern) must not trigger.
8
+ *
9
+ * Run: timeout 120 npx vitest run src/main/__tests__/queueOps-interactive-ac-lint.test.cjs
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import { test, expect } from 'vitest';
15
+ const fs = require('node:fs');
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+ const { lintOneAsync } = require('../queueOps.cjs');
19
+
20
+ function writeTmpPrd(body) {
21
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-lint-interactive-'));
22
+ const p = path.join(dir, 'test-prd.md');
23
+ const raw = `---\ntitle: "test"\ncwd: ~\nestimateMinutes: 10\n---\n\n${body}\n`;
24
+ fs.writeFileSync(p, raw, 'utf8');
25
+ return p;
26
+ }
27
+
28
+ test('flags a PRD with xvfb-run as an AC checkbox line', async () => {
29
+ const p = writeTmpPrd(
30
+ '# Acceptance criteria\n\n- [ ] `xvfb-run -a npx playwright test tests/e2e/foo.spec.ts` passes\n',
31
+ );
32
+ const { findings } = await lintOneAsync(p);
33
+ expect(findings.some((f) => f.rule === 'interactive-xvfb')).toBe(true);
34
+ });
35
+
36
+ test('flags a PRD with playwright electron.launch as an AC checkbox line', async () => {
37
+ const p = writeTmpPrd(
38
+ '# Acceptance criteria\n\n- [ ] Playwright `electron.launch` boots the app and captures a screenshot of the Scheduler tab\n',
39
+ );
40
+ const { findings } = await lintOneAsync(p);
41
+ expect(findings.some((f) => f.rule === 'interactive-electron-launch')).toBe(true);
42
+ });
43
+
44
+ test('does NOT flag Out-of-scope/notes prose that mentions xvfb without being a checkbox line', async () => {
45
+ const p = writeTmpPrd(
46
+ '# Acceptance criteria\n\n- [ ] `timeout 300 npm run typecheck` passes\n' +
47
+ '- [ ] `timeout 300 npx vitest run src/foo.test.ts` passes\n\n' +
48
+ 'NOTE: do NOT add a `playwright test ... under xvfb` acceptance criterion here. Two prior\n' +
49
+ 'links in this chain stalled and were SIGTERM\'d (exit 143) on exactly that step — a headless\n' +
50
+ '`claude -p` executor spawning `xvfb-run`/Playwright hits a tool-use rejection and hangs.\n\n' +
51
+ '# Out of scope\n\n' +
52
+ '- Making xvfb-run/Playwright actually work inside a headless claude -p job — out of scope\n',
53
+ );
54
+ const { findings } = await lintOneAsync(p);
55
+ expect(findings.filter((f) => f.rule.startsWith('interactive-')).length).toBe(0);
56
+ });
57
+
58
+ test('a clean PRD with only typecheck/vitest AC is not flagged', async () => {
59
+ const p = writeTmpPrd(
60
+ '# Acceptance criteria\n\n- [ ] `timeout 300 npm run typecheck` passes\n' +
61
+ '- [ ] `timeout 300 npx vitest run src/foo.test.ts` passes\n',
62
+ );
63
+ const { findings } = await lintOneAsync(p);
64
+ expect(findings.filter((f) => f.rule.startsWith('interactive-')).length).toBe(0);
65
+ });
@@ -0,0 +1,43 @@
1
+ /**
2
+ * scheduler-sigterm-commit.test.cjs — a run SIGTERM'd (exit 143) mid an
3
+ * interactive/headless-incompatible AC step (xvfb Electron screenshot,
4
+ * playwright electron.launch — see the 776/779 incidents, 2026-07-30) can
5
+ * still have landed its deliverable commit before it died. classifySigtermWithCommit
6
+ * decides whether that run should be routed to needs_review (commit found —
7
+ * a human should verify the rest of the AC) instead of staying `failed`
8
+ * forever, without ever silently auto-promoting to `completed`.
9
+ *
10
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-sigterm-commit.test.cjs
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ import { test, expect } from 'vitest';
16
+ const { classifySigtermWithCommit } = require('../scheduler.cjs');
17
+
18
+ test('classifySigtermWithCommit: exit 143 + commit found in window → needs_review', () => {
19
+ const decision = classifySigtermWithCommit(143, true);
20
+ expect(decision.status).toBe('needs_review');
21
+ expect(decision.reason).toMatch(/SIGTERM/);
22
+ expect(decision.reason).toMatch(/verify AC/i);
23
+ });
24
+
25
+ test('classifySigtermWithCommit: exit 143 + no commit found in window → still failed (regression check)', () => {
26
+ const decision = classifySigtermWithCommit(143, false);
27
+ expect(decision).toBeNull();
28
+ });
29
+
30
+ test('classifySigtermWithCommit: exit 1 (non-143 code) is unaffected even with a commit found', () => {
31
+ const decision = classifySigtermWithCommit(1, true);
32
+ expect(decision).toBeNull();
33
+ });
34
+
35
+ test('classifySigtermWithCommit: exit 137 (SIGKILL) is unaffected — scoped narrowly to 143', () => {
36
+ const decision = classifySigtermWithCommit(137, true);
37
+ expect(decision).toBeNull();
38
+ });
39
+
40
+ test('classifySigtermWithCommit: exit 0 is unaffected', () => {
41
+ const decision = classifySigtermWithCommit(0, true);
42
+ expect(decision).toBeNull();
43
+ });
@@ -12,8 +12,8 @@ const browserView = require('./browserView.cjs');
12
12
  const browserCapture = require('./browserCapture.cjs');
13
13
  const configMgr = require('./config.cjs');
14
14
  const transcripts = require('./transcripts.cjs');
15
- const usageMatrix = require('./usageMatrix.cjs');
16
15
  const sessionsStore = require('./sessionsStore.cjs');
16
+ const layoutStore = require('./layoutStore.cjs');
17
17
  const billing = require('./usage.cjs');
18
18
  const { probeMcpStatus } = require('./mcpStatus.cjs');
19
19
  const logs = require('./logs.cjs');
@@ -55,12 +55,10 @@ const memoryTool = require('./memoryTool.cjs');
55
55
  const { registerMemoryAggregateIpc } = require('./memoryAggregate.cjs');
56
56
  const agentMemory = require('./agentMemory.cjs');
57
57
  const git = require('./git.cjs');
58
- const superagent = require('./superagent.cjs');
59
58
  const filesIpc = require('./files.cjs');
60
59
  const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
61
60
  const searchIpc = require('./search.cjs');
62
61
  const repoAnalyzer = require('./repoAnalyzer.cjs');
63
- const hivesIpc = require('./hives.cjs');
64
62
  const webRemote = require('./webRemote.cjs');
65
63
  const { listExchanges } = require('./exchanges.cjs');
66
64
  const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
@@ -298,7 +296,6 @@ async function rebootApp() {
298
296
  ptyManager.attachWindow(mainWindow);
299
297
  configMgr.attachWindow(mainWindow);
300
298
  transcripts.attachWindow(mainWindow);
301
- usageMatrix.attachWindow(mainWindow);
302
299
  browserView.attachWindow(mainWindow);
303
300
  browserCapture.attachWindow(mainWindow);
304
301
  voiceHotkey.init(mainWindow).catch((e) => {
@@ -307,7 +304,6 @@ async function rebootApp() {
307
304
  scheduler.attachWindow(mainWindow);
308
305
  watchers.attachWindow(mainWindow);
309
306
  pluginInstall.attachWindow(mainWindow);
310
- superagent.attachWindow(mainWindow);
311
307
  chatRunner.attachWindow(mainWindow);
312
308
  attachDocEditWindow(mainWindow);
313
309
  rebooting = false;
@@ -755,8 +751,8 @@ ipcMain.handle('app:archive-project', validated(schemas.archiveProject, async ({
755
751
  registerPtyHandlers();
756
752
  configMgr.registerConfigHandlers();
757
753
  transcripts.registerTranscriptHandlers();
758
- usageMatrix.registerHandlers();
759
754
  sessionsStore.registerSessionsHandlers();
755
+ layoutStore.registerLayoutHandlers();
760
756
  billing.registerBillingHandlers();
761
757
  logs.registerLogHandlers();
762
758
  voiceHotkey.registerHotkeyHandlers();
@@ -772,15 +768,21 @@ memoryTool.registerMemoryHandlers();
772
768
  registerMemoryAggregateIpc();
773
769
  agentMemory.registerAgentMemoryHandlers();
774
770
  git.register(ipcMain);
775
- superagent.registerSuperAgentHandlers();
776
771
  filesIpc.registerFilesHandlers();
777
772
  registerDocEditHandlers();
778
773
  searchIpc.registerSearchHandlers();
779
774
  repoAnalyzer.register(ipcMain);
780
- hivesIpc.registerHiveHandlers();
781
775
  webRemote.registerRemoteHandlers();
782
776
  chatRunner.registerChatHandlers();
783
777
 
778
+ // Direct in-process PRD authoring for a queued PromptTicket classified
779
+ // 'develop' (PRD 749 follow-up) — calls the same createPrd() the admin HTTP
780
+ // route (prdCreate.registerAdminRoute above) and the MCP tool use, just
781
+ // in-process instead of over localAdminHttp.cjs.
782
+ ipcMain.handle('chat:create-prd', validated(schemas.schedulerCreatePrd, async (input) => {
783
+ return prdCreate.createPrd(input, scheduler.remote);
784
+ }));
785
+
784
786
  // Exchanges: read the durable per-exchange log (written by chatRunner → recordExchange).
785
787
  ipcMain.handle('exchanges:list', validated(schemas.exchangesList, async (payload) => {
786
788
  try {
@@ -1073,7 +1075,6 @@ app.whenReady().then(async () => {
1073
1075
  ptyManager.attachWindow(mainWindow);
1074
1076
  configMgr.attachWindow(mainWindow);
1075
1077
  transcripts.attachWindow(mainWindow);
1076
- usageMatrix.attachWindow(mainWindow);
1077
1078
  browserView.registerBrowserView({ mainWindow, ipcMain });
1078
1079
  browserCapture.attachWindow(mainWindow);
1079
1080
  browserCapture.registerBrowserCapture({ ipcMain, getView: browserView.getView });
@@ -1083,7 +1084,6 @@ app.whenReady().then(async () => {
1083
1084
  scheduler.attachWindow(mainWindow);
1084
1085
  watchers.attachWindow(mainWindow);
1085
1086
  pluginInstall.attachWindow(mainWindow);
1086
- superagent.attachWindow(mainWindow);
1087
1087
  webRemote.attachWindow(mainWindow);
1088
1088
  chatRunner.attachWindow(mainWindow);
1089
1089
  attachDocEditWindow(mainWindow);
@@ -204,6 +204,19 @@ const sessionsPayload = z.object({
204
204
  activeTabId: z.string().max(128).nullable(),
205
205
  });
206
206
 
207
+ // ──────────────────────────────────────────── Workbench layout
208
+ // layout:save envelope — validated at the shape level only (version + panel
209
+ // id list); `dockview` is dockview's own opaque SerializedDockview blob and
210
+ // is intentionally z.unknown() so a dockview library upgrade never fails
211
+ // validation on a shape this schema doesn't know about. Zero-panel layouts
212
+ // are refused here too (defense in depth — the renderer serializer already
213
+ // refuses to call save() with an empty panels list).
214
+ const layoutEnvelope = z.object({
215
+ version: z.number().int().min(1),
216
+ panels: z.array(z.string().min(1).max(128)).min(1).max(64),
217
+ dockview: z.record(z.string(), z.unknown()),
218
+ }).strict();
219
+
207
220
  // ──────────────────────────────────────────── Schedule
208
221
  const SCHEDULE_SLUG_RE = /^[A-Za-z0-9._-]{1,128}$/;
209
222
  const SCHEDULE_RUN_ID_RE = /^[A-Za-z0-9._:-]{1,64}$/;
@@ -261,6 +274,9 @@ const schedulerCreatePrd = z.object({
261
274
  // prompt back into the chat tab that queued this PRD, via
262
275
  // enqueueExternalPrompt (PRD 753). Same newline-injection guard.
263
276
  sourceTabId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
277
+ // User-selected Feature/Bug tag (PRD 774) carried from the originating
278
+ // PromptTicket — deterministic, never LLM-classified.
279
+ tag: z.enum(['feature', 'bug']).optional(),
264
280
  });
265
281
 
266
282
  // Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
@@ -620,20 +636,6 @@ const pluginsAbort = z.object({
620
636
  slug: z.string().regex(PLUGIN_SLUG_RE).min(1).max(128),
621
637
  }).passthrough();
622
638
 
623
- // SuperAgent — "boss" run that writes a structured prompt to the active
624
- // tab's PTY. Bounds match the inline schemas in superagent.cjs; centralizing
625
- // here so the schema is the boundary fence rather than each handler.
626
- const superagentStart = z.object({
627
- tabId: z.string().min(1).max(128),
628
- prompt: z.string().min(1).max(8 * 1024),
629
- specialistCount: z.number().int().min(1).max(8),
630
- depth: z.enum(['quick', 'standard', 'deep']),
631
- }).strict();
632
-
633
- const superagentTabId = z.object({
634
- tabId: z.string().min(1).max(128),
635
- }).strict();
636
-
637
639
  /**
638
640
  * Wrap an IPC handler with schema validation. Returns a new handler that
639
641
  * parses the payload before calling the original. On invalid payload throws
@@ -747,6 +749,7 @@ module.exports = {
747
749
  configListDir,
748
750
  configWatch,
749
751
  sessionsPayload,
752
+ layoutEnvelope,
750
753
  scheduleSlug,
751
754
  scheduleReadLog,
752
755
  scheduleWritePrd,
@@ -769,8 +772,6 @@ module.exports = {
769
772
  repoAnalyze,
770
773
  pluginsInstall,
771
774
  pluginsAbort,
772
- superagentStart,
773
- superagentTabId,
774
775
  memoryList,
775
776
  memoryRead,
776
777
  memoryWrite,
@@ -0,0 +1,58 @@
1
+ /**
2
+ * LayoutStore — persists the Workbench's ONE system dockview layout so
3
+ * resizes/splits survive an electron restart.
4
+ *
5
+ * Storage: ~/.claude/session-manager/workbench-layout.json
6
+ * Shape: { version: number, panels: string[], dockview: object, savedAt: number }
7
+ *
8
+ * `dockview` is dockview's own opaque `SerializedDockview` blob (from
9
+ * `api.toJSON()`) — only the envelope (version + panels list) is validated
10
+ * here; the blob itself is round-tripped verbatim so a dockview library
11
+ * upgrade never bricks a saved layout. Unknown-panel pruning against the
12
+ * live screen registry happens renderer-side (main has no knowledge of the
13
+ * NavKey registry) before `api.fromJSON()` is called.
14
+ */
15
+
16
+ const fsp = require('node:fs/promises');
17
+ const path = require('node:path');
18
+ const os = require('node:os');
19
+ const { ipcMain } = require('electron');
20
+ const config = require('./config.cjs');
21
+
22
+ function storePath() {
23
+ return path.join(os.homedir(), '.claude', 'session-manager', 'workbench-layout.json');
24
+ }
25
+
26
+ async function load() {
27
+ const p = storePath();
28
+ try {
29
+ const raw = await fsp.readFile(p, 'utf8');
30
+ const data = JSON.parse(raw);
31
+ if (!data || typeof data !== 'object' || !Array.isArray(data.panels) || !data.dockview) {
32
+ console.warn('[layoutStore] load: malformed shape, ignoring');
33
+ return null;
34
+ }
35
+ return {
36
+ version: typeof data.version === 'number' ? data.version : 1,
37
+ panels: data.panels,
38
+ dockview: data.dockview,
39
+ };
40
+ } catch (e) {
41
+ if (e.code !== 'ENOENT') console.warn('[layoutStore] load failed:', e.message);
42
+ return null;
43
+ }
44
+ }
45
+
46
+ async function save({ version, panels, dockview }) {
47
+ const payload = { version, panels, dockview, savedAt: Date.now() };
48
+ await config.writeJson(storePath(), payload);
49
+ return { ok: true };
50
+ }
51
+
52
+ function registerLayoutHandlers() {
53
+ const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
54
+ ipcMain.handle('layout:load', () => load());
55
+ ipcMain.handle('layout:save', v(s.layoutEnvelope, (payload) => save(payload)));
56
+ }
57
+
58
+ module.exports = { registerLayoutHandlers, load, save, storePath };