agentxchain 2.154.10 → 2.155.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 +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 +29 -12
- package/src/lib/continuous-run.js +534 -14
- package/src/lib/governed-state.js +59 -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
|
@@ -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": {
|
|
@@ -16,6 +16,7 @@ import { existsSync, readFileSync } from 'fs';
|
|
|
16
16
|
import { join } from 'path';
|
|
17
17
|
import { getActiveTurn } from './governed-state.js';
|
|
18
18
|
import { getInvalidPhaseTransitionReason } from './gate-evaluator.js';
|
|
19
|
+
import { validateIdleExpansionTurnResult } from './idle-expansion-result-validator.js';
|
|
19
20
|
|
|
20
21
|
// ── Constants ────────────────────────────────────────────────────────────────
|
|
21
22
|
|
|
@@ -99,6 +100,12 @@ export function validateStagedTurnResult(root, state, config, opts = {}) {
|
|
|
99
100
|
return result('schema', 'schema_error', schemaErrors);
|
|
100
101
|
}
|
|
101
102
|
|
|
103
|
+
const activeTurn = getActiveTurn(state) || state?.current_turn || null;
|
|
104
|
+
const idleExpansionResult = validateIdleExpansionTurnResult(turnResult, buildIdleExpansionValidationContext(state, opts, activeTurn));
|
|
105
|
+
if (idleExpansionResult.errors.length > 0) {
|
|
106
|
+
return result('schema', 'schema_error', idleExpansionResult.errors, idleExpansionResult.warnings);
|
|
107
|
+
}
|
|
108
|
+
|
|
102
109
|
// ── Stage B: Assignment Validation ─────────────────────────────────────
|
|
103
110
|
const assignmentErrors = validateAssignment(turnResult, state);
|
|
104
111
|
if (assignmentErrors.length > 0) {
|
|
@@ -443,6 +450,24 @@ function validateDelegation(del, index) {
|
|
|
443
450
|
return errors;
|
|
444
451
|
}
|
|
445
452
|
|
|
453
|
+
function buildIdleExpansionValidationContext(state, opts, activeTurn) {
|
|
454
|
+
const source = activeTurn?.intake_context?.source || null;
|
|
455
|
+
const context = activeTurn?.idle_expansion_context || activeTurn?.intake_context?.idle_expansion || {};
|
|
456
|
+
return {
|
|
457
|
+
required: source === 'vision_idle_expansion',
|
|
458
|
+
expansionIteration: opts.idleExpansionIteration
|
|
459
|
+
?? context.expansion_iteration
|
|
460
|
+
?? state?.idle_expansion?.current_iteration
|
|
461
|
+
?? state?.continuous?.expansion_iteration
|
|
462
|
+
?? null,
|
|
463
|
+
visionHeadingsSnapshot: opts.visionHeadingsSnapshot
|
|
464
|
+
?? context.vision_headings_snapshot
|
|
465
|
+
?? state?.vision_headings_snapshot
|
|
466
|
+
?? state?.continuous?.vision_headings_snapshot
|
|
467
|
+
?? [],
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
|
|
446
471
|
// ── Stage B: Assignment Validation ───────────────────────────────────────────
|
|
447
472
|
|
|
448
473
|
function validateAssignment(tr, state) {
|
package/src/lib/vision-reader.js
CHANGED
|
@@ -11,8 +11,9 @@
|
|
|
11
11
|
* Spec: .planning/VISION_DRIVEN_CONTINUOUS_SPEC.md
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
|
-
import { existsSync, readFileSync, readdirSync } from 'node:fs';
|
|
14
|
+
import { existsSync, readFileSync, readdirSync, statSync } from 'node:fs';
|
|
15
15
|
import { join, resolve as pathResolve, isAbsolute } from 'node:path';
|
|
16
|
+
import { createHash } from 'node:crypto';
|
|
16
17
|
|
|
17
18
|
// ---------------------------------------------------------------------------
|
|
18
19
|
// Parsing
|
|
@@ -216,6 +217,169 @@ export function deriveVisionCandidates(root, visionPath) {
|
|
|
216
217
|
return { ok: true, candidates };
|
|
217
218
|
}
|
|
218
219
|
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Vision snapshot capture (BUG-60 Slice 3)
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
const MAX_PREVIEW_PER_SOURCE_BYTES = 16 * 1024;
|
|
225
|
+
const MAX_PREVIEW_TOTAL_BYTES = 48 * 1024;
|
|
226
|
+
const MAX_SOURCE_FILE_BYTES = 64 * 1024;
|
|
227
|
+
|
|
228
|
+
/**
|
|
229
|
+
* Capture a heading snapshot from parsed VISION.md content.
|
|
230
|
+
* Returns an array of unique heading strings (H1/H2/H3).
|
|
231
|
+
*
|
|
232
|
+
* @param {string} content - Raw VISION.md markdown
|
|
233
|
+
* @returns {string[]}
|
|
234
|
+
*/
|
|
235
|
+
export function captureVisionHeadingsSnapshot(content) {
|
|
236
|
+
if (!content || typeof content !== 'string') return [];
|
|
237
|
+
const headings = [];
|
|
238
|
+
for (const line of content.split('\n')) {
|
|
239
|
+
const match = line.match(/^(#{1,3})\s+(.+)$/);
|
|
240
|
+
if (match) {
|
|
241
|
+
const heading = match[2].trim();
|
|
242
|
+
if (heading && !headings.includes(heading)) {
|
|
243
|
+
headings.push(heading);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return headings;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/**
|
|
251
|
+
* Compute a SHA-256 content hash for VISION.md content.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} content - Raw file content
|
|
254
|
+
* @returns {string} Hex-encoded SHA-256
|
|
255
|
+
*/
|
|
256
|
+
export function computeVisionContentSha(content) {
|
|
257
|
+
if (!content || typeof content !== 'string') return '';
|
|
258
|
+
return createHash('sha256').update(content, 'utf8').digest('hex');
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Build a bounded source manifest for idle-expansion PM charter context.
|
|
263
|
+
*
|
|
264
|
+
* Per BUG-60 Plan §2: manifest includes path, presence, byte_count, warning,
|
|
265
|
+
* extracted H1/H2 headings, and a bounded preview. Preview truncation is
|
|
266
|
+
* deterministic: at most 16KB per source and 48KB total, using head+tail
|
|
267
|
+
* with `[...truncated middle...]` inserted between halves.
|
|
268
|
+
*
|
|
269
|
+
* VISION.md missing/malformed is a hard error. ROADMAP.md and SYSTEM_SPEC.md
|
|
270
|
+
* missing are warnings. ROADMAP/SYSTEM_SPEC malformed if they cannot be
|
|
271
|
+
* decoded as UTF-8, exceed 64KB, or parse into fewer than one H1/H2 heading.
|
|
272
|
+
*
|
|
273
|
+
* @param {string} root - Project root
|
|
274
|
+
* @param {string[]} sources - Array of project-relative source paths
|
|
275
|
+
* @returns {{ ok: boolean, entries: Array<object>, error?: string }}
|
|
276
|
+
*/
|
|
277
|
+
export function buildSourceManifest(root, sources) {
|
|
278
|
+
if (!Array.isArray(sources) || sources.length === 0) {
|
|
279
|
+
return { ok: false, entries: [], error: 'No sources configured for idle expansion.' };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
const entries = [];
|
|
283
|
+
let totalPreviewBytes = 0;
|
|
284
|
+
|
|
285
|
+
for (const sourcePath of sources) {
|
|
286
|
+
const absPath = isAbsolute(sourcePath) ? sourcePath : pathResolve(root, sourcePath);
|
|
287
|
+
const isVision = sourcePath.toLowerCase().includes('vision');
|
|
288
|
+
const entry = { path: sourcePath, present: false, byte_count: 0, warning: null, headings: [], preview: null };
|
|
289
|
+
|
|
290
|
+
if (!existsSync(absPath)) {
|
|
291
|
+
entry.warning = 'file_not_found';
|
|
292
|
+
if (isVision) {
|
|
293
|
+
return { ok: false, entries, error: `VISION.md not found at ${absPath}. Cannot run idle expansion without VISION.md.` };
|
|
294
|
+
}
|
|
295
|
+
entries.push(entry);
|
|
296
|
+
continue;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
let content;
|
|
300
|
+
let byteCount;
|
|
301
|
+
try {
|
|
302
|
+
const stat = statSync(absPath);
|
|
303
|
+
byteCount = stat.size;
|
|
304
|
+
entry.byte_count = byteCount;
|
|
305
|
+
entry.present = true;
|
|
306
|
+
|
|
307
|
+
if (byteCount > MAX_SOURCE_FILE_BYTES && !isVision) {
|
|
308
|
+
entry.warning = 'exceeds_64kb';
|
|
309
|
+
// Still read what we can for preview, but flag it
|
|
310
|
+
content = readFileSync(absPath, 'utf8');
|
|
311
|
+
} else {
|
|
312
|
+
content = readFileSync(absPath, 'utf8');
|
|
313
|
+
}
|
|
314
|
+
} catch (err) {
|
|
315
|
+
entry.warning = 'read_error';
|
|
316
|
+
if (isVision) {
|
|
317
|
+
return { ok: false, entries, error: `Cannot read VISION.md at ${absPath}: ${err.message}` };
|
|
318
|
+
}
|
|
319
|
+
entries.push(entry);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Extract H1/H2 headings
|
|
324
|
+
const headings = [];
|
|
325
|
+
for (const line of content.split('\n')) {
|
|
326
|
+
const match = line.match(/^(#{1,2})\s+(.+)$/);
|
|
327
|
+
if (match) {
|
|
328
|
+
const heading = match[2].trim();
|
|
329
|
+
if (heading && !headings.includes(heading)) {
|
|
330
|
+
headings.push(heading);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
entry.headings = headings;
|
|
335
|
+
|
|
336
|
+
// Malformed check for non-VISION sources
|
|
337
|
+
if (!isVision) {
|
|
338
|
+
if (byteCount > MAX_SOURCE_FILE_BYTES) {
|
|
339
|
+
entry.warning = 'exceeds_64kb';
|
|
340
|
+
} else if (headings.length === 0) {
|
|
341
|
+
entry.warning = 'no_headings';
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// Bounded preview
|
|
346
|
+
const remainingBudget = MAX_PREVIEW_TOTAL_BYTES - totalPreviewBytes;
|
|
347
|
+
const perSourceCap = Math.min(MAX_PREVIEW_PER_SOURCE_BYTES, remainingBudget);
|
|
348
|
+
if (perSourceCap > 0 && content.length > 0) {
|
|
349
|
+
entry.preview = truncatePreview(content, perSourceCap);
|
|
350
|
+
totalPreviewBytes += Buffer.byteLength(entry.preview, 'utf8');
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
entries.push(entry);
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
return { ok: true, entries };
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Deterministic head+tail preview truncation.
|
|
361
|
+
* If content fits within cap, return as-is. Otherwise split into
|
|
362
|
+
* head half + `[...truncated middle...]` + tail half.
|
|
363
|
+
*
|
|
364
|
+
* @param {string} content
|
|
365
|
+
* @param {number} capBytes
|
|
366
|
+
* @returns {string}
|
|
367
|
+
*/
|
|
368
|
+
function truncatePreview(content, capBytes) {
|
|
369
|
+
const contentBytes = Buffer.byteLength(content, 'utf8');
|
|
370
|
+
if (contentBytes <= capBytes) return content;
|
|
371
|
+
|
|
372
|
+
const marker = '\n[...truncated middle...]\n';
|
|
373
|
+
const markerBytes = Buffer.byteLength(marker, 'utf8');
|
|
374
|
+
const usable = capBytes - markerBytes;
|
|
375
|
+
if (usable <= 0) return content.slice(0, 100) + marker;
|
|
376
|
+
|
|
377
|
+
const halfChars = Math.floor(usable / 2);
|
|
378
|
+
const head = content.slice(0, halfChars);
|
|
379
|
+
const tail = content.slice(-halfChars);
|
|
380
|
+
return head + marker + tail;
|
|
381
|
+
}
|
|
382
|
+
|
|
219
383
|
/**
|
|
220
384
|
* Resolve a vision path relative to the project root.
|
|
221
385
|
*
|