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.
Files changed (80) hide show
  1. package/CHANGELOG.md +55 -0
  2. package/README-en.md +12 -9
  3. package/README.md +15 -4
  4. package/config/eslint/.peaks-rules.cjs +39 -26
  5. package/dist/cli/commands/_register.js +2 -1
  6. package/dist/cli/commands/audit-commands.d.ts +1 -1
  7. package/dist/cli/commands/code-gate-command.d.ts +25 -0
  8. package/dist/cli/commands/code-gate-command.js +86 -0
  9. package/dist/cli/commands/code-mode-gate-commands.js +3 -4
  10. package/dist/cli/commands/code-orchestrator-can-do.js +2 -1
  11. package/dist/cli/commands/cron-commands.js +16 -4
  12. package/dist/cli/commands/dashboard-long-run.js +8 -4
  13. package/dist/cli/commands/dispatch-from-dag.d.ts +1 -1
  14. package/dist/cli/commands/hooks-commands.js +56 -3
  15. package/dist/cli/commands/primer-command.d.ts +59 -0
  16. package/dist/cli/commands/primer-command.js +160 -0
  17. package/dist/cli/commands/qa-commands.js +1 -2
  18. package/dist/cli/commands/sediment-commands.d.ts +1 -1
  19. package/dist/cli/commands/slice-commands.js +20 -8
  20. package/dist/cli/commands/statusline-commands.d.ts +1 -0
  21. package/dist/cli/commands/statusline-commands.js +17 -3
  22. package/dist/cli/commands/tech-commands.d.ts +1 -1
  23. package/dist/cli/program.js +8 -0
  24. package/dist/services/artifacts/request-artifact-service.d.ts +2 -2
  25. package/dist/services/artifacts/request-artifact-service.js +10 -1
  26. package/dist/services/audit/enforcers/lint-reference-shape.js +18 -4
  27. package/dist/services/code/auto-compact-orchestrator.js +9 -1
  28. package/dist/services/code/batch-heartbeat-poller.js +26 -0
  29. package/dist/services/code/orchestrator-can-do.d.ts +24 -0
  30. package/dist/services/code/orchestrator-can-do.js +48 -2
  31. package/dist/services/code/post-compact-detector.d.ts +1 -1
  32. package/dist/services/compact-statusline/compact-statusline-service.js +19 -11
  33. package/dist/services/config/config-safety.d.ts +23 -0
  34. package/dist/services/config/config-safety.js +62 -0
  35. package/dist/services/config/config-service.d.ts +1 -1
  36. package/dist/services/config/config-service.js +2 -4
  37. package/dist/services/container/container-lease.js +21 -8
  38. package/dist/services/context/spillover-store.d.ts +1 -1
  39. package/dist/services/crystallization/crystallization-types.js +20 -8
  40. package/dist/services/crystallization/evidence-brief-builder.js +22 -11
  41. package/dist/services/dispatch/dispatch-record-writer.js +231 -141
  42. package/dist/services/evolution/evolution-types.js +38 -21
  43. package/dist/services/feedback/feedback-promotion-service.js +9 -1
  44. package/dist/services/hooks/pre-tool-code-gate.d.ts +46 -0
  45. package/dist/services/hooks/pre-tool-code-gate.js +91 -0
  46. package/dist/services/ide/current-model-detector.js +1 -2
  47. package/dist/services/lint/detect-eslint.js +8 -5
  48. package/dist/services/lint/eslint-runner.d.ts +1 -2
  49. package/dist/services/lint/eslint-runner.js +86 -23
  50. package/dist/services/lint/npx-resolver.d.ts +6 -0
  51. package/dist/services/lint/npx-resolver.js +47 -0
  52. package/dist/services/loop/loop-release-types.js +32 -13
  53. package/dist/services/loop/spec-service.d.ts +7 -1
  54. package/dist/services/loop/spec-service.js +218 -182
  55. package/dist/services/rd/rd-service.js +22 -9
  56. package/dist/services/scan/archetype-service.js +21 -9
  57. package/dist/services/session/binding-store.js +1 -2
  58. package/dist/services/share/bundle-reader.d.ts +6 -9
  59. package/dist/services/share/bundle-reader.js +297 -223
  60. package/dist/services/skills/hooks-settings-service.d.ts +33 -0
  61. package/dist/services/skills/hooks-settings-service.js +48 -3
  62. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  63. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  64. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  65. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  66. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  67. package/dist/services/skills/skill-statusline-service.js +56 -6
  68. package/dist/services/slice/calibration-store.js +21 -8
  69. package/dist/services/slice/slice-check-service.js +35 -14
  70. package/dist/services/slice/slice-decompose-service.js +459 -223
  71. package/dist/services/standards/project-context.js +135 -73
  72. package/dist/services/verdict/envelopes.d.ts +1 -2
  73. package/dist/services/verdict/envelopes.js +1 -2
  74. package/dist/services/verdict/verdict-aggregator.d.ts +1 -2
  75. package/dist/services/vm/vm-lease.js +19 -8
  76. package/dist/services/worktree/worktree-lease.d.ts +0 -10
  77. package/dist/services/worktree/worktree-lease.js +19 -8
  78. package/dist/shared/fs-utils.d.ts +1 -1
  79. package/package.json +7 -4
  80. 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 spec.evaluators) {
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
- const evalKinds = new Set(spec.evaluators.map((e) => e.kind));
96
- for (const s of spec.sla) {
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
- if (spec.termination.strategy === 'max-cycles' && (spec.termination.maxCycles === undefined || spec.termination.maxCycles < 1)) {
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
- if (line.trim() === '' || line.trim().startsWith('#'))
215
- continue;
216
- const indent = leadingSpaces(line);
217
- if (indent < baseIndent)
218
- return { value: obj, next: i };
219
- if (indent > baseIndent)
220
- continue; // caller should escalate
221
- const trimmed = line.trim();
222
- if (!trimmed.includes(':'))
223
- continue;
224
- const parts = splitTopLevel(trimmed, ':');
225
- const key = parts[0] ?? '';
226
- const value = (parts[1] ?? '').trim();
227
- // When `value` still contains a top-level `: ,` pattern, the line is
228
- // an inline object whose first key is the same as `key` (e.g.
229
- // `strategy: max-cycles, maxCycles: 3` →
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
- obj[key] = value === '|' || value === '>' ? '' : null;
256
- continue;
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`), parse as such; otherwise treat as scalar. */
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
- let topColon = -1;
271
- let topComma = -1;
272
- let depth = 0;
273
- let inQuote = null;
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
- let topColon = -1;
306
- let topComma = -1;
307
- let depth = 0;
308
- let inQuote = null;
309
- for (let i = 0; i < trimmed.length; i++) {
310
- const ch = trimmed[i];
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.trim() === '' || line.trim().startsWith('#'))
369
+ if (shouldSkipArrayLine(line, baseIndent))
342
370
  continue;
343
- const indent = leadingSpaces(line);
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
- const item = trimmed.slice(2).trim();
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
- if (inQuote !== null) {
403
- cur += ch;
404
- if (ch === inQuote)
405
- inQuote = null;
406
- continue;
407
- }
408
- if (ch === '"' || ch === "'") {
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.map((x) => x.trim()).filter((x) => x.length > 0);
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
- let depth = 0;
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
- if ((raw.startsWith('"') && raw.endsWith('"')) || (raw.startsWith("'") && raw.endsWith("'"))) {
480
- return raw.slice(1, -1);
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 raw;
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(3, '0')}`);
75
- return [...fixed.slice(0, 16), ...implementation, ...fixed.slice(16)];
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, 8);
312
- const planningTaskIds = taskIds.slice(8, 16);
313
- const implementationTaskIds = taskIds.slice(16, taskIds.length - 8);
314
- const unitTestTaskIds = taskIds.slice(taskIds.length - 8, taskIds.length - 5);
315
- const qualityTaskIds = taskIds.slice(taskIds.length - 5, taskIds.length - 1);
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 < 8 ? 'discovery' : index < 16 ? 'planning' : index < taskIds.length - 8 ? 'implementation candidates' : index < taskIds.length - 5 ? 'unit-test execution' : index < taskIds.length - 1 ? 'quality gates' : 'reducer';
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 - 16;
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 / (1000 * 60 * 60 * 24));
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 >= 20,
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 > 180,
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 >= 20) {
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 < 20,
192
- detected.lockfileAgeDays === null || detected.lockfileAgeDays <= 30,
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 === 3 ? 'high' : 'medium', signals };
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 > 180) || detected.srcFileCount >= 20
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 === 3 ? 'high' : 'medium', signals };
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
- * The reader is also the integration point for AC-26 — the loop
25
- * promote path (which lives in a future slice / CLI command) checks
26
- * `evolution_evaluation.independent_scorer_verdict` before allowing
27
- * a candidate → stable transition; this slice does not add a new
28
- * `peaks loop promote` because `peaks asset crystallize` already
29
- * enforces the brief-section + pre-run gates and creates the
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";