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
@@ -5,7 +5,9 @@
5
5
  * display (PRD 932). Reads what the `project-home-builder` Epic's own
6
6
  * session writes directly with its Write tool
7
7
  * (`session-manager-operations/project-pages/output/*.html` + manifest.json)
8
- * — this module never writes anything, matching the corrected "not an
8
+ * — this module never writes any CONTENT (the one exception, watchOutput()'s
9
+ * `fs.mkdirSync` to ensure the output dir exists before attaching a watcher,
10
+ * creates an empty directory only), matching the corrected "not an
9
11
  * OWNERS namespace" storage/ownership note in
10
12
  * session-manager-operations/architecture/project-pages-pipeline.md and
11
13
  * session-manager-operations/project-pages/README.md. Structure mirrors
@@ -25,10 +27,34 @@
25
27
  const { ipcMain } = require('electron');
26
28
  const fs = require('node:fs');
27
29
  const path = require('node:path');
30
+ const chokidar = require('chokidar');
28
31
  const config = require('./config.cjs');
32
+ const { opsPath } = require('./lib/opsOwnership.cjs');
33
+ const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
34
+
35
+ let mainWindow = null;
36
+ function attachWindow(window) {
37
+ mainWindow = window;
38
+ }
29
39
 
30
40
  function outputDir(cwd) {
31
- return path.join(cwd, 'session-manager-operations', 'project-pages', 'output');
41
+ return opsPath(cwd, 'project-pages', 'output');
42
+ }
43
+
44
+ /**
45
+ * Resolve `cwd`'s output dir, treating the opsOwnership "ephemeral cwd"
46
+ * throw (a tmpdir/linked-worktree project root — see opsOwnership.cjs's
47
+ * resolveProjectRoot) as "no output dir available" rather than letting it
48
+ * propagate. Both get() and the watcher below go through this so a builder
49
+ * Epic running in a worktree can never crash either path.
50
+ */
51
+ function tryOutputDir(realCwd) {
52
+ try {
53
+ return { dir: outputDir(realCwd), ephemeral: false };
54
+ } catch (e) {
55
+ if (e.ephemeral) return { dir: null, ephemeral: true };
56
+ throw e;
57
+ }
32
58
  }
33
59
 
34
60
  // Shipped build-time asset (see PRD "project-home-hosted-html-spec"'s
