agentxchain 2.1.1 → 2.3.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
19
  const VALID_RUNTIME_TYPES = ['manual', 'local_cli', 'api_proxy'];
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'];
@@ -146,6 +148,12 @@ function validateApiProxyPreflightTokenization(runtimeId, runtime, errors) {
146
148
  }
147
149
 
148
150
  if (preflight.enabled === true) {
151
+ if (!SUPPORTED_TOKEN_COUNTER_PROVIDERS.includes(runtime.provider)) {
152
+ errors.push(
153
+ `Runtime "${runtimeId}": preflight_tokenization tokenizer "provider_local" is not supported for provider "${runtime.provider}". Supported providers: ${SUPPORTED_TOKEN_COUNTER_PROVIDERS.join(', ')}`
154
+ );
155
+ }
156
+
149
157
  if (!Number.isInteger(runtime.context_window_tokens) || runtime.context_window_tokens <= 0) {
150
158
  errors.push(`Runtime "${runtimeId}": context_window_tokens is required when preflight_tokenization.enabled is true`);
151
159
  return;
@@ -239,6 +247,8 @@ export function validateV4Config(data, projectRoot) {
239
247
  if (rt.type === 'api_proxy') {
240
248
  if (typeof rt.provider !== 'string' || !rt.provider.trim()) {
241
249
  errors.push(`Runtime "${id}": api_proxy requires "provider" (e.g. "anthropic", "openai")`);
250
+ } else if (!VALID_API_PROXY_PROVIDERS.includes(rt.provider)) {
251
+ errors.push(`Runtime "${id}": api_proxy provider must be one of: ${VALID_API_PROXY_PROVIDERS.join(', ')}`);
242
252
  }
243
253
  if (typeof rt.model !== 'string' || !rt.model.trim()) {
244
254
  errors.push(`Runtime "${id}": api_proxy requires "model" (e.g. "claude-sonnet-4-6")`);
@@ -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;
@@ -13,6 +13,9 @@ import { validateStagedTurnResult } from './turn-result-validator.js';
13
13
  import { finalizeDispatchManifest, verifyDispatchManifest } from './dispatch-manifest.js';
14
14
  import { getDispatchTurnDir } from './turn-paths.js';
15
15
  import { runHooks } from './hook-runner.js';
16
+ import { validateCoordinatorConfig, normalizeCoordinatorConfig } from './coordinator-config.js';
17
+ import { projectRepoAcceptance, evaluateBarriers } from './coordinator-acceptance.js';
18
+ import { readBarriers, saveCoordinatorState, readCoordinatorHistory } from './coordinator-state.js';
16
19
 
17
20
  const VALID_DECISION_CATEGORIES = ['implementation', 'architecture', 'scope', 'process', 'quality', 'release'];
18
21
  const FULL_STAGE_PIPELINE = ['schema', 'assignment', 'artifact', 'verification', 'protocol'];
@@ -213,6 +216,69 @@ function materializeFixtureWorkspace(fixture) {
213
216
  };
214
217
  }
215
218
 
219
+ // ── Tier 3: Multi-workspace materialization ─────────────────────────────────
220
+
221
+ function materializeTier3Workspace(fixture) {
222
+ const root = mkdtempSync(join(tmpdir(), 'agentxchain-conformance-multi-'));
223
+ const setup = fixture.setup || {};
224
+
225
+ // Write coordinator config
226
+ if (setup.coordinator_config) {
227
+ writeJson(join(root, 'agentxchain-multi.json'), setup.coordinator_config);
228
+ }
229
+
230
+ // Materialize governed repo roots
231
+ for (const [repoId, repoSetup] of Object.entries(setup.repos || {})) {
232
+ const repoPath = join(root, repoSetup.path || `./repos/${repoId}`);
233
+ mkdirSync(join(repoPath, '.agentxchain'), { recursive: true });
234
+
235
+ // Write repo-local agentxchain.json
236
+ if (repoSetup.config) {
237
+ writeJson(join(repoPath, 'agentxchain.json'), repoSetup.config);
238
+ }
239
+
240
+ // Write repo-local state
241
+ if (repoSetup.state) {
242
+ const repoConfig = repoSetup.config || {};
243
+ const inflatedRepoConfig = inflateConfig(repoConfig);
244
+ const repoState = inflateState(repoSetup.state, inflatedRepoConfig);
245
+ writeJson(join(repoPath, '.agentxchain', 'state.json'), repoState);
246
+ }
247
+
248
+ // Write repo-local history
249
+ if (repoSetup.history) {
250
+ writeJsonl(join(repoPath, '.agentxchain', 'history.jsonl'), repoSetup.history);
251
+ }
252
+
253
+ // Write repo-local files
254
+ for (const [filePath, content] of Object.entries(repoSetup.files || {})) {
255
+ const absPath = join(repoPath, filePath);
256
+ mkdirSync(dirname(absPath), { recursive: true });
257
+ writeFileSync(absPath, content);
258
+ }
259
+ }
260
+
261
+ // Write coordinator multirepo state if provided
262
+ if (setup.coordinator_state || setup.barriers) {
263
+ const multiDir = join(root, '.agentxchain', 'multirepo');
264
+ mkdirSync(multiDir, { recursive: true });
265
+
266
+ if (setup.coordinator_state) {
267
+ writeJson(join(multiDir, 'state.json'), setup.coordinator_state);
268
+ }
269
+
270
+ if (setup.barriers) {
271
+ writeJson(join(multiDir, 'barriers.json'), setup.barriers);
272
+ }
273
+
274
+ // Write coordinator history
275
+ writeJsonl(join(multiDir, 'history.jsonl'), setup.coordinator_history || []);
276
+ writeJsonl(join(multiDir, 'barrier-ledger.jsonl'), setup.barrier_ledger || []);
277
+ }
278
+
279
+ return root;
280
+ }
281
+
216
282
  function isAssertionObject(value) {
217
283
  return value && typeof value === 'object' && !Array.isArray(value) && typeof value.assert === 'string';
218
284
  }
@@ -699,6 +765,74 @@ function executeFixtureOperation(workspace, fixture) {
699
765
  }
700
766
  }
701
767
 
768
+ function executeTier3Operation(fixture) {
769
+ const root = materializeTier3Workspace(fixture);
770
+ try {
771
+ const operation = fixture.input.operation;
772
+
773
+ if (operation === 'validate_coordinator_config') {
774
+ const configPath = join(root, 'agentxchain-multi.json');
775
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
776
+ const validation = validateCoordinatorConfig(raw);
777
+ if (!validation.ok) {
778
+ const firstError = validation.errors[0] || '';
779
+ let errorType = 'invalid_coordinator_config';
780
+ if (firstError.startsWith('workstream_cycle:')) {
781
+ errorType = 'workstream_cycle';
782
+ }
783
+ return { result: 'error', error_type: errorType, errors: validation.errors };
784
+ }
785
+ return { result: 'success', errors: [] };
786
+ }
787
+
788
+ if (operation === 'project_repo_acceptance') {
789
+ const args = fixture.input.args;
790
+ const configPath = join(root, 'agentxchain-multi.json');
791
+ const raw = JSON.parse(readFileSync(configPath, 'utf8'));
792
+ const normalized = normalizeCoordinatorConfig(raw);
793
+
794
+ // Resolve repo paths against the materialized workspace
795
+ for (const [repoId, repo] of Object.entries(normalized.repos)) {
796
+ const resolvedPath = join(root, repo.path);
797
+ normalized.repos[repoId] = { ...repo, resolved_path: resolvedPath };
798
+ }
799
+ normalized.repo_order = Object.keys(normalized.repos);
800
+
801
+ const stateDir = join(root, '.agentxchain', 'multirepo');
802
+ const state = JSON.parse(readFileSync(join(stateDir, 'state.json'), 'utf8'));
803
+
804
+ const projectionResult = projectRepoAcceptance(
805
+ root, state, normalized,
806
+ args.repo_id, args.accepted_turn, args.workstream_id,
807
+ );
808
+
809
+ if (!projectionResult.ok) {
810
+ let errorType = 'projection_failed';
811
+ if (projectionResult.error && projectionResult.error.includes('Cross-repo write violation')) {
812
+ errorType = 'cross_repo_write_violation';
813
+ }
814
+ return { result: 'error', error_type: errorType, error: projectionResult.error };
815
+ }
816
+
817
+ // Evaluate barriers after projection
818
+ const barrierResult = evaluateBarriers(root, state, normalized);
819
+
820
+ return {
821
+ result: 'success',
822
+ projection_ref: projectionResult.projection_ref,
823
+ barrier_effects: projectionResult.barrier_effects || [],
824
+ barrier_snapshot: Object.fromEntries(
825
+ Object.entries(barrierResult.barriers).map(([id, b]) => [id, { status: b.status }]),
826
+ ),
827
+ };
828
+ }
829
+
830
+ return { result: 'error', error_type: 'unsupported_operation', operation };
831
+ } finally {
832
+ rmSync(root, { recursive: true, force: true });
833
+ }
834
+ }
835
+
702
836
  function compareActualToExpected(fixture, actual) {
703
837
  if (matchExpected(fixture.expected, actual)) {
704
838
  return buildPass(actual);
@@ -707,6 +841,13 @@ function compareActualToExpected(fixture, actual) {
707
841
  }
708
842
 
709
843
  export function runReferenceFixture(fixture) {
844
+ // Tier 3 fixtures use multi-workspace materialization; route before creating a Tier 1/2 workspace
845
+ const operation = fixture.input?.operation;
846
+ if (operation === 'validate_coordinator_config' || operation === 'project_repo_acceptance') {
847
+ const actual = executeTier3Operation(fixture);
848
+ return compareActualToExpected(fixture, actual);
849
+ }
850
+
710
851
  const workspace = materializeFixtureWorkspace(fixture);
711
852
  try {
712
853
  const actual = executeFixtureOperation(workspace, fixture);
@@ -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
  ];