agentxchain 2.25.2 → 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.2",
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
 
@@ -468,13 +527,104 @@ function generateWorkflowKitPlaceholder(artifact, projectName) {
468
527
  return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
469
528
  }
470
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
+
471
615
  export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}, workflowKitConfig = null) {
472
616
  const template = loadGovernedTemplate(templateId);
473
617
  const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
474
- const runtimes = {
475
- ...GOVERNED_RUNTIMES,
476
- 'local-dev': localDevRuntime,
477
- };
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
+ );
478
628
  const config = {
479
629
  schema_version: '1.0',
480
630
  template: template.id,
@@ -483,10 +633,10 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
483
633
  name: projectName,
484
634
  default_branch: 'main'
485
635
  },
486
- roles: GOVERNED_ROLES,
636
+ roles,
487
637
  runtimes,
488
- routing: GOVERNED_ROUTING,
489
- gates: GOVERNED_GATES,
638
+ routing,
639
+ gates,
490
640
  budget: {
491
641
  per_turn_max_usd: 2.0,
492
642
  per_run_max_usd: 50.0,
@@ -496,25 +646,23 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
496
646
  talk_strategy: 'append_only',
497
647
  history_strategy: 'jsonl_append_only'
498
648
  },
499
- prompts: {
500
- pm: '.agentxchain/prompts/pm.md',
501
- dev: '.agentxchain/prompts/dev.md',
502
- qa: '.agentxchain/prompts/qa.md',
503
- eng_director: '.agentxchain/prompts/eng_director.md'
504
- },
649
+ prompts,
505
650
  rules: {
506
651
  challenge_required: true,
507
652
  max_turn_retries: 2,
508
653
  max_deadlock_cycles: 2
509
654
  }
510
655
  };
656
+ if (effectiveWorkflowKitConfig) {
657
+ config.workflow_kit = effectiveWorkflowKitConfig;
658
+ }
511
659
 
512
660
  const state = {
513
661
  schema_version: '1.1',
514
662
  run_id: null,
515
663
  project_id: projectId,
516
664
  status: 'idle',
517
- phase: 'planning',
665
+ phase: initialPhase,
518
666
  accepted_integration_ref: null,
519
667
  active_turns: {},
520
668
  turn_sequence: 0,
@@ -524,11 +672,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
524
672
  escalation: null,
525
673
  queued_phase_transition: null,
526
674
  queued_run_completion: null,
527
- phase_gate_status: {
528
- planning_signoff: 'pending',
529
- implementation_complete: 'pending',
530
- qa_ship_verdict: 'pending'
531
- },
675
+ phase_gate_status: phaseGateStatus,
532
676
  budget_reservations: {},
533
677
  budget_status: {
534
678
  spent_usd: 0,
@@ -550,15 +694,19 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
550
694
  writeFileSync(join(dir, '.agentxchain', 'decision-ledger.jsonl'), '');
551
695
 
552
696
  // Prompt templates
553
- for (const [roleId, role] of Object.entries(GOVERNED_ROLES)) {
554
- 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
+ });
555
703
  const prompt = appendPromptOverride(basePrompt, template.prompt_overrides?.[roleId]);
556
704
  writeFileSync(join(dir, '.agentxchain', 'prompts', `${roleId}.md`), prompt);
557
705
  }
558
706
 
559
707
  // Planning artifacts
560
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`);
561
- 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)}`);
562
710
  writeFileSync(join(dir, '.planning', 'SYSTEM_SPEC.md'), buildSystemSpecContent(projectName, template.system_spec_overlay));
563
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`);
564
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`;
@@ -577,7 +725,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
577
725
 
578
726
  // Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
579
727
  // that are not already handled by the default scaffold above
