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
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-
|
|
10
|
+
<script type="module" crossorigin src="./assets/index-DPSmbyU_.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-
|
|
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.
|
|
3
|
+
"version": "0.38.7",
|
|
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.
|
|
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. **
|
|
28
|
-
the
|
|
29
|
-
|
|
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
|
-
|
|
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`,
|
|
37
|
-
plus any setup/prerequisite steps. This is a draft — `:capture` is
|
|
38
|
-
|
|
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`) +
|
|
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
|
|
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
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
33
|
+
6. Confirm no external network calls (grep for `http://`/`https://` in `<script src>`,
|
|
31
34
|
`<link href>`, `fetch(`, `@import` — should be none).
|
|
32
|
-
|
|
33
|
-
|
|
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}
|
|
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
|
-
|
|
55
|
-
|
|
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
|
+
});
|
package/src/main/index.cjs
CHANGED
|
@@ -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);
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -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 };
|