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,84 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * prdAgentType.cjs — the PRD-frontmatter `agentType` FK, mirroring
5
+ * agentModelResolve.cjs's/epicMint.cjs's established convention for the
6
+ * Epic-level `agentType` FK: THROW ON WRITE, REPORT ON READ.
7
+ *
8
+ * A PRD's `agentType` names the persona (`~/.claude/agents/<name>.md`, or a
9
+ * project overlay at `<cwd>/.claude/agents/<name>.md`) that should execute
10
+ * it — distinct from `tag` (workTypeLibrary.cjs), which is the WORK TYPE.
11
+ * Reuses epicMint.cjs's personaFileExists/resolvePersonaPaths rather than a
12
+ * second persona-file reader, and agentLibrary.cjs's listPersonas() for the
13
+ * "available personas" name list surfaced in the write-time rejection error.
14
+ */
15
+
16
+ const { personaFileExists, resolvePersonaPaths } = require('./epicMint.cjs');
17
+ const { listPersonas } = require('../agentLibrary.cjs');
18
+
19
+ /** PRD execution's documented default persona (dev-lead.md's own frontmatter role). */
20
+ const DEFAULT_PRD_AGENT_TYPE = 'dev-lead';
21
+
22
+ // READ side of the FK — logged at most once per (cwd, agentType) so a hot
23
+ // reconcile/list pass re-parsing the same on-disk PRD every tick doesn't
24
+ // flood opsErrorLog's daily file. Mirrors agentModelResolve.cjs's
25
+ // loggedDanglingPersonas set.
26
+ const loggedDanglingAgentTypes = new Set();
27
+
28
+ /**
29
+ * Read-time tolerance check: a PRD on disk whose persona was renamed or
30
+ * deleted after the PRD was written must still load. Never throws — logs at
31
+ * most once via opsErrorLog when `agentType` is set but unresolvable.
32
+ */
33
+ function reportDanglingAgentTypeOnce(cwd, agentType, deps = {}) {
34
+ if (!cwd || !agentType) return;
35
+ try {
36
+ const checkPersonaExists = deps.personaExists || personaFileExists;
37
+ if (checkPersonaExists(cwd, agentType, deps)) return;
38
+ const key = `${cwd}::${agentType}`;
39
+ if (loggedDanglingAgentTypes.has(key)) return;
40
+ loggedDanglingAgentTypes.add(key);
41
+ // Required lazily, same rationale as agentModelResolve.cjs's
42
+ // logDanglingPersonaOnce: pay for this leaf module only on the (rare)
43
+ // miss path.
44
+ const { appendError } = require('./opsErrorLog.cjs');
45
+ appendError({
46
+ cwd,
47
+ scope: 'prdAgentType',
48
+ level: 'warn',
49
+ message: `PRD agentType '${agentType}' has no resolvable persona file — PRD still loads`,
50
+ meta: { agentType },
51
+ });
52
+ } catch { /* logging must never break PRD loading */ }
53
+ }
54
+
55
+ /**
56
+ * Write-time FK check: throws when `agentType` is supplied but doesn't
57
+ * resolve to a readable persona file, naming the available personas (from
58
+ * the Agent Library) so the caller can pick a real one. A no-op when
59
+ * `agentType` is falsy — the caller is expected to default it separately
60
+ * (buildPrdBody's DEFAULT_PRD_AGENT_TYPE fill-in).
61
+ */
62
+ async function assertAgentTypeWritable(cwd, agentType, deps = {}) {
63
+ if (!agentType) return;
64
+ const checkPersonaExists = deps.personaExists || personaFileExists;
65
+ if (checkPersonaExists(cwd, agentType, deps)) return;
66
+
67
+ const load = deps.listPersonas || listPersonas;
68
+ let available = [];
69
+ try {
70
+ available = (await load()).map((p) => p.name).filter(Boolean).sort();
71
+ } catch { /* best-effort — still throw below even if listing failed */ }
72
+
73
+ const { projectPath, globalPath } = resolvePersonaPaths(cwd, agentType, deps);
74
+ throw new Error(
75
+ `agentType '${agentType}' does not resolve to a readable persona file — checked ${projectPath} and `
76
+ + `${globalPath}. Available personas: ${available.length ? available.join(', ') : '(none found)'}`,
77
+ );
78
+ }
79
+
80
+ module.exports = {
81
+ DEFAULT_PRD_AGENT_TYPE,
82
+ assertAgentTypeWritable,
83
+ reportDanglingAgentTypeOnce,
84
+ };
@@ -25,6 +25,19 @@ const { expandHome } = require('./expandHome.cjs');
25
25
  const { readBody, sendJson } = require('./localAdminHttp.cjs');