@@ -59,13 +85,24 @@ function defaultOutput() {
59
85
  // the empty state for every pre-existing project. So brief.html is read
60
86
  // tolerantly — present, its text is returned; absent, the field is simply
61
87
  // omitted from the payload rather than failing the whole read.
88
+ // Partial-output strategy (see this file's module doc + PRD
89
+ // "project-home-generate-preflight-and-push"): the render route
90
+ // (projectHomeAdminRoutes.cjs's doRender) writes every lens file
91
+ // concurrently and writes manifest.json ONLY AFTER all of them have landed.
92
+ // So manifest.json's existence is already an all-or-nothing commit signal —
93
+ // gating get() on it (below) means a reader can never observe a lens file
94
+ // that arrived without its siblings. No second partial-state representation
95
+ // is needed; this file just relies on that write order.
62
96
  const REQUIRED_LENSES = ['home', 'marketing', 'feature', 'architecture'];
63
97
  const OPTIONAL_LENSES = ['brief'];
64
98
  const LENSES = [...REQUIRED_LENSES, ...OPTIONAL_LENSES];
65
99
 
66
100
  async function get({ cwd }) {
67
101
  const realCwd = config.validatePath(cwd);
68
- const dir = outputDir(realCwd);
102
+ const { dir, ephemeral } = tryOutputDir(realCwd);
103
+ if (ephemeral) {
104
+ return { output: defaultOutput() };
105
+ }
69
106
  const manifestResult = await config.readJson(path.join(dir, 'manifest.json'));
70
107
  if (!manifestResult.exists || !manifestResult.data || manifestResult.parseError) {
71
108
  return { output: defaultOutput() };
@@ -99,14 +136,136 @@ async function get({ cwd }) {
99
136
  return { output };
100
137
  }
101
138
 
139
+ // ─── Push channel (PRD "project-home-generate-preflight-and-push") ────────
140
+ //
141
+ // project-pages:get above is a one-shot pull, fetched once at mount by
142
+ // ProjectHome.tsx. That leaves a finished (or stuck) builder Epic invisible
143
+ // until the next manual refresh/app restart. This section adds a per-cwd
144
+ // chokidar watcher on the output dir (the same debounced-write library
145
+ // config.cjs already uses for its generic file watcher) that recomputes
146
+ // get() and pushes it to the renderer whenever the dir changes.
147
+ //
148
+ // Map<realCwd, { watcher, refCount }> — refcounted like config.cjs's own
149
+ // watchers Map so more than one mounted component watching the same cwd
150
+ // doesn't spawn a second chokidar instance, and doesn't get torn down by
151
+ // the first component's unmount while a second is still watching.
152
+ const outputWatchers = new Map();
153
+
154
+ /**
155
+ * Watch `cwd`'s output dir. Resolves `{ ok: true }` once watching is actually
156
+ * live (or already watching), or `{ ok: false, reason: 'ephemeral' |
157
+ * 'invalid-cwd' }` when there is nothing safe to watch — callers must not
158
+ * treat that as an error, just as "no live updates for this cwd" (e.g. a
159
+ * worktree cwd). Async: waits for chokidar's own 'ready' before resolving so
160
+ * a write landing immediately after the call is never missed in the window
161
+ * before the underlying fs watch handle is actually registered.
162
+ */
163
+ async function watchOutput(cwd) {
164
+ let realCwd;
165
+ try {
166
+ realCwd = config.validatePath(cwd);
167
+ } catch {
168
+ return { ok: false, reason: 'invalid-cwd' };
169
+ }
170
+
171
+ const existing = outputWatchers.get(realCwd);
172
+ if (existing) {
173
+ existing.refCount += 1;
174
+ return { ok: true };
175
+ }
176
+
177
+ const { dir, ephemeral } = tryOutputDir(realCwd);
178
+ if (ephemeral) {
179
+ return { ok: false, reason: 'ephemeral' };
180
+ }
181
+ // Ensure the (possibly not-yet-generated) output dir exists before
182
+ // chokidar attaches: watching a path 3 levels deep that doesn't exist yet
183
+ // does not reliably pick up files later written into it (verified — the
184
+ // watcher silently misses the first write without this). This creates an
185
+ // EMPTY directory only, no content, so it does not conflict with this
186
+ // module's read-only contract or the single-writer law (opsOwnership.cjs)
187
+ // — it bypasses config.cjs's writer-tracked write path deliberately,
188
+ // exactly like a project-home-builder Epic's own untracked Write-tool
189
+ // calls into this same folder (see opsOwnership.cjs's 'project-pages'
190
+ // entry).
191
+ fs.mkdirSync(dir, { recursive: true });
192
+
193
+ const push = async () => {
194
+ let result;
195
+ try {
196
+ result = await get({ cwd: realCwd });
197
+ } catch {
198
+ return; // best-effort; the renderer keeps its last-known output
199
+ }
200
+ sendIfAlive(mainWindow, 'project-pages:changed', { cwd: realCwd, output: result.output });
201
+ };
202
+
203
+ const watcher = chokidar.watch(dir, {
204
+ ignoreInitial: true,
205
+ persistent: true,
206
+ // manifest.json is the LAST file doRender writes (see REQUIRED_LENSES's
207
+ // comment above) — but the burst of per-lens writes leading up to it
208
+ // still fires several fs events in quick succession. awaitWriteFinish
209
+ // (same 50ms/25ms shape config.cjs's generic watcher uses) collapses
210
+ // that burst into one push instead of flapping the UI once per file.
211
+ awaitWriteFinish: { stabilityThreshold: 50, pollInterval: 25 },
212
+ });
213
+ watcher.on('add', push);
214
+ watcher.on('change', push);
215
+ watcher.on('unlink', push);
216
+ watcher.on('error', (err) => {
217
+ console.warn('[projectPages] watcher error for', dir, err?.message);
218
+ });
219
+
220
+ outputWatchers.set(realCwd, { watcher, refCount: 1 });
221
+ await new Promise((resolve) => watcher.once('ready', resolve));
222
+ return { ok: true };
223
+ }
224
+
225
+ /** Decrement `cwd`'s watch refcount; closes the underlying watcher at 0. */
226
+ function unwatchOutput(cwd) {
227
+ let realCwd;
228
+ try {
229
+ realCwd = config.validatePath(cwd);
230
+ } catch {
231
+ return;
232
+ }
233
+ const entry = outputWatchers.get(realCwd);
234
+ if (!entry) return;
235
+ entry.refCount -= 1;
236
+ if (entry.refCount <= 0) {
237
+ entry.watcher.close().catch(() => {});
238
+ outputWatchers.delete(realCwd);
239
+ }
240
+ }
241
+
242
+ /** Window close / app quit teardown — mirrors config.cjs's closeAllWatchers. */
243
+ function closeAllOutputWatchers() {
244
+ for (const { watcher } of outputWatchers.values()) {
245
+ watcher.close().catch(() => {});
246
+ }
247
+ outputWatchers.clear();
248
+ }
249
+
102
250
  function registerProjectPagesIpc() {
103
251
  const { schemas: s, validated: v } = require('./ipcSchemas.cjs');
104
252
  ipcMain.handle('project-pages:get', v(s.projectPagesCwd, get));
253
+ ipcMain.handle('project-pages:watch', v(s.projectPagesCwd, ({ cwd }) => watchOutput(cwd)));
254
+ ipcMain.handle('project-pages:unwatch', v(s.projectPagesCwd, ({ cwd }) => {
255
+ unwatchOutput(cwd);
256
+ return { ok: true };
257
+ }));
105
258
  }
106
259
 
107
260
  module.exports = {
261
+ attachWindow,
108
262
  registerProjectPagesIpc,
109
263
  get,
264
+ watchOutput,
265
+ unwatchOutput,
266
+ closeAllOutputWatchers,
110
267
  DEFAULT_HOME_PATH,
111
268
  readDefaultHomeHtml,
269
+ // Test-only introspection of the refcounted watcher map.
270
+ _outputWatchers: outputWatchers,
112
271
  };
package/src/main/pty.cjs CHANGED
@@ -156,6 +156,11 @@ class PtyManager {
156
156
  // back to its originating Epic and minted an unrelated sibling one
157
157
  // instead (prdCreate.cjs's resolveSourcePromptIdFromClaudeSession).
158
158
  SM_CHAT_SESSION_ID: tabId,
159
+ // The real project cwd (never the worktree spawnCwd resolved below) —
160
+ // forwarded by scheduler-mcp-server.cjs as originProjectRoot so
161
+ // create-prd/open-session/readiness resolve the right project even
162
+ // from a worktree pwd. See projectRootResolve.cjs.
163
+ SM_PROJECT_ROOT: cwd,
159
164
  });
160
165
 
161
166
  const shell = process.env.SHELL || '/bin/bash';
@@ -128,7 +128,7 @@ const INTERACTIVE_AC_PATTERNS = [
128
128
  ];
129
129
 
130
130
  const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
131
- const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
131
+ const { resolvePrdsDirs, resolvePrdWriteDir, listEpicPrdDirs } = require('./lib/prdLocations.cjs');
132
132
 
133
133
  // PRD 808: PRDS_DIR is now the LEGACY global dir — kept as a migration
134
134
  // source and search fallback for not-yet-migrated files. Live PRDs resolve
@@ -139,9 +139,16 @@ function candidatePrdsDirs() {
139
139
  return [PRDS_DIR, ...resolvePrdsDirs()];
140
140
  }
141
141
 
142
- /** Search every candidate PRD dir for `<slug>.md`; returns the containing dir or null. */
143
- async function findPrdDir(slug) {
144
- for (const dir of candidatePrdsDirs()) {
142
+ /**
143
+ * Search every candidate PRD dir for `<slug>.md`; returns the containing dir
144
+ * or null. `cwd`, if given, narrows the search to that one project's own PRD
145
+ * dirs (its flat prds/ dir plus its Epic-scoped dirs) instead of scanning
146
+ * every project machine-wide — same narrowing pattern scheduler.cjs's
147
+ * getPrdParsed uses.
148
+ */
149
+ async function findPrdDir(slug, cwd) {
150
+ const dirs = cwd ? [resolvePrdWriteDir(cwd), ...listEpicPrdDirs(cwd)] : candidatePrdsDirs();
151
+ for (const dir of dirs) {
145
152
  try {
146
153
  await fsp.access(path.join(dir, `${slug}.md`));
147
154
  return dir;
@@ -311,9 +318,9 @@ function archiveDirForSource(srcDir, ts) {
311
318
  return path.join(path.dirname(srcDir), 'prds-archived', ts);
312
319
  }
313
320
 
314
- async function archiveOne(slug, ts) {
321
+ async function archiveOne(slug, ts, cwd) {
315
322
  if (!SLUG_RE.test(slug)) return { ok: false, slug, error: 'invalid slug' };
316
- const srcDir = await findPrdDir(slug);
323
+ const srcDir = await findPrdDir(slug, cwd);
317
324
  if (!srcDir) return { ok: false, slug, error: 'not found in any PRDs dir' };
318
325
  const src = path.resolve(path.join(srcDir, `${slug}.md`));
319
326
  if (!src.startsWith(srcDir + path.sep)) return { ok: false, slug, error: 'path escape (src)' };
@@ -342,7 +349,7 @@ async function archiveOne(slug, ts) {
342
349
  */
343
350
  let retireCompletedSlugsFn = async () => {};
344
351
 
345
- async function archiveMany(slugs) {
352
+ async function archiveMany(slugs, cwd) {
346
353
  if (!Array.isArray(slugs) || slugs.length === 0) {
347
354
  return { ok: true, archived: 0, archivedTo: null, results: [] };
348
355
  }
@@ -353,7 +360,7 @@ async function archiveMany(slugs) {
353
360
  const ts = new Date().toISOString().replace(/[:.]/g, '-');
354
361
  const results = [];
355
362
  for (const slug of slugs) {
356
- results.push(await archiveOne(slug, ts));
363
+ results.push(await archiveOne(slug, ts, cwd));
357
364
  }
358
365
  const archived = results.filter((r) => r.ok).length;
359
366
  const archivedSlugs = results.filter((r) => r.ok).map((r) => r.slug);
@@ -13,7 +13,8 @@
13
13
  * verifyRun({ runDir, prdPath, queueEntry, allJobs }) → Promise<Verdict>
14
14
  *
15
15
  * Verdict shape:
16
- * { verdict: 'clean'|'halt'|'deps_unmet'|'transcript_errors'|'verify_unavailable',
16
+ * { verdict: 'clean'|'halt'|'deps_unmet'|'transcript_errors'|'verify_unavailable'
17
+ * |'no_verdict_sentinel'|'abandoned_background_task',
17
18
  * reason: string,
18
19
  * downgradeTo: 'pending'|'needs_review'|null }
19
20
  *
@@ -23,6 +24,8 @@
23
24
  * deps_unmet → 'pending' (re-fires when deps clear)
24
25
  * transcript_errors → 'needs_review' (requires human flip)
25
26
  * verify_unavailable → 'needs_review'
27
+ * no_verdict_sentinel → 'needs_review'
28
+ * abandoned_background_task → 'needs_review'
26
29
  *
27
30
  * Time complexity: O(N·L) where N = log line count, L = avg content line count.
28
31
  * The log is read once with readFileSync (700 KB typical; well within Node.js
@@ -231,6 +234,28 @@ function parseLog(logPath) {
231
234
  return { events, resultEvent, error: null, transcriptCommitLanded };
232
235
  }
233
236
 
237
+ // Stable, narrow substrings of the harness's auto-background tool_result text
238
+ // (Bash: "Command did not complete within its 120s timeout and was moved to
239
+ // the background... You will be notified when it completes."). Anchored on
240
+ // short distinctive fragments rather than the whole sentence so harness
241
+ // wording drift degrades to the generic no_verdict_sentinel verdict instead
242
+ // of throwing.
243
+ const BACKGROUND_TASK_MARKERS = ['moved to the background', 'You will be notified when it completes'];
244
+
245
+ /**
246
+ * True when any tool_result in the transcript carries the harness's
247
+ * auto-background marker text — i.e. a Bash command was auto-backgrounded
248
+ * after its foreground timeout and the run ended waiting for a notification
249
+ * a headless run structurally cannot receive.
250
+ */
251
+ function hasAbandonedBackgroundTask(events) {
252
+ for (const ev of events) {
253
+ if (ev.kind !== 'tool_result' || !ev.content) continue;
254
+ if (BACKGROUND_TASK_MARKERS.some((marker) => ev.content.includes(marker))) return true;
255
+ }
256
+ return false;
257
+ }
258
+
234
259
  // ─── self-recovery helpers ────────────────────────────────────────────────────
235
260
 
236
261
  /**
@@ -905,15 +930,30 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
905
930
  : undefined;
906
931
 
907
932
  // No pattern hits is not automatically "clean": a run that neither
908
- // committed anything nor ever emitted the finish-protocol sentinel likely
909
- // ended before doing real work (e.g. stopped on a clarifying question).
910
- // Weaker evidence than a caught transcript error, but still not clean.
933
+ // committed anything nor ever emitted the finish-protocol sentinel did not
934
+ // finish for SOME reason. When the transcript shows the harness's
935
+ // auto-background marker, that reason is knowable and cheap to recover
936
+ // from: the run backgrounded a long Bash command past its foreground
937
+ // timeout and ended waiting for a "you will be notified" callback a
938
+ // headless run can never receive — it did not necessarily fail to do the
939
+ // work, it failed to come back and commit it. That gets its own verdict
940
+ // (abandoned_background_task) so the auto-fix investigation checks the
941
+ // working tree for already-written work first, instead of re-planning the
942
+ // whole PRD from a wrong "stopped on a clarifying question" premise.
911
943
  if (sentinel === null && !commitEvidence) {
912
- issues.push({
913
- verdict: 'no_verdict_sentinel',
914
- reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — likely ended before the finish protocol (e.g. stopped on a clarifying question)',
915
- priority: 1,
916
- });
944
+ if (hasAbandonedBackgroundTask(events)) {
945
+ issues.push({
946
+ verdict: 'abandoned_background_task',
947
+ reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel, and the transcript shows a Bash command auto-backgrounded past its foreground timeout — the run likely ended waiting for a background-task notification a headless run cannot receive, not because the work failed; the work is probably already written and just needs to be committed',
948
+ priority: 1,
949
+ });
950
+ } else {
951
+ issues.push({
952
+ verdict: 'no_verdict_sentinel',
953
+ reason: 'run made no commit and emitted no SCHEDULER_VERDICT sentinel — the run ended before the finish protocol for some reason (stopped on a clarifying question, crashed, ran out of turns, or another cause not identifiable from this transcript alone)',
954
+ priority: 1,
955
+ });
956
+ }
917
957
  }
918
958
 
919
959
  // A truthful-looking PASS sentinel with no commit is still not "clean":
@@ -1122,4 +1162,5 @@ module.exports = {
1122
1162
  allDeliverablesAlreadyTracked,
1123
1163
  isAncestorCommit,
1124
1164
  isGitRepo,
1165
+ hasAbandonedBackgroundTask,
1125
1166
  };
@@ -19,6 +19,7 @@ const os = require('node:os');
19
19
  const path = require('node:path');
20
20
  const { splitFrontmatter } = require('../lib/prdFrontmatter.cjs');
21
21
  const { deriveEpicIdFromPrdPath } = require('../lib/prdLocations.cjs');
22
+ const { reportDanglingAgentTypeOnce } = require('../lib/prdAgentType.cjs');
22
23
 
23
24
  /**
24
25
  * Expand a PRD `cwd` value to an absolute path.
@@ -57,12 +58,19 @@ async function parsePrdRaw(filePath) {
57
58
  const m = base.match(/^(\d+)-/);
58
59
  return m ? Number(m[1]) : null;
59
60
  })();
61
+ const cwd = expandCwd(fm.cwd || null);
62
+
63
+ // Read-time tolerance for the agentType FK (throw on write, report on
64
+ // read — prdAgentType.cjs's header): a persona renamed/deleted after this
65
+ // PRD was written must not stop the PRD from loading. Best-effort only —
66
+ // never lets a logging failure affect parsing.
67
+ if (fm.agentType && cwd) reportDanglingAgentTypeOnce(cwd, fm.agentType);
60
68
 
61
69
  return {
62
70
  slug: base,
63
71
  path: filePath,
64
72
  title: fm.title || base,
65
- cwd: expandCwd(fm.cwd || null),
73
+ cwd,
66
74
  estimateMinutes: fm.estimateMinutes ? Number(fm.estimateMinutes) || null : null,
67
75
  parallelGroup: (fm.parallelGroup ? Number(fm.parallelGroup) || null : null) ?? groupFromName ?? 99,
68
76
  // Optional traceability back to the PromptTicket.id (PRD 748) that was
@@ -99,6 +107,15 @@ async function parsePrdRaw(filePath) {
99
107
  // quarantines it instead of queuing it to run.
100
108
  createdVia: fm.createdVia || null,
101
109
  issuedAt: fm.issuedAt || null,
110
+ // WHO executes this PRD (persona name) — distinct from `tag` (work
111
+ // type). Absent on every PRD authored before this field existed; the
112
+ // dependent follow-up PRD (scheduler-launch-as-persona) resolves this
113
+ // into a launch decision, which this module does not do.
114
+ agentType: fm.agentType || null,
115
+ // Opt-in exclusive-lease flag (PRD 1107, quietMachineLease.cjs +
116
+ // schedulerBatch.cjs's pickNextBatch). Only a literal `true` opts in —
117
+ // matches prdFrontmatter.cjs's applyKey semantics for this field.
118
+ quietMachine: fm.quietMachine === 'true',
102
119
  body: body.trim(),
103
120
  };
104
121
  }