claude-code-session-manager 0.74.0 → 0.75.1

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 (112) hide show
  1. package/dist/assets/AgentLibrary-CGplUKxY.js +3 -0
  2. package/dist/assets/DataModel-DQ15KxVb.js +1 -0
  3. package/dist/assets/{History-ShwkUTny.js → History-v2TSC_6A.js} +2 -2
  4. package/dist/assets/{Hooks-Dc5y1rZH.js → Hooks-Co7s6BNB.js} +2 -2
  5. package/dist/assets/HostBilko-DJj8FWPK.js +1 -0
  6. package/dist/assets/{Library-BOL6GqFX.js → Library-DQaC9VQ1.js} +1 -1
  7. package/dist/assets/ListDetail-BwFRfAZm.js +1 -0
  8. package/dist/assets/MarkdownEditor-C155gNpc.js +1 -0
  9. package/dist/assets/{McpServers-DuOTxmc4.js → McpServers-BmLgE_gS.js} +2 -2
  10. package/dist/assets/{Memory-BYJHDz7W.js → Memory-ISwNcS5j.js} +4 -4
  11. package/dist/assets/Panel-1adjEpj-.js +1 -0
  12. package/dist/assets/{Permissions-DhYERDVR.js → Permissions-FrCz5cGa.js} +3 -3
  13. package/dist/assets/Plugins-zUssKp_b.js +2 -0
  14. package/dist/assets/ProvenanceBadge-CAXqA_HX.js +1 -0
  15. package/dist/assets/SaveBar-D9cqms3Y.js +1 -0
  16. package/dist/assets/{Scheduler-Bu6kSkl3.js → Scheduler-dNNc9S11.js} +6 -6
  17. package/dist/assets/ScopeSwitcher-1wBTne7S.js +1 -0
  18. package/dist/assets/Settings-BT_expIg.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-rKaTHmOg.js → SkillReferenceGraph-CVccq4Cu.js} +1 -1
  20. package/dist/assets/{Skills-BHlH2uG5.js → Skills-xFpRY0ld.js} +2 -2
  21. package/dist/assets/SystemPrompt-CEk_71th.js +1 -0
  22. package/dist/assets/TagLibrary-C2zhihQG.js +1 -0
  23. package/dist/assets/{TiptapBody-XoQHiqfF.js → TiptapBody-CQKcHKaD.js} +1 -1
  24. package/dist/assets/Toggle-CQ-IBb8m.js +1 -0
  25. package/dist/assets/{index-Cs_Lq9-w.js → index-BgbV3sKX.js} +659 -655
  26. package/dist/assets/{index-D9C8N3pn.css → index-Dbo9R4e0.css} +1 -1
  27. package/dist/assets/{settingsSchema-BTDJcu2x.js → settingsSchema-D6ccSKqm.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__/scheduler-autofix-outcome.test.cjs +116 -0
  32. package/src/main/__tests__/scheduler-autofix-select.test.cjs +113 -7
  33. package/src/main/__tests__/scheduler-fix-plan-path.test.cjs +119 -0
  34. package/src/main/__tests__/seedAgentPersonas.test.cjs +105 -0
  35. package/src/main/__tests__/seedSchedulerMcp.test.cjs +145 -0
  36. package/src/main/__tests__/seedStatus.test.cjs +100 -0
  37. package/src/main/agentLibrary.cjs +43 -0
  38. package/src/main/index.cjs +42 -1
  39. package/src/main/ipcSchemas.cjs +25 -0
  40. package/src/main/lib/__tests__/delegationReadiness.test.cjs +185 -0
  41. package/src/main/lib/__tests__/epicDelegationStats.test.cjs +137 -0
  42. package/src/main/lib/delegationReadiness.cjs +141 -0
  43. package/src/main/lib/epicDelegationStats.cjs +124 -0
  44. package/src/main/lib/promptSessionSchema.cjs +1 -1
  45. package/src/main/scheduler.cjs +136 -27
  46. package/src/main/seedAgentPersonas.cjs +92 -0
  47. package/src/main/seedDevPlugin.cjs +14 -2
  48. package/src/main/seedSchedulerMcp.cjs +148 -0
  49. package/src/main/seedStatus.cjs +59 -0
  50. package/src/preload/api.d.ts +40 -1
  51. package/src/preload/index.cjs +7 -0
  52. package/src/seed/agents/architect.md +64 -0
  53. package/src/seed/agents/dev-lead.md +42 -0
  54. package/src/seed/prompts/code-review/ac-coverage-check.md +8 -0
  55. package/src/seed/prompts/code-review/correctness-only.md +8 -0
  56. package/src/seed/prompts/code-review/full-spectrum-high.md +8 -0
  57. package/src/seed/prompts/code-review/hallucination-check.md +8 -0
  58. package/src/seed/prompts/code-review/public-api-compat.md +8 -0
  59. package/src/seed/prompts/code-review/readability-naming.md +8 -0
  60. package/src/seed/prompts/debugging/bug-as-failing-test.md +8 -0
  61. package/src/seed/prompts/debugging/git-bisect-regression.md +8 -0
  62. package/src/seed/prompts/debugging/instrument-intermittent-bug.md +8 -0
  63. package/src/seed/prompts/debugging/localize-pipeline-failure.md +8 -0
  64. package/src/seed/prompts/debugging/reproduce-then-diagnose.md +8 -0
  65. package/src/seed/prompts/documentation/adr-from-change.md +8 -0
  66. package/src/seed/prompts/documentation/module-readme.md +8 -0
  67. package/src/seed/prompts/documentation/onboarding-plan.md +8 -0
  68. package/src/seed/prompts/documentation/refresh-claude-md.md +8 -0
  69. package/src/seed/prompts/documentation/tsdoc-public-exports.md +8 -0
  70. package/src/seed/prompts/git-pr/conventional-commit.md +8 -0
  71. package/src/seed/prompts/git-pr/draft-pr-title-body.md +8 -0
  72. package/src/seed/prompts/git-pr/pre-commit-safety-sweep.md +8 -0
  73. package/src/seed/prompts/git-pr/release-notes-block.md +8 -0
  74. package/src/seed/prompts/git-pr/split-large-pr.md +8 -0
  75. package/src/seed/prompts/performance/bundle-startup-audit.md +8 -0
  76. package/src/seed/prompts/performance/complexity-audit.md +8 -0
  77. package/src/seed/prompts/performance/cpu-profile-hot-path.md +8 -0
  78. package/src/seed/prompts/performance/db-query-plan-review.md +8 -0
  79. package/src/seed/prompts/performance/memory-leak-hunt.md +8 -0
  80. package/src/seed/prompts/qa/api-contract-tests.md +8 -0
  81. package/src/seed/prompts/qa/e2e-critical-path.md +8 -0
  82. package/src/seed/prompts/qa/failing-test-for-bug.md +8 -0
  83. package/src/seed/prompts/qa/find-missing-test-coverage.md +8 -0
  84. package/src/seed/prompts/qa/stabilize-flaky-test.md +8 -0
  85. package/src/seed/prompts/qa/tdd-red-first.md +8 -0
  86. package/src/seed/prompts/qa/visual-regression-review.md +8 -0
  87. package/src/seed/prompts/qa/wcag-axe-scan.md +8 -0
  88. package/src/seed/prompts/refactoring/dead-code-sweep.md +8 -0
  89. package/src/seed/prompts/refactoring/extract-duplicated-pattern.md +8 -0
  90. package/src/seed/prompts/refactoring/modernize-legacy-file.md +8 -0
  91. package/src/seed/prompts/refactoring/reduce-cyclomatic-complexity.md +8 -0
  92. package/src/seed/prompts/refactoring/tighten-module-boundaries.md +8 -0
  93. package/src/seed/prompts/security/authz-audit.md +8 -0
  94. package/src/seed/prompts/security/crypto-correctness.md +8 -0
  95. package/src/seed/prompts/security/cwe-top-25-hunt.md +8 -0
  96. package/src/seed/prompts/security/dependency-audit.md +8 -0
  97. package/src/seed/prompts/security/ipc-boundary-hardening.md +8 -0
  98. package/src/seed/prompts/security/owasp-top-10-staged-diff.md +8 -0
  99. package/src/seed/prompts/security/secret-credential-scan.md +8 -0
  100. package/dist/assets/AgentLibrary-CSnWV6uv.js +0 -3
  101. package/dist/assets/HostBilko-evd1hD8q.js +0 -1
  102. package/dist/assets/ListDetail-BPJb2et3.js +0 -1
  103. package/dist/assets/MarkdownEditor-CACsHVuf.js +0 -1
  104. package/dist/assets/Panel-n-p8OQLc.js +0 -1
  105. package/dist/assets/Plugins-D1ckVgo3.js +0 -2
  106. package/dist/assets/ProvenanceBadge-ByTjDwAX.js +0 -1
  107. package/dist/assets/SaveBar-_ORUCmZw.js +0 -1
  108. package/dist/assets/ScopeSwitcher-Css6Sg7Y.js +0 -1
  109. package/dist/assets/Settings-DlyJjexf.js +0 -3
  110. package/dist/assets/SystemPrompt-DsgF8xsl.js +0 -1
  111. package/dist/assets/TagLibrary-Q5QsKMes.js +0 -1
  112. package/dist/assets/Toggle-BJHhHQOq.js +0 -1
@@ -11,7 +11,7 @@ const assert = require('node:assert/strict');
11
11
  const {
12
12
  selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate,
13
13
  healTargetForFix, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH,
14
- isEligibleForImmediateAutoFix,
14
+ isEligibleForImmediateAutoFix, isExhaustedAutoFix, isPlanUnqueued, fixSlugFor,
15
15
  } = require('../scheduler.cjs');
16
16
 
17
17
  const noSiblingOnDisk = () => false;
@@ -34,8 +34,8 @@ test('selects a fresh needs_review job', () => {
34
34
  assert.strictEqual(result[0].slug, '05-my-feature');
35
35
  });
36
36
 
37
- test('excludes job with autoFixAttempted: true', () => {
38
- const jobs = [makeJob({ autoFixAttempted: true })];
37
+ test('excludes job with autoFixAttempted: true whose retry is already spent', () => {
38
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 1 })];
39
39
  const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
40
40
  assert.strictEqual(result.length, 0);
41
41
  });
