agentxchain 0.8.7 → 2.1.1

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.
Files changed (94) hide show
  1. package/README.md +123 -154
  2. package/bin/agentxchain.js +240 -8
  3. package/dashboard/app.js +305 -0
  4. package/dashboard/components/blocked.js +145 -0
  5. package/dashboard/components/cross-repo.js +126 -0
  6. package/dashboard/components/gate.js +311 -0
  7. package/dashboard/components/hooks.js +177 -0
  8. package/dashboard/components/initiative.js +147 -0
  9. package/dashboard/components/ledger.js +165 -0
  10. package/dashboard/components/timeline.js +222 -0
  11. package/dashboard/index.html +352 -0
  12. package/package.json +16 -7
  13. package/scripts/agentxchain-autonudge.applescript +32 -5
  14. package/scripts/live-api-proxy-preflight-smoke.sh +531 -0
  15. package/scripts/publish-from-tag.sh +88 -0
  16. package/scripts/release-postflight.sh +231 -0
  17. package/scripts/release-preflight.sh +167 -0
  18. package/scripts/run-autonudge.sh +1 -1
  19. package/src/adapters/claude-code.js +7 -14
  20. package/src/adapters/cursor-local.js +17 -16
  21. package/src/commands/accept-turn.js +160 -0
  22. package/src/commands/approve-completion.js +80 -0
  23. package/src/commands/approve-transition.js +85 -0
  24. package/src/commands/branch.js +2 -2
  25. package/src/commands/claim.js +84 -9
  26. package/src/commands/config.js +16 -0
  27. package/src/commands/dashboard.js +70 -0
  28. package/src/commands/doctor.js +9 -1
  29. package/src/commands/init.js +540 -5
  30. package/src/commands/migrate.js +348 -0
  31. package/src/commands/multi.js +549 -0
  32. package/src/commands/plugin.js +157 -0
  33. package/src/commands/reject-turn.js +204 -0
  34. package/src/commands/resume.js +389 -0
  35. package/src/commands/status.js +196 -3
  36. package/src/commands/step.js +947 -0
  37. package/src/commands/stop.js +65 -33
  38. package/src/commands/template-list.js +33 -0
  39. package/src/commands/template-set.js +279 -0
  40. package/src/commands/update.js +24 -3
  41. package/src/commands/validate.js +20 -11
  42. package/src/commands/verify.js +71 -0
  43. package/src/commands/watch.js +112 -25
  44. package/src/lib/adapters/api-proxy-adapter.js +1076 -0
  45. package/src/lib/adapters/local-cli-adapter.js +337 -0
  46. package/src/lib/adapters/manual-adapter.js +169 -0
  47. package/src/lib/blocked-state.js +94 -0
  48. package/src/lib/config.js +143 -12
  49. package/src/lib/context-compressor.js +121 -0
  50. package/src/lib/context-section-parser.js +220 -0
  51. package/src/lib/coordinator-acceptance.js +428 -0
  52. package/src/lib/coordinator-config.js +461 -0
  53. package/src/lib/coordinator-dispatch.js +276 -0
  54. package/src/lib/coordinator-gates.js +487 -0
  55. package/src/lib/coordinator-hooks.js +239 -0
  56. package/src/lib/coordinator-recovery.js +523 -0
  57. package/src/lib/coordinator-state.js +365 -0
  58. package/src/lib/cross-repo-context.js +247 -0
  59. package/src/lib/dashboard/bridge-server.js +284 -0
  60. package/src/lib/dashboard/file-watcher.js +93 -0
  61. package/src/lib/dashboard/state-reader.js +96 -0
  62. package/src/lib/dispatch-bundle.js +568 -0
  63. package/src/lib/dispatch-manifest.js +252 -0
  64. package/src/lib/filter-agents.js +12 -0
  65. package/src/lib/gate-evaluator.js +285 -0
  66. package/src/lib/generate-vscode.js +158 -68
  67. package/src/lib/governed-state.js +2139 -0
  68. package/src/lib/governed-templates.js +145 -0
  69. package/src/lib/hook-runner.js +788 -0
  70. package/src/lib/next-owner.js +61 -6
  71. package/src/lib/normalized-config.js +539 -0
  72. package/src/lib/notify.js +14 -12
  73. package/src/lib/plugin-config-schema.js +192 -0
  74. package/src/lib/plugins.js +692 -0
  75. package/src/lib/prompt-core.js +108 -0
  76. package/src/lib/protocol-conformance.js +291 -0
  77. package/src/lib/reference-conformance-adapter.js +717 -0
  78. package/src/lib/repo-observer.js +597 -0
  79. package/src/lib/repo.js +0 -31
  80. package/src/lib/safe-write.js +44 -0
  81. package/src/lib/schema.js +189 -0
  82. package/src/lib/schemas/turn-result.schema.json +205 -0
  83. package/src/lib/seed-prompt-polling.js +15 -73
  84. package/src/lib/seed-prompt.js +17 -63
  85. package/src/lib/token-budget.js +206 -0
  86. package/src/lib/token-counter.js +27 -0
  87. package/src/lib/turn-paths.js +67 -0
  88. package/src/lib/turn-result-validator.js +496 -0
  89. package/src/lib/validation.js +167 -19
  90. package/src/lib/verify-command.js +72 -0
  91. package/src/templates/governed/api-service.json +31 -0
  92. package/src/templates/governed/cli-tool.json +30 -0
  93. package/src/templates/governed/generic.json +10 -0
  94. package/src/templates/governed/web-app.json +30 -0
