claude-code-session-manager 0.122.0 → 0.123.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 (93) hide show
  1. package/dist/assets/{AgentLibrary-BmJ8hIPw.js → AgentLibrary-DxWN3FSN.js} +2 -2
  2. package/dist/assets/{Button-BS4kmz4p.js → Button-CISLFSKb.js} +1 -1
  3. package/dist/assets/DataModel-CYPbhGrx.js +1 -0
  4. package/dist/assets/{History-pg00b2Gd.js → History-Fhouektw.js} +2 -2
  5. package/dist/assets/{Hooks-BTEs0UZY.js → Hooks-COfIcyYV.js} +3 -3
  6. package/dist/assets/{Library-DuaJ-VSn.js → Library-Bt5WOyyu.js} +7 -7
  7. package/dist/assets/ListDetail-DH7uzYd5.js +1 -0
  8. package/dist/assets/{McpServers-Dy-wnG_X.js → McpServers-CwyVEzMN.js} +2 -2
  9. package/dist/assets/{Memory-Bs1T1Jo7.js → Memory-k9TWT9N4.js} +8 -8
  10. package/dist/assets/{Panel-8UhLXEOW.js → Panel-Di8JqCYN.js} +1 -1
  11. package/dist/assets/{Permissions-BfwzvmBS.js → Permissions-FFs6Rq1H.js} +3 -3
  12. package/dist/assets/{Plugins-McjyvvKh.js → Plugins-uK5vl_RE.js} +2 -2
  13. package/dist/assets/{ProvenanceBadge-D0Ov_6WJ.js → ProvenanceBadge-CGDyGDoN.js} +1 -1
  14. package/dist/assets/{SaveBar-Cd6hJR6K.js → SaveBar-Cnwc_XfF.js} +1 -1
  15. package/dist/assets/{Scheduler-BZXKqYJP.js → Scheduler-aACa6_mC.js} +7 -7
  16. package/dist/assets/{ScopeSwitcher-C24ysV1K.js → ScopeSwitcher-CoG3CSlC.js} +1 -1
  17. package/dist/assets/Settings-BG0RM6Di.js +4 -0
  18. package/dist/assets/{SkillReferenceGraph-BsWuiTbw.js → SkillReferenceGraph-COffGu_A.js} +1 -1
  19. package/dist/assets/Skills--qQpFjjf.js +2 -0
  20. package/dist/assets/SystemPrompt-Cr9RRSiZ.js +1 -0
  21. package/dist/assets/TagLibrary-D_ZbXwBv.js +1 -0
  22. package/dist/assets/{TiptapBody-C4kIvIMH.js → TiptapBody-CCHsa8Jv.js} +1 -1
  23. package/dist/assets/{Toggle-pP19zlyk.js → Toggle-BUOj4pws.js} +1 -1
  24. package/dist/assets/{ViewTabs-CpzN8e5I.js → ViewTabs-BdpsL7yr.js} +1 -1
  25. package/dist/assets/{historyTotals-Caq78bb5.js → historyTotals-Dx6ca881.js} +1 -1
  26. package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
  27. package/dist/assets/{index-Bjtguv6I.js → index-CkNGj8FA.js} +151 -151
  28. package/dist/assets/{settingsSchema-CC2ySu3d.js → settingsSchema-BZhkpr9e.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-hDhNo8jE.js → skillFrontmatter-N13KGqbs.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-BuOtY25U.js → useAgentsDirWatch-De9UsbDt.js} +1 -1
  31. package/dist/index.html +2 -2
  32. package/package.json +1 -1
  33. package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -24
  34. package/scripts/scheduler-mcp-server.cjs +142 -78
  35. package/src/main/agentLibrary.cjs +8 -3
  36. package/src/main/build-info.json +4 -4
  37. package/src/main/crashDiagnostics.cjs +24 -0
  38. package/src/main/historyAggregator.cjs +41 -7
  39. package/src/main/index.cjs +13 -3
  40. package/src/main/ipcSchemas.cjs +30 -22
  41. package/src/main/lib/activeSessions.cjs +93 -32
  42. package/src/main/lib/epic/delegationReadiness.cjs +2 -0
  43. package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
  44. package/src/main/lib/epic/epicMint.cjs +1 -1
  45. package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
  46. package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
  47. package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
  48. package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
  49. package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
  50. package/src/main/lib/git/gitWorktree.cjs +9 -4
  51. package/src/main/lib/git/worktreeDeps.cjs +13 -2
  52. package/src/main/lib/git/worktreeSetup.cjs +212 -0
  53. package/src/main/lib/mcpToolCatalog.cjs +36 -9
  54. package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
  55. package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
  56. package/src/main/lib/prd/prdAgentType.cjs +28 -0
  57. package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
  58. package/src/main/lib/prd/prdCreate.cjs +245 -27
  59. package/src/main/lib/prd/prdGateFiles.cjs +31 -0
  60. package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
  61. package/src/main/lib/scheduler/queueStore.cjs +5 -4
  62. package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
  63. package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
  64. package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
  65. package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
  66. package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
  67. package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
  68. package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
  69. package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
  70. package/src/main/pty.cjs +0 -53
  71. package/src/main/runVerify.cjs +38 -11
  72. package/src/main/scheduler/prdParser.cjs +3 -0
  73. package/src/main/scheduler/prdPaths.cjs +48 -2
  74. package/src/main/scheduler.cjs +135 -31
  75. package/src/main/seedDevPlugin.cjs +1 -1
  76. package/src/main/seedSchedulerMcp.cjs +1 -1
  77. package/src/main/templates/PRD_AUTHORING.md +8 -4
  78. package/src/main/webRemote.cjs +38 -132
  79. package/src/preload/api.d.ts +4 -52
  80. package/src/preload/index.cjs +0 -24
  81. package/src/seed/agents/dev-lead.md +2 -1
  82. package/src/seed/agents/validator.md +2 -1
  83. package/dist/assets/DataModel-DZHBZrfL.js +0 -1
  84. package/dist/assets/ListDetail-Cud72s-O.js +0 -1
  85. package/dist/assets/RemoteMachines-gOlFTvWq.js +0 -3
  86. package/dist/assets/Settings-DWnN2qxn.js +0 -4
  87. package/dist/assets/Skills-6pHn6glu.js +0 -2
  88. package/dist/assets/SystemPrompt-DT0LBH-K.js +0 -1
  89. package/dist/assets/TagLibrary-DbJ0zbYL.js +0 -1
  90. package/dist/assets/useRemotePeers-CNQmRBpt.js +0 -1
  91. package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
  92. package/src/main/remoteClient.cjs +0 -334
  93. package/src/main/remoteClientIpc.cjs +0 -133
