claude-code-session-manager 0.74.0 → 0.75.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 (108) hide show
  1. package/dist/assets/AgentLibrary-SpzptNyq.js +3 -0
  2. package/dist/assets/DataModel-C4M9xVLC.js +1 -0
  3. package/dist/assets/{History-ShwkUTny.js → History-92FYrZKS.js} +2 -2
  4. package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-DlnavO1l.js} +2 -2
  5. package/dist/assets/HostBilko-CKI7jt1z.js +1 -0
  6. package/dist/assets/{Library-BOL6GqFX.js → Library-Brhxe7xa.js} +1 -1
  7. package/dist/assets/ListDetail-DmIweYTN.js +1 -0
  8. package/dist/assets/MarkdownEditor-DA4xqWWi.js +1 -0
  9. package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-DWXQGzVc.js} +2 -2
  10. package/dist/assets/{Memory-BYJHDz7W.js → Memory-HCx9Arao.js} +4 -4
  11. package/dist/assets/Panel-B20C6kN8.js +1 -0
  12. package/dist/assets/{Permissions-DhYERDVR.js → Permissions-C4sPgLpA.js} +3 -3
  13. package/dist/assets/Plugins-CaRenCBg.js +2 -0
  14. package/dist/assets/ProvenanceBadge-DfrXzTeh.js +1 -0
  15. package/dist/assets/SaveBar-BmpUx2Yh.js +1 -0
  16. package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-xUD8pQ6q.js} +6 -6
  17. package/dist/assets/ScopeSwitcher-DVlKMck5.js +1 -0
  18. package/dist/assets/Settings-C457mxOK.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-D3dDc5X7.js} +1 -1
  20. package/dist/assets/{Skills-BHlH2uG5.js → Skills-BNDlbftC.js} +2 -2
  21. package/dist/assets/SystemPrompt-d-ykNRec.js +1 -0
  22. package/dist/assets/TagLibrary-D3JWkQK-.js +1 -0
  23. package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-B1Ox6D6E.js} +1 -1
  24. package/dist/assets/Toggle-CaYw6Lrj.js +1 -0
  25. package/dist/assets/{index-Cs_Lq9-w.js → index-CEeLxNCz.js} +659 -655
  26. package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
  27. package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-DUkd45M5.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +3 -1
  30. package/scripts/scheduler-mcp-server.cjs +471 -0
  31. package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
  32. package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
  33. package/src/main/__tests__/seedStatus.test.cjs +100 -0
  34. package/src/main/agentLibrary.cjs +43 -0
  35. package/src/main/index.cjs +42 -1
  36. package/src/main/ipcSchemas.cjs +25 -0
  37. package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
  38. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
  39. package/src/main/lib/delegationReadiness.cjs +141 -0
  40. package/src/main/lib/epicDelegationStats.cjs +124 -0
  41. package/src/main/lib/promptSessionSchema.cjs +1 -1
  42. package/src/main/seedAgentPersonas.cjs +92 -0
  43. package/src/main/seedDevPlugin.cjs +14 -2
  44. package/src/main/seedSchedulerMcp.cjs +148 -0
  45. package/src/main/seedStatus.cjs +59 -0
  46. package/src/preload/api.d.ts +40 -1
  47. package/src/preload/index.cjs +7 -0
  48. package/src/seed/agents/architect.md +64 -0
  49. package/src/seed/agents/dev-lead.md +42 -0
  50. package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
  51. package/src/seed/prompts/code-review/correctness-only.md +8 -0
  52. package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
  53. package/src/seed/prompts/code-review/hallucination-check.md +8 -0
  54. package/src/seed/prompts/code-review/public-api-compat.md +8 -0
  55. package/src/seed/prompts/code-review/readability-naming.md +8 -0
  56. package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
  57. package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
  58. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
  59. package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
  60. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
  61. package/src/seed/prompts/documentation/adr-from-change.md +8 -0
  62. package/src/seed/prompts/documentation/module-readme.md +8 -0
  63. package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
  64. package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
  65. package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
  66. package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
  67. package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
  68. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
  69. package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
  70. package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
  71. package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
  72. package/src/seed/prompts/performance/complexity-audit.md +8 -0
  73. package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
  74. package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
  75. package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
  76. package/src/seed/prompts/qa/api-contract-tests.md +8 -0
  77. package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
  78. package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
  79. package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
  80. package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
  81. package/src/seed/prompts/qa/tdd-red-first.md +8 -0
  82. package/src/seed/prompts/qa/visual-regression-review.md +8 -0
  83. package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
  84. package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
  85. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
  86. package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
  87. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
  88. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
  89. package/src/seed/prompts/security/authz-audit.md +8 -0
  90. package/src/seed/prompts/security/crypto-correctness.md +8 -0
  91. package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
  92. package/src/seed/prompts/security/dependency-audit.md +8 -0
  93. package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
  94. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
  95. package/src/seed/prompts/security/secret-credential-scan.md +8 -0
  96. package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
  97. package/dist/assets/HostBilko-evd1hD8q.js +0 -1
  98. package/dist/assets/ListDetail-BPJb2et3.js +0 -1
  99. package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
  100. package/dist/assets/Panel-n-p8OQLc.js +0 -1
  101. package/dist/assets/Plugins-D1ckVgo3.js +0 -2
  102. package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
  103. package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
  104. package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
  105. package/dist/assets/Settings-DlyJjexf.js +0 -3
  106. package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
  107. package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
  108. package/dist/assets/Toggle-BJHhHQOq.js +0 -1
