agentxchain 2.25.2 → 2.27.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.27.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,115 @@ 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
+ const PHASE_DISPLAY_NAMES = Object.freeze({
561
+ qa: 'QA',
562
+ });
563
+
564
+ function formatPhaseDisplayName(phaseKey) {
565
+ if (PHASE_DISPLAY_NAMES[phaseKey]) {
566
+ return PHASE_DISPLAY_NAMES[phaseKey];
567
+ }
568
+ return phaseKey.replace(/_/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase());
569
+ }
570
+
571
+ function buildRoadmapPhaseTable(routing, roles) {
572
+ const rows = Object.entries(routing).map(([phaseKey, phaseConfig]) => {
573
+ const phaseName = formatPhaseDisplayName(phaseKey);
574
+ const entryRole = phaseConfig.entry_role;
575
+ const role = roles[entryRole];
576
+ const goal = role?.mandate || phaseName;
577
+ const status = phaseKey === Object.keys(routing)[0] ? 'In progress' : 'Pending';
578
+ return `| ${phaseName} | ${goal} | ${status} |`;
579
+ });
580
+ return `| Phase | Goal | Status |\n|-------|------|--------|\n${rows.join('\n')}\n`;
581
+ }
582
+
583
+ function buildPlanningSummaryLines(template, workflowKitConfig) {
584
+ const lines = [
585
+ 'PM_SIGNOFF.md / ROADMAP.md / SYSTEM_SPEC.md',
586
+ 'acceptance-matrix.md / ship-verdict.md',
587
+ 'RELEASE_NOTES.md',
588
+ ];
589
+ const templatePlanningFiles = Array.isArray(template?.planning_artifacts)
590
+ ? template.planning_artifacts
591
+ .map((artifact) => artifact?.filename)
592
+ .filter(Boolean)
593
+ : [];
594
+ const defaultScaffoldPaths = new Set([
595
+ '.planning/PM_SIGNOFF.md',
596
+ '.planning/ROADMAP.md',
597
+ '.planning/SYSTEM_SPEC.md',
598
+ '.planning/IMPLEMENTATION_NOTES.md',
599
+ '.planning/acceptance-matrix.md',
600
+ '.planning/ship-verdict.md',
601
+ '.planning/RELEASE_NOTES.md',
602
+ ]);
603
+ const customWorkflowFiles = [];
604
+
605
+ if (workflowKitConfig?.phases && typeof workflowKitConfig.phases === 'object') {
606
+ for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
607
+ if (!Array.isArray(phaseConfig?.artifacts)) continue;
608
+ for (const artifact of phaseConfig.artifacts) {
609
+ if (!artifact?.path || defaultScaffoldPaths.has(artifact.path)) continue;
610
+ customWorkflowFiles.push(basename(artifact.path));
611
+ }
612
+ }
613
+ }
614
+
615
+ if (templatePlanningFiles.length > 0) {
616
+ lines.push(`template: ${templatePlanningFiles.join(' / ')}`);
617
+ }
618
+ const uniqueCustomWorkflowFiles = [...new Set(customWorkflowFiles)];
619
+ if (uniqueCustomWorkflowFiles.length > 0) {
620
+ lines.push(`workflow: ${uniqueCustomWorkflowFiles.join(' / ')}`);
621
+ }
622
+
623
+ return lines;
624
+ }
625
+
471
626
  export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}, workflowKitConfig = null) {
472
627
  const template = loadGovernedTemplate(templateId);
473
628
  const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
474
- const runtimes = {
475
- ...GOVERNED_RUNTIMES,
476
- 'local-dev': localDevRuntime,
477
- };
629
+ const scaffoldConfig = buildScaffoldConfigFromTemplate(template, localDevRuntime, workflowKitConfig);
630
+ const { roles, runtimes, routing, gates, prompts, workflowKitConfig: effectiveWorkflowKitConfig } = scaffoldConfig;
631
+ const initialPhase = Object.keys(routing)[0] || 'planning';
632
+ const phaseGateStatus = Object.fromEntries(
633
+ [...new Set(
634
+ Object.values(routing)
635
+ .map((route) => route?.exit_gate)
636
+ .filter(Boolean)
637
+ )].map((gateId) => [gateId, 'pending'])
638
+ );
478
639
  const config = {
479
640
  schema_version: '1.0',
480
641
  template: template.id,
@@ -483,10 +644,10 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
483
644
  name: projectName,
484
645
  default_branch: 'main'
485
646
  },
486
- roles: GOVERNED_ROLES,
647
+ roles,
487
648
  runtimes,
488
- routing: GOVERNED_ROUTING,
489
- gates: GOVERNED_GATES,
649
+ routing,
650
+ gates,
490
651
  budget: {
491
652
  per_turn_max_usd: 2.0,
492
653
  per_run_max_usd: 50.0,
@@ -496,25 +657,23 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
496
657
  talk_strategy: 'append_only',
497
658
  history_strategy: 'jsonl_append_only'
498
659
  },
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
- },
660
+ prompts,
505
661
  rules: {
506
662
  challenge_required: true,
507
663
  max_turn_retries: 2,
508
664
  max_deadlock_cycles: 2
509
665
  }
