peaks-cli 1.3.1 → 1.3.3

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 (111) hide show
  1. package/README.md +6 -2
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/core-artifact-commands.js +49 -11
  4. package/dist/src/cli/commands/gate-commands.js +28 -19
  5. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  6. package/dist/src/cli/commands/hook-handle.js +111 -0
  7. package/dist/src/cli/commands/hooks-commands.js +72 -21
  8. package/dist/src/cli/commands/progress-commands.js +9 -2
  9. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  10. package/dist/src/cli/commands/slice-commands.js +4 -2
  11. package/dist/src/cli/commands/statusline-commands.js +75 -17
  12. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  13. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  14. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  15. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  16. package/dist/src/cli/commands/workspace-commands.js +70 -14
  17. package/dist/src/cli/program.js +9 -0
  18. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  19. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  20. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
  21. package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
  22. package/dist/src/services/artifacts/request-artifact-service.js +116 -76
  23. package/dist/src/services/config/config-types.d.ts +1 -1
  24. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  25. package/dist/src/services/context/artifact-meta.js +105 -0
  26. package/dist/src/services/context/context-guard.d.ts +49 -0
  27. package/dist/src/services/context/context-guard.js +91 -0
  28. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  29. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  30. package/dist/src/services/context/headroom-client.d.ts +34 -0
  31. package/dist/src/services/context/headroom-client.js +117 -0
  32. package/dist/src/services/context/shared-channel.d.ts +92 -0
  33. package/dist/src/services/context/shared-channel.js +285 -0
  34. package/dist/src/services/context/threshold.d.ts +35 -0
  35. package/dist/src/services/context/threshold.js +76 -0
  36. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  37. package/dist/src/services/dispatch/batch-counter.js +85 -0
  38. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  39. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  40. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  41. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  42. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  43. package/dist/src/services/dispatch/leak-detector.js +72 -0
  44. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  45. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  46. package/dist/src/services/doctor/doctor-service.d.ts +62 -0
  47. package/dist/src/services/doctor/doctor-service.js +276 -1
  48. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  49. package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
  50. package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
  51. package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
  52. package/dist/src/services/ide/hook-protocol.d.ts +44 -0
  53. package/dist/src/services/ide/hook-protocol.js +71 -0
  54. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  55. package/dist/src/services/ide/hook-translator.js +128 -0
  56. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  57. package/dist/src/services/ide/ide-detector.js +19 -0
  58. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  59. package/dist/src/services/ide/ide-registry.js +45 -0
  60. package/dist/src/services/ide/ide-types.d.ts +120 -0
  61. package/dist/src/services/ide/ide-types.js +2 -0
  62. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  63. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  64. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  65. package/dist/src/services/ide/shared/safe-path.js +29 -0
  66. package/dist/src/services/progress/progress-service.d.ts +1 -1
  67. package/dist/src/services/progress/progress-service.js +18 -14
  68. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  69. package/dist/src/services/security/safe-settings-path.js +104 -0
  70. package/dist/src/services/session/session-manager.d.ts +22 -1
  71. package/dist/src/services/session/session-manager.js +137 -28
  72. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  73. package/dist/src/services/signal/cancel-handler.js +76 -0
  74. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  75. package/dist/src/services/skill/resume-detector.js +334 -0
  76. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  77. package/dist/src/services/skill/skill-scheduler.js +53 -0
  78. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  79. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  80. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  81. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  82. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  83. package/dist/src/services/slice/slice-archive-service.js +111 -0
  84. package/dist/src/services/slice/slice-check-service.js +20 -1
  85. package/dist/src/services/slice/slice-check-types.d.ts +9 -0
  86. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  87. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  88. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  89. package/dist/src/services/solo/status-line-renderer.js +55 -0
  90. package/dist/src/services/workspace/migrate-service.js +124 -2
  91. package/dist/src/services/workspace/migrate-types.d.ts +50 -7
  92. package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
  93. package/dist/src/services/workspace/reconcile-service.js +267 -48
  94. package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
  95. package/dist/src/services/workspace/workspace-service.js +29 -62
  96. package/dist/src/shared/version.d.ts +1 -1
  97. package/dist/src/shared/version.js +1 -1
  98. package/package.json +2 -1
  99. package/schemas/doctor-report.schema.json +2 -2
  100. package/skills/peaks-ide/SKILL.md +159 -0
  101. package/skills/peaks-qa/SKILL.md +58 -1
  102. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  103. package/skills/peaks-rd/SKILL.md +52 -9
  104. package/skills/peaks-solo/SKILL.md +83 -20
  105. package/skills/peaks-solo/references/context-governance.md +144 -0
  106. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  107. package/skills/peaks-solo/references/runbook.md +3 -3
  108. package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
  109. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  110. package/skills/peaks-txt/SKILL.md +19 -0
  111. package/skills/peaks-ui/SKILL.md +28 -1
