agentxchain 2.9.0 → 2.11.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 CHANGED
@@ -109,6 +109,7 @@ agentxchain step
109
109
  | `approve-transition` | Approve a pending human-gated phase transition |
110
110
  | `approve-completion` | Approve a pending human-gated run completion |
111
111
  | `validate` | Validate governed kickoff wiring, a staged turn, or both |
112
+ | `template validate` | Prove the template registry, workflow-kit scaffold contract, and planning artifact completeness (`--json` exposes a `workflow_kit` block) |
112
113
  | `verify protocol` | Run the shipped protocol conformance suite against a target implementation |
113
114
  | `dashboard` | Open the local governance dashboard in your browser for repo-local runs or multi-repo coordinator initiatives, including pending gate approvals |
114
115
  | `plugin install|list|remove` | Install, inspect, or remove governed hook plugins backed by `agentxchain-plugin.json` manifests |
@@ -69,6 +69,7 @@ import { escalateCommand } from '../src/commands/escalate.js';
69
69
  import { acceptTurnCommand } from '../src/commands/accept-turn.js';
70
70
  import { rejectTurnCommand } from '../src/commands/reject-turn.js';
71
71
  import { stepCommand } from '../src/commands/step.js';
72
+ import { runCommand } from '../src/commands/run.js';
72
73
  import { approveTransitionCommand } from '../src/commands/approve-transition.js';
73
74
  import { approveCompletionCommand } from '../src/commands/approve-completion.js';
74
75
  import { dashboardCommand } from '../src/commands/dashboard.js';
@@ -245,6 +246,9 @@ verifyCmd
245
246
  .option('--tier <tier>', 'Conformance tier to verify (1, 2, or 3)', '1')
246
247
  .option('--surface <surface>', 'Restrict verification to a single surface')
247
248
  .option('--target <path>', 'Target root containing .agentxchain-conformance/capabilities.json', '.')
249
+ .option('--remote <url>', 'Remote HTTP conformance endpoint base URL')
250
+ .option('--token <token>', 'Bearer token for remote HTTP conformance endpoint')
251
+ .option('--timeout <ms>', 'Per-fixture remote HTTP timeout in milliseconds', '30000')
248
252
  .option('--format <format>', 'Output format: text or json', 'text')
249
253
  .action(verifyProtocolCommand);
250
254
 
@@ -304,6 +308,16 @@ program
304
308
  .option('--auto-reject', 'Auto-reject and retry on validation failure')
305
309
  .action(stepCommand);
306
310
 
311
+ program
312
+ .command('run')
313
+ .description('Drive a governed run to completion: multi-turn execution with gate prompting')
314
+ .option('--role <role>', 'Override the initial role (default: config-driven selection)')
315
+ .option('--max-turns <n>', 'Maximum turns before stopping (default: 50)', parseInt)
316
+ .option('--auto-approve', 'Auto-approve all gates (non-interactive mode)')
317
+ .option('--verbose', 'Stream adapter subprocess output')
318
+ .option('--dry-run', 'Print what would be dispatched without executing')
319
+ .action(runCommand);
320
+
307
321
  program
308
322
  .command('approve-transition')
309
323
  .description('Approve a pending phase transition that requires human sign-off')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.9.0",
3
+ "version": "2.11.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -20,6 +20,7 @@
20
20
  "test:node": "node --test test/*.test.js",
21
21
  "preflight:release": "bash scripts/release-preflight.sh",
22
22
  "preflight:release:strict": "bash scripts/release-preflight.sh --strict",
23
+ "postflight:release": "bash scripts/release-postflight.sh",
23
24
  "build:macos": "bun build bin/agentxchain.js --compile --target=bun-darwin-arm64 --outfile=dist/agentxchain-macos-arm64",
24
25
  "build:linux": "bun build bin/agentxchain.js --compile --target=bun-linux-x64 --outfile=dist/agentxchain-linux-x64",
25
26
  "publish:npm": "bash scripts/publish-npm.sh"
