peaks-loop 4.0.28 → 4.0.30

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 (74) hide show
  1. package/CHANGELOG.md +30 -0
  2. package/dist/cli/commands/_register.js +4 -0
  3. package/dist/cli/commands/dispatch-commands.d.ts +5 -41
  4. package/dist/cli/commands/dispatch-commands.js +59 -185
  5. package/dist/cli/commands/evidence-commands.d.ts +12 -0
  6. package/dist/cli/commands/evidence-commands.js +43 -0
  7. package/dist/cli/commands/fresh-context-commands.d.ts +15 -0
  8. package/dist/cli/commands/fresh-context-commands.js +24 -0
  9. package/dist/cli/commands/job-commands.js +14 -1
  10. package/dist/cli/commands/request-commands.js +15 -97
  11. package/dist/cli/commands/request-format-helpers.d.ts +19 -0
  12. package/dist/cli/commands/request-format-helpers.js +102 -0
  13. package/dist/cli/commands/worktree-auth-commands.js +2 -472
  14. package/dist/cli/commands/worktree-lease-commands.d.ts +21 -0
  15. package/dist/cli/commands/worktree-lease-commands.js +500 -0
  16. package/dist/services/artifacts/request-artifact-service.js +13 -3
  17. package/dist/services/code/auto-compact-lifecycle.d.ts +102 -0
  18. package/dist/services/code/auto-compact-lifecycle.js +235 -0
  19. package/dist/services/code/auto-compact-orchestrator.d.ts +1 -1
  20. package/dist/services/code/auto-compact-orchestrator.js +1 -222
  21. package/dist/services/codegraph/codegraph-autorefresh.d.ts +22 -0
  22. package/dist/services/codegraph/codegraph-autorefresh.js +91 -0
  23. package/dist/services/codegraph/codegraph-preflight-service.d.ts +53 -0
  24. package/dist/services/codegraph/codegraph-preflight-service.js +226 -0
  25. package/dist/services/context/build-dispatch-system-prompt.d.ts +52 -0
  26. package/dist/services/context/build-dispatch-system-prompt.js +70 -3
  27. package/dist/services/dispatch/dispatch-record-types.d.ts +278 -0
  28. package/dist/services/dispatch/dispatch-record-types.js +15 -0
  29. package/dist/services/dispatch/dispatch-record-upgrade.d.ts +5 -0
  30. package/dist/services/dispatch/dispatch-record-upgrade.js +230 -0
  31. package/dist/services/dispatch/dispatch-record-writer.d.ts +3 -278
  32. package/dist/services/dispatch/dispatch-record-writer.js +3 -245
  33. package/dist/services/dispatch/dispatch-sub-agent.d.ts +43 -0
  34. package/dist/services/dispatch/dispatch-sub-agent.js +55 -0
  35. package/dist/services/dispatch/isolation-lease.d.ts +43 -0
  36. package/dist/services/dispatch/isolation-lease.js +129 -0
  37. package/dist/services/evidence/evidence-generator.d.ts +26 -0
  38. package/dist/services/evidence/evidence-generator.js +349 -0
  39. package/dist/services/fresh-context/config.d.ts +1 -0
  40. package/dist/services/fresh-context/config.js +13 -0
  41. package/dist/services/fresh-context/fresh-context-block.d.ts +12 -0
  42. package/dist/services/fresh-context/fresh-context-block.js +40 -0
  43. package/dist/services/fresh-context/trigger-scan.d.ts +30 -0
  44. package/dist/services/fresh-context/trigger-scan.js +29 -0
  45. package/dist/services/skills/hooks-codegate-superpowers.d.ts +65 -0
  46. package/dist/services/skills/hooks-codegate-superpowers.js +204 -0
  47. package/dist/services/skills/hooks-settings-service.d.ts +2 -64
  48. package/dist/services/skills/hooks-settings-service.js +2 -215
  49. package/dist/services/skills/skill-statusline-renderer.d.ts +2 -34
  50. package/dist/services/skills/skill-statusline-renderer.js +1 -184
  51. package/dist/services/skills/statusline-palette.d.ts +62 -0
  52. package/dist/services/skills/statusline-palette.js +190 -0
  53. package/dist/services/slice/slice-decompose-import-edges.d.ts +8 -0
  54. package/dist/services/slice/slice-decompose-import-edges.js +102 -0
  55. package/dist/services/slice/slice-decompose-service.js +3 -198
  56. package/dist/services/slice/slice-decompose-tarjan.d.ts +8 -0
  57. package/dist/services/slice/slice-decompose-tarjan.js +108 -0
  58. package/dist/services/standards/project-standards-service.d.ts +1 -9
  59. package/dist/services/standards/project-standards-service.js +3 -232
  60. package/dist/services/standards/standards-render.d.ts +23 -0
  61. package/dist/services/standards/standards-render.js +238 -0
  62. package/dist/services/standards/ui-library-dispatch-block.d.ts +27 -0
  63. package/dist/services/standards/ui-library-dispatch-block.js +48 -0
  64. package/dist/services/workspace/reconcile-migrate.d.ts +77 -0
  65. package/dist/services/workspace/reconcile-migrate.js +230 -0
  66. package/dist/services/workspace/reconcile-service.d.ts +0 -72
  67. package/dist/services/workspace/reconcile-service.js +3 -220
  68. package/dist/services/workspace/workspace-service.js +13 -2
  69. package/dist/shared/incrementing-number.d.ts +11 -0
  70. package/dist/shared/incrementing-number.js +16 -3
  71. package/package.json +5 -5
  72. package/skills/peaks-code/SKILL.md +6 -2
  73. package/skills/peaks-code/references/fresh-context-preflight.md +81 -0
  74. package/skills/peaks-code/references/sub-agent-dispatch.md +27 -8