@@ -75,6 +75,39 @@ export declare function applyDeletions(candidates: SessionEntry[], apply: boolea
75
75
  message: string;
76
76
  }>;
77
77
  };
78
+ /**
79
+ * Sync the single `change/<canonicalSessionId>/` live marker under
80
+ * `.peaks/_runtime/change/`. The marker is an EMPTY directory (no
81
+ * symlinks, no manifest, no content). Slice 006 collapses the F3
82
+ * per-change-id symlink layer to a single live marker so the
83
+ * `change/` layer is a single sentinel — easy for the LLM to
84
+ * navigate, easy for tests to assert on.
85
+ *
86
+ * Steps:
87
+ * 1. Ensure `.peaks/_runtime/change/` exists.
88
+ * 2. List its entries.
89
+ * 3. Remove every entry whose name is NOT `<canonicalSessionId>/`
90
+ * (no-op for those whose name matches).
91
+ * 4. If `<canonicalSessionId>/` is missing, create it (empty).
92
+ * 5. Return the diff for telemetry.
93
+ *
94
+ * Path-traversal guards: the canonical session id is validated by
95
+ * the caller (`SESSION_ID_PATTERN` in the session-manager helper).
96
+ * The function only ever operates under the project-root-resolved
97
+ * `.peaks/_runtime/change/` dir, so the resolved paths cannot
98
+ * escape the project tree.
99
+ *
100
+ * @returns `{ removed, created, error }`:
101
+ * - `removed`: list of entry names that were deleted (e.g. `['<oldSid>/']`).
102
+ * - `created`: name of the new marker (e.g. `'<newSid>/'`) or null
103
+ * when the canonical marker already existed (no-op).
104
+ * - `error`: error message string or null.
105
+ */
106
+ export declare function syncChangeMarker(projectRoot: string, canonicalSessionId: string): {
107
+ removed: string[];
108
+ created: string | null;
109
+ error: string | null;
110
+ };
78
111
  /**
79
112
  * One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
80
113
  *
@@ -111,6 +144,42 @@ export declare function migrateOldRuntimeState(projectRoot: string): {
111
144
  message: string;
112
145
  }>;
113
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/<sid>/system/subagent-progress.json`
152
+ * - `.peaks/<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/<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
+ };
114
183
  /**
115
184
  * Top-level orchestrator. Wires migration (added in slice
116
185
  * 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
@@ -16,11 +16,20 @@
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, renameSync, rmSync, statSync, unlinkSync } from 'node:fs';
19
+ import { copyFileSync, existsSync, lstatSync, mkdirSync, readdirSync, renameSync, rmSync, rmdirSync, statSync, unlinkSync } from 'node:fs';
20
20
  import { dirname, join, resolve } from 'node:path';
21
21
  import { getSessionIdCanonical, setCurrentSessionBinding } from '../session/session-manager.js';
22
22
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-session-[a-f0-9]+$/;
23
23
  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/<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';
24
33
  // As of slice 2026-06-05-peaks-runtime-layer these old paths are the
25
34
  // back-compat read-only fallbacks; the canonical new home is
26
35
  // `.peaks/_runtime/`. `migrateOldRuntimeState` moves them to the new
@@ -55,59 +64,75 @@ function runtimeNewBasename(oldBasename) {
55
64
  * of files under the dir excluding `session.json` itself.
56
65
  */