@@ -0,0 +1,145 @@
1
+ /**
2
+ * seedSchedulerMcp.test.cjs — unit tests for src/main/seedSchedulerMcp.cjs.
3
+ *
4
+ * Run: timeout 120 npx vitest run src/main/__tests__/seedSchedulerMcp.test.cjs
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ import { test, expect, beforeEach, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ let tmpHome;
15
+ let realHome;
16
+ let seedSchedulerMcp;
17
+
18
+ // config.cjs bakes os.homedir() into top-level consts at first require
19
+ // (allowedRoots/WRITE_PREFIXES, used by writeJsonSync) — must be purged
20
+ // alongside seedSchedulerMcp.cjs so each test's tmpHome takes effect.
21
+ const MODULES_TO_RELOAD = ['../seedSchedulerMcp.cjs', '../config.cjs'];
22
+
23
+ function purgeRequireCache() {
24
+ for (const m of MODULES_TO_RELOAD) {
25
+ try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
26
+ }
27
+ }
28
+
29
+ beforeEach(() => {
30
+ realHome = process.env.HOME;
31
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-scheduler-mcp-test-'));
32
+ process.env.HOME = tmpHome;
33
+ delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
34
+ purgeRequireCache();
35
+ ({ seedSchedulerMcp } = require('../seedSchedulerMcp.cjs'));
36
+ });
37
+
38
+ afterEach(() => {
39
+ process.env.HOME = realHome;
40
+ delete process.env.SM_SEED_SCHEDULER_MCP_DISABLE;
41
+ fs.rmSync(tmpHome, { recursive: true, force: true });
42
+ purgeRequireCache();
43
+ });
44
+
45
+ function markerPath() {
46
+ return path.join(tmpHome, '.claude', 'session-manager', '.scheduler-mcp-seeded');
47
+ }
48
+
49
+ function claudeJsonPath() {
50
+ return path.join(tmpHome, '.claude.json');
51
+ }
52
+
53
+ function silentLogger() {
54
+ return { log: () => {}, warn: () => {} };
55
+ }
56
+
57
+ function readMarker() {
58
+ return JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
59
+ }
60
+
61
+ test('fresh registration calls addFn with the absolute server path and writes done marker', async () => {
62
+ const calls = [];
63
+ const addFn = async (serverPath) => {
64
+ calls.push(serverPath);
65
+ return { ok: true, exitCode: 0 };
66
+ };
67
+
68
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
69
+
70
+ expect(calls).toHaveLength(1);
71
+ expect(path.isAbsolute(calls[0])).toBe(true);
72
+ expect(calls[0].endsWith(path.join('scripts', 'scheduler-mcp-server.cjs'))).toBe(true);
73
+
74
+ const marker = readMarker();
75
+ expect(marker.done).toBe(true);
76
+ expect(marker.attempts).toBe(0);
77
+ });
78
+
79
+ test('already-registered at user scope short-circuits without calling addFn', async () => {
80
+ fs.writeFileSync(
81
+ claudeJsonPath(),
82
+ JSON.stringify({
83
+ mcpServers: {
84
+ 'session-manager-scheduler': { type: 'stdio', command: 'node', args: ['/some/path/scheduler-mcp-server.cjs'] },
85
+ fetch: { type: 'stdio', command: 'uvx', args: ['mcp-server-fetch'] },
86
+ },
87
+ })
88
+ );
89
+ let called = false;
90
+ const addFn = async () => { called = true; return { ok: true }; };
91
+
92
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
93
+
94
+ expect(called).toBe(false);
95
+ const marker = readMarker();
96
+ expect(marker.done).toBe(true);
97
+
98
+ // Untouched — the pre-existing registration (and sibling entries) stay as-is.
99
+ const claudeJson = JSON.parse(fs.readFileSync(claudeJsonPath(), 'utf8'));
100
+ expect(claudeJson.mcpServers['session-manager-scheduler'].args).toEqual(['/some/path/scheduler-mcp-server.cjs']);
101
+ expect(claudeJson.mcpServers.fetch).toBeDefined();
102
+ });
103
+
104
+ test('done:true marker short-circuits without calling addFn', async () => {
105
+ fs.mkdirSync(path.dirname(markerPath()), { recursive: true });
106
+ fs.writeFileSync(markerPath(), JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
107
+ let called = false;
108
+ const addFn = async () => { called = true; return { ok: true }; };
109
+
110
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
111
+
112
+ expect(called).toBe(false);
113
+ });
114
+
115
+ test('SM_SEED_SCHEDULER_MCP_DISABLE=1 short-circuits', async () => {
116
+ process.env.SM_SEED_SCHEDULER_MCP_DISABLE = '1';
117
+ let called = false;
118
+ const addFn = async () => { called = true; return { ok: true }; };
119
+
120
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
121
+
122
+ expect(called).toBe(false);
123
+ expect(fs.existsSync(markerPath())).toBe(false);
124
+ });
125
+
126
+ test('a failed attempt bumps the counter and stops after MAX_ATTEMPTS', async () => {
127
+ const addFn = async () => ({ ok: false, error: 'boom' });
128
+
129
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
130
+ expect(readMarker()).toMatchObject({ done: false, attempts: 1 });
131
+
132
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
133
+ expect(readMarker()).toMatchObject({ done: false, attempts: 2 });
134
+
135
+ await seedSchedulerMcp({ logger: silentLogger(), addFn });
136
+ expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
137
+
138
+ // MAX_ATTEMPTS reached — a 4th run must not call addFn again or bump further.
139
+ let called = false;
140
+ const trackedAddFn = async () => { called = true; return { ok: false, error: 'boom' }; };
141
+ await seedSchedulerMcp({ logger: silentLogger(), addFn: trackedAddFn });
142
+
143
+ expect(called).toBe(false);
144
+ expect(readMarker()).toMatchObject({ done: false, attempts: 3 });
145
+ });
@@ -0,0 +1,100 @@
1
+ /**
2
+ * seedStatus.test.cjs — unit tests for src/main/seedStatus.cjs.
3
+ *
4
+ * Run: timeout 120 npx vitest run src/main/__tests__/seedStatus.test.cjs
5
+ */
6
+
7
+ 'use strict';
8
+
9
+ import { test, expect, beforeEach, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const os = require('node:os');
12
+ const path = require('node:path');
13
+
14
+ let tmpHome;
15
+ let realHome;
16
+ let getSeedStatus;
17
+
18
+ // config.cjs (via seedSchedulerMcp/seedAgentPersonas) and each seed*.cjs
19
+ // module bake os.homedir() into top-level consts at first require — purge
20
+ // all of them alongside seedStatus.cjs so each test's tmpHome takes effect.
21
+ const MODULES_TO_RELOAD = [
22
+ '../seedStatus.cjs',
23
+ '../seedDevPlugin.cjs',
24
+ '../seedSchedulerMcp.cjs',
25
+ '../seedAgentPersonas.cjs',
26
+ '../config.cjs',
27
+ ];
28
+
29
+ function purgeRequireCache() {
30
+ for (const m of MODULES_TO_RELOAD) {
31
+ try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
32
+ }
33
+ }
34
+
35
+ beforeEach(() => {
36
+ realHome = process.env.HOME;
37
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'seed-status-test-'));
38
+ process.env.HOME = tmpHome;
39
+ purgeRequireCache();
40
+ ({ getSeedStatus } = require('../seedStatus.cjs'));
41
+ });
42
+
43
+ afterEach(() => {
44
+ process.env.HOME = realHome;
45
+ fs.rmSync(tmpHome, { recursive: true, force: true });
46
+ purgeRequireCache();
47
+ });
48
+
49
+ const MARKER_NAMES = {
50
+ 'dev-plugin': '.dev-plugin-seeded',
51
+ 'scheduler-mcp': '.scheduler-mcp-seeded',
52
+ 'agent-personas': '.agent-personas-seeded',
53
+ };
54
+
55
+ function markerPath(id) {
56
+ return path.join(tmpHome, '.claude', 'session-manager', MARKER_NAMES[id]);
57
+ }
58
+
59
+ function writeMarker(id, contents) {
60
+ const p = markerPath(id);
61
+ fs.mkdirSync(path.dirname(p), { recursive: true });
62
+ fs.writeFileSync(p, contents);
63
+ }
64
+
65
+ test('no marker files at all → every seeder reports pending with a fix string', () => {
66
+ const status = getSeedStatus();
67
+ for (const id of Object.keys(MARKER_NAMES)) {
68
+ expect(status[id].status).toBe('pending');
69
+ expect(typeof status[id].fix).toBe('string');
70
+ expect(status[id].fix.length).toBeGreaterThan(0);
71
+ }
72
+ });
73
+
74
+ for (const id of Object.keys(MARKER_NAMES)) {
75
+ test(`${id}: done:true marker → done`, () => {
76
+ writeMarker(id, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
77
+ expect(getSeedStatus()[id].status).toBe('done');
78
+ });
79
+
80
+ test(`${id}: done:false, attempts below max → pending`, () => {
81
+ writeMarker(id, JSON.stringify({ done: false, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
82
+ expect(getSeedStatus()[id].status).toBe('pending');
83
+ });
84
+
85
+ test(`${id}: done:false, attempts >= max → exhausted`, () => {
86
+ writeMarker(id, JSON.stringify({ done: false, attempts: 3, ts: '2026-01-01T00:00:00.000Z' }));
87
+ expect(getSeedStatus()[id].status).toBe('exhausted');
88
+ });
89
+
90
+ test(`${id}: unparseable marker → pending, never throws`, () => {
91
+ writeMarker(id, 'not json at all {{{');
92
+ expect(() => getSeedStatus()).not.toThrow();
93
+ expect(getSeedStatus()[id].status).toBe('pending');
94
+ });
95
+
96
+ test(`${id}: missing marker file → pending`, () => {
97
+ // No file written for this id — directory may not even exist.
98
+ expect(getSeedStatus()[id].status).toBe('pending');
99
+ });
100
+ }
@@ -22,6 +22,7 @@ const fsSync = require('node:fs');
22
22
  const path = require('node:path');
23
23
  const os = require('node:os');
24
24
  const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
25
+ const { resolvePersonaPaths } = require('./lib/epicMint.cjs');
25
26
  const configMgr = require('./config.cjs');
26
27
  const sessionsStore = require('./sessionsStore.cjs');
27
28
 
@@ -249,6 +250,47 @@ async function listPersonas({
249
250
  return personas;
250
251
  }
251
252
 
253
+ /**
254
+ * Reads a persona `.md` file's raw text for `epicIntake.ts`'s `agentBody`
255
+ * input, honoring the same project-overlay-then-global precedence as
256
+ * `epicMint.cjs`'s `resolvePersonaPaths` (a project's `<cwd>/.claude/agents/
257
+ * <name>.md` wins over `~/.claude/agents/<name>.md`) — the same precedence
258
+ * `listPersonas` deliberately does NOT apply, since that lists only the
259
+ * global directory for the Agent Library nav page. Returns `null` when
260
+ * neither location has the file; frontmatter stripping and the 6000-char cap
261
+ * are epicIntake.ts's job, not this reader's.
262
+ */
263
+ async function getPersonaBody({
264
+ cwd,
265
+ name,
266
+ validatePath = configMgr.validatePath,
267
+ } = {}) {
268
+ // Same gate savePersona/deletePersona apply before touching a path built
269
+ // from `name` (agentLibrary.cjs's PERSONA_NAME_RE check) — without it a
270
+ // `name` like "../../other-project/CLAUDE" survives resolvePersonaPaths'
271
+ // plain path.join and validatePath's allowed-roots check (which only
272
+ // confirms the resolved path stays under the home dir / an opened
273
+ // project), letting any readable .md under those roots be read and
274
+ // forwarded into the Epic's opening prompt.
275
+ if (!PERSONA_NAME_RE.test(name || '')) return null;
276
+ const { projectPath, globalPath } = resolvePersonaPaths(cwd, name);
277
+ for (const candidate of [projectPath, globalPath]) {
278
+ let real;
279
+ try {
280
+ real = validatePath(candidate);
281
+ } catch {
282
+ continue;
283
+ }
284
+ try {
285
+ const text = await fsp.readFile(real, 'utf8');
286
+ return { path: real, text };
287
+ } catch {
288
+ continue;
289
+ }
290
+ }
291
+ return null;
292
+ }
293
+
252
294
  module.exports = {
253
295
  listPersonas,
254
296
  openProjects,
@@ -257,6 +299,7 @@ module.exports = {
257
299
  deletePersona,
258
300
  removeOverride,
259
301
  serializePersona,
302
+ getPersonaBody,
260
303
  PERSONA_NAME_RE,
261
304
  ALL_PROJECTS,
262
305
  };
@@ -36,6 +36,7 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
36
36
  const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
37
37
  const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
38
38
  const agentLibrary = require('./agentLibrary.cjs');
39
+ const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
39
40
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
40
41
  const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
41
42
  const crossProjectFeedback = require('./lib/crossProjectFeedback.cjs');
@@ -54,6 +55,9 @@ const teams = require('./teams.cjs');
54
55
  const queueOps = require('./queueOps.cjs');
55
56
  const pluginInstall = require('./pluginInstall.cjs');
56
57
  const { seedDevPlugin } = require('./seedDevPlugin.cjs');
58
+ const { seedAgentPersonas } = require('./seedAgentPersonas.cjs');
59
+ const { seedSchedulerMcp } = require('./seedSchedulerMcp.cjs');
60
+ const { getSeedStatus } = require('./seedStatus.cjs');
57
61
  const otel = require('./otel.cjs');
58
62
  const otelSettings = require('./otelSettings.cjs');
59
63
  const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayToday } = require('./historyAggregator.cjs');
@@ -67,6 +71,7 @@ const { registerProjectBriefIpc } = require('./projectBrief.cjs');
67
71
  const { registerProjectPagesIpc } = require('./projectPages.cjs');
68
72
  const { registerBilkoHostIpc } = require('./bilkoHost.cjs');
69
73
  const promptSessionTranscript = require('./promptSessionTranscript.cjs');
74
+ const { computeEpicDelegationStats } = require('./lib/epicDelegationStats.cjs');
70
75
  const agentMemory = require('./agentMemory.cjs');
71
76
  const git = require('./git.cjs');
72
77
  const heapSnapshot = require('./heapSnapshot.cjs');
@@ -481,6 +486,19 @@ ipcMain.handle('agents:remove-override', async (_e, payload) => {
481
486
  broadcastAgentsChanged();
482
487
  return result;
483
488
  });
489
+ // New Epic AIM composer (epicIntake.ts's `agentBody`/`agentPath`): unlike
490
+ // list-personas above (global dir only), this honors the project-overlay-
491
+ // then-global precedence so an Epic's opening prompt carries the persona
492
+ // body that actually applies to its cwd. Read-only.
493
+ ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody, (payload) =>
494
+ agentLibrary.getPersonaBody(payload)));
495
+
496
+ // "Can this project actually delegate?" probe (PRD: delegation-readiness).
497
+ // Structured data over the four preconditions for scheduler_create_prd being
498
+ // in an agent's tool list at all — surfacing it in the UI is a sibling PRD.
499
+ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadinessCwd, (payload) =>
500
+ checkDelegationReadiness(payload)
501
+ ));
484
502
 
485
503
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
486
504
 
@@ -488,6 +506,9 @@ ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE
488
506
  // on disk or the home self-check failed (e.g. macOS /Users symlink mismatch).
489
507
  ipcMain.handle('app:claude-bin-status', () => bootClaudeBin);
490
508
  ipcMain.handle('app:home-self-check', () => bootHomeSelfCheck);
509
+ // First-boot seeder outcomes ('done' | 'pending' | 'exhausted' per seeder),
510
+ // read live from each seeder's own marker file — see seedStatus.cjs.
511
+ ipcMain.handle('app:seed-status', () => getSeedStatus());
491
512
 
492
513
  ipcMain.handle('app:pick-directory', async () => {
493
514
  console.log('[main] pick-directory invoked');
@@ -802,6 +823,11 @@ ipcMain.handle('promptSessionTranscript:read', validated(schemas.promptSessionTr
802
823
  const turns = await promptSessionTranscript.readTurns(cwd, epicId, { limit });
803
824
  return { turns };
804
825
  }));
826
+ // Derived-at-read-time "PRDs queued vs inline edits" counter (no writer —
827
+ // reads the Epic's own live prds/ dir + its own claude session transcript).
828
+ ipcMain.handle('epicDelegationStats:get', validated(schemas.epicDelegationStatsGet, async ({ cwd, epicId, claudeSessionId }) => {
829
+ return computeEpicDelegationStats(cwd, epicId, claudeSessionId || null);
830
+ }));
805
831
  // Renderer Epic lifecycle trace (PRD 940) — append-only, no read/list IPC.
806
832
  // Kind allowlist enforced by schemas.auditLogAppend, not here.
807
833
  ipcMain.handle('auditLog:append', validated(schemas.auditLogAppend, async ({ kind, cwd, epicId, source }) => {
@@ -1149,9 +1175,24 @@ app.whenReady().then(async () => {
1149
1175
  // First-boot default: install the bundled session-manager-dev plugin (its 10
1150
1176
  // dev skills) from the app's own marketplace. One-shot + idempotent; never
1151
1177
  // throws. SM_SEED_DEV_PLUGIN_DISABLE=1 to opt out.
1152
- seedDevPlugin({ logger: console }).catch((e) => {
1178
+ seedDevPlugin({ logger: console, writeLog: logs.writeLine }).catch((e) => {
1153
1179
  logs.writeLine({ scope: 'seed-dev-plugin', level: 'error', message: 'seed failed', meta: { error: e?.message } });
1154
1180
  });
1181
+ // First-boot default: seed the bundled Architect + Dev Lead Agent personas
1182
+ // into ~/.claude/agents/ so the Agent Library isn't empty on a fresh
1183
+ // install. One-shot + idempotent; never overwrites an existing persona;
1184
+ // never throws. SM_SEED_AGENT_PERSONAS_DISABLE=1 to opt out.
1185
+ seedAgentPersonas({ logger: console, writeLog: logs.writeLine }).catch((e) => {
1186
+ logs.writeLine({ scope: 'seed-agent-personas', level: 'error', message: 'seed failed', meta: { error: e?.message } });
1187
+ });
1188
+ // First-boot default: register the session-manager-scheduler MCP server at
1189
+ // USER scope so scheduler_create_prd is available in every project, not
1190
+ // just this repo's own .mcp.json. One-shot + idempotent; never overwrites
1191
+ // an existing registration; never throws. SM_SEED_SCHEDULER_MCP_DISABLE=1
1192
+ // to opt out.
1193
+ seedSchedulerMcp({ logger: console, writeLog: logs.writeLine }).catch((e) => {
1194
+ logs.writeLine({ scope: 'seed-scheduler-mcp', level: 'error', message: 'seed failed', meta: { error: e?.message } });
1195
+ });
1155
1196
  // History rollup finalize pass: deferred 30s past boot so it never competes
1156
1197
  // with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
1157
1198
  // is just the one-shot in-app pass).
@@ -537,6 +537,17 @@ const projectBriefCwd = z.object({
537
537
  cwd: z.string().min(1).max(4096),
538
538
  }).strict();
539
539
 
540
+ // ──────────────────────────────────────────── Delegation readiness
541
+ const delegationReadinessCwd = z.object({
542
+ cwd: z.string().min(1).max(4096),
543
+ }).strict();
544
+
545
+ // ──────────────────────────────────────────── Agent persona body (New Epic AIM)
546
+ const agentsGetPersonaBody = z.object({
547
+ cwd: z.string().min(1).max(4096),
548
+ name: z.string().min(1).max(200),
549
+ }).strict();
550
+
540
551
  // ──────────────────────────────────────────── Project Pages (PRD 932)
541
552
  // Same validation split as projectBriefCwd above: real path validation is
542
553
  // config.cjs's validatePath at first fs access in projectPages.cjs.
@@ -608,6 +619,17 @@ const promptSessionTranscriptRead = z.object({
608
619
  limit: z.number().int().positive().max(10_000).optional(),
609
620
  }).strict();
610
621
 
622
+ // ──────────────────────────────────────────── Epic delegation stats
623
+ // (derived-at-read-time "PRDs queued vs inline edits" counter surfaced on
624
+ // the Epic card). claudeSessionId is optional — a proposed Epic that never
625
+ // started chatting has none, and the computation just reports 0 inline
626
+ // edits rather than throwing.
627
+ const epicDelegationStatsGet = z.object({
628
+ cwd: z.string().min(1).max(4096),
629
+ epicId: z.string().regex(PROMPT_SESSION_EPIC_ID_RE),
630
+ claudeSessionId: z.string().max(128).optional().nullable(),
631
+ }).strict();
632
+
611
633
  // ──────────────────────────────────────────── Audit log (renderer Epic
612
634
  // lifecycle trace, PRD 940). Kind allowlist is enforced HERE, main-side — the
613
635
  // renderer cannot write arbitrary kinds through this channel, only the six
@@ -949,6 +971,7 @@ module.exports = {
949
971
  projectBriefUpdate,
950
972
  promptSessionTranscriptAppend,
951
973
  promptSessionTranscriptRead,
974
+ epicDelegationStatsGet,
952
975
  promptSessionsMergeActiveIndex,
953
976
  promptSessionsCreateEpic,
954
977
  promptSessionsCreateWorktree,
@@ -974,6 +997,8 @@ module.exports = {
974
997
  chatExternalSend,
975
998
  exchangesList,
976
999
  agentsSavePersona,
1000
+ agentsGetPersonaBody,
1001
+ delegationReadinessCwd,
977
1002
  },
978
1003
  validated,
979
1004
  };
@@ -0,0 +1,185 @@
1
+ /**
2
+ * delegationReadiness.test.cjs — unit tests for the "can this project
3
+ * actually delegate?" probe. Uses a temp HOME and a temp cwd so the real
4
+ * ~/.claude and this repo's own config are never touched.
5
+ *
6
+ * Run: timeout 300 npx vitest run src/main/lib/__tests__/delegationReadiness.test.cjs
7
+ */
8
+
9
+ import { test, expect, afterEach } from 'vitest';
10
+ const fs = require('node:fs');
11
+ const fsp = require('node:fs/promises');
12
+ const os = require('node:os');
13
+ const path = require('node:path');
14
+ const { checkDelegationReadiness } = require('../delegationReadiness.cjs');
15
+
16
+ const tmpDirs = [];
17
+ afterEach(async () => {
18
+ while (tmpDirs.length) {
19
+ const d = tmpDirs.pop();
20
+ await fsp.rm(d, { recursive: true, force: true });
21
+ }
22
+ });
23
+
24
+ async function mkTmp(prefix) {
25
+ const d = await fsp.mkdtemp(path.join(os.tmpdir(), prefix));
26
+ tmpDirs.push(d);
27
+ return d;
28
+ }
29
+
30
+ async function writeJson(absPath, value) {
31
+ await fsp.mkdir(path.dirname(absPath), { recursive: true });
32
+ await fsp.writeFile(absPath, JSON.stringify(value), 'utf8');
33
+ }
34
+
35
+ async function makeGreenFixtures() {
36
+ const homeDir = await mkTmp('sm-delegation-home-');
37
+ const cwd = await mkTmp('sm-delegation-cwd-');
38
+
39
+ await writeJson(path.join(homeDir, '.claude.json'), {
40
+ mcpServers: { 'session-manager-scheduler': { type: 'stdio', command: 'node', args: [] } },
41
+ });
42
+ await writeJson(path.join(homeDir, '.claude', 'settings.json'), {
43
+ enabledPlugins: { 'session-manager-dev@session-manager': true },
44
+ });
45
+ await fsp.mkdir(path.join(homeDir, '.claude', 'agents'), { recursive: true });
46
+ await fsp.writeFile(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'), '# dev-lead', 'utf8');
47
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
48
+ hooks: {
49
+ PreToolUse: [
50
+ {
51
+ matcher: 'Write|Edit|NotebookEdit',
52
+ hooks: [{ type: 'command', command: 'node scripts/hooks/guard-prd-writes.cjs' }],
53
+ },
54
+ ],
55
+ },
56
+ });
57
+
58
+ return { homeDir, cwd };
59
+ }
60
+
61
+ test('all four checks pass on a fully-configured project', async () => {
62
+ const { homeDir, cwd } = await makeGreenFixtures();
63
+ const result = checkDelegationReadiness({ cwd, homeDir });
64
+
65
+ expect(result.ok).toBe(true);
66
+ expect(result.checks).toHaveLength(4);
67
+ expect(result.checks.every((c) => c.ok)).toBe(true);
68
+ expect(result.checks.map((c) => c.id)).toEqual([
69
+ 'scheduler-mcp',
70
+ 'dev-plugin',
71
+ 'agent-personas',
72
+ 'prd-write-guard',
73
+ ]);
74
+ });
75
+
76
+ test('scheduler-mcp: passes via project-scope .mcp.json even without user scope', async () => {
77
+ const { homeDir, cwd } = await makeGreenFixtures();
78
+ await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
79
+ await writeJson(path.join(cwd, '.mcp.json'), {
80
+ mcpServers: { 'session-manager-scheduler': { command: 'node', args: [] } },
81
+ });
82
+
83
+ const result = checkDelegationReadiness({ cwd, homeDir });
84
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp');
85
+ expect(check.ok).toBe(true);
86
+ expect(result.ok).toBe(true);
87
+ });
88
+
89
+ test('scheduler-mcp: fails with a runnable fix when absent from both scopes', async () => {
90
+ const { homeDir, cwd } = await makeGreenFixtures();
91
+ await writeJson(path.join(homeDir, '.claude.json'), { mcpServers: {} });
92
+
93
+ const result = checkDelegationReadiness({ cwd, homeDir });
94
+ const check = result.checks.find((c) => c.id === 'scheduler-mcp');
95
+ expect(check.ok).toBe(false);
96
+ expect(result.ok).toBe(false);
97
+ expect(check.fix).toMatch(/^claude mcp add session-manager-scheduler --scope user/);
98
+ expect(check.fix).toContain('scheduler-mcp-server.cjs');
99
+ });
100
+
101
+ test('dev-plugin: fails independently when enabledPlugins is missing the key', async () => {
102
+ const { homeDir, cwd } = await makeGreenFixtures();
103
+ await writeJson(path.join(homeDir, '.claude', 'settings.json'), { enabledPlugins: {} });
104
+
105
+ const result = checkDelegationReadiness({ cwd, homeDir });
106
+ const check = result.checks.find((c) => c.id === 'dev-plugin');
107
+ expect(check.ok).toBe(false);
108
+ expect(result.ok).toBe(false);
109
+ expect(check.fix).toBeTruthy();
110
+ // the other three checks still pass independently
111
+ expect(result.checks.filter((c) => c.id !== 'dev-plugin').every((c) => c.ok)).toBe(true);
112
+ });
113
+
114
+ test('agent-personas: fails independently when ~/.claude/agents has no personas', async () => {
115
+ const { homeDir, cwd } = await makeGreenFixtures();
116
+ await fsp.rm(path.join(homeDir, '.claude', 'agents', 'dev-lead.md'));
117
+
118
+ const result = checkDelegationReadiness({ cwd, homeDir });
119
+ const check = result.checks.find((c) => c.id === 'agent-personas');
120
+ expect(check.ok).toBe(false);
121
+ expect(result.ok).toBe(false);
122
+ expect(result.checks.filter((c) => c.id !== 'agent-personas').every((c) => c.ok)).toBe(true);
123
+ });
124
+
125
+ test('agent-personas: passes via project-scope .claude/agents/ even without global personas', async () => {
126
+ const { homeDir, cwd } = await makeGreenFixtures();
127
+ await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
128
+ await fsp.mkdir(path.join(cwd, '.claude', 'agents'), { recursive: true });
129
+ await fsp.writeFile(path.join(cwd, '.claude', 'agents', 'builder.md'), '# builder', 'utf8');
130
+
131
+ const result = checkDelegationReadiness({ cwd, homeDir });
132
+ const check = result.checks.find((c) => c.id === 'agent-personas');
133
+ expect(check.ok).toBe(true);
134
+ expect(result.ok).toBe(true);
135
+ });
136
+
137
+ test('agent-personas: fails when the directory does not exist at all', async () => {
138
+ const { homeDir, cwd } = await makeGreenFixtures();
139
+ await fsp.rm(path.join(homeDir, '.claude', 'agents'), { recursive: true, force: true });
140
+
141
+ const result = checkDelegationReadiness({ cwd, homeDir });
142
+ const check = result.checks.find((c) => c.id === 'agent-personas');
143
+ expect(check.ok).toBe(false);
144
+ });
145
+
146
+ test('prd-write-guard: fails independently when the hook is missing', async () => {
147
+ const { homeDir, cwd } = await makeGreenFixtures();
148
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), { hooks: { PreToolUse: [] } });
149
+
150
+ const result = checkDelegationReadiness({ cwd, homeDir });
151
+ const check = result.checks.find((c) => c.id === 'prd-write-guard');
152
+ expect(check.ok).toBe(false);
153
+ expect(result.ok).toBe(false);
154
+ expect(result.checks.filter((c) => c.id !== 'prd-write-guard').every((c) => c.ok)).toBe(true);
155
+ });
156
+
157
+ test('unparseable JSON files yield ok:false with a detail, never a throw', async () => {
158
+ const { homeDir, cwd } = await makeGreenFixtures();
159
+ await fs.promises.writeFile(path.join(homeDir, '.claude.json'), '{ not valid json', 'utf8');
160
+ await fs.promises.writeFile(path.join(homeDir, '.claude', 'settings.json'), '{ not valid json', 'utf8');
161
+ await fs.promises.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
162
+
163
+ expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
164
+ const result = checkDelegationReadiness({ cwd, homeDir });
165
+
166
+ const scheduler = result.checks.find((c) => c.id === 'scheduler-mcp');
167
+ const devPlugin = result.checks.find((c) => c.id === 'dev-plugin');
168
+ const guard = result.checks.find((c) => c.id === 'prd-write-guard');
169
+ expect(scheduler.ok).toBe(false);
170
+ expect(scheduler.detail).toBeTruthy();
171
+ expect(devPlugin.ok).toBe(false);
172
+ expect(devPlugin.detail).toBeTruthy();
173
+ expect(guard.ok).toBe(false);
174
+ expect(guard.detail).toBeTruthy();
175
+ expect(result.ok).toBe(false);
176
+ });
177
+
178
+ test('missing cwd/.claude/settings.json entirely is treated as guard-absent, not a throw', async () => {
179
+ const { homeDir, cwd } = await makeGreenFixtures();
180
+ await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
181
+
182
+ expect(() => checkDelegationReadiness({ cwd, homeDir })).not.toThrow();
183
+ const result = checkDelegationReadiness({ cwd, homeDir });
184
+ expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(false);
185
+ });