agentxchain 2.25.1 → 2.26.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
@@ -88,6 +88,7 @@ Built-in governed templates:
88
88
  - `cli-tool`: command surface, platform support, distribution checklist
89
89
  - `library`: public API, compatibility policy, release and adoption checklist
90
90
  - `web-app`: user flows, UI acceptance, browser support
91
+ - `enterprise-app`: enterprise planning artifacts plus blueprint-backed `architect` and `security_reviewer` phases
91
92
 
92
93
  `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:
93
94
 
@@ -120,7 +120,7 @@ program
120
120
  .option('-y, --yes', 'Skip prompts, use defaults')
121
121
  .option('--governed', 'Create a governed project (orchestrator-owned state)')
122
122
  .option('--dir <path>', 'Scaffold target directory. Use "." for in-place bootstrap.')
123
- .option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app')
123
+ .option('--template <id>', 'Governed scaffold template: generic, api-service, cli-tool, library, web-app, enterprise-app')
124
124
  .option('--dev-command <parts...>', 'Governed local-dev command parts. Include {prompt} for argv prompt delivery.')
125
125
  .option('--dev-prompt-transport <mode>', 'Governed local-dev prompt transport: argv, stdin, dispatch_bundle_only')
126
126
  .option('--schema-version <version>', 'Schema version (3 for legacy, or use --governed for current)')
@@ -469,7 +469,7 @@ intakeCmd
469
469
  .description('Triage a detected intent — set priority, template, charter, and acceptance')
470
470
  .option('--intent <id>', 'Intent ID to triage')
471
471
  .option('--priority <level>', 'Priority level (p0, p1, p2, p3)')
472
- .option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app)')
472
+ .option('--template <id>', 'Governed template (generic, api-service, cli-tool, library, web-app, enterprise-app)')
473
473
  .option('--charter <text>', 'Delivery charter text')
474
474
  .option('--acceptance <text>', 'Comma-separated acceptance criteria')
475
475
  .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.25.1",
3
+ "version": "2.26.0",
4
4
  "description": "CLI for AgentXchain — governed multi-agent software delivery",
5
5
  "type": "module",
6
6
  "bin": {
@@ -142,7 +142,7 @@ const GOVERNED_GATES = {
142
142
  }
143
143
  };
144
144
 
145
- function buildGovernedPrompt(roleId, role) {
145
+ function buildGovernedPrompt(roleId, role, scaffoldContext = {}) {
146
146
  const rolePrompts = {
147
147
  pm: buildPmPrompt,
148
148
  dev: buildDevPrompt,
@@ -154,7 +154,7 @@ function buildGovernedPrompt(roleId, role) {
154
154
  if (builder) return builder(role);
155
155
 
156
156
  // Fallback for custom roles
157
- return buildGenericPrompt(roleId, role);
157
+ return buildGenericPrompt(roleId, role, scaffoldContext);
158
158
  }
159
159
 
160
160
  function buildPmPrompt(role) {
@@ -351,7 +351,66 @@ If you cannot resolve the deadlock:
351
351
  `;
352
352
  }
353
353
 
