claude-code-session-manager 0.76.0 → 0.77.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/{AgentLibrary-CBx9l4zN.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bf0EIE_t.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-CpdtWhC8.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-DyUbMDmg.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-By-wIpry.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CQmo4QVC.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-BQMd6NOm.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-DEp43FXX.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-CLarzwqA.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-B0sCdIy1.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-BhWPVOCD.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-Ddlq8T_O.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-D2oA_2Jl.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DgAgavUM.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-C_zWEtIl.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-2Vx3X5SI.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-BDEUjlTQ.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-Cmrz_LeN.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-DVA1eYDP.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-DmPc3amD.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-zfd5LJkK.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-B_4PNh9T.js → index-BDRSqBl3.js} +175 -175
- package/dist/assets/{index-DIjnPkRN.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-B9es6fdA.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +115 -1
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +138 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +43 -0
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +147 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/config.cjs +4 -1
- package/src/main/index.cjs +8 -1
- package/src/main/ipcSchemas.cjs +51 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +78 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +152 -2
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +4 -2
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +277 -4
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +5 -3
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +1 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +22 -1
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/childWithLog.cjs +40 -5
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +115 -9
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +464 -19
- package/src/main/lib/jobWorktree.cjs +1 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/mcpToolCatalog.cjs +87 -2
- package/src/main/lib/opsOwnership.cjs +12 -0
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +57 -1
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +9 -2
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +53 -3
- package/src/main/lib/reaperHelpers.cjs +18 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +133 -29
- package/src/main/lib/schedulerConfig.cjs +19 -0
- package/src/main/projectPages.cjs +160 -2
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1371 -97
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +118 -2
- package/src/preload/index.cjs +7 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-Qvc4Ek-H.js +0 -1
- package/dist/assets/Scheduler-BmYJvNzK.js +0 -14
- 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
|
|
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
|
-
|
|
319
|
-
|
|
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'],
|