agentxchain 2.135.1 → 2.136.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -123,10 +123,11 @@ import { historyCommand } from '../src/commands/history.js';
123
123
  import { decisionsCommand } from '../src/commands/decisions.js';
124
124
  import { diffCommand } from '../src/commands/diff.js';
125
125
  import { eventsCommand } from '../src/commands/events.js';
126
- import { connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
126
+ import { connectorCapabilitiesCommand, connectorCheckCommand, connectorValidateCommand } from '../src/commands/connector.js';
127
127
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
128
128
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
129
129
  import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
130
+ import { workflowKitDescribeCommand } from '../src/commands/workflow-kit.js';
130
131
 
131
132
  const __dirname = dirname(fileURLToPath(import.meta.url));
132
133
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -299,6 +300,13 @@ connectorCmd
299
300
  .option('--timeout <ms>', 'Per-probe timeout in milliseconds', '8000')
300
301
  .action(connectorCheckCommand);
301
302
 
303
+ connectorCmd
304
+ .command('capabilities [runtime_id]')
305
+ .description('Show merged capability contract for a runtime or all runtimes (machine-readable handshake)')
306
+ .option('-j, --json', 'Output as JSON')
307
+ .option('--all', 'Show capabilities for all configured runtimes')
308
+ .action(connectorCapabilitiesCommand);
309
+
302
310
  connectorCmd
303
311
  .command('validate <runtime_id>')
304
312
  .description('Dispatch one synthetic governed turn through a runtime and validate the staged turn result')
@@ -839,6 +847,16 @@ phaseCmd
839
847
  .option('-j, --json', 'Output as JSON')
840
848
  .action((phaseId, opts) => phaseCommand('show', phaseId, opts));
841
849
 
850
+ const workflowKitCmd = program
851
+ .command('workflow-kit')
852
+ .description('Inspect the workflow kit contract for this project');
853
+
854
+ workflowKitCmd
855
+ .command('describe')
856
+ .description('Show the workflow kit contract — templates, artifacts, semantic validators, gate coverage')
857
+ .option('-j, --json', 'Output as JSON')
858
+ .action(workflowKitDescribeCommand);
859
+
842
860
  const gateCmd = program
843
861
  .command('gate')
844
862
  .description('Inspect governed gate definitions');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.135.1",
3
+ "version": "2.136.1",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -10,7 +10,9 @@
10
10
  ".": "./bin/agentxchain.js",
11
11
  "./adapter-interface": "./src/lib/adapter-interface.js",
12
12
  "./runner-interface": "./src/lib/runner-interface.js",
13
- "./run-loop": "./src/lib/run-loop.js"
13
+ "./run-loop": "./src/lib/run-loop.js",
14
+ "./schemas/agentxchain-config": "./src/lib/schemas/agentxchain-config.schema.json",
15
+ "./schemas/connector-capabilities-output": "./src/lib/schemas/connector-capabilities-output.schema.json"
14
16
  },
