peaks-cli 1.3.4 → 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.
Files changed (34) hide show
  1. package/dist/src/cli/commands/hook-handle.d.ts +2 -2
  2. package/dist/src/cli/commands/hook-handle.js +5 -10
  3. package/dist/src/cli/commands/hooks-commands.js +44 -29
  4. package/dist/src/cli/commands/project-commands.js +7 -1
  5. package/dist/src/cli/commands/workspace-commands.js +1 -2
  6. package/dist/src/cli/program.js +3 -2
  7. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +45 -40
  8. package/dist/src/services/dispatch/sub-agent-dispatcher.js +25 -20
  9. package/dist/src/services/ide/adapters/claude-code-adapter.js +0 -2
  10. package/dist/src/services/ide/adapters/trae-adapter.js +2 -4
  11. package/dist/src/services/ide/ide-types.d.ts +1 -16
  12. package/dist/src/services/progress/progress-service.d.ts +23 -103
  13. package/dist/src/services/progress/progress-service.js +24 -137
  14. package/dist/src/services/scan/file-size-scan.d.ts +4 -0
  15. package/dist/src/services/scan/file-size-scan.js +32 -3
  16. package/dist/src/services/skills/hooks-settings-service.d.ts +57 -5
  17. package/dist/src/services/skills/hooks-settings-service.js +153 -28
  18. package/dist/src/shared/incrementing-number.d.ts +0 -8
  19. package/dist/src/shared/incrementing-number.js +11 -1
  20. package/dist/src/shared/version.d.ts +1 -1
  21. package/dist/src/shared/version.js +1 -1
  22. package/package.json +1 -1
  23. package/skills/peaks-qa/references/qa-fanout-contract.md +6 -6
  24. package/skills/peaks-rd/SKILL.md +0 -15
  25. package/skills/peaks-solo/references/runbook.md +21 -21
  26. package/skills/peaks-solo/references/swarm-dispatch-contract.md +9 -9
  27. package/dist/src/cli/commands/progress-close-kill.d.ts +0 -51
  28. package/dist/src/cli/commands/progress-close-kill.js +0 -152
  29. package/dist/src/cli/commands/progress-commands.d.ts +0 -3
  30. package/dist/src/cli/commands/progress-commands.js +0 -379
  31. package/dist/src/cli/commands/progress-start-spawn.d.ts +0 -59
  32. package/dist/src/cli/commands/progress-start-spawn.js +0 -140
  33. package/dist/src/cli/commands/progress-watch-render.d.ts +0 -80
  34. package/dist/src/cli/commands/progress-watch-render.js +0 -308
@@ -1,34 +1,31 @@
1
1
  /**
2
- * Sub-agent progress surfacing for the RD/QA sub-agents in
3
- * `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
4
- * `peaks progress step` CLI) writes a stable JSON file at
5
- * `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
6
- * `peaks progress watch` CLI polls this file in a separate
7
- * terminal tab and renders elapsed / spinner / sub-step. The
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 watch side polls a local file, not the LLM. Zero token
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
- * Net: the LLM pays a one-time < 10 token cost per slice to
25
- * give the user real-time progress visibility. The user pays
26
- * zero manual setup.
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. Those are concerns of the CLI layer (../cli/commands/
31
- * progress-commands.ts and hooks-settings-service.ts).
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 watch banner, the auto-spawn helper, the close command).
122
- * This MUST agree with `progressPath` — the read/write helpers
123
- * resolve through `progressPath` and use the session sub-directory,
124
- * so the displayed path does too. Without this agreement the
125
- * watch banner would point at a path the file is never written
126
- * to, and the user would `cat` an empty file.
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 surfacing for the RD/QA sub-agents in
3
- * `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
4
- * `peaks progress step` CLI) writes a stable JSON file at
5
- * `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
6
- * `peaks progress watch` CLI polls this file in a separate
7
- * terminal tab and renders elapsed / spinner / sub-step. The
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 watch side polls a local file, not the LLM. Zero token
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
- * Net: the LLM pays a one-time < 10 token cost per slice to
25
- * give the user real-time progress visibility. The user pays
26
- * zero manual setup.
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. Those are concerns of the CLI layer (../cli/commands/
31
- * progress-commands.ts and hooks-settings-service.ts).
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, unlinkSync, writeFileSync } from 'node:fs';
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 watch banner, the auto-spawn helper, the close command).
188
- * This MUST agree with `progressPath` — the read/write helpers
189
- * resolve through `progressPath` and use the session sub-directory,
190
- * so the displayed path does too. Without this agreement the
191
- * watch banner would point at a path the file is never written
192
- * to, and the user would `cat` an empty file.
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
- const trackedRaw = execFileSync('git', ['-C', projectRoot, 'diff', '--name-only', baseRef], { encoding: 'utf8' });
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: files.length,
64
+ checkedFiles,
65
+ deletedFiles,
37
66
  violations
38
67
  };
39
68
  }
@@ -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;