claude-code-session-manager 0.100.0 → 0.101.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/DataModel-CXaH2GED.js +1 -0
- package/dist/assets/{History-Dr4Jr_5z.js → History-Cerii-HA.js} +2 -2
- package/dist/assets/{Hooks-NhrvH3JP.js → Hooks-aXMb5LDy.js} +3 -3
- package/dist/assets/{Library-Dv_EtIMr.js → Library-WrJFKEOJ.js} +1 -1
- package/dist/assets/{MarkdownEditor-DTKDWV_n.js → MarkdownEditor-D1BMCXS7.js} +1 -1
- package/dist/assets/{McpServers-M034G9B3.js → McpServers-Cf0davLd.js} +2 -2
- package/dist/assets/{Memory-DzF1B4gx.js → Memory-Dn6NHWPe.js} +3 -3
- package/dist/assets/Permissions-CTVuFPjg.js +3 -0
- package/dist/assets/{Plugins-Bd3ef35w.js → Plugins-C2TyF8Mn.js} +2 -2
- package/dist/assets/ProvenanceBadge-F_jwHSwE.js +1 -0
- package/dist/assets/{SaveBar-rimJem-Y.js → SaveBar-DA1KVJST.js} +1 -1
- package/dist/assets/{Scheduler-DjgFJl2w.js → Scheduler-Bm6Q_hlu.js} +8 -8
- package/dist/assets/{ScopeSwitcher-CnAzTBrl.js → ScopeSwitcher-BygZlJca.js} +1 -1
- package/dist/assets/Settings-BYCiv5KV.js +3 -0
- package/dist/assets/{SkillReferenceGraph-B9aZDecM.js → SkillReferenceGraph-CcLnl9np.js} +1 -1
- package/dist/assets/{Skills-DRXd18gu.js → Skills-ChffsdTD.js} +2 -2
- package/dist/assets/{SystemPrompt-ahy5-Fwe.js → SystemPrompt-dNkfHE_M.js} +1 -1
- package/dist/assets/{TagLibrary-C_tToDeL.js → TagLibrary-jVlEuB0m.js} +1 -1
- package/dist/assets/{TiptapBody-DvdefS6K.js → TiptapBody-6_cdwcP0.js} +1 -1
- package/dist/assets/{Toggle-6Jl5Njc6.js → Toggle-CVXqGpAP.js} +1 -1
- package/dist/assets/ViewTabs-DO1YVbpc.js +1 -0
- package/dist/assets/{index-Bta-hwud.css → index-D89EUizB.css} +1 -1
- package/dist/assets/{index-vx8O73l8.js → index-DbCQPWWY.js} +571 -571
- package/dist/assets/{settingsSchema-D6iRguNV.js → settingsSchema-Wrvn-Yz1.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/scripts/scheduler-mcp-server.cjs +28 -0
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +4 -3
- package/src/main/index.cjs +17 -1
- package/src/main/ipcSchemas.cjs +8 -0
- package/src/main/lib/builtinMacros.cjs +39 -0
- package/src/main/lib/macroLibrary.cjs +102 -8
- package/src/main/lib/mcpToolCatalog.cjs +23 -0
- package/src/main/lib/opsOwnership.cjs +5 -4
- package/src/main/lib/projectHomeAdminRoutes.cjs +136 -21
- package/src/main/lib/shippedPersonaSeeds.cjs +47 -0
- package/src/main/lib/smfileHeaders.cjs +66 -0
- package/src/main/projectPages.cjs +43 -13
- package/src/main/seedAgentPersonas.cjs +1 -1
- package/src/preload/api.d.ts +12 -0
- package/src/seed/agents/architect.md +2 -1
- package/src/seed/agents/demo-video-builder.md +84 -0
- package/src/seed/agents/dev-lead.md +2 -1
- package/src/seed/agents/project-home-builder.md +66 -36
- package/dist/assets/DataModel-HdR0AxFq.js +0 -1
- package/dist/assets/Permissions-DrgbSGWl.js +0 -3
- package/dist/assets/ProvenanceBadge-DcBHT1Iy.js +0 -1
- package/dist/assets/Settings-Dc1F_sSg.js +0 -3
|
@@ -1,14 +1,22 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* projectHomeAdminRoutes.cjs — the
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* see opsOwnership.cjs). Registered
|
|
7
|
-
* localAdminHttp.cjs transport as
|
|
2
|
+
* projectHomeAdminRoutes.cjs — the two admin routes behind the
|
|
3
|
+
* project_home_write MCP tool and its demo-video sibling:
|
|
4
|
+
* POST /admin/project-home/write {cwd, html} -> project-pages/home.html
|
|
5
|
+
* POST /admin/project-home/demo-video/write {cwd, html} -> project-pages/demo-video/index.html
|
|
6
|
+
* both owned by writer 'project-home' (see opsOwnership.cjs). Registered
|
|
7
|
+
* against the same injected localAdminHttp.cjs transport as
|
|
8
|
+
* prdAdminRoutes.cjs.
|
|
8
9
|
*
|
|
9
10
|
* "Self-contained" is enforced here, not trusted: the document must not
|
|
10
11
|
* reference any remote resource (`<script src>`, `<link href=http*>`,
|
|
11
12
|
* remote `@import` / `url(http*)`), so opening it never causes network egress.
|
|
13
|
+
*
|
|
14
|
+
* The demo-video document runs inline JavaScript (the home page does not), so
|
|
15
|
+
* it additionally rejects network-capable APIs (fetch, WebSocket, dynamic
|
|
16
|
+
* import, …) and has a Content-Security-Policy meta stamped into <head>
|
|
17
|
+
* before it is written — the real network fence, since the viewer renders it
|
|
18
|
+
* via smfile:// in a sandboxed iframe that sends no CSP header itself. The
|
|
19
|
+
* regex checks below are defence in depth, not the control.
|
|
12
20
|
*/
|
|
13
21
|
'use strict';
|
|
14
22
|
|
|
@@ -21,8 +29,9 @@ const config = require('../config.cjs');
|
|
|
21
29
|
const { schemas } = require('../ipcSchemas.cjs');
|
|
22
30
|
|
|
23
31
|
const MAX_HTML_BYTES = 1024 * 1024;
|
|
32
|
+
const MAX_DEMO_VIDEO_BYTES = 2 * 1024 * 1024;
|
|
24
33
|
|
|
25
|
-
// Each detector is a linear regex scan; the input is capped
|
|
34
|
+
// Each detector is a linear regex scan; the input is capped first.
|
|
26
35
|
const REMOTE_REFERENCE_CHECKS = [
|
|
27
36
|
{ re: /<script\b[^>]*\bsrc\s*=/i, message: '<script src> is not allowed — inline all scripts' },
|
|
28
37
|
{ re: /<link\b[^>]*\bhref\s*=\s*["']?\s*(?:https?:)?\/\//i, message: '<link href="http(s)://…"> is not allowed — inline all styles' },
|
|
@@ -30,6 +39,43 @@ const REMOTE_REFERENCE_CHECKS = [
|
|
|
30
39
|
{ re: /url\(\s*["']?\s*(?:https?:)?\/\//i, message: 'url(http(s)://…) is not allowed — embed assets as data: URIs' },
|
|
31
40
|
];
|
|
32
41
|
|
|
42
|
+
// Demo-video documents run inline JS; these reject the network-capable
|
|
43
|
+
// surface a self-contained, CSP-fenced document must never reach for.
|
|
44
|
+
const NETWORK_CHECKS = [
|
|
45
|
+
{ re: /\bfetch\s*\(/i, message: 'fetch(...) is not allowed' },
|
|
46
|
+
{ re: /\bXMLHttpRequest\b/i, message: 'XMLHttpRequest is not allowed' },
|
|
47
|
+
{ re: /\bWebSocket\b/i, message: 'WebSocket is not allowed' },
|
|
48
|
+
{ re: /\bEventSource\b/i, message: 'EventSource is not allowed' },
|
|
49
|
+
{ re: /\bsendBeacon\b/i, message: 'sendBeacon is not allowed' },
|
|
50
|
+
{ re: /\bimport\s*\(/i, message: 'import(...) is not allowed' },
|
|
51
|
+
{ re: /\bimportScripts\b/i, message: 'importScripts is not allowed' },
|
|
52
|
+
{ re: /<iframe\b/i, message: '<iframe> is not allowed' },
|
|
53
|
+
{ re: /<object\b/i, message: '<object> is not allowed' },
|
|
54
|
+
{ re: /<embed\b/i, message: '<embed> is not allowed' },
|
|
55
|
+
{ re: /\bwindow\.open\b/i, message: 'window.open is not allowed' },
|
|
56
|
+
{ re: /\b(?:src|href)\s*=\s*["']?\s*(?:https?:)?\/\//i, message: 'src=/href= pointing at http(s): or // is not allowed' },
|
|
57
|
+
{ re: /["'`](?:fetch|XMLHttpRequest|WebSocket|EventSource|sendBeacon|open|importScripts)["'`]/i, message: 'computed/bracket access to a network API (e.g. window["fetch"]) is not allowed' },
|
|
58
|
+
{ re: /\bWorker\s*\(/i, message: 'Worker(...) is not allowed' },
|
|
59
|
+
{ re: /\bSharedWorker\b/i, message: 'SharedWorker is not allowed' },
|
|
60
|
+
{ re: /\bserviceWorker\b/i, message: 'serviceWorker is not allowed' },
|
|
61
|
+
{ re: /rel\s*=\s*["']?\s*(?:prefetch|preconnect|dns-prefetch|preload)/i, message: 'rel="prefetch|preconnect|dns-prefetch|preload" is not allowed' },
|
|
62
|
+
{ re: /\beval\s*\(/i, message: 'eval(...) is not allowed' },
|
|
63
|
+
{ re: /\bFunction\s*\(/i, message: 'Function(...) is not allowed' },
|
|
64
|
+
{ re: /<base\b/i, message: '<base> is not allowed' },
|
|
65
|
+
{ re: /<form\b/i, message: '<form> is not allowed' },
|
|
66
|
+
{ re: /http-equiv\s*=\s*["']?refresh/i, message: 'meta refresh is not allowed' },
|
|
67
|
+
{ re: /\blocation\s*(?:\.\s*(?:href|assign|replace)\b|=[^=])/i, message: 'location.href/assign/replace/= navigation is not allowed' },
|
|
68
|
+
{ re: /\bdocument\.location\b/i, message: 'document.location is not allowed' },
|
|
69
|
+
{ re: /\b(?:top|parent)\./i, message: 'top./parent. property access is not allowed' },
|
|
70
|
+
{ re: /<a\b[^>]*\bhref\s*=\s*["']?\s*(?:https?:|\/\/|javascript:)/i, message: '<a href="http(s):|//|javascript:…"> is not allowed' },
|
|
71
|
+
];
|
|
72
|
+
|
|
73
|
+
const DURATION_META_RE = /<meta\b[^>]*\bname\s*=\s*["']sm-demo-duration["'][^>]*\bcontent\s*=\s*["'](\d+)["'][^>]*>/i;
|
|
74
|
+
const MIN_DEMO_DURATION_S = 5;
|
|
75
|
+
const MAX_DEMO_DURATION_S = 30;
|
|
76
|
+
|
|
77
|
+
const DEMO_VIDEO_CSP = '<meta http-equiv="Content-Security-Policy" content="default-src \'none\'; script-src \'unsafe-inline\'; style-src \'unsafe-inline\'; img-src data: blob:; media-src data: blob:; font-src data:; connect-src \'none\'">';
|
|
78
|
+
|
|
33
79
|
/** -> null when `html` is acceptable, else a human-readable rejection reason. */
|
|
34
80
|
function validateHomeHtml(html) {
|
|
35
81
|
if (typeof html !== 'string' || html.trim().length === 0) return 'html must be a non-empty string';
|
|
@@ -40,6 +86,42 @@ function validateHomeHtml(html) {
|
|
|
40
86
|
return null;
|
|
41
87
|
}
|
|
42
88
|
|
|
89
|
+
/** -> null when `html` is acceptable, else a human-readable rejection reason. */
|
|
90
|
+
function validateDemoVideoHtml(html) {
|
|
91
|
+
if (typeof html !== 'string' || html.trim().length === 0) return 'html must be a non-empty string';
|
|
92
|
+
if (Buffer.byteLength(html, 'utf8') > MAX_DEMO_VIDEO_BYTES) return `html exceeds the ${MAX_DEMO_VIDEO_BYTES}-byte limit`;
|
|
93
|
+
for (const { re, message } of REMOTE_REFERENCE_CHECKS) {
|
|
94
|
+
if (re.test(html)) return `html must be self-contained: ${message}`;
|
|
95
|
+
}
|
|
96
|
+
for (const { re, message } of NETWORK_CHECKS) {
|
|
97
|
+
if (re.test(html)) return `html must not be network-capable: ${message}`;
|
|
98
|
+
}
|
|
99
|
+
const match = html.match(DURATION_META_RE);
|
|
100
|
+
if (!match) {
|
|
101
|
+
return 'html must declare <meta name="sm-demo-duration" content="N"> with '
|
|
102
|
+
+ `${MIN_DEMO_DURATION_S} <= N <= ${MAX_DEMO_DURATION_S}`;
|
|
103
|
+
}
|
|
104
|
+
const duration = Number(match[1]);
|
|
105
|
+
if (!Number.isFinite(duration) || duration < MIN_DEMO_DURATION_S || duration > MAX_DEMO_DURATION_S) {
|
|
106
|
+
return `sm-demo-duration must be between ${MIN_DEMO_DURATION_S} and ${MAX_DEMO_DURATION_S}, got ${match[1]}`;
|
|
107
|
+
}
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Strips any existing CSP meta and a leading doctype, then returns the CSP
|
|
113
|
+
* as the very first element after a fresh `<!DOCTYPE html>` — a position no
|
|
114
|
+
* decoy `<head` text, HTML comment, or string literal in the document can
|
|
115
|
+
* influence. The HTML parser folds a <meta> emitted right after the doctype
|
|
116
|
+
* into the implicitly-created <head>, and a later <html>/<head> start tag is
|
|
117
|
+
* merged/ignored, so the CSP still applies to the whole document.
|
|
118
|
+
*/
|
|
119
|
+
function injectCsp(html) {
|
|
120
|
+
const withoutCsp = html.replace(/<meta\b[^>]*\bhttp-equiv\s*=\s*["']Content-Security-Policy["'][^>]*>\s*/gi, '');
|
|
121
|
+
const withoutDoctype = withoutCsp.replace(/^\s*<!DOCTYPE\b[^>]*>\s*/i, '');
|
|
122
|
+
return `<!DOCTYPE html>${DEMO_VIDEO_CSP}${withoutDoctype}`;
|
|
123
|
+
}
|
|
124
|
+
|
|
43
125
|
/** Same definition crossProjectFeedback.cjs uses for "is this a Session
|
|
44
126
|
* Manager project": it already has an operations root. */
|
|
45
127
|
function isSessionManagerProject(cwd) {
|
|
@@ -79,16 +161,20 @@ function resolveCwd(rawCwd) {
|
|
|
79
161
|
return { ok: true, cwd: realCwd };
|
|
80
162
|
}
|
|
81
163
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
164
|
+
/**
|
|
165
|
+
* Builds a POST handler shared by both routes: read+parse the body, validate
|
|
166
|
+
* against `schema`, resolve cwd, run `validate(html)`, optionally `transform`
|
|
167
|
+
* the html (CSP injection), then write it atomically as writer 'project-home'.
|
|
168
|
+
*/
|
|
169
|
+
function makeWriteHandler({ schema, maxBytes, validate, transform, targetPath }) {
|
|
170
|
+
return async (req, res) => {
|
|
171
|
+
// Allow headroom over maxBytes for JSON escaping; the html byte limit
|
|
172
|
+
// itself is enforced by `validate` below.
|
|
87
173
|
let raw;
|
|
88
174
|
try {
|
|
89
|
-
raw = await readBody(req,
|
|
175
|
+
raw = await readBody(req, maxBytes * 2 + 4096);
|
|
90
176
|
} catch (e) {
|
|
91
|
-
sendJson(res, 400, { ok: false, error: `request body rejected: ${e?.message ?? 'unreadable'} (html limit is ${
|
|
177
|
+
sendJson(res, 400, { ok: false, error: `request body rejected: ${e?.message ?? 'unreadable'} (html limit is ${maxBytes} bytes)` });
|
|
92
178
|
return;
|
|
93
179
|
}
|
|
94
180
|
let body;
|
|
@@ -100,7 +186,7 @@ function registerAdminRoute(adminHttp) {
|
|
|
100
186
|
}
|
|
101
187
|
let input;
|
|
102
188
|
try {
|
|
103
|
-
input =
|
|
189
|
+
input = schema.parse(body);
|
|
104
190
|
} catch (e) {
|
|
105
191
|
sendJson(res, 400, { ok: false, error: 'invalid payload', details: e?.issues ?? e?.message });
|
|
106
192
|
return;
|
|
@@ -110,7 +196,7 @@ function registerAdminRoute(adminHttp) {
|
|
|
110
196
|
sendJson(res, resolved.status, { ok: false, error: resolved.error });
|
|
111
197
|
return;
|
|
112
198
|
}
|
|
113
|
-
const rejection =
|
|
199
|
+
const rejection = validate(input.html);
|
|
114
200
|
if (rejection) {
|
|
115
201
|
sendJson(res, 400, { ok: false, error: rejection });
|
|
116
202
|
return;
|
|
@@ -118,14 +204,43 @@ function registerAdminRoute(adminHttp) {
|
|
|
118
204
|
// Only a route that is actually about to write widens config.cjs's
|
|
119
205
|
// write boundary to this project root.
|
|
120
206
|
config.addAllowedRoot(resolved.cwd);
|
|
207
|
+
const finalHtml = transform ? transform(input.html) : input.html;
|
|
121
208
|
try {
|
|
122
|
-
const
|
|
123
|
-
await
|
|
124
|
-
|
|
209
|
+
const abs = targetPath(resolved.cwd);
|
|
210
|
+
await fs.promises.mkdir(path.dirname(abs), { recursive: true });
|
|
211
|
+
await config.writeTextAtomic(abs, finalHtml, { writer: 'project-home' });
|
|
212
|
+
sendJson(res, 200, { ok: true, path: abs, bytes: Buffer.byteLength(finalHtml, 'utf8') });
|
|
125
213
|
} catch (e) {
|
|
126
214
|
sendJson(res, 500, { ok: false, error: e?.message ?? 'write failed' });
|
|
127
215
|
}
|
|
128
|
-
}
|
|
216
|
+
};
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function registerAdminRoute(adminHttp) {
|
|
220
|
+
// POST /admin/project-home/write {cwd, html}
|
|
221
|
+
adminHttp.registerRoute('POST', '/admin/project-home/write', makeWriteHandler({
|
|
222
|
+
schema: schemas.projectHomeAdminWriteBody,
|
|
223
|
+
maxBytes: MAX_HTML_BYTES,
|
|
224
|
+
validate: validateHomeHtml,
|
|
225
|
+
targetPath: (cwd) => opsPath(cwd, 'project-pages', 'home.html'),
|
|
226
|
+
}));
|
|
227
|
+
|
|
228
|
+
// POST /admin/project-home/demo-video/write {cwd, html}
|
|
229
|
+
adminHttp.registerRoute('POST', '/admin/project-home/demo-video/write', makeWriteHandler({
|
|
230
|
+
schema: schemas.projectDemoVideoAdminWriteBody,
|
|
231
|
+
maxBytes: MAX_DEMO_VIDEO_BYTES,
|
|
232
|
+
validate: validateDemoVideoHtml,
|
|
233
|
+
transform: injectCsp,
|
|
234
|
+
targetPath: (cwd) => opsPath(cwd, 'project-pages', 'demo-video', 'index.html'),
|
|
235
|
+
}));
|
|
129
236
|
}
|
|
130
237
|
|
|
131
|
-
module.exports = {
|
|
238
|
+
module.exports = {
|
|
239
|
+
registerAdminRoute,
|
|
240
|
+
validateHomeHtml,
|
|
241
|
+
validateDemoVideoHtml,
|
|
242
|
+
injectCsp,
|
|
243
|
+
DEMO_VIDEO_CSP,
|
|
244
|
+
MAX_HTML_BYTES,
|
|
245
|
+
MAX_DEMO_VIDEO_BYTES,
|
|
246
|
+
};
|
|
@@ -39,6 +39,18 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
|
|
|
39
39
|
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
40
40
|
},
|
|
41
41
|
},
|
|
42
|
+
{
|
|
43
|
+
commit: "767c3b8e",
|
|
44
|
+
bodySha256: "40a4d41445e7fdb128f4f1d8d3eef88b857ca466726b3719b36cb69933f09168",
|
|
45
|
+
fm: {
|
|
46
|
+
description: "The primary Actor for an Epic's whole interactive conversation — owns overall plan and decomposition, clarifies scope, searches before building, decomposes work into scheduled PRDs via /develop, tracks them to completion, and verifies before calling anything done. Never implements a PRD itself — that's dev-lead's job, one PRD at a time, headless. Task-type framing is the Epic's Mission tag's job, not this persona's.",
|
|
47
|
+
model: "opus",
|
|
48
|
+
effort: "medium",
|
|
49
|
+
tags: "feature, bug, discussion",
|
|
50
|
+
title: "Engineering — Architect",
|
|
51
|
+
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
52
|
+
},
|
|
53
|
+
},
|
|
42
54
|
],
|
|
43
55
|
"dev-lead": [
|
|
44
56
|
{
|
|
@@ -101,6 +113,17 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
|
|
|
101
113
|
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
102
114
|
},
|
|
103
115
|
},
|
|
116
|
+
{
|
|
117
|
+
commit: "767c3b8e",
|
|
118
|
+
bodySha256: "91232cfd7ce5fb1dc608ff639a07dcb1c5c13c22502ae4790f51707f498ec917",
|
|
119
|
+
fm: {
|
|
120
|
+
description: "Executes exactly one already-scoped PRD at a time, headless, start to finish — reads the PRD's Goal/Acceptance Criteria/Implementation notes and standards.md, implements it, verifies against its own AC, and reports. Has no visibility into the overall plan — that's architect's job. This is the default persona a scheduled PRD runs as: the scheduler resolves a PRD's `agentType` frontmatter field (default `dev-lead`) to this file and launches the headless executor AS this persona via `--append-system-prompt`.",
|
|
121
|
+
model: "sonnet",
|
|
122
|
+
effort: "medium",
|
|
123
|
+
title: "Engineering — Software Engineer",
|
|
124
|
+
tools: "Read, Grep, Glob, Bash, Edit, Write",
|
|
125
|
+
},
|
|
126
|
+
},
|
|
104
127
|
],
|
|
105
128
|
"project-home-builder": [
|
|
106
129
|
{
|
|
@@ -121,6 +144,17 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
|
|
|
121
144
|
tools: "Read, Grep, Glob, Bash, Write, Edit",
|
|
122
145
|
},
|
|
123
146
|
},
|
|
147
|
+
{
|
|
148
|
+
commit: "e2cdd120",
|
|
149
|
+
bodySha256: "8cef2256f31d1d166a305923388d1d0d2f4f099275df24d869decc35a6cb11bb",
|
|
150
|
+
fm: {
|
|
151
|
+
description: "Reads a project and writes ONE self-contained overview page (home.html) for its Project Home tab, then saves it with a single project_home_write call.",
|
|
152
|
+
tools: "Read, Grep, Glob, Bash, Write, Edit",
|
|
153
|
+
title: "Project Home — Builder",
|
|
154
|
+
model: "sonnet",
|
|
155
|
+
effort: "medium",
|
|
156
|
+
},
|
|
157
|
+
},
|
|
124
158
|
],
|
|
125
159
|
"validator": [
|
|
126
160
|
{
|
|
@@ -174,6 +208,19 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
|
|
|
174
208
|
},
|
|
175
209
|
},
|
|
176
210
|
],
|
|
211
|
+
"demo-video-builder": [
|
|
212
|
+
{
|
|
213
|
+
commit: "b4f4f84b",
|
|
214
|
+
bodySha256: "4bc6279a4553889f3996c4eb95803a9b38191a959f512b47ee9168b85e49b889",
|
|
215
|
+
fm: {
|
|
216
|
+
description: "30-second self-contained HTML/JS demo video for Project Home, saved via project_demo_video_write.",
|
|
217
|
+
tools: "Read, Grep, Glob, Bash, Write, Edit",
|
|
218
|
+
title: "Project Home — Demo Video",
|
|
219
|
+
model: "sonnet",
|
|
220
|
+
effort: "medium",
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
],
|
|
177
224
|
});
|
|
178
225
|
|
|
179
226
|
module.exports = { SHIPPED_PERSONA_SEEDS };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const DEMO_VIDEO_CSP_HEADER = [
|
|
4
|
+
"default-src 'none'",
|
|
5
|
+
"script-src 'unsafe-inline'",
|
|
6
|
+
"style-src 'unsafe-inline'",
|
|
7
|
+
'img-src data: blob:',
|
|
8
|
+
'media-src data: blob:',
|
|
9
|
+
"font-src data:",
|
|
10
|
+
"connect-src 'none'",
|
|
11
|
+
"frame-src 'none'",
|
|
12
|
+
"worker-src 'none'",
|
|
13
|
+
"form-action 'none'",
|
|
14
|
+
"base-uri 'none'",
|
|
15
|
+
].join('; ');
|
|
16
|
+
|
|
17
|
+
const DEMO_VIDEO_PATH_MARKER = '/session-manager-operations/project-pages/demo-video/';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Response headers for a file served over the smfile:// protocol. A file
|
|
21
|
+
* under the demo-video project-pages folder gets a CSP response header —
|
|
22
|
+
* document content (a decoy <head>, HTML comments) cannot override this,
|
|
23
|
+
* unlike the meta-tag CSP the demo-video HTML ships inline.
|
|
24
|
+
* @param {string} realPath
|
|
25
|
+
* @param {string} contentType
|
|
26
|
+
*/
|
|
27
|
+
function smfileResponseHeaders(realPath, contentType) {
|
|
28
|
+
const normalised = String(realPath).replace(/\\/g, '/');
|
|
29
|
+
const headers = { 'content-type': contentType };
|
|
30
|
+
if (normalised.includes(DEMO_VIDEO_PATH_MARKER)) {
|
|
31
|
+
headers['content-security-policy'] = DEMO_VIDEO_CSP_HEADER;
|
|
32
|
+
}
|
|
33
|
+
return headers;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Guards against a demo-video document exfiltrating data by navigating its
|
|
38
|
+
* own sandboxed iframe (meta refresh, `location.href = 'https://…'`), which
|
|
39
|
+
* the CSP response header does not govern — `frame-src`/`connect-src` don't
|
|
40
|
+
* stop a top-level navigation of the subframe itself.
|
|
41
|
+
* @param {{ isMainFrame: boolean, currentUrl: string, targetUrl: string }} params
|
|
42
|
+
* @returns {boolean}
|
|
43
|
+
*/
|
|
44
|
+
function shouldBlockFrameNavigation({ isMainFrame, currentUrl, targetUrl }) {
|
|
45
|
+
if (isMainFrame) return false;
|
|
46
|
+
if (typeof currentUrl !== 'string' || !currentUrl.startsWith('smfile:')) return false;
|
|
47
|
+
|
|
48
|
+
let pathname;
|
|
49
|
+
try {
|
|
50
|
+
// Same normalisation as smfileResponseHeaders below: on Windows the
|
|
51
|
+
// absolute path (and therefore the smfile:// URL built from it) uses
|
|
52
|
+
// backslashes, which the forward-slash DEMO_VIDEO_PATH_MARKER would
|
|
53
|
+
// otherwise never match — silently disabling this guard on win32.
|
|
54
|
+
pathname = decodeURIComponent(new URL(currentUrl).pathname).replace(/\\/g, '/');
|
|
55
|
+
} catch {
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
if (!pathname.includes(DEMO_VIDEO_PATH_MARKER)) return false;
|
|
59
|
+
|
|
60
|
+
if (typeof targetUrl !== 'string') return false;
|
|
61
|
+
if (targetUrl.startsWith('smfile:')) return false;
|
|
62
|
+
if (targetUrl === 'about:blank' || targetUrl === 'about:srcdoc') return false;
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
module.exports = { DEMO_VIDEO_CSP_HEADER, smfileResponseHeaders, shouldBlockFrameNavigation };
|
|
@@ -1,12 +1,15 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
* projectPages.cjs — read-only backend for Project Home.
|
|
5
|
-
*
|
|
6
|
-
* home.html
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
4
|
+
* projectPages.cjs — read-only backend for Project Home. Project Home has two
|
|
5
|
+
* self-contained generated artifacts under
|
|
6
|
+
* `session-manager-operations/project-pages/`: `home.html` (written by the
|
|
7
|
+
* `project_home_write` admin route) and `demo-video/index.html` (written by
|
|
8
|
+
* the `project_demo_video_write` admin route) — both in
|
|
9
|
+
* lib/projectHomeAdminRoutes.cjs. This module only reads them: get() returns
|
|
10
|
+
* home.html's content plus the demo video's { path, mtimeMs }, and a
|
|
11
|
+
* refcounted per-cwd watcher pushes `project-pages:changed` for either. It
|
|
12
|
+
* never writes content (watch()'s mkdir creates an empty directory only).
|
|
10
13
|
*/
|
|
11
14
|
|
|
12
15
|
const { ipcMain } = require('electron');
|
|
@@ -26,6 +29,22 @@ function homePath(cwd) {
|
|
|
26
29
|
return opsPath(cwd, 'project-pages', 'home.html');
|
|
27
30
|
}
|
|
28
31
|
|
|
32
|
+
function demoVideoPath(cwd) {
|
|
33
|
+
return opsPath(cwd, 'project-pages', 'demo-video', 'index.html');
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** -> { path: string, mtimeMs: number } | null */
|
|
37
|
+
async function getDemoVideo(realCwd) {
|
|
38
|
+
const file = demoVideoPath(realCwd);
|
|
39
|
+
try {
|
|
40
|
+
const stat = await fs.promises.stat(file);
|
|
41
|
+
return { path: file, mtimeMs: stat.mtimeMs };
|
|
42
|
+
} catch (e) {
|
|
43
|
+
if (e.code === 'ENOENT') return null;
|
|
44
|
+
throw e;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
29
48
|
/**
|
|
30
49
|
* Resolve `cwd`'s home.html path, treating the opsOwnership "ephemeral cwd"
|
|
31
50
|
* throw (a tmpdir/linked-worktree project root) as "no path available"
|
|
@@ -40,14 +59,15 @@ function tryHomePath(realCwd) {
|
|
|
40
59
|
}
|
|
41
60
|
}
|
|
42
61
|
|
|
43
|
-
/** -> { html: string|null, mtimeMs: number|null } */
|
|
62
|
+
/** -> { html: string|null, mtimeMs: number|null, demoVideo: {path, mtimeMs}|null } */
|
|
44
63
|
async function get({ cwd }) {
|
|
45
64
|
const realCwd = config.validatePath(cwd);
|
|
46
65
|
const { file, ephemeral } = tryHomePath(realCwd);
|
|
47
|
-
if (ephemeral) return { html: null, mtimeMs: null };
|
|
66
|
+
if (ephemeral) return { html: null, mtimeMs: null, demoVideo: null };
|
|
48
67
|
const res = await config.readText(file);
|
|
49
|
-
|
|
50
|
-
return { html:
|
|
68
|
+
const demoVideo = await getDemoVideo(realCwd);
|
|
69
|
+
if (!res.exists) return { html: null, mtimeMs: null, demoVideo };
|
|
70
|
+
return { html: res.text, mtimeMs: res.mtimeMs, demoVideo };
|
|
51
71
|
}
|
|
52
72
|
|
|
53
73
|
// ─── Push channel ─────────────────────────────────────────────────────────
|
|
@@ -90,21 +110,31 @@ async function watchOutput(cwd) {
|
|
|
90
110
|
const dir = path.dirname(file);
|
|
91
111
|
fs.mkdirSync(dir, { recursive: true });
|
|
92
112
|
|
|
113
|
+
const demoVideoFile = demoVideoPath(realCwd);
|
|
93
114
|
const push = async (changedPath) => {
|
|
94
|
-
|
|
115
|
+
// Only home.html or demo-video/index.html — ignore every other sibling file.
|
|
116
|
+
if (changedPath) {
|
|
117
|
+
const resolved = path.resolve(changedPath);
|
|
118
|
+
if (resolved !== path.resolve(file) && resolved !== path.resolve(demoVideoFile)) return;
|
|
119
|
+
}
|
|
95
120
|
let result;
|
|
96
121
|
try {
|
|
97
122
|
result = await get({ cwd: realCwd });
|
|
98
123
|
} catch {
|
|
99
124
|
return; // best-effort; the renderer keeps its last-known output
|
|
100
125
|
}
|
|
101
|
-
sendIfAlive(mainWindow, 'project-pages:changed', {
|
|
126
|
+
sendIfAlive(mainWindow, 'project-pages:changed', {
|
|
127
|
+
cwd: realCwd,
|
|
128
|
+
html: result.html,
|
|
129
|
+
mtimeMs: result.mtimeMs,
|
|
130
|
+
demoVideo: result.demoVideo,
|
|
131
|
+
});
|
|
102
132
|
};
|
|
103
133
|
|
|
104
134
|
const watcher = chokidar.watch(dir, {
|
|
105
135
|
ignoreInitial: true,
|
|
106
136
|
persistent: true,
|
|
107
|
-
depth:
|
|
137
|
+
depth: 1,
|
|
108
138
|
// Same 50ms/25ms shape config.cjs's generic watcher uses.
|
|
109
139
|
awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
|
|
110
140
|
});
|
|
@@ -80,7 +80,7 @@ const { writeJsonSync, writeTextAtomic, validatePath, validateWrite } = require(
|
|
|
80
80
|
const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
|
|
81
81
|
const { SHIPPED_PERSONA_SEEDS } = require('./lib/shippedPersonaSeeds.cjs');
|
|
82
82
|
|
|
83
|
-
const PERSONAS = ['architect', 'dev-lead', 'project-home-builder', 'validator'];
|
|
83
|
+
const PERSONAS = ['architect', 'dev-lead', 'project-home-builder', 'validator', 'demo-video-builder'];
|
|
84
84
|
// What `{ done: true }` (the pre-seeded-set marker format) meant: only these two personas existed
|
|
85
85
|
// in PERSONAS at the time. Fixed, not derived from the current PERSONAS array above — otherwise
|
|
86
86
|
// every future persona added to PERSONAS would retroactively count as already delivered to a
|
package/src/preload/api.d.ts
CHANGED
|
@@ -242,6 +242,12 @@ export interface Macro {
|
|
|
242
242
|
tag: AgentPersonaTag;
|
|
243
243
|
prompt: string;
|
|
244
244
|
projects: string[];
|
|
245
|
+
/** Which strip shows this macro. Defaults to `'sessions'` when absent. */
|
|
246
|
+
surface: 'sessions' | 'project-home';
|
|
247
|
+
/** True for a seeded, machine-authored macro (e.g. the built-in Project Home actions). */
|
|
248
|
+
builtin?: boolean;
|
|
249
|
+
/** Bumped by the seed when a builtin macro's definition changes, to reseed it. */
|
|
250
|
+
builtinVersion?: number;
|
|
245
251
|
createdAt: string;
|
|
246
252
|
updatedAt: string;
|
|
247
253
|
}
|
|
@@ -254,6 +260,7 @@ export interface MacroSaveInput {
|
|
|
254
260
|
tag: AgentPersonaTag;
|
|
255
261
|
prompt: string;
|
|
256
262
|
projects?: string[];
|
|
263
|
+
surface?: 'sessions' | 'project-home';
|
|
257
264
|
}
|
|
258
265
|
|
|
259
266
|
/** One global `~/.claude/agents/<name>.md` persona, per the Agent Library nav page. */
|
|
@@ -1325,6 +1332,8 @@ export interface ProjectPagesGetResult {
|
|
|
1325
1332
|
html: string | null;
|
|
1326
1333
|
/** home.html's mtime, or null when `html` is null. */
|
|
1327
1334
|
mtimeMs: number | null;
|
|
1335
|
+
/** project-pages/demo-video/index.html's absolute path + mtime, or null when it does not exist. Load it via smfile:// — never pass its HTML across IPC. */
|
|
1336
|
+
demoVideo: { path: string; mtimeMs: number } | null;
|
|
1328
1337
|
}
|
|
1329
1338
|
|
|
1330
1339
|
// ────────────────────────────────────────────── Per-subagent memory
|
|
@@ -1479,6 +1488,9 @@ export interface ChatRunToolUseEvent {
|
|
|
1479
1488
|
label: string;
|
|
1480
1489
|
/** Populated only for Edit/Write tool_use blocks; undefined for every other kind. */
|
|
1481
1490
|
diff?: { filePath: string; oldText?: string; newText?: string };
|
|
1491
|
+
/** The tool_use input's command/description/pattern/file_path, whichever applies —
|
|
1492
|
+
* empty string when the tool took none of those. */
|
|
1493
|
+
detail?: string;
|
|
1482
1494
|
}
|
|
1483
1495
|
|
|
1484
1496
|
export interface ChatRunNeedsInputEvent {
|
|
@@ -3,9 +3,10 @@ name: architect
|
|
|
3
3
|
description: The primary Actor for an Epic's whole interactive conversation — owns overall plan and decomposition, clarifies scope, searches before building, decomposes work into scheduled PRDs via /develop, tracks them to completion, and verifies before calling anything done. Never implements a PRD itself — that's dev-lead's job, one PRD at a time, headless. Task-type framing is the Epic's Mission tag's job, not this persona's.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, Edit, Write
|
|
5
5
|
model: opus
|
|
6
|
+
effort: medium
|
|
6
7
|
tags: feature, bug, discussion
|
|
7
8
|
title: Engineering — Architect
|
|
8
|
-
seedVersion:
|
|
9
|
+
seedVersion: 2
|
|
9
10
|
---
|
|
10
11
|
|
|
11
12
|
You are the architect. You are the one Actor a human talks to for the whole life of an Epic's
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: demo-video-builder
|
|
3
|
+
description: 30-second self-contained HTML/JS demo video for Project Home, saved via project_demo_video_write.
|
|
4
|
+
tools: Read, Grep, Glob, Bash, Write, Edit
|
|
5
|
+
title: 'Project Home — Demo Video'
|
|
6
|
+
model: sonnet
|
|
7
|
+
effort: medium
|
|
8
|
+
seedVersion: 1
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
You are the demo-video-builder. You make a 30-second animated demo that shows someone what this
|
|
12
|
+
project is for and what it does best. It plays inside Project Home and may later be
|
|
13
|
+
screen-recorded, so it must be deterministic — the same frame at the same time, every run.
|
|
14
|
+
|
|
15
|
+
<context>
|
|
16
|
+
You run inside a Session Manager Epic for the target project. The HTML you produce is rendered
|
|
17
|
+
inside a sandboxed frame in that project's Project Home tab, triggered by a human pressing the
|
|
18
|
+
Demo Video button — never automatically and never in a loop — so this run is the one chance to
|
|
19
|
+
get it right before a person watches it.
|
|
20
|
+
</context>
|
|
21
|
+
|
|
22
|
+
<grounding_rules>
|
|
23
|
+
Ground every goal and feature you show in something you read: the project's manifest
|
|
24
|
+
(`package.json`, `pyproject.toml`, or equivalent), its README or docs, its source tree, or its
|
|
25
|
+
git log. If you can't point to where a claim came from, leave it out rather than guess — a
|
|
26
|
+
shorter video is far better than one with an invented feature. Never invent metrics, logos,
|
|
27
|
+
testimonials, or UI screenshots. Where the project already describes itself well (a tagline, a
|
|
28
|
+
feature list, a mission statement), reuse its own wording instead of paraphrasing.
|
|
29
|
+
</grounding_rules>
|
|
30
|
+
|
|
31
|
+
<storyboard>
|
|
32
|
+
Before writing any code, write a 5-7 scene storyboard with timings that sum to exactly 30
|
|
33
|
+
seconds:
|
|
34
|
+
|
|
35
|
+
1. Title card — the project's name and a one-line purpose.
|
|
36
|
+
2. The problem it solves.
|
|
37
|
+
3-6. One scene per key feature (3 to 4 features), each a short caption plus a simple animated
|
|
38
|
+
diagram or kinetic type built from shapes, SVG, or canvas — never a fake screenshot.
|
|
39
|
+
7. Closing card — the project's name and how to get started (the real command).
|
|
40
|
+
|
|
41
|
+
Keep every caption at 12 words or fewer, and hold each scene on screen long enough to read twice.
|
|
42
|
+
</storyboard>
|
|
43
|
+
|
|
44
|
+
<output_contract>
|
|
45
|
+
Produce exactly one complete HTML document:
|
|
46
|
+
- All CSS and JS live inline, in a single `<style>` block and a single `<script>` block — no
|
|
47
|
+
`<script src="...">`, no `<link href="http...">`, no `@import`, and no `url(http...)`.
|
|
48
|
+
- `<head>` includes `<meta name="sm-demo-duration" content="30">`.
|
|
49
|
+
- A 16:9 stage, 1280x720 logical pixels, scaled to fit the viewport with letterboxing.
|
|
50
|
+
- A single clock drives every scene via `requestAnimationFrame` — no per-scene timers of their
|
|
51
|
+
own, so seeking is exact.
|
|
52
|
+
- Expose `window.smDemo = { duration: 30, seek(t), play(), pause() }`, where `seek(t)` renders
|
|
53
|
+
the exact frame for time `t` in seconds, independent of whatever played before it.
|
|
54
|
+
- Autoplays once on load; shows play/pause, restart, and a progress bar.
|
|
55
|
+
- No audio.
|
|
56
|
+
- No network: no `fetch`, `XMLHttpRequest`, `WebSocket`, `import()`, or `<iframe>`. No external
|
|
57
|
+
fonts or images — system font stack only, and any images as `data:` URIs.
|
|
58
|
+
- Respects `prefers-reduced-motion` by rendering the scenes as a static, steppable storyboard
|
|
59
|
+
instead of animating them.
|
|
60
|
+
- Under 2 MB total.
|
|
61
|
+
- A light/dark neutral palette with sufficient contrast in both.
|
|
62
|
+
</output_contract>
|
|
63
|
+
|
|
64
|
+
<process>
|
|
65
|
+
1. Read, with a bounded pass: the manifest, the README or top-level docs index, the top-level
|
|
66
|
+
directory tree, and `git log --oneline -30` for a sense of recent direction. Stop once you
|
|
67
|
+
have enough to describe the project honestly.
|
|
68
|
+
2. Write the claims list: for every goal or feature you plan to show, note the source you read
|
|
69
|
+
it from. Drop anything without a source.
|
|
70
|
+
3. Write the storyboard per `<storyboard>`, with scene timings summing to exactly 30 seconds.
|
|
71
|
+
4. Build the HTML per `<output_contract>`.
|
|
72
|
+
5. Self-check before saving: scene durations sum to 30, every caption traces to a claim in your
|
|
73
|
+
list, and the document contains none of the forbidden network APIs.
|
|
74
|
+
6. Call `project_demo_video_write` exactly once with `{ html }`. It validates the document and
|
|
75
|
+
writes it as the project's Demo Video.
|
|
76
|
+
7. Report back the storyboard as one short list — scene, timing, and the claim behind it.
|
|
77
|
+
</process>
|
|
78
|
+
|
|
79
|
+
<when_things_fail>
|
|
80
|
+
If `project_demo_video_write` rejects the HTML, fix the specific problem it reported and call it
|
|
81
|
+
again — up to 3 attempts total. If `project_demo_video_write` isn't available in this session at
|
|
82
|
+
all, say so plainly and stop; don't try to build a workaround or any tooling of your own in the
|
|
83
|
+
target project.
|
|
84
|
+
</when_things_fail>
|
|
@@ -3,8 +3,9 @@ name: dev-lead
|
|
|
3
3
|
description: Executes exactly one already-scoped PRD at a time, headless, start to finish — reads the PRD's Goal/Acceptance Criteria/Implementation notes and standards.md, implements it, verifies against its own AC, and reports. Has no visibility into the overall plan — that's architect's job. This is the default persona a scheduled PRD runs as: the scheduler resolves a PRD's `agentType` frontmatter field (default `dev-lead`) to this file and launches the headless executor AS this persona via `--append-system-prompt`.
|
|
4
4
|
tools: Read, Grep, Glob, Bash, Edit, Write
|
|
5
5
|
model: sonnet
|
|
6
|
+
effort: medium
|
|
6
7
|
title: Engineering — Software Engineer
|
|
7
|
-
seedVersion:
|
|
8
|
+
seedVersion: 3
|
|
8
9
|
---
|
|
9
10
|
|
|
10
11
|
You are dev-lead. You execute one PRD — nothing above it, nothing beyond it. You don't decide
|