@gobing-ai/spur 0.4.27 → 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/workflows/task-pipeline.yaml +63 -4
- package/package.json +1 -1
- 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/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/spur.js +627 -131
package/spur.js
CHANGED
|
@@ -55606,6 +55606,38 @@ async function readWorkflowStateVisitRows(db, workflows, opts = {}) {
|
|
|
55606
55606
|
}
|
|
55607
55607
|
return rows;
|
|
55608
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
|
+
}
|
|
55609
55641
|
var TERMINAL_RUN_STATUSES;
|
|
55610
55642
|
var init_workflow_measurements = __esm(() => {
|
|
55611
55643
|
TERMINAL_RUN_STATUSES = ["done", "failed", "cancelled"];
|
|
@@ -58117,7 +58149,7 @@ var init_run_dao = __esm(() => {
|
|
|
58117
58149
|
});
|
|
58118
58150
|
}
|
|
58119
58151
|
traceRows(filter) {
|
|
58120
|
-
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
|
|
58121
58153
|
FROM runs
|
|
58122
58154
|
WHERE (?1 IS NULL OR workflow_name = ?1)
|
|
58123
58155
|
AND (?2 IS NULL OR status = ?2)
|
|
@@ -58131,7 +58163,7 @@ var init_run_dao = __esm(() => {
|
|
|
58131
58163
|
LIMIT ?6`, filter.workflow ?? null, filter.status ?? null, filter.since ?? null, filter.before?.started_at ?? null, filter.before?.id ?? null, filter.limit);
|
|
58132
58164
|
}
|
|
58133
58165
|
traceRowById(runId) {
|
|
58134
|
-
return this.adapter.queryFirst("SELECT id, workflow_name, mode, status, agent, started_at, completed_at, terminal_reason, metadata_json, pid 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);
|
|
58135
58167
|
}
|
|
58136
58168
|
mergeMetadata(runId, patch) {
|
|
58137
58169
|
return this.adapter.run(`UPDATE runs
|
|
@@ -104895,7 +104927,16 @@ class AgentRunActionRunner {
|
|
|
104895
104927
|
error: `agent.run: work item for '${context.stateOrNodeId}' could not be written: ${error.message}`
|
|
104896
104928
|
};
|
|
104897
104929
|
}
|
|
104898
|
-
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
|
+
};
|
|
104899
104940
|
}
|
|
104900
104941
|
async execute(options, context, execution) {
|
|
104901
104942
|
const startedAt = new Date().toISOString();
|
|
@@ -121379,6 +121420,102 @@ var init_process_inventory_service = __esm(() => {
|
|
|
121379
121420
|
init_process_inspector();
|
|
121380
121421
|
});
|
|
121381
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
|
+
|
|
121382
121519
|
// ../../packages/app/src/workflow/run-measurements.ts
|
|
121383
121520
|
function median(values) {
|
|
121384
121521
|
if (values.length === 0)
|
|
@@ -121479,6 +121616,18 @@ async function readStateMetrics(db, workflows, opts = {}) {
|
|
|
121479
121616
|
row.visits += t.visitsInRun;
|
|
121480
121617
|
row.retries += Math.max(0, t.visitsInRun - 1);
|
|
121481
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
|
+
}
|
|
121482
121631
|
const rows = [];
|
|
121483
121632
|
for (const [workflow, states] of byWorkflow) {
|
|
121484
121633
|
for (const [state, row] of states) {
|
|
@@ -121489,14 +121638,27 @@ async function readStateMetrics(db, workflows, opts = {}) {
|
|
|
121489
121638
|
agentRunCount: row.agentRunCount,
|
|
121490
121639
|
wallMsP50: nearestRankPercentile(row.wallSamples, 50),
|
|
121491
121640
|
wallMsP90: nearestRankPercentile(row.wallSamples, 90),
|
|
121492
|
-
retries: row.retries
|
|
121641
|
+
retries: row.retries,
|
|
121642
|
+
...row.hostMs !== undefined ? { hostMs: row.hostMs } : {}
|
|
121493
121643
|
});
|
|
121494
121644
|
}
|
|
121495
121645
|
}
|
|
121496
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);
|
|
121497
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
|
+
}
|
|
121498
121659
|
var init_run_measurements = __esm(() => {
|
|
121499
121660
|
init_src2();
|
|
121661
|
+
init_host_pause();
|
|
121500
121662
|
init_terminal_reason();
|
|
121501
121663
|
});
|
|
121502
121664
|
|
|
@@ -130963,6 +131125,174 @@ var init_token_ledger_watcher = __esm(() => {
|
|
|
130963
131125
|
init_token_ledger_service();
|
|
130964
131126
|
});
|
|
130965
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
|
+
|
|
130966
131296
|
// ../../packages/app/src/workflow/checkpoint-contract.ts
|
|
130967
131297
|
function parseCheckpointMetadata(raw) {
|
|
130968
131298
|
if (!raw.startsWith("---"))
|
|
@@ -131429,6 +131759,16 @@ function commandGateOutcome(exitCode) {
|
|
|
131429
131759
|
return "FAIL";
|
|
131430
131760
|
return "BLOCKED";
|
|
131431
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
|
+
}
|
|
131432
131772
|
function sleep4(ms) {
|
|
131433
131773
|
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
131434
131774
|
}
|
|
@@ -131498,13 +131838,21 @@ class CommandGateActionRunner {
|
|
|
131498
131838
|
const maxAttempts = Math.max(1, typeof retry.maxAttempts === "number" ? retry.maxAttempts : 1);
|
|
131499
131839
|
const delayMs = Math.max(0, typeof retry.delayMs === "number" ? retry.delayMs : 1000);
|
|
131500
131840
|
const retryOn = Array.isArray(retry.on) ? retry.on : [];
|
|
131501
|
-
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;
|
|
131502
131849
|
const env = childProcessEnv(context.vars);
|
|
131503
131850
|
await this.fileSystem.ensureDir(normalize3(join59(normalized, "..")));
|
|
131504
131851
|
let lastStdout = "";
|
|
131505
131852
|
let lastStderr = "";
|
|
131506
131853
|
let lastExitCode = -1;
|
|
131507
131854
|
let attemptsRun = 0;
|
|
131855
|
+
let timedOut = false;
|
|
131508
131856
|
for (let attempt = 1;attempt <= maxAttempts; attempt++) {
|
|
131509
131857
|
attemptsRun = attempt;
|
|
131510
131858
|
const res = await this.processExecutor.run({
|
|
@@ -131519,6 +131867,7 @@ class CommandGateActionRunner {
|
|
|
131519
131867
|
lastStdout = res.stdout;
|
|
131520
131868
|
lastStderr = res.stderr;
|
|
131521
131869
|
lastExitCode = res.exitCode ?? -1;
|
|
131870
|
+
timedOut = res.outcome === "timeout";
|
|
131522
131871
|
if (res.exitCode === 0) {
|
|
131523
131872
|
await this.fileSystem.writeFile(normalized, `PASS
|
|
131524
131873
|
`);
|
|
@@ -131553,7 +131902,8 @@ ${lastStderr}`;
|
|
|
131553
131902
|
attempts: attemptsRun,
|
|
131554
131903
|
stdout: lastStdout,
|
|
131555
131904
|
stderr: lastStderr,
|
|
131556
|
-
resultFile: normalized
|
|
131905
|
+
resultFile: normalized,
|
|
131906
|
+
...timedOut ? { timedOut: true, timeoutMs } : {}
|
|
131557
131907
|
};
|
|
131558
131908
|
if (softFail) {
|
|
131559
131909
|
return { ok: true, data: failureData };
|
|
@@ -135179,13 +135529,14 @@ async function outputArtifactForRun(cwd, runId) {
|
|
|
135179
135529
|
}
|
|
135180
135530
|
return;
|
|
135181
135531
|
}
|
|
135182
|
-
function rowToTraceEntry(row, cwd) {
|
|
135532
|
+
function rowToTraceEntry(row, cwd, nowMs = Date.now(), nodeBudgetMs) {
|
|
135183
135533
|
let isDryRun = false;
|
|
135184
135534
|
let failureReason;
|
|
135185
135535
|
let version;
|
|
135186
135536
|
let definitionDigest;
|
|
135187
135537
|
let startState;
|
|
135188
135538
|
let continuedFrom;
|
|
135539
|
+
let hostPauses;
|
|
135189
135540
|
try {
|
|
135190
135541
|
const meta = JSON.parse(row.metadata_json);
|
|
135191
135542
|
isDryRun = meta.dryRun === true;
|
|
@@ -135206,6 +135557,9 @@ function rowToTraceEntry(row, cwd) {
|
|
|
135206
135557
|
if (typeof meta.continuedFrom === "string" && meta.continuedFrom !== "") {
|
|
135207
135558
|
continuedFrom = meta.continuedFrom;
|
|
135208
135559
|
}
|
|
135560
|
+
const pauses = readHostPauses(meta);
|
|
135561
|
+
if (pauses.length > 0)
|
|
135562
|
+
hostPauses = pauses;
|
|
135209
135563
|
} catch {}
|
|
135210
135564
|
const entry = {
|
|
135211
135565
|
runId: row.id,
|
|
@@ -135223,8 +135577,14 @@ function rowToTraceEntry(row, cwd) {
|
|
|
135223
135577
|
...version !== undefined ? { version } : {},
|
|
135224
135578
|
...definitionDigest !== undefined ? { definitionDigest } : {},
|
|
135225
135579
|
...startState !== undefined ? { startState } : {},
|
|
135226
|
-
...continuedFrom !== undefined ? { continuedFrom } : {}
|
|
135580
|
+
...continuedFrom !== undefined ? { continuedFrom } : {},
|
|
135581
|
+
...hostPauses !== undefined ? { hostPauses } : {}
|
|
135227
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
|
+
}
|
|
135228
135588
|
const nextAction = traceNextAction(entry);
|
|
135229
135589
|
if (nextAction !== undefined)
|
|
135230
135590
|
entry.nextAction = nextAction;
|
|
@@ -135269,7 +135629,11 @@ function traceNextAction(run, outputArtifact) {
|
|
|
135269
135629
|
return { label: "Follow run", kind: "command", value: `spur workflow trace ${run.runId} --follow` };
|
|
135270
135630
|
}
|
|
135271
135631
|
if (run.status === "paused" || run.status === "interrupted") {
|
|
135272
|
-
return {
|
|
135632
|
+
return {
|
|
135633
|
+
label: run.stalled === true ? "Resume stalled run" : "Continue run",
|
|
135634
|
+
kind: "command",
|
|
135635
|
+
value: `spur workflow continue ${run.runId}`
|
|
135636
|
+
};
|
|
135273
135637
|
}
|
|
135274
135638
|
if (run.status === "failed" && outputArtifact !== undefined) {
|
|
135275
135639
|
return { label: "Inspect run log", kind: "path", value: outputArtifact };
|
|
@@ -135380,6 +135744,7 @@ var TRACE_IDENTIFIER, TRACE_RESULT_FIELDS, TRACE_INVOCATION_FIELDS;
|
|
|
135380
135744
|
var init_workflow_trace_projection = __esm(() => {
|
|
135381
135745
|
init_dist5();
|
|
135382
135746
|
init_agent_execution();
|
|
135747
|
+
init_host_pause();
|
|
135383
135748
|
init_run_storage();
|
|
135384
135749
|
init_system_event_envelope();
|
|
135385
135750
|
TRACE_IDENTIFIER = /^[A-Za-z0-9._:-]+$/;
|
|
@@ -135558,20 +135923,19 @@ class WorkflowInspectOps {
|
|
|
135558
135923
|
since: filter.since,
|
|
135559
135924
|
limit: filter.last ?? 20
|
|
135560
135925
|
});
|
|
135561
|
-
|
|
135562
|
-
|
|
135563
|
-
|
|
135564
|
-
};
|
|
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 };
|
|
135565
135929
|
}
|
|
135566
135930
|
async traceRun(db, runId, preloadedRow) {
|
|
135567
135931
|
const runDao = new RunDao(db);
|
|
135568
135932
|
const row = preloadedRow ?? await runDao.traceRowById(runId);
|
|
135569
135933
|
if (!row)
|
|
135570
135934
|
throw new Error(`Run not found: ${runId}`);
|
|
135571
|
-
const
|
|
135935
|
+
const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
|
|
135936
|
+
const run = rowToTraceEntry(row, this.ctx.cwd, Date.now(), pausedNodeBudgetMs(actionRows));
|
|
135572
135937
|
const phaseRows = await new PhaseRunDao(db).phaseRowsByRunId(runId);
|
|
135573
135938
|
const transitionRows = await new TransitionRunDao(db).transitionRowsByRunId(runId);
|
|
135574
|
-
const actionRows = await new ActionRunDao(db).actionRowsByRunId(runId);
|
|
135575
135939
|
const costByActionId = new Map;
|
|
135576
135940
|
if (actionRows.some((a) => a.kind === "agent.run")) {
|
|
135577
135941
|
for (const a of actionRows) {
|
|
@@ -135679,6 +136043,7 @@ var init_workflow_inspect_ops = __esm(() => {
|
|
|
135679
136043
|
init_dist5();
|
|
135680
136044
|
init_decision_service();
|
|
135681
136045
|
init_composition_lint();
|
|
136046
|
+
init_host_pause();
|
|
135682
136047
|
init_plan_projection();
|
|
135683
136048
|
init_run_record();
|
|
135684
136049
|
init_workflow_resolver();
|
|
@@ -136198,8 +136563,9 @@ class WorkflowResumeOps {
|
|
|
136198
136563
|
if (persistedDigest !== null && persistedDigest !== resolved.digest && "resumeDefinitionDigest" in rawMeta) {
|
|
136199
136564
|
resumeVars.__definitionDigest = resolved.digest;
|
|
136200
136565
|
}
|
|
136201
|
-
|
|
136202
|
-
|
|
136566
|
+
const stamped = await this.recordHostPause(runId, hostPause.receipt, opts?.now);
|
|
136567
|
+
if (stamped !== undefined) {
|
|
136568
|
+
await writeReceipt(createNodeFileSystem2(launchWorkdir), launchWorkdir, stamped);
|
|
136203
136569
|
}
|
|
136204
136570
|
const result = await withWorktreeRunLease(await this.ctx.getDb(), launchWorkdir, workflow.name, runId, async (signal) => svc.resumeRun(workflow, runId, {
|
|
136205
136571
|
...signal !== undefined ? { signal } : {},
|
|
@@ -136221,6 +136587,39 @@ class WorkflowResumeOps {
|
|
|
136221
136587
|
runResult.warnings = warnings;
|
|
136222
136588
|
return runResult;
|
|
136223
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
|
+
}
|
|
136224
136623
|
async resolveHostPauseReceipt(runId, workdir, opts, workflow) {
|
|
136225
136624
|
const db = await this.ctx.getDb();
|
|
136226
136625
|
if (!await hostExecutorPersisted(db, runId))
|
|
@@ -136452,6 +136851,7 @@ var init_workflow_resume_ops = __esm(() => {
|
|
|
136452
136851
|
init_checkpoint_contract();
|
|
136453
136852
|
init_composition_lint();
|
|
136454
136853
|
init_host_executor();
|
|
136854
|
+
init_host_pause();
|
|
136455
136855
|
init_progress_projection();
|
|
136456
136856
|
init_receipt();
|
|
136457
136857
|
init_workflow_resolver();
|
|
@@ -138346,6 +138746,7 @@ var init_src4 = __esm(() => {
|
|
|
138346
138746
|
init_task_verdict();
|
|
138347
138747
|
init_token_ledger_service();
|
|
138348
138748
|
init_token_ledger_watcher();
|
|
138749
|
+
init_verdict_replay();
|
|
138349
138750
|
init_verify_answer_lint();
|
|
138350
138751
|
init_warm_turn_dispatch();
|
|
138351
138752
|
init_workflow_clean_ops();
|
|
@@ -145574,81 +145975,6 @@ var init_task_diffstat = __esm(() => {
|
|
|
145574
145975
|
init_task_triage();
|
|
145575
145976
|
});
|
|
145576
145977
|
|
|
145577
|
-
// ../../packages/app/src/services/sdlc-actions.ts
|
|
145578
|
-
function isReservedAction(name) {
|
|
145579
|
-
return RESERVED_ACTIONS.includes(name);
|
|
145580
|
-
}
|
|
145581
|
-
function resolveAction(builder, name) {
|
|
145582
|
-
const reserved = isReservedAction(name);
|
|
145583
|
-
const declared = builder?.actions?.[name];
|
|
145584
|
-
if (declared !== undefined) {
|
|
145585
|
-
return Array.isArray(declared) ? { name, reserved, source: "config", kind: "composite", steps: [...declared] } : { name, reserved, source: "config", kind: "command", command: declared };
|
|
145586
|
-
}
|
|
145587
|
-
if ((builder?.preset ?? "bun") === "bun") {
|
|
145588
|
-
if (reserved)
|
|
145589
|
-
return { name, reserved, source: "preset", kind: "command", command: BUN_PRESET[name] };
|
|
145590
|
-
} else {
|
|
145591
|
-
const command = reserved ? STACK_NEUTRAL_DEFAULTS[name] : undefined;
|
|
145592
|
-
if (command !== undefined)
|
|
145593
|
-
return { name, reserved, source: "default", kind: "command", command };
|
|
145594
|
-
}
|
|
145595
|
-
return { name, reserved, source: "unbound", kind: "unbound" };
|
|
145596
|
-
}
|
|
145597
|
-
function listActions(builder) {
|
|
145598
|
-
const custom = Object.keys(builder?.actions ?? {}).filter((name) => !isReservedAction(name)).sort();
|
|
145599
|
-
return [...RESERVED_ACTIONS, ...custom].map((name) => resolveAction(builder, name));
|
|
145600
|
-
}
|
|
145601
|
-
function renderScoped(command, values) {
|
|
145602
|
-
let rendered = command;
|
|
145603
|
-
for (const [placeholder, items] of [
|
|
145604
|
-
["{files}", values.files],
|
|
145605
|
-
["{paths}", values.paths]
|
|
145606
|
-
]) {
|
|
145607
|
-
if (!rendered.includes(placeholder))
|
|
145608
|
-
continue;
|
|
145609
|
-
if (items === undefined)
|
|
145610
|
-
throw new Error(`renderScoped: ${placeholder} has no value`);
|
|
145611
|
-
if (items.length === 0)
|
|
145612
|
-
throw new Error(`renderScoped: ${placeholder} has an empty list`);
|
|
145613
|
-
rendered = rendered.replaceAll(placeholder, () => items.map(shQuote2).join(" "));
|
|
145614
|
-
}
|
|
145615
|
-
if (rendered.includes("{workspace}")) {
|
|
145616
|
-
const workspace = values.workspace;
|
|
145617
|
-
if (workspace === undefined)
|
|
145618
|
-
throw new Error("renderScoped: {workspace} has no value");
|
|
145619
|
-
if (workspace.length === 0)
|
|
145620
|
-
throw new Error("renderScoped: {workspace} is empty");
|
|
145621
|
-
rendered = rendered.replaceAll("{workspace}", () => shQuote2(workspace));
|
|
145622
|
-
}
|
|
145623
|
-
return rendered;
|
|
145624
|
-
}
|
|
145625
|
-
var RESERVED_ACTIONS, BUN_PRESET, STACK_NEUTRAL_DEFAULTS;
|
|
145626
|
-
var init_sdlc_actions = __esm(() => {
|
|
145627
|
-
init_src();
|
|
145628
|
-
RESERVED_ACTIONS = SDLC_ACTIONS;
|
|
145629
|
-
BUN_PRESET = {
|
|
145630
|
-
build: "bun run build",
|
|
145631
|
-
dev: "bun run dev",
|
|
145632
|
-
clean: "bun run clean",
|
|
145633
|
-
format: "bun run format",
|
|
145634
|
-
autofix: "bun run autofix",
|
|
145635
|
-
lint: "bun run lint",
|
|
145636
|
-
typecheck: "bun run typecheck",
|
|
145637
|
-
test: "bun run test",
|
|
145638
|
-
"test-full": "bun run test:full",
|
|
145639
|
-
"test-heavy": "bun run test:heavy",
|
|
145640
|
-
"rules-pre": "bun run test-pre-check",
|
|
145641
|
-
"rules-post": "bun run test-post-check",
|
|
145642
|
-
check: "bun run spur-check",
|
|
145643
|
-
"check-full": "bun run spur-check:full",
|
|
145644
|
-
"check-feature": "bun run spur-check-feature",
|
|
145645
|
-
"lint-changed": "bunx biome check {files}",
|
|
145646
|
-
"typecheck-changed": "cd {workspace} && bun run typecheck",
|
|
145647
|
-
"test-changed": "cd {workspace} && bun test {paths}"
|
|
145648
|
-
};
|
|
145649
|
-
STACK_NEUTRAL_DEFAULTS = SDLC_STACK_NEUTRAL_ACTIONS;
|
|
145650
|
-
});
|
|
145651
|
-
|
|
145652
145978
|
// ../../packages/app/src/services/quality-gate.ts
|
|
145653
145979
|
import { spawnSync as spawnSync6 } from "child_process";
|
|
145654
145980
|
import { randomUUID as randomUUID12 } from "crypto";
|
|
@@ -145669,6 +145995,21 @@ import {
|
|
|
145669
145995
|
} from "fs";
|
|
145670
145996
|
import { homedir as homedir13, tmpdir as tmpdir5 } from "os";
|
|
145671
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
|
+
}
|
|
145672
146013
|
function isTransientLock(attemptOutput) {
|
|
145673
146014
|
return LOCKED_PATTERN.test(attemptOutput);
|
|
145674
146015
|
}
|
|
@@ -145867,7 +146208,7 @@ function lightScope(changedFiles, exists = existsSync23) {
|
|
|
145867
146208
|
tests.add(candidate);
|
|
145868
146209
|
matched = true;
|
|
145869
146210
|
}
|
|
145870
|
-
} else if (rest.startsWith("tests/") &&
|
|
146211
|
+
} else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
|
|
145871
146212
|
tests.add(file);
|
|
145872
146213
|
matched = true;
|
|
145873
146214
|
}
|
|
@@ -145884,7 +146225,7 @@ function lightScope(changedFiles, exists = existsSync23) {
|
|
|
145884
146225
|
const candidate = `${workspace}/tests/${rest.slice("src/".length).replace(/\.tsx?$/, (ext) => `.test${ext}`)}`;
|
|
145885
146226
|
if (exists(candidate))
|
|
145886
146227
|
tests.add(candidate);
|
|
145887
|
-
} else if (rest.startsWith("tests/") &&
|
|
146228
|
+
} else if (rest.startsWith("tests/") && TEST_FILE_PATTERN2.test(rest)) {
|
|
145888
146229
|
tests.add(file);
|
|
145889
146230
|
}
|
|
145890
146231
|
}
|
|
@@ -145931,11 +146272,13 @@ function planLightChecks(scope, hasTypecheck = workspaceHasTypecheck, builder) {
|
|
|
145931
146272
|
}
|
|
145932
146273
|
return plans;
|
|
145933
146274
|
}
|
|
145934
|
-
function gitChangedFiles(cwd) {
|
|
146275
|
+
function gitChangedFiles(cwd, timeoutMs) {
|
|
145935
146276
|
const abs = (p) => cwd ? join78(cwd, p) : p;
|
|
145936
146277
|
const changed = new Set;
|
|
145937
146278
|
for (const cmd of ["git diff --name-only HEAD", "git ls-files --others --exclude-standard"]) {
|
|
145938
|
-
const result = runShellCommand(cmd, cwd);
|
|
146279
|
+
const result = runShellCommand(cmd, cwd, undefined, timeoutMs);
|
|
146280
|
+
if (result.timedOut)
|
|
146281
|
+
return null;
|
|
145939
146282
|
for (const line of result.output.split(`
|
|
145940
146283
|
`)) {
|
|
145941
146284
|
const file = line.trim();
|
|
@@ -145974,13 +146317,29 @@ function runLightGate(env, options = {}) {
|
|
|
145974
146317
|
const logFile = join78(runDir, `${env.wbs}-light-gate.log`);
|
|
145975
146318
|
const receiptFile = join78(runDir, `${env.wbs}-check-receipt.json`);
|
|
145976
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
|
+
}
|
|
145977
146336
|
const capture = (options.captureGitTree ?? captureGateGitTree)(cwd ?? process.cwd());
|
|
145978
146337
|
const digest = capture.ok ? capture.tree : "";
|
|
145979
146338
|
if (!capture.ok) {
|
|
145980
146339
|
appendFileSync7(abs(logFile), `--- light receipt: no git tree identity \u2014 ${capture.error}
|
|
145981
146340
|
`);
|
|
145982
146341
|
}
|
|
145983
|
-
const scope = lightScope(
|
|
146342
|
+
const scope = lightScope(changed, (p) => existsSync23(abs(p)));
|
|
145984
146343
|
const plans = planLightChecks(scope, (workspace) => workspaceHasTypecheck(abs(workspace)), options.builder);
|
|
145985
146344
|
const reusable = new Map;
|
|
145986
146345
|
let preserveFullReceipt = false;
|
|
@@ -146021,11 +146380,13 @@ function runLightGate(env, options = {}) {
|
|
|
146021
146380
|
continue;
|
|
146022
146381
|
}
|
|
146023
146382
|
const startedAtMs = Date.now();
|
|
146024
|
-
const result = runShellCommand(plan.cmd, cwd);
|
|
146383
|
+
const result = runShellCommand(plan.cmd, cwd, undefined, timeoutMs);
|
|
146025
146384
|
const durationMs = Date.now() - startedAtMs;
|
|
146026
|
-
const status = result.code === 0 ? "PASS" : "FAIL";
|
|
146027
|
-
|
|
146028
|
-
${
|
|
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}`);
|
|
146029
146390
|
checks.push({ id: plan.id, cmd: plan.cmd, status, durationMs, logPath: logFile });
|
|
146030
146391
|
}
|
|
146031
146392
|
const receipt = buildReceipt({
|
|
@@ -146063,7 +146424,7 @@ function gateSleep(ms) {
|
|
|
146063
146424
|
}
|
|
146064
146425
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
146065
146426
|
}
|
|
146066
|
-
function runShellCommand(cmd, cwd, extraEnv) {
|
|
146427
|
+
function runShellCommand(cmd, cwd, extraEnv, timeoutMs) {
|
|
146067
146428
|
const dir = mkdtempSync(join78(tmpdir5(), "spur-quality-gate-"));
|
|
146068
146429
|
const path = join78(dir, "output");
|
|
146069
146430
|
const fd = openSync8(path, "w");
|
|
@@ -146071,13 +146432,15 @@ function runShellCommand(cmd, cwd, extraEnv) {
|
|
|
146071
146432
|
const result = spawnSync6("sh", ["-c", cmd], {
|
|
146072
146433
|
cwd,
|
|
146073
146434
|
stdio: ["ignore", fd, fd],
|
|
146074
|
-
env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv }
|
|
146435
|
+
env: extraEnv === undefined ? undefined : { ...getEnvVars(), ...extraEnv },
|
|
146436
|
+
...timeoutMs !== undefined ? { timeout: timeoutMs, killSignal: "SIGKILL" } : {}
|
|
146075
146437
|
});
|
|
146076
|
-
|
|
146438
|
+
const timedOut = result.error?.code === "ETIMEDOUT" || result.signal === "SIGKILL";
|
|
146439
|
+
if (result.error !== undefined && !timedOut) {
|
|
146077
146440
|
return { output: `sh -c failed: ${result.error.message}
|
|
146078
|
-
`, code: 1 };
|
|
146441
|
+
`, code: 1, timedOut: false };
|
|
146079
146442
|
}
|
|
146080
|
-
return { output: readFileSync21(path, "utf8"), code: result.status ?? 1 };
|
|
146443
|
+
return { output: readFileSync21(path, "utf8"), code: result.status ?? 1, timedOut };
|
|
146081
146444
|
} finally {
|
|
146082
146445
|
closeSync8(fd);
|
|
146083
146446
|
rmSync5(dir, { recursive: true, force: true });
|
|
@@ -146291,6 +146654,8 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146291
146654
|
const attemptFile = rel("-test-fix-attempt");
|
|
146292
146655
|
const abs = (p) => cwd ? join78(cwd, p) : p;
|
|
146293
146656
|
const priorLog = mode === "recheck" && existsSync23(abs(logFile)) ? readFileSync21(abs(logFile), "utf8") : "";
|
|
146657
|
+
const timeoutMs = gateTimeoutMsFor(env, options);
|
|
146658
|
+
let timedOut;
|
|
146294
146659
|
writeFileSync14(abs(logFile), "");
|
|
146295
146660
|
if (mode === "run")
|
|
146296
146661
|
writeFileSync14(abs(attemptFile), `0
|
|
@@ -146345,15 +146710,17 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146345
146710
|
gateRc = 1;
|
|
146346
146711
|
}
|
|
146347
146712
|
if (mode === "recheck" && gateRc === 0 && (env.gateProbeCmd ?? "").length > 0) {
|
|
146348
|
-
const probe = runShellCommand(env.gateProbeCmd ?? "", cwd);
|
|
146713
|
+
const probe = runShellCommand(env.gateProbeCmd ?? "", cwd, undefined, timeoutMs);
|
|
146349
146714
|
writeFileSync14(abs(`${logFile}.probe`), probe.output);
|
|
146350
146715
|
gateRc = probe.code;
|
|
146716
|
+
if (probe.timedOut)
|
|
146717
|
+
timedOut = { cmd: env.gateProbeCmd ?? "" };
|
|
146351
146718
|
appendFileSync7(abs(logFile), probe.output);
|
|
146352
146719
|
rmSync5(abs(`${logFile}.probe`), { force: true });
|
|
146353
146720
|
}
|
|
146354
146721
|
let gateRuntimeMs = 0;
|
|
146355
146722
|
let queueWaitMs = 0;
|
|
146356
|
-
if (gateRc === 0) {
|
|
146723
|
+
if (gateRc === 0 && timedOut === undefined) {
|
|
146357
146724
|
const delayMs = retryDelayMs2(env);
|
|
146358
146725
|
const lockLine = (line) => {
|
|
146359
146726
|
process.stdout.write(line);
|
|
@@ -146367,12 +146734,16 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146367
146734
|
try {
|
|
146368
146735
|
for (gateAttempt = 1;gateAttempt <= MAX_GATE_ATTEMPTS; gateAttempt++) {
|
|
146369
146736
|
const attemptLogPath = `${logFile}.attempt-${gateAttempt}`;
|
|
146370
|
-
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);
|
|
146371
146738
|
writeFileSync14(abs(attemptLogPath), attempt.output);
|
|
146372
146739
|
const locked = isTransientLock(attempt.output);
|
|
146373
146740
|
appendFileSync7(abs(logFile), attempt.output);
|
|
146374
146741
|
rmSync5(abs(attemptLogPath), { force: true });
|
|
146375
146742
|
gateRc = attempt.code;
|
|
146743
|
+
if (attempt.timedOut) {
|
|
146744
|
+
timedOut = { cmd: env.qualityGateCmd ?? "" };
|
|
146745
|
+
break;
|
|
146746
|
+
}
|
|
146376
146747
|
if (gateRc === 0 || !locked || gateAttempt >= MAX_GATE_ATTEMPTS)
|
|
146377
146748
|
break;
|
|
146378
146749
|
const line = retryMessage(gateAttempt);
|
|
@@ -146408,6 +146779,26 @@ function runQualityGate(mode, env, options = {}) {
|
|
|
146408
146779
|
}
|
|
146409
146780
|
}
|
|
146410
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
|
+
}
|
|
146411
146802
|
if (gateRc === 0) {
|
|
146412
146803
|
const bytes = existsSync23(abs(logFile)) ? statSync15(abs(logFile)).size : 0;
|
|
146413
146804
|
const attemptsLabel = gateAttempt > 0 ? String(gateAttempt) : "";
|
|
@@ -146468,12 +146859,19 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146468
146859
|
return 2;
|
|
146469
146860
|
}
|
|
146470
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 };
|
|
146471
146869
|
if (mode === "light") {
|
|
146472
|
-
runLightGate(env,
|
|
146473
|
-
return 0;
|
|
146870
|
+
const light = runLightGate(env, gateOptions);
|
|
146871
|
+
return light.blocked === undefined ? 0 : 2;
|
|
146474
146872
|
}
|
|
146475
146873
|
if (mode === "deferred") {
|
|
146476
|
-
return runDeferredGate(env,
|
|
146874
|
+
return runDeferredGate(env, gateOptions).status === "DEFERRED" ? 0 : 1;
|
|
146477
146875
|
}
|
|
146478
146876
|
if (mode === "status") {
|
|
146479
146877
|
const runDir = join78(options.cwd ?? ".", ".spur", "run");
|
|
@@ -146496,7 +146894,10 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146496
146894
|
return 0;
|
|
146497
146895
|
}
|
|
146498
146896
|
try {
|
|
146499
|
-
|
|
146897
|
+
const result = runQualityGate(mode, env, gateOptions);
|
|
146898
|
+
if (result.status === "PASS")
|
|
146899
|
+
return 0;
|
|
146900
|
+
return result.status === "BLOCKED" ? 2 : 1;
|
|
146500
146901
|
} catch (error) {
|
|
146501
146902
|
if (!(error instanceof GateLockTimeoutError))
|
|
146502
146903
|
throw error;
|
|
@@ -146504,7 +146905,7 @@ function qualityGateMain(argv, rawEnv = getEnvVars(), options = {}) {
|
|
|
146504
146905
|
return 2;
|
|
146505
146906
|
}
|
|
146506
146907
|
}
|
|
146507
|
-
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)";
|
|
146508
146909
|
var init_quality_gate = __esm(() => {
|
|
146509
146910
|
init_dist();
|
|
146510
146911
|
init_proof_input_fingerprint();
|
|
@@ -146512,7 +146913,7 @@ var init_quality_gate = __esm(() => {
|
|
|
146512
146913
|
LOCKED_PATTERN = /SQLiteError: database is locked|SQLite database .*is busy|SQLITE_BUSY/;
|
|
146513
146914
|
FINDINGS_PATTERN = /[A-Za-z0-9_./-]+\.[A-Za-z]+:[0-9]+/g;
|
|
146514
146915
|
COVERAGE_ROW_PATTERN = /^\s*(\S+\.[A-Za-z]+)\s*\|\s*([\d.]+)\s*\|\s*([\d.]+)\s*\|\s*(\S*)/;
|
|
146515
|
-
|
|
146916
|
+
TEST_FILE_PATTERN2 = /\.test\.tsx?$/;
|
|
146516
146917
|
GATE_LOCK_TIMEOUT_MS_DEFAULT = 2 * 60 * 60 * 1000;
|
|
146517
146918
|
GateLockTimeoutError = class GateLockTimeoutError extends Error {
|
|
146518
146919
|
constructor(message) {
|
|
@@ -148404,6 +148805,17 @@ var require_src10 = __commonJS(function(exports, module) {
|
|
|
148404
148805
|
});
|
|
148405
148806
|
|
|
148406
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
|
+
}
|
|
148407
148819
|
function nearestRankP50(values) {
|
|
148408
148820
|
if (values.length === 0)
|
|
148409
148821
|
return null;
|
|
@@ -148527,11 +148939,27 @@ function buildRows(executions, windowSec) {
|
|
|
148527
148939
|
}
|
|
148528
148940
|
function buildStepProfile(input) {
|
|
148529
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
|
+
}));
|
|
148530
148957
|
return {
|
|
148531
148958
|
workflow: input.workflow,
|
|
148532
148959
|
windowSec: input.windowSec,
|
|
148533
148960
|
sampledRuns: input.runs.length,
|
|
148534
|
-
rows
|
|
148961
|
+
rows,
|
|
148962
|
+
...stalledRuns.length > 0 ? { stalledRuns } : {}
|
|
148535
148963
|
};
|
|
148536
148964
|
}
|
|
148537
148965
|
function nonDryRuns(entries) {
|
|
@@ -148555,16 +148983,22 @@ function formatStepProfileHuman(profile) {
|
|
|
148555
148983
|
`p50=${seconds(row.durationMs.p50)}`.padEnd(12),
|
|
148556
148984
|
`max=${seconds(row.durationMs.max)}`.padEnd(12),
|
|
148557
148985
|
`idle=${seconds(row.idleGapMs.p50)}`.padEnd(12),
|
|
148986
|
+
...row.hostMs === undefined ? [] : [`host=${seconds(row.hostMs)}`.padEnd(12)],
|
|
148558
148987
|
`session=${row.session ?? "?"}`.padEnd(15),
|
|
148559
148988
|
`cacheHit=${ratio(row.cacheHit.p50)} (${row.cacheHit.known}/${row.cacheHit.of})`.padEnd(20),
|
|
148560
148989
|
`flags=${row.flags.length === 0 ? "-" : row.flags.join(",")}`
|
|
148561
148990
|
].join(" "));
|
|
148562
148991
|
}
|
|
148992
|
+
for (const run of profile.stalledRuns ?? [])
|
|
148993
|
+
lines.push(`${run.runId}: ${stalledLabel(run.runId, run.stalledMs)}`);
|
|
148563
148994
|
return `${lines.join(`
|
|
148564
148995
|
`)}
|
|
148565
148996
|
`;
|
|
148566
148997
|
}
|
|
148567
148998
|
var DEFAULT_LAST = 20, DEFAULT_WINDOW_SEC = 300;
|
|
148999
|
+
var init_step_profile = __esm(() => {
|
|
149000
|
+
init_host_pause();
|
|
149001
|
+
});
|
|
148568
149002
|
|
|
148569
149003
|
// ../../packages/app/src/workflow/step-profile-cli.ts
|
|
148570
149004
|
function positiveInt(raw, fallback) {
|
|
@@ -148611,6 +149045,12 @@ function runFailure(spurBin, result, what) {
|
|
|
148611
149045
|
`).slice(-1)[0] ?? "";
|
|
148612
149046
|
return failure2(`${what} failed (exit ${result.exitCode}${detail === "" ? "" : `: ${detail}`}) [${spurBin}]`);
|
|
148613
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
|
+
}
|
|
148614
149054
|
function runStepProfileCli(argv, defaultSpurBin = "spur", defaultSpurArgv) {
|
|
148615
149055
|
const spurArgv = argv.includes("--spur-bin") ? undefined : defaultSpurArgv;
|
|
148616
149056
|
const args = parseStepProfileCliArgs(argv, defaultSpurBin);
|
|
@@ -148636,10 +149076,7 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148636
149076
|
return runFailure(args.spurBin, list, `spur ${listArgs.join(" ")}`);
|
|
148637
149077
|
let entries;
|
|
148638
149078
|
try {
|
|
148639
|
-
|
|
148640
|
-
if (!Array.isArray(parsed.entries))
|
|
148641
|
-
throw new Error('no "entries" array');
|
|
148642
|
-
entries = parsed.entries;
|
|
149079
|
+
entries = parseTraceEntries(list.stdout);
|
|
148643
149080
|
} catch (err) {
|
|
148644
149081
|
return failure2(`spur workflow trace output did not parse as JSON: ${String(err)}`);
|
|
148645
149082
|
}
|
|
@@ -148653,12 +149090,27 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148653
149090
|
const parsed = JSON.parse(timeline.stdout);
|
|
148654
149091
|
if (!Array.isArray(parsed.events))
|
|
148655
149092
|
throw new Error('no "events" array');
|
|
148656
|
-
runs.push({
|
|
149093
|
+
runs.push({
|
|
149094
|
+
runId: entry.runId,
|
|
149095
|
+
events: parsed.events,
|
|
149096
|
+
...entry.hostPauses !== undefined ? { hostPauses: entry.hostPauses } : {}
|
|
149097
|
+
});
|
|
148657
149098
|
} catch (err) {
|
|
148658
149099
|
return failure2(`spur workflow trace ${entry.runId} output did not parse as JSON: ${String(err)}`);
|
|
148659
149100
|
}
|
|
148660
149101
|
}
|
|
148661
|
-
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 });
|
|
148662
149114
|
return {
|
|
148663
149115
|
exitCode: 0,
|
|
148664
149116
|
stdout: args.json ? `${JSON.stringify(profile, null, 2)}
|
|
@@ -148669,6 +149121,7 @@ ${STEP_PROFILE_USAGE}`);
|
|
|
148669
149121
|
var STEP_PROFILE_USAGE;
|
|
148670
149122
|
var init_step_profile_cli = __esm(() => {
|
|
148671
149123
|
init_dist5();
|
|
149124
|
+
init_step_profile();
|
|
148672
149125
|
STEP_PROFILE_USAGE = `usage: spur workflow profile <workflow> [--last <N>] [--window <sec>] [--json] [--spur-bin <cmd>]
|
|
148673
149126
|
|
|
148674
149127
|
Read-only step profile from \`spur workflow trace\`: per node and action kind it reports runs,
|
|
@@ -156573,7 +157026,7 @@ var SHARED_OPTIONS = {
|
|
|
156573
157026
|
"pi|claude|codex|gemini|opencode|antigravity|openclaw|omp|grok|agy|all"
|
|
156574
157027
|
],
|
|
156575
157028
|
statusDoneFailed: ["--status <status>", "Filter by status: done, failed"],
|
|
156576
|
-
|
|
157029
|
+
statusWorkflowRun: ["--status <status>", "Filter by status: done, failed, running, paused, interrupted"],
|
|
156577
157030
|
untilAgent: ["--until <state>", "Lifecycle state to wait for (repeatable OR)"],
|
|
156578
157031
|
untilMessage: ["--until <state>", "Wait target for --wait: injected|invoke-exit"],
|
|
156579
157032
|
verboseWorkflow: ["--verbose", "Include transitions and correlation diagnostics in human progress"],
|
|
@@ -159764,7 +160217,7 @@ function registerTaskUpdateCommand(task, context) {
|
|
|
159764
160217
|
// src/commands/task/verdict.ts
|
|
159765
160218
|
init_src4();
|
|
159766
160219
|
function registerTaskVerdictCommand(task, context) {
|
|
159767
|
-
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) => {
|
|
159768
160221
|
await Promise.resolve().then(() => init_src4());
|
|
159769
160222
|
const answerPath = options.fromAnswer ?? `.spur/run/${wbs}-verify-answer.txt`;
|
|
159770
160223
|
let answerText;
|
|
@@ -159783,6 +160236,12 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159783
160236
|
featuresDir: context.fs.resolve(featuresDir)
|
|
159784
160237
|
};
|
|
159785
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
|
+
};
|
|
159786
160245
|
if (lintFindings.length > 0) {
|
|
159787
160246
|
if (options.json) {
|
|
159788
160247
|
context.output.write(JSON.stringify({ wbs, lintFindings }, null, 2));
|
|
@@ -159791,9 +160250,14 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159791
160250
|
for (const f of lintFindings)
|
|
159792
160251
|
context.output.error(` line ${f.line}: ${f.message}`);
|
|
159793
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
|
+
`);
|
|
159794
160257
|
context.setExitCode(1);
|
|
159795
160258
|
return;
|
|
159796
160259
|
}
|
|
160260
|
+
await clearLintFile();
|
|
159797
160261
|
const taskCheckPassed = true;
|
|
159798
160262
|
const taskAc = await readTaskAcceptanceCriteria(wbs, corpusDeps);
|
|
159799
160263
|
const result = deriveVerdict(answerText, taskCheckPassed, taskAc);
|
|
@@ -159803,7 +160267,19 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159803
160267
|
context.setExitCode(1);
|
|
159804
160268
|
return;
|
|
159805
160269
|
}
|
|
159806
|
-
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);
|
|
159807
160283
|
await context.fs.ensureDir(".spur/run");
|
|
159808
160284
|
await context.fs.writeFile(`.spur/run/${wbs}-verdict.json`, `${jsonOut}
|
|
159809
160285
|
`);
|
|
@@ -159811,6 +160287,12 @@ function registerTaskVerdictCommand(task, context) {
|
|
|
159811
160287
|
context.output.write(jsonOut);
|
|
159812
160288
|
} else {
|
|
159813
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
|
+
}
|
|
159814
160296
|
}
|
|
159815
160297
|
if (result.verdict !== "PASS") {
|
|
159816
160298
|
context.setExitCode(1);
|
|
@@ -164711,7 +165193,7 @@ init_src2();
|
|
|
164711
165193
|
var CLI_CONFIG = {
|
|
164712
165194
|
binaryName: "spur",
|
|
164713
165195
|
binaryLabel: "spur",
|
|
164714
|
-
binaryVersion: "0.4.
|
|
165196
|
+
binaryVersion: "0.4.28",
|
|
164715
165197
|
configDir: ".spur",
|
|
164716
165198
|
configFile: ".spur/config.yaml",
|
|
164717
165199
|
databaseFile: ".spur/spur.db"
|
|
@@ -172450,7 +172932,7 @@ init_dist5();
|
|
|
172450
172932
|
import { homedir as homedir17 } from "os";
|
|
172451
172933
|
import { basename as basename23, join as join95, relative as relative19, resolve as resolve37 } from "path";
|
|
172452
172934
|
// ../server/package.json
|
|
172453
|
-
var version4 = "0.4.
|
|
172935
|
+
var version4 = "0.4.28";
|
|
172454
172936
|
|
|
172455
172937
|
// ../server/src/modules/health/index.ts
|
|
172456
172938
|
var startedAt = Date.now();
|
|
@@ -178008,6 +178490,7 @@ init_src2();
|
|
|
178008
178490
|
init_dist5();
|
|
178009
178491
|
init_dist2();
|
|
178010
178492
|
init_composition_baseline();
|
|
178493
|
+
init_host_pause();
|
|
178011
178494
|
init_run_measurements();
|
|
178012
178495
|
init_terminal_reason();
|
|
178013
178496
|
init_run_measurements();
|
|
@@ -178045,9 +178528,13 @@ async function buildReportJson(db, workflows, opts = {}) {
|
|
|
178045
178528
|
function renderStateLines(rows) {
|
|
178046
178529
|
return rows.map((s) => {
|
|
178047
178530
|
const wall = s.wallMsP50 === null || s.wallMsP90 === null ? "n/a" : `p50=${s.wallMsP50}ms p90=${s.wallMsP90}ms`;
|
|
178048
|
-
|
|
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}`;
|
|
178049
178533
|
});
|
|
178050
178534
|
}
|
|
178535
|
+
function renderStalledLines(rows) {
|
|
178536
|
+
return rows.map((r) => `${r.workflow}: ${stalledLabel(r.runId, r.stalledMs)}`);
|
|
178537
|
+
}
|
|
178051
178538
|
function renderWorkflowLines(rows) {
|
|
178052
178539
|
return rows.map((m) => {
|
|
178053
178540
|
const excluded = m.dryRuns > 0 || m.nonTerminalRuns > 0 ? ` [excluded ${m.dryRuns} dry, ${m.nonTerminalRuns} non-terminal]` : "";
|
|
@@ -178263,6 +178750,11 @@ function registerWorkflowCostCommand(workflow, context, deps = {}) {
|
|
|
178263
178750
|
context.output.write(line);
|
|
178264
178751
|
}
|
|
178265
178752
|
}
|
|
178753
|
+
if (options.json !== true) {
|
|
178754
|
+
for (const line of renderStalledLines(await readStalledRuns(db, targets))) {
|
|
178755
|
+
context.output.write(line);
|
|
178756
|
+
}
|
|
178757
|
+
}
|
|
178266
178758
|
} finally {
|
|
178267
178759
|
db.close();
|
|
178268
178760
|
}
|
|
@@ -179240,6 +179732,7 @@ init_src4();
|
|
|
179240
179732
|
import { closeSync as closeSync10, fstatSync as fstatSync5, openSync as openSync10, readFileSync as readFileSync32, readSync as readSync3 } from "fs";
|
|
179241
179733
|
import { join as join107 } from "path";
|
|
179242
179734
|
import { setTimeout as sleep7 } from "timers/promises";
|
|
179735
|
+
var TRACE_STATUSES = ["done", "failed", "running", "paused", "interrupted"];
|
|
179243
179736
|
function formatTraceList2(result) {
|
|
179244
179737
|
if (result.entries.length === 0) {
|
|
179245
179738
|
return "No workflow runs.";
|
|
@@ -179268,6 +179761,9 @@ function formatTraceTimeline(result) {
|
|
|
179268
179761
|
`Outcome: ${run.outcome} Events: ${events.length}`,
|
|
179269
179762
|
""
|
|
179270
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
|
+
}
|
|
179271
179767
|
if (run.nextAction !== undefined)
|
|
179272
179768
|
lines.splice(4, 0, `Next: ${run.nextAction.label} \u2014 ${run.nextAction.value}`);
|
|
179273
179769
|
if (result.outputArtifact !== undefined) {
|
|
@@ -179503,7 +179999,7 @@ function formatTokens2(n) {
|
|
|
179503
179999
|
return String(n);
|
|
179504
180000
|
}
|
|
179505
180001
|
function registerWorkflowTraceCommand(workflow, context) {
|
|
179506
|
-
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) => {
|
|
179507
180003
|
const svc = makeWorkflowService(context);
|
|
179508
180004
|
const last = parseInt(options.last, 10);
|
|
179509
180005
|
if (Number.isNaN(last) || last < 1) {
|
|
@@ -179548,8 +180044,8 @@ function registerWorkflowTraceCommand(workflow, context) {
|
|
|
179548
180044
|
context.setExitCode(1);
|
|
179549
180045
|
return;
|
|
179550
180046
|
}
|
|
179551
|
-
if (options.status !== undefined && !
|
|
179552
|
-
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");
|
|
179553
180049
|
context.setExitCode(1);
|
|
179554
180050
|
return;
|
|
179555
180051
|
}
|