@@ -105,12 +105,112 @@ test('autoFixOutcome no-plan with retries=1 is exhausted (not selected)', () =>
105
105
  assert.strictEqual(result.length, 0);
106
106
  });
107
107
 
108
- test('autoFixAttempted true with no outcome recorded stays excluded (existing 1-attempt cap)', () => {
109
- const jobs = [makeJob({ autoFixAttempted: true })];
108
+ // ---- PRD: unstamped/error outcomes must retry once, not dead-end forever ----
109
+
110
+ test('autoFixAttempted true with NO outcome recorded (unstamped — spawn threw before onExit wired) is retry-eligible once', () => {
111
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 0 })];
112
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
113
+ assert.strictEqual(result.length, 1);
114
+ });
115
+
116
+ test('autoFixAttempted true with no outcome AND retries=1 is exhausted (not selected)', () => {
117
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixRetries: 1 })];
110
118
  const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
111
119
  assert.strictEqual(result.length, 0);
112
120
  });
113
121
 
122
+ test('autoFixOutcome error with retries=0 is retry-eligible', () => {
123
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 0 })];
124
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
125
+ assert.strictEqual(result.length, 1);
126
+ });
127
+
128
+ test('autoFixOutcome error with retries=1 is exhausted (not selected)', () => {
129
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 })];
130
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
131
+ assert.strictEqual(result.length, 0);
132
+ });
133
+
134
+ test('autoFixOutcome plan is NEVER retried, regardless of autoFixRetries', () => {
135
+ const jobs = [makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 0 })];
136
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
137
+ assert.strictEqual(result.length, 0);
138
+ });
139
+
140
+ // ---- isExhaustedAutoFix / isPlanUnqueued (reverifyNeedsReview annotation) ----
141
+
142
+ test('isExhaustedAutoFix: no-plan outcome with retries spent → true', () => {
143
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 1 });
144
+ assert.strictEqual(isExhaustedAutoFix(job), true);
145
+ });
146
+
147
+ test('isExhaustedAutoFix: error outcome with retries spent → true', () => {
148
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'error', autoFixRetries: 1 });
149
+ assert.strictEqual(isExhaustedAutoFix(job), true);
150
+ });
151
+
152
+ test('isExhaustedAutoFix: unstamped outcome with retries spent → true', () => {
153
+ const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
154
+ assert.strictEqual(isExhaustedAutoFix(job), true);
155
+ });
156
+
157
+ test('isExhaustedAutoFix: retries not yet spent → false (still eligible for its one retry)', () => {
158
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan', autoFixRetries: 0 });
159
+ assert.strictEqual(isExhaustedAutoFix(job), false);
160
+ });
161
+
162
+ test('isExhaustedAutoFix: plan outcome is never "exhausted" here — isPlanUnqueued\'s job instead', () => {
163
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan', autoFixRetries: 1 });
164
+ assert.strictEqual(isExhaustedAutoFix(job), false);
165
+ });
166
+
167
+ test('isPlanUnqueued: plan outcome, fix slug absent from queue → true', () => {
168
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
169
+ assert.strictEqual(isPlanUnqueued(job, new Set()), true);
170
+ });
171
+
172
+ test('isPlanUnqueued: plan outcome, fix slug present in queue → false', () => {
173
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
174
+ assert.strictEqual(isPlanUnqueued(job, new Set([fixSlugFor(job)])), false);
175
+ });
176
+
177
+ test('isPlanUnqueued: no-plan outcome is never "plan unqueued"', () => {
178
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'no-plan' });
179
+ assert.strictEqual(isPlanUnqueued(job, new Set()), false);
180
+ });
181
+
182
+ // ---- invariant: every needs_review job is either an auto-fix target, or
183
+ // classified by one of the annotation predicates (i.e. would receive a
184
+ // non-null verifierVerdict) — no job may fall through both. ----
185
+
186
+ test('invariant: attempted+no-outcome, retries spent → not a target, but IS annotatable', () => {
187
+ const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
188
+ const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
189
+ const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, new Set())
190
+ || isUnresolvableNeedsReview(job, { hasRunDir: true });
191
+ assert.strictEqual(isTarget, false);
192
+ assert.strictEqual(isAnnotatable, true);
193
+ });
194
+
195
+ test('invariant: plan outcome, no queue row → not a target, but IS annotatable (autofix_plan_unqueued)', () => {
196
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
197
+ const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
198
+ const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, new Set())
199
+ || isUnresolvableNeedsReview(job, { hasRunDir: true });
200
+ assert.strictEqual(isTarget, false);
201
+ assert.strictEqual(isAnnotatable, true);
202
+ });
203
+
204
+ test('invariant: plan outcome WITH a queue row → not a target and NOT annotated (waiting on the fix-plan job itself)', () => {
205
+ const job = makeJob({ autoFixAttempted: true, autoFixOutcome: 'plan' });
206
+ const queuedSlugs = new Set([fixSlugFor(job)]);
207
+ const isTarget = selectAutoFixTargets([job], { fixSlugExists: noSiblingOnDisk }).length === 1;
208
+ const isAnnotatable = isExhaustedAutoFix(job) || isPlanUnqueued(job, queuedSlugs)
209
+ || isUnresolvableNeedsReview(job, { hasRunDir: true });
210
+ assert.strictEqual(isTarget, false);
211
+ assert.strictEqual(isAnnotatable, false);
212
+ });
213
+
114
214
  test('excludes when fix sibling exists on disk', () => {
115
215
  const jobs = [makeJob()];
116
216
  // fixSlug = '05-fix-my-feature'
@@ -224,12 +324,18 @@ test('isEligibleForImmediateAutoFix: fresh needs_review job (fresh from spawnJob
224
324
  assert.strictEqual(result, true);
225
325
  });
226
326
 
227
- test('isEligibleForImmediateAutoFix: job already autoFixAttempted is excluded (prevents double-spawn with the periodic pass)', () => {
228
- const job = makeJob({ autoFixAttempted: true });
327
+ test('isEligibleForImmediateAutoFix: attempted job with retry already spent is excluded (prevents double-spawn with the periodic pass)', () => {
328
+ const job = makeJob({ autoFixAttempted: true, autoFixRetries: 1 });
229
329
  const result = isEligibleForImmediateAutoFix(job, [job], noSiblingOnDisk);
230
330
  assert.strictEqual(result, false);
231
331
  });
232
332
 
333
+ test('isEligibleForImmediateAutoFix: attempted job with an unstamped outcome and its retry still unspent IS eligible (bounded once)', () => {
334
+ const job = makeJob({ autoFixAttempted: true, autoFixRetries: 0 });
335
+ const result = isEligibleForImmediateAutoFix(job, [job], noSiblingOnDisk);
336
+ assert.strictEqual(result, true);
337
+ });
338
+
233
339
  test('isEligibleForImmediateAutoFix: fix sibling already on disk excludes the job', () => {
234
340
  const job = makeJob();
235
341
  // fixSlug = '05-fix-my-feature'
@@ -0,0 +1,119 @@
1
+ /**
2
+ * scheduler-fix-plan-path.test.cjs — regression test for the self-heal
3
+ * dead-letter bug: spawnInvestigation used to write every auto-fix plan into
4
+ * the RETIRED flat `<cwd>/session-manager-operations/scheduler/prds/` dir
5
+ * (prdDirForCwd), which reconcile()'s consolidateAllFlatPrds sweeps BEFORE
6
+ * scanning for queue rows — so a freshly-authored fix plan (no queue row yet
7
+ * by definition) was archived into `prds-archived/` on the very next pass
8
+ * and never enqueued. Confirmed live in starry-night-ships: job
9
+ * 105-arena-perf-fences produced a valid fix plan that landed in
10
+ * prds-archived/ having never appeared in queue.json or history.jsonl.
11
+ *
12
+ * The fix: resolveFixPlanPath resolves the write dir from the ORIGINAL job's
13
+ * live PRD location (findPrdDir, via resolveVerifyPrdPath) — normally the
14
+ * originating Epic's own `epics/<epicId>/prds/`, which consolidateFlatPrds
15
+ * never touches — falling back to the flat dir only when the original can't
16
+ * be found live anywhere on disk.
17
+ *
18
+ * Run: timeout 120 npx vitest run src/main/__tests__/scheduler-fix-plan-path.test.cjs
19
+ */
20
+
21
+ 'use strict';
22
+
23
+ import { test, expect, beforeAll, afterAll } from 'vitest';
24
+ const fs = require('node:fs');
25
+ const os = require('node:os');
26
+ const path = require('node:path');
27
+
28
+ let tmpHome;
29
+ let originalHome;
30
+ let resolveFixPlanPath;
31
+ let PRDS_DIR;
32
+ let consolidateFlatPrds;
33
+
34
+ beforeAll(() => {
35
+ originalHome = process.env.HOME;
36
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-fix-plan-path-'));
37
+ process.env.HOME = tmpHome;
38
+
39
+ // Load AFTER HOME is stubbed — scheduler.cjs snapshots os.homedir() into
40
+ // PRDS_DIR at module load, not lazily (see scheduler-find-prd-dir.test.cjs).
41
+ ({ resolveFixPlanPath, PRDS_DIR } = require('../scheduler.cjs'));
42
+ ({ consolidateFlatPrds } = require('../lib/prdMigration.cjs'));
43
+
44
+ if (!PRDS_DIR.startsWith(tmpHome)) {
45
+ throw new Error(`refusing to run: PRDS_DIR (${PRDS_DIR}) is not under the temp HOME (${tmpHome})`);
46
+ }
47
+
48
+ // allProjectCwds()/activeProjectCwds() (consumed by findPrdDir's candidate
49
+ // search) discover project cwds by scanning ~/.claude/projects/*/*.jsonl
50
+ // for a `cwd` field — fake one project transcript pointing at the real
51
+ // on-disk cwd used below (mirrors scheduler-verify-prd-path.test.cjs).
52
+ const projectsDir = path.join(tmpHome, '.claude', 'projects');
53
+ const projDir = path.join(projectsDir, 'fake-fix-plan-project-slug');
54
+ fs.mkdirSync(projDir, { recursive: true });
55
+ fs.writeFileSync(
56
+ path.join(projDir, 'session.jsonl'),
57
+ `${JSON.stringify({ cwd: projectCwd() })}\n`,
58
+ 'utf8',
59
+ );
60
+ });
61
+
62
+ afterAll(() => {
63
+ process.env.HOME = originalHome;
64
+ fs.rmSync(tmpHome, { recursive: true, force: true });
65
+ });
66
+
67
+ function projectCwd() {
68
+ return path.join(tmpHome, 'fix-plan-project-cwd');
69
+ }
70
+
71
+ test('resolveFixPlanPath resolves the fix plan into the SAME Epic-scoped dir as the original PRD, and it survives consolidateFlatPrds', async () => {
72
+ const cwd = projectCwd();
73
+ const epicId = 'test-epic-arena-perf';
74
+ const slug = `105-arena-perf-fences-${process.pid}`;
75
+ const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'epics', epicId, 'prds');
76
+ fs.mkdirSync(prdsDir, { recursive: true });
77
+ fs.writeFileSync(path.join(prdsDir, `${slug}.md`), '---\ntitle: arena perf fences\n---\n\n# Goal\n\ntest\n', 'utf8');
78
+
79
+ const failedJob = { slug, cwd, parallelGroup: 5 };
80
+ const resolved = await resolveFixPlanPath(failedJob);
81
+
82
+ const expectedFixSlug = '05-fix-arena-perf-fences-' + process.pid;
83
+ expect(resolved.fixSlug).toBe(expectedFixSlug);
84
+ expect(resolved.livePrdDir).toBe(prdsDir);
85
+ expect(resolved.fixPath).toBe(path.join(prdsDir, `${expectedFixSlug}.md`));
86
+
87
+ // A fix plan written at the resolved path must survive a consolidateFlatPrds
88
+ // pass over the SAME project cwd — the sweep only ever touches the flat dir.
89
+ fs.writeFileSync(resolved.fixPath, `---\ntitle: Fix: arena perf fences\ncwd: ${cwd}\nparallelGroup: 5\nestimateMinutes: 30\n---\n\nfix body\n`, 'utf8');
90
+ await consolidateFlatPrds(cwd);
91
+ expect(fs.existsSync(resolved.fixPath)).toBe(true);
92
+ });
93
+
94
+ test('resolveFixPlanPath falls back to the flat dir when the original PRD cannot be found live anywhere on disk', async () => {
95
+ const cwd = path.join(tmpHome, 'fix-plan-project-cwd-missing');
96
+ const slug = `999-nonexistent-${process.pid}`;
97
+ const failedJob = { slug, cwd, parallelGroup: 3 };
98
+
99
+ const resolved = await resolveFixPlanPath(failedJob);
100
+
101
+ expect(resolved.livePrdDir).toBe(null);
102
+ const flatDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
103
+ expect(resolved.fixPath).toBe(path.join(flatDir, `${resolved.fixSlug}.md`));
104
+ });
105
+
106
+ test('the OLD flat-dir path (regression guard) does NOT survive consolidateFlatPrds — proves the bug this fix addresses', async () => {
107
+ const cwd = path.join(tmpHome, 'fix-plan-project-cwd-old-behavior');
108
+ const flatDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
109
+ fs.mkdirSync(flatDir, { recursive: true });
110
+ const oldFixPath = path.join(flatDir, '07-fix-old-behavior.md');
111
+ fs.writeFileSync(oldFixPath, `---\ntitle: Fix: old behavior\ncwd: ${cwd}\nparallelGroup: 7\nestimateMinutes: 30\n---\n\nfix body\n`, 'utf8');
112
+
113
+ // No queue.json at all — the file has no live queue row, exactly the
114
+ // freshly-authored-fix-plan situation the bug hit.
115
+ await consolidateFlatPrds(cwd);
116
+
117
+ expect(fs.existsSync(oldFixPath)).toBe(false);
118
+ expect(fs.existsSync(path.join(cwd, 'session-manager-operations', 'scheduler', 'prds-archived', '07-fix-old-behavior.md'))).toBe(true);
119
+ });
@@ -0,0 +1,105 @@
1
+ /**
2
+ * seedAgentPersonas.test.cjs — unit tests for src/main/seedAgentPersonas.cjs.
3
+ *
4
+ * Run: timeout 120 npx vitest run src/main/__tests__/seedAgentPersonas.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 seedAgentPersonas;
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 seedAgentPersonas.cjs so each test's tmpHome takes effect.
21
+ const MODULES_TO_RELOAD = ['../seedAgentPersonas.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-agent-personas-test-'));
32
+ process.env.HOME = tmpHome;
33
+ delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
34
+ purgeRequireCache();
35
+ ({ seedAgentPersonas } = require('../seedAgentPersonas.cjs'));
36
+ });
37
+
38
+ afterEach(() => {
39
+ process.env.HOME = realHome;
40
+ delete process.env.SM_SEED_AGENT_PERSONAS_DISABLE;
41
+ fs.rmSync(tmpHome, { recursive: true, force: true });
42
+ purgeRequireCache();
43
+ });
44
+
45
+ function agentsDir() {
46
+ return path.join(tmpHome, '.claude', 'agents');
47
+ }
48
+
49
+ function markerPath() {
50
+ return path.join(tmpHome, '.claude', 'session-manager', '.agent-personas-seeded');
51
+ }
52
+
53
+ function silentLogger() {
54
+ return { log: () => {}, warn: () => {} };
55
+ }
56
+
57
+ test('fresh seed writes both persona files', async () => {
58
+ await seedAgentPersonas({ logger: silentLogger() });
59
+
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);
64
+
65
+ const bundledArchitect = fs.readFileSync(
66
+ path.join(__dirname, '..', '..', 'seed', 'agents', 'architect.md'),
67
+ 'utf8'
68
+ );
69
+ expect(fs.readFileSync(architect, 'utf8')).toBe(bundledArchitect);
70
+
71
+ const marker = JSON.parse(fs.readFileSync(markerPath(), 'utf8'));
72
+ expect(marker.done).toBe(true);
73
+ });
74
+
75
+ test('a pre-existing persona file is left byte-identical', async () => {
76
+ fs.mkdirSync(agentsDir(), { recursive: true });
77
+ const architect = path.join(agentsDir(), 'architect.md');
78
+ const customContent = '# My custom architect\nnever overwrite me\n';
79
+ fs.writeFileSync(architect, customContent);
80
+
81
+ await seedAgentPersonas({ logger: silentLogger() });
82
+
83
+ expect(fs.readFileSync(architect, 'utf8')).toBe(customContent);
84
+ // dev-lead had no pre-existing file, so it should still be seeded.
85
+ expect(fs.existsSync(path.join(agentsDir(), 'dev-lead.md'))).toBe(true);
86
+ });
87
+
88
+ test('done:true marker short-circuits without touching agents dir', async () => {
89
+ const mPath = markerPath();
90
+ fs.mkdirSync(path.dirname(mPath), { recursive: true });
91
+ fs.writeFileSync(mPath, JSON.stringify({ done: true, attempts: 1, ts: '2026-01-01T00:00:00.000Z' }));
92
+
93
+ await seedAgentPersonas({ logger: silentLogger() });
94
+
95
+ expect(fs.existsSync(agentsDir())).toBe(false);
96
+ });
97
+
98
+ test('SM_SEED_AGENT_PERSONAS_DISABLE=1 short-circuits', async () => {
99
+ process.env.SM_SEED_AGENT_PERSONAS_DISABLE = '1';
100
+
101
+ await seedAgentPersonas({ logger: silentLogger() });
102
+
103
+ expect(fs.existsSync(agentsDir())).toBe(false);
104
+ expect(fs.existsSync(markerPath())).toBe(false);
105
+ });
@@ -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
+ }