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.
- package/bin/agentxchain.js +19 -0
- package/package.json +1 -1
- package/src/commands/connector.js +11 -34
- package/src/commands/migrate-intents.js +121 -0
- package/src/commands/workflow-kit.js +186 -0
- package/src/lib/connector-schema-contract.js +57 -0
- package/src/lib/connector-validate.js +35 -0
- package/src/lib/continuous-run.js +5 -3
- package/src/lib/runtime-capabilities.js +31 -0
package/bin/agentxchain.js
CHANGED
|
@@ -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
|
@@ -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 {
|
|
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(
|
|
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 =
|
|
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
|
|
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
|
|
176
|
-
|
|
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
|
+
}
|