agentxchain 2.3.0 → 2.4.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 +1 -0
- package/bin/agentxchain.js +9 -2
- package/package.json +4 -2
- package/src/commands/init.js +1 -0
- package/src/commands/step.js +56 -2
- package/src/commands/template-validate.js +159 -0
- package/src/lib/adapters/mcp-adapter.js +306 -0
- package/src/lib/governed-templates.js +236 -1
- package/src/lib/normalized-config.js +34 -1
- package/src/lib/repo-observer.js +8 -8
- package/src/lib/validation.js +23 -0
- package/src/templates/governed/library.json +31 -0
package/README.md
CHANGED
|
@@ -67,6 +67,7 @@ Built-in governed templates:
|
|
|
67
67
|
- `generic`: baseline governed scaffold
|
|
68
68
|
- `api-service`: API contract, operational readiness, error budget
|
|
69
69
|
- `cli-tool`: command surface, platform support, distribution checklist
|
|
70
|
+
- `library`: public API, compatibility policy, release and adoption checklist
|
|
70
71
|
- `web-app`: user flows, UI acceptance, browser support
|
|
71
72
|
|
|
72
73
|
`step` writes a turn-scoped bundle under `.agentxchain/dispatch/turns/<turn_id>/` and expects a staged result at `.agentxchain/staging/<turn_id>/turn-result.json`. Typical continuation:
|
package/bin/agentxchain.js
CHANGED
|
@@ -79,6 +79,7 @@ import {
|
|
|
79
79
|
} from '../src/commands/plugin.js';
|
|
80
80
|
import { templateSetCommand } from '../src/commands/template-set.js';
|
|
81
81
|
import { templateListCommand } from '../src/commands/template-list.js';
|
|
82
|
+
import { templateValidateCommand } from '../src/commands/template-validate.js';
|
|
82
83
|
import {
|
|
83
84
|
multiInitCommand,
|
|
84
85
|
multiStatusCommand,
|
|
@@ -110,7 +111,7 @@ program
|
|
|
110
111
|
.description('Create a new AgentXchain project folder')
|
|
111
112
|
.option('-y, --yes', 'Skip prompts, use defaults')
|
|
112
113
|
.option('--governed', 'Create a governed project (orchestrator-owned state)')
|
|
113
|
-
.option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, web-app')
|
|
114
|
+
.option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app')
|
|
114
115
|
.option('--schema-version <version>', 'Schema version (3 for legacy, or use --governed for current)')
|
|
115
116
|
.action(initCommand);
|
|
116
117
|
|
|
@@ -336,6 +337,12 @@ templateCmd
|
|
|
336
337
|
.option('-j, --json', 'Output as JSON')
|
|
337
338
|
.action(templateListCommand);
|
|
338
339
|
|
|
340
|
+
templateCmd
|
|
341
|
+
.command('validate')
|
|
342
|
+
.description('Validate the built-in governed template registry and current project template binding')
|
|
343
|
+
.option('-j, --json', 'Output as JSON')
|
|
344
|
+
.action(templateValidateCommand);
|
|
345
|
+
|
|
339
346
|
const multiCmd = program
|
|
340
347
|
.command('multi')
|
|
341
348
|
.description('Multi-repo coordinator orchestration');
|
|
@@ -394,7 +401,7 @@ intakeCmd
|
|
|
394
401
|
.description('Triage a detected intent — set priority, template, charter, and acceptance')
|
|
395
402
|
.requiredOption('--intent <id>', 'Intent ID to triage')
|
|
396
403
|
.option('--priority <level>', 'Priority level (p0, p1, p2, p3)')
|
|
397
|
-
.option('--template <id>', 'Governed template (generic, api-service, cli-tool, web-app)')
|
|
404
|
+
.option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app)')
|
|
398
405
|
.option('--charter <text>', 'Delivery charter text')
|
|
399
406
|
.option('--acceptance <text>', 'Comma-separated acceptance criteria')
|
|
400
407
|
.option('--suppress', 'Suppress the intent instead of triaging')
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "agentxchain",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.4.0",
|
|
4
4
|
"description": "CLI for AgentXchain — governed multi-agent software delivery",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -48,10 +48,12 @@
|
|
|
48
48
|
"homepage": "https://agentxchain.dev",
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@anthropic-ai/tokenizer": "0.0.4",
|
|
51
|
+
"@modelcontextprotocol/sdk": "^1.29.0",
|
|
51
52
|
"chalk": "^5.4.0",
|
|
52
53
|
"commander": "^13.0.0",
|
|
53
54
|
"inquirer": "^12.0.0",
|
|
54
|
-
"ora": "^8.0.0"
|
|
55
|
+
"ora": "^8.0.0",
|
|
56
|
+
"zod": "^4.3.6"
|
|
55
57
|
},
|
|
56
58
|
"engines": {
|
|
57
59
|
"node": ">=18.17.0 || >=20.5.0"
|
package/src/commands/init.js
CHANGED
|
@@ -483,6 +483,7 @@ async function initGoverned(opts) {
|
|
|
483
483
|
console.error(' generic Default governed scaffold');
|
|
484
484
|
console.error(' api-service Governed scaffold for a backend service');
|
|
485
485
|
console.error(' cli-tool Governed scaffold for a CLI tool');
|
|
486
|
+
console.error(' library Governed scaffold for a reusable package');
|
|
486
487
|
console.error(' web-app Governed scaffold for a web application');
|
|
487
488
|
process.exit(1);
|
|
488
489
|
}
|
package/src/commands/step.js
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
* - local_cli: implemented via subprocess dispatch + staged turn result
|
|
18
18
|
* - api_proxy: implemented for synchronous review-only turns and stages
|
|
19
19
|
* provider-backed JSON before validation/acceptance
|
|
20
|
+
* - mcp: implemented for synchronous MCP stdio tool dispatch
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
23
|
import chalk from 'chalk';
|
|
@@ -45,6 +46,7 @@ import {
|
|
|
45
46
|
saveDispatchLogs,
|
|
46
47
|
resolvePromptTransport,
|
|
47
48
|
} from '../lib/adapters/local-cli-adapter.js';
|
|
49
|
+
import { dispatchMcp } from '../lib/adapters/mcp-adapter.js';
|
|
48
50
|
import {
|
|
49
51
|
getDispatchAssignmentPath,
|
|
50
52
|
getDispatchContextPath,
|
|
@@ -423,12 +425,64 @@ export async function stepCommand(opts) {
|
|
|
423
425
|
console.log(chalk.dim(` Tokens: ${apiResult.usage.input_tokens || 0} in / ${apiResult.usage.output_tokens || 0} out`));
|
|
424
426
|
}
|
|
425
427
|
console.log('');
|
|
428
|
+
} else if (runtimeType === 'mcp') {
|
|
429
|
+
console.log(chalk.cyan(`Dispatching to MCP stdio: ${runtime?.command || '(unknown)'}`));
|
|
430
|
+
console.log(chalk.dim(`Turn: ${turn.turn_id} Role: ${roleId} Phase: ${state.phase} Tool: ${runtime?.tool_name || 'agentxchain_turn'}`));
|
|
431
|
+
|
|
432
|
+
const mcpResult = await dispatchMcp(root, state, config, {
|
|
433
|
+
signal: controller.signal,
|
|
434
|
+
onStatus: (msg) => console.log(chalk.dim(` ${msg}`)),
|
|
435
|
+
onStderr: opts.verbose ? (text) => process.stderr.write(chalk.yellow(text)) : undefined,
|
|
436
|
+
verifyManifest: true,
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
if (mcpResult.logs?.length) {
|
|
440
|
+
saveDispatchLogs(root, turn.turn_id, mcpResult.logs);
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
if (mcpResult.aborted) {
|
|
444
|
+
console.log('');
|
|
445
|
+
console.log(chalk.yellow('Aborted. Turn remains assigned.'));
|
|
446
|
+
console.log(chalk.dim('Resume later with: agentxchain step --resume'));
|
|
447
|
+
console.log(chalk.dim('Or accept/reject manually: agentxchain accept-turn / reject-turn'));
|
|
448
|
+
process.exit(0);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (!mcpResult.ok) {
|
|
452
|
+
const blocked = markRunBlocked(root, {
|
|
453
|
+
blockedOn: 'dispatch:mcp_failure',
|
|
454
|
+
category: 'dispatch_error',
|
|
455
|
+
recovery: {
|
|
456
|
+
typed_reason: 'dispatch_error',
|
|
457
|
+
owner: 'human',
|
|
458
|
+
recovery_action: 'Resolve the MCP dispatch issue, then run agentxchain step --resume',
|
|
459
|
+
turn_retained: true,
|
|
460
|
+
detail: mcpResult.error,
|
|
461
|
+
},
|
|
462
|
+
turnId: turn.turn_id,
|
|
463
|
+
hooksConfig,
|
|
464
|
+
});
|
|
465
|
+
if (blocked.ok) {
|
|
466
|
+
state = blocked.state;
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
console.log('');
|
|
470
|
+
console.log(chalk.red(`MCP dispatch failed: ${mcpResult.error}`));
|
|
471
|
+
console.log(chalk.dim('The turn remains assigned. You can:'));
|
|
472
|
+
console.log(chalk.dim(' - Fix the MCP server/runtime and retry: agentxchain step --resume'));
|
|
473
|
+
console.log(chalk.dim(' - Complete manually: edit .agentxchain/staging/turn-result.json'));
|
|
474
|
+
console.log(chalk.dim(' - Reject: agentxchain reject-turn --reason "mcp dispatch failed"'));
|
|
475
|
+
process.exit(1);
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
console.log(chalk.green(`MCP tool completed${mcpResult.toolName ? ` (${mcpResult.toolName})` : ''}. Staged result detected.`));
|
|
479
|
+
console.log('');
|
|
426
480
|
}
|
|
427
481
|
|
|
428
482
|
// ── Phase 3: Wait for turn completion ─────────────────────────────────────
|
|
429
483
|
|
|
430
|
-
if (runtimeType === 'api_proxy') {
|
|
431
|
-
// api_proxy
|
|
484
|
+
if (runtimeType === 'api_proxy' || runtimeType === 'mcp') {
|
|
485
|
+
// api_proxy and mcp are synchronous — result already staged in Phase 2
|
|
432
486
|
} else if (runtimeType === 'local_cli') {
|
|
433
487
|
// ── Local CLI adapter: spawn subprocess ──
|
|
434
488
|
const transport = runtime ? resolvePromptTransport(runtime) : 'dispatch_bundle_only';
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { join } from 'node:path';
|
|
3
|
+
import chalk from 'chalk';
|
|
4
|
+
import { CONFIG_FILE, findProjectRoot } from '../lib/config.js';
|
|
5
|
+
import {
|
|
6
|
+
validateGovernedProjectTemplate,
|
|
7
|
+
validateGovernedTemplateRegistry,
|
|
8
|
+
validateProjectPlanningArtifacts,
|
|
9
|
+
validateAcceptanceHintCompletion,
|
|
10
|
+
} from '../lib/governed-templates.js';
|
|
11
|
+
|
|
12
|
+
function loadProjectTemplateValidation() {
|
|
13
|
+
const root = findProjectRoot();
|
|
14
|
+
if (!root) {
|
|
15
|
+
return {
|
|
16
|
+
present: false,
|
|
17
|
+
root: null,
|
|
18
|
+
template: null,
|
|
19
|
+
source: null,
|
|
20
|
+
ok: true,
|
|
21
|
+
errors: [],
|
|
22
|
+
warnings: [],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const configPath = join(root, CONFIG_FILE);
|
|
27
|
+
let parsed;
|
|
28
|
+
try {
|
|
29
|
+
parsed = JSON.parse(readFileSync(configPath, 'utf8'));
|
|
30
|
+
} catch (err) {
|
|
31
|
+
return {
|
|
32
|
+
present: true,
|
|
33
|
+
root,
|
|
34
|
+
template: null,
|
|
35
|
+
source: 'agentxchain.json',
|
|
36
|
+
ok: false,
|
|
37
|
+
errors: [`Failed to parse ${CONFIG_FILE}: ${err.message}`],
|
|
38
|
+
warnings: [],
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const projectValidation = validateGovernedProjectTemplate(parsed.template);
|
|
43
|
+
return {
|
|
44
|
+
present: true,
|
|
45
|
+
root,
|
|
46
|
+
...projectValidation,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function templateValidateCommand(opts = {}) {
|
|
51
|
+
const registry = validateGovernedTemplateRegistry();
|
|
52
|
+
const project = loadProjectTemplateValidation();
|
|
53
|
+
|
|
54
|
+
// Planning artifact completeness check
|
|
55
|
+
let planningArtifacts = null;
|
|
56
|
+
if (project.present && project.ok && project.root) {
|
|
57
|
+
planningArtifacts = validateProjectPlanningArtifacts(project.root, project.template);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Acceptance hint completion check
|
|
61
|
+
let acceptanceHints = null;
|
|
62
|
+
if (project.present && project.ok && project.root) {
|
|
63
|
+
acceptanceHints = validateAcceptanceHintCompletion(project.root, project.template);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const errors = [
|
|
67
|
+
...registry.errors,
|
|
68
|
+
...project.errors,
|
|
69
|
+
...(planningArtifacts?.errors || []),
|
|
70
|
+
];
|
|
71
|
+
const warnings = [
|
|
72
|
+
...registry.warnings,
|
|
73
|
+
...project.warnings,
|
|
74
|
+
...(planningArtifacts?.warnings || []),
|
|
75
|
+
...(acceptanceHints?.warnings || []),
|
|
76
|
+
];
|
|
77
|
+
const ok = errors.length === 0;
|
|
78
|
+
|
|
79
|
+
const payload = {
|
|
80
|
+
ok,
|
|
81
|
+
registry,
|
|
82
|
+
project,
|
|
83
|
+
planning_artifacts: planningArtifacts,
|
|
84
|
+
acceptance_hints: acceptanceHints,
|
|
85
|
+
errors,
|
|
86
|
+
warnings,
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
if (opts.json) {
|
|
90
|
+
console.log(JSON.stringify(payload, null, 2));
|
|
91
|
+
if (!ok) process.exit(1);
|
|
92
|
+
return;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
console.log('');
|
|
96
|
+
console.log(chalk.bold(' AgentXchain Template Validate'));
|
|
97
|
+
console.log(chalk.dim(' ' + '─'.repeat(44)));
|
|
98
|
+
console.log('');
|
|
99
|
+
|
|
100
|
+
if (ok) {
|
|
101
|
+
console.log(chalk.green(' ✓ Template validation passed.'));
|
|
102
|
+
} else {
|
|
103
|
+
console.log(chalk.red(` ✗ Template validation failed (${errors.length} errors).`));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
console.log('');
|
|
107
|
+
console.log(` ${chalk.dim('Registry:')} ${registry.ok ? chalk.green('OK') : chalk.red('FAIL')} (${registry.registered_ids.length} registered, ${registry.manifest_ids.length} manifests)`);
|
|
108
|
+
|
|
109
|
+
if (project.present) {
|
|
110
|
+
const sourceLabel = project.source === 'implicit_default'
|
|
111
|
+
? 'implicit default'
|
|
112
|
+
: project.source;
|
|
113
|
+
console.log(` ${chalk.dim('Project:')} ${project.ok ? chalk.green('OK') : chalk.red('FAIL')} (${project.template} via ${sourceLabel})`);
|
|
114
|
+
if (project.root && existsSync(project.root)) {
|
|
115
|
+
console.log(` ${chalk.dim('Root:')} ${project.root}`);
|
|
116
|
+
}
|
|
117
|
+
if (planningArtifacts) {
|
|
118
|
+
const total = planningArtifacts.expected.length;
|
|
119
|
+
const found = planningArtifacts.present.length;
|
|
120
|
+
if (total === 0) {
|
|
121
|
+
console.log(` ${chalk.dim('Planning:')} ${chalk.green('OK')} (no template artifacts required)`);
|
|
122
|
+
} else if (planningArtifacts.ok) {
|
|
123
|
+
console.log(` ${chalk.dim('Planning:')} ${chalk.green('OK')} (${found}/${total} present)`);
|
|
124
|
+
} else {
|
|
125
|
+
console.log(` ${chalk.dim('Planning:')} ${chalk.red('FAIL')} (${planningArtifacts.missing.length}/${total} missing: ${planningArtifacts.missing.join(', ')})`);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (acceptanceHints) {
|
|
129
|
+
if (acceptanceHints.total === 0) {
|
|
130
|
+
console.log(` ${chalk.dim('Acceptance:')} ${chalk.green('OK')} (no template hints defined)`);
|
|
131
|
+
} else if (acceptanceHints.unchecked === 0) {
|
|
132
|
+
console.log(` ${chalk.dim('Acceptance:')} ${chalk.green('OK')} (${acceptanceHints.checked}/${acceptanceHints.total} checked)`);
|
|
133
|
+
} else {
|
|
134
|
+
console.log(` ${chalk.dim('Acceptance:')} ${chalk.yellow('WARN')} (${acceptanceHints.unchecked}/${acceptanceHints.total} unchecked)`);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
} else {
|
|
138
|
+
console.log(` ${chalk.dim('Project:')} ${chalk.dim('No project detected; registry-only validation')}`);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (errors.length > 0) {
|
|
142
|
+
console.log('');
|
|
143
|
+
console.log(chalk.red(' Errors:'));
|
|
144
|
+
for (const error of errors) {
|
|
145
|
+
console.log(` - ${error}`);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (warnings.length > 0) {
|
|
150
|
+
console.log('');
|
|
151
|
+
console.log(chalk.yellow(' Warnings:'));
|
|
152
|
+
for (const warning of warnings) {
|
|
153
|
+
console.log(` - ${warning}`);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
console.log('');
|
|
158
|
+
if (!ok) process.exit(1);
|
|
159
|
+
}
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { mkdirSync, existsSync, readFileSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
|
|
4
|
+
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
|
|
5
|
+
import {
|
|
6
|
+
getDispatchAssignmentPath,
|
|
7
|
+
getDispatchContextPath,
|
|
8
|
+
getDispatchPromptPath,
|
|
9
|
+
getDispatchTurnDir,
|
|
10
|
+
getTurnStagingDir,
|
|
11
|
+
getTurnStagingResultPath,
|
|
12
|
+
} from '../turn-paths.js';
|
|
13
|
+
import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
|
|
14
|
+
|
|
15
|
+
export const DEFAULT_MCP_TOOL_NAME = 'agentxchain_turn';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Dispatch a governed turn to an MCP server over stdio.
|
|
19
|
+
*
|
|
20
|
+
* v1 scope:
|
|
21
|
+
* - stdio transport only
|
|
22
|
+
* - single tool call per turn
|
|
23
|
+
* - required governed-turn tool contract
|
|
24
|
+
* - synchronous dispatch/wait flow (like api_proxy)
|
|
25
|
+
*
|
|
26
|
+
* The MCP tool must return a valid AgentXchain turn result either as:
|
|
27
|
+
* - structuredContent, or
|
|
28
|
+
* - JSON text in a text content block
|
|
29
|
+
*/
|
|
30
|
+
export async function dispatchMcp(root, state, config, options = {}) {
|
|
31
|
+
const { signal, onStatus, onStderr, turnId } = options;
|
|
32
|
+
|
|
33
|
+
const turn = resolveTargetTurn(state, turnId);
|
|
34
|
+
if (!turn) {
|
|
35
|
+
return { ok: false, error: 'No active turn in state' };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const manifestCheck = verifyDispatchManifestForAdapter(root, turn.turn_id, options);
|
|
39
|
+
if (!manifestCheck.ok) {
|
|
40
|
+
return { ok: false, error: `Dispatch manifest verification failed: ${manifestCheck.error}` };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const runtimeId = turn.runtime_id;
|
|
44
|
+
const runtime = config.runtimes?.[runtimeId];
|
|
45
|
+
if (!runtime) {
|
|
46
|
+
return { ok: false, error: `Runtime "${runtimeId}" not found in config` };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
|
|
50
|
+
const contextPath = join(root, getDispatchContextPath(turn.turn_id));
|
|
51
|
+
if (!existsSync(promptPath)) {
|
|
52
|
+
return { ok: false, error: 'Dispatch bundle not found. Run writeDispatchBundle() first.' };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const prompt = readFileSync(promptPath, 'utf8');
|
|
56
|
+
const context = existsSync(contextPath) ? readFileSync(contextPath, 'utf8') : '';
|
|
57
|
+
const { command, args } = resolveMcpCommand(runtime);
|
|
58
|
+
if (!command) {
|
|
59
|
+
return { ok: false, error: `Cannot resolve MCP command for runtime "${runtimeId}". Expected "command" field in runtime config.` };
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const timeoutMs = turn.deadline_at
|
|
63
|
+
? Math.max(0, new Date(turn.deadline_at).getTime() - Date.now())
|
|
64
|
+
: 1200000;
|
|
65
|
+
const toolName = resolveMcpToolName(runtime);
|
|
66
|
+
const logs = [];
|
|
67
|
+
|
|
68
|
+
const stagingDir = join(root, getTurnStagingDir(turn.turn_id));
|
|
69
|
+
mkdirSync(stagingDir, { recursive: true });
|
|
70
|
+
|
|
71
|
+
const transport = new StdioClientTransport({
|
|
72
|
+
command,
|
|
73
|
+
args,
|
|
74
|
+
cwd: runtime.cwd ? join(root, runtime.cwd) : root,
|
|
75
|
+
env: buildTransportEnv(process.env),
|
|
76
|
+
stderr: 'pipe',
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
if (transport.stderr) {
|
|
80
|
+
transport.stderr.on('data', (chunk) => {
|
|
81
|
+
const text = chunk.toString();
|
|
82
|
+
logs.push(`[stderr] ${text}`);
|
|
83
|
+
if (onStderr) onStderr(text);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const client = new Client({
|
|
88
|
+
name: 'agentxchain-mcp-adapter',
|
|
89
|
+
version: '1.0.0',
|
|
90
|
+
});
|
|
91
|
+
client.onerror = (error) => {
|
|
92
|
+
logs.push(`[client-error] ${error.message}`);
|
|
93
|
+
};
|
|
94
|
+
transport.onerror = (error) => {
|
|
95
|
+
logs.push(`[transport-error] ${error.message}`);
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
if (signal?.aborted) {
|
|
100
|
+
return { ok: false, aborted: true, logs };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onStatus?.(`Connecting to MCP stdio server (${command})`);
|
|
104
|
+
await client.connect(transport);
|
|
105
|
+
|
|
106
|
+
if (signal?.aborted) {
|
|
107
|
+
return { ok: false, aborted: true, logs };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
onStatus?.(`Listing MCP tools`);
|
|
111
|
+
const toolsResult = await client.listTools(undefined, {
|
|
112
|
+
signal,
|
|
113
|
+
timeout: timeoutMs,
|
|
114
|
+
maxTotalTimeout: timeoutMs,
|
|
115
|
+
});
|
|
116
|
+
const toolNames = toolsResult.tools.map((tool) => tool.name);
|
|
117
|
+
if (!toolNames.includes(toolName)) {
|
|
118
|
+
return {
|
|
119
|
+
ok: false,
|
|
120
|
+
error: `MCP server does not expose required tool "${toolName}". Available tools: ${toolNames.join(', ') || '(none)'}`,
|
|
121
|
+
logs,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (signal?.aborted) {
|
|
126
|
+
return { ok: false, aborted: true, logs };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
onStatus?.(`Calling MCP tool "${toolName}"`);
|
|
130
|
+
const toolResult = await client.callTool({
|
|
131
|
+
name: toolName,
|
|
132
|
+
arguments: {
|
|
133
|
+
run_id: state.run_id,
|
|
134
|
+
turn_id: turn.turn_id,
|
|
135
|
+
role: turn.assigned_role,
|
|
136
|
+
phase: state.phase,
|
|
137
|
+
runtime_id: runtimeId,
|
|
138
|
+
project_root: root,
|
|
139
|
+
dispatch_dir: join(root, getDispatchTurnDir(turn.turn_id)),
|
|
140
|
+
assignment_path: join(root, getDispatchAssignmentPath(turn.turn_id)),
|
|
141
|
+
prompt_path: promptPath,
|
|
142
|
+
context_path: contextPath,
|
|
143
|
+
staging_path: join(root, getTurnStagingResultPath(turn.turn_id)),
|
|
144
|
+
prompt,
|
|
145
|
+
context,
|
|
146
|
+
},
|
|
147
|
+
}, undefined, {
|
|
148
|
+
signal,
|
|
149
|
+
timeout: timeoutMs,
|
|
150
|
+
maxTotalTimeout: timeoutMs,
|
|
151
|
+
resetTimeoutOnProgress: true,
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
if (toolResult?.isError) {
|
|
155
|
+
return {
|
|
156
|
+
ok: false,
|
|
157
|
+
error: buildMcpToolError(toolName, toolResult),
|
|
158
|
+
logs,
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const turnResult = extractTurnResultFromMcpToolResult(toolResult);
|
|
163
|
+
if (!turnResult.ok) {
|
|
164
|
+
return { ok: false, error: turnResult.error, logs };
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const stagingPath = join(root, getTurnStagingResultPath(turn.turn_id));
|
|
168
|
+
writeFileSync(stagingPath, JSON.stringify(turnResult.result, null, 2));
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
ok: true,
|
|
172
|
+
toolName,
|
|
173
|
+
logs,
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
if (signal?.aborted) {
|
|
177
|
+
return { ok: false, aborted: true, logs };
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
ok: false,
|
|
181
|
+
error: `MCP dispatch failed: ${error.message}`,
|
|
182
|
+
logs,
|
|
183
|
+
};
|
|
184
|
+
} finally {
|
|
185
|
+
await safeCloseClient(client, transport);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export function resolveMcpCommand(runtime) {
|
|
190
|
+
if (!runtime?.command) return { command: null, args: [] };
|
|
191
|
+
|
|
192
|
+
if (Array.isArray(runtime.command)) {
|
|
193
|
+
const [command, ...args] = runtime.command;
|
|
194
|
+
return { command, args };
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
return {
|
|
198
|
+
command: runtime.command,
|
|
199
|
+
args: Array.isArray(runtime.args) ? runtime.args : [],
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function resolveMcpToolName(runtime) {
|
|
204
|
+
return typeof runtime?.tool_name === 'string' && runtime.tool_name.trim()
|
|
205
|
+
? runtime.tool_name.trim()
|
|
206
|
+
: DEFAULT_MCP_TOOL_NAME;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function extractTurnResultFromMcpToolResult(toolResult) {
|
|
210
|
+
const directCandidates = [
|
|
211
|
+
toolResult?.structuredContent,
|
|
212
|
+
toolResult?.toolResult?.structuredContent,
|
|
213
|
+
toolResult?.toolResult,
|
|
214
|
+
];
|
|
215
|
+
|
|
216
|
+
for (const candidate of directCandidates) {
|
|
217
|
+
if (looksLikeTurnResult(candidate)) {
|
|
218
|
+
return { ok: true, result: candidate };
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const textBlocks = [
|
|
223
|
+
...(Array.isArray(toolResult?.content) ? toolResult.content : []),
|
|
224
|
+
...(Array.isArray(toolResult?.toolResult?.content) ? toolResult.toolResult.content : []),
|
|
225
|
+
].filter((item) => item?.type === 'text' && typeof item.text === 'string');
|
|
226
|
+
|
|
227
|
+
for (const block of textBlocks) {
|
|
228
|
+
const parsed = tryParseJson(block.text);
|
|
229
|
+
if (looksLikeTurnResult(parsed) || isPlainObject(parsed)) {
|
|
230
|
+
return { ok: true, result: parsed };
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
if (textBlocks.length === 0) {
|
|
235
|
+
return {
|
|
236
|
+
ok: false,
|
|
237
|
+
error: 'MCP tool returned no structuredContent and no text content blocks containing a turn result.',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
error: 'MCP tool returned text content, but none of the text blocks contained valid turn-result JSON.',
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function buildMcpToolError(toolName, toolResult) {
|
|
248
|
+
const text = Array.isArray(toolResult?.content)
|
|
249
|
+
? toolResult.content
|
|
250
|
+
.filter((item) => item?.type === 'text' && typeof item.text === 'string')
|
|
251
|
+
.map((item) => item.text.trim())
|
|
252
|
+
.filter(Boolean)
|
|
253
|
+
.join(' ')
|
|
254
|
+
: '';
|
|
255
|
+
if (text) {
|
|
256
|
+
return `MCP tool "${toolName}" returned an error: ${text}`;
|
|
257
|
+
}
|
|
258
|
+
return `MCP tool "${toolName}" returned an error without diagnostic text.`;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function buildTransportEnv(env) {
|
|
262
|
+
const result = {};
|
|
263
|
+
for (const [key, value] of Object.entries(env || {})) {
|
|
264
|
+
if (typeof value === 'string') {
|
|
265
|
+
result[key] = value;
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
function isPlainObject(value) {
|
|
272
|
+
return !!value && typeof value === 'object' && !Array.isArray(value);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function looksLikeTurnResult(value) {
|
|
276
|
+
if (!isPlainObject(value)) return false;
|
|
277
|
+
const hasIdentity = 'run_id' in value || 'turn_id' in value;
|
|
278
|
+
const hasLifecycle = 'status' in value || 'role' in value || 'runtime_id' in value;
|
|
279
|
+
return hasIdentity && hasLifecycle;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function tryParseJson(value) {
|
|
283
|
+
try {
|
|
284
|
+
return JSON.parse(value);
|
|
285
|
+
} catch {
|
|
286
|
+
return null;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
function resolveTargetTurn(state, turnId) {
|
|
291
|
+
if (turnId && state?.active_turns?.[turnId]) {
|
|
292
|
+
return state.active_turns[turnId];
|
|
293
|
+
}
|
|
294
|
+
return state?.current_turn || Object.values(state?.active_turns || {})[0];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
async function safeCloseClient(client, transport) {
|
|
298
|
+
try {
|
|
299
|
+
await client.close();
|
|
300
|
+
return;
|
|
301
|
+
} catch {}
|
|
302
|
+
|
|
303
|
+
try {
|
|
304
|
+
await transport.close();
|
|
305
|
+
} catch {}
|
|
306
|
+
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readFileSync } from 'node:fs';
|
|
1
|
+
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
2
2
|
import { dirname, join } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
|
|
@@ -9,6 +9,7 @@ export const VALID_GOVERNED_TEMPLATE_IDS = Object.freeze([
|
|
|
9
9
|
'generic',
|
|
10
10
|
'api-service',
|
|
11
11
|
'cli-tool',
|
|
12
|
+
'library',
|
|
12
13
|
'web-app',
|
|
13
14
|
]);
|
|
14
15
|
|
|
@@ -143,3 +144,237 @@ export function loadGovernedTemplate(templateId) {
|
|
|
143
144
|
export function loadAllGovernedTemplates() {
|
|
144
145
|
return VALID_GOVERNED_TEMPLATE_IDS.map((templateId) => loadGovernedTemplate(templateId));
|
|
145
146
|
}
|
|
147
|
+
|
|
148
|
+
export function listGovernedTemplateManifestIds(manifestDir = GOVERNED_TEMPLATES_DIR) {
|
|
149
|
+
if (!existsSync(manifestDir)) {
|
|
150
|
+
return [];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return readdirSync(manifestDir)
|
|
154
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
155
|
+
.map((entry) => entry.slice(0, -'.json'.length))
|
|
156
|
+
.sort();
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export function validateGovernedTemplateRegistry(options = {}) {
|
|
160
|
+
const manifestDir = options.manifestDir || GOVERNED_TEMPLATES_DIR;
|
|
161
|
+
const registeredIds = options.registeredIds || [...VALID_GOVERNED_TEMPLATE_IDS];
|
|
162
|
+
const errors = [];
|
|
163
|
+
const warnings = [];
|
|
164
|
+
const manifestIds = listGovernedTemplateManifestIds(manifestDir);
|
|
165
|
+
|
|
166
|
+
for (const templateId of registeredIds) {
|
|
167
|
+
const manifestPath = join(manifestDir, `${templateId}.json`);
|
|
168
|
+
if (!existsSync(manifestPath)) {
|
|
169
|
+
errors.push(`Registered template "${templateId}" is missing its manifest file.`);
|
|
170
|
+
continue;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
let manifest;
|
|
174
|
+
try {
|
|
175
|
+
manifest = JSON.parse(readFileSync(manifestPath, 'utf8'));
|
|
176
|
+
} catch (err) {
|
|
177
|
+
errors.push(`Template "${templateId}" manifest is invalid JSON: ${err.message}`);
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
const validation = validateGovernedTemplateManifest(manifest, templateId);
|
|
182
|
+
if (!validation.ok) {
|
|
183
|
+
errors.push(`Template "${templateId}" manifest is invalid: ${validation.errors.join('; ')}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
for (const manifestId of manifestIds) {
|
|
188
|
+
if (!registeredIds.includes(manifestId)) {
|
|
189
|
+
errors.push(`Manifest "${manifestId}.json" exists on disk but is not registered in VALID_GOVERNED_TEMPLATE_IDS.`);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
ok: errors.length === 0,
|
|
195
|
+
registered_ids: registeredIds,
|
|
196
|
+
manifest_ids: manifestIds,
|
|
197
|
+
errors,
|
|
198
|
+
warnings,
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export function validateProjectPlanningArtifacts(root, templateId) {
|
|
203
|
+
const effectiveTemplateId = templateId || 'generic';
|
|
204
|
+
const errors = [];
|
|
205
|
+
const warnings = [];
|
|
206
|
+
|
|
207
|
+
let manifest;
|
|
208
|
+
try {
|
|
209
|
+
manifest = loadGovernedTemplate(effectiveTemplateId);
|
|
210
|
+
} catch {
|
|
211
|
+
// Template load failure is already reported by validateGovernedProjectTemplate.
|
|
212
|
+
// Skip artifact check — we cannot know what artifacts to expect.
|
|
213
|
+
return {
|
|
214
|
+
ok: true,
|
|
215
|
+
template: effectiveTemplateId,
|
|
216
|
+
expected: [],
|
|
217
|
+
present: [],
|
|
218
|
+
missing: [],
|
|
219
|
+
errors,
|
|
220
|
+
warnings: ['Template could not be loaded; planning artifact check skipped.'],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
const artifacts = manifest.planning_artifacts || [];
|
|
225
|
+
const expected = artifacts.map((a) => a.filename);
|
|
226
|
+
const present = [];
|
|
227
|
+
const missing = [];
|
|
228
|
+
|
|
229
|
+
for (const filename of expected) {
|
|
230
|
+
const artifactPath = join(root, '.planning', filename);
|
|
231
|
+
if (existsSync(artifactPath)) {
|
|
232
|
+
present.push(filename);
|
|
233
|
+
} else {
|
|
234
|
+
missing.push(filename);
|
|
235
|
+
errors.push(`Template "${effectiveTemplateId}" requires planning artifact ".planning/${filename}" but it is missing.`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return {
|
|
240
|
+
ok: errors.length === 0,
|
|
241
|
+
template: effectiveTemplateId,
|
|
242
|
+
expected,
|
|
243
|
+
present,
|
|
244
|
+
missing,
|
|
245
|
+
errors,
|
|
246
|
+
warnings,
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
const TEMPLATE_GUIDANCE_HEADER = '## Template Guidance';
|
|
251
|
+
|
|
252
|
+
export function validateAcceptanceHintCompletion(root, templateId) {
|
|
253
|
+
const effectiveTemplateId = templateId || 'generic';
|
|
254
|
+
const errors = [];
|
|
255
|
+
const warnings = [];
|
|
256
|
+
|
|
257
|
+
let manifest;
|
|
258
|
+
try {
|
|
259
|
+
manifest = loadGovernedTemplate(effectiveTemplateId);
|
|
260
|
+
} catch {
|
|
261
|
+
return {
|
|
262
|
+
ok: true,
|
|
263
|
+
template: effectiveTemplateId,
|
|
264
|
+
total: 0,
|
|
265
|
+
checked: 0,
|
|
266
|
+
unchecked: 0,
|
|
267
|
+
missing_file: false,
|
|
268
|
+
missing_section: false,
|
|
269
|
+
unchecked_hints: [],
|
|
270
|
+
errors,
|
|
271
|
+
warnings: ['Template could not be loaded; acceptance hint check skipped.'],
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
const hints = manifest.acceptance_hints || [];
|
|
276
|
+
if (hints.length === 0) {
|
|
277
|
+
return {
|
|
278
|
+
ok: true,
|
|
279
|
+
template: effectiveTemplateId,
|
|
280
|
+
total: 0,
|
|
281
|
+
checked: 0,
|
|
282
|
+
unchecked: 0,
|
|
283
|
+
missing_file: false,
|
|
284
|
+
missing_section: false,
|
|
285
|
+
unchecked_hints: [],
|
|
286
|
+
errors,
|
|
287
|
+
warnings,
|
|
288
|
+
};
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const matrixPath = join(root, '.planning', 'acceptance-matrix.md');
|
|
292
|
+
if (!existsSync(matrixPath)) {
|
|
293
|
+
warnings.push('acceptance-matrix.md not found; cannot verify template acceptance hints.');
|
|
294
|
+
return {
|
|
295
|
+
ok: true,
|
|
296
|
+
template: effectiveTemplateId,
|
|
297
|
+
total: hints.length,
|
|
298
|
+
checked: 0,
|
|
299
|
+
unchecked: hints.length,
|
|
300
|
+
missing_file: true,
|
|
301
|
+
missing_section: false,
|
|
302
|
+
unchecked_hints: [...hints],
|
|
303
|
+
errors,
|
|
304
|
+
warnings,
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const matrixContent = readFileSync(matrixPath, 'utf8');
|
|
309
|
+
const sectionIndex = matrixContent.indexOf(TEMPLATE_GUIDANCE_HEADER);
|
|
310
|
+
if (sectionIndex === -1) {
|
|
311
|
+
warnings.push('acceptance-matrix.md has no "## Template Guidance" section; cannot verify template acceptance hints.');
|
|
312
|
+
return {
|
|
313
|
+
ok: true,
|
|
314
|
+
template: effectiveTemplateId,
|
|
315
|
+
total: hints.length,
|
|
316
|
+
checked: 0,
|
|
317
|
+
unchecked: hints.length,
|
|
318
|
+
missing_file: false,
|
|
319
|
+
missing_section: true,
|
|
320
|
+
unchecked_hints: [...hints],
|
|
321
|
+
errors,
|
|
322
|
+
warnings,
|
|
323
|
+
};
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
// Parse the Template Guidance section for checked/unchecked items
|
|
327
|
+
const sectionContent = matrixContent.slice(sectionIndex);
|
|
328
|
+
const checkedPattern = /^- \[x\]\s+(.+)$/gim;
|
|
329
|
+
const checkedTexts = new Set();
|
|
330
|
+
let match;
|
|
331
|
+
while ((match = checkedPattern.exec(sectionContent)) !== null) {
|
|
332
|
+
checkedTexts.add(match[1].trim());
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const uncheckedHints = [];
|
|
336
|
+
let checkedCount = 0;
|
|
337
|
+
|
|
338
|
+
for (const hint of hints) {
|
|
339
|
+
if (checkedTexts.has(hint)) {
|
|
340
|
+
checkedCount++;
|
|
341
|
+
} else {
|
|
342
|
+
uncheckedHints.push(hint);
|
|
343
|
+
warnings.push(`Acceptance hint unchecked: "${hint}"`);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
return {
|
|
348
|
+
ok: true,
|
|
349
|
+
template: effectiveTemplateId,
|
|
350
|
+
total: hints.length,
|
|
351
|
+
checked: checkedCount,
|
|
352
|
+
unchecked: uncheckedHints.length,
|
|
353
|
+
missing_file: false,
|
|
354
|
+
missing_section: false,
|
|
355
|
+
unchecked_hints: uncheckedHints,
|
|
356
|
+
errors,
|
|
357
|
+
warnings,
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
export function validateGovernedProjectTemplate(templateId, source = 'agentxchain.json') {
|
|
362
|
+
const effectiveTemplateId = templateId || 'generic';
|
|
363
|
+
const effectiveSource = templateId ? source : 'implicit_default';
|
|
364
|
+
const errors = [];
|
|
365
|
+
const warnings = [];
|
|
366
|
+
|
|
367
|
+
try {
|
|
368
|
+
loadGovernedTemplate(effectiveTemplateId);
|
|
369
|
+
} catch (err) {
|
|
370
|
+
errors.push(err.message);
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
return {
|
|
374
|
+
ok: errors.length === 0,
|
|
375
|
+
template: effectiveTemplateId,
|
|
376
|
+
source: effectiveSource,
|
|
377
|
+
errors,
|
|
378
|
+
warnings,
|
|
379
|
+
};
|
|
380
|
+
}
|
|
@@ -16,7 +16,7 @@ import { validateHooksConfig } from './hook-runner.js';
|
|
|
16
16
|
import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
|
|
17
17
|
|
|
18
18
|
const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
|
|
19
|
-
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy'];
|
|
19
|
+
const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp'];
|
|
20
20
|
const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
21
21
|
const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
22
22
|
const VALID_PHASES = ['planning', 'implementation', 'qa'];
|
|
@@ -46,6 +46,36 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
|
|
|
46
46
|
'safety_margin_tokens',
|
|
47
47
|
];
|
|
48
48
|
|
|
49
|
+
function validateMcpRuntime(runtimeId, runtime, errors) {
|
|
50
|
+
const command = runtime?.command;
|
|
51
|
+
|
|
52
|
+
if (typeof command === 'string') {
|
|
53
|
+
if (!command.trim()) {
|
|
54
|
+
errors.push(`Runtime "${runtimeId}": mcp command must be a non-empty string`);
|
|
55
|
+
}
|
|
56
|
+
} else if (Array.isArray(command)) {
|
|
57
|
+
if (command.length === 0 || command.some((part) => typeof part !== 'string' || !part.trim())) {
|
|
58
|
+
errors.push(`Runtime "${runtimeId}": mcp command array must contain at least one non-empty string and no empty parts`);
|
|
59
|
+
}
|
|
60
|
+
} else {
|
|
61
|
+
errors.push(`Runtime "${runtimeId}": mcp requires "command" as a string or string array`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if ('args' in runtime) {
|
|
65
|
+
if (!Array.isArray(runtime.args) || runtime.args.some((part) => typeof part !== 'string')) {
|
|
66
|
+
errors.push(`Runtime "${runtimeId}": mcp args must be an array of strings`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if ('tool_name' in runtime && (typeof runtime.tool_name !== 'string' || !runtime.tool_name.trim())) {
|
|
71
|
+
errors.push(`Runtime "${runtimeId}": mcp tool_name must be a non-empty string`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
if ('cwd' in runtime && (typeof runtime.cwd !== 'string' || !runtime.cwd.trim())) {
|
|
75
|
+
errors.push(`Runtime "${runtimeId}": mcp cwd must be a non-empty string`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
49
79
|
function validateApiProxyRetryPolicy(runtimeId, retryPolicy, errors) {
|
|
50
80
|
if (!retryPolicy || typeof retryPolicy !== 'object' || Array.isArray(retryPolicy)) {
|
|
51
81
|
errors.push(`Runtime "${runtimeId}": retry_policy must be an object`);
|
|
@@ -263,6 +293,9 @@ export function validateV4Config(data, projectRoot) {
|
|
|
263
293
|
validateApiProxyPreflightTokenization(id, rt, errors);
|
|
264
294
|
}
|
|
265
295
|
}
|
|
296
|
+
if (rt.type === 'mcp') {
|
|
297
|
+
validateMcpRuntime(id, rt, errors);
|
|
298
|
+
}
|
|
266
299
|
}
|
|
267
300
|
}
|
|
268
301
|
|
package/src/lib/repo-observer.js
CHANGED
|
@@ -270,13 +270,13 @@ export function buildObservedArtifact(observation, baseline) {
|
|
|
270
270
|
*
|
|
271
271
|
* Normalization rules (spec §5.3):
|
|
272
272
|
* - manual + external pass → attested_pass
|
|
273
|
-
* -
|
|
274
|
-
* -
|
|
273
|
+
* - executable runtime + pass + machine evidence all zero → pass
|
|
274
|
+
* - executable runtime + pass + no reproducible evidence → not_reproducible
|
|
275
275
|
* - any external fail → fail
|
|
276
276
|
* - external skipped → skipped
|
|
277
277
|
*
|
|
278
278
|
* @param {object} verification — the actor-supplied verification object
|
|
279
|
-
* @param {string} runtimeType — 'manual' | 'local_cli' | 'api_proxy'
|
|
279
|
+
* @param {string} runtimeType — 'manual' | 'local_cli' | 'api_proxy' | 'mcp'
|
|
280
280
|
* @returns {{ status: string, reason: string, reproducible: boolean }}
|
|
281
281
|
*/
|
|
282
282
|
export function normalizeVerification(verification, runtimeType) {
|
|
@@ -299,18 +299,18 @@ export function normalizeVerification(verification, runtimeType) {
|
|
|
299
299
|
return { status: 'attested_pass', reason: 'API proxy runtime — no direct execution environment', reproducible: false };
|
|
300
300
|
}
|
|
301
301
|
|
|
302
|
-
// local_cli — check for machine evidence
|
|
302
|
+
// local_cli / mcp — check for machine evidence
|
|
303
303
|
const evidence = verification?.machine_evidence;
|
|
304
304
|
if (Array.isArray(evidence) && evidence.length > 0) {
|
|
305
305
|
const allZero = evidence.every(e => typeof e.exit_code === 'number' && e.exit_code === 0);
|
|
306
306
|
if (allZero) {
|
|
307
|
-
return { status: 'pass', reason:
|
|
307
|
+
return { status: 'pass', reason: `${runtimeType} turn provided machine evidence with zero exit codes`, reproducible: true };
|
|
308
308
|
}
|
|
309
|
-
return { status: 'not_reproducible', reason:
|
|
309
|
+
return { status: 'not_reproducible', reason: `${runtimeType} turn has machine evidence with non-zero exit codes despite claiming pass`, reproducible: false };
|
|
310
310
|
}
|
|
311
311
|
|
|
312
|
-
//
|
|
313
|
-
return { status: 'not_reproducible', reason:
|
|
312
|
+
// executable runtime + pass but no machine evidence
|
|
313
|
+
return { status: 'not_reproducible', reason: `${runtimeType} turn claimed pass but provided no machine evidence`, reproducible: false };
|
|
314
314
|
}
|
|
315
315
|
|
|
316
316
|
// ── Declared vs Observed Comparison ─────────────────────────────────────────
|
package/src/lib/validation.js
CHANGED
|
@@ -2,6 +2,12 @@ import { existsSync, readFileSync, readdirSync } from 'fs';
|
|
|
2
2
|
import { join } from 'path';
|
|
3
3
|
import { validateStagedTurnResult, STAGING_PATH } from './turn-result-validator.js';
|
|
4
4
|
import { getActiveTurn } from './governed-state.js';
|
|
5
|
+
import {
|
|
6
|
+
validateGovernedProjectTemplate,
|
|
7
|
+
validateGovernedTemplateRegistry,
|
|
8
|
+
validateProjectPlanningArtifacts,
|
|
9
|
+
validateAcceptanceHintCompletion,
|
|
10
|
+
} from './governed-templates.js';
|
|
5
11
|
|
|
6
12
|
const DEFAULT_REQUIRED_FILES = [
|
|
7
13
|
'.planning/PROJECT.md',
|
|
@@ -87,6 +93,23 @@ export function validateGovernedProject(root, rawConfig, config, opts = {}) {
|
|
|
87
93
|
const errors = [];
|
|
88
94
|
const warnings = [];
|
|
89
95
|
|
|
96
|
+
const templateRegistry = validateGovernedTemplateRegistry();
|
|
97
|
+
errors.push(...templateRegistry.errors);
|
|
98
|
+
warnings.push(...templateRegistry.warnings);
|
|
99
|
+
|
|
100
|
+
const projectTemplate = validateGovernedProjectTemplate(rawConfig?.template);
|
|
101
|
+
errors.push(...projectTemplate.errors);
|
|
102
|
+
warnings.push(...projectTemplate.warnings);
|
|
103
|
+
|
|
104
|
+
// Validate planning artifact completeness against the configured template
|
|
105
|
+
const planningArtifacts = validateProjectPlanningArtifacts(root, rawConfig?.template);
|
|
106
|
+
errors.push(...planningArtifacts.errors);
|
|
107
|
+
warnings.push(...planningArtifacts.warnings);
|
|
108
|
+
|
|
109
|
+
// Validate acceptance hint completion against the configured template
|
|
110
|
+
const acceptanceHints = validateAcceptanceHintCompletion(root, rawConfig?.template);
|
|
111
|
+
warnings.push(...acceptanceHints.warnings);
|
|
112
|
+
|
|
90
113
|
const mustExist = [
|
|
91
114
|
config.files?.state || '.agentxchain/state.json',
|
|
92
115
|
config.files?.history || '.agentxchain/history.jsonl',
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"id": "library",
|
|
3
|
+
"display_name": "Library",
|
|
4
|
+
"description": "Governed scaffold for reusable packages with public-API, compatibility, and adoption planning.",
|
|
5
|
+
"version": "1",
|
|
6
|
+
"protocol_compatibility": ["1.0", "1.1"],
|
|
7
|
+
"planning_artifacts": [
|
|
8
|
+
{
|
|
9
|
+
"filename": "public-api.md",
|
|
10
|
+
"content_template": "# Public API — {{project_name}}\n\n## Supported Entrypoints\n- Package name:\n- Import / require paths:\n- Stable vs experimental exports:\n\n## Consumer-Facing Surface\n| Export / command | Consumer use case | Stability | Notes |\n|------------------|-------------------|-----------|-------|\n| | | | |\n\n## Breaking-Change Triggers\n- What counts as breaking:\n- What can ship behind deprecation first:\n- What must stay internal:\n"
|
|
11
|
+
},
|
|
12
|
+
{
|
|
13
|
+
"filename": "compatibility-policy.md",
|
|
14
|
+
"content_template": "# Compatibility Policy — {{project_name}}\n\n## Versioning Contract\n- Semver policy:\n- Supported runtime versions:\n- Supported platforms / environments:\n\n## Deprecation And Migration\n| Surface | Deprecation path | Migration guidance | Removal target |\n|---------|------------------|--------------------|----------------|\n| | | | |\n\n## Compatibility Risks\n- Dependency upgrade hazards:\n- Packaging / module-format risks:\n- Consumer breakage watchpoints:\n"
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"filename": "release-adoption.md",
|
|
18
|
+
"content_template": "# Release And Adoption — {{project_name}}\n\n## Consumer Smoke Proof\n- Install command:\n- Import / usage smoke check:\n- Example consumer environment:\n\n## Release Notes Inputs\n- User-visible changes:\n- Upgrade steps:\n- Rollback or pinning advice:\n\n## Adoption Risks\n- Migration blockers:\n- Documentation gaps:\n- Support expectations after release:\n"
|
|
19
|
+
}
|
|
20
|
+
],
|
|
21
|
+
"prompt_overrides": {
|
|
22
|
+
"pm": "Define the exported surface, compatibility promise, and consumer upgrade expectations before the team treats the package as ready to ship.",
|
|
23
|
+
"dev": "Treat exported entrypoints, backward compatibility, and package-consumer install or import paths as product behavior, not release polish.",
|
|
24
|
+
"qa": "Verify public API stability, install/import smoke, migration notes, and compatibility risks across supported runtimes before sign-off."
|
|
25
|
+
},
|
|
26
|
+
"acceptance_hints": [
|
|
27
|
+
"Public API surface reviewed and intentionally versioned",
|
|
28
|
+
"Compatibility or migration expectations documented for consumers",
|
|
29
|
+
"Install/import or package-consumer smoke path verified"
|
|
30
|
+
]
|
|
31
|
+
}
|