@@ -82,6 +82,7 @@ const { detectRateLimitInLog } = require('./lib/claude/rateLimitDetect.cjs');
82
82
  const { stripAppOwnedChurn } = require('./lib/scheduler/jobDirtFilter.cjs');
83
83
  const { GATE_AUTHORITY_VERDICTS, decideGateAuthority } = require('./lib/scheduler/gateAuthority.cjs');
84
84
  const { decideBaselineAuthority } = require('./lib/scheduler/baselineGate.cjs');
85
+ const { formatSetupOutcome: formatWorktreeSetupOutcome, isSetupSettled: isWorktreeSetupSettled } = require('./lib/git/worktreeSetup.cjs');
85
86
  const { linkDependencyDirs } = require('./lib/git/worktreeDeps.cjs');
86
87
  const { checkMainDependencyHealth, repairMainDependencies } = require('./lib/git/depHealth.cjs');
87
88
  // cwd -> last main-checkout dependency repair attempt (ms); caps repair to once per cooldown.
@@ -107,6 +108,8 @@ const { appendResponseEventIfKnown, appendResponseEventWithReason } = require('.
107
108
  const { buildReviewNotice, selectDueReviewNotices, formatReviewNotice, holdMsFromEnv } = require('./lib/scheduler/reviewNotice.cjs');
108
109
  const epicValidationHook = require('./lib/epic/epicValidationHook.cjs');
109
110
  const prdCreate = require('./lib/prd/prdCreate.cjs');
111
+ const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
112
+ const { expandHome } = require('./lib/util/expandHome.cjs');
110
113
  const { parseValidationSentinels } = require('./lib/scheduler/validationSentinels.cjs');
111
114
  const { hasDownstreamValidator } = require('./lib/prd/planValidator.cjs');
112
115
  const promptSessionTranscript = require('./promptSessionTranscript.cjs');
@@ -187,6 +190,8 @@ const queueStore = require('./lib/scheduler/queueStore.cjs');
187
190
  const supervisorRecord = require('./lib/scheduler/jobSupervisorRecord.cjs');
188
191
  const adoptedRunSupervisor = require('./lib/scheduler/adoptedRunSupervisor.cjs');
189
192
  const { splitFrontmatter, parsePrdFile, serializePrdFile } = require('./lib/prd/prdFrontmatter.cjs');
193
+ const { applyStructuredPrdPatch } = require('./lib/prd/prdBodyPatch.cjs');
194
+ const { resolvePersonaDefaultEstimate, DEFAULT_PRD_AGENT_TYPE } = require('./lib/prd/prdAgentType.cjs');
190
195
  const { resolveDepSlug, findNearMatches } = require('./lib/prd/depSlugResolve.cjs');
191
196
  const { computeDispositionRewrite, mintPlanId, resolveInheritedPlanId } = require('./lib/prd/prdDisposition.cjs');
192
197
  const { allProjectCwds } = require('./lib/activeSessions.cjs');
@@ -539,7 +544,7 @@ function biasJobOomScore(pid) {
539
544
  async function recheckPrdSourceForJob(job) {
540
545
  const slug = job && job.slug;
541
546
  if (typeof slug !== 'string' || !slug || path.basename(slug) !== slug) return null;
542
- try { require('./lib/activeSessions.cjs').bustProjectCwdCache(); } catch { /* best-effort */ }
547
+ try { require('./lib/activeSessions.cjs').bustProjectCwdCache({ deep: true }); } catch { /* best-effort */ }
543
548
  const dirs = [...(await prdPaths.jobScopedPrdDirs(job))];
544
549
  const found = await prdPaths.findPrdDirForJob(job);
545
550
  if (found) dirs.push(found);
@@ -563,7 +568,7 @@ async function recheckPrdSourceForJob(job) {
563
568
  * fixture that leaked into the live queue and was cleaned up by its own
564
569
  * `finally` block). Retire the row rather than fail it.
565
570
  */
566
- function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, reason = 'prd-archived') {
571
+ function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, reason = 'prd-archived', extraMeta = {}) {
567
572
  const msg = reason === 'prd-missing'
568
573
  ? 'PRD source no longer exists on disk — retiring stale queue entry'
569
574
  : (() => {
@@ -575,7 +580,7 @@ function prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd,
575
580
  const finishedAt = Date.now();
576
581
  config.writeJsonSync(metaPath, {
577
582
  slug: job.slug, cwd, sessionId, exitCode: 0, skipped: reason,
578
- note: msg, startedAt, finishedAt, durationMs: 0, ...SCHEDULER_META_IDENTITY,
583
+ note: msg, startedAt, finishedAt, durationMs: 0, ...SCHEDULER_META_IDENTITY, ...extraMeta,
579
584
  });
580
585
  return { exitCode: 0, durationMs: 0, skipped: reason, note: msg, sessionId };
581
586
  }
@@ -1604,6 +1609,7 @@ async function reconcile(state) {
1604
1609
  // membership, so moving the file between Epic dirs must re-point the row.
1605
1610
  epicId: p.epicId ?? job.epicId ?? null,
1606
1611
  dependsOn: p.dependsOn,
1612
+ files: p.files ?? [],
1607
1613
  disposition: p.disposition ?? null,
1608
1614
  planId: p.planId ?? null,
1609
1615
  quietMachine: p.quietMachine === true,
@@ -1727,6 +1733,7 @@ async function reconcile(state) {
1727
1733
  sourceTabId: p.sourceTabId ?? inv.row?.sourceTabId ?? null,
1728
1734
  epicId: p.epicId ?? inv.row?.epicId ?? null,
1729
1735
  dependsOn: p.dependsOn,
1736
+ files: p.files ?? [],
1730
1737
  disposition: p.disposition ?? null,
1731
1738
  planId: p.planId ?? null,
1732
1739
  quietMachine: p.quietMachine === true,
@@ -1862,6 +1869,7 @@ async function reconcile(state) {
1862
1869
  sourceTabId: p.sourceTabId,
1863
1870
  epicId: p.epicId ?? null,
1864
1871
  dependsOn: p.dependsOn,
1872
+ files: p.files ?? [],
1865
1873
  disposition: p.disposition ?? null,
1866
1874
  planId: p.planId ?? null,
1867
1875
  quietMachine: p.quietMachine === true,
@@ -4205,13 +4213,14 @@ function ensureTimeoutShimOnce() {
4205
4213
  * Watchdogs are declared as an array; the result-tailer's exit-code mapping
4206
4214
  * (success+killedBySignal → 0) is scheduler-specific and lives in onExit.
4207
4215
  */
4208
- async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null, reviewInRun = true) {
4216
+ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget = null, foreignWip = null, launchEnv = null, onPhase = null, reviewInRun = true, worktreeSetup = null) {
4209
4217
  // First statement — stamps the 'exec-entered' dispatch-phase breadcrumb
4210
4218
  // before openLog below, so a hang inside openLog/spawn itself still shows
4211
4219
  // execution reached this function (see spawnJob's dispatchPhase comment).
4212
4220
  if (onPhase) await onPhase('exec-entered');
4213
4221
  const logPath = path.join(runDir, `${job.slug}.log`);
4214
4222
  const metaPath = path.join(runDir, `${job.slug}.meta.json`);
4223
+ const setupMeta = worktreeSetup ? { worktreeSetup } : {};
4215
4224
  // `cwd` stays the MAIN tree throughout — PRD lookup (findPrdDir/prdPathForJob)
4216
4225
  // and every git call below key off this value, never the worktree. Only the
4217
4226
  // spawned child's own process cwd (spawnCwd) may point at a job worktree —
@@ -4237,6 +4246,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4237
4246
 
4238
4247
  safeLog(`[scheduler] starting ${job.slug} at ${new Date().toISOString()}\n[scheduler] cwd=${cwd}` +
4239
4248
  (spawnCwd !== cwd ? ` (running isolated in worktree ${spawnCwd})` : '') + '\n\n');
4249
+ if (worktreeSetup) {
4250
+ try {
4251
+ const setupLines = formatWorktreeSetupOutcome(worktreeSetup);
4252
+ if (setupLines.length) safeLog(setupLines.join('\n') + '\n\n');
4253
+ } catch { /* logging must never break a run */ }
4254
+ }
4240
4255
 
4241
4256
  // Dead-cwd guard: verify the target directory exists and is traversable
4242
4257
  // before handing it to the child process. Checks spawnCwd (where the child
@@ -4249,7 +4264,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4249
4264
  // Sync write: this is an early-exit error path inside an async function,
4250
4265
  // so we could await, but using the sync variant keeps the error path
4251
4266
  // ordering identical to the spawn-failed branch below (also sync).
4252
- config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY });
4267
+ config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY, ...setupMeta });
4253
4268
  return { exitCode: -1, durationMs: 0, error: errMsg, sessionId };
4254
4269
  }
4255
4270
 
@@ -4327,12 +4342,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4327
4342
  // real failure. Anything else (malformed/unreadable PRD) is a real
4328
4343
  // failure and keeps exitCode: -1.
4329
4344
  if (await prdPaths.archivedTwinExists(job)) {
4330
- return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
4345
+ return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived', setupMeta);
4331
4346
  }
4332
4347
  if (e2 && e2.code === 'ENOENT') {
4333
4348
  const rc = await recheckBeforeMissingSkip();
4334
4349
  if (rc && rc !== 'resolved') return rc;
4335
- if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
4350
+ if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing', setupMeta);
4336
4351
  } else {
4337
4352
  safeLog(`[scheduler] failed to read PRD: ${e2?.message}\n`);
4338
4353
  closeFd();
@@ -4347,12 +4362,12 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4347
4362
  // than a hard failure. Anything else (a real read/parse error) still
4348
4363
  // fails the job.
4349
4364
  if (await prdPaths.archivedTwinExists(job)) {
4350
- return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived');
4365
+ return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-archived', setupMeta);
4351
4366
  }
4352
4367
  if (e && e.code === 'ENOENT') {
4353
4368
  const rc = await recheckBeforeMissingSkip();
4354
4369
  if (rc && rc !== 'resolved') return rc;
4355
- if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing');
4370
+ if (!rc) return prdArchivedSkipResult(job, cwd, sessionId, startedAt, safeLog, closeFd, metaPath, 'prd-missing', setupMeta);
4356
4371
  } else {
4357
4372
  safeLog(`[scheduler] failed to read PRD: ${e?.message}\n`);
4358
4373
  closeFd();
@@ -4437,7 +4452,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4437
4452
  if (!promptCheck.ok) {
4438
4453
  safeLog(`[scheduler] ${promptCheck.error}\n`);
4439
4454
  closeFd();
4440
- config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: promptCheck.error, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY });
4455
+ config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: promptCheck.error, startedAt, finishedAt: Date.now(), durationMs: 0, ...SCHEDULER_META_IDENTITY, ...setupMeta });
4441
4456
  return { exitCode: -1, durationMs: 0, error: promptCheck.error, sessionId };
4442
4457
  }
4443
4458
 
@@ -4603,7 +4618,8 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4603
4618
  // is calibrated against). `quietMachine` jobs and any PRD with an
4604
4619
  // explicit `budgetExempt: true` opt out entirely — logged once here so
4605
4620
  // an unbounded job is never silently unbounded.
4606
- const jobBudgetMs = budget.computeJobBudgetMs(job.estimateMinutes);
4621
+ const jobBudgetMs = budget.computeJobBudgetMs(
4622
+ job.estimateMinutes ?? resolvePersonaDefaultEstimate(job.cwd, job.agentType || DEFAULT_PRD_AGENT_TYPE));
4607
4623
  const budgetExempt = budget.isJobBudgetExempt(job);
4608
4624
  if (budgetExempt) {
4609
4625
  safeLog(`[scheduler] wall-clock budget watchdog EXEMPT for ${job.slug} ` +
@@ -4701,7 +4717,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4701
4717
  sl(`\n[scheduler] ${errMsg}\n`);
4702
4718
  // Sync write: inside a Promise executor callback; must flush meta
4703
4719
  // before resolve() so the spawnJob mutate() that follows sees it.
4704
- config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked, ...SCHEDULER_META_IDENTITY, originSessionId, contextDigestApplied });
4720
+ config.writeJsonSync(metaPath, { slug: job.slug, cwd, sessionId, exitCode: -1, error: errMsg, startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked, ...SCHEDULER_META_IDENTITY, ...setupMeta, originSessionId, contextDigestApplied });
4705
4721
  resolve({ exitCode: -1, durationMs, error: errMsg, leakedDescendants: leaked, sessionId });
4706
4722
  return;
4707
4723
  }
