agentxchain 2.136.0 → 2.137.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.
@@ -66,6 +66,7 @@ import { kickoffCommand } from '../src/commands/kickoff.js';
66
66
  import { rebindCommand } from '../src/commands/rebind.js';
67
67
  import { branchCommand } from '../src/commands/branch.js';
68
68
  import { migrateCommand } from '../src/commands/migrate.js';
69
+ import { migrateIntentsCommand } from '../src/commands/migrate-intents.js';
69
70
  import { resumeCommand } from '../src/commands/resume.js';
70
71
  import { unblockCommand } from '../src/commands/unblock.js';
71
72
  import { injectCommand } from '../src/commands/inject.js';
@@ -127,6 +128,7 @@ import { connectorCapabilitiesCommand, connectorCheckCommand, connectorValidateC
127
128
  import { scheduleDaemonCommand, scheduleListCommand, scheduleRunDueCommand, scheduleStatusCommand } from '../src/commands/schedule.js';
128
129
  import { chainLatestCommand, chainListCommand, chainShowCommand } from '../src/commands/chain.js';
129
130
  import { missionAttachChainCommand, missionBindCoordinatorCommand, missionListCommand, missionPlanApproveCommand, missionPlanAutopilotCommand, missionPlanCommand, missionPlanLaunchCommand, missionPlanListCommand, missionPlanShowCommand, missionShowCommand, missionStartCommand } from '../src/commands/mission.js';
131
+ import { workflowKitDescribeCommand } from '../src/commands/workflow-kit.js';
130
132
 
131
133
  const __dirname = dirname(fileURLToPath(import.meta.url));
132
134
  const pkg = JSON.parse(readFileSync(join(__dirname, '..', 'package.json'), 'utf8'));
@@ -644,6 +646,13 @@ program
644
646
  .option('-j, --json', 'Output migration report as JSON')
645
647
  .action(migrateCommand);
646
648
 
649
+ program
650
+ .command('migrate-intents')
651
+ .description('Archive legacy intents with no run scope (pre-BUG-34 repair)')
652
+ .option('-j, --json', 'Output as JSON')
653
+ .option('--dry-run', 'List legacy intents without modifying them')
654
+ .action(migrateIntentsCommand);
655
+
647
656
  program
648
657
  .command('resume')
649
658
  .description('Resume a governed project: initialize or continue a run and assign the next turn')
@@ -846,6 +855,16 @@ phaseCmd
846
855
  .option('-j, --json', 'Output as JSON')
847
856
  .action((phaseId, opts) => phaseCommand('show', phaseId, opts));
848
857
 
858
+ const workflowKitCmd = program
859
+ .command('workflow-kit')
860
+ .description('Inspect the workflow kit contract for this project');
861
+
862
+ workflowKitCmd
863
+ .command('describe')
864
+ .description('Show the workflow kit contract — templates, artifacts, semantic validators, gate coverage')
865
+ .option('-j, --json', 'Output as JSON')
866
+ .action(workflowKitDescribeCommand);
867
+
849
868
  const gateCmd = program
850
869
  .command('gate')
851
870
  .description('Inspect governed gate definitions');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "agentxchain",
3
- "version": "2.136.0",
3
+ "version": "2.137.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -3,7 +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 { getRuntimeCapabilityContract, getRoleRuntimeCapabilityContract, getCapabilityDeclarationWarnings } from '../lib/runtime-capabilities.js';
6
+ import { buildRuntimeCapabilityReport } from '../lib/runtime-capabilities.js';
7
7
 
8
8
  function printJson(result, exitCode) {
9
9
  console.log(JSON.stringify(result, null, 2));
@@ -165,6 +165,14 @@ function printValidateText(result, exitCode) {
165
165
  }
166
166
  }
167
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
+
168
176
  if (result.dispatch) {
169
177
  console.log('');
170
178
  console.log(` ${chalk.dim('Dispatch:')} ${result.dispatch.ok ? chalk.green('ok') : chalk.red('failed')}`);
@@ -193,37 +201,6 @@ function printValidateText(result, exitCode) {
193
201
  process.exit(exitCode);
194
202
  }
195
203
 
196
- function buildCapabilityReport(runtimeId, runtime, roles) {
197
- const mergedContract = getRuntimeCapabilityContract(runtime);
198
- const declaredCapabilities = (runtime.capabilities && typeof runtime.capabilities === 'object' && !Array.isArray(runtime.capabilities))
199
- ? { ...runtime.capabilities }
200
- : {};
201
- const declarationWarnings = getCapabilityDeclarationWarnings(runtime);
202
-
203
- const roleBindings = [];
204
- for (const [roleId, role] of Object.entries(roles)) {
205
- const boundRuntime = role.runtime_id || role.runtime;
206
- if (boundRuntime !== runtimeId) continue;
207
- const roleContract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
208
- roleBindings.push({
209
- role_id: roleContract.role_id,
210
- role_write_authority: roleContract.role_write_authority,
211
- effective_write_path: roleContract.effective_write_path,
212
- workflow_artifact_ownership: roleContract.workflow_artifact_ownership,
213
- notes: roleContract.notes,
214
- });
215
- }
216
-
217
- return {
218
- runtime_id: runtimeId,
219
- runtime_type: runtime.type || 'unknown',
220
- declared_capabilities: declaredCapabilities,
221
- merged_contract: mergedContract,
222
- declaration_warnings: declarationWarnings,
223
- role_bindings: roleBindings,
224
- };
225
- }
226
-
227
204
  function printCapabilitiesText(report) {
228
205
  console.log('');
229
206
  console.log(chalk.bold(` ${report.runtime_id}`) + chalk.dim(` (${report.runtime_type})`));
@@ -280,7 +257,7 @@ export async function connectorCapabilitiesCommand(runtimeId, options = {}) {
280
257
  if (options.all) {
281
258
  const reports = [];
282
259
  for (const [id, runtime] of Object.entries(runtimes)) {
283
- reports.push(buildCapabilityReport(id, runtime, roles));
260
+ reports.push(buildRuntimeCapabilityReport(id, runtime, roles));
284
261
  }
285
262
  const payload = { runtimes: reports };
286
263
  if (options.json) { printJson(payload, 0); return; }
@@ -317,7 +294,7 @@ export async function connectorCapabilitiesCommand(runtimeId, options = {}) {
317
294
  process.exit(2);
318
295
  }
319
296
 
320
- const report = buildCapabilityReport(runtimeId, runtimes[runtimeId], roles);
297
+ const report = buildRuntimeCapabilityReport(runtimeId, runtimes[runtimeId], roles);
321
298
  if (options.json) { printJson(report, 0); return; }
322
299
 
323
300
  console.log('');
@@ -0,0 +1,121 @@
1
+ /**
2
+ * One-shot repair command for legacy intents stuck with approved_run_id: null.
3
+ *
4
+ * Belt-and-suspenders insurance for BUG-41: the automatic startup migration
5
+ * is now idempotent, but operators who already have stuck repos need a direct
6
+ * lever that works without starting a governed run.
7
+ */
8
+
9
+ import { existsSync, readFileSync, readdirSync } from 'node:fs';
10
+ import { join } from 'node:path';
11
+ import chalk from 'chalk';
12
+
13
+ import { findProjectRoot } from '../lib/config.js';
14
+ import { migratePreBug34Intents } from '../lib/intent-startup-migration.js';
15
+
16
+ function loadRunId(root) {
17
+ const statePath = join(root, '.agentxchain', 'state.json');
18
+ if (!existsSync(statePath)) return 'manual-migration';
19
+ try {
20
+ const state = JSON.parse(readFileSync(statePath, 'utf8'));
21
+ return state.run_id || 'manual-migration';
22
+ } catch {
23
+ return 'manual-migration';
24
+ }
25
+ }
26
+
27
+ function listLegacyIntents(root) {
28
+ const intentsDir = join(root, '.agentxchain', 'intake', 'intents');
29
+ if (!existsSync(intentsDir)) return [];
30
+
31
+ const DISPATCHABLE = new Set(['planned', 'approved']);
32
+ const results = [];
33
+
34
+ for (const file of readdirSync(intentsDir)) {
35
+ if (!file.endsWith('.json') || file.startsWith('.tmp-')) continue;
36
+ const intentPath = join(intentsDir, file);
37
+ let intent;
38
+ try {
39
+ intent = JSON.parse(readFileSync(intentPath, 'utf8'));
40
+ } catch {
41
+ continue;
42
+ }
43
+ if (!intent || !DISPATCHABLE.has(intent.status)) continue;
44
+ if (intent.cross_run_durable === true) continue;
45
+ if (intent.approved_run_id) continue;
46
+ results.push({ file, intent_id: intent.intent_id || file.replace('.json', ''), status: intent.status });
47
+ }
48
+ return results;
49
+ }
50
+
51
+ export function migrateIntentsCommand(opts) {
52
+ const root = findProjectRoot();
53
+ if (!root) {
54
+ if (opts.json) {
55
+ console.log(JSON.stringify({ error: 'No agentxchain.json found. Run from inside a governed project.' }));
56
+ } else {
57
+ console.error(chalk.red(' No agentxchain.json found. Run this from inside a governed project.'));
58
+ }
59
+ process.exit(1);
60
+ }
61
+
62
+ const legacyIntents = listLegacyIntents(root);
63
+
64
+ if (opts.dryRun) {
65
+ if (opts.json) {
66
+ console.log(JSON.stringify({
67
+ archived_count: legacyIntents.length,
68
+ archived_intent_ids: legacyIntents.map(i => i.intent_id),
69
+ dry_run: true,
70
+ message: legacyIntents.length > 0
71
+ ? `Would archive ${legacyIntents.length} pre-BUG-34 intent(s)`
72
+ : 'No legacy intents found',
73
+ }, null, 2));
74
+ } else {
75
+ if (legacyIntents.length === 0) {
76
+ console.log(chalk.green(' No legacy intents found. Nothing to migrate.'));
77
+ } else {
78
+ console.log(chalk.yellow(` Would archive ${legacyIntents.length} legacy intent(s):`));
79
+ for (const item of legacyIntents) {
80
+ console.log(` ${chalk.dim('•')} ${item.intent_id} (${item.status})`);
81
+ }
82
+ }
83
+ }
84
+ return;
85
+ }
86
+
87
+ if (legacyIntents.length === 0) {
88
+ if (opts.json) {
89
+ console.log(JSON.stringify({
90
+ archived_count: 0,
91
+ archived_intent_ids: [],
92
+ dry_run: false,
93
+ message: 'No legacy intents found',
94
+ }, null, 2));
95
+ } else {
96
+ console.log(chalk.green(' No legacy intents found. Nothing to migrate.'));
97
+ }
98
+ return;
99
+ }
100
+
101
+ const runId = loadRunId(root);
102
+ const result = migratePreBug34Intents(root, runId);
103
+
104
+ if (opts.json) {
105
+ console.log(JSON.stringify({
106
+ archived_count: result.archived_migration_count,
107
+ archived_intent_ids: result.archived_migration_intent_ids,
108
+ dry_run: false,
109
+ message: result.migration_notice || `Archived ${result.archived_migration_count} pre-BUG-34 intent(s)`,
110
+ }, null, 2));
111
+ } else {
112
+ if (result.archived_migration_count === 0) {
113
+ console.log(chalk.green(' No legacy intents found. Nothing to migrate.'));
114
+ } else {
115
+ console.log(chalk.green(` ✓ ${result.migration_notice}`));
116
+ for (const id of result.archived_migration_intent_ids) {
117
+ console.log(` ${chalk.dim('•')} ${id}`);
118
+ }
119
+ }
120
+ }
121
+ }
@@ -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,6 +107,12 @@ 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
+ );
109
116
 
110
117
  // Surface capability declaration warnings for self-declared connectors
111
118
  const { getCapabilityDeclarationWarnings } = await import('./runtime-capabilities.js');
@@ -118,6 +125,24 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
118
125
  let costUsd = null;
119
126
 
120
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
+
121
146
  copyRepoForValidation(sourceRoot, scratchRoot);
122
147
  initializeScratchGit(scratchRoot);
123
148
 
@@ -132,6 +157,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
132
157
  role_id: roleSelection.roleId,
133
158
  timeout_ms: timeoutMs,
134
159
  warnings,
160
+ schema_contract: schemaContract,
135
161
  error: 'Failed to load governed config inside scratch workspace.',
136
162
  scratch_root: scratchRoot,
137
163
  };
@@ -149,6 +175,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
149
175
  role_id: roleSelection.roleId,
150
176
  timeout_ms: timeoutMs,
151
177
  warnings,
178
+ schema_contract: schemaContract,
152
179
  dispatch: null,
153
180
  validation: null,
154
181
  error: spawnProbe.detail,
@@ -174,6 +201,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
174
201
  role_id: roleSelection.roleId,
175
202
  timeout_ms: timeoutMs,
176
203
  warnings,
204
+ schema_contract: schemaContract,
177
205
  error: initResult.error,
178
206
  scratch_root: scratchRoot,
179
207
  };
@@ -190,6 +218,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
190
218
  role_id: roleSelection.roleId,
191
219
  timeout_ms: timeoutMs,
192
220
  warnings,
221
+ schema_contract: schemaContract,
193
222
  error: assignResult.error,
194
223
  scratch_root: scratchRoot,
195
224
  };
@@ -207,6 +236,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
207
236
  role_id: roleSelection.roleId,
208
237
  timeout_ms: timeoutMs,
209
238
  warnings,
239
+ schema_contract: schemaContract,
210
240
  error: 'Synthetic validation turn was not assigned.',
211
241
  scratch_root: scratchRoot,
212
242
  };
