agentxchain 2.3.0 → 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -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:
@@ -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.0",
3
+ "version": "2.5.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"
@@ -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
  }
@@ -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 or streamable_http 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 { describeMcpRuntimeTarget, dispatchMcp, resolveMcpTransport } from '../lib/adapters/mcp-adapter.js';
48
50
  import {
49
51
  getDispatchAssignmentPath,
50
52
  getDispatchContextPath,
@@ -423,12 +425,65 @@ 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
+ const mcpTransport = resolveMcpTransport(runtime);
430
+ console.log(chalk.cyan(`Dispatching to MCP ${mcpTransport}: ${describeMcpRuntimeTarget(runtime)}`));
431
+ console.log(chalk.dim(`Turn: ${turn.turn_id} Role: ${roleId} Phase: ${state.phase} Tool: ${runtime?.tool_name || 'agentxchain_turn'}`));
432
+
433
+ const mcpResult = await dispatchMcp(root, state, config, {
434
+ signal: controller.signal,
435
+ onStatus: (msg) => console.log(chalk.dim(` ${msg}`)),
436
+ onStderr: opts.verbose ? (text) => process.stderr.write(chalk.yellow(text)) : undefined,
437
+ verifyManifest: true,
438
+ });
439
+
440
+ if (mcpResult.logs?.length) {
441
+ saveDispatchLogs(root, turn.turn_id, mcpResult.logs);
442
+ }
443
+
444
+ if (mcpResult.aborted) {
445
+ console.log('');
446
+ console.log(chalk.yellow('Aborted. Turn remains assigned.'));
447
+ console.log(chalk.dim('Resume later with: agentxchain step --resume'));
448
+ console.log(chalk.dim('Or accept/reject manually: agentxchain accept-turn / reject-turn'));
449
+ process.exit(0);
450
+ }
451
+
452
+ if (!mcpResult.ok) {
453
+ const blocked = markRunBlocked(root, {
454
+ blockedOn: 'dispatch:mcp_failure',
455
+ category: 'dispatch_error',
456
+ recovery: {
457
+ typed_reason: 'dispatch_error',
458
+ owner: 'human',
459
+ recovery_action: 'Resolve the MCP dispatch issue, then run agentxchain step --resume',
460
+ turn_retained: true,
461
+ detail: mcpResult.error,
462
+ },
463
+ turnId: turn.turn_id,
464
+ hooksConfig,
465
+ });
466
+ if (blocked.ok) {
467
+ state = blocked.state;
468
+ }
469
+
470
+ console.log('');
471
+ console.log(chalk.red(`MCP dispatch failed: ${mcpResult.error}`));
472
+ console.log(chalk.dim('The turn remains assigned. You can:'));
473
+ console.log(chalk.dim(' - Fix the MCP server/runtime and retry: agentxchain step --resume'));
474
+ console.log(chalk.dim(' - Complete manually: edit .agentxchain/staging/turn-result.json'));
475
+ console.log(chalk.dim(' - Reject: agentxchain reject-turn --reason "mcp dispatch failed"'));
476
+ process.exit(1);
477
+ }
478
+
479
+ console.log(chalk.green(`MCP tool completed${mcpResult.toolName ? ` (${mcpResult.toolName})` : ''}. Staged result detected.`));
480
+ console.log('');
426
481
  }
427
482
 
428
483
  // ── Phase 3: Wait for turn completion ─────────────────────────────────────
429
484
 