510
666
  };
667
+ if (effectiveWorkflowKitConfig) {
668
+ config.workflow_kit = effectiveWorkflowKitConfig;
669
+ }
511
670
 
512
671
  const state = {
513
672
  schema_version: '1.1',
514
673
  run_id: null,
515
674
  project_id: projectId,
516
675
  status: 'idle',
517
- phase: 'planning',
676
+ phase: initialPhase,
518
677
  accepted_integration_ref: null,
519
678
  active_turns: {},
520
679
  turn_sequence: 0,
@@ -524,11 +683,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
524
683
  escalation: null,
525
684
  queued_phase_transition: null,
526
685
  queued_run_completion: null,
527
- phase_gate_status: {
528
- planning_signoff: 'pending',
529
- implementation_complete: 'pending',
530
- qa_ship_verdict: 'pending'
531
- },
686
+ phase_gate_status: phaseGateStatus,
532
687
  budget_reservations: {},
533
688
  budget_status: {
534
689
  spent_usd: 0,
@@ -550,15 +705,19 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
550
705
  writeFileSync(join(dir, '.agentxchain', 'decision-ledger.jsonl'), '');
551
706
 
552
707
  // Prompt templates
553
- for (const [roleId, role] of Object.entries(GOVERNED_ROLES)) {
554
- const basePrompt = buildGovernedPrompt(roleId, role);
708
+ for (const [roleId, role] of Object.entries(roles)) {
709
+ const basePrompt = buildGovernedPrompt(roleId, role, {
710
+ routing,
711
+ gates,
712
+ workflowKitConfig: effectiveWorkflowKitConfig,
713
+ });
555
714
  const prompt = appendPromptOverride(basePrompt, template.prompt_overrides?.[roleId]);
556
715
  writeFileSync(join(dir, '.agentxchain', 'prompts', `${roleId}.md`), prompt);
557
716
  }
558
717
 
559
718
  // Planning artifacts
560
719
  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`);
720
+ writeFileSync(join(dir, '.planning', 'ROADMAP.md'), `# Roadmap — ${projectName}\n\n## Phases\n\n${buildRoadmapPhaseTable(routing, roles)}`);
562
721
  writeFileSync(join(dir, '.planning', 'SYSTEM_SPEC.md'), buildSystemSpecContent(projectName, template.system_spec_overlay));
563
722
  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
723
  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 +736,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
577
736
 
578
737
  // Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
579
738
  // that are not already handled by the default scaffold above
580
- if (workflowKitConfig && workflowKitConfig.phases && typeof workflowKitConfig.phases === 'object') {
739
+ if (effectiveWorkflowKitConfig && effectiveWorkflowKitConfig.phases && typeof effectiveWorkflowKitConfig.phases === 'object') {
581
740
  const defaultScaffoldPaths = new Set([
582
741
  '.planning/PM_SIGNOFF.md',
583
742
  '.planning/ROADMAP.md',
@@ -588,7 +747,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
588
747
  '.planning/RELEASE_NOTES.md',
589
748
  ]);
590
749
 
591
- for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
750
+ for (const phaseConfig of Object.values(effectiveWorkflowKitConfig.phases)) {
592
751
  if (!Array.isArray(phaseConfig.artifacts)) continue;
593
752
  for (const artifact of phaseConfig.artifacts) {
594
753
  if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
@@ -629,6 +788,7 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
629
788
  async function initGoverned(opts) {
630
789
  let projectName, folderName;
631
790
  const templateId = opts.template || 'generic';
791
+ let selectedTemplate;
632
792
  let explicitDir;
633
793
 
634
794
  try {
@@ -649,6 +809,7 @@ async function initGoverned(opts) {
649
809
  console.error(' web-app Governed scaffold for a web application');
650
810
  process.exit(1);
651
811
  }
812
+ selectedTemplate = loadGovernedTemplate(templateId);
652
813
 
653
814
  if (opts.yes) {
654
815
  projectName = explicitDir
@@ -721,47 +882,54 @@ async function initGoverned(opts) {
721
882
  }
722
883
  }
723
884
 
724
- scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
885
+ const { config } = scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
725
886
 
726
887
  console.log('');
727
888
  console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
728
889
  console.log('');
890
+ const promptRoleIds = Object.keys(config.roles);
891
+ const phaseNames = Object.keys(config.routing);
729
892
  console.log(` ${chalk.dim('├──')} agentxchain.json ${chalk.dim('(governed)')}`);
730
893
  console.log(` ${chalk.dim('├──')} .agentxchain/`);
731
894
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} state.json / history.jsonl / decision-ledger.jsonl`);
732
895
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} staging/`);
733
- console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} prompts/ ${chalk.dim('(pm, dev, qa, eng_director)')}`);
896
+ console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} prompts/ ${chalk.dim(`(${promptRoleIds.join(', ')})`)}`);
734
897
  console.log(` ${chalk.dim('│')} ${chalk.dim('├──')} reviews/`);
735
898
  console.log(` ${chalk.dim('│')} ${chalk.dim('└──')} dispatch/`);
736
899
  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`);
900
+ const planningSummaryLines = buildPlanningSummaryLines(selectedTemplate, config.workflow_kit);
901
+ for (const [index, line] of planningSummaryLines.entries()) {
902
+ const branch = index === planningSummaryLines.length - 1 ? '└──' : '├──';
903
+ console.log(` ${chalk.dim('│')} ${chalk.dim(branch)} ${line}`);
904
+ }
740
905
  console.log(` ${chalk.dim('└──')} TALK.md`);
741
906
  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)')}`);
907
+ console.log(` ${chalk.dim('Roles:')} ${promptRoleIds.join(', ')}`);
908
+ 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
909
  console.log(` ${chalk.dim('Template:')} ${templateId}`);
745
910
  console.log(` ${chalk.dim('Dev runtime:')} ${formatGovernedRuntimeCommand(localDevRuntime)} ${chalk.dim(`(${localDevRuntime.prompt_transport})`)}`);
746
911
  console.log(` ${chalk.dim('Protocol:')} governed convergence`);
747
912
  console.log('');
748
913
 
749
914
  // 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);
915
+ const allRuntimes = config.runtimes;
916
+ const manualRoleIds = Object.entries(config.roles)
917
+ .filter(([, role]) => allRuntimes[role.runtime]?.type === 'manual')
918
+ .map(([roleId]) => roleId);
919
+ const rolesNeedingKeys = Object.entries(config.roles)
920
+ .filter(([, role]) => Boolean(allRuntimes[role.runtime]?.auth_env))
921
+ .map(([roleId, role]) => ({ roleId, env: allRuntimes[role.runtime].auth_env }));
922
+ if (rolesNeedingKeys.length > 0) {
923
+ const envVars = [...new Set(rolesNeedingKeys.map((r) => r.env))];
924
+ const roleNames = rolesNeedingKeys.map((r) => r.roleId);
757
925
  const hasKeys = envVars.every(v => process.env[v]);
758
926
  if (hasKeys) {
759
927
  console.log(` ${chalk.green('Ready:')} all runtimes configured (${envVars.join(', ')} detected)`);
760
928
  } else {
761
- console.log(` ${chalk.yellow('Mixed-mode:')} pm and eng_director work immediately (manual).`);
929
+ console.log(` ${chalk.yellow('Mixed-mode:')} ${manualRoleIds.join(', ')} work immediately (manual).`);
762
930
  console.log(` ${chalk.yellow(' ')}${roleNames.join(', ')} need ${chalk.bold(envVars.join(', '))} to dispatch automatically.`);
763
931
  console.log(` ${chalk.yellow(' ')}Without it, those turns fall back to manual input.`);
764
- if (allRuntimes['manual-qa']) {
932
+ if (config.roles?.qa?.runtime === 'api-qa' && allRuntimes['manual-qa']) {
765
933
  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
934
  }
767
935
  }
@@ -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 ───────────────────────────────────────────────
@@ -105,7 +105,7 @@ function getPhaseGateHints(phase, roleId, config) {
105
105
  hints.push('.planning/IMPLEMENTATION_NOTES.md — record what you built and how to verify');
106
106
  } else if (phase === 'qa' && (roleId === 'qa' || roleId === 'human')) {
107
107
  hints.push('.planning/acceptance-matrix.md — mark each requirement PASS/FAIL');
108
- hints.push('.planning/ship-verdict.md — change "## Verdict: PENDING" → "## Verdict: SHIP"');
108
+ hints.push('.planning/ship-verdict.md — change "## Verdict: PENDING" → "## Verdict: YES"');
109
109
  hints.push('.planning/RELEASE_NOTES.md — user impact, verification summary, upgrade notes');
110
110
  }
111
111
 
@@ -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
+ }