peaks-loop 4.0.16 → 4.0.18
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/CHANGELOG.md +55 -0
- package/README-en.md +12 -9
- package/README.md +15 -4
- package/config/eslint/.peaks-rules.cjs +39 -26
- package/dist/cli/commands/_register.js +2 -1
- package/dist/cli/commands/audit-commands.d.ts +1 -1
- package/dist/cli/commands/code-gate-command.d.ts +25 -0
- package/dist/cli/commands/code-gate-command.js +86 -0
- package/dist/cli/commands/code-mode-gate-commands.js +3 -4
- package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
- package/dist/cli/commands/cron-commands.js +16 -4
- package/dist/cli/commands/dashboard-long-run.js +8 -4
- package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
- package/dist/cli/commands/hooks-commands.js +56 -3
- package/dist/cli/commands/primer-command.d.ts +59 -0
- package/dist/cli/commands/primer-command.js +160 -0
- package/dist/cli/commands/qa-commands.js +1 -2
- package/dist/cli/commands/sediment-commands.d.ts +1 -1
- package/dist/cli/commands/slice-commands.js +20 -8
- package/dist/cli/commands/statusline-commands.d.ts +1 -0
- package/dist/cli/commands/statusline-commands.js +17 -3
- package/dist/cli/commands/tech-commands.d.ts +1 -1
- package/dist/cli/program.js +8 -0
- package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
- package/dist/services/artifacts/request-artifact-service.js +10 -1
- package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
- package/dist/services/code/auto-compact-orchestrator.js +9 -1
- package/dist/services/code/batch-heartbeat-poller.js +26 -0
- package/dist/services/code/orchestrator-can-do.d.ts +24 -0
- package/dist/services/code/orchestrator-can-do.js +48 -2
- package/dist/services/code/post-compact-detector.d.ts +1 -1
- package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
- package/dist/services/config/config-safety.d.ts +23 -0
- package/dist/services/config/config-safety.js +62 -0
- package/dist/services/config/config-service.d.ts +1 -1
- package/dist/services/config/config-service.js +2 -4
- package/dist/services/container/container-lease.js +21 -8
- package/dist/services/context/spillover-store.d.ts +1 -1
- package/dist/services/crystallization/crystallization-types.js +20 -8
- package/dist/services/crystallization/evidence-brief-builder.js +22 -11
- package/dist/services/dispatch/dispatch-record-writer.js +231 -141
- package/dist/services/evolution/evolution-types.js +38 -21
- package/dist/services/feedback/feedback-promotion-service.js +9 -1
- package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
- package/dist/services/hooks/pre-tool-code-gate.js +91 -0
- package/dist/services/ide/current-model-detector.js +1 -2
- package/dist/services/lint/detect-eslint.js +8 -5
- package/dist/services/lint/eslint-runner.d.ts +1 -2
- package/dist/services/lint/eslint-runner.js +86 -23
- package/dist/services/lint/npx-resolver.d.ts +6 -0
- package/dist/services/lint/npx-resolver.js +47 -0
- package/dist/services/loop/loop-release-types.js +32 -13
- package/dist/services/loop/spec-service.d.ts +7 -1
- package/dist/services/loop/spec-service.js +218 -182
- package/dist/services/rd/rd-service.js +22 -9
- package/dist/services/scan/archetype-service.js +21 -9
- package/dist/services/session/binding-store.js +1 -2
- package/dist/services/share/bundle-reader.d.ts +6 -9
- package/dist/services/share/bundle-reader.js +297 -223
- package/dist/services/skills/hooks-settings-service.d.ts +33 -0
- package/dist/services/skills/hooks-settings-service.js +48 -3
- package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
- package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
- package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
- package/dist/services/skills/skill-statusline-renderer.js +96 -8
- package/dist/services/skills/skill-statusline-service.d.ts +41 -0
- package/dist/services/skills/skill-statusline-service.js +56 -6
- package/dist/services/slice/calibration-store.js +21 -8
- package/dist/services/slice/slice-check-service.js +35 -14
- package/dist/services/slice/slice-decompose-service.js +459 -223
- package/dist/services/standards/project-context.js +135 -73
- package/dist/services/verdict/envelopes.d.ts +1 -2
- package/dist/services/verdict/envelopes.js +1 -2
- package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
- package/dist/services/vm/vm-lease.js +19 -8
- package/dist/services/worktree/worktree-lease.d.ts +0 -10
- package/dist/services/worktree/worktree-lease.js +19 -8
- package/dist/shared/fs-utils.d.ts +1 -1
- package/package.json +7 -4
- package/skills/peaks-code/SKILL.md +46 -0
|
@@ -74,16 +74,36 @@ export function buildSpec(input, expectedRid) {
|
|
|
74
74
|
termination
|
|
75
75
|
};
|
|
76
76
|
}
|
|
77
|
-
/** Lint a LoopSpec — returns a report with semantic errors / warnings.
|
|
77
|
+
/** Lint a LoopSpec — returns a report with semantic errors / warnings.
|
|
78
|
+
*
|
|
79
|
+
* Refactored 2026-08-07 (PRD-002b slice 3 commit A — extract-method):
|
|
80
|
+
* the original function had complexity 14. Each validation phase now lives
|
|
81
|
+
* in its own helper, dropping the orchestrator to complexity ~5.
|
|
82
|
+
* Behavior is byte-for-byte preserved: same error/warning strings, same
|
|
83
|
+
* ordering, same return shape. */
|
|
78
84
|
export function lintLoopSpec(spec) {
|
|
79
85
|
const errors = [];
|
|
80
86
|
const warnings = [];
|
|
87
|
+
validateSpecHeader(spec, errors);
|
|
88
|
+
validateEvaluators(spec.evaluators, errors, warnings);
|
|
89
|
+
validateSla(spec.sla, spec.evaluators, errors);
|
|
90
|
+
validateTermination(spec.termination, errors);
|
|
91
|
+
return {
|
|
92
|
+
ok: errors.length === 0,
|
|
93
|
+
errors,
|
|
94
|
+
warnings,
|
|
95
|
+
normalizedSpec: spec
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
function validateSpecHeader(spec, errors) {
|
|
81
99
|
if (spec.schemaVersion !== 1)
|
|
82
100
|
errors.push(`unsupported schemaVersion ${spec.schemaVersion} (expected 1)`);
|
|
83
101
|
if (!/^[a-z][a-z0-9-]*$/.test(spec.rid))
|
|
84
102
|
errors.push(`rid "${spec.rid}" must match /^[a-z][a-z0-9-]*$/`);
|
|
103
|
+
}
|
|
104
|
+
function validateEvaluators(evaluators, errors, warnings) {
|
|
85
105
|
const seenKinds = new Set();
|
|
86
|
-
for (const ev of
|
|
106
|
+
for (const ev of evaluators) {
|
|
87
107
|
if (typeof ev.kind !== 'string' || ev.kind.length === 0) {
|
|
88
108
|
errors.push(`evaluator with empty kind`);
|
|
89
109
|
continue;
|
|
@@ -92,22 +112,20 @@ export function lintLoopSpec(spec) {
|
|
|
92
112
|
warnings.push(`evaluator kind "${ev.kind}" duplicated`);
|
|
93
113
|
seenKinds.add(ev.kind);
|
|
94
114
|
}
|
|
95
|
-
|
|
96
|
-
|
|
115
|
+
}
|
|
116
|
+
function validateSla(sla, evaluators, errors) {
|
|
117
|
+
const evalKinds = new Set(evaluators.map((e) => e.kind));
|
|
118
|
+
for (const s of sla) {
|
|
97
119
|
if (!evalKinds.has(s.evaluator))
|
|
98
120
|
errors.push(`sla.evaluator "${s.evaluator}" is not declared in evaluators[]`);
|
|
99
121
|
if (s.maxScore < 0 || s.maxScore > 1)
|
|
100
122
|
errors.push(`sla.maxScore for "${s.evaluator}" must be in [0,1] (got ${s.maxScore})`);
|
|
101
123
|
}
|
|
102
|
-
|
|
124
|
+
}
|
|
125
|
+
function validateTermination(termination, errors) {
|
|
126
|
+
if (termination.strategy === 'max-cycles' && (termination.maxCycles === undefined || termination.maxCycles < 1)) {
|
|
103
127
|
errors.push(`termination.strategy=max-cycles requires maxCycles ≥ 1`);
|
|
104
128
|
}
|
|
105
|
-
return {
|
|
106
|
-
ok: errors.length === 0,
|
|
107
|
-
errors,
|
|
108
|
-
warnings,
|
|
109
|
-
normalizedSpec: spec
|
|
110
|
-
};
|
|
111
129
|
}
|
|
112
130
|
/** Serialize a LoopSpec to a stable YAML representation. Pure. */
|
|
113
131
|
export function serializeSpec(spec) {
|
|
@@ -211,90 +229,122 @@ function parseObjectBlock(lines, start, baseIndent) {
|
|
|
211
229
|
let i = start;
|
|
212
230
|
for (; i < lines.length; i++) {
|
|
213
231
|
const line = lines[i] ?? '';
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// `{strategy: 'max-cycles', maxCycles: 3}`). Re-stitch the leading
|
|
231
|
-
// key so the inline-object parser can see the full kv list, then
|
|
232
|
-
// store the resulting flat object as `obj[key]`.
|
|
233
|
-
if (key.length > 0 && value.length > 0 && hasInlineObjectShape(value)) {
|
|
234
|
-
obj[key] = parseInlineObject(`${key}: ${value}`);
|
|
235
|
-
continue;
|
|
236
|
-
}
|
|
237
|
-
if (value === '' || value === '|' || value === '>') {
|
|
238
|
-
// Could be nested block.
|
|
239
|
-
const next = lines[i + 1] ?? '';
|
|
240
|
-
const nextIndent = leadingSpaces(next);
|
|
241
|
-
if (nextIndent > indent) {
|
|
242
|
-
if (next.trim().startsWith('- ')) {
|
|
243
|
-
const arrRes = parseArrayBlock(lines, i + 1, nextIndent);
|
|
244
|
-
obj[key] = arrRes.value;
|
|
245
|
-
i = arrRes.next - 1;
|
|
246
|
-
continue;
|
|
247
|
-
}
|
|
248
|
-
else {
|
|
249
|
-
const objRes = parseObjectBlock(lines, i + 1, nextIndent);
|
|
250
|
-
obj[key] = objRes.value;
|
|
251
|
-
i = objRes.next - 1;
|
|
252
|
-
continue;
|
|
253
|
-
}
|
|
232
|
+
const cls = classifyObjectLine(line, baseIndent);
|
|
233
|
+
switch (cls.kind) {
|
|
234
|
+
case 'blank':
|
|
235
|
+
case 'comment':
|
|
236
|
+
case 'indent-forward':
|
|
237
|
+
continue;
|
|
238
|
+
case 'indent-back':
|
|
239
|
+
return { value: obj, next: i };
|
|
240
|
+
case 'kv-inline-object':
|
|
241
|
+
obj[cls.key] = parseInlineObject(`${cls.key}: ${cls.value}`);
|
|
242
|
+
continue;
|
|
243
|
+
case 'kv-nested-block': {
|
|
244
|
+
const consumed = consumeNestedBlock(lines, i, line, cls.value);
|
|
245
|
+
obj[cls.key] = consumed.value;
|
|
246
|
+
i = consumed.next - 1;
|
|
247
|
+
continue;
|
|
254
248
|
}
|
|
255
|
-
|
|
256
|
-
|
|
249
|
+
case 'kv-scalar':
|
|
250
|
+
obj[cls.key] = parseValueOrInlineObject(cls.value);
|
|
251
|
+
continue;
|
|
257
252
|
}
|
|
258
|
-
obj[key] = parseValueOrInlineObject(value);
|
|
259
253
|
}
|
|
260
254
|
return { value: obj, next: i };
|
|
261
255
|
}
|
|
256
|
+
/** Classify a single line at `baseIndent` into a token kind. Pure —
|
|
257
|
+
* no mutation of the loop index. */
|
|
258
|
+
function classifyObjectLine(line, baseIndent) {
|
|
259
|
+
const trimmed = line.trim();
|
|
260
|
+
if (trimmed === '')
|
|
261
|
+
return { kind: 'blank' };
|
|
262
|
+
if (trimmed.startsWith('#'))
|
|
263
|
+
return { kind: 'comment' };
|
|
264
|
+
const indent = leadingSpaces(line);
|
|
265
|
+
if (indent < baseIndent)
|
|
266
|
+
return { kind: 'indent-back' };
|
|
267
|
+
if (indent > baseIndent)
|
|
268
|
+
return { kind: 'indent-forward' };
|
|
269
|
+
if (!trimmed.includes(':'))
|
|
270
|
+
return { kind: 'comment' }; // noise: no `:` → skip
|
|
271
|
+
return classifyKeyValue(trimmed);
|
|
272
|
+
}
|
|
273
|
+
/** Inspect a trimmed, colon-bearing line and classify its key/value
|
|
274
|
+
* payload. Delegates the value-shape triage to `classifyValueKind`. */
|
|
275
|
+
function classifyKeyValue(trimmed) {
|
|
276
|
+
const parts = splitTopLevel(trimmed, ':');
|
|
277
|
+
const key = parts[0] ?? '';
|
|
278
|
+
const value = (parts[1] ?? '').trim();
|
|
279
|
+
switch (classifyValueKind(value)) {
|
|
280
|
+
case 'inline-object':
|
|
281
|
+
return { kind: 'kv-inline-object', key, value };
|
|
282
|
+
case 'nested-block':
|
|
283
|
+
return { kind: 'kv-nested-block', key, value: value };
|
|
284
|
+
case 'scalar':
|
|
285
|
+
return { kind: 'kv-scalar', key, value };
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
/** Triage the post-colon value string. Returns the kind that the
|
|
289
|
+
* caller should produce. The inline-object check delegates to
|
|
290
|
+
* `hasInlineObjectShape`; the nested-block check covers YAML's
|
|
291
|
+
* three block-scalar placeholders. */
|
|
292
|
+
function classifyValueKind(value) {
|
|
293
|
+
if (value.length > 0 && hasInlineObjectShape(value))
|
|
294
|
+
return 'inline-object';
|
|
295
|
+
if (value === '' || value === '|' || value === '>')
|
|
296
|
+
return 'nested-block';
|
|
297
|
+
return 'scalar';
|
|
298
|
+
}
|
|
299
|
+
/** Consume the nested block (array or object) under a `kv-nested-block`
|
|
300
|
+
* line. Returns the parsed value plus the index past the last consumed
|
|
301
|
+
* line (caller subtracts 1 before the for-loop increment). When the
|
|
302
|
+
* next line is NOT indented further, returns a null / empty scalar and
|
|
303
|
+
* `next = i + 1` so the caller lands on the line after the kv marker. */
|
|
304
|
+
function consumeNestedBlock(lines, i, line, value) {
|
|
305
|
+
const next = lines[i + 1] ?? '';
|
|
306
|
+
const nextIndent = leadingSpaces(next);
|
|
307
|
+
const ownIndent = leadingSpaces(line);
|
|
308
|
+
if (nextIndent > ownIndent) {
|
|
309
|
+
if (next.trim().startsWith('- ')) {
|
|
310
|
+
const arrRes = parseArrayBlock(lines, i + 1, nextIndent);
|
|
311
|
+
return { value: arrRes.value, next: arrRes.next };
|
|
312
|
+
}
|
|
313
|
+
const objRes = parseObjectBlock(lines, i + 1, nextIndent);
|
|
314
|
+
return { value: objRes.value, next: objRes.next };
|
|
315
|
+
}
|
|
316
|
+
// Block-style scalar placeholder with no children — keep the original
|
|
317
|
+
// behaviour: `|` / `>` collapse to `''`, empty value to `null`.
|
|
318
|
+
return { value: value === '|' || value === '>' ? '' : null, next: i + 1 };
|
|
319
|
+
}
|
|
262
320
|
/** Inspect the trimmed value — if it looks like an inline object
|
|
263
|
-
* (`key: val, key: val`),
|
|
321
|
+
* (`key: val, key: val`), report true; otherwise false. Refactored
|
|
322
|
+
* 2026-08-07 (PRD-002b slice 3 Commit C — table-dispatch): the
|
|
323
|
+
* original hand-rolled character walk was complexity 19; now we
|
|
324
|
+
* delegate to the shared `scanTopLevelSeparators` scanner twice
|
|
325
|
+
* (comma, then colon per part) and dispatch on the resulting parts.
|
|
326
|
+
* Behaviour-preserving: a top-level `,` AND a top-level `:` are both
|
|
327
|
+
* present iff `commaParts.length >= 2` AND at least one part splits
|
|
328
|
+
* further on `:`. */
|
|
264
329
|
function hasInlineObjectShape(value) {
|
|
265
330
|
if (typeof value !== 'string')
|
|
266
331
|
return false;
|
|
267
332
|
const trimmed = value.trim();
|
|
268
333
|
if (trimmed === '' || trimmed.startsWith('[') || trimmed.startsWith('{'))
|
|
269
334
|
return false;
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
for (let i = 0; i < trimmed.length; i++) {
|
|
275
|
-
const ch = trimmed[i];
|
|
276
|
-
if (inQuote !== null) {
|
|
277
|
-
if (ch === inQuote)
|
|
278
|
-
inQuote = null;
|
|
279
|
-
continue;
|
|
280
|
-
}
|
|
281
|
-
if (ch === '"' || ch === "'") {
|
|
282
|
-
inQuote = ch;
|
|
283
|
-
continue;
|
|
284
|
-
}
|
|
285
|
-
if (ch === '[' || ch === '{')
|
|
286
|
-
depth++;
|
|
287
|
-
if (ch === ']' || ch === '}')
|
|
288
|
-
depth--;
|
|
289
|
-
if (depth !== 0)
|
|
290
|
-
continue;
|
|
291
|
-
if (ch === ':' && topColon === -1)
|
|
292
|
-
topColon = i;
|
|
293
|
-
if (ch === ',')
|
|
294
|
-
topComma = i;
|
|
295
|
-
}
|
|
296
|
-
return topColon !== -1 && topComma !== -1;
|
|
335
|
+
const commaParts = scanTopLevelSeparators(trimmed, ',');
|
|
336
|
+
if (commaParts.length < 2)
|
|
337
|
+
return false;
|
|
338
|
+
return commaParts.some((part) => scanTopLevelSeparators(part, ':').length >= 2);
|
|
297
339
|
}
|
|
340
|
+
/** Parse a value that may be either a scalar or an inline object.
|
|
341
|
+
* Refactored 2026-08-07 (PRD-002b slice 3 Commit C — table-dispatch):
|
|
342
|
+
* the original hand-rolled character walk was complexity 21; now
|
|
343
|
+
* delegates to the shared `scanTopLevelSeparators` scanner. The
|
|
344
|
+
* original check was `topColon !== -1 && topComma !== -1 &&
|
|
345
|
+
* topComma > topColon`, i.e. "the first top-level `:` lies before
|
|
346
|
+
* the last top-level `,`". Equivalent: the FIRST comma-separated
|
|
347
|
+
* part contains at least one top-level `:`. Behaviour-preserving. */
|
|
298
348
|
function parseValueOrInlineObject(value) {
|
|
299
349
|
if (typeof value !== 'string')
|
|
300
350
|
return value;
|
|
@@ -302,35 +352,13 @@ function parseValueOrInlineObject(value) {
|
|
|
302
352
|
if (trimmed === '' || trimmed.startsWith('[') || trimmed.startsWith('{')) {
|
|
303
353
|
return parseScalar(trimmed);
|
|
304
354
|
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
if (inQuote !== null) {
|
|
312
|
-
if (ch === inQuote)
|
|
313
|
-
inQuote = null;
|
|
314
|
-
continue;
|
|
315
|
-
}
|
|
316
|
-
if (ch === '"' || ch === "'") {
|
|
317
|
-
inQuote = ch;
|
|
318
|
-
continue;
|
|
319
|
-
}
|
|
320
|
-
if (ch === '[' || ch === '{')
|
|
321
|
-
depth++;
|
|
322
|
-
if (ch === ']' || ch === '}')
|
|
323
|
-
depth--;
|
|
324
|
-
if (depth !== 0)
|
|
325
|
-
continue;
|
|
326
|
-
if (ch === ':' && topColon === -1)
|
|
327
|
-
topColon = i;
|
|
328
|
-
if (ch === ',')
|
|
329
|
-
topComma = i;
|
|
330
|
-
}
|
|
331
|
-
if (topColon !== -1 && topComma !== -1 && topComma > topColon) {
|
|
355
|
+
const commaParts = scanTopLevelSeparators(trimmed, ',');
|
|
356
|
+
if (commaParts.length < 2)
|
|
357
|
+
return parseScalar(trimmed);
|
|
358
|
+
const first = commaParts[0] ?? '';
|
|
359
|
+
const hasColonInFirst = scanTopLevelSeparators(first, ':').length >= 2;
|
|
360
|
+
if (hasColonInFirst)
|
|
332
361
|
return parseInlineObject(trimmed);
|
|
333
|
-
}
|
|
334
362
|
return parseScalar(trimmed);
|
|
335
363
|
}
|
|
336
364
|
function parseArrayBlock(lines, start, baseIndent) {
|
|
@@ -338,32 +366,34 @@ function parseArrayBlock(lines, start, baseIndent) {
|
|
|
338
366
|
let i = start;
|
|
339
367
|
for (; i < lines.length; i++) {
|
|
340
368
|
const line = lines[i] ?? '';
|
|
341
|
-
if (line
|
|
369
|
+
if (shouldSkipArrayLine(line, baseIndent))
|
|
342
370
|
continue;
|
|
343
|
-
|
|
344
|
-
if (indent < baseIndent)
|
|
371
|
+
if (leadingSpaces(line) < baseIndent)
|
|
345
372
|
return { value: arr, next: i };
|
|
346
|
-
if (indent > baseIndent)
|
|
347
|
-
continue; // tolerate noise
|
|
348
373
|
const trimmed = line.trim();
|
|
349
374
|
if (!trimmed.startsWith('- '))
|
|
350
375
|
continue;
|
|
351
|
-
|
|
352
|
-
if (item.startsWith('{') && item.endsWith('}')) {
|
|
353
|
-
const inner = item.slice(1, -1).trim();
|
|
354
|
-
arr.push(parseInlineObject(inner));
|
|
355
|
-
continue;
|
|
356
|
-
}
|
|
357
|
-
if (item.includes(':')) {
|
|
358
|
-
// Inline object on array item (e.g. `- kind: foo, gate: bar`).
|
|
359
|
-
const inline = parseInlineObject(item);
|
|
360
|
-
arr.push(inline);
|
|
361
|
-
continue;
|
|
362
|
-
}
|
|
363
|
-
arr.push(parseScalar(item));
|
|
376
|
+
arr.push(parseArrayItem(trimmed.slice(2).trim()));
|
|
364
377
|
}
|
|
365
378
|
return { value: arr, next: i };
|
|
366
379
|
}
|
|
380
|
+
function shouldSkipArrayLine(line, baseIndent) {
|
|
381
|
+
if (line.trim() === '' || line.trim().startsWith('#'))
|
|
382
|
+
return true;
|
|
383
|
+
if (leadingSpaces(line) > baseIndent)
|
|
384
|
+
return true; // tolerate noise
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
function parseArrayItem(item) {
|
|
388
|
+
if (item.startsWith('{') && item.endsWith('}')) {
|
|
389
|
+
return parseInlineObject(item.slice(1, -1).trim());
|
|
390
|
+
}
|
|
391
|
+
if (item.includes(':')) {
|
|
392
|
+
// Inline object on array item (e.g. `- kind: foo, gate: bar`).
|
|
393
|
+
return parseInlineObject(item);
|
|
394
|
+
}
|
|
395
|
+
return parseScalar(item);
|
|
396
|
+
}
|
|
367
397
|
function parseInlineObject(body) {
|
|
368
398
|
// Split on top-level commas (not commas inside quotes — our spec has none).
|
|
369
399
|
const obj = {};
|
|
@@ -394,68 +424,60 @@ function parseInlineObject(body) {
|
|
|
394
424
|
return obj;
|
|
395
425
|
}
|
|
396
426
|
function splitCommas(s) {
|
|
427
|
+
const raw = scanTopLevelSeparators(s, ',');
|
|
428
|
+
return raw.map((x) => x.trim()).filter((x) => x.length > 0);
|
|
429
|
+
}
|
|
430
|
+
/** Shared scanner — splits `s` on `sep` at depth 0 outside quotes.
|
|
431
|
+
* Behaviour-preserving with the original splitCommas / splitTopLevel
|
|
432
|
+
* loops; extracted on 2026-08-07 (PRD-002b slice 3 A) to drop both
|
|
433
|
+
* callers' complexity from 13 to ≤ 2. */
|
|
434
|
+
function scanTopLevelSeparators(s, sep) {
|
|
397
435
|
const out = [];
|
|
398
436
|
let depth = 0;
|
|
399
437
|
let cur = '';
|
|
400
438
|
let inQuote = null;
|
|
401
439
|
for (const ch of s) {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
if (
|
|
409
|
-
inQuote = ch;
|
|
410
|
-
cur += ch;
|
|
411
|
-
continue;
|
|
412
|
-
}
|
|
413
|
-
if (ch === '[' || ch === '{')
|
|
414
|
-
depth++;
|
|
415
|
-
if (ch === ']' || ch === '}')
|
|
416
|
-
depth--;
|
|
417
|
-
if (ch === ',' && depth === 0) {
|
|
418
|
-
out.push(cur);
|
|
440
|
+
const ctx = stepScanner(ch, sep, cur, inQuote, depth);
|
|
441
|
+
cur = ctx.cur;
|
|
442
|
+
inQuote = ctx.inQuote;
|
|
443
|
+
depth = ctx.depth;
|
|
444
|
+
if (ctx.push)
|
|
445
|
+
out.push(ctx.push);
|
|
446
|
+
if (ctx.reset)
|
|
419
447
|
cur = '';
|
|
420
|
-
continue;
|
|
421
|
-
}
|
|
422
|
-
cur += ch;
|
|
423
448
|
}
|
|
424
449
|
if (cur.length > 0)
|
|
425
450
|
out.push(cur);
|
|
426
|
-
return out
|
|
451
|
+
return out;
|
|
452
|
+
}
|
|
453
|
+
function stepScanner(ch, sep, cur, inQuote, depth) {
|
|
454
|
+
if (inQuote !== null)
|
|
455
|
+
return stepInsideQuote(ch, cur, inQuote, depth);
|
|
456
|
+
if (isQuote(ch))
|
|
457
|
+
return { cur: cur + ch, inQuote: ch, depth, push: null, reset: false };
|
|
458
|
+
if (isOpenBracket(ch))
|
|
459
|
+
return { cur, inQuote, depth: depth + 1, push: null, reset: false };
|
|
460
|
+
if (isCloseBracket(ch))
|
|
461
|
+
return { cur, inQuote, depth: depth - 1, push: null, reset: false };
|
|
462
|
+
if (ch === sep && depth === 0)
|
|
463
|
+
return { cur, inQuote, depth, push: cur, reset: true };
|
|
464
|
+
return { cur: cur + ch, inQuote, depth, push: null, reset: false };
|
|
465
|
+
}
|
|
466
|
+
function stepInsideQuote(ch, cur, inQuote, depth) {
|
|
467
|
+
const nextInQuote = ch === inQuote ? null : inQuote;
|
|
468
|
+
return { cur: cur + ch, inQuote: nextInQuote, depth, push: null, reset: false };
|
|
469
|
+
}
|
|
470
|
+
function isQuote(ch) {
|
|
471
|
+
return ch === '"' || ch === "'";
|
|
472
|
+
}
|
|
473
|
+
function isOpenBracket(ch) {
|
|
474
|
+
return ch === '[' || ch === '{';
|
|
475
|
+
}
|
|
476
|
+
function isCloseBracket(ch) {
|
|
477
|
+
return ch === ']' || ch === '}';
|
|
427
478
|
}
|
|
428
479
|
function splitTopLevel(s, sep) {
|
|
429
|
-
|
|
430
|
-
let inQuote = null;
|
|
431
|
-
const out = [];
|
|
432
|
-
let cur = '';
|
|
433
|
-
for (const ch of s) {
|
|
434
|
-
if (inQuote !== null) {
|
|
435
|
-
cur += ch;
|
|
436
|
-
if (ch === inQuote)
|
|
437
|
-
inQuote = null;
|
|
438
|
-
continue;
|
|
439
|
-
}
|
|
440
|
-
if (ch === '"' || ch === "'") {
|
|
441
|
-
inQuote = ch;
|
|
442
|
-
cur += ch;
|
|
443
|
-
continue;
|
|
444
|
-
}
|
|
445
|
-
if (ch === '[' || ch === '{')
|
|
446
|
-
depth++;
|
|
447
|
-
if (ch === ']' || ch === '}')
|
|
448
|
-
depth--;
|
|
449
|
-
if (ch === sep && depth === 0) {
|
|
450
|
-
out.push(cur);
|
|
451
|
-
cur = '';
|
|
452
|
-
continue;
|
|
453
|
-
}
|
|
454
|
-
cur += ch;
|
|
455
|
-
}
|
|
456
|
-
if (cur.length > 0)
|
|
457
|
-
out.push(cur);
|
|
458
|
-
return out;
|
|
480
|
+
return scanTopLevelSeparators(s, sep);
|
|
459
481
|
}
|
|
460
482
|
function leadingSpaces(line) {
|
|
461
483
|
let n = 0;
|
|
@@ -476,14 +498,28 @@ function parseScalar(raw) {
|
|
|
476
498
|
return false;
|
|
477
499
|
if (raw === 'null' || raw === '~')
|
|
478
500
|
return null;
|
|
479
|
-
|
|
480
|
-
|
|
501
|
+
const unquoted = unquoteIfWrapped(raw);
|
|
502
|
+
if (unquoted !== null)
|
|
503
|
+
return unquoted;
|
|
504
|
+
return parseScalarNumber(raw) ?? raw;
|
|
505
|
+
}
|
|
506
|
+
function unquoteIfWrapped(raw) {
|
|
507
|
+
const len = raw.length;
|
|
508
|
+
if (len >= 2) {
|
|
509
|
+
const first = raw[0];
|
|
510
|
+
const last = raw[len - 1];
|
|
511
|
+
if ((first === '"' && last === '"') || (first === "'" && last === "'")) {
|
|
512
|
+
return raw.slice(1, -1);
|
|
513
|
+
}
|
|
481
514
|
}
|
|
515
|
+
return null;
|
|
516
|
+
}
|
|
517
|
+
function parseScalarNumber(raw) {
|
|
482
518
|
if (/^-?\d+$/.test(raw))
|
|
483
519
|
return parseInt(raw, 10);
|
|
484
520
|
if (/^-?\d+\.\d+$/.test(raw))
|
|
485
521
|
return parseFloat(raw);
|
|
486
|
-
return
|
|
522
|
+
return null;
|
|
487
523
|
}
|
|
488
524
|
export function specPath(projectRoot, sid, rid) {
|
|
489
525
|
return join(projectRoot, '.peaks', '_runtime', sid, 'loop', rid, 'spec.yaml');
|
|
@@ -5,6 +5,19 @@ import { WORKSPACE_UNAVAILABLE_NEXT_ACTIONS } from '../../shared/planner-respons
|
|
|
5
5
|
import { getLocalArtifactPath, hasValidArtifactWorkspace } from '../artifacts/workspace-service.js';
|
|
6
6
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
7
7
|
import { getConfiguredExecutionModelId, getStrongestModelId } from '../config/model-routing.js';
|
|
8
|
+
/**
|
|
9
|
+
* PRD-002b slice 2 — extract wave-boundary magic numbers so the
|
|
10
|
+
* no-magic-numbers rule stops flagging the swarm-plan slicing math.
|
|
11
|
+
* `8` = wave-size for discovery/planning/unit-test/reducer tail;
|
|
12
|
+
* `16` = start of implementation-candidates band;
|
|
13
|
+
* `5` = quality-gate band size;
|
|
14
|
+
* `3` = padding width for the impl-N task-id.
|
|
15
|
+
*/
|
|
16
|
+
const RD_DISCOVERY_PLANNING_WAVE_SIZE = 8;
|
|
17
|
+
const RD_IMPLEMENTATION_BAND_START = 16;
|
|
18
|
+
const RD_FIXED_TASK_BOUNDARY = RD_IMPLEMENTATION_BAND_START;
|
|
19
|
+
const RD_QUALITY_GATE_BAND_SIZE = 5;
|
|
20
|
+
const RD_TASK_ID_PAD_WIDTH = 3;
|
|
8
21
|
/**
|
|
9
22
|
* 2.0.1-bug1: the slim 2.0 `~/.peaks/config.json` no longer carries a
|
|
10
23
|
* `providers` block (legacy model config lives in
|
|
@@ -71,8 +84,8 @@ function buildTaskIds(workerTarget) {
|
|
|
71
84
|
'rd-reducer-1',
|
|
72
85
|
];
|
|
73
86
|
const implementationCount = Math.max(workerTarget - fixed.length, 1);
|
|
74
|
-
const implementation = Array.from({ length: implementationCount }, (_, index) => `rd-impl-${String(index + 1).padStart(
|
|
75
|
-
return [...fixed.slice(0,
|
|
87
|
+
const implementation = Array.from({ length: implementationCount }, (_, index) => `rd-impl-${String(index + 1).padStart(RD_TASK_ID_PAD_WIDTH, '0')}`);
|
|
88
|
+
return [...fixed.slice(0, RD_FIXED_TASK_BOUNDARY), ...implementation, ...fixed.slice(RD_FIXED_TASK_BOUNDARY)];
|
|
76
89
|
}
|
|
77
90
|
function isInsidePath(childPath, parentPath) {
|
|
78
91
|
const relativePath = relative(parentPath, childPath);
|
|
@@ -308,11 +321,11 @@ function buildPlan(request, standardsOverlay) {
|
|
|
308
321
|
}
|
|
309
322
|
const taskIds = buildTaskIds(workerTarget);
|
|
310
323
|
const concreteTargetAreas = getConcreteTargetAreas(request, artifactWorkspacePath, techStatus.status === 'approved');
|
|
311
|
-
const discoveryTaskIds = taskIds.slice(0,
|
|
312
|
-
const planningTaskIds = taskIds.slice(
|
|
313
|
-
const implementationTaskIds = taskIds.slice(
|
|
314
|
-
const unitTestTaskIds = taskIds.slice(taskIds.length -
|
|
315
|
-
const qualityTaskIds = taskIds.slice(taskIds.length -
|
|
324
|
+
const discoveryTaskIds = taskIds.slice(0, RD_DISCOVERY_PLANNING_WAVE_SIZE);
|
|
325
|
+
const planningTaskIds = taskIds.slice(RD_DISCOVERY_PLANNING_WAVE_SIZE, RD_IMPLEMENTATION_BAND_START);
|
|
326
|
+
const implementationTaskIds = taskIds.slice(RD_IMPLEMENTATION_BAND_START, taskIds.length - RD_DISCOVERY_PLANNING_WAVE_SIZE);
|
|
327
|
+
const unitTestTaskIds = taskIds.slice(taskIds.length - RD_DISCOVERY_PLANNING_WAVE_SIZE, taskIds.length - RD_QUALITY_GATE_BAND_SIZE);
|
|
328
|
+
const qualityTaskIds = taskIds.slice(taskIds.length - RD_QUALITY_GATE_BAND_SIZE, taskIds.length - 1);
|
|
316
329
|
const reducerTaskIds = taskIds.slice(taskIds.length - 1);
|
|
317
330
|
const waves = [
|
|
318
331
|
{ name: 'discovery', taskIds: [...discoveryTaskIds] },
|
|
@@ -331,9 +344,9 @@ function buildPlan(request, standardsOverlay) {
|
|
|
331
344
|
reducer: qualityTaskIds,
|
|
332
345
|
};
|
|
333
346
|
const tasks = taskIds.map((taskId, index) => {
|
|
334
|
-
const wave = index <
|
|
347
|
+
const wave = index < RD_DISCOVERY_PLANNING_WAVE_SIZE ? 'discovery' : index < RD_IMPLEMENTATION_BAND_START ? 'planning' : index < taskIds.length - RD_DISCOVERY_PLANNING_WAVE_SIZE ? 'implementation candidates' : index < taskIds.length - RD_QUALITY_GATE_BAND_SIZE ? 'unit-test execution' : index < taskIds.length - 1 ? 'quality gates' : 'reducer';
|
|
335
348
|
const briefPath = `rd/swarm/workers/${taskId}/brief.md`;
|
|
336
|
-
const implementationIndex = index -
|
|
349
|
+
const implementationIndex = index - RD_IMPLEMENTATION_BAND_START;
|
|
337
350
|
const targetArea = wave === 'implementation candidates' && hasConcreteTargetAreas(concreteTargetAreas)
|
|
338
351
|
? selectConcreteTargetArea(concreteTargetAreas, implementationIndex)
|
|
339
352
|
: `area-${wave}`;
|
|
@@ -14,6 +14,18 @@ const BACKEND_DEP_NAMES = [
|
|
|
14
14
|
];
|
|
15
15
|
const BACKEND_DIR_CANDIDATES = ['server', 'backend', 'api', 'apps/server', 'apps/api', 'packages/server', 'packages/api'];
|
|
16
16
|
const MONOREPO_CONFIG_FILES = ['pnpm-workspace.yaml', 'lerna.json', 'turbo.json', 'nx.json', 'rush.json'];
|
|
17
|
+
/**
|
|
18
|
+
* PRD-002b slice 2 — extract archetype-detection thresholds. Names
|
|
19
|
+
* describe the meaning; values are bytewise-identical to the original
|
|
20
|
+
* literals (signaled in commit message).
|
|
21
|
+
*/
|
|
22
|
+
// eslint-disable-next-line no-magic-numbers -- canonical ms-per-day math (1000 * 60s * 60min * 24h)
|
|
23
|
+
const MS_PER_DAY = 1000 * 60 * 60 * 24;
|
|
24
|
+
const GREENFIELD_MAX_SRC_FILES = 20;
|
|
25
|
+
const LEGACY_MIN_SRC_FILES = 20;
|
|
26
|
+
const LOCKFILE_STALE_DAYS = 180;
|
|
27
|
+
const GREENFIELD_MAX_LOCKFILE_DAYS = 30;
|
|
28
|
+
const HIGH_CONFIDENCE_SIGNAL_COUNT = 3;
|
|
17
29
|
const SWAGGER_CANDIDATE_PATHS = [
|
|
18
30
|
'swagger.json',
|
|
19
31
|
'swagger.yaml',
|
|
@@ -125,7 +137,7 @@ async function lockfileAgeDays(projectRoot) {
|
|
|
125
137
|
if (await pathExists(full)) {
|
|
126
138
|
const stats = await stat(full);
|
|
127
139
|
const ageMs = Date.now() - stats.mtimeMs;
|
|
128
|
-
return Math.floor(ageMs /
|
|
140
|
+
return Math.floor(ageMs / MS_PER_DAY);
|
|
129
141
|
}
|
|
130
142
|
}
|
|
131
143
|
return null;
|
|
@@ -158,12 +170,12 @@ function decideArchetype(detected) {
|
|
|
158
170
|
});
|
|
159
171
|
signals.push({
|
|
160
172
|
name: 'src-size',
|
|
161
|
-
matched: detected.srcFileCount >=
|
|
173
|
+
matched: detected.srcFileCount >= GREENFIELD_MAX_SRC_FILES,
|
|
162
174
|
detail: `${detected.srcFileCount} source files in src/`
|
|
163
175
|
});
|
|
164
176
|
signals.push({
|
|
165
177
|
name: 'lockfile-age',
|
|
166
|
-
matched: detected.lockfileAgeDays !== null && detected.lockfileAgeDays >
|
|
178
|
+
matched: detected.lockfileAgeDays !== null && detected.lockfileAgeDays > LOCKFILE_STALE_DAYS,
|
|
167
179
|
detail: detected.lockfileAgeDays === null ? 'no lockfile' : `${detected.lockfileAgeDays} days`
|
|
168
180
|
});
|
|
169
181
|
if (!detected.hasPackageJson) {
|
|
@@ -184,26 +196,26 @@ function decideArchetype(detected) {
|
|
|
184
196
|
signals
|
|
185
197
|
};
|
|
186
198
|
}
|
|
187
|
-
if (hasBackend && detected.srcFileCount >=
|
|
199
|
+
if (hasBackend && detected.srcFileCount >= LEGACY_MIN_SRC_FILES) {
|
|
188
200
|
return { archetype: 'legacy-fullstack', confidence: 'high', signals };
|
|
189
201
|
}
|
|
190
202
|
const greenfieldSignals = [
|
|
191
|
-
detected.srcFileCount <
|
|
192
|
-
detected.lockfileAgeDays === null || detected.lockfileAgeDays <=
|
|
203
|
+
detected.srcFileCount < GREENFIELD_MAX_SRC_FILES,
|
|
204
|
+
detected.lockfileAgeDays === null || detected.lockfileAgeDays <= GREENFIELD_MAX_LOCKFILE_DAYS,
|
|
193
205
|
!detected.hasSwaggerOrProto
|
|
194
206
|
];
|
|
195
207
|
const greenfieldSignalCount = greenfieldSignals.filter(Boolean).length;
|
|
196
208
|
// Greenfield must show both a small src AND a fresh/missing lockfile — otherwise an empty-src legacy stub still looks like greenfield.
|
|
197
209
|
if (!hasBackend && greenfieldSignals[0] === true && greenfieldSignals[1] === true) {
|
|
198
|
-
return { archetype: 'greenfield', confidence: greenfieldSignalCount ===
|
|
210
|
+
return { archetype: 'greenfield', confidence: greenfieldSignalCount === HIGH_CONFIDENCE_SIGNAL_COUNT ? 'high' : 'medium', signals };
|
|
199
211
|
}
|
|
200
212
|
const legacySignalCount = [
|
|
201
213
|
!hasBackend,
|
|
202
214
|
!detected.hasSwaggerOrProto,
|
|
203
|
-
(detected.lockfileAgeDays !== null && detected.lockfileAgeDays >
|
|
215
|
+
(detected.lockfileAgeDays !== null && detected.lockfileAgeDays > LOCKFILE_STALE_DAYS) || detected.srcFileCount >= LEGACY_MIN_SRC_FILES
|
|
204
216
|
].filter(Boolean).length;
|
|
205
217
|
if (!hasBackend && legacySignalCount >= 2) {
|
|
206
|
-
return { archetype: 'legacy-frontend', confidence: legacySignalCount ===
|
|
218
|
+
return { archetype: 'legacy-frontend', confidence: legacySignalCount === HIGH_CONFIDENCE_SIGNAL_COUNT ? 'high' : 'medium', signals };
|
|
207
219
|
}
|
|
208
220
|
if (hasBackend) {
|
|
209
221
|
return { archetype: 'legacy-fullstack', confidence: 'medium', signals };
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
* crashed processes are acceptable because re-read will re-validate
|
|
18
18
|
* with the zod schema and either re-migrate or rebuild empty.
|
|
19
19
|
*/
|
|
20
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
21
|
-
import { copyFileSync, openSync, closeSync, constants as fsConstants } from 'node:fs';
|
|
20
|
+
import { constants as fsConstants, copyFileSync, closeSync, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
22
21
|
import { dirname, join } from 'node:path';
|
|
23
22
|
import { z } from 'zod';
|
|
24
23
|
export const InstanceRecordSchema = z.object({
|
|
@@ -21,15 +21,12 @@
|
|
|
21
21
|
* imported release to `stable` (AC-26 — peaks loop promote
|
|
22
22
|
* reads evolution_evaluation rows).
|
|
23
23
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
* initial `candidate` row. The integration test for AC-26 verifies
|
|
31
|
-
* that a freshly imported bundle has no `evolution_evaluation`
|
|
32
|
-
* row and that promotion therefore cannot proceed.
|
|
24
|
+
* Slice 4 (PRD-002b): split the four high-cohort functions
|
|
25
|
+
* (readBundle 15, importLoopBundle 20, materialiseRelatedBees 28,
|
|
26
|
+
* importBeeBundle arrow 38) into small helpers with table dispatch
|
|
27
|
+
* for kind routing and early-return for guard paths. Public API
|
|
28
|
+
* (readBundle, ReadBundleArgs, ReadBundleResult, the four error
|
|
29
|
+
* classes) is preserved verbatim.
|
|
33
30
|
*/
|
|
34
31
|
import type Database from "better-sqlite3";
|
|
35
32
|
import { SHARE_BUNDLE_ERROR_CODES, type PeaksBundleKind } from "./bundle-types.js";
|