580
- if (workflowKitConfig && workflowKitConfig.phases && typeof workflowKitConfig.phases === 'object') {
728
+ if (effectiveWorkflowKitConfig && effectiveWorkflowKitConfig.phases && typeof effectiveWorkflowKitConfig.phases === 'object') {
581
729
  const defaultScaffoldPaths = new Set([
582
730
  '.planning/PM_SIGNOFF.md',
583
731
  '.planning/ROADMAP.md',
@@ -588,7 +736,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
588
736
  '.planning/RELEASE_NOTES.md',
589
737
  ]);
590
738
 
591
- for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
739
+ for (const phaseConfig of Object.values(effectiveWorkflowKitConfig.phases)) {
592
740
  if (!Array.isArray(phaseConfig.artifacts)) continue;
593
741
  for (const artifact of phaseConfig.artifacts) {
594
742
  if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
@@ -629,6 +777,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
629
777
  async function initGoverned(opts) {
630
778
  let projectName, folderName;
631
779
  const templateId = opts.template || 'generic';
780
+ let selectedTemplate;
632
781
  let explicitDir;
633
782
 
634
783
  try {
@@ -649,6 +798,7 @@ async function initGoverned(opts) {
649
798
  console.error(' web-app Governed scaffold for a web application');
650
799
  process.exit(1);
651
800
  }
801
+ selectedTemplate = loadGovernedTemplate(templateId);
652
802
 
653
803
  if (opts.yes) {
654
804
  projectName = explicitDir
@@ -721,47 +871,54 @@ async function initGoverned(opts) {
721
871
  }
722
872
  }
723
873
 
724
- scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
874
+ const { config } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
725
875
 
726
876
  console.log('');
727
877
  console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
728
878
  console.log('');
879
+ const promptRoleIds = Object.keys(config.roles);
880
+ const phaseNames = Object.keys(config.routing);
729
881
  console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim('(governed)')}`);
730
882
  console.log(` ${chalk.dim('├──')} .agentxchain/`);
731
883
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} state.json / history.jsonl / decision-ledger.jsonl`);
732
884
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} staging/`);
733
- 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(', ')})`)}`);
734
886
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} reviews/`);
735
887
  console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} dispatch/`);
736
888
  console.log(` ${chalk.dim('├──')} .planning/`);
737
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md`);
738
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} acceptance-matrix.md / ship-verdict.md`);
739
- 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
+ }
740
894
  console.log(` ${chalk.dim('└──')} TALK.md`);
741
895
  console.log('');
742
- console.log(` ${chalk.dim('Roles:')} pm, dev, qa, eng_director`);
743
- 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)')}`);
744
898
  console.log(` ${chalk.dim('Template:')} ${templateId}`);
745
899
  console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
746
900
  console.log(` ${chalk.dim('Protocol:')} governed convergence`);
747
901
  console.log('');
748
902
 
749
903
  // Readiness hint: tell user which roles work immediately vs which need API keys
750
- const allRuntimes = { ...GOVERNED_RUNTIMES, 'local-dev': localDevRuntime };
751
- const needsKey = Object.entries(allRuntimes)
752
- .filter(([, rt]) => rt.auth_env)
753
- .map(([id, rt]) => ({ id, env: rt.auth_env }));
754
- if (needsKey.length > 0) {
755
- const envVars = [...new Set(needsKey.map(r => r.env))];
756
- 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);
757
914
  const hasKeys = envVars.every(v => process.env[v]);
758
915
  if (hasKeys) {
759
916
  console.log(` ${chalk.green('Ready:')} all runtimes configured (${envVars.join(', ')} detected)`);
760
917
  } else {
761
- 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).`);
762
919
  console.log(` ${chalk.yellow(' ')}${roleNames.join(', ')} need ${chalk.bold(envVars.join(', '))} to dispatch automatically.`);
763
920
  console.log(` ${chalk.yellow(' ')}Without it, those turns fall back to manual input.`);
764
- if (allRuntimes['manual-qa']) {
921
+ if (config.roles?.qa?.runtime === 'api-qa' && allRuntimes['manual-qa']) {
765
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')}.`);
766
923
  }
767
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 ───────────────────────────────────────────────
@@ -54,10 +54,15 @@ function buildEffectiveGateArtifacts(config, gateDef, phase) {
54
54
  required: false,
55
55
  useLegacySemantics: false,
56
56
  semanticChecks: [],
57
+ owned_by: null,
57
58
  };
58
59
 
59
60
  existing.required = existing.required || artifact.required !== false;
60
61
 