15
17
  "files": [
16
18
  "bin/",
@@ -154,6 +154,8 @@ if [[ "$PUBLISH_GATE" -eq 1 ]]; then
154
154
  test/release-identity-hardening.test.js
155
155
  test/normalized-config.test.js
156
156
  test/conformance.test.js
157
+ test/beta-scenario-emission-guard.test.js
158
+ test/claim-reality-preflight.test.js
157
159
  test/beta-tester-scenarios/*.test.js
158
160
  )
159
161
  GATE_TEST_ARGS=()
@@ -3,6 +3,7 @@ import chalk from 'chalk';
3
3
  import { loadProjectContext } from '../lib/config.js';
4
4
  import { DEFAULT_VALIDATE_TIMEOUT_MS, validateConfiguredConnector } from '../lib/connector-validate.js';
5
5
  import { DEFAULT_TIMEOUT_MS, probeConfiguredConnectors } from '../lib/connector-probe.js';
6
+ import { buildRuntimeCapabilityReport } from '../lib/runtime-capabilities.js';
6
7
 
7
8
  function printJson(result, exitCode) {
8
9
  console.log(JSON.stringify(result, null, 2));
@@ -164,6 +165,14 @@ function printValidateText(result, exitCode) {
164
165
  }
165
166
  }
166
167
 
168
+ if (result.schema_contract) {
169
+ console.log('');
170
+ console.log(` ${chalk.dim('Schema:')} ${result.schema_contract.ok ? chalk.green('ok') : chalk.red('failed')}`);
171
+ if (Array.isArray(result.schema_contract.failures) && result.schema_contract.failures.length > 0) {
172
+ console.log(` ${chalk.dim('Contract:')} ${result.schema_contract.failures.join(' | ')}`);
173
+ }
174
+ }
175
+
167
176
  if (result.dispatch) {
168
177
  console.log('');
169
178
  console.log(` ${chalk.dim('Dispatch:')} ${result.dispatch.ok ? chalk.green('ok') : chalk.red('failed')}`);
@@ -192,6 +201,110 @@ function printValidateText(result, exitCode) {
192
201
  process.exit(exitCode);
193
202
  }
194
203
 
204
+ function printCapabilitiesText(report) {
205
+ console.log('');
206
+ console.log(chalk.bold(` ${report.runtime_id}`) + chalk.dim(` (${report.runtime_type})`));
207
+ console.log(chalk.dim(' ' + '-'.repeat(44)));
208
+
209
+ const c = report.merged_contract;
210
+ console.log(` ${chalk.dim('Transport:')} ${c.transport}`);
211
+ console.log(` ${chalk.dim('Write files:')} ${c.can_write_files}`);
212
+ console.log(` ${chalk.dim('Proposals:')} ${c.proposal_support}`);
213
+ console.log(` ${chalk.dim('Artifact ownership:')} ${c.workflow_artifact_ownership}`);
214
+ console.log(` ${chalk.dim('Local binary:')} ${c.requires_local_binary ? 'yes' : 'no'}`);
215
+
216
+ if (Object.keys(report.declared_capabilities).length > 0) {
217
+ console.log('');
218
+ console.log(` ${chalk.dim('Declared overrides:')}`);
219
+ for (const [k, v] of Object.entries(report.declared_capabilities)) {
220
+ console.log(` ${k}: ${v}`);
221
+ }
222
+ }
223
+
224
+ if (report.declaration_warnings.length > 0) {
225
+ console.log('');
226
+ for (const w of report.declaration_warnings) {
227
+ console.log(` ${chalk.yellow('!')} ${w}`);
228
+ }
229
+ }
230
+
231
+ if (report.role_bindings.length > 0) {
232
+ console.log('');
233
+ console.log(` ${chalk.dim('Role bindings:')}`);
234
+ for (const rb of report.role_bindings) {
235
+ const badge = rb.effective_write_path.startsWith('invalid') ? chalk.red('INVALID') : chalk.green('OK');
236
+ console.log(` ${badge} ${rb.role_id} (${rb.role_write_authority}) -> ${rb.effective_write_path}`);
237
+ for (const note of rb.notes) {
238
+ console.log(` ${chalk.dim(note)}`);
239
+ }
240
+ }
241
+ }
242
+ }
243
+
244
+ export async function connectorCapabilitiesCommand(runtimeId, options = {}) {
245
+ const context = loadProjectContext();
246
+ if (!context) {
247
+ const payload = { error: 'No governed agentxchain.json found.' };
248
+ if (options.json) { printJson(payload, 2); return; }
249
+ console.error(chalk.red('No governed agentxchain.json found. Run this inside a governed project.'));
250
+ process.exit(2);
251
+ }
252
+
253
+ const config = context.config;
254
+ const runtimes = config.runtimes || {};
255
+ const roles = config.roles || {};
256
+
257
+ if (options.all) {
258
+ const reports = [];
259
+ for (const [id, runtime] of Object.entries(runtimes)) {
260
+ reports.push(buildRuntimeCapabilityReport(id, runtime, roles));
261
+ }
262
+ const payload = { runtimes: reports };
263
+ if (options.json) { printJson(payload, 0); return; }
264
+
265
+ console.log('');
266
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
267
+ console.log(chalk.dim(' ' + '='.repeat(44)));
268
+ if (reports.length === 0) {
269
+ console.log(' No runtimes configured.');
270
+ } else {
271
+ for (const r of reports) { printCapabilitiesText(r); }
272
+ }
273
+ console.log('');
274
+ process.exit(0);
275
+ }
276
+
277
+ if (!runtimeId) {
278
+ const payload = { error: 'Runtime ID required. Use --all to list all runtimes.', available_runtimes: Object.keys(runtimes) };
279
+ if (options.json) { printJson(payload, 2); return; }
280
+ console.error(chalk.red('Runtime ID required. Use --all to list all runtimes.'));
281
+ if (Object.keys(runtimes).length > 0) {
282
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
283
+ }
284
+ process.exit(2);
285
+ }
286
+
287
+ if (!runtimes[runtimeId]) {
288
+ const payload = { error: `Runtime "${runtimeId}" not found.`, available_runtimes: Object.keys(runtimes) };
289
+ if (options.json) { printJson(payload, 2); return; }
290
+ console.error(chalk.red(`Runtime "${runtimeId}" not found.`));
291
+ if (Object.keys(runtimes).length > 0) {
292
+ console.error(chalk.dim(`Available: ${Object.keys(runtimes).join(', ')}`));
293
+ }
294
+ process.exit(2);
295
+ }
296
+
297
+ const report = buildRuntimeCapabilityReport(runtimeId, runtimes[runtimeId], roles);
298
+ if (options.json) { printJson(report, 0); return; }
299
+
300
+ console.log('');
301
+ console.log(chalk.bold(' AgentXchain Connector Capabilities'));
302
+ console.log(chalk.dim(' ' + '='.repeat(44)));
303
+ printCapabilitiesText(report);
304
+ console.log('');
305
+ process.exit(0);
306
+ }
307
+
195
308
  export async function connectorValidateCommand(runtimeId, options = {}) {
196
309
  const context = loadProjectContext();
197
310
  if (!context) {
@@ -326,6 +326,9 @@ export async function restartCommand(opts) {
326
326
  console.log(chalk.red(`Failed to reactivate run: ${reactivated.error}`));
327
327
  process.exit(1);
328
328
  }
329
+ if (reactivated.migration_notice) {
330
+ console.log(chalk.yellow(reactivated.migration_notice));
331
+ }
329
332
  }
330
333
 
331
334
  // Determine role from option or routing
@@ -144,6 +144,9 @@ export async function resumeCommand(opts) {
144
144
  process.exit(1);
145
145
  }
146
146
  state = reactivated.state;
147
+ if (reactivated.migration_notice) {
148
+ console.log(chalk.yellow(reactivated.migration_notice));
149
+ }
147
150
 
148
151
  // Write dispatch bundle for the existing turn
149
152
  const bundleResult = writeDispatchBundle(root, state, config);
@@ -204,6 +207,9 @@ export async function resumeCommand(opts) {
204
207
  process.exit(1);
205
208
  }
206
209
  state = reactivated.state;
210
+ if (reactivated.migration_notice) {
211
+ console.log(chalk.yellow(reactivated.migration_notice));
212
+ }
207
213
 
208
214
  const bundleResult = writeDispatchBundle(root, state, config, { turnId: retainedTurn.turn_id });
209
215
  if (!bundleResult.ok) {
@@ -233,6 +239,9 @@ export async function resumeCommand(opts) {
233
239
  }
234
240
  state = initResult.state;
235
241
  console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
242
+ if (initResult.migration_notice) {
243
+ console.log(chalk.yellow(initResult.migration_notice));
244
+ }
236
245
  }
237
246
 
238
247
  // §47: paused + run_id exists → resume same run
@@ -244,6 +253,9 @@ export async function resumeCommand(opts) {
244
253
  }
245
254
  state = reactivated.state;
246
255
  console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
256
+ if (reactivated.migration_notice) {
257
+ console.log(chalk.yellow(reactivated.migration_notice));
258
+ }
247
259
  }
248
260
 
249
261
  // §47: paused + run_id exists → resume same run
@@ -255,6 +267,9 @@ export async function resumeCommand(opts) {
255
267
  }
256
268
  state = reactivated.state;
257
269
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
270
+ if (reactivated.migration_notice) {
271
+ console.log(chalk.yellow(reactivated.migration_notice));
272
+ }
258
273
  }
259
274
 
260
275
  // Print run-context header before dispatch
@@ -260,6 +260,9 @@ export async function stepCommand(opts) {
260
260
  process.exit(1);
261
261
  }
262
262
  state = reactivated.state;
263
+ if (reactivated.migration_notice) {
264
+ console.log(chalk.yellow(reactivated.migration_notice));
265
+ }
263
266
  skipAssignment = true;
264
267
 
265
268
  // BUG-1 fix: refresh baseline snapshot to capture files dirtied between assignment and dispatch
@@ -285,6 +288,9 @@ export async function stepCommand(opts) {
285
288
  }
286
289
  state = initResult.state;
287
290
  console.log(chalk.green(`Initialized governed run: ${state.run_id}`));
291
+ if (initResult.migration_notice) {
292
+ console.log(chalk.yellow(initResult.migration_notice));
293
+ }
288
294
  }
289
295
 
290
296
  // paused → resume
@@ -296,6 +302,9 @@ export async function stepCommand(opts) {
296
302
  }
297
303
  state = reactivated.state;
298
304
  console.log(chalk.green(`Resumed blocked run: ${state.run_id}`));
305
+ if (reactivated.migration_notice) {
306
+ console.log(chalk.yellow(reactivated.migration_notice));
307
+ }
299
308
  }
300
309
 
301
310
  if (!skipAssignment && state.status === 'paused' && state.run_id) {
@@ -306,6 +315,9 @@ export async function stepCommand(opts) {
306
315
  }
307
316
  state = reactivated.state;
308
317
  console.log(chalk.green(`Resumed governed run: ${state.run_id}`));
318
+ if (reactivated.migration_notice) {
319
+ console.log(chalk.yellow(reactivated.migration_notice));
320
+ }
309
321
  }
310
322
 
311
323
  // Assign the turn
@@ -0,0 +1,186 @@
1
+ import chalk from 'chalk';
2
+ import { existsSync } from 'fs';
3
+ import { join } from 'path';
4
+ import { loadProjectContext } from '../lib/config.js';
5
+ import {
6
+ VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS,
7
+ } from '../lib/workflow-kit-phase-templates.js';
8
+ import { getEffectiveGateArtifacts } from '../lib/gate-evaluator.js';
9
+
10
+ const WORKFLOW_KIT_VERSION = '1.0';
11
+
12
+ const SEMANTIC_VALIDATOR_IDS = [
13
+ 'pm_signoff',
14
+ 'system_spec',
15
+ 'implementation_notes',
16
+ 'acceptance_matrix',
17
+ 'ship_verdict',
18
+ 'release_notes',
19
+ 'section_check',
20
+ ];
21
+
22
+ const DEFAULT_TEMPLATE_BY_PHASE = {
23
+ planning: 'planning-default',
24
+ implementation: 'implementation-default',
25
+ qa: 'qa-default',
26
+ };
27
+
28
+ export { WORKFLOW_KIT_VERSION, SEMANTIC_VALIDATOR_IDS };
29
+
30
+ function getGateLinkedPhases(config, gateId, gateDef) {
31
+ const linkedPhases = [];
32
+
33
+ if (typeof gateDef?.phase === 'string' && gateDef.phase.trim()) {
34
+ linkedPhases.push(gateDef.phase.trim());
35
+ }
36
+
37
+ for (const [phaseId, route] of Object.entries(config.routing || {})) {
38
+ if (route?.exit_gate === gateId && !linkedPhases.includes(phaseId)) {
39
+ linkedPhases.push(phaseId);
40
+ }
41
+ }
42
+
43
+ return linkedPhases;
44
+ }
45
+
46
+ function buildWorkflowKitContract(root, config) {
47
+ const phaseIds = Object.keys(config.routing || {});
48
+ const hasExplicitKit = config.workflow_kit?._explicit === true;
49
+
50
+ const templatesInUse = new Set();
51
+ const phases = {};
52
+
53
+ for (const phaseId of phaseIds) {
54
+ const phaseKit = config.workflow_kit?.phases?.[phaseId] || null;
55
+ // For default (non-explicit) workflow kits, infer template from phase name
56
+ const template = phaseKit?.template || (!hasExplicitKit ? (DEFAULT_TEMPLATE_BY_PHASE[phaseId] || null) : null);
57
+ if (template) templatesInUse.add(template);
58
+
59
+ const source = !hasExplicitKit
60
+ ? 'default'
61
+ : phaseKit
62
+ ? 'explicit'
63
+ : 'not_declared';
64
+
65
+ const artifacts = (phaseKit?.artifacts || [])
66
+ .filter((a) => a && typeof a.path === 'string')
67
+ .map((artifact) => ({
68
+ path: artifact.path,
69
+ required: artifact.required !== false,
70
+ semantics: artifact.semantics || null,
71
+ exists: existsSync(join(root, artifact.path)),
72
+ }));
73
+
74
+ phases[phaseId] = { template: template || null, source, artifacts };
75
+ }
76
+
77
+ const overallSource = !hasExplicitKit
78
+ ? 'default'
79
+ : phaseIds.every((id) => phases[id].source === 'explicit')
80
+ ? 'explicit'
81
+ : 'mixed';
82
+
83
+ const gateArtifactCoverage = {};
84
+ const gates = config.gates || {};
85
+ for (const [gateId, gateDef] of Object.entries(gates)) {
86
+ const linkedPhases = getGateLinkedPhases(config, gateId, gateDef);
87
+ const artifactPaths = [];
88
+ const seenPaths = new Set();
89
+
90
+ for (const linkedPhase of linkedPhases) {
91
+ for (const artifact of getEffectiveGateArtifacts(config, gateDef, linkedPhase)) {
92
+ if (!seenPaths.has(artifact.path)) {
93
+ seenPaths.add(artifact.path);
94
+ artifactPaths.push(artifact.path);
95
+ }
96
+ }
97
+ }
98
+
99
+ gateArtifactCoverage[gateId] = {
100
+ linked_phases: linkedPhases,
101
+ predicates_referencing_artifacts: artifactPaths.length,
102
+ artifacts_covered: artifactPaths,
103
+ };
104
+ }
105
+
106
+ return {
107
+ workflow_kit_version: WORKFLOW_KIT_VERSION,
108
+ source: overallSource,
109
+ phase_templates: {
110
+ available: [...VALID_WORKFLOW_KIT_PHASE_TEMPLATE_IDS],
111
+ in_use: [...templatesInUse],
112
+ },
113
+ phases,
114
+ semantic_validators: [...SEMANTIC_VALIDATOR_IDS],
115
+ gate_artifact_coverage: gateArtifactCoverage,
116
+ };
117
+ }
118
+
119
+ export { buildWorkflowKitContract };
120
+
121
+ export function workflowKitDescribeCommand(opts) {
122
+ const context = loadProjectContext();
123
+ if (!context) {
124
+ console.log(chalk.red(' No agentxchain.json found. Run `agentxchain init` first.'));
125
+ process.exit(1);
126
+ }
127
+
128
+ const { root, config, version } = context;
129
+ if (version !== 4 || config.protocol_mode !== 'governed') {
130
+ console.log(chalk.red(' Not a governed AgentXchain project (requires v4 config).'));
131
+ process.exit(1);
132
+ }
133
+
134
+ const phaseIds = Object.keys(config.routing || {});
135
+ if (phaseIds.length === 0) {
136
+ console.log(chalk.red(' No governed phases are defined in routing.'));
137
+ process.exit(1);
138
+ }
139
+
140
+ const contract = buildWorkflowKitContract(root, config);
141
+
142
+ if (opts.json) {
143
+ console.log(JSON.stringify(contract, null, 2));
144
+ return;
145
+ }
146
+
147
+ console.log(chalk.bold(`\n Workflow Kit v${contract.workflow_kit_version}\n`));
148
+ console.log(` Source: ${contract.source}`);
149
+ console.log(` Templates: ${contract.phase_templates.available.length} available, ${contract.phase_templates.in_use.length} in use`);
150
+ if (contract.phase_templates.in_use.length > 0) {
151
+ console.log(` ${chalk.dim(contract.phase_templates.in_use.join(', '))}`);
152
+ }
153
+ console.log(` Validators: ${contract.semantic_validators.join(', ')}`);
154
+ console.log('');
155
+
156
+ for (const [phaseId, phase] of Object.entries(contract.phases)) {
157
+ const templateLabel = phase.template ? ` (${phase.template})` : '';
158
+ console.log(chalk.bold(` Phase: ${chalk.cyan(phaseId)}`) + chalk.dim(templateLabel));
159
+ console.log(` Source: ${phase.source}`);
160
+
161
+ if (phase.artifacts.length === 0) {
162
+ console.log(` ${chalk.dim('No artifacts declared')}`);
163
+ } else {
164
+ for (const artifact of phase.artifacts) {
165
+ const icon = artifact.exists
166
+ ? chalk.green('✓')
167
+ : artifact.required
168
+ ? chalk.red('✗')
169
+ : chalk.yellow('○');
170
+ const sem = artifact.semantics ? chalk.dim(` [${artifact.semantics}]`) : '';
171
+ const req = artifact.required ? '' : chalk.dim(' (optional)');
172
+ console.log(` ${icon} ${artifact.path}${sem}${req}`);
173
+ }
174
+ }
175
+ console.log('');
176
+ }
177
+
178
+ const gateEntries = Object.entries(contract.gate_artifact_coverage);
179
+ if (gateEntries.length > 0) {
180
+ console.log(chalk.bold(' Gate artifact coverage:'));
181
+ for (const [gateId, cov] of gateEntries) {
182
+ console.log(` ${gateId}: ${cov.predicates_referencing_artifacts} artifact(s) — ${cov.artifacts_covered.join(', ') || chalk.dim('none')}`);
183
+ }
184
+ console.log('');
185
+ }
186
+ }
@@ -0,0 +1,57 @@
1
+ import { buildRuntimeCapabilityReport } from './runtime-capabilities.js';
2
+
3
+ export const CONFIG_SCHEMA_ARTIFACT = 'agentxchain/schemas/agentxchain-config';
4
+ export const CAPABILITIES_OUTPUT_SCHEMA_ARTIFACT = 'agentxchain/schemas/connector-capabilities-output';
5
+
6
+ export function buildConnectorSchemaContract(rawConfig, normalizedConfig, runtimeId, roleId) {
7
+ const continuity = {
8
+ raw_config_runtime_present: Boolean(rawConfig?.runtimes?.[runtimeId]),
9
+ raw_role_present: Boolean(rawConfig?.roles?.[roleId]),
10
+ raw_role_binding_matches_runtime: false,
11
+ capabilities_report_runtime_matches: false,
12
+ capabilities_report_role_binding_matches: false,
13
+ };
14
+ const failures = [];
15
+
16
+ const rawRole = rawConfig?.roles?.[roleId];
17
+ if (continuity.raw_role_present) {
18
+ continuity.raw_role_binding_matches_runtime = rawRole?.runtime === runtimeId;
19
+ }
20
+
21
+ const report = buildRuntimeCapabilityReport(
22
+ runtimeId,
23
+ normalizedConfig?.runtimes?.[runtimeId],
24
+ normalizedConfig?.roles || {}
25
+ );
26
+ continuity.capabilities_report_runtime_matches = report.runtime_id === runtimeId;
27
+ continuity.capabilities_report_role_binding_matches = report.role_bindings.some(
28
+ (binding) => binding.role_id === roleId
29
+ );
30
+
31
+ if (!continuity.raw_config_runtime_present) {
32
+ failures.push(`Raw config does not define runtime "${runtimeId}".`);
33
+ }
34
+ if (!continuity.raw_role_present) {
35
+ failures.push(`Raw config does not define role "${roleId}".`);
36
+ } else if (!continuity.raw_role_binding_matches_runtime) {
37
+ failures.push(
38
+ `Raw role binding mismatch: roles.${roleId}.runtime is "${rawRole.runtime}" instead of "${runtimeId}".`
39
+ );
40
+ }
41
+ if (!continuity.capabilities_report_runtime_matches) {
42
+ failures.push(`Capability report emitted runtime "${report.runtime_id}" instead of "${runtimeId}".`);
43
+ }
44
+ if (!continuity.capabilities_report_role_binding_matches) {
45
+ failures.push(`Capability report omitted the "${roleId}" binding under runtime "${runtimeId}".`);
46
+ }
47
+
48
+ return {
49
+ ok: failures.length === 0,
50
+ config_schema_artifact: CONFIG_SCHEMA_ARTIFACT,
51
+ capabilities_output_schema_artifact: CAPABILITIES_OUTPUT_SCHEMA_ARTIFACT,
52
+ runtime_id: runtimeId,
53
+ role_id: roleId,
54
+ continuity,
55
+ failures,
56
+ };
57
+ }
@@ -23,6 +23,7 @@ import { dispatchRemoteAgent } from './adapters/remote-agent-adapter.js';
23
23
  import { getDispatchPromptPath, getTurnStagingResultPath } from './turn-paths.js';
24
24
  import { validateStagedTurnResult } from './turn-result-validator.js';
25
25
  import { probeRuntimeSpawnContext } from './runtime-spawn-context.js';
26
+ import { buildConnectorSchemaContract } from './connector-schema-contract.js';
26
27
 
27
28
  const VALIDATABLE_RUNTIME_TYPES = new Set(['local_cli', 'api_proxy', 'mcp', 'remote_agent']);
28
29
  const DEFAULT_VALIDATE_TIMEOUT_MS = 120_000;
@@ -106,12 +107,42 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
106
107
  const tempBase = mkdtempSync(join(tmpdir(), 'axc-connector-validate-'));
107
108
  const scratchRoot = join(tempBase, 'workspace');
108
109
  const warnings = [...roleSelection.warnings];
110
+ const schemaContract = buildConnectorSchemaContract(
111
+ sourceContext.rawConfig,
112
+ sourceContext.config,
113
+ runtimeId,
114
+ roleSelection.roleId
115
+ );
116
+
117
+ // Surface capability declaration warnings for self-declared connectors
118
+ const { getCapabilityDeclarationWarnings } = await import('./runtime-capabilities.js');
119
+ const capWarnings = getCapabilityDeclarationWarnings(runtime);
120
+ warnings.push(...capWarnings);
121
+
109
122
  let keepArtifacts = options.keepArtifacts === true;
110
123
  let dispatch = null;
111
124
  let validation = null;
112
125
  let costUsd = null;
113
126
 
114
127
  try {
128
+ if (!schemaContract.ok) {
129
+ return {
130
+ ok: false,
131
+ exitCode: 1,
132
+ overall: 'fail',
133
+ runtime_id: runtimeId,
134
+ runtime_type: runtime.type,
135
+ role_id: roleSelection.roleId,
136
+ timeout_ms: timeoutMs,
137
+ warnings,
138
+ schema_contract: schemaContract,
139
+ dispatch: null,
140
+ validation: null,
141
+ error: 'Schema contract continuity failed before synthetic dispatch.',
142
+ scratch_root: null,
143
+ };
144
+ }
145
+
115
146
  copyRepoForValidation(sourceRoot, scratchRoot);
116
147
  initializeScratchGit(scratchRoot);
117
148
 
@@ -126,6 +157,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
126
157
  role_id: roleSelection.roleId,
127
158
  timeout_ms: timeoutMs,
128
159
  warnings,
160
+ schema_contract: schemaContract,
129
161
  error: 'Failed to load governed config inside scratch workspace.',
130
162
  scratch_root: scratchRoot,
131
163
  };
@@ -143,6 +175,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
143
175
  role_id: roleSelection.roleId,
144
176
  timeout_ms: timeoutMs,
145
177
  warnings,
178
+ schema_contract: schemaContract,
146
179
  dispatch: null,
147
180
  validation: null,
148
181
  error: spawnProbe.detail,
@@ -168,6 +201,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
168
201
  role_id: roleSelection.roleId,
169
202
  timeout_ms: timeoutMs,
170
203
  warnings,
204
+ schema_contract: schemaContract,
171
205
  error: initResult.error,
172
206
  scratch_root: scratchRoot,
173
207
  };
@@ -184,6 +218,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
184
218
  role_id: roleSelection.roleId,
185
219
  timeout_ms: timeoutMs,
186
220
  warnings,
221
+ schema_contract: schemaContract,
187
222
  error: assignResult.error,
188
223
  scratch_root: scratchRoot,
189
224
  };
@@ -201,6 +236,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
201
236
  role_id: roleSelection.roleId,
202
237
  timeout_ms: timeoutMs,
203
238
  warnings,
239
+ schema_contract: schemaContract,
204
240
  error: 'Synthetic validation turn was not assigned.',
205
241
  scratch_root: scratchRoot,
206
242
  };
@@ -217,6 +253,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
217
253
  role_id: roleSelection.roleId,
218
254
  timeout_ms: timeoutMs,
219
255
  warnings,
256
+ schema_contract: schemaContract,
220
257
  error: bundleResult.error,
221
258
  scratch_root: scratchRoot,
222
259
  };
@@ -260,6 +297,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
260
297
  role_id: roleSelection.roleId,
261
298
  timeout_ms: timeoutMs,
262
299
  warnings,
300
+ schema_contract: schemaContract,
263
301
  dispatch,
264
302
  validation: null,
265
303
  scratch_root: scratchRoot,
@@ -282,6 +320,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
282
320
  role_id: roleSelection.roleId,
283
321
  timeout_ms: timeoutMs,
284
322
  warnings,
323
+ schema_contract: schemaContract,
285
324
  dispatch,
286
325
  validation: {
287
326
  ok: false,
@@ -304,6 +343,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
304
343
  role_id: roleSelection.roleId,
305
344
  timeout_ms: timeoutMs,
306
345
  warnings,
346
+ schema_contract: schemaContract,
307
347
  dispatch,
308
348
  validation: {
309
349
  ok: true,
@@ -326,6 +366,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
326
366
  role_id: roleSelection.roleId,
327
367
  timeout_ms: timeoutMs,
328
368
  warnings,
369
+ schema_contract: schemaContract,
329
370
  dispatch,
330
371
  validation,
331
372
  error: error.message,