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.
Files changed (49) hide show
  1. package/dist/assets/DataModel-CXaH2GED.js +1 -0
  2. package/dist/assets/{History-Dr4Jr_5z.js → History-Cerii-HA.js} +2 -2
  3. package/dist/assets/{Hooks-NhrvH3JP.js → Hooks-aXMb5LDy.js} +3 -3
  4. package/dist/assets/{Library-Dv_EtIMr.js → Library-WrJFKEOJ.js} +1 -1
  5. package/dist/assets/{MarkdownEditor-DTKDWV_n.js → MarkdownEditor-D1BMCXS7.js} +1 -1
  6. package/dist/assets/{McpServers-M034G9B3.js → McpServers-Cf0davLd.js} +2 -2
  7. package/dist/assets/{Memory-DzF1B4gx.js → Memory-Dn6NHWPe.js} +3 -3
  8. package/dist/assets/Permissions-CTVuFPjg.js +3 -0
  9. package/dist/assets/{Plugins-Bd3ef35w.js → Plugins-C2TyF8Mn.js} +2 -2
  10. package/dist/assets/ProvenanceBadge-F_jwHSwE.js +1 -0
  11. package/dist/assets/{SaveBar-rimJem-Y.js → SaveBar-DA1KVJST.js} +1 -1
  12. package/dist/assets/{Scheduler-DjgFJl2w.js → Scheduler-Bm6Q_hlu.js} +8 -8
  13. package/dist/assets/{ScopeSwitcher-CnAzTBrl.js → ScopeSwitcher-BygZlJca.js} +1 -1
  14. package/dist/assets/Settings-BYCiv5KV.js +3 -0
  15. package/dist/assets/{SkillReferenceGraph-B9aZDecM.js → SkillReferenceGraph-CcLnl9np.js} +1 -1
  16. package/dist/assets/{Skills-DRXd18gu.js → Skills-ChffsdTD.js} +2 -2
  17. package/dist/assets/{SystemPrompt-ahy5-Fwe.js → SystemPrompt-dNkfHE_M.js} +1 -1
  18. package/dist/assets/{TagLibrary-C_tToDeL.js → TagLibrary-jVlEuB0m.js} +1 -1
  19. package/dist/assets/{TiptapBody-DvdefS6K.js → TiptapBody-6_cdwcP0.js} +1 -1
  20. package/dist/assets/{Toggle-6Jl5Njc6.js → Toggle-CVXqGpAP.js} +1 -1
  21. package/dist/assets/ViewTabs-DO1YVbpc.js +1 -0
  22. package/dist/assets/{index-Bta-hwud.css → index-D89EUizB.css} +1 -1
  23. package/dist/assets/{index-vx8O73l8.js → index-DbCQPWWY.js} +571 -571
  24. package/dist/assets/{settingsSchema-D6iRguNV.js → settingsSchema-Wrvn-Yz1.js} +1 -1
  25. package/dist/index.html +2 -2
  26. package/package.json +1 -1
  27. package/scripts/scheduler-mcp-server.cjs +28 -0
  28. package/src/main/build-info.json +4 -4
  29. package/src/main/chatRunner.cjs +4 -3
  30. package/src/main/index.cjs +17 -1
  31. package/src/main/ipcSchemas.cjs +8 -0
  32. package/src/main/lib/builtinMacros.cjs +39 -0
  33. package/src/main/lib/macroLibrary.cjs +102 -8
  34. package/src/main/lib/mcpToolCatalog.cjs +23 -0
  35. package/src/main/lib/opsOwnership.cjs +5 -4
  36. package/src/main/lib/projectHomeAdminRoutes.cjs +136 -21
  37. package/src/main/lib/shippedPersonaSeeds.cjs +47 -0
  38. package/src/main/lib/smfileHeaders.cjs +66 -0
  39. package/src/main/projectPages.cjs +43 -13
  40. package/src/main/seedAgentPersonas.cjs +1 -1
  41. package/src/preload/api.d.ts +12 -0
  42. package/src/seed/agents/architect.md +2 -1
  43. package/src/seed/agents/demo-video-builder.md +84 -0
  44. package/src/seed/agents/dev-lead.md +2 -1
  45. package/src/seed/agents/project-home-builder.md +66 -36
  46. package/dist/assets/DataModel-HdR0AxFq.js +0 -1
  47. package/dist/assets/Permissions-DrgbSGWl.js +0 -3
  48. package/dist/assets/ProvenanceBadge-DcBHT1Iy.js +0 -1
  49. package/dist/assets/Settings-Dc1F_sSg.js +0 -3
@@ -1,14 +1,22 @@
1
1
  /**
2
- * projectHomeAdminRoutes.cjs — the single admin route behind the
3
- * `project_home_write` MCP tool: POST /admin/project-home/write {cwd, html}
4
- * writes one self-contained HTML document to
5
- * `session-manager-operations/project-pages/home.html` (owner: project-home,
6
- * see opsOwnership.cjs). Registered against the same injected
7
- * localAdminHttp.cjs transport as prdAdminRoutes.cjs.
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 at 1MB first.
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
- function registerAdminRoute(adminHttp) {
83
- // POST /admin/project-home/write {cwd, html}
84
- adminHttp.registerRoute('POST', '/admin/project-home/write', async (req, res) => {
85
- // Allow headroom over MAX_HTML_BYTES for JSON escaping; the html byte
86
- // limit itself is enforced by validateHomeHtml below.
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, MAX_HTML_BYTES * 2 + 4096);
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 ${MAX_HTML_BYTES} bytes)` });
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 = schemas.projectHomeAdminWriteBody.parse(body);
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 = validateHomeHtml(input.html);
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 homePath = opsPath(resolved.cwd, 'project-pages', 'home.html');
123
- await config.writeTextAtomic(homePath, input.html, { writer: 'project-home' });
124
- sendJson(res, 200, { ok: true, path: homePath, bytes: Buffer.byteLength(input.html, 'utf8') });
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 = { registerAdminRoute, validateHomeHtml, MAX_HTML_BYTES };
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. The whole feature is
5
- * ONE self-contained HTML file, `session-manager-operations/project-pages/
6
- * home.html`, written by the `project_home_write` admin route
7
- * (lib/projectHomeAdminRoutes.cjs). This module only reads it: get() plus a
8
- * refcounted per-cwd watcher that pushes `project-pages:changed`. It never
9
- * writes content (watch()'s mkdir creates an empty directory only).
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
- if (!res.exists) return { html: null, mtimeMs: null };
50
- return { html: res.text, mtimeMs: res.mtimeMs };
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
- if (changedPath && path.resolve(changedPath) !== path.resolve(file)) return; // only home.html
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', { cwd: realCwd, html: result.html, mtimeMs: result.mtimeMs });
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: 0,
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
@@ -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: 1
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: 2
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