@@ -4763,7 +4779,7 @@ async function executeJob(job, runDir, defaultCwd, onPid, execCwd, resumeTarget
4763
4779
  startedAt, finishedAt: Date.now(), durationMs, leakedDescendants: leaked,
4764
4780
  agentResultSubtype, mappedFromSignal: mappedToSuccess ? signal || `code=${exitCode}` : null,
4765
4781
  killedByWatchdog: effectiveKilledByWatchdog, budgetKillReason,
4766
- ...SCHEDULER_META_IDENTITY,
4782
+ ...SCHEDULER_META_IDENTITY, ...setupMeta,
4767
4783
  originSessionId, contextDigestApplied,
4768
4784
  });
4769
4785
  resolve({
@@ -5722,6 +5738,23 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
5722
5738
  await broadcast({ flush: true });
5723
5739
  return;
5724
5740
  }
5741
+ // Early returns after the preflight worktree exists must not leak the
5742
+ // checkout or its cap count (retries would also re-run the whole setup).
5743
+ const releasePreflightWorktree = async () => {
5744
+ if (!preflightWorktree.ok) return;
5745
+ try {
5746
+ await jobWorktree.cleanupJobWorktree({
5747
+ cwd: job.cwd || defaultCwd,
5748
+ dir: preflightWorktree.dir,
5749
+ branch: preflightWorktree.branch,
5750
+ keepBranch: false,
5751
+ });
5752
+ } catch (err) {
5753
+ try {
5754
+ reportSchedulerError("preflight worktree cleanup failed", job.slug, err);
5755
+ } catch { /* reporting must never throw out of spawnJob */ }
5756
+ }
5757
+ };
5725
5758
  // Launch circuit breaker (lib/scheduler/launchFailure.cjs, issue #11). Re-evaluated
5726
5759
  // here, not just in tickQueue, because the block can change between the
5727
5760
  // pick and this dispatch (another job's probe just failed). 'blocked' →
@@ -5761,6 +5794,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
5761
5794
  });
