agentxchain 2.149.1 → 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/README.md +1 -1
- 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/doctor.js +4 -4
- package/src/commands/init.js +1 -1
- 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 +25 -23
- package/src/lib/claude-local-auth.js +183 -7
- package/src/lib/connector-probe.js +12 -23
- package/src/lib/connector-validate.js +5 -5
- package/src/lib/governed-state.js +31 -1
- package/src/lib/mission-plans.js +14 -2
- package/src/templates/governed/enterprise-app.json +1 -1
- package/src/templates/governed/full-local-cli.json +4 -4
package/README.md
CHANGED
|
@@ -89,7 +89,7 @@ agentxchain step --role pm
|
|
|
89
89
|
|
|
90
90
|
If you skipped `--goal` during scaffold, run `agentxchain config --set project.goal "Build an API change planner for release teams"` before the first governed turn instead of re-running init in place.
|
|
91
91
|
|
|
92
|
-
The default governed dev runtime is `claude --print --dangerously-skip-permissions` with stdin prompt delivery. The non-interactive governed path needs write access, so do not pretend
|
|
92
|
+
The default governed dev runtime is `claude --print --dangerously-skip-permissions --bare` with stdin prompt delivery. The non-interactive governed path needs write access and env-based auth, so do not pretend plain `claude --print` is sufficient for unattended implementation turns. If your local coding agent uses a different launch contract, set it during scaffold creation:
|
|
93
93
|
|
|
94
94
|
```bash
|
|
95
95
|
agentxchain init --governed --dir my-agentxchain-project --dev-command ./scripts/dev-agent.sh --dev-prompt-transport dispatch_bundle_only -y
|
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/doctor.js
CHANGED
|
@@ -59,7 +59,7 @@ export async function doctorCommand(opts = {}) {
|
|
|
59
59
|
|
|
60
60
|
// ── Governed (v4) Doctor ────────────────────────────────────────────────────
|
|
61
61
|
|
|
62
|
-
function governedDoctor(root, rawConfig, opts) {
|
|
62
|
+
async function governedDoctor(root, rawConfig, opts) {
|
|
63
63
|
const checks = [];
|
|
64
64
|
const cliVersionHealth = getCliVersionHealth();
|
|
65
65
|
let stateRunId = null;
|
|
@@ -93,7 +93,7 @@ function governedDoctor(root, rawConfig, opts) {
|
|
|
93
93
|
const runtimes = (normalized && normalized.runtimes) || rawConfig.runtimes || {};
|
|
94
94
|
const rolesByRuntime = buildRolesByRuntime(normalized?.roles || {});
|
|
95
95
|
for (const [rtId, rt] of Object.entries(runtimes)) {
|
|
96
|
-
const check = checkRuntimeReachable(root, rtId, rt, rolesByRuntime[rtId] || []);
|
|
96
|
+
const check = await checkRuntimeReachable(root, rtId, rt, rolesByRuntime[rtId] || []);
|
|
97
97
|
checks.push(check);
|
|
98
98
|
}
|
|
99
99
|
const connectorProbe = getConnectorProbeRecommendation(runtimes);
|
|
@@ -488,7 +488,7 @@ function buildCliVersionCheck(cliVersionHealth) {
|
|
|
488
488
|
};
|
|
489
489
|
}
|
|
490
490
|
|
|
491
|
-
function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
491
|
+
async function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
492
492
|
const base = { id: `runtime_${rtId}`, name: `Runtime: ${rtId}` };
|
|
493
493
|
|
|
494
494
|
if (!rt || !rt.type) {
|
|
@@ -502,7 +502,7 @@ function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
|
502
502
|
case 'local_cli': {
|
|
503
503
|
const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
|
|
504
504
|
if (probe.ok) {
|
|
505
|
-
const claudeAuthIssue = getClaudeSubprocessAuthIssue(rt);
|
|
505
|
+
const claudeAuthIssue = await getClaudeSubprocessAuthIssue(rt);
|
|
506
506
|
if (claudeAuthIssue) {
|
|
507
507
|
return attachRuntimeContract({
|
|
508
508
|
...base,
|
package/src/commands/init.js
CHANGED
|
@@ -98,7 +98,7 @@ const GOVERNED_ROLES = {
|
|
|
98
98
|
|
|
99
99
|
const DEFAULT_GOVERNED_LOCAL_DEV_RUNTIME = Object.freeze({
|
|
100
100
|
type: 'local_cli',
|
|
101
|
-
command: ['claude', '--print', '--dangerously-skip-permissions'],
|
|
101
|
+
command: ['claude', '--print', '--dangerously-skip-permissions', '--bare'],
|
|
102
102
|
cwd: '.',
|
|
103
103
|
prompt_transport: 'stdin',
|
|
104
104
|
});
|
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)}`);
|
|
@@ -128,13 +128,14 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
128
128
|
const spawnEnv = { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id };
|
|
129
129
|
const stdinBytes = transport === 'stdin' ? Buffer.byteLength(fullPrompt, 'utf8') : 0;
|
|
130
130
|
const diagnosticArgs = redactPromptArgs(args, fullPrompt, transport);
|
|
131
|
-
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime, spawnEnv);
|
|
131
|
+
const claudeAuthIssue = await getClaudeSubprocessAuthIssue(runtime, spawnEnv);
|
|
132
132
|
|
|
133
133
|
if (claudeAuthIssue) {
|
|
134
134
|
appendDiagnostic(logs, 'claude_auth_preflight_failed', {
|
|
135
135
|
runtime_id: runtimeId,
|
|
136
136
|
turn_id: turn.turn_id,
|
|
137
137
|
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
138
|
+
smoke_probe: claudeAuthIssue.smoke_probe,
|
|
138
139
|
recommendation: claudeAuthIssue.fix,
|
|
139
140
|
});
|
|
140
141
|
return {
|
|
@@ -263,7 +264,29 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
263
264
|
armStartupWatchdog();
|
|
264
265
|
});
|
|
265
266
|
|
|
266
|
-
//
|
|
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.
|
|
267
290
|
if (child.stdin) {
|
|
268
291
|
child.stdin.on('error', (err) => {
|
|
269
292
|
appendDiagnostic(logs, 'stdin_error', {
|
|
@@ -286,27 +309,6 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
286
309
|
}
|
|
287
310
|
}
|
|
288
311
|
|
|
289
|
-
// Collect stdout/stderr
|
|
290
|
-
if (child.stdout) {
|
|
291
|
-
child.stdout.on('data', (chunk) => {
|
|
292
|
-
const text = chunk.toString();
|
|
293
|
-
stdoutBytes += Buffer.byteLength(text);
|
|
294
|
-
recordFirstOutput('stdout');
|
|
295
|
-
logs.push(text);
|
|
296
|
-
if (onStdout) onStdout(text);
|
|
297
|
-
});
|
|
298
|
-
}
|
|
299
|
-
|
|
300
|
-
if (child.stderr) {
|
|
301
|
-
child.stderr.on('data', (chunk) => {
|
|
302
|
-
const text = chunk.toString();
|
|
303
|
-
stderrBytes += Buffer.byteLength(text);
|
|
304
|
-
stderrExcerpt = appendDiagnosticExcerpt(stderrExcerpt, text, DIAGNOSTIC_STDERR_EXCERPT_LIMIT);
|
|
305
|
-
logs.push('[stderr] ' + text);
|
|
306
|
-
if (onStderr) onStderr(text);
|
|
307
|
-
});
|
|
308
|
-
}
|
|
309
|
-
|
|
310
312
|
// Timeout handling per §20.4
|
|
311
313
|
let timeoutHandle;
|
|
312
314
|
let sigkillHandle;
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { spawn } from 'node:child_process';
|
|
2
|
+
|
|
1
3
|
const CLAUDE_ENV_AUTH_KEYS = [
|
|
2
4
|
'ANTHROPIC_API_KEY',
|
|
3
5
|
'CLAUDE_API_KEY',
|
|
@@ -6,6 +8,9 @@ const CLAUDE_ENV_AUTH_KEYS = [
|
|
|
6
8
|
'CLAUDE_CODE_USE_BEDROCK',
|
|
7
9
|
];
|
|
8
10
|
|
|
11
|
+
const DEFAULT_SMOKE_PROBE_TIMEOUT_MS = 10_000;
|
|
12
|
+
const DEFAULT_SMOKE_PROBE_STDIN = 'ok';
|
|
13
|
+
|
|
9
14
|
function normalizeCommandTokens(runtime) {
|
|
10
15
|
if (Array.isArray(runtime?.command)) {
|
|
11
16
|
return runtime.command.flatMap((element) =>
|
|
@@ -41,7 +46,26 @@ export function hasClaudeEnvAuth(env = process.env) {
|
|
|
41
46
|
return Object.values(getClaudeEnvAuthPresence(env)).some(Boolean);
|
|
42
47
|
}
|
|
43
48
|
|
|
44
|
-
|
|
49
|
+
function buildClaudeSubprocessAuthIssue(env, smokeProbe = null) {
|
|
50
|
+
const auth_env_present = getClaudeEnvAuthPresence(env);
|
|
51
|
+
return {
|
|
52
|
+
auth_env_present,
|
|
53
|
+
smoke_probe: smokeProbe,
|
|
54
|
+
detail: 'Claude local_cli runtime has no env-based auth and is missing "--bare"; non-interactive subprocesses can hang on macOS keychain reads.',
|
|
55
|
+
fix: 'Export ANTHROPIC_API_KEY or CLAUDE_CODE_OAUTH_TOKEN before running AgentXchain, or add "--bare" to the Claude command if you intentionally want env-only auth.',
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function resolveSmokeProbeTimeoutMs(env, options = {}) {
|
|
60
|
+
if (Number.isFinite(options?.timeoutMs) && options.timeoutMs > 0) {
|
|
61
|
+
return options.timeoutMs;
|
|
62
|
+
}
|
|
63
|
+
const raw = env?.AGENTXCHAIN_CLAUDE_AUTH_PROBE_TIMEOUT_MS;
|
|
64
|
+
const parsed = Number.parseInt(raw, 10);
|
|
65
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_SMOKE_PROBE_TIMEOUT_MS;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function getClaudeSubprocessAuthIssue(runtime, env = process.env, options = {}) {
|
|
45
69
|
if (!isClaudeLocalCliRuntime(runtime)) {
|
|
46
70
|
return null;
|
|
47
71
|
}
|
|
@@ -50,12 +74,164 @@ export function getClaudeSubprocessAuthIssue(runtime, env = process.env) {
|
|
|
50
74
|
return null;
|
|
51
75
|
}
|
|
52
76
|
|
|
53
|
-
const
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
77
|
+
const smokeProbe = await runClaudeSmokeProbe({
|
|
78
|
+
runtime,
|
|
79
|
+
env,
|
|
80
|
+
timeoutMs: resolveSmokeProbeTimeoutMs(env, options),
|
|
81
|
+
stdinPayload: options?.stdinPayload,
|
|
82
|
+
spawnImpl: options?.spawnImpl,
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
if (smokeProbe.kind === 'stdout_observed' || smokeProbe.kind === 'spawn_error' || smokeProbe.kind === 'skipped') {
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (smokeProbe.kind === 'hang' || smokeProbe.kind === 'exit_nonzero' || smokeProbe.kind === 'stderr_only') {
|
|
90
|
+
return buildClaudeSubprocessAuthIssue(env, smokeProbe);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Bounded smoke probe that spawns the runtime's actual Claude command with a
|
|
98
|
+
* tiny prompt on stdin and a watchdog. Returns a classification:
|
|
99
|
+
*
|
|
100
|
+
* { kind: 'stdout_observed' } — real stdout arrived before watchdog;
|
|
101
|
+
* the setup is NOT hanging on auth.
|
|
102
|
+
* { kind: 'hang', elapsed_ms } — watchdog fired with no stdout/stderr
|
|
103
|
+
* bytes; the keychain-hang shape (BUG-54).
|
|
104
|
+
* { kind: 'stderr_only', ... } — process wrote stderr but no stdout
|
|
105
|
+
* before watchdog (auth error or similar).
|
|
106
|
+
* { kind: 'exit_nonzero', ... } — process exited non-zero with no stdout
|
|
107
|
+
* (explicit auth failure — not a hang).
|
|
108
|
+
* { kind: 'spawn_error', ... } — spawn itself failed (ENOENT / EPERM).
|
|
109
|
+
* { kind: 'skipped', reason } — probe disabled or unavailable.
|
|
110
|
+
*
|
|
111
|
+
* This is the positive-case-testable alternative to the static shape-check in
|
|
112
|
+
* `getClaudeSubprocessAuthIssue`: it observes what the subprocess actually
|
|
113
|
+
* does rather than predicting what it *might* do from config shape alone.
|
|
114
|
+
*
|
|
115
|
+
* Added 2026-04-21 for the BUG-56 false-positive fix. See
|
|
116
|
+
* `.planning/BUG_56_FALSE_POSITIVE_RETRO.md` for the decision trail.
|
|
117
|
+
*
|
|
118
|
+
* @param {{ runtime: object, env?: object, timeoutMs?: number, stdinPayload?: string, spawnImpl?: Function }} opts
|
|
119
|
+
* @returns {Promise<object>}
|
|
120
|
+
*/
|
|
121
|
+
export async function runClaudeSmokeProbe(opts) {
|
|
122
|
+
const runtime = opts?.runtime ?? null;
|
|
123
|
+
const env = opts?.env ?? process.env;
|
|
124
|
+
const timeoutMs = Number.isFinite(opts?.timeoutMs) ? opts.timeoutMs : DEFAULT_SMOKE_PROBE_TIMEOUT_MS;
|
|
125
|
+
const stdinPayload = typeof opts?.stdinPayload === 'string' ? opts.stdinPayload : DEFAULT_SMOKE_PROBE_STDIN;
|
|
126
|
+
const spawnImpl = typeof opts?.spawnImpl === 'function' ? opts.spawnImpl : spawn;
|
|
127
|
+
|
|
128
|
+
if (!isClaudeLocalCliRuntime(runtime)) {
|
|
129
|
+
return { kind: 'skipped', reason: 'not_claude_local_cli' };
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
const tokens = normalizeCommandTokens(runtime);
|
|
133
|
+
if (tokens.length === 0) {
|
|
134
|
+
return { kind: 'skipped', reason: 'empty_command' };
|
|
135
|
+
}
|
|
136
|
+
const [command, ...args] = tokens;
|
|
137
|
+
|
|
138
|
+
return new Promise((resolve) => {
|
|
139
|
+
let child;
|
|
140
|
+
try {
|
|
141
|
+
child = spawnImpl(command, args, {
|
|
142
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
143
|
+
env,
|
|
144
|
+
});
|
|
145
|
+
} catch (error) {
|
|
146
|
+
resolve({
|
|
147
|
+
kind: 'spawn_error',
|
|
148
|
+
errno: error?.errno ?? null,
|
|
149
|
+
code: error?.code ?? null,
|
|
150
|
+
message: error?.message ?? String(error),
|
|
151
|
+
});
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
if (!child || typeof child.on !== 'function') {
|
|
156
|
+
resolve({ kind: 'spawn_error', code: 'NO_CHILD_HANDLE', message: 'spawn returned no child handle' });
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const start = Date.now();
|
|
161
|
+
let stdoutBytes = 0;
|
|
162
|
+
let stderrBytes = 0;
|
|
163
|
+
let stderrBuf = '';
|
|
164
|
+
let settled = false;
|
|
165
|
+
|
|
166
|
+
const finish = (result) => {
|
|
167
|
+
if (settled) return;
|
|
168
|
+
settled = true;
|
|
169
|
+
try { child.kill('SIGTERM'); } catch { /* ignore */ }
|
|
170
|
+
resolve(result);
|
|
171
|
+
};
|
|
172
|
+
|
|
173
|
+
const watchdog = setTimeout(() => {
|
|
174
|
+
const elapsed_ms = Date.now() - start;
|
|
175
|
+
if (stdoutBytes > 0) {
|
|
176
|
+
finish({ kind: 'stdout_observed', elapsed_ms });
|
|
177
|
+
} else if (stderrBytes > 0) {
|
|
178
|
+
finish({ kind: 'stderr_only', elapsed_ms, stderr_snippet: stderrBuf.slice(0, 500) });
|
|
179
|
+
} else {
|
|
180
|
+
finish({ kind: 'hang', elapsed_ms });
|
|
181
|
+
}
|
|
182
|
+
}, timeoutMs);
|
|
183
|
+
if (typeof watchdog.unref === 'function') watchdog.unref();
|
|
184
|
+
|
|
185
|
+
child.stdout?.on('data', (chunk) => {
|
|
186
|
+
stdoutBytes += chunk.length;
|
|
187
|
+
if (stdoutBytes > 0 && !settled) {
|
|
188
|
+
clearTimeout(watchdog);
|
|
189
|
+
finish({ kind: 'stdout_observed', elapsed_ms: Date.now() - start });
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
child.stderr?.on('data', (chunk) => {
|
|
194
|
+
stderrBytes += chunk.length;
|
|
195
|
+
stderrBuf += chunk.toString('utf8');
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
child.on('error', (error) => {
|
|
199
|
+
clearTimeout(watchdog);
|
|
200
|
+
finish({
|
|
201
|
+
kind: 'spawn_error',
|
|
202
|
+
errno: error?.errno ?? null,
|
|
203
|
+
code: error?.code ?? null,
|
|
204
|
+
message: error?.message ?? String(error),
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
child.on('exit', (code, signal) => {
|
|
209
|
+
if (settled) return;
|
|
210
|
+
clearTimeout(watchdog);
|
|
211
|
+
const elapsed_ms = Date.now() - start;
|
|
212
|
+
if (stdoutBytes > 0) {
|
|
213
|
+
finish({ kind: 'stdout_observed', elapsed_ms });
|
|
214
|
+
} else if (code !== 0) {
|
|
215
|
+
finish({
|
|
216
|
+
kind: 'exit_nonzero',
|
|
217
|
+
elapsed_ms,
|
|
218
|
+
exit_code: code,
|
|
219
|
+
exit_signal: signal,
|
|
220
|
+
stderr_snippet: stderrBuf.slice(0, 500),
|
|
221
|
+
});
|
|
222
|
+
} else if (stderrBytes > 0) {
|
|
223
|
+
finish({ kind: 'stderr_only', elapsed_ms, stderr_snippet: stderrBuf.slice(0, 500) });
|
|
224
|
+
} else {
|
|
225
|
+
finish({ kind: 'hang', elapsed_ms });
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
try {
|
|
230
|
+
child.stdin?.end(`${stdinPayload}\n`);
|
|
231
|
+
} catch {
|
|
232
|
+
// best-effort; error will surface via 'error' event if real
|
|
233
|
+
}
|
|
234
|
+
});
|
|
59
235
|
}
|
|
60
236
|
|
|
61
237
|
export { CLAUDE_ENV_AUTH_KEYS, normalizeCommandTokens };
|
|
@@ -165,30 +165,30 @@ async function probeLocalCommand(runtimeId, runtime, probeKindLabel, options = {
|
|
|
165
165
|
};
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
//
|
|
173
|
-
//
|
|
174
|
-
//
|
|
175
|
-
//
|
|
176
|
-
// else. If they fix auth (or add --bare) but still do not have claude
|
|
177
|
-
// installed, the next connector check surfaces command_presence after they
|
|
178
|
-
// fix the config — that is the correct operator progression.
|
|
168
|
+
const claudeAuthIssue = await getClaudeSubprocessAuthIssue(runtime, process.env, {
|
|
169
|
+
timeoutMs: options.claudeAuthProbeTimeoutMs,
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
// DEC-BUG56-PREFLIGHT-PROBE-OVER-SHAPE-CHECK-001
|
|
173
|
+
// Auth-preflight is observation-based: a no-env/no-bare Claude runtime is
|
|
174
|
+
// only refused when a bounded smoke probe actually hangs or fails without
|
|
175
|
+
// stdout. Working Claude Max keychain setups must pass this gate.
|
|
179
176
|
if (claudeAuthIssue) {
|
|
180
177
|
return {
|
|
181
178
|
...base,
|
|
182
179
|
level: 'fail',
|
|
183
180
|
probe_kind: 'auth_preflight',
|
|
184
|
-
command:
|
|
181
|
+
command: formatTarget(runtime) || head,
|
|
185
182
|
error_code: 'claude_auth_preflight_failed',
|
|
186
183
|
detail: claudeAuthIssue.detail,
|
|
187
184
|
fix: claudeAuthIssue.fix,
|
|
188
185
|
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
186
|
+
smoke_probe: claudeAuthIssue.smoke_probe,
|
|
189
187
|
};
|
|
190
188
|
}
|
|
191
189
|
|
|
190
|
+
const spawnProbe = probeRuntimeSpawnContext(options.root || process.cwd(), runtime, { runtimeId });
|
|
191
|
+
|
|
192
192
|
if (!spawnProbe.ok) {
|
|
193
193
|
return {
|
|
194
194
|
...base,
|
|
@@ -401,8 +401,6 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
401
401
|
// Prompt transport validation
|
|
402
402
|
const transport = runtime.prompt_transport || 'dispatch_bundle_only';
|
|
403
403
|
const knownTransports = KNOWN_CLI_TRANSPORTS[binaryName];
|
|
404
|
-
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime);
|
|
405
|
-
|
|
406
404
|
if (transport === 'argv' && !commandTokens.some((token) => token.includes('{prompt}'))) {
|
|
407
405
|
warnings.push({
|
|
408
406
|
probe_kind: 'transport_intent',
|
|
@@ -422,15 +420,6 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
422
420
|
});
|
|
423
421
|
}
|
|
424
422
|
|
|
425
|
-
if (claudeAuthIssue) {
|
|
426
|
-
warnings.push({
|
|
427
|
-
probe_kind: 'auth_preflight',
|
|
428
|
-
level: 'warn',
|
|
429
|
-
detail: claudeAuthIssue.detail,
|
|
430
|
-
fix: claudeAuthIssue.fix,
|
|
431
|
-
});
|
|
432
|
-
}
|
|
433
|
-
|
|
434
423
|
return { warnings };
|
|
435
424
|
}
|
|
436
425
|
|
|
@@ -105,11 +105,10 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
|
|
|
105
105
|
};
|
|
106
106
|
}
|
|
107
107
|
|
|
108
|
-
// DEC-
|
|
109
|
-
//
|
|
110
|
-
//
|
|
111
|
-
|
|
112
|
-
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime);
|
|
108
|
+
// DEC-BUG56-PREFLIGHT-PROBE-OVER-SHAPE-CHECK-001 — refuse Claude local_cli
|
|
109
|
+
// auth-hang shapes only after a bounded smoke probe observes no stdout.
|
|
110
|
+
// Working Claude Max keychain setups must pass instead of false-positive.
|
|
111
|
+
const claudeAuthIssue = await getClaudeSubprocessAuthIssue(runtime);
|
|
113
112
|
if (claudeAuthIssue) {
|
|
114
113
|
return {
|
|
115
114
|
ok: false,
|
|
@@ -131,6 +130,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
|
|
|
131
130
|
error_code: 'claude_auth_preflight_failed',
|
|
132
131
|
error: claudeAuthIssue.detail,
|
|
133
132
|
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
133
|
+
smoke_probe: claudeAuthIssue.smoke_probe,
|
|
134
134
|
fix: claudeAuthIssue.fix,
|
|
135
135
|
dispatch: null,
|
|
136
136
|
validation: null,
|
|
@@ -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';
|
|
@@ -80,7 +80,7 @@
|
|
|
80
80
|
"manual-pm": { "type": "manual" },
|
|
81
81
|
"local-dev": {
|
|
82
82
|
"type": "local_cli",
|
|
83
|
-
"command": ["claude", "--print", "--dangerously-skip-permissions"],
|
|
83
|
+
"command": ["claude", "--print", "--dangerously-skip-permissions", "--bare"],
|
|
84
84
|
"cwd": ".",
|
|
85
85
|
"prompt_transport": "stdin"
|
|
86
86
|
},
|
|
@@ -44,25 +44,25 @@
|
|
|
44
44
|
"runtimes": {
|
|
45
45
|
"local-pm": {
|
|
46
46
|
"type": "local_cli",
|
|
47
|
-
"command": ["claude", "--print", "--dangerously-skip-permissions"],
|
|
47
|
+
"command": ["claude", "--print", "--dangerously-skip-permissions", "--bare"],
|
|
48
48
|
"cwd": ".",
|
|
49
49
|
"prompt_transport": "stdin"
|
|
50
50
|
},
|
|
51
51
|
"local-dev": {
|
|
52
52
|
"type": "local_cli",
|
|
53
|
-
"command": ["claude", "--print", "--dangerously-skip-permissions"],
|
|
53
|
+
"command": ["claude", "--print", "--dangerously-skip-permissions", "--bare"],
|
|
54
54
|
"cwd": ".",
|
|
55
55
|
"prompt_transport": "stdin"
|
|
56
56
|
},
|
|
57
57
|
"local-qa": {
|
|
58
58
|
"type": "local_cli",
|
|
59
|
-
"command": ["claude", "--print", "--dangerously-skip-permissions"],
|
|
59
|
+
"command": ["claude", "--print", "--dangerously-skip-permissions", "--bare"],
|
|
60
60
|
"cwd": ".",
|
|
61
61
|
"prompt_transport": "stdin"
|
|
62
62
|
},
|
|
63
63
|
"local-director": {
|
|
64
64
|
"type": "local_cli",
|
|
65
|
-
"command": ["claude", "--print", "--dangerously-skip-permissions"],
|
|
65
|
+
"command": ["claude", "--print", "--dangerously-skip-permissions", "--bare"],
|
|
66
66
|
"cwd": ".",
|
|
67
67
|
"prompt_transport": "stdin"
|
|
68
68
|
}
|