354
- function buildGenericPrompt(roleId, role) {
354
+ function summarizeRoleWorkflow(roleId, scaffoldContext = {}) {
355
+ const routing = scaffoldContext.routing || {};
356
+ const gates = scaffoldContext.gates || {};
357
+ const workflowKitConfig = scaffoldContext.workflowKitConfig || {};
358
+ const ownedPhases = Object.entries(routing)
359
+ .filter(([, route]) => route?.entry_role === roleId)
360
+ .map(([phaseName]) => phaseName);
361
+ const gateLines = [];
362
+ const artifactLines = [];
363
+ const ownershipLines = [];
364
+
365
+ for (const phaseName of ownedPhases) {
366
+ const exitGate = routing[phaseName]?.exit_gate;
367
+ if (exitGate) {
368
+ gateLines.push(`- ${phaseName}: ${exitGate}`);
369
+ }
370
+
371
+ const phaseArtifacts = Array.isArray(workflowKitConfig?.phases?.[phaseName]?.artifacts)
372
+ ? workflowKitConfig.phases[phaseName].artifacts.filter((artifact) => artifact?.path)
373
+ : [];
374
+ const workflowArtifacts = phaseArtifacts
375
+ .map((artifact) => artifact.path)
376
+ .filter(Boolean);
377
+ const gateArtifacts = Array.isArray(gates?.[exitGate]?.requires_files)
378
+ ? gates[exitGate].requires_files.filter(Boolean)
379
+ : [];
380
+ const ownedArtifacts = [...new Set([...workflowArtifacts, ...gateArtifacts])];
381
+ if (ownedArtifacts.length > 0) {
382
+ artifactLines.push(`- ${phaseName}: ${ownedArtifacts.join(', ')}`);
383
+ }
384
+
385
+ const enforcedArtifacts = phaseArtifacts
386
+ .filter((artifact) => artifact.owned_by === roleId)
387
+ .map((artifact) => artifact.path);
388
+ if (enforcedArtifacts.length > 0) {
389
+ const verb = enforcedArtifacts.length === 1 ? 'requires' : 'require';
390
+ ownershipLines.push(
391
+ `- ${phaseName}: ${enforcedArtifacts.join(', ')} ${verb} an accepted turn from you before the gate can pass`,
392
+ );
393
+ }
394
+ }
395
+
396
+ return { ownedPhases, gateLines, artifactLines, ownershipLines };
397
+ }
398
+
399
+ function buildGenericPrompt(roleId, role, scaffoldContext = {}) {
400
+ const workflowSummary = summarizeRoleWorkflow(roleId, scaffoldContext);
401
+ const primaryPhasesSection = workflowSummary.ownedPhases.length > 0
402
+ ? `\n## Primary Phases\n\n- ${workflowSummary.ownedPhases.join(', ')}\n`
403
+ : '';
404
+ const phaseGatesSection = workflowSummary.gateLines.length > 0
405
+ ? `\n## Phase Gates\n\n${workflowSummary.gateLines.join('\n')}\n`
406
+ : '';
407
+ const workflowArtifactsSection = workflowSummary.artifactLines.length > 0
408
+ ? `\n## Workflow Artifacts You Own\n\n${workflowSummary.artifactLines.join('\n')}\n`
409
+ : '';
410
+ const ownershipSection = workflowSummary.ownershipLines.length > 0
411
+ ? `\n## Ownership Enforcement\n\n${workflowSummary.ownershipLines.join('\n')}\n`
412
+ : '';
413
+
355
414
  return `# ${role.title} — Role Prompt
356
415
 
357
416
  You are the **${role.title}** on this project.
@@ -371,7 +430,7 @@ ${role.write_authority === 'authoritative'
371
430
  ? 'You may modify product files directly.'
372
431
  : role.write_authority === 'proposed'
373
432
  ? 'You may propose changes via patches.'
374
- : 'You may NOT modify product files. Only create review artifacts under `.planning/` and `.agentxchain/reviews/`.'}
433
+ : 'You may NOT modify product files. Only create review artifacts under `.planning/` and `.agentxchain/reviews/`.'}${primaryPhasesSection}${phaseGatesSection}${workflowArtifactsSection}${ownershipSection}
375
434
  `;
376
435
  }
377
436
 
@@ -454,13 +513,118 @@ function formatInitTarget(dir) {
454
513
  return dir;
455
514
  }
456
515
 
457
- export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}) {
516
+ function generateWorkflowKitPlaceholder(artifact, projectName) {
517
+ const filename = basename(artifact.path);
518
+ const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
519
+
520
+ if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
521
+ const sections = artifact.semantics_config.required_sections
522
+ .map(s => `${s}\n\n(Content here.)\n`)
523
+ .join('\n');
524
+ return `# ${title} — ${projectName}\n\n${sections}`;
525
+ }
526
+
527
+ return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
528
+ }
529
+
530
+ function cloneJsonCompatible(value) {
531
+ return value == null ? value : JSON.parse(JSON.stringify(value));
532
+ }
533
+
534
+ function buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig) {
535
+ const blueprint = template.scaffold_blueprint || null;
536
+ const roles = cloneJsonCompatible(blueprint?.roles || GOVERNED_ROLES);
537
+ const runtimes = cloneJsonCompatible(blueprint?.runtimes || GOVERNED_RUNTIMES);
538
+
539
+ if (!blueprint || Object.values(roles).some((role) => role?.runtime === 'local-dev')) {
540
+ runtimes['local-dev'] = localDevRuntime;
541
+ }
542
+
543
+ const routing = cloneJsonCompatible(blueprint?.routing || GOVERNED_ROUTING);
544
+ const gates = cloneJsonCompatible(blueprint?.gates || GOVERNED_GATES);
545
+ const effectiveWorkflowKitConfig = workflowKitConfig || cloneJsonCompatible(blueprint?.workflow_kit || null);
546
+ const prompts = Object.fromEntries(
547
+ Object.keys(roles).map((roleId) => [roleId, `.agentxchain/prompts/${roleId}.md`])
548
+ );
549
+
550
+ return {
551
+ roles,
552
+ runtimes,
553
+ routing,
554
+ gates,
555
+ prompts,
556
+ workflowKitConfig: effectiveWorkflowKitConfig,
557
+ };
558
+ }
559
+
560
+ function buildRoadmapPhaseTable(routing, roles) {
561
+ const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
562
+ const phaseName = phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
563
+ const entryRole = phaseConfig.entry_role;
564
+ const role = roles[entryRole];
565
+ const goal = role?.mandate || phaseName;
566
+ const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
567
+ return `| ${phaseName} | ${goal} | ${status} |`;
568
+ });
569
+ return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
570
+ }
571
+
572
+ function buildPlanningSummaryLines(template, workflowKitConfig) {
573
+ const lines = [
574
+ 'PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md',
575
+ 'acceptance-matrix.md / ship-verdict.md',
576
+ 'RELEASE_NOTES.md',
577
+ ];
578
+ const templatePlanningFiles = Array.isArray(template?.planning_artifacts)
579
+ ? template.planning_artifacts
580
+ .map((artifact) => artifact?.filename)
581
+ .filter(Boolean)
582
+ : [];
583
+ const defaultScaffoldPaths = new Set([
584
+ '.planning/PM_SIGNOFF.md',
585
+ '.planning/ROADMAP.md',
586
+ '.planning/SYSTEM_SPEC.md',
587
+ '.planning/IMPLEMENTATION_NOTES.md',
588
+ '.planning/acceptance-matrix.md',
589
+ '.planning/ship-verdict.md',
590
+ '.planning/RELEASE_NOTES.md',
591
+ ]);
592
+ const customWorkflowFiles = [];
593
+
594
+ if (workflowKitConfig?.phases && typeof workflowKitConfig.phases === 'object') {
595
+ for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
596
+ if (!Array.isArray(phaseConfig?.artifacts)) continue;
597
+ for (const artifact of phaseConfig.artifacts) {
598
+ if (!artifact?.path || defaultScaffoldPaths.has(artifact.path)) continue;
599
+ customWorkflowFiles.push(basename(artifact.path));
600
+ }
601
+ }
602
+ }
603
+
604
+ if (templatePlanningFiles.length > 0) {
605
+ lines.push(`template: ${templatePlanningFiles.join(' / ')}`);
606
+ }
607
+ const uniqueCustomWorkflowFiles = [...new Set(customWorkflowFiles)];
608
+ if (uniqueCustomWorkflowFiles.length > 0) {
609
+ lines.push(`workflow: ${uniqueCustomWorkflowFiles.join(' / ')}`);
610
+ }
611
+
612
+ return lines;
613
+ }
614
+
615
+ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}, workflowKitConfig = null) {
458
616
  const template = loadGovernedTemplate(templateId);
459
617
  const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
460
- const runtimes = {
461
- ...GOVERNED_RUNTIMES,
462
- 'local-dev': localDevRuntime,
463
- };
618
+ const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
619
+ const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
620
+ const initialPhase = Object.keys(routing)[0] || 'planning';
621
+ const phaseGateStatus = Object.fromEntries(
622
+ [...new Set(
623
+ Object.values(routing)
624
+ .map((route) => route?.exit_gate)
625
+ .filter(Boolean)
626
+ )].map((gateId) => [gateId, 'pending'])
627
+ );
464
628
  const config = {
465
629
  schema_version: '1.0',
466
630
  template: template.id,
@@ -469,10 +633,10 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
469
633
  name: projectName,
470
634
  default_branch: 'main'
471
635
  },
472
- roles: GOVERNED_ROLES,
636
+ roles,
473
637
  runtimes,
474
- routing: GOVERNED_ROUTING,
475
- gates: GOVERNED_GATES,
638
+ routing,
639
+ gates,
476
640
  budget: {
477
641
  per_turn_max_usd: 2.0,
478
642
  per_run_max_usd: 50.0,
@@ -482,25 +646,23 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
482
646
  talk_strategy: 'append_only',
483
647
  history_strategy: 'jsonl_append_only'
484
648
  },
485
- prompts: {
486
- pm: '.agentxchain/prompts/pm.md',
487
- dev: '.agentxchain/prompts/dev.md',
488
- qa: '.agentxchain/prompts/qa.md',
489
- eng_director: '.agentxchain/prompts/eng_director.md'
490
- },
649
+ prompts,
491
650
  rules: {
492
651
  challenge_required: true,
493
652
  max_turn_retries: 2,
494
653
  max_deadlock_cycles: 2
495
654
  }
496
655
  };