57
66
  export function discoverSessions(projectRoot) {
67
+ const runtimeRoot = join(projectRoot, '.peaks', '_runtime');
58
68
  const peaksRoot = join(projectRoot, '.peaks');
59
- if (!existsSync(peaksRoot))
60
- return [];
61
- let names;
62
- try {
63
- names = readdirSync(peaksRoot);
64
- }
65
- catch {
66
- return [];
67
- }
69
+ // As of slice 003, the canonical home for session dirs is
70
+ // `.peaks/_runtime/<sid>/`. The legacy top-level layout is
71
+ // read for back-compat (one minor release) so pre-migration
72
+ // trees keep working. Both are scanned; duplicates (same sid
73
+ // in both homes) are de-duplicated with the canonical home
74
+ // winning.
75
+ const seen = new Set();
68
76
  const entries = [];
69
- for (const name of names) {
70
- if (!SESSION_ID_PATTERN.test(name))
71
- continue;
72
- const dir = join(peaksRoot, name);
73
- let stat;
77
+ const collect = (root) => {
78
+ if (!existsSync(root))
79
+ return;
80
+ let names;
74
81
  try {
75
- // lstatSync: false for symlinks (prevents rm -rf from following a
76
- // malicious symlink that points outside the project root).
77
- stat = lstatSync(dir);
82
+ names = readdirSync(root);
78
83
  }
79
84
  catch {
80
- continue;
85
+ return;
81
86
  }
82
- if (!stat.isDirectory())
83
- continue;
84
- const metaPath = join(dir, META_FILE);
85
- let lastActivity = null;
86
- if (existsSync(metaPath)) {
87
- try {
88
- lastActivity = statSync(metaPath).mtimeMs;
89
- }
90
- catch {
91
- lastActivity = null;
92
- }
87
+ for (const name of names) {
88
+ if (!SESSION_ID_PATTERN.test(name))
89
+ continue;
90
+ if (seen.has(name))
91
+ continue;
92
+ seen.add(name);
93
+ scanSessionDir(root, name, entries);
93
94
  }
94
- let childNames;
95
+ };
96
+ collect(runtimeRoot);
97
+ collect(peaksRoot);
98
+ entries.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
99
+ return entries;
100
+ }
101
+ function scanSessionDir(peaksRoot, name, entries) {
102
+ const dir = join(peaksRoot, name);
103
+ let stat;
104
+ try {
105
+ stat = lstatSync(dir);
106
+ }
107
+ catch {
108
+ return;
109
+ }
110
+ if (!stat.isDirectory())
111
+ return;
112
+ const metaPath = join(dir, META_FILE);
113
+ let lastActivity = null;
114
+ if (existsSync(metaPath)) {
95
115
  try {
96
- childNames = readdirSync(dir);
116
+ lastActivity = statSync(metaPath).mtimeMs;
97
117
  }
98
118
  catch {
99
- childNames = [];
119
+ lastActivity = null;
100
120
  }
101
- let artifactCount = 0;
102
- for (const child of childNames) {
103
- if (child === META_FILE)
104
- continue;
105
- artifactCount += 1;
106
- }
107
- entries.push({ sessionId: name, path: dir, lastActivity, artifactCount });
108
121
  }
109
- entries.sort((a, b) => a.sessionId.localeCompare(b.sessionId));
110
- return entries;
122
+ let childNames;
123
+ try {
124
+ childNames = readdirSync(dir);
125
+ }
126
+ catch {
127
+ childNames = [];
128
+ }
129
+ let artifactCount = 0;
130
+ for (const child of childNames) {
131
+ if (child === META_FILE)
132
+ continue;
133
+ artifactCount += 1;
134
+ }
135
+ entries.push({ sessionId: name, path: dir, lastActivity, artifactCount });
111
136
  }
112
137
  /**
113
138
  * 4-tier canonical selection. Tiers evaluated in order; first one that
@@ -299,6 +324,76 @@ function readActiveSkillSessionId(projectRoot) {
299
324
  }
300
325
  return null;
301
326
  }
327
+ /**
328
+ * Sync the single `change/<canonicalSessionId>/` live marker under
329
+ * `.peaks/_runtime/change/`. The marker is an EMPTY directory (no
330
+ * symlinks, no manifest, no content). Slice 006 collapses the F3
331
+ * per-change-id symlink layer to a single live marker so the
332
+ * `change/` layer is a single sentinel — easy for the LLM to
333
+ * navigate, easy for tests to assert on.
334
+ *
335
+ * Steps:
336
+ * 1. Ensure `.peaks/_runtime/change/` exists.
337
+ * 2. List its entries.
338
+ * 3. Remove every entry whose name is NOT `<canonicalSessionId>/`
339
+ * (no-op for those whose name matches).
340
+ * 4. If `<canonicalSessionId>/` is missing, create it (empty).
341
+ * 5. Return the diff for telemetry.
342
+ *
343
+ * Path-traversal guards: the canonical session id is validated by
344
+ * the caller (`SESSION_ID_PATTERN` in the session-manager helper).
345
+ * The function only ever operates under the project-root-resolved
346
+ * `.peaks/_runtime/change/` dir, so the resolved paths cannot
347
+ * escape the project tree.
348
+ *
349
+ * @returns `{ removed, created, error }`:
350
+ * - `removed`: list of entry names that were deleted (e.g. `['<oldSid>/']`).
351
+ * - `created`: name of the new marker (e.g. `'<newSid>/'`) or null
352
+ * when the canonical marker already existed (no-op).
353
+ * - `error`: error message string or null.
354
+ */
355
+ export function syncChangeMarker(projectRoot, canonicalSessionId) {
356
+ const root = resolve(projectRoot);
357
+ const changeDir = join(root, '.peaks', '_runtime', 'change');
358
+ const removed = [];
359
+ let created = null;
360
+ let error = null;
361
+ try {
362
+ if (!existsSync(changeDir)) {
363
+ mkdirSync(changeDir, { recursive: true });
364
+ }
365
+ const markerPath = join(changeDir, canonicalSessionId);
366
+ let existingNames = [];
367
+ try {
368
+ existingNames = readdirSync(changeDir);
369
+ }
370
+ catch {
371
+ existingNames = [];
372
+ }
373
+ for (const name of existingNames) {
374
+ if (name === canonicalSessionId)
375
+ continue;
376
+ const stale = join(changeDir, name);
377
+ try {
378
+ rmSync(stale, { recursive: true, force: true });
379
+ removed.push(name);
380
+ }
381
+ catch (e) {
382
+ // Best-effort: a single un-deletable entry must not abort the
383
+ // sync (the caller gets the rest of the diff).
384
+ error = e instanceof Error ? e.message : String(e);
385
+ }
386
+ }
387
+ if (!existsSync(markerPath)) {
388
+ mkdirSync(markerPath, { recursive: true });
389
+ created = canonicalSessionId;
390
+ }
391
+ }
392
+ catch (e) {
393
+ error = e instanceof Error ? e.message : String(e);
394
+ }
395
+ return { removed, created, error };
396
+ }
302
397
  /**
303
398
  * One-time migration step (added in slice 2026-06-05-peaks-runtime-layer).
304
399
  *
@@ -402,6 +497,89 @@ function copyDirRecursiveSync(src, dest) {
402
497
  }
403
498
  }
404
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/<sid>/system/subagent-progress.json`
505
+ * - `.peaks/<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/<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 */ }
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 */ }
580
+ }
581
+ return { migratedFiles, errors };
582
+ }
405
583
  /**
406
584
  * Top-level orchestrator. Wires migration (added in slice
407
585
  * 2026-06-05-peaks-runtime-layer), discovery, canonical pick, re-point,
@@ -417,11 +595,19 @@ export function reconcileWorkspace(options) {
417
595
  // before that read means the new path is the only path observed by
418
596
  // `getSessionIdCanonical` after this call returns.
419
597
  const migration = migrateOldRuntimeState(projectRoot);
420
- const migrateErrors = migration.errors.map((e) => ({
421
- kind: 'migrate',
422
- path: e.path,
423
- message: e.message
424
- }));
598
+ const subAgentMigration = migrateSubAgentState(projectRoot);
599
+ const migrateErrors = [
600
+ ...migration.errors.map((e) => ({
601
+ kind: 'migrate',
602
+ path: e.path,
603
+ message: e.message
604
+ })),
605
+ ...subAgentMigration.errors.map((e) => ({
606
+ kind: 'migrate',
607
+ path: e.path,
608
+ message: e.message
609
+ }))
610
+ ];
425
611
  const sessions = discoverSessions(projectRoot);
426
612
  const activeSkillSessionId = readActiveSkillSessionId(projectRoot);
427
613
  const canonical = pickCanonicalSession(sessions, activeSkillSessionId);
@@ -445,6 +631,36 @@ export function reconcileWorkspace(options) {
445
631
  }
446
632
  const deletionCandidates = findDeletionCandidates(sessions, ageThresholdMs);
447
633
  const deletionResult = applyDeletions(deletionCandidates, apply);
634
+ // Slice 006: sync the single `change/<canonicalSessionId>/` live
635
+ // marker (replaces the F3 per-change-id symlink layer). The marker
636
+ // is an empty dir; the function removes every other entry under
637
+ // `.peaks/_runtime/change/`. Idempotent. This step is independent
638
+ // of the apply flag — syncing the marker is a derived-state write,
639
+ // not a destructive side effect.
640
+ const changeMarker = canonical === null
641
+ ? { removed: [], created: null, error: 'no canonical session' }
642
+ : syncChangeMarker(projectRoot, canonical.sessionId);
643
+ // Slice 006: clean up the F3-introduced `.peaks/_runtime/<sid>/system/`
644
+ // subdir under EVERY session dir (not just the canonical one). The
645
+ // subdir was created eagerly by `initWorkspace` (F3) but was never
646
+ // used. The cleanup is idempotent: re-running on a tree without the
647
+ // subdir is a no-op. We walk every discovered session, not just the
648
+ // canonical one, because the user has 6 F3 sessions with the cruft
649
+ // and the spec says all of them must be removed. Logged in the
650
+ // systemCleaned array.
651
+ const systemCleaned = [];
652
+ for (const session of sessions) {
653
+ const systemDir = join(session.path, 'system');
654
+ if (existsSync(systemDir)) {
655
+ try {
656
+ rmSync(systemDir, { recursive: true, force: true });
657
+ systemCleaned.push(systemDir);
658
+ }
659
+ catch {
660
+ // Best-effort: a locked subdir does not block the rest of reconcile.
661
+ }
662
+ }
663
+ }
448
664
  return {
449
665
  projectRoot,
450
666
  sessions,
@@ -459,6 +675,9 @@ export function reconcileWorkspace(options) {
459
675
  apply,
460
676
  repointed,
461
677
  migratedFiles: migration.migratedFiles,
462
- errors: [...migrateErrors, ...deletionResult.errors]
678
+ subAgentStateMigrated: subAgentMigration.migratedFiles.length,
679
+ errors: [...migrateErrors, ...deletionResult.errors],
680
+ changeMarker,
681
+ systemCleaned
463
682
  };
464
683
  }
@@ -65,6 +65,18 @@ export type ReconcileResult = {
65
65
  * consumers can ignore this field.
66
66
  */
