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 +1 -0
- package/bin/agentxchain.js +2 -2
- package/package.json +1 -1
- package/src/commands/init.js +201 -44
- package/src/commands/template-list.js +4 -0
- package/src/commands/template-set.js +6 -0
- package/src/lib/gate-evaluator.js +26 -2
- package/src/lib/governed-state.js +3 -1
- package/src/lib/governed-templates.js +47 -3
- package/src/lib/normalized-config.js +13 -2
- package/src/templates/governed/enterprise-app.json +195 -0
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
|
|
package/bin/agentxchain.js
CHANGED
|
@@ -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
package/src/commands/init.js
CHANGED
|
@@ -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
|
|
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
|
|
475
|
-
|
|
476
|
-
|
|
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
|
|
636
|
+
roles,
|
|
487
637
|
runtimes,
|
|
488
|
-
routing
|
|
489
|
-
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:
|
|
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(
|
|
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
|
|
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 (
|
|
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(
|
|
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('
|
|
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
|
-
|
|
738
|
-
|
|
739
|
-
|
|
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:')}
|
|
743
|
-
console.log(` ${chalk.dim('Phases:')}
|
|
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 =
|
|
751
|
-
const
|
|
752
|
-
.filter(([,
|
|
753
|
-
.map(([
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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:')}
|
|
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
|
|
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
|
|
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 (!
|
|
54
|
-
errors.push(`prompt_overrides contains
|
|
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
|
+
}
|