@gobing-ai/spur 0.4.26 → 0.4.28
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/marketplace.json +1 -1
- package/config/rules/boundary/dao-boundary.yaml +2 -0
- package/config/rules/structure/test-location.yaml +2 -0
- package/config/workflows/task-pipeline.yaml +63 -4
- package/package.json +2 -2
- package/plugins/sp/commands/dev-runall.md +6 -5
- package/plugins/sp/plugin.json +1 -1
- package/plugins/sp/skills/code-implementation/SKILL.md +6 -0
- package/plugins/sp/skills/code-testing/SKILL.md +7 -0
- package/plugins/sp/skills/spur-dev/references/cross-cutting.md +2 -2
- package/plugins/sp/skills/spur-dev/references/dev-operations.md +1 -1
- package/plugins/sp/skills/spur-dev/references/execution-batch-report.md +16 -8
- package/plugins/sp/skills/spur-dev/references/execution-batch.md +29 -5
- package/plugins/sp/skills/spur-dev/references/execution-parallel-isolation.md +2 -2
- package/plugins/sp/skills/spur-dev/references/execution-workflow.md +11 -0
- package/plugins/sp/skills/spur-dev/references/execution-worktree-landing.md +10 -6
- package/plugins/sp/skills/spur-dev/references/execution-worktree-setup.md +15 -0
- package/plugins/sp/skills/spur-dev/references/flag-glossary.md +9 -0
- package/spur.js +814 -225
package/spur.js
CHANGED
|
@@ -45392,6 +45392,32 @@ var init_forensic_query = __esm(() => {
|
|
|
45392
45392
|
END`;
|
|
45393
45393
|
});
|
|
45394
45394
|
|
|
45395
|
+
// ../../packages/domain/src/analytics/selector-time-range.ts
|
|
45396
|
+
function buildTimeRangeClauses(range, target) {
|
|
45397
|
+
if (!COLUMN.test(target.column))
|
|
45398
|
+
throw new Error(`invalid time-range column: ${target.column}`);
|
|
45399
|
+
const bound = BOUND_SQL[target.grain];
|
|
45400
|
+
const clauses = [];
|
|
45401
|
+
const params = [];
|
|
45402
|
+
if (range.since !== null) {
|
|
45403
|
+
clauses.push(`${target.column} >= ${bound}`);
|
|
45404
|
+
params.push(range.since);
|
|
45405
|
+
}
|
|
45406
|
+
if (range.until !== null) {
|
|
45407
|
+
clauses.push(`${target.column} <= ${bound}`);
|
|
45408
|
+
params.push(range.until);
|
|
45409
|
+
}
|
|
45410
|
+
return { clauses, params };
|
|
45411
|
+
}
|
|
45412
|
+
var BOUND_SQL, COLUMN;
|
|
45413
|
+
var init_selector_time_range = __esm(() => {
|
|
45414
|
+
BOUND_SQL = {
|
|
45415
|
+
day: "DATE(?)",
|
|
45416
|
+
timestamp: "strftime('%Y-%m-%dT%H:%M:%SZ', ?)"
|
|
45417
|
+
};
|
|
45418
|
+
COLUMN = /^[A-Za-z_][A-Za-z0-9_]*(\.[A-Za-z_][A-Za-z0-9_]*)?$/;
|
|
45419
|
+
});
|
|
45420
|
+
|
|
45395
45421
|
// ../../packages/domain/src/analytics/history-board-marts.ts
|
|
45396
45422
|
function resolveSummaryReadPath(input) {
|
|
45397
45423
|
if (!input.fresh)
|
|
@@ -45544,17 +45570,7 @@ async function deriveDimensionMarts(db, days) {
|
|
|
45544
45570
|
await db.batch(deriveDimensionMartsOps(days));
|
|
45545
45571
|
}
|
|
45546
45572
|
function dayClauses(sel) {
|
|
45547
|
-
|
|
45548
|
-
const params = [];
|
|
45549
|
-
if (sel.since !== null) {
|
|
45550
|
-
clauses.push("d.day >= DATE(?)");
|
|
45551
|
-
params.push(sel.since);
|
|
45552
|
-
}
|
|
45553
|
-
if (sel.until !== null) {
|
|
45554
|
-
clauses.push("d.day <= DATE(?)");
|
|
45555
|
-
params.push(sel.until);
|
|
45556
|
-
}
|
|
45557
|
-
return { clauses, params };
|
|
45573
|
+
return buildTimeRangeClauses(sel, { column: "d.day", grain: "day" });
|
|
45558
45574
|
}
|
|
45559
45575
|
function dimensionKeyClauses(sel, dimension) {
|
|
45560
45576
|
let values = null;
|
|
@@ -45771,6 +45787,7 @@ async function historyBoardSummaryFromMart(db, sel, dimension) {
|
|
|
45771
45787
|
}
|
|
45772
45788
|
var MART_DIMENSIONS, MART_MIN_RANGE_DAYS = 7, MEASURE_COLS = "messages, tool_calls, skill_calls, fresh_input_tokens, cache_read_tokens, cache_write_tokens, output_tokens, duration_ms, duration_samples", MART_INSERT_COLS, MSG_DAY_PRED = "(m.bucket_start >= ? AND m.bucket_start < DATE(?, '+1 day'))", TOOL_DAY_PRED = "(t.bucket_start >= ? AND t.bucket_start < DATE(?, '+1 day'))", SKILL_DAY_PRED = "(s.bucket_start >= ? AND s.bucket_start < DATE(?, '+1 day'))", KPI_RANGE_KEY = "trend";
|
|
45773
45789
|
var init_history_board_marts = __esm(() => {
|
|
45790
|
+
init_selector_time_range();
|
|
45774
45791
|
MART_DIMENSIONS = ["model", "source", "tool", "skill"];
|
|
45775
45792
|
MART_INSERT_COLS = `${MEASURE_COLS}, errors`;
|
|
45776
45793
|
});
|
|
@@ -46201,16 +46218,10 @@ function appendRankedSteps(operations, kind, rows) {
|
|
|
46201
46218
|
});
|
|
46202
46219
|
}
|
|
46203
46220
|
function buildRollupWhere(sel, alias, options) {
|
|
46204
|
-
const clauses =
|
|
46205
|
-
|
|
46206
|
-
|
|
46207
|
-
|
|
46208
|
-
params.push(sel.since);
|
|
46209
|
-
}
|
|
46210
|
-
if (sel.until !== null) {
|
|
46211
|
-
clauses.push(`${alias}.${options.timestamp} <= ${options.dateOnly ? "DATE(?)" : "strftime('%Y-%m-%dT%H:%M:%SZ', ?)"}`);
|
|
46212
|
-
params.push(sel.until);
|
|
46213
|
-
}
|
|
46221
|
+
const { clauses, params } = buildTimeRangeClauses(sel, {
|
|
46222
|
+
column: `${alias}.${options.timestamp}`,
|
|
46223
|
+
grain: options.dateOnly ? "day" : "timestamp"
|
|
46224
|
+
});
|
|
46214
46225
|
if (sel.sources !== null && sel.sources.length > 0) {
|
|
46215
46226
|
clauses.push(`${alias}.source IN (${sel.sources.map(() => "?").join(", ")})`);
|
|
46216
46227
|
params.push(...sel.sources);
|
|
@@ -47363,6 +47374,7 @@ var init_history_board_rollup = __esm(() => {
|
|
|
47363
47374
|
init_forensic_query();
|
|
47364
47375
|
init_history_board_marts();
|
|
47365
47376
|
init_rollup_watermark();
|
|
47377
|
+
init_selector_time_range();
|
|
47366
47378
|
init_tool_alias();
|
|
47367
47379
|
init_tool_name_sql();
|
|
47368
47380
|
init_tool_name_sql();
|
|
@@ -55594,6 +55606,38 @@ async function readWorkflowStateVisitRows(db, workflows, opts = {}) {
|
|
|
55594
55606
|
}
|
|
55595
55607
|
return rows;
|
|
55596
55608
|
}
|
|
55609
|
+
async function readWorkflowHostPauseRows(db, workflows, opts = {}) {
|
|
55610
|
+
const sql = `SELECT json_extract(r.metadata_json, '$.hostPauses') AS hostPausesJson
|
|
55611
|
+
FROM runs r
|
|
55612
|
+
WHERE r.workflow_name = ?
|
|
55613
|
+
AND ${perStatePopulation(opts.sinceMs)}
|
|
55614
|
+
AND json_extract(r.metadata_json, '$.hostPauses') IS NOT NULL`;
|
|
55615
|
+
const rows = [];
|
|
55616
|
+
for (const workflow of workflows) {
|
|
55617
|
+
const found = await db.queryAll(sql, workflow, ...TERMINAL_RUN_STATUSES, ...opts.sinceMs !== undefined ? [opts.sinceMs] : []);
|
|
55618
|
+
for (const r of found)
|
|
55619
|
+
rows.push({ workflow, hostPausesJson: r.hostPausesJson });
|
|
55620
|
+
}
|
|
55621
|
+
return rows;
|
|
55622
|
+
}
|
|
55623
|
+
async function readWorkflowOpenPauseRows(db, workflows) {
|
|
55624
|
+
const sql = `SELECT r.id AS runId, r.updated_at AS updatedAt,
|
|
55625
|
+
(SELECT a.result_json FROM action_runs a
|
|
55626
|
+
WHERE a.run_id = r.id AND (a.status = 'paused' OR a.result_json LIKE '%"pause":true%')
|
|
55627
|
+
ORDER BY a.created_at DESC, a.rowid DESC LIMIT 1) AS pausedResultJson
|
|
55628
|
+
FROM runs r
|
|
55629
|
+
WHERE r.workflow_name = ?
|
|
55630
|
+
AND r.status IN ('paused', 'interrupted')
|
|
55631
|
+
AND json_extract(r.metadata_json, '$.dryRun') IS NOT 1
|
|
55632
|
+
ORDER BY r.id`;
|
|
55633
|
+
const rows = [];
|
|
55634
|
+
for (const workflow of workflows) {
|
|
55635
|
+
const found = await db.queryAll(sql, workflow);
|
|
55636
|
+
for (const r of found)
|
|
55637
|
+
rows.push({ workflow, ...r });
|
|
55638
|
+
}
|
|
55639
|
+
return rows;
|
|
55640
|
+
}
|
|
55597
55641
|
var TERMINAL_RUN_STATUSES;
|
|
55598
55642
|
var init_workflow_measurements = __esm(() => {
|
|
55599
55643
|
TERMINAL_RUN_STATUSES = ["done", "failed", "cancelled"];
|
|
@@ -58105,7 +58149,7 @@ var init_run_dao = __esm(() => {
|
|
|
58105
58149
|
});
|
|
58106
58150
|
}
|
|
58107
58151
|
traceRows(filter) {
|
|
58108
|
-
return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json
|
|
58152
|
+
return this.adapter.queryAll(`SELECT id, workflow_name, mode, status, agent, started_at, completed_at, metadata_json, updated_at
|
|
58109
58153
|
FROM runs
|
|
58110
58154
|
WHERE (?1 IS NULL OR workflow_name = ?1)
|
|
58111
58155
|
AND (?2 IS NULL OR status = ?2)
|
|
@@ -58119,7 +58163,7 @@ var init_run_dao = __esm(() => {
|
|
|
58119
58163
|
LIMIT ?6`, filter.workflow ?? null, filter.status ?? null, filter.since ?? null, filter.before?.started_at ?? null, filter.before?.id ?? null, filter.limit);
|
|
58120
58164
|
}
|
|
58121
58165
|
traceRowById(runId) {
|
|
58122
|
-
return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, terminal_reason, metadata_json FROM runs WHERE id = ?", runId);
|
|
58166
|
+
return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, updated_at, terminal_reason, metadata_json, pid FROM runs WHERE id = ?", runId);
|
|
58123
58167
|
}
|
|
58124
58168
|
mergeMetadata(runId, patch) {
|
|
58125
58169
|
return this.adapter.run(`UPDATE runs
|
|
@@ -58132,6 +58176,17 @@ var init_run_dao = __esm(() => {
|
|
|
58132
58176
|
stampMetadata(runId, metadata) {
|
|
58133
58177
|
return this.mergeMetadata(runId, metadata);
|
|
58134
58178
|
}
|
|
58179
|
+
mergeMetadataIfStatus(runId, patch, statuses) {
|
|
58180
|
+
if (statuses.length === 0)
|
|
58181
|
+
return Promise.resolve();
|
|
58182
|
+
const placeholders = statuses.map((_, i) => `?${i + 3}`).join(", ");
|
|
58183
|
+
return this.adapter.run(`UPDATE runs
|
|
58184
|
+
SET metadata_json = json_patch(
|
|
58185
|
+
COALESCE(NULLIF(metadata_json, ''), '{}'),
|
|
58186
|
+
?1
|
|
58187
|
+
)
|
|
58188
|
+
WHERE id = ?2 AND status IN (${placeholders})`, JSON.stringify(patch), runId, ...statuses);
|
|
58189
|
+
}
|
|
58135
58190
|
stampRunIdentity(runId, definitionDigest, workflowVersion, source) {
|
|
58136
58191
|
const sourceSet = source === undefined ? "" : `,
|
|
58137
58192
|
'$.definitionSource.path', ?4,
|
|
@@ -72279,6 +72334,64 @@ var init_dist11 = __esm(() => {
|
|
|
72279
72334
|
init_types3();
|
|
72280
72335
|
});
|
|
72281
72336
|
|
|
72337
|
+
// ../../packages/domain/src/migration-sql.generated.ts
|
|
72338
|
+
var HISTORY_DIMENSION_MARTS_SCHEMA_SQL = `-- 0743: Dimension marts (task 0743).
|
|
72339
|
+
-- Two day-grain mart tables carrying the ADR-106 nine-measure additive vector for the
|
|
72340
|
+
-- four materialized Summary dimensions. Every measure column is nullable for exactly one
|
|
72341
|
+
-- reason: ADR-106 records a measure that is not well defined at a dimension as NULL, never
|
|
72342
|
+
-- as a zero that would be indistinguishable from a measured absence of activity.
|
|
72343
|
+
--
|
|
72344
|
+
-- - tool dimension: skill_calls is not applicable -> NULL
|
|
72345
|
+
-- - skill dimension: tool_calls is not applicable -> NULL
|
|
72346
|
+
-- - source dimension: duration_ms / duration_samples are not applicable -> NULL
|
|
72347
|
+
--
|
|
72348
|
+
-- history_board_kpi_window stores the current and previous aggregate window KPIs for a
|
|
72349
|
+
-- named range window. All statements are idempotent (CREATE TABLE IF NOT EXISTS).
|
|
72350
|
+
|
|
72351
|
+
CREATE TABLE IF NOT EXISTS history_board_dimension_daily (
|
|
72352
|
+
dimension TEXT NOT NULL,
|
|
72353
|
+
dimension_key TEXT NOT NULL,
|
|
72354
|
+
day TEXT NOT NULL,
|
|
72355
|
+
-- ADR-106 nine-member additive measure vector. NULLABLE, never a NOT NULL column with a
|
|
72356
|
+
-- zero default: a not-applicable measure is stored as NULL.
|
|
72357
|
+
messages INTEGER,
|
|
72358
|
+
tool_calls INTEGER,
|
|
72359
|
+
skill_calls INTEGER,
|
|
72360
|
+
fresh_input_tokens REAL,
|
|
72361
|
+
cache_read_tokens REAL,
|
|
72362
|
+
cache_write_tokens REAL,
|
|
72363
|
+
output_tokens REAL,
|
|
72364
|
+
duration_ms INTEGER,
|
|
72365
|
+
duration_samples INTEGER,
|
|
72366
|
+
-- Per-(dimension, key, day) tool-error count (not part of the ADR-106 additive vector but
|
|
72367
|
+
-- needed by the top-tools error-rate projection. NULLABLE like every other scalar.
|
|
72368
|
+
errors INTEGER,
|
|
72369
|
+
PRIMARY KEY (dimension, dimension_key, day)
|
|
72370
|
+
);
|
|
72371
|
+
|
|
72372
|
+
CREATE TABLE IF NOT EXISTS history_board_kpi_window (
|
|
72373
|
+
range_key TEXT NOT NULL,
|
|
72374
|
+
window_kind TEXT NOT NULL CHECK (window_kind IN ('current', 'previous')),
|
|
72375
|
+
-- ADR-106 nine-member additive measure vector, plus the non-additive totals the KPI
|
|
72376
|
+
-- projection needs (sessions and tool_errors are counts, never summed as means).
|
|
72377
|
+
messages INTEGER,
|
|
72378
|
+
tool_calls INTEGER,
|
|
72379
|
+
skill_calls INTEGER,
|
|
72380
|
+
fresh_input_tokens REAL,
|
|
72381
|
+
cache_read_tokens REAL,
|
|
72382
|
+
cache_write_tokens REAL,
|
|
72383
|
+
output_tokens REAL,
|
|
72384
|
+
duration_ms INTEGER,
|
|
72385
|
+
duration_samples INTEGER,
|
|
72386
|
+
sessions INTEGER,
|
|
72387
|
+
tool_errors INTEGER,
|
|
72388
|
+
PRIMARY KEY (range_key, window_kind)
|
|
72389
|
+
);
|
|
72390
|
+
|
|
72391
|
+
CREATE INDEX IF NOT EXISTS idx_history_board_dimension_daily_day
|
|
72392
|
+
ON history_board_dimension_daily (dimension, day);
|
|
72393
|
+
`;
|
|
72394
|
+
|
|
72282
72395
|
// ../../packages/domain/src/schema/inbox-messages.ts
|
|
72283
72396
|
var inboxMessagesTable, inboxMessages4, INBOX_MESSAGES_TABLE_SQL;
|
|
72284
72397
|
var init_inbox_messages3 = __esm(() => {
|
|
@@ -73224,49 +73337,6 @@ CREATE TABLE IF NOT EXISTS history_board_rollup_bucket (
|
|
|
73224
73337
|
|
|
73225
73338
|
CREATE INDEX IF NOT EXISTS idx_history_message_imported_at
|
|
73226
73339
|
ON history_message (imported_at);
|
|
73227
|
-
`, HISTORY_DIMENSION_MARTS_SCHEMA_SQL = `
|
|
73228
|
-
CREATE TABLE IF NOT EXISTS history_board_dimension_daily (
|
|
73229
|
-
dimension TEXT NOT NULL,
|
|
73230
|
-
dimension_key TEXT NOT NULL,
|
|
73231
|
-
day TEXT NOT NULL,
|
|
73232
|
-
-- ADR-106 nine-member additive measure vector. NULLABLE, never a NOT NULL column with a
|
|
73233
|
-
-- zero default: a not-applicable measure is stored as NULL.
|
|
73234
|
-
messages INTEGER,
|
|
73235
|
-
tool_calls INTEGER,
|
|
73236
|
-
skill_calls INTEGER,
|
|
73237
|
-
fresh_input_tokens REAL,
|
|
73238
|
-
cache_read_tokens REAL,
|
|
73239
|
-
cache_write_tokens REAL,
|
|
73240
|
-
output_tokens REAL,
|
|
73241
|
-
duration_ms INTEGER,
|
|
73242
|
-
duration_samples INTEGER,
|
|
73243
|
-
-- Per-(dimension, key, day) tool-error count (not part of the ADR-106 additive vector but
|
|
73244
|
-
-- needed by the top-tools error-rate projection. NULLABLE like every other scalar.
|
|
73245
|
-
errors INTEGER,
|
|
73246
|
-
PRIMARY KEY (dimension, dimension_key, day)
|
|
73247
|
-
);
|
|
73248
|
-
|
|
73249
|
-
CREATE TABLE IF NOT EXISTS history_board_kpi_window (
|
|
73250
|
-
range_key TEXT NOT NULL,
|
|
73251
|
-
window_kind TEXT NOT NULL CHECK (window_kind IN ('current', 'previous')),
|
|
73252
|
-
-- ADR-106 nine-member additive measure vector, plus the non-additive totals the KPI
|
|
73253
|
-
-- projection needs (sessions and tool_errors are counts, never summed as means).
|
|
73254
|
-
messages INTEGER,
|
|
73255
|
-
tool_calls INTEGER,
|
|
73256
|
-
skill_calls INTEGER,
|
|
73257
|
-
fresh_input_tokens REAL,
|
|
73258
|
-
cache_read_tokens REAL,
|
|
73259
|
-
cache_write_tokens REAL,
|
|
73260
|
-
output_tokens REAL,
|
|
73261
|
-
duration_ms INTEGER,
|
|
73262
|
-
duration_samples INTEGER,
|
|
73263
|
-
sessions INTEGER,
|
|
73264
|
-
tool_errors INTEGER,
|
|
73265
|
-
PRIMARY KEY (range_key, window_kind)
|
|
73266
|
-
);
|
|
73267
|
-
|
|
73268
|
-
CREATE INDEX IF NOT EXISTS idx_history_board_dimension_daily_day
|
|
73269
|
-
ON history_board_dimension_daily (dimension, day);
|
|
73270
73340
|
`, RETENTION_COMPACTION_META_SCHEMA_SQL = `
|
|
73271
73341
|
CREATE TABLE IF NOT EXISTS spur_retention_meta (
|
|
73272
73342
|
kind TEXT NOT NULL,
|
|
@@ -100265,6 +100335,31 @@ import {
|
|
|
100265
100335
|
writeSync as writeSync2
|
|
100266
100336
|
} from "fs";
|
|
100267
100337
|
import { dirname as dirname8, join as join18 } from "path";
|
|
100338
|
+
function writeStateFile(statePath, state) {
|
|
100339
|
+
const temp = `${statePath}.tmp`;
|
|
100340
|
+
try {
|
|
100341
|
+
writeFileSync3(temp, `${JSON.stringify(state, null, 2)}
|
|
100342
|
+
`);
|
|
100343
|
+
renameSync3(temp, statePath);
|
|
100344
|
+
} catch {
|
|
100345
|
+
try {
|
|
100346
|
+
unlinkSync2(temp);
|
|
100347
|
+
} catch {}
|
|
100348
|
+
}
|
|
100349
|
+
}
|
|
100350
|
+
function projectRunStateStatus(dir, runId, status, at) {
|
|
100351
|
+
const statePath = join18(dir, `${runId}.state.json`);
|
|
100352
|
+
let prior;
|
|
100353
|
+
try {
|
|
100354
|
+
const parsed = JSON.parse(readFileSync5(statePath, "utf8"));
|
|
100355
|
+
if (parsed === null || typeof parsed !== "object" || Array.isArray(parsed))
|
|
100356
|
+
return;
|
|
100357
|
+
prior = parsed;
|
|
100358
|
+
} catch {
|
|
100359
|
+
return;
|
|
100360
|
+
}
|
|
100361
|
+
writeStateFile(statePath, { ...prior, status, updatedAt: at, finalizedAt: at });
|
|
100362
|
+
}
|
|
100268
100363
|
|
|
100269
100364
|
class WorkflowRunLogSink {
|
|
100270
100365
|
filePath;
|
|
@@ -100404,16 +100499,7 @@ class WorkflowRunLogSink {
|
|
|
100404
100499
|
...this.lineage?.startState !== undefined ? { startState: this.lineage.startState } : {},
|
|
100405
100500
|
...this.lineage?.continuedFrom !== undefined ? { continuedFrom: this.lineage.continuedFrom } : {}
|
|
100406
100501
|
};
|
|
100407
|
-
|
|
100408
|
-
try {
|
|
100409
|
-
writeFileSync3(temp, `${JSON.stringify(state, null, 2)}
|
|
100410
|
-
`);
|
|
100411
|
-
renameSync3(temp, this.statePath);
|
|
100412
|
-
} catch {
|
|
100413
|
-
try {
|
|
100414
|
-
unlinkSync2(temp);
|
|
100415
|
-
} catch {}
|
|
100416
|
-
}
|
|
100502
|
+
writeStateFile(this.statePath, state);
|
|
100417
100503
|
}
|
|
100418
100504
|
onSteering(ack) {
|
|
100419
100505
|
const note = ack.note === undefined ? "" : ` \xB7 ${bounded3(ack.note, MAX_STEERING_NOTE_CHARS)}`;
|
|
@@ -104841,7 +104927,16 @@ class AgentRunActionRunner {
|
|
|
104841
104927
|
error: `agent.run: work item for '${context.stateOrNodeId}' could not be written: ${error.message}`
|
|
104842
104928
|
};
|
|
104843
104929
|
}
|
|
104844
|
-
return {
|
|
104930
|
+
return {
|
|
104931
|
+
ok: true,
|
|
104932
|
+
pause: true,
|
|
104933
|
+
data: {
|
|
104934
|
+
surface: "host",
|
|
104935
|
+
workItem,
|
|
104936
|
+
reason: "host-work-item",
|
|
104937
|
+
...invocation.timeoutMs !== undefined ? { timeoutMs: invocation.timeoutMs } : {}
|
|
104938
|
+
}
|
|
104939
|
+
};
|
|
104845
104940
|
}
|
|
104846
104941
|
async execute(options, context, execution) {
|
|
104847
104942
|
const startedAt = new Date().toISOString();
|
|
@@ -121325,6 +121420,102 @@ var init_process_inventory_service = __esm(() => {
|
|
|
121325
121420
|
init_process_inspector();
|
|
121326
121421
|
});
|
|
121327
121422
|
|
|
121423
|
+
// ../../packages/app/src/workflow/host-pause.ts
|
|
121424
|
+
function readHostPauses(metadata) {
|
|
121425
|
+
if (metadata === null || typeof metadata !== "object")
|
|
121426
|
+
return [];
|
|
121427
|
+
const raw = metadata[HOST_PAUSES_KEY];
|
|
121428
|
+
if (!Array.isArray(raw))
|
|
121429
|
+
return [];
|
|
121430
|
+
const out = [];
|
|
121431
|
+
for (const entry of raw) {
|
|
121432
|
+
if (entry === null || typeof entry !== "object")
|
|
121433
|
+
continue;
|
|
121434
|
+
const row = entry;
|
|
121435
|
+
const { node, action, pausedAt, resumedAt, hostDurationMs } = row;
|
|
121436
|
+
if (typeof node !== "string" || typeof pausedAt !== "string" || typeof resumedAt !== "string")
|
|
121437
|
+
continue;
|
|
121438
|
+
if (typeof hostDurationMs !== "number" || !Number.isFinite(hostDurationMs))
|
|
121439
|
+
continue;
|
|
121440
|
+
out.push({
|
|
121441
|
+
node,
|
|
121442
|
+
action: typeof action === "string" ? action : "agent.run",
|
|
121443
|
+
pausedAt,
|
|
121444
|
+
resumedAt,
|
|
121445
|
+
hostDurationMs,
|
|
121446
|
+
...typeof row.executor === "string" ? { executor: row.executor } : {}
|
|
121447
|
+
});
|
|
121448
|
+
}
|
|
121449
|
+
return out;
|
|
121450
|
+
}
|
|
121451
|
+
function appendHostPause(metadata, pause) {
|
|
121452
|
+
return [...readHostPauses(metadata), pause];
|
|
121453
|
+
}
|
|
121454
|
+
function hostPauseMsByNode(pauses) {
|
|
121455
|
+
const totals = new Map;
|
|
121456
|
+
for (const pause of pauses)
|
|
121457
|
+
totals.set(pause.node, (totals.get(pause.node) ?? 0) + pause.hostDurationMs);
|
|
121458
|
+
return totals;
|
|
121459
|
+
}
|
|
121460
|
+
function hostDurationMs(pausedAt, resumedAt) {
|
|
121461
|
+
const start = Date.parse(pausedAt);
|
|
121462
|
+
const end = Date.parse(resumedAt);
|
|
121463
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end < start)
|
|
121464
|
+
return null;
|
|
121465
|
+
return end - start;
|
|
121466
|
+
}
|
|
121467
|
+
function isDegenerateReceiptInterval(startedAt, finishedAt) {
|
|
121468
|
+
if (startedAt === undefined || finishedAt === undefined)
|
|
121469
|
+
return true;
|
|
121470
|
+
const start = Date.parse(startedAt);
|
|
121471
|
+
const end = Date.parse(finishedAt);
|
|
121472
|
+
if (!Number.isFinite(start) || !Number.isFinite(end))
|
|
121473
|
+
return true;
|
|
121474
|
+
return end <= start;
|
|
121475
|
+
}
|
|
121476
|
+
function resolveStallThresholdMs(nodeTimeoutMs, readEnvVar = getEnvVar) {
|
|
121477
|
+
if (nodeTimeoutMs !== undefined && Number.isFinite(nodeTimeoutMs) && nodeTimeoutMs > 0)
|
|
121478
|
+
return nodeTimeoutMs;
|
|
121479
|
+
const raw = readEnvVar(STALL_THRESHOLD_MS_ENV);
|
|
121480
|
+
if (raw !== undefined) {
|
|
121481
|
+
const parsed = Number(raw);
|
|
121482
|
+
if (Number.isFinite(parsed) && parsed > 0)
|
|
121483
|
+
return Math.trunc(parsed);
|
|
121484
|
+
}
|
|
121485
|
+
return STALL_THRESHOLD_MS_DEFAULT;
|
|
121486
|
+
}
|
|
121487
|
+
function openPauseMs(pausedAt, nowMs) {
|
|
121488
|
+
if (pausedAt === null || pausedAt === undefined)
|
|
121489
|
+
return null;
|
|
121490
|
+
const start = typeof pausedAt === "number" ? pausedAt : /^\d+$/.test(pausedAt) ? Number(pausedAt) : Date.parse(pausedAt);
|
|
121491
|
+
if (!Number.isFinite(start))
|
|
121492
|
+
return null;
|
|
121493
|
+
return Math.max(0, nowMs - start);
|
|
121494
|
+
}
|
|
121495
|
+
function pausedNodeBudgetMs(rows) {
|
|
121496
|
+
for (let i = rows.length - 1;i >= 0; i--) {
|
|
121497
|
+
const row = rows[i];
|
|
121498
|
+
if (row === undefined || row.result_json === null)
|
|
121499
|
+
continue;
|
|
121500
|
+
if (row.status !== "paused" && !/"pause"\s*:\s*true/.test(row.result_json))
|
|
121501
|
+
continue;
|
|
121502
|
+
try {
|
|
121503
|
+
const budget = JSON.parse(row.result_json)?.data?.timeoutMs;
|
|
121504
|
+
return typeof budget === "number" && Number.isFinite(budget) && budget > 0 ? budget : undefined;
|
|
121505
|
+
} catch {
|
|
121506
|
+
return;
|
|
121507
|
+
}
|
|
121508
|
+
}
|
|
121509
|
+
return;
|
|
121510
|
+
}
|
|
121511
|
+
function stalledLabel(runId, pauseMs) {
|
|
121512
|
+
return `stalled \u2014 paused ${Math.round(pauseMs / 1000)}s; resume: spur workflow continue ${runId}`;
|
|
121513
|
+
}
|
|
121514
|
+
var HOST_PAUSES_KEY = "hostPauses", STALL_THRESHOLD_MS_DEFAULT = 1800000, STALL_THRESHOLD_MS_ENV = "SPUR_WORKFLOW_STALL_MS";
|
|
121515
|
+
var init_host_pause = __esm(() => {
|
|
121516
|
+
init_src();
|
|
121517
|
+
});
|
|
121518
|
+
|
|
121328
121519
|
// ../../packages/app/src/workflow/run-measurements.ts
|
|
121329
121520
|
function median(values) {
|
|
121330
121521
|
if (values.length === 0)
|
|
@@ -121425,6 +121616,18 @@ async function readStateMetrics(db, workflows, opts = {}) {
|
|
|
121425
121616
|
row.visits += t.visitsInRun;
|
|
121426
121617
|
row.retries += Math.max(0, t.visitsInRun - 1);
|
|
121427
121618
|
}
|
|
121619
|
+
for (const h of await readWorkflowHostPauseRows(db, workflows, opts)) {
|
|
121620
|
+
let parsed;
|
|
121621
|
+
try {
|
|
121622
|
+
parsed = JSON.parse(h.hostPausesJson);
|
|
121623
|
+
} catch {
|
|
121624
|
+
continue;
|
|
121625
|
+
}
|
|
121626
|
+
for (const [node, ms] of hostPauseMsByNode(readHostPauses({ hostPauses: parsed }))) {
|
|
121627
|
+
const row = rowFor(h.workflow, node);
|
|
121628
|
+
row.hostMs = (row.hostMs ?? 0) + ms;
|
|
121629
|
+
}
|
|
121630
|
+
}
|
|
121428
121631
|
const rows = [];
|
|
121429
121632
|
for (const [workflow, states] of byWorkflow) {
|
|
121430
121633
|
for (const [state, row] of states) {
|
|
@@ -121435,14 +121638,27 @@ async function readStateMetrics(db, workflows, opts = {}) {
|
|
|
121435
121638
|
agentRunCount: row.agentRunCount,
|
|
121436
121639
|
wallMsP50: nearestRankPercentile(row.wallSamples, 50),
|
|
121437
121640
|
wallMsP90: nearestRankPercentile(row.wallSamples, 90),
|
|
121438
|
-
retries: row.retries
|
|
121641
|
+
retries: row.retries,
|
|
121642
|
+
...row.hostMs !== undefined ? { hostMs: row.hostMs } : {}
|
|
121439
121643
|
});
|
|
121440
121644
|
}
|
|
121441
121645
|
}
|
|
121442
121646
|
return rows.sort((a, b) => a.workflow === b.workflow ? a.state < b.state ? -1 : a.state > b.state ? 1 : 0 : a.workflow < b.workflow ? -1 : 1);
|
|
121443
121647
|
}
|
|
121648
|
+
async function readStalledRuns(db, workflows, nowMs = Date.now()) {
|
|
121649
|
+
const stalled = [];
|
|
121650
|
+
for (const row of await readWorkflowOpenPauseRows(db, workflows)) {
|
|
121651
|
+
const ageMs = openPauseMs(row.updatedAt, nowMs);
|
|
121652
|
+
const budget = pausedNodeBudgetMs([{ status: "paused", result_json: row.pausedResultJson }]);
|
|
121653
|
+
if (ageMs !== null && ageMs > resolveStallThresholdMs(budget)) {
|
|
121654
|
+
stalled.push({ workflow: row.workflow, runId: row.runId, stalledMs: ageMs });
|
|
121655
|
+
}
|
|
121656
|
+
}
|
|
121657
|
+
return stalled;
|
|
121658
|
+
}
|
|
121444
121659
|
var init_run_measurements = __esm(() => {
|
|
121445
121660
|
init_src2();
|
|
121661
|
+
init_host_pause();
|
|
121446
121662
|
init_terminal_reason();
|
|
121447
121663
|
});
|
|
121448
121664
|
|
|
@@ -130909,6 +131125,174 @@ var init_token_ledger_watcher = __esm(() => {
|
|
|
130909
131125
|
init_token_ledger_service();
|
|
130910
131126
|
});
|
|
130911
131127
|
|
|
131128
|
+
// ../../packages/app/src/services/sdlc-actions.ts
|
|
131129
|
+
function isReservedAction(name) {
|
|
131130
|
+
return RESERVED_ACTIONS.includes(name);
|
|
131131
|
+
}
|
|
131132
|
+
function resolveAction(builder, name) {
|
|
131133
|
+
const reserved = isReservedAction(name);
|
|
131134
|
+
const declared = builder?.actions?.[name];
|
|
131135
|
+
if (declared !== undefined) {
|
|
131136
|
+
return Array.isArray(declared) ? { name, reserved, source: "config", kind: "composite", steps: [...declared] } : { name, reserved, source: "config", kind: "command", command: declared };
|
|
131137
|
+
}
|
|
131138
|
+
if ((builder?.preset ?? "bun") === "bun") {
|
|
131139
|
+
if (reserved)
|
|
131140
|
+
return { name, reserved, source: "preset", kind: "command", command: BUN_PRESET[name] };
|
|
131141
|
+
} else {
|
|
131142
|
+
const command = reserved ? STACK_NEUTRAL_DEFAULTS[name] : undefined;
|
|
131143
|
+
if (command !== undefined)
|
|
131144
|
+
return { name, reserved, source: "default", kind: "command", command };
|
|
131145
|
+
}
|
|
131146
|
+
return { name, reserved, source: "unbound", kind: "unbound" };
|
|
131147
|
+
}
|
|
131148
|
+
function listActions(builder) {
|
|
131149
|
+
const custom = Object.keys(builder?.actions ?? {}).filter((name) => !isReservedAction(name)).sort();
|
|
131150
|
+
return [...RESERVED_ACTIONS, ...custom].map((name) => resolveAction(builder, name));
|
|
131151
|
+
}
|
|
131152
|
+
function renderScoped(command, values) {
|
|
131153
|
+
let rendered = command;
|
|
131154
|
+
for (const [placeholder, items] of [
|
|
131155
|
+
["{files}", values.files],
|
|
131156
|
+
["{paths}", values.paths]
|
|
131157
|
+
]) {
|
|
131158
|
+
if (!rendered.includes(placeholder))
|
|
131159
|
+
continue;
|
|
131160
|
+
if (items === undefined)
|
|
131161
|
+
throw new Error(`renderScoped: ${placeholder} has no value`);
|
|
131162
|
+
if (items.length === 0)
|
|
131163
|
+
throw new Error(`renderScoped: ${placeholder} has an empty list`);
|
|
131164
|
+
rendered = rendered.replaceAll(placeholder, () => items.map(shQuote2).join(" "));
|
|
131165
|
+
}
|
|
131166
|
+
if (rendered.includes("{workspace}")) {
|
|
131167
|
+
const workspace = values.workspace;
|
|
131168
|
+
if (workspace === undefined)
|
|
131169
|
+
throw new Error("renderScoped: {workspace} has no value");
|
|
131170
|
+
if (workspace.length === 0)
|
|
131171
|
+
throw new Error("renderScoped: {workspace} is empty");
|
|
131172
|
+
rendered = rendered.replaceAll("{workspace}", () => shQuote2(workspace));
|
|
131173
|
+
}
|
|
131174
|
+
return rendered;
|
|
131175
|
+
}
|
|
131176
|
+
var RESERVED_ACTIONS, BUN_PRESET, STACK_NEUTRAL_DEFAULTS;
|
|
131177
|
+
var init_sdlc_actions = __esm(() => {
|
|
131178
|
+
init_src();
|
|
131179
|
+
RESERVED_ACTIONS = SDLC_ACTIONS;
|
|
131180
|
+
BUN_PRESET = {
|
|
131181
|
+
build: "bun run build",
|
|
131182
|
+
dev: "bun run dev",
|
|
131183
|
+
clean: "bun run clean",
|
|
131184
|
+
format: "bun run format",
|
|
131185
|
+
autofix: "bun run autofix",
|
|
131186
|
+
lint: "bun run lint",
|
|
131187
|
+
typecheck: "bun run typecheck",
|
|
131188
|
+
test: "bun run test",
|
|
131189
|
+
"test-full": "bun run test:full",
|
|
131190
|
+
"test-heavy": "bun run test:heavy",
|
|
131191
|
+
"rules-pre": "bun run test-pre-check",
|
|
131192
|
+
"rules-post": "bun run test-post-check",
|
|
131193
|
+
check: "bun run spur-check",
|
|
131194
|
+
"check-full": "bun run spur-check:full",
|
|
131195
|
+
"check-feature": "bun run spur-check-feature",
|
|
131196
|
+
"lint-changed": "bunx biome check {files}",
|
|
131197
|
+
"typecheck-changed": "cd {workspace} && bun run typecheck",
|
|
131198
|
+
"test-changed": "cd {workspace} && bun test {paths}"
|
|
131199
|
+
};
|
|
131200
|
+
STACK_NEUTRAL_DEFAULTS = SDLC_STACK_NEUTRAL_ACTIONS;
|
|
131201
|
+
});
|
|
131202
|
+
|
|
131203
|
+
// ../../packages/app/src/services/verdict-replay.ts
|
|
131204
|
+
function citedTestAnchors(rows, max) {
|
|
131205
|
+
const seen = new Map;
|
|
131206
|
+
for (const row of rows) {
|
|
131207
|
+
for (const anchor of extractBacktickLineAnchors(row.evidence ?? "")) {
|
|
131208
|
+
if (!TEST_FILE_PATTERN.test(anchor.path) || seen.has(anchor.path))
|
|
131209
|
+
continue;
|
|
131210
|
+
seen.set(anchor.path, anchor.raw);
|
|
131211
|
+
}
|
|
131212
|
+
}
|
|
131213
|
+
return new Map([...seen].slice(0, max));
|
|
131214
|
+
}
|
|
131215
|
+
function replayWorkspace(file) {
|
|
131216
|
+
const parts = file.split("/");
|
|
131217
|
+
if (parts.length >= 3 && (parts[0] === "apps" || parts[0] === "packages"))
|
|
131218
|
+
return `${parts[0]}/${parts[1]}`;
|
|
131219
|
+
return ".";
|
|
131220
|
+
}
|
|
131221
|
+
async function replayCitedTests(rows, options = {}) {
|
|
131222
|
+
const cwd = options.cwd ?? process.cwd();
|
|
131223
|
+
const timeoutMs = options.timeoutMs ?? REPLAY_TIMEOUT_MS;
|
|
131224
|
+
const anchors = citedTestAnchors(rows, options.maxFiles ?? REPLAY_MAX_FILES);
|
|
131225
|
+
const files = [...anchors.keys()];
|
|
131226
|
+
const executor = options.executor ?? new NodeProcessExecutor2;
|
|
131227
|
+
const fileSystem = options.fs ?? createNodeFileSystem2();
|
|
131228
|
+
const resolved = resolveAction(options.builder, "test-changed");
|
|
131229
|
+
const template = resolved.kind === "command" ? resolved.command : resolved.kind === "composite" ? resolved.steps.join(" && ") : "";
|
|
131230
|
+
const ran = [];
|
|
131231
|
+
const skipped = [];
|
|
131232
|
+
const findings = [];
|
|
131233
|
+
const thresholdByWorkspace = new Map;
|
|
131234
|
+
for (const file of files) {
|
|
131235
|
+
const anchor = anchors.get(file) ?? file;
|
|
131236
|
+
const exists = await fileSystem.exists(`${cwd}/${file}`);
|
|
131237
|
+
if (!exists) {
|
|
131238
|
+
findings.push({ anchor, file, missing: true });
|
|
131239
|
+
continue;
|
|
131240
|
+
}
|
|
131241
|
+
if (template.length === 0)
|
|
131242
|
+
continue;
|
|
131243
|
+
const workspace = replayWorkspace(file);
|
|
131244
|
+
let threshold = thresholdByWorkspace.get(workspace);
|
|
131245
|
+
if (threshold === undefined) {
|
|
131246
|
+
threshold = await declaresCoverageThreshold(fileSystem, `${cwd}/${workspace}/bunfig.toml`);
|
|
131247
|
+
thresholdByWorkspace.set(workspace, threshold);
|
|
131248
|
+
}
|
|
131249
|
+
if (threshold) {
|
|
131250
|
+
skipped.push({ file, reason: `coverageThreshold in ${workspace}/bunfig.toml` });
|
|
131251
|
+
continue;
|
|
131252
|
+
}
|
|
131253
|
+
const path = workspace === "." ? file : file.slice(workspace.length + 1);
|
|
131254
|
+
const command = renderScoped(template, { workspace, paths: [path] });
|
|
131255
|
+
const result = await executor.run({
|
|
131256
|
+
command: "sh",
|
|
131257
|
+
args: ["-c", command],
|
|
131258
|
+
cwd,
|
|
131259
|
+
forceBuffered: true,
|
|
131260
|
+
rejectOnError: false,
|
|
131261
|
+
timeout: timeoutMs
|
|
131262
|
+
});
|
|
131263
|
+
const exitCode = result.exitCode ?? null;
|
|
131264
|
+
ran.push({ file, exitCode });
|
|
131265
|
+
if (result.outcome === "timeout")
|
|
131266
|
+
findings.push({ anchor, file, timedOut: true, exitCode });
|
|
131267
|
+
else if (exitCode !== 0)
|
|
131268
|
+
findings.push({ anchor, file, exitCode });
|
|
131269
|
+
}
|
|
131270
|
+
return { files, ran, skipped, findings };
|
|
131271
|
+
}
|
|
131272
|
+
async function declaresCoverageThreshold(fileSystem, bunfigPath) {
|
|
131273
|
+
try {
|
|
131274
|
+
if (!await fileSystem.exists(bunfigPath))
|
|
131275
|
+
return false;
|
|
131276
|
+
return COVERAGE_THRESHOLD_PATTERN.test(await fileSystem.readFile(bunfigPath));
|
|
131277
|
+
} catch {
|
|
131278
|
+
return false;
|
|
131279
|
+
}
|
|
131280
|
+
}
|
|
131281
|
+
function formatReplayReport(replay) {
|
|
131282
|
+
return {
|
|
131283
|
+
summary: `Replay: ${replay.files.length} cited test file(s), ${replay.findings.length} finding(s)${replay.skipped.length > 0 ? `, ${replay.skipped.length} skipped` : ""}`,
|
|
131284
|
+
findings: replay.findings.map((finding) => ` ${finding.anchor}: ${finding.missing ? "missing" : finding.timedOut ? "timed out" : `exit ${finding.exitCode ?? "unknown"}`}`)
|
|
131285
|
+
};
|
|
131286
|
+
}
|
|
131287
|
+
var REPLAY_MAX_FILES = 5, REPLAY_TIMEOUT_MS = 120000, TEST_FILE_PATTERN, COVERAGE_THRESHOLD_PATTERN;
|
|
131288
|
+
var init_verdict_replay = __esm(() => {
|
|
131289
|
+
init_dist5();
|
|
131290
|
+
init_sdlc_actions();
|
|
131291
|
+
init_task_check();
|
|
131292
|
+
TEST_FILE_PATTERN = /\.(?:test|spec)\.[cm]?[jt]sx?$/;
|
|
131293
|
+
COVERAGE_THRESHOLD_PATTERN = /^\s*coverageThreshold\s*=/m;
|
|
131294
|
+
});
|
|
131295
|
+
|
|
130912
131296
|
// ../../packages/app/src/workflow/checkpoint-contract.ts
|
|
130913
131297
|
function parseCheckpointMetadata(raw) {
|
|
130914
131298
|
if (!raw.startsWith("---"))
|
|
@@ -131099,6 +131483,32 @@ var init_workflow_catalog_scan = __esm(() => {
|
|
|
131099
131483
|
import { realpathSync as realpathSync8, statSync as statSync12 } from "fs";
|
|
131100
131484
|
import { readdir as readdir3, rm as rm2 } from "fs/promises";
|
|
131101
131485
|
import { dirname as dirname32, join as join58, relative as relative14, resolve as resolve21, sep as sep11 } from "path";
|
|
131486
|
+
async function reapRun(db, engine, run, reasons, cwd) {
|
|
131487
|
+
const dao = new RunDao(db);
|
|
131488
|
+
const interrupted = run.status === "running" ? await engine.interruptRun(run.id, "interrupted") : undefined;
|
|
131489
|
+
if (interrupted === undefined) {
|
|
131490
|
+
await dao.finalizeStale(run.id, reasons.finalized);
|
|
131491
|
+
} else {
|
|
131492
|
+
await dao.mergeMetadataIfStatus(run.id, { staleReason: reasons.interrupted }, ["interrupted"]);
|
|
131493
|
+
}
|
|
131494
|
+
projectRunStateStatus(runStoragePaths(cwd).recordsDir, run.id, interrupted === undefined ? "failed" : "interrupted", new Date().toISOString());
|
|
131495
|
+
return interrupted === undefined ? "failed" : "interrupted";
|
|
131496
|
+
}
|
|
131497
|
+
async function reapIfWorkerLost(db, cwd, runId) {
|
|
131498
|
+
const dao = new RunDao(db);
|
|
131499
|
+
const row = await dao.traceRowById(runId);
|
|
131500
|
+
if (row === undefined)
|
|
131501
|
+
return { row, reaped: false };
|
|
131502
|
+
if (row.status !== "running" && row.status !== "pending")
|
|
131503
|
+
return { row, reaped: false };
|
|
131504
|
+
const pid = row.pid;
|
|
131505
|
+
if (pid === null || isPidAlive(pid))
|
|
131506
|
+
return { row, reaped: false };
|
|
131507
|
+
const reason = `worker-lost: pid ${pid} not alive`;
|
|
131508
|
+
const engine = new WorkflowService(createDefaultWorkflowEngineHost({}), new DbWorkflowPersistenceAdapter(db));
|
|
131509
|
+
await reapRun(db, engine, row, { interrupted: reason, finalized: reason }, cwd);
|
|
131510
|
+
return { row, reaped: true };
|
|
131511
|
+
}
|
|
131102
131512
|
|
|
131103
131513
|
class WorkflowCleanOps {
|
|
131104
131514
|
ctx;
|
|
@@ -131113,14 +131523,10 @@ class WorkflowCleanOps {
|
|
|
131113
131523
|
if (!dryRun) {
|
|
131114
131524
|
const engine = new WorkflowService(createDefaultWorkflowEngineHost({}), new DbWorkflowPersistenceAdapter(db));
|
|
131115
131525
|
for (const run of stale) {
|
|
131116
|
-
|
|
131117
|
-
|
|
131118
|
-
|
|
131119
|
-
}
|
|
131120
|
-
await dao.mergeMetadata(run.id, {
|
|
131121
|
-
staleReason: `stale: owner lost > ${olderThanMinutes}m (spur workflow clean)`
|
|
131122
|
-
});
|
|
131123
|
-
}
|
|
131526
|
+
await reapRun(db, engine, run, {
|
|
131527
|
+
interrupted: `stale: owner lost > ${olderThanMinutes}m (spur workflow clean)`,
|
|
131528
|
+
finalized: `stale: non-terminal > ${olderThanMinutes}m (spur workflow clean)`
|
|
131529
|
+
}, this.ctx.cwd);
|
|
131124
131530
|
}
|
|
131125
131531
|
}
|
|
131126
131532
|
return {
|
|
@@ -131337,7 +131743,9 @@ var init_workflow_clean_ops = __esm(() => {
|
|
|
131337
131743
|
init_src2();
|
|
131338
131744
|
init_dist9();
|
|
131339
131745
|
init_dist5();
|
|
131746
|
+
init_workflow_run_log_sink();
|
|
131340
131747
|
init_checkpoint_contract();
|
|
131748
|
+
init_project_process();
|
|
131341
131749
|
init_run_storage();
|
|
131342
131750
|
init_workflow_catalog_scan();
|
|
131343
131751
|
});
|
|
@@ -131351,6 +131759,16 @@ function commandGateOutcome(exitCode) {
|
|
|
131351
131759
|
return "FAIL";
|
|
131352
131760
|
return "BLOCKED";
|
|
131353
131761
|
}
|
|
131762
|
+
function asOptionalTimeoutMs(value) {
|
|
131763
|
+
if (value === undefined || value === null)
|
|
131764
|
+
return { ok: true, ms: undefined };
|
|
131765
|
+
if (typeof value === "string" && value.trim() === "")
|
|
131766
|
+
return { ok: true, ms: undefined };
|
|
131767
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
131768
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
131769
|
+
return { ok: false };
|
|
131770
|
+
return { ok: true, ms: Math.trunc(parsed) };
|
|
131771
|
+
}
|
|
131354
131772
|
function sleep4(ms) {
|
|
131355
131773
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131356
131774
|
}
|
|
@@ -131420,13 +131838,21 @@ class CommandGateActionRunner {
|
|
|
131420
131838
|
const maxAttempts = Math.max(1, typeof retry.maxAttempts === "number" ? retry.maxAttempts : 1);
|
|
131421
131839
|
const delayMs = Math.max(0, typeof retry.delayMs === "number" ? retry.delayMs : 1000);
|
|
131422
131840
|
const retryOn = Array.isArray(retry.on) ? retry.on : [];
|
|
131423
|
-
const
|
|
131841
|
+
const timeout = asOptionalTimeoutMs(options.timeoutMs);
|
|
131842
|
+
if (!timeout.ok) {
|
|
131843
|
+
return {
|
|
131844
|
+
ok: false,
|
|
131845
|
+
error: `command.gate: timeoutMs must be a positive number, got "${String(options.timeoutMs)}"`
|
|
131846
|
+
};
|
|
131847
|
+
}
|
|
131848
|
+
const timeoutMs = timeout.ms;
|
|
131424
131849
|
const env = childProcessEnv(context.vars);
|
|
131425
131850
|
await this.fileSystem.ensureDir(normalize3(join59(normalized, "..")));
|
|
131426
131851
|
let lastStdout = "";
|
|
131427
131852
|
let lastStderr = "";
|
|
131428
131853
|
let lastExitCode = -1;
|
|
131429
131854
|
let attemptsRun = 0;
|
|
131855
|
+
let timedOut = false;
|
|
131430
131856
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
131431
131857
|
attemptsRun = attempt;
|
|
131432
131858
|
const res = await this.processExecutor.run({
|
|
@@ -131441,6 +131867,7 @@ class CommandGateActionRunner {
|
|
|
131441
131867
|
lastStdout = res.stdout;
|
|
131442
131868
|
lastStderr = res.stderr;
|
|
131443
131869
|
lastExitCode = res.exitCode ?? -1;
|
|
131870
|
+
timedOut = res.outcome === "timeout";
|
|
131444
131871
|
if (res.exitCode === 0) {
|
|
131445
131872
|
await this.fileSystem.writeFile(normalized, `PASS
|
|
131446
131873
|
`);
|
|
@@ -131475,7 +131902,8 @@ ${lastStderr}`;
|
|
|
131475
131902
|
attempts: attemptsRun,
|
|
131476
131903
|
stdout: lastStdout,
|
|
131477
131904
|
stderr: lastStderr,
|
|
131478
|
-
resultFile: normalized
|
|
131905
|
+
resultFile: normalized,
|
|
131906
|
+
...timedOut ? { timedOut: true, timeoutMs } : {}
|
|
131479
131907
|
};
|
|
131480
131908
|
if (softFail) {
|
|
131481
131909
|
return { ok: true, data: failureData };
|
|
@@ -135101,18 +135529,21 @@ async function outputArtifactForRun(cwd, runId) {
|
|
|
135101
135529
|
}
|
|
135102
135530
|
return;
|
|
135103
135531
|
}
|
|
135104
|
-
function rowToTraceEntry(row, cwd) {
|
|
135532
|
+
function rowToTraceEntry(row, cwd, nowMs = Date.now(), nodeBudgetMs) {
|
|
135105
135533
|
let isDryRun = false;
|
|
135106
135534
|
let failureReason;
|
|
135107
135535
|
let version;
|
|
135108
135536
|
let definitionDigest;
|
|
135109
135537
|
let startState;
|
|
135110
135538
|
let continuedFrom;
|
|
135539
|
+
let hostPauses;
|
|
135111
135540
|
try {
|
|
135112
135541
|
const meta = JSON.parse(row.metadata_json);
|
|
135113
135542
|
isDryRun = meta.dryRun === true;
|
|
135114
135543
|
if (typeof meta.failureReason === "string" && meta.failureReason !== "") {
|
|
135115
135544
|
failureReason = meta.failureReason;
|
|
135545
|
+
} else if (typeof meta.staleReason === "string" && meta.staleReason !== "") {
|
|
135546
|
+
failureReason = meta.staleReason;
|
|
135116
135547
|
}
|
|
135117
135548
|
if ("workflowVersion" in meta && (typeof meta.workflowVersion === "string" || meta.workflowVersion === null)) {
|
|
135118
135549
|
version = meta.workflowVersion;
|
|
@@ -135126,6 +135557,9 @@ function rowToTraceEntry(row, cwd) {
|
|
|
135126
135557
|
if (typeof meta.continuedFrom === "string" && meta.continuedFrom !== "") {
|
|
135127
135558
|
continuedFrom = meta.continuedFrom;
|
|
135128
135559
|
}
|
|
135560
|
+
const pauses = readHostPauses(meta);
|
|
135561
|
+
if (pauses.length > 0)
|
|
135562
|
+
hostPauses = pauses;
|
|
135129
135563
|
} catch {}
|
|
135130
135564
|
const entry = {
|
|
135131
135565
|
runId: row.id,
|
|
@@ -135143,8 +135577,14 @@ function rowToTraceEntry(row, cwd) {
|
|
|
135143
135577
|
...version !== undefined ? { version } : {},
|
|
135144
135578
|
...definitionDigest !== undefined ? { definitionDigest } : {},
|
|
135145
135579
|
...startState !== undefined ? { startState } : {},
|
|
135146
|
-
...continuedFrom !== undefined ? { continuedFrom } : {}
|
|
135580
|
+
...continuedFrom !== undefined ? { continuedFrom } : {},
|
|
135581
|
+
...hostPauses !== undefined ? { hostPauses } : {}
|
|
135147
135582
|
};
|
|
135583
|
+
const pauseAgeMs = row.status === "paused" || row.status === "interrupted" ? openPauseMs(row.updated_at ?? null, nowMs) : null;
|
|
135584
|
+
if (pauseAgeMs !== null && pauseAgeMs > resolveStallThresholdMs(nodeBudgetMs)) {
|
|
135585
|
+
entry.stalled = true;
|
|
135586
|
+
entry.stalledMs = pauseAgeMs;
|
|
135587
|
+
}
|
|
135148
135588
|
const nextAction = traceNextAction(entry);
|
|
135149
135589
|
if (nextAction !== undefined)
|
|
135150
135590
|
entry.nextAction = nextAction;
|
|
@@ -135189,7 +135629,11 @@ function traceNextAction(run, outputArtifact) {
|
|
|
135189
135629
|
return { label: "Follow run", kind: "command", value: `spur workflow trace ${run.runId} --follow` };
|
|
135190
135630
|
}
|
|
135191
135631
|
if (run.status === "paused" || run.status === "interrupted") {
|
|
135192
|
-
return {
|
|
135632
|
+
return {
|
|
135633
|
+
label: run.stalled === true ? "Resume stalled run" : "Continue run",
|
|
135634
|
+
kind: "command",
|
|
135635
|
+
value: `spur workflow continue ${run.runId}`
|
|
135636
|
+
};
|
|
135193
135637
|
}
|
|
135194
135638
|
if (run.status === "failed" && outputArtifact !== undefined) {
|
|
135195
135639
|
return { label: "Inspect run log", kind: "path", value: outputArtifact };
|
|
@@ -135300,6 +135744,7 @@ var TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
|
|
|
135300
135744
|
var init_workflow_trace_projection = __esm(() => {
|
|
135301
135745
|
init_dist5();
|
|
135302
135746
|
init_agent_execution();
|
|
135747
|
+
init_host_pause();
|
|
135303
135748
|
init_run_storage();
|
|
135304
135749
|
init_system_event_envelope();
|
|
135305
135750
|
TRACE_IDENTIFIER = /^[A-Za-z0-9._:-]+$/;
|
|
@@ -135478,20 +135923,19 @@ class WorkflowInspectOps {
|
|
|
135478
135923
|
since: filter.since,
|
|
135479
135924
|
limit: filter.last ?? 20
|
|
135480
135925
|
});
|
|
135481
|
-
|
|
135482
|
-
|
|
135483
|
-
|
|
135484
|
-
};
|
|
135926
|
+
const actionDao = new ActionRunDao(db);
|
|
135927
|
+
const entries = await Promise.all(rows.map(async (row) => rowToTraceEntry(row, this.ctx.cwd, Date.now(), row.status === "paused" || row.status === "interrupted" ? pausedNodeBudgetMs(await actionDao.actionRowsByRunId(row.id)) : undefined)));
|
|
135928
|
+
return { entries, total: rows.length };
|
|
135485
135929
|
}
|
|
135486
|
-
async traceRun(db, runId) {
|
|
135930
|
+
async traceRun(db, runId, preloadedRow) {
|
|
135487
135931
|
const runDao = new RunDao(db);
|
|
135488
|
-
const row = await runDao.traceRowById(runId);
|
|
135932
|
+
const row = preloadedRow ?? await runDao.traceRowById(runId);
|
|
135489
135933
|
if (!row)
|
|
135490
135934
|
throw new Error(`Run not found: ${runId}`);
|
|
135491
|
-
const
|
|
135935
|
+
const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
|
|
135936
|
+
const run = rowToTraceEntry(row, this.ctx.cwd, Date.now(), pausedNodeBudgetMs(actionRows));
|
|
135492
135937
|
const phaseRows = await new PhaseRunDao(db).phaseRowsByRunId(runId);
|
|
135493
135938
|
const transitionRows = await new TransitionRunDao(db).transitionRowsByRunId(runId);
|
|
135494
|
-
const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
|
|
135495
135939
|
const costByActionId = new Map;
|
|
135496
135940
|
if (actionRows.some((a) => a.kind === "agent.run")) {
|
|
135497
135941
|
for (const a of actionRows) {
|
|
@@ -135599,6 +136043,7 @@ var init_workflow_inspect_ops = __esm(() => {
|
|
|
135599
136043
|
init_dist5();
|
|
135600
136044
|
init_decision_service();
|
|
135601
136045
|
init_composition_lint();
|
|
136046
|
+
init_host_pause();
|
|
135602
136047
|
init_plan_projection();
|
|
135603
136048
|
init_run_record();
|
|
135604
136049
|
init_workflow_resolver();
|
|
@@ -136118,8 +136563,9 @@ class WorkflowResumeOps {
|
|
|
136118
136563
|
if (persistedDigest !== null && persistedDigest !== resolved.digest && "resumeDefinitionDigest" in rawMeta) {
|
|
136119
136564
|
resumeVars.__definitionDigest = resolved.digest;
|
|
136120
136565
|
}
|
|
136121
|
-
|
|
136122
|
-
|
|
136566
|
+
const stamped = await this.recordHostPause(runId, hostPause.receipt, opts?.now);
|
|
136567
|
+
if (stamped !== undefined) {
|
|
136568
|
+
await writeReceipt(createNodeFileSystem2(launchWorkdir), launchWorkdir, stamped);
|
|
136123
136569
|
}
|
|
136124
136570
|
const result = await withWorktreeRunLease(await this.ctx.getDb(), launchWorkdir, workflow.name, runId, async (signal) => svc.resumeRun(workflow, runId, {
|
|
136125
136571
|
...signal !== undefined ? { signal } : {},
|
|
@@ -136141,6 +136587,39 @@ class WorkflowResumeOps {
|
|
|
136141
136587
|
runResult.warnings = warnings;
|
|
136142
136588
|
return runResult;
|
|
136143
136589
|
}
|
|
136590
|
+
async recordHostPause(runId, receipt, now) {
|
|
136591
|
+
const db = await this.ctx.getDb();
|
|
136592
|
+
const rows = await new ActionRunDao(db).actionRowsByRunId(runId);
|
|
136593
|
+
const paused = [...rows].reverse().find((row) => row.status === "paused" || row.result_json !== null && /"pause"\s*:\s*true/.test(row.result_json));
|
|
136594
|
+
const pausedAt = paused?.started_at ?? null;
|
|
136595
|
+
if (pausedAt === null)
|
|
136596
|
+
return receipt;
|
|
136597
|
+
const resumedAt = (now ?? new Date).toISOString();
|
|
136598
|
+
const pauseMs = hostDurationMs(pausedAt, resumedAt);
|
|
136599
|
+
if (pauseMs === null)
|
|
136600
|
+
return receipt;
|
|
136601
|
+
const run = await new RunDao(db).traceRowById(runId);
|
|
136602
|
+
let metadata = {};
|
|
136603
|
+
try {
|
|
136604
|
+
metadata = JSON.parse(run?.metadata_json ?? "{}");
|
|
136605
|
+
} catch {
|
|
136606
|
+
metadata = {};
|
|
136607
|
+
}
|
|
136608
|
+
const pauses = appendHostPause(metadata, {
|
|
136609
|
+
node: paused?.node ?? "unknown",
|
|
136610
|
+
action: paused?.kind ?? "agent.run",
|
|
136611
|
+
pausedAt,
|
|
136612
|
+
resumedAt,
|
|
136613
|
+
hostDurationMs: pauseMs,
|
|
136614
|
+
executor: receipt?.executor ?? "host"
|
|
136615
|
+
});
|
|
136616
|
+
await new RunDao(db).mergeMetadata(runId, { [HOST_PAUSES_KEY]: pauses });
|
|
136617
|
+
if (receipt === undefined)
|
|
136618
|
+
return;
|
|
136619
|
+
if (!isDegenerateReceiptInterval(receipt.startedAt, receipt.finishedAt ?? undefined))
|
|
136620
|
+
return receipt;
|
|
136621
|
+
return { ...receipt, startedAt: pausedAt, finishedAt: resumedAt };
|
|
136622
|
+
}
|
|
136144
136623
|
async resolveHostPauseReceipt(runId, workdir, opts, workflow) {
|
|
136145
136624
|
const db = await this.ctx.getDb();
|
|
136146
136625
|
if (!await hostExecutorPersisted(db, runId))
|
|
@@ -136372,6 +136851,7 @@ var init_workflow_resume_ops = __esm(() => {
|
|
|
136372
136851
|
init_checkpoint_contract();
|
|
136373
136852
|
init_composition_lint();
|
|
136374
136853
|
init_host_executor();
|
|
136854
|
+
init_host_pause();
|
|
136375
136855
|
init_progress_projection();
|
|
136376
136856
|
init_receipt();
|
|
136377
136857
|
init_workflow_resolver();
|
|
@@ -136636,8 +137116,10 @@ class WorkflowRunOps {
|
|
|
136636
137116
|
await dao.cancelRun(runId);
|
|
136637
137117
|
const after = await dao.traceRowById(runId);
|
|
136638
137118
|
const finalized = after?.status === "failed" && isNonTerminal;
|
|
136639
|
-
if (finalized)
|
|
137119
|
+
if (finalized) {
|
|
137120
|
+
projectRunStateStatus(runStoragePaths(this.ctx.cwd).recordsDir, runId, "failed", new Date().toISOString());
|
|
136640
137121
|
await releaseWorktreeRunLease(db, this.ctx.cwd, runId);
|
|
137122
|
+
}
|
|
136641
137123
|
return { runId, finalized, status: after?.status ?? "not_found", killed };
|
|
136642
137124
|
}
|
|
136643
137125
|
async stampFailureReason(runId, result) {
|
|
@@ -136657,11 +137139,13 @@ var init_workflow_run_ops = __esm(() => {
|
|
|
136657
137139
|
init_src2();
|
|
136658
137140
|
init_dist9();
|
|
136659
137141
|
init_dist5();
|
|
137142
|
+
init_workflow_run_log_sink();
|
|
136660
137143
|
init_composition_baseline();
|
|
136661
137144
|
init_host_executor();
|
|
136662
137145
|
init_start_state();
|
|
136663
137146
|
init_workflow_resolver();
|
|
136664
137147
|
init_event_bridge();
|
|
137148
|
+
init_run_storage();
|
|
136665
137149
|
init_workflow_run_recording();
|
|
136666
137150
|
init_workflow_run_vars();
|
|
136667
137151
|
init_workflow_worktree_lease();
|
|
@@ -136722,7 +137206,8 @@ class WorkflowAppService {
|
|
|
136722
137206
|
async trace(runIdOrFilter) {
|
|
136723
137207
|
const db = await this.ctx.getDb();
|
|
136724
137208
|
if (typeof runIdOrFilter === "string") {
|
|
136725
|
-
|
|
137209
|
+
const probe = await reapIfWorkerLost(db, this.ctx.cwd, runIdOrFilter);
|
|
137210
|
+
return this.inspectOps.traceRun(db, runIdOrFilter, probe.reaped ? undefined : probe.row);
|
|
136726
137211
|
}
|
|
136727
137212
|
return this.inspectOps.traceList(db, runIdOrFilter ?? {});
|
|
136728
137213
|
}
|
|
@@ -138261,8 +138746,10 @@ var init_src4 = __esm(() => {
|
|
|
138261
138746
|
init_task_verdict();
|
|
138262
138747
|
init_token_ledger_service();
|
|
138263
138748
|
init_token_ledger_watcher();
|
|
138749
|
+
init_verdict_replay();
|
|
138264
138750
|
init_verify_answer_lint();
|
|
138265
138751
|
init_warm_turn_dispatch();
|
|
138752
|
+
init_workflow_clean_ops();
|
|
138266
138753
|
init_workflow_service();
|
|
138267
138754
|
init_worktree_persist();
|
|
138268
138755
|
init_write_slot_service();
|
|
@@ -145488,81 +145975,6 @@ var init_task_diffstat = __esm(() => {
|
|
|
145488
145975
|
init_task_triage();
|
|
145489
145976
|
});
|
|
145490
145977
|
|
|
145491
|
-
// ../../packages/app/src/services/sdlc-actions.ts
|
|
145492
|
-
function isReservedAction(name) {
|
|
145493
|
-
return RESERVED_ACTIONS.includes(name);
|
|
145494
|
-
}
|
|
145495
|
-
function resolveAction(builder, name) {
|
|
145496
|
-
const reserved = isReservedAction(name);
|
|
145497
|
-
const declared = builder?.actions?.[name];
|
|
145498
|
-
if (declared !== undefined) {
|
|
145499
|
-
return Array.isArray(declared) ? { name, reserved, source: "config", kind: "composite", steps: [...declared] } : { name, reserved, source: "config", kind: "command", command: declared };
|
|
145500
|
-
}
|
|
145501
|
-
if ((builder?.preset ?? "bun") === "bun") {
|
|
145502
|
-
if (reserved)
|
|
145503
|
-
return { name, reserved, source: "preset", kind: "command", command: BUN_PRESET[name] };
|
|
145504
|
-
} else {
|
|
145505
|
-
const command = reserved ? STACK_NEUTRAL_DEFAULTS[name] : undefined;
|
|
145506
|
-
if (command !== undefined)
|
|
145507
|
-
return { name, reserved, source: "default", kind: "command", command };
|
|
145508
|
-
}
|
|
145509
|
-
return { name, reserved, source: "unbound", kind: "unbound" };
|
|
145510
|
-
}
|
|
145511
|
-
function listActions(builder) {
|
|
145512
|
-
const custom = Object.keys(builder?.actions ?? {}).filter((name) => !isReservedAction(name)).sort();
|
|
145513
|
-
return [...RESERVED_ACTIONS, ...custom].map((name) => resolveAction(builder, name));
|
|
145514
|
-
}
|
|
145515
|
-
function renderScoped(command, values) {
|
|
145516
|
-
let rendered = command;
|
|
145517
|
-
for (const [placeholder, items] of [
|
|
145518
|
-
["{files}", values.files],
|
|
145519
|
-
["{paths}", values.paths]
|
|
145520
|
-
]) {
|
|
145521
|
-
if (!rendered.includes(placeholder))
|
|
145522
|
-
continue;
|
|
145523
|
-
if (items === undefined)
|
|
145524
|
-
throw new Error(`renderScoped: ${placeholder} has no value`);
|
|
145525
|
-
if (items.length === 0)
|
|
145526
|
-
throw new Error(`renderScoped: ${placeholder} has an empty list`);
|
|
145527
|
-
rendered = rendered.replaceAll(placeholder, () => items.map(shQuote2).join(" "));
|
|
145528
|
-
}
|
|
145529
|
-
if (rendered.includes("{workspace}")) {
|
|
145530
|
-
const workspace = values.workspace;
|
|
145531
|
-
if (workspace === undefined)
|
|
145532
|
-
throw new Error("renderScoped: {workspace} has no value");
|
|
145533
|
-
if (workspace.length === 0)
|
|
145534
|
-
throw new Error("renderScoped: {workspace} is empty");
|
|
145535
|
-
rendered = rendered.replaceAll("{workspace}", () => shQuote2(workspace));
|
|
145536
|
-
}
|
|
145537
|
-
return rendered;
|
|
145538
|
-
}
|
|
145539
|
-
var RESERVED_ACTIONS, BUN_PRESET, STACK_NEUTRAL_DEFAULTS;
|
|
145540
|
-
var init_sdlc_actions = __esm(() => {
|
|
145541
|
-
init_src();
|
|
145542
|
-
RESERVED_ACTIONS = SDLC_ACTIONS;
|
|
145543
|
-
BUN_PRESET = {
|
|
145544
|
-
build: "bun run build",
|
|
145545
|
-
dev: "bun run dev",
|
|
145546
|
-
clean: "bun run clean",
|
|
145547
|
-
format: "bun run format",
|
|
145548
|
-
autofix: "bun run autofix",
|
|
145549
|
-
lint: "bun run lint",
|
|
145550
|
-
typecheck: "bun run typecheck",
|
|
145551
|
-
test: "bun run test",
|
|
145552
|
-
"test-full": "bun run test:full",
|
|
145553
|
-
"test-heavy": "bun run test:heavy",
|
|
145554
|
-
"rules-pre": "bun run test-pre-check",
|
|
145555
|
-
"rules-post": "bun run test-post-check",
|
|
145556
|
-
check: "bun run spur-check",
|
|
145557
|
-
"check-full": "bun run spur-check:full",
|
|
145558
|
-
"check-feature": "bun run spur-check-feature",
|
|
145559
|
-
"lint-changed": "bunx biome check {files}",
|
|
145560
|
-
"typecheck-changed": "cd {workspace} && bun run typecheck",
|
|
145561
|
-
"test-changed": "cd {workspace} && bun test {paths}"
|
|
145562
|
-
};
|
|
145563
|
-
STACK_NEUTRAL_DEFAULTS = SDLC_STACK_NEUTRAL_ACTIONS;
|
|
145564
|
-
});
|
|
145565
|
-
|
|
145566
145978
|
// ../../packages/app/src/services/quality-gate.ts
|
|
145567
145979
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
145568
145980
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
@@ -145583,6 +145995,21 @@ import {
|
|
|
145583
145995
|
} from "fs";
|
|
145584
145996
|
import { homedir as homedir13, tmpdir as tmpdir5 } from "os";
|
|
145585
145997
|
import { dirname as dirname38, join as join78 } from "path";
|
|
145998
|
+
function resolveGateTimeoutMs(env) {
|
|
145999
|
+
const raw = env[GATE_TIMEOUT_MS_ENV];
|
|
146000
|
+
if (raw === undefined || raw.trim() === "")
|
|
146001
|
+
return { ok: true, ms: GATE_TIMEOUT_MS_DEFAULT };
|
|
146002
|
+
const parsed = Number(raw);
|
|
146003
|
+
if (!Number.isFinite(parsed) || parsed <= 0)
|
|
146004
|
+
return { ok: false, error: `quality-gate: ${GATE_TIMEOUT_MS_ENV} must be a positive number, got "${raw}"` };
|
|
146005
|
+
return { ok: true, ms: Math.trunc(parsed) };
|
|
146006
|
+
}
|
|
146007
|
+
function gateTimeoutMsFor(env, options) {
|
|
146008
|
+
if (options.gateTimeoutMs !== undefined && options.gateTimeoutMs > 0)
|
|
146009
|
+
return options.gateTimeoutMs;
|
|
146010
|
+
const resolved = resolveGateTimeoutMs(env);
|
|
146011
|
+
return resolved.ok ? resolved.ms : GATE_TIMEOUT_MS_DEFAULT;
|
|
146012
|
+
}
|
|
145586
146013
|
function isTransientLock(attemptOutput) {
|
|
145587
146014
|
return LOCKED_PATTERN.test(attemptOutput);
|
|
145588
146015
|
}
|
|
@@ -145781,7 +146208,7 @@ function lightScope(changedFiles, exists = existsSync23) {
|
|
|
145781
146208
|
tests.add(candidate);
|
|
145782
146209
|
matched = true;
|
|
145783
146210
|
}
|
|
145784
|
-
} else if (rest.startsWith("tests/") &&
|
|
146211
|
+
} else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
|
|
145785
146212
|
tests.add(file);
|
|
145786
146213
|
matched = true;
|
|
145787
146214
|
}
|
|
@@ -145798,7 +146225,7 @@ function lightScope(changedFiles, exists = existsSync23) {
|
|
|
145798
146225
|
const candidate = `${workspace}/tests/${rest.slice("src/".length).replace(/\.tsx?$/, (ext) => `.test${ext}`)}`;
|
|
145799
146226
|
if (exists(candidate))
|
|
145800
146227
|
tests.add(candidate);
|
|
145801
|
-
} else if (rest.startsWith("tests/") &&
|
|
146228
|
+
} else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
|
|
145802
146229
|
tests.add(file);
|
|
145803
146230
|
}
|
|
145804
146231
|
}
|
|
@@ -145845,11 +146272,13 @@ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck, builder) {
|
|
|
145845
146272
|
}
|
|
145846
146273
|
return plans;
|
|
145847
146274
|
}
|
|
145848
|
-
function gitChangedFiles(cwd) {
|
|
146275
|
+
function gitChangedFiles(cwd, timeoutMs) {
|
|
145849
146276
|
const abs = (p) => cwd ? join78(cwd, p) : p;
|
|
145850
146277
|
const changed = new Set;
|
|
145851
146278
|
for (const cmd of ["git diff --name-only HEAD", "git ls-files --others --exclude-standard"]) {
|
|
145852
|
-
const result = runShellCommand(cmd, cwd);
|
|
146279
|
+
const result = runShellCommand(cmd, cwd, undefined, timeoutMs);
|
|
146280
|
+
if (result.timedOut)
|
|
146281
|
+
return null;
|
|
145853
146282
|
for (const line of result.output.split(`
|
|
145854
146283
|
`)) {
|
|
145855
146284
|
const file = line.trim();
|
|
@@ -145888,13 +146317,29 @@ function runLightGate(env, options = {}) {
|
|
|
145888
146317
|
const logFile = join78(runDir, `${env.wbs}-light-gate.log`);
|
|
145889
146318
|
const receiptFile = join78(runDir, `${env.wbs}-check-receipt.json`);
|
|
145890
146319
|
writeFileSync14(abs(logFile), "");
|
|
146320
|
+
const timeoutMs = gateTimeoutMsFor(env, options);
|
|
146321
|
+
const changed = gitChangedFiles(cwd, timeoutMs);
|
|
146322
|
+
if (changed === null) {
|
|
146323
|
+
const line = `--- light gate BLOCKED: changed-path git read timed out after ${timeoutMs}ms
|
|
146324
|
+
`;
|
|
146325
|
+
appendFileSync7(abs(logFile), line);
|
|
146326
|
+
process.stderr.write(line);
|
|
146327
|
+
return {
|
|
146328
|
+
status: "FAIL",
|
|
146329
|
+
scope: { files: [], workspaces: [], tests: [] },
|
|
146330
|
+
checks: [],
|
|
146331
|
+
logFile,
|
|
146332
|
+
receiptFile,
|
|
146333
|
+
blocked: line.trim()
|
|
146334
|
+
};
|
|
146335
|
+
}
|
|
145891
146336
|
const capture = (options.captureGitTree ?? captureGateGitTree)(cwd ?? process.cwd());
|
|
145892
146337
|
const digest = capture.ok ? capture.tree : "";
|
|
145893
146338
|
if (!capture.ok) {
|
|
145894
146339
|
appendFileSync7(abs(logFile), `--- light receipt: no git tree identity \u2014 ${capture.error}
|
|
145895
146340
|
`);
|
|
145896
146341
|
}
|
|
145897
|
-
const scope = lightScope(
|
|
146342
|
+
const scope = lightScope(changed, (p) => existsSync23(abs(p)));
|
|
145898
146343
|
const plans = planLightChecks(scope, (workspace) => workspaceHasTypecheck(abs(workspace)), options.builder);
|
|
145899
146344
|
const reusable = new Map;
|
|
145900
146345
|
let preserveFullReceipt = false;
|
|
@@ -145935,11 +146380,13 @@ function runLightGate(env, options = {}) {
|
|
|
145935
146380
|
continue;
|
|
145936
146381
|
}
|
|
145937
146382
|
const startedAtMs = Date.now();
|
|
145938
|
-
const result = runShellCommand(plan.cmd, cwd);
|
|
146383
|
+
const result = runShellCommand(plan.cmd, cwd, undefined, timeoutMs);
|
|
145939
146384
|
const durationMs = Date.now() - startedAtMs;
|
|
145940
|
-
const status = result.code === 0 ? "PASS" : "FAIL";
|
|
145941
|
-
|
|
145942
|
-
${
|
|
146385
|
+
const status = result.code === 0 && !result.timedOut ? "PASS" : "FAIL";
|
|
146386
|
+
const header = result.timedOut ? `--- light ${plan.id}: ${status} (timeout after ${timeoutMs}ms, ${durationMs}ms elapsed)
|
|
146387
|
+
` : `--- light ${plan.id}: ${status} (${durationMs}ms)
|
|
146388
|
+
`;
|
|
146389
|
+
appendFileSync7(abs(logFile), `${header}${result.output}`);
|
|
145943
146390
|
checks.push({ id: plan.id, cmd: plan.cmd, status, durationMs, logPath: logFile });
|
|
145944
146391
|
}
|
|
145945
146392
|
const receipt = buildReceipt({
|
|
@@ -145977,7 +146424,7 @@ function gateSleep(ms) {
|
|
|
145977
146424
|
}
|
|
145978
146425
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
145979
146426
|
}
|
|
145980
|
-
function runShellCommand(cmd, cwd, extraEnv) {
|
|
146427
|
+
function runShellCommand(cmd, cwd, extraEnv, timeoutMs) {
|
|
145981
146428
|
const dir = mkdtempSync(join78(tmpdir5(), "spur-quality-gate-"));
|
|
145982
146429
|
const path = join78(dir, "output");
|
|
145983
146430
|
const fd = openSync8(path, "w");
|
|
@@ -145985,13 +146432,15 @@ function runShellCommand(cmd, cwd, extraEnv) {
|
|
|
145985
146432
|
const result = spawnSync6("sh", ["-c", cmd], {
|
|
145986
146433
|
cwd,
|
|
145987
146434
|
stdio: ["ignore", fd, fd],
|
|
145988
|
-
env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv }
|
|
146435
|
+
env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv },
|
|
146436
|
+
...timeoutMs !== undefined ? { timeout: timeoutMs, killSignal: "SIGKILL" } : {}
|
|
145989
146437
|
});
|
|
145990
|
-
|
|
146438
|
+
const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGKILL";
|
|
146439
|
+
if (result.error !== undefined && !timedOut) {
|
|
145991
146440
|
return { output: `sh -c failed: ${result.error.message}
|
|
145992
|
-
`, code: 1 };
|
|
146441
|
+
`, code: 1, timedOut: false };
|
|
145993
146442
|
}
|
|
145994
|
-
return { output: readFileSync21(path, "utf8"), code: result.status ?? 1 };
|
|
146443
|
+
return { output: readFileSync21(path, "utf8"), code: result.status ?? 1, timedOut };
|
|
145995
146444
|
} finally {
|
|
145996
146445
|
closeSync8(fd);
|
|
145997
146446
|
rmSync5(dir, { recursive: true, force: true });
|
|
@@ -146205,6 +146654,8 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146205
146654
|
const attemptFile = rel("-test-fix-attempt");
|
|
146206
146655
|
const abs = (p) => cwd ? join78(cwd, p) : p;
|
|
146207
146656
|
const priorLog = mode === "recheck" && existsSync23(abs(logFile)) ? readFileSync21(abs(logFile), "utf8") : "";
|
|
146657
|
+
const timeoutMs = gateTimeoutMsFor(env, options);
|
|
146658
|
+
let timedOut;
|
|
146208
146659
|
writeFileSync14(abs(logFile), "");
|
|
146209
146660
|
if (mode === "run")
|
|
146210
146661
|
writeFileSync14(abs(attemptFile), `0
|
|
@@ -146259,15 +146710,17 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146259
146710
|
gateRc = 1;
|
|
146260
146711
|
}
|
|
146261
146712
|
if (mode === "recheck" && gateRc === 0 && (env.gateProbeCmd ?? "").length > 0) {
|
|
146262
|
-
const probe = runShellCommand(env.gateProbeCmd ?? "", cwd);
|
|
146713
|
+
const probe = runShellCommand(env.gateProbeCmd ?? "", cwd, undefined, timeoutMs);
|
|
146263
146714
|
writeFileSync14(abs(`${logFile}.probe`), probe.output);
|
|
146264
146715
|
gateRc = probe.code;
|
|
146716
|
+
if (probe.timedOut)
|
|
146717
|
+
timedOut = { cmd: env.gateProbeCmd ?? "" };
|
|
146265
146718
|
appendFileSync7(abs(logFile), probe.output);
|
|
146266
146719
|
rmSync5(abs(`${logFile}.probe`), { force: true });
|
|
146267
146720
|
}
|
|
146268
146721
|
let gateRuntimeMs = 0;
|
|
146269
146722
|
let queueWaitMs = 0;
|
|
146270
|
-
if (gateRc === 0) {
|
|
146723
|
+
if (gateRc === 0 && timedOut === undefined) {
|
|
146271
146724
|
const delayMs = retryDelayMs2(env);
|
|
146272
146725
|
const lockLine = (line) => {
|
|
146273
146726
|
process.stdout.write(line);
|
|
@@ -146281,12 +146734,16 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146281
146734
|
try {
|
|
146282
146735
|
for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
|
|
146283
146736
|
const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
|
|
146284
|
-
const attempt = runShellCommand(env.qualityGateCmd ?? "", cwd, lock.token.length > 0 ? { [GATE_LOCK_TOKEN_ENV]: lock.token } : undefined);
|
|
146737
|
+
const attempt = runShellCommand(env.qualityGateCmd ?? "", cwd, lock.token.length > 0 ? { [GATE_LOCK_TOKEN_ENV]: lock.token } : undefined, timeoutMs);
|
|
146285
146738
|
writeFileSync14(abs(attemptLogPath), attempt.output);
|
|
146286
146739
|
const locked = isTransientLock(attempt.output);
|
|
146287
146740
|
appendFileSync7(abs(logFile), attempt.output);
|
|
146288
146741
|
rmSync5(abs(attemptLogPath), { force: true });
|
|
146289
146742
|
gateRc = attempt.code;
|
|
146743
|
+
if (attempt.timedOut) {
|
|
146744
|
+
timedOut = { cmd: env.qualityGateCmd ?? "" };
|
|
146745
|
+
break;
|
|
146746
|
+
}
|
|
146290
146747
|
if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
|
|
146291
146748
|
break;
|
|
146292
146749
|
const line = retryMessage(gateAttempt);
|
|
@@ -146322,6 +146779,26 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146322
146779
|
}
|
|
146323
146780
|
}
|
|
146324
146781
|
}
|
|
146782
|
+
if (timedOut !== undefined) {
|
|
146783
|
+
const header = `gate timeout after ${timeoutMs}ms: ${timedOut.cmd}
|
|
146784
|
+
`;
|
|
146785
|
+
const tail = tailLines(readFileSync21(abs(logFile), "utf8"), 40);
|
|
146786
|
+
writeFileSync14(abs(findingsFile), `${header}${tail}`);
|
|
146787
|
+
writeFileSync14(abs(statusFile), `BLOCKED
|
|
146788
|
+
`);
|
|
146789
|
+
appendFileSync7(abs(logFile), `git-tree: ${tree.ok ? tree.tree : "unbound"}
|
|
146790
|
+
`);
|
|
146791
|
+
process.stderr.write(`quality gate BLOCKED \u2014 ${header}`);
|
|
146792
|
+
return {
|
|
146793
|
+
status: "BLOCKED",
|
|
146794
|
+
attempts: gateAttempt,
|
|
146795
|
+
logFile,
|
|
146796
|
+
findingsFile,
|
|
146797
|
+
statusFile,
|
|
146798
|
+
attemptFile,
|
|
146799
|
+
receiptFile: undefined
|
|
146800
|
+
};
|
|
146801
|
+
}
|
|
146325
146802
|
if (gateRc === 0) {
|
|
146326
146803
|
const bytes = existsSync23(abs(logFile)) ? statSync15(abs(logFile)).size : 0;
|
|
146327
146804
|
const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : "";
|
|
@@ -146382,12 +146859,19 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146382
146859
|
return 2;
|
|
146383
146860
|
}
|
|
146384
146861
|
const env = { ...rawEnv, wbs };
|
|
146862
|
+
const timeout = resolveGateTimeoutMs(env);
|
|
146863
|
+
if (!timeout.ok) {
|
|
146864
|
+
process.stderr.write(`${timeout.error}
|
|
146865
|
+
`);
|
|
146866
|
+
return 2;
|
|
146867
|
+
}
|
|
146868
|
+
const gateOptions = { ...options, gateTimeoutMs: timeout.ms };
|
|
146385
146869
|
if (mode === "light") {
|
|
146386
|
-
runLightGate(env,
|
|
146387
|
-
return 0;
|
|
146870
|
+
const light = runLightGate(env, gateOptions);
|
|
146871
|
+
return light.blocked === undefined ? 0 : 2;
|
|
146388
146872
|
}
|
|
146389
146873
|
if (mode === "deferred") {
|
|
146390
|
-
return runDeferredGate(env,
|
|
146874
|
+
return runDeferredGate(env, gateOptions).status === "DEFERRED" ? 0 : 1;
|
|
146391
146875
|
}
|
|
146392
146876
|
if (mode === "status") {
|
|
146393
146877
|
const runDir = join78(options.cwd ?? ".", ".spur", "run");
|
|
@@ -146410,7 +146894,10 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146410
146894
|
return 0;
|
|
146411
146895
|
}
|
|
146412
146896
|
try {
|
|
146413
|
-
|
|
146897
|
+
const result = runQualityGate(mode, env, gateOptions);
|
|
146898
|
+
if (result.status === "PASS")
|
|
146899
|
+
return 0;
|
|
146900
|
+
return result.status === "BLOCKED" ? 2 : 1;
|
|
146414
146901
|
} catch (error) {
|
|
146415
146902
|
if (!(error instanceof GateLockTimeoutError))
|
|
146416
146903
|
throw error;
|
|
@@ -146418,7 +146905,7 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146418
146905
|
return 2;
|
|
146419
146906
|
}
|
|
146420
146907
|
}
|
|
146421
|
-
var MAX_GATE_ATTEMPTS = 5, MAX_FINDINGS = 20, RETRY_DELAY_MS_DEFAULT = 1e4, RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS", LOCKED_PATTERN, FINDINGS_PATTERN, COVERAGE_ROW_PATTERN, RECEIPT_SCHEMA_VERSION = "check-receipt/v1",
|
|
146908
|
+
var MAX_GATE_ATTEMPTS = 5, MAX_FINDINGS = 20, RETRY_DELAY_MS_DEFAULT = 1e4, RETRY_DELAY_MS_ENV = "SPUR_QUALITY_GATE_RETRY_DELAY_MS", GATE_TIMEOUT_MS_ENV = "gateTimeoutMs", GATE_TIMEOUT_MS_DEFAULT = 900000, LOCKED_PATTERN, FINDINGS_PATTERN, COVERAGE_ROW_PATTERN, RECEIPT_SCHEMA_VERSION = "check-receipt/v1", TEST_FILE_PATTERN2, GATE_LOCK_DIR_ENV = "SPUR_GATE_LOCK_DIR", GATE_LOCK_POLL_MS_ENV = "SPUR_GATE_LOCK_POLL_MS", GATE_LOCK_OFF_ENV = "SPUR_GATE_LOCK", GATE_LOCK_TOKEN_ENV = "SPUR_GATE_LOCK_TOKEN", GATE_LOCK_POLL_MS_DEFAULT = 5000, GATE_LOCK_TIMEOUT_MS_ENV = "SPUR_GATE_LOCK_TIMEOUT_MS", GATE_LOCK_TIMEOUT_MS_DEFAULT, GATE_LOCK_MKDIR_HOLD_MS_ENV = "SPUR_GATE_LOCK_TEST_MKDIR_HOLD_MS", GATE_LOCK_LOST_RACE_SLEEP_MS = 20, GATE_LOCK_LOST_RACE_LOG_AT = 250, GateLockTimeoutError, QUALITY_GATE_MODES, QUALITY_GATE_USAGE = "usage: spur task gate <wbs> <run|recheck|light|deferred|status> (env: qualityGateCmd, gateProbeCmd, runId)";
|
|
146422
146909
|
var init_quality_gate = __esm(() => {
|
|
146423
146910
|
init_dist();
|
|
146424
146911
|
init_proof_input_fingerprint();
|
|
@@ -146426,7 +146913,7 @@ var init_quality_gate = __esm(() => {
|
|
|
146426
146913
|
LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
|
|
146427
146914
|
FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
146428
146915
|
COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
|
|
146429
|
-
|
|
146916
|
+
TEST_FILE_PATTERN2 = /\.test\.tsx?$/;
|
|
146430
146917
|
GATE_LOCK_TIMEOUT_MS_DEFAULT = 2 * 60 * 60 * 1000;
|
|
146431
146918
|
GateLockTimeoutError = class GateLockTimeoutError extends Error {
|
|
146432
146919
|
constructor(message) {
|
|
@@ -148318,6 +148805,17 @@ var require_src10 = __commonJS(function(exports, module) {
|
|
|
148318
148805
|
});
|
|
148319
148806
|
|
|
148320
148807
|
// ../../packages/app/src/workflow/step-profile.ts
|
|
148808
|
+
function rowKey(node, actionKind) {
|
|
148809
|
+
return `${node}\x00${actionKind}`;
|
|
148810
|
+
}
|
|
148811
|
+
function hostMsByRow(pauses) {
|
|
148812
|
+
const totals = new Map;
|
|
148813
|
+
for (const pause of pauses) {
|
|
148814
|
+
const key = rowKey(pause.node, pause.action ?? "agent.run");
|
|
148815
|
+
totals.set(key, (totals.get(key) ?? 0) + pause.hostDurationMs);
|
|
148816
|
+
}
|
|
148817
|
+
return totals;
|
|
148818
|
+
}
|
|
148321
148819
|
function nearestRankP50(values) {
|
|
148322
148820
|
if (values.length === 0)
|
|
148323
148821
|
return null;
|
|
@@ -148441,11 +148939,27 @@ function buildRows(executions, windowSec) {
|
|
|
148441
148939
|
}
|
|
148442
148940
|
function buildStepProfile(input) {
|
|
148443
148941
|
const executions = input.runs.flatMap((run) => extractExecutions(run.runId, run.events));
|
|
148942
|
+
const hostTotals = new Map;
|
|
148943
|
+
for (const run of input.runs) {
|
|
148944
|
+
for (const [key, ms] of hostMsByRow(run.hostPauses ?? [])) {
|
|
148945
|
+
hostTotals.set(key, (hostTotals.get(key) ?? 0) + ms);
|
|
148946
|
+
}
|
|
148947
|
+
}
|
|
148948
|
+
const rows = buildRows(executions, input.windowSec).map((row) => {
|
|
148949
|
+
const hostMs = hostTotals.get(rowKey(row.node, row.actionKind));
|
|
148950
|
+
return hostMs === undefined ? row : { ...row, hostMs };
|
|
148951
|
+
});
|
|
148952
|
+
const stalledRuns = nonDryRuns(input.paused ?? []).filter((entry) => entry.stalled === true).map((entry) => ({
|
|
148953
|
+
runId: entry.runId,
|
|
148954
|
+
stalledMs: entry.stalledMs ?? 0,
|
|
148955
|
+
resume: `spur workflow continue ${entry.runId}`
|
|
148956
|
+
}));
|
|
148444
148957
|
return {
|
|
148445
148958
|
workflow: input.workflow,
|
|
148446
148959
|
windowSec: input.windowSec,
|
|
148447
148960
|
sampledRuns: input.runs.length,
|
|
148448
|
-
rows
|
|
148961
|
+
rows,
|
|
148962
|
+
...stalledRuns.length > 0 ? { stalledRuns } : {}
|
|
148449
148963
|
};
|
|
148450
148964
|
}
|
|
148451
148965
|
function nonDryRuns(entries) {
|
|
@@ -148469,16 +148983,22 @@ function formatStepProfileHuman(profile) {
|
|
|
148469
148983
|
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
148470
148984
|
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
148471
148985
|
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
148986
|
+
...row.hostMs === undefined ? [] : [`host=${seconds(row.hostMs)}`.padEnd(12)],
|
|
148472
148987
|
`session=${row.session ?? "?"}`.padEnd(15),
|
|
148473
148988
|
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
148474
148989
|
`flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
|
|
148475
148990
|
].join(" "));
|
|
148476
148991
|
}
|
|
148992
|
+
for (const run of profile.stalledRuns ?? [])
|
|
148993
|
+
lines.push(`${run.runId}: ${stalledLabel(run.runId, run.stalledMs)}`);
|
|
148477
148994
|
return `${lines.join(`
|
|
148478
148995
|
`)}
|
|
148479
148996
|
`;
|
|
148480
148997
|
}
|
|
148481
148998
|
var DEFAULT_LAST = 20, DEFAULT_WINDOW_SEC = 300;
|
|
148999
|
+
var init_step_profile = __esm(() => {
|
|
149000
|
+
init_host_pause();
|
|
149001
|
+
});
|
|
148482
149002
|
|
|
148483
149003
|
// ../../packages/app/src/workflow/step-profile-cli.ts
|
|
148484
149004
|
function positiveInt(raw, fallback) {
|
|
@@ -148525,6 +149045,12 @@ function runFailure(spurBin, result, what) {
|
|
|
148525
149045
|
`).slice(-1)[0] ?? "";
|
|
148526
149046
|
return failure2(`${what} failed (exit ${result.exitCode}${detail === "" ? "" : `: ${detail}`}) [${spurBin}]`);
|
|
148527
149047
|
}
|
|
149048
|
+
function parseTraceEntries(stdout) {
|
|
149049
|
+
const parsed = JSON.parse(stdout);
|
|
149050
|
+
if (!Array.isArray(parsed.entries))
|
|
149051
|
+
throw new Error('no "entries" array');
|
|
149052
|
+
return parsed.entries;
|
|
149053
|
+
}
|
|
148528
149054
|
function runStepProfileCli(argv, defaultSpurBin = "spur", defaultSpurArgv) {
|
|
148529
149055
|
const spurArgv = argv.includes("--spur-bin") ? undefined : defaultSpurArgv;
|
|
148530
149056
|
const args = parseStepProfileCliArgs(argv, defaultSpurBin);
|
|
@@ -148550,10 +149076,7 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148550
149076
|
return runFailure(args.spurBin, list, `spur ${listArgs.join(" ")}`);
|
|
148551
149077
|
let entries;
|
|
148552
149078
|
try {
|
|
148553
|
-
|
|
148554
|
-
if (!Array.isArray(parsed.entries))
|
|
148555
|
-
throw new Error('no "entries" array');
|
|
148556
|
-
entries = parsed.entries;
|
|
149079
|
+
entries = parseTraceEntries(list.stdout);
|
|
148557
149080
|
} catch (err) {
|
|
148558
149081
|
return failure2(`spur workflow trace output did not parse as JSON: ${String(err)}`);
|
|
148559
149082
|
}
|
|
@@ -148567,12 +149090,27 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148567
149090
|
const parsed = JSON.parse(timeline.stdout);
|
|
148568
149091
|
if (!Array.isArray(parsed.events))
|
|
148569
149092
|
throw new Error('no "events" array');
|
|
148570
|
-
runs.push({
|
|
149093
|
+
runs.push({
|
|
149094
|
+
runId: entry.runId,
|
|
149095
|
+
events: parsed.events,
|
|
149096
|
+
...entry.hostPauses !== undefined ? { hostPauses: entry.hostPauses } : {}
|
|
149097
|
+
});
|
|
148571
149098
|
} catch (err) {
|
|
148572
149099
|
return failure2(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
|
|
148573
149100
|
}
|
|
148574
149101
|
}
|
|
148575
|
-
const
|
|
149102
|
+
const pausedArgs = [...listArgs];
|
|
149103
|
+
pausedArgs[pausedArgs.indexOf("done")] = "paused";
|
|
149104
|
+
const pausedList = runSpurJson(args.spurBin, pausedArgs, spurArgv);
|
|
149105
|
+
if (!pausedList.ok)
|
|
149106
|
+
return runFailure(args.spurBin, pausedList, `spur ${pausedArgs.join(" ")}`);
|
|
149107
|
+
let paused;
|
|
149108
|
+
try {
|
|
149109
|
+
paused = parseTraceEntries(pausedList.stdout);
|
|
149110
|
+
} catch (err) {
|
|
149111
|
+
return failure2(`spur workflow trace output did not parse as JSON: ${String(err)}`);
|
|
149112
|
+
}
|
|
149113
|
+
const profile = buildStepProfile({ workflow: args.workflow, windowSec: args.windowSec, runs, paused });
|
|
148576
149114
|
return {
|
|
148577
149115
|
exitCode: 0,
|
|
148578
149116
|
stdout: args.json ? `${JSON.stringify(profile, null, 2)}
|
|
@@ -148583,6 +149121,7 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148583
149121
|
var STEP_PROFILE_USAGE;
|
|
148584
149122
|
var init_step_profile_cli = __esm(() => {
|
|
148585
149123
|
init_dist5();
|
|
149124
|
+
init_step_profile();
|
|
148586
149125
|
STEP_PROFILE_USAGE = `usage: spur workflow profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
|
|
148587
149126
|
|
|
148588
149127
|
Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
|
|
@@ -156487,7 +157026,7 @@ var SHARED_OPTIONS = {
|
|
|
156487
157026
|
"pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all"
|
|
156488
157027
|
],
|
|
156489
157028
|
statusDoneFailed: ["--status <status>", "Filter by status: done, failed"],
|
|
156490
|
-
|
|
157029
|
+
statusWorkflowRun: ["--status <status>", "Filter by status: done, failed, running, paused, interrupted"],
|
|
156491
157030
|
untilAgent: ["--until <state>", "Lifecycle state to wait for (repeatable OR)"],
|
|
156492
157031
|
untilMessage: ["--until <state>", "Wait target for --wait: injected|invoke-exit"],
|
|
156493
157032
|
verboseWorkflow: ["--verbose", "Include transitions and correlation diagnostics in human progress"],
|
|
@@ -159678,7 +160217,7 @@ function registerTaskUpdateCommand(task, context) {
|
|
|
159678
160217
|
// src/commands/task/verdict.ts
|
|
159679
160218
|
init_src4();
|
|
159680
160219
|
function registerTaskVerdictCommand(task, context) {
|
|
159681
|
-
task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
|
|
160220
|
+
task.command("verdict").summary("Derive PASS/PARTIAL/FAIL/UNKNOWN verdict from verify answer text (replaces pipeline grep/shell).").argument("<wbs>", "Task WBS number").option("--from-answer <path>", "Path to verify answer text file").option("--replay", "Re-execute the test files the verdict rows cite; a failure downgrades PASS to PARTIAL").option(...SHARED_OPTIONS.folderTasks).option(...SHARED_OPTIONS.json).option(...SHARED_OPTIONS.jsonEnvelope).action(async (wbs, options) => {
|
|
159682
160221
|
await Promise.resolve().then(() => init_src4());
|
|
159683
160222
|
const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
|
|
159684
160223
|
let answerText;
|
|
@@ -159697,6 +160236,12 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159697
160236
|
featuresDir: context.fs.resolve(featuresDir)
|
|
159698
160237
|
};
|
|
159699
160238
|
const lintFindings = await lintVerifyAnswerForTask(wbs, answerText, corpusDeps);
|
|
160239
|
+
const lintPath = `.spur/run/${wbs}-verdict-lint.txt`;
|
|
160240
|
+
const clearLintFile = async () => {
|
|
160241
|
+
try {
|
|
160242
|
+
await context.fs.deleteFile(lintPath);
|
|
160243
|
+
} catch {}
|
|
160244
|
+
};
|
|
159700
160245
|
if (lintFindings.length > 0) {
|
|
159701
160246
|
if (options.json) {
|
|
159702
160247
|
context.output.write(JSON.stringify({ wbs, lintFindings }, null, 2));
|
|
@@ -159705,9 +160250,14 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159705
160250
|
for (const f of lintFindings)
|
|
159706
160251
|
context.output.error(` line ${f.line}: ${f.message}`);
|
|
159707
160252
|
}
|
|
160253
|
+
await context.fs.ensureDir(".spur/run");
|
|
160254
|
+
await context.fs.writeFile(lintPath, `${lintFindings.map((f) => `line ${f.line}: ${f.message}`).join(`
|
|
160255
|
+
`)}
|
|
160256
|
+
`);
|
|
159708
160257
|
context.setExitCode(1);
|
|
159709
160258
|
return;
|
|
159710
160259
|
}
|
|
160260
|
+
await clearLintFile();
|
|
159711
160261
|
const taskCheckPassed = true;
|
|
159712
160262
|
const taskAc = await readTaskAcceptanceCriteria(wbs, corpusDeps);
|
|
159713
160263
|
const result = deriveVerdict(answerText, taskCheckPassed, taskAc);
|
|
@@ -159717,7 +160267,19 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159717
160267
|
context.setExitCode(1);
|
|
159718
160268
|
return;
|
|
159719
160269
|
}
|
|
159720
|
-
const
|
|
160270
|
+
const replay = options.replay === true ? await replayCitedTests([...result.requirements, ...result.acceptanceCriteria ?? []], {
|
|
160271
|
+
cwd: context.cwd,
|
|
160272
|
+
builder: context.spurConfig?.builder
|
|
160273
|
+
}) : undefined;
|
|
160274
|
+
const downgraded = replay !== undefined && replay.findings.length > 0 && result.verdict === "PASS";
|
|
160275
|
+
if (downgraded)
|
|
160276
|
+
result.verdict = "PARTIAL";
|
|
160277
|
+
const jsonOut = JSON.stringify({
|
|
160278
|
+
wbs,
|
|
160279
|
+
...result,
|
|
160280
|
+
...replay !== undefined ? { replay } : {},
|
|
160281
|
+
source: "spur-task-verdict"
|
|
160282
|
+
}, null, 2);
|
|
159721
160283
|
await context.fs.ensureDir(".spur/run");
|
|
159722
160284
|
await context.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
|
|
159723
160285
|
`);
|
|
@@ -159725,6 +160287,12 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159725
160287
|
context.output.write(jsonOut);
|
|
159726
160288
|
} else {
|
|
159727
160289
|
context.output.write(`Verdict: ${result.verdict} (${result.requirements.length} requirements, ${result.checks.length} checks)`);
|
|
160290
|
+
if (replay !== undefined) {
|
|
160291
|
+
const report = formatReplayReport(replay);
|
|
160292
|
+
context.output.write(report.summary);
|
|
160293
|
+
for (const line of report.findings)
|
|
160294
|
+
context.output.error(line);
|
|
160295
|
+
}
|
|
159728
160296
|
}
|
|
159729
160297
|
if (result.verdict !== "PASS") {
|
|
159730
160298
|
context.setExitCode(1);
|
|
@@ -164625,7 +165193,7 @@ init_src2();
|
|
|
164625
165193
|
var CLI_CONFIG = {
|
|
164626
165194
|
binaryName: "spur",
|
|
164627
165195
|
binaryLabel: "spur",
|
|
164628
|
-
binaryVersion: "0.4.
|
|
165196
|
+
binaryVersion: "0.4.28",
|
|
164629
165197
|
configDir: ".spur",
|
|
164630
165198
|
configFile: ".spur/config.yaml",
|
|
164631
165199
|
databaseFile: ".spur/spur.db"
|
|
@@ -172364,7 +172932,7 @@ init_dist5();
|
|
|
172364
172932
|
import { homedir as homedir17 } from "os";
|
|
172365
172933
|
import { basename as basename23, join as join95, relative as relative19, resolve as resolve37 } from "path";
|
|
172366
172934
|
// ../server/package.json
|
|
172367
|
-
var version4 = "0.4.
|
|
172935
|
+
var version4 = "0.4.28";
|
|
172368
172936
|
|
|
172369
172937
|
// ../server/src/modules/health/index.ts
|
|
172370
172938
|
var startedAt = Date.now();
|
|
@@ -177922,6 +178490,7 @@ init_src2();
|
|
|
177922
178490
|
init_dist5();
|
|
177923
178491
|
init_dist2();
|
|
177924
178492
|
init_composition_baseline();
|
|
178493
|
+
init_host_pause();
|
|
177925
178494
|
init_run_measurements();
|
|
177926
178495
|
init_terminal_reason();
|
|
177927
178496
|
init_run_measurements();
|
|
@@ -177959,9 +178528,13 @@ async function buildReportJson(db, workflows, opts = {}) {
|
|
|
177959
178528
|
function renderStateLines(rows) {
|
|
177960
178529
|
return rows.map((s) => {
|
|
177961
178530
|
const wall = s.wallMsP50 === null || s.wallMsP90 === null ? "n/a" : `p50=${s.wallMsP50}ms p90=${s.wallMsP90}ms`;
|
|
177962
|
-
|
|
178531
|
+
const host = s.hostMs === undefined ? "" : ` host=${s.hostMs}ms`;
|
|
178532
|
+
return `${s.workflow}/${s.state}: visits=${s.visits} agent.run=${s.agentRunCount} wall=${wall}${host} retries=${s.retries}`;
|
|
177963
178533
|
});
|
|
177964
178534
|
}
|
|
178535
|
+
function renderStalledLines(rows) {
|
|
178536
|
+
return rows.map((r) => `${r.workflow}: ${stalledLabel(r.runId, r.stalledMs)}`);
|
|
178537
|
+
}
|
|
177965
178538
|
function renderWorkflowLines(rows) {
|
|
177966
178539
|
return rows.map((m) => {
|
|
177967
178540
|
const excluded = m.dryRuns > 0 || m.nonTerminalRuns > 0 ? ` [excluded ${m.dryRuns} dry, ${m.nonTerminalRuns} non-terminal]` : "";
|
|
@@ -178177,6 +178750,11 @@ function registerWorkflowCostCommand(workflow, context, deps = {}) {
|
|
|
178177
178750
|
context.output.write(line);
|
|
178178
178751
|
}
|
|
178179
178752
|
}
|
|
178753
|
+
if (options.json !== true) {
|
|
178754
|
+
for (const line of renderStalledLines(await readStalledRuns(db, targets))) {
|
|
178755
|
+
context.output.write(line);
|
|
178756
|
+
}
|
|
178757
|
+
}
|
|
178180
178758
|
} finally {
|
|
178181
178759
|
db.close();
|
|
178182
178760
|
}
|
|
@@ -178537,8 +179115,10 @@ function formatWorkflowProgress(projection) {
|
|
|
178537
179115
|
}
|
|
178538
179116
|
function registerWorkflowProgressCommand(workflow, context) {
|
|
178539
179117
|
workflow.command("progress").description("Project the structured progress of a workflow run: current state, per-action attempts, and candidate next transitions.").argument("<run-id>", "Run id to project").option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (runId, options) => {
|
|
179118
|
+
const db = await context.getDb();
|
|
179119
|
+
await reapIfWorkerLost(db, context.cwd, runId);
|
|
178540
179120
|
const projection = await projectWorkflowProgress(runId, {
|
|
178541
|
-
db
|
|
179121
|
+
db,
|
|
178542
179122
|
projectRoot: context.cwd
|
|
178543
179123
|
});
|
|
178544
179124
|
if (projection.diagnostics.some((diagnostic) => diagnostic.code === "orphan-row")) {
|
|
@@ -179152,6 +179732,7 @@ init_src4();
|
|
|
179152
179732
|
import { closeSync as closeSync10, fstatSync as fstatSync5, openSync as openSync10, readFileSync as readFileSync32, readSync as readSync3 } from "fs";
|
|
179153
179733
|
import { join as join107 } from "path";
|
|
179154
179734
|
import { setTimeout as sleep7 } from "timers/promises";
|
|
179735
|
+
var TRACE_STATUSES = ["done", "failed", "running", "paused", "interrupted"];
|
|
179155
179736
|
function formatTraceList2(result) {
|
|
179156
179737
|
if (result.entries.length === 0) {
|
|
179157
179738
|
return "No workflow runs.";
|
|
@@ -179180,6 +179761,9 @@ function formatTraceTimeline(result) {
|
|
|
179180
179761
|
`Outcome: ${run.outcome} Events: ${events.length}`,
|
|
179181
179762
|
""
|
|
179182
179763
|
];
|
|
179764
|
+
if (run.stalled === true && run.stalledMs !== undefined) {
|
|
179765
|
+
lines.splice(4, 0, `Stalled: paused ${Math.round(run.stalledMs / 1000)}s \u2014 resume: spur workflow continue ${run.runId}`);
|
|
179766
|
+
}
|
|
179183
179767
|
if (run.nextAction !== undefined)
|
|
179184
179768
|
lines.splice(4, 0, `Next: ${run.nextAction.label} \u2014 ${run.nextAction.value}`);
|
|
179185
179769
|
if (result.outputArtifact !== undefined) {
|
|
@@ -179238,6 +179822,14 @@ function formatTimelineEvent(event) {
|
|
|
179238
179822
|
function followDeadlineLine(runId, elapsedMs, status) {
|
|
179239
179823
|
return `Follow timed out after ${elapsedMs}ms: run ${runId} status=${status} \u2014 run continues; resume with spur workflow trace ${runId} --follow`;
|
|
179240
179824
|
}
|
|
179825
|
+
function followFinalizedLine(timeline) {
|
|
179826
|
+
const run = timeline.run;
|
|
179827
|
+
const reason = run.failureReason ? ` \u2014 ${run.failureReason}` : "";
|
|
179828
|
+
const duration = run.durationMs === null ? "unavailable" : `${run.durationMs}ms`;
|
|
179829
|
+
const next = run.nextAction === undefined ? "" : `
|
|
179830
|
+
Next: ${run.nextAction.label} \u2014 ${run.nextAction.value}`;
|
|
179831
|
+
return `Run finalized: ${run.status}${reason} \u2014 outcome=${run.outcome} duration=${duration}${next}`;
|
|
179832
|
+
}
|
|
179241
179833
|
async function followTrace(service, runId, pollMs, write, wait = (ms) => sleep7(ms), now = Date.now, deadlineMs) {
|
|
179242
179834
|
const start = now();
|
|
179243
179835
|
const deadline = deadlineMs === undefined ? Number.POSITIVE_INFINITY : start + deadlineMs;
|
|
@@ -179260,8 +179852,10 @@ async function followTrace(service, runId, pollMs, write, wait = (ms) => sleep7(
|
|
|
179260
179852
|
}
|
|
179261
179853
|
write(formatTraceTimeline(timeline));
|
|
179262
179854
|
const seen = new Set(timeline.events.map((event) => JSON.stringify(event)));
|
|
179263
|
-
if (isTerminalTraceStatus(timeline.run.status))
|
|
179855
|
+
if (isTerminalTraceStatus(timeline.run.status)) {
|
|
179856
|
+
write(followFinalizedLine(timeline));
|
|
179264
179857
|
return false;
|
|
179858
|
+
}
|
|
179265
179859
|
while (true) {
|
|
179266
179860
|
await wait(pollMs);
|
|
179267
179861
|
const next = await service.trace(runId);
|
|
@@ -179273,12 +179867,7 @@ async function followTrace(service, runId, pollMs, write, wait = (ms) => sleep7(
|
|
|
179273
179867
|
write(formatTimelineEvent(event));
|
|
179274
179868
|
}
|
|
179275
179869
|
if (isTerminalTraceStatus(next.run.status)) {
|
|
179276
|
-
|
|
179277
|
-
const duration = next.run.durationMs === null ? "unavailable" : `${next.run.durationMs}ms`;
|
|
179278
|
-
write(`Run finalized: ${next.run.status}${reason} \u2014 outcome=${next.run.outcome} duration=${duration}`);
|
|
179279
|
-
if (next.run.nextAction !== undefined) {
|
|
179280
|
-
write(`Next: ${next.run.nextAction.label} \u2014 ${next.run.nextAction.value}`);
|
|
179281
|
-
}
|
|
179870
|
+
write(followFinalizedLine(next));
|
|
179282
179871
|
return false;
|
|
179283
179872
|
}
|
|
179284
179873
|
const at = now();
|
|
@@ -179410,7 +179999,7 @@ function formatTokens2(n) {
|
|
|
179410
179999
|
return String(n);
|
|
179411
180000
|
}
|
|
179412
180001
|
function registerWorkflowTraceCommand(workflow, context) {
|
|
179413
|
-
workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.
|
|
180002
|
+
workflow.command("trace").description("Show persisted workflow run history.").argument("[run-id]", "Run ID for per-run timeline detail").option("--workflow <name>", "Filter by workflow name").option(...SHARED_OPTIONS.statusWorkflowRun).option(...SHARED_OPTIONS.since).option(...SHARED_OPTIONS.last, "20").option("--follow", "Replay a run timeline and poll persisted state until it becomes terminal").option(...SHARED_OPTIONS.pollWorkflow, "1000").option("--output", "With --follow: stream .spur/memory/runs/<RUNID>.md instead of the DB timeline").option(...SHARED_OPTIONS.timeout).option(...SHARED_OPTIONS.jsonSupported).option(...SHARED_OPTIONS.jsonEnvelope).action(async (runId, options) => {
|
|
179414
180003
|
const svc = makeWorkflowService(context);
|
|
179415
180004
|
const last = parseInt(options.last, 10);
|
|
179416
180005
|
if (Number.isNaN(last) || last < 1) {
|
|
@@ -179455,8 +180044,8 @@ function registerWorkflowTraceCommand(workflow, context) {
|
|
|
179455
180044
|
context.setExitCode(1);
|
|
179456
180045
|
return;
|
|
179457
180046
|
}
|
|
179458
|
-
if (options.status !== undefined && !
|
|
179459
|
-
writeJsonError(context.output, options,
|
|
180047
|
+
if (options.status !== undefined && !TRACE_STATUSES.includes(options.status)) {
|
|
180048
|
+
writeJsonError(context.output, options, `--status must be one of: ${TRACE_STATUSES.join(", ")}`, "VALIDATION_FAILED");
|
|
179460
180049
|
context.setExitCode(1);
|
|
179461
180050
|
return;
|
|
179462
180051
|
}
|