agentxchain 2.127.0 → 2.129.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 +7 -2
- package/bin/agentxchain.js +44 -4
- package/package.json +1 -1
- package/scripts/verify-post-publish.sh +55 -5
- package/src/commands/connector.js +17 -2
- package/src/commands/doctor.js +122 -1
- package/src/commands/events.js +7 -1
- package/src/commands/init.js +55 -14
- package/src/commands/inject.js +1 -1
- package/src/commands/mission.js +142 -0
- package/src/commands/reissue-turn.js +122 -0
- package/src/commands/reject-turn.js +24 -4
- package/src/commands/restart.js +9 -2
- package/src/commands/resume.js +20 -9
- package/src/commands/run.js +13 -0
- package/src/commands/status.js +46 -4
- package/src/commands/step.js +49 -10
- package/src/commands/validate.js +78 -20
- package/src/lib/adapters/local-cli-adapter.js +7 -1
- package/src/lib/cli-version.js +106 -0
- package/src/lib/connector-probe.js +149 -6
- package/src/lib/continuous-run.js +14 -86
- package/src/lib/dispatch-bundle.js +39 -0
- package/src/lib/governed-state.js +474 -10
- package/src/lib/governed-templates.js +1 -0
- package/src/lib/intake.js +221 -77
- package/src/lib/missions.js +56 -4
- package/src/lib/normalized-config.js +50 -15
- package/src/lib/repo-observer.js +7 -2
- package/src/lib/run-events.js +4 -0
- package/src/lib/run-loop.js +5 -0
- package/src/lib/runner-interface.js +2 -0
- package/src/lib/session-checkpoint.js +18 -2
- 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) {
|
|
@@ -278,7 +278,13 @@ function resolveCommand(runtime, fullPrompt) {
|
|
|
278
278
|
|
|
279
279
|
// Shape 1: command is an array
|
|
280
280
|
if (Array.isArray(runtime.command)) {
|
|
281
|
-
|
|
281
|
+
// Normalize: if the first element contains spaces (e.g., ["echo test"]), split it
|
|
282
|
+
// into binary + args. Only the first element is split — later elements may contain
|
|
283
|
+
// legitimate spaces (e.g., script text for `node -e "..."`).
|
|
284
|
+
const first = runtime.command[0] || '';
|
|
285
|
+
const headParts = typeof first === 'string' && first.includes(' ') ? first.split(/\s+/) : [first];
|
|
286
|
+
const [cmd, ...headArgs] = headParts;
|
|
287
|
+
const rest = [...headArgs, ...runtime.command.slice(1)];
|
|
282
288
|
const args = transport === 'argv'
|
|
283
289
|
? rest.map(arg => arg === '{prompt}' ? fullPrompt : arg)
|
|
284
290
|
: rest.filter(arg => arg !== '{prompt}');
|
|
@@ -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(' ');
|
|
@@ -38,7 +68,9 @@ function formatTarget(runtime) {
|
|
|
38
68
|
|
|
39
69
|
function commandHead(runtime) {
|
|
40
70
|
if (Array.isArray(runtime?.command)) {
|
|
41
|
-
|
|
71
|
+
const first = runtime.command[0] || null;
|
|
72
|
+
if (first && first.includes(' ')) return first.split(/\s+/)[0];
|
|
73
|
+
return first;
|
|
42
74
|
}
|
|
43
75
|
if (typeof runtime?.command === 'string' && runtime.command.trim()) {
|
|
44
76
|
return runtime.command.trim().split(/\s+/)[0];
|
|
@@ -272,8 +304,103 @@ async function probeHttpRuntime(runtimeId, runtime, timeoutMs) {
|
|
|
272
304
|
};
|
|
273
305
|
}
|
|
274
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
|
+
|
|
275
401
|
export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
276
402
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
403
|
+
const roles = options.roles || null;
|
|
277
404
|
|
|
278
405
|
if (!runtime || !PROBEABLE_RUNTIME_TYPES.has(runtime.type)) {
|
|
279
406
|
return {
|
|
@@ -287,7 +414,19 @@ export async function probeConnectorRuntime(runtimeId, runtime, options = {}) {
|
|
|
287
414
|
}
|
|
288
415
|
|
|
289
416
|
if (runtime.type === 'local_cli') {
|
|
290
|
-
|
|
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;
|
|
291
430
|
}
|
|
292
431
|
|
|
293
432
|
if (runtime.type === 'api_proxy') {
|
|
@@ -308,6 +447,7 @@ export async function probeConfiguredConnectors(config, options = {}) {
|
|
|
308
447
|
const timeoutMs = Number.isFinite(options.timeoutMs) ? options.timeoutMs : DEFAULT_TIMEOUT_MS;
|
|
309
448
|
const requestedRuntimeId = options.runtimeId || null;
|
|
310
449
|
const onProbeStart = typeof options.onProbeStart === 'function' ? options.onProbeStart : null;
|
|
450
|
+
const roles = config?.roles || null;
|
|
311
451
|
|
|
312
452
|
const runtimeEntries = Object.entries(config?.runtimes || {})
|
|
313
453
|
.filter(([, runtime]) => PROBEABLE_RUNTIME_TYPES.has(runtime?.type))
|
|
@@ -324,30 +464,33 @@ export async function probeConfiguredConnectors(config, options = {}) {
|
|
|
324
464
|
}
|
|
325
465
|
const [runtimeId, runtime] = match;
|
|
326
466
|
onProbeStart?.(runtimeId, runtime);
|
|
327
|
-
const connector = await probeConnectorRuntime(runtimeId, runtime, { timeoutMs });
|
|
467
|
+
const connector = await probeConnectorRuntime(runtimeId, runtime, { timeoutMs, roles });
|
|
328
468
|
return summarizeResults([connector], timeoutMs);
|
|
329
469
|
}
|
|
330
470
|
|
|
331
471
|
const connectors = [];
|
|
332
472
|
for (const [runtimeId, runtime] of runtimeEntries) {
|
|
333
473
|
onProbeStart?.(runtimeId, runtime);
|
|
334
|
-
connectors.push(await probeConnectorRuntime(runtimeId, runtime, { timeoutMs }));
|
|
474
|
+
connectors.push(await probeConnectorRuntime(runtimeId, runtime, { timeoutMs, roles }));
|
|
335
475
|
}
|
|
336
476
|
return summarizeResults(connectors, timeoutMs);
|
|
337
477
|
}
|
|
338
478
|
|
|
339
479
|
function summarizeResults(connectors, timeoutMs) {
|
|
340
480
|
const failCount = connectors.filter((item) => item.level === 'fail').length;
|
|
481
|
+
const warnCount = connectors.filter((item) => item.level === 'warn').length;
|
|
341
482
|
const passCount = connectors.filter((item) => item.level === 'pass').length;
|
|
483
|
+
const overall = failCount > 0 ? 'fail' : warnCount > 0 ? 'warn' : 'pass';
|
|
342
484
|
return {
|
|
343
485
|
ok: failCount === 0,
|
|
344
486
|
exitCode: failCount === 0 ? 0 : 1,
|
|
345
|
-
overall
|
|
487
|
+
overall,
|
|
346
488
|
timeout_ms: timeoutMs,
|
|
347
489
|
pass_count: passCount,
|
|
490
|
+
warn_count: warnCount,
|
|
348
491
|
fail_count: failCount,
|
|
349
492
|
connectors,
|
|
350
493
|
};
|
|
351
494
|
}
|
|
352
495
|
|
|
353
|
-
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
|
// ---------------------------------------------------------------------------
|
|
@@ -420,7 +345,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
420
345
|
}
|
|
421
346
|
|
|
422
347
|
// Step 1: Check intake queue for pending work
|
|
423
|
-
const queued =
|
|
348
|
+
const queued = findNextDispatchableIntent(root);
|
|
424
349
|
let targetIntentId = null;
|
|
425
350
|
let visionObjective = null;
|
|
426
351
|
|
|
@@ -467,7 +392,10 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
467
392
|
trigger: visionObjective ? 'vision_scan' : 'intake',
|
|
468
393
|
triggerReason: visionObjective || readIntent(root, targetIntentId)?.charter || null,
|
|
469
394
|
});
|
|
470
|
-
const preparedIntent =
|
|
395
|
+
const preparedIntent = prepareIntentForDispatch(root, targetIntentId, {
|
|
396
|
+
allowTerminalRestart: true,
|
|
397
|
+
provenance,
|
|
398
|
+
});
|
|
471
399
|
if (!preparedIntent.ok) {
|
|
472
400
|
log(`Continuous start error: ${preparedIntent.error}`);
|
|
473
401
|
session.status = 'failed';
|
|
@@ -476,7 +404,7 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
476
404
|
}
|
|
477
405
|
|
|
478
406
|
// Execute the governed run
|
|
479
|
-
session.current_run_id = preparedIntent.
|
|
407
|
+
session.current_run_id = preparedIntent.run_id;
|
|
480
408
|
session.current_vision_objective = visionObjective || preparedIntent.intent?.charter || null;
|
|
481
409
|
session.status = 'running';
|
|
482
410
|
writeContinuousSession(root, session);
|
|
@@ -497,12 +425,12 @@ export async function advanceContinuousRunOnce(context, session, contOpts, execu
|
|
|
497
425
|
status: 'failed',
|
|
498
426
|
action: 'run_failed',
|
|
499
427
|
stop_reason: err.message,
|
|
500
|
-
run_id: preparedIntent.
|
|
428
|
+
run_id: preparedIntent.run_id || null,
|
|
501
429
|
intent_id: targetIntentId,
|
|
502
430
|
};
|
|
503
431
|
}
|
|
504
432
|
|
|
505
|
-
session.current_run_id = execution.result?.state?.run_id || preparedIntent.
|
|
433
|
+
session.current_run_id = execution.result?.state?.run_id || preparedIntent.run_id || null;
|
|
506
434
|
session.cumulative_spent_usd = (session.cumulative_spent_usd || 0) + getExecutionRunSpentUsd(execution);
|
|
507
435
|
|
|
508
436
|
const stopReason = execution.result?.stop_reason;
|
|
@@ -16,8 +16,10 @@
|
|
|
16
16
|
|
|
17
17
|
import { readFileSync, writeFileSync, existsSync, mkdirSync, rmSync, readdirSync } from 'fs';
|
|
18
18
|
import { join } from 'path';
|
|
19
|
+
import { execSync } from 'child_process';
|
|
19
20
|
import { getActiveTurn, getActiveTurns } from './governed-state.js';
|
|
20
21
|
import { renderInheritedContextMarkdown } from './run-context-inheritance.js';
|
|
22
|
+
import { isOperationalPath } from './repo-observer.js';
|
|
21
23
|
import { renderRepoDecisionsMarkdown } from './repo-decisions.js';
|
|
22
24
|
import {
|
|
23
25
|
DISPATCH_INDEX_PATH,
|
|
@@ -83,6 +85,25 @@ export function writeDispatchBundle(root, state, config, opts = {}) {
|
|
|
83
85
|
const bundleDir = join(root, getDispatchTurnDir(turn.turn_id));
|
|
84
86
|
const warnings = [...(opts.warnings || [])];
|
|
85
87
|
|
|
88
|
+
// BUG-5: Warn operator about dirty workspace files at dispatch time
|
|
89
|
+
try {
|
|
90
|
+
const statusOutput = execSync('git status --porcelain', { cwd: root, encoding: 'utf8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
91
|
+
if (statusOutput) {
|
|
92
|
+
const dirtyFiles = statusOutput.split('\n').map(l => l.slice(3).trim()).filter(Boolean);
|
|
93
|
+
const nonOperationalDirty = dirtyFiles.filter(f => !isOperationalPath(f));
|
|
94
|
+
if (nonOperationalDirty.length > 0) {
|
|
95
|
+
const baseline = turn.baseline;
|
|
96
|
+
const snapshotKeys = baseline?.dirty_snapshot ? Object.keys(baseline.dirty_snapshot) : [];
|
|
97
|
+
const unsnapshotted = nonOperationalDirty.filter(f => !snapshotKeys.includes(f));
|
|
98
|
+
if (unsnapshotted.length > 0) {
|
|
99
|
+
warnings.push(
|
|
100
|
+
`Workspace has ${unsnapshotted.length} uncommitted file(s) not in the dispatch baseline: ${unsnapshotted.slice(0, 5).join(', ')}${unsnapshotted.length > 5 ? '...' : ''}. These will be excluded from files_changed validation. If the subprocess modifies them, add them to files_changed.`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch { /* non-fatal — skip if git unavailable */ }
|
|
106
|
+
|
|
86
107
|
// Clear and recreate only the targeted turn bundle
|
|
87
108
|
try {
|
|
88
109
|
rmSync(bundleDir, { recursive: true, force: true });
|
|
@@ -311,6 +332,24 @@ function renderPrompt(role, roleId, turn, state, config, root) {
|
|
|
311
332
|
lines.push('');
|
|
312
333
|
}
|
|
313
334
|
|
|
335
|
+
if (turn.intake_context) {
|
|
336
|
+
lines.push('### Active Injected Intent — respond to this as your primary charter');
|
|
337
|
+
lines.push('');
|
|
338
|
+
if (turn.intake_context.charter) {
|
|
339
|
+
lines.push(turn.intake_context.charter);
|
|
340
|
+
lines.push('');
|
|
341
|
+
}
|
|
342
|
+
if (Array.isArray(turn.intake_context.acceptance_contract) && turn.intake_context.acceptance_contract.length > 0) {
|
|
343
|
+
lines.push('Acceptance contract:');
|
|
344
|
+
turn.intake_context.acceptance_contract.forEach((requirement, index) => {
|
|
345
|
+
lines.push(`${index + 1}. ${requirement}`);
|
|
346
|
+
});
|
|
347
|
+
lines.push('');
|
|
348
|
+
}
|
|
349
|
+
lines.push('You must explicitly address every acceptance item in your turn summary, artifacts, or verification evidence. Do not treat this as background context.');
|
|
350
|
+
lines.push('');
|
|
351
|
+
}
|
|
352
|
+
|
|
314
353
|
// Retry context
|
|
315
354
|
if (turn.attempt > 1 && turn.last_rejection) {
|
|
316
355
|
lines.push('## Previous Attempt Failed');
|