@@ -108,78 +108,6 @@ export declare function syncChangeMarker(projectRoot: string, canonicalSessionId
108
108
  created: string | null;
109
109
  error: string | null;
110
110
  };
111
- /**
112
- * One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
113
- *
114
- * Move the legacy runtime files at:
115
- * - `.peaks/.session.json`
116
- * - `.peaks/.active-skill.json`
117
- * - `.peaks/sop-state/`
118
- * into their new canonical home at:
119
- * - `.peaks/_runtime/session.json`
120
- * - `.peaks/_runtime/active-skill.json`
121
- * - `.peaks/_runtime/sop-state/`
122
- *
123
- * Behavior:
124
- * - Idempotent: re-running on a tree that is already on the new
125
- * layout produces `migratedFiles: []`.
126
- * - Best-effort: uses `fs.renameSync` (atomic on POSIX, best-effort
127
- * on Windows) and falls back to `copyFileSync` + `unlinkSync` if
128
- * rename throws (e.g. cross-device move on Windows). Errors are
129
- * collected per file and returned in the `errors` array so the
130
- * reconcile envelope can surface them without blocking the rest of
131
- * the migration.
132
- * - Creates `.peaks/_runtime/` on demand if any of the old paths
133
- * are present.
134
- *
135
- * @returns `{ migratedFiles, errors }`. `migratedFiles` lists the
136
- * *old* relative paths (e.g. `.peaks/.session.json`) that were
137
- * successfully moved, in move order. `errors` lists per-file
138
- * failures with the old path and a human-readable message.
139
- */
140
- export declare function migrateOldRuntimeState(projectRoot: string): {
141
- migratedFiles: string[];
142
- errors: Array<{
143
- path: string;
144
- message: string;
145
- }>;
146
- };
147
- /**
148
- * One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
149
- *
150
- * Move the legacy per-session sub-agent state files at:
151
- * - `.peaks/_runtime/<sid>/system/subagent-progress.json`
152
- * - `.peaks/_runtime/<sid>/system/progress-spawn.json`
153
- * into the new canonical home at:
154
- * - `.peaks/_sub_agents/<sid>/subagent-progress.json`
155
- * - `.peaks/_sub_agents/<sid>/progress-spawn.json`
156
- *
157
- * Behavior:
158
- * - Idempotent: re-running on a tree that is already on the new layout
159
- * produces `migratedFiles: []`.
160
- * - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
161
- * unlinkSync` if rename throws (e.g. cross-device move on Windows).
162
- * - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
163
- * subdir is only removed when it has zero other files, so a tree where
164
- * the user had unrelated content in `system/` is left untouched.
165
- * - New-path-wins: when both old and new files exist, the old file is
166
- * removed (the new path is authoritative).
167
- *
168
- * Walks every discovered session — not just the canonical one — so a user
169
- * with 6 pre-migration sessions gets all of them migrated in one reconcile
170
- * pass.
171
- *
172
- * @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
173
- * relative paths (e.g. `.peaks/_runtime/<sid>/system/subagent-progress.json`) that
174
- * were successfully moved. `errors` lists per-file failures.
175
- */
176
- export declare function migrateSubAgentState(projectRoot: string): {
177
- migratedFiles: string[];
178
- errors: Array<{
179
- path: string;
180
- message: string;
181
- }>;
182
- };
183
111
  /**
184
112
  * Top-level orchestrator. Wires migration (added in slice
185
113
  * 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
@@ -16,43 +16,12 @@
16
16
  * Pure hand-rolled; uses only node:fs, node:path, and the existing
17
17
  * session-manager helper for writing the binding. No new dependencies.
18
18
  */