67
67
  migratedFiles: string[];
68
+ /**
69
+ * Count of legacy per-session sub-agent state files moved from
70
+ * `.peaks/<sid>/system/{subagent-progress,progress-spawn}.json` into
71
+ * `.peaks/_sub_agents/<sid>/` during this reconcile run.
72
+ *
73
+ * Added in slice 2026-06-06-sub-agent-spawn-bug-and-decouple. The
74
+ * detailed list of moved files is not surfaced here (the count is
75
+ * what the CLI summary and QA test assert on); the underlying
76
+ * `migrateSubAgentState` helper returns the full path list for
77
+ * forensics. Additive — older consumers can ignore this field.
78
+ */
79
+ subAgentStateMigrated: number;
68
80
  /**
69
81
  * Errors encountered during the migration step. Each entry has a
70
82
  * `kind: 'migrate'` discriminator so consumers can tell migration
@@ -78,6 +90,31 @@ export type ReconcileResult = {
78
90
  path: string;
79
91
  message: string;
80
92
  }>;
93
+ /**
94
+ * Slice 006 (2026-06-06-change-folder-simplify-and-lazy-role-subdirs):
95
+ * result of syncing the single `change/<canonicalSessionId>/` live
96
+ * marker under `.peaks/_runtime/change/`. The marker is an empty
97
+ * directory; every other entry under `change/` is removed. The
98
+ * `removed` array lists entry names that were deleted (e.g.
99
+ * `['<oldSid>/']`); `created` is the name of the new marker (or
100
+ * `null` when the canonical marker already existed — no-op);
101
+ * `error` is the first error message (if any) encountered during
102
+ * the sync. Additive — older consumers can ignore this field.
103
+ * Replaces the F3 `changeLinks` field.
104
+ */
105
+ changeMarker: {
106
+ removed: string[];
107
+ created: string | null;
108
+ error: string | null;
109
+ };
110
+ /**
111
+ * Slice 006: list of absolute paths to `.peaks/_runtime/<sid>/system/`
112
+ * subdirs that were removed by this reconcile run. The F3
113
+ * `initWorkspace` eagerly created the `system/` subdir; slice 006
114
+ * deletes it during reconcile. Empty when the canonical session
115
+ * had no `system/` subdir. Additive.
116
+ */
117
+ systemCleaned: string[];
81
118
  };