430
- if (runtimeType === 'api_proxy') {
431
- // api_proxy is synchronous — result already staged in Phase 2
485
+ if (runtimeType === 'api_proxy' || runtimeType === 'mcp') {
486
+ // api_proxy and mcp are synchronous — result already staged in Phase 2
432
487
  } else if (runtimeType === 'local_cli') {
433
488
  // ── Local CLI adapter: spawn subprocess ──
434
489
  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,369 @@
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 { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
6
+ import {
7
+ getDispatchAssignmentPath,
8
+ getDispatchContextPath,
9
+ getDispatchPromptPath,
10
+ getDispatchTurnDir,
11
+ getTurnStagingDir,
12
+ getTurnStagingResultPath,
13
+ } from '../turn-paths.js';
14
+ import { verifyDispatchManifestForAdapter } from '../dispatch-manifest.js';
15
+
16
+ export const DEFAULT_MCP_TOOL_NAME = 'agentxchain_turn';
17
+ export const DEFAULT_MCP_TRANSPORT = 'stdio';
18
+
19
+ /**
20
+ * Dispatch a governed turn to an MCP server.
21
+ *
22
+ * Current scope:
23
+ * - stdio or streamable_http transport
24
+ * - single tool call per turn
25
+ * - required governed-turn tool contract
26
+ * - synchronous dispatch/wait flow (like api_proxy)
27
+ *
28
+ * The MCP tool must return a valid AgentXchain turn result either as:
29
+ * - structuredContent, or
30
+ * - JSON text in a text content block
31
+ */
32
+ export async function dispatchMcp(root, state, config, options = {}) {
33
+ const { signal, onStatus, onStderr, turnId } = options;
34
+
35
+ const turn = resolveTargetTurn(state, turnId);
36
+ if (!turn) {
37
+ return { ok: false, error: 'No active turn in state' };
38
+ }
39
+
40
+ const manifestCheck = verifyDispatchManifestForAdapter(root, turn.turn_id, options);
41
+ if (!manifestCheck.ok) {
42
+ return { ok: false, error: `Dispatch manifest verification failed: ${manifestCheck.error}` };
43
+ }
44
+
45
+ const runtimeId = turn.runtime_id;
46
+ const runtime = config.runtimes?.[runtimeId];
47
+ if (!runtime) {
48
+ return { ok: false, error: `Runtime "${runtimeId}" not found in config` };
49
+ }
50
+
51
+ const promptPath = join(root, getDispatchPromptPath(turn.turn_id));
52
+ const contextPath = join(root, getDispatchContextPath(turn.turn_id));
53
+ if (!existsSync(promptPath)) {
54
+ return { ok: false, error: 'Dispatch bundle not found. Run writeDispatchBundle() first.' };
55
+ }
56
+
57
+ const prompt = readFileSync(promptPath, 'utf8');
58
+ const context = existsSync(contextPath) ? readFileSync(contextPath, 'utf8') : '';
59
+ const { command, args } = resolveMcpCommand(runtime);
60
+ const transportType = resolveMcpTransport(runtime);
61
+ if (transportType === 'stdio' && !command) {
62
+ return { ok: false, error: `Cannot resolve MCP command for runtime "${runtimeId}". Expected "command" field in runtime config.` };
63
+ }
64
+
65
+ const timeoutMs = turn.deadline_at
66
+ ? Math.max(0, new Date(turn.deadline_at).getTime() - Date.now())
67
+ : 1200000;
68
+ const toolName = resolveMcpToolName(runtime);
69
+ const logs = [];
70
+
71
+ const stagingDir = join(root, getTurnStagingDir(turn.turn_id));
72
+ mkdirSync(stagingDir, { recursive: true });
73
+
74
+ const transport = buildMcpClientTransport({
75
+ root,
76
+ runtime,
77
+ command,
78
+ args,
79
+ logs,
80
+ onStderr,
81
+ });
82
+ if (!transport.ok) {
83
+ return { ok: false, error: transport.error, logs };
84
+ }
85
+
86
+ const client = new Client({
87
+ name: 'agentxchain-mcp-adapter',
88
+ version: '1.0.0',
89
+ });
90
+ client.onerror = (error) => {
91
+ logs.push(`[client-error] ${error.message}`);
92
+ };
93
+ transport.onerror = (error) => {
94
+ logs.push(`[transport-error] ${error.message}`);
95
+ };
96
+
97
+ try {
98
+ if (signal?.aborted) {
99
+ return { ok: false, aborted: true, logs };
100
+ }
101
+
102
+ onStatus?.(`Connecting to MCP ${transportType} server (${describeMcpRuntimeTarget(runtime)})`);
103
+ await client.connect(transport.transport);
104
+
105
+ if (signal?.aborted) {
106
+ return { ok: false, aborted: true, logs };
107
+ }
108
+
109
+ onStatus?.(`Listing MCP tools`);
110
+ const toolsResult = await client.listTools(undefined, {
111
+ signal,
112
+ timeout: timeoutMs,
113
+ maxTotalTimeout: timeoutMs,
114
+ });
115
+ const toolNames = toolsResult.tools.map((tool) => tool.name);
116
+ if (!toolNames.includes(toolName)) {
117
+ return {
118
+ ok: false,
119
+ error: `MCP server does not expose required tool "${toolName}". Available tools: ${toolNames.join(', ') || '(none)'}`,
120
+ logs,
121
+ };
122
+ }
123
+
124
+ if (signal?.aborted) {
125
+ return { ok: false, aborted: true, logs };
126
+ }
127
+
128
+ onStatus?.(`Calling MCP tool "${toolName}"`);
129
+ const toolResult = await client.callTool({
130
+ name: toolName,
131
+ arguments: {
132
+ run_id: state.run_id,
133
+ turn_id: turn.turn_id,
134
+ role: turn.assigned_role,
135
+ phase: state.phase,
136
+ runtime_id: runtimeId,
137
+ project_root: root,
138
+ dispatch_dir: join(root, getDispatchTurnDir(turn.turn_id)),
139
+ assignment_path: join(root, getDispatchAssignmentPath(turn.turn_id)),
140
+ prompt_path: promptPath,
141
+ context_path: contextPath,
142
+ staging_path: join(root, getTurnStagingResultPath(turn.turn_id)),
143
+ prompt,
144
+ context,
145
+ },
146
+ }, undefined, {
147
+ signal,
148
+ timeout: timeoutMs,
149
+ maxTotalTimeout: timeoutMs,
150
+ resetTimeoutOnProgress: true,
151
+ });
152
+
153
+ if (toolResult?.isError) {
154
+ return {
155
+ ok: false,
156
+ error: buildMcpToolError(toolName, toolResult),
157
+ logs,
158
+ };
159
+ }
160
+
161
+ const turnResult = extractTurnResultFromMcpToolResult(toolResult);
162
+ if (!turnResult.ok) {
163
+ return { ok: false, error: turnResult.error, logs };
164
+ }
165
+
166
+ const stagingPath = join(root, getTurnStagingResultPath(turn.turn_id));
167
+ writeFileSync(stagingPath, JSON.stringify(turnResult.result, null, 2));
168
+
169
+ return {
170
+ ok: true,
171
+ toolName,
172
+ logs,
173
+ };
174
+ } catch (error) {
175
+ if (signal?.aborted) {
176
+ return { ok: false, aborted: true, logs };
177
+ }
178
+ return {
179
+ ok: false,
180
+ error: `MCP dispatch failed: ${error.message}`,
181
+ logs,
182
+ };
183
+ } finally {
184
+ await safeCloseClient(client, transport.transport);
185
+ }
186
+ }
187
+
188
+ export function resolveMcpCommand(runtime) {
189
+ if (!runtime?.command) return { command: null, args: [] };
190
+
191
+ if (Array.isArray(runtime.command)) {
192
+ const [command, ...args] = runtime.command;
193
+ return { command, args };
194
+ }
195
+
196
+ return {
197
+ command: runtime.command,
198
+ args: Array.isArray(runtime.args) ? runtime.args : [],
199
+ };
200
+ }
201
+
202
+ export function resolveMcpToolName(runtime) {
203
+ return typeof runtime?.tool_name === 'string' && runtime.tool_name.trim()
204
+ ? runtime.tool_name.trim()
205
+ : DEFAULT_MCP_TOOL_NAME;
206
+ }
207
+
208
+ export function resolveMcpTransport(runtime) {
209
+ return typeof runtime?.transport === 'string' && runtime.transport.trim()
210
+ ? runtime.transport.trim()
211
+ : DEFAULT_MCP_TRANSPORT;
212
+ }
213
+
214
+ export function describeMcpRuntimeTarget(runtime) {
215
+ return resolveMcpTransport(runtime) === 'streamable_http'
216
+ ? runtime?.url || '(unknown)'
217
+ : resolveMcpCommand(runtime).command || '(unknown)';
218
+ }
219
+
220
+ export function extractTurnResultFromMcpToolResult(toolResult) {
221
+ const directCandidates = [
222
+ toolResult?.structuredContent,
223
+ toolResult?.toolResult?.structuredContent,
224
+ toolResult?.toolResult,
225
+ ];
226
+
227
+ for (const candidate of directCandidates) {
228
+ if (looksLikeTurnResult(candidate)) {
229
+ return { ok: true, result: candidate };
230
+ }
231
+ }
232
+
233
+ const textBlocks = [
234
+ ...(Array.isArray(toolResult?.content) ? toolResult.content : []),
235
+ ...(Array.isArray(toolResult?.toolResult?.content) ? toolResult.toolResult.content : []),
236
+ ].filter((item) => item?.type === 'text' && typeof item.text === 'string');
237
+
238
+ for (const block of textBlocks) {
239
+ const parsed = tryParseJson(block.text);
240
+ if (looksLikeTurnResult(parsed) || isPlainObject(parsed)) {
241
+ return { ok: true, result: parsed };
242
+ }
243
+ }
244
+
245
+ if (textBlocks.length === 0) {
246
+ return {
247
+ ok: false,
248
+ error: 'MCP tool returned no structuredContent and no text content blocks containing a turn result.',
249
+ };
250
+ }
251
+
252
+ return {
253
+ ok: false,
254
+ error: 'MCP tool returned text content, but none of the text blocks contained valid turn-result JSON.',
255
+ };
256
+ }
257
+
258
+ function buildMcpToolError(toolName, toolResult) {
259
+ const text = Array.isArray(toolResult?.content)
260
+ ? toolResult.content
261
+ .filter((item) => item?.type === 'text' && typeof item.text === 'string')
262
+ .map((item) => item.text.trim())
263
+ .filter(Boolean)
264
+ .join(' ')
265
+ : '';
266
+ if (text) {
267
+ return `MCP tool "${toolName}" returned an error: ${text}`;
268
+ }
269
+ return `MCP tool "${toolName}" returned an error without diagnostic text.`;
270
+ }
271
+
272
+ function buildTransportEnv(env) {
273
+ const result = {};
274
+ for (const [key, value] of Object.entries(env || {})) {
275
+ if (typeof value === 'string') {
276
+ result[key] = value;
277
+ }
278
+ }
279
+ return result;
280
+ }
281
+
282
+ function buildMcpClientTransport({ root, runtime, command, args, logs, onStderr }) {
283
+ if (resolveMcpTransport(runtime) === 'streamable_http') {
284
+ try {
285
+ const requestHeaders = buildRequestHeaders(runtime?.headers);
286
+ return {
287
+ ok: true,
288
+ transport: new StreamableHTTPClientTransport(new URL(runtime.url), {
289
+ requestInit: requestHeaders ? { headers: requestHeaders } : undefined,
290
+ }),
291
+ };
292
+ } catch (error) {
293
+ logs.push(`[transport-error] ${error.message}`);
294
+ return {
295
+ ok: false,
296
+ error: `Cannot resolve MCP streamable_http runtime: ${error.message}`,
297
+ };
298
+ }
299
+ }
300
+
301
+ const transport = new StdioClientTransport({
302
+ command,
303
+ args,
304
+ cwd: runtime.cwd ? join(root, runtime.cwd) : root,
305
+ env: buildTransportEnv(process.env),
306
+ stderr: 'pipe',
307
+ });
308
+
309
+ if (transport.stderr) {
310
+ transport.stderr.on('data', (chunk) => {
311
+ const text = chunk.toString();
312
+ logs.push(`[stderr] ${text}`);
313
+ if (onStderr) onStderr(text);
314
+ });
315
+ }
316
+
317
+ return { ok: true, transport };
318
+ }
319
+
320
+ function buildRequestHeaders(headers) {
321
+ if (!headers || typeof headers !== 'object' || Array.isArray(headers)) {
322
+ return null;
323
+ }
324
+
325
+ const result = {};
326
+ for (const [key, value] of Object.entries(headers)) {
327
+ if (typeof value === 'string') {
328
+ result[key] = value;
329
+ }
330
+ }
331
+ return Object.keys(result).length > 0 ? result : null;
332
+ }
333
+
334
+ function isPlainObject(value) {
335
+ return !!value && typeof value === 'object' && !Array.isArray(value);
336
+ }
337
+
338
+ function looksLikeTurnResult(value) {
339
+ if (!isPlainObject(value)) return false;
340
+ const hasIdentity = 'run_id' in value || 'turn_id' in value;
341
+ const hasLifecycle = 'status' in value || 'role' in value || 'runtime_id' in value;
342
+ return hasIdentity && hasLifecycle;
343
+ }
344
+
345
+ function tryParseJson(value) {
346
+ try {
347
+ return JSON.parse(value);
348
+ } catch {
349
+ return null;
350
+ }
351
+ }
352
+
353
+ function resolveTargetTurn(state, turnId) {
354
+ if (turnId && state?.active_turns?.[turnId]) {
355
+ return state.active_turns[turnId];
356
+ }
357
+ return state?.current_turn || Object.values(state?.active_turns || {})[0];
358
+ }
359
+
360
+ async function safeCloseClient(client, transport) {
361
+ try {
362
+ await client.close();
363
+ return;
364
+ } catch {}
365
+
366
+ try {
367
+ await transport.close();
368
+ } catch {}
369
+ }
@@ -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,9 +16,10 @@ 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
+ const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
22
23
  const VALID_PHASES = ['planning', 'implementation', 'qa'];
23
24
  const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
24
25
  const VALID_API_PROXY_RETRY_CLASSES = [
@@ -46,6 +47,92 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
46
47
  'safety_margin_tokens',
47
48
  ];
48
49
 
50
+ function validateMcpRuntime(runtimeId, runtime, errors) {
51
+ const transport = typeof runtime?.transport === 'string' && runtime.transport.trim()
52
+ ? runtime.transport.trim()
53
+ : 'stdio';
54
+ const command = runtime?.command;
55
+
56
+ if (!VALID_MCP_TRANSPORTS.includes(transport)) {
57
+ errors.push(`Runtime "${runtimeId}": mcp transport must be one of: ${VALID_MCP_TRANSPORTS.join(', ')}`);
58
+ }
59
+
60
+ if ('tool_name' in runtime && (typeof runtime.tool_name !== 'string' || !runtime.tool_name.trim())) {
61
+ errors.push(`Runtime "${runtimeId}": mcp tool_name must be a non-empty string`);
62
+ }
63
+
64
+ if (transport === 'streamable_http') {
65
+ if (typeof runtime?.url !== 'string' || !runtime.url.trim()) {
66
+ errors.push(`Runtime "${runtimeId}": mcp streamable_http requires "url"`);
67
+ } else {
68
+ try {
69
+ const parsed = new URL(runtime.url);
70
+ if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
71
+ errors.push(`Runtime "${runtimeId}": mcp url must use http or https`);
72
+ }
73
+ } catch {
74
+ errors.push(`Runtime "${runtimeId}": mcp url must be a valid absolute URL`);
75
+ }
76
+ }
77
+
78
+ if ('headers' in runtime) {
79
+ if (!runtime.headers || typeof runtime.headers !== 'object' || Array.isArray(runtime.headers)) {
80
+ errors.push(`Runtime "${runtimeId}": mcp headers must be an object of string values`);
81
+ } else {
82
+ for (const [key, value] of Object.entries(runtime.headers)) {
83
+ if (typeof value !== 'string') {
84
+ errors.push(`Runtime "${runtimeId}": mcp headers["${key}"] must be a string`);
85
+ }
86
+ }
87
+ }
88
+ }
89
+
90
+ if ('command' in runtime) {
91
+ errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "command"`);
92
+ }
93
+
94
+ if ('args' in runtime) {
95
+ errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "args"`);
96
+ }
97
+
98
+ if ('cwd' in runtime) {
99
+ errors.push(`Runtime "${runtimeId}": mcp streamable_http does not accept "cwd"`);
100
+ }
101
+
102
+ return;
103
+ }
104
+
105
+ if (typeof command === 'string') {
106
+ if (!command.trim()) {
107
+ errors.push(`Runtime "${runtimeId}": mcp command must be a non-empty string`);
108
+ }
109
+ } else if (Array.isArray(command)) {
110
+ if (command.length === 0 || command.some((part) => typeof part !== 'string' || !part.trim())) {
111
+ errors.push(`Runtime "${runtimeId}": mcp command array must contain at least one non-empty string and no empty parts`);
112
+ }
113
+ } else {
114
+ errors.push(`Runtime "${runtimeId}": mcp requires "command" as a string or string array`);
115
+ }
116
+
117
+ if ('args' in runtime) {
118
+ if (!Array.isArray(runtime.args) || runtime.args.some((part) => typeof part !== 'string')) {
119
+ errors.push(`Runtime "${runtimeId}": mcp args must be an array of strings`);
120
+ }
121
+ }
122
+
123
+ if ('cwd' in runtime && (typeof runtime.cwd !== 'string' || !runtime.cwd.trim())) {
124
+ errors.push(`Runtime "${runtimeId}": mcp cwd must be a non-empty string`);
125
+ }
126
+
127
+ if ('url' in runtime) {
128
+ errors.push(`Runtime "${runtimeId}": mcp stdio does not accept "url"`);
129
+ }
130
+
131
+ if ('headers' in runtime) {
132
+ errors.push(`Runtime "${runtimeId}": mcp stdio does not accept "headers"`);
133
+ }
134
+ }
135
+
49
136
  function validateApiProxyRetryPolicy(runtimeId, retryPolicy, errors) {
50
137
  if (!retryPolicy || typeof retryPolicy !== 'object' || Array.isArray(retryPolicy)) {
51
138
  errors.push(`Runtime "${runtimeId}": retry_policy must be an object`);
@@ -263,6 +350,9 @@ export function validateV4Config(data, projectRoot) {
263
350
  validateApiProxyPreflightTokenization(id, rt, errors);
264
351
  }
265
352
  }
353
+ if (rt.type === 'mcp') {
354
+ validateMcpRuntime(id, rt, errors);
355
+ }
266
356
  }
267
357
  }
268
358
 
@@ -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
- * - local_cli + pass + machine evidence all zero → pass
274
- * - local_cli + pass + no reproducible evidence → not_reproducible
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: 'local_cli turn provided machine evidence with zero exit codes', reproducible: true };
307
+ return { status: 'pass', reason: `${runtimeType} turn provided machine evidence with zero exit codes`, reproducible: true };
308
308
  }
309
- return { status: 'not_reproducible', reason: 'local_cli turn has machine evidence with non-zero exit codes despite claiming pass', reproducible: false };
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
- // local_cli + pass but no machine evidence
313
- return { status: 'not_reproducible', reason: 'local_cli turn claimed pass but provided no machine evidence', reproducible: false };
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 ─────────────────────────────────────────
@@ -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
+ }