agentxchain 2.154.11 → 2.155.1
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 +1 -0
- package/package.json +1 -1
- package/src/commands/init.js +24 -0
- package/src/commands/run.js +14 -2
- package/src/commands/schedule.js +30 -12
- package/src/lib/continuous-run.js +536 -16
- package/src/lib/governed-state.js +8 -0
- package/src/lib/idle-expansion-result-validator.js +251 -0
- package/src/lib/intake.js +85 -1
- package/src/lib/normalized-config.js +64 -0
- package/src/lib/schemas/agentxchain-config.schema.json +31 -0
- package/src/lib/schemas/turn-result.schema.json +67 -0
- package/src/lib/turn-result-validator.js +25 -0
- package/src/lib/vision-reader.js +165 -1
|
@@ -20,6 +20,7 @@ import { join, dirname } from 'path';
|
|
|
20
20
|
import { randomBytes, createHash } from 'crypto';
|
|
21
21
|
import { safeWriteJson } from './safe-write.js';
|
|
22
22
|
import { validateStagedTurnResult } from './turn-result-validator.js';
|
|
23
|
+
import { summarizeIdleExpansionResult } from './idle-expansion-result-validator.js';
|
|
23
24
|
import { evaluatePhaseExit, evaluateRunCompletion, getNextPhase } from './gate-evaluator.js';
|
|
24
25
|
import { evaluateApprovalPolicy } from './approval-policy.js';
|
|
25
26
|
import { evaluatePolicies } from './policy-evaluator.js';
|
|
@@ -3565,6 +3566,9 @@ export function assignGovernedTurn(root, config, roleId, options = {}) {
|
|
|
3565
3566
|
if (options.intakeContext) {
|
|
3566
3567
|
newTurn.intake_context = options.intakeContext;
|
|
3567
3568
|
}
|
|
3569
|
+
if (options.idleExpansionContext) {
|
|
3570
|
+
newTurn.idle_expansion_context = options.idleExpansionContext;
|
|
3571
|
+
}
|
|
3568
3572
|
|
|
3569
3573
|
// Attach delegation context if this turn fulfills a pending delegation
|
|
3570
3574
|
const delegationQueue = state.delegation_queue || [];
|
|
@@ -4870,6 +4874,7 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4870
4874
|
}
|
|
4871
4875
|
|
|
4872
4876
|
const acceptedSequence = (state.turn_sequence || 0) + 1;
|
|
4877
|
+
const idleExpansionResultSummary = summarizeIdleExpansionResult(turnResult);
|
|
4873
4878
|
const historyEntry = {
|
|
4874
4879
|
turn_id: turnResult.turn_id,
|
|
4875
4880
|
run_id: turnResult.run_id,
|
|
@@ -4891,6 +4896,9 @@ function _acceptGovernedTurnLocked(root, config, opts) {
|
|
|
4891
4896
|
proposed_next_role: turnResult.proposed_next_role,
|
|
4892
4897
|
phase_transition_request: turnResult.phase_transition_request,
|
|
4893
4898
|
run_completion_request: Boolean(turnResult.run_completion_request),
|
|
4899
|
+
...(idleExpansionResultSummary
|
|
4900
|
+
? { idle_expansion_result_summary: idleExpansionResultSummary }
|
|
4901
|
+
: {}),
|
|
4894
4902
|
assigned_sequence: Number.isInteger(currentTurn.assigned_sequence) ? currentTurn.assigned_sequence : acceptedSequence,
|
|
4895
4903
|
accepted_sequence: acceptedSequence,
|
|
4896
4904
|
concurrent_with: Array.isArray(currentTurn.concurrent_with) ? currentTurn.concurrent_with : [],
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
import { VALID_GOVERNED_TEMPLATE_IDS } from './governed-templates.js';
|
|
2
|
+
|
|
3
|
+
const VALID_KINDS = ['new_intake_intent', 'vision_exhausted'];
|
|
4
|
+
const VALID_TRACE_KINDS = ['advances', 'supports', 'unblocks'];
|
|
5
|
+
const VALID_EXHAUSTION_STATUSES = ['complete', 'deferred', 'out_of_scope'];
|
|
6
|
+
const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
|
|
7
|
+
|
|
8
|
+
export function validateIdleExpansionTurnResult(turnResult, context = {}) {
|
|
9
|
+
const required = context.required === true;
|
|
10
|
+
const errors = [];
|
|
11
|
+
const warnings = [];
|
|
12
|
+
const result = turnResult?.idle_expansion_result;
|
|
13
|
+
|
|
14
|
+
if (result === undefined || result === null) {
|
|
15
|
+
if (required) {
|
|
16
|
+
errors.push('idle_expansion_result is required for vision_idle_expansion turns.');
|
|
17
|
+
}
|
|
18
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
if (typeof result !== 'object' || Array.isArray(result)) {
|
|
22
|
+
return {
|
|
23
|
+
ok: false,
|
|
24
|
+
errors: ['idle_expansion_result must be an object.'],
|
|
25
|
+
warnings,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (!VALID_KINDS.includes(result.kind)) {
|
|
30
|
+
errors.push(`idle_expansion_result.kind must be one of: ${VALID_KINDS.join(', ')}.`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
if (!Number.isInteger(result.expansion_iteration) || result.expansion_iteration < 1) {
|
|
34
|
+
errors.push('idle_expansion_result.expansion_iteration must be a positive integer.');
|
|
35
|
+
} else if (
|
|
36
|
+
Number.isInteger(context.expansionIteration)
|
|
37
|
+
&& context.expansionIteration >= 1
|
|
38
|
+
&& result.expansion_iteration !== context.expansionIteration
|
|
39
|
+
) {
|
|
40
|
+
errors.push(
|
|
41
|
+
`idle_expansion_result.expansion_iteration mismatch: got ${result.expansion_iteration}, expected ${context.expansionIteration}.`
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const visionHeadings = normalizeVisionHeadingSnapshot(context.visionHeadingsSnapshot);
|
|
46
|
+
const traceErrors = validateVisionTraceability(result.vision_traceability, {
|
|
47
|
+
required: result.kind === 'new_intake_intent',
|
|
48
|
+
visionHeadings,
|
|
49
|
+
});
|
|
50
|
+
errors.push(...traceErrors);
|
|
51
|
+
|
|
52
|
+
if (result.kind === 'new_intake_intent') {
|
|
53
|
+
errors.push(...validateNewIntakeIntent(result.new_intake_intent));
|
|
54
|
+
if ('vision_exhausted' in result) {
|
|
55
|
+
errors.push('idle_expansion_result.vision_exhausted is only allowed when kind is "vision_exhausted".');
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.kind === 'vision_exhausted') {
|
|
60
|
+
errors.push(...validateVisionExhausted(result.vision_exhausted, visionHeadings));
|
|
61
|
+
if ('new_intake_intent' in result) {
|
|
62
|
+
errors.push('idle_expansion_result.new_intake_intent is only allowed when kind is "new_intake_intent".');
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function summarizeIdleExpansionResult(turnResult) {
|
|
70
|
+
const result = turnResult?.idle_expansion_result;
|
|
71
|
+
if (!result || typeof result !== 'object' || Array.isArray(result)) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (result.kind === 'new_intake_intent') {
|
|
76
|
+
return {
|
|
77
|
+
kind: result.kind,
|
|
78
|
+
expansion_iteration: result.expansion_iteration,
|
|
79
|
+
new_intent_title: truncate(result.new_intake_intent?.title || '', 120),
|
|
80
|
+
priority: result.new_intake_intent?.priority || null,
|
|
81
|
+
template: result.new_intake_intent?.template || null,
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (result.kind === 'vision_exhausted') {
|
|
86
|
+
const firstReason = Array.isArray(result.vision_exhausted?.classification)
|
|
87
|
+
? result.vision_exhausted.classification.find((entry) => typeof entry?.reason === 'string')?.reason || ''
|
|
88
|
+
: '';
|
|
89
|
+
return {
|
|
90
|
+
kind: result.kind,
|
|
91
|
+
expansion_iteration: result.expansion_iteration,
|
|
92
|
+
reason_excerpt: truncate(firstReason, 160),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return {
|
|
97
|
+
kind: result.kind || null,
|
|
98
|
+
expansion_iteration: result.expansion_iteration || null,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function validateVisionTraceability(traceability, { required, visionHeadings }) {
|
|
103
|
+
const errors = [];
|
|
104
|
+
|
|
105
|
+
if (!Array.isArray(traceability)) {
|
|
106
|
+
errors.push('idle_expansion_result.vision_traceability must be an array.');
|
|
107
|
+
return errors;
|
|
108
|
+
}
|
|
109
|
+
if (required && traceability.length === 0) {
|
|
110
|
+
errors.push('idle_expansion_result.vision_traceability must cite at least one VISION.md heading for new_intake_intent.');
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
for (let i = 0; i < traceability.length; i++) {
|
|
114
|
+
const entry = traceability[i];
|
|
115
|
+
const prefix = `idle_expansion_result.vision_traceability[${i}]`;
|
|
116
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
117
|
+
errors.push(`${prefix} must be an object.`);
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const heading = typeof entry.vision_heading === 'string' ? entry.vision_heading.trim() : '';
|
|
122
|
+
if (!heading) {
|
|
123
|
+
errors.push(`${prefix}.vision_heading must be a non-empty string.`);
|
|
124
|
+
} else if (visionHeadings.length > 0 && !visionHeadings.includes(heading)) {
|
|
125
|
+
errors.push(`${prefix}.vision_heading "${heading}" is not present in the session VISION.md heading snapshot.`);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (entry.goal !== undefined && (typeof entry.goal !== 'string' || !entry.goal.trim())) {
|
|
129
|
+
errors.push(`${prefix}.goal must be a non-empty string when provided.`);
|
|
130
|
+
}
|
|
131
|
+
if (entry.kind !== undefined && !VALID_TRACE_KINDS.includes(entry.kind)) {
|
|
132
|
+
errors.push(`${prefix}.kind must be one of: ${VALID_TRACE_KINDS.join(', ')}.`);
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return errors;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function validateNewIntakeIntent(intent) {
|
|
140
|
+
const errors = [];
|
|
141
|
+
const prefix = 'idle_expansion_result.new_intake_intent';
|
|
142
|
+
|
|
143
|
+
if (!intent || typeof intent !== 'object' || Array.isArray(intent)) {
|
|
144
|
+
return [`${prefix} must be an object.`];
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
for (const field of ['title', 'charter']) {
|
|
148
|
+
if (typeof intent[field] !== 'string' || !intent[field].trim()) {
|
|
149
|
+
errors.push(`${prefix}.${field} must be a non-empty string.`);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!VALID_PRIORITIES.includes(intent.priority)) {
|
|
154
|
+
errors.push(`${prefix}.priority must be one of: ${VALID_PRIORITIES.join(', ')}.`);
|
|
155
|
+
}
|
|
156
|
+
if (!VALID_GOVERNED_TEMPLATE_IDS.includes(intent.template)) {
|
|
157
|
+
errors.push(`${prefix}.template must be one of: ${VALID_GOVERNED_TEMPLATE_IDS.join(', ')}.`);
|
|
158
|
+
}
|
|
159
|
+
if (!Array.isArray(intent.acceptance_contract) || intent.acceptance_contract.length === 0) {
|
|
160
|
+
errors.push(`${prefix}.acceptance_contract must be a non-empty array.`);
|
|
161
|
+
} else {
|
|
162
|
+
for (let i = 0; i < intent.acceptance_contract.length; i++) {
|
|
163
|
+
if (typeof intent.acceptance_contract[i] !== 'string' || !intent.acceptance_contract[i].trim()) {
|
|
164
|
+
errors.push(`${prefix}.acceptance_contract[${i}] must be a non-empty string.`);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
return errors;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
function validateVisionExhausted(visionExhausted, visionHeadings) {
|
|
173
|
+
const errors = [];
|
|
174
|
+
const prefix = 'idle_expansion_result.vision_exhausted';
|
|
175
|
+
|
|
176
|
+
if (!visionExhausted || typeof visionExhausted !== 'object' || Array.isArray(visionExhausted)) {
|
|
177
|
+
return [`${prefix} must be an object.`];
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const classification = visionExhausted.classification;
|
|
181
|
+
if (!Array.isArray(classification) || classification.length === 0) {
|
|
182
|
+
errors.push(`${prefix}.classification must be a non-empty array.`);
|
|
183
|
+
return errors;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const classifiedHeadings = new Set();
|
|
187
|
+
for (let i = 0; i < classification.length; i++) {
|
|
188
|
+
const entry = classification[i];
|
|
189
|
+
const entryPrefix = `${prefix}.classification[${i}]`;
|
|
190
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) {
|
|
191
|
+
errors.push(`${entryPrefix} must be an object.`);
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const heading = typeof entry.vision_heading === 'string' ? entry.vision_heading.trim() : '';
|
|
196
|
+
if (!heading) {
|
|
197
|
+
errors.push(`${entryPrefix}.vision_heading must be a non-empty string.`);
|
|
198
|
+
} else {
|
|
199
|
+
classifiedHeadings.add(heading);
|
|
200
|
+
if (visionHeadings.length > 0 && !visionHeadings.includes(heading)) {
|
|
201
|
+
errors.push(`${entryPrefix}.vision_heading "${heading}" is not present in the session VISION.md heading snapshot.`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
if (!VALID_EXHAUSTION_STATUSES.includes(entry.status)) {
|
|
205
|
+
errors.push(`${entryPrefix}.status must be one of: ${VALID_EXHAUSTION_STATUSES.join(', ')}.`);
|
|
206
|
+
}
|
|
207
|
+
if (typeof entry.reason !== 'string' || !entry.reason.trim()) {
|
|
208
|
+
errors.push(`${entryPrefix}.reason must be a non-empty string.`);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
for (const heading of visionHeadings) {
|
|
213
|
+
if (!classifiedHeadings.has(heading)) {
|
|
214
|
+
errors.push(`${prefix}.classification must classify VISION.md heading "${heading}".`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return errors;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function normalizeVisionHeadingSnapshot(snapshot) {
|
|
222
|
+
if (!Array.isArray(snapshot)) {
|
|
223
|
+
return [];
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
const headings = [];
|
|
227
|
+
for (const item of snapshot) {
|
|
228
|
+
const heading = typeof item === 'string'
|
|
229
|
+
? item.trim()
|
|
230
|
+
: typeof item?.heading === 'string'
|
|
231
|
+
? item.heading.trim()
|
|
232
|
+
: typeof item?.title === 'string'
|
|
233
|
+
? item.title.trim()
|
|
234
|
+
: '';
|
|
235
|
+
if (heading && !headings.includes(heading)) {
|
|
236
|
+
headings.push(heading);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
return headings;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function truncate(value, maxLength) {
|
|
243
|
+
if (typeof value !== 'string') {
|
|
244
|
+
return '';
|
|
245
|
+
}
|
|
246
|
+
const trimmed = value.trim();
|
|
247
|
+
if (trimmed.length <= maxLength) {
|
|
248
|
+
return trimmed;
|
|
249
|
+
}
|
|
250
|
+
return `${trimmed.slice(0, maxLength - 3)}...`;
|
|
251
|
+
}
|
package/src/lib/intake.js
CHANGED
|
@@ -29,10 +29,11 @@ import {
|
|
|
29
29
|
getPhaseOrder,
|
|
30
30
|
} from './intent-phase-scope.js';
|
|
31
31
|
|
|
32
|
-
const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan'];
|
|
32
|
+
const VALID_SOURCES = ['manual', 'ci_failure', 'git_ref_change', 'schedule', 'vision_scan', 'vision_idle_expansion'];
|
|
33
33
|
const VALID_PRIORITIES = ['p0', 'p1', 'p2', 'p3'];
|
|
34
34
|
const EVENT_ID_RE = /^evt_\d+_[0-9a-f]{4}$/;
|
|
35
35
|
const INTENT_ID_RE = /^intent_\d+_[0-9a-f]{4}$/;
|
|
36
|
+
const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
|
|
36
37
|
|
|
37
38
|
// V3-S1 through S5 states. `failed` remains read-tolerant for historical/manual
|
|
38
39
|
// intent files, but current first-party intake writers do not transition into it.
|
|
@@ -66,6 +67,17 @@ function computeDedupKey(source, signal) {
|
|
|
66
67
|
return `${source}:${hash}`;
|
|
67
68
|
}
|
|
68
69
|
|
|
70
|
+
export function buildVisionIdleExpansionSignal(sessionId, expansionIteration, acceptedTurnId) {
|
|
71
|
+
const expansionKey = createHash('sha256')
|
|
72
|
+
.update(`${sessionId}::${expansionIteration}::${acceptedTurnId}`)
|
|
73
|
+
.digest('hex');
|
|
74
|
+
return {
|
|
75
|
+
expansion_key: expansionKey,
|
|
76
|
+
expansion_iteration: expansionIteration,
|
|
77
|
+
accepted_turn_id: acceptedTurnId,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
69
81
|
function nowISO() {
|
|
70
82
|
return new Date().toISOString();
|
|
71
83
|
}
|
|
@@ -268,6 +280,13 @@ export function validateEventPayload(payload) {
|
|
|
268
280
|
errors.push('signal must be a non-empty object');
|
|
269
281
|
}
|
|
270
282
|
|
|
283
|
+
if (payload.source === 'vision_idle_expansion' && payload.signal && typeof payload.signal === 'object' && !Array.isArray(payload.signal)) {
|
|
284
|
+
errors.push(...validateVisionIdleExpansionSignal(payload.signal));
|
|
285
|
+
errors.push(...validateVisionIdleExpansionContext(payload.idle_expansion_context));
|
|
286
|
+
} else if (payload.idle_expansion_context !== undefined) {
|
|
287
|
+
errors.push('idle_expansion_context is only allowed for source "vision_idle_expansion"');
|
|
288
|
+
}
|
|
289
|
+
|
|
271
290
|
if (!Array.isArray(payload.evidence) || payload.evidence.length === 0) {
|
|
272
291
|
errors.push('evidence must be a non-empty array');
|
|
273
292
|
} else {
|
|
@@ -288,6 +307,57 @@ export function validateEventPayload(payload) {
|
|
|
288
307
|
return { valid: errors.length === 0, errors };
|
|
289
308
|
}
|
|
290
309
|
|
|
310
|
+
function validateVisionIdleExpansionSignal(signal) {
|
|
311
|
+
const errors = [];
|
|
312
|
+
const keys = Object.keys(signal).sort();
|
|
313
|
+
const expected = ['accepted_turn_id', 'expansion_iteration', 'expansion_key'];
|
|
314
|
+
if (JSON.stringify(keys) !== JSON.stringify(expected)) {
|
|
315
|
+
errors.push(`vision_idle_expansion signal must contain exactly: ${expected.join(', ')}`);
|
|
316
|
+
return errors;
|
|
317
|
+
}
|
|
318
|
+
if (typeof signal.expansion_key !== 'string' || !SHA256_HEX_RE.test(signal.expansion_key)) {
|
|
319
|
+
errors.push('vision_idle_expansion signal.expansion_key must be a SHA-256 hex string');
|
|
320
|
+
}
|
|
321
|
+
if (!Number.isInteger(signal.expansion_iteration) || signal.expansion_iteration < 1) {
|
|
322
|
+
errors.push('vision_idle_expansion signal.expansion_iteration must be an integer >= 1');
|
|
323
|
+
}
|
|
324
|
+
if (typeof signal.accepted_turn_id !== 'string' || !signal.accepted_turn_id.trim()) {
|
|
325
|
+
errors.push('vision_idle_expansion signal.accepted_turn_id must be a non-empty string');
|
|
326
|
+
}
|
|
327
|
+
return errors;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function validateVisionIdleExpansionContext(context) {
|
|
331
|
+
const errors = [];
|
|
332
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) {
|
|
333
|
+
return ['idle_expansion_context must be a JSON object for source "vision_idle_expansion"'];
|
|
334
|
+
}
|
|
335
|
+
if (!Number.isInteger(context.expansion_iteration) || context.expansion_iteration < 1) {
|
|
336
|
+
errors.push('idle_expansion_context.expansion_iteration must be an integer >= 1');
|
|
337
|
+
}
|
|
338
|
+
if (!Array.isArray(context.vision_headings_snapshot) || context.vision_headings_snapshot.length === 0) {
|
|
339
|
+
errors.push('idle_expansion_context.vision_headings_snapshot must be a non-empty array');
|
|
340
|
+
} else {
|
|
341
|
+
for (let i = 0; i < context.vision_headings_snapshot.length; i++) {
|
|
342
|
+
const heading = context.vision_headings_snapshot[i];
|
|
343
|
+
if (typeof heading !== 'string' || !heading.trim()) {
|
|
344
|
+
errors.push(`idle_expansion_context.vision_headings_snapshot[${i}] must be a non-empty string`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
return errors;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function normalizeVisionIdleExpansionContext(context) {
|
|
352
|
+
if (!context || typeof context !== 'object' || Array.isArray(context)) return null;
|
|
353
|
+
return {
|
|
354
|
+
expansion_iteration: context.expansion_iteration,
|
|
355
|
+
vision_headings_snapshot: Array.isArray(context.vision_headings_snapshot)
|
|
356
|
+
? context.vision_headings_snapshot.map((heading) => String(heading).trim()).filter(Boolean)
|
|
357
|
+
: [],
|
|
358
|
+
};
|
|
359
|
+
}
|
|
360
|
+
|
|
291
361
|
export function validateTriageFields(fields, config = null) {
|
|
292
362
|
const errors = [];
|
|
293
363
|
|
|
@@ -357,6 +427,12 @@ export function recordEvent(root, payload) {
|
|
|
357
427
|
evidence: payload.evidence,
|
|
358
428
|
dedup_key: dedupKey,
|
|
359
429
|
};
|
|
430
|
+
const idleExpansionContext = payload.source === 'vision_idle_expansion'
|
|
431
|
+
? normalizeVisionIdleExpansionContext(payload.idle_expansion_context)
|
|
432
|
+
: null;
|
|
433
|
+
if (idleExpansionContext) {
|
|
434
|
+
event.idle_expansion_context = idleExpansionContext;
|
|
435
|
+
}
|
|
360
436
|
|
|
361
437
|
safeWriteJson(join(dirs.events, `${eventId}.json`), event);
|
|
362
438
|
|
|
@@ -380,6 +456,9 @@ export function recordEvent(root, payload) {
|
|
|
380
456
|
{ from: null, to: 'detected', at: now, reason: 'event ingested' },
|
|
381
457
|
],
|
|
382
458
|
};
|
|
459
|
+
if (idleExpansionContext) {
|
|
460
|
+
intent.idle_expansion_context = idleExpansionContext;
|
|
461
|
+
}
|
|
383
462
|
|
|
384
463
|
safeWriteJson(join(dirs.intents, `${intentId}.json`), intent);
|
|
385
464
|
|
|
@@ -968,6 +1047,9 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
968
1047
|
return loadedEvent;
|
|
969
1048
|
}
|
|
970
1049
|
const { event } = loadedEvent;
|
|
1050
|
+
const idleExpansionContext = event.source === 'vision_idle_expansion'
|
|
1051
|
+
? normalizeVisionIdleExpansionContext(intent.idle_expansion_context || event.idle_expansion_context)
|
|
1052
|
+
: null;
|
|
971
1053
|
const intakeContext = {
|
|
972
1054
|
intent_id: intent.intent_id,
|
|
973
1055
|
event_id: intent.event_id,
|
|
@@ -976,6 +1058,7 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
976
1058
|
charter: intent.charter || null,
|
|
977
1059
|
acceptance_contract: Array.isArray(intent.acceptance_contract) ? intent.acceptance_contract : [],
|
|
978
1060
|
phase_scope: intent.phase_scope || null,
|
|
1061
|
+
...(idleExpansionContext ? { idle_expansion: idleExpansionContext } : {}),
|
|
979
1062
|
};
|
|
980
1063
|
|
|
981
1064
|
// Load governed project context
|
|
@@ -1072,6 +1155,7 @@ export function startIntent(root, intentId, options = {}) {
|
|
|
1072
1155
|
// Assign governed turn
|
|
1073
1156
|
const assignResult = assignGovernedTurn(root, config, roleId.role, {
|
|
1074
1157
|
intakeContext,
|
|
1158
|
+
...(idleExpansionContext ? { idleExpansionContext } : {}),
|
|
1075
1159
|
});
|
|
1076
1160
|
if (!assignResult.ok) {
|
|
1077
1161
|
return { ok: false, error: `turn assignment failed: ${assignResult.error}`, exitCode: 1 };
|
|
@@ -647,6 +647,7 @@ export function validateRunLoopConfig(runLoop) {
|
|
|
647
647
|
}
|
|
648
648
|
|
|
649
649
|
export const VALID_RECONCILE_OPERATOR_COMMITS = ['manual', 'auto_safe_only', 'disabled'];
|
|
650
|
+
export { VALID_ON_IDLE, RESERVED_ON_IDLE };
|
|
650
651
|
|
|
651
652
|
function validateRunLoopContinuousConfig(path, continuous, errors) {
|
|
652
653
|
if (typeof continuous !== 'object' || Array.isArray(continuous)) {
|
|
@@ -669,6 +670,32 @@ function validateRunLoopContinuousConfig(path, continuous, errors) {
|
|
|
669
670
|
);
|
|
670
671
|
}
|
|
671
672
|
}
|
|
673
|
+
// BUG-60: validate on_idle policy
|
|
674
|
+
if (continuous.on_idle !== undefined && continuous.on_idle !== null) {
|
|
675
|
+
if (typeof continuous.on_idle !== 'string') {
|
|
676
|
+
errors.push(`${path}.on_idle must be one of: ${VALID_ON_IDLE.join(', ')}`);
|
|
677
|
+
} else if (!VALID_ON_IDLE.includes(continuous.on_idle)) {
|
|
678
|
+
errors.push(`${path}.on_idle must be one of: ${VALID_ON_IDLE.join(', ')}`);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
// BUG-60: validate idle_expansion block when present
|
|
682
|
+
if (continuous.idle_expansion !== undefined && continuous.idle_expansion !== null) {
|
|
683
|
+
if (typeof continuous.idle_expansion !== 'object' || Array.isArray(continuous.idle_expansion)) {
|
|
684
|
+
errors.push(`${path}.idle_expansion must be an object`);
|
|
685
|
+
} else {
|
|
686
|
+
if (continuous.idle_expansion.max_expansions !== undefined) {
|
|
687
|
+
validatePositiveInteger(`${path}.idle_expansion.max_expansions`, continuous.idle_expansion.max_expansions, 'expansion count', errors);
|
|
688
|
+
}
|
|
689
|
+
if (continuous.idle_expansion.malformed_retry_limit !== undefined
|
|
690
|
+
&& continuous.idle_expansion.malformed_retry_limit !== null) {
|
|
691
|
+
if (typeof continuous.idle_expansion.malformed_retry_limit !== 'number'
|
|
692
|
+
|| !Number.isInteger(continuous.idle_expansion.malformed_retry_limit)
|
|
693
|
+
|| continuous.idle_expansion.malformed_retry_limit < 0) {
|
|
694
|
+
errors.push(`${path}.idle_expansion.malformed_retry_limit must be a non-negative integer`);
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
}
|
|
672
699
|
}
|
|
673
700
|
|
|
674
701
|
function validateAutoRetryOnGhostConfig(path, value, errors) {
|
|
@@ -1329,9 +1356,43 @@ export function normalizeV4(raw) {
|
|
|
1329
1356
|
};
|
|
1330
1357
|
}
|
|
1331
1358
|
|
|
1359
|
+
const VALID_ON_IDLE = ['exit', 'perpetual', 'human_review'];
|
|
1360
|
+
const RESERVED_ON_IDLE = [];
|
|
1361
|
+
|
|
1362
|
+
function normalizeIdleExpansion(raw) {
|
|
1363
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
1364
|
+
return {
|
|
1365
|
+
sources: ['.planning/VISION.md', '.planning/ROADMAP.md', '.planning/SYSTEM_SPEC.md'],
|
|
1366
|
+
max_expansions: 5,
|
|
1367
|
+
role: 'pm',
|
|
1368
|
+
output: 'intake_intent_or_vision_exhausted',
|
|
1369
|
+
malformed_retry_limit: 1,
|
|
1370
|
+
};
|
|
1371
|
+
}
|
|
1372
|
+
const sources = Array.isArray(raw.sources) && raw.sources.length > 0
|
|
1373
|
+
? raw.sources.filter(s => typeof s === 'string' && s.length > 0)
|
|
1374
|
+
: ['.planning/VISION.md', '.planning/ROADMAP.md', '.planning/SYSTEM_SPEC.md'];
|
|
1375
|
+
return {
|
|
1376
|
+
sources: sources.length > 0 ? sources : ['.planning/VISION.md'],
|
|
1377
|
+
max_expansions: Number.isInteger(raw.max_expansions) && raw.max_expansions >= 1 ? raw.max_expansions : 5,
|
|
1378
|
+
role: typeof raw.role === 'string' && raw.role.length > 0 ? raw.role : 'pm',
|
|
1379
|
+
output: 'intake_intent_or_vision_exhausted',
|
|
1380
|
+
malformed_retry_limit: Number.isInteger(raw.malformed_retry_limit) && raw.malformed_retry_limit >= 0
|
|
1381
|
+
? raw.malformed_retry_limit : 1,
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1332
1385
|
function normalizeContinuousConfig(raw) {
|
|
1333
1386
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return null;
|
|
1334
1387
|
if (raw.enabled !== true) return null;
|
|
1388
|
+
|
|
1389
|
+
// Normalize on_idle — reserved and invalid values fall back to 'exit';
|
|
1390
|
+
// validation layer reports actionable errors to the operator.
|
|
1391
|
+
let on_idle = 'exit';
|
|
1392
|
+
if (typeof raw.on_idle === 'string' && VALID_ON_IDLE.includes(raw.on_idle)) {
|
|
1393
|
+
on_idle = raw.on_idle;
|
|
1394
|
+
}
|
|
1395
|
+
|
|
1335
1396
|
return {
|
|
1336
1397
|
enabled: true,
|
|
1337
1398
|
vision_path: raw.vision_path || '.planning/VISION.md',
|
|
@@ -1341,6 +1402,9 @@ function normalizeContinuousConfig(raw) {
|
|
|
1341
1402
|
per_session_max_usd: Number.isFinite(raw.per_session_max_usd) && raw.per_session_max_usd > 0
|
|
1342
1403
|
? raw.per_session_max_usd
|
|
1343
1404
|
: null,
|
|
1405
|
+
auto_checkpoint: raw.auto_checkpoint === true || raw.auto_checkpoint === false ? raw.auto_checkpoint : undefined,
|
|
1406
|
+
on_idle,
|
|
1407
|
+
idle_expansion: on_idle === 'perpetual' ? normalizeIdleExpansion(raw.idle_expansion) : null,
|
|
1344
1408
|
};
|
|
1345
1409
|
}
|
|
1346
1410
|
|
|
@@ -109,6 +109,37 @@
|
|
|
109
109
|
"type": "object",
|
|
110
110
|
"description": "Continuous-run control knobs.",
|
|
111
111
|
"properties": {
|
|
112
|
+
"on_idle": {
|
|
113
|
+
"type": "string",
|
|
114
|
+
"enum": ["exit", "perpetual", "human_review"],
|
|
115
|
+
"description": "Policy after max_idle_cycles with no derivable work: exit, dispatch PM idle-expansion, or pause for operator review."
|
|
116
|
+
},
|
|
117
|
+
"idle_expansion": {
|
|
118
|
+
"type": "object",
|
|
119
|
+
"description": "PM idle-expansion policy used when on_idle is perpetual.",
|
|
120
|
+
"properties": {
|
|
121
|
+
"sources": {
|
|
122
|
+
"type": "array",
|
|
123
|
+
"items": { "type": "string" },
|
|
124
|
+
"description": "Project-relative source files the PM expansion turn should inspect. Defaults to VISION, ROADMAP, and SYSTEM_SPEC."
|
|
125
|
+
},
|
|
126
|
+
"max_expansions": {
|
|
127
|
+
"type": "integer",
|
|
128
|
+
"minimum": 1,
|
|
129
|
+
"description": "Maximum consecutive PM idle-expansion attempts before stopping with vision_expansion_exhausted. Default 5."
|
|
130
|
+
},
|
|
131
|
+
"role": {
|
|
132
|
+
"type": "string",
|
|
133
|
+
"description": "Role used for the PM idle-expansion turn. Default pm."
|
|
134
|
+
},
|
|
135
|
+
"malformed_retry_limit": {
|
|
136
|
+
"type": "integer",
|
|
137
|
+
"minimum": 0,
|
|
138
|
+
"description": "Reserved retry budget for malformed idle_expansion_result outputs. Default 1."
|
|
139
|
+
}
|
|
140
|
+
},
|
|
141
|
+
"additionalProperties": true
|
|
142
|
+
},
|
|
112
143
|
"auto_retry_on_ghost": {
|
|
113
144
|
"type": "object",
|
|
114
145
|
"description": "Bounded ghost-turn retry policy for continuous/full-auto sessions.",
|
|
@@ -251,6 +251,73 @@
|
|
|
251
251
|
}
|
|
252
252
|
}
|
|
253
253
|
},
|
|
254
|
+
"idle_expansion_result": {
|
|
255
|
+
"type": "object",
|
|
256
|
+
"description": "Required only for vision_idle_expansion turns. PM output that either proposes the next intake intent or declares the product vision exhausted.",
|
|
257
|
+
"required": ["kind", "expansion_iteration", "vision_traceability"],
|
|
258
|
+
"additionalProperties": false,
|
|
259
|
+
"properties": {
|
|
260
|
+
"kind": {
|
|
261
|
+
"enum": ["new_intake_intent", "vision_exhausted"]
|
|
262
|
+
},
|
|
263
|
+
"expansion_iteration": {
|
|
264
|
+
"type": "integer",
|
|
265
|
+
"minimum": 1
|
|
266
|
+
},
|
|
267
|
+
"vision_traceability": {
|
|
268
|
+
"type": "array",
|
|
269
|
+
"items": {
|
|
270
|
+
"type": "object",
|
|
271
|
+
"required": ["vision_heading"],
|
|
272
|
+
"additionalProperties": false,
|
|
273
|
+
"properties": {
|
|
274
|
+
"vision_heading": { "type": "string", "minLength": 1 },
|
|
275
|
+
"goal": { "type": "string", "minLength": 1 },
|
|
276
|
+
"kind": { "enum": ["advances", "supports", "unblocks"] }
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
},
|
|
280
|
+
"new_intake_intent": {
|
|
281
|
+
"type": "object",
|
|
282
|
+
"required": ["title", "charter", "acceptance_contract", "priority", "template"],
|
|
283
|
+
"additionalProperties": false,
|
|
284
|
+
"properties": {
|
|
285
|
+
"title": { "type": "string", "minLength": 1 },
|
|
286
|
+
"charter": { "type": "string", "minLength": 1 },
|
|
287
|
+
"acceptance_contract": {
|
|
288
|
+
"type": "array",
|
|
289
|
+
"minItems": 1,
|
|
290
|
+
"items": { "type": "string", "minLength": 1 }
|
|
291
|
+
},
|
|
292
|
+
"priority": { "enum": ["p0", "p1", "p2", "p3"] },
|
|
293
|
+
"template": {
|
|
294
|
+
"enum": ["generic", "api-service", "cli-tool", "library", "web-app", "full-local-cli", "enterprise-app"]
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
},
|
|
298
|
+
"vision_exhausted": {
|
|
299
|
+
"type": "object",
|
|
300
|
+
"required": ["classification"],
|
|
301
|
+
"additionalProperties": false,
|
|
302
|
+
"properties": {
|
|
303
|
+
"classification": {
|
|
304
|
+
"type": "array",
|
|
305
|
+
"minItems": 1,
|
|
306
|
+
"items": {
|
|
307
|
+
"type": "object",
|
|
308
|
+
"required": ["vision_heading", "status", "reason"],
|
|
309
|
+
"additionalProperties": false,
|
|
310
|
+
"properties": {
|
|
311
|
+
"vision_heading": { "type": "string", "minLength": 1 },
|
|
312
|
+
"status": { "enum": ["complete", "deferred", "out_of_scope"] },
|
|
313
|
+
"reason": { "type": "string", "minLength": 1 }
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
},
|
|
254
321
|
"cost": {
|
|
255
322
|
"type": "object",
|
|
256
323
|
"properties": {
|