agentxchain 2.101.0 → 2.103.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/bin/agentxchain.js +21 -0
- package/package.json +1 -1
- package/src/commands/benchmark-workloads.js +206 -0
- package/src/commands/benchmark.js +775 -0
- package/src/commands/decisions.js +29 -3
- package/src/commands/doctor.js +17 -0
- package/src/commands/role.js +24 -10
- package/src/lib/admission-control.js +247 -0
- package/src/lib/dispatch-bundle.js +1 -1
- package/src/lib/export-diff.js +19 -3
- package/src/lib/export-verifier.js +53 -23
- package/src/lib/export.js +4 -14
- package/src/lib/governed-state.js +3 -1
- package/src/lib/normalized-config.js +8 -72
- package/src/lib/repo-decisions.js +163 -3
- package/src/lib/report.js +40 -5
- package/src/lib/run-loop.js +8 -0
- package/src/lib/validation.js +5 -3
|
@@ -366,6 +366,11 @@ export function validateV4Config(data, projectRoot) {
|
|
|
366
366
|
if (!VALID_WRITE_AUTHORITIES.includes(role.write_authority)) {
|
|
367
367
|
errors.push(`Role "${id}": write_authority must be one of: ${VALID_WRITE_AUTHORITIES.join(', ')}`);
|
|
368
368
|
}
|
|
369
|
+
if (role.decision_authority !== undefined && role.decision_authority !== null) {
|
|
370
|
+
if (!Number.isInteger(role.decision_authority) || role.decision_authority < 0 || role.decision_authority > 99) {
|
|
371
|
+
errors.push(`Role "${id}": decision_authority must be an integer between 0 and 99`);
|
|
372
|
+
}
|
|
373
|
+
}
|
|
369
374
|
if (typeof role.runtime !== 'string' || !role.runtime.trim()) errors.push(`Role "${id}": runtime required`);
|
|
370
375
|
}
|
|
371
376
|
}
|
|
@@ -556,82 +561,13 @@ export function validateV4Config(data, projectRoot) {
|
|
|
556
561
|
errors.push(...timeoutValidation.errors);
|
|
557
562
|
}
|
|
558
563
|
|
|
559
|
-
|
|
564
|
+
// Admission control (ADM-001..004) is handled by the validate, doctor, and
|
|
565
|
+
// run-loop paths which call runAdmissionControl() directly. Config schema
|
|
566
|
+
// validation here should not duplicate that surface.
|
|
560
567
|
|
|
561
568
|
return { ok: errors.length === 0, errors, warnings };
|
|
562
569
|
}
|
|
563
570
|
|
|
564
|
-
export function collectRemoteReviewOnlyGateWarnings(data) {
|
|
565
|
-
const warnings = [];
|
|
566
|
-
const routing = data?.routing;
|
|
567
|
-
const gates = data?.gates;
|
|
568
|
-
const roles = data?.roles;
|
|
569
|
-
const runtimes = data?.runtimes;
|
|
570
|
-
|
|
571
|
-
if (!routing || !gates || !roles || !runtimes) {
|
|
572
|
-
return warnings;
|
|
573
|
-
}
|
|
574
|
-
|
|
575
|
-
for (const [phase, route] of Object.entries(routing)) {
|
|
576
|
-
const exitGateId = route?.exit_gate;
|
|
577
|
-
if (!exitGateId || !gates[exitGateId]) {
|
|
578
|
-
continue;
|
|
579
|
-
}
|
|
580
|
-
|
|
581
|
-
const requiredFiles = Array.isArray(gates[exitGateId]?.requires_files)
|
|
582
|
-
? gates[exitGateId].requires_files.filter(filePath => typeof filePath === 'string' && filePath.trim())
|
|
583
|
-
: [];
|
|
584
|
-
if (requiredFiles.length === 0) {
|
|
585
|
-
continue;
|
|
586
|
-
}
|
|
587
|
-
|
|
588
|
-
const candidateRoleIds = [
|
|
589
|
-
route?.entry_role,
|
|
590
|
-
...(Array.isArray(route?.allowed_next_roles) ? route.allowed_next_roles : []),
|
|
591
|
-
].filter((roleId) => roleId && roleId !== 'human');
|
|
592
|
-
|
|
593
|
-
if (candidateRoleIds.length === 0) {
|
|
594
|
-
continue;
|
|
595
|
-
}
|
|
596
|
-
|
|
597
|
-
const candidateRoles = [...new Set(candidateRoleIds)]
|
|
598
|
-
.map((roleId) => {
|
|
599
|
-
const role = roles[roleId];
|
|
600
|
-
const runtime = role?.runtime ? runtimes[role.runtime] : null;
|
|
601
|
-
if (!role || !runtime) {
|
|
602
|
-
return null;
|
|
603
|
-
}
|
|
604
|
-
return { roleId, role, runtime };
|
|
605
|
-
})
|
|
606
|
-
.filter(Boolean);
|
|
607
|
-
|
|
608
|
-
if (candidateRoles.length === 0) {
|
|
609
|
-
continue;
|
|
610
|
-
}
|
|
611
|
-
|
|
612
|
-
const hasFileProducingRole = candidateRoles.some(({ role }) =>
|
|
613
|
-
role.write_authority === 'authoritative' || role.write_authority === 'proposed');
|
|
614
|
-
if (hasFileProducingRole) {
|
|
615
|
-
continue;
|
|
616
|
-
}
|
|
617
|
-
|
|
618
|
-
const allRemoteReviewOnly = candidateRoles.every(({ role, runtime }) =>
|
|
619
|
-
role.write_authority === 'review_only' && (runtime.type === 'api_proxy' || runtime.type === 'remote_agent'));
|
|
620
|
-
if (!allRemoteReviewOnly) {
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
const roleSummary = candidateRoles
|
|
625
|
-
.map(({ roleId, runtime }) => `${roleId}:${runtime.type}`)
|
|
626
|
-
.join(', ');
|
|
627
|
-
warnings.push(
|
|
628
|
-
`Routing "${phase}" exits through gate "${exitGateId}" with requires_files (${requiredFiles.join(', ')}) but all participating roles are review_only remote runtimes (${roleSummary}). Those files cannot be produced through governed turns; add a proposed/authoritative writer, remove the gate files, or expect operator-managed out-of-band artifacts.`,
|
|
629
|
-
);
|
|
630
|
-
}
|
|
631
|
-
|
|
632
|
-
return warnings;
|
|
633
|
-
}
|
|
634
|
-
|
|
635
571
|
export function validateBudgetConfig(budget) {
|
|
636
572
|
const errors = [];
|
|
637
573
|
|
|
@@ -37,6 +37,74 @@ export function getRepoDecisionById(root, decisionId) {
|
|
|
37
37
|
return readRepoDecisions(root).find(d => d.id === decisionId) || null;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
export function getDecisionAuthorityMetadata(roleId, config) {
|
|
41
|
+
const resolved = resolveDecisionAuthority(roleId, config);
|
|
42
|
+
if (resolved === null) return null;
|
|
43
|
+
if (typeof resolved === 'object' && resolved.unknown) {
|
|
44
|
+
return {
|
|
45
|
+
level: resolved.level,
|
|
46
|
+
source: 'unknown_role',
|
|
47
|
+
role: roleId || null,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
if (roleId === 'human') {
|
|
51
|
+
const explicitHumanAuthority = typeof config?.roles?.human?.decision_authority === 'number';
|
|
52
|
+
return {
|
|
53
|
+
level: resolved,
|
|
54
|
+
source: explicitHumanAuthority ? 'configured' : 'human_default',
|
|
55
|
+
role: roleId,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
level: resolved,
|
|
60
|
+
source: 'configured',
|
|
61
|
+
role: roleId || null,
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function summarizeRepoDecisions(decisions, config) {
|
|
66
|
+
if (!Array.isArray(decisions) || decisions.length === 0) return null;
|
|
67
|
+
const active = decisions.filter((d) => d.status === 'active');
|
|
68
|
+
const overridden = decisions.filter((d) => d.status === 'overridden');
|
|
69
|
+
const addAuthority = (decision) => {
|
|
70
|
+
const authority = getDecisionAuthorityMetadata(decision.role, config);
|
|
71
|
+
return {
|
|
72
|
+
id: decision.id,
|
|
73
|
+
category: decision.category,
|
|
74
|
+
statement: decision.statement,
|
|
75
|
+
role: decision.role,
|
|
76
|
+
run_id: decision.run_id,
|
|
77
|
+
overrides: decision.overrides || null,
|
|
78
|
+
durability: decision.durability || 'repo',
|
|
79
|
+
authority_level: authority?.level ?? null,
|
|
80
|
+
authority_source: authority?.source || null,
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
total: decisions.length,
|
|
85
|
+
active_count: active.length,
|
|
86
|
+
overridden_count: overridden.length,
|
|
87
|
+
active: active.map(addAuthority),
|
|
88
|
+
overridden: overridden.map((d) => {
|
|
89
|
+
const authority = getDecisionAuthorityMetadata(d.role, config);
|
|
90
|
+
return {
|
|
91
|
+
id: d.id,
|
|
92
|
+
overridden_by: d.overridden_by,
|
|
93
|
+
statement: d.statement,
|
|
94
|
+
overrides: d.overrides || null,
|
|
95
|
+
durability: d.durability || 'repo',
|
|
96
|
+
role: d.role || null,
|
|
97
|
+
authority_level: authority?.level ?? null,
|
|
98
|
+
authority_source: authority?.source || null,
|
|
99
|
+
};
|
|
100
|
+
}),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export function buildRepoDecisionsSummary(decisions) {
|
|
105
|
+
return summarizeRepoDecisions(decisions, null);
|
|
106
|
+
}
|
|
107
|
+
|
|
40
108
|
// ── Write ───────────────────────────────────────────────────────────────────
|
|
41
109
|
|
|
42
110
|
export function appendRepoDecision(root, entry) {
|
|
@@ -62,7 +130,16 @@ export function overrideRepoDecision(root, targetId, overridingId) {
|
|
|
62
130
|
|
|
63
131
|
// ── Validate Override ───────────────────────────────────────────────────────
|
|
64
132
|
|
|
65
|
-
|
|
133
|
+
/**
|
|
134
|
+
* Validate that an override is allowed.
|
|
135
|
+
* @param {string} root - project root
|
|
136
|
+
* @param {object} decision - the overriding decision (must have .overrides, .id, optionally .role)
|
|
137
|
+
* @param {object} [config] - agentxchain config (used for authority enforcement)
|
|
138
|
+
* @returns {{ ok: boolean, error?: string, warning?: string }}
|
|
139
|
+
*
|
|
140
|
+
* DEC-SPEC: .planning/DECISION_AUTHORITY_SPEC.md
|
|
141
|
+
*/
|
|
142
|
+
export function validateOverride(root, decision, config) {
|
|
66
143
|
if (!decision.overrides) return { ok: true };
|
|
67
144
|
const targetId = decision.overrides;
|
|
68
145
|
const target = getRepoDecisionById(root, targetId);
|
|
@@ -75,21 +152,104 @@ export function validateOverride(root, decision) {
|
|
|
75
152
|
if (target.status !== 'active') {
|
|
76
153
|
return { ok: false, error: `decisions: ${targetId} has status "${target.status}", only active repo decisions can be overridden.` };
|
|
77
154
|
}
|
|
155
|
+
|
|
156
|
+
// Authority enforcement (opt-in via decision_authority on roles)
|
|
157
|
+
const authorityResult = checkOverrideAuthority(decision, target, config);
|
|
158
|
+
if (!authorityResult.ok) return authorityResult;
|
|
159
|
+
|
|
160
|
+
return authorityResult.warning ? { ok: true, warning: authorityResult.warning } : { ok: true };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Resolve the decision_authority level for a role.
|
|
165
|
+
* - 'human' defaults to 100 unless explicitly configured.
|
|
166
|
+
* - Unknown roles default to 0 (with warning).
|
|
167
|
+
* - Null means opt-out (no enforcement).
|
|
168
|
+
*/
|
|
169
|
+
export function resolveDecisionAuthority(roleId, config) {
|
|
170
|
+
if (!config || !config.roles) return null;
|
|
171
|
+
if (roleId === 'human') {
|
|
172
|
+
const humanRole = config.roles.human;
|
|
173
|
+
if (humanRole && typeof humanRole.decision_authority === 'number') {
|
|
174
|
+
return humanRole.decision_authority;
|
|
175
|
+
}
|
|
176
|
+
return 100; // human default
|
|
177
|
+
}
|
|
178
|
+
const role = config.roles[roleId];
|
|
179
|
+
if (!role) return { level: 0, unknown: true };
|
|
180
|
+
if (typeof role.decision_authority !== 'number') return null;
|
|
181
|
+
return role.decision_authority;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check whether the overriding role has sufficient authority to override
|
|
186
|
+
* a decision made by the target role.
|
|
187
|
+
*/
|
|
188
|
+
function checkOverrideAuthority(overridingDecision, targetDecision, config) {
|
|
189
|
+
if (!config || !config.roles) return { ok: true };
|
|
190
|
+
|
|
191
|
+
const overridingRole = overridingDecision.role;
|
|
192
|
+
const targetRole = targetDecision.role;
|
|
193
|
+
|
|
194
|
+
// Same-role override is always allowed
|
|
195
|
+
if (overridingRole && targetRole && overridingRole === targetRole) return { ok: true };
|
|
196
|
+
|
|
197
|
+
const targetAuth = resolveDecisionAuthority(targetRole, config);
|
|
198
|
+
const overridingAuth = resolveDecisionAuthority(overridingRole, config);
|
|
199
|
+
|
|
200
|
+
// Handle unknown target role
|
|
201
|
+
let warning;
|
|
202
|
+
if (targetAuth && typeof targetAuth === 'object' && targetAuth.unknown) {
|
|
203
|
+
warning = `decisions: target decision role '${targetRole}' not found in current config, treating as authority 0.`;
|
|
204
|
+
// targetAuth is effectively 0, allow override
|
|
205
|
+
return { ok: true, warning };
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Opt-in: if either side is null (not configured), allow
|
|
209
|
+
if (targetAuth === null || overridingAuth === null) return { ok: true };
|
|
210
|
+
|
|
211
|
+
// Handle unknown overriding role (shouldn't normally happen, but be safe)
|
|
212
|
+
const overridingLevel = (typeof overridingAuth === 'object' && overridingAuth.unknown) ? 0 : overridingAuth;
|
|
213
|
+
const targetLevel = (typeof targetAuth === 'object') ? 0 : targetAuth;
|
|
214
|
+
|
|
215
|
+
if (overridingLevel < targetLevel) {
|
|
216
|
+
return {
|
|
217
|
+
ok: false,
|
|
218
|
+
error: `decisions: role '${overridingRole}' (authority ${overridingLevel}) cannot override ${targetDecision.id} made by '${targetRole}' (authority ${targetLevel}). Override requires authority >= ${targetLevel}.`,
|
|
219
|
+
};
|
|
220
|
+
}
|
|
221
|
+
|
|
78
222
|
return { ok: true };
|
|
79
223
|
}
|
|
80
224
|
|
|
81
225
|
// ── Render ──────────────────────────────────────────────────────────────────
|
|
82
226
|
|
|
83
|
-
export function renderRepoDecisionsMarkdown(activeDecisions) {
|
|
227
|
+
export function renderRepoDecisionsMarkdown(activeDecisions, config) {
|
|
84
228
|
if (!activeDecisions || activeDecisions.length === 0) return '';
|
|
229
|
+
const hasAuthorityPolicy = Object.values(config?.roles || {}).some((role) => (
|
|
230
|
+
role && typeof role.decision_authority === 'number'
|
|
231
|
+
));
|
|
85
232
|
const lines = [
|
|
86
233
|
'## Active Repo Decisions',
|
|
87
234
|
'',
|
|
88
235
|
'These decisions persist from prior governed runs. Comply or explicitly override with rationale.',
|
|
89
236
|
'',
|
|
90
237
|
];
|
|
238
|
+
if (hasAuthorityPolicy) {
|
|
239
|
+
lines.push('When both roles declare `decision_authority`, overrides require authority greater than or equal to the originating role.');
|
|
240
|
+
lines.push('');
|
|
241
|
+
}
|
|
91
242
|
for (const d of activeDecisions) {
|
|
92
|
-
|
|
243
|
+
const authority = getDecisionAuthorityMetadata(d.role, config);
|
|
244
|
+
const authorityText = authority
|
|
245
|
+
? authority.source === 'human_default'
|
|
246
|
+
? ' authority 100 (human default)'
|
|
247
|
+
: authority.source === 'unknown_role'
|
|
248
|
+
? ' authority 0 (role no longer in config)'
|
|
249
|
+
: ` authority ${authority.level}`
|
|
250
|
+
: '';
|
|
251
|
+
const supersedes = d.overrides ? ` Supersedes ${d.overrides}.` : '';
|
|
252
|
+
lines.push(`- **${d.id}** (${d.category}, by ${d.role || 'unknown'}${authorityText}): ${d.statement}${supersedes}`);
|
|
93
253
|
}
|
|
94
254
|
lines.push('');
|
|
95
255
|
return lines.join('\n');
|
package/src/lib/report.js
CHANGED
|
@@ -1320,7 +1320,13 @@ export function formatGovernanceReportText(report) {
|
|
|
1320
1320
|
lines.push('', 'Repo Decisions:');
|
|
1321
1321
|
lines.push(` Active: ${run.repo_decisions.active_count} Overridden: ${run.repo_decisions.overridden_count}`);
|
|
1322
1322
|
for (const d of run.repo_decisions.active) {
|
|
1323
|
-
|
|
1323
|
+
const supersedes = d.overrides ? ` | supersedes ${d.overrides}` : '';
|
|
1324
|
+
const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1325
|
+
lines.push(` - ${d.id} (${d.category}): ${d.statement}${supersedes}${authority}`);
|
|
1326
|
+
}
|
|
1327
|
+
for (const d of run.repo_decisions.overridden || []) {
|
|
1328
|
+
const authority = d.authority_level == null ? '' : ` | authority ${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1329
|
+
lines.push(` - ${d.id} (overridden by ${d.overridden_by || 'unknown'}${authority})`);
|
|
1324
1330
|
}
|
|
1325
1331
|
}
|
|
1326
1332
|
|
|
@@ -1825,10 +1831,20 @@ export function formatGovernanceReportMarkdown(report) {
|
|
|
1825
1831
|
if (run.repo_decisions?.active?.length > 0) {
|
|
1826
1832
|
lines.push('', '## Repo Decisions', '');
|
|
1827
1833
|
lines.push(`Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}`, '');
|
|
1828
|
-
lines.push('| ID | Category | Statement | Role | Run |', '
|
|
1834
|
+
lines.push('| ID | Category | Statement | Role | Authority | Run | Supersedes |', '|----|----------|-----------|------|-----------|-----|------------|');
|
|
1829
1835
|
for (const d of run.repo_decisions.active) {
|
|
1830
1836
|
const stmt = (d.statement || '').replace(/\|/g, '\\|');
|
|
1831
|
-
|
|
1837
|
+
const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1838
|
+
lines.push(`| ${d.id} | ${d.category} | ${stmt} | ${d.role || '—'} | ${authority} | \`${(d.run_id || '').slice(0, 12)}\` | ${d.overrides || '—'} |`);
|
|
1839
|
+
}
|
|
1840
|
+
if (run.repo_decisions.overridden?.length > 0) {
|
|
1841
|
+
lines.push('', 'Overridden decisions:', '');
|
|
1842
|
+
lines.push('| ID | Statement | Authority | Overridden By |', '|----|-----------|-----------|---------------|');
|
|
1843
|
+
for (const d of run.repo_decisions.overridden) {
|
|
1844
|
+
const stmt = (d.statement || '').replace(/\|/g, '\\|');
|
|
1845
|
+
const authority = d.authority_level == null ? '—' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`;
|
|
1846
|
+
lines.push(`| ${d.id} | ${stmt} | ${authority} | ${d.overridden_by || '—'} |`);
|
|
1847
|
+
}
|
|
1832
1848
|
}
|
|
1833
1849
|
}
|
|
1834
1850
|
|
|
@@ -2453,9 +2469,28 @@ function renderRunHtml(report) {
|
|
|
2453
2469
|
if (run.repo_decisions?.active?.length > 0) {
|
|
2454
2470
|
let rdHtml = `<p>Active: ${run.repo_decisions.active_count} | Overridden: ${run.repo_decisions.overridden_count}</p>`;
|
|
2455
2471
|
rdHtml += htmlTable(
|
|
2456
|
-
['ID', 'Category', 'Statement', 'Role', 'Run'],
|
|
2457
|
-
run.repo_decisions.active.map((d) => [
|
|
2472
|
+
['ID', 'Category', 'Statement', 'Role', 'Authority', 'Run', 'Supersedes'],
|
|
2473
|
+
run.repo_decisions.active.map((d) => [
|
|
2474
|
+
esc(d.id),
|
|
2475
|
+
esc(d.category),
|
|
2476
|
+
esc(d.statement || ''),
|
|
2477
|
+
esc(d.role || '\u2014'),
|
|
2478
|
+
esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
|
|
2479
|
+
`<code>${esc((d.run_id || '').slice(0, 12))}</code>`,
|
|
2480
|
+
esc(d.overrides || '\u2014'),
|
|
2481
|
+
]),
|
|
2458
2482
|
);
|
|
2483
|
+
if (run.repo_decisions.overridden?.length > 0) {
|
|
2484
|
+
rdHtml += htmlTable(
|
|
2485
|
+
['ID', 'Statement', 'Authority', 'Overridden By'],
|
|
2486
|
+
run.repo_decisions.overridden.map((d) => [
|
|
2487
|
+
esc(d.id),
|
|
2488
|
+
esc(d.statement || ''),
|
|
2489
|
+
esc(d.authority_level == null ? '\u2014' : `${d.authority_level}${d.authority_source === 'human_default' ? ' (human default)' : ''}`),
|
|
2490
|
+
esc(d.overridden_by || '\u2014'),
|
|
2491
|
+
]),
|
|
2492
|
+
);
|
|
2493
|
+
}
|
|
2459
2494
|
sections.push(`<div class="section">${htmlSection('Repo Decisions', rdHtml)}</div>`);
|
|
2460
2495
|
}
|
|
2461
2496
|
|
package/src/lib/run-loop.js
CHANGED
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
RUNNER_INTERFACE_VERSION,
|
|
33
33
|
} from './runner-interface.js';
|
|
34
34
|
|
|
35
|
+
import { runAdmissionControl } from './admission-control.js';
|
|
35
36
|
import { mkdirSync, writeFileSync } from 'fs';
|
|
36
37
|
import { join, dirname } from 'path';
|
|
37
38
|
|
|
@@ -65,6 +66,13 @@ export async function runLoop(root, config, callbacks, options = {}) {
|
|
|
65
66
|
}
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
// ── Admission control — reject provably dead-end configs ────────────────
|
|
70
|
+
const admission = runAdmissionControl(config, config);
|
|
71
|
+
if (!admission.ok) {
|
|
72
|
+
return makeResult(false, 'admission_rejected', null, 0, [], 0,
|
|
73
|
+
admission.errors.map(e => `Admission control: ${e}`));
|
|
74
|
+
}
|
|
75
|
+
|
|
68
76
|
// ── Initialize if idle ──────────────────────────────────────────────────
|
|
69
77
|
let state = loadState(root, config);
|
|
70
78
|
const shouldRestartCompleted = state?.status === 'completed' && options.startNewRunFromCompleted === true;
|
package/src/lib/validation.js
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
validateAcceptanceHintCompletion,
|
|
10
10
|
validateGovernedWorkflowKit,
|
|
11
11
|
} from './governed-templates.js';
|
|
12
|
-
import {
|
|
12
|
+
import { runAdmissionControl } from './admission-control.js';
|
|
13
13
|
|
|
14
14
|
const DEFAULT_REQUIRED_FILES = [
|
|
15
15
|
'.planning/PROJECT.md',
|
|
@@ -116,8 +116,10 @@ export function validateGovernedProject(root, rawConfig, config, opts = {}) {
|
|
|
116
116
|
errors.push(...workflowKit.errors);
|
|
117
117
|
warnings.push(...workflowKit.warnings);
|
|
118
118
|
|
|
119
|
-
//
|
|
120
|
-
|
|
119
|
+
// Admission control — reject provably dead-end configs
|
|
120
|
+
const admission = runAdmissionControl(config, rawConfig);
|
|
121
|
+
errors.push(...admission.errors);
|
|
122
|
+
warnings.push(...admission.warnings);
|
|
121
123
|
|
|
122
124
|
const mustExist = [
|
|
123
125
|
config.files?.state || '.agentxchain/state.json',
|