peaks-cli 1.3.3 → 1.3.5
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/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/hook-handle.d.ts +2 -2
- package/dist/src/cli/commands/hook-handle.js +5 -10
- package/dist/src/cli/commands/hooks-commands.js +44 -29
- package/dist/src/cli/commands/project-commands.js +15 -5
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +1 -2
- package/dist/src/cli/program.js +3 -2
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
- package/dist/src/services/ide/adapters/claude-code-adapter.js +27 -2
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +19 -11
- package/dist/src/services/ide/adapters/trae-adapter.js +45 -19
- package/dist/src/services/ide/hook-protocol.d.ts +7 -4
- package/dist/src/services/ide/hook-protocol.js +7 -4
- package/dist/src/services/ide/ide-types.d.ts +61 -16
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +23 -103
- package/dist/src/services/progress/progress-service.js +24 -137
- package/dist/src/services/scan/file-size-scan.d.ts +4 -0
- package/dist/src/services/scan/file-size-scan.js +32 -3
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
- package/dist/src/services/skills/hooks-settings-service.js +153 -28
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/shared/incrementing-number.d.ts +0 -8
- package/dist/src/shared/incrementing-number.js +11 -1
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +1 -1
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +104 -62
- package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
- package/skills/peaks-rd/SKILL.md +88 -73
- package/skills/peaks-solo/SKILL.md +52 -22
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/runbook.md +21 -21
- package/skills/peaks-solo/references/sub-agent-dispatch.md +44 -1
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
- package/skills/peaks-ui/SKILL.md +18 -9
- package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
- package/dist/src/cli/commands/progress-close-kill.js +0 -152
- package/dist/src/cli/commands/progress-commands.d.ts +0 -3
- package/dist/src/cli/commands/progress-commands.js +0 -379
- package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
- package/dist/src/cli/commands/progress-start-spawn.js +0 -140
- package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
- package/dist/src/cli/commands/progress-watch-render.js +0 -308
|
@@ -1,34 +1,31 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sub-agent progress
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `peaks progress start` CLI auto-spawns the watch in a new
|
|
9
|
-
* terminal window so the user does not have to remember to do
|
|
10
|
-
* it.
|
|
2
|
+
* Sub-agent progress file for the RD/QA sub-agents in `peaks-solo`'s
|
|
3
|
+
* Swarm phase. A sub-agent (or the LLM via the `peaks progress step`
|
|
4
|
+
* CLI) writes a stable JSON file at
|
|
5
|
+
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The dispatch +
|
|
6
|
+
* heartbeat flow (slice #009 + #010) reads this file as the source
|
|
7
|
+
* of truth for "is this sub-agent still alive?".
|
|
11
8
|
*
|
|
12
9
|
* Token cost design (the binding constraint of this feature):
|
|
13
10
|
* - The LLM side (step CLI) writes the file at most once per
|
|
14
11
|
* phase transition. That is approximately one Bash call per
|
|
15
12
|
* RD/QA sub-step. In a typical 5-step sub-agent slice the
|
|
16
13
|
* cost is < 10 output tokens.
|
|
17
|
-
* - The
|
|
18
|
-
* cost.
|
|
19
|
-
* - The auto-spawn side (start CLI) is invoked once per
|
|
20
|
-
* session by the LLM at the first phase transition. One
|
|
21
|
-
* Bash call. The user closes the new terminal at any time;
|
|
22
|
-
* no further side effects.
|
|
14
|
+
* - The dispatch side polls the file via `peaks sub-agent
|
|
15
|
+
* heartbeat`, not the LLM. Zero token cost.
|
|
23
16
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* Slice #014: the legacy `peaks progress start|watch|close` auto-spawn
|
|
18
|
+
* surface is DELETED. With dispatch + heartbeat (slice #009 + #010), the
|
|
19
|
+
* same sub-agent now runs in the same IDE/terminal as the main loop, so
|
|
20
|
+
* a separate watch window is dead weight. The only remaining consumer
|
|
21
|
+
* of this module is the `peaks progress step` write path + the
|
|
22
|
+
* dispatcher's read-back of the progress file.
|
|
27
23
|
*
|
|
28
24
|
* This module is pure filesystem. It does NOT import the LLM
|
|
29
25
|
* harness, does NOT spawn terminals, and does NOT talk to any
|
|
30
|
-
* IPC.
|
|
31
|
-
*
|
|
26
|
+
* IPC. The dispatch-side consumption is in
|
|
27
|
+
* `src/services/dispatch/sub-agent-dispatcher.ts` and reads the
|
|
28
|
+
* progress file via `subAgentProgressPath`.
|
|
32
29
|
*/
|
|
33
30
|
export type SubAgentProgressPhase = 'starting' | 'running' | 'verifying' | 'completing' | 'finished' | 'failed' | 'idle';
|
|
34
31
|
export type SubAgentProgressStep = {
|
|
@@ -118,88 +115,11 @@ export declare function resolveProgressProjectRoot(override: string | undefined,
|
|
|
118
115
|
/**
|
|
119
116
|
* Compute the absolute path to the progress file for a given
|
|
120
117
|
* project root, for callers that need to display / fs.watch it
|
|
121
|
-
* (the
|
|
122
|
-
* This MUST agree with `progressPath` — the read/write
|
|
123
|
-
* resolve through `progressPath` and use the session
|
|
124
|
-
* so the displayed path does too. Without this
|
|
125
|
-
*
|
|
126
|
-
*
|
|
118
|
+
* (the dispatcher's read-back, the LLM-side step write banner,
|
|
119
|
+
* etc.). This MUST agree with `progressPath` — the read/write
|
|
120
|
+
* helpers resolve through `progressPath` and use the session
|
|
121
|
+
* sub-directory, so the displayed path does too. Without this
|
|
122
|
+
* agreement the dispatcher's read-back would point at a path
|
|
123
|
+
* the writer never touches.
|
|
127
124
|
*/
|
|
128
125
|
export declare function subAgentProgressPath(projectRoot: string): string;
|
|
129
|
-
/**
|
|
130
|
-
* Compute the absolute path to the spawn record for a given
|
|
131
|
-
* project root. Exported so `peaks progress close` (and the
|
|
132
|
-
* start command's success payload) can advertise the on-disk
|
|
133
|
-
* location without re-deriving the session sub-directory.
|
|
134
|
-
*/
|
|
135
|
-
export declare function subAgentSpawnPath(projectRoot: string): string;
|
|
136
|
-
export type ProgressSpawnRecord = {
|
|
137
|
-
version: 1;
|
|
138
|
-
sessionId: string;
|
|
139
|
-
pid: number;
|
|
140
|
-
platform: NodeJS.Platform;
|
|
141
|
-
command: string;
|
|
142
|
-
args: string[];
|
|
143
|
-
spawnedAt: string;
|
|
144
|
-
reason?: string;
|
|
145
|
-
/** The title we asked the terminal emulator to set. */
|
|
146
|
-
windowTitle: string;
|
|
147
|
-
};
|
|
148
|
-
export type WriteSpawnRecordOptions = {
|
|
149
|
-
projectRoot: string;
|
|
150
|
-
pid: number;
|
|
151
|
-
platform: NodeJS.Platform;
|
|
152
|
-
command: string;
|
|
153
|
-
args: string[];
|
|
154
|
-
reason?: string;
|
|
155
|
-
windowTitle: string;
|
|
156
|
-
};
|
|
157
|
-
export declare function writeSpawnRecord(options: WriteSpawnRecordOptions): ProgressSpawnRecord | null;
|
|
158
|
-
export type ReadSpawnRecordResult = {
|
|
159
|
-
ok: true;
|
|
160
|
-
data: ProgressSpawnRecord;
|
|
161
|
-
path: string;
|
|
162
|
-
} | {
|
|
163
|
-
ok: false;
|
|
164
|
-
reason: 'no-binding' | 'no-spawn-record' | 'invalid-json';
|
|
165
|
-
};
|
|
166
|
-
export declare function readSpawnRecord(projectRoot: string): ReadSpawnRecordResult;
|
|
167
|
-
/**
|
|
168
|
-
* Idempotency check for the `peaks progress start` CLI (and the Task-tool
|
|
169
|
-
* PreToolUse hook that fires it on every Task call). Returns `true` when a
|
|
170
|
-
* recent spawn record exists for this project's session, meaning a watch
|
|
171
|
-
* window was opened within the last `ttlMs` milliseconds. The LLM-side
|
|
172
|
-
* caller treats `true` as "no-op — a watch is already running somewhere".
|
|
173
|
-
*
|
|
174
|
-
* Why TTL instead of pid liveness: the spawned terminal's pid is the
|
|
175
|
-
* osascript/gnome-terminal process, which exits as soon as it spawns the
|
|
176
|
-
* nested shell. Checking pid liveness gives false negatives (the process
|
|
177
|
-
* is gone by design). The spawn record is the canonical "watch was opened
|
|
178
|
-
* for this session" marker. After `ttlMs` the record is treated as stale
|
|
179
|
-
* (e.g. the user closed the window long ago) and a fresh start proceeds.
|
|
180
|
-
*
|
|
181
|
-
* `ttlMs` defaults to 5 minutes — long enough to cover a typical sub-agent
|
|
182
|
-
* slice (RD parallel fan-out + QA verdict), short enough that a user who
|
|
183
|
-
* closed the window gets a fresh one on the next Task call.
|
|
184
|
-
*/
|
|
185
|
-
export type RecentSpawnCheck = {
|
|
186
|
-
recent: boolean;
|
|
187
|
-
reason: 'recent-spawn' | 'no-spawn-record' | 'no-binding' | 'invalid-json' | 'stale-spawn' | 'process-dead';
|
|
188
|
-
/** When reason is 'recent-spawn' or 'stale-spawn', the spawn record. */
|
|
189
|
-
record?: ProgressSpawnRecord;
|
|
190
|
-
ageMs?: number;
|
|
191
|
-
};
|
|
192
|
-
export declare function isRecentSpawn(projectRoot: string, now?: () => number, ttlMs?: number): RecentSpawnCheck;
|
|
193
|
-
export declare function clearSpawnRecord(projectRoot: string): boolean;
|
|
194
|
-
export type PhaseClosingTrigger = 'finished' | 'failed';
|
|
195
|
-
/**
|
|
196
|
-
* True if a transition into the given phase should auto-close
|
|
197
|
-
* the spawned watch window. `finished` and `failed` both
|
|
198
|
-
* indicate the sub-agent is done; a `blocked` verdict on a
|
|
199
|
-
* `finished` step is intentionally NOT a close trigger
|
|
200
|
-
* because a blocked slice usually means the user needs to
|
|
201
|
-
* read the watch output before deciding what to do. The CLI
|
|
202
|
-
* layer reads `data.current.phase`, not the verdict, so this
|
|
203
|
-
* helper is the only close-decision source of truth.
|
|
204
|
-
*/
|
|
205
|
-
export declare function phaseAutoClosesSpawn(phase: SubAgentProgressPhase): boolean;
|
|
@@ -1,36 +1,33 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sub-agent progress
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
* `peaks progress start` CLI auto-spawns the watch in a new
|
|
9
|
-
* terminal window so the user does not have to remember to do
|
|
10
|
-
* it.
|
|
2
|
+
* Sub-agent progress file for the RD/QA sub-agents in `peaks-solo`'s
|
|
3
|
+
* Swarm phase. A sub-agent (or the LLM via the `peaks progress step`
|
|
4
|
+
* CLI) writes a stable JSON file at
|
|
5
|
+
* `.peaks/_sub_agents/<sid>/subagent-progress.json`. The dispatch +
|
|
6
|
+
* heartbeat flow (slice #009 + #010) reads this file as the source
|
|
7
|
+
* of truth for "is this sub-agent still alive?".
|
|
11
8
|
*
|
|
12
9
|
* Token cost design (the binding constraint of this feature):
|
|
13
10
|
* - The LLM side (step CLI) writes the file at most once per
|
|
14
11
|
* phase transition. That is approximately one Bash call per
|
|
15
12
|
* RD/QA sub-step. In a typical 5-step sub-agent slice the
|
|
16
13
|
* cost is < 10 output tokens.
|
|
17
|
-
* - The
|
|
18
|
-
* cost.
|
|
19
|
-
* - The auto-spawn side (start CLI) is invoked once per
|
|
20
|
-
* session by the LLM at the first phase transition. One
|
|
21
|
-
* Bash call. The user closes the new terminal at any time;
|
|
22
|
-
* no further side effects.
|
|
14
|
+
* - The dispatch side polls the file via `peaks sub-agent
|
|
15
|
+
* heartbeat`, not the LLM. Zero token cost.
|
|
23
16
|
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
17
|
+
* Slice #014: the legacy `peaks progress start|watch|close` auto-spawn
|
|
18
|
+
* surface is DELETED. With dispatch + heartbeat (slice #009 + #010), the
|
|
19
|
+
* same sub-agent now runs in the same IDE/terminal as the main loop, so
|
|
20
|
+
* a separate watch window is dead weight. The only remaining consumer
|
|
21
|
+
* of this module is the `peaks progress step` write path + the
|
|
22
|
+
* dispatcher's read-back of the progress file.
|
|
27
23
|
*
|
|
28
24
|
* This module is pure filesystem. It does NOT import the LLM
|
|
29
25
|
* harness, does NOT spawn terminals, and does NOT talk to any
|
|
30
|
-
* IPC.
|
|
31
|
-
*
|
|
26
|
+
* IPC. The dispatch-side consumption is in
|
|
27
|
+
* `src/services/dispatch/sub-agent-dispatcher.ts` and reads the
|
|
28
|
+
* progress file via `subAgentProgressPath`.
|
|
32
29
|
*/
|
|
33
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
30
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
34
31
|
import { dirname, join, resolve } from 'node:path';
|
|
35
32
|
import { getSessionIdCanonical } from '../session/session-manager.js';
|
|
36
33
|
import { findProjectRoot } from '../config/config-safety.js';
|
|
@@ -43,7 +40,6 @@ import { findProjectRoot } from '../config/config-safety.js';
|
|
|
43
40
|
// --apply` (see `migrateSubAgentState` in reconcile-service.ts).
|
|
44
41
|
const SUB_AGENTS_DIR = '_sub_agents';
|
|
45
42
|
const PROGRESS_FILE_NAME = 'subagent-progress.json';
|
|
46
|
-
const SPAWN_FILE_NAME = 'progress-spawn.json';
|
|
47
43
|
function progressPath(projectRoot) {
|
|
48
44
|
// The progress file lives at `.peaks/_sub_agents/<sid>/subagent-progress.json`.
|
|
49
45
|
// The leading `_sub_agents/` is a meta-classification (mirrors `_runtime/`,
|
|
@@ -184,122 +180,13 @@ export function resolveProgressProjectRoot(override, cwd) {
|
|
|
184
180
|
/**
|
|
185
181
|
* Compute the absolute path to the progress file for a given
|
|
186
182
|
* project root, for callers that need to display / fs.watch it
|
|
187
|
-
* (the
|
|
188
|
-
* This MUST agree with `progressPath` — the read/write
|
|
189
|
-
* resolve through `progressPath` and use the session
|
|
190
|
-
* so the displayed path does too. Without this
|
|
191
|
-
*
|
|
192
|
-
*
|
|
183
|
+
* (the dispatcher's read-back, the LLM-side step write banner,
|
|
184
|
+
* etc.). This MUST agree with `progressPath` — the read/write
|
|
185
|
+
* helpers resolve through `progressPath` and use the session
|
|
186
|
+
* sub-directory, so the displayed path does too. Without this
|
|
187
|
+
* agreement the dispatcher's read-back would point at a path
|
|
188
|
+
* the writer never touches.
|
|
193
189
|
*/
|
|
194
190
|
export function subAgentProgressPath(projectRoot) {
|
|
195
191
|
return progressPath(resolve(projectRoot));
|
|
196
192
|
}
|
|
197
|
-
/**
|
|
198
|
-
* Compute the absolute path to the spawn record for a given
|
|
199
|
-
* project root. Exported so `peaks progress close` (and the
|
|
200
|
-
* start command's success payload) can advertise the on-disk
|
|
201
|
-
* location without re-deriving the session sub-directory.
|
|
202
|
-
*/
|
|
203
|
-
export function subAgentSpawnPath(projectRoot) {
|
|
204
|
-
const sessionId = getSessionIdCanonical(projectRoot);
|
|
205
|
-
const subDir = sessionId ?? 'unbound';
|
|
206
|
-
return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
|
|
207
|
-
}
|
|
208
|
-
function spawnRecordPath(projectRoot) {
|
|
209
|
-
const sessionId = getSessionIdCanonical(projectRoot);
|
|
210
|
-
const subDir = sessionId ?? 'unbound';
|
|
211
|
-
return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
|
|
212
|
-
}
|
|
213
|
-
export function writeSpawnRecord(options) {
|
|
214
|
-
const sessionId = getSessionIdCanonical(options.projectRoot);
|
|
215
|
-
if (sessionId === null)
|
|
216
|
-
return null;
|
|
217
|
-
const now = nowIso();
|
|
218
|
-
const record = {
|
|
219
|
-
version: 1,
|
|
220
|
-
sessionId,
|
|
221
|
-
pid: options.pid,
|
|
222
|
-
platform: options.platform,
|
|
223
|
-
command: options.command,
|
|
224
|
-
args: options.args,
|
|
225
|
-
spawnedAt: now,
|
|
226
|
-
...(options.reason !== undefined ? { reason: options.reason } : {}),
|
|
227
|
-
windowTitle: options.windowTitle
|
|
228
|
-
};
|
|
229
|
-
const path = spawnRecordPath(options.projectRoot);
|
|
230
|
-
ensureParentDir(path);
|
|
231
|
-
writeFileSync(path, JSON.stringify(record, null, 2) + '\n', 'utf8');
|
|
232
|
-
return record;
|
|
233
|
-
}
|
|
234
|
-
export function readSpawnRecord(projectRoot) {
|
|
235
|
-
const sessionId = getSessionIdCanonical(projectRoot);
|
|
236
|
-
if (sessionId === null)
|
|
237
|
-
return { ok: false, reason: 'no-binding' };
|
|
238
|
-
const path = spawnRecordPath(projectRoot);
|
|
239
|
-
if (!existsSync(path))
|
|
240
|
-
return { ok: false, reason: 'no-spawn-record' };
|
|
241
|
-
try {
|
|
242
|
-
const data = JSON.parse(readFileSync(path, 'utf8'));
|
|
243
|
-
if (data.version !== 1 || typeof data.pid !== 'number') {
|
|
244
|
-
return { ok: false, reason: 'invalid-json' };
|
|
245
|
-
}
|
|
246
|
-
return { ok: true, data, path };
|
|
247
|
-
}
|
|
248
|
-
catch {
|
|
249
|
-
return { ok: false, reason: 'invalid-json' };
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
export function isRecentSpawn(projectRoot, now = Date.now, ttlMs = 5 * 60 * 1000) {
|
|
253
|
-
const result = readSpawnRecord(projectRoot);
|
|
254
|
-
if (!result.ok) {
|
|
255
|
-
return { recent: false, reason: result.reason };
|
|
256
|
-
}
|
|
257
|
-
const record = result.data;
|
|
258
|
-
const spawnedMs = Date.parse(record.spawnedAt);
|
|
259
|
-
if (Number.isNaN(spawnedMs)) {
|
|
260
|
-
// Treat unparseable timestamps as a stale record so the next start
|
|
261
|
-
// proceeds normally. This is the conservative choice — the alternative
|
|
262
|
-
// (treating unparseable as "always recent") would block legitimate
|
|
263
|
-
// re-spawns forever.
|
|
264
|
-
return { recent: false, reason: 'stale-spawn', record };
|
|
265
|
-
}
|
|
266
|
-
const ageMs = now() - spawnedMs;
|
|
267
|
-
if (ageMs < 0) {
|
|
268
|
-
// Clock skew or future-dated record: trust the record as "recent" so
|
|
269
|
-
// we do not double-spawn. Same conservative default as a 0-age record.
|
|
270
|
-
return { recent: true, reason: 'recent-spawn', record, ageMs: 0 };
|
|
271
|
-
}
|
|
272
|
-
if (ageMs >= ttlMs) {
|
|
273
|
-
return { recent: false, reason: 'stale-spawn', record, ageMs };
|
|
274
|
-
}
|
|
275
|
-
return { recent: true, reason: 'recent-spawn', record, ageMs };
|
|
276
|
-
}
|
|
277
|
-
export function clearSpawnRecord(projectRoot) {
|
|
278
|
-
const path = spawnRecordPath(projectRoot);
|
|
279
|
-
if (!existsSync(path))
|
|
280
|
-
return false;
|
|
281
|
-
try {
|
|
282
|
-
unlinkSync(path);
|
|
283
|
-
return true;
|
|
284
|
-
}
|
|
285
|
-
catch {
|
|
286
|
-
return false;
|
|
287
|
-
}
|
|
288
|
-
}
|
|
289
|
-
const PHASES_THAT_AUTO_CLOSE = new Set([
|
|
290
|
-
'finished',
|
|
291
|
-
'failed'
|
|
292
|
-
]);
|
|
293
|
-
/**
|
|
294
|
-
* True if a transition into the given phase should auto-close
|
|
295
|
-
* the spawned watch window. `finished` and `failed` both
|
|
296
|
-
* indicate the sub-agent is done; a `blocked` verdict on a
|
|
297
|
-
* `finished` step is intentionally NOT a close trigger
|
|
298
|
-
* because a blocked slice usually means the user needs to
|
|
299
|
-
* read the watch output before deciding what to do. The CLI
|
|
300
|
-
* layer reads `data.current.phase`, not the verdict, so this
|
|
301
|
-
* helper is the only close-decision source of truth.
|
|
302
|
-
*/
|
|
303
|
-
export function phaseAutoClosesSpawn(phase) {
|
|
304
|
-
return PHASES_THAT_AUTO_CLOSE.has(phase);
|
|
305
|
-
}
|
|
@@ -7,6 +7,10 @@ export type FileSizeScanResult = {
|
|
|
7
7
|
ok: boolean;
|
|
8
8
|
threshold: number;
|
|
9
9
|
checkedFiles: number;
|
|
10
|
+
/** Files that appeared in `git diff` but no longer exist on disk (e.g.
|
|
11
|
+
* deleted in the working tree). Pre-#015 the scan crashed on these via
|
|
12
|
+
* ENOENT; now they are reported here as informational data. */
|
|
13
|
+
deletedFiles: string[];
|
|
10
14
|
violations: FileSizeViolation[];
|
|
11
15
|
};
|
|
12
16
|
export type FileSizeScanOptions = {
|
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
import { execFileSync } from 'node:child_process';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
2
|
+
import { existsSync, readFileSync, statSync } from 'node:fs';
|
|
3
3
|
import { join } from 'node:path';
|
|
4
4
|
export const DEFAULT_FILE_SIZE_THRESHOLD = 800;
|
|
5
5
|
function getChangedFiles(projectRoot, baseRef) {
|
|
6
6
|
try {
|
|
7
|
-
|
|
7
|
+
// --diff-filter=AM keeps only Added + Modified entries. Deleted files
|
|
8
|
+
// (--diff-filter=D) are intentionally excluded: they have no on-disk
|
|
9
|
+
// body to count, and a refactor that deletes large files is exactly
|
|
10
|
+
// when the gate should NOT block. Slice #015 fix.
|
|
11
|
+
const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', '--diff-filter=AM', baseRef], { encoding: 'utf8' });
|
|
8
12
|
const tracked = trackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
9
13
|
const untrackedRaw = execFileSync('git', ['-C', projectRoot, 'ls-files', '--others', '--exclude-standard'], { encoding: 'utf8' });
|
|
10
14
|
const untracked = untrackedRaw.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
@@ -23,8 +27,32 @@ export function scanFileSize(options) {
|
|
|
23
27
|
const threshold = options.threshold ?? DEFAULT_FILE_SIZE_THRESHOLD;
|
|
24
28
|
const files = getChangedFiles(options.projectRoot, baseRef);
|
|
25
29
|
const violations = [];
|
|
30
|
+
const deletedFiles = [];
|
|
31
|
+
let checkedFiles = 0;
|
|
26
32
|
for (const file of files) {
|
|
27
33
|
const absolute = join(options.projectRoot, file);
|
|
34
|
+
// Pre-#015: readFileSync threw ENOENT for files that appear in
|
|
35
|
+
// `git diff --name-only` but no longer exist on disk (e.g. a refactor
|
|
36
|
+
// that deletes source files). That aborted the entire
|
|
37
|
+
// `peaks request transition rd → implemented` flow with
|
|
38
|
+
// `code: PREREQUISITES_MISSING`. Now we skip missing paths — a
|
|
39
|
+
// deleted file has no lines to count. Belt-and-braces: the
|
|
40
|
+
// `getChangedFiles` filter above already excludes `--diff-filter=D`,
|
|
41
|
+
// but a manually-passed `baseRef` (tests) or a path that was
|
|
42
|
+
// untracked-then-deleted still flows through here, so the
|
|
43
|
+
// existsSync guard stays as a second line of defense.
|
|
44
|
+
if (!existsSync(absolute)) {
|
|
45
|
+
try {
|
|
46
|
+
const st = statSync(absolute);
|
|
47
|
+
if (!st.isFile())
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
deletedFiles.push(file);
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
checkedFiles += 1;
|
|
28
56
|
const lines = countLines(absolute);
|
|
29
57
|
if (lines > threshold) {
|
|
30
58
|
violations.push({ file, lines });
|
|
@@ -33,7 +61,8 @@ export function scanFileSize(options) {
|
|
|
33
61
|
return {
|
|
34
62
|
ok: violations.length === 0,
|
|
35
63
|
threshold,
|
|
36
|
-
checkedFiles
|
|
64
|
+
checkedFiles,
|
|
65
|
+
deletedFiles,
|
|
37
66
|
violations
|
|
38
67
|
};
|
|
39
68
|
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function getSessionDir(projectRoot: string, sessionId: string): string;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Canonical session-directory resolver.
|
|
3
|
+
*
|
|
4
|
+
* As of slice 2026-06-05-peaks-runtime-layer the per-session workspace
|
|
5
|
+
* lives at `<root>/.peaks/_runtime/<sessionId>/` (NOT at the legacy
|
|
6
|
+
* `<root>/.peaks/<sessionId>/` location). All **write** paths MUST route
|
|
7
|
+
* through this helper. The legacy top-level path is preserved as a
|
|
8
|
+
* back-compat **read** fallback only (see
|
|
9
|
+
* `src/services/artifacts/request-artifact-service.ts:662` etc.).
|
|
10
|
+
*
|
|
11
|
+
* The corresponding test in
|
|
12
|
+
* `tests/unit/services/session/session-dir-canonical.test.ts` enforces
|
|
13
|
+
* two invariants:
|
|
14
|
+
*
|
|
15
|
+
* (a) `getSessionDir(root, sid)` returns `<root>/.peaks/_runtime/<sid>`.
|
|
16
|
+
* (b) A static scan of `src/` flags any direct join of `.peaks` +
|
|
17
|
+
* `sessionId` that does NOT route through this resolver. The
|
|
18
|
+
* back-compat **read** sites are excluded by explicit allow-list.
|
|
19
|
+
*
|
|
20
|
+
* @param projectRoot - Absolute path to the project root.
|
|
21
|
+
* @param sessionId - The session identifier (e.g. `2026-06-06-session-5b1095`).
|
|
22
|
+
* @returns Absolute path to the canonical session directory.
|
|
23
|
+
*/
|
|
24
|
+
import { join } from 'node:path';
|
|
25
|
+
export function getSessionDir(projectRoot, sessionId) {
|
|
26
|
+
return join(projectRoot, '.peaks', '_runtime', sessionId);
|
|
27
|
+
}
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding, type SessionInfo, type SessionMeta } from './session-manager.js';
|
|
2
|
+
export { getSessionDir } from './getSessionDir.js';
|
|
@@ -1 +1,2 @@
|
|
|
1
1
|
export { ensureSession, getSessionId, getCurrentSessionDir, listSessions, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas, getProjectScanPath, hasProjectScan, setCurrentSessionBinding, rotateSessionBinding } from './session-manager.js';
|
|
2
|
+
export { getSessionDir } from './getSessionDir.js';
|
|
@@ -38,15 +38,36 @@ export type HookInstallOptions = {
|
|
|
38
38
|
* Throws if the IDE is not registered in the adapter registry.
|
|
39
39
|
*/
|
|
40
40
|
readonly ide?: IdeId;
|
|
41
|
+
/**
|
|
42
|
+
* Slice #013 (bugfix — peaks hooks install --no-progress): when `true`,
|
|
43
|
+
* skip emitting the progress-start PreToolUse hook entry while still
|
|
44
|
+
* installing the gate-enforce entry. The progress-start hook auto-spawns
|
|
45
|
+
* a new terminal running `peaks progress watch`; with dispatch +
|
|
46
|
+
* heartbeat (slice #009 + #010) that auto-spawn is dead weight. Default
|
|
47
|
+
* `false` preserves the pre-slice install shape (both entries). The
|
|
48
|
+
* sentinel-based install is idempotent: re-running with `skipProgress:
|
|
49
|
+
* true` over a settings.json that previously had the progress entry
|
|
50
|
+
* installed will remove that entry. `uninstall` honors the same flag
|
|
51
|
+
* so it can find and remove both entries when both are present and
|
|
52
|
+
* only the gate-enforce entry when only the gate-enforce is present.
|
|
53
|
+
*
|
|
54
|
+
* Slice #014 (refactor — full removal of legacy progress-start surface):
|
|
55
|
+
* the field is preserved for API stability, but the underlying
|
|
56
|
+
* install only ever emits the gate-enforce entry. The progress-start
|
|
57
|
+
* entry is no longer installed regardless of this flag's value. The
|
|
58
|
+
* legacy `peaks progress start|watch|close` CLI surface is gone
|
|
59
|
+
* (replaced by `peaks sub-agent dispatch|heartbeat|share`); the hook
|
|
60
|
+
* entry would have been pointing at a `peaks progress start` that no
|
|
61
|
+
* longer exists. The sentinel `peaks progress start` constant is still
|
|
62
|
+
* exported (some tests + back-compat reads rely on it) but no new
|
|
63
|
+
* hook entries use it.
|
|
64
|
+
*/
|
|
65
|
+
readonly skipProgress?: boolean;
|
|
41
66
|
};
|
|
42
67
|
/** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
|
|
43
68
|
export declare const HOOK_ENFORCE_SENTINEL = "peaks gate enforce";
|
|
44
|
-
/** Sentinel substring identifying a peaks-managed sub-agent-progress hook entry. */
|
|
45
|
-
export declare const HOOK_PROGRESS_SENTINEL = "peaks progress start";
|
|
46
69
|
/** Default (claude-code) hook command — kept as a stable export for tests. */
|
|
47
70
|
export declare const HOOK_ENFORCE_COMMAND = "peaks gate enforce --project \"${CLAUDE_PROJECT_DIR}\"";
|
|
48
|
-
/** Default (claude-code) progress command — kept as a stable export for tests. */
|
|
49
|
-
export declare const HOOK_PROGRESS_COMMAND = "peaks progress start --project \"${CLAUDE_PROJECT_DIR}\" --reason \"auto-spawn for sub-agent Task\" --quiet";
|
|
50
71
|
export type HookInstallPlan = {
|
|
51
72
|
scope: HookScope;
|
|
52
73
|
settingsPath: string;
|
|
@@ -70,6 +91,37 @@ export type HookStatus = {
|
|
|
70
91
|
exists: boolean;
|
|
71
92
|
installed: boolean;
|
|
72
93
|
};
|
|
94
|
+
/**
|
|
95
|
+
* Slice #014: read the *actually-installed* peaks-managed hook entries
|
|
96
|
+
* from a settings object. Replaces the pre-#014 `listInstalledEntriesForIde`
|
|
97
|
+
* helper in `hooks-commands.ts`, which returned the IDE-EXPECTED list
|
|
98
|
+
* (a hardcoded 2-entry array per adapter) rather than what was on disk.
|
|
99
|
+
* That bug surfaced when slice #013's local cleanup installed
|
|
100
|
+
* `peaks hooks install --no-progress` (gate-enforce only), but the
|
|
101
|
+
* status command still reported `entries: [Bash, Task]` because the
|
|
102
|
+
* helper didn't read the file.
|
|
103
|
+
*
|
|
104
|
+
* The new helper:
|
|
105
|
+
* 1. reads each `hooks.<event>` array,
|
|
106
|
+
* 2. filters to entries that are peaks-managed for the given IDE
|
|
107
|
+
* (matches the legacy sentinel set: gate-enforce + the no-longer-
|
|
108
|
+
* installed progress-start),
|
|
109
|
+
* 3. returns one `{ matcher, sentinel }` row per entry, taking the
|
|
110
|
+
* FIRST matching sentinel per entry (entries have a single command
|
|
111
|
+
* handler in practice, but the loop tolerates multi-handler
|
|
112
|
+
* entries by taking the first match).
|
|
113
|
+
*
|
|
114
|
+
* Pre-#014 settings.json files that have a stale progress-start entry
|
|
115
|
+
* will see it surface in the result. This is intentional: the status
|
|
116
|
+
* command is the user's tool for "what is on disk right now", and
|
|
117
|
+
* surfacing a stale entry is the only way the user can know to run
|
|
118
|
+
* `peaks hooks install` (which now strips it) or `peaks hooks
|
|
119
|
+
* uninstall` (which removes it).
|
|
120
|
+
*/
|
|
121
|
+
export declare function readInstalledEntriesFromSettings(settings: Record<string, unknown>, ide: IdeId): ReadonlyArray<{
|
|
122
|
+
matcher: string;
|
|
123
|
+
sentinel: string;
|
|
124
|
+
}>;
|
|
73
125
|
/** A typed descriptor for a single peaks-managed hook entry. */
|
|
74
126
|
export type PeaksHookEntry = {
|
|
75
127
|
sentinel: string;
|
|
@@ -77,7 +129,7 @@ export type PeaksHookEntry = {
|
|
|
77
129
|
command: string;
|
|
78
130
|
event: string;
|
|
79
131
|
};
|
|
80
|
-
/** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. */
|
|
132
|
+
/** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. Slice #014: only the gate-enforce entry. */
|
|
81
133
|
export declare const PEAKS_HOOK_ENTRIES: ReadonlyArray<PeaksHookEntry>;
|
|
82
134
|
export declare function planHookInstall(scope: HookScope, projectRoot?: string, options?: HookInstallOptions): HookInstallPlan;
|
|
83
135
|
export declare function applyHookInstall(scope: HookScope, projectRoot?: string, options?: HookInstallOptions): HookInstallResult;
|