agentxchain 2.25.1 → 2.26.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -0
- package/bin/agentxchain.js +2 -2
- package/package.json +1 -1
- package/src/commands/init.js +259 -43
- package/src/commands/template-list.js +4 -0
- package/src/commands/template-set.js +6 -0
- package/src/lib/gate-evaluator.js +142 -30
- package/src/lib/governed-state.js +3 -1
- package/src/lib/governed-templates.js +152 -25
- package/src/lib/normalized-config.js +191 -1
- package/src/lib/workflow-gate-semantics.js +59 -0
- package/src/templates/governed/enterprise-app.json +195 -0
|
@@ -19,7 +19,131 @@
|
|
|
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
|
+
owned_by: null,
|
|
58
|
+
};
|
|
59
|
+
|
|
60
|
+
existing.required = existing.required || artifact.required !== false;
|
|
61
|
+
|
|
62
|
+
if (artifact.owned_by && typeof artifact.owned_by === 'string') {
|
|
63
|
+
existing.owned_by = artifact.owned_by;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (artifact.semantics) {
|
|
67
|
+
const legacySemanticId = existing.useLegacySemantics ? getSemanticIdForPath(artifact.path) : null;
|
|
68
|
+
if (artifact.semantics !== legacySemanticId) {
|
|
69
|
+
existing.semanticChecks.push({
|
|
70
|
+
semantics: artifact.semantics,
|
|
71
|
+
semantics_config: artifact.semantics_config || null,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
byPath.set(artifact.path, existing);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return [...byPath.values()];
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function addMissingFile(result, filePath) {
|
|
83
|
+
if (!result.missing_files.includes(filePath)) {
|
|
84
|
+
result.missing_files.push(filePath);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function prefixSemanticReason(filePath, reason) {
|
|
89
|
+
if (!reason || reason.includes(filePath)) {
|
|
90
|
+
return reason;
|
|
91
|
+
}
|
|
92
|
+
return `${filePath}: ${reason}`;
|
|
93
|
+
}
|
|
94
|
+
|
|
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 }) {
|
|
106
|
+
const failures = [];
|
|
107
|
+
const artifacts = buildEffectiveGateArtifacts(config, gateDef, phase);
|
|
108
|
+
|
|
109
|
+
for (const artifact of artifacts) {
|
|
110
|
+
const absPath = join(root, artifact.path);
|
|
111
|
+
if (!existsSync(absPath)) {
|
|
112
|
+
if (artifact.required) {
|
|
113
|
+
addMissingFile(result, artifact.path);
|
|
114
|
+
failures.push(`Required file missing: ${artifact.path}`);
|
|
115
|
+
}
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (artifact.useLegacySemantics) {
|
|
120
|
+
const semanticCheck = evaluateWorkflowGateSemantics(root, artifact.path);
|
|
121
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
122
|
+
failures.push(semanticCheck.reason);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
for (const semantic of artifact.semanticChecks) {
|
|
127
|
+
const semanticCheck = evaluateArtifactSemantics(root, {
|
|
128
|
+
path: artifact.path,
|
|
129
|
+
semantics: semantic.semantics,
|
|
130
|
+
semantics_config: semantic.semantics_config,
|
|
131
|
+
});
|
|
132
|
+
if (semanticCheck && !semanticCheck.ok) {
|
|
133
|
+
failures.push(prefixSemanticReason(artifact.path, semanticCheck.reason));
|
|
134
|
+
}
|
|
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
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return failures;
|
|
146
|
+
}
|
|
23
147
|
|
|
24
148
|
/**
|
|
25
149
|
* Evaluate whether the current phase exit gate is satisfied.
|
|
@@ -117,21 +241,15 @@ export function evaluatePhaseExit({ state, config, acceptedTurn, root }) {
|
|
|
117
241
|
|
|
118
242
|
const failures = [];
|
|
119
243
|
|
|
120
|
-
// 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
|
-
}
|
|
244
|
+
// Predicate: requires_files + ownership
|
|
245
|
+
failures.push(...evaluateGateArtifacts({
|
|
246
|
+
root,
|
|
247
|
+
config,
|
|
248
|
+
gateDef,
|
|
249
|
+
phase: currentPhase,
|
|
250
|
+
result,
|
|
251
|
+
state,
|
|
252
|
+
}));
|
|
135
253
|
|
|
136
254
|
// Predicate: requires_verification_pass
|
|
137
255
|
if (gateDef.requires_verification_pass) {
|
|
@@ -240,20 +358,14 @@ export function evaluateRunCompletion({ state, config, acceptedTurn, root }) {
|
|
|
240
358
|
const result = { ...baseResult, gate_id: gateId };
|
|
241
359
|
const failures = [];
|
|
242
360
|
|
|
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
|
-
}
|
|
361
|
+
failures.push(...evaluateGateArtifacts({
|
|
362
|
+
root,
|
|
363
|
+
config,
|
|
364
|
+
gateDef,
|
|
365
|
+
phase: currentPhase,
|
|
366
|
+
result,
|
|
367
|
+
state,
|
|
368
|
+
}));
|
|
257
369
|
|
|
258
370
|
if (gateDef.requires_verification_pass) {
|
|
259
371
|
const verificationStatus = acceptedTurn.verification?.status;
|
|
@@ -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
|
}
|
|
@@ -446,7 +490,26 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
446
490
|
const gateRequiredFiles = uniqueStrings(
|
|
447
491
|
Object.values(config?.gates || {}).flatMap((gate) => Array.isArray(gate?.requires_files) ? gate.requires_files : [])
|
|
448
492
|
);
|
|
449
|
-
|
|
493
|
+
|
|
494
|
+
// Collect workflow-kit artifact paths from explicit config
|
|
495
|
+
const wkArtifactPaths = [];
|
|
496
|
+
const wk = config?.workflow_kit;
|
|
497
|
+
if (wk && wk.phases && typeof wk.phases === 'object') {
|
|
498
|
+
for (const phaseConfig of Object.values(wk.phases)) {
|
|
499
|
+
if (Array.isArray(phaseConfig.artifacts)) {
|
|
500
|
+
for (const a of phaseConfig.artifacts) {
|
|
501
|
+
if (a.path && a.required !== false) {
|
|
502
|
+
wkArtifactPaths.push(a.path);
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const hasExplicitWorkflowKit = wk && wk._explicit === true;
|
|
510
|
+
const hasExplicitWorkflowKitArtifacts = hasExplicitWorkflowKit && Object.keys(wk.phases || {}).length > 0;
|
|
511
|
+
const baseFiles = hasExplicitWorkflowKit ? wkArtifactPaths : GOVERNED_WORKFLOW_KIT_BASE_FILES;
|
|
512
|
+
const requiredFiles = uniqueStrings([...baseFiles, ...gateRequiredFiles]);
|
|
450
513
|
const present = [];
|
|
451
514
|
const missing = [];
|
|
452
515
|
|
|
@@ -459,32 +522,38 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
459
522
|
}
|
|
460
523
|
}
|
|
461
524
|
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
525
|
+
// Build structural checks: from explicit workflow_kit semantics or hardcoded defaults
|
|
526
|
+
let structuralChecks;
|
|
527
|
+
if (hasExplicitWorkflowKit) {
|
|
528
|
+
structuralChecks = buildStructuralChecksFromWorkflowKit(root, wk, errors);
|
|
529
|
+
} else {
|
|
530
|
+
structuralChecks = GOVERNED_WORKFLOW_KIT_STRUCTURAL_CHECKS.map((check) => {
|
|
531
|
+
const absPath = join(root, check.file);
|
|
532
|
+
if (!existsSync(absPath)) {
|
|
533
|
+
return {
|
|
534
|
+
id: check.id,
|
|
535
|
+
file: check.file,
|
|
536
|
+
ok: false,
|
|
537
|
+
skipped: true,
|
|
538
|
+
description: check.description,
|
|
539
|
+
};
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
const content = readFileSync(absPath, 'utf8');
|
|
543
|
+
const ok = check.pattern.test(content);
|
|
544
|
+
if (!ok) {
|
|
545
|
+
errors.push(`Workflow kit file "${check.file}" must preserve its structural marker: ${check.description}.`);
|
|
546
|
+
}
|
|
547
|
+
|
|
465
548
|
return {
|
|
466
549
|
id: check.id,
|
|
467
550
|
file: check.file,
|
|
468
|
-
ok
|
|
469
|
-
skipped:
|
|
551
|
+
ok,
|
|
552
|
+
skipped: false,
|
|
470
553
|
description: check.description,
|
|
471
554
|
};
|
|
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
|
-
});
|
|
555
|
+
});
|
|
556
|
+
}
|
|
488
557
|
|
|
489
558
|
return {
|
|
490
559
|
ok: errors.length === 0,
|
|
@@ -498,6 +567,64 @@ export function validateGovernedWorkflowKit(root, config = {}) {
|
|
|
498
567
|
};
|
|
499
568
|
}
|
|
500
569
|
|
|
570
|
+
function buildStructuralChecksFromWorkflowKit(root, wk, errors) {
|
|
571
|
+
const checks = [];
|
|
572
|
+
if (!wk.phases) return checks;
|
|
573
|
+
|
|
574
|
+
for (const [phase, phaseConfig] of Object.entries(wk.phases)) {
|
|
575
|
+
if (!Array.isArray(phaseConfig.artifacts)) continue;
|
|
576
|
+
for (const artifact of phaseConfig.artifacts) {
|
|
577
|
+
if (!artifact.semantics) continue;
|
|
578
|
+
|
|
579
|
+
if (artifact.semantics === 'section_check' && artifact.semantics_config?.required_sections?.length) {
|
|
580
|
+
for (const section of artifact.semantics_config.required_sections) {
|
|
581
|
+
const checkId = `wk_${phase}_${artifact.path.replace(/[^a-zA-Z0-9]/g, '_')}_section_${section.replace(/[^a-zA-Z0-9]/g, '_')}`;
|
|
582
|
+
const description = `${artifact.path} defines ${section}`;
|
|
583
|
+
const absPath = join(root, artifact.path);
|
|
584
|
+
|
|
585
|
+
if (!existsSync(absPath)) {
|
|
586
|
+
checks.push({ id: checkId, file: artifact.path, ok: false, skipped: true, description });
|
|
587
|
+
continue;
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
const content = readFileSync(absPath, 'utf8');
|
|
591
|
+
const ok = content.includes(section);
|
|
592
|
+
if (!ok) {
|
|
593
|
+
errors.push(`Workflow kit file "${artifact.path}" must contain section: ${section}.`);
|
|
594
|
+
}
|
|
595
|
+
checks.push({ id: checkId, file: artifact.path, ok, skipped: false, description });
|
|
596
|
+
}
|
|
597
|
+
} else if (artifact.semantics !== 'section_check') {
|
|
598
|
+
// Built-in semantic check — generate a structural check entry
|
|
599
|
+
const checkId = `wk_${phase}_${artifact.semantics}`;
|
|
600
|
+
const description = `${artifact.path} passes ${artifact.semantics} validation`;
|
|
601
|
+
const absPath = join(root, artifact.path);
|
|
602
|
+
|
|
603
|
+
if (!existsSync(absPath)) {
|
|
604
|
+
checks.push({ id: checkId, file: artifact.path, ok: false, skipped: true, description });
|
|
605
|
+
continue;
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
// For built-in validators, delegate to the hardcoded check if one exists
|
|
609
|
+
const hardcoded = GOVERNED_WORKFLOW_KIT_STRUCTURAL_CHECKS.find(c => c.file === artifact.path);
|
|
610
|
+
if (hardcoded) {
|
|
611
|
+
const content = readFileSync(absPath, 'utf8');
|
|
612
|
+
const ok = hardcoded.pattern.test(content);
|
|
613
|
+
if (!ok) {
|
|
614
|
+
errors.push(`Workflow kit file "${artifact.path}" must preserve its structural marker: ${hardcoded.description}.`);
|
|
615
|
+
}
|
|
616
|
+
checks.push({ id: checkId, file: artifact.path, ok, skipped: false, description: hardcoded.description });
|
|
617
|
+
} else {
|
|
618
|
+
// No hardcoded check for this semantic — mark as passing (runtime gate handles full validation)
|
|
619
|
+
checks.push({ id: checkId, file: artifact.path, ok: true, skipped: false, description });
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
return checks;
|
|
626
|
+
}
|
|
627
|
+
|
|
501
628
|
export const SYSTEM_SPEC_OVERLAY_SEPARATOR = '## Template-Specific Guidance';
|
|
502
629
|
|
|
503
630
|
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,127 @@ 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, data.roles);
|
|
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, roles) {
|
|
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
|
+
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
|
+
}
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
592
|
+
}
|
|
593
|
+
|
|
454
594
|
/**
|
|
455
595
|
* Normalize a legacy v3 config into the internal shape.
|
|
456
596
|
* Does NOT modify the original file — this is a read-time transformation.
|
|
@@ -492,6 +632,7 @@ export function normalizeV3(raw) {
|
|
|
492
632
|
hooks: {},
|
|
493
633
|
notifications: {},
|
|
494
634
|
budget: null,
|
|
635
|
+
workflow_kit: normalizeWorkflowKit(undefined, DEFAULT_PHASES),
|
|
495
636
|
retention: {
|
|
496
637
|
talk_strategy: 'append_only',
|
|
497
638
|
history_strategy: 'jsonl_append_only',
|
|
@@ -536,6 +677,9 @@ export function normalizeV4(raw) {
|
|
|
536
677
|
}
|
|
537
678
|
}
|
|
538
679
|
|
|
680
|
+
const routing = raw.routing || {};
|
|
681
|
+
const routingPhases = Object.keys(routing).length > 0 ? Object.keys(routing) : DEFAULT_PHASES;
|
|
682
|
+
|
|
539
683
|
return {
|
|
540
684
|
schema_version: 4,
|
|
541
685
|
protocol_mode: 'governed',
|
|
@@ -547,11 +691,12 @@ export function normalizeV4(raw) {
|
|
|
547
691
|
},
|
|
548
692
|
roles,
|
|
549
693
|
runtimes: raw.runtimes || {},
|
|
550
|
-
routing
|
|
694
|
+
routing,
|
|
551
695
|
gates: raw.gates || {},
|
|
552
696
|
hooks: raw.hooks || {},
|
|
553
697
|
notifications: raw.notifications || {},
|
|
554
698
|
budget: raw.budget || null,
|
|
699
|
+
workflow_kit: normalizeWorkflowKit(raw.workflow_kit, routingPhases),
|
|
555
700
|
retention: raw.retention || {
|
|
556
701
|
talk_strategy: 'append_only',
|
|
557
702
|
history_strategy: 'jsonl_append_only',
|
|
@@ -633,6 +778,51 @@ export function getMaxConcurrentTurns(config, phase) {
|
|
|
633
778
|
}
|
|
634
779
|
|
|
635
780
|
|
|
781
|
+
/**
|
|
782
|
+
* Normalize workflow_kit config.
|
|
783
|
+
* When absent, builds defaults from routing phases using DEFAULT_PHASE_ARTIFACTS.
|
|
784
|
+
* When present, normalizes artifact entries.
|
|
785
|
+
*/
|
|
786
|
+
export function normalizeWorkflowKit(raw, routingPhases) {
|
|
787
|
+
if (raw === undefined || raw === null) {
|
|
788
|
+
return buildDefaultWorkflowKit(routingPhases);
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
// Empty object is an explicit opt-out — no artifacts
|
|
792
|
+
if (typeof raw === 'object' && !Array.isArray(raw) && Object.keys(raw).length === 0) {
|
|
793
|
+
return { phases: {}, _explicit: true };
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
const phases = {};
|
|
797
|
+
if (raw.phases) {
|
|
798
|
+
for (const [phase, phaseConfig] of Object.entries(raw.phases)) {
|
|
799
|
+
phases[phase] = {
|
|
800
|
+
artifacts: (phaseConfig.artifacts || []).map(a => ({
|
|
801
|
+
path: a.path,
|
|
802
|
+
semantics: a.semantics || null,
|
|
803
|
+
semantics_config: a.semantics_config || null,
|
|
804
|
+
owned_by: a.owned_by || null,
|
|
805
|
+
required: a.required !== false,
|
|
806
|
+
})),
|
|
807
|
+
};
|
|
808
|
+
}
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
return { phases, _explicit: true };
|
|
812
|
+
}
|
|
813
|
+
|
|
814
|
+
function buildDefaultWorkflowKit(routingPhases) {
|
|
815
|
+
const phases = {};
|
|
816
|
+
for (const phase of routingPhases) {
|
|
817
|
+
if (DEFAULT_PHASE_ARTIFACTS[phase]) {
|
|
818
|
+
phases[phase] = {
|
|
819
|
+
artifacts: DEFAULT_PHASE_ARTIFACTS[phase].map(a => ({ ...a, semantics_config: null })),
|
|
820
|
+
};
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
return { phases };
|
|
824
|
+
}
|
|
825
|
+
|
|
636
826
|
// --- Internal helpers ---
|
|
637
827
|
|
|
638
828
|
function inferWriteAuthority(agentId) {
|