claude-code-session-manager 0.116.1 → 0.118.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 (65) hide show
  1. package/dist/assets/{AgentLibrary-CkS69HnN.js → AgentLibrary-CEI6vsmu.js} +2 -2
  2. package/dist/assets/{Button-BQxkfIbW.js → Button-BuyzGzO8.js} +1 -1
  3. package/dist/assets/{DataModel-YVgdJTZF.js → DataModel-CbXI_rEf.js} +1 -1
  4. package/dist/assets/{History-DneHh4jI.js → History-axy4nJyY.js} +2 -2
  5. package/dist/assets/{Hooks-Bt4xIjfo.js → Hooks-D5ghOppL.js} +3 -3
  6. package/dist/assets/{Library-DoP5FdXq.js → Library-wBI4Fwh3.js} +7 -7
  7. package/dist/assets/{ListDetail-BYxyl13e.js → ListDetail-CY5hhwn9.js} +1 -1
  8. package/dist/assets/{McpServers-CKm_iZsf.js → McpServers-CwEVZqfu.js} +2 -2
  9. package/dist/assets/Memory-Beh2r4Xk.js +25 -0
  10. package/dist/assets/{Panel-Cv8aYElQ.js → Panel-yOYf80ax.js} +1 -1
  11. package/dist/assets/{Permissions-CFmWwqyV.js → Permissions-DA-SMXO3.js} +3 -3
  12. package/dist/assets/{Plugins-CuYNgf_d.js → Plugins-qfKt6BxH.js} +2 -2
  13. package/dist/assets/ProvenanceBadge-DFyBe7Po.js +1 -0
  14. package/dist/assets/RemoteMachines-AG0K7kqS.js +3 -0
  15. package/dist/assets/{SaveBar-Cb4Q8TBK.js → SaveBar-35Nw2YsZ.js} +1 -1
  16. package/dist/assets/{Scheduler-ysJHQvW4.js → Scheduler-BPBAU0TO.js} +8 -8
  17. package/dist/assets/{ScopeSwitcher-BTOWfifI.js → ScopeSwitcher-BCXNrrrs.js} +1 -1
  18. package/dist/assets/{Settings-C6fH8diZ.js → Settings-C4yhzfZI.js} +4 -4
  19. package/dist/assets/{SkillReferenceGraph-esAcniTQ.js → SkillReferenceGraph-Ca5z2avC.js} +1 -1
  20. package/dist/assets/{Skills-4_vqzrSF.js → Skills-5WWusmKG.js} +1 -1
  21. package/dist/assets/SystemPrompt-ofVCdHAW.js +1 -0
  22. package/dist/assets/TagLibrary-rSrpD-Bz.js +1 -0
  23. package/dist/assets/{TiptapBody-EkZlmD9k.js → TiptapBody-DAYcdcVt.js} +1 -1
  24. package/dist/assets/{Toggle-CpR97XGM.js → Toggle-BzHdpXZI.js} +1 -1
  25. package/dist/assets/{ViewTabs-ekJfFek8.js → ViewTabs-D82jJ4VS.js} +1 -1
  26. package/dist/assets/{historyTotals-Bg0GVcsF.js → historyTotals-DMGFqK5h.js} +1 -1
  27. package/dist/assets/{index-CZsDDyLy.css → index-DAMBm3UN.css} +1 -1
  28. package/dist/assets/{index-DQSEUdx0.js → index-DZ5TJKNI.js} +501 -500
  29. package/dist/assets/{settingsSchema-BG5jZbhn.js → settingsSchema-BfnNE6ne.js} +1 -1
  30. package/dist/assets/{skillFrontmatter-DF6xI6Wb.js → skillFrontmatter-ED1DjPE1.js} +1 -1
  31. package/dist/assets/{useAgentsDirWatch-Dox-xCx9.js → useAgentsDirWatch-CbpQIu3P.js} +1 -1
  32. package/dist/assets/{useRemotePeers-DHo62LbA.js → useRemotePeers-Dy_o0WlY.js} +1 -1
  33. package/dist/index.html +2 -2
  34. package/package.json +1 -1
  35. package/plugins/session-manager-dev/skills/develop/SKILL.md +23 -10
  36. package/scripts/scheduler-mcp-server.cjs +16 -1
  37. package/src/main/build-info.json +4 -4
  38. package/src/main/lib/epic/epicSettle.cjs +105 -0
  39. package/src/main/lib/epic/epicSweepValidator.cjs +74 -0
  40. package/src/main/lib/epic/epicValidationHook.cjs +171 -141
  41. package/src/main/lib/feedback-remote/macroAdminRoutes.cjs +2 -1
  42. package/src/main/lib/git/gitWorktree.cjs +7 -0
  43. package/src/main/lib/git/worktreeDeps.cjs +77 -0
  44. package/src/main/lib/mcpToolCatalog.cjs +6 -2
  45. package/src/main/lib/memoryMdNetwork.cjs +121 -0
  46. package/src/main/lib/memoryRefResolve.cjs +64 -0
  47. package/src/main/lib/memoryStale.cjs +27 -11
  48. package/src/main/lib/persona-macro/macroLibrary.cjs +49 -2
  49. package/src/main/lib/scheduler/baselineGate.cjs +128 -0
  50. package/src/main/lib/scheduler/definitionOfDone.cjs +9 -3
  51. package/src/main/lib/scheduler/needsReviewLedger.cjs +1 -1
  52. package/src/main/lib/scheduler/reviewNotice.cjs +53 -0
  53. package/src/main/memoryAssist.cjs +71 -31
  54. package/src/main/memoryTool.cjs +18 -6
  55. package/src/main/promptSessionEvents.cjs +6 -2
  56. package/src/main/runVerify.cjs +39 -3
  57. package/src/main/scheduler/reviewLadder.cjs +25 -6
  58. package/src/main/scheduler.cjs +121 -62
  59. package/src/main/templates/PRD_AUTHORING.md +2 -0
  60. package/src/preload/api.d.ts +24 -2
  61. package/dist/assets/Memory-BWMQuAoC.js +0 -18
  62. package/dist/assets/ProvenanceBadge-BMLVPyHr.js +0 -1
  63. package/dist/assets/RemoteMachines-D04Mby_W.js +0 -3
  64. package/dist/assets/SystemPrompt-CdUq4KXx.js +0 -1
  65. package/dist/assets/TagLibrary-CVFIphBv.js +0 -1
