substrate-ai 0.20.64 → 0.20.66
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/adapter-registry-BbVWH3Yv.js +4 -0
- package/dist/cli/index.js +172 -25
- package/dist/{decision-router-BA__VYIp.js → decision-router-DblHY8se.js} +1 -1
- package/dist/{decisions-4F91LrVD.js → decisions-DilHo99V.js} +2 -2
- package/dist/{dist-W2emvN3F.js → dist-K_RRWnBX.js} +2 -2
- package/dist/{errors-CKFu8YI9.js → errors-pSiZbn6e.js} +2 -2
- package/dist/{experimenter-BgpUcUaW.js → experimenter-DT9v2Pto.js} +1 -1
- package/dist/health-D6MKxV46.js +1715 -0
- package/dist/health-rGXaJvYJ.js +8 -0
- package/dist/index-c924O9mj.d.ts +1432 -0
- package/dist/index.d.ts +56 -735
- package/dist/index.js +2 -2
- package/dist/interactive-prompt-kzkG24Rn.js +183 -0
- package/dist/{health-DudlnqXd.js → manifest-read-g4zt3DXJ.js} +280 -2014
- package/dist/modules/interactive-prompt/index.d.ts +86 -0
- package/dist/modules/interactive-prompt/index.js +6 -0
- package/dist/recovery-engine-BKGBeBnW.js +281 -0
- package/dist/{routing-0ykvBl_4.js → routing-CzF0p6lI.js} +2 -2
- package/dist/{run-CCxsv-9M.js → run-7h2-DIjt.js} +282 -37
- package/dist/run-DB9P6m_P.js +14 -0
- package/dist/src/modules/decision-router/index.js +1 -1
- package/dist/src/modules/recovery-engine/index.d.ts +1101 -0
- package/dist/src/modules/recovery-engine/index.js +5 -0
- package/dist/{upgrade-OFeC_NIx.js → upgrade-DxzQ1nss.js} +3 -3
- package/dist/{upgrade-aW7GYL2F.js → upgrade-MP9XzrI6.js} +2 -2
- package/dist/version-manager-impl-GZDUBt0Q.js +4 -0
- package/dist/work-graph-repository-DZyJv5pV.js +265 -0
- package/package.json +1 -1
- package/dist/adapter-registry-k7ZX3Bz6.js +0 -4
- package/dist/health-CLNmnZiw.js +0 -6
- package/dist/run-ChxsPICN.js +0 -10
- package/dist/version-manager-impl-BCSf5E3j.js +0 -4
- /package/dist/{decisions-C0pz9Clx.js → decisions-CzSIEeGP.js} +0 -0
- /package/dist/{routing-CcBOCuC9.js → routing-DFxoKHDt.js} +0 -0
- /package/dist/{version-manager-impl-FH4TTnXm.js → version-manager-impl-qFBiO4Eh.js} +0 -0
|
@@ -1,1533 +1,62 @@
|
|
|
1
1
|
import { createLogger } from "./logger-KeHncl-f.js";
|
|
2
|
-
import {
|
|
3
|
-
import { createRequire } from "module";
|
|
2
|
+
import { LEARNING_FINDING, createDecision, getDecisionsByCategory } from "./dist-K_RRWnBX.js";
|
|
4
3
|
import * as path$1 from "path";
|
|
5
|
-
import {
|
|
4
|
+
import { join } from "path";
|
|
6
5
|
import { readFile } from "fs/promises";
|
|
7
6
|
import { EventEmitter } from "node:events";
|
|
8
7
|
import { YAMLException, load } from "js-yaml";
|
|
9
8
|
import { existsSync, promises, readFileSync, readdirSync, statSync } from "node:fs";
|
|
10
|
-
import { execSync, spawn
|
|
9
|
+
import { execSync, spawn } from "node:child_process";
|
|
11
10
|
import * as path$2 from "node:path";
|
|
12
11
|
import { basename as basename$1, dirname as dirname$1, join as join$1, resolve as resolve$1 } from "node:path";
|
|
13
12
|
import { z } from "zod";
|
|
14
|
-
import { mkdir as mkdir$1, open, readFile as readFile$1, unlink, writeFile as writeFile$1 } from "node:fs/promises";
|
|
13
|
+
import { mkdir as mkdir$1, open, readFile as readFile$1, unlink as unlink$1, writeFile as writeFile$1 } from "node:fs/promises";
|
|
15
14
|
import * as fs from "fs";
|
|
16
|
-
import {
|
|
17
|
-
import { createRequire as createRequire$1 } from "node:module";
|
|
15
|
+
import { createRequire } from "node:module";
|
|
18
16
|
import { fileURLToPath } from "node:url";
|
|
19
17
|
import { execSync as execSync$1 } from "child_process";
|
|
20
18
|
|
|
21
|
-
//#region
|
|
22
|
-
var __create = Object.create;
|
|
23
|
-
var __defProp = Object.defineProperty;
|
|
24
|
-
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
25
|
-
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
26
|
-
var __getProtoOf = Object.getPrototypeOf;
|
|
27
|
-
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
28
|
-
var __commonJS = (cb, mod) => function() {
|
|
29
|
-
return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
|
|
30
|
-
};
|
|
31
|
-
var __copyProps = (to, from, except, desc) => {
|
|
32
|
-
if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
|
|
33
|
-
key = keys[i];
|
|
34
|
-
if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, {
|
|
35
|
-
get: ((k) => from[k]).bind(null, key),
|
|
36
|
-
enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable
|
|
37
|
-
});
|
|
38
|
-
}
|
|
39
|
-
return to;
|
|
40
|
-
};
|
|
41
|
-
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", {
|
|
42
|
-
value: mod,
|
|
43
|
-
enumerable: true
|
|
44
|
-
}) : target, mod));
|
|
45
|
-
var __require = /* @__PURE__ */ createRequire(import.meta.url);
|
|
46
|
-
|
|
47
|
-
//#endregion
|
|
48
|
-
//#region src/utils/git-root.ts
|
|
49
|
-
/**
|
|
50
|
-
* Resolve the main git repository root, even from a linked worktree.
|
|
51
|
-
*
|
|
52
|
-
* In the main worktree, `--git-common-dir` returns `.git` (relative).
|
|
53
|
-
* In a linked worktree, it returns the absolute path to the main `.git` dir.
|
|
54
|
-
* Either way, `dirname()` of the resolved absolute path yields the repo root.
|
|
55
|
-
*
|
|
56
|
-
* Falls back to `cwd` if not in a git repo or git is unavailable.
|
|
57
|
-
*/
|
|
58
|
-
async function resolveMainRepoRoot(cwd = process.cwd()) {
|
|
59
|
-
return new Promise((res) => {
|
|
60
|
-
let stdout = "";
|
|
61
|
-
const proc = spawn("git", ["rev-parse", "--git-common-dir"], {
|
|
62
|
-
cwd,
|
|
63
|
-
stdio: [
|
|
64
|
-
"ignore",
|
|
65
|
-
"pipe",
|
|
66
|
-
"pipe"
|
|
67
|
-
]
|
|
68
|
-
});
|
|
69
|
-
if (proc.stdout !== null) proc.stdout.on("data", (chunk) => {
|
|
70
|
-
stdout += chunk.toString("utf-8");
|
|
71
|
-
});
|
|
72
|
-
proc.on("error", () => {
|
|
73
|
-
res(cwd);
|
|
74
|
-
});
|
|
75
|
-
proc.on("close", (code) => {
|
|
76
|
-
if (code !== 0) {
|
|
77
|
-
res(cwd);
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const commonDir = stdout.trim();
|
|
81
|
-
if (!commonDir) {
|
|
82
|
-
res(cwd);
|
|
83
|
-
return;
|
|
84
|
-
}
|
|
85
|
-
const absCommonDir = resolve$1(cwd, commonDir);
|
|
86
|
-
res(dirname$1(absCommonDir));
|
|
87
|
-
});
|
|
88
|
-
});
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
//#endregion
|
|
92
|
-
//#region src/persistence/adapter.ts
|
|
93
|
-
/**
|
|
94
|
-
* Create a DatabaseAdapter for the specified (or auto-detected) backend.
|
|
95
|
-
*
|
|
96
|
-
* This shim wraps the core factory and injects the concrete DoltClient
|
|
97
|
-
* constructor as the doltClientFactory parameter, so monolith callers
|
|
98
|
-
* get Dolt support transparently.
|
|
99
|
-
*/
|
|
100
|
-
function createDatabaseAdapter$1(config) {
|
|
101
|
-
return createDatabaseAdapter(config, (repoPath) => new DoltClient({ repoPath }));
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
//#endregion
|
|
105
|
-
//#region src/modules/stop-after/types.ts
|
|
106
|
-
/**
|
|
107
|
-
* Stop-After Gate Module — Types
|
|
108
|
-
*
|
|
109
|
-
* Defines the PhaseName type and all parameter/result types for the stop-after gate.
|
|
110
|
-
* VALID_PHASES is the canonical source for pipeline phase names; auto.ts imports from here.
|
|
111
|
-
*/
|
|
112
|
-
/** Canonical pipeline phase names. This is the single source of truth for all phase lists. */
|
|
113
|
-
const VALID_PHASES = [
|
|
114
|
-
"research",
|
|
115
|
-
"analysis",
|
|
116
|
-
"planning",
|
|
117
|
-
"solutioning",
|
|
118
|
-
"implementation"
|
|
119
|
-
];
|
|
120
|
-
/**
|
|
121
|
-
* Alias for VALID_PHASES retained for backward compatibility with existing imports.
|
|
122
|
-
* @deprecated Use VALID_PHASES directly.
|
|
123
|
-
*/
|
|
124
|
-
const STOP_AFTER_VALID_PHASES = VALID_PHASES;
|
|
125
|
-
|
|
126
|
-
//#endregion
|
|
127
|
-
//#region src/cli/commands/pipeline-shared.ts
|
|
128
|
-
/**
|
|
129
|
-
* Parse a DB timestamp string to a Date, correctly treating it as UTC.
|
|
130
|
-
*
|
|
131
|
-
* SQLite stores timestamps as "YYYY-MM-DD HH:MM:SS" without a timezone suffix.
|
|
132
|
-
* JavaScript's Date constructor parses strings without a timezone suffix as
|
|
133
|
-
* *local time*, which causes staleness/duration to be calculated incorrectly
|
|
134
|
-
* on machines not in UTC.
|
|
135
|
-
*
|
|
136
|
-
* Fix: append 'Z' if the string has no timezone marker so it is always
|
|
137
|
-
* parsed as UTC.
|
|
138
|
-
*/
|
|
139
|
-
function parseDbTimestampAsUtc(ts) {
|
|
140
|
-
if (ts.endsWith("Z") || /[+-]\d{2}:\d{2}$/.test(ts)) return new Date(ts);
|
|
141
|
-
return new Date(ts.replace(" ", "T") + "Z");
|
|
142
|
-
}
|
|
143
|
-
const __filename = fileURLToPath(import.meta.url);
|
|
144
|
-
const __dirname$1 = dirname(__filename);
|
|
145
|
-
/**
|
|
146
|
-
* Find the package root by walking up until we find package.json.
|
|
147
|
-
* Works regardless of build output structure (tsdown bundles into
|
|
148
|
-
* dist/cli/index.js, not dist/cli/commands/auto.js).
|
|
149
|
-
*/
|
|
150
|
-
function findPackageRoot(startDir) {
|
|
151
|
-
let dir = startDir;
|
|
152
|
-
while (dir !== dirname(dir)) {
|
|
153
|
-
if (existsSync$1(join(dir, "package.json"))) return dir;
|
|
154
|
-
dir = dirname(dir);
|
|
155
|
-
}
|
|
156
|
-
return startDir;
|
|
157
|
-
}
|
|
158
|
-
const PACKAGE_ROOT = join(__dirname$1, "..", "..", "..");
|
|
159
|
-
/**
|
|
160
|
-
* Resolve the absolute path to the bmad-method package's src/ directory.
|
|
161
|
-
* Uses createRequire so it works in ESM without import.meta.resolve polyfills.
|
|
162
|
-
* Returns null if bmad-method is not installed.
|
|
163
|
-
*/
|
|
164
|
-
function resolveBmadMethodSrcPath(fromDir = __dirname$1) {
|
|
165
|
-
try {
|
|
166
|
-
const require$1 = createRequire$1(join(fromDir, "synthetic.js"));
|
|
167
|
-
const pkgJsonPath = require$1.resolve("bmad-method/package.json");
|
|
168
|
-
return join(dirname(pkgJsonPath), "src");
|
|
169
|
-
} catch {
|
|
170
|
-
return null;
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Read the version field from bmad-method's package.json.
|
|
175
|
-
* Returns 'unknown' if not resolvable.
|
|
176
|
-
*/
|
|
177
|
-
function resolveBmadMethodVersion(fromDir = __dirname$1) {
|
|
178
|
-
try {
|
|
179
|
-
const require$1 = createRequire$1(join(fromDir, "synthetic.js"));
|
|
180
|
-
const pkgJsonPath = require$1.resolve("bmad-method/package.json");
|
|
181
|
-
const pkg = require$1(pkgJsonPath);
|
|
182
|
-
return pkg.version ?? "unknown";
|
|
183
|
-
} catch {
|
|
184
|
-
return "unknown";
|
|
185
|
-
}
|
|
186
|
-
}
|
|
187
|
-
/** BMAD baseline token total for full pipeline comparison (analysis+planning+solutioning+implementation) */
|
|
188
|
-
const BMAD_BASELINE_TOKENS_FULL = 56800;
|
|
189
|
-
/** BMAD baseline token total for create+dev+review comparison */
|
|
190
|
-
const BMAD_BASELINE_TOKENS = 23800;
|
|
191
|
-
/** Story key pattern: e.g. "10-1", "1-1a", "NEW-26", "E6" */
|
|
192
|
-
const STORY_KEY_PATTERN$1 = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)?$/;
|
|
193
|
-
/**
|
|
194
|
-
* Top-level keys in .claude/settings.json that substrate owns.
|
|
195
|
-
* On init, these are set/updated unconditionally.
|
|
196
|
-
* User-defined keys outside this set are never touched.
|
|
197
|
-
*/
|
|
198
|
-
const SUBSTRATE_OWNED_SETTINGS_KEYS = ["statusLine"];
|
|
199
|
-
function getSubstrateDefaultSettings() {
|
|
200
|
-
return { statusLine: {
|
|
201
|
-
type: "command",
|
|
202
|
-
command: "bash \"$CLAUDE_PROJECT_DIR\"/.claude/statusline.sh",
|
|
203
|
-
padding: 0
|
|
204
|
-
} };
|
|
205
|
-
}
|
|
206
|
-
/**
|
|
207
|
-
* Format output according to the requested format.
|
|
208
|
-
*/
|
|
209
|
-
function formatOutput(data, format, success = true, errorMessage) {
|
|
210
|
-
if (format === "json") {
|
|
211
|
-
if (!success) return JSON.stringify({
|
|
212
|
-
success: false,
|
|
213
|
-
error: errorMessage ?? "Unknown error"
|
|
214
|
-
});
|
|
215
|
-
return JSON.stringify({
|
|
216
|
-
success: true,
|
|
217
|
-
data
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
if (typeof data === "string") return data;
|
|
221
|
-
return JSON.stringify(data, null, 2);
|
|
222
|
-
}
|
|
223
|
-
/**
|
|
224
|
-
* Build a human-readable token telemetry display from summary rows.
|
|
225
|
-
*/
|
|
226
|
-
function formatTokenTelemetry(summary, baselineTokens = BMAD_BASELINE_TOKENS) {
|
|
227
|
-
if (summary.length === 0) return "No token usage recorded.";
|
|
228
|
-
let totalInput = 0;
|
|
229
|
-
let totalOutput = 0;
|
|
230
|
-
let totalCost = 0;
|
|
231
|
-
const lines = ["Pipeline Token Usage:"];
|
|
232
|
-
for (const row of summary) {
|
|
233
|
-
totalInput += row.total_input_tokens;
|
|
234
|
-
totalOutput += row.total_output_tokens;
|
|
235
|
-
totalCost += row.total_cost_usd;
|
|
236
|
-
const cost = `$${row.total_cost_usd.toFixed(4)}`;
|
|
237
|
-
lines.push(` ${row.phase} (${row.agent}): ${row.total_input_tokens.toLocaleString()} input / ${row.total_output_tokens.toLocaleString()} output (${cost})`);
|
|
238
|
-
}
|
|
239
|
-
lines.push(" " + "─".repeat(55));
|
|
240
|
-
const costDisplay = `$${totalCost.toFixed(4)}`;
|
|
241
|
-
lines.push(` Total: ${totalInput.toLocaleString()} input / ${totalOutput.toLocaleString()} output (${costDisplay})`);
|
|
242
|
-
const totalTokens = totalInput + totalOutput;
|
|
243
|
-
const savingsPct = baselineTokens > 0 ? Math.round((baselineTokens - totalTokens) / baselineTokens * 100) : 0;
|
|
244
|
-
const savingsLabel = savingsPct >= 0 ? `Savings: ${savingsPct}%` : `Overhead: +${Math.abs(savingsPct)}%`;
|
|
245
|
-
lines.push(` BMAD Baseline: ${baselineTokens.toLocaleString()} tokens → ${savingsLabel}`);
|
|
246
|
-
return lines.join("\n");
|
|
247
|
-
}
|
|
248
|
-
/**
|
|
249
|
-
* Validate a story key has the expected format: <epic>-<story> (e.g., "10-1").
|
|
250
|
-
*/
|
|
251
|
-
function validateStoryKey(key) {
|
|
252
|
-
return STORY_KEY_PATTERN$1.test(key);
|
|
253
|
-
}
|
|
254
|
-
/**
|
|
255
|
-
* Build the AC5 JSON status schema for a pipeline run.
|
|
256
|
-
*/
|
|
257
|
-
function buildPipelineStatusOutput(run, tokenSummary, decisionsCount, storiesCount) {
|
|
258
|
-
const phases = {};
|
|
259
|
-
const phaseTokenMap = {};
|
|
260
|
-
for (const row of tokenSummary) {
|
|
261
|
-
if (!phaseTokenMap[row.phase]) phaseTokenMap[row.phase] = {
|
|
262
|
-
input: 0,
|
|
263
|
-
output: 0
|
|
264
|
-
};
|
|
265
|
-
phaseTokenMap[row.phase].input += row.total_input_tokens;
|
|
266
|
-
phaseTokenMap[row.phase].output += row.total_output_tokens;
|
|
267
|
-
}
|
|
268
|
-
let phaseHistory = [];
|
|
269
|
-
try {
|
|
270
|
-
if (run.config_json) {
|
|
271
|
-
const config = JSON.parse(run.config_json);
|
|
272
|
-
phaseHistory = config.phaseHistory ?? [];
|
|
273
|
-
}
|
|
274
|
-
} catch {}
|
|
275
|
-
const currentPhase = run.current_phase ?? null;
|
|
276
|
-
for (const phaseName of VALID_PHASES) {
|
|
277
|
-
const historyEntry = phaseHistory.find((h) => h.phase === phaseName);
|
|
278
|
-
const tokenUsage = phaseTokenMap[phaseName] ?? {
|
|
279
|
-
input: 0,
|
|
280
|
-
output: 0
|
|
281
|
-
};
|
|
282
|
-
if (historyEntry?.completedAt) {
|
|
283
|
-
phases[phaseName] = {
|
|
284
|
-
status: "complete",
|
|
285
|
-
completed_at: historyEntry.completedAt,
|
|
286
|
-
token_usage: tokenUsage
|
|
287
|
-
};
|
|
288
|
-
if (historyEntry.startedAt) phases[phaseName].started_at = historyEntry.startedAt;
|
|
289
|
-
} else if (phaseName === currentPhase || historyEntry?.startedAt) phases[phaseName] = {
|
|
290
|
-
status: "running",
|
|
291
|
-
started_at: historyEntry?.startedAt,
|
|
292
|
-
token_usage: tokenUsage
|
|
293
|
-
};
|
|
294
|
-
else phases[phaseName] = { status: "pending" };
|
|
295
|
-
}
|
|
296
|
-
let totalInput = 0;
|
|
297
|
-
let totalOutput = 0;
|
|
298
|
-
let totalCost = 0;
|
|
299
|
-
for (const row of tokenSummary) {
|
|
300
|
-
totalInput += row.total_input_tokens;
|
|
301
|
-
totalOutput += row.total_output_tokens;
|
|
302
|
-
totalCost += row.total_cost_usd;
|
|
303
|
-
}
|
|
304
|
-
let activeDispatches = 0;
|
|
305
|
-
let storiesSummary;
|
|
306
|
-
try {
|
|
307
|
-
if (run.token_usage_json) {
|
|
308
|
-
const state = JSON.parse(run.token_usage_json);
|
|
309
|
-
if (state.stories && Object.keys(state.stories).length > 0) {
|
|
310
|
-
const now = Date.now();
|
|
311
|
-
let completed = 0;
|
|
312
|
-
let inProgress = 0;
|
|
313
|
-
let escalated = 0;
|
|
314
|
-
let pending = 0;
|
|
315
|
-
const details = {};
|
|
316
|
-
for (const [key, s] of Object.entries(state.stories)) {
|
|
317
|
-
const phase = s.phase ?? "PENDING";
|
|
318
|
-
if (phase !== "PENDING" && phase !== "COMPLETE" && phase !== "ESCALATED") activeDispatches++;
|
|
319
|
-
if (phase === "COMPLETE") completed++;
|
|
320
|
-
else if (phase === "ESCALATED") escalated++;
|
|
321
|
-
else if (phase === "PENDING") pending++;
|
|
322
|
-
else inProgress++;
|
|
323
|
-
const elapsed = s.startedAt != null ? Math.max(0, Math.round((now - new Date(s.startedAt).getTime()) / 1e3)) : 0;
|
|
324
|
-
details[key] = {
|
|
325
|
-
phase,
|
|
326
|
-
review_cycles: s.reviewCycles ?? 0,
|
|
327
|
-
elapsed_seconds: elapsed
|
|
328
|
-
};
|
|
329
|
-
}
|
|
330
|
-
storiesSummary = {
|
|
331
|
-
completed,
|
|
332
|
-
in_progress: inProgress,
|
|
333
|
-
escalated,
|
|
334
|
-
pending,
|
|
335
|
-
details
|
|
336
|
-
};
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
|
-
} catch {}
|
|
340
|
-
const derivedStoriesCount = storiesSummary !== void 0 ? storiesSummary.completed + storiesSummary.in_progress + storiesSummary.escalated + storiesSummary.pending : storiesCount;
|
|
341
|
-
const derivedStoriesCompleted = storiesSummary !== void 0 ? storiesSummary.completed : 0;
|
|
342
|
-
return {
|
|
343
|
-
run_id: run.id,
|
|
344
|
-
current_phase: currentPhase,
|
|
345
|
-
phases,
|
|
346
|
-
total_tokens: {
|
|
347
|
-
input: totalInput,
|
|
348
|
-
output: totalOutput,
|
|
349
|
-
cost_usd: totalCost
|
|
350
|
-
},
|
|
351
|
-
decisions_count: decisionsCount,
|
|
352
|
-
stories_count: derivedStoriesCount,
|
|
353
|
-
stories_completed: derivedStoriesCompleted,
|
|
354
|
-
last_activity: run.updated_at ?? "",
|
|
355
|
-
staleness_seconds: Math.round((Date.now() - parseDbTimestampAsUtc(run.updated_at ?? "").getTime()) / 1e3),
|
|
356
|
-
last_event_ts: run.updated_at ?? "",
|
|
357
|
-
active_dispatches: activeDispatches,
|
|
358
|
-
...storiesSummary !== void 0 ? { stories: storiesSummary } : {}
|
|
359
|
-
};
|
|
360
|
-
}
|
|
361
|
-
/**
|
|
362
|
-
* Format a pipeline status summary in human-readable format.
|
|
363
|
-
*/
|
|
364
|
-
function formatPipelineStatusHuman(status) {
|
|
365
|
-
const lines = [];
|
|
366
|
-
lines.push(`Pipeline Run: ${status.run_id}`);
|
|
367
|
-
lines.push(` Current Phase: ${status.current_phase ?? "N/A"}`);
|
|
368
|
-
lines.push("");
|
|
369
|
-
lines.push(" Phase Status:");
|
|
370
|
-
const statusIcons = {
|
|
371
|
-
complete: "[DONE]",
|
|
372
|
-
running: "[RUN] ",
|
|
373
|
-
pending: "[ ]"
|
|
374
|
-
};
|
|
375
|
-
for (const [phaseName, phaseInfo] of Object.entries(status.phases)) {
|
|
376
|
-
const icon = statusIcons[phaseInfo.status] ?? "[?]";
|
|
377
|
-
let line = ` ${icon} ${phaseName}`;
|
|
378
|
-
if (phaseInfo.status === "complete" && phaseInfo.completed_at) line += ` (completed: ${phaseInfo.completed_at})`;
|
|
379
|
-
if (phaseInfo.token_usage && (phaseInfo.token_usage.input > 0 || phaseInfo.token_usage.output > 0)) line += ` — tokens: ${phaseInfo.token_usage.input.toLocaleString()} in / ${phaseInfo.token_usage.output.toLocaleString()} out`;
|
|
380
|
-
lines.push(line);
|
|
381
|
-
}
|
|
382
|
-
lines.push("");
|
|
383
|
-
lines.push(` Total Tokens: ${(status.total_tokens.input + status.total_tokens.output).toLocaleString()} (in: ${status.total_tokens.input.toLocaleString()}, out: ${status.total_tokens.output.toLocaleString()})`);
|
|
384
|
-
lines.push(` Total Cost: $${status.total_tokens.cost_usd.toFixed(4)}`);
|
|
385
|
-
lines.push(` Decisions: ${status.decisions_count}`);
|
|
386
|
-
lines.push(` Stories: ${status.stories_count}`);
|
|
387
|
-
if (status.stories !== void 0 && Object.keys(status.stories.details).length > 0) {
|
|
388
|
-
lines.push("");
|
|
389
|
-
lines.push(" Sprint Progress:");
|
|
390
|
-
lines.push(" " + "─".repeat(68));
|
|
391
|
-
lines.push(` ${"STORY".padEnd(10)} ${"PHASE".padEnd(24)} ${"CYCLES".padEnd(8)} ELAPSED`);
|
|
392
|
-
lines.push(" " + "─".repeat(68));
|
|
393
|
-
for (const [key, detail] of Object.entries(status.stories.details)) {
|
|
394
|
-
const elapsed = detail.elapsed_seconds > 0 ? `${detail.elapsed_seconds}s` : "-";
|
|
395
|
-
lines.push(` ${key.padEnd(10)} ${detail.phase.padEnd(24)} ${String(detail.review_cycles).padEnd(8)} ${elapsed}`);
|
|
396
|
-
}
|
|
397
|
-
lines.push(" " + "─".repeat(68));
|
|
398
|
-
lines.push(` Completed: ${status.stories.completed} In Progress: ${status.stories.in_progress} Escalated: ${status.stories.escalated} Pending: ${status.stories.pending}`);
|
|
399
|
-
}
|
|
400
|
-
return lines.join("\n");
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Format a complete pipeline run summary.
|
|
404
|
-
*/
|
|
405
|
-
function formatPipelineSummary(run, tokenSummary, decisionsCount, storiesCount, durationMs, format) {
|
|
406
|
-
let totalInput = 0;
|
|
407
|
-
let totalOutput = 0;
|
|
408
|
-
let totalCost = 0;
|
|
409
|
-
for (const row of tokenSummary) {
|
|
410
|
-
totalInput += row.total_input_tokens;
|
|
411
|
-
totalOutput += row.total_output_tokens;
|
|
412
|
-
totalCost += row.total_cost_usd;
|
|
413
|
-
}
|
|
414
|
-
const totalTokens = totalInput + totalOutput;
|
|
415
|
-
const savingsPct = BMAD_BASELINE_TOKENS_FULL > 0 ? Math.round((BMAD_BASELINE_TOKENS_FULL - totalTokens) / BMAD_BASELINE_TOKENS_FULL * 100) : 0;
|
|
416
|
-
const durationSec = Math.round(durationMs / 1e3);
|
|
417
|
-
if (format === "json") return JSON.stringify({
|
|
418
|
-
run_id: run.id,
|
|
419
|
-
status: run.status,
|
|
420
|
-
duration_ms: durationMs,
|
|
421
|
-
phases_completed: VALID_PHASES.length,
|
|
422
|
-
decisions_count: decisionsCount,
|
|
423
|
-
stories_count: storiesCount,
|
|
424
|
-
token_usage: {
|
|
425
|
-
input: totalInput,
|
|
426
|
-
output: totalOutput,
|
|
427
|
-
total: totalTokens,
|
|
428
|
-
cost_usd: totalCost,
|
|
429
|
-
bmad_baseline: BMAD_BASELINE_TOKENS_FULL,
|
|
430
|
-
savings_pct: savingsPct
|
|
431
|
-
}
|
|
432
|
-
});
|
|
433
|
-
const lines = [
|
|
434
|
-
"┌─────────────────────────────────────────────────────┐",
|
|
435
|
-
"│ Pipeline Run Summary │",
|
|
436
|
-
"└─────────────────────────────────────────────────────┘",
|
|
437
|
-
` Run ID: ${run.id}`,
|
|
438
|
-
` Status: ${run.status}`,
|
|
439
|
-
` Duration: ${durationSec}s`,
|
|
440
|
-
` Phases Complete: ${VALID_PHASES.length}`,
|
|
441
|
-
` Decisions: ${decisionsCount}`,
|
|
442
|
-
` Stories: ${storiesCount}`,
|
|
443
|
-
"",
|
|
444
|
-
` Token Usage: ${totalTokens.toLocaleString()} total`,
|
|
445
|
-
` Input: ${totalInput.toLocaleString()}`,
|
|
446
|
-
` Output: ${totalOutput.toLocaleString()}`,
|
|
447
|
-
` Cost: $${totalCost.toFixed(4)}`,
|
|
448
|
-
"",
|
|
449
|
-
` BMAD Baseline: ${BMAD_BASELINE_TOKENS_FULL.toLocaleString()} tokens`,
|
|
450
|
-
` Token Savings: ${savingsPct >= 0 ? savingsPct + "%" : "N/A (overhead)"}`
|
|
451
|
-
];
|
|
452
|
-
return lines.join("\n");
|
|
453
|
-
}
|
|
454
|
-
|
|
455
|
-
//#endregion
|
|
456
|
-
//#region src/modules/work-graph/cycle-detector.ts
|
|
457
|
-
/**
|
|
458
|
-
* detectCycles — DFS-based cycle detection for story dependency graphs.
|
|
459
|
-
*
|
|
460
|
-
* Story 31-7: Cycle Detection in Work Graph
|
|
461
|
-
*
|
|
462
|
-
* Pure function; no database or I/O dependencies.
|
|
463
|
-
*/
|
|
464
|
-
/**
|
|
465
|
-
* Detect cycles in a directed dependency graph represented as an edge list.
|
|
466
|
-
*
|
|
467
|
-
* Each edge `{ story_key, depends_on }` means story_key depends on depends_on
|
|
468
|
-
* (i.e. story_key → depends_on is the directed edge we traverse).
|
|
469
|
-
*
|
|
470
|
-
* Uses iterative DFS with an explicit stack to avoid call-stack overflows on
|
|
471
|
-
* large graphs, but also supports a nested recursive helper for cycle path
|
|
472
|
-
* reconstruction.
|
|
473
|
-
*
|
|
474
|
-
* @param edges - List of dependency edges to check.
|
|
475
|
-
* @returns `null` if the graph is acyclic (safe to persist), or a `string[]`
|
|
476
|
-
* containing the cycle path with the first and last element being the same
|
|
477
|
-
* story key (e.g. `['A', 'B', 'A']`).
|
|
478
|
-
*/
|
|
479
|
-
function detectCycles(edges) {
|
|
480
|
-
const adj = new Map();
|
|
481
|
-
for (const { story_key, depends_on } of edges) {
|
|
482
|
-
if (!adj.has(story_key)) adj.set(story_key, []);
|
|
483
|
-
adj.get(story_key).push(depends_on);
|
|
484
|
-
}
|
|
485
|
-
const visited = new Set();
|
|
486
|
-
const visiting = new Set();
|
|
487
|
-
const path$3 = [];
|
|
488
|
-
function dfs(node) {
|
|
489
|
-
if (visiting.has(node)) {
|
|
490
|
-
const cycleStart = path$3.indexOf(node);
|
|
491
|
-
return [...path$3.slice(cycleStart), node];
|
|
492
|
-
}
|
|
493
|
-
if (visited.has(node)) return null;
|
|
494
|
-
visiting.add(node);
|
|
495
|
-
path$3.push(node);
|
|
496
|
-
for (const neighbor of adj.get(node) ?? []) {
|
|
497
|
-
const cycle = dfs(neighbor);
|
|
498
|
-
if (cycle !== null) return cycle;
|
|
499
|
-
}
|
|
500
|
-
path$3.pop();
|
|
501
|
-
visiting.delete(node);
|
|
502
|
-
visited.add(node);
|
|
503
|
-
return null;
|
|
504
|
-
}
|
|
505
|
-
const allNodes = new Set([...edges.map((e) => e.story_key), ...edges.map((e) => e.depends_on)]);
|
|
506
|
-
for (const node of allNodes) if (!visited.has(node)) {
|
|
507
|
-
const cycle = dfs(node);
|
|
508
|
-
if (cycle !== null) return cycle;
|
|
509
|
-
}
|
|
510
|
-
return null;
|
|
511
|
-
}
|
|
512
|
-
|
|
513
|
-
//#endregion
|
|
514
|
-
//#region src/modules/state/work-graph-repository.ts
|
|
515
|
-
var WorkGraphRepository = class {
|
|
516
|
-
constructor(db) {
|
|
517
|
-
this.db = db;
|
|
518
|
-
}
|
|
519
|
-
/**
|
|
520
|
-
* Insert or replace a work-graph story node.
|
|
521
|
-
* Uses DELETE + INSERT so it works on InMemoryDatabaseAdapter (which does
|
|
522
|
-
* not support ON DUPLICATE KEY UPDATE).
|
|
523
|
-
*/
|
|
524
|
-
async upsertStory(story) {
|
|
525
|
-
await this.db.query(`DELETE FROM wg_stories WHERE story_key = ?`, [story.story_key]);
|
|
526
|
-
await this.db.query(`INSERT INTO wg_stories (story_key, epic, title, status, spec_path, created_at, updated_at, completed_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, [
|
|
527
|
-
story.story_key,
|
|
528
|
-
story.epic,
|
|
529
|
-
story.title ?? null,
|
|
530
|
-
story.status,
|
|
531
|
-
story.spec_path ?? null,
|
|
532
|
-
story.created_at ?? null,
|
|
533
|
-
story.updated_at ?? null,
|
|
534
|
-
story.completed_at ?? null
|
|
535
|
-
]);
|
|
536
|
-
}
|
|
537
|
-
/**
|
|
538
|
-
* Insert a dependency edge. Idempotent — if a row with the same
|
|
539
|
-
* (story_key, depends_on) already exists it is silently skipped.
|
|
540
|
-
*/
|
|
541
|
-
async addDependency(dep) {
|
|
542
|
-
const existing = await this.db.query(`SELECT story_key FROM story_dependencies WHERE story_key = ? AND depends_on = ?`, [dep.story_key, dep.depends_on]);
|
|
543
|
-
if (existing.length > 0) return;
|
|
544
|
-
await this.db.query(`INSERT INTO story_dependencies (story_key, depends_on, dependency_type, source, created_at) VALUES (?, ?, ?, ?, ?)`, [
|
|
545
|
-
dep.story_key,
|
|
546
|
-
dep.depends_on,
|
|
547
|
-
dep.dependency_type,
|
|
548
|
-
dep.source,
|
|
549
|
-
dep.created_at ?? null
|
|
550
|
-
]);
|
|
551
|
-
}
|
|
552
|
-
/**
|
|
553
|
-
* Persist contract-based dependency edges to `story_dependencies` as
|
|
554
|
-
* best-effort, idempotent writes.
|
|
555
|
-
*
|
|
556
|
-
* - edges where `reason` does NOT start with `'dual export:'` are persisted
|
|
557
|
-
* as `dependency_type = 'blocks'` (hard prerequisites).
|
|
558
|
-
* - edges where `reason` starts with `'dual export:'` are persisted as
|
|
559
|
-
* `dependency_type = 'informs'` (serialization hints, not hard gates).
|
|
560
|
-
*
|
|
561
|
-
* Idempotency is delegated to `addDependency()`, which skips the INSERT if
|
|
562
|
-
* a row with the same `(story_key, depends_on)` already exists.
|
|
563
|
-
*
|
|
564
|
-
* @param edges - Readonly list of contract dependency edges to persist.
|
|
565
|
-
*/
|
|
566
|
-
async addContractDependencies(edges) {
|
|
567
|
-
if (edges.length === 0) return;
|
|
568
|
-
for (const edge of edges) {
|
|
569
|
-
const dependency_type = edge.reason?.startsWith("dual export:") ? "informs" : "blocks";
|
|
570
|
-
await this.addDependency({
|
|
571
|
-
story_key: edge.to,
|
|
572
|
-
depends_on: edge.from,
|
|
573
|
-
dependency_type,
|
|
574
|
-
source: "contract",
|
|
575
|
-
created_at: new Date().toISOString()
|
|
576
|
-
});
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
/**
|
|
580
|
-
* Return all work-graph stories, optionally filtered by epic and/or status.
|
|
581
|
-
*/
|
|
582
|
-
async listStories(filter) {
|
|
583
|
-
if (!filter || !filter.epic && !filter.status) return this.db.query(`SELECT * FROM wg_stories`);
|
|
584
|
-
const conditions = [];
|
|
585
|
-
const params = [];
|
|
586
|
-
if (filter.epic) {
|
|
587
|
-
conditions.push(`epic = ?`);
|
|
588
|
-
params.push(filter.epic);
|
|
589
|
-
}
|
|
590
|
-
if (filter.status) {
|
|
591
|
-
conditions.push(`status = ?`);
|
|
592
|
-
params.push(filter.status);
|
|
593
|
-
}
|
|
594
|
-
const where = conditions.join(" AND ");
|
|
595
|
-
return this.db.query(`SELECT * FROM wg_stories WHERE ${where}`, params);
|
|
596
|
-
}
|
|
597
|
-
/**
|
|
598
|
-
* Update the `status` (and optionally `completed_at`) of an existing
|
|
599
|
-
* work-graph story.
|
|
600
|
-
*
|
|
601
|
-
* This is a read-modify-write operation: SELECT existing row → build
|
|
602
|
-
* updated WgStory → upsertStory(). If no row exists for `storyKey` the
|
|
603
|
-
* call is a no-op (AC4).
|
|
604
|
-
*
|
|
605
|
-
* @param storyKey - Story identifier, e.g. "31-4"
|
|
606
|
-
* @param status - Target WgStoryStatus value
|
|
607
|
-
* @param opts - Optional `completedAt` ISO string for terminal phases
|
|
608
|
-
*/
|
|
609
|
-
async updateStoryStatus(storyKey, status, opts) {
|
|
610
|
-
const rows = await this.db.query(`SELECT * FROM wg_stories WHERE story_key = ?`, [storyKey]);
|
|
611
|
-
if (rows.length === 0) return;
|
|
612
|
-
const existing = rows[0];
|
|
613
|
-
const now = new Date().toISOString();
|
|
614
|
-
const isTerminal = status === "complete" || status === "escalated";
|
|
615
|
-
const updated = {
|
|
616
|
-
...existing,
|
|
617
|
-
status,
|
|
618
|
-
updated_at: now,
|
|
619
|
-
completed_at: isTerminal ? opts?.completedAt ?? now : existing.completed_at
|
|
620
|
-
};
|
|
621
|
-
await this.upsertStory(updated);
|
|
622
|
-
}
|
|
623
|
-
/**
|
|
624
|
-
* Return stories that are eligible for dispatch.
|
|
625
|
-
*
|
|
626
|
-
* A story is ready when:
|
|
627
|
-
* 1. Its status is 'planned' or 'ready', AND
|
|
628
|
-
* 2. It has no 'blocks' dependency whose blocking story is not 'complete'.
|
|
629
|
-
*
|
|
630
|
-
* Soft ('informs') dependencies never block dispatch.
|
|
631
|
-
*
|
|
632
|
-
* This is implemented programmatically rather than via the `ready_stories`
|
|
633
|
-
* VIEW so that the InMemoryDatabaseAdapter can handle it without VIEW support.
|
|
634
|
-
*/
|
|
635
|
-
async getReadyStories() {
|
|
636
|
-
const allStories = await this.db.query(`SELECT * FROM wg_stories`);
|
|
637
|
-
const candidates = allStories.filter((s) => s.status === "planned" || s.status === "ready");
|
|
638
|
-
if (candidates.length === 0) return [];
|
|
639
|
-
const deps = await this.db.query(`SELECT story_key, depends_on FROM story_dependencies WHERE dependency_type = 'blocks'`);
|
|
640
|
-
if (deps.length === 0) return candidates;
|
|
641
|
-
const blockerStatus = new Map(allStories.map((s) => [s.story_key, s.status]));
|
|
642
|
-
const depsMap = new Map();
|
|
643
|
-
for (const d of deps) {
|
|
644
|
-
if (!depsMap.has(d.story_key)) depsMap.set(d.story_key, []);
|
|
645
|
-
depsMap.get(d.story_key).push(d.depends_on);
|
|
646
|
-
}
|
|
647
|
-
return candidates.filter((s) => {
|
|
648
|
-
const blocking = depsMap.get(s.story_key) ?? [];
|
|
649
|
-
return blocking.every((dep) => blockerStatus.get(dep) === "complete");
|
|
650
|
-
});
|
|
651
|
-
}
|
|
652
|
-
/**
|
|
653
|
-
* Return stories that are planned/ready but cannot be dispatched because
|
|
654
|
-
* at least one hard-blocking ('blocks') dependency is not yet complete.
|
|
655
|
-
*
|
|
656
|
-
* For each blocked story, the returned object includes the full WgStory
|
|
657
|
-
* record plus the list of unsatisfied blockers (key, title, status).
|
|
658
|
-
*
|
|
659
|
-
* Soft ('informs') dependencies are ignored here, matching getReadyStories().
|
|
660
|
-
*/
|
|
661
|
-
/**
|
|
662
|
-
* Query the database for all 'blocks' dependency rows and run DFS cycle
|
|
663
|
-
* detection over them.
|
|
664
|
-
*
|
|
665
|
-
* Returns an empty array if no cycle is found (consistent with other
|
|
666
|
-
* repository methods that return empty arrays rather than null).
|
|
667
|
-
*
|
|
668
|
-
* Only 'blocks' deps are checked — soft 'informs' deps cannot cause
|
|
669
|
-
* dispatch deadlocks (AC5).
|
|
670
|
-
*/
|
|
671
|
-
async detectCycles() {
|
|
672
|
-
const rows = await this.db.query(`SELECT story_key, depends_on FROM story_dependencies WHERE dependency_type = 'blocks'`);
|
|
673
|
-
const cycle = detectCycles(rows);
|
|
674
|
-
return cycle ?? [];
|
|
675
|
-
}
|
|
676
|
-
async getBlockedStories() {
|
|
677
|
-
const allStories = await this.db.query(`SELECT * FROM wg_stories`);
|
|
678
|
-
const candidates = allStories.filter((s) => s.status === "planned" || s.status === "ready");
|
|
679
|
-
if (candidates.length === 0) return [];
|
|
680
|
-
const deps = await this.db.query(`SELECT story_key, depends_on FROM story_dependencies WHERE dependency_type = 'blocks'`);
|
|
681
|
-
if (deps.length === 0) return [];
|
|
682
|
-
const statusMap = new Map(allStories.map((s) => [s.story_key, s]));
|
|
683
|
-
const depsMap = new Map();
|
|
684
|
-
for (const d of deps) {
|
|
685
|
-
if (!depsMap.has(d.story_key)) depsMap.set(d.story_key, []);
|
|
686
|
-
depsMap.get(d.story_key).push(d.depends_on);
|
|
687
|
-
}
|
|
688
|
-
const result = [];
|
|
689
|
-
for (const story of candidates) {
|
|
690
|
-
const blockerKeys = depsMap.get(story.story_key) ?? [];
|
|
691
|
-
const unsatisfied = blockerKeys.filter((key) => statusMap.get(key)?.status !== "complete").map((key) => {
|
|
692
|
-
const s = statusMap.get(key);
|
|
693
|
-
return {
|
|
694
|
-
key,
|
|
695
|
-
title: s?.title ?? key,
|
|
696
|
-
status: s?.status ?? "unknown"
|
|
697
|
-
};
|
|
698
|
-
});
|
|
699
|
-
if (unsatisfied.length > 0) result.push({
|
|
700
|
-
story,
|
|
701
|
-
blockers: unsatisfied
|
|
702
|
-
});
|
|
703
|
-
}
|
|
704
|
-
return result;
|
|
705
|
-
}
|
|
706
|
-
};
|
|
707
|
-
|
|
708
|
-
//#endregion
|
|
709
|
-
//#region src/modules/state/file-store.ts
|
|
710
|
-
/**
|
|
711
|
-
* In-memory / file-backed StateStore implementation.
|
|
712
|
-
*
|
|
713
|
-
* Suitable for CI environments and testing where orchestrator state is
|
|
714
|
-
* ephemeral. Use DoltStateStore for branch-per-story isolation and versioned
|
|
715
|
-
* history in production.
|
|
716
|
-
*/
|
|
717
|
-
var FileStateStore = class {
|
|
718
|
-
_basePath;
|
|
719
|
-
_stories = new Map();
|
|
720
|
-
_metrics = [];
|
|
721
|
-
_contracts = new Map();
|
|
722
|
-
_contractVerifications = new Map();
|
|
723
|
-
/** Key-value metrics store: outer key = runId, inner key = metric key */
|
|
724
|
-
_kvMetrics = new Map();
|
|
725
|
-
constructor(options = {}) {
|
|
726
|
-
this._basePath = options.basePath;
|
|
727
|
-
}
|
|
728
|
-
async initialize() {}
|
|
729
|
-
async close() {}
|
|
730
|
-
async getStoryState(storyKey) {
|
|
731
|
-
return this._stories.get(storyKey);
|
|
732
|
-
}
|
|
733
|
-
async setStoryState(storyKey, state) {
|
|
734
|
-
this._stories.set(storyKey, {
|
|
735
|
-
...state,
|
|
736
|
-
storyKey
|
|
737
|
-
});
|
|
738
|
-
}
|
|
739
|
-
async queryStories(filter) {
|
|
740
|
-
const all = Array.from(this._stories.values());
|
|
741
|
-
return all.filter((record) => {
|
|
742
|
-
if (filter.phase !== void 0) {
|
|
743
|
-
const phases = Array.isArray(filter.phase) ? filter.phase : [filter.phase];
|
|
744
|
-
if (!phases.includes(record.phase)) return false;
|
|
745
|
-
}
|
|
746
|
-
if (filter.sprint !== void 0 && record.sprint !== filter.sprint) return false;
|
|
747
|
-
if (filter.storyKey !== void 0 && record.storyKey !== filter.storyKey) return false;
|
|
748
|
-
return true;
|
|
749
|
-
});
|
|
750
|
-
}
|
|
751
|
-
async recordMetric(metric) {
|
|
752
|
-
const record = {
|
|
753
|
-
...metric,
|
|
754
|
-
recordedAt: metric.recordedAt ?? new Date().toISOString()
|
|
755
|
-
};
|
|
756
|
-
this._metrics.push(record);
|
|
757
|
-
}
|
|
758
|
-
async queryMetrics(filter) {
|
|
759
|
-
const storyKey = filter.storyKey ?? filter.story_key;
|
|
760
|
-
const taskType = filter.taskType ?? filter.task_type;
|
|
761
|
-
return this._metrics.filter((m) => {
|
|
762
|
-
if (storyKey !== void 0 && m.storyKey !== storyKey) return false;
|
|
763
|
-
if (taskType !== void 0 && m.taskType !== taskType) return false;
|
|
764
|
-
if (filter.sprint !== void 0 && m.sprint !== filter.sprint) return false;
|
|
765
|
-
if (filter.dateFrom !== void 0 && m.recordedAt !== void 0 && m.recordedAt < filter.dateFrom) return false;
|
|
766
|
-
if (filter.dateTo !== void 0 && m.recordedAt !== void 0 && m.recordedAt > filter.dateTo) return false;
|
|
767
|
-
if (filter.since !== void 0 && m.recordedAt !== void 0 && m.recordedAt < filter.since) return false;
|
|
768
|
-
return true;
|
|
769
|
-
});
|
|
770
|
-
}
|
|
771
|
-
/**
|
|
772
|
-
* Persist an arbitrary key-value metric for a run.
|
|
773
|
-
* Stored in memory AND written to `{basePath}/kv-metrics.json` when basePath is set.
|
|
774
|
-
*/
|
|
775
|
-
async setMetric(runId, key, value) {
|
|
776
|
-
let runMap = this._kvMetrics.get(runId);
|
|
777
|
-
if (runMap === void 0) {
|
|
778
|
-
runMap = new Map();
|
|
779
|
-
this._kvMetrics.set(runId, runMap);
|
|
780
|
-
}
|
|
781
|
-
runMap.set(key, value);
|
|
782
|
-
if (this._basePath !== void 0) await this._flushKvMetrics();
|
|
783
|
-
}
|
|
784
|
-
/**
|
|
785
|
-
* Retrieve a previously stored key-value metric for a run.
|
|
786
|
-
* Reads from in-memory cache, falling back to the JSON file when basePath is set.
|
|
787
|
-
*/
|
|
788
|
-
async getMetric(runId, key) {
|
|
789
|
-
const inMemory = this._kvMetrics.get(runId)?.get(key);
|
|
790
|
-
if (inMemory !== void 0) return inMemory;
|
|
791
|
-
if (this._basePath !== void 0) try {
|
|
792
|
-
const filePath = join$1(this._basePath, "kv-metrics.json");
|
|
793
|
-
const content = await readFile$1(filePath, "utf-8");
|
|
794
|
-
const parsed = JSON.parse(content);
|
|
795
|
-
return parsed[runId]?.[key] ?? void 0;
|
|
796
|
-
} catch {}
|
|
797
|
-
return void 0;
|
|
798
|
-
}
|
|
799
|
-
/** Serialize the in-memory kv metrics map to JSON on disk. */
|
|
800
|
-
async _flushKvMetrics() {
|
|
801
|
-
if (this._basePath === void 0) return;
|
|
802
|
-
const serialized = {};
|
|
803
|
-
for (const [runId, runMap] of this._kvMetrics) {
|
|
804
|
-
serialized[runId] = {};
|
|
805
|
-
for (const [key, value] of runMap) serialized[runId][key] = value;
|
|
806
|
-
}
|
|
807
|
-
const filePath = join$1(this._basePath, "kv-metrics.json");
|
|
808
|
-
await writeFile$1(filePath, JSON.stringify(serialized, null, 2), "utf-8");
|
|
809
|
-
}
|
|
810
|
-
async getContracts(storyKey) {
|
|
811
|
-
return this._contracts.get(storyKey) ?? [];
|
|
812
|
-
}
|
|
813
|
-
async setContracts(storyKey, contracts) {
|
|
814
|
-
this._contracts.set(storyKey, contracts.map((c) => ({ ...c })));
|
|
815
|
-
}
|
|
816
|
-
async queryContracts(filter) {
|
|
817
|
-
const all = [];
|
|
818
|
-
for (const records of this._contracts.values()) for (const r of records) all.push(r);
|
|
819
|
-
return all.filter((r) => {
|
|
820
|
-
if (filter?.storyKey !== void 0 && r.storyKey !== filter.storyKey) return false;
|
|
821
|
-
if (filter?.direction !== void 0 && r.direction !== filter.direction) return false;
|
|
822
|
-
return true;
|
|
823
|
-
});
|
|
824
|
-
}
|
|
825
|
-
async setContractVerification(storyKey, results) {
|
|
826
|
-
this._contractVerifications.set(storyKey, results.map((r) => ({ ...r })));
|
|
827
|
-
if (this._basePath !== void 0) {
|
|
828
|
-
const serialized = {};
|
|
829
|
-
for (const [key, records] of this._contractVerifications) serialized[key] = records;
|
|
830
|
-
const filePath = join$1(this._basePath, "contract-verifications.json");
|
|
831
|
-
await writeFile$1(filePath, JSON.stringify(serialized, null, 2), "utf-8");
|
|
832
|
-
}
|
|
833
|
-
}
|
|
834
|
-
async getContractVerification(storyKey) {
|
|
835
|
-
return this._contractVerifications.get(storyKey) ?? [];
|
|
836
|
-
}
|
|
837
|
-
async branchForStory(_storyKey) {}
|
|
838
|
-
async mergeStory(_storyKey) {}
|
|
839
|
-
async rollbackStory(_storyKey) {}
|
|
840
|
-
async diffStory(storyKey) {
|
|
841
|
-
return {
|
|
842
|
-
storyKey,
|
|
843
|
-
tables: []
|
|
844
|
-
};
|
|
845
|
-
}
|
|
846
|
-
async getHistory(_limit) {
|
|
847
|
-
return [];
|
|
848
|
-
}
|
|
849
|
-
};
|
|
850
|
-
|
|
851
|
-
//#endregion
|
|
852
|
-
//#region src/modules/state/errors.ts
|
|
853
|
-
/**
|
|
854
|
-
* Typed error classes for the Dolt state store.
|
|
855
|
-
*/
|
|
856
|
-
var StateStoreError = class extends Error {
|
|
857
|
-
code;
|
|
858
|
-
constructor(code, message) {
|
|
859
|
-
super(message);
|
|
860
|
-
this.name = "StateStoreError";
|
|
861
|
-
this.code = code;
|
|
862
|
-
}
|
|
863
|
-
};
|
|
864
|
-
var DoltMergeConflictError = class extends StateStoreError {
|
|
865
|
-
table;
|
|
866
|
-
conflictingKeys;
|
|
867
|
-
rowKey;
|
|
868
|
-
ourValue;
|
|
869
|
-
theirValue;
|
|
870
|
-
constructor(table, conflictingKeys, options) {
|
|
871
|
-
super("DOLT_MERGE_CONFLICT", `Merge conflict in table '${table}' on keys: ${conflictingKeys.join(", ")}`);
|
|
872
|
-
this.name = "DoltMergeConflictError";
|
|
873
|
-
this.table = table;
|
|
874
|
-
this.conflictingKeys = conflictingKeys;
|
|
875
|
-
if (options) {
|
|
876
|
-
this.rowKey = options.rowKey;
|
|
877
|
-
this.ourValue = options.ourValue;
|
|
878
|
-
this.theirValue = options.theirValue;
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
};
|
|
882
|
-
/** Alias for DoltMergeConflictError — used by orchestrator branch lifecycle. */
|
|
883
|
-
const DoltMergeConflict = DoltMergeConflictError;
|
|
884
|
-
|
|
885
|
-
//#endregion
|
|
886
|
-
//#region src/modules/state/dolt-store.ts
|
|
887
|
-
const log = createLogger("modules:state:dolt");
|
|
888
|
-
/**
|
|
889
|
-
* Validate that a story key matches the expected pattern (e.g. "26-7", "1-1a", "NEW-26", "E6").
|
|
890
|
-
* Prevents SQL injection via string-interpolated identifiers.
|
|
891
|
-
*/
|
|
892
|
-
const STORY_KEY_PATTERN = /^[A-Za-z0-9]+(-[A-Za-z0-9]+)?$/;
|
|
893
|
-
function assertValidStoryKey(storyKey) {
|
|
894
|
-
if (!STORY_KEY_PATTERN.test(storyKey)) throw new DoltQueryError("assertValidStoryKey", `Invalid story key: '${storyKey}'. Must match pattern <key> or <epic>-<story> (e.g. "E6", "10-1", "1-1a", "NEW-26").`);
|
|
895
|
-
}
|
|
896
|
-
/**
|
|
897
|
-
* Dolt-backed implementation of the StateStore interface.
|
|
898
|
-
*
|
|
899
|
-
* Constructor accepts a deps object for DI: `{ repoPath, client }`.
|
|
900
|
-
* Call `initialize()` before any CRUD operations.
|
|
901
|
-
*/
|
|
902
|
-
var DoltStateStore = class DoltStateStore {
|
|
903
|
-
_repoPath;
|
|
904
|
-
_client;
|
|
905
|
-
_storyBranches = new Map();
|
|
906
|
-
constructor(options) {
|
|
907
|
-
this._repoPath = options.repoPath;
|
|
908
|
-
this._client = options.client;
|
|
909
|
-
}
|
|
910
|
-
/**
|
|
911
|
-
* Return the branch name for a story if one has been created via branchForStory(),
|
|
912
|
-
* or undefined to use the default (main) branch.
|
|
913
|
-
*/
|
|
914
|
-
_branchFor(storyKey) {
|
|
915
|
-
return this._storyBranches.get(storyKey);
|
|
916
|
-
}
|
|
917
|
-
async initialize() {
|
|
918
|
-
await this._client.connect();
|
|
919
|
-
await this._runMigrations();
|
|
920
|
-
await this.flush("substrate: schema migrations");
|
|
921
|
-
log.debug("DoltStateStore initialized at %s", this._repoPath);
|
|
922
|
-
}
|
|
923
|
-
async close() {
|
|
924
|
-
await this._client.close();
|
|
925
|
-
}
|
|
926
|
-
async _runMigrations() {
|
|
927
|
-
const ddl = [
|
|
928
|
-
`CREATE TABLE IF NOT EXISTS stories (
|
|
929
|
-
story_key VARCHAR(100) NOT NULL,
|
|
930
|
-
phase VARCHAR(30) NOT NULL DEFAULT 'PENDING',
|
|
931
|
-
review_cycles INT NOT NULL DEFAULT 0,
|
|
932
|
-
last_verdict VARCHAR(64) NULL,
|
|
933
|
-
error TEXT NULL,
|
|
934
|
-
started_at VARCHAR(64) NULL,
|
|
935
|
-
completed_at VARCHAR(64) NULL,
|
|
936
|
-
sprint VARCHAR(50) NULL,
|
|
937
|
-
PRIMARY KEY (story_key)
|
|
938
|
-
)`,
|
|
939
|
-
`CREATE TABLE IF NOT EXISTS metrics (
|
|
940
|
-
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
941
|
-
story_key VARCHAR(100) NOT NULL,
|
|
942
|
-
task_type VARCHAR(100) NOT NULL,
|
|
943
|
-
model VARCHAR(100) NULL,
|
|
944
|
-
tokens_in BIGINT NULL,
|
|
945
|
-
tokens_out BIGINT NULL,
|
|
946
|
-
cache_read_tokens BIGINT NULL,
|
|
947
|
-
cost_usd DOUBLE NULL,
|
|
948
|
-
wall_clock_ms BIGINT NULL,
|
|
949
|
-
review_cycles INT NULL,
|
|
950
|
-
stall_count INT NULL,
|
|
951
|
-
result VARCHAR(30) NULL,
|
|
952
|
-
recorded_at VARCHAR(64) NULL,
|
|
953
|
-
sprint VARCHAR(50) NULL,
|
|
954
|
-
PRIMARY KEY (id)
|
|
955
|
-
)`,
|
|
956
|
-
`CREATE TABLE IF NOT EXISTS contracts (
|
|
957
|
-
story_key VARCHAR(100) NOT NULL,
|
|
958
|
-
contract_name VARCHAR(200) NOT NULL,
|
|
959
|
-
direction VARCHAR(20) NOT NULL,
|
|
960
|
-
schema_path VARCHAR(500) NULL,
|
|
961
|
-
transport VARCHAR(200) NULL,
|
|
962
|
-
PRIMARY KEY (story_key, contract_name, direction)
|
|
963
|
-
)`,
|
|
964
|
-
`CREATE TABLE IF NOT EXISTS review_verdicts (
|
|
965
|
-
id BIGINT NOT NULL AUTO_INCREMENT,
|
|
966
|
-
story_key VARCHAR(100) NOT NULL,
|
|
967
|
-
task_type VARCHAR(100) NOT NULL,
|
|
968
|
-
verdict VARCHAR(64) NOT NULL,
|
|
969
|
-
issues_count INT NULL,
|
|
970
|
-
notes TEXT NULL,
|
|
971
|
-
timestamp VARCHAR(64) NULL,
|
|
972
|
-
PRIMARY KEY (id)
|
|
973
|
-
)`
|
|
974
|
-
];
|
|
975
|
-
for (const sql of ddl) await this._client.query(sql);
|
|
976
|
-
try {
|
|
977
|
-
const colRows = await this._client.query(`SHOW COLUMNS FROM repo_map_symbols LIKE 'dependencies'`);
|
|
978
|
-
if (colRows.length === 0) {
|
|
979
|
-
await this._client.query(`ALTER TABLE repo_map_symbols ADD COLUMN dependencies JSON`);
|
|
980
|
-
await this._client.query(`INSERT IGNORE INTO _schema_version (version, description) VALUES (6, 'Add dependencies JSON column to repo_map_symbols (Epic 28-3)')`);
|
|
981
|
-
log.info({
|
|
982
|
-
component: "dolt-state",
|
|
983
|
-
migration: "v5-to-v6",
|
|
984
|
-
column: "dependencies",
|
|
985
|
-
table: "repo_map_symbols"
|
|
986
|
-
}, "Applied migration v5-to-v6: added dependencies column to repo_map_symbols");
|
|
987
|
-
}
|
|
988
|
-
} catch {
|
|
989
|
-
log.debug("Skipping repo_map_symbols migration: table not yet created");
|
|
990
|
-
}
|
|
991
|
-
log.debug("Schema migrations applied");
|
|
992
|
-
}
|
|
993
|
-
/**
|
|
994
|
-
* Commit pending Dolt changes on the current branch.
|
|
995
|
-
* Callers can invoke this after a batch of writes for explicit durability.
|
|
996
|
-
*/
|
|
997
|
-
async flush(message = "substrate: auto-commit") {
|
|
998
|
-
try {
|
|
999
|
-
await this._client.execArgs(["add", "."]);
|
|
1000
|
-
await this._client.execArgs([
|
|
1001
|
-
"commit",
|
|
1002
|
-
"--allow-empty",
|
|
1003
|
-
"-m",
|
|
1004
|
-
message
|
|
1005
|
-
]);
|
|
1006
|
-
log.debug("Dolt flush committed: %s", message);
|
|
1007
|
-
} catch (err) {
|
|
1008
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1009
|
-
log.warn({ detail }, "Dolt flush failed (non-fatal)");
|
|
1010
|
-
}
|
|
1011
|
-
}
|
|
1012
|
-
async getStoryState(storyKey) {
|
|
1013
|
-
const rows = await this._client.query("SELECT * FROM stories WHERE story_key = ?", [storyKey]);
|
|
1014
|
-
if (rows.length === 0) return void 0;
|
|
1015
|
-
return this._rowToStory(rows[0]);
|
|
1016
|
-
}
|
|
1017
|
-
async setStoryState(storyKey, state) {
|
|
1018
|
-
const branch = this._branchFor(storyKey);
|
|
1019
|
-
const sql = `REPLACE INTO stories
|
|
1020
|
-
(story_key, phase, review_cycles, last_verdict, error, started_at, completed_at, sprint)
|
|
1021
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
1022
|
-
await this._client.query(sql, [
|
|
1023
|
-
storyKey,
|
|
1024
|
-
state.phase,
|
|
1025
|
-
state.reviewCycles,
|
|
1026
|
-
state.lastVerdict ?? null,
|
|
1027
|
-
state.error ?? null,
|
|
1028
|
-
state.startedAt ?? null,
|
|
1029
|
-
state.completedAt ?? null,
|
|
1030
|
-
state.sprint ?? null
|
|
1031
|
-
], branch);
|
|
1032
|
-
}
|
|
1033
|
-
async queryStories(filter) {
|
|
1034
|
-
const conditions = [];
|
|
1035
|
-
const params = [];
|
|
1036
|
-
if (filter.phase !== void 0) {
|
|
1037
|
-
const phases = Array.isArray(filter.phase) ? filter.phase : [filter.phase];
|
|
1038
|
-
const placeholders = phases.map(() => "?").join(", ");
|
|
1039
|
-
conditions.push(`phase IN (${placeholders})`);
|
|
1040
|
-
params.push(...phases);
|
|
1041
|
-
}
|
|
1042
|
-
if (filter.sprint !== void 0) {
|
|
1043
|
-
conditions.push("sprint = ?");
|
|
1044
|
-
params.push(filter.sprint);
|
|
1045
|
-
}
|
|
1046
|
-
if (filter.storyKey !== void 0) {
|
|
1047
|
-
conditions.push("story_key = ?");
|
|
1048
|
-
params.push(filter.storyKey);
|
|
1049
|
-
}
|
|
1050
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1051
|
-
const sql = `SELECT * FROM stories ${where} ORDER BY story_key`;
|
|
1052
|
-
const rows = await this._client.query(sql, params);
|
|
1053
|
-
return rows.map((r) => this._rowToStory(r));
|
|
1054
|
-
}
|
|
1055
|
-
_rowToStory(row) {
|
|
1056
|
-
return {
|
|
1057
|
-
storyKey: row.story_key,
|
|
1058
|
-
phase: row.phase,
|
|
1059
|
-
reviewCycles: Number(row.review_cycles),
|
|
1060
|
-
lastVerdict: row.last_verdict ?? void 0,
|
|
1061
|
-
error: row.error ?? void 0,
|
|
1062
|
-
startedAt: row.started_at ?? void 0,
|
|
1063
|
-
completedAt: row.completed_at ?? void 0,
|
|
1064
|
-
sprint: row.sprint ?? void 0
|
|
1065
|
-
};
|
|
1066
|
-
}
|
|
1067
|
-
async recordMetric(metric) {
|
|
1068
|
-
const branch = this._branchFor(metric.storyKey);
|
|
1069
|
-
const recordedAt = metric.recordedAt ?? metric.timestamp ?? new Date().toISOString();
|
|
1070
|
-
const sql = `INSERT INTO metrics
|
|
1071
|
-
(story_key, task_type, model, tokens_in, tokens_out, cache_read_tokens,
|
|
1072
|
-
cost_usd, wall_clock_ms, review_cycles, stall_count, result, recorded_at, sprint)
|
|
1073
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`;
|
|
1074
|
-
await this._client.query(sql, [
|
|
1075
|
-
metric.storyKey,
|
|
1076
|
-
metric.taskType,
|
|
1077
|
-
metric.model ?? null,
|
|
1078
|
-
metric.tokensIn ?? null,
|
|
1079
|
-
metric.tokensOut ?? null,
|
|
1080
|
-
metric.cacheReadTokens ?? null,
|
|
1081
|
-
metric.costUsd ?? null,
|
|
1082
|
-
metric.wallClockMs ?? null,
|
|
1083
|
-
metric.reviewCycles ?? null,
|
|
1084
|
-
metric.stallCount ?? null,
|
|
1085
|
-
metric.result ?? null,
|
|
1086
|
-
recordedAt,
|
|
1087
|
-
metric.sprint ?? null
|
|
1088
|
-
], branch);
|
|
1089
|
-
}
|
|
1090
|
-
async queryMetrics(filter) {
|
|
1091
|
-
const conditions = [];
|
|
1092
|
-
const params = [];
|
|
1093
|
-
const storyKey = filter.storyKey ?? filter.story_key;
|
|
1094
|
-
const taskType = filter.taskType ?? filter.task_type;
|
|
1095
|
-
if (storyKey !== void 0) {
|
|
1096
|
-
conditions.push("story_key = ?");
|
|
1097
|
-
params.push(storyKey);
|
|
1098
|
-
}
|
|
1099
|
-
if (taskType !== void 0) {
|
|
1100
|
-
conditions.push("task_type = ?");
|
|
1101
|
-
params.push(taskType);
|
|
1102
|
-
}
|
|
1103
|
-
if (filter.sprint !== void 0) {
|
|
1104
|
-
conditions.push("sprint = ?");
|
|
1105
|
-
params.push(filter.sprint);
|
|
1106
|
-
}
|
|
1107
|
-
if (filter.dateFrom !== void 0) {
|
|
1108
|
-
conditions.push("recorded_at >= ?");
|
|
1109
|
-
params.push(filter.dateFrom);
|
|
1110
|
-
}
|
|
1111
|
-
if (filter.dateTo !== void 0) {
|
|
1112
|
-
conditions.push("recorded_at <= ?");
|
|
1113
|
-
params.push(filter.dateTo);
|
|
1114
|
-
}
|
|
1115
|
-
if (filter.since !== void 0) {
|
|
1116
|
-
conditions.push("recorded_at >= ?");
|
|
1117
|
-
params.push(filter.since);
|
|
1118
|
-
}
|
|
1119
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1120
|
-
if (filter.aggregate) {
|
|
1121
|
-
const sql$1 = `SELECT task_type,
|
|
1122
|
-
AVG(cost_usd) AS avg_cost_usd,
|
|
1123
|
-
SUM(tokens_in) AS sum_tokens_in,
|
|
1124
|
-
SUM(tokens_out) AS sum_tokens_out,
|
|
1125
|
-
COUNT(*) AS count
|
|
1126
|
-
FROM metrics ${where} GROUP BY task_type ORDER BY task_type`;
|
|
1127
|
-
const aggRows = await this._client.query(sql$1, params);
|
|
1128
|
-
return aggRows.map((r) => this._aggregateRowToMetric(r));
|
|
1129
|
-
}
|
|
1130
|
-
const sql = `SELECT * FROM metrics ${where} ORDER BY id`;
|
|
1131
|
-
const rows = await this._client.query(sql, params);
|
|
1132
|
-
return rows.map((r) => this._rowToMetric(r));
|
|
1133
|
-
}
|
|
1134
|
-
_aggregateRowToMetric(row) {
|
|
1135
|
-
return {
|
|
1136
|
-
storyKey: "",
|
|
1137
|
-
taskType: row.task_type,
|
|
1138
|
-
costUsd: row.avg_cost_usd ?? void 0,
|
|
1139
|
-
tokensIn: row.sum_tokens_in ?? void 0,
|
|
1140
|
-
tokensOut: row.sum_tokens_out ?? void 0,
|
|
1141
|
-
count: row.count,
|
|
1142
|
-
result: "aggregate"
|
|
1143
|
-
};
|
|
1144
|
-
}
|
|
1145
|
-
_rowToMetric(row) {
|
|
1146
|
-
return {
|
|
1147
|
-
storyKey: row.story_key,
|
|
1148
|
-
taskType: row.task_type,
|
|
1149
|
-
model: row.model ?? void 0,
|
|
1150
|
-
tokensIn: row.tokens_in ?? void 0,
|
|
1151
|
-
tokensOut: row.tokens_out ?? void 0,
|
|
1152
|
-
cacheReadTokens: row.cache_read_tokens ?? void 0,
|
|
1153
|
-
costUsd: row.cost_usd ?? void 0,
|
|
1154
|
-
wallClockMs: row.wall_clock_ms ?? void 0,
|
|
1155
|
-
reviewCycles: row.review_cycles ?? void 0,
|
|
1156
|
-
stallCount: row.stall_count ?? void 0,
|
|
1157
|
-
result: row.result ?? void 0,
|
|
1158
|
-
recordedAt: row.recorded_at ?? void 0,
|
|
1159
|
-
sprint: row.sprint ?? void 0,
|
|
1160
|
-
timestamp: row.timestamp ?? row.recorded_at ?? void 0
|
|
1161
|
-
};
|
|
1162
|
-
}
|
|
1163
|
-
async getContracts(storyKey) {
|
|
1164
|
-
const rows = await this._client.query("SELECT * FROM contracts WHERE story_key = ? ORDER BY contract_name", [storyKey]);
|
|
1165
|
-
return rows.map((r) => this._rowToContract(r));
|
|
1166
|
-
}
|
|
1167
|
-
async setContracts(storyKey, contracts) {
|
|
1168
|
-
const branch = this._branchFor(storyKey);
|
|
1169
|
-
await this._client.query("DELETE FROM contracts WHERE story_key = ?", [storyKey], branch);
|
|
1170
|
-
for (const c of contracts) await this._client.query(`INSERT INTO contracts (story_key, contract_name, direction, schema_path, transport)
|
|
1171
|
-
VALUES (?, ?, ?, ?, ?)`, [
|
|
1172
|
-
c.storyKey,
|
|
1173
|
-
c.contractName,
|
|
1174
|
-
c.direction,
|
|
1175
|
-
c.schemaPath,
|
|
1176
|
-
c.transport ?? null
|
|
1177
|
-
], branch);
|
|
1178
|
-
}
|
|
1179
|
-
_rowToContract(row) {
|
|
1180
|
-
return {
|
|
1181
|
-
storyKey: row.story_key,
|
|
1182
|
-
contractName: row.contract_name,
|
|
1183
|
-
direction: row.direction,
|
|
1184
|
-
schemaPath: row.schema_path,
|
|
1185
|
-
transport: row.transport ?? void 0
|
|
1186
|
-
};
|
|
1187
|
-
}
|
|
1188
|
-
async queryContracts(filter) {
|
|
1189
|
-
const conditions = [];
|
|
1190
|
-
const params = [];
|
|
1191
|
-
if (filter?.storyKey !== void 0) {
|
|
1192
|
-
conditions.push("story_key = ?");
|
|
1193
|
-
params.push(filter.storyKey);
|
|
1194
|
-
}
|
|
1195
|
-
if (filter?.direction !== void 0) {
|
|
1196
|
-
conditions.push("direction = ?");
|
|
1197
|
-
params.push(filter.direction);
|
|
1198
|
-
}
|
|
1199
|
-
const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
1200
|
-
const sql = `SELECT * FROM contracts ${where} ORDER BY story_key, contract_name`;
|
|
1201
|
-
const rows = await this._client.query(sql, params);
|
|
1202
|
-
return rows.map((r) => this._rowToContract(r));
|
|
1203
|
-
}
|
|
1204
|
-
async setContractVerification(storyKey, results) {
|
|
1205
|
-
const branch = this._branchFor(storyKey);
|
|
1206
|
-
await this._client.query(`DELETE FROM review_verdicts WHERE story_key = ? AND task_type = 'contract-verification'`, [storyKey], branch);
|
|
1207
|
-
const failCount = results.filter((r) => r.verdict === "fail").length;
|
|
1208
|
-
for (const r of results) await this._client.query(`INSERT INTO review_verdicts (story_key, task_type, verdict, issues_count, notes, timestamp)
|
|
1209
|
-
VALUES (?, 'contract-verification', ?, ?, ?, ?)`, [
|
|
1210
|
-
storyKey,
|
|
1211
|
-
r.verdict,
|
|
1212
|
-
failCount,
|
|
1213
|
-
JSON.stringify({
|
|
1214
|
-
contractName: r.contractName,
|
|
1215
|
-
mismatchDescription: r.mismatchDescription
|
|
1216
|
-
}),
|
|
1217
|
-
r.verifiedAt
|
|
1218
|
-
], branch);
|
|
1219
|
-
}
|
|
1220
|
-
async getContractVerification(storyKey) {
|
|
1221
|
-
const rows = await this._client.query(`SELECT * FROM review_verdicts WHERE story_key = ? AND task_type = 'contract-verification' ORDER BY timestamp DESC`, [storyKey]);
|
|
1222
|
-
return rows.map((row) => {
|
|
1223
|
-
let contractName = "";
|
|
1224
|
-
let mismatchDescription;
|
|
1225
|
-
if (row.notes !== null) try {
|
|
1226
|
-
const parsed = JSON.parse(row.notes);
|
|
1227
|
-
if (typeof parsed.contractName === "string") contractName = parsed.contractName;
|
|
1228
|
-
if (typeof parsed.mismatchDescription === "string") mismatchDescription = parsed.mismatchDescription;
|
|
1229
|
-
} catch {}
|
|
1230
|
-
return {
|
|
1231
|
-
storyKey: row.story_key,
|
|
1232
|
-
contractName,
|
|
1233
|
-
verdict: row.verdict,
|
|
1234
|
-
...mismatchDescription !== void 0 ? { mismatchDescription } : {},
|
|
1235
|
-
verifiedAt: row.timestamp ?? new Date().toISOString()
|
|
1236
|
-
};
|
|
1237
|
-
});
|
|
1238
|
-
}
|
|
1239
|
-
async branchForStory(storyKey) {
|
|
1240
|
-
assertValidStoryKey(storyKey);
|
|
1241
|
-
const branchName = `story/${storyKey}`;
|
|
1242
|
-
try {
|
|
1243
|
-
await this._client.query(`CALL DOLT_BRANCH('${branchName}')`, [], "main");
|
|
1244
|
-
this._storyBranches.set(storyKey, branchName);
|
|
1245
|
-
log.debug("Created Dolt branch %s for story %s", branchName, storyKey);
|
|
1246
|
-
} catch (err) {
|
|
1247
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1248
|
-
throw new DoltQueryError(`CALL DOLT_BRANCH('${branchName}')`, detail);
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
async mergeStory(storyKey) {
|
|
1252
|
-
assertValidStoryKey(storyKey);
|
|
1253
|
-
const branchName = this._storyBranches.get(storyKey);
|
|
1254
|
-
if (branchName === void 0) {
|
|
1255
|
-
log.warn({ storyKey }, "mergeStory called but no branch registered — no-op");
|
|
1256
|
-
return;
|
|
1257
|
-
}
|
|
1258
|
-
try {
|
|
1259
|
-
try {
|
|
1260
|
-
await this._client.query(`CALL DOLT_ADD('-A')`, [], branchName);
|
|
1261
|
-
await this._client.query(`CALL DOLT_COMMIT('-m', 'Story ${storyKey}: pre-merge commit', '--allow-empty')`, [], branchName);
|
|
1262
|
-
} catch {}
|
|
1263
|
-
try {
|
|
1264
|
-
await this._client.query(`CALL DOLT_ADD('-A')`, [], "main");
|
|
1265
|
-
await this._client.query(`CALL DOLT_COMMIT('-m', 'substrate: pre-merge auto-commit', '--allow-empty')`, [], "main");
|
|
1266
|
-
} catch {}
|
|
1267
|
-
const mergeRows = await this._client.query(`CALL DOLT_MERGE('${branchName}')`, [], "main");
|
|
1268
|
-
const mergeResult = mergeRows[0];
|
|
1269
|
-
if (mergeResult && (mergeResult.conflicts ?? 0) > 0) {
|
|
1270
|
-
let table = "stories";
|
|
1271
|
-
let rowKey = "unknown";
|
|
1272
|
-
let ourValue;
|
|
1273
|
-
let theirValue;
|
|
1274
|
-
try {
|
|
1275
|
-
const conflictRows = await this._client.query(`SELECT * FROM dolt_conflicts_stories LIMIT 1`, [], "main");
|
|
1276
|
-
if (conflictRows.length > 0) {
|
|
1277
|
-
const row = conflictRows[0];
|
|
1278
|
-
rowKey = String(row["base_story_key"] ?? row["our_story_key"] ?? "unknown");
|
|
1279
|
-
ourValue = JSON.stringify(row["our_status"] ?? row);
|
|
1280
|
-
theirValue = JSON.stringify(row["their_status"] ?? row);
|
|
1281
|
-
}
|
|
1282
|
-
} catch {}
|
|
1283
|
-
this._storyBranches.delete(storyKey);
|
|
1284
|
-
throw new DoltMergeConflictError(table, [rowKey], {
|
|
1285
|
-
rowKey,
|
|
1286
|
-
ourValue,
|
|
1287
|
-
theirValue
|
|
1288
|
-
});
|
|
1289
|
-
}
|
|
1290
|
-
try {
|
|
1291
|
-
await this._client.query(`CALL DOLT_COMMIT('-m', 'Merge story ${storyKey}: COMPLETE')`, [], "main");
|
|
1292
|
-
} catch (commitErr) {
|
|
1293
|
-
const msg = commitErr instanceof Error ? commitErr.message : String(commitErr);
|
|
1294
|
-
if (!msg.includes("nothing to commit")) throw commitErr;
|
|
1295
|
-
}
|
|
1296
|
-
this._storyBranches.delete(storyKey);
|
|
1297
|
-
log.debug("Merged branch %s into main for story %s", branchName, storyKey);
|
|
1298
|
-
} catch (err) {
|
|
1299
|
-
if (err instanceof DoltMergeConflictError) throw err;
|
|
1300
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1301
|
-
throw new DoltQueryError(`CALL DOLT_MERGE('${branchName}')`, detail);
|
|
1302
|
-
}
|
|
1303
|
-
}
|
|
1304
|
-
async rollbackStory(storyKey) {
|
|
1305
|
-
assertValidStoryKey(storyKey);
|
|
1306
|
-
const branchName = this._storyBranches.get(storyKey);
|
|
1307
|
-
if (branchName === void 0) {
|
|
1308
|
-
log.warn({ storyKey }, "rollbackStory called but no branch registered — no-op");
|
|
1309
|
-
return;
|
|
1310
|
-
}
|
|
1311
|
-
try {
|
|
1312
|
-
await this._client.query(`CALL DOLT_BRANCH('-D', '${branchName}')`, [], "main");
|
|
1313
|
-
this._storyBranches.delete(storyKey);
|
|
1314
|
-
log.debug("Rolled back (deleted) branch %s for story %s", branchName, storyKey);
|
|
1315
|
-
} catch (err) {
|
|
1316
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1317
|
-
log.warn({
|
|
1318
|
-
detail,
|
|
1319
|
-
storyKey,
|
|
1320
|
-
branchName
|
|
1321
|
-
}, "rollbackStory failed (non-fatal)");
|
|
1322
|
-
this._storyBranches.delete(storyKey);
|
|
1323
|
-
}
|
|
1324
|
-
}
|
|
1325
|
-
/**
|
|
1326
|
-
* Tables queried by diffStory(). Each table is checked for row-level changes
|
|
1327
|
-
* via SELECT * FROM DOLT_DIFF('main', branchName, tableName).
|
|
1328
|
-
*/
|
|
1329
|
-
static DIFF_TABLES = [
|
|
1330
|
-
"stories",
|
|
1331
|
-
"contracts",
|
|
1332
|
-
"metrics",
|
|
1333
|
-
"dispatch_log",
|
|
1334
|
-
"build_results",
|
|
1335
|
-
"review_verdicts"
|
|
1336
|
-
];
|
|
1337
|
-
async diffStory(storyKey) {
|
|
1338
|
-
assertValidStoryKey(storyKey);
|
|
1339
|
-
const branchName = this._storyBranches.get(storyKey);
|
|
1340
|
-
if (branchName === void 0) return this._diffMergedStory(storyKey);
|
|
1341
|
-
try {
|
|
1342
|
-
await this._client.query(`CALL DOLT_ADD('-A')`, [], branchName);
|
|
1343
|
-
await this._client.query(`CALL DOLT_COMMIT('-m', 'Story ${storyKey}: pre-diff snapshot', '--allow-empty')`, [], branchName);
|
|
1344
|
-
} catch {}
|
|
1345
|
-
return this._diffRange("main", branchName, storyKey);
|
|
1346
|
-
}
|
|
1347
|
-
/**
|
|
1348
|
-
* Diff a merged story by finding its merge commit in the Dolt log.
|
|
1349
|
-
* Queries the `dolt_log` system table for commits referencing the story,
|
|
1350
|
-
* then diffs `<hash>~1` vs `<hash>` for row-level changes.
|
|
1351
|
-
*/
|
|
1352
|
-
async _diffMergedStory(storyKey) {
|
|
1353
|
-
try {
|
|
1354
|
-
const rows = await this._client.query(`SELECT commit_hash FROM dolt_log WHERE message LIKE ? LIMIT 1`, [`%${storyKey}%`]);
|
|
1355
|
-
if (rows.length === 0) return {
|
|
1356
|
-
storyKey,
|
|
1357
|
-
tables: []
|
|
1358
|
-
};
|
|
1359
|
-
const hash = String(rows[0].commit_hash);
|
|
1360
|
-
if (!hash) return {
|
|
1361
|
-
storyKey,
|
|
1362
|
-
tables: []
|
|
1363
|
-
};
|
|
1364
|
-
return this._diffRange(`${hash}~1`, hash, storyKey);
|
|
1365
|
-
} catch {
|
|
1366
|
-
return {
|
|
1367
|
-
storyKey,
|
|
1368
|
-
tables: []
|
|
1369
|
-
};
|
|
1370
|
-
}
|
|
1371
|
-
}
|
|
1372
|
-
/**
|
|
1373
|
-
* Compute row-level diffs between two Dolt revisions (branches or commit hashes)
|
|
1374
|
-
* across all tracked tables.
|
|
1375
|
-
*/
|
|
1376
|
-
async _diffRange(fromRef, toRef, storyKey) {
|
|
1377
|
-
const tableDiffs = [];
|
|
1378
|
-
for (const table of DoltStateStore.DIFF_TABLES) try {
|
|
1379
|
-
const rows = await this._client.query(`SELECT * FROM DOLT_DIFF('${fromRef}', '${toRef}', '${table}')`, [], "main");
|
|
1380
|
-
if (rows.length === 0) continue;
|
|
1381
|
-
const added = [];
|
|
1382
|
-
const modified = [];
|
|
1383
|
-
const deleted = [];
|
|
1384
|
-
for (const row of rows) {
|
|
1385
|
-
const diffType = row["diff_type"];
|
|
1386
|
-
const rowKey = this._extractRowKey(row);
|
|
1387
|
-
const before = this._extractPrefixedFields(row, "before_");
|
|
1388
|
-
const after = this._extractPrefixedFields(row, "after_");
|
|
1389
|
-
const diffRow = {
|
|
1390
|
-
rowKey,
|
|
1391
|
-
...before !== void 0 && { before },
|
|
1392
|
-
...after !== void 0 && { after }
|
|
1393
|
-
};
|
|
1394
|
-
if (diffType === "added") added.push(diffRow);
|
|
1395
|
-
else if (diffType === "modified") modified.push(diffRow);
|
|
1396
|
-
else if (diffType === "removed") deleted.push(diffRow);
|
|
1397
|
-
}
|
|
1398
|
-
if (added.length > 0 || modified.length > 0 || deleted.length > 0) tableDiffs.push({
|
|
1399
|
-
table,
|
|
1400
|
-
added,
|
|
1401
|
-
modified,
|
|
1402
|
-
deleted
|
|
1403
|
-
});
|
|
1404
|
-
} catch {}
|
|
1405
|
-
return {
|
|
1406
|
-
storyKey,
|
|
1407
|
-
tables: tableDiffs
|
|
1408
|
-
};
|
|
1409
|
-
}
|
|
1410
|
-
/**
|
|
1411
|
-
* Extract a human-readable row key from a DOLT_DIFF result row.
|
|
1412
|
-
* Tries after_ fields first (for added/modified rows), then before_ fields
|
|
1413
|
-
* (for removed rows). Skips commit_hash pseudo-columns.
|
|
1414
|
-
*/
|
|
1415
|
-
_extractRowKey(row) {
|
|
1416
|
-
for (const prefix of ["after_", "before_"]) for (const [key, val] of Object.entries(row)) if (key.startsWith(prefix) && !key.endsWith("_commit_hash") && val !== null && val !== void 0) return String(val);
|
|
1417
|
-
return "unknown";
|
|
1418
|
-
}
|
|
1419
|
-
/**
|
|
1420
|
-
* Extract all fields with a given prefix from a DOLT_DIFF result row,
|
|
1421
|
-
* stripping the prefix from the key names. Returns undefined if no matching
|
|
1422
|
-
* fields are found.
|
|
1423
|
-
*/
|
|
1424
|
-
_extractPrefixedFields(row, prefix) {
|
|
1425
|
-
const result = {};
|
|
1426
|
-
for (const [key, val] of Object.entries(row)) if (key.startsWith(prefix)) result[key.slice(prefix.length)] = val;
|
|
1427
|
-
return Object.keys(result).length > 0 ? result : void 0;
|
|
1428
|
-
}
|
|
1429
|
-
/** In-memory KV store for per-run arbitrary metrics. Not persisted to Dolt. */
|
|
1430
|
-
_kvMetrics = new Map();
|
|
1431
|
-
async setMetric(runId, key, value) {
|
|
1432
|
-
let runMap = this._kvMetrics.get(runId);
|
|
1433
|
-
if (runMap === void 0) {
|
|
1434
|
-
runMap = new Map();
|
|
1435
|
-
this._kvMetrics.set(runId, runMap);
|
|
1436
|
-
}
|
|
1437
|
-
runMap.set(key, value);
|
|
1438
|
-
}
|
|
1439
|
-
async getMetric(runId, key) {
|
|
1440
|
-
return this._kvMetrics.get(runId)?.get(key);
|
|
1441
|
-
}
|
|
1442
|
-
async getHistory(limit) {
|
|
1443
|
-
const effectiveLimit = limit ?? 20;
|
|
1444
|
-
try {
|
|
1445
|
-
const rows = await this._client.query(`SELECT commit_hash, date, message, committer FROM dolt_log LIMIT ?`, [effectiveLimit]);
|
|
1446
|
-
const entries = [];
|
|
1447
|
-
for (const row of rows) {
|
|
1448
|
-
const hash = String(row.commit_hash ?? "");
|
|
1449
|
-
const dateVal = row.date;
|
|
1450
|
-
const timestamp = dateVal instanceof Date ? dateVal.toISOString() : String(dateVal ?? "");
|
|
1451
|
-
const message = String(row.message ?? "");
|
|
1452
|
-
const author = row.committer ? String(row.committer) : void 0;
|
|
1453
|
-
const storyKeyMatch = /story\/([0-9]+-[0-9]+)/i.exec(message);
|
|
1454
|
-
entries.push({
|
|
1455
|
-
hash,
|
|
1456
|
-
timestamp,
|
|
1457
|
-
storyKey: storyKeyMatch ? storyKeyMatch[1] : null,
|
|
1458
|
-
message,
|
|
1459
|
-
author
|
|
1460
|
-
});
|
|
1461
|
-
}
|
|
1462
|
-
return entries;
|
|
1463
|
-
} catch (err) {
|
|
1464
|
-
const detail = err instanceof Error ? err.message : String(err);
|
|
1465
|
-
throw new DoltQueryError("getHistory", detail);
|
|
1466
|
-
}
|
|
1467
|
-
}
|
|
1468
|
-
};
|
|
1469
|
-
|
|
1470
|
-
//#endregion
|
|
1471
|
-
//#region src/modules/state/index.ts
|
|
1472
|
-
const logger$2 = createLogger("state:factory");
|
|
19
|
+
//#region src/utils/git-root.ts
|
|
1473
20
|
/**
|
|
1474
|
-
*
|
|
1475
|
-
* canonical state path under `basePath`.
|
|
21
|
+
* Resolve the main git repository root, even from a linked worktree.
|
|
1476
22
|
*
|
|
1477
|
-
*
|
|
1478
|
-
*
|
|
1479
|
-
*
|
|
1480
|
-
*/
|
|
1481
|
-
function detectDoltAvailableSync(basePath) {
|
|
1482
|
-
const result = spawnSync("dolt", ["version"], { stdio: "ignore" });
|
|
1483
|
-
const binaryFound = result.error == null && result.status === 0;
|
|
1484
|
-
if (!binaryFound) return {
|
|
1485
|
-
available: false,
|
|
1486
|
-
reason: "dolt binary not found on PATH"
|
|
1487
|
-
};
|
|
1488
|
-
const stateDoltDir = join$1(basePath, ".substrate", "state", ".dolt");
|
|
1489
|
-
const repoExists = existsSync(stateDoltDir);
|
|
1490
|
-
if (!repoExists) return {
|
|
1491
|
-
available: false,
|
|
1492
|
-
reason: `Dolt repo not initialised at ${stateDoltDir}`
|
|
1493
|
-
};
|
|
1494
|
-
return {
|
|
1495
|
-
available: true,
|
|
1496
|
-
reason: "dolt binary found and repo initialised"
|
|
1497
|
-
};
|
|
1498
|
-
}
|
|
1499
|
-
/**
|
|
1500
|
-
* Create a StateStore backed by the specified backend.
|
|
23
|
+
* In the main worktree, `--git-common-dir` returns `.git` (relative).
|
|
24
|
+
* In a linked worktree, it returns the absolute path to the main `.git` dir.
|
|
25
|
+
* Either way, `dirname()` of the resolved absolute path yields the repo root.
|
|
1501
26
|
*
|
|
1502
|
-
*
|
|
1503
|
-
* @returns A StateStore instance. Call `initialize()` before use.
|
|
27
|
+
* Falls back to `cwd` if not in a git repo or git is unavailable.
|
|
1504
28
|
*/
|
|
1505
|
-
function
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
const
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
29
|
+
async function resolveMainRepoRoot(cwd = process.cwd()) {
|
|
30
|
+
return new Promise((res) => {
|
|
31
|
+
let stdout = "";
|
|
32
|
+
const proc = spawn("git", ["rev-parse", "--git-common-dir"], {
|
|
33
|
+
cwd,
|
|
34
|
+
stdio: [
|
|
35
|
+
"ignore",
|
|
36
|
+
"pipe",
|
|
37
|
+
"pipe"
|
|
38
|
+
]
|
|
1513
39
|
});
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
}
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
40
|
+
if (proc.stdout !== null) proc.stdout.on("data", (chunk) => {
|
|
41
|
+
stdout += chunk.toString("utf-8");
|
|
42
|
+
});
|
|
43
|
+
proc.on("error", () => {
|
|
44
|
+
res(cwd);
|
|
45
|
+
});
|
|
46
|
+
proc.on("close", (code) => {
|
|
47
|
+
if (code !== 0) {
|
|
48
|
+
res(cwd);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
const commonDir = stdout.trim();
|
|
52
|
+
if (!commonDir) {
|
|
53
|
+
res(cwd);
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const absCommonDir = resolve$1(cwd, commonDir);
|
|
57
|
+
res(dirname$1(absCommonDir));
|
|
58
|
+
});
|
|
59
|
+
});
|
|
1531
60
|
}
|
|
1532
61
|
|
|
1533
62
|
//#endregion
|
|
@@ -1781,7 +310,11 @@ const RootCauseCategorySchema = z.enum([
|
|
|
1781
310
|
"test-failure",
|
|
1782
311
|
"resource-exhaustion",
|
|
1783
312
|
"infrastructure",
|
|
1784
|
-
"unclassified"
|
|
313
|
+
"unclassified",
|
|
314
|
+
"ac-missing-evidence",
|
|
315
|
+
"runtime-probe-fail",
|
|
316
|
+
"source-ac-drift",
|
|
317
|
+
"cross-story-concurrent-modification"
|
|
1785
318
|
]);
|
|
1786
319
|
const FindingSchema = z.object({
|
|
1787
320
|
id: z.string().uuid(),
|
|
@@ -1821,7 +354,11 @@ const CATEGORY_DESCRIPTIONS = {
|
|
|
1821
354
|
"test-failure": "Tests failed after story implementation",
|
|
1822
355
|
"resource-exhaustion": "Story produced fewer than 100 output tokens (resource exhaustion suspected)",
|
|
1823
356
|
"infrastructure": "System-level infrastructure error (OOM, disk, permissions, or SIGKILL)",
|
|
1824
|
-
"unclassified": "No error text available"
|
|
357
|
+
"unclassified": "No error text available",
|
|
358
|
+
"ac-missing-evidence": "Acceptance criteria evidence check found missing or insufficient AC coverage",
|
|
359
|
+
"runtime-probe-fail": "Runtime probe verification failed (probe execution or assertion failure)",
|
|
360
|
+
"source-ac-drift": "Source AC fidelity check detected drift between story ACs and source epic",
|
|
361
|
+
"cross-story-concurrent-modification": "Cross-story consistency check detected concurrent file modification conflict"
|
|
1825
362
|
};
|
|
1826
363
|
/**
|
|
1827
364
|
* Construct a validated Finding from a StoryFailureContext and classification result.
|
|
@@ -2766,8 +1303,8 @@ function resolveGraphPath() {
|
|
|
2766
1303
|
join$1(__dirname, "graphs/sdlc-pipeline.dot")
|
|
2767
1304
|
];
|
|
2768
1305
|
try {
|
|
2769
|
-
const require
|
|
2770
|
-
const sdlcPkgPath = require
|
|
1306
|
+
const require = createRequire(import.meta.url);
|
|
1307
|
+
const sdlcPkgPath = require.resolve("@substrate-ai/sdlc/package.json");
|
|
2771
1308
|
candidates.push(join$1(dirname$1(sdlcPkgPath), "graphs", "sdlc-pipeline.dot"));
|
|
2772
1309
|
} catch {}
|
|
2773
1310
|
for (const candidate of candidates) if (existsSync(candidate)) return candidate;
|
|
@@ -5531,6 +4068,11 @@ var VerificationPipeline = class {
|
|
|
5531
4068
|
* 6. SourceAcFidelityCheck — Story 58-2: cross-references rendered story artifact
|
|
5532
4069
|
* against the source epic's hard clauses (MUST/SHALL/paths)
|
|
5533
4070
|
*
|
|
4071
|
+
* On-demand check (NOT in default pipeline):
|
|
4072
|
+
* - AcTraceabilityCheck — Story 74-1: heuristic AC-to-test traceability.
|
|
4073
|
+
* Invoked only via `--verify-ac` on `substrate report` and `substrate run`.
|
|
4074
|
+
* Excluded from default Tier A/B to avoid paying the cost on every run.
|
|
4075
|
+
*
|
|
5534
4076
|
* @param bus Typed event bus for verification events.
|
|
5535
4077
|
* @param config Optional config (used to forward threshold to TrivialOutputCheck).
|
|
5536
4078
|
*/
|
|
@@ -5769,6 +4311,151 @@ async function runStaleVerificationRecovery(input) {
|
|
|
5769
4311
|
};
|
|
5770
4312
|
}
|
|
5771
4313
|
|
|
4314
|
+
//#endregion
|
|
4315
|
+
//#region packages/sdlc/dist/verification/checks/ac-traceability-check.js
|
|
4316
|
+
/**
|
|
4317
|
+
* Tokenize a string to a deduplicated set of lowercase alphanumeric tokens.
|
|
4318
|
+
* Non-alphanumeric characters are replaced with spaces before splitting.
|
|
4319
|
+
*/
|
|
4320
|
+
function tokenize(s) {
|
|
4321
|
+
const words = s.toLowerCase().replace(/[^a-z0-9\s]/g, " ").split(/\s+/).filter(Boolean);
|
|
4322
|
+
return new Set(words);
|
|
4323
|
+
}
|
|
4324
|
+
/**
|
|
4325
|
+
* Compute Jaccard-based word overlap between two strings.
|
|
4326
|
+
* Returns 0 when either string has no tokens.
|
|
4327
|
+
*/
|
|
4328
|
+
function wordOverlap(a, b) {
|
|
4329
|
+
const setA = tokenize(a);
|
|
4330
|
+
const setB = tokenize(b);
|
|
4331
|
+
if (setA.size === 0 || setB.size === 0) return 0;
|
|
4332
|
+
let intersection = 0;
|
|
4333
|
+
for (const w of setA) if (setB.has(w)) intersection++;
|
|
4334
|
+
const union = new Set([...setA, ...setB]).size;
|
|
4335
|
+
return intersection / union;
|
|
4336
|
+
}
|
|
4337
|
+
/**
|
|
4338
|
+
* Extract numbered AC items from the `## Acceptance Criteria` section of a
|
|
4339
|
+
* story spec. Handles both plain `## Acceptance Criteria` headings and the
|
|
4340
|
+
* bold-wrapped variant `**Acceptance Criteria:**`.
|
|
4341
|
+
*
|
|
4342
|
+
* Stops scanning at the next `##` heading (or end-of-file).
|
|
4343
|
+
*
|
|
4344
|
+
* Returns an array of trimmed AC text strings (numbering stripped).
|
|
4345
|
+
*/
|
|
4346
|
+
function parseAcList(storyContent) {
|
|
4347
|
+
const lines = storyContent.split("\n");
|
|
4348
|
+
const items = [];
|
|
4349
|
+
let inAcSection = false;
|
|
4350
|
+
for (const line of lines) {
|
|
4351
|
+
if (/^##\s+Acceptance Criteria/i.test(line) || /\*\*Acceptance Criteria:\*\*/i.test(line)) {
|
|
4352
|
+
inAcSection = true;
|
|
4353
|
+
continue;
|
|
4354
|
+
}
|
|
4355
|
+
if (inAcSection && /^##\s/.test(line)) break;
|
|
4356
|
+
if (!inAcSection) continue;
|
|
4357
|
+
const numbered = line.match(/^\s*(?:\d+[.):]|AC\d+[.):])\s*(.+)/);
|
|
4358
|
+
if (numbered) {
|
|
4359
|
+
const text = (numbered[1] ?? "").trim();
|
|
4360
|
+
if (text.length > 0) items.push(text);
|
|
4361
|
+
}
|
|
4362
|
+
}
|
|
4363
|
+
return items;
|
|
4364
|
+
}
|
|
4365
|
+
const TEST_FILE_PATTERNS = [
|
|
4366
|
+
/\.test\.ts$/,
|
|
4367
|
+
/\.test\.js$/,
|
|
4368
|
+
/test/i
|
|
4369
|
+
];
|
|
4370
|
+
/** Returns true when the path matches a known test file pattern. */
|
|
4371
|
+
function isTestFile(path$3) {
|
|
4372
|
+
return TEST_FILE_PATTERNS.some((p) => p.test(path$3));
|
|
4373
|
+
}
|
|
4374
|
+
const TEST_DESC_RE = /(?:describe|it|test)\s*\(\s*['"`]([^'"`]+)['"`]/g;
|
|
4375
|
+
/**
|
|
4376
|
+
* Extract all string literals passed to `describe(`, `it(`, or `test(` in the
|
|
4377
|
+
* provided source text.
|
|
4378
|
+
*/
|
|
4379
|
+
function extractTestDescriptions(source) {
|
|
4380
|
+
const results = [];
|
|
4381
|
+
let m;
|
|
4382
|
+
TEST_DESC_RE.lastIndex = 0;
|
|
4383
|
+
while ((m = TEST_DESC_RE.exec(source)) !== null) {
|
|
4384
|
+
const desc = m[1];
|
|
4385
|
+
if (desc !== void 0 && desc.trim().length > 0) results.push(desc.trim());
|
|
4386
|
+
}
|
|
4387
|
+
return results;
|
|
4388
|
+
}
|
|
4389
|
+
/**
|
|
4390
|
+
* Run the AC-to-test traceability heuristic.
|
|
4391
|
+
*
|
|
4392
|
+
* Steps:
|
|
4393
|
+
* 1. Parse AC list from `input.storyContent`.
|
|
4394
|
+
* 2. Filter `input.filesModified` to test files.
|
|
4395
|
+
* 3. Read each test file and extract describe/it/test descriptions.
|
|
4396
|
+
* 4. For each AC × each test description, compute word-overlap score.
|
|
4397
|
+
* 5. A score ≥ 0.4 marks the AC as "matched".
|
|
4398
|
+
* 6. Return the matrix with `confidence: 'approximate'`.
|
|
4399
|
+
*
|
|
4400
|
+
* File I/O errors are silently ignored per file (the file is simply skipped).
|
|
4401
|
+
* When no test files are found, all ACs are "not matched" and a warning is emitted.
|
|
4402
|
+
*/
|
|
4403
|
+
async function runAcTraceabilityCheck(input) {
|
|
4404
|
+
const { storyKey, storyContent, filesModified } = input;
|
|
4405
|
+
const fileReader = input._readFile ?? ((p) => readFile(p, "utf-8"));
|
|
4406
|
+
const warnings = [];
|
|
4407
|
+
const acList = parseAcList(storyContent);
|
|
4408
|
+
const testFiles = filesModified.filter(isTestFile);
|
|
4409
|
+
if (testFiles.length === 0) {
|
|
4410
|
+
warnings.push(`No test files found in filesModified for story ${storyKey}. All ACs marked as unmatched.`);
|
|
4411
|
+
const matrix$1 = acList.map((acText) => ({
|
|
4412
|
+
acText,
|
|
4413
|
+
matched: false,
|
|
4414
|
+
testName: null,
|
|
4415
|
+
score: 0
|
|
4416
|
+
}));
|
|
4417
|
+
return {
|
|
4418
|
+
storyKey,
|
|
4419
|
+
matrix: matrix$1,
|
|
4420
|
+
confidence: "approximate",
|
|
4421
|
+
warnings
|
|
4422
|
+
};
|
|
4423
|
+
}
|
|
4424
|
+
const allTestDescriptions = [];
|
|
4425
|
+
for (const filePath of testFiles) try {
|
|
4426
|
+
const source = await fileReader(filePath);
|
|
4427
|
+
if (source != null) {
|
|
4428
|
+
const descs = extractTestDescriptions(source);
|
|
4429
|
+
allTestDescriptions.push(...descs);
|
|
4430
|
+
}
|
|
4431
|
+
} catch {}
|
|
4432
|
+
const MATCH_THRESHOLD = .4;
|
|
4433
|
+
const matrix = acList.map((acText) => {
|
|
4434
|
+
let bestScore = 0;
|
|
4435
|
+
let bestTestName = null;
|
|
4436
|
+
for (const desc of allTestDescriptions) {
|
|
4437
|
+
const score = wordOverlap(acText, desc);
|
|
4438
|
+
if (score > bestScore) {
|
|
4439
|
+
bestScore = score;
|
|
4440
|
+
bestTestName = desc;
|
|
4441
|
+
}
|
|
4442
|
+
}
|
|
4443
|
+
const matched = bestScore >= MATCH_THRESHOLD;
|
|
4444
|
+
return {
|
|
4445
|
+
acText,
|
|
4446
|
+
matched,
|
|
4447
|
+
testName: matched ? bestTestName : null,
|
|
4448
|
+
score: bestScore
|
|
4449
|
+
};
|
|
4450
|
+
});
|
|
4451
|
+
return {
|
|
4452
|
+
storyKey,
|
|
4453
|
+
matrix,
|
|
4454
|
+
confidence: "approximate",
|
|
4455
|
+
warnings
|
|
4456
|
+
};
|
|
4457
|
+
}
|
|
4458
|
+
|
|
5772
4459
|
//#endregion
|
|
5773
4460
|
//#region packages/sdlc/dist/run-model/cli-flags.js
|
|
5774
4461
|
/**
|
|
@@ -6040,7 +4727,12 @@ const ProposalSchema = z.object({
|
|
|
6040
4727
|
z.string()
|
|
6041
4728
|
]),
|
|
6042
4729
|
story_key: z.string().optional(),
|
|
6043
|
-
payload: z.record(z.string(), z.unknown()).optional()
|
|
4730
|
+
payload: z.record(z.string(), z.unknown()).optional(),
|
|
4731
|
+
storyKey: z.string().optional(),
|
|
4732
|
+
rootCause: z.string().optional(),
|
|
4733
|
+
attempts: z.number().int().optional(),
|
|
4734
|
+
suggestedAction: z.string().optional(),
|
|
4735
|
+
blastRadius: z.array(z.string()).optional()
|
|
6044
4736
|
});
|
|
6045
4737
|
/**
|
|
6046
4738
|
* Zod schema for the full run manifest data.
|
|
@@ -6519,6 +5211,64 @@ var RunManifest = class RunManifest {
|
|
|
6519
5211
|
return this._enqueue(() => this._appendRecoveryEntryImpl(entry));
|
|
6520
5212
|
}
|
|
6521
5213
|
/**
|
|
5214
|
+
* Raw implementation — must only be called from within `_enqueue`.
|
|
5215
|
+
*/
|
|
5216
|
+
async _appendProposalImpl(proposal) {
|
|
5217
|
+
let existingData;
|
|
5218
|
+
try {
|
|
5219
|
+
const read = await RunManifest.read(this.runId, this.baseDir, this.doltAdapter);
|
|
5220
|
+
const { generation: _gen, updated_at: _ts,...rest } = read;
|
|
5221
|
+
existingData = rest;
|
|
5222
|
+
} catch {
|
|
5223
|
+
const now = new Date().toISOString();
|
|
5224
|
+
existingData = {
|
|
5225
|
+
run_id: this.runId,
|
|
5226
|
+
cli_flags: {},
|
|
5227
|
+
story_scope: [],
|
|
5228
|
+
supervisor_pid: null,
|
|
5229
|
+
supervisor_session_id: null,
|
|
5230
|
+
per_story_state: {},
|
|
5231
|
+
recovery_history: [],
|
|
5232
|
+
cost_accumulation: {
|
|
5233
|
+
per_story: {},
|
|
5234
|
+
run_total: 0
|
|
5235
|
+
},
|
|
5236
|
+
pending_proposals: [],
|
|
5237
|
+
created_at: now
|
|
5238
|
+
};
|
|
5239
|
+
}
|
|
5240
|
+
const targetKey = proposal.storyKey ?? proposal.story_key;
|
|
5241
|
+
if (targetKey !== void 0) {
|
|
5242
|
+
const alreadyPresent = existingData.pending_proposals.some((p) => {
|
|
5243
|
+
const existingKey = p.storyKey ?? p.story_key;
|
|
5244
|
+
return existingKey === targetKey;
|
|
5245
|
+
});
|
|
5246
|
+
if (alreadyPresent) return;
|
|
5247
|
+
}
|
|
5248
|
+
await this._writeImpl({
|
|
5249
|
+
...existingData,
|
|
5250
|
+
pending_proposals: [...existingData.pending_proposals, proposal]
|
|
5251
|
+
});
|
|
5252
|
+
}
|
|
5253
|
+
/**
|
|
5254
|
+
* Atomically append a proposal to `pending_proposals`, deduplicating by storyKey.
|
|
5255
|
+
*
|
|
5256
|
+
* If a proposal with the same `storyKey` (or `story_key`) is already present in
|
|
5257
|
+
* `pending_proposals`, the call is a silent no-op (idempotent). This guards
|
|
5258
|
+
* against duplicate proposals from concurrent or repeated recovery invocations.
|
|
5259
|
+
*
|
|
5260
|
+
* Enqueues the operation via `_enqueue` so concurrent calls are serialized.
|
|
5261
|
+
* Non-fatal: callers MUST wrap in `.catch((err) => logger.warn(...))`.
|
|
5262
|
+
* The pipeline must never abort due to a manifest write failure.
|
|
5263
|
+
*
|
|
5264
|
+
* Story 73-1 (Recovery Engine). Canonical manifest helper per AC3.
|
|
5265
|
+
*
|
|
5266
|
+
* @param proposal - Proposal to append (id, created_at, description, type + Epic 73 fields)
|
|
5267
|
+
*/
|
|
5268
|
+
async appendProposal(proposal) {
|
|
5269
|
+
return this._enqueue(() => this._appendProposalImpl(proposal));
|
|
5270
|
+
}
|
|
5271
|
+
/**
|
|
6522
5272
|
* Create a new manifest with `generation: 0` and write it.
|
|
6523
5273
|
* Returns a bound `RunManifest` instance.
|
|
6524
5274
|
*/
|
|
@@ -6854,11 +5604,11 @@ var SupervisorLock = class {
|
|
|
6854
5604
|
mode = null;
|
|
6855
5605
|
/** File handle held open to maintain the advisory lock (flock mode only). */
|
|
6856
5606
|
lockHandle = null;
|
|
6857
|
-
constructor(runId, manifest, logger$
|
|
5607
|
+
constructor(runId, manifest, logger$1) {
|
|
6858
5608
|
this.runId = runId;
|
|
6859
5609
|
this.manifest = manifest;
|
|
6860
5610
|
this.baseDir = manifest.baseDir;
|
|
6861
|
-
this.logger = logger$
|
|
5611
|
+
this.logger = logger$1 ?? defaultLogger;
|
|
6862
5612
|
}
|
|
6863
5613
|
/** Advisory lock file path (primary path). */
|
|
6864
5614
|
get lockPath() {
|
|
@@ -6905,19 +5655,19 @@ var SupervisorLock = class {
|
|
|
6905
5655
|
existingPid = data.supervisor_pid;
|
|
6906
5656
|
} catch {}
|
|
6907
5657
|
if (existingPid === null) {
|
|
6908
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
5658
|
+
await unlink$1(this.lockPath).catch(() => void 0);
|
|
6909
5659
|
await this.acquire(pid, sessionId, opts);
|
|
6910
5660
|
return;
|
|
6911
5661
|
}
|
|
6912
5662
|
const isAlive = this.isPidAlive(existingPid);
|
|
6913
5663
|
if (!isAlive) {
|
|
6914
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
5664
|
+
await unlink$1(this.lockPath).catch(() => void 0);
|
|
6915
5665
|
await this.acquire(pid, sessionId, opts);
|
|
6916
5666
|
return;
|
|
6917
5667
|
}
|
|
6918
5668
|
if (force) {
|
|
6919
5669
|
await this.forceKillOwner(existingPid);
|
|
6920
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
5670
|
+
await unlink$1(this.lockPath).catch(() => void 0);
|
|
6921
5671
|
await this.acquire(pid, sessionId, opts);
|
|
6922
5672
|
return;
|
|
6923
5673
|
}
|
|
@@ -6939,7 +5689,7 @@ var SupervisorLock = class {
|
|
|
6939
5689
|
} catch {}
|
|
6940
5690
|
this.lockHandle = null;
|
|
6941
5691
|
this.mode = null;
|
|
6942
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
5692
|
+
await unlink$1(this.lockPath).catch(() => void 0);
|
|
6943
5693
|
throw postOpenErr;
|
|
6944
5694
|
}
|
|
6945
5695
|
}
|
|
@@ -6959,7 +5709,7 @@ var SupervisorLock = class {
|
|
|
6959
5709
|
} catch {}
|
|
6960
5710
|
this.lockHandle = null;
|
|
6961
5711
|
}
|
|
6962
|
-
await unlink(this.lockPath).catch(() => void 0);
|
|
5712
|
+
await unlink$1(this.lockPath).catch(() => void 0);
|
|
6963
5713
|
} else if (this.mode === "pid-file") await this.releaseViaPidFile();
|
|
6964
5714
|
this.mode = null;
|
|
6965
5715
|
await this.manifest.update({
|
|
@@ -6999,7 +5749,7 @@ var SupervisorLock = class {
|
|
|
6999
5749
|
this.mode = "pid-file";
|
|
7000
5750
|
}
|
|
7001
5751
|
async releaseViaPidFile() {
|
|
7002
|
-
await unlink(this.pidPath).catch(() => void 0);
|
|
5752
|
+
await unlink$1(this.pidPath).catch(() => void 0);
|
|
7003
5753
|
}
|
|
7004
5754
|
/**
|
|
7005
5755
|
* Send SIGTERM to the existing supervisor and wait up to 500ms for it to exit.
|
|
@@ -7032,7 +5782,7 @@ var SupervisorLock = class {
|
|
|
7032
5782
|
|
|
7033
5783
|
//#endregion
|
|
7034
5784
|
//#region src/cli/commands/manifest-read.ts
|
|
7035
|
-
const logger
|
|
5785
|
+
const logger = createLogger("manifest-read");
|
|
7036
5786
|
/**
|
|
7037
5787
|
* Read the active run ID from `.substrate/current-run-id`.
|
|
7038
5788
|
*
|
|
@@ -7064,7 +5814,7 @@ async function readCurrentRunId(dbRoot) {
|
|
|
7064
5814
|
async function resolveRunManifest(dbRoot, runId) {
|
|
7065
5815
|
const resolvedRunId = runId ?? await readCurrentRunId(dbRoot);
|
|
7066
5816
|
if (!resolvedRunId) {
|
|
7067
|
-
logger
|
|
5817
|
+
logger.debug("run manifest not found — falling back to Dolt (no current-run-id)");
|
|
7068
5818
|
return {
|
|
7069
5819
|
manifest: null,
|
|
7070
5820
|
runId: null
|
|
@@ -7079,7 +5829,7 @@ async function resolveRunManifest(dbRoot, runId) {
|
|
|
7079
5829
|
runId: resolvedRunId
|
|
7080
5830
|
};
|
|
7081
5831
|
} catch {
|
|
7082
|
-
logger
|
|
5832
|
+
logger.debug({ runId: resolvedRunId }, "run manifest not found — falling back to Dolt");
|
|
7083
5833
|
return {
|
|
7084
5834
|
manifest: null,
|
|
7085
5835
|
runId: resolvedRunId
|
|
@@ -7088,489 +5838,5 @@ async function resolveRunManifest(dbRoot, runId) {
|
|
|
7088
5838
|
}
|
|
7089
5839
|
|
|
7090
5840
|
//#endregion
|
|
7091
|
-
|
|
7092
|
-
|
|
7093
|
-
/** Default stall threshold in seconds — also used by supervisor default */
|
|
7094
|
-
const DEFAULT_STALL_THRESHOLD_SECONDS = 600;
|
|
7095
|
-
/**
|
|
7096
|
-
* Determine whether a ps output line represents the substrate pipeline orchestrator.
|
|
7097
|
-
* Handles invocation via:
|
|
7098
|
-
* - `substrate run` (globally installed)
|
|
7099
|
-
* - `substrate-ai run`
|
|
7100
|
-
* - `node dist/cli/index.js run` (npm run substrate:dev)
|
|
7101
|
-
* - `npx substrate run`
|
|
7102
|
-
* - any node process whose command contains `run` with `--events` or `--stories`
|
|
7103
|
-
*
|
|
7104
|
-
* When `projectRoot` is provided, additionally checks that the command line
|
|
7105
|
-
* contains that path (via `--project-root` flag or as part of the binary/CWD path).
|
|
7106
|
-
* This ensures multi-project environments match the correct orchestrator.
|
|
7107
|
-
*/
|
|
7108
|
-
function isOrchestratorProcessLine(line, projectRoot) {
|
|
7109
|
-
if (line.includes("grep")) return false;
|
|
7110
|
-
let isOrchestrator = false;
|
|
7111
|
-
if (line.includes("substrate run")) isOrchestrator = true;
|
|
7112
|
-
else if (line.includes("substrate-ai run")) isOrchestrator = true;
|
|
7113
|
-
else if (line.includes("index.js run")) isOrchestrator = true;
|
|
7114
|
-
else if (line.includes("node") && /\srun(\s|$)/.test(line) && (line.includes("substrate") || line.includes("--events") || line.includes("--stories"))) isOrchestrator = true;
|
|
7115
|
-
if (!isOrchestrator) return false;
|
|
7116
|
-
if (projectRoot !== void 0) return line.includes(projectRoot);
|
|
7117
|
-
return true;
|
|
7118
|
-
}
|
|
7119
|
-
function inspectProcessTree(opts) {
|
|
7120
|
-
const { projectRoot, substrateDirPath, execFileSync: execFileSyncOverride, readFileSync: readFileSyncOverride } = opts ?? {};
|
|
7121
|
-
const result = {
|
|
7122
|
-
orchestrator_pid: null,
|
|
7123
|
-
child_pids: [],
|
|
7124
|
-
zombies: []
|
|
7125
|
-
};
|
|
7126
|
-
try {
|
|
7127
|
-
let psOutput;
|
|
7128
|
-
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
7129
|
-
encoding: "utf-8",
|
|
7130
|
-
timeout: 5e3
|
|
7131
|
-
});
|
|
7132
|
-
else {
|
|
7133
|
-
const { execFileSync: execFileSync$1 } = __require("node:child_process");
|
|
7134
|
-
psOutput = execFileSync$1("ps", ["-eo", "pid,ppid,stat,command"], {
|
|
7135
|
-
encoding: "utf-8",
|
|
7136
|
-
timeout: 5e3
|
|
7137
|
-
});
|
|
7138
|
-
}
|
|
7139
|
-
const lines = psOutput.split("\n");
|
|
7140
|
-
if (substrateDirPath !== void 0) try {
|
|
7141
|
-
const readFileSyncFn = readFileSyncOverride ?? ((path$3, encoding) => readFileSync(path$3, encoding));
|
|
7142
|
-
const pidContent = readFileSyncFn(join(substrateDirPath, "orchestrator.pid"), "utf-8");
|
|
7143
|
-
const pid = parseInt(pidContent.trim(), 10);
|
|
7144
|
-
if (!isNaN(pid) && pid > 0) {
|
|
7145
|
-
const isAlive = lines.some((line) => {
|
|
7146
|
-
const parts = line.trim().split(/\s+/);
|
|
7147
|
-
if (parts.length < 3) return false;
|
|
7148
|
-
return parseInt(parts[0], 10) === pid && !parts[2].includes("Z");
|
|
7149
|
-
});
|
|
7150
|
-
if (isAlive) result.orchestrator_pid = pid;
|
|
7151
|
-
else result.pid_file_dead = true;
|
|
7152
|
-
}
|
|
7153
|
-
} catch {}
|
|
7154
|
-
if (result.orchestrator_pid === null) {
|
|
7155
|
-
for (const line of lines) if (isOrchestratorProcessLine(line, projectRoot)) {
|
|
7156
|
-
const match = line.trim().match(/^(\d+)/);
|
|
7157
|
-
if (match) {
|
|
7158
|
-
result.orchestrator_pid = parseInt(match[1], 10);
|
|
7159
|
-
break;
|
|
7160
|
-
}
|
|
7161
|
-
}
|
|
7162
|
-
}
|
|
7163
|
-
if (result.orchestrator_pid !== null) for (const line of lines) {
|
|
7164
|
-
const parts = line.trim().split(/\s+/);
|
|
7165
|
-
if (parts.length >= 3) {
|
|
7166
|
-
const pid = parseInt(parts[0], 10);
|
|
7167
|
-
const ppid = parseInt(parts[1], 10);
|
|
7168
|
-
const stat$2 = parts[2];
|
|
7169
|
-
if (ppid === result.orchestrator_pid && pid !== result.orchestrator_pid) {
|
|
7170
|
-
result.child_pids.push(pid);
|
|
7171
|
-
if (stat$2.includes("Z")) result.zombies.push(pid);
|
|
7172
|
-
}
|
|
7173
|
-
}
|
|
7174
|
-
}
|
|
7175
|
-
} catch {}
|
|
7176
|
-
return result;
|
|
7177
|
-
}
|
|
7178
|
-
/**
|
|
7179
|
-
* Collect all descendant PIDs of the given root PIDs by walking the process
|
|
7180
|
-
* tree recursively. This ensures that grandchildren of the orchestrator
|
|
7181
|
-
* (e.g. node subprocesses spawned by `claude -p`) are also killed during
|
|
7182
|
-
* stall recovery, leaving no orphan processes.
|
|
7183
|
-
*
|
|
7184
|
-
* Returns only the descendants — the root PIDs themselves are NOT included.
|
|
7185
|
-
*/
|
|
7186
|
-
function getAllDescendantPids(rootPids, execFileSyncOverride) {
|
|
7187
|
-
if (rootPids.length === 0) return [];
|
|
7188
|
-
try {
|
|
7189
|
-
let psOutput;
|
|
7190
|
-
if (execFileSyncOverride !== void 0) psOutput = execFileSyncOverride("ps", ["-eo", "pid,ppid"], {
|
|
7191
|
-
encoding: "utf-8",
|
|
7192
|
-
timeout: 5e3
|
|
7193
|
-
});
|
|
7194
|
-
else {
|
|
7195
|
-
const { execFileSync: execFileSync$1 } = __require("node:child_process");
|
|
7196
|
-
psOutput = execFileSync$1("ps", ["-eo", "pid,ppid"], {
|
|
7197
|
-
encoding: "utf-8",
|
|
7198
|
-
timeout: 5e3
|
|
7199
|
-
});
|
|
7200
|
-
}
|
|
7201
|
-
const childrenOf = new Map();
|
|
7202
|
-
for (const line of psOutput.split("\n")) {
|
|
7203
|
-
const parts = line.trim().split(/\s+/);
|
|
7204
|
-
if (parts.length >= 2) {
|
|
7205
|
-
const pid = parseInt(parts[0], 10);
|
|
7206
|
-
const ppid = parseInt(parts[1], 10);
|
|
7207
|
-
if (!isNaN(pid) && !isNaN(ppid) && pid > 0) {
|
|
7208
|
-
if (!childrenOf.has(ppid)) childrenOf.set(ppid, []);
|
|
7209
|
-
childrenOf.get(ppid).push(pid);
|
|
7210
|
-
}
|
|
7211
|
-
}
|
|
7212
|
-
}
|
|
7213
|
-
const descendants = [];
|
|
7214
|
-
const seen = new Set(rootPids);
|
|
7215
|
-
const queue = [...rootPids];
|
|
7216
|
-
while (queue.length > 0) {
|
|
7217
|
-
const current = queue.shift();
|
|
7218
|
-
const children = childrenOf.get(current) ?? [];
|
|
7219
|
-
for (const child of children) if (!seen.has(child)) {
|
|
7220
|
-
seen.add(child);
|
|
7221
|
-
descendants.push(child);
|
|
7222
|
-
queue.push(child);
|
|
7223
|
-
}
|
|
7224
|
-
}
|
|
7225
|
-
return descendants;
|
|
7226
|
-
} catch {
|
|
7227
|
-
return [];
|
|
7228
|
-
}
|
|
7229
|
-
}
|
|
7230
|
-
/**
|
|
7231
|
-
* Derive health story counts from manifest `per_story_state`.
|
|
7232
|
-
* Maps manifest status strings to health output buckets.
|
|
7233
|
-
*/
|
|
7234
|
-
function buildHealthStoryCountsFromManifest(perStoryState) {
|
|
7235
|
-
const counts = {
|
|
7236
|
-
active: 0,
|
|
7237
|
-
completed: 0,
|
|
7238
|
-
escalated: 0,
|
|
7239
|
-
pending: 0,
|
|
7240
|
-
failed: 0
|
|
7241
|
-
};
|
|
7242
|
-
for (const entry of Object.values(perStoryState)) switch (entry.status) {
|
|
7243
|
-
case "complete":
|
|
7244
|
-
counts.completed++;
|
|
7245
|
-
break;
|
|
7246
|
-
case "escalated":
|
|
7247
|
-
counts.escalated++;
|
|
7248
|
-
break;
|
|
7249
|
-
case "failed":
|
|
7250
|
-
case "verification-failed":
|
|
7251
|
-
counts.failed++;
|
|
7252
|
-
break;
|
|
7253
|
-
case "pending":
|
|
7254
|
-
case "gated":
|
|
7255
|
-
counts.pending++;
|
|
7256
|
-
break;
|
|
7257
|
-
case "dispatched":
|
|
7258
|
-
case "in-review":
|
|
7259
|
-
case "recovered":
|
|
7260
|
-
default:
|
|
7261
|
-
counts.active++;
|
|
7262
|
-
break;
|
|
7263
|
-
}
|
|
7264
|
-
return counts;
|
|
7265
|
-
}
|
|
7266
|
-
/**
|
|
7267
|
-
* Fetch pipeline health data as a structured object without any stdout side-effects.
|
|
7268
|
-
* Used by runSupervisorAction to poll health without formatting overhead.
|
|
7269
|
-
*
|
|
7270
|
-
* Returns a NO_PIPELINE_RUNNING health object for all graceful "no data" cases
|
|
7271
|
-
* (missing DB, missing run, terminal run status). Throws only on unexpected errors.
|
|
7272
|
-
*/
|
|
7273
|
-
async function getAutoHealthData(options) {
|
|
7274
|
-
const { runId, projectRoot, stateStore, stateStoreConfig } = options;
|
|
7275
|
-
const dbRoot = await resolveMainRepoRoot(projectRoot);
|
|
7276
|
-
const dbPath = join(dbRoot, ".substrate", "substrate.db");
|
|
7277
|
-
let doltStateInfo;
|
|
7278
|
-
if (stateStoreConfig?.backend === "dolt" && stateStore) {
|
|
7279
|
-
const repoPath = stateStoreConfig.basePath ?? projectRoot;
|
|
7280
|
-
const doltDirPath = join(repoPath, ".dolt");
|
|
7281
|
-
const initialized = existsSync(doltDirPath);
|
|
7282
|
-
let responsive = false;
|
|
7283
|
-
let version;
|
|
7284
|
-
let branches;
|
|
7285
|
-
let currentBranch;
|
|
7286
|
-
try {
|
|
7287
|
-
await stateStore.getHistory(1);
|
|
7288
|
-
responsive = true;
|
|
7289
|
-
try {
|
|
7290
|
-
const { execFile: ef } = await import("node:child_process");
|
|
7291
|
-
const { promisify: p } = await import("node:util");
|
|
7292
|
-
const execFileAsync = p(ef);
|
|
7293
|
-
const { stdout } = await execFileAsync("dolt", ["version"]);
|
|
7294
|
-
const match = stdout.match(/dolt version (\S+)/);
|
|
7295
|
-
if (match) version = match[1];
|
|
7296
|
-
} catch {}
|
|
7297
|
-
try {
|
|
7298
|
-
const { execFile: ef } = await import("node:child_process");
|
|
7299
|
-
const { promisify: p } = await import("node:util");
|
|
7300
|
-
const execFileAsync = p(ef);
|
|
7301
|
-
const { stdout } = await execFileAsync("dolt", ["branch", "--list"], { cwd: repoPath });
|
|
7302
|
-
const lines = stdout.split("\n").filter((l) => l.trim().length > 0);
|
|
7303
|
-
branches = lines.map((l) => {
|
|
7304
|
-
const trimmed = l.trim();
|
|
7305
|
-
if (trimmed.startsWith("* ")) {
|
|
7306
|
-
currentBranch = trimmed.slice(2).trim();
|
|
7307
|
-
return currentBranch;
|
|
7308
|
-
}
|
|
7309
|
-
return trimmed;
|
|
7310
|
-
});
|
|
7311
|
-
} catch {}
|
|
7312
|
-
} catch {
|
|
7313
|
-
responsive = false;
|
|
7314
|
-
}
|
|
7315
|
-
doltStateInfo = {
|
|
7316
|
-
initialized,
|
|
7317
|
-
responsive,
|
|
7318
|
-
...version !== void 0 ? { version } : {},
|
|
7319
|
-
...branches !== void 0 ? { branches } : {},
|
|
7320
|
-
...currentBranch !== void 0 ? { current_branch: currentBranch } : {}
|
|
7321
|
-
};
|
|
7322
|
-
}
|
|
7323
|
-
const NO_PIPELINE = {
|
|
7324
|
-
verdict: "NO_PIPELINE_RUNNING",
|
|
7325
|
-
run_id: null,
|
|
7326
|
-
status: null,
|
|
7327
|
-
current_phase: null,
|
|
7328
|
-
staleness_seconds: 0,
|
|
7329
|
-
last_activity: "",
|
|
7330
|
-
process: {
|
|
7331
|
-
orchestrator_pid: null,
|
|
7332
|
-
child_pids: [],
|
|
7333
|
-
zombies: []
|
|
7334
|
-
},
|
|
7335
|
-
stories: {
|
|
7336
|
-
active: 0,
|
|
7337
|
-
completed: 0,
|
|
7338
|
-
escalated: 0,
|
|
7339
|
-
details: {}
|
|
7340
|
-
},
|
|
7341
|
-
...doltStateInfo !== void 0 ? { dolt_state: doltStateInfo } : {}
|
|
7342
|
-
};
|
|
7343
|
-
const doltDir = join(dbRoot, ".substrate", "state", ".dolt");
|
|
7344
|
-
if (!existsSync(dbPath) && !existsSync(doltDir)) return NO_PIPELINE;
|
|
7345
|
-
const adapter = createDatabaseAdapter$1({
|
|
7346
|
-
backend: "auto",
|
|
7347
|
-
basePath: dbRoot
|
|
7348
|
-
});
|
|
7349
|
-
try {
|
|
7350
|
-
await initSchema(adapter);
|
|
7351
|
-
let run;
|
|
7352
|
-
if (runId !== void 0) run = await getPipelineRunById(adapter, runId);
|
|
7353
|
-
else {
|
|
7354
|
-
let currentRunId;
|
|
7355
|
-
try {
|
|
7356
|
-
const currentRunIdPath = join(dbRoot, ".substrate", "current-run-id");
|
|
7357
|
-
const content = readFileSync(currentRunIdPath, "utf-8").trim();
|
|
7358
|
-
const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
7359
|
-
if (UUID_RE.test(content)) currentRunId = content;
|
|
7360
|
-
} catch {}
|
|
7361
|
-
if (currentRunId !== void 0) run = await getPipelineRunById(adapter, currentRunId);
|
|
7362
|
-
if (run === void 0) run = await getLatestRun(adapter);
|
|
7363
|
-
}
|
|
7364
|
-
if (run === void 0) {
|
|
7365
|
-
const substrateDirPath$1 = join(dbRoot, ".substrate");
|
|
7366
|
-
const fallbackProcessInfo = inspectProcessTree({
|
|
7367
|
-
projectRoot: dbRoot,
|
|
7368
|
-
substrateDirPath: substrateDirPath$1
|
|
7369
|
-
});
|
|
7370
|
-
if (fallbackProcessInfo.orchestrator_pid !== null) return {
|
|
7371
|
-
verdict: "HEALTHY",
|
|
7372
|
-
run_id: null,
|
|
7373
|
-
status: "running",
|
|
7374
|
-
current_phase: "implementation",
|
|
7375
|
-
staleness_seconds: 0,
|
|
7376
|
-
last_activity: new Date().toISOString(),
|
|
7377
|
-
process: fallbackProcessInfo,
|
|
7378
|
-
stories: {
|
|
7379
|
-
active: 0,
|
|
7380
|
-
completed: 0,
|
|
7381
|
-
escalated: 0,
|
|
7382
|
-
details: {}
|
|
7383
|
-
},
|
|
7384
|
-
...doltStateInfo !== void 0 ? { dolt_state: doltStateInfo } : {}
|
|
7385
|
-
};
|
|
7386
|
-
return NO_PIPELINE;
|
|
7387
|
-
}
|
|
7388
|
-
const updatedAt = parseDbTimestampAsUtc(run.updated_at ?? "");
|
|
7389
|
-
const stalenessSeconds = Math.round((Date.now() - updatedAt.getTime()) / 1e3);
|
|
7390
|
-
let storyDetails = {};
|
|
7391
|
-
let active = 0;
|
|
7392
|
-
let completed = 0;
|
|
7393
|
-
let escalated = 0;
|
|
7394
|
-
let pending = 0;
|
|
7395
|
-
try {
|
|
7396
|
-
if (run.token_usage_json) {
|
|
7397
|
-
const state = JSON.parse(run.token_usage_json);
|
|
7398
|
-
if (state.stories) for (const [key, s] of Object.entries(state.stories)) {
|
|
7399
|
-
storyDetails[key] = {
|
|
7400
|
-
phase: s.phase,
|
|
7401
|
-
review_cycles: s.reviewCycles
|
|
7402
|
-
};
|
|
7403
|
-
if (s.phase === "COMPLETE") completed++;
|
|
7404
|
-
else if (s.phase === "ESCALATED") escalated++;
|
|
7405
|
-
else if (s.phase === "PENDING") pending++;
|
|
7406
|
-
else active++;
|
|
7407
|
-
}
|
|
7408
|
-
}
|
|
7409
|
-
} catch {}
|
|
7410
|
-
let manifestSupervisor;
|
|
7411
|
-
let manifestStoryCounts;
|
|
7412
|
-
try {
|
|
7413
|
-
const { manifest: resolvedManifest } = await resolveRunManifest(dbRoot, run.id);
|
|
7414
|
-
if (resolvedManifest !== null) {
|
|
7415
|
-
const manifestData = await resolvedManifest.read();
|
|
7416
|
-
manifestSupervisor = {
|
|
7417
|
-
pid: manifestData.supervisor_pid,
|
|
7418
|
-
session_id: manifestData.supervisor_session_id
|
|
7419
|
-
};
|
|
7420
|
-
manifestStoryCounts = buildHealthStoryCountsFromManifest(manifestData.per_story_state);
|
|
7421
|
-
logger.debug({ runId: run.id }, "health: story counts and supervisor read from manifest");
|
|
7422
|
-
}
|
|
7423
|
-
} catch {
|
|
7424
|
-
logger.debug({ runId: run.id }, "health: manifest read failed — using token_usage_json counts");
|
|
7425
|
-
}
|
|
7426
|
-
const finalActive = manifestStoryCounts?.active ?? active;
|
|
7427
|
-
const finalCompleted = manifestStoryCounts?.completed ?? completed;
|
|
7428
|
-
const finalEscalated = manifestStoryCounts?.escalated ?? escalated;
|
|
7429
|
-
const finalPending = manifestStoryCounts?.pending ?? pending;
|
|
7430
|
-
const finalFailed = manifestStoryCounts?.failed;
|
|
7431
|
-
const substrateDirPath = join(dbRoot, ".substrate");
|
|
7432
|
-
const processInfo = options._processInfoOverride ?? inspectProcessTree({
|
|
7433
|
-
projectRoot,
|
|
7434
|
-
substrateDirPath
|
|
7435
|
-
});
|
|
7436
|
-
let verdict = "NO_PIPELINE_RUNNING";
|
|
7437
|
-
if (run.status === "running") if (processInfo.orchestrator_pid !== null) verdict = "HEALTHY";
|
|
7438
|
-
else if (processInfo.pid_file_dead === true) verdict = "STALLED";
|
|
7439
|
-
else if (processInfo.zombies.length > 0) verdict = "STALLED";
|
|
7440
|
-
else if (stalenessSeconds > DEFAULT_STALL_THRESHOLD_SECONDS) verdict = "STALLED";
|
|
7441
|
-
else if (finalActive > 0) verdict = "STALLED";
|
|
7442
|
-
else verdict = "HEALTHY";
|
|
7443
|
-
else if (run.status === "completed" || run.status === "failed" || run.status === "stopped") verdict = "NO_PIPELINE_RUNNING";
|
|
7444
|
-
const warnings = [];
|
|
7445
|
-
if (doltStateInfo !== void 0 && doltStateInfo.responsive === false) warnings.push("Dolt not connected — decision store queries may fail, story context will be degraded");
|
|
7446
|
-
if (finalEscalated > 0) warnings.push(`${finalEscalated} story(ies) escalated — operator intervention may be needed`);
|
|
7447
|
-
const healthOutput = {
|
|
7448
|
-
verdict,
|
|
7449
|
-
run_id: run.id,
|
|
7450
|
-
status: run.status,
|
|
7451
|
-
current_phase: run.current_phase ?? null,
|
|
7452
|
-
staleness_seconds: stalenessSeconds,
|
|
7453
|
-
last_activity: run.updated_at ?? "",
|
|
7454
|
-
process: processInfo,
|
|
7455
|
-
stories: {
|
|
7456
|
-
active: finalActive,
|
|
7457
|
-
completed: finalCompleted,
|
|
7458
|
-
escalated: finalEscalated,
|
|
7459
|
-
pending: finalPending,
|
|
7460
|
-
...finalFailed !== void 0 ? { failed: finalFailed } : {},
|
|
7461
|
-
details: storyDetails
|
|
7462
|
-
},
|
|
7463
|
-
...manifestSupervisor !== void 0 ? { manifest_supervisor: manifestSupervisor } : {},
|
|
7464
|
-
...doltStateInfo !== void 0 ? { dolt_state: doltStateInfo } : {},
|
|
7465
|
-
...warnings.length > 0 ? { warnings } : {}
|
|
7466
|
-
};
|
|
7467
|
-
return healthOutput;
|
|
7468
|
-
} finally {
|
|
7469
|
-
try {
|
|
7470
|
-
await adapter.close();
|
|
7471
|
-
} catch {}
|
|
7472
|
-
}
|
|
7473
|
-
}
|
|
7474
|
-
async function runHealthAction(options) {
|
|
7475
|
-
const { outputFormat } = options;
|
|
7476
|
-
try {
|
|
7477
|
-
const health = await getAutoHealthData(options);
|
|
7478
|
-
if (outputFormat === "json") process.stdout.write(formatOutput(health, "json", true) + "\n");
|
|
7479
|
-
else {
|
|
7480
|
-
const verdictLabel = health.verdict === "HEALTHY" ? "HEALTHY" : health.verdict === "STALLED" ? "STALLED" : "NO PIPELINE RUNNING";
|
|
7481
|
-
process.stdout.write(`\nPipeline Health: ${verdictLabel}\n`);
|
|
7482
|
-
if (health.run_id !== null) {
|
|
7483
|
-
process.stdout.write(` Run: ${health.run_id}\n`);
|
|
7484
|
-
process.stdout.write(` Status: ${health.status}\n`);
|
|
7485
|
-
process.stdout.write(` Phase: ${health.current_phase ?? "N/A"}\n`);
|
|
7486
|
-
process.stdout.write(` Last Active: ${health.last_activity} (${health.staleness_seconds}s ago)\n`);
|
|
7487
|
-
const processInfo = health.process;
|
|
7488
|
-
if (processInfo.orchestrator_pid !== null) {
|
|
7489
|
-
process.stdout.write(` Orchestrator: PID ${processInfo.orchestrator_pid}\n`);
|
|
7490
|
-
process.stdout.write(` Children: ${processInfo.child_pids.length} active`);
|
|
7491
|
-
if (processInfo.zombies.length > 0) process.stdout.write(` (${processInfo.zombies.length} ZOMBIE)`);
|
|
7492
|
-
process.stdout.write("\n");
|
|
7493
|
-
} else process.stdout.write(" Orchestrator: not running\n");
|
|
7494
|
-
const storyDetails = health.stories.details;
|
|
7495
|
-
if (Object.keys(storyDetails).length > 0) {
|
|
7496
|
-
process.stdout.write("\n Stories:\n");
|
|
7497
|
-
for (const [key, s] of Object.entries(storyDetails)) process.stdout.write(` ${key}: ${s.phase} (${s.review_cycles} review cycles)\n`);
|
|
7498
|
-
process.stdout.write(`\n Summary: ${health.stories.active} active, ${health.stories.completed} completed, ${health.stories.escalated} escalated\n`);
|
|
7499
|
-
}
|
|
7500
|
-
}
|
|
7501
|
-
if (health.verdict === "STALLED") {
|
|
7502
|
-
process.stdout.write("\n Recommended Actions:\n");
|
|
7503
|
-
if (health.process.orchestrator_pid !== null) process.stdout.write(` 1. Kill stalled orchestrator: kill ${health.process.orchestrator_pid}\n`);
|
|
7504
|
-
if (health.process.zombies.length > 0) process.stdout.write(` ${health.process.orchestrator_pid !== null ? "2" : "1"}. Kill zombie processes: kill ${health.process.zombies.join(" ")}\n`);
|
|
7505
|
-
process.stdout.write(` ${health.process.orchestrator_pid !== null ? "3" : "2"}. Resume the run: substrate resume\n`);
|
|
7506
|
-
process.stdout.write(` ${health.process.orchestrator_pid !== null ? "4" : "3"}. Or start fresh: substrate run --events --stories <keys>\n`);
|
|
7507
|
-
} else if (health.verdict === "NO_PIPELINE_RUNNING" && health.stories.escalated > 0) {
|
|
7508
|
-
process.stdout.write("\n Recommended Actions:\n");
|
|
7509
|
-
process.stdout.write(" 1. Retry escalated stories: substrate retry-escalated\n");
|
|
7510
|
-
process.stdout.write(" 2. Or start a new run: substrate run --events\n");
|
|
7511
|
-
}
|
|
7512
|
-
if (health.warnings !== void 0 && health.warnings.length > 0) {
|
|
7513
|
-
process.stdout.write("\n Warnings:\n");
|
|
7514
|
-
for (const w of health.warnings) process.stdout.write(` ⚠ ${w}\n`);
|
|
7515
|
-
}
|
|
7516
|
-
if (health.dolt_state !== void 0) {
|
|
7517
|
-
const ds = health.dolt_state;
|
|
7518
|
-
const initStr = ds.initialized ? "yes" : "no";
|
|
7519
|
-
const respStr = ds.responsive ? "yes" : "no";
|
|
7520
|
-
const verStr = ds.version !== void 0 ? ` (v${ds.version})` : "";
|
|
7521
|
-
process.stdout.write(`\n Dolt State: initialized=${initStr} responsive=${respStr}${verStr}\n`);
|
|
7522
|
-
}
|
|
7523
|
-
}
|
|
7524
|
-
return 0;
|
|
7525
|
-
} catch (err) {
|
|
7526
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
7527
|
-
if (outputFormat === "json") process.stdout.write(formatOutput(null, "json", false, msg) + "\n");
|
|
7528
|
-
else process.stderr.write(`Error: ${msg}\n`);
|
|
7529
|
-
logger.error({ err }, "health action failed");
|
|
7530
|
-
return 1;
|
|
7531
|
-
}
|
|
7532
|
-
}
|
|
7533
|
-
function registerHealthCommand(program, _version = "0.0.0", projectRoot = process.cwd()) {
|
|
7534
|
-
program.command("health").description("Check pipeline health: process status, stall detection, and verdict").option("--run-id <id>", "Pipeline run ID to query (defaults to latest)").option("--project-root <path>", "Project root directory", projectRoot).option("--output-format <format>", "Output format: human (default) or json", "human").action(async (opts) => {
|
|
7535
|
-
const outputFormat = opts.outputFormat === "json" ? "json" : "human";
|
|
7536
|
-
const root = opts.projectRoot;
|
|
7537
|
-
let stateStore;
|
|
7538
|
-
let stateStoreConfig;
|
|
7539
|
-
const doltStatePath = join(root, ".substrate", "state", ".dolt");
|
|
7540
|
-
if (existsSync(doltStatePath)) {
|
|
7541
|
-
const basePath = join(root, ".substrate", "state");
|
|
7542
|
-
stateStoreConfig = {
|
|
7543
|
-
backend: "dolt",
|
|
7544
|
-
basePath
|
|
7545
|
-
};
|
|
7546
|
-
try {
|
|
7547
|
-
stateStore = createStateStore({
|
|
7548
|
-
backend: "dolt",
|
|
7549
|
-
basePath
|
|
7550
|
-
});
|
|
7551
|
-
await stateStore.initialize();
|
|
7552
|
-
} catch {
|
|
7553
|
-
stateStore = void 0;
|
|
7554
|
-
stateStoreConfig = void 0;
|
|
7555
|
-
}
|
|
7556
|
-
}
|
|
7557
|
-
try {
|
|
7558
|
-
const exitCode = await runHealthAction({
|
|
7559
|
-
outputFormat,
|
|
7560
|
-
runId: opts.runId,
|
|
7561
|
-
projectRoot: root,
|
|
7562
|
-
stateStore,
|
|
7563
|
-
stateStoreConfig
|
|
7564
|
-
});
|
|
7565
|
-
process.exitCode = exitCode;
|
|
7566
|
-
} finally {
|
|
7567
|
-
try {
|
|
7568
|
-
await stateStore?.close();
|
|
7569
|
-
} catch {}
|
|
7570
|
-
}
|
|
7571
|
-
});
|
|
7572
|
-
}
|
|
7573
|
-
|
|
7574
|
-
//#endregion
|
|
7575
|
-
export { BMAD_BASELINE_TOKENS_FULL, DEFAULT_STALL_THRESHOLD_SECONDS, DoltMergeConflict, FileStateStore, FindingsInjector, RunManifest, RuntimeProbeListSchema, STOP_AFTER_VALID_PHASES, STORY_KEY_PATTERN$1 as STORY_KEY_PATTERN, SUBSTRATE_OWNED_SETTINGS_KEYS, SupervisorLock, VALID_PHASES, WorkGraphRepository, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, __commonJS, __require, __toESM, aggregateProbeAuthorMetrics, applyConfigToGraph, buildPipelineStatusOutput, createDatabaseAdapter$1 as createDatabaseAdapter, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, createStateStore, detectCycles, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, findPackageRoot, formatOutput, formatPipelineStatusHuman, formatPipelineSummary, formatTokenTelemetry, getAllDescendantPids, getAutoHealthData, getSubstrateDefaultSettings, inspectProcessTree, isOrchestratorProcessLine, parseDbTimestampAsUtc, parseRuntimeProbes, readCurrentRunId, registerHealthCommand, renderFindings, resolveBmadMethodSrcPath, resolveBmadMethodVersion, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runHealthAction, runStaleVerificationRecovery, validateStoryKey };
|
|
7576
|
-
//# sourceMappingURL=health-DudlnqXd.js.map
|
|
5841
|
+
export { FindingsInjector, RunManifest, RuntimeProbeListSchema, SupervisorLock, ZERO_FINDINGS_BY_AUTHOR, ZERO_FINDING_COUNTS, ZERO_PROBE_AUTHOR_METRICS, aggregateProbeAuthorMetrics, applyConfigToGraph, createDefaultVerificationPipeline, createGraphOrchestrator, createSdlcCodeReviewHandler, createSdlcCreateStoryHandler, createSdlcDevStoryHandler, createSdlcPhaseHandler, detectsEventDrivenAC, detectsStateIntegratingAC, extractTargetFilesFromStoryContent, parseRuntimeProbes, readCurrentRunId, renderFindings, resolveGraphPath, resolveMainRepoRoot, resolveRunManifest, rollupFindingCounts, rollupFindingsByAuthor, rollupProbeAuthorByClass, rollupProbeAuthorMetrics, runAcTraceabilityCheck, runStaleVerificationRecovery };
|
|
5842
|
+
//# sourceMappingURL=manifest-read-g4zt3DXJ.js.map
|