agentxchain 2.148.0 → 2.149.1
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/dashboard/components/timeline.js +15 -2
- package/package.json +1 -1
- package/scripts/reproduce-bug-54.mjs +623 -0
- package/src/commands/connector.js +23 -4
- package/src/commands/doctor.js +11 -0
- package/src/commands/run.js +18 -3
- package/src/commands/status.js +30 -3
- package/src/commands/step.js +8 -2
- package/src/lib/adapters/local-cli-adapter.js +49 -2
- package/src/lib/claude-local-auth.js +61 -0
- package/src/lib/connector-probe.js +48 -21
- package/src/lib/connector-validate.js +34 -0
- package/src/lib/dispatch-progress.js +32 -6
- package/src/lib/dispatch-streams.js +21 -0
- package/src/lib/governed-state.js +84 -6
- package/src/lib/normalized-config.js +12 -0
- package/src/lib/schemas/agentxchain-config.schema.json +5 -0
- package/src/lib/stale-turn-watchdog.js +31 -6
- package/src/lib/turn-checkpoint.js +58 -11
|
@@ -5,6 +5,17 @@ import { DEFAULT_VALIDATE_TIMEOUT_MS, validateConfiguredConnector } from '../lib
|
|
|
5
5
|
import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
|
|
6
6
|
import { buildRuntimeCapabilityReport } from '../lib/runtime-capabilities.js';
|
|
7
7
|
|
|
8
|
+
function warningDetail(warning) {
|
|
9
|
+
if (typeof warning === 'string') {
|
|
10
|
+
return warning;
|
|
11
|
+
}
|
|
12
|
+
return warning?.detail || JSON.stringify(warning);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function warningFix(warning) {
|
|
16
|
+
return typeof warning === 'object' && warning?.fix ? warning.fix : null;
|
|
17
|
+
}
|
|
18
|
+
|
|
8
19
|
function printJson(result, exitCode) {
|
|
9
20
|
console.log(JSON.stringify(result, null, 2));
|
|
10
21
|
process.exit(exitCode);
|
|
@@ -49,11 +60,15 @@ function printText(result, exitCode) {
|
|
|
49
60
|
console.log(` ${chalk.dim('Time:')} ${connector.latency_ms}ms`);
|
|
50
61
|
}
|
|
51
62
|
console.log(` ${chalk.dim('Detail:')} ${connector.detail}`);
|
|
63
|
+
if (connector.fix) {
|
|
64
|
+
console.log(` ${chalk.dim('Fix:')} ${connector.fix}`);
|
|
65
|
+
}
|
|
52
66
|
if (Array.isArray(connector.authority_warnings) && connector.authority_warnings.length > 0) {
|
|
53
67
|
for (const warning of connector.authority_warnings) {
|
|
54
|
-
console.log(` ${chalk.yellow('⚠')} ${warning
|
|
55
|
-
|
|
56
|
-
|
|
68
|
+
console.log(` ${chalk.yellow('⚠')} ${warningDetail(warning)}`);
|
|
69
|
+
const fix = warningFix(warning);
|
|
70
|
+
if (fix) {
|
|
71
|
+
console.log(` ${chalk.dim('Fix:')} ${fix}`);
|
|
57
72
|
}
|
|
58
73
|
}
|
|
59
74
|
}
|
|
@@ -161,7 +176,11 @@ function printValidateText(result, exitCode) {
|
|
|
161
176
|
if (Array.isArray(result.warnings) && result.warnings.length > 0) {
|
|
162
177
|
console.log('');
|
|
163
178
|
for (const warning of result.warnings) {
|
|
164
|
-
console.log(` ${chalk.yellow('!')} ${warning}`);
|
|
179
|
+
console.log(` ${chalk.yellow('!')} ${warningDetail(warning)}`);
|
|
180
|
+
const fix = warningFix(warning);
|
|
181
|
+
if (fix) {
|
|
182
|
+
console.log(` ${chalk.dim('Fix:')} ${fix}`);
|
|
183
|
+
}
|
|
165
184
|
}
|
|
166
185
|
}
|
|
167
186
|
|
package/src/commands/doctor.js
CHANGED
|
@@ -21,6 +21,7 @@ import { detectActiveTurnBindingDrift, detectStateBundleDesync } from '../lib/go
|
|
|
21
21
|
import { findPendingApprovedIntents } from '../lib/intake.js';
|
|
22
22
|
import { checkCleanBaseline } from '../lib/repo-observer.js';
|
|
23
23
|
import { probeRuntimeSpawnContext } from '../lib/runtime-spawn-context.js';
|
|
24
|
+
import { getClaudeSubprocessAuthIssue } from '../lib/claude-local-auth.js';
|
|
24
25
|
|
|
25
26
|
export async function doctorCommand(opts = {}) {
|
|
26
27
|
const root = findProjectRoot(process.cwd());
|
|
@@ -500,6 +501,16 @@ function checkRuntimeReachable(root, rtId, rt, boundRoleEntries = []) {
|
|
|
500
501
|
|
|
501
502
|
case 'local_cli': {
|
|
502
503
|
const probe = probeRuntimeSpawnContext(root, rt, { runtimeId: rtId });
|
|
504
|
+
if (probe.ok) {
|
|
505
|
+
const claudeAuthIssue = getClaudeSubprocessAuthIssue(rt);
|
|
506
|
+
if (claudeAuthIssue) {
|
|
507
|
+
return attachRuntimeContract({
|
|
508
|
+
...base,
|
|
509
|
+
level: 'warn',
|
|
510
|
+
detail: `${probe.detail} ${claudeAuthIssue.detail} ${claudeAuthIssue.fix}`,
|
|
511
|
+
}, rtId, rt, boundRoleEntries);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
503
514
|
return attachRuntimeContract({ ...base, level: probe.ok ? 'pass' : 'fail', detail: probe.detail }, rtId, rt, boundRoleEntries);
|
|
504
515
|
}
|
|
505
516
|
|
package/src/commands/run.js
CHANGED
|
@@ -25,6 +25,7 @@ import { validateParentRun } from '../lib/run-history.js';
|
|
|
25
25
|
import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
|
|
26
26
|
import {
|
|
27
27
|
dispatchLocalCli,
|
|
28
|
+
resolveStartupWatchdogMs,
|
|
28
29
|
saveDispatchLogs,
|
|
29
30
|
resolvePromptTransport,
|
|
30
31
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
@@ -52,6 +53,7 @@ import { emitRunEvent } from '../lib/run-events.js';
|
|
|
52
53
|
import { checkpointAcceptedTurn } from '../lib/turn-checkpoint.js';
|
|
53
54
|
import { failTurnStartup } from '../lib/stale-turn-watchdog.js';
|
|
54
55
|
import { hasMinimumTurnResultShape } from '../lib/turn-result-shape.js';
|
|
56
|
+
import { isKnownTurnRunningProofStream } from '../lib/dispatch-streams.js';
|
|
55
57
|
|
|
56
58
|
export async function runCommand(opts) {
|
|
57
59
|
const context = loadProjectContext();
|
|
@@ -343,7 +345,10 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
343
345
|
});
|
|
344
346
|
};
|
|
345
347
|
|
|
346
|
-
const ensureRunningState = (stream =
|
|
348
|
+
const ensureRunningState = (stream = null, at = new Date().toISOString()) => {
|
|
349
|
+
if (stream != null && !isKnownTurnRunningProofStream(stream)) {
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
347
352
|
if (runningMarked) return;
|
|
348
353
|
runningMarked = true;
|
|
349
354
|
transitionActiveTurnLifecycle(projectRoot, turn.turn_id, 'running', { stream, at });
|
|
@@ -359,7 +364,16 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
359
364
|
};
|
|
360
365
|
|
|
361
366
|
const recordOutputActivity = (stream, text) => {
|
|
362
|
-
|
|
367
|
+
// DEC-BUG54-STDERR-IS-NOT-STARTUP-PROOF-002 (Turn 88) extended to the
|
|
368
|
+
// run-command lifecycle in Turn 89: stderr activity must NOT promote a
|
|
369
|
+
// turn from `starting` to `running`. stdout (or the adapter's
|
|
370
|
+
// onFirstOutput callback, which is stdout/staged_result only post-Turn
|
|
371
|
+
// 88) is the only signal that satisfies the lifecycle transition.
|
|
372
|
+
// stderr is still tracked by the progress tracker for silence detection
|
|
373
|
+
// and operator diagnostics.
|
|
374
|
+
if (stream != null && isKnownTurnRunningProofStream(stream)) {
|
|
375
|
+
ensureRunningState(stream);
|
|
376
|
+
}
|
|
363
377
|
const lines = text.split('\n').length - 1 || 1;
|
|
364
378
|
const wasSilent = tracker.onOutput(stream, lines);
|
|
365
379
|
if (wasSilent) {
|
|
@@ -473,9 +487,10 @@ export async function executeGovernedRun(context, opts = {}) {
|
|
|
473
487
|
|
|
474
488
|
if (adapterResult.startupFailure) {
|
|
475
489
|
const freshState = loadProjectState(projectRoot, cfg) || state;
|
|
490
|
+
const startupThresholdMs = resolveStartupWatchdogMs(cfg, runtime);
|
|
476
491
|
failTurnStartup(projectRoot, freshState, cfg, turn.turn_id, {
|
|
477
492
|
failure_type: adapterResult.startupFailureType || 'no_subprocess_output',
|
|
478
|
-
threshold_ms:
|
|
493
|
+
threshold_ms: startupThresholdMs,
|
|
479
494
|
running_ms: freshState?.active_turns?.[turn.turn_id]?.started_at
|
|
480
495
|
? Math.max(0, Date.now() - new Date(freshState.active_turns[turn.turn_id].started_at).getTime())
|
|
481
496
|
: 0,
|
package/src/commands/status.js
CHANGED
|
@@ -383,7 +383,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
383
383
|
console.log(` ${chalk.dim(' or:')} ${chalk.cyan(`agentxchain accept-turn --turn ${turn.turn_id}`)} — re-attempt acceptance`);
|
|
384
384
|
}
|
|
385
385
|
if (turn.status === 'failed_start') {
|
|
386
|
-
console.log(` ${chalk.dim('Reason:')} ${turn.failed_start_reason
|
|
386
|
+
console.log(` ${chalk.dim('Reason:')} ${normalizeStartupFailureReasonForDisplay(turn.failed_start_reason)}`);
|
|
387
387
|
const recover = turn.recovery_command || `agentxchain reissue-turn --turn ${turn.turn_id} --reason ghost`;
|
|
388
388
|
console.log(` ${chalk.dim('Recover:')} ${chalk.cyan(recover)}`);
|
|
389
389
|
}
|
|
@@ -443,7 +443,7 @@ function renderGovernedStatus(context, opts) {
|
|
|
443
443
|
console.log(` ${chalk.dim(' or:')} ${chalk.cyan(mergeAction.command)}`);
|
|
444
444
|
}
|
|
445
445
|
if (singleActiveTurn.status === 'failed_start') {
|
|
446
|
-
console.log(` ${chalk.dim('Reason:')} ${singleActiveTurn.failed_start_reason
|
|
446
|
+
console.log(` ${chalk.dim('Reason:')} ${normalizeStartupFailureReasonForDisplay(singleActiveTurn.failed_start_reason)}`);
|
|
447
447
|
const recover = singleActiveTurn.recovery_command || `agentxchain reissue-turn --turn ${singleActiveTurn.turn_id} --reason ghost`;
|
|
448
448
|
console.log(` ${chalk.dim('Recover:')} ${chalk.cyan(recover)}`);
|
|
449
449
|
}
|
|
@@ -883,6 +883,24 @@ function pluralizeRepoDecisionCount(count, singular, plural) {
|
|
|
883
883
|
return `${count} ${count === 1 ? singular : plural}`;
|
|
884
884
|
}
|
|
885
885
|
|
|
886
|
+
// BUG-54 vocabulary discipline (`DEC-BUG54-OPERATOR-SUBTYPE-DISPLAY-001`).
|
|
887
|
+
// Operator-facing status surfaces must render a typed startup-failure subtype,
|
|
888
|
+
// not the raw adapter signal `no_subprocess_output`. Public docs
|
|
889
|
+
// (website-v2/docs/cli.mdx) only document `runtime_spawn_failed` and
|
|
890
|
+
// `stdout_attach_failed` as the operator-visible subtypes; the `no_subprocess_output`
|
|
891
|
+
// label is an internal adapter/classification fallback and must not leak to the
|
|
892
|
+
// CLI status display. The adapter semantics for `no_subprocess_output` ("we
|
|
893
|
+
// watched for stdout and saw none inside the startup watchdog window") are
|
|
894
|
+
// identical to the operator subtype `stdout_attach_failed`, so that is the
|
|
895
|
+
// correct display normalization.
|
|
896
|
+
const TYPED_STARTUP_FAILURE_SUBTYPES = new Set(['runtime_spawn_failed', 'stdout_attach_failed']);
|
|
897
|
+
function normalizeStartupFailureReasonForDisplay(rawReason) {
|
|
898
|
+
if (typeof rawReason === 'string' && TYPED_STARTUP_FAILURE_SUBTYPES.has(rawReason)) {
|
|
899
|
+
return rawReason;
|
|
900
|
+
}
|
|
901
|
+
return 'stdout_attach_failed';
|
|
902
|
+
}
|
|
903
|
+
|
|
886
904
|
function filterDispatchProgressForActiveTurns(progressByTurn, activeTurns) {
|
|
887
905
|
const filtered = {};
|
|
888
906
|
if (!progressByTurn || typeof progressByTurn !== 'object') {
|
|
@@ -897,7 +915,7 @@ function filterDispatchProgressForActiveTurns(progressByTurn, activeTurns) {
|
|
|
897
915
|
return filtered;
|
|
898
916
|
}
|
|
899
917
|
|
|
900
|
-
function formatDispatchActivityLine(progress) {
|
|
918
|
+
export function formatDispatchActivityLine(progress) {
|
|
901
919
|
if (!progress || typeof progress !== 'object') return null;
|
|
902
920
|
const lastAct = progress.last_activity_at ? new Date(progress.last_activity_at) : null;
|
|
903
921
|
const agoSec = lastAct && !Number.isNaN(lastAct.getTime())
|
|
@@ -918,6 +936,15 @@ function formatDispatchActivityLine(progress) {
|
|
|
918
936
|
if (progress.activity_type === 'response') {
|
|
919
937
|
return chalk.green('API response received');
|
|
920
938
|
}
|
|
939
|
+
// DEC-BUG54-DIAGNOSTIC-ACTIVITY-TYPE-001 (Turn 91): stderr-only activity
|
|
940
|
+
// must be rendered as yellow "Diagnostic output only" — never as the green
|
|
941
|
+
// "Producing output" signal that previously leaked onto the operator
|
|
942
|
+
// surface for failing-startup subprocesses whose stdout never attached.
|
|
943
|
+
if (progress.activity_type === 'diagnostic_only') {
|
|
944
|
+
const agoLabel = agoSec != null && agoSec > 0 ? `, last ${agoSec}s ago` : '';
|
|
945
|
+
return chalk.yellow('Diagnostic output only') +
|
|
946
|
+
` (${progress.stderr_lines || 0} stderr lines, no stdout yet${agoLabel})`;
|
|
947
|
+
}
|
|
921
948
|
const agoLabel = agoSec != null && agoSec > 0 ? `, last ${agoSec}s ago` : '';
|
|
922
949
|
return chalk.green('Producing output') + ` (${progress.output_lines || 0} lines${agoLabel})`;
|
|
923
950
|
}
|
package/src/commands/step.js
CHANGED
|
@@ -49,6 +49,7 @@ import {
|
|
|
49
49
|
} from '../lib/adapters/manual-adapter.js';
|
|
50
50
|
import {
|
|
51
51
|
dispatchLocalCli,
|
|
52
|
+
resolveStartupWatchdogMs,
|
|
52
53
|
saveDispatchLogs,
|
|
53
54
|
resolvePromptTransport,
|
|
54
55
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
@@ -73,6 +74,7 @@ import { shouldSuggestManualQaFallback } from '../lib/manual-qa-fallback.js';
|
|
|
73
74
|
import { evaluateApprovalSlaReminders } from '../lib/notification-runner.js';
|
|
74
75
|
import { consumeNextApprovedIntent } from '../lib/intake.js';
|
|
75
76
|
import { failTurnStartup, reconcileStaleTurns } from '../lib/stale-turn-watchdog.js';
|
|
77
|
+
import { isKnownTurnRunningProofStream } from '../lib/dispatch-streams.js';
|
|
76
78
|
|
|
77
79
|
export async function stepCommand(opts) {
|
|
78
80
|
const context = loadProjectContext();
|
|
@@ -697,7 +699,10 @@ export async function stepCommand(opts) {
|
|
|
697
699
|
state = starting.state;
|
|
698
700
|
}
|
|
699
701
|
};
|
|
700
|
-
const ensureRunningState = (stream =
|
|
702
|
+
const ensureRunningState = (stream = null, at = new Date().toISOString()) => {
|
|
703
|
+
if (stream != null && !isKnownTurnRunningProofStream(stream)) {
|
|
704
|
+
return;
|
|
705
|
+
}
|
|
701
706
|
if (runningMarked) return;
|
|
702
707
|
runningMarked = true;
|
|
703
708
|
const running = transitionActiveTurnLifecycle(root, turn.turn_id, 'running', { stream, at });
|
|
@@ -756,9 +761,10 @@ export async function stepCommand(opts) {
|
|
|
756
761
|
|
|
757
762
|
if (cliResult.startupFailure) {
|
|
758
763
|
const freshState = loadProjectState(root, config) || state;
|
|
764
|
+
const startupThresholdMs = resolveStartupWatchdogMs(config, runtime);
|
|
759
765
|
const failed = failTurnStartup(root, freshState, config, turn.turn_id, {
|
|
760
766
|
failure_type: cliResult.startupFailureType || 'no_subprocess_output',
|
|
761
|
-
threshold_ms:
|
|
767
|
+
threshold_ms: startupThresholdMs,
|
|
762
768
|
running_ms: freshState?.active_turns?.[turn.turn_id]?.started_at
|
|
763
769
|
? Math.max(0, Date.now() - new Date(freshState.active_turns[turn.turn_id].started_at).getTime())
|
|
764
770
|
: 0,
|
|
@@ -30,6 +30,7 @@ import {
|
|
|
30
30
|
} from '../turn-paths.js';
|
|
31
31
|
import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
32
32
|
import { hasMeaningfulStagedResult } from '../staged-result-proof.js';
|
|
33
|
+
import { getClaudeSubprocessAuthIssue } from '../claude-local-auth.js';
|
|
33
34
|
|
|
34
35
|
const DIAGNOSTIC_ENV_KEYS = [
|
|
35
36
|
'PATH',
|
|
@@ -39,6 +40,7 @@ const DIAGNOSTIC_ENV_KEYS = [
|
|
|
39
40
|
'TMPDIR',
|
|
40
41
|
'AGENTXCHAIN_TURN_ID',
|
|
41
42
|
];
|
|
43
|
+
const DIAGNOSTIC_STDERR_EXCERPT_LIMIT = 800;
|
|
42
44
|
|
|
43
45
|
/**
|
|
44
46
|
* Launch a local CLI subprocess for a governed turn.
|
|
@@ -64,7 +66,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
64
66
|
onStderr,
|
|
65
67
|
onSpawnAttached,
|
|
66
68
|
onFirstOutput,
|
|
67
|
-
startupWatchdogMs
|
|
69
|
+
startupWatchdogMs: startupWatchdogOverrideMs,
|
|
68
70
|
turnId,
|
|
69
71
|
} = options;
|
|
70
72
|
|
|
@@ -85,6 +87,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
85
87
|
if (!runtime) {
|
|
86
88
|
return { ok: false, error: `Runtime "${runtimeId}" not found in config` };
|
|
87
89
|
}
|
|
90
|
+
const startupWatchdogMs = startupWatchdogOverrideMs ?? resolveStartupWatchdogMs(config, runtime);
|
|
88
91
|
|
|
89
92
|
// Read the dispatch bundle prompt
|
|
90
93
|
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
@@ -125,6 +128,21 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
125
128
|
const spawnEnv = { ...process.env, AGENTXCHAIN_TURN_ID: turn.turn_id };
|
|
126
129
|
const stdinBytes = transport === 'stdin' ? Buffer.byteLength(fullPrompt, 'utf8') : 0;
|
|
127
130
|
const diagnosticArgs = redactPromptArgs(args, fullPrompt, transport);
|
|
131
|
+
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime, spawnEnv);
|
|
132
|
+
|
|
133
|
+
if (claudeAuthIssue) {
|
|
134
|
+
appendDiagnostic(logs, 'claude_auth_preflight_failed', {
|
|
135
|
+
runtime_id: runtimeId,
|
|
136
|
+
turn_id: turn.turn_id,
|
|
137
|
+
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
138
|
+
recommendation: claudeAuthIssue.fix,
|
|
139
|
+
});
|
|
140
|
+
return {
|
|
141
|
+
ok: false,
|
|
142
|
+
error: `${claudeAuthIssue.detail} ${claudeAuthIssue.fix}`,
|
|
143
|
+
logs,
|
|
144
|
+
};
|
|
145
|
+
}
|
|
128
146
|
|
|
129
147
|
return new Promise((resolve) => {
|
|
130
148
|
if (signal?.aborted) {
|
|
@@ -163,6 +181,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
163
181
|
|
|
164
182
|
let settled = false;
|
|
165
183
|
let firstOutputAt = null;
|
|
184
|
+
let firstOutputStream = null;
|
|
166
185
|
let spawnConfirmedAt = null;
|
|
167
186
|
let spawnConfirmedAtMs = null;
|
|
168
187
|
let firstOutputLatencyMs = null;
|
|
@@ -171,6 +190,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
171
190
|
let startupFailureType = null;
|
|
172
191
|
let stdoutBytes = 0;
|
|
173
192
|
let stderrBytes = 0;
|
|
193
|
+
let stderrExcerpt = '';
|
|
174
194
|
|
|
175
195
|
const settle = (result) => {
|
|
176
196
|
if (settled) return;
|
|
@@ -211,6 +231,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
211
231
|
const recordFirstOutput = (stream) => {
|
|
212
232
|
if (firstOutputAt) return;
|
|
213
233
|
firstOutputAt = new Date().toISOString();
|
|
234
|
+
firstOutputStream = stream;
|
|
214
235
|
firstOutputLatencyMs = spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs);
|
|
215
236
|
clearStartupWatchdog();
|
|
216
237
|
appendDiagnostic(logs, 'first_output', {
|
|
@@ -280,7 +301,7 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
280
301
|
child.stderr.on('data', (chunk) => {
|
|
281
302
|
const text = chunk.toString();
|
|
282
303
|
stderrBytes += Buffer.byteLength(text);
|
|
283
|
-
|
|
304
|
+
stderrExcerpt = appendDiagnosticExcerpt(stderrExcerpt, text, DIAGNOSTIC_STDERR_EXCERPT_LIMIT);
|
|
284
305
|
logs.push('[stderr] ' + text);
|
|
285
306
|
if (onStderr) onStderr(text);
|
|
286
307
|
});
|
|
@@ -349,14 +370,20 @@ export async function dispatchLocalCli(root, state, config, options = {}) {
|
|
|
349
370
|
pid: child.pid ?? null,
|
|
350
371
|
exit_code: exitCode,
|
|
351
372
|
signal: killSignal,
|
|
373
|
+
exit_signal: killSignal,
|
|
352
374
|
spawn_confirmed_at: spawnConfirmedAt,
|
|
353
375
|
elapsed_since_spawn_ms: spawnConfirmedAtMs == null ? null : Math.max(0, Date.now() - spawnConfirmedAtMs),
|
|
354
376
|
first_output_at: firstOutputAt,
|
|
377
|
+
first_output_stream: firstOutputStream,
|
|
355
378
|
startup_latency_ms: firstOutputLatencyMs,
|
|
356
379
|
stdout_bytes: stdoutBytes,
|
|
357
380
|
stderr_bytes: stderrBytes,
|
|
358
381
|
staged_result_ready: hasResult,
|
|
382
|
+
watchdog_fired: startupTimedOut,
|
|
359
383
|
};
|
|
384
|
+
if (stderrExcerpt) {
|
|
385
|
+
exitDiagnostic.stderr_excerpt = stderrExcerpt;
|
|
386
|
+
}
|
|
360
387
|
if (startupTimedOut) {
|
|
361
388
|
exitDiagnostic.startup_failure_type = startupFailureType || 'no_subprocess_output';
|
|
362
389
|
} else if (!spawnConfirmedAt) {
|
|
@@ -543,6 +570,16 @@ function resolvePromptTransport(runtime) {
|
|
|
543
570
|
return hasPlaceholder ? 'argv' : 'dispatch_bundle_only';
|
|
544
571
|
}
|
|
545
572
|
|
|
573
|
+
function resolveStartupWatchdogMs(config, runtime) {
|
|
574
|
+
if (runtime?.type === 'local_cli' && Number.isInteger(runtime?.startup_watchdog_ms) && runtime.startup_watchdog_ms > 0) {
|
|
575
|
+
return runtime.startup_watchdog_ms;
|
|
576
|
+
}
|
|
577
|
+
if (Number.isInteger(config?.run_loop?.startup_watchdog_ms) && config.run_loop.startup_watchdog_ms > 0) {
|
|
578
|
+
return config.run_loop.startup_watchdog_ms;
|
|
579
|
+
}
|
|
580
|
+
return 30_000;
|
|
581
|
+
}
|
|
582
|
+
|
|
546
583
|
/**
|
|
547
584
|
* Check if the staged result file exists and has meaningful content.
|
|
548
585
|
* Delegates to the shared `hasMeaningfulStagedResult` helper so watchdog,
|
|
@@ -595,4 +632,14 @@ function normalizeDiagnosticError(err) {
|
|
|
595
632
|
};
|
|
596
633
|
}
|
|
597
634
|
|
|
635
|
+
function appendDiagnosticExcerpt(existing, chunk, limit) {
|
|
636
|
+
const combined = `${existing}${chunk}`;
|
|
637
|
+
if (combined.length <= limit) {
|
|
638
|
+
return combined;
|
|
639
|
+
}
|
|
640
|
+
return combined.slice(combined.length - limit);
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
export { resolveCommand };
|
|
598
644
|
export { resolvePromptTransport };
|
|
645
|
+
export { resolveStartupWatchdogMs };
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
const CLAUDE_ENV_AUTH_KEYS = [
|
|
2
|
+
'ANTHROPIC_API_KEY',
|
|
3
|
+
'CLAUDE_API_KEY',
|
|
4
|
+
'CLAUDE_CODE_OAUTH_TOKEN',
|
|
5
|
+
'CLAUDE_CODE_USE_VERTEX',
|
|
6
|
+
'CLAUDE_CODE_USE_BEDROCK',
|
|
7
|
+
];
|
|
8
|
+
|
|
9
|
+
function normalizeCommandTokens(runtime) {
|
|
10
|
+
if (Array.isArray(runtime?.command)) {
|
|
11
|
+
return runtime.command.flatMap((element) =>
|
|
12
|
+
typeof element === 'string' ? element.trim().split(/\s+/).filter(Boolean) : []
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
if (typeof runtime?.command === 'string' && runtime.command.trim()) {
|
|
16
|
+
return runtime.command.trim().split(/\s+/).filter(Boolean);
|
|
17
|
+
}
|
|
18
|
+
return [];
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function isClaudeLocalCliRuntime(runtime) {
|
|
22
|
+
const tokens = normalizeCommandTokens(runtime);
|
|
23
|
+
if (tokens.length === 0) {
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
const head = tokens[0].toLowerCase();
|
|
27
|
+
return head === 'claude' || head.endsWith('/claude');
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function hasClaudeBareFlag(runtime) {
|
|
31
|
+
return normalizeCommandTokens(runtime).includes('--bare');
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function getClaudeEnvAuthPresence(env = process.env) {
|
|
35
|
+
return Object.fromEntries(
|
|
36
|
+
CLAUDE_ENV_AUTH_KEYS.map((key) => [key, Boolean(env?.[key])]),
|
|
37
|
+
);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function hasClaudeEnvAuth(env = process.env) {
|
|
41
|
+
return Object.values(getClaudeEnvAuthPresence(env)).some(Boolean);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function getClaudeSubprocessAuthIssue(runtime, env = process.env) {
|
|
45
|
+
if (!isClaudeLocalCliRuntime(runtime)) {
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (hasClaudeBareFlag(runtime) || hasClaudeEnvAuth(env)) {
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const auth_env_present = getClaudeEnvAuthPresence(env);
|
|
54
|
+
return {
|
|
55
|
+
auth_env_present,
|
|
56
|
+
detail: 'Claude local_cli runtime has no env-based auth and is missing "--bare"; non-interactive subprocesses can hang on macOS keychain reads.',
|
|
57
|
+
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.',
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export { CLAUDE_ENV_AUTH_KEYS, normalizeCommandTokens };
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
PROVIDER_ENDPOINTS,
|
|
5
5
|
} from './adapters/api-proxy-adapter.js';
|
|
6
6
|
import { probeRuntimeSpawnContext } from './runtime-spawn-context.js';
|
|
7
|
+
import { getClaudeSubprocessAuthIssue, normalizeCommandTokens } from './claude-local-auth.js';
|
|
7
8
|
|
|
8
9
|
const PROBEABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
9
10
|
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
@@ -165,6 +166,38 @@ async function probeLocalCommand(runtimeId, runtime, probeKindLabel, options = {
|
|
|
165
166
|
}
|
|
166
167
|
|
|
167
168
|
const spawnProbe = probeRuntimeSpawnContext(options.root || process.cwd(), runtime, { runtimeId });
|
|
169
|
+
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime);
|
|
170
|
+
|
|
171
|
+
// DEC-BUG54-CLAUDE-AUTH-PREFLIGHT-001 / DEC-BUG54-VALIDATE-AUTH-PREFLIGHT-001
|
|
172
|
+
// Auth-preflight is a config-shape defect that must fire regardless of whether
|
|
173
|
+
// the binary currently resolves on PATH. Matches connector-validate.js:108-138
|
|
174
|
+
// ordering: a Claude local_cli runtime with no env auth and no --bare is a
|
|
175
|
+
// deterministic hang-on-spawn shape the operator must fix before anything
|
|
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.
|
|
179
|
+
if (claudeAuthIssue) {
|
|
180
|
+
return {
|
|
181
|
+
...base,
|
|
182
|
+
level: 'fail',
|
|
183
|
+
probe_kind: 'auth_preflight',
|
|
184
|
+
command: spawnProbe.command || head,
|
|
185
|
+
error_code: 'claude_auth_preflight_failed',
|
|
186
|
+
detail: claudeAuthIssue.detail,
|
|
187
|
+
fix: claudeAuthIssue.fix,
|
|
188
|
+
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
if (!spawnProbe.ok) {
|
|
193
|
+
return {
|
|
194
|
+
...base,
|
|
195
|
+
level: 'fail',
|
|
196
|
+
command: spawnProbe.command || head,
|
|
197
|
+
detail: spawnProbe.detail,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
|
|
168
201
|
if (spawnProbe.ok) {
|
|
169
202
|
return {
|
|
170
203
|
...base,
|
|
@@ -173,13 +206,6 @@ async function probeLocalCommand(runtimeId, runtime, probeKindLabel, options = {
|
|
|
173
206
|
detail: spawnProbe.detail,
|
|
174
207
|
};
|
|
175
208
|
}
|
|
176
|
-
|
|
177
|
-
return {
|
|
178
|
-
...base,
|
|
179
|
-
level: 'fail',
|
|
180
|
-
command: spawnProbe.command || head,
|
|
181
|
-
detail: spawnProbe.detail,
|
|
182
|
-
};
|
|
183
209
|
}
|
|
184
210
|
|
|
185
211
|
async function probeApiProxy(runtimeId, runtime, timeoutMs) {
|
|
@@ -375,6 +401,7 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
375
401
|
// Prompt transport validation
|
|
376
402
|
const transport = runtime.prompt_transport || 'dispatch_bundle_only';
|
|
377
403
|
const knownTransports = KNOWN_CLI_TRANSPORTS[binaryName];
|
|
404
|
+
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime);
|
|
378
405
|
|
|
379
406
|
if (transport === 'argv' && !commandTokens.some((token) => token.includes('{prompt}'))) {
|
|
380
407
|
warnings.push({
|
|
@@ -395,24 +422,21 @@ function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
|
395
422
|
});
|
|
396
423
|
}
|
|
397
424
|
|
|
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
|
+
|
|
398
434
|
return { warnings };
|
|
399
435
|
}
|
|
400
436
|
|
|
401
437
|
/**
|
|
402
438
|
* Normalize a runtime's command field into an array of tokens.
|
|
403
439
|
*/
|
|
404
|
-
function normalizeCommandTokens(runtime) {
|
|
405
|
-
if (Array.isArray(runtime?.command)) {
|
|
406
|
-
return runtime.command.flatMap((element) =>
|
|
407
|
-
typeof element === 'string' ? element.trim().split(/\s+/).filter(Boolean) : []
|
|
408
|
-
);
|
|
409
|
-
}
|
|
410
|
-
if (typeof runtime?.command === 'string' && runtime.command.trim()) {
|
|
411
|
-
return runtime.command.trim().split(/\s+/).filter(Boolean);
|
|
412
|
-
}
|
|
413
|
-
return [];
|
|
414
|
-
}
|
|
415
|
-
|
|
416
440
|
export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
417
441
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
418
442
|
const roles = options.roles || null;
|
|
@@ -433,8 +457,11 @@ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
|
433
457
|
// Add authority-intent and transport analysis when roles are available
|
|
434
458
|
if (roles) {
|
|
435
459
|
const { warnings } = analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles);
|
|
436
|
-
|
|
437
|
-
|
|
460
|
+
const visibleWarnings = result.error_code === 'claude_auth_preflight_failed'
|
|
461
|
+
? warnings.filter((warning) => warning.probe_kind !== 'auth_preflight')
|
|
462
|
+
: warnings;
|
|
463
|
+
if (visibleWarnings.length > 0) {
|
|
464
|
+
result.authority_warnings = visibleWarnings;
|
|
438
465
|
// Promote result level to 'warn' if binary is present but authority intent is wrong
|
|
439
466
|
if (result.level === 'pass') {
|
|
440
467
|
result.level = 'warn';
|
|
@@ -24,6 +24,7 @@ import { getDispatchPromptPath, getTurnStagingResultPath } from './turn-paths.js
|
|
|
24
24
|
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
25
25
|
import { probeRuntimeSpawnContext } from './runtime-spawn-context.js';
|
|
26
26
|
import { buildConnectorSchemaContract } from './connector-schema-contract.js';
|
|
27
|
+
import { getClaudeSubprocessAuthIssue } from './claude-local-auth.js';
|
|
27
28
|
|
|
28
29
|
const VALIDATABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
29
30
|
const DEFAULT_VALIDATE_TIMEOUT_MS = 120_000;
|
|
@@ -104,6 +105,39 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
|
|
|
104
105
|
};
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// DEC-BUG54-CLAUDE-AUTH-PREFLIGHT-001 — refuse the known-hanging Claude
|
|
109
|
+
// local_cli shape before burning the scratch-workspace + synthetic-dispatch
|
|
110
|
+
// ceremony. The adapter also refuses this shape via `claude_auth_preflight_failed`,
|
|
111
|
+
// but the operator gets a faster, identical-fix message if we catch it here.
|
|
112
|
+
const claudeAuthIssue = getClaudeSubprocessAuthIssue(runtime);
|
|
113
|
+
if (claudeAuthIssue) {
|
|
114
|
+
return {
|
|
115
|
+
ok: false,
|
|
116
|
+
exitCode: 1,
|
|
117
|
+
overall: 'fail',
|
|
118
|
+
runtime_id: runtimeId,
|
|
119
|
+
runtime_type: runtime.type,
|
|
120
|
+
role_id: roleSelection.roleId,
|
|
121
|
+
timeout_ms: timeoutMs,
|
|
122
|
+
warnings: [
|
|
123
|
+
...roleSelection.warnings,
|
|
124
|
+
{
|
|
125
|
+
probe_kind: 'auth_preflight',
|
|
126
|
+
level: 'fail',
|
|
127
|
+
detail: claudeAuthIssue.detail,
|
|
128
|
+
fix: claudeAuthIssue.fix,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
error_code: 'claude_auth_preflight_failed',
|
|
132
|
+
error: claudeAuthIssue.detail,
|
|
133
|
+
auth_env_present: claudeAuthIssue.auth_env_present,
|
|
134
|
+
fix: claudeAuthIssue.fix,
|
|
135
|
+
dispatch: null,
|
|
136
|
+
validation: null,
|
|
137
|
+
scratch_root: null,
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
107
141
|
const tempBase = mkdtempSync(join(tmpdir(), 'axc-connector-validate-'));
|
|
108
142
|
const scratchRoot = join(tempBase, 'workspace');
|
|
109
143
|
const warnings = [...roleSelection.warnings];
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
|
|
12
12
|
import { writeFileSync, unlinkSync, readFileSync, existsSync, mkdirSync, readdirSync } from 'node:fs';
|
|
13
13
|
import { join, dirname, basename } from 'node:path';
|
|
14
|
+
import {
|
|
15
|
+
isDispatchProgressDiagnosticStream,
|
|
16
|
+
isDispatchProgressProofOutputStream,
|
|
17
|
+
} from './dispatch-streams.js';
|
|
14
18
|
|
|
15
19
|
export const LEGACY_DISPATCH_PROGRESS_PATH = '.agentxchain/dispatch-progress.json';
|
|
16
20
|
export const DISPATCH_PROGRESS_FILE_PREFIX = '.agentxchain/dispatch-progress-';
|
|
@@ -138,15 +142,37 @@ export function createDispatchProgressTracker(root, turn, options = {}) {
|
|
|
138
142
|
const now = new Date().toISOString();
|
|
139
143
|
const wasSilent = state.activity_type === 'silent';
|
|
140
144
|
state.last_activity_at = now;
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
+
// DEC-BUG54-STDERR-IS-NOT-STARTUP-PROOF-002 (Turn 88) extended to the
|
|
146
|
+
// progress tracker in Turn 89: stderr is diagnostic evidence, not usable
|
|
147
|
+
// startup proof. Only stdout may set `first_output_at`. stderr still
|
|
148
|
+
// increments `stderr_lines` for silence detection and diagnostics.
|
|
149
|
+
let recognizedActivity = false;
|
|
150
|
+
if (isDispatchProgressDiagnosticStream(stream)) {
|
|
145
151
|
state.stderr_lines += lineCount;
|
|
146
|
-
|
|
152
|
+
recognizedActivity = true;
|
|
153
|
+
} else if (isDispatchProgressProofOutputStream(stream)) {
|
|
154
|
+
state.first_output_at = state.first_output_at || now;
|
|
147
155
|
state.output_lines += lineCount;
|
|
156
|
+
recognizedActivity = true;
|
|
157
|
+
}
|
|
158
|
+
// DEC-BUG54-DIAGNOSTIC-ACTIVITY-TYPE-001 (Turn 91): activity_type and
|
|
159
|
+
// activity_summary must reflect whether operator-usable stdout proof has
|
|
160
|
+
// arrived. A stderr-only subprocess that never attached stdout must NOT
|
|
161
|
+
// be rendered as "Producing output" on the operator status surface —
|
|
162
|
+
// that is a false live-progress signal for a failing startup. Only when
|
|
163
|
+
// `output_lines > 0` may we claim 'output'; otherwise recognized stderr
|
|
164
|
+
// activity is surfaced as 'diagnostic_only'. Unknown stream labels do
|
|
165
|
+
// not mutate activity_type (Turn 90 closed-vocabulary contract).
|
|
166
|
+
if (recognizedActivity) {
|
|
167
|
+
if (state.output_lines > 0) {
|
|
168
|
+
state.activity_type = 'output';
|
|
169
|
+
state.activity_summary = `Producing output (${state.output_lines} lines)`;
|
|
170
|
+
} else {
|
|
171
|
+
state.activity_type = 'diagnostic_only';
|
|
172
|
+
state.activity_summary = `Diagnostic output only (${state.stderr_lines} stderr lines)`;
|
|
173
|
+
}
|
|
174
|
+
state.silent_since = null;
|
|
148
175
|
}
|
|
149
|
-
state.activity_summary = `Producing output (${state.output_lines} lines)`;
|
|
150
176
|
dirty = true;
|
|
151
177
|
maybeWrite();
|
|
152
178
|
if (adapter_type === 'local_cli') {
|