claude-code-session-manager 0.76.0 → 0.78.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 (123) hide show
  1. package/dist/assets/{AgentLibrary-CBx9l4zN.js → AgentLibrary-13pfo8uY.js} +2 -2
  2. package/dist/assets/{DataModel-Bf0EIE_t.js → DataModel-SUyQbFlg.js} +1 -1
  3. package/dist/assets/{History-CpdtWhC8.js → History-2GJMS703.js} +2 -2
  4. package/dist/assets/{Hooks-DyUbMDmg.js → Hooks-DM2nS3RT.js} +2 -2
  5. package/dist/assets/{HostBilko-By-wIpry.js → HostBilko-BLeC-lpp.js} +1 -1
  6. package/dist/assets/{Library-CQmo4QVC.js → Library-BaRkU9m0.js} +1 -1
  7. package/dist/assets/{ListDetail-BQMd6NOm.js → ListDetail-D5scjSKq.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DEp43FXX.js → MarkdownEditor-B1lgAo9T.js} +1 -1
  9. package/dist/assets/{McpServers-CLarzwqA.js → McpServers-BzyThQSM.js} +1 -1
  10. package/dist/assets/{Memory-B0sCdIy1.js → Memory-7UdaOTtl.js} +6 -6
  11. package/dist/assets/{Panel-BhWPVOCD.js → Panel-JbTMaOPq.js} +1 -1
  12. package/dist/assets/{Permissions-Ddlq8T_O.js → Permissions-UBam0bJG.js} +2 -2
  13. package/dist/assets/{Plugins-D2oA_2Jl.js → Plugins-B3gUDkeb.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DgAgavUM.js → ProvenanceBadge-CeHOub7m.js} +1 -1
  15. package/dist/assets/SaveBar-BcvQEq6h.js +1 -0
  16. package/dist/assets/Scheduler-Dc5qiP24.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-C_zWEtIl.js → ScopeSwitcher-BvGQmw4Y.js} +1 -1
  18. package/dist/assets/{Settings-2Vx3X5SI.js → Settings-C2dEFb-v.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-BDEUjlTQ.js → SkillReferenceGraph-CIwlosBc.js} +1 -1
  20. package/dist/assets/{Skills-Cmrz_LeN.js → Skills-C0GzzVrQ.js} +2 -2
  21. package/dist/assets/{SystemPrompt-DVA1eYDP.js → SystemPrompt-mtGPK8zo.js} +1 -1
  22. package/dist/assets/TagLibrary-DX54-mpd.js +1 -0
  23. package/dist/assets/{TiptapBody-DmPc3amD.js → TiptapBody-yADC2RWE.js} +1 -1
  24. package/dist/assets/{Toggle-zfd5LJkK.js → Toggle-CRxaCYLI.js} +1 -1
  25. package/dist/assets/{index-DIjnPkRN.css → index-CYhdtisq.css} +1 -1
  26. package/dist/assets/{index-B_4PNh9T.js → index-D6ymGESc.js} +469 -469
  27. package/dist/assets/{settingsSchema-B9es6fdA.js → settingsSchema-TtMvT5Sx.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/project-pages-logic/dist/logic.cjs +4709 -0
  32. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  33. package/scripts/render-project-pages.cjs +70 -0
  34. package/scripts/scheduler-mcp-server.cjs +115 -1
  35. package/scripts/validate-project-pages-summary.cjs +62 -0
  36. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  37. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  38. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  39. package/src/main/__tests__/prdCreate.test.cjs +138 -0
  40. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  41. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  42. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  43. package/src/main/__tests__/projectPages.test.cjs +73 -1
  44. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  45. package/src/main/__tests__/runVerify.test.cjs +94 -0
  46. package/src/main/__tests__/scheduler-autofix-select.test.cjs +43 -0
  47. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  48. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  49. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  50. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  51. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  52. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  53. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  54. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  55. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  56. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  57. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +147 -0
  58. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  59. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  60. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  61. package/src/main/config.cjs +4 -1
  62. package/src/main/index.cjs +16 -4
  63. package/src/main/ipcSchemas.cjs +53 -0
  64. package/src/main/lib/__tests__/childWithLog.test.cjs +78 -0
  65. package/src/main/lib/__tests__/delegationReadiness.test.cjs +197 -2
  66. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +4 -2
  67. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  68. package/src/main/lib/__tests__/gitWorktree.test.cjs +277 -4
  69. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  70. package/src/main/lib/__tests__/jobWorktree.test.cjs +5 -3
  71. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  72. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  73. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +1 -0
  74. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  75. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  76. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  77. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  78. package/src/main/lib/__tests__/reaperHelpers.test.cjs +22 -1
  79. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  80. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  81. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  82. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  83. package/src/main/lib/agentModelResolve.cjs +58 -0
  84. package/src/main/lib/childWithLog.cjs +40 -5
  85. package/src/main/lib/claudeBin.cjs +54 -1
  86. package/src/main/lib/definitionOfDone.cjs +3 -2
  87. package/src/main/lib/delegationReadiness.cjs +115 -9
  88. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  89. package/src/main/lib/fixChainDepth.cjs +45 -0
  90. package/src/main/lib/gitWorktree.cjs +464 -19
  91. package/src/main/lib/jobWorktree.cjs +1 -0
  92. package/src/main/lib/landedSinceRun.cjs +55 -0
  93. package/src/main/lib/launchFailure.cjs +357 -0
  94. package/src/main/lib/mcpToolCatalog.cjs +87 -2
  95. package/src/main/lib/opsOwnership.cjs +12 -0
  96. package/src/main/lib/prdAgentType.cjs +84 -0
  97. package/src/main/lib/prdCreate.cjs +57 -1
  98. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  99. package/src/main/lib/prdFrontmatter.cjs +17 -3
  100. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  101. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  102. package/src/main/lib/queueHealth.cjs +38 -0
  103. package/src/main/lib/queueStore.cjs +9 -2
  104. package/src/main/lib/quietMachineLease.cjs +48 -0
  105. package/src/main/lib/rcaReport.cjs +53 -3
  106. package/src/main/lib/reaperHelpers.cjs +18 -1
  107. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  108. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  109. package/src/main/lib/schedulerBatch.cjs +133 -29
  110. package/src/main/lib/schedulerConfig.cjs +19 -0
  111. package/src/main/projectPages.cjs +160 -2
  112. package/src/main/runVerify.cjs +50 -9
  113. package/src/main/scheduler/prdParser.cjs +18 -1
  114. package/src/main/scheduler.cjs +1371 -97
  115. package/src/main/seedAgentPersonas.cjs +62 -21
  116. package/src/main/templates/project-pages-catalog.json +741 -0
  117. package/src/main/templates/project-pages-pipeline.md +417 -0
  118. package/src/preload/api.d.ts +126 -6
  119. package/src/preload/index.cjs +8 -0
  120. package/src/seed/agents/project-home-builder.md +59 -0
  121. package/dist/assets/SaveBar-Qvc4Ek-H.js +0 -1
  122. package/dist/assets/Scheduler-BmYJvNzK.js +0 -14
  123. package/dist/assets/TagLibrary-DYJGAKZu.js +0 -1
@@ -0,0 +1,181 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * projectPageSummarySchema.cjs — hand-authored JSON Schema description of
5
+ * `ProjectPageSummary` (src/renderer/lib/projectPages/summaryType.ts), for
6
+ * the /admin/project-home/contract route (PRD: project-home-admin-routes).
7
+ *
8
+ * This schema is DESCRIPTIVE, not the enforcement mechanism — the actual
9
+ * gate a posted summary must pass is `validateProjectPageSummary` (shipped
10
+ * in scripts/project-pages-logic/dist/logic.cjs, called by
11
+ * /admin/project-home/validate-summary and /admin/project-home/render).
12
+ * Kept hand-written rather than derived at runtime so the contract route
13
+ * never needs to load the TypeScript source; if summaryType.ts's shape
14
+ * changes, update this alongside it (same discipline as any other
15
+ * TS-type-mirrored constant in this codebase).
16
+ */
17
+
18
+ const PROJECT_PAGE_SUMMARY_JSON_SCHEMA = Object.freeze({
19
+ $schema: 'http://json-schema.org/draft-07/schema#',
20
+ title: 'ProjectPageSummary',
21
+ type: 'object',
22
+ required: ['identity', 'stats', 'pillars', 'feature', 'architecture', 'quotes'],
23
+ properties: {
24
+ identity: {
25
+ type: 'object',
26
+ required: ['name', 'tag', 'version', 'oneLine', 'claim', 'sub', 'audience', 'install'],
27
+ properties: {
28
+ name: { type: 'string' },
29
+ tag: { type: 'string' },
30
+ version: { type: 'string' },
31
+ oneLine: { type: 'string' },
32
+ claim: { type: 'string' },
33
+ sub: { type: 'string' },
34
+ audience: { type: 'string' },
35
+ install: { type: 'string' },
36
+ },
37
+ description: 'Every string field must be non-empty and must not be the literal "TODO".',
38
+ },
39
+ stats: {
40
+ type: 'array',
41
+ items: {
42
+ type: 'object',
43
+ required: ['v', 'k', 'n'],
44
+ properties: { v: { type: 'string' }, k: { type: 'string' }, n: { type: 'string' } },
45
+ },
46
+ },
47
+ pillars: {
48
+ type: 'array',
49
+ items: {
50
+ type: 'object',
51
+ required: ['t', 'd', 'k'],
52
+ properties: { t: { type: 'string' }, d: { type: 'string' }, k: { type: 'string' } },
53
+ },
54
+ },
55
+ quotes: {
56
+ type: 'array',
57
+ description: 'Never fabricate testimonials — omit entirely rather than invent a quote.',
58
+ items: {
59
+ type: 'object',
60
+ required: ['q', 'a', 'r'],
61
+ properties: { q: { type: 'string' }, a: { type: 'string' }, r: { type: 'string' } },
62
+ },
63
+ },
64
+ feature: {
65
+ type: 'object',
66
+ required: ['name', 'kicker', 'status', 'owner', 'oneLine', 'problem', 'solution', 'steps', 'rules', 'specs', 'faq', 'timeline'],
67
+ properties: {
68
+ name: { type: 'string' },
69
+ kicker: { type: 'string' },
70
+ status: { type: 'string' },
71
+ owner: { type: 'string' },
72
+ oneLine: { type: 'string' },
73
+ problem: { type: 'string' },
74
+ solution: { type: 'string' },
75
+ steps: { type: 'array', items: { type: 'object', required: ['t', 'd'], properties: { t: { type: 'string' }, d: { type: 'string' } } } },
76
+ rules: { type: 'array', items: { type: 'object', required: ['t', 'd'], properties: { t: { type: 'string' }, d: { type: 'string' } } } },
77
+ specs: { type: 'array', items: { type: 'array', description: '[label, value, note]', items: { type: 'string' }, minItems: 3, maxItems: 3 } },
78
+ faq: { type: 'array', items: { type: 'object', required: ['q', 'a'], properties: { q: { type: 'string' }, a: { type: 'string' } } } },
79
+ timeline: {
80
+ type: 'array',
81
+ items: {
82
+ type: 'object',
83
+ required: ['w', 't', 's'],
84
+ properties: { w: { type: 'string' }, t: { type: 'string' }, s: { type: 'string', enum: ['done', 'next', 'idea'] } },
85
+ },
86
+ },
87
+ },
88
+ },
89
+ architecture: {
90
+ type: 'object',
91
+ required: ['summary', 'principles', 'layers', 'modules', 'flow', 'decisions', 'risks'],
92
+ properties: {
93
+ summary: { type: 'string' },
94
+ principles: { type: 'array', items: { type: 'object', required: ['t', 'd'], properties: { t: { type: 'string' }, d: { type: 'string' } } } },
95
+ layers: {
96
+ type: 'array',
97
+ items: {
98
+ type: 'object',
99
+ required: ['n', 'd', 'f', 'tone'],
100
+ properties: { n: { type: 'string' }, d: { type: 'string' }, f: { type: 'string' }, tone: { type: 'string', enum: ['accent', 'butter', 'sage', 'mute'] } },
101
+ },
102
+ },
103
+ modules: {
104
+ type: 'array',
105
+ items: {
106
+ type: 'object',
107
+ required: ['n', 'd', 'f', 'dep', 'heat'],
108
+ properties: { n: { type: 'string' }, d: { type: 'string' }, f: { type: 'number' }, dep: { type: 'array', items: { type: 'string' } }, heat: { type: 'number' } },
109
+ },
110
+ },
111
+ flow: {
112
+ type: 'array',
113
+ items: {
114
+ type: 'object',
115
+ required: ['a', 'b', 't', 'n'],
116
+ properties: { a: { type: 'string' }, b: { type: 'string' }, t: { type: 'string' }, n: { type: 'string' } },
117
+ },
118
+ },
119
+ decisions: {
120
+ type: 'array',
121
+ items: {
122
+ type: 'object',
123
+ required: ['id', 't', 'w', 's'],
124
+ properties: { id: { type: 'string' }, t: { type: 'string' }, w: { type: 'string' }, s: { type: 'string', enum: ['accepted', 'proposed', 'superseded'] } },
125
+ },
126
+ },
127
+ risks: {
128
+ type: 'array',
129
+ description: 'No clean source in the Brief — leave empty rather than invent.',
130
+ items: {
131
+ type: 'object',
132
+ required: ['t', 'd', 's'],
133
+ properties: { t: { type: 'string' }, d: { type: 'string' }, s: { type: 'string', enum: ['open', 'watching', 'mitigated'] } },
134
+ },
135
+ },
136
+ },
137
+ },
138
+ brief: {
139
+ type: 'object',
140
+ description: 'OPTIONAL — omit entirely when this project has no generated brief.json yet.',
141
+ required: ['purpose', 'what', 'areas', 'scope', 'conventions'],
142
+ properties: {
143
+ purpose: { type: 'string' },
144
+ what: { type: 'array', items: { type: 'string' } },
145
+ areas: {
146
+ type: 'array',
147
+ items: {
148
+ type: 'object',
149
+ required: ['name', 'files', 'note', 'epic', 'heat'],
150
+ properties: { name: { type: 'string' }, files: { type: 'number' }, note: { type: 'string' }, epic: { type: ['string', 'null'] }, heat: { type: 'number' } },
151
+ },
152
+ },
153
+ scope: {
154
+ type: 'array',
155
+ items: {
156
+ type: 'object',
157
+ required: ['when', 'kind', 'text', 'src'],
158
+ properties: { when: { type: 'string' }, kind: { type: 'string', enum: ['added', 'narrowed', 'decided'] }, text: { type: 'string' }, src: { type: 'string' } },
159
+ },
160
+ },
161
+ conventions: { type: 'array', items: { type: 'string' } },
162
+ },
163
+ },
164
+ },
165
+ });
166
+
167
+ // ProjectPagePicks — Record<lensId, Record<slotId, variantId>>. Matched
168
+ // against each lens's own catalog.slots[].variants[].id at render time by
169
+ // the renderer bundle itself, so this schema only bounds the wire shape.
170
+ const PROJECT_PAGE_PICKS_JSON_SCHEMA = Object.freeze({
171
+ $schema: 'http://json-schema.org/draft-07/schema#',
172
+ title: 'ProjectPagePicks',
173
+ type: 'object',
174
+ additionalProperties: {
175
+ type: 'object',
176
+ additionalProperties: { type: 'string' },
177
+ },
178
+ description: 'lensId -> slotId -> variantId. Every lensId/slotId/variantId must match one from the catalog.',
179
+ });
180
+
181
+ module.exports = { PROJECT_PAGE_SUMMARY_JSON_SCHEMA, PROJECT_PAGE_PICKS_JSON_SCHEMA };
@@ -0,0 +1,38 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * queueHealth.cjs — pure per-project rollup for the periodic queue-health
5
+ * sweep (PRD 1109). Kept separate from scheduler.cjs so it can be unit
6
+ * tested without importing electron/ipcMain, matching reaperHelpers.cjs.
7
+ */
8
+
9
+ // Non-terminal statuses that mean a row is parked waiting on a human rather
10
+ // than actively progressing (pending/running are expected to move on their
11
+ // own; investigating is mid-flight and already tracked by
12
+ // findStrandedInvestigations).
13
+ const STUCK_STATUSES = new Set(['needs_review', 'quarantined']);
14
+
15
+ /**
16
+ * computeQueueHealth(jobs) → [{ cwd, neverRan, looksDone, stuck }]
17
+ *
18
+ * Read-only: never mutates a row. Counts, per project cwd, how many rows
19
+ * carry `gateOutcome === 'never_ran'`, how many carry a `looksDone`
20
+ * annotation, and how many sit in a stuck (parked, non-terminal) status.
21
+ * Only projects with a non-zero total are returned — the sweep logs one
22
+ * line per non-zero project, nothing for a clean queue.
23
+ */
24
+ function computeQueueHealth(jobs) {
25
+ const byCwd = new Map();
26
+ for (const j of Array.isArray(jobs) ? jobs : []) {
27
+ if (!j) continue;
28
+ const cwd = j.cwd || '(unknown)';
29
+ if (!byCwd.has(cwd)) byCwd.set(cwd, { cwd, neverRan: 0, looksDone: 0, stuck: 0 });
30
+ const bucket = byCwd.get(cwd);
31
+ if (j.gateOutcome === 'never_ran') bucket.neverRan += 1;
32
+ if (j.looksDone) bucket.looksDone += 1;
33
+ if (STUCK_STATUSES.has(j.status)) bucket.stuck += 1;
34
+ }
35
+ return [...byCwd.values()].filter((b) => b.neverRan + b.looksDone + b.stuck > 0);
36
+ }
37
+
38
+ module.exports = { computeQueueHealth, STUCK_STATUSES };
@@ -183,6 +183,11 @@ function shapeMachine(raw) {
183
183
  scheduledFor: data.scheduledFor ?? null,
184
184
  lastRunAt: data.lastRunAt ?? null,
185
185
  paused: data.paused ?? null,
186
+ // Launch circuit breaker (lib/launchFailure.cjs): per-persona blocks and
187
+ // the degraded-mode env a persona is currently launching with. Machine
188
+ // state, not per-project: the broken thing is the installed CLI.
189
+ launchBlocks: data.launchBlocks && typeof data.launchBlocks === 'object' ? data.launchBlocks : {},
190
+ launchMitigations: data.launchMitigations && typeof data.launchMitigations === 'object' ? data.launchMitigations : {},
186
191
  };
187
192
  }
