agentxchain 2.128.0 → 2.130.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 +2 -0
- package/bin/agentxchain.js +38 -4
- package/package.json +1 -1
- package/scripts/verify-post-publish.sh +55 -5
- package/src/commands/accept-turn.js +14 -0
- package/src/commands/checkpoint-turn.js +35 -0
- package/src/commands/connector.js +17 -2
- package/src/commands/doctor.js +151 -1
- package/src/commands/events.js +7 -1
- package/src/commands/init.js +42 -11
- package/src/commands/inject.js +1 -1
- package/src/commands/mission.js +803 -7
- package/src/commands/reissue-turn.js +122 -0
- package/src/commands/reject-turn.js +60 -6
- package/src/commands/restart.js +81 -10
- package/src/commands/resume.js +20 -9
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +58 -4
- package/src/commands/step.js +49 -10
- package/src/commands/validate.js +78 -20
- package/src/lib/cli-version.js +106 -0
- package/src/lib/connector-probe.js +146 -5
- package/src/lib/continuous-run.js +22 -87
- package/src/lib/coordinator-dispatch.js +25 -0
- package/src/lib/dispatch-bundle.js +39 -0
- package/src/lib/governed-state.js +624 -11
- package/src/lib/governed-templates.js +1 -0
- package/src/lib/intake.js +233 -77
- package/src/lib/mission-plans.js +510 -6
- package/src/lib/missions.js +65 -6
- package/src/lib/normalized-config.js +50 -15
- package/src/lib/repo-observer.js +8 -2
- package/src/lib/run-events.js +5 -0
- package/src/lib/run-loop.js +25 -0
- package/src/lib/runner-interface.js +2 -0
- package/src/lib/session-checkpoint.js +18 -2
- package/src/lib/turn-checkpoint.js +221 -0
- package/src/templates/governed/full-local-cli.json +71 -0
package/src/commands/validate.js
CHANGED
|
@@ -1,34 +1,96 @@
|
|
|
1
|
+
import { readFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import chalk from 'chalk';
|
|
2
|
-
import { loadConfig, loadProjectContext } from '../lib/config.js';
|
|
4
|
+
import { findProjectRoot, loadConfig, loadProjectContext } from '../lib/config.js';
|
|
3
5
|
import { validateGovernedProject, validateProject } from '../lib/validation.js';
|
|
4
6
|
import { getGovernedVersionSurface, formatGovernedVersionLabel } from '../lib/protocol-version.js';
|
|
7
|
+
import { detectConfigVersion, loadNormalizedConfig } from '../lib/normalized-config.js';
|
|
5
8
|
|
|
6
9
|
export async function validateCommand(opts) {
|
|
10
|
+
const root = findProjectRoot(process.cwd());
|
|
11
|
+
if (!root) {
|
|
12
|
+
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
13
|
+
process.exit(1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const mode = opts.mode || 'full';
|
|
17
|
+
let rawConfig;
|
|
18
|
+
try {
|
|
19
|
+
rawConfig = JSON.parse(readFileSync(join(root, 'agentxchain.json'), 'utf8'));
|
|
20
|
+
} catch (err) {
|
|
21
|
+
console.log(chalk.red(`agentxchain.json is invalid JSON: ${err.message}`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
if (detectConfigVersion(rawConfig) === 4) {
|
|
26
|
+
const normalized = loadNormalizedConfig(rawConfig, root);
|
|
27
|
+
const validation = normalized.ok
|
|
28
|
+
? validateGovernedProject(root, rawConfig, normalized.normalized, {
|
|
29
|
+
mode,
|
|
30
|
+
expectedAgent: opts.agent || null,
|
|
31
|
+
})
|
|
32
|
+
: {
|
|
33
|
+
ok: false,
|
|
34
|
+
mode,
|
|
35
|
+
errors: normalized.errors,
|
|
36
|
+
warnings: normalized.warnings || [],
|
|
37
|
+
};
|
|
38
|
+
const governedVersionSurface = getGovernedVersionSurface(rawConfig);
|
|
39
|
+
|
|
40
|
+
if (opts.json) {
|
|
41
|
+
console.log(JSON.stringify({
|
|
42
|
+
...validation,
|
|
43
|
+
protocol_mode: 'governed',
|
|
44
|
+
...governedVersionSurface,
|
|
45
|
+
version: 4,
|
|
46
|
+
}, null, 2));
|
|
47
|
+
} else {
|
|
48
|
+
console.log('');
|
|
49
|
+
console.log(chalk.bold(` AgentXchain Validate (${mode})`));
|
|
50
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
51
|
+
console.log(chalk.dim(` Root: ${root}`));
|
|
52
|
+
console.log(chalk.dim(` Protocol: governed (${formatGovernedVersionLabel(rawConfig)})`));
|
|
53
|
+
console.log('');
|
|
54
|
+
|
|
55
|
+
if (validation.ok) {
|
|
56
|
+
console.log(chalk.green(' ✓ Validation passed.'));
|
|
57
|
+
} else {
|
|
58
|
+
console.log(chalk.red(` ✗ Validation failed (${validation.errors.length} errors).`));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (validation.errors.length > 0) {
|
|
62
|
+
console.log('');
|
|
63
|
+
console.log(chalk.red(' Errors:'));
|
|
64
|
+
for (const e of validation.errors) console.log(` - ${e}`);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (validation.warnings.length > 0) {
|
|
68
|
+
console.log('');
|
|
69
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
70
|
+
for (const w of validation.warnings) console.log(` - ${w}`);
|
|
71
|
+
}
|
|
72
|
+
console.log('');
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (!validation.ok) process.exit(1);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
|
|
7
79
|
const context = loadProjectContext();
|
|
8
80
|
if (!context) {
|
|
9
81
|
console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
|
|
10
82
|
process.exit(1);
|
|
11
83
|
}
|
|
12
84
|
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
expectedAgent: opts.agent || null,
|
|
18
|
-
})
|
|
19
|
-
: validateProject(context.root, loadConfig()?.config || context.rawConfig, {
|
|
20
|
-
mode,
|
|
21
|
-
expectedAgent: opts.agent || null,
|
|
22
|
-
});
|
|
85
|
+
const validation = validateProject(context.root, loadConfig()?.config || context.rawConfig, {
|
|
86
|
+
mode,
|
|
87
|
+
expectedAgent: opts.agent || null,
|
|
88
|
+
});
|
|
23
89
|
|
|
24
90
|
if (opts.json) {
|
|
25
|
-
const governedVersionSurface = context.config.protocol_mode === 'governed'
|
|
26
|
-
? getGovernedVersionSurface(context.rawConfig)
|
|
27
|
-
: {};
|
|
28
91
|
console.log(JSON.stringify({
|
|
29
92
|
...validation,
|
|
30
93
|
protocol_mode: context.config.protocol_mode,
|
|
31
|
-
...governedVersionSurface,
|
|
32
94
|
version: context.version,
|
|
33
95
|
}, null, 2));
|
|
34
96
|
} else {
|
|
@@ -36,11 +98,7 @@ export async function validateCommand(opts) {
|
|
|
36
98
|
console.log(chalk.bold(` AgentXchain Validate (${mode})`));
|
|
37
99
|
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
38
100
|
console.log(chalk.dim(` Root: ${context.root}`));
|
|
39
|
-
|
|
40
|
-
console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (${formatGovernedVersionLabel(context.rawConfig)})`));
|
|
41
|
-
} else {
|
|
42
|
-
console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (v${context.version})`));
|
|
43
|
-
}
|
|
101
|
+
console.log(chalk.dim(` Protocol: ${context.config.protocol_mode} (v${context.version})`));
|
|
44
102
|
console.log('');
|
|
45
103
|
|
|
46
104
|
if (validation.ok) {
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
import { execFileSync } from 'child_process';
|
|
2
|
+
import { readFileSync } from 'fs';
|
|
3
|
+
import { dirname, join } from 'path';
|
|
4
|
+
import { fileURLToPath } from 'url';
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const pkg = JSON.parse(readFileSync(join(__dirname, '../../package.json'), 'utf8'));
|
|
8
|
+
const SEMVER_RE = /^v?(\d+)\.(\d+)\.(\d+)(?:[-+].*)?$/;
|
|
9
|
+
|
|
10
|
+
export const CURRENT_CLI_VERSION = pkg.version;
|
|
11
|
+
|
|
12
|
+
export function normalizeCliVersion(version) {
|
|
13
|
+
if (typeof version !== 'string') return null;
|
|
14
|
+
const trimmed = version.trim();
|
|
15
|
+
const match = SEMVER_RE.exec(trimmed);
|
|
16
|
+
if (!match) return null;
|
|
17
|
+
return `${Number(match[1])}.${Number(match[2])}.${Number(match[3])}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function compareCliVersions(left, right) {
|
|
21
|
+
const a = normalizeCliVersion(left);
|
|
22
|
+
const b = normalizeCliVersion(right);
|
|
23
|
+
if (!a || !b) return null;
|
|
24
|
+
|
|
25
|
+
const aParts = a.split('.').map(Number);
|
|
26
|
+
const bParts = b.split('.').map(Number);
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < 3; i += 1) {
|
|
29
|
+
if (aParts[i] > bParts[i]) return 1;
|
|
30
|
+
if (aParts[i] < bParts[i]) return -1;
|
|
31
|
+
}
|
|
32
|
+
return 0;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function getPublishedDocsMinimumCliVersion() {
|
|
36
|
+
const envOverride = normalizeCliVersion(process.env.AGENTXCHAIN_DOCS_MIN_VERSION || '');
|
|
37
|
+
if (envOverride) {
|
|
38
|
+
return {
|
|
39
|
+
version: envOverride,
|
|
40
|
+
source: 'env',
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
try {
|
|
45
|
+
const stdout = execFileSync('npm', ['view', 'agentxchain', 'version'], {
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
48
|
+
timeout: 3000,
|
|
49
|
+
}).trim();
|
|
50
|
+
const published = normalizeCliVersion(stdout);
|
|
51
|
+
if (!published) return null;
|
|
52
|
+
return {
|
|
53
|
+
version: published,
|
|
54
|
+
source: 'npm',
|
|
55
|
+
};
|
|
56
|
+
} catch {
|
|
57
|
+
return null;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function getCliVersionHealth() {
|
|
62
|
+
const current = normalizeCliVersion(CURRENT_CLI_VERSION);
|
|
63
|
+
const docsFloor = getPublishedDocsMinimumCliVersion();
|
|
64
|
+
if (!docsFloor) {
|
|
65
|
+
return {
|
|
66
|
+
current_version: current,
|
|
67
|
+
docs_min_cli_version: null,
|
|
68
|
+
status: 'unknown',
|
|
69
|
+
source: null,
|
|
70
|
+
stale: false,
|
|
71
|
+
detail: 'Could not verify the published docs CLI floor.',
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const compare = compareCliVersions(current, docsFloor.version);
|
|
76
|
+
if (compare === null) {
|
|
77
|
+
return {
|
|
78
|
+
current_version: current,
|
|
79
|
+
docs_min_cli_version: docsFloor.version,
|
|
80
|
+
status: 'unknown',
|
|
81
|
+
source: docsFloor.source,
|
|
82
|
+
stale: false,
|
|
83
|
+
detail: 'Could not compare CLI versions.',
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (compare < 0) {
|
|
88
|
+
return {
|
|
89
|
+
current_version: current,
|
|
90
|
+
docs_min_cli_version: docsFloor.version,
|
|
91
|
+
status: 'stale',
|
|
92
|
+
source: docsFloor.source,
|
|
93
|
+
stale: true,
|
|
94
|
+
detail: `Public docs target agentxchain >= ${docsFloor.version}, but this CLI is ${current}. Upgrade with npm/Homebrew or use npx --yes -p agentxchain@latest -c "agentxchain doctor".`,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
return {
|
|
99
|
+
current_version: current,
|
|
100
|
+
docs_min_cli_version: docsFloor.version,
|
|
101
|
+
status: 'ok',
|
|
102
|
+
source: docsFloor.source,
|
|
103
|
+
stale: false,
|
|
104
|
+
detail: `Running ${current}; published docs floor is ${docsFloor.version}.`,
|
|
105
|
+
};
|
|
106
|
+
}
|
|
@@ -9,6 +9,36 @@ import {
|
|
|
9
9
|
const PROBEABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
|
|
10
10
|
const DEFAULT_TIMEOUT_MS = 8_000;
|
|
11
11
|
|
|
12
|
+
/**
|
|
13
|
+
* Known local CLI tools and their authoritative-mode flags.
|
|
14
|
+
* Each entry maps a binary name pattern to the flag required for true
|
|
15
|
+
* unattended authoritative writes and any flags that look similar but
|
|
16
|
+
* do NOT grant full authority.
|
|
17
|
+
*/
|
|
18
|
+
const KNOWN_CLI_AUTHORITY_FLAGS = [
|
|
19
|
+
{
|
|
20
|
+
binary: 'claude',
|
|
21
|
+
authoritative_flag: '--dangerously-skip-permissions',
|
|
22
|
+
weak_flags: [],
|
|
23
|
+
label: 'Claude Code',
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
binary: 'codex',
|
|
27
|
+
authoritative_flag: '--dangerously-bypass-approvals-and-sandbox',
|
|
28
|
+
weak_flags: ['--full-auto'],
|
|
29
|
+
label: 'OpenAI Codex CLI',
|
|
30
|
+
},
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Known prompt transport requirements per CLI binary.
|
|
35
|
+
* Maps binary name to expected transport.
|
|
36
|
+
*/
|
|
37
|
+
const KNOWN_CLI_TRANSPORTS = {
|
|
38
|
+
claude: 'stdin',
|
|
39
|
+
codex: 'argv',
|
|
40
|
+
};
|
|
41
|
+
|
|
12
42
|
function formatCommand(command, args = []) {
|
|
13
43
|
if (Array.isArray(command)) {
|
|
14
44
|
return command.join(' ');
|
|
@@ -274,8 +304,103 @@ async function probeHttpRuntime(runtimeId, runtime, timeoutMs) {
|
|
|
274
304
|
};
|
|
275
305
|
}
|
|
276
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Analyze a local_cli runtime's command shape for authority-intent alignment.
|
|
309
|
+
* Returns an array of warnings (may be empty).
|
|
310
|
+
*
|
|
311
|
+
* @param {string} runtimeId
|
|
312
|
+
* @param {object} runtime - runtime config entry
|
|
313
|
+
* @param {object} roles - map of roleId → role config (to determine write_authority)
|
|
314
|
+
* @returns {{ warnings: Array<{probe_kind: string, level: string, detail: string, fix?: string}> }}
|
|
315
|
+
*/
|
|
316
|
+
function analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles) {
|
|
317
|
+
const warnings = [];
|
|
318
|
+
if (runtime?.type !== 'local_cli') return { warnings };
|
|
319
|
+
|
|
320
|
+
const commandTokens = normalizeCommandTokens(runtime);
|
|
321
|
+
if (commandTokens.length === 0) return { warnings };
|
|
322
|
+
|
|
323
|
+
const binaryName = commandTokens[0].toLowerCase();
|
|
324
|
+
const allFlags = commandTokens.slice(1).join(' ');
|
|
325
|
+
|
|
326
|
+
// Find roles that use this runtime
|
|
327
|
+
const boundRoles = Object.entries(roles || {})
|
|
328
|
+
.filter(([, role]) => role?.runtime === runtimeId)
|
|
329
|
+
.map(([roleId, role]) => ({ roleId, write_authority: role.write_authority }));
|
|
330
|
+
|
|
331
|
+
if (boundRoles.length === 0) return { warnings };
|
|
332
|
+
|
|
333
|
+
const authoritativeRoles = boundRoles.filter((r) => r.write_authority === 'authoritative');
|
|
334
|
+
|
|
335
|
+
// Check known CLI authority flags
|
|
336
|
+
const knownCli = KNOWN_CLI_AUTHORITY_FLAGS.find((entry) => binaryName === entry.binary || binaryName.endsWith(`/${entry.binary}`));
|
|
337
|
+
if (knownCli && authoritativeRoles.length > 0) {
|
|
338
|
+
const hasAuthFlag = commandTokens.some((token) => token === knownCli.authoritative_flag);
|
|
339
|
+
if (!hasAuthFlag) {
|
|
340
|
+
const usesWeakFlag = knownCli.weak_flags.find((wf) => commandTokens.some((token) => token === wf));
|
|
341
|
+
const roleNames = authoritativeRoles.map((r) => r.roleId).join(', ');
|
|
342
|
+
if (usesWeakFlag) {
|
|
343
|
+
warnings.push({
|
|
344
|
+
probe_kind: 'authority_intent',
|
|
345
|
+
level: 'warn',
|
|
346
|
+
detail: `${knownCli.label} uses "${usesWeakFlag}" which does NOT grant full unattended authority — role(s) [${roleNames}] require authoritative writes`,
|
|
347
|
+
fix: `Replace "${usesWeakFlag}" with "${knownCli.authoritative_flag}" in the command array`,
|
|
348
|
+
});
|
|
349
|
+
} else {
|
|
350
|
+
warnings.push({
|
|
351
|
+
probe_kind: 'authority_intent',
|
|
352
|
+
level: 'warn',
|
|
353
|
+
detail: `${knownCli.label} command is missing "${knownCli.authoritative_flag}" — role(s) [${roleNames}] require authoritative writes but the subprocess may block on approval prompts`,
|
|
354
|
+
fix: `Add "${knownCli.authoritative_flag}" to the command array`,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
// Prompt transport validation
|
|
361
|
+
const transport = runtime.prompt_transport || 'dispatch_bundle_only';
|
|
362
|
+
const knownTransport = KNOWN_CLI_TRANSPORTS[binaryName];
|
|
363
|
+
|
|
364
|
+
if (transport === 'argv' && !commandTokens.some((token) => token.includes('{prompt}'))) {
|
|
365
|
+
warnings.push({
|
|
366
|
+
probe_kind: 'transport_intent',
|
|
367
|
+
level: 'warn',
|
|
368
|
+
detail: `prompt_transport is "argv" but no {prompt} placeholder found in command — the prompt will not be delivered`,
|
|
369
|
+
fix: `Add a "{prompt}" placeholder to the command array, e.g. ["${binaryName}", ..., "{prompt}"]`,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
if (knownTransport && transport !== knownTransport && transport !== 'dispatch_bundle_only') {
|
|
374
|
+
const transportLabel = knownCli ? knownCli.label : binaryName;
|
|
375
|
+
warnings.push({
|
|
376
|
+
probe_kind: 'transport_intent',
|
|
377
|
+
level: 'warn',
|
|
378
|
+
detail: `${transportLabel} typically uses "${knownTransport}" transport, but this runtime is configured with "${transport}"`,
|
|
379
|
+
fix: `Set prompt_transport to "${knownTransport}" or "dispatch_bundle_only"`,
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
return { warnings };
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
/**
|
|
387
|
+
* Normalize a runtime's command field into an array of tokens.
|
|
388
|
+
*/
|
|
389
|
+
function normalizeCommandTokens(runtime) {
|
|
390
|
+
if (Array.isArray(runtime?.command)) {
|
|
391
|
+
return runtime.command.flatMap((element) =>
|
|
392
|
+
typeof element === 'string' ? element.trim().split(/\s+/).filter(Boolean) : []
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (typeof runtime?.command === 'string' && runtime.command.trim()) {
|
|
396
|
+
return runtime.command.trim().split(/\s+/).filter(Boolean);
|
|
397
|
+
}
|
|
398
|
+
return [];
|
|
399
|
+
}
|
|
400
|
+
|
|
277
401
|
export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
278
402
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
403
|
+
const roles = options.roles || null;
|
|
279
404
|
|
|
280
405
|
if (!runtime || !PROBEABLE_RUNTIME_TYPES.has(runtime.type)) {
|
|
281
406
|
return {
|
|
@@ -289,7 +414,19 @@ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
|
289
414
|
}
|
|
290
415
|
|
|
291
416
|
if (runtime.type === 'local_cli') {
|
|
292
|
-
|
|
417
|
+
const result = await probeLocalCommand(runtimeId, runtime, 'command_presence');
|
|
418
|
+
// Add authority-intent and transport analysis when roles are available
|
|
419
|
+
if (roles) {
|
|
420
|
+
const { warnings } = analyzeLocalCliAuthorityIntent(runtimeId, runtime, roles);
|
|
421
|
+
if (warnings.length > 0) {
|
|
422
|
+
result.authority_warnings = warnings;
|
|
423
|
+
// Promote result level to 'warn' if binary is present but authority intent is wrong
|
|
424
|
+
if (result.level === 'pass') {
|
|
425
|
+
result.level = 'warn';
|
|
426
|
+
}
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
return result;
|
|
293
430
|
}
|
|
294
431
|
|
|
295
432
|
if (runtime.type === 'api_proxy') {
|
|
@@ -310,6 +447,7 @@ export async function probeConfiguredConnectors(config, options = {}) {
|
|
|
310
447
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
311
448
|
const requestedRuntimeId = options.runtimeId || null;
|
|
312
449
|
const onProbeStart = typeof options.onProbeStart === 'function' ? options.onProbeStart : null;
|
|
450
|
+
const roles = config?.roles || null;
|
|
313
451
|
|
|
314
452
|
const runtimeEntries = Object.entries(config?.runtimes || {})
|
|
315
453
|
.filter(([, runtime]) => PROBEABLE_RUNTIME_TYPES.has(runtime?.type))
|
|
@@ -326,30 +464,33 @@ export async function probeConfiguredConnectors(config, options = {}) {
|
|
|
326
464
|
}
|
|
327
465
|
const [runtimeId, runtime] = match;
|
|
328
466
|
onProbeStart?.(runtimeId, runtime);
|
|
329
|
-
const connector = await probeConnectorRuntime(runtimeId, runtime, { timeoutMs });
|
|
467
|
+
const connector = await probeConnectorRuntime(runtimeId, runtime, { timeoutMs, roles });
|
|
330
468
|
return summarizeResults([connector], timeoutMs);
|
|
331
469
|
}
|
|
332
470
|
|
|
333
471
|
const connectors = [];
|
|
334
472
|
for (const [runtimeId, runtime] of runtimeEntries) {
|
|
335
473
|
onProbeStart?.(runtimeId, runtime);
|
|
336
|
-
connectors.push(await probeConnectorRuntime(runtimeId, runtime, { timeoutMs }));
|
|
474
|
+
connectors.push(await probeConnectorRuntime(runtimeId, runtime, { timeoutMs, roles }));
|
|
337
475
|
}
|
|
338
476
|
return summarizeResults(connectors, timeoutMs);
|
|
339
477
|
}
|
|
340
478
|
|
|
341
479
|
function summarizeResults(connectors, timeoutMs) {
|
|
342
480
|
const failCount = connectors.filter((item) => item.level === 'fail').length;
|
|
481
|
+
const warnCount = connectors.filter((item) => item.level === 'warn').length;
|
|
343
482
|
const passCount = connectors.filter((item) => item.level === 'pass').length;
|
|
483
|
+
const overall = failCount > 0 ? 'fail' : warnCount > 0 ? 'warn' : 'pass';
|
|
344
484
|
return {
|
|
345
485
|
ok: failCount === 0,
|
|
346
486
|
exitCode: failCount === 0 ? 0 : 1,
|
|
347
|
-
overall
|
|
487
|
+
overall,
|
|
348
488
|
timeout_ms: timeoutMs,
|
|
349
489
|
pass_count: passCount,
|
|
490
|
+
warn_count: warnCount,
|
|
350
491
|
fail_count: failCount,
|
|
351
492
|
connectors,
|
|
352
493
|
};
|
|
353
494
|
}
|
|
354
495
|
|
|
355
|
-
export { DEFAULT_TIMEOUT_MS, PROBEABLE_RUNTIME_TYPES };
|
|
496
|
+
export { DEFAULT_TIMEOUT_MS, PROBEABLE_RUNTIME_TYPES, KNOWN_CLI_AUTHORITY_FLAGS, KNOWN_CLI_TRANSPORTS, analyzeLocalCliAuthorityIntent, normalizeCommandTokens };
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Decision: DEC-VISION-CONTINUOUS-001
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
|
-
import { existsSync, readFileSync,
|
|
12
|
+
import { existsSync, readFileSync, mkdirSync, unlinkSync } from 'node:fs';
|
|
13
13
|
import { join } from 'node:path';
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { resolveVisionPath, deriveVisionCandidates } from './vision-reader.js';
|
|
@@ -17,8 +17,9 @@ import {
|
|
|
17
17
|
recordEvent,
|
|
18
18
|
triageIntent,
|
|
19
19
|
approveIntent,
|
|
20
|
-
|
|
21
|
-
|
|
20
|
+
findNextDispatchableIntent,
|
|
21
|
+
prepareIntentForDispatch,
|
|
22
|
+
consumeNextApprovedIntent,
|
|
22
23
|
resolveIntent,
|
|
23
24
|
} from './intake.js';
|
|
24
25
|
import { loadProjectState } from './config.js';
|
|
@@ -112,40 +113,6 @@ function isBlockedContinuousExecution(execution) {
|
|
|
112
113
|
// Intake queue check
|
|
113
114
|
// ---------------------------------------------------------------------------
|
|
114
115
|
|
|
115
|
-
/**
|
|
116
|
-
* Find the next approved or planned intent in the intake queue.
|
|
117
|
-
*
|
|
118
|
-
* @param {string} root
|
|
119
|
-
* @returns {{ ok: boolean, intentId?: string, status?: string }}
|
|
120
|
-
*/
|
|
121
|
-
export function findNextQueuedIntent(root) {
|
|
122
|
-
const intentsDir = join(root, '.agentxchain', 'intake', 'intents');
|
|
123
|
-
if (!existsSync(intentsDir)) return { ok: false };
|
|
124
|
-
|
|
125
|
-
const files = readdirSync(intentsDir).filter(f => f.endsWith('.json') && !f.startsWith('.tmp-'));
|
|
126
|
-
|
|
127
|
-
// Priority order: planned > approved (planned is closer to execution)
|
|
128
|
-
let bestPlanned = null;
|
|
129
|
-
let bestApproved = null;
|
|
130
|
-
|
|
131
|
-
for (const file of files) {
|
|
132
|
-
try {
|
|
133
|
-
const intent = JSON.parse(readFileSync(join(intentsDir, file), 'utf8'));
|
|
134
|
-
if (intent.status === 'planned' && !bestPlanned) {
|
|
135
|
-
bestPlanned = { intentId: intent.intent_id, status: 'planned' };
|
|
136
|
-
} else if (intent.status === 'approved' && !bestApproved) {
|
|
137
|
-
bestApproved = { intentId: intent.intent_id, status: 'approved' };
|
|
138
|
-
}
|
|
139
|
-
} catch {
|
|
140
|
-
// skip corrupt
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
if (bestPlanned) return { ok: true, ...bestPlanned };
|
|
145
|
-
if (bestApproved) return { ok: true, ...bestApproved };
|
|
146
|
-
return { ok: false };
|
|
147
|
-
}
|
|
148
|
-
|
|
149
116
|
function readIntent(root, intentId) {
|
|
150
117
|
const intentPath = join(root, '.agentxchain', 'intake', 'intents', `${intentId}.json`);
|
|
151
118
|
if (!existsSync(intentPath)) return null;
|
|
@@ -166,50 +133,8 @@ function buildContinuousProvenance(intentId, options = {}) {
|
|
|
166
133
|
};
|
|
167
134
|
}
|
|
168
135
|
|
|
169
|
-
function
|
|
170
|
-
|
|
171
|
-
if (!intent) {
|
|
172
|
-
return { ok: false, error: `intent ${intentId} not found` };
|
|
173
|
-
}
|
|
174
|
-
|
|
175
|
-
if (intent.status === 'approved') {
|
|
176
|
-
const planned = planIntent(root, intentId);
|
|
177
|
-
if (!planned.ok) {
|
|
178
|
-
return { ok: false, error: `plan failed: ${planned.error}` };
|
|
179
|
-
}
|
|
180
|
-
intent = planned.intent;
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
if (intent.status === 'planned') {
|
|
184
|
-
const started = startIntent(root, intentId, {
|
|
185
|
-
allowTerminalRestart: true,
|
|
186
|
-
provenance: options.provenance,
|
|
187
|
-
});
|
|
188
|
-
if (!started.ok) {
|
|
189
|
-
return { ok: false, error: `start failed: ${started.error}` };
|
|
190
|
-
}
|
|
191
|
-
intent = started.intent;
|
|
192
|
-
return {
|
|
193
|
-
ok: true,
|
|
194
|
-
intent,
|
|
195
|
-
runId: started.run_id,
|
|
196
|
-
turnId: started.turn_id,
|
|
197
|
-
};
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
if (intent.status === 'executing') {
|
|
201
|
-
return {
|
|
202
|
-
ok: true,
|
|
203
|
-
intent,
|
|
204
|
-
runId: intent.target_run || null,
|
|
205
|
-
turnId: intent.target_turn || null,
|
|
206
|
-
};
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
return {
|
|
210
|
-
ok: false,
|
|
211
|
-
error: `intent ${intentId} is in unsupported status "${intent.status}" for continuous execution`,
|
|
212
|
-
};
|
|
136
|
+
export function findNextQueuedIntent(root) {
|
|
137
|
+
return findNextDispatchableIntent(root);
|
|
213
138
|
}
|
|
214
139
|
|
|
215
140
|
// ---------------------------------------------------------------------------
|
|
@@ -314,6 +239,7 @@ export function resolveContinuousOptions(opts, config) {
|
|
|
314
239
|
triageApproval: opts.triageApproval ?? configCont.triage_approval ?? 'auto',
|
|
315
240
|
cooldownSeconds: opts.cooldownSeconds ?? configCont.cooldown_seconds ?? 5,
|
|
316
241
|
perSessionMaxUsd: opts.sessionBudget ?? configCont.per_session_max_usd ?? null,
|
|
242
|
+
autoCheckpoint: opts.autoCheckpoint ?? configCont.auto_checkpoint ?? true,
|
|
317
243
|
};
|
|
318
244
|
}
|
|
319
245
|
|
|
@@ -382,7 +308,12 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
382
308
|
|
|
383
309
|
let execution;
|
|
384
310
|
try {
|
|
385
|
-
execution = await executeGovernedRun(context, {
|
|
311
|
+
execution = await executeGovernedRun(context, {
|
|
312
|
+
autoApprove: true,
|
|
313
|
+
autoCheckpoint: contOpts.autoCheckpoint,
|
|
314
|
+
report: true,
|
|
315
|
+
log,
|
|
316
|
+
});
|
|
386
317
|
} catch (err) {
|
|
387
318
|
session.status = 'failed';
|
|
388
319
|
writeContinuousSession(root, session);
|
|
@@ -420,7 +351,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
420
351
|
}
|
|
421
352
|
|
|
422
353
|
// Step 1: Check intake queue for pending work
|
|
423
|
-
const queued =
|
|
354
|
+
const queued = findNextDispatchableIntent(root);
|
|
424
355
|
let targetIntentId = null;
|
|
425
356
|
let visionObjective = null;
|
|
426
357
|
|
|
@@ -467,7 +398,10 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
467
398
|
trigger: visionObjective ? 'vision_scan' : 'intake',
|
|
468
399
|
triggerReason: visionObjective || readIntent(root, targetIntentId)?.charter || null,
|
|
469
400
|
});
|
|
470
|
-
const preparedIntent =
|
|
401
|
+
const preparedIntent = prepareIntentForDispatch(root, targetIntentId, {
|
|
402
|
+
allowTerminalRestart: true,
|
|
403
|
+
provenance,
|
|
404
|
+
});
|
|
471
405
|
if (!preparedIntent.ok) {
|
|
472
406
|
log(`Continuous start error: ${preparedIntent.error}`);
|
|
473
407
|
session.status = 'failed';
|
|
@@ -476,7 +410,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
476
410
|
}
|
|
477
411
|
|
|
478
412
|
// Execute the governed run
|
|
479
|
-
session.current_run_id = preparedIntent.
|
|
413
|
+
session.current_run_id = preparedIntent.run_id;
|
|
480
414
|
session.current_vision_objective = visionObjective || preparedIntent.intent?.charter || null;
|
|
481
415
|
session.status = 'running';
|
|
482
416
|
writeContinuousSession(root, session);
|
|
@@ -485,6 +419,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
485
419
|
try {
|
|
486
420
|
execution = await executeGovernedRun(context, {
|
|
487
421
|
autoApprove: true,
|
|
422
|
+
autoCheckpoint: contOpts.autoCheckpoint,
|
|
488
423
|
report: true,
|
|
489
424
|
log,
|
|
490
425
|
});
|
|
@@ -497,12 +432,12 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
497
432
|
status: 'failed',
|
|
498
433
|
action: 'run_failed',
|
|
499
434
|
stop_reason: err.message,
|
|
500
|
-
run_id: preparedIntent.
|
|
435
|
+
run_id: preparedIntent.run_id || null,
|
|
501
436
|
intent_id: targetIntentId,
|
|
502
437
|
};
|
|
503
438
|
}
|
|
504
439
|
|
|
505
|
-
session.current_run_id = execution.result?.state?.run_id || preparedIntent.
|
|
440
|
+
session.current_run_id = execution.result?.state?.run_id || preparedIntent.run_id || null;
|
|
506
441
|
session.cumulative_spent_usd = (session.cumulative_spent_usd || 0) + getExecutionRunSpentUsd(execution);
|
|
507
442
|
|
|
508
443
|
const stopReason = execution.result?.stop_reason;
|
|
@@ -213,6 +213,31 @@ export function selectNextAssignment(workspacePath, state, config) {
|
|
|
213
213
|
return firstFailure || { ok: false, reason: 'no_assignable_workstream', detail: 'No workstream is assignable in the current phase' };
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
export function selectAssignmentForWorkstream(workspacePath, state, config, workstreamId) {
|
|
217
|
+
const workstream = config.workstreams?.[workstreamId];
|
|
218
|
+
if (!workstream) {
|
|
219
|
+
return {
|
|
220
|
+
ok: false,
|
|
221
|
+
reason: 'workstream_missing',
|
|
222
|
+
detail: `Unknown workstream "${workstreamId}"`,
|
|
223
|
+
workstream_id: workstreamId,
|
|
224
|
+
};
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (workstream.phase !== state.phase) {
|
|
228
|
+
return {
|
|
229
|
+
ok: false,
|
|
230
|
+
reason: 'phase_mismatch',
|
|
231
|
+
detail: `Workstream "${workstreamId}" is in phase "${workstream.phase}", but coordinator is currently in phase "${state.phase}"`,
|
|
232
|
+
workstream_id: workstreamId,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
const history = readCoordinatorHistory(workspacePath);
|
|
237
|
+
const barriers = readBarriers(workspacePath);
|
|
238
|
+
return evaluateWorkstream(workspacePath, state, config, workstreamId, history, barriers);
|
|
239
|
+
}
|
|
240
|
+
|
|
216
241
|
export function dispatchCoordinatorTurn(workspacePath, state, config, assignment) {
|
|
217
242
|
if (!assignment?.ok) {
|
|
218
243
|
return { ok: false, error: 'Assignment is required before dispatch' };
|