@@ -0,0 +1,365 @@
1
+ /**
2
+ * agentxchain run — drive a governed run to completion.
3
+ *
4
+ * Thin CLI surface over the runLoop library. Wires runLoop callbacks to:
5
+ * - Existing adapter system (api_proxy, local_cli, mcp)
6
+ * - Interactive gate prompting (stdin) or auto-approve mode
7
+ * - Terminal output via chalk
8
+ *
9
+ * Does NOT support manual adapter — use `agentxchain step` for that.
10
+ * Does NOT call assignTurn/acceptTurn/rejectTurn directly — runLoop owns the state machine.
11
+ *
12
+ * See .planning/AGENTXCHAIN_RUN_SPEC.md for the full specification.
13
+ */
14
+
15
+ import chalk from 'chalk';
16
+ import { createInterface } from 'readline';
17
+ import { readFileSync, existsSync } from 'fs';
18
+ import { join } from 'path';
19
+ import { loadProjectContext, loadProjectState } from '../lib/config.js';
20
+ import { runLoop } from '../lib/run-loop.js';
21
+ import { dispatchApiProxy } from '../lib/adapters/api-proxy-adapter.js';
22
+ import {
23
+ dispatchLocalCli,
24
+ saveDispatchLogs,
25
+ resolvePromptTransport,
26
+ } from '../lib/adapters/local-cli-adapter.js';
27
+ import { dispatchMcp, resolveMcpTransport, describeMcpRuntimeTarget } from '../lib/adapters/mcp-adapter.js';
28
+ import { runHooks } from '../lib/hook-runner.js';
29
+ import { finalizeDispatchManifest } from '../lib/dispatch-manifest.js';
30
+ import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
31
+ import { resolveGovernedRole } from '../lib/role-resolution.js';
32
+ import {
33
+ getDispatchAssignmentPath,
34
+ getDispatchContextPath,
35
+ getDispatchEffectiveContextPath,
36
+ getDispatchPromptPath,
37
+ getDispatchTurnDir,
38
+ getTurnStagingResultPath,
39
+ } from '../lib/turn-paths.js';
40
+
41
+ export async function runCommand(opts) {
42
+ const context = loadProjectContext();
43
+ if (!context) {
44
+ console.log(chalk.red('No agentxchain.json found. Run `agentxchain init` first.'));
45
+ process.exit(1);
46
+ }
47
+
48
+ const { root, config } = context;
49
+
50
+ if (config.protocol_mode !== 'governed') {
51
+ console.log(chalk.red('The run command is only available for governed projects.'));
52
+ console.log(chalk.dim('Legacy projects use: agentxchain start'));
53
+ process.exit(1);
54
+ }
55
+
56
+ const maxTurns = opts.maxTurns || 50;
57
+ const autoApprove = !!opts.autoApprove;
58
+ const verbose = !!opts.verbose;
59
+ const overrideResolution = opts.role
60
+ ? resolveGovernedRole({ override: opts.role, state: null, config })
61
+ : null;
62
+
63
+ if (overrideResolution?.error) {
64
+ console.log(chalk.red(overrideResolution.error));
65
+ if (overrideResolution.availableRoles.length) {
66
+ console.log(chalk.dim(`Available roles: ${overrideResolution.availableRoles.join(', ')}`));
67
+ }
68
+ process.exit(1);
69
+ }
70
+
71
+ // ── Dry run ───────────────────────────────────────────────────────────────
72
+ if (opts.dryRun) {
73
+ const dryRunState = loadProjectState(root, config);
74
+ const roleId = overrideResolution?.roleId || resolveRole(null, dryRunState, config);
75
+ console.log(chalk.cyan('Dry run — no execution'));
76
+ console.log(` First role: ${roleId || chalk.dim('(unresolved)')}`);
77
+ console.log(` Max turns: ${maxTurns}`);
78
+ console.log(` Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`);
79
+ const roleIds = Object.keys(config.roles || {});
80
+ for (const rid of roleIds) {
81
+ const role = config.roles[rid];
82
+ const rtId = role.runtime;
83
+ const rt = config.runtimes?.[rtId];
84
+ const rtType = rt?.type || role.runtime_class || 'manual';
85
+ const supported = rtType !== 'manual';
86
+ console.log(` ${supported ? chalk.green('✓') : chalk.red('✗')} ${rid} → ${rtType}${supported ? '' : ' (not supported in run mode)'}`);
87
+ }
88
+ process.exit(0);
89
+ }
90
+
91
+ // ── SIGINT handling ─────────────────────────────────────────────────────
92
+ let aborted = false;
93
+ let sigintCount = 0;
94
+ const controller = new AbortController();
95
+
96
+ process.on('SIGINT', () => {
97
+ sigintCount++;
98
+ if (sigintCount >= 2) {
99
+ process.exit(130);
100
+ }
101
+ aborted = true;
102
+ controller.abort();
103
+ console.log(chalk.yellow('\nSIGINT received — finishing current turn, then stopping.'));
104
+ });
105
+
106
+ // ── Run header ──────────────────────────────────────────────────────────
107
+ console.log(chalk.cyan.bold('agentxchain run'));
108
+ console.log(chalk.dim(` Max turns: ${maxTurns} Gate mode: ${autoApprove ? 'auto-approve' : 'interactive'}`));
109
+ console.log('');
110
+
111
+ // ── Track first-call for --role override ────────────────────────────────
112
+ let firstSelectRole = true;
113
+
114
+ // ── Callbacks ───────────────────────────────────────────────────────────
115
+ const callbacks = {
116
+ selectRole(state, cfg) {
117
+ if (aborted) return null;
118
+
119
+ if (firstSelectRole && opts.role) {
120
+ firstSelectRole = false;
121
+ return overrideResolution.roleId;
122
+ }
123
+ firstSelectRole = false;
124
+ return resolveRole(null, state, cfg);
125
+ },
126
+
127
+ async dispatch(ctx) {
128
+ const { turn, state, config: cfg, root: projectRoot } = ctx;
129
+ const roleId = turn.assigned_role;
130
+ const role = cfg.roles?.[roleId];
131
+ const runtimeId = turn.runtime_id;
132
+ const runtime = cfg.runtimes?.[runtimeId];
133
+ const runtimeType = runtime?.type || role?.runtime_class || 'manual';
134
+ const hooksConfig = cfg.hooks || {};
135
+
136
+ // Manual adapter is not supported in run mode
137
+ if (runtimeType === 'manual') {
138
+ console.log(chalk.yellow(`Skipping manual role "${roleId}" — use agentxchain step for manual dispatch.`));
139
+ return { accept: false, reason: 'manual adapter is not supported in run mode — use agentxchain step' };
140
+ }
141
+
142
+ // ── after_dispatch hooks ──────────────────────────────────────────
143
+ if (hooksConfig.after_dispatch?.length > 0) {
144
+ const hookResult = runHooks(projectRoot, hooksConfig, 'after_dispatch', {
145
+ turn_id: turn.turn_id,
146
+ role_id: roleId,
147
+ bundle_path: getDispatchTurnDir(turn.turn_id),
148
+ bundle_files: ['ASSIGNMENT.json', 'PROMPT.md', 'CONTEXT.md'],
149
+ }, {
150
+ run_id: state.run_id,
151
+ turn_id: turn.turn_id,
152
+ protectedPaths: [
153
+ getDispatchAssignmentPath(turn.turn_id),
154
+ getDispatchPromptPath(turn.turn_id),
155
+ getDispatchContextPath(turn.turn_id),
156
+ getDispatchEffectiveContextPath(turn.turn_id),
157
+ ],
158
+ });
159
+
160
+ if (!hookResult.ok) {
161
+ return { accept: false, reason: `after_dispatch hook blocked: ${hookResult.error || 'hook failure'}` };
162
+ }
163
+ }
164
+
165
+ // ── Finalize dispatch manifest ────────────────────────────────────
166
+ const manifestResult = finalizeDispatchManifest(projectRoot, turn.turn_id, {
167
+ run_id: state.run_id,
168
+ role: roleId,
169
+ });
170
+ if (!manifestResult.ok) {
171
+ return { accept: false, reason: `dispatch manifest failed: ${manifestResult.error}` };
172
+ }
173
+
174
+ // ── Route to adapter ──────────────────────────────────────────────
175
+ const adapterOpts = {
176
+ signal: controller.signal,
177
+ onStatus: (msg) => console.log(chalk.dim(` ${msg}`)),
178
+ verifyManifest: true,
179
+ };
180
+
181
+ if (verbose) {
182
+ adapterOpts.onStdout = (text) => process.stdout.write(chalk.dim(text));
183
+ adapterOpts.onStderr = (text) => process.stderr.write(chalk.yellow(text));
184
+ }
185
+
186
+ let adapterResult;
187
+
188
+ if (runtimeType === 'api_proxy') {
189
+ console.log(chalk.dim(` Dispatching to API proxy: ${runtime?.provider || '?'} / ${runtime?.model || '?'}`));
190
+ adapterResult = await dispatchApiProxy(projectRoot, state, cfg, adapterOpts);
191
+ } else if (runtimeType === 'mcp') {
192
+ const transport = resolveMcpTransport(runtime);
193
+ console.log(chalk.dim(` Dispatching to MCP ${transport}: ${describeMcpRuntimeTarget(runtime)}`));
194
+ adapterResult = await dispatchMcp(projectRoot, state, cfg, adapterOpts);
195
+ } else if (runtimeType === 'local_cli') {
196
+ const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
197
+ console.log(chalk.dim(` Dispatching to local CLI: ${runtime?.command || '(default)'} transport: ${transport}`));
198
+ adapterResult = await dispatchLocalCli(projectRoot, state, cfg, adapterOpts);
199
+ } else {
200
+ return { accept: false, reason: `unknown runtime type "${runtimeType}"` };
201
+ }
202
+
203
+ // Save adapter logs
204
+ if (adapterResult.logs?.length) {
205
+ saveDispatchLogs(projectRoot, turn.turn_id, adapterResult.logs);
206
+ }
207
+
208
+ // Aborted
209
+ if (adapterResult.aborted) {
210
+ return { accept: false, reason: 'dispatch aborted by operator' };
211
+ }
212
+
213
+ // Timed out
214
+ if (adapterResult.timedOut) {
215
+ return { accept: false, reason: 'dispatch timed out' };
216
+ }
217
+
218
+ // Adapter failure
219
+ if (!adapterResult.ok) {
220
+ const errorDetail = adapterResult.classified
221
+ ? `${adapterResult.classified.error_class}: ${adapterResult.classified.recovery}`
222
+ : adapterResult.error;
223
+ return { accept: false, reason: errorDetail || 'adapter dispatch failed' };
224
+ }
225
+
226
+ // ── Read staged result ────────────────────────────────────────────
227
+ const stagingFile = join(projectRoot, getTurnStagingResultPath(turn.turn_id));
228
+ if (!existsSync(stagingFile)) {
229
+ return { accept: false, reason: 'adapter completed but no staged result found' };
230
+ }
231
+
232
+ let turnResult;
233
+ try {
234
+ turnResult = JSON.parse(readFileSync(stagingFile, 'utf8'));
235
+ } catch (err) {
236
+ return { accept: false, reason: `failed to parse staged result: ${err.message}` };
237
+ }
238
+
239
+ return { accept: true, turnResult };
240
+ },
241
+
242
+ async approveGate(gateType, state) {
243
+ if (autoApprove) {
244
+ console.log(chalk.yellow(` Auto-approved ${gateType} gate`));
245
+ return true;
246
+ }
247
+
248
+ // Non-TTY → fail-closed
249
+ if (!process.stdin.isTTY) {
250
+ console.log(chalk.yellow(` Gate pause: ${gateType} — stdin is not a TTY, failing closed.`));
251
+ console.log(chalk.dim(' Use --auto-approve for non-interactive mode.'));
252
+ return false;
253
+ }
254
+
255
+ const target = gateType === 'phase_transition'
256
+ ? state.pending_phase_transition?.target || '(next phase)'
257
+ : 'run completion';
258
+
259
+ console.log('');
260
+ console.log(chalk.yellow.bold(`Gate pause: ${gateType}`));
261
+ console.log(chalk.dim(` Phase: ${state.phase} → ${target}`));
262
+
263
+ const answer = await promptUser(` Approve? [y/N] `);
264
+ const approved = /^y(es)?$/i.test(answer.trim());
265
+ return approved;
266
+ },
267
+
268
+ onEvent(event) {
269
+ switch (event.type) {
270
+ case 'turn_assigned':
271
+ console.log(chalk.cyan(`Turn assigned: ${event.turn?.turn_id} → ${event.role}`));
272
+ break;
273
+ case 'turn_accepted':
274
+ console.log(chalk.green(`Turn accepted: ${event.turn?.turn_id}`));
275
+ break;
276
+ case 'turn_rejected':
277
+ console.log(chalk.yellow(`Turn rejected: ${event.turn?.turn_id} — ${event.reason || 'no reason'}`));
278
+ break;
279
+ case 'gate_paused':
280
+ console.log(chalk.yellow(`Gate paused: ${event.gateType}`));
281
+ break;
282
+ case 'gate_approved':
283
+ console.log(chalk.green(`Gate approved: ${event.gateType}`));
284
+ break;
285
+ case 'gate_held':
286
+ console.log(chalk.yellow(`Gate held: ${event.gateType} — run paused`));
287
+ break;
288
+ case 'blocked':
289
+ console.log(chalk.red(`Run blocked`));
290
+ break;
291
+ case 'completed':
292
+ console.log(chalk.green.bold('Run completed'));
293
+ break;
294
+ case 'caller_stopped':
295
+ console.log(chalk.yellow('Run stopped by caller'));
296
+ break;
297
+ }
298
+ },
299
+ };
300
+
301
+ // ── Execute ─────────────────────────────────────────────────────────────
302
+ const result = await runLoop(root, config, callbacks, { maxTurns });
303
+
304
+ // ── Summary ─────────────────────────────────────────────────────────────
305
+ console.log('');
306
+ console.log(chalk.dim('─── Run Summary ───'));
307
+ console.log(` Status: ${result.ok ? chalk.green('completed') : chalk.yellow(result.stop_reason)}`);
308
+ console.log(` Turns: ${result.turns_executed}`);
309
+ console.log(` Gates: ${result.gates_approved} approved`);
310
+ console.log(` Errors: ${result.errors.length ? chalk.red(result.errors.length) : 'none'}`);
311
+
312
+ if (result.errors.length) {
313
+ for (const err of result.errors) {
314
+ console.log(chalk.red(` ${err}`));
315
+ }
316
+ }
317
+
318
+ // Recovery guidance for blocked/rejected states
319
+ if (result.state && (result.stop_reason === 'blocked' || result.stop_reason === 'reject_exhausted' || result.stop_reason === 'dispatch_error')) {
320
+ const recovery = deriveRecoveryDescriptor(result.state);
321
+ if (recovery) {
322
+ console.log('');
323
+ console.log(chalk.yellow(` Recovery: ${recovery.typed_reason}`));
324
+ console.log(chalk.dim(` Action: ${recovery.recovery_action}`));
325
+ if (recovery.detail) {
326
+ console.log(chalk.dim(` Detail: ${recovery.detail}`));
327
+ }
328
+ }
329
+ }
330
+
331
+ // ── Exit code ───────────────────────────────────────────────────────────
332
+ const successReasons = new Set(['completed', 'gate_held', 'caller_stopped', 'max_turns_reached']);
333
+ if (result.ok || successReasons.has(result.stop_reason)) {
334
+ process.exit(0);
335
+ }
336
+ process.exit(1);
337
+ }
338
+
339
+ // ── Helpers ───────────────────────────────────────────────────────────────
340
+
341
+ /**
342
+ * Resolve the target role for the next turn.
343
+ * Same routing contract as step.js resolveTargetRole, minus interactive logging.
344
+ */
345
+ function resolveRole(override, state, config) {
346
+ const resolved = resolveGovernedRole({ override, state, config });
347
+ return resolved.error ? null : resolved.roleId;
348
+ }
349
+
350
+ /**
351
+ * Prompt the user via stdin readline.
352
+ */
353
+ function promptUser(question) {
354
+ return new Promise((resolve) => {
355
+ const rl = createInterface({
356
+ input: process.stdin,
357
+ output: process.stdout,
358
+ });
359
+ rl.question(question, (answer) => {
360
+ rl.close();
361
+ resolve(answer);
362
+ });
363
+ rl.on('close', () => resolve(''));
364
+ });
365
+ }
@@ -61,6 +61,7 @@ import { safeWriteJson } from '../lib/safe-write.js';
61
61
  import { deriveRecoveryDescriptor } from '../lib/blocked-state.js';
