agentxchain 2.25.1 → 2.25.2
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/package.json +1 -1
- package/src/commands/init.js +61 -2
- package/src/lib/gate-evaluator.js +117 -29
- package/src/lib/governed-templates.js +105 -22
- package/src/lib/normalized-config.js +180 -1
- package/src/lib/workflow-gate-semantics.js +59 -0
package/package.json
CHANGED
package/src/commands/init.js
CHANGED
|
@@ -454,7 +454,21 @@ function formatInitTarget(dir) {
|
|
|
454
454
|
return dir;
|
|
455
455
|
}
|
|
456
456
|
|
|
457
|
-
|
|
457
|
+
function generateWorkflowKitPlaceholder(artifact, projectName) {
|
|
458
|
+
const filename = basename(artifact.path);
|
|
459
|
+
const title = filename.replace(/\.[^.]+$/, '').replace(/[-_]/g, ' ');
|
|
460
|
+
|
|
461
|
+
if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
|
|
462
|
+
const sections = artifact.semantics_config.required_sections
|
|
463
|
+
.map(s => `${s}\n\n(Content here.)\n`)
|
|
464
|
+
.join('\n');
|
|
465
|
+
return `# ${title} — ${projectName}\n\n${sections}`;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return `# ${title} — ${projectName}\n\n(Operator fills this in.)\n`;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
export function scaffoldGoverned(dir, projectName, projectId, templateId = 'generic', runtimeOptions = {}, workflowKitConfig = null) {
|
|
458
472
|
const template = loadGovernedTemplate(templateId);
|
|
459
473
|
const { runtime: localDevRuntime } = resolveGovernedLocalDevRuntime(runtimeOptions);
|
|
460
474
|
const runtimes = {
|
|
@@ -561,6 +575,37 @@ export function scaffoldGoverned(dir, projectName, projectId, templateId = 'gene
|
|
|
561
575
|
);
|
|
562
576
|
}
|
|
563
577
|
|
|
578
|
+
// Workflow-kit custom artifacts — only scaffold files from explicit workflow_kit config
|
|
579
|
+
// that are not already handled by the default scaffold above
|
|
580
|
+
if (workflowKitConfig && workflowKitConfig.phases && typeof workflowKitConfig.phases === 'object') {
|
|
581
|
+
const defaultScaffoldPaths = new Set([
|
|
582
|
+
'.planning/PM_SIGNOFF.md',
|
|
583
|
+
'.planning/ROADMAP.md',
|
|
584
|
+
'.planning/SYSTEM_SPEC.md',
|
|
585
|
+
'.planning/IMPLEMENTATION_NOTES.md',
|
|
586
|
+
'.planning/acceptance-matrix.md',
|
|
587
|
+
'.planning/ship-verdict.md',
|
|
588
|
+
'.planning/RELEASE_NOTES.md',
|
|
589
|
+
]);
|
|
590
|
+
|
|
591
|
+
for (const phaseConfig of Object.values(workflowKitConfig.phases)) {
|
|
592
|
+
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
593
|
+
for (const artifact of phaseConfig.artifacts) {
|
|
594
|
+
if (!artifact.path || defaultScaffoldPaths.has(artifact.path)) continue;
|
|
595
|
+
const absPath = join(dir, artifact.path);
|
|
596
|
+
if (existsSync(absPath)) continue;
|
|
597
|
+
|
|
598
|
+
// Ensure parent directory exists
|
|
599
|
+
const parentDir = dirname(absPath);
|
|
600
|
+
if (!existsSync(parentDir)) mkdirSync(parentDir, { recursive: true });
|
|
601
|
+
|
|
602
|
+
// Generate placeholder content based on semantics type
|
|
603
|
+
const content = generateWorkflowKitPlaceholder(artifact, projectName);
|
|
604
|
+
writeFileSync(absPath, content);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
|
|
564
609
|
// TALK.md
|
|
565
610
|
writeFileSync(join(dir, 'TALK.md'), `# ${projectName} — Team Talk File\n\nCanonical human-readable handoff log for all agents.\n\n---\n\n`);
|
|
566
611
|
|
|
@@ -662,7 +707,21 @@ async function initGoverned(opts) {
|
|
|
662
707
|
|
|
663
708
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true });
|
|
664
709
|
|
|
665
|
-
|
|
710
|
+
// If reinitializing a project that has explicit workflow_kit config, preserve it for scaffold
|
|
711
|
+
let workflowKitConfig = null;
|
|
712
|
+
const existingConfigPath = join(dir, CONFIG_FILE);
|
|
713
|
+
if (existsSync(existingConfigPath)) {
|
|
714
|
+
try {
|
|
715
|
+
const existing = JSON.parse(readFileSync(existingConfigPath, 'utf8'));
|
|
716
|
+
if (existing.workflow_kit && typeof existing.workflow_kit === 'object' && Object.keys(existing.workflow_kit).length > 0) {
|
|
717
|
+
workflowKitConfig = existing.workflow_kit;
|
|
718
|
+
}
|
|
719
|
+
} catch {
|
|
720
|
+
// Ignore parse errors — scaffold will overwrite the config anyway
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
scaffoldGoverned(dir, projectName, projectId, templateId, opts, workflowKitConfig);
|
|
666
725
|
|
|
667
726
|
console.log('');
|
|
668
727
|
console.log(chalk.green(` ✓ Created governed project ${chalk.bold(targetLabel)}/`));
|
|
@@ -19,7 +19,109 @@
|
|
|
19
19
|
|
|
20
20
|
import { existsSync } from 'fs';
|
|
21
21
|
import { join } from 'path';
|
|
22
|
-
import {
|
|
22
|
+
import {
|
|
23
|
+
evaluateArtifactSemantics,
|
|
24
|
+
evaluateWorkflowGateSemantics,
|
|
25
|
+
getSemanticIdForPath,
|
|
26
|
+
} from './workflow-gate-semantics.js';
|
|
27
|
+
|
|
28
|
+
function getWorkflowArtifactsForPhase(config, phase) {
|
|
29
|
+
const artifacts = config?.workflow_kit?.phases?.[phase]?.artifacts;
|
|
30
|
+
return Array.isArray(artifacts) ? artifacts : [];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function buildEffectiveGateArtifacts(config, gateDef, phase) {
|
|
34
|
+
const byPath = new Map();
|
|
35
|
+
|
|
36
|
+
if (Array.isArray(gateDef?.requires_files)) {
|
|
37
|
+
for (const filePath of gateDef.requires_files) {
|
|
38
|
+
byPath.set(filePath, {
|
|
39
|
+
path: filePath,
|
|
40
|
+
required: true,
|
|
41
|
+
useLegacySemantics: true,
|
|
42
|
+
semanticChecks: [],
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const artifact of getWorkflowArtifactsForPhase(config, phase)) {
|
|
48
|
+
if (!artifact?.path) {
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const existing = byPath.get(artifact.path) || {
|
|
53
|
+
path: artifact.path,
|
|
54
|
+
required: false,
|
|
55
|
+
useLegacySemantics: false,
|
|
56
|
+
semanticChecks: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
existing.required = existing.required || artifact.required !== false;
|
|
60
|
+
|
|
61
|
+
if (artifact.semantics) {
|
|
62
|
+
const legacySemanticId = existing.useLegacySemantics ? getSemanticIdForPath(artifact.path) : null;
|
|
63
|
+
if (artifact.semantics !== legacySemanticId) {
|
|
64
|
+
existing.semanticChecks.push({
|
|
65
|
+
semantics: artifact.semantics,
|
|
66
|
+
semantics_config: artifact.semantics_config || null,
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
byPath.set(artifact.path, existing);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return [...byPath.values()];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function addMissingFile(result, filePath) {
|
|
78
|
+
if (!result.missing_files.includes(filePath)) {
|
|
79
|
+
result.missing_files.push(filePath);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function prefixSemanticReason(filePath, reason) {
|
|
84
|
+
if (!reason || reason.includes(filePath)) {
|
|
85
|
+
return reason;
|
|
86
|
+
}
|
|
87
|
+
return `${filePath}: ${reason}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function evaluateGateArtifacts({ root, config, gateDef, phase, result }) {
|
|
91
|
+
const failures = [];
|
|
92
|
+
const artifacts = buildEffectiveGateArtifacts(config, gateDef, phase);
|
|
93
|
+
|
|
94
|
+
for (const artifact of artifacts) {
|
|
95
|
+
const absPath = join(root, artifact.path);
|
|
96
|
+
if (!existsSync(absPath)) {
|
|
97
|
+
if (artifact.required) {
|
|
98
|
+
addMissingFile(result, artifact.path);
|
|
99
|
+
failures.push(`Required file missing: ${artifact.path}`);
|
|
100
|
+
}
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
if (artifact.useLegacySemantics) {
|
|
105
|
+
const semanticCheck = evaluateWorkflowGateSemantics(root, artifact.path);
|
|
106
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
107
|
+
failures.push(semanticCheck.reason);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const semantic of artifact.semanticChecks) {
|
|
112
|
+
const semanticCheck = evaluateArtifactSemantics(root, {
|
|
113
|
+
path: artifact.path,
|
|
114
|
+
semantics: semantic.semantics,
|
|
115
|
+
semantics_config: semantic.semantics_config,
|
|
116
|
+
});
|
|
117
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
118
|
+
failures.push(prefixSemanticReason(artifact.path, semanticCheck.reason));
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return failures;
|
|
124
|
+
}
|
|
23
125
|
|
|
24
126
|
/**
|
|
25
127
|
* Evaluate whether the current phase exit gate is satisfied.
|
|
@@ -118,20 +220,13 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
|
|
|
118
220
|
const failures = [];
|
|
119
221
|
|
|
120
222
|
// Predicate: requires_files
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
const semanticCheck = evaluateWorkflowGateSemantics(root, filePath);
|
|
130
|
-
if (semanticCheck && !semanticCheck.ok) {
|
|
131
|
-
failures.push(semanticCheck.reason);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
223
|
+
failures.push(...evaluateGateArtifacts({
|
|
224
|
+
root,
|
|
225
|
+
config,
|
|
226
|
+
gateDef,
|
|
227
|
+
phase: currentPhase,
|
|
228
|
+
result,
|
|
229
|
+
}));
|
|
135
230
|
|
|
136
231
|
// Predicate: requires_verification_pass
|
|
137
232
|
if (gateDef.requires_verification_pass) {
|
|
@@ -240,20 +335,13 @@ export function evaluateRunCompletion({ state, config, acceptedTurn, root }) {
|
|
|
240
335
|
const result = { ...baseResult, gate_id: gateId };
|
|
241
336
|
const failures = [];
|
|
242
337
|
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
const semanticCheck = evaluateWorkflowGateSemantics(root, filePath);
|
|
252
|
-
if (semanticCheck && !semanticCheck.ok) {
|
|
253
|
-
failures.push(semanticCheck.reason);
|
|
254
|
-
}
|
|
255
|
-
}
|
|
256
|
-
}
|
|
338
|
+
failures.push(...evaluateGateArtifacts({
|
|
339
|
+
root,
|
|
340
|
+
config,
|
|
341
|
+
gateDef,
|
|
342
|
+
phase: currentPhase,
|
|
343
|
+
result,
|
|
344
|
+
}));
|
|
257
345
|
|
|
258
346
|
if (gateDef.requires_verification_pass) {
|
|
259
347
|
const verificationStatus = acceptedTurn.verification?.status;
|
|
@@ -446,7 +446,26 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
446
446
|
const gateRequiredFiles = uniqueStrings(
|
|
447
447
|
Object.values(config?.gates || {}).flatMap((gate) => Array.isArray(gate?.requires_files) ? gate.requires_files : [])
|
|
448
448
|
);
|
|
449
|
-
|
|
449
|
+
|
|
450
|
+
// Collect workflow-kit artifact paths from explicit config
|
|
451
|
+
const wkArtifactPaths = [];
|
|
452
|
+
const wk = config?.workflow_kit;
|
|
453
|
+
if (wk && wk.phases && typeof wk.phases === 'object') {
|
|
454
|
+
for (const phaseConfig of Object.values(wk.phases)) {
|
|
455
|
+
if (Array.isArray(phaseConfig.artifacts)) {
|
|
456
|
+
for (const a of phaseConfig.artifacts) {
|
|
457
|
+
if (a.path && a.required !== false) {
|
|
458
|
+
wkArtifactPaths.push(a.path);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
const hasExplicitWorkflowKit = wk && wk._explicit === true;
|
|
466
|
+
const hasExplicitWorkflowKitArtifacts = hasExplicitWorkflowKit && Object.keys(wk.phases || {}).length > 0;
|
|
467
|
+
const baseFiles = hasExplicitWorkflowKit ? wkArtifactPaths : GOVERNED_WORKFLOW_KIT_BASE_FILES;
|
|
468
|
+
const requiredFiles = uniqueStrings([...baseFiles, ...gateRequiredFiles]);
|
|
450
469
|
const present = [];
|
|
451
470
|
const missing = [];
|
|
452
471
|
|
|
@@ -459,32 +478,38 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
459
478
|
}
|
|
460
479
|
}
|
|
461
480
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
481
|
+
// Build structural checks: from explicit workflow_kit semantics or hardcoded defaults
|
|
482
|
+
let structuralChecks;
|
|
483
|
+
if (hasExplicitWorkflowKit) {
|
|
484
|
+
structuralChecks = buildStructuralChecksFromWorkflowKit(root, wk, errors);
|
|
485
|
+
} else {
|
|
486
|
+
structuralChecks = GOVERNED_WORKFLOW_KIT_STRUCTURAL_CHECKS.map((check) => {
|
|
487
|
+
const absPath = join(root, check.file);
|
|
488
|
+
if (!existsSync(absPath)) {
|
|
489
|
+
return {
|
|
490
|
+
id: check.id,
|
|
491
|
+
file: check.file,
|
|
492
|
+
ok: false,
|
|
493
|
+
skipped: true,
|
|
494
|
+
description: check.description,
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
const content = readFileSync(absPath, 'utf8');
|
|
499
|
+
const ok = check.pattern.test(content);
|
|
500
|
+
if (!ok) {
|
|
501
|
+
errors.push(`Workflow kit file "${check.file}" must preserve its structural marker: ${check.description}.`);
|
|
502
|
+
}
|
|
503
|
+
|
|
465
504
|
return {
|
|
466
505
|
id: check.id,
|
|
467
506
|
file: check.file,
|
|
468
|
-
ok
|
|
469
|
-
skipped:
|
|
507
|
+
ok,
|
|
508
|
+
skipped: false,
|
|
470
509
|
description: check.description,
|
|
471
510
|
};
|
|
472
|
-
}
|
|
473
|
-
|
|
474
|
-
const content = readFileSync(absPath, 'utf8');
|
|
475
|
-
const ok = check.pattern.test(content);
|
|
476
|
-
if (!ok) {
|
|
477
|
-
errors.push(`Workflow kit file "${check.file}" must preserve its structural marker: ${check.description}.`);
|
|
478
|
-
}
|
|
479
|
-
|
|
480
|
-
return {
|
|
481
|
-
id: check.id,
|
|
482
|
-
file: check.file,
|
|
483
|
-
ok,
|
|
484
|
-
skipped: false,
|
|
485
|
-
description: check.description,
|
|
486
|
-
};
|
|
487
|
-
});
|
|
511
|
+
});
|
|
512
|
+
}
|
|
488
513
|
|
|
489
514
|
return {
|
|
490
515
|
ok: errors.length === 0,
|
|
@@ -498,6 +523,64 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
498
523
|
};
|
|
499
524
|
}
|
|
500
525
|
|
|
526
|
+
function buildStructuralChecksFromWorkflowKit(root, wk, errors) {
|
|
527
|
+
const checks = [];
|
|
528
|
+
if (!wk.phases) return checks;
|
|
529
|
+
|
|
530
|
+
for (const [phase, phaseConfig] of Object.entries(wk.phases)) {
|
|
531
|
+
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
532
|
+
for (const artifact of phaseConfig.artifacts) {
|
|
533
|
+
if (!artifact.semantics) continue;
|
|
534
|
+
|
|
535
|
+
if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
|
|
536
|
+
for (const section of artifact.semantics_config.required_sections) {
|
|
537
|
+
const checkId = `wk_${phase}_${artifact.path.replace(/[^a-zA-Z0-9]/g, '_')}_section_${section.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
538
|
+
const description = `${artifact.path} defines ${section}`;
|
|
539
|
+
const absPath = join(root, artifact.path);
|
|
540
|
+
|
|
541
|
+
if (!existsSync(absPath)) {
|
|
542
|
+
checks.push({ id: checkId, file: artifact.path, ok: false, skipped: true, description });
|
|
543
|
+
continue;
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
const content = readFileSync(absPath, 'utf8');
|
|
547
|
+
const ok = content.includes(section);
|
|
548
|
+
if (!ok) {
|
|
549
|
+
errors.push(`Workflow kit file "${artifact.path}" must contain section: ${section}.`);
|
|
550
|
+
}
|
|
551
|
+
checks.push({ id: checkId, file: artifact.path, ok, skipped: false, description });
|
|
552
|
+
}
|
|
553
|
+
} else if (artifact.semantics !== 'section_check') {
|
|
554
|
+
// Built-in semantic check — generate a structural check entry
|
|
555
|
+
const checkId = `wk_${phase}_${artifact.semantics}`;
|
|
556
|
+
const description = `${artifact.path} passes ${artifact.semantics} validation`;
|
|
557
|
+
const absPath = join(root, artifact.path);
|
|
558
|
+
|
|
559
|
+
if (!existsSync(absPath)) {
|
|
560
|
+
checks.push({ id: checkId, file: artifact.path, ok: false, skipped: true, description });
|
|
561
|
+
continue;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
// For built-in validators, delegate to the hardcoded check if one exists
|
|
565
|
+
const hardcoded = GOVERNED_WORKFLOW_KIT_STRUCTURAL_CHECKS.find(c => c.file === artifact.path);
|
|
566
|
+
if (hardcoded) {
|
|
567
|
+
const content = readFileSync(absPath, 'utf8');
|
|
568
|
+
const ok = hardcoded.pattern.test(content);
|
|
569
|
+
if (!ok) {
|
|
570
|
+
errors.push(`Workflow kit file "${artifact.path}" must preserve its structural marker: ${hardcoded.description}.`);
|
|
571
|
+
}
|
|
572
|
+
checks.push({ id: checkId, file: artifact.path, ok, skipped: false, description: hardcoded.description });
|
|
573
|
+
} else {
|
|
574
|
+
// No hardcoded check for this semantic — mark as passing (runtime gate handles full validation)
|
|
575
|
+
checks.push({ id: checkId, file: artifact.path, ok: true, skipped: false, description });
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return checks;
|
|
582
|
+
}
|
|
583
|
+
|
|
501
584
|
export const SYSTEM_SPEC_OVERLAY_SEPARATOR = '## Template-Specific Guidance';
|
|
502
585
|
|
|
503
586
|
export function buildSystemSpecContent(projectName, overlay) {
|
|
@@ -22,7 +22,29 @@ const VALID_API_PROXY_PROVIDERS = ['anthropic', 'openai'];
|
|
|
22
22
|
export const VALID_PROMPT_TRANSPORTS = ['argv', 'stdin', 'dispatch_bundle_only'];
|
|
23
23
|
const VALID_MCP_TRANSPORTS = ['stdio', 'streamable_http'];
|
|
24
24
|
const DEFAULT_PHASES = ['planning', 'implementation', 'qa'];
|
|
25
|
+
export { DEFAULT_PHASES };
|
|
25
26
|
const VALID_PHASE_NAME = /^[a-z][a-z0-9_-]*$/;
|
|
27
|
+
const VALID_SEMANTIC_IDS = ['pm_signoff', 'system_spec', 'implementation_notes', 'acceptance_matrix', 'ship_verdict', 'release_notes', 'section_check'];
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Default artifact map for phases when workflow_kit is absent from config.
|
|
31
|
+
* Only phases present in this map get default artifacts.
|
|
32
|
+
*/
|
|
33
|
+
const DEFAULT_PHASE_ARTIFACTS = {
|
|
34
|
+
planning: [
|
|
35
|
+
{ path: '.planning/PM_SIGNOFF.md', semantics: 'pm_signoff', required: true },
|
|
36
|
+
{ path: '.planning/SYSTEM_SPEC.md', semantics: 'system_spec', required: true },
|
|
37
|
+
{ path: '.planning/ROADMAP.md', semantics: null, required: true },
|
|
38
|
+
],
|
|
39
|
+
implementation: [
|
|
40
|
+
{ path: '.planning/IMPLEMENTATION_NOTES.md', semantics: 'implementation_notes', required: true },
|
|
41
|
+
],
|
|
42
|
+
qa: [
|
|
43
|
+
{ path: '.planning/acceptance-matrix.md', semantics: 'acceptance_matrix', required: true },
|
|
44
|
+
{ path: '.planning/ship-verdict.md', semantics: 'ship_verdict', required: true },
|
|
45
|
+
{ path: '.planning/RELEASE_NOTES.md', semantics: 'release_notes', required: true },
|
|
46
|
+
],
|
|
47
|
+
};
|
|
26
48
|
const VALID_API_PROXY_RETRY_JITTER = ['none', 'full'];
|
|
27
49
|
const VALID_API_PROXY_RETRY_CLASSES = [
|
|
28
50
|
'rate_limited',
|
|
@@ -448,9 +470,117 @@ export function validateV4Config(data, projectRoot) {
|
|
|
448
470
|
errors.push(...notificationValidation.errors);
|
|
449
471
|
}
|
|
450
472
|
|
|
473
|
+
// Workflow Kit (optional but validated if present)
|
|
474
|
+
if (data.workflow_kit !== undefined) {
|
|
475
|
+
const wkValidation = validateWorkflowKitConfig(data.workflow_kit, data.routing);
|
|
476
|
+
errors.push(...wkValidation.errors);
|
|
477
|
+
}
|
|
478
|
+
|
|
451
479
|
return { ok: errors.length === 0, errors };
|
|
452
480
|
}
|
|
453
481
|
|
|
482
|
+
/**
|
|
483
|
+
* Validate the workflow_kit config section.
|
|
484
|
+
* Returns { ok, errors, warnings }.
|
|
485
|
+
*/
|
|
486
|
+
export function validateWorkflowKitConfig(wk, routing) {
|
|
487
|
+
const errors = [];
|
|
488
|
+
const warnings = [];
|
|
489
|
+
|
|
490
|
+
if (wk === null || (typeof wk === 'object' && !Array.isArray(wk) && Object.keys(wk).length === 0)) {
|
|
491
|
+
// Empty workflow_kit is a valid opt-out
|
|
492
|
+
return { ok: true, errors, warnings };
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
if (!wk || typeof wk !== 'object' || Array.isArray(wk)) {
|
|
496
|
+
errors.push('workflow_kit must be an object');
|
|
497
|
+
return { ok: false, errors, warnings };
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
if (wk.phases !== undefined) {
|
|
501
|
+
if (!wk.phases || typeof wk.phases !== 'object' || Array.isArray(wk.phases)) {
|
|
502
|
+
errors.push('workflow_kit.phases must be an object');
|
|
503
|
+
return { ok: false, errors, warnings };
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
const routingPhases = routing ? new Set(Object.keys(routing)) : new Set(DEFAULT_PHASES);
|
|
507
|
+
|
|
508
|
+
for (const [phase, phaseConfig] of Object.entries(wk.phases)) {
|
|
509
|
+
if (!VALID_PHASE_NAME.test(phase)) {
|
|
510
|
+
errors.push(`workflow_kit phase name "${phase}" must be lowercase alphanumeric starting with a letter (hyphens and underscores allowed)`);
|
|
511
|
+
continue;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (!routingPhases.has(phase)) {
|
|
515
|
+
warnings.push(`workflow_kit declares phase "${phase}" which is not in routing`);
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
if (!phaseConfig || typeof phaseConfig !== 'object' || Array.isArray(phaseConfig)) {
|
|
519
|
+
errors.push(`workflow_kit.phases.${phase} must be an object`);
|
|
520
|
+
continue;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
if (!Array.isArray(phaseConfig.artifacts)) {
|
|
524
|
+
errors.push(`workflow_kit.phases.${phase}.artifacts must be an array`);
|
|
525
|
+
continue;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const seenPaths = new Set();
|
|
529
|
+
for (let i = 0; i < phaseConfig.artifacts.length; i++) {
|
|
530
|
+
const artifact = phaseConfig.artifacts[i];
|
|
531
|
+
const prefix = `workflow_kit.phases.${phase}.artifacts[${i}]`;
|
|
532
|
+
|
|
533
|
+
if (!artifact || typeof artifact !== 'object') {
|
|
534
|
+
errors.push(`${prefix} must be an object`);
|
|
535
|
+
continue;
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
if (typeof artifact.path !== 'string' || !artifact.path.trim()) {
|
|
539
|
+
errors.push(`${prefix} requires a non-empty path`);
|
|
540
|
+
continue;
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
if (artifact.path.includes('..')) {
|
|
544
|
+
errors.push(`${prefix} path must not traverse above project root (contains "..")`);
|
|
545
|
+
continue;
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
if (seenPaths.has(artifact.path)) {
|
|
549
|
+
errors.push(`duplicate artifact path "${artifact.path}" in phase "${phase}"`);
|
|
550
|
+
}
|
|
551
|
+
seenPaths.add(artifact.path);
|
|
552
|
+
|
|
553
|
+
if (artifact.semantics !== null && artifact.semantics !== undefined) {
|
|
554
|
+
if (typeof artifact.semantics !== 'string') {
|
|
555
|
+
errors.push(`${prefix} semantics must be a string or null`);
|
|
556
|
+
} else if (!VALID_SEMANTIC_IDS.includes(artifact.semantics)) {
|
|
557
|
+
errors.push(`${prefix} unknown semantics validator "${artifact.semantics}"; valid values: ${VALID_SEMANTIC_IDS.join(', ')}`);
|
|
558
|
+
} else if (artifact.semantics === 'section_check') {
|
|
559
|
+
if (!artifact.semantics_config || typeof artifact.semantics_config !== 'object') {
|
|
560
|
+
errors.push(`${prefix} section_check requires semantics_config`);
|
|
561
|
+
} else if (!Array.isArray(artifact.semantics_config.required_sections) || artifact.semantics_config.required_sections.length === 0) {
|
|
562
|
+
errors.push(`${prefix} section_check requires semantics_config.required_sections as a non-empty array`);
|
|
563
|
+
} else {
|
|
564
|
+
for (const section of artifact.semantics_config.required_sections) {
|
|
565
|
+
if (typeof section !== 'string' || !section.trim()) {
|
|
566
|
+
errors.push(`${prefix} section_check required_sections must contain non-empty strings`);
|
|
567
|
+
break;
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
if (artifact.required !== undefined && typeof artifact.required !== 'boolean') {
|
|
575
|
+
errors.push(`${prefix} required must be a boolean`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
582
|
+
}
|
|
583
|
+
|
|
454
584
|
/**
|
|
455
585
|
* Normalize a legacy v3 config into the internal shape.
|
|
456
586
|
* Does NOT modify the original file — this is a read-time transformation.
|
|
@@ -492,6 +622,7 @@ export function normalizeV3(raw) {
|
|
|
492
622
|
hooks: {},
|
|
493
623
|
notifications: {},
|
|
494
624
|
budget: null,
|
|
625
|
+
workflow_kit: normalizeWorkflowKit(undefined, DEFAULT_PHASES),
|
|
495
626
|
retention: {
|
|
496
627
|
talk_strategy: 'append_only',
|
|
497
628
|
history_strategy: 'jsonl_append_only',
|
|
@@ -536,6 +667,9 @@ export function normalizeV4(raw) {
|
|
|
536
667
|
}
|
|
537
668
|
}
|
|
538
669
|
|
|
670
|
+
const routing = raw.routing || {};
|
|
671
|
+
const routingPhases = Object.keys(routing).length > 0 ? Object.keys(routing) : DEFAULT_PHASES;
|
|
672
|
+
|
|
539
673
|
return {
|
|
540
674
|
schema_version: 4,
|
|
541
675
|
protocol_mode: 'governed',
|
|
@@ -547,11 +681,12 @@ export function normalizeV4(raw) {
|
|
|
547
681
|
},
|
|
548
682
|
roles,
|
|
549
683
|
runtimes: raw.runtimes || {},
|
|
550
|
-
routing
|
|
684
|
+
routing,
|
|
551
685
|
gates: raw.gates || {},
|
|
552
686
|
hooks: raw.hooks || {},
|
|
553
687
|
notifications: raw.notifications || {},
|
|
554
688
|
budget: raw.budget || null,
|
|
689
|
+
workflow_kit: normalizeWorkflowKit(raw.workflow_kit, routingPhases),
|
|
555
690
|
retention: raw.retention || {
|
|
556
691
|
talk_strategy: 'append_only',
|
|
557
692
|
history_strategy: 'jsonl_append_only',
|
|
@@ -633,6 +768,50 @@ export function getMaxConcurrentTurns(config, phase) {
|
|
|
633
768
|
}
|
|
634
769
|
|
|
635
770
|
|
|
771
|
+
/**
|
|
772
|
+
* Normalize workflow_kit config.
|
|
773
|
+
* When absent, builds defaults from routing phases using DEFAULT_PHASE_ARTIFACTS.
|
|
774
|
+
* When present, normalizes artifact entries.
|
|
775
|
+
*/
|
|
776
|
+
export function normalizeWorkflowKit(raw, routingPhases) {
|
|
777
|
+
if (raw === undefined || raw === null) {
|
|
778
|
+
return buildDefaultWorkflowKit(routingPhases);
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
// Empty object is an explicit opt-out — no artifacts
|
|
782
|
+
if (typeof raw === 'object' && !Array.isArray(raw) && Object.keys(raw).length === 0) {
|
|
783
|
+
return { phases: {}, _explicit: true };
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
const phases = {};
|
|
787
|
+
if (raw.phases) {
|
|
788
|
+
for (const [phase, phaseConfig] of Object.entries(raw.phases)) {
|
|
789
|
+
phases[phase] = {
|
|
790
|
+
artifacts: (phaseConfig.artifacts || []).map(a => ({
|
|
791
|
+
path: a.path,
|
|
792
|
+
semantics: a.semantics || null,
|
|
793
|
+
semantics_config: a.semantics_config || null,
|
|
794
|
+
required: a.required !== false,
|
|
795
|
+
})),
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
|
|
800
|
+
return { phases, _explicit: true };
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
function buildDefaultWorkflowKit(routingPhases) {
|
|
804
|
+
const phases = {};
|
|
805
|
+
for (const phase of routingPhases) {
|
|
806
|
+
if (DEFAULT_PHASE_ARTIFACTS[phase]) {
|
|
807
|
+
phases[phase] = {
|
|
808
|
+
artifacts: DEFAULT_PHASE_ARTIFACTS[phase].map(a => ({ ...a, semantics_config: null })),
|
|
809
|
+
};
|
|
810
|
+
}
|
|
811
|
+
}
|
|
812
|
+
return { phases };
|
|
813
|
+
}
|
|
814
|
+
|
|
636
815
|
// --- Internal helpers ---
|
|
637
816
|
|
|
638
817
|
function inferWriteAuthority(agentId) {
|
|
@@ -8,6 +8,14 @@ export const ACCEPTANCE_MATRIX_PATH = '.planning/acceptance-matrix.md';
|
|
|
8
8
|
export const SHIP_VERDICT_PATH = '.planning/ship-verdict.md';
|
|
9
9
|
export const RELEASE_NOTES_PATH = '.planning/RELEASE_NOTES.md';
|
|
10
10
|
export const RECOVERY_REPORT_PATH = '.agentxchain/multirepo/RECOVERY_REPORT.md';
|
|
11
|
+
const PATH_SEMANTIC_IDS = {
|
|
12
|
+
[PM_SIGNOFF_PATH]: 'pm_signoff',
|
|
13
|
+
[SYSTEM_SPEC_PATH]: 'system_spec',
|
|
14
|
+
[IMPLEMENTATION_NOTES_PATH]: 'implementation_notes',
|
|
15
|
+
[ACCEPTANCE_MATRIX_PATH]: 'acceptance_matrix',
|
|
16
|
+
[SHIP_VERDICT_PATH]: 'ship_verdict',
|
|
17
|
+
[RELEASE_NOTES_PATH]: 'release_notes',
|
|
18
|
+
};
|
|
11
19
|
|
|
12
20
|
const AFFIRMATIVE_SHIP_VERDICTS = new Set(['YES', 'SHIP', 'SHIP IT']);
|
|
13
21
|
const AFFIRMATIVE_ACCEPTANCE_STATUSES = new Set(['PASS', 'PASSED', 'OK', 'YES']);
|
|
@@ -354,6 +362,57 @@ function evaluateShipVerdict(content) {
|
|
|
354
362
|
return { ok: true };
|
|
355
363
|
}
|
|
356
364
|
|
|
365
|
+
function evaluateSectionCheck(content, config) {
|
|
366
|
+
if (!config?.required_sections?.length) {
|
|
367
|
+
return { ok: true };
|
|
368
|
+
}
|
|
369
|
+
const missing = config.required_sections.filter(
|
|
370
|
+
section => !content.includes(section)
|
|
371
|
+
);
|
|
372
|
+
if (missing.length > 0) {
|
|
373
|
+
return {
|
|
374
|
+
ok: false,
|
|
375
|
+
reason: `Document must contain sections: ${missing.join(', ')}`,
|
|
376
|
+
};
|
|
377
|
+
}
|
|
378
|
+
return { ok: true };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const SEMANTIC_VALIDATORS = {
|
|
382
|
+
pm_signoff: evaluatePmSignoff,
|
|
383
|
+
system_spec: evaluateSystemSpec,
|
|
384
|
+
implementation_notes: evaluateImplementationNotes,
|
|
385
|
+
acceptance_matrix: evaluateAcceptanceMatrix,
|
|
386
|
+
ship_verdict: evaluateShipVerdict,
|
|
387
|
+
release_notes: evaluateReleaseNotes,
|
|
388
|
+
section_check: evaluateSectionCheck,
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
export function getSemanticIdForPath(relPath) {
|
|
392
|
+
return PATH_SEMANTIC_IDS[relPath] || null;
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
/**
|
|
396
|
+
* Evaluate a workflow artifact's semantic constraints by validator ID.
|
|
397
|
+
* @param {string} root - project root directory
|
|
398
|
+
* @param {object} artifact - { path, semantics, semantics_config }
|
|
399
|
+
* @returns {{ ok: boolean, reason?: string } | null}
|
|
400
|
+
*/
|
|
401
|
+
export function evaluateArtifactSemantics(root, artifact) {
|
|
402
|
+
if (!artifact.semantics) return null;
|
|
403
|
+
const validator = SEMANTIC_VALIDATORS[artifact.semantics];
|
|
404
|
+
if (!validator) return null;
|
|
405
|
+
const content = readFile(root, artifact.path);
|
|
406
|
+
if (content === null) return null;
|
|
407
|
+
if (artifact.semantics === 'section_check') {
|
|
408
|
+
return validator(content, artifact.semantics_config);
|
|
409
|
+
}
|
|
410
|
+
return validator(content);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
/**
|
|
414
|
+
* Path-based dispatch (backward compat for code calling this directly).
|
|
415
|
+
*/
|
|
357
416
|
export function evaluateWorkflowGateSemantics(root, relPath) {
|
|
358
417
|
const content = readFile(root, relPath);
|
|
359
418
|
if (content === null) {
|