claude-code-session-manager 0.76.0 → 0.78.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 (123) hide show
  1. package/dist/assets/{AgentLibrary-CBx9l4zN.js → AgentLibrary-13pfo8uY.js} +2 -2
  2. package/dist/assets/{DataModel-Bf0EIE_t.js → DataModel-SUyQbFlg.js} +1 -1
  3. package/dist/assets/{History-CpdtWhC8.js → History-2GJMS703.js} +2 -2
  4. package/dist/assets/{Hooks-DyUbMDmg.js → Hooks-DM2nS3RT.js} +2 -2
  5. package/dist/assets/{HostBilko-By-wIpry.js → HostBilko-BLeC-lpp.js} +1 -1
  6. package/dist/assets/{Library-CQmo4QVC.js → Library-BaRkU9m0.js} +1 -1
  7. package/dist/assets/{ListDetail-BQMd6NOm.js → ListDetail-D5scjSKq.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DEp43FXX.js → MarkdownEditor-B1lgAo9T.js} +1 -1
  9. package/dist/assets/{McpServers-CLarzwqA.js → McpServers-BzyThQSM.js} +1 -1
  10. package/dist/assets/{Memory-B0sCdIy1.js → Memory-7UdaOTtl.js} +6 -6
  11. package/dist/assets/{Panel-BhWPVOCD.js → Panel-JbTMaOPq.js} +1 -1
  12. package/dist/assets/{Permissions-Ddlq8T_O.js → Permissions-UBam0bJG.js} +2 -2
  13. package/dist/assets/{Plugins-D2oA_2Jl.js → Plugins-B3gUDkeb.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DgAgavUM.js → ProvenanceBadge-CeHOub7m.js} +1 -1
  15. package/dist/assets/SaveBar-BcvQEq6h.js +1 -0
  16. package/dist/assets/Scheduler-Dc5qiP24.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-C_zWEtIl.js → ScopeSwitcher-BvGQmw4Y.js} +1 -1
  18. package/dist/assets/{Settings-2Vx3X5SI.js → Settings-C2dEFb-v.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-BDEUjlTQ.js → SkillReferenceGraph-CIwlosBc.js} +1 -1
  20. package/dist/assets/{Skills-Cmrz_LeN.js → Skills-C0GzzVrQ.js} +2 -2
  21. package/dist/assets/{SystemPrompt-DVA1eYDP.js → SystemPrompt-mtGPK8zo.js} +1 -1
  22. package/dist/assets/TagLibrary-DX54-mpd.js +1 -0
  23. package/dist/assets/{TiptapBody-DmPc3amD.js → TiptapBody-yADC2RWE.js} +1 -1
  24. package/dist/assets/{Toggle-zfd5LJkK.js → Toggle-CRxaCYLI.js} +1 -1
  25. package/dist/assets/{index-DIjnPkRN.css → index-CYhdtisq.css} +1 -1
  26. package/dist/assets/{index-B_4PNh9T.js → index-D6ymGESc.js} +469 -469
  27. package/dist/assets/{settingsSchema-B9es6fdA.js → settingsSchema-TtMvT5Sx.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  32. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  33. package/scripts/render-project-pages.cjs +70 -0
  34. package/scripts/scheduler-mcp-server.cjs +115 -1
  35. package/scripts/validate-project-pages-summary.cjs +62 -0
  36. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  37. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  38. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  39. package/src/main/__tests__/prdCreate.test.cjs +138 -0
  40. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  41. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  42. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  43. package/src/main/__tests__/projectPages.test.cjs +73 -1
  44. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  45. package/src/main/__tests__/runVerify.test.cjs +94 -0
  46. package/src/main/__tests__/scheduler-autofix-select.test.cjs +43 -0
  47. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  48. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  49. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  50. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  51. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  52. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  53. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  54. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  55. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  56. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  57. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +147 -0
  58. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  59. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  60. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  61. package/src/main/config.cjs +4 -1
  62. package/src/main/index.cjs +16 -4
  63. package/src/main/ipcSchemas.cjs +53 -0
  64. package/src/main/lib/__tests__/childWithLog.test.cjs +78 -0
  65. package/src/main/lib/__tests__/delegationReadiness.test.cjs +197 -2
  66. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +4 -2
  67. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  68. package/src/main/lib/__tests__/gitWorktree.test.cjs +277 -4
  69. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  70. package/src/main/lib/__tests__/jobWorktree.test.cjs +5 -3
  71. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  72. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  73. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +1 -0
  74. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  75. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  76. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  77. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  78. package/src/main/lib/__tests__/reaperHelpers.test.cjs +22 -1
  79. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  80. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  81. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  82. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  83. package/src/main/lib/agentModelResolve.cjs +58 -0
  84. package/src/main/lib/childWithLog.cjs +40 -5
  85. package/src/main/lib/claudeBin.cjs +54 -1
  86. package/src/main/lib/definitionOfDone.cjs +3 -2
  87. package/src/main/lib/delegationReadiness.cjs +115 -9
  88. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  89. package/src/main/lib/fixChainDepth.cjs +45 -0
  90. package/src/main/lib/gitWorktree.cjs +464 -19
  91. package/src/main/lib/jobWorktree.cjs +1 -0
  92. package/src/main/lib/landedSinceRun.cjs +55 -0
  93. package/src/main/lib/launchFailure.cjs +357 -0
  94. package/src/main/lib/mcpToolCatalog.cjs +87 -2
  95. package/src/main/lib/opsOwnership.cjs +12 -0
  96. package/src/main/lib/prdAgentType.cjs +84 -0
  97. package/src/main/lib/prdCreate.cjs +57 -1
  98. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  99. package/src/main/lib/prdFrontmatter.cjs +17 -3
  100. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  101. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  102. package/src/main/lib/queueHealth.cjs +38 -0
  103. package/src/main/lib/queueStore.cjs +9 -2
  104. package/src/main/lib/quietMachineLease.cjs +48 -0
  105. package/src/main/lib/rcaReport.cjs +53 -3
  106. package/src/main/lib/reaperHelpers.cjs +18 -1
  107. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  108. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  109. package/src/main/lib/schedulerBatch.cjs +133 -29
  110. package/src/main/lib/schedulerConfig.cjs +19 -0
  111. package/src/main/projectPages.cjs +160 -2
  112. package/src/main/runVerify.cjs +50 -9
  113. package/src/main/scheduler/prdParser.cjs +18 -1
  114. package/src/main/scheduler.cjs +1371 -97
  115. package/src/main/seedAgentPersonas.cjs +62 -21
  116. package/src/main/templates/project-pages-catalog.json +741 -0
  117. package/src/main/templates/project-pages-pipeline.md +417 -0
  118. package/src/preload/api.d.ts +126 -6
  119. package/src/preload/index.cjs +8 -0
  120. package/src/seed/agents/project-home-builder.md +59 -0
  121. package/dist/assets/SaveBar-Qvc4Ek-H.js +0 -1
  122. package/dist/assets/Scheduler-BmYJvNzK.js +0 -14
  123. package/dist/assets/TagLibrary-DYJGAKZu.js +0 -1
@@ -0,0 +1,194 @@
1
+ /**
2
+ * scheduler-worktree-cap-defer.test.cjs — PRD 1112: a job that cannot get
3
+ * worktree isolation because the concurrency cap is currently exhausted must
4
+ * be DEFERRED (left 'pending', retried on the next dispatch pass), never
5
+ * spawned in a shared working tree the way every OTHER worktree.ok===false
6
+ * reason (e.g. "not a git repository") still falls back to running in place.
7
+ *
8
+ * Drives spawnJob end-to-end against a real git fixture repo, forcing the
9
+ * cap via SM_JOB_WORKTREE_MAX=1 plus a real occupying worktree (so the cap
10
+ * check itself, not a mock, is what defers the job).
11
+ *
12
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-worktree-cap-defer.test.cjs
13
+ */
14
+
15
+ 'use strict';
16
+
17
+ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
18
+ const fs = require('node:fs');
19
+ const os = require('node:os');
20
+ const path = require('node:path');
21
+ const { execFileSync } = require('node:child_process');
22
+
23
+ let tmpHome;
24
+ let originalHome;
25
+ let originalMax;
26
+ let spawnJob;
27
+ let gitWorktree;
28
+
29
+ function git(args, cwd) {
30
+ return execFileSync('git', args, { cwd, encoding: 'utf8' });
31
+ }
32
+
33
+ function initRepo(dir) {
34
+ fs.mkdirSync(dir, { recursive: true });
35
+ git(['init', '-q'], dir);
36
+ git(['config', 'user.email', 'test@example.com'], dir);
37
+ git(['config', 'user.name', 'Test'], dir);
38
+ fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
39
+ git(['add', '-A'], dir);
40
+ git(['commit', '-q', '-m', 'initial'], dir);
41
+ }
42
+
43
+ // See scheduler-quiet-machine-lease.test.cjs's identical helper: readQueue()'s
44
+ // per-project discovery walks ~/.claude/projects/*/*.jsonl transcripts for a
45
+ // `cwd` field to find known project queue.json shards.
46
+ function registerActiveProject(cwd) {
47
+ const projectsDir = path.join(tmpHome, '.claude', 'projects');
48
+ const slugDir = path.join(projectsDir, `fake-project-slug-${path.basename(cwd)}`);
49
+ fs.mkdirSync(slugDir, { recursive: true });
50
+ fs.writeFileSync(path.join(slugDir, 'transcript.jsonl'), JSON.stringify({ cwd }) + '\n');
51
+ }
52
+
53
+ function writeProjectQueue(cwd, jobs) {
54
+ const stateDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'state');
55
+ fs.mkdirSync(stateDir, { recursive: true });
56
+ fs.writeFileSync(path.join(stateDir, 'queue.json'), JSON.stringify({ jobs }, null, 2));
57
+ return path.join(stateDir, 'queue.json');
58
+ }
59
+
60
+ // Stub `claude` binary: writes a marker file into its OWN process.cwd() so a
61
+ // test can prove whether — and where — it was actually spawned, then emits a
62
+ // clean stream-json success result.
63
+ function writeClaudeStub() {
64
+ const stubPath = path.join(os.tmpdir(), `sm-claude-stub-capdefer-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
65
+ const body = `
66
+ const fs = require('fs');
67
+ const path = require('path');
68
+ const { execFileSync } = require('child_process');
69
+ fs.writeFileSync(path.join(process.cwd(), 'ran-here.marker'), 'yes', 'utf8');
70
+ // Land a real commit + finish-protocol sentinel when possible, so a run
71
+ // that actually reaches the finish protocol lands 'completed' — a
72
+ // best-effort no-op when the cwd isn't a git repo (e.g. the non-git
73
+ // fallback case), where the run parks in needs_review instead, which is
74
+ // fine — this test only cares whether it ran in place, not its verdict.
75
+ try {
76
+ execFileSync('git', ['add', '-A'], { cwd: process.cwd() });
77
+ execFileSync('git', ['commit', '-q', '-m', 'stub commit'], { cwd: process.cwd() });
78
+ } catch { /* not a git repo — best-effort only */ }
79
+ process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok\\nSCHEDULER_VERDICT: PASS' }) + '\\n');
80
+ process.exit(0);
81
+ `;
82
+ fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
83
+ return stubPath;
84
+ }
85
+
86
+ beforeAll(() => {
87
+ originalHome = process.env.HOME;
88
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-'));
89
+ process.env.HOME = tmpHome;
90
+ originalMax = process.env.SM_JOB_WORKTREE_MAX;
91
+ ({ spawnJob } = require('../scheduler.cjs'));
92
+ gitWorktree = require('../lib/gitWorktree.cjs');
93
+ });
94
+
95
+ afterAll(() => {
96
+ process.env.HOME = originalHome;
97
+ if (originalMax === undefined) delete process.env.SM_JOB_WORKTREE_MAX;
98
+ else process.env.SM_JOB_WORKTREE_MAX = originalMax;
99
+ fs.rmSync(tmpHome, { recursive: true, force: true });
100
+ });
101
+
102
+ afterEach(() => {
103
+ delete process.env.SM_CLAUDE_BIN;
104
+ delete process.env.SM_JOB_WORKTREE_MAX;
105
+ gitWorktree._resetActiveWorktreeCountForTests('job', 0);
106
+ });
107
+
108
+ test('a job held behind the worktree cap stays pending with a heldReason, is never spawned in place, and dispatches once the cap frees up', async () => {
109
+ const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-project-'));
110
+ initRepo(projectCwd);
111
+ registerActiveProject(projectCwd);
112
+
113
+ const slug = `1112-test-held-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
114
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
115
+ fs.mkdirSync(prdsDir, { recursive: true });
116
+ fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Held behind the worktree cap.', 'utf8');
117
+ const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
118
+
119
+ process.env.SM_JOB_WORKTREE_MAX = '1';
120
+ process.env.SM_CLAUDE_BIN = writeClaudeStub();
121
+
122
+ // Occupy the single worktree slot with a REAL checkout, so the cap check
123
+ // spawnJob relies on is exercising the actual gitWorktree plumbing, not a
124
+ // mock.
125
+ const occupier = await gitWorktree.createJobWorktree({ cwd: projectCwd, slug: 'occupier' });
126
+ expect(occupier.ok).toBe(true);
127
+
128
+ const runId = `run-${slug}`;
129
+ const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
130
+ fs.mkdirSync(runDir, { recursive: true });
131
+
132
+ try {
133
+ await spawnJob({ slug, cwd: projectCwd }, runId, runDir, projectCwd);
134
+
135
+ // Held: still pending, never spawned, heldReason visible on the row.
136
+ let jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
137
+ let row = jobs.find((j) => j.slug === slug);
138
+ expect(row.status).toBe('pending');
139
+ expect(row.heldReason).toMatch(/worktree cap reached/);
140
+ expect(fs.existsSync(path.join(projectCwd, 'ran-here.marker'))).toBe(false);
141
+ expect(fs.existsSync(path.join(occupier.dir, 'ran-here.marker'))).toBe(false);
142
+
143
+ // Free the cap (mirrors cleanupJobWorktree releasing the counter), then
144
+ // retry — the held job dispatches on this next pass, exactly like a
145
+ // sessionSlots-blocked job retries once a slot frees up.
146
+ await gitWorktree.cleanupJobWorktree({ cwd: projectCwd, dir: occupier.dir, branch: occupier.branch });
147
+
148
+ await spawnJob({ slug, cwd: projectCwd }, `${runId}-2`, runDir, projectCwd);
149
+
150
+ jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
151
+ row = jobs.find((j) => j.slug === slug);
152
+ expect(row.status).toBe('completed');
153
+ expect(row.heldReason).toBeUndefined();
154
+ } finally {
155
+ fs.rmSync(projectCwd, { recursive: true, force: true });
156
+ fs.rmSync(runDir, { recursive: true, force: true });
157
+ }
158
+ }, 30_000);
159
+
160
+ test('a non-git cwd still falls back to running in place (unchanged behaviour — only cap-reached is a deferral)', async () => {
161
+ const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-worktree-cap-defer-nongit-'));
162
+ fs.mkdirSync(projectCwd, { recursive: true }); // deliberately NOT a git repo
163
+ registerActiveProject(projectCwd);
164
+
165
+ const slug = `1112-test-nongit-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
166
+ const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
167
+ fs.mkdirSync(prdsDir, { recursive: true });
168
+ fs.writeFileSync(path.join(prdsDir, `${slug}.md`), 'Non-git cwd, must run in place.', 'utf8');
169
+ const queuePath = writeProjectQueue(projectCwd, [{ slug, status: 'pending', cwd: projectCwd }]);
170
+
171
+ process.env.SM_CLAUDE_BIN = writeClaudeStub();
172
+
173
+ const runId = `run-${slug}`;
174
+ const runDir = path.join(tmpHome, '.claude', 'session-manager', 'scheduled-plans', 'runs', runId);
175
+ fs.mkdirSync(runDir, { recursive: true });
176
+
177
+ try {
178
+ await spawnJob({ slug, cwd: projectCwd }, runId, runDir, projectCwd);
179
+
180
+ const jobs = JSON.parse(fs.readFileSync(queuePath, 'utf8')).jobs;
181
+ const row = jobs.find((j) => j.slug === slug);
182
+ // Not held/deferred — it actually ran (a non-git cwd can't commit, so it
183
+ // parks in needs_review rather than completed; this test only cares that
184
+ // it was dispatched in place, not its exact verdict).
185
+ expect(row.status).not.toBe('pending');
186
+ expect(row.heldReason).toBeUndefined();
187
+ expect(row.worktreeFallbackReason).toMatch(/not a git repository/);
188
+ // Ran IN PLACE — the marker landed directly in projectCwd, not a worktree.
189
+ expect(fs.existsSync(path.join(projectCwd, 'ran-here.marker'))).toBe(true);
190
+ } finally {
191
+ fs.rmSync(projectCwd, { recursive: true, force: true });
192
+ fs.rmSync(runDir, { recursive: true, force: true });
193
+ }
194
+ }, 30_000);
@@ -54,22 +54,20 @@ function silentLogger() {
54
54
  return { log: () => {}, warn: () => {} };
55
55
  }
56
56
 
57
- test('fresh seed writes both persona files', async () => {
58
- await seedAgentPersonas({ logger: silentLogger() });
57
+ const ALL_PERSONAS = ['architect', 'dev-lead', 'project-home-builder'];
59
58
 
60
- const architect = path.join(agentsDir(), 'architect.md');
61
- const devLead = path.join(agentsDir(), 'dev-lead.md');
62
- expect(fs.existsSync(architect)).toBe(true);
63
- expect(fs.existsSync(devLead)).toBe(true);
59
+ test('fresh seed writes every persona file', async () => {
60
+ await seedAgentPersonas({ logger: silentLogger() });
64
61
 
65
- const bundledArchitect = fs.readFileSync(
66
- path.join(__dirname, '..', '..', 'seed', 'agents', 'architect.md'),
67
- 'utf8'
68
- );
69
- expect(fs.readFileSync(architect, 'utf8')).toBe(bundledArchitect);
62
+ for (const name of ALL_PERSONAS) {
63
+ const dest = path.join(agentsDir(), `${name}.md`);
64
+ expect(fs.existsSync(dest)).toBe(true);
65
+ const bundled = fs.readFileSync(path.join(__dirname, '..', '..', 'seed', 'agents', `${name}.md`), 'utf8');
66
+ expect(fs.readFileSync(dest, 'utf8')).toBe(bundled);
67
+ }
70
68
 
71
69
  const marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
72
- expect(marker.done).toBe(true);
70
+ expect(marker.seeded.sort()).toEqual([...ALL_PERSONAS].sort());
73
71
  });
74
72
 
75
73
  test('a pre-existing persona file is left byte-identical', async () => {
@@ -81,20 +79,83 @@ test('a pre-existing persona file is left byte-identical', async () => {
81
79
  await seedAgentPersonas({ logger: silentLogger() });
82
80
 
83
81
  expect(fs.readFileSync(architect, 'utf8')).toBe(customContent);
84
- // dev-lead had no pre-existing file, so it should still be seeded.
82
+ // dev-lead and project-home-builder had no pre-existing file, so they should still be seeded.
85
83
  expect(fs.existsSync(path.join(agentsDir(), 'dev-lead.md'))).toBe(true);
84
+ expect(fs.existsSync(path.join(agentsDir(), 'project-home-builder.md'))).toBe(true);
86
85
  });
87
86
 
88
- test('done:true marker short-circuits without touching agents dir', async () => {
87
+ test('a machine already fully seeded under the old done:true marker only picks up a newly added persona', async () => {
89
88
  const mPath = markerPath();
90
89
  fs.mkdirSync(path.dirname(mPath), { recursive: true });
90
+ // Legacy marker shape from before the seeded-set change — no `seeded` array.
91
91
  fs.writeFileSync(mPath, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
92
+ fs.mkdirSync(agentsDir(), { recursive: true });
93
+ fs.writeFileSync(path.join(agentsDir(), 'architect.md'), 'old architect content\n');
94
+ fs.writeFileSync(path.join(agentsDir(), 'dev-lead.md'), 'old dev-lead content\n');
95
+
96
+ await seedAgentPersonas({ logger: silentLogger() });
97
+
98
+ // Pre-existing files from the old run are untouched...
99
+ expect(fs.readFileSync(path.join(agentsDir(), 'architect.md'), 'utf8')).toBe('old architect content\n');
100
+ expect(fs.readFileSync(path.join(agentsDir(), 'dev-lead.md'), 'utf8')).toBe('old dev-lead content\n');
101
+ // ...but the newly added persona is delivered.
102
+ expect(fs.existsSync(path.join(agentsDir(), 'project-home-builder.md'))).toBe(true);
103
+
104
+ const marker = JSON.parse(fs.readFileSync(mPath, 'utf8'));
105
+ expect(marker.seeded.sort()).toEqual([...ALL_PERSONAS].sort());
106
+ });
107
+
108
+ test('a machine already seeded under the new seeded-set marker picks up a newly added persona', async () => {
109
+ const mPath = markerPath();
110
+ fs.mkdirSync(path.dirname(mPath), { recursive: true });
111
+ fs.writeFileSync(mPath, JSON.stringify({ seeded: ['architect', 'dev-lead'], attempts: 0, ts: '2026-01-01T00:00:00.000Z' }));
112
+
113
+ await seedAgentPersonas({ logger: silentLogger() });
114
+
115
+ expect(fs.existsSync(path.join(agentsDir(), 'project-home-builder.md'))).toBe(true);
116
+ const marker = JSON.parse(fs.readFileSync(mPath, 'utf8'));
117
+ expect(marker.seeded.sort()).toEqual([...ALL_PERSONAS].sort());
118
+ });
119
+
120
+ test('a machine with every known persona already in the seeded set short-circuits without touching agents dir', async () => {
121
+ const mPath = markerPath();
122
+ fs.mkdirSync(path.dirname(mPath), { recursive: true });
123
+ fs.writeFileSync(mPath, JSON.stringify({ seeded: ALL_PERSONAS, attempts: 0, ts: '2026-01-01T00:00:00.000Z' }));
92
124
 
93
125
  await seedAgentPersonas({ logger: silentLogger() });
94
126
 
95
127
  expect(fs.existsSync(agentsDir())).toBe(false);
96
128
  });
97
129
 
130
+ test('a corrupt/unparseable marker file still seeds correctly', async () => {
131
+ const mPath = markerPath();
132
+ fs.mkdirSync(path.dirname(mPath), { recursive: true });
133
+ fs.writeFileSync(mPath, '{ not valid json');
134
+
135
+ await seedAgentPersonas({ logger: silentLogger() });
136
+
137
+ for (const name of ALL_PERSONAS) {
138
+ expect(fs.existsSync(path.join(agentsDir(), `${name}.md`))).toBe(true);
139
+ }
140
+ });
141
+
142
+ test('the bundled project-home-builder persona contains no session-manager-repo paths', () => {
143
+ const content = fs.readFileSync(
144
+ path.join(__dirname, '..', '..', 'seed', 'agents', 'project-home-builder.md'),
145
+ 'utf8'
146
+ );
147
+ const forbidden = [
148
+ 'session-manager-operations/architecture/',
149
+ '.claude/agents/',
150
+ 'session-manager-operations/design-mocks/',
151
+ 'scripts/',
152
+ 'npm run build:project-pages',
153
+ ];
154
+ for (const substr of forbidden) {
155
+ expect(content).not.toContain(substr);
156
+ }
157
+ });
158
+
98
159
  test('SM_SEED_AGENT_PERSONAS_DISABLE=1 short-circuits', async () => {
99
160
  process.env.SM_SEED_AGENT_PERSONAS_DISABLE = '1';
100
161
 
@@ -148,6 +148,9 @@ function validateWrite(realAbs) {
148
148
  // (PRD 825; scheduler.cjs remote.writePrd)
149
149
  // project-brief projectBrief.cjs's brief.json (PRD 837)
150
150
  // bilko-host bilkoHost.cjs's bundle prep
151
+ // project-pages projectHomeAdminRoutes.cjs's app-side render path
152
+ // (summary.json/picks.json/output/*; PRD:
153
+ // project-home-admin-routes)
151
154
  // Paths come from opsOwnership.opsPath (PRD 1082) — the one ops-root
152
155
  // resolver — which THROWS for an ephemeral root (a linked worktree that
153
156
  // pty.cjs registered as an allowed root for its spawn cwd, or
@@ -155,7 +158,7 @@ function validateWrite(realAbs) {
155
158
  // never a writable destination, so its exemptions simply do not exist.
156
159
  let opsSubs;
157
160
  try {
158
- opsSubs = ['prompt-sessions', 'scheduler', 'project-brief', 'bilko-host'].map((ns) => opsPath(realRoot, ns));
161
+ opsSubs = ['prompt-sessions', 'scheduler', 'project-brief', 'bilko-host', 'project-pages'].map((ns) => opsPath(realRoot, ns));
159
162
  } catch {
160
163
  opsSubs = [];
161
164
  }
@@ -27,6 +27,7 @@ const scheduler = require('./scheduler.cjs');
27
27
  const { createAdminHttp } = require('./lib/localAdminHttp.cjs');
28
28
  const prdCreate = require('./lib/prdCreate.cjs');
29
29
  const prdAdminRoutes = require('./lib/prdAdminRoutes.cjs');
30
+ const projectHomeAdminRoutes = require('./lib/projectHomeAdminRoutes.cjs');
30
31
  const { appendAuditEvent } = require('./lib/auditLog.cjs');
31
32
  const chatRunner = require('./chatRunner.cjs');
32
33
  const promptSessionEvents = require('./promptSessionEvents.cjs');
@@ -36,7 +37,7 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
36
37
  const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
37
38
  const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
38
39
  const agentLibrary = require('./agentLibrary.cjs');
39
- const { checkDelegationReadiness, installPrdWriteGuard } = require('./lib/delegationReadiness.cjs');
40
+ const { checkDelegationReadiness, installPrdWriteGuard, installDestructiveGitGuard } = require('./lib/delegationReadiness.cjs');
40
41
  const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./lib/mcpToolCatalog.cjs');
41
42
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
42
43
  const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
@@ -45,6 +46,7 @@ const adminHttp = createAdminHttp();
45
46
  scheduler.registerAdminRoutes(adminHttp);
46
47
  prdCreate.registerAdminRoute(adminHttp, scheduler.remote);
47
48
  prdAdminRoutes.registerAdminRoute(adminHttp, scheduler.remote);
49
+ projectHomeAdminRoutes.registerAdminRoute(adminHttp);
48
50
  chatRunner.registerAdminRoute(adminHttp);
49
51
  // Project-to-project feedback conduit — the app is the only process holding
50
52
  // several projects' operations roots open, so it performs the cross-folder
@@ -69,7 +71,7 @@ const schedulerConfig = require('./lib/schedulerConfig.cjs');
69
71
  const memoryTool = require('./memoryTool.cjs');
70
72
  const { registerMemoryAggregateIpc } = require('./memoryAggregate.cjs');
71
73
  const { registerProjectBriefIpc } = require('./projectBrief.cjs');
72
- const { registerProjectPagesIpc } = require('./projectPages.cjs');
74
+ const { registerProjectPagesIpc, attachWindow: attachProjectPagesWindow, closeAllOutputWatchers: closeAllProjectPagesWatchers } = require('./projectPages.cjs');
73
75
  const { registerBilkoHostIpc } = require('./bilkoHost.cjs');
74
76
  const promptSessionTranscript = require('./promptSessionTranscript.cjs');
75
77
  const { computeEpicDelegationStats } = require('./lib/epicDelegationStats.cjs');
@@ -300,6 +302,7 @@ function relaunchViaNpx() {
300
302
  async function rebootApp() {
301
303
  ptyManager.killAll();
302
304
  configMgr.closeAllWatchers();
305
+ closeAllProjectPagesWatchers();
303
306
  transcripts.closeAll();
304
307
  watchers.manager.killAll();
305
308
 
@@ -326,6 +329,7 @@ async function rebootApp() {
326
329
  chatRunner.attachWindow(mainWindow);
327
330
  promptSessionEvents.attachWindow(mainWindow);
328
331
  attachDocEditWindow(mainWindow);
332
+ attachProjectPagesWindow(mainWindow);
329
333
  rebooting = false;
330
334
  return;
331
335
  }
@@ -509,12 +513,17 @@ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadiness
509
513
  checkDelegationReadiness(payload)
510
514
  ));
511
515
 
512
- // The one readiness check with a sanctioned one-press install. Session Manager
516
+ // The two readiness checks with a sanctioned one-press install. Session Manager
513
517
  // standardizes on the REFERENCE approach (absolute path to this repo's guard
514
- // script, never a vendored copy) — see installPrdWriteGuard's header.
518
+ // script, never a vendored copy) — see installPrdWriteGuard's header. They stay
519
+ // two handlers (and two PreToolUse entries) because the hooks have different
520
+ // matchers: Write|Edit|NotebookEdit vs Bash — installDestructiveGitGuard's header.
515
521
  ipcMain.handle('app:install-prd-write-guard', validated(schemas.delegationReadinessCwd, (payload) =>
516
522
  installPrdWriteGuard(payload)
517
523
  ));
524
+ ipcMain.handle('app:install-destructive-git-guard', validated(schemas.delegationReadinessCwd, (payload) =>
525
+ installDestructiveGitGuard(payload)
526
+ ));
518
527
 
519
528
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
520
529
 
@@ -1168,6 +1177,7 @@ app.whenReady().then(async () => {
1168
1177
  chatRunner.attachWindow(mainWindow);
1169
1178
  promptSessionEvents.attachWindow(mainWindow);
1170
1179
  attachDocEditWindow(mainWindow);
1180
+ attachProjectPagesWindow(mainWindow);
1171
1181
  // Scheduler ownership (PRD 834): exactly one instance machine-wide runs
1172
1182
  // scheduler mutation (boot reconciliation, ticking, sweep, supervisor) and
1173
1183
  // the admin API. Electron's own single-instance lock is keyed by userData
@@ -1344,6 +1354,7 @@ app.on('window-all-closed', () => {
1344
1354
  if (rebooting) return; // new window is about to be created
1345
1355
  ptyManager.killAll();
1346
1356
  configMgr.closeAllWatchers();
1357
+ closeAllProjectPagesWatchers();
1347
1358
  transcripts.closeAll();
1348
1359
  watchers.manager.killAll();
1349
1360
  if (process.platform !== 'darwin') app.quit();
@@ -1358,6 +1369,7 @@ app.on('before-quit', () => {
1358
1369
  supervisor.stopSupervisor();
1359
1370
  ptyManager.killAll();
1360
1371
  configMgr.closeAllWatchers();
1372
+ closeAllProjectPagesWatchers();
1361
1373
  transcripts.closeAll();
1362
1374
  watchers.manager.killAll();
1363
1375
  adminHttp.stop().catch(() => {});
@@ -10,6 +10,7 @@ const os = require('node:os');
10
10
  const path = require('node:path');
11
11
  const { PromptSessionSchema, EpicTagSchema, EpicSourceSchema, EpicIntakeSectionSchema } = require('./lib/promptSessionSchema.cjs');
12
12
  const { PrdWorkTypeSchema } = require('./lib/workTypeLibrary.cjs');
13
+ const { PERSONA_NAME_RE } = require('./agentLibrary.cjs');
13
14
  const { AgentPersonaSaveSchema } = require('./lib/agentPersonaSchema.cjs');
14
15
 
15
16
  // ──────────────────────────────────────────── PTY
@@ -224,6 +225,12 @@ const promptSessionsMergeToMain = z.object({
224
225
  epicId: z.string().regex(PROMPT_SESSION_ID_RE),
225
226
  branch: z.string().regex(/^sm-epic\/[A-Za-z0-9_-]{1,64}$/),
226
227
  dir: z.string().min(1).max(4096),
228
+ // Base-tree WIP paths carried into this worktree at creation time
229
+ // (gitWorktree.cjs's createWorktree, PRD 1094) — threaded through so
230
+ // integrateEpicBranch can apply the same carried-wip-only skip the job
231
+ // path already gets. Absent on a worktree created before this field
232
+ // existed, or one whose base tree was clean at creation.
233
+ carriedPaths: z.array(z.string().min(1).max(4096)).max(2000).optional(),
227
234
  });
228
235
 
229
236
  // Renderer entry points for the per-project "disable Epic worktree
@@ -299,6 +306,13 @@ const PRD_CREATE_SLUG_RE = /^[a-z0-9]+(-[a-z0-9]+)*$/;
299
306
  // frontmatter-shaped lines into the body. Block newlines at the boundary
300
307
  // instead of trying to escape them later.
301
308
  const NO_NEWLINE_RE = /^[^\r\n]*$/;
309
+ // WHO executes a PRD — a persona name (agentLibrary.cjs's PERSONA_NAME_RE),
310
+ // distinct from `tag` (the WORK TYPE, above). Not a closed enum here: the
311
+ // Agent Library is user-editable, so the actual FK check (does this name
312
+ // resolve to a readable persona file) happens at write time in
313
+ // prdCreate.cjs's createPrd via prdAgentType.cjs's assertAgentTypeWritable —
314
+ // this schema only enforces the name SHAPE.
315
+ const PrdAgentTypeSchema = z.string().regex(PERSONA_NAME_RE, 'agent name must be lowercase, hyphenated (e.g. "dev-lead")');
302
316
  const schedulerCreatePrd = z.object({
303
317
  title: z.string().min(1).max(200).regex(NO_NEWLINE_RE, 'must not contain newlines'),
304
318
  // Optional (PRD: worktree-cwd Epic-lookup hazard) — when omitted, prdCreate.cjs's
@@ -352,6 +366,18 @@ const schedulerCreatePrd = z.object({
352
366
  // excluded here for the same reason adminPrdFrontmatterPatch excludes it
353
367
  // below — a discussion-tagged Epic never reaches PRD authoring.
354
368
  tag: PrdWorkTypeSchema.optional(),
369
+ // WHO executes this PRD (persona name, `~/.claude/agents/<name>.md`) —
370
+ // independent of `tag` above. Optional: prdCreate.cjs's buildPrdBody fills
371
+ // in DEFAULT_PRD_AGENT_TYPE ('dev-lead') when omitted.
372
+ agentType: PrdAgentTypeSchema.optional(),
373
+ // Opt-in exclusive-lease flag (PRD 1107): the scheduler dispatches this PRD
374
+ // only once zero other jobs are running machine-wide, and holds every other
375
+ // job off the whole session-slot pool while it runs — for a PRD whose
376
+ // acceptance criteria are wall-clock/timing measurements (frame time,
377
+ // performance fences) that CPU contention from sibling jobs would otherwise
378
+ // invalidate. See prdCreate.cjs's buildPrdBody and schedulerBatch.cjs's
379
+ // pickNextBatch.
380
+ quietMachine: z.boolean().optional(),
355
381
  });
356
382
 
357
383
  // Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
@@ -418,12 +444,17 @@ const adminPrdFrontmatterPatch = z.object({
418
444
  // discussion-tagged Epic never reaches PRD authoring) — see
419
445
  // workTypeLibrary.cjs's PrdWorkTypeSchema.
420
446
  tag: PrdWorkTypeSchema.optional(),
447
+ // WHO executes this PRD — patchable after creation too, same as `tag`.
448
+ agentType: PrdAgentTypeSchema.optional(),
421
449
  // Provenance stamp (PRD-authoring lockdown). Free-text rather than an enum
422
450
  // — 'scheduler-api' (create-time) and 'legacy-adopted' (migration/manual
423
451
  // adopt) are the two values this codebase writes today, but the field
424
452
  // itself is descriptive, not a closed set.
425
453
  createdVia: z.string().min(1).max(64).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
426
454
  issuedAt: z.string().min(1).max(64).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
455
+ // Opt-in exclusive-lease flag (PRD 1107) — patchable after creation too,
456
+ // same as every other recognized key here.
457
+ quietMachine: z.boolean().optional(),
427
458
  });
428
459
 
429
460
  const adminUpdatePrd = z.object({
@@ -565,6 +596,8 @@ const projectBriefCwd = z.object({
565
596
  }).strict();
566
597
 
567
598
  // ──────────────────────────────────────────── Delegation readiness
599
+ // Shared by app:delegation-readiness, app:install-prd-write-guard and
600
+ // app:install-destructive-git-guard — all three take exactly { cwd }.
568
601
  const delegationReadinessCwd = z.object({
569
602
  cwd: z.string().min(1).max(4096),
570
603
  }).strict();
@@ -582,6 +615,23 @@ const projectPagesCwd = z.object({
582
615
  cwd: z.string().min(1).max(4096),
583
616
  }).strict();
584
617
 
618
+ // ──────────────────────────────────────────── Project Home admin routes
619
+ // (PRD: project-home-admin-routes). Same validation split as
620
+ // projectPagesCwd above: real path validation is projectRootResolve.cjs +
621
+ // config.cjs's validatePath at first fs access.
622
+ const projectHomeAdminCwdQuery = z.object({
623
+ cwd: z.string().min(1).max(4096),
624
+ }).strict();
625
+ const projectHomeAdminValidateSummaryBody = z.object({
626
+ cwd: z.string().min(1).max(4096),
627
+ summary: z.unknown(),
628
+ }).strict();
629
+ const projectHomeAdminRenderBody = z.object({
630
+ cwd: z.string().min(1).max(4096),
631
+ summary: z.unknown(),
632
+ picks: z.unknown(),
633
+ }).strict();
634
+
585
635
  // ──────────────────────────────────────────── Host on Bilko.run
586
636
  // Same validation split: real path validation is config.cjs's validatePath
587
637
  // at first fs access in bilkoHost.cjs — these schemas only bound wire shape.
@@ -990,6 +1040,9 @@ module.exports = {
990
1040
  memoryStale,
991
1041
  projectBriefCwd,
992
1042
  projectPagesCwd,
1043
+ projectHomeAdminCwdQuery,
1044
+ projectHomeAdminValidateSummaryBody,
1045
+ projectHomeAdminRenderBody,
993
1046
  bilkoHostCwd,
994
1047
  bilkoHostPrepareBundle,
995
1048
  bilkoHostAddDocument,
@@ -61,3 +61,81 @@ describe('childWithLog post-exit group sweep (PRD 1065)', () => {
61
61
  15000,
62
62
  );
63
63
  });
64
+
65
+ // PRD 1110 — the sweep above kills silently; onExit must also report what it
66
+ // swept so a project whose jobs leak repeatedly can see it instead of
67
+ // inferring it from ps.
68
+ describe('childWithLog onExit leakedDescendants reporting (PRD 1110)', () => {
69
+ it.skipIf(process.platform !== 'linux')(
70
+ 'reports the same backgrounded grandchild the sweep reaped',
71
+ async () => {
72
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-child-leak-'));
73
+ const logPath = path.join(dir, 'job.log');
74
+ const pidFile = path.join(dir, 'grandchild.pid');
75
+ const { fd, safeLog, closeFd } = openLog(logPath);
76
+
77
+ const exitInfo = await new Promise((resolve) => {
78
+ withChildAndLog({
79
+ fd,
80
+ logPath,
81
+ safeLog,
82
+ closeFd,
83
+ spawn: {
84
+ command: 'sh',
85
+ args: [
86
+ '-c',
87
+ `sleep 30 & echo $! > ${pidFile}; exec sleep 0.2`,
88
+ ],
89
+ options: { detached: true },
90
+ },
91
+ onExit: (info) => resolve(info),
92
+ });
93
+ });
94
+
95
+ for (let i = 0; i < 20 && !fs.existsSync(pidFile); i++) {
96
+ await new Promise((r) => setTimeout(r, 50));
97
+ }
98
+ const grandchildPid = Number(fs.readFileSync(pidFile, 'utf8').trim());
99
+
100
+ expect(Array.isArray(exitInfo.leakedDescendants)).toBe(true);
101
+ expect(exitInfo.leakedDescendants.length).toBeGreaterThan(0);
102
+ const found = exitInfo.leakedDescendants.find((p) => p.pid === grandchildPid);
103
+ expect(found).toBeTruthy();
104
+ expect(found.comm).toBeTruthy();
105
+ expect(typeof found.pcpu).toBe('number');
106
+ expect(typeof found.etimes).toBe('number');
107
+
108
+ // Let the sweep's SIGTERM->SIGKILL cascade finish so the grandchild
109
+ // doesn't leak past this test.
110
+ await new Promise((r) => setTimeout(r, POST_EXIT_GROUP_SWEEP_GRACE_MS + 1000));
111
+ },
112
+ 15000,
113
+ );
114
+
115
+ it.skipIf(process.platform !== 'linux')(
116
+ 'reports [] for a clean job that leaked nothing',
117
+ async () => {
118
+ dir = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-child-clean-'));
119
+ const logPath = path.join(dir, 'job.log');
120
+ const { fd, safeLog, closeFd } = openLog(logPath);
121
+
122
+ const exitInfo = await new Promise((resolve) => {
123
+ withChildAndLog({
124
+ fd,
125
+ logPath,
126
+ safeLog,
127
+ closeFd,
128
+ spawn: {
129
+ command: 'sh',
130
+ args: ['-c', 'exit 0'],
131
+ options: { detached: true },
132
+ },
133
+ onExit: (info) => resolve(info),
134
+ });
135
+ });
136
+
137
+ expect(exitInfo.leakedDescendants).toEqual([]);
138
+ },
139
+ 15000,
140
+ );
141
+ });