62
+ if (artifact.owned_by && typeof artifact.owned_by === 'string') {
63
+ existing.owned_by = artifact.owned_by;
64
+ }
65
+
61
66
  if (artifact.semantics) {
62
67
  const legacySemanticId = existing.useLegacySemantics ? getSemanticIdForPath(artifact.path) : null;
63
68
  if (artifact.semantics !== legacySemanticId) {
@@ -87,7 +92,17 @@ function prefixSemanticReason(filePath, reason) {
87
92
  return `${filePath}: ${reason}`;
88
93
  }
89
94
 
90
- function evaluateGateArtifacts({ root, config, gateDef, phase, result }) {
95
+ function hasRoleParticipationInPhase(state, phase, roleId) {
96
+ const history = state?.history;
97
+ if (!Array.isArray(history)) {
98
+ return false;
99
+ }
100
+ return history.some(
101
+ turn => turn.phase === phase && turn.role === roleId,
102
+ );
103
+ }
104
+
105
+ function evaluateGateArtifacts({ root, config, gateDef, phase, result, state }) {
91
106
  const failures = [];
92
107
  const artifacts = buildEffectiveGateArtifacts(config, gateDef, phase);
93
108
 
@@ -118,6 +133,13 @@ function evaluateGateArtifacts({ root, config, gateDef, phase, result }) {
118
133
  failures.push(prefixSemanticReason(artifact.path, semanticCheck.reason));
119
134
  }
120
135
  }
136
+
137
+ // Charter enforcement: verify owning role participated in this phase
138
+ if (artifact.owned_by && !hasRoleParticipationInPhase(state, phase, artifact.owned_by)) {
139
+ failures.push(
140
+ `"${artifact.path}" requires participation from role "${artifact.owned_by}" in phase "${phase}", but no accepted turn from that role was found`,
141
+ );
142
+ }
121
143
  }
122
144
 
123
145
  return failures;
@@ -219,13 +241,14 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
219
241
 
220
242
  const failures = [];
221
243
 
222
- // Predicate: requires_files
244
+ // Predicate: requires_files + ownership
223
245
  failures.push(...evaluateGateArtifacts({
224
246
  root,
225
247
  config,
226
248
  gateDef,
227
249
  phase: currentPhase,
228
250
  result,
251
+ state,
229
252
  }));
230
253
 
231
254
  // Predicate: requires_verification_pass
@@ -341,6 +364,7 @@ export function evaluateRunCompletion({ state, config, acceptedTurn, root }) {
341
364
  gateDef,
342
365
  phase: currentPhase,
343
366
  result,
367
+ state,
344
368
  }));
345
369
 
346
370
  if (gateDef.requires_verification_pass) {
@@ -2129,6 +2129,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2129
2129
  turn_id: turnResult.turn_id,
2130
2130
  run_id: turnResult.run_id,
2131
2131
  role: turnResult.role,
2132
+ phase: state.phase,
2132
2133
  runtime_id: turnResult.runtime_id,
2133
2134
  status: turnResult.status,
2134
2135
  summary: turnResult.summary,
@@ -2280,12 +2281,13 @@ function _acceptGovernedTurnLocked(root, config, opts) {
2280
2281
  };
2281
2282
  }
