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.
- package/README.md +6 -2
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/slice-commands.js +4 -2
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workspace-commands.js +70 -14
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
- package/dist/src/services/artifacts/request-artifact-service.js +116 -76
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/doctor/doctor-service.d.ts +62 -0
- package/dist/src/services/doctor/doctor-service.js +276 -1
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +120 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +137 -28
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/slice/slice-check-service.js +20 -1
- package/dist/src/services/slice/slice-check-types.d.ts +9 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/workspace/migrate-service.js +124 -2
- package/dist/src/services/workspace/migrate-types.d.ts +50 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
- package/dist/src/services/workspace/reconcile-service.js +267 -48
- package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
- package/dist/src/services/workspace/workspace-service.js +29 -62
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +58 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +52 -9
- package/skills/peaks-solo/SKILL.md +83 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +19 -0
- 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
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
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
|
-
|
|
70
|
-
if (!
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
let stat;
|
|
77
|
+
const collect = (root) => {
|
|
78
|
+
if (!existsSync(root))
|
|
79
|
+
return;
|
|
80
|
+
let names;
|
|
74
81
|
try {
|
|
75
|
-
|
|
76
|
-
// malicious symlink that points outside the project root).
|
|
77
|
-
stat = lstatSync(dir);
|
|
82
|
+
names = readdirSync(root);
|
|
78
83
|
}
|
|
79
84
|
catch {
|
|
80
|
-
|
|
85
|
+
return;
|
|
81
86
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
|
|
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
|
-
|
|
116
|
+
lastActivity = statSync(metaPath).mtimeMs;
|
|
97
117
|
}
|
|
98
118
|
catch {
|
|
99
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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.
|
|
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
|
|
86
|
-
//
|
|
87
|
-
//
|
|
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
|
|
99
|
-
//
|
|
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
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
|
119
|
-
//
|
|
120
|
-
//
|
|
121
|
-
//
|
|
122
|
-
//
|
|
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
|
+
export declare const CLI_VERSION = "1.3.3";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.3.
|
|
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.
|
|
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"
|