agentxchain 2.2.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.
@@ -13,9 +13,11 @@
13
13
  */
14
14
 
15
15
  import { validateHooksConfig } from './hook-runner.js';
16
+ import { SUPPORTED_TOKEN_COUNTER_PROVIDERS } from './token-counter.js';
16
17
 
17
18
  const VALID_WRITE_AUTHORITIES = ['authoritative', 'proposed', 'review_only'];
18
- const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy'];
19
+ const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy', 'mcp'];
20
+ const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
19
21
  const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
20
22
  const VALID_PHASES = ['planning', 'implementation', 'qa'];
21
23
  const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
@@ -44,6 +46,36 @@ const VALID_API_PROXY_PREFLIGHT_FIELDS = [
44
46
  'safety_margin_tokens',
45
47
  ];
46
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
+
47
79
  function validateApiProxyRetryPolicy(runtimeId, retryPolicy, errors) {
48
80
  if (!retryPolicy || typeof retryPolicy !== 'object' || Array.isArray(retryPolicy)) {
49
81
  errors.push(`Runtime "${runtimeId}": retry_policy must be an object`);
@@ -146,6 +178,12 @@ function validateApiProxyPreflightTokenization(runtimeId, runtime, errors) {
146
178
  }
147
179
 
148
180
  if (preflight.enabled === true) {
181
+ if (!SUPPORTED_TOKEN_COUNTER_PROVIDERS.includes(runtime.provider)) {
182
+ errors.push(
183
+ `Runtime "${runtimeId}": preflight_tokenization tokenizer "provider_local" is not supported for provider "${runtime.provider}". Supported providers: ${SUPPORTED_TOKEN_COUNTER_PROVIDERS.join(', ')}`
184
+ );
185
+ }
186
+
149
187
  if (!Number.isInteger(runtime.context_window_tokens) || runtime.context_window_tokens <= 0) {
150
188
  errors.push(`Runtime "${runtimeId}": context_window_tokens is required when preflight_tokenization.enabled is true`);
151
189
  return;
@@ -239,6 +277,8 @@ export function validateV4Config(data, projectRoot) {
239
277
  if (rt.type === 'api_proxy') {
240
278
  if (typeof rt.provider !== 'string' || !rt.provider.trim()) {
241
279
  errors.push(`Runtime "${id}": api_proxy requires "provider" (e.g. "anthropic", "openai")`);
280
+ } else if (!VALID_API_PROXY_PROVIDERS.includes(rt.provider)) {
281
+ errors.push(`Runtime "${id}": api_proxy provider must be one of: ${VALID_API_PROXY_PROVIDERS.join(', ')}`);
242
282
  }
243
283
  if (typeof rt.model !== 'string' || !rt.model.trim()) {
244
284
  errors.push(`Runtime "${id}": api_proxy requires "model" (e.g. "claude-sonnet-4-6")`);
@@ -253,6 +293,9 @@ export function validateV4Config(data, projectRoot) {
253
293
  validateApiProxyPreflightTokenization(id, rt, errors);
254
294
  }
255
295
  }
296
+ if (rt.type === 'mcp') {
297
+ validateMcpRuntime(id, rt, errors);
298
+ }
256
299
  }
257
300
  }
258
301
 
@@ -5,7 +5,7 @@ import { fileURLToPath } from 'node:url';
5
5
 
6
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
7
7
  const DEFAULT_FIXTURE_ROOT = resolve(__dirname, '..', '..', '..', '.agentxchain-conformance', 'fixtures');
8
- const VALID_RESPONSE_STATUSES = new Set(['pass', 'fail', 'error']);
8
+ const VALID_RESPONSE_STATUSES = new Set(['pass', 'fail', 'error', 'not_implemented']);
9
9
  const VALID_TIERS = new Set([1, 2, 3]);
10
10
 
11
11
  function readJsonFile(filePath) {
@@ -119,16 +119,18 @@ function createTierSummary(status = 'skipped', note = null) {
119
119
  fixtures_passed: 0,
120
120
  fixtures_failed: 0,
121
121
  fixtures_errored: 0,
122
+ fixtures_not_implemented: 0,
122
123
  surfaces: {},
123
124
  failures: [],
124
125
  errors: [],
126
+ not_implemented: [],
125
127
  ...(note ? { note } : {}),
126
128
  };
127
129
  }
128
130
 
129
131
  function ensureSurfaceSummary(tierSummary, surface) {
130
132
  if (!tierSummary.surfaces[surface]) {
131
- tierSummary.surfaces[surface] = { passed: 0, failed: 0, errored: 0 };
133
+ tierSummary.surfaces[surface] = { passed: 0, failed: 0, errored: 0, not_implemented: 0 };
132
134
  }
133
135
  return tierSummary.surfaces[surface];
134
136
  }
@@ -149,7 +151,7 @@ function executeFixture(targetRoot, adapterCommand, fixture) {
149
151
  };
150
152
  }
151
153
 
152
- if (![0, 1, 2].includes(result.status ?? -1)) {
154
+ if (![0, 1, 2, 3].includes(result.status ?? -1)) {
153
155
  return {
154
156
  status: 'error',
155
157
  message: `Adapter exited with unsupported status ${result.status}`,
@@ -182,7 +184,7 @@ function executeFixture(targetRoot, adapterCommand, fixture) {
182
184
  };
183
185
  }
184
186
 
185
- const expectedExitCode = parsed.status === 'pass' ? 0 : parsed.status === 'fail' ? 1 : 2;
187
+ const expectedExitCode = parsed.status === 'pass' ? 0 : parsed.status === 'fail' ? 1 : parsed.status === 'not_implemented' ? 3 : 2;
186
188
  if (result.status !== expectedExitCode) {
187
189
  return {
188
190
  status: 'error',
@@ -210,6 +212,17 @@ export function verifyProtocolConformance({
210
212
 
211
213
  const resolvedTargetRoot = resolve(targetRoot);
212
214
  const capabilities = loadCapabilities(resolvedTargetRoot);
215
+
216
+ // Enforce surface claims when capabilities.surfaces exists and --surface is requested
217
+ if (surface && capabilities.surfaces && typeof capabilities.surfaces === 'object') {
218
+ if (!capabilities.surfaces[surface]) {
219
+ throw new Error(
220
+ `Surface "${surface}" is not claimed in capabilities.json. ` +
221
+ `Claimed surfaces: ${Object.keys(capabilities.surfaces).join(', ')}`
222
+ );
223
+ }
224
+ }
225
+
213
226
  const fixtureEntries = selectFixtureFiles(fixtureRoot, requestedTier, surface);
214
227
  const claimedTiers = new Set(capabilities.tiers);
215
228
  const report = {
@@ -250,6 +263,17 @@ export function verifyProtocolConformance({
250
263
  continue;
251
264
  }
252
265
 
266
+ if (adapterResult.status === 'not_implemented') {
267
+ tierSummary.fixtures_not_implemented += 1;
268
+ surfaceSummary.not_implemented += 1;
269
+ tierSummary.not_implemented.push({
270
+ fixture_id: fixture.fixture_id,
271
+ surface: fixture.surface,
272
+ message: adapterResult.message || 'Not implemented',
273
+ });
274
+ continue;
275
+ }
276
+
253
277
  if (adapterResult.status === 'fail') {
254
278
  tierSummary.fixtures_failed += 1;
255
279
  surfaceSummary.failed += 1;
@@ -25,6 +25,7 @@ import { join } from 'path';
25
25
  const OPERATIONAL_PATH_PREFIXES = [
26
26
  '.agentxchain/dispatch/',
27
27
  '.agentxchain/staging/',
28
+ '.agentxchain/intake/',
28
29
  '.agentxchain/locks/',
29
30
  '.agentxchain/transactions/',
30
31
  ];
@@ -269,13 +270,13 @@ export function buildObservedArtifact(observation, baseline) {
269
270
  *
270
271
  * Normalization rules (spec §5.3):
271
272
  * - manual + external pass → attested_pass
272
- * - local_cli + pass + machine evidence all zero → pass
273
- * - 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
274
275
  * - any external fail → fail
275
276
  * - external skipped → skipped
276
277
  *
277
278
  * @param {object} verification — the actor-supplied verification object
278
- * @param {string} runtimeType — 'manual' | 'local_cli' | 'api_proxy'
279
+ * @param {string} runtimeType — 'manual' | 'local_cli' | 'api_proxy' | 'mcp'
279
280
  * @returns {{ status: string, reason: string, reproducible: boolean }}
280
281
  */
281
282
  export function normalizeVerification(verification, runtimeType) {
@@ -298,18 +299,18 @@ export function normalizeVerification(verification, runtimeType) {
298
299
  return { status: 'attested_pass', reason: 'API proxy runtime — no direct execution environment', reproducible: false };
299
300
  }
300
301
 
301
- // local_cli — check for machine evidence
302
+ // local_cli / mcp — check for machine evidence
302
303
  const evidence = verification?.machine_evidence;
303
304
  if (Array.isArray(evidence) && evidence.length > 0) {
304
305
  const allZero = evidence.every(e => typeof e.exit_code === 'number' && e.exit_code === 0);
305
306
  if (allZero) {
306
- 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 };
307
308
  }
308
- 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 };
309
310
  }
310
311
 
311
- // local_cli + pass but no machine evidence
312
- 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 };
313
314
  }
314
315
 
315
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
+ }