claude-code-session-manager 0.75.3 → 0.77.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 (164) hide show
  1. package/dist/assets/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
  2. package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
  3. package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
  4. package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
  5. package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
  6. package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
  7. package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
  9. package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
  10. package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
  11. package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
  12. package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
  13. package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
  15. package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
  16. package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
  18. package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
  20. package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
  21. package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
  22. package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
  23. package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
  24. package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
  25. package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
  26. package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
  27. package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.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/lib/activeSessions.cjs +116 -6
  32. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  33. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  34. package/scripts/render-project-pages.cjs +70 -0
  35. package/scripts/scheduler-mcp-server.cjs +269 -96
  36. package/scripts/validate-project-pages-summary.cjs +62 -0
  37. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  38. package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
  39. package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
  40. package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
  41. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  42. package/src/main/__tests__/prdCreate.test.cjs +247 -0
  43. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  44. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  45. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  46. package/src/main/__tests__/projectPages.test.cjs +73 -1
  47. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  48. package/src/main/__tests__/runVerify.test.cjs +94 -0
  49. package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
  50. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  51. package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
  52. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  53. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  54. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  55. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  56. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  57. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  58. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  59. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  60. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  61. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
  62. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  63. package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
  64. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  65. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  66. package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
  67. package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
  68. package/src/main/bilkoHost.cjs +4 -3
  69. package/src/main/chatRunner.cjs +6 -1
  70. package/src/main/config.cjs +25 -33
  71. package/src/main/health.cjs +153 -2
  72. package/src/main/index.cjs +64 -5
  73. package/src/main/ipcSchemas.cjs +69 -1
  74. package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
  75. package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
  76. package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
  77. package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
  78. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
  79. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  80. package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
  81. package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
  82. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  83. package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
  84. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  85. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  86. package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
  87. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
  88. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  89. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
  90. package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
  91. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  92. package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
  93. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  94. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  95. package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
  96. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
  97. package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
  98. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  99. package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
  100. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  101. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  102. package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
  103. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  104. package/src/main/lib/activeIndexMerge.cjs +15 -0
  105. package/src/main/lib/activeIndexRebuild.cjs +133 -0
  106. package/src/main/lib/agentModelResolve.cjs +58 -0
  107. package/src/main/lib/buildTarget.cjs +3 -2
  108. package/src/main/lib/childWithLog.cjs +69 -2
  109. package/src/main/lib/claudeBin.cjs +54 -1
  110. package/src/main/lib/crossProjectFeedback.cjs +8 -1
  111. package/src/main/lib/definitionOfDone.cjs +3 -2
  112. package/src/main/lib/delegationReadiness.cjs +514 -26
  113. package/src/main/lib/ephemeralCwd.cjs +78 -0
  114. package/src/main/lib/epicDelegationStats.cjs +2 -1
  115. package/src/main/lib/epicMint.cjs +17 -1
  116. package/src/main/lib/epicStatusMirror.cjs +95 -0
  117. package/src/main/lib/epicValidationHook.cjs +2 -1
  118. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  119. package/src/main/lib/fixChainDepth.cjs +45 -0
  120. package/src/main/lib/gitWorktree.cjs +520 -21
  121. package/src/main/lib/jobWorktree.cjs +2 -0
  122. package/src/main/lib/landedSinceRun.cjs +55 -0
  123. package/src/main/lib/launchFailure.cjs +357 -0
  124. package/src/main/lib/loadGate.cjs +134 -0
  125. package/src/main/lib/mcpToolCatalog.cjs +370 -0
  126. package/src/main/lib/opsErrorLog.cjs +12 -1
  127. package/src/main/lib/opsOwnership.cjs +106 -0
  128. package/src/main/lib/prdAdminRoutes.cjs +43 -3
  129. package/src/main/lib/prdAgentType.cjs +84 -0
  130. package/src/main/lib/prdCreate.cjs +103 -15
  131. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  132. package/src/main/lib/prdFrontmatter.cjs +17 -3
  133. package/src/main/lib/prdLocations.cjs +13 -6
  134. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  135. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  136. package/src/main/lib/projectRootResolve.cjs +134 -0
  137. package/src/main/lib/promptSessionSchema.cjs +7 -0
  138. package/src/main/lib/queueHealth.cjs +38 -0
  139. package/src/main/lib/queueStore.cjs +40 -7
  140. package/src/main/lib/quietMachineLease.cjs +48 -0
  141. package/src/main/lib/rcaReport.cjs +54 -4
  142. package/src/main/lib/reaperHelpers.cjs +64 -1
  143. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  144. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  145. package/src/main/lib/schedulerBatch.cjs +301 -55
  146. package/src/main/lib/schedulerConfig.cjs +99 -0
  147. package/src/main/projectBrief.cjs +3 -2
  148. package/src/main/projectPages.cjs +162 -3
  149. package/src/main/promptSessionTranscript.cjs +0 -0
  150. package/src/main/pty.cjs +5 -0
  151. package/src/main/queueOps.cjs +15 -8
  152. package/src/main/runVerify.cjs +50 -9
  153. package/src/main/scheduler/prdParser.cjs +18 -1
  154. package/src/main/scheduler.cjs +1701 -130
  155. package/src/main/seedAgentPersonas.cjs +62 -21
  156. package/src/main/seedSchedulerMcp.cjs +58 -4
  157. package/src/main/templates/project-pages-catalog.json +741 -0
  158. package/src/main/templates/project-pages-pipeline.md +417 -0
  159. package/src/preload/api.d.ts +187 -3
  160. package/src/preload/index.cjs +9 -0
  161. package/src/seed/agents/project-home-builder.md +59 -0
  162. package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
  163. package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
  164. package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
