claude-code-session-manager 0.100.0 → 0.102.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-CrthqiBI.js +1 -0
- package/dist/assets/{History-Dr4Jr_5z.js → History-Ci8AMY-Z.js} +2 -2
- package/dist/assets/{Hooks-NhrvH3JP.js → Hooks-BUERs_jz.js} +3 -3
- package/dist/assets/{Library-Dv_EtIMr.js → Library-DkEaEjWs.js} +1 -1
- package/dist/assets/{MarkdownEditor-DTKDWV_n.js → MarkdownEditor-Gb_Vm2cQ.js} +1 -1
- package/dist/assets/{McpServers-M034G9B3.js → McpServers-ChtBd9J1.js} +2 -2
- package/dist/assets/{Memory-DzF1B4gx.js → Memory-hO5wBnK4.js} +3 -3
- package/dist/assets/Permissions-DaqZyJrw.js +3 -0
- package/dist/assets/{Plugins-Bd3ef35w.js → Plugins-Ck5p4acV.js} +2 -2
- package/dist/assets/ProvenanceBadge-CnuIadnk.js +1 -0
- package/dist/assets/{SaveBar-rimJem-Y.js → SaveBar-rSB7cm1z.js} +1 -1
- package/dist/assets/{Scheduler-DjgFJl2w.js → Scheduler-DpdAfUgp.js} +8 -8
- package/dist/assets/{ScopeSwitcher-CnAzTBrl.js → ScopeSwitcher-qH4Rc4Kf.js} +1 -1
- package/dist/assets/Settings-CFP_-hF5.js +3 -0
- package/dist/assets/{SkillReferenceGraph-B9aZDecM.js → SkillReferenceGraph-CTNaQVhr.js} +1 -1
- package/dist/assets/{Skills-DRXd18gu.js → Skills-Deim4Jh9.js} +2 -2
- package/dist/assets/{SystemPrompt-ahy5-Fwe.js → SystemPrompt-B-HhbNPo.js} +1 -1
- package/dist/assets/{TagLibrary-C_tToDeL.js → TagLibrary-La0wD_-k.js} +1 -1
- package/dist/assets/{TiptapBody-DvdefS6K.js → TiptapBody-6MYJQp2Z.js} +1 -1
- package/dist/assets/{Toggle-6Jl5Njc6.js → Toggle-BzQFK045.js} +1 -1
- package/dist/assets/ViewTabs-CPKoy15S.js +1 -0
- package/dist/assets/{index-Bta-hwud.css → index-D89EUizB.css} +1 -1
- package/dist/assets/{index-vx8O73l8.js → index-DbZfl7rl.js} +573 -571
- package/dist/assets/{settingsSchema-D6iRguNV.js → settingsSchema-C_mZIOIl.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +16 -2
- 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 +58 -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/main/templates/PRD_AUTHORING.md +2 -2
- package/src/preload/api.d.ts +12 -0
- package/src/seed/agents/architect.md +2 -1
- package/src/seed/agents/demo-video-builder.md +114 -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,30 @@ 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
|
+
{
|
|
224
|
+
commit: "93091f6f",
|
|
225
|
+
bodySha256: "57e4272bbdc8960eb033fcf45d4138b05c53a6c14d601bbb464283a96095e172",
|
|
226
|
+
fm: {
|
|
227
|
+
description: "30-second self-contained HTML/JS demo video for Project Home, saved via project_demo_video_write.",
|
|
228
|
+
tools: "Read, Grep, Glob, Bash, Write, Edit",
|
|
229
|
+
title: "Project Home — Demo Video",
|
|
230
|
+
model: "sonnet",
|
|
231
|
+
effort: "medium",
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
],
|
|
177
235
|
});
|
|
178
236
|
|
|
179
237
|
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
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
<!-- PRD_AUTHORING.md
|
|
1
|
+
<!-- PRD_AUTHORING.md v5 -->
|
|
2
2
|
# PRD Authoring Guide — Scheduler Safety Rules
|
|
3
3
|
|
|
4
4
|
Rule: every PRD you queue follows the rules below.
|
|
@@ -73,7 +73,7 @@ Rule: every PRD needs these fields.
|
|
|
73
73
|
|
|
74
74
|
`parallelGroup` is deprecated and ignored. `dependsOn: [<slug>, ...]` is the only ordering primitive.
|
|
75
75
|
|
|
76
|
-
`planId` is written by the API, never by you. It groups PRDs into the plan (wave) they belong to: an `append` PRD, or one with an explicit `dependsOn`, inherits the `planId` of the PRD it attaches behind; a fresh PRD mints a new one.
|
|
76
|
+
`planId` is written by the API, never by you. It groups PRDs into the plan (wave) they belong to: an `append` PRD, or one with an explicit `dependsOn`, inherits the `planId` of the PRD it attaches behind; a fresh PRD mints a new one. The Scheduler groups a plan card as a validate PRD (slug `validate-*`) plus everything it depends on, titled by that PRD's title.
|
|
77
77
|
|
|
78
78
|
Artifact-only PRDs (`deliverable: artifact` + `artifactPaths: [...]`) — use this ONLY when every deliverable is a file the repo deliberately git-excludes, so "no commit" is the correct outcome, not a miss.
|
|
79
79
|
1. Every artifact path is listed in `artifactPaths` and is relative, never `..`.
|
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
|