@@ -223,6 +253,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
223
253
  role_id: roleSelection.roleId,
224
254
  timeout_ms: timeoutMs,
225
255
  warnings,
256
+ schema_contract: schemaContract,
226
257
  error: bundleResult.error,
227
258
  scratch_root: scratchRoot,
228
259
  };
@@ -266,6 +297,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
266
297
  role_id: roleSelection.roleId,
267
298
  timeout_ms: timeoutMs,
268
299
  warnings,
300
+ schema_contract: schemaContract,
269
301
  dispatch,
270
302
  validation: null,
271
303
  scratch_root: scratchRoot,
@@ -288,6 +320,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
288
320
  role_id: roleSelection.roleId,
289
321
  timeout_ms: timeoutMs,
290
322
  warnings,
323
+ schema_contract: schemaContract,
291
324
  dispatch,
292
325
  validation: {
293
326
  ok: false,
@@ -310,6 +343,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
310
343
  role_id: roleSelection.roleId,
311
344
  timeout_ms: timeoutMs,
312
345
  warnings,
346
+ schema_contract: schemaContract,
313
347
  dispatch,
314
348
  validation: {
315
349
  ok: true,
@@ -332,6 +366,7 @@ export async function validateConfiguredConnector(sourceRoot, options = {}) {
332
366
  role_id: roleSelection.roleId,
333
367
  timeout_ms: timeoutMs,
334
368
  warnings,
369
+ schema_contract: schemaContract,
335
370
  dispatch,
336
371
  validation,
337
372
  error: error.message,
@@ -154,7 +154,7 @@ function reconcileContinuousStartupState(context, session, contOpts, log) {
154
154
  sessionChanged = true;
155
155
  }
156
156
 
157
- if (scopedRunId && session.startup_reconciled_run_id !== scopedRunId) {
157
+ if (scopedRunId) {
158
158
  const startupIntents = archiveStaleIntentsForRun(root, scopedRunId, {
159
159
  protocolVersion: governedState?.protocol_version || config?.schema_version || '2.x',
160
160
  });
@@ -172,8 +172,10 @@ function reconcileContinuousStartupState(context, session, contOpts, log) {
172
172
  const migrationNotice = formatLegacyIntentMigrationNotice(startupIntents.archived_migration_intent_ids);
173
173
  if (migrationNotice) log(migrationNotice);
174
174
  }
175
- session.startup_reconciled_run_id = scopedRunId;
176
- sessionChanged = true;
175
+ if (session.startup_reconciled_run_id !== scopedRunId) {
176
+ session.startup_reconciled_run_id = scopedRunId;
177
+ sessionChanged = true;
178
+ }
177
179
  }
178
180
 
179
181
  if (sessionChanged) {
@@ -240,3 +240,34 @@ export function summarizeRoleRuntimeCapability(contract) {
240
240
  `owned_by=${contract.workflow_artifact_ownership}`,
241
241
  ].join('; ');
242
242
  }
243
+
244
+ export function buildRuntimeCapabilityReport(runtimeId, runtime, roles) {
245
+ const mergedContract = getRuntimeCapabilityContract(runtime);
246
+ const declaredCapabilities = (runtime?.capabilities && typeof runtime.capabilities === 'object' && !Array.isArray(runtime.capabilities))
247
+ ? { ...runtime.capabilities }
248
+ : {};
249
+ const declarationWarnings = getCapabilityDeclarationWarnings(runtime);
250
+
251
+ const roleBindings = [];
252
+ for (const [roleId, role] of Object.entries(roles || {})) {
253
+ const boundRuntime = role.runtime_id || role.runtime;
254
+ if (boundRuntime !== runtimeId) continue;
255
+ const roleContract = getRoleRuntimeCapabilityContract(roleId, role, runtime);
256
+ roleBindings.push({
257
+ role_id: roleContract.role_id,
258
+ role_write_authority: roleContract.role_write_authority,
259
+ effective_write_path: roleContract.effective_write_path,
260
+ workflow_artifact_ownership: roleContract.workflow_artifact_ownership,
261
+ notes: roleContract.notes,
262
+ });
263
+ }
264
+
265
+ return {
266
+ runtime_id: runtimeId,
267
+ runtime_type: runtime?.type || 'unknown',
268
+ declared_capabilities: declaredCapabilities,
269
+ merged_contract: mergedContract,
270
+ declaration_warnings: declarationWarnings,
271
+ role_bindings: roleBindings,
272
+ };
273
+ }