188
193
 
@@ -229,7 +234,7 @@ function shapeJobs(raw, file) {
229
234
  * consulted so writeSplit can persist "this project now has zero jobs".
230
235
  */
231
236
  function readMergedSync(opts) {
232
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, paused: null, invalidJobs: [] };
237
+ const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
233
238
  const sourceCwds = [];
234
239
  try {
235
240
  Object.assign(out, shapeMachine(fs.readFileSync(MACHINE_STATE_PATH, 'utf8')));
@@ -259,7 +264,7 @@ function readMergedSync(opts) {
259
264
 
260
265
  /** Async twin of readMergedSync for IPC hot paths. */
261
266
  async function readMerged(opts) {
262
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, paused: null, invalidJobs: [] };
267
+ const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, paused: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [] };
263
268
  const sourceCwds = [];
264
269
  try {
265
270
  Object.assign(out, shapeMachine(await fsp.readFile(MACHINE_STATE_PATH, 'utf8')));
@@ -309,6 +314,8 @@ async function writeSplit(state, defaultCwd) {
309
314
  scheduledFor: state.scheduledFor ?? null,
310
315
  lastRunAt: state.lastRunAt ?? null,
311
316
  paused: state.paused ?? null,
317
+ launchBlocks: state.launchBlocks ?? {},
318
+ launchMitigations: state.launchMitigations ?? {},
312
319
  });
313
320
 
314
321
  const byCwd = new Map();
@@ -0,0 +1,48 @@
1
+ /**
2
+ * quietMachineLease.cjs — the exclusive, machine-wide dispatch lock a
3
+ * `quietMachine: true` PRD holds for the duration of its run (PRD 1107).
4
+ *
5
+ * Process-local, like sessionSlots.cjs's holder map — reset on restart by
6
+ * design; a crashed lease must never wedge the queue permanently. This is
7
+ * NOT a second concurrency pool: sessionSlots.cjs remains the sole slot
8
+ * pool. This module only answers "is a quiet-machine job currently running,
9
+ * and if so, hold every other dispatch until it releases?" — schedulerBatch
10
+ * .cjs's pickNextBatch reads isHeld() to gate the WHOLE tick, not just one
11
+ * project.
12
+ */
13
+ 'use strict';
14
+
15
+ // slug of the job currently holding the lease, or null.
16
+ let heldBySlug = null;
17
+
18
+ function isHeld() {
19
+ return heldBySlug !== null;
20
+ }
21
+
22
+ /** acquire(slug) → true if the lease was free and is now held by `slug`. */
23
+ function acquire(slug) {
24
+ if (heldBySlug !== null) return false;
25
+ heldBySlug = String(slug);
26
+ return true;
27
+ }
28
+
29
+ /** release(slug) — idempotent; releasing when not held, or held by a
30
+ * different slug, is a no-op (mirrors sessionSlots.release's tolerance for
31
+ * an already-released token). */
32
+ function release(slug) {
33
+ if (heldBySlug === null) return false;
34
+ if (heldBySlug !== String(slug)) return false;
35
+ heldBySlug = null;
36
+ return true;
37
+ }
38
+
39
+ function holder() {
40
+ return heldBySlug;
41
+ }
42
+
43
+ /** Test hook: force the lease back to free. */
44
+ function __resetForTests() {
45
+ heldBySlug = null;
46
+ }
47
+
48
+ module.exports = { isHeld, acquire, release, holder, __resetForTests };
@@ -47,6 +47,7 @@ const VERDICT_LABELS = {
47
47
  verify_unavailable: 'verify unavailable',
48
48
  uncommitted_changes: 'uncommitted changes',
49
49
  no_verdict_sentinel: 'no commit or verdict sentinel',
50
+ abandoned_background_task: 'abandoned auto-backgrounded task',
50
51
  pass_no_commit: 'PASS sentinel but no commit landed',
51
52
  pass_no_commit_already_shipped: 'PASS with no commit — deliverables already shipped',
52
53
  pass_no_commit_prior_run_verified: 'PASS with no commit — prior run of this slug already landed the work',
@@ -67,9 +68,42 @@ const FAILURE_CLASSES = {
67
68
  NO_SENTINEL: 'no-sentinel',
68
69
  UNCOMMITTED: 'uncommitted-changes',
69
70
  TRANSCRIPT_ERRORS: 'transcript-errors',
71
+ ABANDONED_BACKGROUND_TASK: 'abandoned-background-task',
70
72
  UNKNOWN: 'unknown',
71
73
  };
72
74
 
75
+ // ─── Recovery actions — one per failure class, machine-readable ────────────
76
+ // Closed set the scheduler routes on: 'archive' (stale re-run, do not
77
+ // re-queue), 'resume-and-commit' (PRD 1111's --resume dispatch owns this),
78
+ // 'verify-and-close' (the work likely landed, just missing its sentinel),
79
+ // 'investigate' (the only class that still buys a fix-plan investigation).
80
+ const RECOVERY_ACTIONS = {
81
+ [FAILURE_CLASSES.ALREADY_SHIPPED]: 'archive',
82
+ [FAILURE_CLASSES.SELF_QUEUE]: 'investigate',
83
+ [FAILURE_CLASSES.STUCK_LOOP]: 'investigate',
84
+ [FAILURE_CLASSES.POST_AC_OVERRUN]: 'investigate',
85
+ [FAILURE_CLASSES.NO_SENTINEL]: 'verify-and-close',
86
+ [FAILURE_CLASSES.UNCOMMITTED]: 'resume-and-commit',
87
+ [FAILURE_CLASSES.TRANSCRIPT_ERRORS]: 'investigate',
88
+ // NOT 'resume-and-commit': that action is only ever actually dispatched by
89
+ // selectResumeRecoveryTarget in scheduler.cjs, which gates strictly on
90
+ // verifierVerdict === 'uncommitted_changes' plus a sessionId + recorded
91
+ // uncommittedPaths — none of which an abandoned_background_task job has.
92
+ // Labeling it 'resume-and-commit' here would be pure telemetry fiction (the
93
+ // RCA report / job.rcaRecoveryAction would claim a resume attempt that
94
+ // never happens); 'investigate' is what actually runs (spawnInvestigation),
95
+ // and buildInvestigationPrompt's abandoned-background-task note is what
96
+ // carries the "check for salvaged work, commit before re-implementing"
97
+ // guidance into that cold-read investigation.
98
+ [FAILURE_CLASSES.ABANDONED_BACKGROUND_TASK]: 'investigate',
99
+ [FAILURE_CLASSES.UNKNOWN]: 'investigate',
100
+ };
101
+
102
+ /** Pure — returns the closed-set recovery action for a failure class, defaulting to 'investigate'. */
103
+ function recoveryActionFor(failureClass) {
104
+ return RECOVERY_ACTIONS[failureClass] ?? 'investigate';
105
+ }
106
+
73
107
  const PREVENTION_HINTS = {
74
108
  [FAILURE_CLASSES.ALREADY_SHIPPED]:
75
109
  "This run found its acceptance criteria already satisfied by a prior commit and correctly made no change, so no commit landed and the verifier returned `pass_no_commit`. This is a stale re-run, not an execution failure — the PRD's `.md` was never moved out of `session-manager-operations/scheduler/prds/` after the work shipped. Archive the PRD into `session-manager-operations/scheduler/prds-archived/` instead of re-queuing or re-running it.",
@@ -85,6 +119,8 @@ const PREVENTION_HINTS = {
85
119
  'Stage the exact paths you created or modified — never a blanket/wildcard git-add of the whole tree, since the queue can run sibling jobs against this same working tree and a blanket add sweeps up their in-flight edits — and `git commit` as the final finish-protocol step before printing the verdict sentinel; never end a run with a dirty working tree.',
86
120
  [FAILURE_CLASSES.TRANSCRIPT_ERRORS]:
87
121
  'Recover or annotate every error within ~10 lines (e.g. `# expected/handled: <why>`) instead of leaving a bare Traceback near the end of the transcript.',
122
+ [FAILURE_CLASSES.ABANDONED_BACKGROUND_TASK]:
123
+ 'A headless run cannot receive a background-task completion notification — never let a long Bash command auto-background past its foreground timeout and then wait for it. Run long commands with an explicit bound (`timeout <N> <cmd>`) so they finish in the foreground, or poll their output file directly instead of waiting on Monitor/notification.',
88
124
  [FAILURE_CLASSES.UNKNOWN]:
89
125
  'Re-run the acceptance criteria gate locally against the failure log to pin down the specific break before re-queuing.',
90
126
  };
@@ -139,6 +175,16 @@ function classifyFailure({ verdict, logTail }) {
139
175
  return FAILURE_CLASSES.ALREADY_SHIPPED;
140
176
  }
141
177
 
178
+ // Checked before SELF_QUEUE/STUCK_LOOP: the verifier already did the real
179
+ // detection work (scanning for the harness's own auto-background marker
180
+ // text) to produce this verdict — trust it rather than re-deriving from the
181
+ // log tail, which can otherwise false-match SELF_QUEUE_WAKEUP_RE/STUCK_LOOP_RE
182
+ // on words like "sleep"/"background" that appear in the harness's own
183
+ // auto-background tool_result.
184
+ if (verdict === 'abandoned_background_task') {
185
+ return FAILURE_CLASSES.ABANDONED_BACKGROUND_TASK;
186
+ }
187
+
142
188
  // Checked before STUCK_LOOP: a backgrounded command + ScheduleWakeup
143
189
  // (variant b) can land words like "sleep"/"poll" in the tail via the PRD's
144
190
  // own AC text, which would otherwise false-match STUCK_LOOP_RE (see PRD 771).
@@ -226,7 +272,8 @@ function readPrdBody(cwd, slug) {
226
272
  */
227
273
  function buildRcaMarkdown({ job, verdict, meta, logTail, acText, failureClass, investigationText }) {
228
274
  const title = `Root cause: ${job.slug} → needs_review (${humanVerdict(verdict)})`;
229
- const fm = ['---', `title: ${title}`, 'source: scheduler-rca', 'type: rca', 'severity: normal', `prdSlug: ${job.slug}`, `runId: ${job.runId}`, `verdict: ${verdict}`, '---'].join('\n');
275
+ const recoveryAction = recoveryActionFor(failureClass);
276
+ const fm = ['---', `title: ${title}`, 'source: scheduler-rca', 'type: rca', 'severity: normal', `prdSlug: ${job.slug}`, `runId: ${job.runId}`, `verdict: ${verdict}`, `failure-class: ${failureClass}`, `recovery-action: ${recoveryAction}`, '---'].join('\n');
230
277
 
231
278
  const last60 = lastNLines(logTail, 60);
232
279
  const exitCode = meta?.exitCode ?? job.exitCode ?? 'unknown';
@@ -315,8 +362,9 @@ async function writeRcaReport({ job, runDir, verdict, annotations, investigation
315
362
  fs.renameSync(tmp, reportPath);
316
363
 
317
364
  const summary = rcaSummaryLine(job, verdict);
318
- console.log(`[rca] wrote ${reportPath} (${failureClass})`);
319
- return { filed: true, path: reportPath, summary, failureClass };
365
+ const recoveryAction = recoveryActionFor(failureClass);
366
+ console.log(`[rca] wrote ${reportPath} (${failureClass} → ${recoveryAction})`);
367
+ return { filed: true, path: reportPath, summary, failureClass, recoveryAction };
320
368
  } catch (e) {
321
369
  console.error('[rca] error writing RCA report', e?.message ?? String(e));
322
370
  return { filed: false, reason: 'error', error: e?.message ?? String(e) };
@@ -334,4 +382,6 @@ module.exports = {
334
382
  buildRcaMarkdown,
335
383
  FAILURE_CLASSES,
336
384
  VERDICT_LABELS,
385
+ RECOVERY_ACTIONS,
386
+ recoveryActionFor,
337
387
  };
@@ -64,6 +64,23 @@ function classifyRunOutcome(logPath) {
64
64
  }
65
65
  }
66
66
 
67
+ /**
68
+ * Map classifyRunOutcome()'s four-way result onto the smaller, persisted
69
+ * `gateOutcome` a job row carries. 'no_result' really means the verdict gate
70
+ * never fired — the process died or was killed before it could emit one —
71
+ * a different fact from 'failed' (a verdict event exists and says error,
72
+ * i.e. the gate ran and went red). Collapsing both behind the same free-text
73
+ * `error` string is exactly the ambiguity this field exists to remove.
74
+ */
75
+ function mapOutcomeToGateOutcome(outcome) {
76
+ switch (outcome) {
77
+ case 'success': return 'passed';
78
+ case 'failed': return 'failed';
79
+ case 'no_result': return 'never_ran';
80
+ default: return 'unknown';
81
+ }
82
+ }
83
+
67
84
  // Max times an orphaned job may be re-queued before giving up (marking failed).
68
85
  // Single source of truth: both the in-app reaper (scheduler.cjs) and the external
69
86
  // offline watchdog (watchdogHelpers.cjs) import this so their give-up budgets can
@@ -116,4 +133,4 @@ function selectReapableJobs(jobs, now, { pidAlive, grace } = {}) {
116
133
  return { reapable, warnings };
117
134
  }
118
135
 
119
- module.exports = { claudePidAlive, classifyRunOutcome, ORPHAN_REQUEUE_CAP, selectReapableJobs };
136
+ module.exports = { claudePidAlive, classifyRunOutcome, mapOutcomeToGateOutcome, ORPHAN_REQUEUE_CAP, selectReapableJobs };
@@ -77,6 +77,11 @@ const ScheduleJobSchema = z
77
77
  finishedAt: z.string().nullable().optional(),
78
78
  exitCode: z.number().nullable().optional(),
79
79
  error: z.string().nullable().optional(),
80
+ // Structured terminal-outcome label set alongside the free-text `error`
81
+ // (never replacing it) so a queue reader can tell "the gate ran and went
82
+ // red" (failed) apart from "the gate never got to run" (never_ran) —
83
+ // see reaperHelpers.cjs's mapOutcomeToGateOutcome (PRD 1109).
84
+ gateOutcome: z.enum(['passed', 'failed', 'never_ran', 'unknown']).optional(),
80
85
  sessionId: z.string().optional(),
81
86
  runtime: ScheduleJobRuntimeSchema.optional(),
82
87
  verifierVerdict: z.string().optional(),
@@ -86,6 +91,32 @@ const ScheduleJobSchema = z
86
91
  sourcePromptId: z.string().nullable().optional(),
87
92
  epicId: z.string().nullable().optional(),
88
93
  statusHistory: z.array(ScheduleJobStatusHistoryEntrySchema).optional(),
94
+ // Opt-in exclusive-lease PRD flag (PRD 1107) — serializes this job against
95
+ // every other job machine-wide for its run so a timing-sensitive gate
96
+ // (frame time, performance fence) measures without CPU contention. See
97
+ // quietMachineLease.cjs + schedulerBatch.cjs's pickNextBatch.
98
+ quietMachine: z.boolean().optional(),
99
+ // Set true when the job was dispatched anyway after waiting past
100
+ // quietMachineWaitMs without the machine going quiet — tells a reader the
101
+ // job's measurements ran under contention, not on a quiet machine.
102
+ quietLeaseDegraded: z.boolean().optional(),
103
+ // Salvaged uncommitted diff patch path (PRD 1098).
104
+ salvagePatch: z.string().nullable().optional(),
105
+ // Leftover-attribution fields: the newly-dirty paths a terminal run left
106
+ // uncommitted, diffed against the persisted pre-run baseline
107
+ // (guardBaseline/guardHeadBefore below). Set on EVERY terminal outcome
108
+ // that left dirt (completed/failed/needs_review alike), not just the
109
+ // exit=0 commit-guard path — deleted when the run left nothing. Capped
110
+ // at LEFTOVER_PATHS_CAP paths (leftoverCount carries the true total).
111
+ leftoverPaths: z.array(z.string()).optional(),
112
+ leftoverCount: z.number().optional(),
113
+ leftoverPathsTruncated: z.boolean().optional(),
114
+ // Pre-run working-tree snapshot, persisted at dispatch so a finalizer
115
+ // that never reaches spawnJob's own completion path (reapDeadRunningJobs)
116
+ // can still compute a truthful newly-dirty delta. Cleared once the run
117
+ // finalizes — never meant to outlive one run.
118
+ guardBaseline: z.array(z.string()).optional(),
119
+ guardHeadBefore: z.string().nullable().optional(),
89
120
  })
90
121
  .passthrough();
91
122
 
@@ -55,7 +55,11 @@ const STATUS_HISTORY_CAP = 20;
55
55
  * (admin reset can target a job mid-investigation)
56
56
  * - failed->investigating (spawn a probe), failed->pending (admin reset /
57
57
  * transient retry), failed->completed (auto-promote a fix-plan's healed
58
- * original)
58
+ * original), failed->needs_review (reverifyNeedsReview's looksDone pass,
59
+ * PRD 1102: an unverified-shaped failed row — no sentinel, no result
60
+ * event — with a commit landed after its run window that touches its own
61
+ * declared paths is surfaced to a human as needs_review, never silently
62
+ * auto-completed)
59
63
  * - needs_review->investigating, needs_review->pending, needs_review->completed
60
64
  * (heal on reverify, or auto-promote) — same shape as `failed`
61
65
  * - completed->pending (force-only reset, gated separately by
@@ -80,7 +84,7 @@ const LEGAL_TRANSITIONS = {
80
84
  pending: ['running', 'completed', 'failed', 'skipped'],
81
85
  running: ['completed', 'failed', 'needs_review', 'skipped', 'pending'],
82
86
  investigating: ['failed', 'needs_review', 'completed', 'pending', 'skipped'],
83
- failed: ['investigating', 'pending', 'completed'],
87
+ failed: ['investigating', 'pending', 'completed', 'needs_review'],
84
88
  needs_review: ['investigating', 'pending', 'completed', 'skipped'],
85
89
  completed: ['pending'],
86
90
  skipped: ['pending'],