sequant 2.1.1 → 2.1.2
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/.claude-plugin/plugin.json +1 -1
- package/dist/src/commands/init.js +4 -3
- package/dist/src/commands/run-compat.d.ts +14 -0
- package/dist/src/commands/run-compat.js +12 -0
- package/dist/src/commands/run.d.ts +4 -26
- package/dist/src/commands/run.js +92 -765
- package/dist/src/commands/status.js +9 -0
- package/dist/src/index.d.ts +11 -0
- package/dist/src/index.js +9 -0
- package/dist/src/lib/errors.d.ts +93 -0
- package/dist/src/lib/errors.js +97 -0
- package/dist/src/lib/settings.d.ts +236 -0
- package/dist/src/lib/settings.js +482 -37
- package/dist/src/lib/workflow/batch-executor.js +12 -4
- package/dist/src/lib/workflow/config-resolver.d.ts +50 -0
- package/dist/src/lib/workflow/config-resolver.js +167 -0
- package/dist/src/lib/workflow/error-classifier.d.ts +17 -7
- package/dist/src/lib/workflow/error-classifier.js +113 -15
- package/dist/src/lib/workflow/phase-executor.js +14 -2
- package/dist/src/lib/workflow/run-log-schema.d.ts +12 -0
- package/dist/src/lib/workflow/run-log-schema.js +7 -1
- package/dist/src/lib/workflow/run-orchestrator.d.ts +124 -0
- package/dist/src/lib/workflow/run-orchestrator.js +482 -0
- package/package.json +1 -1
- package/templates/skills/assess/SKILL.md +84 -35
- package/templates/skills/exec/SKILL.md +7 -27
- package/templates/skills/fullsolve/SKILL.md +329 -137
- package/templates/skills/qa/SKILL.md +23 -46
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RunOrchestrator — CLI-free execution engine for sequant workflows.
|
|
3
|
+
*
|
|
4
|
+
* Owns the full lifecycle: config → issue discovery → dispatch → results.
|
|
5
|
+
* Importable and usable without Commander.js or CLI context.
|
|
6
|
+
*
|
|
7
|
+
* @module
|
|
8
|
+
*/
|
|
9
|
+
import chalk from "chalk";
|
|
10
|
+
import { spawnSync } from "child_process";
|
|
11
|
+
import pLimit from "p-limit";
|
|
12
|
+
import { detectDefaultBranch, ensureWorktrees, ensureWorktreesChain, getWorktreeDiffStats, } from "./worktree-manager.js";
|
|
13
|
+
import { LogWriter } from "./log-writer.js";
|
|
14
|
+
import { StateManager } from "./state-manager.js";
|
|
15
|
+
import { ShutdownManager } from "../shutdown.js";
|
|
16
|
+
import { getIssueInfo, sortByDependencies, parseBatches, runIssueWithLogging, } from "./batch-executor.js";
|
|
17
|
+
import { reconcileStateAtStartup } from "./state-utils.js";
|
|
18
|
+
import { getCommitHash } from "./git-diff-utils.js";
|
|
19
|
+
import { MetricsWriter } from "./metrics-writer.js";
|
|
20
|
+
import { determineOutcome } from "./metrics-schema.js";
|
|
21
|
+
import { getTokenUsageForRun } from "./token-utils.js";
|
|
22
|
+
import { resolveRunOptions, buildExecutionConfig } from "./config-resolver.js";
|
|
23
|
+
// ── Orchestrator ────────────────────────────────────────────────────────────
|
|
24
|
+
/**
|
|
25
|
+
* CLI-free workflow execution engine.
|
|
26
|
+
*
|
|
27
|
+
* Two usage modes:
|
|
28
|
+
* 1. Full lifecycle: `RunOrchestrator.run(init, issueNumbers)` — handles
|
|
29
|
+
* services, worktrees, state guard, execution, and metrics.
|
|
30
|
+
* 2. Low-level: `new RunOrchestrator(config).execute(issueNumbers)` — caller
|
|
31
|
+
* manages setup/teardown.
|
|
32
|
+
*/
|
|
33
|
+
export class RunOrchestrator {
|
|
34
|
+
cfg;
|
|
35
|
+
constructor(config) {
|
|
36
|
+
this.validate(config);
|
|
37
|
+
this.cfg = config;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Full lifecycle execution — the primary entry point for programmatic use.
|
|
41
|
+
*
|
|
42
|
+
* Handles: config resolution → services setup → state guard →
|
|
43
|
+
* issue discovery → worktree creation → execution → metrics → cleanup.
|
|
44
|
+
*/
|
|
45
|
+
static async run(init, issueArgs, batches) {
|
|
46
|
+
const { options, settings, manifest, onProgress } = init;
|
|
47
|
+
// ── Config resolution ──────────────────────────────────────────────
|
|
48
|
+
const mergedOptions = resolveRunOptions(options, settings);
|
|
49
|
+
const baseBranch = init.baseBranch ??
|
|
50
|
+
options.base ??
|
|
51
|
+
settings.run.defaultBase ??
|
|
52
|
+
detectDefaultBranch(mergedOptions.verbose ?? false);
|
|
53
|
+
// ── Parse issues ───────────────────────────────────────────────────
|
|
54
|
+
let issueNumbers;
|
|
55
|
+
let resolvedBatches = batches ?? null;
|
|
56
|
+
if (mergedOptions.batch &&
|
|
57
|
+
mergedOptions.batch.length > 0 &&
|
|
58
|
+
!resolvedBatches) {
|
|
59
|
+
resolvedBatches = parseBatches(mergedOptions.batch);
|
|
60
|
+
issueNumbers = resolvedBatches.flat();
|
|
61
|
+
}
|
|
62
|
+
else if (resolvedBatches) {
|
|
63
|
+
issueNumbers = resolvedBatches.flat();
|
|
64
|
+
}
|
|
65
|
+
else {
|
|
66
|
+
issueNumbers = issueArgs
|
|
67
|
+
.map((i) => parseInt(i, 10))
|
|
68
|
+
.filter((n) => !isNaN(n));
|
|
69
|
+
}
|
|
70
|
+
if (issueNumbers.length === 0) {
|
|
71
|
+
return {
|
|
72
|
+
results: [],
|
|
73
|
+
logPath: null,
|
|
74
|
+
exitCode: 0,
|
|
75
|
+
worktreeMap: new Map(),
|
|
76
|
+
issueInfoMap: new Map(),
|
|
77
|
+
config: buildExecutionConfig(mergedOptions, settings, 0),
|
|
78
|
+
mergedOptions,
|
|
79
|
+
logWriter: null,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
// Sort by dependencies
|
|
83
|
+
if (issueNumbers.length > 1 && !resolvedBatches) {
|
|
84
|
+
issueNumbers = sortByDependencies(issueNumbers);
|
|
85
|
+
}
|
|
86
|
+
// ── Build execution config ─────────────────────────────────────────
|
|
87
|
+
const config = buildExecutionConfig(mergedOptions, settings, issueNumbers.length);
|
|
88
|
+
// ── Services setup ─────────────────────────────────────────────────
|
|
89
|
+
let logWriter = null;
|
|
90
|
+
const shouldLog = !mergedOptions.noLog &&
|
|
91
|
+
!config.dryRun &&
|
|
92
|
+
(mergedOptions.logJson ?? settings.run.logJson);
|
|
93
|
+
if (shouldLog) {
|
|
94
|
+
const runConfig = {
|
|
95
|
+
phases: config.phases,
|
|
96
|
+
sequential: config.sequential,
|
|
97
|
+
qualityLoop: config.qualityLoop,
|
|
98
|
+
maxIterations: config.maxIterations,
|
|
99
|
+
chain: mergedOptions.chain,
|
|
100
|
+
qaGate: mergedOptions.qaGate,
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
logWriter = new LogWriter({
|
|
104
|
+
logPath: mergedOptions.logPath ?? settings.run.logPath,
|
|
105
|
+
verbose: config.verbose,
|
|
106
|
+
startCommit: getCommitHash(process.cwd()),
|
|
107
|
+
});
|
|
108
|
+
await logWriter.initialize(runConfig);
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
112
|
+
console.log(chalk.yellow(` ! Log initialization failed, continuing without logging: ${msg}`));
|
|
113
|
+
logWriter = null;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
let stateManager = null;
|
|
117
|
+
if (!config.dryRun) {
|
|
118
|
+
stateManager = new StateManager({ verbose: config.verbose });
|
|
119
|
+
}
|
|
120
|
+
const shutdown = new ShutdownManager();
|
|
121
|
+
if (logWriter) {
|
|
122
|
+
const writer = logWriter;
|
|
123
|
+
shutdown.registerCleanup("Finalize run logs", async () => {
|
|
124
|
+
await writer.finalize();
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
// ── Pre-flight state guard ─────────────────────────────────────────
|
|
128
|
+
if (stateManager && !config.dryRun) {
|
|
129
|
+
try {
|
|
130
|
+
const reconcileResult = await reconcileStateAtStartup({
|
|
131
|
+
verbose: config.verbose,
|
|
132
|
+
});
|
|
133
|
+
if (reconcileResult.success && reconcileResult.advanced.length > 0) {
|
|
134
|
+
console.log(chalk.gray(` State reconciled: ${reconcileResult.advanced.map((n) => `#${n}`).join(", ")} → merged`));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
catch (error) {
|
|
138
|
+
logNonFatalWarning(" ! State reconciliation failed, continuing...", error, config.verbose);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
if (stateManager && !config.dryRun && !mergedOptions.force) {
|
|
142
|
+
const activeIssues = [];
|
|
143
|
+
for (const issueNumber of issueNumbers) {
|
|
144
|
+
try {
|
|
145
|
+
const issueState = await stateManager.getIssueState(issueNumber);
|
|
146
|
+
if (issueState &&
|
|
147
|
+
(issueState.status === "ready_for_merge" ||
|
|
148
|
+
issueState.status === "merged")) {
|
|
149
|
+
console.log(chalk.yellow(` ! #${issueNumber}: already ${issueState.status} — skipping (use --force to re-run)`));
|
|
150
|
+
}
|
|
151
|
+
else {
|
|
152
|
+
activeIssues.push(issueNumber);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
catch (error) {
|
|
156
|
+
logNonFatalWarning(` ! State lookup failed for #${issueNumber}, including anyway...`, error, config.verbose);
|
|
157
|
+
activeIssues.push(issueNumber);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
if (activeIssues.length < issueNumbers.length) {
|
|
161
|
+
issueNumbers = activeIssues;
|
|
162
|
+
if (issueNumbers.length === 0) {
|
|
163
|
+
console.log(chalk.yellow(`\n All issues already completed. Use --force to re-run.`));
|
|
164
|
+
shutdown.dispose();
|
|
165
|
+
return {
|
|
166
|
+
results: [],
|
|
167
|
+
logPath: null,
|
|
168
|
+
exitCode: 0,
|
|
169
|
+
worktreeMap: new Map(),
|
|
170
|
+
issueInfoMap: new Map(),
|
|
171
|
+
config,
|
|
172
|
+
mergedOptions,
|
|
173
|
+
logWriter: null,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// ── Issue info + worktree setup ────────────────────────────────────
|
|
179
|
+
const issueInfoMap = new Map();
|
|
180
|
+
for (const issueNumber of issueNumbers) {
|
|
181
|
+
issueInfoMap.set(issueNumber, await getIssueInfo(issueNumber));
|
|
182
|
+
}
|
|
183
|
+
const useWorktreeIsolation = mergedOptions.worktreeIsolation !== false && issueNumbers.length > 0;
|
|
184
|
+
let worktreeMap = new Map();
|
|
185
|
+
if (useWorktreeIsolation && !config.dryRun) {
|
|
186
|
+
const issueData = issueNumbers.map((num) => ({
|
|
187
|
+
number: num,
|
|
188
|
+
title: issueInfoMap.get(num)?.title || `Issue #${num}`,
|
|
189
|
+
}));
|
|
190
|
+
if (mergedOptions.chain) {
|
|
191
|
+
worktreeMap = await ensureWorktreesChain(issueData, config.verbose, manifest.packageManager, baseBranch);
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
worktreeMap = await ensureWorktrees(issueData, config.verbose, manifest.packageManager, baseBranch);
|
|
195
|
+
}
|
|
196
|
+
for (const [issueNum, worktree] of worktreeMap.entries()) {
|
|
197
|
+
if (!worktree.existed) {
|
|
198
|
+
shutdown.registerCleanup(`Cleanup worktree for #${issueNum}`, async () => {
|
|
199
|
+
spawnSync("git", ["worktree", "remove", "--force", worktree.path], {
|
|
200
|
+
stdio: "pipe",
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
// ── Execute ────────────────────────────────────────────────────────
|
|
207
|
+
let results = [];
|
|
208
|
+
try {
|
|
209
|
+
const orchestrator = new RunOrchestrator({
|
|
210
|
+
config,
|
|
211
|
+
options: mergedOptions,
|
|
212
|
+
issueInfoMap,
|
|
213
|
+
worktreeMap,
|
|
214
|
+
services: { logWriter, stateManager, shutdownManager: shutdown },
|
|
215
|
+
packageManager: manifest.packageManager,
|
|
216
|
+
baseBranch,
|
|
217
|
+
onProgress,
|
|
218
|
+
});
|
|
219
|
+
if (resolvedBatches) {
|
|
220
|
+
for (let batchIdx = 0; batchIdx < resolvedBatches.length; batchIdx++) {
|
|
221
|
+
const batch = resolvedBatches[batchIdx];
|
|
222
|
+
console.log(chalk.blue(`\n Batch ${batchIdx + 1}/${resolvedBatches.length}: Issues ${batch.map((n) => `#${n}`).join(", ")}`));
|
|
223
|
+
const batchResults = await orchestrator.execute(batch);
|
|
224
|
+
results.push(...batchResults);
|
|
225
|
+
const batchFailed = batchResults.some((r) => !r.success);
|
|
226
|
+
if (batchFailed && config.sequential) {
|
|
227
|
+
console.log(chalk.yellow(`\n ! Batch ${batchIdx + 1} failed, stopping batch execution`));
|
|
228
|
+
break;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
results = await orchestrator.execute(issueNumbers);
|
|
234
|
+
}
|
|
235
|
+
// ── Finalize logs ──────────────────────────────────────────────
|
|
236
|
+
let logPath = null;
|
|
237
|
+
if (logWriter) {
|
|
238
|
+
logPath = await logWriter.finalize({
|
|
239
|
+
endCommit: getCommitHash(process.cwd()),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
// ── Record metrics ─────────────────────────────────────────────
|
|
243
|
+
if (!config.dryRun && results.length > 0) {
|
|
244
|
+
try {
|
|
245
|
+
await RunOrchestrator.recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers);
|
|
246
|
+
}
|
|
247
|
+
catch (metricsError) {
|
|
248
|
+
logNonFatalWarning(" ! Metrics recording failed, continuing...", metricsError, config.verbose);
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
return {
|
|
252
|
+
results,
|
|
253
|
+
logPath,
|
|
254
|
+
exitCode: results.some((r) => !r.success) && !config.dryRun ? 1 : 0,
|
|
255
|
+
worktreeMap,
|
|
256
|
+
issueInfoMap,
|
|
257
|
+
config,
|
|
258
|
+
mergedOptions,
|
|
259
|
+
logWriter,
|
|
260
|
+
};
|
|
261
|
+
}
|
|
262
|
+
finally {
|
|
263
|
+
shutdown.dispose();
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Execute workflow for the given issue numbers.
|
|
268
|
+
* Returns one IssueResult per issue.
|
|
269
|
+
*/
|
|
270
|
+
async execute(issueNumbers) {
|
|
271
|
+
if (issueNumbers.length === 0) {
|
|
272
|
+
return [];
|
|
273
|
+
}
|
|
274
|
+
const batchCtx = this.buildBatchContext();
|
|
275
|
+
const { config } = this.cfg;
|
|
276
|
+
const options = this.cfg.options;
|
|
277
|
+
// Chain mode implies sequential
|
|
278
|
+
if (options.chain) {
|
|
279
|
+
config.sequential = true;
|
|
280
|
+
}
|
|
281
|
+
if (config.sequential) {
|
|
282
|
+
return this.executeSequential(issueNumbers, batchCtx, options);
|
|
283
|
+
}
|
|
284
|
+
return this.executeParallel(issueNumbers, batchCtx);
|
|
285
|
+
}
|
|
286
|
+
// ── Private helpers ─────────────────────────────────────────────────────
|
|
287
|
+
validate(config) {
|
|
288
|
+
if (!config.config) {
|
|
289
|
+
throw new Error("OrchestratorConfig.config is required");
|
|
290
|
+
}
|
|
291
|
+
if (!config.config.phases ||
|
|
292
|
+
!Array.isArray(config.config.phases) ||
|
|
293
|
+
config.config.phases.length === 0) {
|
|
294
|
+
throw new Error("OrchestratorConfig.config.phases must be a non-empty array");
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
buildBatchContext() {
|
|
298
|
+
const { config, options, issueInfoMap, worktreeMap, services } = this.cfg;
|
|
299
|
+
return {
|
|
300
|
+
config,
|
|
301
|
+
options,
|
|
302
|
+
issueInfoMap,
|
|
303
|
+
worktreeMap,
|
|
304
|
+
logWriter: services.logWriter ?? null,
|
|
305
|
+
stateManager: services.stateManager ?? null,
|
|
306
|
+
shutdownManager: services.shutdownManager,
|
|
307
|
+
packageManager: this.cfg.packageManager,
|
|
308
|
+
baseBranch: this.cfg.baseBranch,
|
|
309
|
+
onProgress: this.cfg.onProgress,
|
|
310
|
+
};
|
|
311
|
+
}
|
|
312
|
+
async executeSequential(issueNumbers, batchCtx, options) {
|
|
313
|
+
const results = [];
|
|
314
|
+
const shutdown = this.cfg.services.shutdownManager;
|
|
315
|
+
for (let i = 0; i < issueNumbers.length; i++) {
|
|
316
|
+
const issueNumber = issueNumbers[i];
|
|
317
|
+
if (shutdown?.shuttingDown) {
|
|
318
|
+
break;
|
|
319
|
+
}
|
|
320
|
+
const result = await this.executeOneIssue({
|
|
321
|
+
issueNumber,
|
|
322
|
+
batchCtx,
|
|
323
|
+
chain: options.chain
|
|
324
|
+
? { enabled: true, isLast: i === issueNumbers.length - 1 }
|
|
325
|
+
: undefined,
|
|
326
|
+
});
|
|
327
|
+
results.push(result);
|
|
328
|
+
if (!result.success) {
|
|
329
|
+
if (options.qaGate && options.chain) {
|
|
330
|
+
const qaFailed = result.phaseResults.some((p) => p.phase === "qa" && !p.success);
|
|
331
|
+
if (qaFailed)
|
|
332
|
+
break;
|
|
333
|
+
}
|
|
334
|
+
break;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
return results;
|
|
338
|
+
}
|
|
339
|
+
async executeParallel(issueNumbers, batchCtx) {
|
|
340
|
+
const limit = pLimit(this.cfg.config.concurrency);
|
|
341
|
+
const shutdown = this.cfg.services.shutdownManager;
|
|
342
|
+
const settledResults = await Promise.allSettled(issueNumbers.map((issueNumber) => limit(async () => {
|
|
343
|
+
if (shutdown?.shuttingDown) {
|
|
344
|
+
return {
|
|
345
|
+
issueNumber,
|
|
346
|
+
success: false,
|
|
347
|
+
phaseResults: [],
|
|
348
|
+
durationSeconds: 0,
|
|
349
|
+
loopTriggered: false,
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
return this.executeOneIssue({
|
|
353
|
+
issueNumber,
|
|
354
|
+
batchCtx: { ...batchCtx, onProgress: this.cfg.onProgress },
|
|
355
|
+
parallelIssueNumber: issueNumber,
|
|
356
|
+
});
|
|
357
|
+
})));
|
|
358
|
+
return settledResults.map((settled, i) => {
|
|
359
|
+
if (settled.status === "fulfilled") {
|
|
360
|
+
return settled.value;
|
|
361
|
+
}
|
|
362
|
+
return {
|
|
363
|
+
issueNumber: issueNumbers[i],
|
|
364
|
+
success: false,
|
|
365
|
+
phaseResults: [],
|
|
366
|
+
durationSeconds: 0,
|
|
367
|
+
loopTriggered: false,
|
|
368
|
+
};
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
async executeOneIssue(args) {
|
|
372
|
+
const { issueNumber, batchCtx, chain, parallelIssueNumber } = args;
|
|
373
|
+
const { config, options, issueInfoMap, worktreeMap, logWriter, stateManager, shutdownManager, packageManager, baseBranch, onProgress, } = batchCtx;
|
|
374
|
+
const issueInfo = issueInfoMap.get(issueNumber) ?? {
|
|
375
|
+
title: `Issue #${issueNumber}`,
|
|
376
|
+
labels: [],
|
|
377
|
+
};
|
|
378
|
+
const worktreeInfo = worktreeMap.get(issueNumber);
|
|
379
|
+
if (logWriter) {
|
|
380
|
+
logWriter.startIssue(issueNumber, issueInfo.title, issueInfo.labels);
|
|
381
|
+
}
|
|
382
|
+
const ctx = {
|
|
383
|
+
issueNumber,
|
|
384
|
+
title: issueInfo.title,
|
|
385
|
+
labels: issueInfo.labels,
|
|
386
|
+
config,
|
|
387
|
+
options,
|
|
388
|
+
services: { logWriter, stateManager, shutdownManager },
|
|
389
|
+
worktree: worktreeInfo
|
|
390
|
+
? { path: worktreeInfo.path, branch: worktreeInfo.branch }
|
|
391
|
+
: undefined,
|
|
392
|
+
chain,
|
|
393
|
+
packageManager,
|
|
394
|
+
baseBranch,
|
|
395
|
+
onProgress,
|
|
396
|
+
};
|
|
397
|
+
const result = await runIssueWithLogging(ctx);
|
|
398
|
+
if (logWriter && result.prNumber && result.prUrl) {
|
|
399
|
+
logWriter.setPRInfo(result.prNumber, result.prUrl, parallelIssueNumber);
|
|
400
|
+
}
|
|
401
|
+
if (logWriter) {
|
|
402
|
+
logWriter.completeIssue(parallelIssueNumber);
|
|
403
|
+
}
|
|
404
|
+
return result;
|
|
405
|
+
}
|
|
406
|
+
static async recordMetrics(config, mergedOptions, results, worktreeMap, issueNumbers) {
|
|
407
|
+
const metricsWriter = new MetricsWriter({ verbose: config.verbose });
|
|
408
|
+
const totalDuration = results.reduce((sum, r) => sum + (r.durationSeconds ?? 0), 0);
|
|
409
|
+
const allPhases = new Set();
|
|
410
|
+
for (const result of results) {
|
|
411
|
+
for (const pr of result.phaseResults) {
|
|
412
|
+
const phase = pr.phase;
|
|
413
|
+
if ([
|
|
414
|
+
"spec",
|
|
415
|
+
"security-review",
|
|
416
|
+
"testgen",
|
|
417
|
+
"exec",
|
|
418
|
+
"test",
|
|
419
|
+
"qa",
|
|
420
|
+
"loop",
|
|
421
|
+
].includes(phase)) {
|
|
422
|
+
allPhases.add(phase);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
let totalFilesChanged = 0;
|
|
427
|
+
let totalLinesAdded = 0;
|
|
428
|
+
let totalQaIterations = 0;
|
|
429
|
+
for (const result of results) {
|
|
430
|
+
const wt = worktreeMap.get(result.issueNumber);
|
|
431
|
+
if (wt?.path) {
|
|
432
|
+
const s = getWorktreeDiffStats(wt.path);
|
|
433
|
+
totalFilesChanged += s.filesChanged;
|
|
434
|
+
totalLinesAdded += s.linesAdded;
|
|
435
|
+
}
|
|
436
|
+
if (result.loopTriggered) {
|
|
437
|
+
totalQaIterations += result.phaseResults.filter((p) => p.phase === "loop").length;
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
const cliFlags = [];
|
|
441
|
+
if (mergedOptions.sequential)
|
|
442
|
+
cliFlags.push("--sequential");
|
|
443
|
+
if (mergedOptions.chain)
|
|
444
|
+
cliFlags.push("--chain");
|
|
445
|
+
if (mergedOptions.qaGate)
|
|
446
|
+
cliFlags.push("--qa-gate");
|
|
447
|
+
if (mergedOptions.qualityLoop)
|
|
448
|
+
cliFlags.push("--quality-loop");
|
|
449
|
+
if (mergedOptions.testgen)
|
|
450
|
+
cliFlags.push("--testgen");
|
|
451
|
+
const tokenUsage = getTokenUsageForRun(undefined, true);
|
|
452
|
+
const passed = results.filter((r) => r.success).length;
|
|
453
|
+
await metricsWriter.recordRun({
|
|
454
|
+
issues: issueNumbers,
|
|
455
|
+
phases: Array.from(allPhases),
|
|
456
|
+
outcome: determineOutcome(passed, results.length),
|
|
457
|
+
duration: totalDuration,
|
|
458
|
+
model: process.env.ANTHROPIC_MODEL ?? "opus",
|
|
459
|
+
flags: cliFlags,
|
|
460
|
+
metrics: {
|
|
461
|
+
tokensUsed: tokenUsage.tokensUsed,
|
|
462
|
+
filesChanged: totalFilesChanged,
|
|
463
|
+
linesAdded: totalLinesAdded,
|
|
464
|
+
acceptanceCriteria: 0,
|
|
465
|
+
qaIterations: totalQaIterations,
|
|
466
|
+
inputTokens: tokenUsage.inputTokens || undefined,
|
|
467
|
+
outputTokens: tokenUsage.outputTokens || undefined,
|
|
468
|
+
cacheTokens: tokenUsage.cacheTokens || undefined,
|
|
469
|
+
},
|
|
470
|
+
});
|
|
471
|
+
if (config.verbose) {
|
|
472
|
+
console.log(chalk.gray(" Metrics recorded to .sequant/metrics.json"));
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
/** Log a non-fatal warning: one-line summary always, detail in verbose. */
|
|
477
|
+
export function logNonFatalWarning(message, error, verbose) {
|
|
478
|
+
console.log(chalk.yellow(message));
|
|
479
|
+
if (verbose) {
|
|
480
|
+
console.log(chalk.gray(` ${error}`));
|
|
481
|
+
}
|
|
482
|
+
}
|