@@ -0,0 +1,103 @@
1
+ /**
2
+ * prdAgentType.test.cjs — the PRD `agentType` FK (lib/prdAgentType.cjs):
3
+ * throw on write, report on read, mirroring agentModelResolve.cjs's/
4
+ * epicMint.cjs's established convention for the Epic-level agentType FK.
5
+ *
6
+ * Run: timeout 120 npx vitest run src/main/__tests__/prdAgentType.test.cjs
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ import { test, expect } from 'vitest';
12
+
13
+ const fs = require('node:fs');
14
+ const fsp = require('node:fs/promises');
15
+ const os = require('node:os');
16
+ const path = require('node:path');
17
+ const {
18
+ DEFAULT_PRD_AGENT_TYPE,
19
+ assertAgentTypeWritable,
20
+ reportDanglingAgentTypeOnce,
21
+ } = require('../lib/prdAgentType.cjs');
22
+ const { todayFile } = require('../lib/opsErrorLog.cjs');
23
+
24
+ async function mkTmpDir(prefix) {
25
+ return fsp.mkdtemp(path.join(os.tmpdir(), prefix));
26
+ }
27
+
28
+ test('DEFAULT_PRD_AGENT_TYPE is dev-lead', () => {
29
+ expect(DEFAULT_PRD_AGENT_TYPE).toBe('dev-lead');
30
+ });
31
+
32
+ test('assertAgentTypeWritable is a no-op when agentType is omitted', async () => {
33
+ await expect(assertAgentTypeWritable('/some/cwd', null)).resolves.toBeUndefined();
34
+ await expect(assertAgentTypeWritable('/some/cwd', undefined)).resolves.toBeUndefined();
35
+ await expect(assertAgentTypeWritable('/some/cwd', '')).resolves.toBeUndefined();
36
+ });
37
+
38
+ test('assertAgentTypeWritable resolves without throwing when a project-overlay persona file exists', async () => {
39
+ const cwd = await mkTmpDir('sm-prd-agenttype-');
40
+ try {
41
+ const agentsDir = path.join(cwd, '.claude', 'agents');
42
+ await fsp.mkdir(agentsDir, { recursive: true });
43
+ await fsp.writeFile(path.join(agentsDir, 'my-persona.md'), '---\nname: my-persona\n---\nbody\n', 'utf8');
44
+ await expect(assertAgentTypeWritable(cwd, 'my-persona')).resolves.toBeUndefined();
45
+ } finally {
46
+ await fsp.rm(cwd, { recursive: true, force: true });
47
+ }
48
+ });
49
+
50
+ test('assertAgentTypeWritable rejects an unknown persona name, naming available personas in the error', async () => {
51
+ const cwd = await mkTmpDir('sm-prd-agenttype-');
52
+ try {
53
+ await expect(
54
+ assertAgentTypeWritable(cwd, 'zzz-definitely-not-a-real-persona-12345'),
55
+ ).rejects.toThrow(/does not resolve to a readable persona file/);
56
+ await expect(
57
+ assertAgentTypeWritable(cwd, 'zzz-definitely-not-a-real-persona-12345'),
58
+ ).rejects.toThrow(/Available personas/);
59
+ } finally {
60
+ await fsp.rm(cwd, { recursive: true, force: true });
61
+ }
62
+ });
63
+
64
+ // READ side of the FK, mirroring agentModelResolve.test.cjs's sibling test
65
+ // for the Epic-level FK: verify against the REAL opsErrorLog write path
66
+ // (todayFile), not a mock — a plain mkdtemp() cwd is not "ephemeral"
67
+ // (ephemeralCwd.cjs only refuses os.tmpdir() itself / managed worktree
68
+ // roots), so appendError writes a real line here.
69
+ test('reportDanglingAgentTypeOnce logs once via opsErrorLog when the persona does not resolve, and never throws', async () => {
70
+ const cwd = await mkTmpDir('sm-prd-agenttype-log-');
71
+ try {
72
+ expect(() => {
73
+ reportDanglingAgentTypeOnce(cwd, 'ghost-persona-xyz', { personaExists: () => false });
74
+ reportDanglingAgentTypeOnce(cwd, 'ghost-persona-xyz', { personaExists: () => false });
75
+ }).not.toThrow();
76
+
77
+ const lines = fs.readFileSync(todayFile(cwd), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
78
+ const matches = lines.filter((l) => l.message.includes('ghost-persona-xyz'));
79
+ // Logged once despite two calls above — dedup key is (cwd, agentType).
80
+ expect(matches).toHaveLength(1);
81
+ expect(matches[0].level).toBe('warn');
82
+ expect(matches[0].scope).toBe('prdAgentType');
83
+ } finally {
84
+ await fsp.rm(cwd, { recursive: true, force: true });
85
+ }
86
+ });
87
+
88
+ test('reportDanglingAgentTypeOnce is a no-op when the persona resolves', async () => {
89
+ const cwd = await mkTmpDir('sm-prd-agenttype-log-');
90
+ try {
91
+ reportDanglingAgentTypeOnce(cwd, 'real-persona', { personaExists: () => true });
92
+ expect(fs.existsSync(todayFile(cwd))).toBe(false);
93
+ } finally {
94
+ await fsp.rm(cwd, { recursive: true, force: true });
95
+ }
96
+ });
97
+
98
+ test('reportDanglingAgentTypeOnce is a no-op when cwd or agentType is missing', () => {
99
+ expect(() => {
100
+ reportDanglingAgentTypeOnce(null, 'x', { personaExists: () => false });
101
+ reportDanglingAgentTypeOnce('/some/cwd-missing-agenttype', null, { personaExists: () => false });
102
+ }).not.toThrow();
103
+ });
@@ -15,6 +15,7 @@ const fsp = require('node:fs/promises');
15
15
  const os = require('node:os');
16
16
  const path = require('node:path');
17
17
  const http = require('node:http');
18
+ const { execFileSync } = require('node:child_process');
18
19
  const {
19
20
  deriveSlugFromTitle,
20
21
  buildPrdBody,
@@ -79,6 +80,23 @@ test('buildPrdBody emits required frontmatter keys and body sections in order',
79
80
  expect(body.includes('Lay out hot data contiguously')).toBeFalsy();
80
81
  });
81
82
 
83
+ test('buildPrdBody emits agentType: dev-lead by default when omitted (PRD 1114)', () => {
84
+ const body = buildPrdBody({
85
+ title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
86
+ acceptanceCriteria: ['a'], implementationNotes: 'n',
87
+ });
88
+ expect(body).toMatch(/^agentType: dev-lead$/m);
89
+ });
90
+
91
+ test('buildPrdBody emits the supplied agentType instead of the default', () => {
92
+ const body = buildPrdBody({
93
+ title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
94
+ acceptanceCriteria: ['a'], implementationNotes: 'n',
95
+ agentType: 'architect',
96
+ });
97
+ expect(body).toMatch(/^agentType: architect$/m);
98
+ });
99
+
82
100
  test('buildPrdBody omits parallelGroup frontmatter key when not supplied', () => {
83
101
  const body = buildPrdBody({
84
102
  title: 't', cwd: '~/x', estimateMinutes: 5, goal: 'g',
@@ -529,6 +547,56 @@ test('POST /admin/scheduler/create-prd with tag writes it into the created PRD f
529
547
  }
530
548
  });
531
549
 
550
+ // ──────────────────────────────────────────── PRD 1114: agentType (WHO
551
+ // executes a PRD) — throw on write for an unresolvable persona, default to
552
+ // dev-lead when omitted.
553
+
554
+ test('createPrd rejects an unknown agentType at write time, naming available personas, and writes no file', async () => {
555
+ const prdsDir = await mkTmpPrdsDir();
556
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
557
+ const writePrdSpy = vi.fn(remote.writePrd);
558
+ remote.writePrd = writePrdSpy;
559
+
560
+ const result = await createPrd(
561
+ validCreateBody({ agentType: 'zzz-definitely-not-a-real-persona-12345' }),
562
+ remote,
563
+ );
564
+
565
+ expect(result.ok).toBe(false);
566
+ expect(result.status).toBe(400);
567
+ expect(result.error).toMatch(/does not resolve to a readable persona file/);
568
+ expect(result.error).toMatch(/Available personas/);
569
+ expect(writePrdSpy).not.toHaveBeenCalled();
570
+ const entries = await fsp.readdir(prdsDir);
571
+ expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
572
+ });
573
+
574
+ test('createPrd accepts a caller-supplied agentType that resolves to a project-overlay persona file', async () => {
575
+ const prdsDir = await mkTmpPrdsDir();
576
+ const cwd = path.dirname(path.dirname(prdsDir)); // mkTmpPrdsDir's root (prdsDir = root/.claude/prds)
577
+ const agentsDir = path.join(cwd, '.claude', 'agents');
578
+ await fsp.mkdir(agentsDir, { recursive: true });
579
+ await fsp.writeFile(path.join(agentsDir, 'my-persona.md'), '---\nname: my-persona\n---\nbody\n', 'utf8');
580
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
581
+
582
+ const result = await createPrd(validCreateBody({ cwd, slug: 'with-agent-type', agentType: 'my-persona' }), remote);
583
+
584
+ expect(result.ok).toBe(true);
585
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
586
+ expect(written).toMatch(/^agentType: my-persona$/m);
587
+ });
588
+
589
+ test('createPrd defaults agentType to dev-lead when omitted, without any persona-existence check', async () => {
590
+ const prdsDir = await mkTmpPrdsDir();
591
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
592
+
593
+ const result = await createPrd(validCreateBody({ slug: 'no-agent-type-supplied' }), remote);
594
+
595
+ expect(result.ok).toBe(true);
596
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
597
+ expect(written).toMatch(/^agentType: dev-lead$/m);
598
+ });
599
+
532
600
  // ──────────────────────────────────────────── originClaudeSessionId fallback
533
601
  // (2026-08-01 fix: a PRD queued via the scheduler_create_prd MCP tool from
534
602
  // inside an Epic's own chat session, without an explicit sourcePromptId,
@@ -583,6 +651,185 @@ test('createPrd resolves sourcePromptId from originClaudeSessionId when the call
583
651
  });
584
652
 
585
653
  // Same real-homedir rationale as the two tests above.
654
+ // ──────────────────────────────────────────── worktree-cwd Epic-lookup
655
+ // hazard (PRD: projectRootResolve.cjs) — reproduces the live starry-night-
656
+ // ships shape: main's active-index.json has the Epic, a linked worktree's
657
+ // own (git-tracked, frozen-at-branch-time) copy does not. Passing the
658
+ // worktree's pwd as `cwd` must still resolve to main and join the Epic there.
659
+
660
+ function git(args, cwd) {
661
+ return execFileSync('git', args, { cwd, encoding: 'utf8' });
662
+ }
663
+
664
+ function initRepo(dir) {
665
+ fs.mkdirSync(dir, { recursive: true });
666
+ git(['init', '-q'], dir);
667
+ git(['config', 'user.email', 'test@example.com'], dir);
668
+ git(['config', 'user.name', 'Test'], dir);
669
+ fs.writeFileSync(path.join(dir, 'README.md'), 'hello\n', 'utf8');
670
+ git(['add', '-A'], dir);
671
+ git(['commit', '-q', '-m', 'initial'], dir);
672
+ }
673
+
674
+ test('createPrd resolves a worktree cwd to the main tree and joins the Epic whose row only exists in main\'s active-index (live starry-night-ships shape)', async () => {
675
+ const tmpRoot = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-worktree-cwd-hazard-'));
676
+ const mainRepo = path.join(tmpRoot, 'main');
677
+ const worktreeDir = path.join(tmpRoot, 'worktree', 'epic-1');
678
+ config.addAllowedRoot(mainRepo);
679
+ try {
680
+ initRepo(mainRepo);
681
+ fs.mkdirSync(path.dirname(worktreeDir), { recursive: true });
682
+ git(['worktree', 'add', '-b', 'sm-epic/epic-1', worktreeDir, 'HEAD'], mainRepo);
683
+
684
+ // Main's index has the Epic; the worktree's own copy (as checked out at
685
+ // branch time) does not — the exact live-repro shape.
686
+ await writeActiveIndexFixture(mainRepo, {
687
+ 'epic-1': { id: 'epic-1', claudeSessionId: 'claude-sess-epic-1', status: 'active' },
688
+ });
689
+
690
+ const prdsDir = path.join(mainRepo, 'session-manager-operations', 'scheduler', 'epics', 'epic-1', 'prds');
691
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
692
+ const receivedCwds = [];
693
+ const realAllocate = remote.allocateParallelGroup.bind(remote);
694
+ remote.allocateParallelGroup = async (cwd) => { receivedCwds.push(cwd); return realAllocate(cwd); };
695
+
696
+ const result = await createPrd(
697
+ validCreateBody({ cwd: worktreeDir, slug: 'joins-from-worktree', sourcePromptId: 'epic-1' }),
698
+ remote,
699
+ );
700
+
701
+ expect(result.ok).toBe(true);
702
+ // Resolved and passed to the remote as the MAIN tree, never the worktree.
703
+ expect(receivedCwds.every((c) => c === mainRepo)).toBe(true);
704
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
705
+ expect(written).toMatch(/sourcePromptId: epic-1/);
706
+ expect(written).toMatch(new RegExp(`cwd: ${mainRepo.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`));
707
+ } finally {
708
+ await fsp.rm(tmpRoot, { recursive: true, force: true });
709
+ }
710
+ });
711
+
712
+ test('createPrd omits cwd entirely and resolves the project purely from sourcePromptId (cwd optional inside an Epic session)', async () => {
713
+ const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-cwd-optional-epic-'));
714
+ config.addAllowedRoot(root);
715
+ // resolveProjectContext's cross-project scan (used when cwd is omitted
716
+ // entirely) is built on activeSessions.allProjectCwds — a scan of REAL
717
+ // ~/.claude/projects transcripts. In production this is always populated
718
+ // (the calling session itself opened `root` as a project), so seed a
719
+ // matching transcript here rather than the fixture root being invisible to
720
+ // the scan. Cleaned up in `finally`.
721
+ const transcriptDir = path.join(os.homedir(), '.claude', 'projects', `sm-cwd-optional-test-${Date.now()}`);
722
+ try {
723
+ await writeActiveIndexFixture(root, {
724
+ 'epic-cwd-optional': { id: 'epic-cwd-optional', claudeSessionId: 'claude-sess-cwd-optional', status: 'active' },
725
+ });
726
+ await fsp.mkdir(transcriptDir, { recursive: true });
727
+ await fsp.writeFile(path.join(transcriptDir, 'a.jsonl'), `${JSON.stringify({ cwd: root })}\n`, 'utf8');
728
+
729
+ const prdsDir = path.join(root, 'session-manager-operations', 'scheduler', 'epics', 'epic-cwd-optional', 'prds');
730
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
731
+
732
+ const body = validCreateBody({ slug: 'no-cwd-supplied', sourcePromptId: 'epic-cwd-optional' });
733
+ delete body.cwd;
734
+
735
+ const result = await createPrd(body, remote);
736
+
737
+ expect(result.ok).toBe(true);
738
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
739
+ expect(written).toMatch(/sourcePromptId: epic-cwd-optional/);
740
+ } finally {
741
+ await fsp.rm(transcriptDir, { recursive: true, force: true });
742
+ await fsp.rm(root, { recursive: true, force: true });
743
+ }
744
+ });
745
+
746
+ test('createPrd with neither cwd nor a resolvable Epic fails with a clear cwd-required error', async () => {
747
+ const remote = {
748
+ async allocateParallelGroup() { throw new Error('must not be called'); },
749
+ async readPrd() { throw new Error('must not be called'); },
750
+ async writePrd() { throw new Error('must not be called'); },
751
+ };
752
+ const body = validCreateBody();
753
+ delete body.cwd;
754
+
755
+ const result = await createPrd(body, remote);
756
+
757
+ expect(result.ok).toBe(false);
758
+ expect(result.status).toBe(400);
759
+ expect(result.error).toMatch(/cwd is required/);
760
+ });
761
+
762
+ // ──────────────────────────────────────────── PRD 1113: fix-chain depth guard
763
+ // at the authoring boundary. lib/prdCreate.cjs's createPrd() is the shared
764
+ // write path behind both scheduler_create_prd (MCP) and chat:create-prd
765
+ // (IPC) — enforcing here covers both callers with one check.
766
+
767
+ test('createPrd refuses a depth-2 fix-chain slug (fix-of-a-fix) with a distinct, human-escalation error and writes no file', async () => {
768
+ const prdsDir = await mkTmpPrdsDir();
769
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
770
+ const writePrdSpy = vi.fn(remote.writePrd);
771
+ remote.writePrd = writePrdSpy;
772
+
773
+ const result = await createPrd(validCreateBody({ slug: 'fix-fix-widget-frobnication' }), remote);
774
+
775
+ expect(result.ok).toBe(false);
776
+ expect(result.status).toBe(409);
777
+ // Distinct from the provenance/validation error text elsewhere in this
778
+ // file ('cwd rejected', 'invalid PRD payload', 'already exists') and
779
+ // explicitly states escalation, not another attempt, is correct.
780
+ expect(result.error).toMatch(/fix-chain depth cap/i);
781
+ expect(result.error).toMatch(/not the correct response/i);
782
+ expect(result.error).toMatch(/human/i);
783
+ expect(writePrdSpy).not.toHaveBeenCalled();
784
+ const entries = await fsp.readdir(prdsDir);
785
+ expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
786
+ });
787
+
788
+ test('createPrd refuses a depth-3 fix-chain slug the same way as depth-2', async () => {
789
+ const prdsDir = await mkTmpPrdsDir();
790
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
791
+
792
+ const result = await createPrd(validCreateBody({ slug: 'fix-fix-fix-widget-frobnication' }), remote);
793
+
794
+ expect(result.ok).toBe(false);
795
+ expect(result.status).toBe(409);
796
+ const entries = await fsp.readdir(prdsDir);
797
+ expect(entries.filter((f) => f.endsWith('.md')).length).toBe(0);
798
+ });
799
+
800
+ test('createPrd allows a depth-1 fix slug through the same code path that rejects depth 2 (matches MAX_INVESTIGATION_DEPTH=1)', async () => {
801
+ const prdsDir = await mkTmpPrdsDir();
802
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
803
+
804
+ const result = await createPrd(validCreateBody({ slug: 'fix-widget-frobnication' }), remote);
805
+
806
+ expect(result.ok).toBe(true);
807
+ expect(result.filename).toMatch(/^\d+-fix-widget-frobnication\.md$/);
808
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
809
+ expect(written).toMatch(/title: Add widget frobnication/);
810
+ });
811
+
812
+ test('createPrd is unaffected by the fix-chain guard for an ordinary slug with no fix chain at all', async () => {
813
+ const prdsDir = await mkTmpPrdsDir();
814
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
815
+
816
+ const result = await createPrd(validCreateBody(), remote);
817
+
818
+ expect(result.ok).toBe(true);
819
+ const written = await fsp.readFile(path.join(prdsDir, result.filename), 'utf8');
820
+ expect(written).toMatch(/title: Add widget frobnication/);
821
+ });
822
+
823
+ test('createPrd is not fooled by a slug where a later segment merely reads "fix" (false-positive bait)', async () => {
824
+ const prdsDir = await mkTmpPrdsDir();
825
+ const remote = makeFakeRemoteWithPrdsDir(prdsDir);
826
+
827
+ const result = await createPrd(validCreateBody({ slug: 'fix-loop-extraction-and-round-trip' }), remote);
828
+
829
+ expect(result.ok).toBe(true);
830
+ expect(result.filename).toMatch(/^\d+-fix-loop-extraction-and-round-trip\.md$/);
831
+ });
832
+
586
833
  test('createPrd leaves sourcePromptId unset when originClaudeSessionId matches no known Epic (plain SessionTab chat)', async () => {
587
834
  const root = await fsp.mkdtemp(path.join(os.homedir(), '.sm-origin-session-no-match-'));
588
835
  config.addAllowedRoot(root);
@@ -0,0 +1,117 @@
1
+ /**
2
+ * prdFrontmatterAgentType.test.cjs — PRD 1114: `agentType` (WHO executes a
3
+ * PRD, distinct from `tag` — the WORK TYPE) is a recognized PRD frontmatter
4
+ * key, round-tripped byte-identically by prdFrontmatter.cjs's
5
+ * parsePrdFile/serializePrdFile pair, and surfaced (with read-time tolerance
6
+ * for a dangling persona) by scheduler/prdParser.cjs's parsePrdRaw.
7
+ *
8
+ * Run: timeout 120 npx vitest run src/main/__tests__/prdFrontmatterAgentType.test.cjs
9
+ */
10
+
11
+ 'use strict';
12
+
13
+ import { test, expect } from 'vitest';
14
+
15
+ const fs = require('node:fs');
16
+ const fsp = require('node:fs/promises');
17
+ const os = require('node:os');
18
+ const path = require('node:path');
19
+
20
+ const { parsePrdFile, serializePrdFile } = require('../lib/prdFrontmatter.cjs');
21
+ const { parsePrdRaw, _resetCache } = require('../scheduler/prdParser.cjs');
22
+ const { todayFile } = require('../lib/opsErrorLog.cjs');
23
+
24
+ function mkTmpPrdsDir() {
25
+ return fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prd-agenttype-fm-'));
26
+ }
27
+
28
+ test('parsePrdFile recognizes agentType and serializePrdFile round-trips it byte-identically', () => {
29
+ const raw = [
30
+ '---',
31
+ 'title: A PRD with a persona',
32
+ 'cwd: ~/Projects/session-manager',
33
+ 'estimateMinutes: 30',
34
+ 'tag: bug',
35
+ 'agentType: dev-lead',
36
+ '---',
37
+ '# Goal',
38
+ '',
39
+ 'Fix the thing.',
40
+ '',
41
+ ].join('\n');
42
+
43
+ const { frontmatter, body } = parsePrdFile(raw);
44
+ expect(frontmatter.agentType).toBe('dev-lead');
45
+ expect(serializePrdFile(frontmatter, body)).toBe(raw);
46
+ });
47
+
48
+ test('parsePrdFile leaves agentType undefined when omitted — additive-only, no regression for old PRDs', () => {
49
+ const raw = [
50
+ '---',
51
+ 'title: An older PRD authored before this field existed',
52
+ 'cwd: ~/Projects/session-manager',
53
+ 'estimateMinutes: 30',
54
+ '---',
55
+ 'body',
56
+ '',
57
+ ].join('\n');
58
+ const { frontmatter } = parsePrdFile(raw);
59
+ expect(frontmatter.agentType).toBeUndefined();
60
+ });
61
+
62
+ // Real opsErrorLog write path (not a mock) — see prdAgentType.test.cjs's
63
+ // header for why a plain mkdtemp() cwd is safe to use here (not "ephemeral").
64
+ test('scheduler/prdParser.cjs parsePrdRaw exposes agentType, and tolerates a dangling persona by logging once instead of throwing', async () => {
65
+ const dir = await mkTmpPrdsDir();
66
+ _resetCache();
67
+
68
+ const danglingPath = path.join(dir, '1-dangling.md');
69
+ await fsp.writeFile(danglingPath, [
70
+ '---',
71
+ 'title: A PRD whose persona was later deleted',
72
+ `cwd: ${dir}`,
73
+ 'estimateMinutes: 10',
74
+ 'agentType: ghost-persona-that-does-not-exist-anywhere',
75
+ '---',
76
+ '# Goal',
77
+ '',
78
+ 'Still loads.',
79
+ '',
80
+ ].join('\n'));
81
+
82
+ const parsed = await parsePrdRaw(danglingPath);
83
+ expect(parsed.agentType).toBe('ghost-persona-that-does-not-exist-anywhere');
84
+
85
+ const lines = fs.readFileSync(todayFile(dir), 'utf8').split('\n').filter(Boolean).map((l) => JSON.parse(l));
86
+ const matches = lines.filter((l) => l.message.includes('ghost-persona-that-does-not-exist-anywhere'));
87
+ expect(matches).toHaveLength(1);
88
+ expect(matches[0].scope).toBe('prdAgentType');
89
+ expect(matches[0].level).toBe('warn');
90
+
91
+ // A second parse of the SAME (cwd, agentType) pair must not log again.
92
+ _resetCache();
93
+ await parsePrdRaw(danglingPath);
94
+ const linesAfter = fs.readFileSync(todayFile(dir), 'utf8').split('\n').filter(Boolean);
95
+ expect(linesAfter).toHaveLength(1);
96
+ });
97
+
98
+ test('scheduler/prdParser.cjs parsePrdRaw leaves agentType null when omitted', async () => {
99
+ const dir = await mkTmpPrdsDir();
100
+ _resetCache();
101
+
102
+ const ordinaryPath = path.join(dir, '2-ordinary.md');
103
+ await fsp.writeFile(ordinaryPath, [
104
+ '---',
105
+ 'title: Ordinary job',
106
+ `cwd: ${dir}`,
107
+ 'estimateMinutes: 10',
108
+ '---',
109
+ '# Goal',
110
+ '',
111
+ 'Do the thing.',
112
+ '',
113
+ ].join('\n'));
114
+
115
+ const parsed = await parsePrdRaw(ordinaryPath);
116
+ expect(parsed.agentType).toBeNull();
117
+ });
@@ -0,0 +1,108 @@
1
+ /**
2
+ * prdFrontmatterQuietMachine.test.cjs — PRD 1107: `quietMachine: true` is an
3
+ * optional PRD frontmatter field, recognized by prdFrontmatter.cjs's
4
+ * parsePrdFile/serializePrdFile round-trip AND by scheduler/prdParser.cjs's
5
+ * dispatch-time parsePrdRaw. Only a literal `true` opts in; omitted or any
6
+ * other value behaves exactly like today (additive-only, no required-field
7
+ * regression for the thousands of PRDs written before this field existed).
8
+ *
9
+ * Run: timeout 120 npx vitest run src/main/__tests__/prdFrontmatterQuietMachine.test.cjs
10
+ */
11
+
12
+ 'use strict';
13
+
14
+ import { test, expect } from 'vitest';
15
+ const fsp = require('node:fs/promises');
16
+ const os = require('node:os');
17
+ const path = require('node:path');
18
+
19
+ const { parsePrdFile, serializePrdFile } = require('../lib/prdFrontmatter.cjs');
20
+ const { parsePrdRaw, _resetCache } = require('../scheduler/prdParser.cjs');
21
+
22
+ function mkTmpPrdsDir() {
23
+ return fsp.mkdtemp(path.join(os.tmpdir(), 'sm-prd-quietmachine-'));
24
+ }
25
+
26
+ test('parsePrdFile recognizes quietMachine: true and serializePrdFile round-trips it byte-identically', () => {
27
+ const raw = [
28
+ '---',
29
+ 'title: A timing-sensitive PRD',
30
+ 'cwd: ~/Projects/session-manager',
31
+ 'estimateMinutes: 30',
32
+ 'quietMachine: true',
33
+ '---',
34
+ '# Goal',
35
+ '',
36
+ 'Measure frame time without contention.',
37
+ '',
38
+ ].join('\n');
39
+
40
+ const { frontmatter, body } = parsePrdFile(raw);
41
+ expect(frontmatter.quietMachine).toBe(true);
42
+ expect(serializePrdFile(frontmatter, body)).toBe(raw);
43
+ });
44
+
45
+ test('parsePrdFile ignores quietMachine: false — only a literal true opts in', () => {
46
+ const raw = [
47
+ '---',
48
+ 'title: Not opted in',
49
+ 'cwd: ~/Projects/session-manager',
50
+ 'estimateMinutes: 30',
51
+ 'quietMachine: false',
52
+ '---',
53
+ 'body',
54
+ '',
55
+ ].join('\n');
56
+ const { frontmatter } = parsePrdFile(raw);
57
+ expect(frontmatter.quietMachine).toBeUndefined();
58
+ });
59
+
60
+ test('parsePrdFile leaves quietMachine undefined when omitted — additive-only, no regression for old PRDs', () => {
61
+ const raw = [
62
+ '---',
63
+ 'title: An older PRD authored before this field existed',
64
+ 'cwd: ~/Projects/session-manager',
65
+ 'estimateMinutes: 30',
66
+ '---',
67
+ 'body',
68
+ '',
69
+ ].join('\n');
70
+ const { frontmatter } = parsePrdFile(raw);
71
+ expect(frontmatter.quietMachine).toBeUndefined();
72
+ });
73
+
74
+ test('scheduler/prdParser.cjs parsePrdRaw exposes quietMachine as a typed boolean, true only when the frontmatter literally says true', async () => {
75
+ const dir = await mkTmpPrdsDir();
76
+ _resetCache();
77
+
78
+ const quietPath = path.join(dir, '1-quiet.md');
79
+ await fsp.writeFile(quietPath, [
80
+ '---',
81
+ 'title: Quiet job',
82
+ 'cwd: ~/Projects/session-manager',
83
+ 'estimateMinutes: 10',
84
+ 'quietMachine: true',
85
+ '---',
86
+ '# Goal',
87
+ '',
88
+ 'Do the thing quietly.',
89
+ '',
90
+ ].join('\n'));
91
+ const quiet = await parsePrdRaw(quietPath);
92
+ expect(quiet.quietMachine).toBe(true);
93
+
94
+ const ordinaryPath = path.join(dir, '2-ordinary.md');
95
+ await fsp.writeFile(ordinaryPath, [
96
+ '---',
97
+ 'title: Ordinary job',
98
+ 'cwd: ~/Projects/session-manager',
99
+ 'estimateMinutes: 10',
100
+ '---',
101
+ '# Goal',
102
+ '',
103
+ 'Do the thing.',
104
+ '',
105
+ ].join('\n'));
106
+ const ordinary = await parsePrdRaw(ordinaryPath);
107
+ expect(ordinary.quietMachine).toBe(false);
108
+ });