5762
5795
  if (launchGate.state === 'blocked') {
5763
5796
  console.log(`[scheduler] ${job.slug}: deferring — ${launchGate.reason}`);
5797
+ await releasePreflightWorktree();
5764
5798
  await broadcast({ flush: true });
5765
5799
  return;
5766
5800
  }
@@ -5872,7 +5906,10 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
5872
5906
  }
5873
5907
  });
5874
5908
  await broadcast({ flush: true });
5875
- if (dispatchSkippedAlreadyCompleted) return;
5909
+ if (dispatchSkippedAlreadyCompleted) {
5910
+ await releasePreflightWorktree();
5911
+ return;
5912
+ }
5876
5913
 
5877
5914
  await mutate((s) => {
5878
5915
  const idx = s.jobs.findIndex((x) => x.slug === job.slug);
@@ -5940,6 +5977,19 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
5940
5977
  const worktree = preflightWorktree;
5941
5978
  if (worktree.ok) {
5942
5979
  console.log(`[scheduler] ${job.slug}: isolated in worktree ${worktree.dir} (branch ${worktree.branch})`);
5980
+ if (worktree.setup && !isWorktreeSetupSettled(worktree.setup)) {
5981
+ try {
5982
+ const cmds = (Array.isArray(worktree.setup.commands) ? worktree.setup.commands : [])
5983
+ .map((c) => `${c && c.cmd}: exit ${c && c.code} in ${(c && c.ms) || 0}ms`);
5984
+ appendError({
5985
+ cwd: job.cwd || defaultCwd,
5986
+ scope: 'scheduler',
5987
+ level: 'warn',
5988
+ message: `${job.slug}: worktree setup did not pass (${worktree.setup.reason})${cmds.length ? ' — ' + cmds.join('; ') : ''}`,
5989
+ meta: { slug: job.slug, cwd: job.cwd || defaultCwd, reason: worktree.setup.reason, commands: (worktree.setup.commands || []).map((c) => ({ cmd: c && c.cmd, code: c && c.code, ms: c && c.ms })) },
5990
+ });
5991
+ } catch { /* durable logging must never break dispatch */ }
5992
+ }
5943
5993
  } else {
5944
5994
  console.log(`[scheduler] ${job.slug}: running in main tree (worktree not used: ${worktree.reason})`);
5945
5995
  // A job losing worktree isolation must never be a silent downgrade
@@ -6045,7 +6095,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
6045
6095
  s.jobs[idx].dispatchPhaseAt = new Date().toISOString();
6046
6096
  }
6047
6097
  });