82
119
  export type ReconcileOptions = {
83
120
  projectRoot: string;
@@ -1,36 +1,8 @@
1
1
  import { mkdir } from 'node:fs/promises';
2
2
  import { join } from 'node:path';
3
3
  import { isDirectory } from '../../shared/fs.js';
4
- import { getSessionId, setCurrentSessionBinding } from '../session/session-manager.js';
4
+ import { getSessionId, setCurrentSessionBinding, setSessionMeta } from '../session/session-manager.js';
5
5
  import { setCurrentChangeId } from '../../shared/change-id.js';
6
- /**
7
- * Per-slice subdirectories created **inside the change-id dir**
8
- * (`.peaks/<change-id>/...`). These are the reviewable
9
- * artifacts and are tracked in git. The `system/` subdir is
10
- * intentionally NOT in this list — it lives under the session
11
- * dir (`.peaks/_runtime/<session-id>/system/`), since it holds
12
- * live sub-agent progress and spawn records, which are ephemeral.
13
- */
14
- const CHANGE_ARTIFACT_SUBDIRECTORIES = [
15
- 'prd/source',
16
- 'prd/requests',
17
- 'ui/requests',
18
- 'rd/requests',
19
- 'qa/test-cases',
20
- 'qa/test-reports',
21
- 'qa/requests',
22
- 'qa/screenshots',
23
- 'sc',
24
- 'txt'
25
- ];
26
- /**
27
- * Per-session subdirectories created **inside the session dir**
28
- * (`.peaks/_runtime/<session-id>/...`). These are the ephemeral
29
- * state and are gitignored.
30
- */
31
- const SESSION_EPHEMERAL_SUBDIRECTORIES = [
32
- 'system'
33
- ];
34
6
  const SESSION_ID_PATTERN = /^\d{4}-\d{2}-\d{2}-[a-z][a-z0-9-]*[a-z0-9]$/;
35
7
  const PROHIBITED_SUFFIXES = ['session', 'work', 'task', 'test', 'temp', 'tmp'];
36
8
  // Auto-generated session ID pattern: YYYY-MM-DD-session-<6位hex>
@@ -77,14 +49,21 @@ export function validateSessionId(sessionId) {
77
49
  }
78
50
  export async function initWorkspace(options) {
79
51
  validateSessionId(options.sessionId);
80
- // Phase 6 refactor (slice 2026-06-05-change-id-as-unit-of-work):
81
- // - Reviewable artifacts (rd/, qa/, prd/, txt/) are created at
52
+ // Phase 6 refactor (slice 2026-06-05-change-id-as-unit-of-work) +
53
+ // slice 006 (2026-06-06-change-folder-simplify-and-lazy-role-subdirs):
54
+ // - Reviewable artifacts (rd/, qa/, prd/, txt/) live at
82
55
  // `.peaks/<change-id>/<role>/` (tracked in git) when a change-id
83
- // is given. This is the canonical home for cross-session content.
56
+ // is given. The role subdirs are NOT pre-created — the writer
57
+ // (e.g. `peaks request init`, `peaks rd`) creates the parent
58
+ // dirs on demand via `mkdirSync(..., { recursive: true })`.
84
59
  // - The session dir `.peaks/_runtime/<session-id>/` (gitignored)
85
- // holds only ephemeral state currently `system/` (live
86
- // sub-agent progress + spawn records). The session id remains
87
- // the binding for that ephemeral state.
60
+ // now holds ONLY the canonical `session.json` metadata. The
61
+ // F3-introduced `system/` subdir is gone slice 006 removes
62
+ // it via `peaks workspace reconcile`; new init calls do not
63
+ // pre-create it.
64
+ // - The change-id dir is created when `--change-id` is given,
65
+ // but its role subdirs (prd/, qa/, rd/, sc/, txt/) are NOT
66
+ // pre-created either — same lazy-mkdir rule.
88
67
  //
89
68
  // The CLI accepts `--change-id <id>` to bind the change. The legacy
90
69
  // session-scoped layout (`.peaks/<session-id>/<role>/<file>`) is
@@ -95,8 +74,8 @@ export async function initWorkspace(options) {
95
74
  const created = [];
96
75
  const alreadyExisted = [];
97
76
  // 1. Create the session dir (canonical location `.peaks/_runtime/<sid>/`)
98
- // with ONLY ephemeral subdirs (`system/`). The session dir is
99
- // gitignored.
77
+ // with NO subdirs. The session dir is gitignored; the role
78
+ // subdirs and the `system/` subdir are gone entirely (slice 006).
100
79
  if (await isDirectory(sessionRoot)) {
101
80
  alreadyExisted.push('.');
102
81
  }
@@ -104,22 +83,20 @@ export async function initWorkspace(options) {
104
83
  await mkdir(sessionRoot, { recursive: true });
105
84
  created.push('.');
106
85
  }
107
- for (const sub of SESSION_EPHEMERAL_SUBDIRECTORIES) {
108
- const full = join(sessionRoot, sub);
109
- if (await isDirectory(full)) {
110
- alreadyExisted.push(sub);
111
- }
112
- else {
113
- await mkdir(full, { recursive: true });
114
- created.push(sub);
115
- }
116
- }
86
+ // 1a. Write the per-session metadata file. Slice 006 makes
87
+ // `.peaks/_runtime/<sid>/session.json` the durable session
88
+ // metadata (the body's source of truth, the `peaks workspace
89
+ // reconcile` discovery source). The file is created on first
90
+ // init and refreshed on every subsequent init. Idempotent.
91
+ setSessionMeta(options.projectRoot, options.sessionId, {});
117
92
  // 2. If a change-id is given, also create the change-id dir at
118
- // `.peaks/<change-id>/` (tracked) with the reviewable subdirs.
119
- // When the caller did NOT specify a change-id, this step is
120
- // skipped reviewable writes for this session are then blocked
121
- // until a change-id is bound (or the user re-runs init with
122
- // `--change-id`). Surfaces in `changeIdAction: 'none'`.
93
+ // `.peaks/<change-id>/` (tracked) with NO role subdirs. The
94
+ // role subdirs (prd/, qa/, rd/, sc/, txt/) are created on demand
95
+ // by the writer at the write site. When the caller did NOT
96
+ // specify a change-id, this step is skipped reviewable writes
97
+ // for this session are then blocked until a change-id is bound
98
+ // (or the user re-runs init with `--change-id`). Surfaces in
99
+ // `changeIdAction: 'none'`.
123
100
  let resolvedChangeId = null;
124
101
  let changeIdAction = 'none';
125
102
  if (options.changeId !== undefined && options.changeId.length > 0) {
@@ -132,16 +109,6 @@ export async function initWorkspace(options) {
132
109
  await mkdir(changeDir, { recursive: true });
133
110
  created.push(resolvedChangeId);
134
111
  }
135
- for (const sub of CHANGE_ARTIFACT_SUBDIRECTORIES) {
136
- const full = join(changeDir, sub);
137
- if (await isDirectory(full)) {
138
- alreadyExisted.push(sub);
139
- }
140
- else {
141
- await mkdir(full, { recursive: true });
142
- created.push(sub);
143
- }
144
- }
145
112
  // 3. Bind the change-id so RD/QA/PRD services know where to write
146
113
  // reviewable artifacts. The binding is a symlink at
147
114
  // `.peaks/_runtime/current-change` pointing at the change-id dir.
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.3.1";
1
+ export declare const CLI_VERSION = "1.3.3";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.3.1";
1
+ export const CLI_VERSION = "1.3.3";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.3.1",
3
+ "version": "1.3.3",
4
4
  "description": "Cross-AI-IDE workflow-gating CLI + skill family (Claude Code shipped, Trae in progress; Codex / Cursor / Qoder / Tongyi Lingma on the roadmap).",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -56,6 +56,7 @@
56
56
  "@colbymchenry/codegraph": "0.7.10",
57
57
  "chalk": "^5.6.2",
58
58
  "commander": "^12.1.0",
59
+ "headroom-ai": "0.22.4",
59
60
  "ora": "^8.2.0",
60
61
  "shadcn": "4.7.0",
61
62
  "terminal-kit": "^3.1.2"