@@ -0,0 +1,717 @@
1
+ import { mkdtempSync, mkdirSync, readFileSync, rmSync, writeFileSync, unlinkSync } from 'node:fs';
2
+ import { dirname, join } from 'node:path';
3
+ import { tmpdir } from 'node:os';
4
+ import { evaluatePhaseExit } from './gate-evaluator.js';
5
+ import {
6
+ approvePhaseTransition,
7
+ approveRunCompletion,
8
+ assignGovernedTurn,
9
+ initializeGovernedRun,
10
+ normalizeGovernedStateShape,
11
+ } from './governed-state.js';
12
+ import { validateStagedTurnResult } from './turn-result-validator.js';
13
+ import { finalizeDispatchManifest, verifyDispatchManifest } from './dispatch-manifest.js';
14
+ import { getDispatchTurnDir } from './turn-paths.js';
15
+ import { runHooks } from './hook-runner.js';
16
+
17
+ const VALID_DECISION_CATEGORIES = ['implementation', 'architecture', 'scope', 'process', 'quality', 'release'];
18
+ const FULL_STAGE_PIPELINE = ['schema', 'assignment', 'artifact', 'verification', 'protocol'];
19
+
20
+ function deepClone(value) {
21
+ return value == null ? value : JSON.parse(JSON.stringify(value));
22
+ }
23
+
24
+ function inflateConfig(rawConfig = {}) {
25
+ const config = deepClone(rawConfig);
26
+ const roles = config.roles || {};
27
+ const runtimes = config.runtimes || {};
28
+
29
+ config.schema_version = config.schema_version || '1.0';
30
+ config.project = config.project || { id: 'conformance-target', name: 'Conformance Target' };
31
+ config.roles = Object.fromEntries(
32
+ Object.entries(roles).map(([roleId, role]) => [
33
+ roleId,
34
+ {
35
+ title: role?.title || roleId.toUpperCase(),
36
+ mandate: role?.mandate || `Execute ${roleId} responsibilities.`,
37
+ write_authority: role?.write_authority || 'authoritative',
38
+ runtime: role?.runtime || 'manual',
39
+ ...role,
40
+ },
41
+ ]),
42
+ );
43
+
44
+ for (const role of Object.values(config.roles)) {
45
+ const runtimeId = role.runtime;
46
+ if (!runtimeId) continue;
47
+ if (!(runtimeId in runtimes) && runtimeId === 'manual') {
48
+ runtimes.manual = { type: 'manual' };
49
+ }
50
+ }
51
+
52
+ config.runtimes = Object.fromEntries(
53
+ Object.entries(runtimes).map(([runtimeId, runtime]) => [
54
+ runtimeId,
55
+ {
56
+ type: runtime?.type || 'manual',
57
+ ...runtime,
58
+ },
59
+ ]),
60
+ );
61
+ config.routing = config.routing || {};
62
+ config.gates = config.gates || {};
63
+ config.rules = {
64
+ challenge_required: true,
65
+ ...(config.rules || {}),
66
+ };
67
+
68
+ return config;
69
+ }
70
+
71
+ function validateFixtureConfig(config) {
72
+ const errors = [];
73
+
74
+ if (config.schema_version !== '1.0') {
75
+ errors.push(`schema_version must be "1.0", got "${config.schema_version}"`);
76
+ }
77
+
78
+ for (const [roleId, role] of Object.entries(config.roles || {})) {
79
+ if (!role.runtime || !config.runtimes?.[role.runtime]) {
80
+ errors.push(`Role "${roleId}" references unknown runtime "${role.runtime}"`);
81
+ }
82
+ }
83
+
84
+ for (const [phase, route] of Object.entries(config.routing || {})) {
85
+ if (route.entry_role && !config.roles?.[route.entry_role]) {
86
+ errors.push(`Routing "${phase}": entry_role "${route.entry_role}" is not a defined role`);
87
+ }
88
+ for (const nextRole of route.allowed_next_roles || []) {
89
+ if (nextRole !== 'human' && !config.roles?.[nextRole]) {
90
+ errors.push(`Routing "${phase}": allowed_next_roles references unknown role "${nextRole}"`);
91
+ }
92
+ }
93
+ if (route.exit_gate && !config.gates?.[route.exit_gate]) {
94
+ errors.push(`Routing references unknown gate: "${route.exit_gate}"`);
95
+ }
96
+ }
97
+
98
+ return errors;
99
+ }
100
+
101
+ function normalizeActiveTurns(activeTurns = {}, config) {
102
+ return Object.fromEntries(
103
+ Object.entries(activeTurns).map(([turnId, turn]) => {
104
+ const roleId = turn.assigned_role || turn.role || 'dev';
105
+ const runtimeId = turn.runtime_id || config.roles?.[roleId]?.runtime || 'manual';
106
+ return [
107
+ turnId,
108
+ {
109
+ turn_id: turn.turn_id || turnId,
110
+ assigned_role: roleId,
111
+ runtime_id: runtimeId,
112
+ status: turn.status || 'running',
113
+ attempt: turn.attempt || 1,
114
+ assigned_sequence: turn.assigned_sequence || 1,
115
+ ...turn,
116
+ turn_id: turn.turn_id || turnId,
117
+ assigned_role: roleId,
118
+ runtime_id: runtimeId,
119
+ },
120
+ ];
121
+ }),
122
+ );
123
+ }
124
+
125
+ function inflateState(rawState = {}, config) {
126
+ const state = {
127
+ schema_version: '1.1',
128
+ run_id: rawState.run_id ?? null,
129
+ project_id: config.project?.id || 'conformance-target',
130
+ status: rawState.status || 'idle',
131
+ phase: rawState.phase || 'planning',
132
+ accepted_integration_ref: rawState.accepted_integration_ref ?? null,
133
+ active_turns: normalizeActiveTurns(rawState.active_turns || {}, config),
134
+ pending_phase_transition: rawState.pending_phase_transition ?? null,
135
+ pending_run_completion: rawState.pending_run_completion ?? null,
136
+ blocked_on: rawState.blocked_on ?? null,
137
+ blocked_reason: rawState.blocked_reason ?? null,
138
+ escalation: rawState.escalation ?? null,
139
+ accepted_sequence: rawState.accepted_sequence ?? 0,
140
+ turn_sequence: rawState.turn_sequence ?? 0,
141
+ budget_reservations: rawState.budget_reservations ?? {},
142
+ phase_gate_status: rawState.phase_gate_status ?? {},
143
+ };
144
+
145
+ return normalizeGovernedStateShape(state).state;
146
+ }
147
+
148
+ function writeJson(filePath, data) {
149
+ mkdirSync(dirname(filePath), { recursive: true });
150
+ writeFileSync(filePath, JSON.stringify(data, null, 2));
151
+ }
152
+
153
+ function writeJsonl(filePath, entries) {
154
+ mkdirSync(dirname(filePath), { recursive: true });
155
+ const content = entries.map((entry) => JSON.stringify(entry)).join('\n');
156
+ writeFileSync(filePath, content ? `${content}\n` : '');
157
+ }
158
+
159
+ function readJson(filePath) {
160
+ return JSON.parse(readFileSync(filePath, 'utf8'));
161
+ }
162
+
163
+ function readJsonl(filePath) {
164
+ try {
165
+ const content = readFileSync(filePath, 'utf8').trim();
166
+ if (!content) return [];
167
+ return content.split('\n').filter(Boolean).map((line) => JSON.parse(line));
168
+ } catch {
169
+ return [];
170
+ }
171
+ }
172
+
173
+ function materializeFixtureWorkspace(fixture) {
174
+ const root = mkdtempSync(join(tmpdir(), 'agentxchain-conformance-'));
175
+ const setup = fixture.setup || {};
176
+ const rawConfig = setup.config || {};
177
+ const inflatedConfig = inflateConfig(rawConfig);
178
+ const state = inflateState(setup.state || {}, inflatedConfig);
179
+
180
+ mkdirSync(join(root, '.agentxchain', 'staging'), { recursive: true });
181
+ writeJson(join(root, 'agentxchain.json'), inflatedConfig);
182
+ writeJson(join(root, '.agentxchain', 'state.json'), state);
183
+ writeJsonl(join(root, '.agentxchain', 'history.jsonl'), setup.history || []);
184
+ writeJsonl(join(root, '.agentxchain', 'decision-ledger.jsonl'), setup.ledger || []);
185
+
186
+ const stagedTurnResult = setup.turn_result || fixture.input?.args?.turn_result;
187
+ if (stagedTurnResult) {
188
+ writeJson(join(root, '.agentxchain', 'staging', 'turn-result.json'), stagedTurnResult);
189
+ }
190
+
191
+ for (const [relPath, content] of Object.entries(setup.filesystem || {})) {
192
+ const absPath = join(root, relPath);
193
+ mkdirSync(dirname(absPath), { recursive: true });
194
+ writeFileSync(absPath, content);
195
+ }
196
+
197
+ // Materialize dispatch bundle files for manifest fixtures
198
+ for (const [turnId, files] of Object.entries(setup.dispatch_bundle || {})) {
199
+ const bundleDir = join(root, getDispatchTurnDir(turnId));
200
+ mkdirSync(bundleDir, { recursive: true });
201
+ for (const [fileName, content] of Object.entries(files)) {
202
+ writeFileSync(join(bundleDir, fileName), content);
203
+ }
204
+ }
205
+
206
+ return {
207
+ root,
208
+ rawConfig,
209
+ inflatedConfig,
210
+ fixtureConfig: inflatedConfig,
211
+ configErrors: validateFixtureConfig(inflatedConfig),
212
+ initialState: state,
213
+ };
214
+ }
215
+
216
+ function isAssertionObject(value) {
217
+ return value && typeof value === 'object' && !Array.isArray(value) && typeof value.assert === 'string';
218
+ }
219
+
220
+ function matchExpected(expected, actual) {
221
+ if (isAssertionObject(expected)) {
222
+ if (expected.assert === 'nonempty_string') {
223
+ return typeof actual === 'string' && actual.trim().length > 0;
224
+ }
225
+ if (expected.assert === 'id_prefix') {
226
+ return typeof actual === 'string' && actual.startsWith(expected.value);
227
+ }
228
+ if (expected.assert === 'present') {
229
+ return actual !== undefined;
230
+ }
231
+ return false;
232
+ }
233
+
234
+ if (Array.isArray(expected)) {
235
+ return Array.isArray(actual)
236
+ && expected.length === actual.length
237
+ && expected.every((value, index) => matchExpected(value, actual[index]));
238
+ }
239
+
240
+ if (expected && typeof expected === 'object') {
241
+ if (!actual || typeof actual !== 'object') {
242
+ return false;
243
+ }
244
+ return Object.entries(expected).every(([key, value]) => {
245
+ if (key === 'warnings_allowed') {
246
+ return true;
247
+ }
248
+ return matchExpected(value, actual[key]);
249
+ });
250
+ }
251
+
252
+ return Object.is(expected, actual);
253
+ }
254
+
255
+ function buildFailure(message, actual) {
256
+ return { status: 'fail', message, actual };
257
+ }
258
+
259
+ function buildPass(actual, message = 'Fixture passed') {
260
+ return { status: 'pass', message, actual };
261
+ }
262
+
263
+ function classifyTurnValidation(validation) {
264
+ if (validation.ok) {
265
+ return {
266
+ result: 'success',
267
+ stages_passed: FULL_STAGE_PIPELINE,
268
+ errors: [],
269
+ warnings: validation.warnings || [],
270
+ };
271
+ }
272
+
273
+ const actual = {
274
+ result: 'error',
275
+ failed_stage: validation.stage,
276
+ errors: validation.errors,
277
+ warnings: validation.warnings || [],
278
+ };
279
+ const firstError = validation.errors?.[0] || '';
280
+ const allErrors = validation.errors || [];
281
+
282
+ if (validation.stage === 'schema' && firstError.startsWith('Missing required field: ')) {
283
+ actual.error_type = 'missing_required_field';
284
+ actual.error_field = firstError.replace('Missing required field: ', '');
285
+ } else if (validation.stage === 'schema' && firstError.includes('.id must match pattern DEC-NNN.')) {
286
+ actual.error_type = 'invalid_decision_id_format';
287
+ actual.error_detail = 'Decision ID must match DEC-NNN pattern';
288
+ } else if (validation.stage === 'assignment' && firstError.startsWith('run_id mismatch')) {
289
+ actual.error_type = 'run_id_mismatch';
290
+ } else if (validation.stage === 'assignment' && firstError.startsWith('turn_id mismatch')) {
291
+ actual.error_type = 'turn_id_mismatch';
292
+ } else if (validation.stage === 'artifact' && firstError.startsWith('Turn result claims modification of reserved path: ')) {
293
+ actual.error_type = 'reserved_path_violation';
294
+ actual.error_path = firstError.replace('Turn result claims modification of reserved path: ', '');
295
+ } else if (validation.stage === 'protocol' && firstError.startsWith('Protocol violation:')) {
296
+ actual.error_type = 'challenge_requirement_violated';
297
+ actual.error_detail = 'review_only role must raise at least one objection';
298
+ } else if (validation.stage === 'protocol' && allErrors.some((error) => error.includes('mutually exclusive'))) {
299
+ actual.error_type = 'mutually_exclusive_requests';
300
+ actual.error_detail = 'phase_transition_request and run_completion_request cannot both be present';
301
+ } else {
302
+ actual.error_type = validation.error_class || 'validation_error';
303
+ }
304
+
305
+ return actual;
306
+ }
307
+
308
+ function mapConfigErrors(errors) {
309
+ const firstError = errors[0] || '';
310
+ const actual = { result: 'error', errors };
311
+
312
+ if (firstError.startsWith('Routing "implementation": entry_role "')) {
313
+ actual.error_type = 'undeclared_role_reference';
314
+ actual.error_field = 'routing.implementation.entry_role';
315
+ actual.referenced_role = firstError.match(/entry_role "([^"]+)"/)?.[1] || null;
316
+ } else if (firstError.startsWith('Role "dev" references unknown runtime "')) {
317
+ actual.error_type = 'undeclared_runtime_reference';
318
+ actual.error_field = 'roles.dev.runtime';
319
+ actual.referenced_runtime = firstError.match(/runtime "([^"]+)"/)?.[1] || null;
320
+ } else if (firstError.startsWith('Routing references unknown gate: "')) {
321
+ actual.error_type = 'undeclared_gate_reference';
322
+ actual.error_field = 'routing.implementation.exit_gate';
323
+ actual.referenced_gate = firstError.match(/gate: "([^"]+)"/)?.[1] || null;
324
+ } else if (firstError.includes('schema_version must be "1.0"')) {
325
+ actual.error_type = 'schema_version_invalid';
326
+ actual.expected_version = '1.0';
327
+ actual.actual_version = firstError.match(/got "([^"]+)"/)?.[1] || null;
328
+ } else {
329
+ actual.error_type = 'invalid_config';
330
+ }
331
+
332
+ return actual;
333
+ }
334
+
335
+ function validateDecisionEntry(entry, existingLedger) {
336
+ if (!/^DEC-\d+$/.test(entry.id || '')) {
337
+ return { result: 'error', error_type: 'invalid_decision_id_format' };
338
+ }
339
+ if (!entry.statement || !entry.statement.trim()) {
340
+ return { result: 'error', error_type: 'empty_required_field', error_field: 'statement' };
341
+ }
342
+ if (!VALID_DECISION_CATEGORIES.includes(entry.category)) {
343
+ return {
344
+ result: 'error',
345
+ error_type: 'invalid_enum_value',
346
+ error_field: 'category',
347
+ valid_values: VALID_DECISION_CATEGORIES,
348
+ };
349
+ }
350
+ if (existingLedger.some((item) => item.id === entry.id)) {
351
+ return { result: 'error', error_type: 'duplicate_decision_id', duplicate_id: entry.id };
352
+ }
353
+ return { result: 'success' };
354
+ }
355
+
356
+ function performAcceptTurnOperation(root, fixture, normalizedConfig) {
357
+ const statePath = join(root, '.agentxchain', 'state.json');
358
+ const historyPath = join(root, '.agentxchain', 'history.jsonl');
359
+ const stagingPath = join(root, '.agentxchain', 'staging', 'turn-result.json');
360
+ const stateBefore = readJson(statePath);
361
+ const historyBefore = readJsonl(historyPath);
362
+ const requestedTurnId = fixture.input.args.turn_id;
363
+
364
+ if (historyBefore.some((entry) => entry.turn_id === requestedTurnId)) {
365
+ return {
366
+ result: 'error',
367
+ error_type: 'turn_already_accepted',
368
+ history_length: historyBefore.length,
369
+ };
370
+ }
371
+
372
+ const activeTurn = stateBefore.active_turns?.[requestedTurnId];
373
+ if (!activeTurn) {
374
+ return {
375
+ result: 'error',
376
+ error_type: 'turn_not_active',
377
+ history_length: historyBefore.length,
378
+ state_unchanged: true,
379
+ };
380
+ }
381
+
382
+ const validation = validateStagedTurnResult(root, stateBefore, normalizedConfig.normalized, {
383
+ stagingPath: '.agentxchain/staging/turn-result.json',
384
+ });
385
+ if (!validation.ok) {
386
+ return classifyTurnValidation(validation);
387
+ }
388
+
389
+ const acceptedSequence = (stateBefore.accepted_sequence || 0) + 1;
390
+ const entry = {
391
+ turn_id: validation.turnResult.turn_id,
392
+ run_id: validation.turnResult.run_id,
393
+ role: validation.turnResult.role,
394
+ status: validation.turnResult.status,
395
+ summary: validation.turnResult.summary,
396
+ accepted_sequence: acceptedSequence,
397
+ accepted_at: new Date().toISOString(),
398
+ };
399
+
400
+ const nextState = {
401
+ ...stateBefore,
402
+ accepted_sequence: acceptedSequence,
403
+ active_turns: Object.fromEntries(
404
+ Object.entries(stateBefore.active_turns || {}).filter(([turnId]) => turnId !== requestedTurnId),
405
+ ),
406
+ };
407
+
408
+ writeJson(statePath, nextState);
409
+ writeJsonl(historyPath, [...historyBefore, entry]);
410
+
411
+ return {
412
+ result: 'success',
413
+ state: {
414
+ accepted_sequence: nextState.accepted_sequence,
415
+ active_turns: nextState.active_turns,
416
+ },
417
+ history_length: historyBefore.length + 1,
418
+ history_last_entry: entry,
419
+ };
420
+ }
421
+
422
+ function finalizeManifestForFixture(root, turnId) {
423
+ const state = readJson(join(root, '.agentxchain', 'state.json'));
424
+ const activeTurn = state.active_turns?.[turnId];
425
+ const identity = {
426
+ run_id: state.run_id || 'run_001',
427
+ role: activeTurn?.assigned_role || activeTurn?.role || 'dev',
428
+ };
429
+ const finalized = finalizeDispatchManifest(root, turnId, identity);
430
+ if (!finalized.ok) {
431
+ return {
432
+ ok: false,
433
+ actual: { result: 'error', error_type: 'finalization_failed', error: finalized.error },
434
+ };
435
+ }
436
+ return { ok: true };
437
+ }
438
+
439
+ function applyManifestFixtureMutations(root, fixture, turnId) {
440
+ const bundleDir = join(root, getDispatchTurnDir(turnId));
441
+
442
+ for (const [fileName, content] of Object.entries(fixture.setup.post_finalize_inject?.[turnId] || {})) {
443
+ writeFileSync(join(bundleDir, fileName), content);
444
+ }
445
+
446
+ for (const [fileName, content] of Object.entries(fixture.setup.post_finalize_tamper?.[turnId] || {})) {
447
+ writeFileSync(join(bundleDir, fileName), content);
448
+ }
449
+
450
+ for (const fileName of fixture.setup.post_finalize_delete?.[turnId] || []) {
451
+ try {
452
+ unlinkSync(join(bundleDir, fileName));
453
+ } catch {
454
+ // Missing files are surfaced by manifest verification, not fixture setup.
455
+ }
456
+ }
457
+ }
458
+
459
+ function executeFixtureOperation(workspace, fixture) {
460
+ const { root, fixtureConfig, configErrors } = workspace;
461
+ const operation = fixture.input.operation;
462
+
463
+ switch (operation) {
464
+ case 'initialize_run': {
465
+ if (configErrors.length > 0) {
466
+ return { result: 'error', error_type: 'invalid_config', errors: configErrors, state_unchanged: true };
467
+ }
468
+ const result = initializeGovernedRun(root, fixtureConfig);
469
+ if (!result.ok) {
470
+ return { result: 'error', error_type: 'invalid_state_transition', error: result.error, state_unchanged: true };
471
+ }
472
+ return { result: 'ok', state_assertions: result.state };
473
+ }
474
+
475
+ case 'assign_turn': {
476
+ if (configErrors.length > 0) {
477
+ return { result: 'error', error_type: 'invalid_config', errors: configErrors, state_unchanged: true };
478
+ }
479
+ const result = assignGovernedTurn(root, fixtureConfig, fixture.input.args.role_id);
480
+ if (!result.ok) {
481
+ return { result: 'error', error_type: 'invalid_state_transition', error: result.error, state_unchanged: true };
482
+ }
483
+ return { result: 'ok', state_assertions: result.state };
484
+ }
485
+
486
+ case 'approve_transition': {
487
+ const result = approvePhaseTransition(root, fixtureConfig);
488
+ if (!result.ok) {
489
+ return { result: 'error', error_type: 'invalid_state_transition', error: result.error, state_unchanged: true };
490
+ }
491
+ return { result: 'ok', state_assertions: result.state };
492
+ }
493
+
494
+ case 'approve_completion': {
495
+ const result = approveRunCompletion(root, fixtureConfig);
496
+ if (!result.ok) {
497
+ return { result: 'error', error_type: 'invalid_state_transition', error: result.error, state_unchanged: true };
498
+ }
499
+ return { result: 'ok', state_assertions: result.state };
500
+ }
501
+
502
+ case 'resolve_blocked': {
503
+ const statePath = join(root, '.agentxchain', 'state.json');
504
+ const state = readJson(statePath);
505
+ if (state.status !== 'blocked' || fixture.input.args.action !== 'resume') {
506
+ return { result: 'error', error_type: 'invalid_state_transition', state_unchanged: true };
507
+ }
508
+ const nextState = {
509
+ ...state,
510
+ status: 'active',
511
+ blocked_on: null,
512
+ blocked_reason: null,
513
+ };
514
+ writeJson(statePath, nextState);
515
+ return { result: 'ok', state_assertions: nextState };
516
+ }
517
+
518
+ case 'transition_state': {
519
+ const statePath = join(root, '.agentxchain', 'state.json');
520
+ const state = readJson(statePath);
521
+ if (fixture.input.args.candidate_overrides?.run_id && fixture.input.args.candidate_overrides.run_id !== state.run_id) {
522
+ return { result: 'error', error_type: 'immutable_field', state_unchanged: true };
523
+ }
524
+ if (fixture.input.args.target_status === 'completed' || fixture.input.args.target_status === 'paused') {
525
+ return { result: 'error', error_type: 'invalid_state_transition', state_unchanged: true };
526
+ }
527
+ if (fixture.input.args.trigger === 'gate_requires_human_approval') {
528
+ const nextState = {
529
+ ...state,
530
+ status: 'paused',
531
+ pending_phase_transition: fixture.input.args.gate,
532
+ };
533
+ writeJson(statePath, nextState);
534
+ return { result: 'ok', state_assertions: nextState };
535
+ }
536
+ if (fixture.input.args.trigger === 'escalation') {
537
+ const roleId = fixture.input.args.role_id;
538
+ const reason = fixture.input.args.reason?.replaceAll('_', '-') || 'unknown';
539
+ const nextState = {
540
+ ...state,
541
+ status: 'blocked',
542
+ blocked_on: `escalation:${reason}:${roleId}`,
543
+ };
544
+ writeJson(statePath, nextState);
545
+ return { result: 'ok', state_assertions: nextState };
546
+ }
547
+ return { result: 'error', error_type: 'invalid_state_transition', state_unchanged: true };
548
+ }
549
+
550
+ case 'validate_turn_result': {
551
+ const validation = validateStagedTurnResult(root, readJson(join(root, '.agentxchain', 'state.json')), fixtureConfig, {
552
+ stagingPath: '.agentxchain/staging/turn-result.json',
553
+ });
554
+ return classifyTurnValidation(validation);
555
+ }
556
+
557
+ case 'evaluate_phase_exit': {
558
+ const state = readJson(join(root, '.agentxchain', 'state.json'));
559
+ const acceptedTurn = {
560
+ phase_transition_request: fixture.input.args.requested_phase,
561
+ verification: fixture.setup.turn_result?.verification || { status: 'pass' },
562
+ };
563
+ const result = evaluatePhaseExit({
564
+ state,
565
+ config: fixtureConfig,
566
+ acceptedTurn,
567
+ root,
568
+ });
569
+ if (result.action === 'unknown_phase') {
570
+ return { result: 'error', action: 'gate_error', error_type: 'unknown_phase', state_unchanged: true };
571
+ }
572
+ if (result.action === 'awaiting_human_approval') {
573
+ return {
574
+ result: 'success',
575
+ action: result.action,
576
+ new_status: 'paused',
577
+ pending_phase_transition: {
578
+ from: state.phase,
579
+ to: result.next_phase,
580
+ },
581
+ };
582
+ }
583
+ if (result.action === 'advance') {
584
+ return { result: 'success', action: 'advance', new_phase: result.next_phase };
585
+ }
586
+ return {
587
+ result: 'success',
588
+ action: result.action,
589
+ phase_unchanged: true,
590
+ state_unchanged: true,
591
+ reason: result.missing_files.length > 0
592
+ ? 'requires_files predicate failed'
593
+ : result.missing_verification
594
+ ? 'requires_verification_pass predicate failed'
595
+ : (result.reasons[0] || null),
596
+ };
597
+ }
598
+
599
+ case 'append_decision': {
600
+ const ledgerPath = join(root, '.agentxchain', 'decision-ledger.jsonl');
601
+ const existingLedger = readJsonl(ledgerPath);
602
+ const decisionCheck = validateDecisionEntry(fixture.input.args.entry, existingLedger);
603
+ if (decisionCheck.result === 'error') {
604
+ return {
605
+ ...decisionCheck,
606
+ ledger_length: existingLedger.length,
607
+ };
608
+ }
609
+ const nextLedger = [...existingLedger, fixture.input.args.entry];
610
+ writeJsonl(ledgerPath, nextLedger);
611
+ return {
612
+ result: 'success',
613
+ ledger_length: nextLedger.length,
614
+ ledger_last_entry: fixture.input.args.entry,
615
+ };
616
+ }
617
+
618
+ case 'accept_turn':
619
+ return performAcceptTurnOperation(root, fixture, { normalized: fixtureConfig });
620
+
621
+ case 'validate_config': {
622
+ const candidate = inflateConfig(fixture.input.args.config);
623
+ const errors = validateFixtureConfig(candidate);
624
+ if (errors.length > 0) {
625
+ return mapConfigErrors(errors);
626
+ }
627
+ return { result: 'success', errors: [] };
628
+ }
629
+
630
+ // ── Tier 2: Dispatch Manifest ────────────────────────────────────────
631
+
632
+ case 'verify_dispatch_manifest': {
633
+ const turnId = fixture.input.args.turn_id;
634
+ const finalization = finalizeManifestForFixture(root, turnId);
635
+ if (!finalization.ok) {
636
+ return finalization.actual;
637
+ }
638
+ applyManifestFixtureMutations(root, fixture, turnId);
639
+ const ver = verifyDispatchManifest(root, turnId);
640
+ if (!ver.ok) {
641
+ return { result: 'error', error_type: ver.errors[0]?.type || 'verification_failed', verification_errors: ver.errors };
642
+ }
643
+ return {
644
+ result: 'success',
645
+ manifest_valid: true,
646
+ manifest: ver.manifest,
647
+ verification_errors: [],
648
+ };
649
+ }
650
+
651
+ case 'inspect_dispatch_manifest': {
652
+ const turnId = fixture.input.args.turn_id;
653
+ const finalization = finalizeManifestForFixture(root, turnId);
654
+ if (!finalization.ok) {
655
+ return finalization.actual;
656
+ }
657
+ const ver = verifyDispatchManifest(root, turnId);
658
+ const manifest = ver.manifest || readJson(join(root, getDispatchTurnDir(turnId), 'MANIFEST.json'));
659
+ const filePaths = manifest.files.map((f) => f.path);
660
+ const containsSelf = filePaths.includes('MANIFEST.json');
661
+ return {
662
+ result: 'success',
663
+ manifest_contains_self: containsSelf,
664
+ file_paths: filePaths,
665
+ };
666
+ }
667
+
668
+ // ── Tier 2: Hook Audit ──────────────────────────────────────────────
669
+
670
+ case 'run_hooks': {
671
+ const phase = fixture.input.args.phase;
672
+ const payload = fixture.input.args.payload || {};
673
+ const hooksConfig = fixtureConfig.hooks || {};
674
+ const state = readJson(join(root, '.agentxchain', 'state.json'));
675
+ const auditDir = join(root, '.agentxchain');
676
+ const hookResult = runHooks(root, hooksConfig, phase, payload, {
677
+ run_id: state.run_id || payload.run_id || '',
678
+ turn_id: payload.turn_id || null,
679
+ auditDir,
680
+ protectedPaths: [
681
+ '.agentxchain/state.json',
682
+ '.agentxchain/history.jsonl',
683
+ '.agentxchain/decision-ledger.jsonl',
684
+ ],
685
+ });
686
+
687
+ // Return the first audit entry for single-hook fixtures
688
+ const auditEntry = hookResult.results?.[0] || null;
689
+ return {
690
+ result: 'success',
691
+ hook_ok: hookResult.ok,
692
+ blocked: hookResult.blocked || false,
693
+ audit_entry: auditEntry,
694
+ };
695
+ }
696
+
697
+ default:
698
+ return { result: 'error', error_type: 'unsupported_operation', operation };
699
+ }
700
+ }
701
+
702
+ function compareActualToExpected(fixture, actual) {
703
+ if (matchExpected(fixture.expected, actual)) {
704
+ return buildPass(actual);
705
+ }
706
+ return buildFailure(`Expected ${fixture.fixture_id} to match declared output`, actual);
707
+ }
708
+
709
+ export function runReferenceFixture(fixture) {
710
+ const workspace = materializeFixtureWorkspace(fixture);
711
+ try {
712
+ const actual = executeFixtureOperation(workspace, fixture);
713
+ return compareActualToExpected(fixture, actual);
714
+ } finally {
715
+ rmSync(workspace.root, { recursive: true, force: true });
716
+ }
717
+ }