26
26
  const { readActiveIndex } = require('./epicMint.cjs');
27
27
  const { appendAuditEvent } = require('./auditLog.cjs');
28
+ const { resolveProjectContext } = require('./projectRootResolve.cjs');
29
+ const { fixChainDepthOf, baseSlugOf } = require('./fixChainDepth.cjs');
30
+ const { DEFAULT_PRD_AGENT_TYPE, assertAgentTypeWritable } = require('./prdAgentType.cjs');
31
+
32
+ // Fix-chain depth cap (PRD 1113): a fix-of-a-fix (depth >= 2) is refused at
33
+ // this shared write path rather than caught later — scheduler.cjs's
34
+ // MAX_INVESTIGATION_DEPTH=1 caps the auto-fix loop's OWN recursion the same
35
+ // way, so the two limits must stay numerically consistent (depth 1 allowed,
36
+ // depth 2 refused). This guard exists because that auto-fix cap only ever
37
+ // sees PRDs `spawnInvestigation` itself authors; a human or planner session
38
+ // authoring a `*-fix-fix-*` slug through this API bypasses it entirely —
39
+ // which is exactly what happened on starry-night-ships (see PRD 1113's Goal).
40
+ const FIX_CHAIN_DEPTH_CAP = 1;
28
41
 
29
42
  const STANDARDS_PATH = path.join(
30
43
  __dirname, '..', '..', '..',
@@ -50,7 +63,7 @@ function deriveSlugFromTitle(title) {
50
63
  function buildPrdBody(input) {
51
64
  const {
52
65
  title, cwd, estimateMinutes, goal, acceptanceCriteria,
53
- implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, dependsOn,
66
+ implementationNotes, outOfScope, sourcePromptId, sourceTabId, tag, agentType, dependsOn, quietMachine,
54
67
  } = input;
55
68
 
56
69
  // No `parallelGroup` frontmatter key by convention (SKILL.md) — the NN-
@@ -75,8 +88,21 @@ function buildPrdBody(input) {
75
88
  // Optional, additive: the user-selected Feature/Bug tag (PRD 774) carried
76
89
  // through from the originating PromptTicket — deterministic, never LLM-classified.
77
90
  if (tag) fmLines.push(`tag: ${tag}`);
91
+ // WHO executes this PRD — distinct from `tag` (the WORK TYPE, above).
92
+ // Always emitted (unlike tag): a PRD with no agentType is ambiguous about
93
+ // who runs it, so createPrd() fills in DEFAULT_PRD_AGENT_TYPE ('dev-lead')
94
+ // before calling this function when the caller omitted one. Validated at
95
+ // write time by createPrd() via prdAgentType.cjs's assertAgentTypeWritable
96
+ // — this function only serializes, it never validates.
97
+ fmLines.push(`agentType: ${agentType || DEFAULT_PRD_AGENT_TYPE}`);
78
98
  // Explicit ordering (PRD 832): replaces the retired shared-NN convention.
79
99
  if (dependsOn && dependsOn.length) fmLines.push(`dependsOn: [${dependsOn.join(', ')}]`);
100
+ // Opt-in exclusive-lease flag (PRD 1107): serializes this job against
101
+ // every other job machine-wide for its run, for a PRD whose acceptance
102
+ // criteria are wall-clock/timing measurements that CPU contention from
103
+ // sibling jobs would otherwise invalidate. Only emitted when true — see
104
+ // prdFrontmatter.cjs's applyKey for the matching "only true opts in" parse.
105
+ if (quietMachine === true) fmLines.push('quietMachine: true');
80
106
  fmLines.push('---', '');
81
107
 
82
108
  const acLines = acceptanceCriteria.map((line) => `- [ ] ${line}`).join('\n');
@@ -138,17 +164,41 @@ function resolveSourcePromptIdFromClaudeSession(cwd, claudeSessionId) {
138
164
  * (400/409/500), left to the caller since only the HTTP route needs it.
139
165
  */
140
166
  async function createPrd(input, remote) {
141
- // cwd is untrusted: this is the first *creating* mutation on a
142
- // token-authed API whose product is "a command that will later run
143
- // with --dangerously-skip-permissions in a chosen cwd". Route it
144
- // through config.cjs's validatePath (allowedRoots = home dir) —
145
- // same boundary every other fs-touching IPC handler uses — never a
146
- // bespoke check here.
147
- // Normalize a `~`-prefixed cwd BEFORE it reaches allocateParallelGroup/
148
- // readPrd/writePrd — those pass cwd through to safeSlugPathIn-adjacent path
149
- // joins that treat it as a literal path segment, so an unexpanded `~/...`
150
- // fails downstream as a bare "invalid slug" instead of a clear cwd error.
151
- const cwd = expandHome(input.cwd);
167
+ // cwd is untrusted AND, since this PRD, optional — a caller inside an
168
+ // Epic's git worktree naturally passes its worktree pwd (or omits cwd
169
+ // entirely and relies on originClaudeSessionId/sourcePromptId). Resolve the
170
+ // real project + owning Epic server-side BEFORE any validation: a worktree
171
+ // cwd currently PASSES config.validatePath and then reads the WRONG
172
+ // active-index.json (a git-tracked snapshot frozen at branch time), which
173
+ // is why "the Epic got lost" reports keep recurring even though the Epic
174
+ // never left main's index. See projectRootResolve.cjs's header.
175
+ const rawCwd = input.cwd ? expandHome(input.cwd) : null;
176
+ const originProjectRoot = input.originProjectRoot ? expandHome(input.originProjectRoot) : null;
177
+ const resolved = resolveProjectContext({
178
+ cwd: rawCwd,
179
+ originClaudeSessionId: input.originClaudeSessionId,
180
+ // Only ask the resolver to hunt for the project BY Epic id when no cwd
181
+ // (nor a trusted SM_PROJECT_ROOT hint) was supplied at all — an explicit
182
+ // cwd that merely needs worktree/ops-path normalization is already fixed
183
+ // by projectRootOf above, so the common already-correct-cwd call (cwd +
184
+ // sourcePromptId both supplied) never pays for a cross-project scan.
185
+ epicId: (rawCwd || originProjectRoot) ? undefined : input.sourcePromptId,
186
+ originProjectRoot,
187
+ });
188
+
189
+ if (!resolved.cwd) {
190
+ return {
191
+ ok: false,
192
+ status: 400,
193
+ error: 'cwd is required — pass cwd explicitly, or originClaudeSessionId/sourcePromptId that resolves '
194
+ + 'to an existing, already-approved Epic (create/approve an Epic first)',
195
+ };
196
+ }
197
+
198
+ // A resolved Epic's own project cwd always wins over the supplied/hinted
199
+ // one — see resolveProjectContext's header. `cwd` below is that final,
200
+ // already-normalized (worktree -> main tree) value.
201
+ const cwd = resolved.cwd;
152
202
  let realCwd;
153
203
  try {
154
204
  realCwd = config.validatePath(cwd);
@@ -164,9 +214,16 @@ async function createPrd(input, remote) {
164
214
  config.addAllowedRoot(realCwd);
165
215
  input = { ...input, cwd };
166
216
 
167
- if (!input.sourcePromptId && input.originClaudeSessionId) {
168
- const resolved = resolveSourcePromptIdFromClaudeSession(cwd, input.originClaudeSessionId);
169
- if (resolved) input = { ...input, sourcePromptId: resolved };
217
+ if (resolved.epicId) {
218
+ input = { ...input, sourcePromptId: resolved.epicId };
219
+ } else if (!input.sourcePromptId && input.originClaudeSessionId) {
220
+ // Fallback kept for the narrow case a caller passes originClaudeSessionId
221
+ // but resolveProjectContext (which also scans cross-project) somehow
222
+ // missed it — resolveSourcePromptIdFromClaudeSession is the single-cwd
223
+ // primitive resolveProjectContext itself is built from, so this can only
224
+ // ever be a no-op in practice; kept for defense-in-depth, not duplicated logic.
225
+ const fallback = resolveSourcePromptIdFromClaudeSession(cwd, input.originClaudeSessionId);
226
+ if (fallback) input = { ...input, sourcePromptId: fallback };
170
227
  }
171
228
 
172
229
  const slug = input.slug || deriveSlugFromTitle(input.title);
@@ -185,6 +242,23 @@ async function createPrd(input, remote) {
185
242
  const nn = await remote.allocateParallelGroup(input.cwd);
186
243
  const filenameSlug = `${nn}-${slug}`;
187
244
 
245
+ // Fix-chain depth guard (PRD 1113): refuse a fix-of-a-fix before it's ever
246
+ // written. depth 0/1 pass through unchanged (an ordinary PRD, or a first
247
+ // fix-plan, is never blocked); depth >= 2 means the base job has already
248
+ // failed to close via at least one prior fix attempt for reasons a repeat
249
+ // attempt won't resolve.
250
+ const chainDepth = fixChainDepthOf(filenameSlug);
251
+ if (chainDepth > FIX_CHAIN_DEPTH_CAP) {
252
+ const base = baseSlugOf(filenameSlug);
253
+ return {
254
+ ok: false,
255
+ status: 409,
256
+ error: `Fix-chain depth cap exceeded: "${base}" has already failed to close ${chainDepth} time(s) for ` +
257
+ `infrastructure reasons (refusing to write "${filenameSlug}.md" at depth ${chainDepth}). Authoring a ` +
258
+ 'deeper fix PRD is not the correct response — stop and surface this to a human instead of retrying again.',
259
+ };
260
+ }
261
+
188
262
  // An explicit `parallelGroup` bypasses allocateParallelGroup()'s
189
263
  // collision-proof reservation, so re-check for an existing file at
190
264
  // this exact destination before writing — remote.writePrd itself has
@@ -196,6 +270,20 @@ async function createPrd(input, remote) {
196
270
  return { ok: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
197
271
  }
198
272
 
273
+ // Write-time FK check (throw on write, report on read — see
274
+ // prdAgentType.cjs's header): a caller-supplied agentType that doesn't
275
+ // resolve to a readable persona file is rejected before anything is
276
+ // written, naming the available personas. A no-op when agentType is
277
+ // omitted — buildPrdBody fills in DEFAULT_PRD_AGENT_TYPE for that case,
278
+ // and the default persona is trusted without a lookup.
279
+ if (input.agentType) {
280
+ try {
281
+ await assertAgentTypeWritable(input.cwd, input.agentType);
282
+ } catch (e) {
283
+ return { ok: false, status: 400, error: e?.message ?? 'invalid agentType' };
284
+ }
285
+ }
286
+
199
287
  const body = buildPrdBody(input);
200
288
  const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
201
289
  if (!writeResult?.ok) {
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * prdDeclaredPaths.cjs — derive the file paths a PRD declares it touches, for
5
+ * the reverify self-heal pass's widened evidence window (PRD 1102).
6
+ *
7
+ * Reuses definitionOfDone.cjs's existing `extractSection` (already the PRD
8
+ * markdown section parser used by flagRiskySurfaces) rather than
9
+ * re-implementing one — see standards.md's API-reuse rule.
10
+ */
11
+
12
+ const fs = require('node:fs');
13
+ const { splitFrontmatter } = require('./prdFrontmatter.cjs');
14
+ const { extractSection } = require('./definitionOfDone.cjs');
15
+
16
+ // Backtick-quoted, extension-bearing relative paths, e.g. `src/main/scheduler.cjs`
17
+ // or `src/main/scheduler.cjs:490` (trailing :line dropped). Requires at least
18
+ // one `/` so bare identifiers like `npm` or `RESCANNABLE_VERDICTS` never match.
19
+ const PATH_RE = /`([a-zA-Z0-9_][\w.-]*(?:\/[\w.-]+)+\.[a-zA-Z0-9]+)(?::\d+)?`/g;
20
+
21
+ /**
22
+ * Extract declared file paths from a PRD body (frontmatter already
23
+ * stripped). Scans the `# Implementation notes` and `# Acceptance criteria`
24
+ * sections only — the same sections flagRiskySurfaces and the PRD-authoring
25
+ * convention already treat as where files-touched are named. Returns `[]`
26
+ * (never fabricates a path) when neither section names one.
27
+ *
28
+ * @param {string} prdBody
29
+ * @returns {string[]} deduped, in first-seen order
30
+ */
31
+ function extractDeclaredPaths(prdBody) {
32
+ if (!prdBody || typeof prdBody !== 'string') return [];
33
+ const text = [
34
+ extractSection(prdBody, 'Implementation notes'),
35
+ extractSection(prdBody, 'Acceptance criteria'),
36
+ ].join('\n');
37
+
38
+ const paths = [];
39
+ const seen = new Set();
40
+ let m;
41
+ PATH_RE.lastIndex = 0;
42
+ while ((m = PATH_RE.exec(text))) {
43
+ if (!seen.has(m[1])) {
44
+ seen.add(m[1]);
45
+ paths.push(m[1]);
46
+ }
47
+ }
48
+ return paths;
49
+ }
50
+
51
+ /**
52
+ * Read a PRD file and extract its declared paths. Never throws — a missing
53
+ * or unreadable PRD (e.g. archived and swept) resolves to `[]`.
54
+ *
55
+ * @param {string|null} prdPath
56
+ * @returns {string[]}
57
+ */
58
+ function declaredPathsForPrd(prdPath) {
59
+ if (!prdPath) return [];
60
+ let raw;
61
+ try {
62
+ raw = fs.readFileSync(prdPath, 'utf8');
63
+ } catch {
64
+ return [];
65
+ }
66
+ const { body } = splitFrontmatter(raw);
67
+ return extractDeclaredPaths(body);
68
+ }
69
+
70
+ module.exports = { extractDeclaredPaths, declaredPathsForPrd };
@@ -62,7 +62,7 @@ function splitFrontmatter(raw) {
62
62
  * either surface never reorders or drops a key it didn't touch. Recognized
63
63
  * key set intentionally mirrors the TS module exactly (title, cwd,
64
64
  * estimateMinutes, parallelGroup, sourcePromptId, sourceTabId, tag,
65
- * createdVia, issuedAt); every other frontmatter key (e.g. dependsOn)
65
+ * agentType, createdVia, issuedAt); every other frontmatter key (e.g. dependsOn)
66
66
  * round-trips via `extras`, same as the renderer's editor today.
67
67
  *
68
68
  * `createdVia`/`issuedAt` (PRD provenance-lockdown) are recognized here so
@@ -72,8 +72,8 @@ function splitFrontmatter(raw) {
72
72
  * into `extras`.
73
73
  */
74
74
 
75
- const RECOGNIZED_KEYS = new Set(['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'createdVia', 'issuedAt']);
76
- const EMIT_ORDER = ['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'createdVia', 'issuedAt'];
75
+ const RECOGNIZED_KEYS = new Set(['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'agentType', 'createdVia', 'issuedAt', 'quietMachine']);
76
+ const EMIT_ORDER = ['title', 'cwd', 'estimateMinutes', 'parallelGroup', 'sourcePromptId', 'sourceTabId', 'tag', 'agentType', 'createdVia', 'issuedAt', 'quietMachine'];
77
77
  const FRONTMATTER_RE = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/;
78
78
 
79
79
  function indentOf(line) {
@@ -125,12 +125,26 @@ function applyKey(fm, key, after) {
125
125
  // Matches the TS source's applyKey exactly — see PRD_TAG_VALUES above.
126
126
  if (PRD_TAG_VALUES.has(v)) fm.tag = v;
127
127
  return;
128
+ case 'agentType':
129
+ // WHO executes this PRD — a persona name, not a closed enum (the
130
+ // Agent Library is user-editable). FK resolution against the Agent
131
+ // Library happens in prdAgentType.cjs, not here — this parser only
132
+ // round-trips the string.
133
+ fm.agentType = String(v);
134
+ return;
128
135
  case 'createdVia':
129
136
  fm.createdVia = String(v);
130
137
  return;
131
138
  case 'issuedAt':
132
139
  fm.issuedAt = String(v);
133
140
  return;
141
+ case 'quietMachine':
142
+ // Opt-in exclusive-lease flag (PRD 1107) — only a literal `true`
143
+ // frontmatter value opts in; anything else (including an explicit
144
+ // `false`) is treated as "not set" so the field never round-trips as
145
+ // a no-op line for the overwhelming majority of PRDs that omit it.
146
+ if (v === true) fm.quietMachine = true;
147
+ return;
134
148
  }
135
149
  }
136
150
 
@@ -19,8 +19,14 @@ const fs = require('node:fs');
19
19
  const path = require('node:path');
20
20
  const { activeProjectCwds, allProjectCwds } = require('../../../scripts/lib/activeSessions.cjs');
21
21
 
22
- const PRD_SUBPATH = ['session-manager-operations', 'scheduler', 'prds'];
23
- const EPICS_SUBPATH = ['session-manager-operations', 'scheduler', 'epics'];
22
+ const { opsPath, OPS_ROOT_DIR } = require('./opsOwnership.cjs');
23
+
24
+ // Segments UNDER the ops root; the ops root itself is resolved by
25
+ // opsOwnership.opsPath (PRD 1082) — never joined onto a raw cwd here.
26
+ const PRD_SUBPATH = ['scheduler', 'prds'];
27
+ const EPICS_SUBPATH = ['scheduler', 'epics'];
28
+ // Segment count from a project root to an epic's own dir: <ops-root> + EPICS_SUBPATH.
29
+ const EPICS_DEPTH_FROM_PROJECT = 1 + EPICS_SUBPATH.length;
24
30
 
25
31
  /**
26
32
  * resolveEpicsRoot(cwd) → `<cwd>/session-manager-operations/scheduler/epics`
@@ -30,7 +36,7 @@ function resolveEpicsRoot(cwd) {
30
36
  if (!cwd || typeof cwd !== 'string') {
31
37
  throw new Error('resolveEpicsRoot: cwd is required');
32
38
  }
33
- return path.join(cwd, ...EPICS_SUBPATH);
39
+ return opsPath(cwd, ...EPICS_SUBPATH);
34
40
  }
35
41
 
36
42
  /**
@@ -69,7 +75,8 @@ function listEpicPrdDirs(cwd) {
69
75
  * its Epic (the layout every new PRD uses) is still found.
70
76
  */
71
77
  function listArchivedPrdDirs(cwd) {
72
- const dirs = [path.join(cwd, ...PRD_SUBPATH, '..', 'prds-archived')];
78
+ let dirs;
79
+ try { dirs = [opsPath(cwd, ...PRD_SUBPATH, '..', 'prds-archived')]; } catch { return []; }
73
80
  let root;
74
81
  try { root = resolveEpicsRoot(cwd); } catch { return dirs; }
75
82
  let entries;
@@ -91,7 +98,7 @@ function resolvePrdWriteDir(cwd) {
91
98
  if (!cwd || typeof cwd !== 'string') {
92
99
  throw new Error('resolvePrdWriteDir: cwd is required');
93
100
  }
94
- return path.join(cwd, ...PRD_SUBPATH);
101
+ return opsPath(cwd, ...PRD_SUBPATH);
95
102
  }
96
103
 
97
104
  /**
@@ -202,7 +209,7 @@ function deriveEpicIdFromPrdPath(filePath) {
202
209
  const epicId = path.basename(epicDir);
203
210
  if (!epicId || epicId === '.' || epicId === path.sep) return null;
204
211
  let projectCwd = path.dirname(epicDir); // epicsRoot (.../scheduler/epics)
205
- for (let i = 0; i < EPICS_SUBPATH.length; i++) projectCwd = path.dirname(projectCwd);
212
+ for (let i = 0; i < EPICS_DEPTH_FROM_PROJECT; i++) projectCwd = path.dirname(projectCwd);
206
213
  let epicsRoot;
207
214
  try { epicsRoot = resolveEpicsRoot(projectCwd); } catch { return null; }
208
215
  if (path.join(epicsRoot, epicId, dirName) !== prdsDir) return null;