2282
2283
  } else {
2284
+ const nextHistoryEntries = [...historyEntries, historyEntry];
2283
2285
  const postAcceptanceState = {
2284
2286
  ...state,
2285
2287
  active_turns: remainingTurns,
2286
2288
  turn_sequence: acceptedSequence,
2289
+ history: nextHistoryEntries,
2287
2290
  };
2288
- const nextHistoryEntries = [...historyEntries, historyEntry];
2289
2291
  const completionSource = turnResult.run_completion_request
2290
2292
  ? turnResult
2291
2293
  : findHistoryTurnRequest(nextHistoryEntries, state.queued_run_completion?.requested_by_turn, 'run_completion');
@@ -1,6 +1,7 @@
1
1
  import { existsSync, readFileSync, readdirSync } from 'node:fs';
2
2
  import { dirname, join } from 'node:path';
3
3
  import { fileURLToPath } from 'node:url';
4
+ import { validateV4Config } from './normalized-config.js';
4
5
 
5
6
  const __dirname = dirname(fileURLToPath(import.meta.url));
6
7
 
@@ -11,9 +12,10 @@ export const VALID_GOVERNED_TEMPLATE_IDS = Object.freeze([
11
12
  'cli-tool',
12
13
  'library',
13
14
  'web-app',
15
+ 'enterprise-app',
14
16
  ]);
15
17
 
16
- const VALID_PROMPT_OVERRIDE_ROLES = new Set(['pm', 'dev', 'qa', 'eng_director']);
18
+ const VALID_ROLE_ID_PATTERN = /^[a-z0-9_-]+$/;
17
19
 
18
20
  function validatePlanningArtifacts(artifacts, errors) {
19
21
  if (!Array.isArray(artifacts)) {
@@ -50,8 +52,8 @@ function validatePromptOverrides(promptOverrides, errors) {
50
52
  }
51
53
 
52
54
  for (const [roleId, content] of Object.entries(promptOverrides)) {
53
- if (!VALID_PROMPT_OVERRIDE_ROLES.has(roleId)) {
54
- errors.push(`prompt_overrides contains unknown role "${roleId}"`);
55
+ if (!VALID_ROLE_ID_PATTERN.test(roleId)) {
56
+ errors.push(`prompt_overrides contains invalid role ID "${roleId}" (must match ${VALID_ROLE_ID_PATTERN})`);
55
57
  }
56
58
  if (typeof content !== 'string' || !content.trim()) {
57
59
  errors.push(`prompt_overrides["${roleId}"] must be a non-empty string`);
@@ -73,6 +75,47 @@ function validateAcceptanceHints(acceptanceHints, errors) {
73
75
  }
74
76
  }
75
77
 
78
+ const VALID_SCAFFOLD_BLUEPRINT_KEYS = new Set([
79
+ 'roles',
80
+ 'runtimes',
81
+ 'routing',
82
+ 'gates',
83
+ 'workflow_kit',
84
+ ]);
85
+
86
+ function validateScaffoldBlueprint(scaffoldBlueprint, errors) {
87
+ if (scaffoldBlueprint === undefined) return;
88
+ if (!scaffoldBlueprint || typeof scaffoldBlueprint !== 'object' || Array.isArray(scaffoldBlueprint)) {
89
+ errors.push('scaffold_blueprint must be an object when provided');
90
+ return;
91
+ }
92
+
93
+ for (const key of Object.keys(scaffoldBlueprint)) {
94
+ if (!VALID_SCAFFOLD_BLUEPRINT_KEYS.has(key)) {
95
+ errors.push(`scaffold_blueprint contains unknown key "${key}"`);
96
+ }
97
+ }
98
+
99
+ const validation = validateV4Config({
100
+ schema_version: '1.0',
101
+ project: {
102
+ id: 'template-manifest',
103
+ name: 'Template Manifest',
104
+ },
105
+ roles: scaffoldBlueprint.roles,
106
+ runtimes: scaffoldBlueprint.runtimes,
107
+ routing: scaffoldBlueprint.routing,
108
+ gates: scaffoldBlueprint.gates,
109
+ workflow_kit: scaffoldBlueprint.workflow_kit,
110
+ });
111
+
112
+ if (!validation.ok) {
113
+ for (const error of validation.errors) {
114
+ errors.push(`scaffold_blueprint ${error}`);
115
+ }
116
+ }
117
+ }
118
+
76
119
  const VALID_SPEC_OVERLAY_KEYS = new Set([
77
120
  'purpose_guidance',
78
121
  'interface_guidance',
@@ -138,6 +181,7 @@ export function validateGovernedTemplateManifest(manifest, expectedId = null) {
138
181
  validatePromptOverrides(manifest.prompt_overrides, errors);
139
182
  validateAcceptanceHints(manifest.acceptance_hints, errors);
140
183
  validateSystemSpecOverlay(manifest.system_spec_overlay, errors);
184
+ validateScaffoldBlueprint(manifest.scaffold_blueprint, errors);
141
185
 
142
186
  return { ok: errors.length === 0, errors };
143
187
  }
@@ -472,7 +472,7 @@ export function validateV4Config(data, projectRoot) {
472
472
 
473
473
  // Workflow Kit (optional but validated if present)
474
474
  if (data.workflow_kit !== undefined) {
475
- const wkValidation = validateWorkflowKitConfig(data.workflow_kit, data.routing);
475
+ const wkValidation = validateWorkflowKitConfig(data.workflow_kit, data.routing, data.roles);
476
476
  errors.push(...wkValidation.errors);
477
477
  }
478
478
 
@@ -483,7 +483,7 @@ export function validateV4Config(data, projectRoot) {
483
483
  * Validate the workflow_kit config section.
484
484
  * Returns { ok, errors, warnings }.
485
485
  */
486
- export function validateWorkflowKitConfig(wk, routing) {
486
+ export function validateWorkflowKitConfig(wk, routing, roles) {
487
487
  const errors = [];
488
488
  const warnings = [];
489
489
 
@@ -574,6 +574,16 @@ export function validateWorkflowKitConfig(wk, routing) {
574
574
  if (artifact.required !== undefined && typeof artifact.required !== 'boolean') {
575
575
  errors.push(`${prefix} required must be a boolean`);
576
576
  }
577
+
578
+ if (artifact.owned_by !== undefined && artifact.owned_by !== null) {
579
+ if (typeof artifact.owned_by !== 'string') {
580
+ errors.push(`${prefix} owned_by must be a string`);
581
+ } else if (!/^[a-z0-9_-]+$/.test(artifact.owned_by)) {
582
+ errors.push(`${prefix} owned_by "${artifact.owned_by}" is not a valid role ID (must be lowercase alphanumeric with hyphens/underscores)`);
583
+ } else if (roles && typeof roles === 'object' && !roles[artifact.owned_by]) {
584
+ errors.push(`${prefix} owned_by "${artifact.owned_by}" does not reference a defined role`);
585
+ }
586
+ }
577
587
  }
578
588
  }
579
589
  }
@@ -791,6 +801,7 @@ export function normalizeWorkflowKit(raw, routingPhases) {
791
801
  path: a.path,
792
802
  semantics: a.semantics || null,
793
803
  semantics_config: a.semantics_config || null,
804
+ owned_by: a.owned_by || null,
794
805
  required: a.required !== false,
795
806
  })),
796
807
  };
@@ -0,0 +1,195 @@
1
+ {
2
+ "id": "enterprise-app",
3
+ "display_name": "Enterprise App",
4
+ "description": "Governed scaffold for multi-role product delivery with explicit architecture and security-review phases.",
5
+ "version": "1",
6
+ "protocol_compatibility": ["1.0", "1.1"],
7
+ "planning_artifacts": [
8
+ {
9
+ "filename": "integration-boundaries.md",
10
+ "content_template": "# Integration Boundaries — {{project_name}}\n\n## Systems And Owners\n| System | Owner | Dependency type | Contract |\n|--------|-------|-----------------|----------|\n| | | | |\n\n## Cross-Team Dependencies\n- External approvals required:\n- Deployment dependencies:\n- Data-sharing constraints:\n"
11
+ },
12
+ {
13
+ "filename": "data-classification.md",
14
+ "content_template": "# Data Classification — {{project_name}}\n\n## Data Types\n| Data type | Sensitivity | Storage boundary | Notes |\n|-----------|-------------|------------------|-------|\n| | | | |\n\n## Compliance Constraints\n- Retention requirements:\n- Encryption expectations:\n- Audit obligations:\n"
15
+ },
16
+ {
17
+ "filename": "risk-register.md",
18
+ "content_template": "# Risk Register — {{project_name}}\n\n## Delivery Risks\n| Risk | Likelihood | Impact | Mitigation | Owner |\n|------|------------|--------|------------|-------|\n| | | | | |\n\n## Ship Blockers\n- Blocking risk thresholds:\n- Conditions for mitigated ship:\n"
19
+ }
20
+ ],
21
+ "prompt_overrides": {
22
+ "pm": "Drive cross-functional clarity early. Enterprise delivery fails when integration owners, rollout boundaries, or security obligations are left implicit.",
23
+ "architect": "Own the architecture phase. Make interface boundaries, data flow, and trade-offs explicit before dev starts implementation.",
24
+ "dev": "Treat integration safety, data handling, and architecture drift as first-class delivery risks. Do not bypass the architecture or security contracts.",
25
+ "security_reviewer": "Threat-model the shipped change, record findings explicitly, and block progression when risks are unresolved or undocumented.",
26
+ "qa": "Do not sign off until the architecture and security-review artifacts agree with the shipped behavior and verification evidence."
27
+ },
28
+ "acceptance_hints": [
29
+ "Architecture decisions reviewed before implementation started",
30
+ "Security review findings closed or explicitly accepted",
31
+ "Integration boundaries and rollout risks documented before ship request"
32
+ ],
33
+ "system_spec_overlay": {
34
+ "purpose_guidance": "Describe the enterprise workflow, the internal or external users it serves, and why this product needs governed multi-role delivery instead of a simple feature implementation.",
35
+ "interface_guidance": "List the product boundaries, key integrations, data flows, and authority handoffs. Reference integration-boundaries.md for the cross-system contract.",
36
+ "behavior_guidance": "Describe the expected system behavior across architecture, implementation, and security-review concerns. Include deployment and rollback assumptions.",
37
+ "error_cases_guidance": "List integration failures, auth or entitlement risks, rollout hazards, and security findings that would block release. Reference risk-register.md for mitigation expectations.",
38
+ "acceptance_tests_guidance": "- [ ] Architecture artifact documents boundaries and trade-offs\n- [ ] Security review captures findings and explicit verdict\n- [ ] Verification covers critical user and integration paths\n- [ ] Rollout or mitigation plan is documented before ship approval"
39
+ },
40
+ "scaffold_blueprint": {
41
+ "roles": {
42
+ "pm": {
43
+ "title": "Product Manager",
44
+ "mandate": "Protect scope clarity, user value, and cross-functional delivery readiness.",
45
+ "write_authority": "review_only",
46
+ "runtime": "manual-pm"
47
+ },
48
+ "architect": {
49
+ "title": "Architect",
50
+ "mandate": "Define the system boundary, integration contracts, and technical trade-offs before implementation commits to a design.",
51
+ "write_authority": "review_only",
52
+ "runtime": "manual-architect"
53
+ },
54
+ "dev": {
55
+ "title": "Developer",
56
+ "mandate": "Implement approved work safely and verify behavior.",
57
+ "write_authority": "authoritative",
58
+ "runtime": "local-dev"
59
+ },
60
+ "security_reviewer": {
61
+ "title": "Security Reviewer",
62
+ "mandate": "Challenge data handling, auth boundaries, and exploit paths before work proceeds to final QA.",
63
+ "write_authority": "review_only",
64
+ "runtime": "manual-security"
65
+ },
66
+ "qa": {
67
+ "title": "QA",
68
+ "mandate": "Challenge correctness, acceptance coverage, and ship readiness.",
69
+ "write_authority": "review_only",
70
+ "runtime": "api-qa"
71
+ },
72
+ "eng_director": {
73
+ "title": "Engineering Director",
74
+ "mandate": "Resolve tactical deadlocks and enforce technical coherence.",
75
+ "write_authority": "review_only",
76
+ "runtime": "manual-director"
77
+ }
78
+ },
79
+ "runtimes": {
80
+ "manual-pm": { "type": "manual" },
81
+ "local-dev": {
82
+ "type": "local_cli",
83
+ "command": ["claude", "--print", "--dangerously-skip-permissions"],
84
+ "cwd": ".",
85
+ "prompt_transport": "stdin"
86
+ },
87
+ "api-qa": {
88
+ "type": "api_proxy",
89
+ "provider": "anthropic",
90
+ "model": "claude-sonnet-4-6",
91
+ "auth_env": "ANTHROPIC_API_KEY"
92
+ },
93
+ "manual-qa": { "type": "manual" },
94
+ "manual-architect": { "type": "manual" },
95
+ "manual-security": { "type": "manual" },
96
+ "manual-director": { "type": "manual" }
97
+ },
98
+ "routing": {
99
+ "planning": {
100
+ "entry_role": "pm",
101
+ "allowed_next_roles": ["pm", "architect", "eng_director", "human"],
102
+ "exit_gate": "planning_signoff"
103
+ },
104
+ "architecture": {
105
+ "entry_role": "architect",
106
+ "allowed_next_roles": ["architect", "dev", "pm", "eng_director", "human"],
107
+ "exit_gate": "architecture_review"
108
+ },
109
+ "implementation": {
110
+ "entry_role": "dev",
111
+ "allowed_next_roles": ["dev", "security_reviewer", "qa", "architect", "eng_director", "human"],
112
+ "exit_gate": "implementation_complete"
113
+ },
114
+ "security_review": {
115
+ "entry_role": "security_reviewer",
116
+ "allowed_next_roles": ["dev", "qa", "architect", "eng_director", "human"],
117
+ "exit_gate": "security_review_signoff"
118
+ },
119
+ "qa": {
120
+ "entry_role": "qa",
121
+ "allowed_next_roles": ["dev", "qa", "eng_director", "human"],
122
+ "exit_gate": "qa_ship_verdict"
123
+ }
124
+ },
125
+ "gates": {
126
+ "planning_signoff": {
127
+ "requires_files": [".planning/PM_SIGNOFF.md", ".planning/ROADMAP.md", ".planning/SYSTEM_SPEC.md"],
128
+ "requires_human_approval": true
129
+ },
130
+ "architecture_review": {
131
+ "requires_files": [".planning/ARCHITECTURE.md"]
132
+ },
133
+ "implementation_complete": {
134
+ "requires_files": [".planning/IMPLEMENTATION_NOTES.md"],
135
+ "requires_verification_pass": true
136
+ },
137
+ "security_review_signoff": {
138
+ "requires_files": [".planning/SECURITY_REVIEW.md"]
139
+ },
140
+ "qa_ship_verdict": {
141
+ "requires_files": [".planning/acceptance-matrix.md", ".planning/ship-verdict.md", ".planning/RELEASE_NOTES.md"],
142
+ "requires_human_approval": true
143
+ }
144
+ },
145
+ "workflow_kit": {
146
+ "phases": {
147
+ "planning": {
148
+ "artifacts": [
149
+ { "path": ".planning/PM_SIGNOFF.md", "semantics": "pm_signoff", "required": true },
150
+ { "path": ".planning/SYSTEM_SPEC.md", "semantics": "system_spec", "required": true },
151
+ { "path": ".planning/ROADMAP.md", "semantics": null, "required": true }
152
+ ]
153
+ },
154
+ "architecture": {
155
+ "artifacts": [
156
+ {
157
+ "path": ".planning/ARCHITECTURE.md",
158
+ "semantics": "section_check",
159
+ "owned_by": "architect",
160
+ "semantics_config": {
161
+ "required_sections": ["## Context", "## Proposed Design", "## Trade-offs", "## Risks"]
162
+ },
163
+ "required": true
164
+ }
165
+ ]
166
+ },
167
+ "implementation": {
168
+ "artifacts": [
169
+ { "path": ".planning/IMPLEMENTATION_NOTES.md", "semantics": "implementation_notes", "required": true }
170
+ ]
171
+ },
172
+ "security_review": {
173
+ "artifacts": [
174
+ {
175
+ "path": ".planning/SECURITY_REVIEW.md",
176
+ "semantics": "section_check",
177
+ "owned_by": "security_reviewer",
178
+ "semantics_config": {
179
+ "required_sections": ["## Threat Model", "## Findings", "## Verdict"]
180
+ },
181
+ "required": true
182
+ }
183
+ ]
184
+ },
185
+ "qa": {
186
+ "artifacts": [
187
+ { "path": ".planning/acceptance-matrix.md", "semantics": "acceptance_matrix", "required": true },
188
+ { "path": ".planning/ship-verdict.md", "semantics": "ship_verdict", "required": true },
189
+ { "path": ".planning/RELEASE_NOTES.md", "semantics": "release_notes", "required": true }
190
+ ]
191
+ }
192
+ }
193
+ }
194
+ }
195
+ }