19
- import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync, writeFileSync } from 'node:fs';
20
- import { dirname, join, resolve } from 'node:path';
19
+ import { existsSync, lstatSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from 'node:fs';
20
+ import { join, resolve } from 'node:path';
21
21
  import { getSessionIdCanonical, setCurrentSessionBinding } from '../session/session-manager.js';
22
+ import { migrateOldRuntimeState, migrateSubAgentState } from './reconcile-migrate.js';
22
23
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
23
24
  const META_FILE = 'session.json';
24
- // Sub-agent state file basenames (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
25
- // The legacy location was `.peaks/_runtime/<sid>/system/<filename>`; the canonical new
26
- // location is `.peaks/_sub_agents/<sid>/<filename>`. `migrateSubAgentState`
27
- // moves the two files between these homes on every `reconcileWorkspace` run.
28
- const SUB_AGENT_MIGRATION_FILES = [
29
- 'subagent-progress.json',
30
- 'progress-spawn.json'
31
- ];
32
- const SUB_AGENTS_DIR = '_sub_agents';
33
- // As of slice 2026-06-05-peaks-runtime-layer these old paths are the
34
- // back-compat read-only fallbacks; the canonical new home is
35
- // `.peaks/_runtime/`. `migrateOldRuntimeState` moves them to the new
36
- // location on disk. The leading dot is dropped when computing the
37
- // new basename (e.g. `.session.json` → `session.json`), so the new
38
- // layout is `.peaks/_runtime/{session.json,active-skill.json,sop-state/}`.
39
- const RUNTIME_OLD_PATHS = [
40
- '.session.json',
41
- '.active-skill.json',
42
- 'sop-state'
43
- ];
44
- const RUNTIME_DIR = join('.peaks', '_runtime');
45
- /**
46
- * Map a legacy path basename (e.g. `.session.json`) to its canonical
47
- * new basename (e.g. `session.json`). The dot is dropped so the new
48
- * layer reads naturally. Directories pass through unchanged.
49
- */
50
- function runtimeNewBasename(oldBasename) {
51
- if (oldBasename.startsWith('.') && oldBasename.length > 1) {
52
- return oldBasename.slice(1);
53
- }
54
- return oldBasename;
55
- }
56
25
  /**
57
26
  * Walk the project root's `.peaks/` directory and return an entry per
58
27
  * session dir matching the standard naming pattern, sorted by name
@@ -394,192 +363,6 @@ export function syncChangeMarker(projectRoot, canonicalSessionId) {
394
363
  }
395
364
  return { removed, created, error };
396
365
  }
397
- /**
398
- * One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
399
- *
400
- * Move the legacy runtime files at:
401
- * - `.peaks/.session.json`
402
- * - `.peaks/.active-skill.json`
403
- * - `.peaks/sop-state/`
404
- * into their new canonical home at:
405
- * - `.peaks/_runtime/session.json`
406
- * - `.peaks/_runtime/active-skill.json`
407
- * - `.peaks/_runtime/sop-state/`
408
- *
409
- * Behavior:
410
- * - Idempotent: re-running on a tree that is already on the new
411
- * layout produces `migratedFiles: []`.
412
- * - Best-effort: uses `fs.renameSync` (atomic on POSIX, best-effort
413
- * on Windows) and falls back to `copyFileSync` + `unlinkSync` if
414
- * rename throws (e.g. cross-device move on Windows). Errors are
415
- * collected per file and returned in the `errors` array so the
416
- * reconcile envelope can surface them without blocking the rest of
417
- * the migration.
418
- * - Creates `.peaks/_runtime/` on demand if any of the old paths
419
- * are present.
420
- *
421
- * @returns `{ migratedFiles, errors }`. `migratedFiles` lists the
422
- * *old* relative paths (e.g. `.peaks/.session.json`) that were
423
- * successfully moved, in move order. `errors` lists per-file
424
- * failures with the old path and a human-readable message.
425
- */
426
- export function migrateOldRuntimeState(projectRoot) {
427
- const root = resolve(projectRoot);
428
- const peaksRoot = join(root, '.peaks');
429
- const newDir = join(root, RUNTIME_DIR);
430
- const migratedFiles = [];
431
- const errors = [];
432
- for (const rel of RUNTIME_OLD_PATHS) {
433
- const oldPath = join(peaksRoot, rel);
434
- if (!existsSync(oldPath))
435
- continue;
436
- // Skip if the corresponding new path already exists — we treat the
437
- // new path as authoritative when both exist, so the old file would
438
- // only be stale data.
439
- const newPath = join(newDir, runtimeNewBasename(rel));
440
- if (existsSync(newPath)) {
441
- // Best-effort cleanup of the stale old file so a re-run stays
442
- // idempotent and the tree converges on the new layout.
443
- try {
444
- rmSync(oldPath, { recursive: true, force: true });
445
- }
446
- catch (error) {
447
- errors.push({
448
- path: rel,
449
- message: `Could not remove stale legacy file after migration: ${error instanceof Error ? error.message : String(error)}`
450
- });
451
- }
452
- continue;
453
- }
454
- try {
455
- // Ensure the new parent dir exists. `mkdirSync(dirname(newPath), { recursive: true })`
456
- // covers both the file case (`.peaks/_runtime`) and the
457
- // directory case (`.peaks/_runtime/sop-state`).
458
- mkdirSync(dirname(newPath), { recursive: true });
459
- try {
460
- renameSync(oldPath, newPath);
461
- }
462
- catch (renameError) {
463
- // Cross-device or locked-file fallback: copy + unlink.
464
- const stat = lstatSync(oldPath);
465
- if (stat.isDirectory()) {
466
- // Recursive copy for the sop-state dir.
467
- copyDirRecursiveSync(oldPath, newPath);
468
- rmSync(oldPath, { recursive: true, force: true });
469
- }
470
- else {
471
- copyFileSync(oldPath, newPath);
472
- unlinkSync(oldPath);
473
- }
474
- }
475
- migratedFiles.push(join('.peaks', rel));
476
- }
477
- catch (error) {
478
- errors.push({
479
- path: rel,
480
- message: error instanceof Error ? error.message : String(error)
481
- });
482
- }
483
- }
484
- return { migratedFiles, errors };
485
- }
486
- function copyDirRecursiveSync(src, dest) {
487
- mkdirSync(dest, { recursive: true });
488
- for (const name of readdirSync(src)) {
489
- const childSrc = join(src, name);
490
- const childDest = join(dest, name);
491
- const stat = lstatSync(childSrc);
492
- if (stat.isDirectory()) {
493
- copyDirRecursiveSync(childSrc, childDest);
494
- }
495
- else {
496
- copyFileSync(childSrc, childDest);
497
- }
498
- }
499
- }
500
- /**
501
- * One-time sub-agent state migration (slice 2026-06-06-sub-agent-spawn-bug-and-decouple).
502
- *
503
- * Move the legacy per-session sub-agent state files at:
504
- * - `.peaks/_runtime/<sid>/system/subagent-progress.json`
505
- * - `.peaks/_runtime/<sid>/system/progress-spawn.json`
506
- * into the new canonical home at:
507
- * - `.peaks/_sub_agents/<sid>/subagent-progress.json`
508
- * - `.peaks/_sub_agents/<sid>/progress-spawn.json`
509
- *
510
- * Behavior:
511
- * - Idempotent: re-running on a tree that is already on the new layout
512
- * produces `migratedFiles: []`.
513
- * - Best-effort: uses `fs.renameSync` and falls back to `copyFileSync +
514
- * unlinkSync` if rename throws (e.g. cross-device move on Windows).
515
- * - Empty `<sid>/system/` dir removal (R-2 guard): the legacy `system/`
516
- * subdir is only removed when it has zero other files, so a tree where
517
- * the user had unrelated content in `system/` is left untouched.
518
- * - New-path-wins: when both old and new files exist, the old file is
519
- * removed (the new path is authoritative).
520
- *
521
- * Walks every discovered session — not just the canonical one — so a user
522
- * with 6 pre-migration sessions gets all of them migrated in one reconcile
523
- * pass.
524
- *
525
- * @returns `{ migratedFiles, errors }`. `migratedFiles` lists the *old*
526
- * relative paths (e.g. `.peaks/_runtime/<sid>/system/subagent-progress.json`) that
527
- * were successfully moved. `errors` lists per-file failures.
528
- */
529
- export function migrateSubAgentState(projectRoot) {
530
- const root = resolve(projectRoot);
531
- const newDir = join(root, '.peaks', SUB_AGENTS_DIR);
532
- const migratedFiles = [];
533
- const errors = [];
534
- for (const session of discoverSessions(projectRoot)) {
535
- const oldSystemDir = join(session.path, 'system');
536
- if (!existsSync(oldSystemDir))
537
- continue;
538
- const newSessionDir = join(newDir, session.sessionId);
539
- mkdirSync(newSessionDir, { recursive: true });
540
- for (const fname of SUB_AGENT_MIGRATION_FILES) {
541
- const oldPath = join(oldSystemDir, fname);
542
- const newPath = join(newSessionDir, fname);
543
- if (!existsSync(oldPath))
544
- continue;
545
- if (existsSync(newPath)) {
546
- // New path is authoritative; remove stale old file.
547
- try {
548
- rmSync(oldPath, { force: true });
549
- }
550
- catch { /* best effort */ } // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
551
- continue;
552
- }
553
- try {
554
- try {
555
- renameSync(oldPath, newPath);
556
- }
557
- catch (renameError) {
558
- // Cross-device or locked-file fallback: copy + unlink.
559
- copyFileSync(oldPath, newPath);
560
- unlinkSync(oldPath);
561
- }
562
- migratedFiles.push(join('.peaks', session.sessionId, 'system', fname));
563
- }
564
- catch (error) {
565
- errors.push({
566
- path: oldPath,
567
- message: error instanceof Error ? error.message : String(error)
568
- });
569
- }
570
- }
571
- // R-2 guard: only remove the legacy system/ dir when it has zero
572
- // remaining files (the user might have unrelated content there).
573
- try {
574
- const remaining = readdirSync(oldSystemDir);
575
- if (remaining.length === 0) {
576
- rmdirSync(oldSystemDir);
577
- }
578
- }
579
- catch { /* best effort */ } // TODO(g2): legacy silent catch — grace: 1 minor release (v2.14.0)
580
- }
581
- return { migratedFiles, errors };
582
- }
583
366
  /**
584
367
  * Top-level orchestrator. Wires migration (added in slice
585
368
  * 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
@@ -2,7 +2,7 @@ import { mkdir } from 'node:fs/promises';
2
2
  import { existsSync, lstatSync, readdirSync } from 'node:fs';
3
3
  import { join } from 'node:path';
4
4
  import { isDirectory } from 'peaks-loop-shared/fs';
5
- import { getSessionId, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
5
+ import { getSessionIdCanonical, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
6
6
  import { normalizePath } from '../../shared/path-utils.js';
7
7
  /**
8
8
  * Slice 2026-06-29-change-id-root-removal: list the immediate children of
@@ -257,7 +257,18 @@ export async function initWorkspace(options) {
257
257
  // a parallel session without closing the previous one. Refuse to bind —
258
258
  // this is the "strict" mode the user picked. The user must finish or delete
259
259
  // the existing session first.
260
- const existingSessionId = getSessionId(options.projectRoot);
260
+ // Sub-agent session-rebind guard: read the existing binding via the
261
+ // canonicalize-on-read variant. A sub-agent re-running `peaks workspace
262
+ // init` may pass a project root whose spelling differs from the stored
263
+ // form (relative `"."`, symlink, separator/case variance on Windows).
264
+ // The strict `getSessionId` returns null on those spellings, which sent
265
+ // init down the "no prior binding → adopt" path and silently clobbered
266
+ // `.peaks/_runtime/session.json` with a phantom session, orphaning the
267
+ // parent's binding. `getSessionIdCanonical` resolves the stored form
268
+ // against the caller so a live parent binding is detected and the
269
+ // differing-id branch below refuses (or requires --allow-session-rebind)
270
+ // instead of silently overwriting.
271
+ const existingSessionId = getSessionIdCanonical(options.projectRoot);
261
272
  let previousSessionId = null;
262
273
  let bound = false;
263
274
  if (existingSessionId === null) {
@@ -11,6 +11,17 @@
11
11
  * @returns Next available number (1, 2, 3, ...)
12
12
  */
13
13
  export declare function getNextNumber(dirPath: string): number;
14
+ /**
15
+ * Convert a human-readable description into the kebab-case slug used in
16
+ * numbered artifact filenames. Lowercases, collapses non-alphanumerics to
17
+ * `-`, trims leading/trailing dashes, and caps at `MAX_FILENAME_SLUG_LENGTH`.
18
+ *
19
+ * Exported separately so lookup paths (e.g. `request-artifact-service.ts`)
20
+ * can compute the SAME slug the writer produced and match on-disk filenames
21
+ * case-insensitively — the writer lowercases, so a mixed-case request id
22
+ * like `2026-09-06-split-batchA` lands as `...-split-batcha.md`.
23
+ */
24
+ export declare function slugifyDescription(description: string): string;
14
25
  export declare function buildNumberedFilename(number: number, description: string): string;
15
26
  /**
16
27
  * Get the next numbered file path in a directory.
@@ -44,13 +44,26 @@ export function getNextNumber(dirPath) {
44
44
  // the slug may be at most 248 chars (giving 4 + 248 + 3 = 255 total).
45
45
  const MAX_FILENAME_LENGTH = 255;
46
46
  const MAX_FILENAME_SLUG_LENGTH = MAX_FILENAME_LENGTH - 7;
47
- export function buildNumberedFilename(number, description) {
48
- const padded = String(number).padStart(3, '0');
49
- const slug = description
47
+ /**
48
+ * Convert a human-readable description into the kebab-case slug used in
49
+ * numbered artifact filenames. Lowercases, collapses non-alphanumerics to
50
+ * `-`, trims leading/trailing dashes, and caps at `MAX_FILENAME_SLUG_LENGTH`.
51
+ *
52
+ * Exported separately so lookup paths (e.g. `request-artifact-service.ts`)
53
+ * can compute the SAME slug the writer produced and match on-disk filenames
54
+ * case-insensitively — the writer lowercases, so a mixed-case request id
55
+ * like `2026-09-06-split-batchA` lands as `...-split-batcha.md`.
56
+ */
57
+ export function slugifyDescription(description) {
58
+ return description
50
59
  .toLowerCase()
51
60
  .replace(/[^a-z0-9]+/g, '-')
52
61
  .replace(/^-|-$/g, '')
53
62
  .slice(0, MAX_FILENAME_SLUG_LENGTH);
63
+ }
64
+ export function buildNumberedFilename(number, description) {
65
+ const padded = String(number).padStart(3, '0');
66
+ const slug = slugifyDescription(description);
54
67
  return `${padded}-${slug}.md`;
55
68
  }
56
69
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.28",
3
+ "version": "4.0.30",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,10 +101,10 @@
101
101
  "fzf": "^0.5.2",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^4.4.3",
104
- "peaks-loop-internal-runtime": "0.0.13",
105
- "peaks-loop-mut": "0.1.26",
106
- "peaks-loop-shared": "0.0.62",
107
- "peaks-loop-shared-channel": "0.0.30"
104
+ "peaks-loop-internal-runtime": "0.0.15",
105
+ "peaks-loop-mut": "0.1.28",
106
+ "peaks-loop-shared": "0.0.64",
107
+ "peaks-loop-shared-channel": "0.0.32"
108
108
  },
