agentxchain 2.17.0 → 2.19.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.
@@ -0,0 +1,632 @@
1
+ import { mkdirSync, writeFileSync, readFileSync, rmSync } from 'fs';
2
+ import { join } from 'path';
3
+ import { tmpdir } from 'os';
4
+ import { randomBytes } from 'crypto';
5
+ import { execSync } from 'child_process';
6
+ import chalk from 'chalk';
7
+
8
+ /**
9
+ * `agentxchain demo` — zero-friction first-run experience.
10
+ *
11
+ * Runs a complete PM → Dev → QA governed lifecycle in a temp dir
12
+ * using programmatically staged turn results. No API keys, no
13
+ * external tools, no manual steps. Shows governance in action.
14
+ */
15
+
16
+ // ── Config ──────────────────────────────────────────────────────────────────
17
+
18
+ function makeConfig() {
19
+ return {
20
+ schema_version: 4,
21
+ protocol_mode: 'governed',
22
+ project: { id: 'agentxchain-demo', name: 'AgentXchain Demo', default_branch: 'main' },
23
+ roles: {
24
+ pm: {
25
+ title: 'Product Manager',
26
+ mandate: 'Protect user value, scope clarity, and acceptance criteria.',
27
+ write_authority: 'review_only',
28
+ runtime_class: 'manual',
29
+ runtime_id: 'manual-pm',
30
+ },
31
+ dev: {
32
+ title: 'Developer',
33
+ mandate: 'Implement approved work safely and verify behavior.',
34
+ write_authority: 'authoritative',
35
+ runtime_class: 'manual',
36
+ runtime_id: 'manual-dev',
37
+ },
38
+ qa: {
39
+ title: 'QA Reviewer',
40
+ mandate: 'Challenge correctness, acceptance coverage, and ship readiness.',
41
+ write_authority: 'review_only',
42
+ runtime_class: 'manual',
43
+ runtime_id: 'manual-qa',
44
+ },
45
+ },
46
+ runtimes: {
47
+ 'manual-pm': { type: 'manual' },
48
+ 'manual-dev': { type: 'manual' },
49
+ 'manual-qa': { type: 'manual' },
50
+ },
51
+ routing: {
52
+ planning: {
53
+ entry_role: 'pm',
54
+ allowed_next_roles: ['pm', 'human'],
55
+ exit_gate: 'planning_signoff',
56
+ },
57
+ implementation: {
58
+ entry_role: 'dev',
59
+ allowed_next_roles: ['dev', 'qa', 'human'],
60
+ exit_gate: 'implementation_complete',
61
+ },
62
+ qa: {
63
+ entry_role: 'qa',
64
+ allowed_next_roles: ['dev', 'qa', 'human'],
65
+ exit_gate: 'qa_ship_verdict',
66
+ },
67
+ },
68
+ gates: {
69
+ planning_signoff: {
70
+ requires_files: ['.planning/PM_SIGNOFF.md'],
71
+ requires_human_approval: true,
72
+ },
73
+ implementation_complete: {
74
+ requires_files: ['.planning/IMPLEMENTATION_NOTES.md'],
75
+ requires_verification_pass: true,
76
+ },
77
+ qa_ship_verdict: {
78
+ requires_files: ['.planning/acceptance-matrix.md', '.planning/ship-verdict.md'],
79
+ requires_human_approval: true,
80
+ },
81
+ },
82
+ budget: { per_turn_max_usd: 1.0, per_run_max_usd: 5.0 },
83
+ rules: { challenge_required: true, max_turn_retries: 2, max_deadlock_cycles: 1 },
84
+ files: {
85
+ talk: 'TALK.md',
86
+ history: '.agentxchain/history.jsonl',
87
+ state: '.agentxchain/state.json',
88
+ },
89
+ compat: {
90
+ next_owner_source: 'state-json',
91
+ lock_based_coordination: false,
92
+ original_version: 4,
93
+ },
94
+ };
95
+ }
96
+
97
+ // ── Canned turn results ─────────────────────────────────────────────────────
98
+
99
+ function makePmTurnResult(runId, turnId) {
100
+ return {
101
+ schema_version: '1.0',
102
+ run_id: runId,
103
+ turn_id: turnId,
104
+ role: 'pm',
105
+ runtime_id: 'manual-pm',
106
+ status: 'completed',
107
+ summary: 'Scoped auth token rotation service: key expiry, graceful rollover, and audit logging. Established 3 acceptance criteria with security constraints.',
108
+ decisions: [
109
+ {
110
+ id: 'DEC-001',
111
+ category: 'scope',
112
+ statement: 'MVP scope: single-module token rotation with expiry check, graceful rollover, and audit trail.',
113
+ rationale: 'Security-sensitive surface — minimal scope reduces attack surface while proving governance value.',
114
+ },
115
+ {
116
+ id: 'DEC-002',
117
+ category: 'scope',
118
+ statement: 'Three acceptance criteria: safe rotation with rollback, monotonic expiry checks, and audit log on every lifecycle event.',
119
+ rationale: 'Each criterion maps to a testable assertion. Compliance requires traceability.',
120
+ },
121
+ ],
122
+ objections: [
123
+ {
124
+ id: 'OBJ-001',
125
+ severity: 'high',
126
+ statement: 'No rollback plan if new tokens fail validation. Live API keys could be invalidated without a recovery path.',
127
+ status: 'raised',
128
+ },
129
+ ],
130
+ files_changed: [],
131
+ artifacts_created: [],
132
+ verification: {
133
+ status: 'pass',
134
+ commands: [],
135
+ evidence_summary: 'Planning review — no code to verify.',
136
+ machine_evidence: [],
137
+ },
138
+ artifact: { type: 'review', ref: null },
139
+ proposed_next_role: 'human',
140
+ phase_transition_request: 'implementation',
141
+ needs_human_reason: null,
142
+ cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
143
+ };
144
+ }
145
+
146
+ function makeDevTurnResult(runId, turnId) {
147
+ return {
148
+ schema_version: '1.0',
149
+ run_id: runId,
150
+ turn_id: turnId,
151
+ role: 'dev',
152
+ runtime_id: 'manual-dev',
153
+ status: 'completed',
154
+ summary: 'Implemented token rotation with rollback, monotonic expiry, and audit trail. All 3 tests passing.',
155
+ decisions: [
156
+ {
157
+ id: 'DEC-003',
158
+ category: 'implementation',
159
+ statement: 'Added atomic rollback: new token is validated before old token is invalidated.',
160
+ rationale: 'Addresses OBJ-001 — live keys are never invalidated without a validated replacement.',
161
+ },
162
+ ],
163
+ objections: [
164
+ {
165
+ id: 'OBJ-002',
166
+ severity: 'medium',
167
+ statement: 'Token expiry check uses wall-clock time without monotonic fallback. Clock skew could skip rotation or double-rotate.',
168
+ status: 'raised',
169
+ },
170
+ ],
171
+ files_changed: ['token-rotator.js', 'token-rotator.test.js', '.planning/IMPLEMENTATION_NOTES.md'],
172
+ artifacts_created: [],
173
+ verification: {
174
+ status: 'pass',
175
+ commands: ['node token-rotator.test.js'],
176
+ evidence_summary: '3/3 tests passing: safe rotation with rollback, expiry bounds, audit emission.',
177
+ machine_evidence: [
178
+ { command: 'node token-rotator.test.js', exit_code: 0, stdout_excerpt: '3 tests passed, 0 failed' },
179
+ ],
180
+ },
181
+ artifact: { type: 'commit', ref: 'token-rotator.js' },
182
+ proposed_next_role: 'qa',
183
+ phase_transition_request: 'qa',
184
+ needs_human_reason: null,
185
+ cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
186
+ };
187
+ }
188
+
189
+ function makeQaTurnResult(runId, turnId) {
190
+ return {
191
+ schema_version: '1.0',
192
+ run_id: runId,
193
+ turn_id: turnId,
194
+ role: 'qa',
195
+ runtime_id: 'manual-qa',
196
+ status: 'completed',
197
+ summary: 'Reviewed token rotation against acceptance matrix. All 3 criteria met. Ship verdict: YES.',
198
+ decisions: [
199
+ {
200
+ id: 'DEC-004',
201
+ category: 'quality',
202
+ statement: 'All acceptance criteria verified: rollback safety, expiry monotonicity, and audit completeness.',
203
+ rationale: 'Token rotation, rollback, and audit trail all function as specified.',
204
+ },
205
+ {
206
+ id: 'DEC-005',
207
+ category: 'release',
208
+ statement: 'Ship verdict: YES. Security-sensitive implementation meets all acceptance criteria.',
209
+ rationale: 'OBJ-002 (clock skew) is noted for follow-up but not blocking for controlled environments.',
210
+ },
211
+ ],
212
+ objections: [
213
+ {
214
+ id: 'OBJ-003',
215
+ severity: 'medium',
216
+ statement: 'No audit entry emitted on rotation failure. Compliance requires traceability for every key lifecycle event.',
217
+ status: 'raised',
218
+ },
219
+ ],
220
+ files_changed: [],
221
+ artifacts_created: [],
222
+ verification: {
223
+ status: 'pass',
224
+ commands: [],
225
+ evidence_summary: 'Review-only turn. Verified against acceptance matrix.',
226
+ machine_evidence: [],
227
+ },
228
+ artifact: { type: 'review', ref: null },
229
+ proposed_next_role: 'human',
230
+ phase_transition_request: null,
231
+ run_completion_request: true,
232
+ needs_human_reason: null,
233
+ cost: { input_tokens: 0, output_tokens: 0, usd: 0 },
234
+ };
235
+ }
236
+
237
+ // ── Scaffold helpers ────────────────────────────────────────────────────────
238
+
239
+ function scaffoldProject(root) {
240
+ const config = makeConfig();
241
+ writeFileSync(join(root, 'agentxchain.json'), JSON.stringify(config, null, 2));
242
+ mkdirSync(join(root, '.agentxchain/prompts'), { recursive: true });
243
+ mkdirSync(join(root, '.planning'), { recursive: true });
244
+
245
+ writeFileSync(join(root, '.agentxchain/state.json'), JSON.stringify({
246
+ schema_version: '1.1',
247
+ status: 'idle',
248
+ phase: 'planning',
249
+ run_id: null,
250
+ active_turns: {},
251
+ next_role: null,
252
+ pending_phase_transition: null,
253
+ pending_run_completion: null,
254
+ blocked_on: null,
255
+ blocked_reason: null,
256
+ }, null, 2));
257
+
258
+ writeFileSync(join(root, '.agentxchain/history.jsonl'), '');
259
+ writeFileSync(join(root, '.agentxchain/decision-ledger.jsonl'), '');
260
+ writeFileSync(join(root, '.agentxchain/prompts/pm.md'), '# PM Prompt\nYou are the Product Manager.');
261
+ writeFileSync(join(root, '.agentxchain/prompts/dev.md'), '# Dev Prompt\nYou are the Developer.');
262
+ writeFileSync(join(root, '.agentxchain/prompts/qa.md'), '# QA Prompt\nYou are the QA Reviewer.');
263
+ writeFileSync(join(root, 'TALK.md'), '# Collaboration Log\n');
264
+
265
+ // Planning artifacts — PM_SIGNOFF starts blocked (flipped after PM turn)
266
+ writeFileSync(join(root, '.planning/PM_SIGNOFF.md'), '# PM Planning Sign-Off\n\nApproved: NO\n');
267
+ writeFileSync(join(root, '.planning/ROADMAP.md'), '# Roadmap\n\n(PM fills this)\n');
268
+
269
+ return config;
270
+ }
271
+
272
+ function gitInit(root) {
273
+ execSync('git init', { cwd: root, stdio: 'ignore' });
274
+ execSync('git config user.email "demo@agentxchain.dev"', { cwd: root, stdio: 'ignore' });
275
+ execSync('git config user.name "AgentXchain Demo"', { cwd: root, stdio: 'ignore' });
276
+ execSync('git add -A', { cwd: root, stdio: 'ignore' });
277
+ execSync('git commit -m "demo: scaffold governed project"', { cwd: root, stdio: 'ignore' });
278
+ }
279
+
280
+ function gitCommit(root, message) {
281
+ execSync('git add -A', { cwd: root, stdio: 'ignore' });
282
+ execSync(`git commit -m "${message}" --allow-empty`, { cwd: root, stdio: 'ignore' });
283
+ }
284
+
285
+ function stageTurnResult(root, turnId, result) {
286
+ const stagingDir = join(root, '.agentxchain/staging', turnId);
287
+ mkdirSync(stagingDir, { recursive: true });
288
+ writeFileSync(join(stagingDir, 'turn-result.json'), JSON.stringify(result, null, 2));
289
+ }
290
+
291
+ // ── Output helpers ──────────────────────────────────────────────────────────
292
+
293
+ function header(text) {
294
+ console.log('');
295
+ console.log(chalk.bold.cyan(` ── ${text} ──`));
296
+ }
297
+
298
+ function step(text) {
299
+ console.log(chalk.dim(' ▸ ') + text);
300
+ }
301
+
302
+ function lesson(text) {
303
+ console.log(chalk.dim(' → ') + chalk.italic(text));
304
+ }
305
+
306
+ function success(text) {
307
+ console.log(chalk.dim(' ▸ ') + chalk.green(text));
308
+ }
309
+
310
+ // ── Main ────────────────────────────────────────────────────────────────────
311
+
312
+ export async function demoCommand(opts = {}) {
313
+ const jsonMode = opts.json || false;
314
+ const verbose = opts.verbose || false;
315
+ const startTime = Date.now();
316
+
317
+ const root = join(tmpdir(), `agentxchain-demo-${randomBytes(6).toString('hex')}`);
318
+ mkdirSync(root, { recursive: true });
319
+
320
+ const result = {
321
+ ok: false,
322
+ run_id: null,
323
+ turns: [],
324
+ decisions: 0,
325
+ objections: 0,
326
+ duration_ms: 0,
327
+ error: null,
328
+ };
329
+
330
+ try {
331
+ // Verify git is available
332
+ try {
333
+ execSync('git --version', { stdio: 'ignore' });
334
+ } catch {
335
+ throw new Error('git is required for the demo but was not found in PATH');
336
+ }
337
+
338
+ // Lazy-load runner interface to avoid circular imports at module level
339
+ const {
340
+ initRun,
341
+ assignTurn,
342
+ acceptTurn,
343
+ approvePhaseGate,
344
+ approveCompletionGate,
345
+ } = await import('../lib/runner-interface.js');
346
+
347
+ if (!jsonMode) {
348
+ console.log('');
349
+ console.log(chalk.bold(' AgentXchain Demo — Governed Multi-Agent Delivery'));
350
+ console.log(chalk.dim(' ' + '─'.repeat(51)));
351
+ }
352
+
353
+ // ── Scaffold ──────────────────────────────────────────────────────────
354
+ if (!jsonMode) step('Scaffolding governed project...');
355
+ const config = scaffoldProject(root);
356
+ gitInit(root);
357
+
358
+ // ── Init run ──────────────────────────────────────────────────────────
359
+ const runResult = initRun(root, config);
360
+ if (!runResult.ok) throw new Error(`initRun failed: ${runResult.error}`);
361
+ const runId = runResult.state.run_id;
362
+ result.run_id = runId;
363
+
364
+ if (!jsonMode) step(`Starting governed run: ${chalk.bold(runId.slice(0, 16))}...`);
365
+
366
+ // ── PM Turn (Planning) ────────────────────────────────────────────────
367
+ if (!jsonMode) header('PM Turn — Planning Phase');
368
+
369
+ const pmAssign = assignTurn(root, config, 'pm');
370
+ if (!pmAssign.ok) throw new Error(`PM assign failed: ${pmAssign.error}`);
371
+ const pmTurnId = pmAssign.turn.turn_id;
372
+
373
+ if (!jsonMode) step(`Assigned PM turn: ${chalk.dim(pmTurnId.slice(0, 16))}...`);
374
+
375
+ // Stage PM turn result
376
+ const pmResult = makePmTurnResult(runId, pmTurnId);
377
+ stageTurnResult(root, pmTurnId, pmResult);
378
+
379
+ if (!jsonMode) {
380
+ step('PM scoped auth token rotation: key expiry, graceful rollover, audit trail');
381
+ step(`PM raised ${chalk.yellow('1 objection')}: "No rollback plan — live API keys could be invalidated without recovery"`);
382
+ lesson('Without mandatory challenge, this missing rollback plan would have reached implementation unchecked');
383
+ step(`PM recorded ${chalk.blue('2 decisions')} in the decision ledger`);
384
+ }
385
+
386
+ // Write planning artifacts BEFORE acceptance
387
+ writeFileSync(join(root, '.planning/ROADMAP.md'),
388
+ '# Roadmap\n\n## Acceptance Criteria\n\n1. Token rotation with atomic rollback — old key stays valid until new key is verified\n2. Expiry checks use monotonic time — no clock-skew-induced double-rotation\n3. Audit log emitted on every key lifecycle event (create, rotate, expire, revoke)\n');
389
+ writeFileSync(join(root, '.planning/PM_SIGNOFF.md'),
390
+ '# PM Planning Sign-Off\n\nApproved: YES\n');
391
+ gitCommit(root, 'demo: pm planning work');
392
+
393
+ const pmAccept = acceptTurn(root, config);
394
+ if (!pmAccept.ok) throw new Error(`PM accept failed: ${pmAccept.error}`);
395
+ gitCommit(root, 'demo: accept pm turn');
396
+
397
+ if (!jsonMode) success('Turn accepted ✓');
398
+ result.turns.push({ role: 'pm', turn_id: pmTurnId, phase: 'planning' });
399
+ result.decisions += pmResult.decisions.length;
400
+ result.objections += pmResult.objections.length;
401
+
402
+ // ── Phase Gate: planning → implementation ─────────────────────────────
403
+ if (!jsonMode) header('Phase Gate — planning → implementation');
404
+
405
+ const gateResult = approvePhaseGate(root, config);
406
+ if (!gateResult.ok) throw new Error(`Phase gate failed: ${gateResult.error}`);
407
+ gitCommit(root, 'demo: approve phase transition');
408
+
409
+ if (!jsonMode) {
410
+ success('Gate passed: PM_SIGNOFF.md contains "Approved: YES"');
411
+ lesson('This gate stopped 3 AI agents from proceeding until a human confirmed the security scope was correct');
412
+ }
413
+
414
+ // ── Dev Turn (Implementation) ─────────────────────────────────────────
415
+ if (!jsonMode) header('Dev Turn — Implementation Phase');
416
+
417
+ const devAssign = assignTurn(root, config, 'dev');
418
+ if (!devAssign.ok) throw new Error(`Dev assign failed: ${devAssign.error}`);
419
+ const devTurnId = devAssign.turn.turn_id;
420
+
421
+ if (!jsonMode) step(`Assigned Dev turn: ${chalk.dim(devTurnId.slice(0, 16))}...`);
422
+
423
+ // Write implementation files
424
+ writeFileSync(join(root, 'token-rotator.js'), `// Auth Token Rotation Service — governed implementation
425
+ const audit = [];
426
+ let currentToken = { key: 'tok_initial', created: Date.now(), expires: Date.now() + 3600000 };
427
+ let previousToken = null;
428
+
429
+ function rotate(newKey) {
430
+ if (!newKey || typeof newKey !== 'string') {
431
+ audit.push({ event: 'rotate_failed', reason: 'invalid_key', ts: Date.now() });
432
+ throw new Error('Invalid token key: must be a non-empty string');
433
+ }
434
+ // Atomic rollback: validate new token before invalidating old
435
+ const candidate = { key: newKey, created: Date.now(), expires: Date.now() + 3600000 };
436
+ previousToken = currentToken; // preserve rollback path
437
+ currentToken = candidate;
438
+ audit.push({ event: 'rotated', from: previousToken.key, to: newKey, ts: Date.now() });
439
+ return currentToken;
440
+ }
441
+
442
+ function rollback() {
443
+ if (!previousToken) throw new Error('No previous token to roll back to');
444
+ const rolled = previousToken;
445
+ currentToken = previousToken;
446
+ previousToken = null;
447
+ audit.push({ event: 'rollback', to: rolled.key, ts: Date.now() });
448
+ return currentToken;
449
+ }
450
+
451
+ function getAuditLog() { return [...audit]; }
452
+
453
+ module.exports = { rotate, rollback, getAuditLog, getCurrent: () => currentToken };
454
+ `);
455
+
456
+ writeFileSync(join(root, 'token-rotator.test.js'), `const assert = require('assert');
457
+ const { rotate, rollback, getAuditLog } = require('./token-rotator');
458
+
459
+ // Test 1: Safe rotation with rollback
460
+ const newToken = rotate('tok_v2');
461
+ assert.strictEqual(newToken.key, 'tok_v2');
462
+ const rolledBack = rollback();
463
+ assert.strictEqual(rolledBack.key, 'tok_initial');
464
+
465
+ // Test 2: Invalid key rejected with audit trail
466
+ try { rotate(''); assert.fail('Should throw'); }
467
+ catch (e) { assert.match(e.message, /Invalid token key/); }
468
+
469
+ // Test 3: Audit log captures all lifecycle events
470
+ const log = getAuditLog();
471
+ assert.ok(log.some(e => e.event === 'rotated'), 'rotation logged');
472
+ assert.ok(log.some(e => e.event === 'rollback'), 'rollback logged');
473
+ assert.ok(log.some(e => e.event === 'rotate_failed'), 'failure logged');
474
+ console.log('3 tests passed, 0 failed');
475
+ `);
476
+
477
+ writeFileSync(join(root, '.planning/IMPLEMENTATION_NOTES.md'), `# Implementation Notes
478
+
479
+ ## Changes
480
+
481
+ - Created \`token-rotator.js\` with atomic rollback, expiry, and audit logging
482
+ - Created \`token-rotator.test.js\` with 3 test cases covering all acceptance criteria
483
+ - Resolved OBJ-001: new tokens are validated before old tokens are invalidated
484
+
485
+ ## Verification
486
+
487
+ - \`node token-rotator.test.js\` → 3/3 passing
488
+ `);
489
+ gitCommit(root, 'demo: dev implementation');
490
+
491
+ // Stage dev turn result
492
+ const devResult = makeDevTurnResult(runId, devTurnId);
493
+ stageTurnResult(root, devTurnId, devResult);
494
+
495
+ if (!jsonMode) {
496
+ step('Dev implemented token rotation with atomic rollback and audit trail');
497
+ step(`Dev resolved PM objection: ${chalk.green('OBJ-001 — rollback path now implemented')}`);
498
+ step(`Dev raised ${chalk.yellow('1 new objection')}: "Clock skew could skip rotation or double-rotate"`);
499
+ lesson('The dev caught a clock-skew bug the PM missed. Independent challenge surfaces different failure classes');
500
+ step(`Verification: ${chalk.green('3/3 tests passing')}`);
501
+ }
502
+
503
+ const devAccept = acceptTurn(root, config);
504
+ if (!devAccept.ok) throw new Error(`Dev accept failed: ${devAccept.error}`);
505
+ gitCommit(root, 'demo: accept dev turn');
506
+
507
+ if (!jsonMode) success('Turn accepted ✓');
508
+ result.turns.push({ role: 'dev', turn_id: devTurnId, phase: 'implementation' });
509
+ result.decisions += devResult.decisions.length;
510
+ result.objections += devResult.objections.length;
511
+
512
+ // implementation_complete gate auto-advances (no requires_human_approval)
513
+ // so the phase is already 'qa' after dev acceptance
514
+ if (!jsonMode) {
515
+ header('Phase Gate — implementation → qa (auto-evaluated)');
516
+ success('Gate passed: IMPLEMENTATION_NOTES.md has real content, verification passed');
517
+ lesson('Without this gate, untested code could reach QA review — wasting a review turn on code that doesn\'t run');
518
+ }
519
+
520
+ // ── QA Turn (Review) ──────────────────────────────────────────────────
521
+ if (!jsonMode) header('QA Turn — Review Phase');
522
+
523
+ const qaAssign = assignTurn(root, config, 'qa');
524
+ if (!qaAssign.ok) throw new Error(`QA assign failed: ${qaAssign.error}`);
525
+ const qaTurnId = qaAssign.turn.turn_id;
526
+
527
+ if (!jsonMode) step(`Assigned QA turn: ${chalk.dim(qaTurnId.slice(0, 16))}...`);
528
+
529
+ // Write QA artifacts
530
+ writeFileSync(join(root, '.planning/acceptance-matrix.md'), `# Acceptance Matrix
531
+
532
+ | Req # | Requirement | Status |
533
+ |-------|-------------|--------|
534
+ | 1 | Token rotation with atomic rollback | PASS |
535
+ | 2 | Monotonic expiry checks | PASS |
536
+ | 3 | Audit log on every lifecycle event | PASS |
537
+ `);
538
+
539
+ writeFileSync(join(root, '.planning/ship-verdict.md'), `# Ship Verdict
540
+
541
+ ## Verdict: SHIP
542
+
543
+ All acceptance criteria met. OBJ-002 (clock skew) noted for follow-up. OBJ-003 (failure audit) noted for next sprint.
544
+ `);
545
+
546
+ writeFileSync(join(root, '.planning/RELEASE_NOTES.md'), `# Release Notes — v1.0.0
547
+
548
+ ## What shipped
549
+
550
+ - Auth token rotation with atomic rollback and audit trail
551
+ - 3/3 acceptance criteria met
552
+ - Governed delivery: PM → Dev → QA with mandatory challenge at every turn
553
+ - 3 issues caught by governance that would have shipped undetected without challenge
554
+ `);
555
+ gitCommit(root, 'demo: qa review artifacts');
556
+
557
+ // Stage QA turn result
558
+ const qaResult = makeQaTurnResult(runId, qaTurnId);
559
+ stageTurnResult(root, qaTurnId, qaResult);
560
+
561
+ if (!jsonMode) {
562
+ step('QA reviewed token rotation against acceptance matrix');
563
+ step(`QA verdict: ${chalk.green('All 3 criteria PASS')}`);
564
+ step(`QA raised ${chalk.yellow('1 objection')}: "No audit entry on rotation failure — compliance gap"`);
565
+ lesson('QA found a compliance gap neither PM nor dev raised. Three perspectives > one');
566
+ step(`Ship verdict: ${chalk.green('SHIP')}`);
567
+ }
568
+
569
+ const qaAccept = acceptTurn(root, config);
570
+ if (!qaAccept.ok) throw new Error(`QA accept failed: ${qaAccept.error}`);
571
+ gitCommit(root, 'demo: accept qa turn');
572
+
573
+ if (!jsonMode) success('Turn accepted ✓');
574
+ result.turns.push({ role: 'qa', turn_id: qaTurnId, phase: 'qa' });
575
+ result.decisions += qaResult.decisions.length;
576
+ result.objections += qaResult.objections.length;
577
+
578
+ // ── Run Completion ────────────────────────────────────────────────────
579
+ if (!jsonMode) header('Run Completion');
580
+
581
+ const completionResult = approveCompletionGate(root, config);
582
+ if (!completionResult.ok) throw new Error(`Completion failed: ${completionResult.error}`);
583
+
584
+ const completedAt = JSON.parse(readFileSync(join(root, '.agentxchain/state.json'), 'utf8')).completed_at;
585
+
586
+ if (!jsonMode) {
587
+ success(`Approved completion: qa_ship_verdict gate passed`);
588
+ step(`Run completed at ${chalk.dim(completedAt || new Date().toISOString())}`);
589
+ }
590
+
591
+ // ── Summary ───────────────────────────────────────────────────────────
592
+ result.ok = true;
593
+ result.duration_ms = Date.now() - startTime;
594
+
595
+ if (!jsonMode) {
596
+ header('Summary');
597
+ console.log('');
598
+ console.log(` Run: ${chalk.bold(runId.slice(0, 16))}...`);
599
+ console.log(` Turns: ${chalk.bold('3')} (PM, Dev, QA)`);
600
+ console.log(` Decisions: ${chalk.blue(String(result.decisions))} recorded in decision ledger`);
601
+ console.log(` Objections: ${chalk.yellow(String(result.objections))} raised across all turns`);
602
+ console.log(` Duration: ${chalk.dim((result.duration_ms / 1000).toFixed(1) + 's')}`);
603
+ console.log(` Caught: ${chalk.green('3 issues that would have shipped undetected without governed challenge')}`);
604
+ console.log('');
605
+ console.log(chalk.dim(' ─'.repeat(26)));
606
+ console.log('');
607
+ console.log(` ${chalk.bold('Try it for real:')} agentxchain init --governed`);
608
+ console.log(` ${chalk.bold('Read more:')} https://agentxchain.dev/docs/quickstart`);
609
+ console.log('');
610
+ }
611
+
612
+ if (jsonMode) {
613
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
614
+ }
615
+ } catch (err) {
616
+ result.ok = false;
617
+ result.error = err.message;
618
+ result.duration_ms = Date.now() - startTime;
619
+
620
+ if (jsonMode) {
621
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
622
+ } else {
623
+ console.error(chalk.red(`\n Demo failed: ${err.message}`));
624
+ if (verbose) console.error(chalk.dim(` ${err.stack}`));
625
+ }
626
+
627
+ process.exitCode = 1;
628
+ } finally {
629
+ // Always clean up
630
+ try { rmSync(root, { recursive: true, force: true }); } catch {}
631
+ }
632
+ }