62
62
  import { runHooks } from '../lib/hook-runner.js';
63
63
  import { finalizeDispatchManifest, verifyDispatchManifest } from '../lib/dispatch-manifest.js';
64
+ import { resolveGovernedRole } from '../lib/role-resolution.js';
64
65
 
65
66
  export async function stepCommand(opts) {
66
67
  const context = loadProjectContext();
@@ -836,45 +837,24 @@ function printLifecycleHookFailure(title, result, { turnId, roleId, action }) {
836
837
  }
837
838
 
838
839
  function resolveTargetRole(opts, state, config) {
839
- const phase = state.phase;
840
- const routing = config.routing?.[phase];
841
-
842
- if (opts.role) {
843
- if (!config.roles?.[opts.role]) {
844
- console.log(chalk.red(`Unknown role: "${opts.role}"`));
845
- console.log(chalk.dim(`Available roles: ${Object.keys(config.roles || {}).join(', ')}`));
846
- return null;
847
- }
848
- if (routing?.allowed_next_roles && !routing.allowed_next_roles.includes(opts.role) && opts.role !== 'human') {
849
- console.log(chalk.yellow(`Warning: role "${opts.role}" is not in allowed_next_roles for phase "${phase}".`));
850
- }
851
- return opts.role;
852
- }
853
-
854
- // Use stored next_recommended_role if routing-legal
855
- if (state.next_recommended_role) {
856
- const recommended = state.next_recommended_role;
857
- if (config.roles?.[recommended]) {
858
- const isLegal = !routing?.allowed_next_roles || routing.allowed_next_roles.includes(recommended);
859
- if (isLegal) {
860
- console.log(chalk.dim(`Using recommended role: ${recommended} (from previous turn)`));
861
- return recommended;
862
- }
840
+ const resolved = resolveGovernedRole({ override: opts.role || null, state, config });
841
+ if (resolved.error) {
842
+ console.log(chalk.red(resolved.error));
843
+ if (resolved.availableRoles.length) {
844
+ console.log(chalk.dim(`Available roles: ${resolved.availableRoles.join(', ')}`));
863
845
  }
846
+ return null;
864
847
  }
865
848
 
866
- if (routing?.entry_role) {
867
- return routing.entry_role;
849
+ if (!opts.role && state.next_recommended_role && resolved.roleId === state.next_recommended_role) {
850
+ console.log(chalk.dim(`Using recommended role: ${resolved.roleId} (from previous turn)`));
868
851
  }
869
852
 
870
- const roles = Object.keys(config.roles || {});
871
- if (roles.length > 0) {
872
- console.log(chalk.yellow(`No entry_role for phase "${phase}". Defaulting to "${roles[0]}".`));
873
- return roles[0];
853
+ for (const warning of resolved.warnings) {
854
+ console.log(chalk.yellow(`Warning: ${warning}`));
874
855
  }
875
856
 
876
- console.log(chalk.red('No roles defined in config.'));
877
- return null;
857
+ return resolved.roleId;
878
858
  }
879
859
 
880
860
  function printRecoverySummary(state, heading) {
@@ -7,7 +7,9 @@ import {
7
7
  validateGovernedTemplateRegistry,
8
8
  validateProjectPlanningArtifacts,
9
9
  validateAcceptanceHintCompletion,
10
+ validateGovernedWorkflowKit,
10
11
  } from '../lib/governed-templates.js';
12
+ import { loadNormalizedConfig } from '../lib/normalized-config.js';
11
13
 
12
14
  function loadProjectTemplateValidation() {
13
15
  const root = findProjectRoot();
@@ -17,6 +19,7 @@ function loadProjectTemplateValidation() {
17
19
  root: null,
18
20
  template: null,
19
21
  source: null,
22
+ normalized_config: null,
20
23
  ok: true,
21
24
  errors: [],
22
25
  warnings: [],
@@ -33,16 +36,24 @@ function loadProjectTemplateValidation() {
33
36
  root,
34
37
  template: null,
35
38
  source: 'agentxchain.json',
39
+ normalized_config: null,
36
40
  ok: false,
37
41
  errors: [`Failed to parse ${CONFIG_FILE}: ${err.message}`],
38
42
  warnings: [],
39
43
  };
40
44
  }
41
45
 
46
+ let normalizedConfig = null;
47
+ const normalized = loadNormalizedConfig(parsed, root);
48
+ if (normalized.ok) {
49
+ normalizedConfig = normalized.normalized;
50
+ }
51
+
42
52
  const projectValidation = validateGovernedProjectTemplate(parsed.template);
43
53
  return {
44
54
  present: true,
45
55
  root,
56
+ normalized_config: normalizedConfig,
46
57
  ...projectValidation,
47
58
  };
48
59
  }
@@ -63,16 +74,36 @@ export function templateValidateCommand(opts = {}) {
63
74
  acceptanceHints = validateAcceptanceHintCompletion(project.root, project.template);
64
75
  }
65
76
 
77
+ let workflowKit = null;
78
+ if (project.present && project.ok && project.root) {
79
+ if (project.normalized_config?.protocol_mode === 'governed') {
80
+ workflowKit = validateGovernedWorkflowKit(project.root, project.normalized_config);
81
+ } else if (!project.normalized_config) {
82
+ workflowKit = {
83
+ ok: true,
84
+ required_files: [],
85
+ gate_required_files: [],
86
+ present: [],
87
+ missing: [],
88
+ structural_checks: [],
89
+ errors: [],
90
+ warnings: ['Workflow kit validation skipped because project config could not be normalized.'],
91
+ };
92
+ }
93
+ }
94
+
66
95
  const errors = [
67
96
  ...registry.errors,
68
97
  ...project.errors,
69
98
  ...(planningArtifacts?.errors || []),
99
+ ...(workflowKit?.errors || []),
70
100
  ];
71
101
  const warnings = [
72
102
  ...registry.warnings,
73
103
  ...project.warnings,
74
104
  ...(planningArtifacts?.warnings || []),
75
105
  ...(acceptanceHints?.warnings || []),
106
+ ...(workflowKit?.warnings || []),
76
107
  ];
77
108
  const ok = errors.length === 0;
78
109
 
@@ -82,6 +113,7 @@ export function templateValidateCommand(opts = {}) {
82
113
  project,
83
114
  planning_artifacts: planningArtifacts,
84
115
  acceptance_hints: acceptanceHints,
116
+ workflow_kit: workflowKit,
85
117
  errors,
86
118
  warnings,
87
119
  };
@@ -125,6 +157,16 @@ export function templateValidateCommand(opts = {}) {
125
157
  console.log(` ${chalk.dim('Planning:')} ${chalk.red('FAIL')} (${planningArtifacts.missing.length}/${total} missing: ${planningArtifacts.missing.join(', ')})`);
126
158
  }
127
159
  }
160
+ if (workflowKit) {
161
+ const fileCount = workflowKit.required_files.length;
162
+ const checkCount = workflowKit.structural_checks.length;
163
+ const passedChecks = workflowKit.structural_checks.filter((check) => check.ok).length;
164
+ if (workflowKit.ok) {
165
+ console.log(` ${chalk.dim('Workflow:')} ${chalk.green('OK')} (${workflowKit.present.length}/${fileCount} files, ${passedChecks}/${checkCount} checks)`);
166
+ } else {
167
+ console.log(` ${chalk.dim('Workflow:')} ${chalk.red('FAIL')} (${workflowKit.missing.length}/${fileCount} missing, ${checkCount - passedChecks}/${checkCount} checks failed)`);
168
+ }
169
+ }
128
170
  if (acceptanceHints) {
129
171
  if (acceptanceHints.total === 0) {
130
172
  console.log(` ${chalk.dim('Acceptance:')} ${chalk.green('OK')} (no template hints defined)`);
@@ -3,27 +3,42 @@ import { resolve } from 'node:path';
3
3
  import { loadExportArtifact, verifyExportArtifact } from '../lib/export-verifier.js';
4
4
  import { verifyProtocolConformance } from '../lib/protocol-conformance.js';
5
5
 
6
- export async function verifyProtocolCommand(opts) {
7
- const target = opts.target ? resolve(opts.target) : process.cwd();
6
+ export async function verifyProtocolCommand(opts, command) {
8
7
  const requestedTier = Number.parseInt(String(opts.tier || '1'), 10);
8
+ const timeout = Number.parseInt(String(opts.timeout || '30000'), 10);
9
9
  const format = opts.format || 'text';
10
+ const targetSource = command?.getOptionValueSource?.('target');
11
+ const timeoutSource = command?.getOptionValueSource?.('timeout');
12
+ const hasExplicitTarget = targetSource && targetSource !== 'default';
13
+ const remote = opts.remote || null;
14
+
15
+ if (remote && hasExplicitTarget) {
16
+ emitProtocolVerifyError(format, 'Cannot specify both --target and --remote');
17
+ process.exit(2);
18
+ }
19
+
20
+ if (!remote && opts.token) {
21
+ emitProtocolVerifyError(format, 'Cannot specify --token without --remote');
22
+ process.exit(2);
23
+ }
24
+
25
+ if (!remote && timeoutSource && timeoutSource !== 'default') {
26
+ emitProtocolVerifyError(format, 'Cannot specify --timeout without --remote');
27
+ process.exit(2);
28
+ }
10
29
 
11
30
  let result;
12
31
  try {
13
- result = verifyProtocolConformance({
14
- targetRoot: target,
32
+ result = await verifyProtocolConformance({
33
+ targetRoot: remote ? null : resolve(opts.target || process.cwd()),
34
+ remote,
35
+ token: opts.token || null,
36
+ timeout,
15
37
  requestedTier,
16
38
  surface: opts.surface || null,
17
39
  });
18
40
  } catch (error) {
19
- if (format === 'json') {
20
- console.log(JSON.stringify({
21
- overall: 'error',
22
- message: error.message,
23
- }, null, 2));
24
- } else {
25
- console.log(chalk.red(`Protocol verification failed: ${error.message}`));
26
- }
41
+ emitProtocolVerifyError(format, error.message);
27
42
  process.exit(2);
28
43
  }
29
44
 
@@ -36,6 +51,18 @@ export async function verifyProtocolCommand(opts) {
36
51
  process.exit(result.exitCode);
37
52
  }
38
53
 
54
+ function emitProtocolVerifyError(format, message) {
55
+ if (format === 'json') {
56
+ console.log(JSON.stringify({
57
+ overall: 'error',
58
+ message,
59
+ }, null, 2));
60
+ return;
61
+ }
62
+
63
+ console.log(chalk.red(`Protocol verification failed: ${message}`));
64
+ }
65
+
39
66
  export async function verifyExportCommand(opts) {
40
67
  const format = opts.format || 'text';
41
68
  const loaded = loadExportArtifact(opts.input || '-', process.cwd());
@@ -72,7 +99,8 @@ function printProtocolReport(report) {
72
99
  console.log('');
73
100
  console.log(chalk.bold(' AgentXchain Protocol Conformance'));
74
101
  console.log(chalk.dim(' ' + '─'.repeat(44)));
75
- console.log(chalk.dim(` Target: ${report.target_root}`));
102
+ const targetLabel = report.remote || report.target_root;
103
+ console.log(chalk.dim(` Target: ${targetLabel}`));
76
104
  console.log(chalk.dim(` Implementation: ${report.implementation}`));
77
105
  console.log(chalk.dim(` Tier requested: ${report.tier_requested}`));
78
106
  console.log('');