agentxchain 2.149.2 → 2.150.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/scripts/prepublish-gate.sh +132 -0
- package/scripts/release-bump.sh +2 -2
- package/scripts/render-github-release-body.mjs +11 -4
- package/scripts/reproduce-bug-54.mjs +81 -15
- package/src/commands/mission.js +6 -2
- package/src/commands/restart.js +1 -1
- package/src/commands/turn.js +14 -5
- package/src/lib/adapters/local-cli-adapter.js +23 -22
- package/src/lib/governed-state.js +31 -1
- package/src/lib/mission-plans.js +14 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.150.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"test": "npm run test:vitest && npm run test:node",
|
|
29
29
|
"test:vitest": "vitest run --reporter=verbose",
|
|
30
30
|
"test:beta": "node --test test/beta-tester-scenarios/*.test.js",
|
|
31
|
-
"test:node": "node --test test/*.test.js test/beta-tester-scenarios/*.test.js",
|
|
31
|
+
"test:node": "node --test --test-timeout=60000 --test-concurrency=4 test/*.test.js test/beta-tester-scenarios/*.test.js",
|
|
32
32
|
"preflight:release": "bash scripts/release-preflight.sh",
|
|
33
33
|
"preflight:release:strict": "bash scripts/release-preflight.sh --strict",
|
|
34
34
|
"check:release-alignment": "node scripts/check-release-alignment.mjs",
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# Prepublish gate — local-first quality floor before `git tag`.
|
|
3
|
+
#
|
|
4
|
+
# Replaces per-commit remote CI coverage that `.github/workflows/ci.yml` will
|
|
5
|
+
# drop when CICD-SHRINK lands. Runs the same checks the GitHub-hosted runners
|
|
6
|
+
# ran, in-process on the agent's box, before any tag or publish-workflow is
|
|
7
|
+
# triggered. See .planning/CICD_REDUCTION_PLAN.md §7.
|
|
8
|
+
#
|
|
9
|
+
# Usage:
|
|
10
|
+
# bash cli/scripts/prepublish-gate.sh <target-version>
|
|
11
|
+
#
|
|
12
|
+
# Exit 0 + prints "PREPUBLISH GATE PASSED for <version>" → safe to tag/push.
|
|
13
|
+
# Exit non-zero → do NOT tag, do NOT push. Fix the failure locally first.
|
|
14
|
+
#
|
|
15
|
+
# Discipline rule (CICD-SHRINK acceptance, new in DEC-RELEASE-CUT-AND-PUSH-AS-ATOMIC-001):
|
|
16
|
+
# the release-cut turn MUST include this script's "PREPUBLISH GATE PASSED" line
|
|
17
|
+
# in the turn's Evidence block before `git tag` is created.
|
|
18
|
+
|
|
19
|
+
set -uo pipefail
|
|
20
|
+
|
|
21
|
+
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
|
22
|
+
CLI_DIR="${SCRIPT_DIR}/.."
|
|
23
|
+
cd "$CLI_DIR"
|
|
24
|
+
|
|
25
|
+
usage() {
|
|
26
|
+
echo "Usage: bash cli/scripts/prepublish-gate.sh <target-version>" >&2
|
|
27
|
+
echo " <target-version> Semver string (e.g., 2.150.0)." >&2
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if [[ $# -ne 1 ]]; then
|
|
31
|
+
usage
|
|
32
|
+
exit 1
|
|
33
|
+
fi
|
|
34
|
+
|
|
35
|
+
TARGET_VERSION="$1"
|
|
36
|
+
if ! [[ "$TARGET_VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
|
37
|
+
echo "Error: invalid semver '${TARGET_VERSION}'" >&2
|
|
38
|
+
usage
|
|
39
|
+
exit 1
|
|
40
|
+
fi
|
|
41
|
+
|
|
42
|
+
echo "Prepublish gate — target version ${TARGET_VERSION}"
|
|
43
|
+
echo "================================================="
|
|
44
|
+
echo "cwd: ${CLI_DIR}"
|
|
45
|
+
echo ""
|
|
46
|
+
|
|
47
|
+
STEP_STATUS=0
|
|
48
|
+
step_fail() {
|
|
49
|
+
echo ""
|
|
50
|
+
echo " FAIL: $1"
|
|
51
|
+
STEP_STATUS=1
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ---------------------------------------------------------------------------
|
|
55
|
+
# Step 1 — Full test suite (replaces per-push CI).
|
|
56
|
+
#
|
|
57
|
+
# Runs via `npm test` so Vitest + Node test phases are both exercised, and the
|
|
58
|
+
# Node phase inherits the `--test-timeout=60000 --test-concurrency=4` caps from
|
|
59
|
+
# package.json (DEC-BUG57-FAILFAST-NODE-TEST-001). We pass `--test-timeout`
|
|
60
|
+
# explicitly too so the gate cannot be silently weakened by a future
|
|
61
|
+
# package.json change. The node runner honors the last `--test-timeout` wins.
|
|
62
|
+
# ---------------------------------------------------------------------------
|
|
63
|
+
echo "[1/4] Full test suite — cd cli && npm test -- --test-timeout=60000"
|
|
64
|
+
if npm test -- --test-timeout=60000; then
|
|
65
|
+
echo " PASS: full test suite green"
|
|
66
|
+
else
|
|
67
|
+
step_fail "full test suite failed"
|
|
68
|
+
fi
|
|
69
|
+
echo ""
|
|
70
|
+
|
|
71
|
+
# ---------------------------------------------------------------------------
|
|
72
|
+
# Step 2 — Release preflight in publish-gate mode.
|
|
73
|
+
#
|
|
74
|
+
# `release-preflight.sh --publish-gate` enforces strict mode (clean tree,
|
|
75
|
+
# bumped package.json, CHANGELOG entry present, release-alignment manifest
|
|
76
|
+
# aligned, pack dry-run succeeds) and runs only the release-gate-critical
|
|
77
|
+
# test subset. Step 1 already ran the full suite; step 2 enforces the
|
|
78
|
+
# release-specific invariants.
|
|
79
|
+
# ---------------------------------------------------------------------------
|
|
80
|
+
echo "[2/4] Release preflight — bash scripts/release-preflight.sh --publish-gate"
|
|
81
|
+
if bash "${SCRIPT_DIR}/release-preflight.sh" --publish-gate --target-version "${TARGET_VERSION}"; then
|
|
82
|
+
echo " PASS: release-preflight gate green"
|
|
83
|
+
else
|
|
84
|
+
step_fail "release-preflight gate failed"
|
|
85
|
+
fi
|
|
86
|
+
echo ""
|
|
87
|
+
|
|
88
|
+
# ---------------------------------------------------------------------------
|
|
89
|
+
# Step 3 — npm pack --dry-run (claim-reality coverage).
|
|
90
|
+
#
|
|
91
|
+
# Proves the tarball the publish workflow will upload is reproducible from
|
|
92
|
+
# HEAD. The publish workflow itself re-runs this; we catch pack failures here
|
|
93
|
+
# before the tag is even created, so a broken `files:` glob or missing dist
|
|
94
|
+
# artifact never reaches remote CI.
|
|
95
|
+
# ---------------------------------------------------------------------------
|
|
96
|
+
echo "[3/4] Pack dry-run — npm pack --dry-run"
|
|
97
|
+
if npm pack --dry-run >/dev/null 2>&1; then
|
|
98
|
+
echo " PASS: npm pack --dry-run succeeded"
|
|
99
|
+
else
|
|
100
|
+
step_fail "npm pack --dry-run failed (rerun with streamed output for details)"
|
|
101
|
+
npm pack --dry-run 2>&1 | tail -20 || true
|
|
102
|
+
fi
|
|
103
|
+
echo ""
|
|
104
|
+
|
|
105
|
+
# ---------------------------------------------------------------------------
|
|
106
|
+
# Step 4 — Release-alignment manifest (17-surface drift gate).
|
|
107
|
+
#
|
|
108
|
+
# `check-release-alignment.mjs` owns the shared manifest of every surface that
|
|
109
|
+
# must reference the target version (CHANGELOG, website release pages,
|
|
110
|
+
# capabilities.json, implementor guide, launch evidence, onboarding docs,
|
|
111
|
+
# marketing drafts, llms.txt, homebrew mirror, package.json). Step 2 runs the
|
|
112
|
+
# same check, but we invoke it directly so the final status line is visible
|
|
113
|
+
# in the gate's own output — agents reading this log get an explicit
|
|
114
|
+
# alignment-pass signal without having to scrape the preflight's mid-run
|
|
115
|
+
# block.
|
|
116
|
+
# ---------------------------------------------------------------------------
|
|
117
|
+
echo "[4/4] Release alignment — node scripts/check-release-alignment.mjs --scope current"
|
|
118
|
+
if node "${SCRIPT_DIR}/check-release-alignment.mjs" --scope current --target-version "${TARGET_VERSION}"; then
|
|
119
|
+
echo " PASS: release alignment green"
|
|
120
|
+
else
|
|
121
|
+
step_fail "release alignment failed"
|
|
122
|
+
fi
|
|
123
|
+
echo ""
|
|
124
|
+
|
|
125
|
+
echo "================================================="
|
|
126
|
+
if [[ "${STEP_STATUS}" -ne 0 ]]; then
|
|
127
|
+
echo "PREPUBLISH GATE FAILED for ${TARGET_VERSION} — do NOT tag, do NOT push."
|
|
128
|
+
exit 1
|
|
129
|
+
fi
|
|
130
|
+
|
|
131
|
+
echo "PREPUBLISH GATE PASSED for ${TARGET_VERSION} — safe to tag and push."
|
|
132
|
+
exit 0
|
package/scripts/release-bump.sh
CHANGED
|
@@ -342,12 +342,12 @@ else
|
|
|
342
342
|
PREFLIGHT_FAILED=0
|
|
343
343
|
|
|
344
344
|
# 8.5a. Full test suite with release env vars
|
|
345
|
-
if env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test >/dev/null 2>&1; then
|
|
345
|
+
if env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test -- --test-timeout=60000 >/dev/null 2>&1; then
|
|
346
346
|
echo " OK: test suite passed"
|
|
347
347
|
else
|
|
348
348
|
echo " FAIL: test suite failed" >&2
|
|
349
349
|
echo " Re-running with output for diagnostics..." >&2
|
|
350
|
-
env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test 2>&1 | tail -30 >&2
|
|
350
|
+
env AGENTXCHAIN_RELEASE_TARGET_VERSION="${TARGET_VERSION}" AGENTXCHAIN_RELEASE_PREFLIGHT=1 npm test -- --test-timeout=60000 2>&1 | tail -30 >&2
|
|
351
351
|
PREFLIGHT_FAILED=1
|
|
352
352
|
fi
|
|
353
353
|
|
|
@@ -57,20 +57,27 @@ function extractSummaryParagraph(text, version) {
|
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
function extractAggregateEvidenceLine(text) {
|
|
60
|
-
const matches = [...text.matchAll(/^-\s
|
|
60
|
+
const matches = [...text.matchAll(/^-\s+(.*\b(\d+)\s+tests\b.*\b0 failures\b.*)$/gm)];
|
|
61
61
|
if (matches.length === 0) {
|
|
62
62
|
throw new Error('Concrete aggregate evidence line missing from governed release page');
|
|
63
63
|
}
|
|
64
64
|
|
|
65
65
|
const aggregate = matches.reduce((best, match) => {
|
|
66
|
-
const count = Number(match[
|
|
66
|
+
const count = Number(match[2]);
|
|
67
67
|
if (!best || count > best.count) {
|
|
68
|
-
return { count, line: match[
|
|
68
|
+
return { count, line: match[1] };
|
|
69
69
|
}
|
|
70
70
|
return best;
|
|
71
71
|
}, null);
|
|
72
72
|
|
|
73
|
-
|
|
73
|
+
const line = aggregate.line.replace(/\*\*/g, '').replace(/`/g, '').replace(/,/g, '').trim();
|
|
74
|
+
const evidenceMatch = line.match(/\b\d+\s+tests\b.*\b0 failures\b.*/);
|
|
75
|
+
if (!evidenceMatch) {
|
|
76
|
+
throw new Error('Concrete aggregate evidence line missing from governed release page');
|
|
77
|
+
}
|
|
78
|
+
const prefix = line.slice(0, evidenceMatch.index).trim();
|
|
79
|
+
const evidence = evidenceMatch[0].trim();
|
|
80
|
+
return `- ${evidence}${prefix ? ` — ${prefix.replace(/[→-]\s*$/, '').trim()}` : ''}`;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
function getPreviousVersionTag(repoRoot, version) {
|
|
@@ -58,7 +58,7 @@
|
|
|
58
58
|
* purpose), and does NOT require the governed dispatcher to be running.
|
|
59
59
|
*/
|
|
60
60
|
|
|
61
|
-
import { spawn } from 'child_process';
|
|
61
|
+
import { spawn, spawnSync } from 'child_process';
|
|
62
62
|
import { existsSync, readFileSync, readdirSync, statSync, writeFileSync } from 'fs';
|
|
63
63
|
import { join, resolve } from 'path';
|
|
64
64
|
import { fileURLToPath } from 'url';
|
|
@@ -315,20 +315,6 @@ async function runOneAttempt({
|
|
|
315
315
|
attempt.spawn_attached_elapsed_ms = now - t0;
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
-
if (child.stdin) {
|
|
319
|
-
child.stdin.on('error', (err) => {
|
|
320
|
-
// Capture but do not fail — adapter behavior matches: stdin EPIPE is
|
|
321
|
-
// logged and the spawn continues to play out via close/error events.
|
|
322
|
-
attempt.stderr += `[repro:stdin_error] ${err?.code || ''} ${err?.message || ''}\n`;
|
|
323
|
-
});
|
|
324
|
-
try {
|
|
325
|
-
if (transport === 'stdin') child.stdin.write(fullPrompt);
|
|
326
|
-
child.stdin.end();
|
|
327
|
-
} catch (err) {
|
|
328
|
-
attempt.stderr += `[repro:stdin_throw] ${err?.code || ''} ${err?.message || ''}\n`;
|
|
329
|
-
}
|
|
330
|
-
}
|
|
331
|
-
|
|
332
318
|
if (child.stdout) {
|
|
333
319
|
child.stdout.on('data', (chunk) => {
|
|
334
320
|
const text = chunk.toString();
|
|
@@ -343,6 +329,20 @@ async function runOneAttempt({
|
|
|
343
329
|
});
|
|
344
330
|
}
|
|
345
331
|
|
|
332
|
+
if (child.stdin) {
|
|
333
|
+
child.stdin.on('error', (err) => {
|
|
334
|
+
// Capture but do not fail — adapter behavior matches: stdin EPIPE is
|
|
335
|
+
// logged and the spawn continues to play out via close/error events.
|
|
336
|
+
attempt.stderr += `[repro:stdin_error] ${err?.code || ''} ${err?.message || ''}\n`;
|
|
337
|
+
});
|
|
338
|
+
try {
|
|
339
|
+
if (transport === 'stdin') child.stdin.write(fullPrompt);
|
|
340
|
+
child.stdin.end();
|
|
341
|
+
} catch (err) {
|
|
342
|
+
attempt.stderr += `[repro:stdin_throw] ${err?.code || ''} ${err?.message || ''}\n`;
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
|
|
346
346
|
if (child.stderr) {
|
|
347
347
|
child.stderr.on('data', (chunk) => {
|
|
348
348
|
const text = chunk.toString();
|
|
@@ -496,6 +496,7 @@ async function main() {
|
|
|
496
496
|
const stdinBytes = transport === 'stdin' ? Buffer.byteLength(fullPrompt) : 0;
|
|
497
497
|
const diagnosticArgs = redactArgs(args, fullPrompt, transport);
|
|
498
498
|
const envSnapshot = snapshotEnv(spawnEnv);
|
|
499
|
+
const commandProbe = probeCommand(command, runtimeCwd, spawnEnv);
|
|
499
500
|
|
|
500
501
|
const header = {
|
|
501
502
|
repro_version: 1,
|
|
@@ -510,6 +511,7 @@ async function main() {
|
|
|
510
511
|
stdin_bytes: stdinBytes,
|
|
511
512
|
prompt_source: promptSource,
|
|
512
513
|
env_snapshot: envSnapshot,
|
|
514
|
+
command_probe: commandProbe,
|
|
513
515
|
watchdog_ms: opts.noWatchdog ? null : watchdogMs,
|
|
514
516
|
no_watchdog: opts.noWatchdog,
|
|
515
517
|
attempts_planned: opts.attempts,
|
|
@@ -529,6 +531,11 @@ async function main() {
|
|
|
529
531
|
console.error(`[repro] prompt : ${promptSource.kind} (${promptSource.length_bytes} bytes)`);
|
|
530
532
|
console.error(`[repro] watchdog_ms : ${header.watchdog_ms ?? 'disabled'}`);
|
|
531
533
|
console.error(`[repro] auth env : ${JSON.stringify(envSnapshot.auth_env_present)}`);
|
|
534
|
+
if (commandProbe.kind === 'claude_version') {
|
|
535
|
+
console.error(`[repro] claude probe : status=${commandProbe.status ?? '-'} signal=${commandProbe.signal ?? '-'} stdout=${JSON.stringify(commandProbe.stdout || '')}`);
|
|
536
|
+
} else {
|
|
537
|
+
console.error(`[repro] command probe: ${commandProbe.kind} (${commandProbe.reason})`);
|
|
538
|
+
}
|
|
532
539
|
console.error(`[repro] attempts : ${header.attempts_planned}`);
|
|
533
540
|
console.error('');
|
|
534
541
|
|
|
@@ -617,6 +624,65 @@ function summarize(attempts) {
|
|
|
617
624
|
};
|
|
618
625
|
}
|
|
619
626
|
|
|
627
|
+
function probeCommand(command, cwd, env) {
|
|
628
|
+
if (!isClaudeCommand(command)) {
|
|
629
|
+
return {
|
|
630
|
+
kind: 'skipped',
|
|
631
|
+
reason: 'not a claude command',
|
|
632
|
+
};
|
|
633
|
+
}
|
|
634
|
+
try {
|
|
635
|
+
const result = spawnSync(command, ['--version'], {
|
|
636
|
+
cwd,
|
|
637
|
+
env,
|
|
638
|
+
encoding: 'utf8',
|
|
639
|
+
timeout: 10_000,
|
|
640
|
+
maxBuffer: 1024 * 1024,
|
|
641
|
+
});
|
|
642
|
+
return {
|
|
643
|
+
kind: 'claude_version',
|
|
644
|
+
command,
|
|
645
|
+
args: ['--version'],
|
|
646
|
+
timeout_ms: 10_000,
|
|
647
|
+
status: result.status,
|
|
648
|
+
signal: result.signal,
|
|
649
|
+
stdout: result.stdout || '',
|
|
650
|
+
stderr: result.stderr || '',
|
|
651
|
+
error: result.error ? {
|
|
652
|
+
code: result.error.code ?? null,
|
|
653
|
+
errno: result.error.errno ?? null,
|
|
654
|
+
syscall: result.error.syscall ?? null,
|
|
655
|
+
message: result.error.message || String(result.error),
|
|
656
|
+
} : null,
|
|
657
|
+
timed_out: result.error?.code === 'ETIMEDOUT',
|
|
658
|
+
};
|
|
659
|
+
} catch (err) {
|
|
660
|
+
return {
|
|
661
|
+
kind: 'claude_version',
|
|
662
|
+
command,
|
|
663
|
+
args: ['--version'],
|
|
664
|
+
timeout_ms: 10_000,
|
|
665
|
+
status: null,
|
|
666
|
+
signal: null,
|
|
667
|
+
stdout: '',
|
|
668
|
+
stderr: '',
|
|
669
|
+
error: {
|
|
670
|
+
code: err?.code ?? null,
|
|
671
|
+
errno: err?.errno ?? null,
|
|
672
|
+
syscall: err?.syscall ?? null,
|
|
673
|
+
message: err?.message || String(err),
|
|
674
|
+
},
|
|
675
|
+
timed_out: err?.code === 'ETIMEDOUT',
|
|
676
|
+
};
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function isClaudeCommand(command) {
|
|
681
|
+
if (typeof command !== 'string') return false;
|
|
682
|
+
const normalized = command.replace(/\\/g, '/');
|
|
683
|
+
return normalized === 'claude' || normalized.endsWith('/claude');
|
|
684
|
+
}
|
|
685
|
+
|
|
620
686
|
main().catch((err) => {
|
|
621
687
|
console.error(`[repro] fatal: ${err?.stack || err?.message || err}`);
|
|
622
688
|
process.exit(99);
|
package/src/commands/mission.js
CHANGED
|
@@ -1377,7 +1377,9 @@ export async function missionPlanAutopilotCommand(planTarget, opts) {
|
|
|
1377
1377
|
}
|
|
1378
1378
|
|
|
1379
1379
|
if (waveNum === maxWaves) {
|
|
1380
|
-
terminalReason =
|
|
1380
|
+
terminalReason = totalFailed > 0
|
|
1381
|
+
? (continueOnFailure ? 'plan_incomplete' : 'failure_stopped')
|
|
1382
|
+
: 'wave_limit_reached';
|
|
1381
1383
|
break;
|
|
1382
1384
|
}
|
|
1383
1385
|
|
|
@@ -2030,7 +2032,9 @@ async function coordinatorAutopilot(planTarget, opts, context, mission) {
|
|
|
2030
2032
|
}
|
|
2031
2033
|
|
|
2032
2034
|
if (waveNum === maxWaves) {
|
|
2033
|
-
terminalReason =
|
|
2035
|
+
terminalReason = totalFailed > 0
|
|
2036
|
+
? (continueOnFailure ? 'plan_incomplete' : 'failure_stopped')
|
|
2037
|
+
: 'wave_limit_reached';
|
|
2034
2038
|
break;
|
|
2035
2039
|
}
|
|
2036
2040
|
|
package/src/commands/restart.js
CHANGED
|
@@ -219,7 +219,7 @@ export async function restartCommand(opts) {
|
|
|
219
219
|
process.exit(1);
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
-
if (state.status === 'blocked') {
|
|
222
|
+
if (state.status === 'blocked' && !state.pending_phase_transition && !state.pending_run_completion) {
|
|
223
223
|
console.log(chalk.red('Run is blocked. Resolve the blocker first.'));
|
|
224
224
|
const recovery = deriveRecoveryDescriptor(state, config);
|
|
225
225
|
if (recovery) {
|
package/src/commands/turn.js
CHANGED
|
@@ -120,7 +120,14 @@ function buildArtifactIndex(root, turnId) {
|
|
|
120
120
|
}
|
|
121
121
|
|
|
122
122
|
function buildTurnPayload(turnId, turn, state, artifacts, assignment, config) {
|
|
123
|
-
|
|
123
|
+
// Effective start time for display: BUG-51 hardening clears `started_at`
|
|
124
|
+
// when a turn transitions to `dispatched` so ghost-turn detection can key
|
|
125
|
+
// off `dispatched_at`. For manual runtimes (no subprocess), no later
|
|
126
|
+
// `starting` transition re-sets `started_at`, so fall back to
|
|
127
|
+
// `dispatched_at` (operator-dispatched = effective start) and
|
|
128
|
+
// `assigned_at` as a last resort so the timing surface stays populated.
|
|
129
|
+
const effectiveStartedAt = turn.started_at || turn.dispatched_at || turn.assigned_at || null;
|
|
130
|
+
const elapsedMs = getElapsedMs(effectiveStartedAt);
|
|
124
131
|
const payload = {
|
|
125
132
|
turn_id: turnId,
|
|
126
133
|
run_id: state.run_id || assignment?.run_id || null,
|
|
@@ -129,7 +136,7 @@ function buildTurnPayload(turnId, turn, state, artifacts, assignment, config) {
|
|
|
129
136
|
runtime: turn.runtime_id,
|
|
130
137
|
status: turn.status,
|
|
131
138
|
attempt: turn.attempt,
|
|
132
|
-
started_at:
|
|
139
|
+
started_at: effectiveStartedAt,
|
|
133
140
|
elapsed_ms: elapsedMs,
|
|
134
141
|
dispatch_dir: getDispatchTurnDir(turnId),
|
|
135
142
|
staging_result_path: assignment?.staging_result_path || null,
|
|
@@ -152,7 +159,9 @@ function buildTurnPayload(turnId, turn, state, artifacts, assignment, config) {
|
|
|
152
159
|
}
|
|
153
160
|
|
|
154
161
|
function printTurnSummary(turnId, turn, state, artifacts, assignment, config) {
|
|
155
|
-
|
|
162
|
+
// See buildTurnPayload for rationale on fallback ordering.
|
|
163
|
+
const effectiveStartedAt = turn.started_at || turn.dispatched_at || turn.assigned_at || null;
|
|
164
|
+
const elapsedMs = getElapsedMs(effectiveStartedAt);
|
|
156
165
|
console.log('');
|
|
157
166
|
console.log(chalk.bold(` Turn: ${chalk.cyan(turnId)}`));
|
|
158
167
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
@@ -162,8 +171,8 @@ function printTurnSummary(turnId, turn, state, artifacts, assignment, config) {
|
|
|
162
171
|
console.log(` ${chalk.dim('Runtime:')} ${turn.runtime_id}`);
|
|
163
172
|
console.log(` ${chalk.dim('Status:')} ${turn.status}`);
|
|
164
173
|
console.log(` ${chalk.dim('Attempt:')} ${turn.attempt}`);
|
|
165
|
-
if (
|
|
166
|
-
console.log(` ${chalk.dim('Started:')} ${
|
|
174
|
+
if (effectiveStartedAt) {
|
|
175
|
+
console.log(` ${chalk.dim('Started:')} ${effectiveStartedAt}`);
|
|
167
176
|
}
|
|
168
177
|
if (elapsedMs != null) {
|
|
169
178
|
console.log(` ${chalk.dim('Elapsed:')} ${formatElapsed(elapsedMs)}`);
|
|
@@ -264,7 +264,29 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
264
264
|
armStartupWatchdog();
|
|
265
265
|
});
|
|
266
266
|
|
|
267
|
-
//
|
|
267
|
+
// Collect stdout/stderr
|
|
268
|
+
if (child.stdout) {
|
|
269
|
+
child.stdout.on('data', (chunk) => {
|
|
270
|
+
const text = chunk.toString();
|
|
271
|
+
stdoutBytes += Buffer.byteLength(text);
|
|
272
|
+
recordFirstOutput('stdout');
|
|
273
|
+
logs.push(text);
|
|
274
|
+
if (onStdout) onStdout(text);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
if (child.stderr) {
|
|
279
|
+
child.stderr.on('data', (chunk) => {
|
|
280
|
+
const text = chunk.toString();
|
|
281
|
+
stderrBytes += Buffer.byteLength(text);
|
|
282
|
+
stderrExcerpt = appendDiagnosticExcerpt(stderrExcerpt, text, DIAGNOSTIC_STDERR_EXCERPT_LIMIT);
|
|
283
|
+
logs.push('[stderr] ' + text);
|
|
284
|
+
if (onStderr) onStderr(text);
|
|
285
|
+
});
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// Deliver prompt only after output listeners are registered. This removes
|
|
289
|
+
// the remaining adapter-side ordering risk for fast stdin-driven children.
|
|
268
290
|
if (child.stdin) {
|
|
269
291
|
child.stdin.on('error', (err) => {
|
|
270
292
|
appendDiagnostic(logs, 'stdin_error', {
|
|
@@ -287,27 +309,6 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
287
309
|
}
|
|
288
310
|
}
|
|
289
311
|
|
|
290
|
-
// Collect stdout/stderr
|
|
291
|
-
if (child.stdout) {
|
|
292
|
-
child.stdout.on('data', (chunk) => {
|
|
293
|
-
const text = chunk.toString();
|
|
294
|
-
stdoutBytes += Buffer.byteLength(text);
|
|
295
|
-
recordFirstOutput('stdout');
|
|
296
|
-
logs.push(text);
|
|
297
|
-
if (onStdout) onStdout(text);
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
if (child.stderr) {
|
|
302
|
-
child.stderr.on('data', (chunk) => {
|
|
303
|
-
const text = chunk.toString();
|
|
304
|
-
stderrBytes += Buffer.byteLength(text);
|
|
305
|
-
stderrExcerpt = appendDiagnosticExcerpt(stderrExcerpt, text, DIAGNOSTIC_STDERR_EXCERPT_LIMIT);
|
|
306
|
-
logs.push('[stderr] ' + text);
|
|
307
|
-
if (onStderr) onStderr(text);
|
|
308
|
-
});
|
|
309
|
-
}
|
|
310
|
-
|
|
311
312
|
// Timeout handling per §20.4
|
|
312
313
|
let timeoutHandle;
|
|
313
314
|
let sigkillHandle;
|
|
@@ -1383,6 +1383,10 @@ function classifyAcceptanceOverlap(targetTurn, conflictFiles, historyEntries, co
|
|
|
1383
1383
|
const forwardRevisionTurns = new Map();
|
|
1384
1384
|
|
|
1385
1385
|
for (const entry of historyEntries) {
|
|
1386
|
+
if (targetTurn?.run_id && entry.run_id && entry.run_id !== targetTurn.run_id) {
|
|
1387
|
+
continue;
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1386
1390
|
if ((entry.accepted_sequence || 0) <= (targetTurn.assigned_sequence || 0)) {
|
|
1387
1391
|
continue;
|
|
1388
1392
|
}
|
|
@@ -2096,6 +2100,24 @@ function inferApprovalPauseFromState(state, config) {
|
|
|
2096
2100
|
};
|
|
2097
2101
|
}
|
|
2098
2102
|
|
|
2103
|
+
function shouldNormalizeApprovalPauseBlock(state, inferred) {
|
|
2104
|
+
if (!state || typeof state !== 'object') {
|
|
2105
|
+
return false;
|
|
2106
|
+
}
|
|
2107
|
+
|
|
2108
|
+
const blockedOn = typeof state.blocked_on === 'string' ? state.blocked_on : '';
|
|
2109
|
+
const recovery = state.blocked_reason?.recovery;
|
|
2110
|
+
const typedReason = recovery?.typed_reason || null;
|
|
2111
|
+
|
|
2112
|
+
if (blockedOn.startsWith('human_approval:')) {
|
|
2113
|
+
return true;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
return state.status === 'paused'
|
|
2117
|
+
&& state.blocked_reason != null
|
|
2118
|
+
&& (!typedReason || typedReason === inferred.typedReason);
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2099
2121
|
export function reconcileApprovalPausesWithConfig(state, config) {
|
|
2100
2122
|
if (!state || typeof state !== 'object' || !config) {
|
|
2101
2123
|
return { state, changed: false };
|
|
@@ -2117,7 +2139,7 @@ export function reconcileApprovalPausesWithConfig(state, config) {
|
|
|
2117
2139
|
changed = true;
|
|
2118
2140
|
}
|
|
2119
2141
|
|
|
2120
|
-
if (nextState
|
|
2142
|
+
if (shouldNormalizeApprovalPauseBlock(nextState, inferred)) {
|
|
2121
2143
|
nextState = {
|
|
2122
2144
|
...nextState,
|
|
2123
2145
|
status: 'paused',
|
|
@@ -3034,12 +3056,20 @@ export function assignGovernedTurn(root, config, roleId, options = {}) {
|
|
|
3034
3056
|
const concurrentWith = Object.keys(activeTurns);
|
|
3035
3057
|
|
|
3036
3058
|
// Build the new turn object
|
|
3059
|
+
// `started_at` is seeded at assignment so that direct assign→accept flows
|
|
3060
|
+
// (non-dispatched, non-subprocess turns) still carry timing into history and
|
|
3061
|
+
// the turn_accepted event payload (per TURN_TIMING_OBSERVABILITY_SPEC.md).
|
|
3062
|
+
// BUG-51's dispatched-lifecycle path explicitly deletes this and the
|
|
3063
|
+
// starting/running transitions re-set it, so dispatch-driven turns still
|
|
3064
|
+
// reflect true subprocess-startup timing.
|
|
3037
3065
|
const newTurn = {
|
|
3038
3066
|
turn_id: turnId,
|
|
3067
|
+
run_id: state.run_id,
|
|
3039
3068
|
assigned_role: roleId,
|
|
3040
3069
|
status: 'assigned',
|
|
3041
3070
|
attempt: 1,
|
|
3042
3071
|
assigned_at: now,
|
|
3072
|
+
started_at: now,
|
|
3043
3073
|
deadline_at: new Date(Date.now() + timeoutMinutes * 60 * 1000).toISOString(),
|
|
3044
3074
|
runtime_id: runtimeId,
|
|
3045
3075
|
baseline,
|
package/src/lib/mission-plans.js
CHANGED
|
@@ -489,8 +489,8 @@ function isAcceptedRepoHistoryEntry(entry) {
|
|
|
489
489
|
return Boolean(entry?.accepted_at) || entry?.status === 'accepted';
|
|
490
490
|
}
|
|
491
491
|
|
|
492
|
-
const REPO_FAILURE_STATUSES = new Set(['failed_acceptance', 'failed', 'rejected', 'retrying', 'conflicted']);
|
|
493
|
-
const RETRYABLE_COORDINATOR_FAILURE_STATUSES = new Set(['failed', 'failed_acceptance']);
|
|
492
|
+
const REPO_FAILURE_STATUSES = new Set(['failed_acceptance', 'failed', 'failed_start', 'rejected', 'retrying', 'conflicted']);
|
|
493
|
+
const RETRYABLE_COORDINATOR_FAILURE_STATUSES = new Set(['failed', 'failed_acceptance', 'failed_start']);
|
|
494
494
|
|
|
495
495
|
function getLatestRepoDispatches(launchRecord) {
|
|
496
496
|
const latestByRepo = new Map();
|
|
@@ -708,6 +708,18 @@ function synchronizeCoordinatorWorkstreamStatuses(root, plan, coordinatorConfig,
|
|
|
708
708
|
continue;
|
|
709
709
|
}
|
|
710
710
|
|
|
711
|
+
if (launchRecord?.status === 'failed' || (launchRecord?.terminal_reason && launchRecord.terminal_reason !== 'completed')) {
|
|
712
|
+
if (ws.launch_status !== 'needs_attention') {
|
|
713
|
+
ws.launch_status = 'needs_attention';
|
|
714
|
+
changed = true;
|
|
715
|
+
}
|
|
716
|
+
if (plan.status !== 'needs_attention') {
|
|
717
|
+
plan.status = 'needs_attention';
|
|
718
|
+
changed = true;
|
|
719
|
+
}
|
|
720
|
+
continue;
|
|
721
|
+
}
|
|
722
|
+
|
|
711
723
|
if ((launchRecord?.repo_dispatches?.length || 0) > 0 || progress.accepted_repo_count > 0) {
|
|
712
724
|
if (ws.launch_status !== 'launched') {
|
|
713
725
|
ws.launch_status = 'launched';
|