6048
- }, reviewInRun);
6098
+ }, reviewInRun, worktree.ok ? worktree.setup : { ran: false, ok: false, reason: 'in-place', waitedMs: 0, ms: 0, commands: [] });
6049
6099
  } finally {
6050
6100
  if (worktree.ok) {
6051
6101
  ({ worktreeLeftoverDirty, salvagePatch, worktreeIntegrationFailure, worktreeIntegrationDetail, mergeAutoResolved, mergeAutoResolvedPaths, integratedSha } =
@@ -8894,15 +8944,23 @@ async function runGateShadow(job) {
8894
8944
  } catch { ancestorOk = false; }
8895
8945
  const memo = job.gateShadow?.baseline;
8896
8946
  let baseOutcome = memo && memo.head === headBefore && memo.outcome ? memo.outcome : null;
8947
+ let baseSetup = baseOutcome ? (memo.setup ?? null) : null;
8897
8948
  if (!baseOutcome) {
8898
8949
  const scratch = path.join(os.tmpdir(), `sm-baseline-gate-${process.pid}-${Date.now()}`);
8899
8950
  let added = false;
8900
8951
  try {
8901
8952
  await execGitAt(job.cwd, ['worktree', 'add', '--detach', scratch, job.guardHeadBefore], { timeout: 120_000 });
8902
8953
  added = true;
8903
- await linkDependencyDirs({ mainCwd: job.cwd, worktreeCwd: scratch });
8904
- const r = await runGateSequence(gate.sequence, { cwd: scratch });
8905
- baseOutcome = r.status === 'busy' ? { status: 'unavailable', reason: 'busy', results: [] } : r;
8954
+ const deps = await linkDependencyDirs({ mainCwd: job.cwd, worktreeCwd: scratch });
8955
+ baseSetup = deps && deps.setup ? deps.setup : null;
8956
+ if (!isWorktreeSetupSettled(baseSetup)) {
8957
+ // A declared setup that did not pass can leave a broken cache (cold Godot
8958
+ // import); a base gate failing for that reason must not excuse HEAD's failures.
8959
+ baseOutcome = { status: 'unavailable', reason: `base-setup-${baseSetup?.reason ?? 'error'}`, results: [] };
8960
+ } else {
8961
+ const r = await runGateSequence(gate.sequence, { cwd: scratch });
8962
+ baseOutcome = r.status === 'busy' ? { status: 'unavailable', reason: 'busy', results: [] } : r;
8963
+ }
8906
8964
  } catch (e) {
8907
8965
  baseOutcome = { status: 'unavailable', reason: `base-gate-error: ${e && e.message ? e.message : e}`, results: [] };
8908
8966
  } finally {
@@ -8914,7 +8972,7 @@ async function runGateShadow(job) {
8914
8972
  }
8915
8973
  }
8916
8974
  baselineDecision = decideBaselineAuthority({ job, headOutcome: outcome, baseOutcome, evidenceOk, ancestorOk, cleanOk, headStable });
8917
- gateShadow.baseline = { head: headBefore, base: job.guardHeadBefore, outcome: baseOutcome, ...baselineDecision };
8975
+ gateShadow.baseline = { head: headBefore, base: job.guardHeadBefore, outcome: baseOutcome, setup: baseSetup, ...baselineDecision };
8918
8976
  }
8919
8977
 
8920
8978
  const runId = job.runId || reviewLadder.resolveRunId(job);
@@ -10358,12 +10416,20 @@ function toCompactPrdEntry(entry) {
10358
10416
  * `schedule:list-prds` IPC handler and the admin HTTP `GET
10359
10417
  * /admin/scheduler/prds` route (PRD 1024) — neither re-implements this scan.
10360
10418
  */
10361
- async function listPrdsInternal() {
10419
+ function normalizeListCwd(cwd) {
10420
+ if (typeof cwd !== 'string' || cwd.length === 0) return cwd;
10421
+ // Same normalization createPrd applies to frontmatter cwd: tilde expansion,
10422
+ // trailing-slash strip, Epic-worktree path -> main checkout.
10423
+ return resolveProjectContext({ cwd: expandHome(cwd) }).cwd || cwd;
10424
+ }
10425
+
10426
+ async function listPrdsInternal({ cwd: rawCwd } = {}) {
10362
10427
  ensureDirs();
10428
+ const cwd = normalizeListCwd(rawCwd);
10363
10429
  const out = [];
10364
10430
  const seenSlugs = new Set();
10365
10431
 
10366
- async function readDirInto(dir, { archived }) {
10432
+ async function readDirInto(dir, { archived, nested = false }) {
10367
10433
  let entries;
10368
10434
  try {
10369
10435
  entries = await fsp.readdir(dir);
@@ -10373,9 +10439,20 @@ async function listPrdsInternal() {
10373
10439
  }
10374
10440
  return;
10375
10441
  }
10376
- for (const name of entries) {
10377
- if (!name.endsWith('.md') || name.startsWith('.')) continue;
10378
- const filePath = path.join(dir, name);
10442
+ const files = entries.filter((n) => n.endsWith('.md') && !n.startsWith('.')).map((n) => path.join(dir, n));
10443
+ if (archived && !nested) {
10444
+ // prds-archived/<ISO-ts>/*.md — exactly one level down (mirrors
10445
+ // budget.cjs computeDepHistorySatisfaction); never recurse further.
10446
+ for (const name of entries) {
10447
+ if (name.endsWith('.md') || name.startsWith('.')) continue;
10448
+ const sub = path.join(dir, name);
10449
+ let st;
10450
+ try { st = await fsp.lstat(sub); } catch { continue; }
10451
+ if (st.isDirectory()) await readDirInto(sub, { archived, nested: true });
10452
+ }
10453
+ }
10454
+ for (const filePath of files) {
10455
+ const name = path.basename(filePath);
10379
10456
  try {
10380
10457
  const parsed = await parsePrd(filePath);
10381
10458
  // A slug can't be both live and archived at once, but a duplicate
@@ -10410,12 +10487,21 @@ async function listPrdsInternal() {
10410
10487
  // Live PRDs first, so an archived duplicate (shouldn't exist, but a
10411
10488
  // stale rename copy is possible) never shadows the still-runnable live
10412
10489
  // entry.
10413
- for (const dir of prdPaths.candidatePrdsDirs()) {
10490
+ // A non-empty cwd scopes the scan to that one project's dirs (cost = that
10491
+ // project's files, not every project's); no cwd = the machine-wide scan.
10492
+ const scoped = typeof cwd === 'string' && cwd.length > 0;
10493
+ const liveDirs = scoped
10494
+ ? [...new Set([schedulerPaths.prdsRoot(), prdPaths.prdDirForCwd(cwd), ...listEpicPrdDirs(cwd)])]
10495
+ : prdPaths.candidatePrdsDirs();
10496
+ const archivedDirs = scoped
10497
+ ? [...new Set(listArchivedPrdDirs(cwd))]
10498
+ : prdPaths.candidateArchivedPrdsDirs();
10499
+ for (const dir of liveDirs) {
10414
10500
  await readDirInto(dir, { archived: false });
10415
10501
  }
10416
10502
 
10417
10503
  const archivedStart = out.length;
10418
- for (const dir of prdPaths.candidateArchivedPrdsDirs()) {
10504
+ for (const dir of archivedDirs) {
10419
10505
  await readDirInto(dir, { archived: true });
10420
10506
  }
10421
10507
 
@@ -10658,9 +10744,10 @@ const remote = {
10658
10744
  // paging by offset can't skip or duplicate an entry between requests as
10659
10745
  // long as the underlying PRD set doesn't change mid-page.
10660
10746
  async listPrds(filter = {}) {
10661
- const all = await listPrdsInternal();
10747
+ const filterCwd = normalizeListCwd(filter.cwd);
10748
+ const all = await listPrdsInternal({ cwd: filterCwd });
10662
10749
  const filtered = all.filter((entry) => {
10663
- if (filter.cwd && entry.cwd !== filter.cwd) return false;
10750
+ if (filterCwd && entry.cwd !== filterCwd) return false;
10664
10751
  if (filter.epicId && entry.epicId !== filter.epicId) return false;
10665
10752
  if (filter.status && entry.status !== filter.status) return false;
10666
10753
  return true;
@@ -10748,7 +10835,15 @@ const remote = {
10748
10835
  // prdFrontmatter.cjs's parsePrdFile/serializePrdFile round-trip pair (PRD
10749
10836
  // 1024) so unrecognized keys (e.g. dependsOn) and untouched recognized
10750
10837
  // keys' original line formatting survive unchanged.
10751
- async updatePrd({ slug, cwd, frontmatter, body }) {
10838
+ async updatePrd({ slug, cwd, frontmatter, body, gate, files, acceptanceCriteria }) {
10839
+ const structured = {};
10840
+ if (gate !== undefined) structured.gate = gate;
10841
+ if (files !== undefined) structured.files = files;
10842
+ if (acceptanceCriteria !== undefined) structured.acceptanceCriteria = acceptanceCriteria;
10843
+ const hasStructured = Object.keys(structured).length > 0;
10844
+ if (hasStructured && body !== undefined) {
10845
+ return { ok: false, error: 'Pass either body (full replacement) or the structured fields gate/files/acceptanceCriteria, not both.' };
10846
+ }
10752
10847
  const job = await this.getJob(slug);
10753
10848
  const EDITABLE_JOB_STATUSES = new Set(['pending', 'quarantined', 'needs_review', 'failed', 'skipped']);
10754
10849
  if (job && !EDITABLE_JOB_STATUSES.has(job.status)) {
@@ -10828,13 +10923,22 @@ const remote = {
10828
10923
  fm[key] = frontmatter[key];
10829
10924
  }
10830
10925
  }
10831
- const newBody = body !== undefined ? body : origBody;
10926
+ let newBody = body !== undefined ? body : origBody;
10927
+ let patchWarnings = [];
10928
+ if (hasStructured) {
10929
+ const patched = applyStructuredPrdPatch(origBody, structured);
10930
+ if (!patched.ok) return { ok: false, error: patched.error };
10931
+ newBody = patched.body;
10932
+ patchWarnings = patched.warnings || [];
10933
+ }
10832
10934
  const newRaw = serializePrdFile(fm, newBody);
10833
10935
 
10834
10936
  try {
10835
10937
  await config.writeTextAtomic(filePath, newRaw, { writer: 'scheduler' });
10836
10938
  const stat = await fsp.stat(filePath);
10837
- return { ok: true, slug, bytesWritten: stat.size };
10939
+ return patchWarnings.length
10940
+ ? { ok: true, slug, bytesWritten: stat.size, warnings: patchWarnings }
10941
+ : { ok: true, slug, bytesWritten: stat.size };
10838
10942
  } catch (e) {
10839
10943
  return { ok: false, error: e?.message ?? 'write failed' };
10840
10944
  }
@@ -198,4 +198,4 @@ async function seedDevPlugin({
198
198
  }
199
199
  }
200
200
 
201
- module.exports = { seedDevPlugin, upgradeIfStale, marketplaceIsStale, compareSemver, markerPath, MAX_ATTEMPTS };
201
+ module.exports = { seedDevPlugin, writeMarker, PLUGIN_SLUG, MARKETPLACE, ENABLED_KEY, upgradeIfStale, marketplaceIsStale, compareSemver, markerPath, MAX_ATTEMPTS };
@@ -209,4 +209,4 @@ async function seedSchedulerMcp({ logger = console, addFn = runClaudeMcpAdd, rem
209
209
  }
210
210
  }
211
211
 
212
- module.exports = { seedSchedulerMcp, mcpAddArgv, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
212
+ module.exports = { seedSchedulerMcp, writeMarker, runClaudeMcpAdd, runClaudeMcpRemove, mcpAddArgv, markerPath, MAX_ATTEMPTS, serverScriptPath, SERVER_NAME };
@@ -1,4 +1,4 @@
1
- <!-- PRD_AUTHORING.md v9 -->
1
+ <!-- PRD_AUTHORING.md v13 -->
2
2
  # PRD Authoring Guide — Scheduler Safety Rules
3
3
 
4
4
  Rule: every PRD you queue follows the rules below.
@@ -69,7 +69,7 @@ if (!found) {
69
69
  Rule: every PRD needs these fields.
70
70
  1. `title` — one line, plain English.
71
71
  2. `cwd` — the Epic's project root, as an absolute path or `~/…`. The API always sets it to the Epic's project, whatever you pass. To work in another project, open an Epic in that project. The folder must exist when you queue.
72
- 3. `estimateMinutes` — optional; omit it. A run without one gets the 45-minute kill-budget floor. Pass one only if the run legitimately needs longer. Most PRDs take 5–10 minutes (§8).
72
+ 3. `estimateMinutes` — optional; omit it. A run without one gets its persona's `defaultEstimateMinutes`, else the 45-minute floor. Pass one only if the run legitimately needs longer. Most PRDs take 5–10 minutes (§8).
73
73
 
74
74
  `parallelGroup` is deprecated and ignored. `dependsOn: [<slug>, ...]` is the only ordering primitive.
75
75
 
@@ -100,12 +100,13 @@ Both fields are required in every `scheduler_create_prd` call.
100
100
  7. Keep each entry on one line, at most 500 chars, with plain spaces between words.
101
101
  8. Use `["none"]` only for docs or config with no runnable check. Write exactly `none`. Never mix `none` with commands.
102
102
  9. Put `NAME=value` words before `timeout`, never after it, for example `CI=1 timeout 300 npm test`. A leading `TMPDIR=$(mktemp -d) ` is allowed but not needed: the scheduler gives each gate its own TMPDIR.
103
+ 10. A gate command must not create or modify tracked files; write outputs to the per-gate TMPDIR or a gitignored path. Why: a dirtied tree fails the clean-tree check.
103
104
 
104
105
  ### files
105
106
 
106
107
  1. 1–50 repo-relative paths. A folder ends with `/`.
107
108
  2. No absolute paths, no `~` at the start, no `..` or `.` segments, no `*` or `?`, no `\` or backticks, no leading `-`. Use `/` between folders.
108
- 3. PRDs that can run at the same time must not share a file. If two PRDs touch the same file, chain them with `dependsOn`.
109
+ 3. The scheduler never runs two PRDs with overlapping files at once (held as `files-overlap`); `dependsOn` is for real output or order dependencies only.
109
110
 
110
111
  ### What the API writes
111
112
 
@@ -125,7 +126,7 @@ A short Epic-context digest is prepended automatically for orientation only —
125
126
 
126
127
  The executor can't ask you anything: no `ScheduleWakeup`, Cron, Monitor, `AskUserQuestion`, plan mode, or worktree tools; background tasks are off (a `timeout` command is always on PATH — the app installs a shim on macOS). It must never stop to ask a question — it makes the safest reasonable call and reports it, so write the PRD so that call is obvious.
127
128
 
128
- Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `.venv`/`venv` (and `node_modules` for non-pnpm projects) symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store. Why: a pnpm install through the symlink purged the main checkout's node_modules (Bilko-Game-Academy 2026-10-18). A step that needs gitignored runtime state (databases, `downloads/`, browser profiles, logs) or a live side effect must address the main checkout by absolute path (e.g. `$HOME/Projects/<repo>/...`), be artifact-only (a deliverable artifact + `artifactPaths`, no tracked edits), and `dependsOn` the PRD that lands its tracked edits. Host crontab installs stay human-only (see §14).
129
+ Worktree reach: every PRD runs in an isolated git worktree holding tracked files plus `node_modules` and non-uv `.venv`/`venv` symlinked from the main checkout; pnpm projects (pnpm-lock.yaml) get a worktree-local `node_modules` installed offline from the store, and uv projects (uv.lock) get a worktree-local env built by an offline frozen `uv sync`, not a symlink. Why: a pnpm install through the symlink purged the main checkout's node_modules (Bilko-Game-Academy 2026-10-18). A step that needs gitignored runtime state (databases, `downloads/`, browser profiles, logs) or a live side effect must address the main checkout by absolute path (e.g. `$HOME/Projects/<repo>/...`), be artifact-only (a deliverable artifact + `artifactPaths`, no tracked edits), and `dependsOn` the PRD that lands its tracked edits. A project may declare `worktreeSetup` gate-syntax commands in a tracked `.session-manager.json` at the repo root; they run once per fresh job worktree and baseline scratch worktree, before the executor starts, with cwd = the worktree root (a project in a subfolder names it, e.g. `timeout 600 godot --headless --path game --import`). Setups run one at a time per project and are never skipped; they must write only inside the worktree (node_modules and non-uv venvs are symlinks into the main checkout). The outcome is the `[scheduler] worktree setup:` lines at the top of the run log plus `worktreeSetup` in the run's meta.json. Host crontab installs stay human-only (see §14).
129
130
  Why: burrow PRD 1516 halted because its relative `.venv` python against `downloads/fb-tracker.db`, `scripts/install-cron.sh` and `pipeline-cron.sh --now` resolved inside the worktree (no runtime state; install-cron would bake a temporary worktree path into crontab), while sibling 1494 succeeded the same day using an absolute main-checkout path and artifact-only output. `scheduler_create_prd` warns on relative gitignored paths.
130
131
 
131
132
  ---
@@ -150,6 +151,8 @@ npm test || { echo "HALT: npm test failed — see above"; exit 1; }
150
151
 
151
152
  Note: a `rateLimited` exit-1 is the scheduler's own benign auto-pause (it resumes at the next 5h reset) — don't engineer retry logic for it.
152
153
 
154
+ Note: `HALT:` in a script's output, a PRD, or a report is plain text to the scheduler. Only the verdict line decides the outcome; a report line that starts with `HALT:` re-queues the job only when the report has no `SCHEDULER_VERDICT` line. Why: the verifier used to match `HALT:` anywhere in a report, so finished PASS runs that only described a tool's failure path were re-queued and parked (Rabbits-Game `163-attack-scorecard`, 2026-10-24).
155
+
153
156
  ---
154
157
 
155
158
  ## §10 Negative-assertion checks must exit 0 on the clean case
@@ -224,3 +227,4 @@ Before queueing a new PRD, verify each of these:
224
227
  - [ ] **§11 End green:** the gate runs last; any intentional failure runs early and is captured.
225
228
  - [ ] **Gate baseline:** every gate command ran green once at HEAD in the main checkout; a red suite is scoped to the PRD's own test files and the PRD notes say why. Why: a gate already red on base parks as `sentinel_fail`.
226
229
  - [ ] **No human-only gates:** no gate or criterion needs a human or the passage of time (cron install, live post wait, live payment, dashboard action); those end `SCHEDULER_VERDICT: NEEDS_HUMAN <question>`. Why: no executor can pass them.
230
+ - [ ] **Gate purity:** a gate command must not create or modify tracked files; outputs go to the per-gate TMPDIR or a gitignored path. Why: a dirtied tree fails the clean-tree check.