656
+ if (effectiveWorkflowKitConfig) {
657
+ config.workflow_kit = effectiveWorkflowKitConfig;
658
+ }
497
659
 
498
660
  const state = {
499
661
  schema_version: '1.1',
500
662
  run_id: null,
501
663
  project_id: projectId,
502
664
  status: 'idle',
503
- phase: 'planning',
665
+ phase: initialPhase,
504
666
  accepted_integration_ref: null,
505
667
  active_turns: {},
506
668
  turn_sequence: 0,
@@ -510,11 +672,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
510
672
  escalation: null,
511
673
  queued_phase_transition: null,
512
674
  queued_run_completion: null,
513
- phase_gate_status: {
514
- planning_signoff: 'pending',
515
- implementation_complete: 'pending',
516
- qa_ship_verdict: 'pending'
517
- },
675
+ phase_gate_status: phaseGateStatus,
518
676
  budget_reservations: {},
519
677
  budget_status: {
520
678
  spent_usd: 0,
@@ -536,15 +694,19 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
536
694
  writeFileSync(join(dir, '.agentxchain', 'decision-ledger.jsonl'), '');
537
695
 
538
696
  // Prompt templates
539
- for (const [roleId, role] of Object.entries(GOVERNED_ROLES)) {
540
- const basePrompt = buildGovernedPrompt(roleId, role);
697
+ for (const [roleId, role] of Object.entries(roles)) {
698
+ const basePrompt = buildGovernedPrompt(roleId, role, {
699
+ routing,
700
+ gates,
701
+ workflowKitConfig: effectiveWorkflowKitConfig,
702
+ });
541
703
  const prompt = appendPromptOverride(basePrompt, template.prompt_overrides?.[roleId]);
542
704
  writeFileSync(join(dir, '.agentxchain', 'prompts', `${roleId}.md`), prompt);
543
705
  }
544
706
 
545
707
  // Planning artifacts
546
708
  writeFileSync(join(dir, '.planning', 'PM_SIGNOFF.md'), `# PM Signoff — ${projectName}\n\nApproved: NO\n\n> This scaffold starts blocked on purpose. Change this to \`Approved: YES\` only after a human reviews the planning artifacts and is ready to open the planning gate.\n\n## Discovery Checklist\n- [ ] Target user defined\n- [ ] Core pain point defined\n- [ ] Core workflow defined\n- [ ] MVP scope defined\n- [ ] Out-of-scope list defined\n- [ ] Success metric defined\n\n## Notes for team\n(PM and human add final kickoff notes here.)\n`);
547
- writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${projectName}\n\n## Phases\n\n| Phase | Goal | Status |\n|-------|------|--------|\n| Planning | Align scope, requirements, acceptance criteria | In progress |\n| Implementation | Build and verify | Pending |\n| QA | Challenge correctness and ship readiness | Pending |\n`);
709
+ writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${projectName}\n\n## Phases\n\n${buildRoadmapPhaseTable(routing, roles)}`);
548
710
  writeFileSync(join(dir, '.planning', 'SYSTEM_SPEC.md'), buildSystemSpecContent(projectName, template.system_spec_overlay));
549
711
  writeFileSync(join(dir, '.planning', 'IMPLEMENTATION_NOTES.md'), `# Implementation Notes — ${projectName}\n\n## Changes\n\n(Dev fills this during implementation)\n\n## Verification\n\n(Dev fills this during implementation)\n\n## Unresolved Follow-ups\n\n(Dev lists any known gaps, tech debt, or follow-up items here.)\n`);
550
712
  const baseAcceptanceMatrix = `# Acceptance Matrix — ${projectName}\n\n| Req # | Requirement | Acceptance criteria | Test status | Last tested | Status |\n|-------|-------------|-------------------|-------------|-------------|--------|\n| (QA fills this from ROADMAP.md) | | | | | |\n`;
@@ -561,6 +723,37 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
561
723
  );
562
724
  }
563
725
 
726
+ // Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
727
+ // that are not already handled by the default scaffold above
728
+ if (effectiveWorkflowKitConfig && effectiveWorkflowKitConfig.phases && typeof effectiveWorkflowKitConfig.phases === 'object') {
729
+ const defaultScaffoldPaths = new Set([
730
+ '.planning/PM_SIGNOFF.md',
731
+ '.planning/ROADMAP.md',
732
+ '.planning/SYSTEM_SPEC.md',
733
+ '.planning/IMPLEMENTATION_NOTES.md',
734
+ '.planning/acceptance-matrix.md',
735
+ '.planning/ship-verdict.md',
736
+ '.planning/RELEASE_NOTES.md',
737
+ ]);
738
+
739
+ for (const phaseConfig of Object.values(effectiveWorkflowKitConfig.phases)) {
740
+ if (!Array.isArray(phaseConfig.artifacts)) continue;
741
+ for (const artifact of phaseConfig.artifacts) {
742
+ if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
743
+ const absPath = join(dir, artifact.path);
744
+ if (existsSync(absPath)) continue;
745
+
746
+ // Ensure parent directory exists
747
+ const parentDir = dirname(absPath);
748
+ if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
749
+
750
+ // Generate placeholder content based on semantics type
751
+ const content = generateWorkflowKitPlaceholder(artifact, projectName);
752
+ writeFileSync(absPath, content);
753
+ }
754
+ }
755
+ }
756
+
564
757
  // TALK.md
565
758
  writeFileSync(join(dir, 'TALK.md'), `# ${projectName} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n---\n\n`);
566
759
 
@@ -584,6 +777,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
584
777
  async function initGoverned(opts) {
585
778
  let projectName, folderName;
586
779
  const templateId = opts.template || 'generic';
780
+ let selectedTemplate;
587
781
  let explicitDir;
588
782
 
589
783
  try {
@@ -604,6 +798,7 @@ async function initGoverned(opts) {
604
798
  console.error(' web-app Governed scaffold for a web application');
605
799
  process.exit(1);
606
800
  }
801
+ selectedTemplate = loadGovernedTemplate(templateId);
607
802
 
608
803
  if (opts.yes) {
609
804
  projectName = explicitDir
@@ -662,47 +857,68 @@ async function initGoverned(opts) {
662
857
 
663
858
  if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
664
859
 
665
- scaffoldGoverned(dir, projectName, projectId, templateId, opts);
860
+ // If reinitializing a project that has explicit workflow_kit config, preserve it for scaffold
861
+ let workflowKitConfig = null;
862
+ const existingConfigPath = join(dir, CONFIG_FILE);
863
+ if (existsSync(existingConfigPath)) {
864
+ try {
865
+ const existing = JSON.parse(readFileSync(existingConfigPath, 'utf8'));
866
+ if (existing.workflow_kit && typeof existing.workflow_kit === 'object' && Object.keys(existing.workflow_kit).length > 0) {
867
+ workflowKitConfig = existing.workflow_kit;
868
+ }
869
+ } catch {
870
+ // Ignore parse errors — scaffold will overwrite the config anyway
871
+ }
872
+ }
873
+
874
+ const { config } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
666
875
 
667
876
  console.log('');
668
877
  console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
669
878
  console.log('');
879
+ const promptRoleIds = Object.keys(config.roles);
880
+ const phaseNames = Object.keys(config.routing);
670
881
  console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim('(governed)')}`);
671
882
  console.log(` ${chalk.dim('├──')} .agentxchain/`);
672
883
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} state.json / history.jsonl / decision-ledger.jsonl`);
673
884
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} staging/`);
674
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} prompts/ ${chalk.dim('(pm, dev, qa, eng_director)')}`);
885
+ console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} prompts/ ${chalk.dim(`(${promptRoleIds.join(', ')})`)}`);
675
886
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} reviews/`);
676
887
  console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} dispatch/`);
677
888
  console.log(` ${chalk.dim('├──')} .planning/`);
678
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md`);
679
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} acceptance-matrix.md / ship-verdict.md`);
680
- console.log(` ${chalk.dim('')} ${chalk.dim('└──')} RELEASE_NOTES.md`);
889
+ const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate, config.workflow_kit);
890
+ for (const [index, line] of planningSummaryLines.entries()) {
891
+ const branch = index === planningSummaryLines.length - 1 ? '└──' : '├──';
892
+ console.log(` ${chalk.dim('│')} ${chalk.dim(branch)} ${line}`);
893
+ }
681
894
  console.log(` ${chalk.dim('└──')} TALK.md`);
682
895
  console.log('');
683
- console.log(` ${chalk.dim('Roles:')} pm, dev, qa, eng_director`);
684
- console.log(` ${chalk.dim('Phases:')} planningimplementation → qa ${chalk.dim('(default; extend via routing in agentxchain.json)')}`);
896
+ console.log(` ${chalk.dim('Roles:')} ${promptRoleIds.join(', ')}`);
897
+ console.log(` ${chalk.dim('Phases:')} ${phaseNames.join('')} ${chalk.dim(selectedTemplate.scaffold_blueprint ? '(template-defined; edit routing in agentxchain.json to customize)' : '(default; extend via routing in agentxchain.json)')}`);
685
898
  console.log(` ${chalk.dim('Template:')} ${templateId}`);
686
899
  console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
687
900
  console.log(` ${chalk.dim('Protocol:')} governed convergence`);
688
901
  console.log('');
689
902
 
690
903
  // Readiness hint: tell user which roles work immediately vs which need API keys
691
- const allRuntimes = { ...GOVERNED_RUNTIMES, 'local-dev': localDevRuntime };
692
- const needsKey = Object.entries(allRuntimes)
693
- .filter(([, rt]) => rt.auth_env)
694
- .map(([id, rt]) => ({ id, env: rt.auth_env }));
695
- if (needsKey.length > 0) {
696
- const envVars = [...new Set(needsKey.map(r => r.env))];
697
- const roleNames = needsKey.map(r => r.id);
904
+ const allRuntimes = config.runtimes;
905
+ const manualRoleIds = Object.entries(config.roles)
906
+ .filter(([, role]) => allRuntimes[role.runtime]?.type === 'manual')
907
+ .map(([roleId]) => roleId);
908
+ const rolesNeedingKeys = Object.entries(config.roles)
909
+ .filter(([, role]) => Boolean(allRuntimes[role.runtime]?.auth_env))
910
+ .map(([roleId, role]) => ({ roleId, env: allRuntimes[role.runtime].auth_env }));
911
+ if (rolesNeedingKeys.length > 0) {
912
+ const envVars = [...new Set(rolesNeedingKeys.map((r) => r.env))];
913
+ const roleNames = rolesNeedingKeys.map((r) => r.roleId);
698
914
  const hasKeys = envVars.every(v => process.env[v]);
699
915
  if (hasKeys) {
700
916
  console.log(` ${chalk.green('Ready:')} all runtimes configured (${envVars.join(', ')} detected)`);
701
917
  } else {
702
- console.log(` ${chalk.yellow('Mixed-mode:')} pm and eng_director work immediately (manual).`);
918
+ console.log(` ${chalk.yellow('Mixed-mode:')} ${manualRoleIds.join(', ')} work immediately (manual).`);
703
919
  console.log(` ${chalk.yellow(' ')}${roleNames.join(', ')} need ${chalk.bold(envVars.join(', '))} to dispatch automatically.`);
704
920
  console.log(` ${chalk.yellow(' ')}Without it, those turns fall back to manual input.`);
705
- if (allRuntimes['manual-qa']) {
921
+ if (config.roles?.qa?.runtime === 'api-qa' && allRuntimes['manual-qa']) {
706
922
  console.log(` ${chalk.yellow(' ')}No-key QA path: change ${chalk.bold('roles.qa.runtime')} from ${chalk.bold('"api-qa"')} to ${chalk.bold('"manual-qa"')} in ${chalk.bold('agentxchain.json')}.`);
707
923
  }
708
924
  }
@@ -10,6 +10,7 @@ export function templateListCommand(opts) {
10
10
  description: t.description,
11
11
  planning_artifacts: (t.planning_artifacts || []).map((a) => a.filename),
12
12
  prompt_overrides: Object.keys(t.prompt_overrides || {}),
13
+ scaffold_blueprint_roles: Object.keys(t.scaffold_blueprint?.roles || {}),
13
14
  acceptance_hints: t.acceptance_hints || [],
14
15
  }));
15
16
  console.log(JSON.stringify(output, null, 2));
@@ -27,6 +28,9 @@ export function templateListCommand(opts) {
27
28
  if (t.prompt_overrides && Object.keys(t.prompt_overrides).length > 0) {
28
29
  console.log(` Prompt overrides: ${Object.keys(t.prompt_overrides).join(', ')}`);
29
30
  }
31
+ if (t.scaffold_blueprint?.roles && Object.keys(t.scaffold_blueprint.roles).length > 0) {
32
+ console.log(` Scaffold roles: ${Object.keys(t.scaffold_blueprint.roles).join(', ')}`);
33
+ }
30
34
  console.log('');
31
35
  }
32
36
  console.log(chalk.dim(` Usage: agentxchain template set <id>\n`));
@@ -64,6 +64,12 @@ export async function templateSetCommand(templateId, opts) {
64
64
 
65
65
  // ── Load manifest ─────────────────────────────────────────────────────
66
66
  const manifest = loadGovernedTemplate(templateId);
67
+ if (manifest.scaffold_blueprint) {
68
+ console.error(chalk.red(` Error: Template "${templateId}" defines a custom governed team blueprint.`));
69
+ console.error(chalk.yellow(` Use ${chalk.bold(`agentxchain init --governed --template ${templateId}`)} for new repos.`));
70
+ console.error(chalk.yellow(' Retrofitting an existing repo to a blueprint-backed team is deferred until a dedicated migrator exists.'));
71
+ process.exit(1);
72
+ }
67
73
  const projectName = config.project?.name || 'Untitled';
68
74
 
69
75
  // ── Build mutation plan ───────────────────────────────────────────────