109
109
  "devDependencies": {
110
110
  "@changesets/cli": "2.31.1",
@@ -86,10 +86,10 @@ Two-layer enforcement — do NOT try to bypass either:
86
86
  STOP. Run:
87
87
 
88
88
  ```bash
89
- peaks sub-agent dispatch rd --prompt '<your task>' --request-id <rid> --project . --batch-id <uuid> --json
89
+ peaks sub-agent dispatch rd --prompt '<your task>' --graph-node <nid> --workflow-id <wid> --request-id <rid> --project . --batch-id <uuid> --json
90
90
  ```
91
91
 
92
- The RD sub-agent owns the actual `Edit`/`Write`/`MultiEdit` tool calls against source code. The orchestrator only emits the dispatch request.
92
+ `--graph-node <nid>` is REQUIRED (RD §4 D4c). Prepare the node first: `peaks workflow init --skill peaks-code`, then `peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch`. The RD sub-agent owns the actual `Edit`/`Write`/`MultiEdit` tool calls against source code. The orchestrator only emits the dispatch request.
93
93
 
94
94
  **Anti-pattern:** directly calling `Edit`/`Write`/`MultiEdit` on `src/**` from the orchestrator session because "the change is tiny" / "it's just one line" / "the dispatch overhead is too high" / "the LLM feels confident". None of these override the Code-Gate; the probe + the hook both fail-closed.
95
95
 
@@ -139,6 +139,10 @@ Sub-agents MUST NOT use raw `git worktree add`. The only authorized path is `pea
139
139
 
140
140
  Full content extracted to **`references/startup-sequence.md`** (Steps 0 / 0.5-0.87 / 1 / 2 / 2.3 / 2.5 / N / N+1 / N+2 + sub-agent sharing + boundaries). Read that file in full at session start; the sequence is MANDATORY.
141
141
 
142
+ ## Peaks-Loop Fresh-context preflight (search-first, MANDATORY at orchestration-start)
143
+
144
+ Before the first planning action, run `peaks fresh-context preflight --prompt "<user request>" --json`. If `data.triggered` is true, search Context7 (priority, ~30s timeout) then WebSearch (fallback), synthesize ≤5 binding directives (用 X / 别用 Y / 因为 Z), and write the `## Fresh context` block (with the 以下信息优先于训练知识,冲突时以此为准 framing line) to `.peaks/_runtime/<sessionId>/fresh-context.md` — the RD/PRD dispatch site injects it automatically. Fail-soft (search failure → no block, slice continues). Kill-switch: `peaks config set --key freshContext.enabled --value false`. → `references/fresh-context-preflight.md`.
145
+
142
146
  ## Peaks-Loop Step 0.8 — Job-shape detection (BLOCKING on LLM judgement)
143
147
 
144
148
  > **BLOCKING on LLM judgement.** Before Step 1 mode selection, Code MUST surface a Job-shape decision via `peaks code detect-job` and persist it to `.peaks/_runtime/<sessionId>/job-shape.json`. The CLI is a recorder + gate — the LLM makes the judgement; downstream steps call `read-job-shape` and refuse if missing. See `references/step-0-8-gate.md` for the full contract. Skipping `peaks code detect-job` blocks the workflow at the next `read-job-shape` call (`JOB_SHAPE_NOT_DECIDED`).
@@ -0,0 +1,81 @@
1
+ # Fresh-context preflight (search-first)
2
+
3
+ > **Slice 2026-09-07-search-first-preflight.** Signal-triggered preflight that
4
+ > runs at **orchestration-start, before the first planning action**, for every
5
+ > peaks-* orchestrator (peaks-code, peaks-prd, peaks-rd, peaks-qa, … — not
6
+ > peaks-code specific). It hedges against model training-data lag by searching
7
+ > for fresh context and injecting binding directives into the PRD/RD dispatch.
8
+
9
+ ## When
10
+
11
+ Run the deterministic trigger scan as the very first step of a workflow, on the
12
+ user's raw request text:
13
+
14
+ ```text
15
+ peaks fresh-context preflight --prompt "<user request>" --json
16
+ ```
17
+
18
+ Read the `--json` envelope's `data` object:
19
+
20
+ | field | meaning |
21
+ |---|---|
22
+ | `data.triggered` | `true` → run the search + synthesis below |
23
+ | `data.forced` | `true` → the user explicitly asked for a live search (联网搜 / 查最新 / 搜一下) |
24
+ | `data.signals` | the signal keywords that matched (升级 / 迁移 / 最新 / latest / new / 版本 / 兼容 / breaking / upgrade / migrate / 新框架 / 推荐库 / 选型) |
25
+ | `data.enabled` | the `freshContext.enabled` kill-switch (false → no-op) |
26
+
27
+ If `data.triggered` is `false` (no signal, or kill-switch off), **do nothing** —
28
+ the workflow proceeds exactly as before (byte-identical dispatch prompts).
29
+
30
+ ## Search (only when `triggered: true`)
31
+
32
+ 1. **Context7** (priority, `@upstash/context7-mcp`, ~30s timeout).
33
+ 2. **WebSearch** fallback when Context7 returns empty / errors / times out.
34
+
35
+ Both fail-soft: if Context7 times out AND WebSearch fails, the slice continues
36
+ with **no block and no hard error** (acceptance criterion #8).
37
+
38
+ ## Synthesize (≤5 binding directives)
39
+
40
+ From the search results, synthesize **≤5 binding directives**, each in the
41
+ imperative form **"use X / do NOT use Y / because Z"** (用 X / 别用 Y / 因为 Z):
42
+
43
+ ```text
44
+ - 用 React 19 的 `use()`(因为 18 的并发渲染 API 已过时)
45
+ - 别用 `react-router-dom` v6 的 `Switch`(因为 v7 已移除,冲突时以此为准)
46
+ ```
47
+
48
+ Keep it to **5 or fewer** lines. Filter SEO noise — only keep directives that
49
+ change a decision the model would otherwise get wrong from training data.
50
+
51
+ ## Write the block
52
+
53
+ Wrap the directives in a `## Fresh context` block with the authoritative
54
+ framing line (M1), and write it to
55
+ `.peaks/_runtime/<sessionId>/fresh-context.md`:
56
+
57
+ ```markdown
58
+ ## Fresh context
59
+
60
+ 以下信息优先于训练知识,冲突时以此为准。
61
+
62
+ 1. 用 X(因为 Z)
63
+ 2. 别用 Y(因为 Z)
64
+ ```
65
+
66
+ The RD/PRD dispatch site auto-injects this block (after the project-stack
67
+ block, before the memory/task content). No manual step is needed beyond
68
+ writing the file.
69
+
70
+ ## Kill-switch
71
+
72
+ `peaks config set --key freshContext.enabled --value false` disables the whole
73
+ preflight (no search, no injection). The default is enabled (absent key →
74
+ `true`).
75
+
76
+ ## RD planning note
77
+
78
+ RD's planning artifact MUST record a "Fresh context 遵循" section listing which
79
+ of the ≤5 directives the implementation followed (acceptance criterion #6).
80
+ QA / code-review then verify the implemented version/API against the block
81
+ (acceptance criterion #7).
@@ -39,9 +39,23 @@ stays IDE-agnostic.
39
39
  **Command**:
40
40
 
41
41
  ```
42
- peaks sub-agent dispatch <role> --prompt <text> [--request-id <rid>] [--session-id <sid>] [--project <repo>] [--batch-id <uuid>] --json
42
+ peaks sub-agent dispatch <role> --prompt <text> --graph-node <nid> [--workflow-id <wid>] [--request-id <rid>] [--session-id <sid>] [--project <repo>] [--batch-id <uuid>] --json
43
43
  ```
44
44
 
45
+ > **`--graph-node` is REQUIRED (RD §4 D4c, effective 4.0.8).** Every
46
+ > single dispatch must bind to a prepared workflow graph node. Prepare
47
+ > one first:
48
+ >
49
+ > ```
50
+ > peaks workflow init --skill peaks-code # → returns <wid>
51
+ > peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch
52
+ > ```
53
+ >
54
+ > then dispatch with `--graph-node <nid> --workflow-id <wid>`. Omitting
55
+ > `--graph-node` rejects with `PEAKS_GRAPH_NODE_REQUIRED`; a node that is
56
+ > not prepared (or has the wrong kind) rejects with
57
+ > `PEAKS_GRAPH_NODE_NOT_PREPARED` / `PEAKS_GRAPH_NODE_KIND_INVALID`.
58
+
45
59
  **Envelope** (AC-8) — **2.1.0** (slice 2026-06-23-audit-4th #E1):
46
60
 
47
61
  > **Audit-3rd #4 + audit-4th #E1**: the `data.prompt` field was
@@ -342,16 +356,20 @@ grown unboundedly without this fix).
342
356
  When writing a SKILL.md that fans out sub-agents:
343
357
 
344
358
  1. Use `peaks sub-agent dispatch <role>` (never `Task(...)`).
345
- 2. Issue all dispatches in a single message; the LLM will fire all
359
+ 2. Prepare the graph node first (RD §4 D4c — `--graph-node` is required):
360
+ `peaks workflow init --skill peaks-code`, then
361
+ `peaks workflow node prepare --workflow <wid> --node <nid> --kind dispatch`.
362
+ 3. Issue all dispatches in a single message; the LLM will fire all
346
363
  returned toolCalls in parallel.
347
- 3. Pass `--request-id` and `--session-id` (or omit and let the CLI
348
- resolve the active session).
349
- 4. The sub-agent prompt **must** include the heartbeat instruction
364
+ 4. Pass `--graph-node <nid> --workflow-id <wid>` on every dispatch, plus
365
+ `--request-id` and `--session-id` (or omit and let the CLI resolve the
366
+ active session).
367
+ 5. The sub-agent prompt **must** include the heartbeat instruction
350
368
  (30 s cadence; override via `heartbeatIntervalSec` if needed).
351
- 5. After the fan-out returns, the Dispatcher reducer reads the
369
+ 6. After the fan-out returns, the Dispatcher reducer reads the
352
370
  dispatch record + the artifacts the sub-agents wrote, marks
353
371
  `disposed: true` on each record, and advances the state machine.
354
- 6. The poller handles the 5-min stale case as a warning, never as a
372
+ 7. The poller handles the 5-min stale case as a warning, never as a
355
373
  failure. The user is the one who decides to cancel.
356
374
 
357
375
  ## Cross-reference
@@ -380,6 +398,7 @@ peaks sub-agent dispatch <role> \
380
398
  hand back prose.', plus the heartbeat instruction: 'While running, call
381
399
  peaks sub-agent heartbeat --record <dispatchRecordPath> --status <state> --progress <pct> --note \"<text>\"
382
400
  at least every 30 seconds.'>" \
401
+ --graph-node <nid> --workflow-id <wid> \
383
402
  --request-id <rid> --session-id <session-id> --project <repo> --json
384
403
  ```
385
404
 
@@ -426,7 +445,7 @@ peaks skill presence:set peaks-code --project <repo> --mode <mode> --gate swarm-
426
445
 
427
446
  ## Detached Mode (Phase A, slice 2026-08-10)
428
447
 
429
- `peaks sub-agent dispatch <role> --prompt <text> --request-id <rid> --mode detached --vendor claude|codex|copilot [--no-throttle --max-concurrent <N>] --json` spawns a real OS process independent of the orchestrator IDE session.
448
+ `peaks sub-agent dispatch <role> --prompt <text> --graph-node <nid> --workflow-id <wid> --request-id <rid> --mode detached --vendor claude|codex|copilot [--no-throttle --max-concurrent <N>] --json` spawns a real OS process independent of the orchestrator IDE session. `--graph-node` remains required here (RD §4 D4c).
430
449
 
431
450
  - **Cross-platform spawn**: Windows uses `DETACHED_PROCESS` + `CREATE_NEW_PROCESS_GROUP`; POSIX uses `setsid` + `nohup`. Implementation: `packages/peaks-loop-internal-runtime/src/process-supervisor.ts`.
432
451
  - **Minimum-context prompt**: `PromptBuilder` emits a 5–8KB slice `{rid, role, vendor, files, refs}` plus the verbatim `<peaks-auto-compact>` marker. The forbidden marker `@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@` MUST NOT appear in any prompt (unit-tested; regression fails vitest).