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.
Files changed (38) hide show
  1. package/README.md +2 -0
  2. package/bin/agentxchain.js +38 -4
  3. package/package.json +1 -1
  4. package/scripts/verify-post-publish.sh +55 -5
  5. package/src/commands/accept-turn.js +14 -0
  6. package/src/commands/checkpoint-turn.js +35 -0
  7. package/src/commands/connector.js +17 -2
  8. package/src/commands/doctor.js +151 -1
  9. package/src/commands/events.js +7 -1
  10. package/src/commands/init.js +42 -11
  11. package/src/commands/inject.js +1 -1
  12. package/src/commands/mission.js +803 -7
  13. package/src/commands/reissue-turn.js +122 -0
  14. package/src/commands/reject-turn.js +60 -6
  15. package/src/commands/restart.js +81 -10
  16. package/src/commands/resume.js +20 -9
  17. package/src/commands/run.js +13 -0
  18. package/src/commands/status.js +58 -4
  19. package/src/commands/step.js +49 -10
  20. package/src/commands/validate.js +78 -20
  21. package/src/lib/cli-version.js +106 -0
  22. package/src/lib/connector-probe.js +146 -5
  23. package/src/lib/continuous-run.js +22 -87
  24. package/src/lib/coordinator-dispatch.js +25 -0
  25. package/src/lib/dispatch-bundle.js +39 -0
  26. package/src/lib/governed-state.js +624 -11
  27. package/src/lib/governed-templates.js +1 -0
  28. package/src/lib/intake.js +233 -77
  29. package/src/lib/mission-plans.js +510 -6
  30. package/src/lib/missions.js +65 -6
  31. package/src/lib/normalized-config.js +50 -15
  32. package/src/lib/repo-observer.js +8 -2
  33. package/src/lib/run-events.js +5 -0
  34. package/src/lib/run-loop.js +25 -0
  35. package/src/lib/runner-interface.js +2 -0
  36. package/src/lib/session-checkpoint.js +18 -2
  37. package/src/lib/turn-checkpoint.js +221 -0
  38. package/src/templates/governed/full-local-cli.json +71 -0
@@ -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 mode = opts.mode || 'full';
14
- const validation = context.config.protocol_mode === 'governed'
15
- ? validateGovernedProject(context.root, context.rawConfig, context.config, {
16
- mode,
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
- if (context.config.protocol_mode === 'governed') {
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
- return probeLocalCommand(runtimeId, runtime, 'command_presence');
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: failCount === 0 ? 'pass' : 'fail',
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, readdirSync, mkdirSync, unlinkSync } from 'node:fs';
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
- planIntent,
21
- startIntent,
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 prepareIntentForRun(root, intentId, options = {}) {
170
- let intent = readIntent(root, intentId);
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, { autoApprove: true, report: true, log });
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 = findNextQueuedIntent(root);
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 = prepareIntentForRun(root, targetIntentId, { provenance });
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.runId;
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.runId || null,
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.runId || null;
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' };