@@ -32,6 +32,7 @@ const config = require('./config.cjs');
32
32
 
33
33
  const { MEMORY_SLUG_RE: SLUG_RE } = require('./lib/memorySlug.cjs');
34
34
  const { scoreMemories } = require('./lib/memoryStale.cjs');
35
+ const { makeRefResolver, listTrackedFiles } = require('./lib/memoryRefResolve.cjs');
35
36
  const { addIndexLine, removeIndexLines, indexHealth } = require('./lib/memoryIndex.cjs');
36
37
  const {
37
38
  resolveWorkspaceMemoryDir,
@@ -318,13 +319,24 @@ async function stale({ workspace, cwd, agent }) {
318
319
  baseDir = null;
319
320
  }
320
321
  }
321
- const existsPath = baseDir
322
- ? (relPath) => {
323
- const full = path.resolve(baseDir, relPath);
324
- if (full !== baseDir && !full.startsWith(baseDir + path.sep)) return false;
325
- return fs.existsSync(full);
322
+ let existsPath = () => true;
323
+ if (baseDir) {
324
+ const resolveRef = makeRefResolver({ cwd: baseDir, trackedFiles: await listTrackedFiles(baseDir) });
325
+ existsPath = (relPath, roots = []) => {
326
+ const full = path.resolve(baseDir, relPath);
327
+ if (full !== baseDir && !full.startsWith(baseDir + path.sep)) return false;
328
+ const safeRoots = [];
329
+ for (const root of roots) {
330
+ try {
331
+ // Validate the expanded form so `~/x` is checked against the home boundary too.
332
+ const expanded = root === '~' || root.startsWith('~/') ? path.join(os.homedir(), root.slice(1)) : root;
333
+ config.validatePath(expanded);
334
+ safeRoots.push(root);
335
+ } catch { /* skip an out-of-bounds root */ }
326
336
  }
327
- : () => true;
337
+ return resolveRef(relPath, safeRoots);
338
+ };
339
+ }
328
340
 
329
341
  const entries = [];
330
342
  for (const name of names) {
@@ -69,7 +69,9 @@ function mintEventId() {
69
69
  * meta arg (not a fifth positional parameter) with `validation`: a
70
70
  * scheduler check-in is born `'unvalidated'` — never `'verified'` —
71
71
  * regardless of the job's self-reported outcome; the authoring Epic's own
72
- * validation pass is what may later move it to 'verified'/'refuted'. All
72
+ * validation pass is what may later move it to 'verified'/'refuted'. `agentType`
73
+ * (the job's agent) and `digestKey` (the settle digest's stable key) ride the
74
+ * same meta arg and are copied the same way. All
73
75
  * keys are added to the event object only when present in `meta`, never as
74
76
  * `undefined`-valued keys, so events appended without them serialize
75
77
  * identically to before.
@@ -84,7 +86,7 @@ async function appendResponseEventWithReason(cwd, sourcePromptId, text, meta = {
84
86
  const data = result.data;
85
87
  const session = data.sessions && data.sessions[sourcePromptId];
86
88
  if (!session) return { ok: false, reason: 'no-session' };
87
- if (session.status !== 'active') return { ok: false, reason: 'not-active' };
89
+ if (session.status !== 'active' && !(meta && meta.allowInactive === true)) return { ok: false, reason: 'not-active' };
88
90
  const events = (data.events && data.events[sourcePromptId]) || [];
89
91
  const tail = events.length > 0 ? events[events.length - 1] : null;
90
92
  if (!tail) return { ok: false, reason: 'no-events' };
@@ -99,6 +101,8 @@ async function appendResponseEventWithReason(cwd, sourcePromptId, text, meta = {
99
101
  if (meta && meta.prdSlug) event.prdSlug = meta.prdSlug;
100
102
  if (meta && meta.outcome) event.outcome = meta.outcome;
101
103
  if (meta && meta.validation) event.validation = meta.validation;
104
+ if (meta && meta.agentType) event.agentType = meta.agentType;
105
+ if (meta && meta.digestKey) event.digestKey = meta.digestKey;
102
106
  data.events[sourcePromptId] = [...events, event];
103
107
  await config.writeJson(path, data, { writer: 'scheduler' });
104
108
  broadcast(EVENT_APPENDED_CHANNEL, { cwd, promptSessionId: sourcePromptId, event });
@@ -526,17 +526,17 @@ function checkDeps(queueEntry, allJobs, prdBody) {
526
526
  // ─── sentinel scanner ─────────────────────────────────────────────────────────
527
527
 
528
528
  /**
529
- * Scan for a `SCHEDULER_VERDICT: PASS|FAIL|BLOCKED_BY_FOREIGN_WIP` sentinel
529
+ * Scan for a `SCHEDULER_VERDICT: PASS|FAIL|BLOCKED_BY_FOREIGN_WIP|NEEDS_HUMAN` sentinel
530
530
  * line in the run output.
531
531
  *
532
532
  * Checks `resultEvent.resultText` first (the agent's final message), then the
533
533
  * last tool_result content. Anchored to line-start so prose mentioning the
534
534
  * string in mid-sentence does not match.
535
535
  *
536
- * Returns 'pass', 'fail', 'blocked_by_foreign_wip', or null.
536
+ * Returns 'pass', 'fail', 'blocked_by_foreign_wip', 'needs_human', or null.
537
537
  */
538
538
  function scanSentinel(resultEvent, events) {
539
- const RE = /^SCHEDULER_VERDICT:\s*(PASS|FAIL|BLOCKED_BY_FOREIGN_WIP)\b/m;
539
+ const RE = /^SCHEDULER_VERDICT:\s*(PASS|FAIL|BLOCKED_BY_FOREIGN_WIP|NEEDS_HUMAN)\b/m;
540
540
 
541
541
  if (resultEvent) {
542
542
  const m = resultEvent.resultText.match(RE);
@@ -577,6 +577,28 @@ function scanSentinelReason(resultEvent, events) {
577
577
  return '';
578
578
  }
579
579
 
580
+ /**
581
+ * One-line question after `SCHEDULER_VERDICT: NEEDS_HUMAN` (same two-source
582
+ * scan order as scanSentinel). Trimmed, capped at 300 chars; '' when absent.
583
+ */
584
+ function scanHumanQuestion(resultEvent, events) {
585
+ const RE = /^SCHEDULER_VERDICT:\s*NEEDS_HUMAN\b[ \t]*([^\n]*)/m;
586
+ const clip = (m) => m[1].trim().slice(0, 300);
587
+ if (resultEvent && resultEvent.resultText) {
588
+ const m = resultEvent.resultText.match(RE);
589
+ if (m) return clip(m);
590
+ }
591
+ let lastToolResult = null;
592
+ for (const ev of events) {
593
+ if (ev.kind === 'tool_result') lastToolResult = ev;
594
+ }
595
+ if (lastToolResult && lastToolResult.content) {
596
+ const m = lastToolResult.content.match(RE);
597
+ if (m) return clip(m);
598
+ }
599
+ return '';
600
+ }
601
+
580
602
  /**
581
603
  * Scan for a `FOREIGN_WIP_PATHS: <comma-separated paths>` evidence line —
582
604
  * the mandatory companion to a `SCHEDULER_VERDICT: BLOCKED_BY_FOREIGN_WIP`
@@ -1121,6 +1143,19 @@ async function verifyRun({
1121
1143
  );
1122
1144
  }
1123
1145
 
1146
+ // The executor needs a decision/action only the human can take. Parks as
1147
+ // needs_review with the question; every automatic retry rung skips it.
1148
+ if (sentinel === 'needs_human') {
1149
+ const humanQuestion = scanHumanQuestion(resultEvent, events);
1150
+ const kept = [...annotations, ...issues.map((i) => ({ verdict: i.verdict, reason: i.reason }))];
1151
+ return conclude(
1152
+ 'needs_human',
1153
+ 'executor reported SCHEDULER_VERDICT: NEEDS_HUMAN' + (humanQuestion ? ': ' + humanQuestion : ''),
1154
+ 'needs_review',
1155
+ { sentinel, humanQuestion, ...(kept.length ? { annotations: kept } : {}) },
1156
+ );
1157
+ }
1158
+
1124
1159
  // Ground truth outranks heuristics: an exit-0 run that landed a commit
1125
1160
  // ATTRIBUTABLE TO THIS RUN (spawnJob's own guardHeadBefore/headAtExit
1126
1161
  // delta, or the repo-wide committedDuringRun window scan) and never
@@ -1420,6 +1455,7 @@ module.exports = {
1420
1455
  parseLog,
1421
1456
  scanSentinel,
1422
1457
  scanSentinelReason,
1458
+ scanHumanQuestion,
1423
1459
  scanForeignWipPathsClaim,
1424
1460
  isMergeMainSlug,
1425
1461
  extractMergeMainPrNumber,
@@ -34,9 +34,15 @@ const { DEFAULT_PROJECT_CWD } = require('../lib/scheduler/schedulerBatch.cjs');
34
34
  * Kill-switch: SM_RESUME_RECOVERY_DISABLE=1 restores today's behaviour
35
35
  * exactly (always returns null), mirroring SM_RCA_DISABLE/SM_DOD_DISABLE.
36
36
  */
37
+ /** A row parked on a question only the human can answer — no automatic rung may touch it. */
38
+ function isNeedsHuman(job) {
39
+ return !!job && job.verifierVerdict === 'needs_human';
40
+ }
41
+
37
42
  function selectResumeRecoveryTarget(job) {
38
43
  if (process.env.SM_RESUME_RECOVERY_DISABLE === '1') return null;
39
44
  if (!job || job.status !== 'needs_review') return null;
45
+ if (isNeedsHuman(job)) return null;
40
46
  if (job.verifierVerdict !== 'uncommitted_changes') return null;
41
47
  if (typeof job.sessionId !== 'string' || job.sessionId.length === 0) return null;
42
48
  if (job.resumeRecoveryAttempted === true) return null;
@@ -100,6 +106,7 @@ function isMechanicalRecoveryFutile(job, currentHeadSha) {
100
106
  function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
101
107
  if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
102
108
  if (!job || job.status !== 'needs_review') return null;
109
+ if (isNeedsHuman(job)) return null;
103
110
  if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
104
111
  if (job.mechanicalRecoveryAttempted === true) return null;
105
112
  if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
@@ -791,6 +798,7 @@ function selectExhaustedNeedsReviewTargets(jobs, now, thresholdMs) {
791
798
  const targets = [];
792
799
  for (const j of jobs ?? []) {
793
800
  if (j.status !== 'needs_review') continue;
801
+ if (isNeedsHuman(j)) continue;
794
802
  if (!isEligibleForNeedsReviewAutoResolve(j, jobs)) continue;
795
803
  if ((j.exhaustedResolveAttempts ?? 0) > NEEDS_REVIEW_RESOLVE_CAP) continue;
796
804
  const history = j.statusHistory || [];
@@ -833,6 +841,7 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
833
841
  const slugsInQueue = new Set(jobs.map((j) => j.slug));
834
842
  return jobs.filter((job) => {
835
843
  if (job.status !== 'needs_review') return false;
844
+ if (isNeedsHuman(job)) return false;
836
845
  // A job parked here because it was blocked by a sibling's foreign WIP
837
846
  // three times in a row (never its own regression) has nothing for a
838
847
  // fix-plan investigation to diagnose — there is no code defect to
@@ -926,7 +935,7 @@ function isEligibleForImmediateAutoFix(job, allJobs, fixSlugExists) {
926
935
  *
927
936
  * Returns the first tier-1 row, else the tier-2 row, else null.
928
937
  */
929
- function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false } = {}) {
938
+ function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false, baselineDisabled = process.env.SM_BASELINE_GATE_AUTHORITY_DISABLE === '1' } = {}) {
930
939
  const eligible = jobs.filter((j) => {
931
940
  if (j.status !== 'needs_review') return false;
932
941
  const head = headByCwd.get(j.cwd);
@@ -938,16 +947,25 @@ function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false } =
938
947
  return !j.gateShadow || j.gateShadow.head !== head || j.gateShadow.definitive !== true;
939
948
  };
940
949
 
941
- if (!authorityDisabled) {
950
+ // Baseline authority: a sentinel_fail park with a landed commit and a known
951
+ // base commit may complete when its red gate was already red at that base.
952
+ const baselineEligible = (j) => !baselineDisabled
953
+ && j.verifierVerdict === 'sentinel_fail'
954
+ && typeof j.landedCommit === 'string' && j.landedCommit.length > 0
955
+ && typeof j.guardHeadBefore === 'string' && j.guardHeadBefore.length > 0;
956
+
957
+ if (!authorityDisabled || !baselineDisabled) {
942
958
  const ranAtMs = (j) => {
943
959
  const t = j.gateShadow ? Date.parse(j.gateShadow.ranAt) : NaN;
944
960
  return Number.isFinite(t) ? t : -Infinity; // never-run or unparseable sorts oldest
945
961
  };
946
962
  const tier1 = eligible.filter((j) => isStale(j)
947
- && j.verifierVerdict
948
- && GATE_AUTHORITY_VERDICTS.includes(j.verifierVerdict)
949
- && typeof j.landedCommit === 'string'
950
- && j.landedCommit.length > 0);
963
+ && ((!authorityDisabled
964
+ && j.verifierVerdict
965
+ && GATE_AUTHORITY_VERDICTS.includes(j.verifierVerdict)
966
+ && typeof j.landedCommit === 'string'
967
+ && j.landedCommit.length > 0)
968
+ || baselineEligible(j)));
951
969
  if (tier1.length) {
952
970
  const ranked = tier1
953
971
  .map((j, i) => ({ j, i, neverRun: !j.gateShadow, ranAt: ranAtMs(j) }))
@@ -964,6 +982,7 @@ function selectGateShadowTarget(jobs, headByCwd, { authorityDisabled = false } =
964
982
  }
965
983
 
966
984
  module.exports = {
985
+ isNeedsHuman,
967
986
  selectResumeRecoveryTarget,
968
987
  MECHANICALLY_RESOLVABLE_VERDICTS,
969
988
  isMechanicalRecoveryFutile,
@@ -79,6 +79,8 @@ const { sweepStrandedJobBranches } = require('./lib/git/branchSweep.cjs');
79
79
  const { detectRateLimitInLog } = require('./lib/claude/rateLimitDetect.cjs');
80
80
  const { stripAppOwnedChurn } = require('./lib/scheduler/jobDirtFilter.cjs');
81
81
  const { GATE_AUTHORITY_VERDICTS, decideGateAuthority } = require('./lib/scheduler/gateAuthority.cjs');
82
+ const { decideBaselineAuthority } = require('./lib/scheduler/baselineGate.cjs');
83
+ const { linkDependencyDirs } = require('./lib/git/worktreeDeps.cjs');
82
84
  const { resolveBindingRateLimitReset } = require('./lib/claude/rateLimitWindow.cjs');
83
85
  const { isResetFresh, bindingWindow, degradedBudget, meterFailureBudget } = require('./lib/claude/usageCircuit.cjs');
84
86
  const { computeQueueHealth } = require('./lib/scheduler/queueHealth.cjs');
@@ -97,7 +99,8 @@ const sessionsStore = require('./sessionsStore.cjs');
97
99
  const { enqueueExternalPrompt } = require('./chatRunner.cjs');
98
100
  const { appendResponseEventIfKnown, appendResponseEventWithReason } = require('./promptSessionEvents.cjs');
99
101
  const { buildReviewNotice, selectDueReviewNotices, formatReviewNotice, holdMsFromEnv } = require('./lib/scheduler/reviewNotice.cjs');
100
- const { maybeEnqueueValidationPrompt } = require('./lib/epic/epicValidationHook.cjs');
102
+ const epicValidationHook = require('./lib/epic/epicValidationHook.cjs');
103
+ const prdCreate = require('./lib/prd/prdCreate.cjs');
101
104
  const { parseValidationSentinels } = require('./lib/scheduler/validationSentinels.cjs');
102
105
  const { hasDownstreamValidator } = require('./lib/prd/planValidator.cjs');
103
106
  const promptSessionTranscript = require('./promptSessionTranscript.cjs');
@@ -347,12 +350,18 @@ ${verdictStepNum}. VERDICT LINE — end your final report with exactly one of th
347
350
  SCHEDULER_VERDICT: PASS
348
351
  SCHEDULER_VERDICT: FAIL <one-line reason>
349
352
  SCHEDULER_VERDICT: BLOCKED_BY_FOREIGN_WIP
353
+ SCHEDULER_VERDICT: NEEDS_HUMAN <one-line question>
350
354
  - Print PASS only when the gate is green AND the commit from step ${commitStepNum} landed.
351
355
  - Print FAIL when the gate is red or the commit could not land.
352
356
  - Never print PASS on a red gate. Why: a false PASS ships broken work that
353
357
  no one notices. A true PASS plus a landed commit lets the scheduler ignore
354
358
  harmless noise in your transcript, such as grep hits for "Error", an early
355
359
  red test run, or debug tracebacks.
360
+ - Print NEEDS_HUMAN only when finishing needs a decision or action only the
361
+ human can take (credentials, an account/dashboard action, a product
362
+ choice, an install on the host) — never for a red gate or a bug you could
363
+ fix. Commit any finished work first. Phrase the question so a yes/no or
364
+ one-line answer unblocks it.
356
365
  - Print BLOCKED_BY_FOREIGN_WIP ONLY when your gate failed because of a
357
366
  sibling job's unfinished, uncommitted file — never because of your own
358
367
  change — AND every failing path is one this prompt already listed as
@@ -2664,6 +2673,7 @@ function resetJobFields(job, errorMsg, opts = {}) {
2664
2673
  job.error = errorMsg ?? null;
2665
2674
  delete job.runtime;
2666
2675
  delete job.verifierVerdict;
2676
+ delete job.humanQuestion;
2667
2677
  delete job.uncommittedPaths;
2668
2678
  delete job.resumeRecoveryAttempted;
2669
2679
  // Same one-attempt-per-episode category as resumeRecoveryAttempted above —
@@ -3009,6 +3019,29 @@ async function resolveFixPlanPath(failedJob) {
3009
3019
  return { fixPath, fixSlug, group, livePrdDir };
3010
3020
  }
3011
3021
 
3022
+ /**
3023
+ * defaultScheduleSettle — production wiring for notifyOriginatingTab's
3024
+ * scheduleSettle option: arms the debounced Epic settle runner with live deps.
3025
+ * `remote` is defined later in the module but only read at call time.
3026
+ */
3027
+ function defaultScheduleSettle({ cwd, epicId }) {
3028
+ const deps = {
3029
+ loadJobs: async () => (await readQueue()).jobs || [],
3030
+ loadHistory: () => queueHistory.readHistory({ limit: 500 }),
3031
+ createPrd: (input) => prdCreate.createPrd(input, remote),
3032
+ resolveBaseSha: async (c, commit) => {
3033
+ try {
3034
+ return String(await execGitAt(c, ['rev-parse', `${commit}^`])).trim() || null;
3035
+ } catch {
3036
+ return null;
3037
+ }
3038
+ },
3039
+ sendPrompt: enqueueExternalPrompt,
3040
+ appendResponseEvent: appendResponseEventIfKnown,
3041
+ };
3042
+ epicValidationHook.scheduleEpicSettle({ cwd, epicId }, (args) => epicValidationHook.runEpicSettle(args, deps));
3043
+ }
3044
+
3012
3045
  /**
3013
3046
  * notifyOriginatingTab(job) → void
3014
3047
  *
@@ -3047,16 +3080,19 @@ async function notifyOriginatingTab(job, {
3047
3080
  appendResponseEvent = appendResponseEventIfKnown,
3048
3081
  appendTranscriptTurn = promptSessionTranscript.appendTurn,
3049
3082
  readResultFromLog = extractResultTextFromLog,
3050
- enqueueValidation = maybeEnqueueValidationPrompt,
3051
- // PRD 1408: same queue rows spawnJob/tickQueue already use — read fresh
3052
- // here (not threaded through the terminal-transition snapshot) since this
3053
- // fires well after dispatch, when the plan may have gained/dropped a
3054
- // trailing validator row since this job was spawned.
3055
- loadJobs = async () => (await readQueue()).jobs || [],
3083
+ scheduleSettle = defaultScheduleSettle,
3056
3084
  } = {}) {
3057
3085
  try {
3058
3086
  const prd = await resolveNotifyPrd(job, parsePrdRaw);
3059
- const message = `PRD ${job.slug} finished: ${job.status}. Check Scheduler for details.`;
3087
+ let message = `PRD ${job.slug} finished: ${job.status}. Check Scheduler for details.`;
3088
+ if (job.verifierVerdict === 'pass_preexisting_gate_red') {
3089
+ const failures = Array.isArray(job.preexistingGateFailures) ? job.preexistingGateFailures : [];
3090
+ const shown = failures.slice(0, 10).join('; ');
3091
+ const more = failures.length > 10 ? ` (+${failures.length - 10} more)` : '';
3092
+ message = `PRD ${job.slug} completed; the gate was already red before this PRD`
3093
+ + (failures.length ? `. Pre-existing failures: ${shown}${more}` : '')
3094
+ + '. Check Scheduler for details.';
3095
+ }
3060
3096
 
3061
3097
  // The EPIC this check-in belongs to. Deliberately does NOT consider
3062
3098
  // `sourceTabId` — that is a Terminal tab id, not an Epic id, and
@@ -3157,55 +3193,22 @@ async function notifyOriginatingTab(job, {
3157
3193
  prdSlug: job.slug,
3158
3194
  outcome: job.status,
3159
3195
  validation: 'unvalidated',
3196
+ ...(typeof job.agentType === 'string' ? { agentType: job.agentType } : {}),
3160
3197
  }).catch((e) => {
3161
3198
  console.error('[scheduler] notifyOriginatingTab appendResponseEvent error', job?.slug, e);
3162
3199
  return false;
3163
3200
  });
3164
3201
  if (routed) {
3165
- // PRD 986: a successful check-in append triggers ONE validation
3166
- // prompt into the authoring Epic's own session, asking it to verify
3167
- // every acceptance criterion against the real working tree and reply
3168
- // VERIFIED or REFUTED. Fire-and-forget, same convention as this
3169
- // function's own call site in tickQueue: a failure to enqueue is
3170
- // logged inside the hook, never thrown, and never blocks the job's
3171
- // status transition or the response-event append (which already
3172
- // happened above). All gating (kill-switch SM_EPIC_VALIDATION_DISABLE,
3173
- // active-Epic check, once-per-(epicId, prdSlug), loop guard, slot-pool
3174
- // routing) lives in lib/epic/epicValidationHook.cjs.
3175
- //
3176
- // PRD 1407: a validator job's own check-in never enqueues a
3177
- // validation prompt for itself — it IS the plan's validation pass,
3178
- // not a work-item awaiting one.
3179
- //
3180
- // PRD 1408: nor does an ordinary work-item whose plan already ends in
3181
- // a trailing validator job depending on it — that validator's own
3182
- // check-in (above) is the single validation pass for the whole plan,
3183
- // so a per-PRD prompt here would be a second, redundant ask into the
3184
- // same (Opus/Fable-tier) authoring session.
3185
- if (job.agentType !== 'validator') {
3186
- let skipForDownstreamValidator = false;
3187
- try {
3188
- skipForDownstreamValidator = hasDownstreamValidator(job, await loadJobs());
3189
- } catch (e) {
3190
- console.error('[scheduler] notifyOriginatingTab loadJobs error', job?.slug, e);
3191
- }
3192
- if (!skipForDownstreamValidator) {
3193
- try {
3194
- enqueueValidation(
3195
- {
3196
- cwd: job.cwd || null,
3197
- epicId,
3198
- prdSlug: job.slug,
3199
- prdPath: prd?.path || prdPaths.archivedPrdPathForJob(job) || null,
3200
- outcome: job.status,
3201
- eventValidation: 'unvalidated',
3202
- },
3203
- { sendPrompt },
3204
- );
3205
- } catch (e) {
3206
- console.error('[scheduler] notifyOriginatingTab enqueueValidation error', job?.slug, e);
3207
- }
3208
- }
3202
+ // The debounced Epic settle runner (lib/epic/epicValidationHook.cjs)
3203
+ // owns all validation messaging now (replaces the per-PRD prompts of
3204
+ // PRDs 986/1407/1408): every successful terminal check-in, a
3205
+ // validator's own included (its finish usually settles the Epic),
3206
+ // just re-arms the per-Epic debounce. A finished PRD never sends a
3207
+ // chat prompt into its authoring Epic by itself.
3208
+ try {
3209
+ scheduleSettle({ cwd: job.cwd || null, epicId });
3210
+ } catch (e) {
3211
+ console.error('[scheduler] notifyOriginatingTab scheduleSettle error', job?.slug, e);
3209
3212
  }
3210
3213
  return;
3211
3214
  }
@@ -3383,6 +3386,7 @@ async function flushDueReviewNotices({
3383
3386
  prdSlug: group.jobs[0].slug,
3384
3387
  outcome: 'needs_review',
3385
3388
  validation: 'unvalidated',
3389
+ allowInactive: true,
3386
3390
  });
3387
3391
  outcome = typeof result === 'boolean' ? { ok: result, reason: result ? undefined : 'refused' } : result;
3388
3392
  } catch (e) {
@@ -6438,17 +6442,10 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
6438
6442
  let mechanicalRecoveryTarget = null;
6439
6443
  let terminalNotifySnapshot = null;
6440
6444
  const newlyCompletedPrds = [];
6441
- // PRD 1408: captured inside the mutate() below (the one in-memory read of
6442
- // `s.jobs` this finalize pass already does) so notifyOriginatingTab's own
6443
- // hasDownstreamValidator check can reuse it instead of a second, redundant
6444
- // readQueue() — that call federates queue.json shards across every known
6445
- // project cwd on the machine, not just this job's own project.
6446
- let jobsSnapshotForNotify = null;
6447
6445
  // Base HEAD for the mechanical-recovery futility check — read here, never inside mutate() (no I/O).
6448
6446
  const finalizeHeadSha = worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
6449
6447
  ? await gitTreeGuard.gitHead(guardCwd) : null;
6450
6448
  await mutate((s) => {
6451
- jobsSnapshotForNotify = s.jobs.slice();
6452
6449
  const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
6453
6450
  // A job already moved off 'running' by someone else (namely
6454
6451
  // remote.cancelJob, PRD 1024 — it SIGTERMs the process then finalizes
@@ -6637,6 +6634,11 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
6637
6634
  } else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
6638
6635
  delete s.jobs[i2].verifierVerdict;
6639
6636
  }
6637
+ if (verifyResult?.verdict === 'needs_human') {
6638
+ s.jobs[i2].humanQuestion = verifyResult.humanQuestion || '';
6639
+ } else {
6640
+ delete s.jobs[i2].humanQuestion;
6641
+ }
6640
6642
  stampIntegrationFailure(s.jobs[i2], worktreeIntegrationFailure ? worktreeIntegrationDetail : null);
6641
6643
  transitionJob(s.jobs[i2], effectiveStatus, { reason: finalizeReason, source: 'spawnJob:finalize' });
6642
6644
  s.jobs[i2].finishedAt = new Date().toISOString();
@@ -6860,9 +6862,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null, sib
6860
6862
  });
6861
6863
 
6862
6864
  if (terminalNotifySnapshot) {
6863
- notifyOriginatingTab(terminalNotifySnapshot, {
6864
- loadJobs: async () => jobsSnapshotForNotify || [],
6865
- }).catch((e) => {
6865
+ notifyOriginatingTab(terminalNotifySnapshot).catch((e) => {
6866
6866
  console.error('[scheduler] notifyOriginatingTab error', job.slug, e);
6867
6867
  });
6868
6868
  }
@@ -8793,6 +8793,52 @@ async function runGateShadow(job) {
8793
8793
  gateShadow.authority = decision;
8794
8794
  }
8795
8795
 
8796
+ // Baseline authority: a sentinel_fail park whose gate is red at HEAD may
8797
+ // still complete when every HEAD failure already failed at the job's base
8798
+ // commit. The base run is memoized per HEAD on gateShadow.baseline.
8799
+ let baselineDecision = null;
8800
+ if (
8801
+ job.verifierVerdict === 'sentinel_fail'
8802
+ && outcome.status === 'red' && definitive
8803
+ && typeof job.landedCommit === 'string' && job.landedCommit.length > 0
8804
+ && typeof job.guardHeadBefore === 'string' && job.guardHeadBefore.length > 0
8805
+ && process.env.SM_BASELINE_GATE_AUTHORITY_DISABLE !== '1'
8806
+ ) {
8807
+ const startedAtMs = typeof job.startedAt === 'string' ? Date.parse(job.startedAt) : NaN;
8808
+ let evidenceOk = false;
8809
+ if (Number.isFinite(startedAtMs)) {
8810
+ try { evidenceOk = await resolveLandedCommitEvidence(job.cwd, job.landedCommit, job.startedAt); } catch { evidenceOk = false; }
8811
+ }
8812
+ let ancestorOk = false;
8813
+ try {
8814
+ await execGitAt(job.cwd, ['merge-base', '--is-ancestor', job.landedCommit, headBefore], { timeout: 10_000 });
8815
+ ancestorOk = true;
8816
+ } catch { ancestorOk = false; }
8817
+ const memo = job.gateShadow?.baseline;
8818
+ let baseOutcome = memo && memo.head === headBefore && memo.outcome ? memo.outcome : null;
8819
+ if (!baseOutcome) {
8820
+ const scratch = path.join(os.tmpdir(), `sm-baseline-gate-${process.pid}-${Date.now()}`);
8821
+ let added = false;
8822
+ try {
8823
+ await execGitAt(job.cwd, ['worktree', 'add', '--detach', scratch, job.guardHeadBefore], { timeout: 120_000 });
8824
+ added = true;
8825
+ await linkDependencyDirs({ mainCwd: job.cwd, worktreeCwd: scratch });
8826
+ const r = await runGateSequence(gate.sequence, { cwd: scratch });
8827
+ baseOutcome = r.status === 'busy' ? { status: 'unavailable', reason: 'busy', results: [] } : r;
8828
+ } catch (e) {
8829
+ baseOutcome = { status: 'unavailable', reason: `base-gate-error: ${e && e.message ? e.message : e}`, results: [] };
8830
+ } finally {
8831
+ if (added) {
8832
+ try { await execGitAt(job.cwd, ['worktree', 'remove', '--force', scratch], { timeout: 60_000 }); } catch { /* best-effort */ }
8833
+ }
8834
+ try { fs.rmSync(scratch, { recursive: true, force: true }); } catch { /* best-effort */ }
8835
+ try { await execGitAt(job.cwd, ['worktree', 'prune'], { timeout: 30_000 }); } catch { /* best-effort */ }
8836
+ }
8837
+ }
8838
+ baselineDecision = decideBaselineAuthority({ job, headOutcome: outcome, baseOutcome, evidenceOk, ancestorOk, cleanOk, headStable });
8839
+ gateShadow.baseline = { head: headBefore, base: job.guardHeadBefore, outcome: baseOutcome, ...baselineDecision };
8840
+ }
8841
+
8796
8842
  const runId = job.runId || reviewLadder.resolveRunId(job);
8797
8843
  if (runId) {
8798
8844
  const verdictsPath = path.join(schedulerPaths.runsDir(), runId, `${job.slug}.verdicts.json`);
@@ -8827,6 +8873,19 @@ async function runGateShadow(job) {
8827
8873
  continue;
8828
8874
  }
8829
8875
  j.gateShadow = gateShadow;
8876
+ if (baselineDecision?.complete) {
8877
+ const v = j.verifierVerdict;
8878
+ transitionJob(j, 'completed', {
8879
+ reason: `gate red at ${headShort} but identical to base ${String(job.guardHeadBefore).slice(0, 7)}; verdict ${v} overridden`,
8880
+ source: 'gateAuthority:baseline',
8881
+ });
8882
+ j.error = null;
8883
+ j.verifierVerdict = 'pass_preexisting_gate_red';
8884
+ j.preexistingGateFailures = baselineDecision.preexisting;
8885
+ delete j.looksDone;
8886
+ resolved = { slug: j.slug, cwd: j.cwd, verdict: v };
8887
+ continue;
8888
+ }
8830
8889
  if (!decision?.complete) continue;
8831
8890
  const v = j.verifierVerdict;
8832
8891
  transitionJob(j, 'completed', {
@@ -233,3 +233,5 @@ Before queueing a new PRD, verify each of these:
233
233
  - [ ] **§10 Negative assertions:** every "should find nothing" check is inverted to exit 0 on the clean case.
234
234
  - [ ] **§11 End green:** the gate runs last; any intentional failure runs early and is captured.
235
235
  - [ ] **§12 No stranded probes:** a probe error is corrected or annotated; an expected `timeout` cap branches on exit 124.
236
+ - [ ] **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`.
237
+ - [ ] **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.
@@ -259,6 +259,15 @@ export interface McpCatalogResult {
259
259
  * same concept, not a free-form string list. */
260
260
  export type AgentPersonaTag = 'feature' | 'bug' | 'discussion' | 'build' | 'project-home-builder';
261
261
 
262
+ /** One declared `{{name}}` argument of a macro prompt (macroLibrary.cjs `MacroInputSchema`). */
263
+ export interface MacroInput {
264
+ name: string;
265
+ label: string;
266
+ default?: string;
267
+ required?: boolean;
268
+ options?: string[];
269
+ }
270
+
262
271
  /** One user-defined sidebar HOT KEYS macro (macroLibrary.cjs). `projects` = absolute cwds or `'*'` (every project). */
263
272
  export interface Macro {
264
273
  id: string;
@@ -275,6 +284,7 @@ export interface Macro {
275
284
  builtinVersion?: number;
276
285
  createdAt: string;
277
286
  updatedAt: string;
287
+ inputs?: MacroInput[];
278
288
  }
279
289
 
280
290
  /** Omit `id` to create; set it to update an existing macro. */
@@ -286,6 +296,8 @@ export interface MacroSaveInput {
286
296
  prompt: string;
287
297
  projects?: string[];
288
298
  surface?: 'sessions' | 'project-home';
299
+ /** Omitted keeps the stored list on update; `[]` clears it. */
300
+ inputs?: MacroInput[];
289
301
  }
290
302
 
291
303
  /** One global `~/.claude/agents/<name>.md` persona, per the Agent Library nav page. */
@@ -1347,6 +1359,16 @@ export interface MemoryStaleEntry {
1347
1359
  danglingLinks: string[];
1348
1360
  }
1349
1361
 
1362
+ export type MemoryFixProposal = {
1363
+ ok: boolean;
1364
+ action: 'update' | 'delete' | 'fold' | 'keep';
1365
+ summary: string;
1366
+ reason: string;
1367
+ content: string;
1368
+ fold: { target: string; anchor: string; insert: string } | null;
1369
+ error?: string;
1370
+ };
1371
+
1350
1372
  export interface MemoryStaleResult {
1351
1373
  entries: MemoryStaleEntry[];
1352
1374
  workspace: string;
@@ -2131,9 +2153,9 @@ export interface SessionManagerAPI {
2131
2153
  /** Gated `claude -p` question over the memory folder. */
2132
2154
  ask: (input: { question: string; target?: MemoryTarget; contextName?: string }) =>
2133
2155
  Promise<{ ok: boolean; answer: string; citations: string[]; error?: string }>;
2134
- /** Gated `claude -p` proposed rewrite fixing a memory's dead refs; requires `target.cwd`. */
2156
+ /** Gated `claude -p` triage of a memory against the md network: update, delete, fold into a CLAUDE.md, or keep; requires `target.cwd`. */
2135
2157
  proposeFix: (input: { name: string; target?: MemoryTarget }) =>
2136
- Promise<{ ok: boolean; content: string; summary: string; error?: string }>;
2158
+ Promise<MemoryFixProposal>;
2137
2159
  };
2138
2160
  projectBrief: {
2139
2161
  /** Read brief.json (or null if none yet) plus cheaply-computed source/drift metadata. Never fires an LLM call. */