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
@@ -10,36 +10,24 @@ import { getAdapter } from '../ide/ide-registry.js';
10
10
  // values are computed lazily inside each public function call.
11
11
  /** Sentinel substring identifying a Claude-Code gate-enforce hook entry. */
12
12
  export const HOOK_ENFORCE_SENTINEL = 'peaks gate enforce';
13
- /** Sentinel substring identifying a peaks-managed sub-agent-progress hook entry. */
14
- export const HOOK_PROGRESS_SENTINEL = 'peaks progress start';
15
13
  /** Default (claude-code) hook command — kept as a stable export for tests. */
16
14
  export const HOOK_ENFORCE_COMMAND = `peaks gate enforce --project "\${CLAUDE_PROJECT_DIR}"`;
17
- /** Default (claude-code) progress command — kept as a stable export for tests. */
18
- export const HOOK_PROGRESS_COMMAND = `peaks progress start --project "\${CLAUDE_PROJECT_DIR}" --reason "auto-spawn for sub-agent Task" --quiet`;
19
15
  function resolveHookSpec(ide) {
20
16
  const adapter = getAdapter(ide);
21
17
  if (ide === 'claude-code') {
22
18
  return {
23
19
  hookEnforceCommand: `peaks gate enforce --project "\${${adapter.envVar}}"`,
24
- hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
25
20
  hookEnforceSentinel: HOOK_ENFORCE_SENTINEL,
26
- hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
27
21
  hookEnforceMatcher: adapter.toolMatcher, // 'Bash'
28
- hookProgressMatcher: adapter.subAgentToolMatcher, // 'Task' (slice 2026-06-06-sub-agent-spawn-bug-and-decouple — adapter now self-reports sub-agent tool name)
29
- hookEnforceEvent: adapter.hookEvent, // 'PreToolUse'
30
- hookProgressEvent: adapter.hookEvent // 'PreToolUse' for Claude
22
+ hookEnforceEvent: adapter.hookEvent // 'PreToolUse'
31
23
  };
32
24
  }
33
25
  if (ide === 'trae') {
34
26
  return {
35
27
  hookEnforceCommand: `peaks hook handle --project "\${${adapter.envVar}}"`,
36
- hookProgressCommand: `peaks progress start --project "\${${adapter.envVar}}" --reason "auto-spawn for sub-agent ${adapter.subAgentToolMatcher}" --quiet`,
37
28
  hookEnforceSentinel: 'peaks hook handle',
38
- hookProgressSentinel: HOOK_PROGRESS_SENTINEL,
39
29
  hookEnforceMatcher: adapter.toolMatcher, // 'terminal'
40
- hookProgressMatcher: adapter.subAgentToolMatcher, // 'Task' (UNVERIFIED for Trae; matches prior hardcoded literal so byte-level install output is unchanged)
41
- hookEnforceEvent: adapter.hookEvent, // 'beforeToolCall'
42
- hookProgressEvent: adapter.hookEvent // 'beforeToolCall' (no separate progress event yet for Trae)
30
+ hookEnforceEvent: adapter.hookEvent // 'beforeToolCall'
43
31
  };
44
32
  }
45
33
  // Future adapters (codex, cursor, qoder, tongyi-lingma) — not yet registered.
@@ -51,6 +39,10 @@ function resolveHookSpec(ide) {
51
39
  function resolveIde(options) {
52
40
  return options?.ide ?? 'claude-code';
53
41
  }
42
+ /** Slice #013: read the skipProgress opt-in flag. Slice #014: the underlying install no longer emits the progress-start entry, so the flag is effectively a no-op (kept for API stability). */
43
+ function resolveSkipProgress(options) {
44
+ return options?.skipProgress === true;
45
+ }
54
46
  /** Resolve settings root dir for a scope. */
55
47
  function resolveSettingsRoot(scope, projectRoot) {
56
48
  if (scope === 'global')
@@ -101,24 +93,104 @@ function entryIsPeaksManaged(entry, sentinels) {
101
93
  return sentinels.some((sentinel) => cmd.includes(sentinel));
102
94
  });
103
95
  }
96
+ /**
97
+ * Slice #014: read the *actually-installed* peaks-managed hook entries
98
+ * from a settings object. Replaces the pre-#014 `listInstalledEntriesForIde`
99
+ * helper in `hooks-commands.ts`, which returned the IDE-EXPECTED list
100
+ * (a hardcoded 2-entry array per adapter) rather than what was on disk.
101
+ * That bug surfaced when slice #013's local cleanup installed
102
+ * `peaks hooks install --no-progress` (gate-enforce only), but the
103
+ * status command still reported `entries: [Bash, Task]` because the
104
+ * helper didn't read the file.
105
+ *
106
+ * The new helper:
107
+ * 1. reads each `hooks.<event>` array,
108
+ * 2. filters to entries that are peaks-managed for the given IDE
109
+ * (matches the legacy sentinel set: gate-enforce + the no-longer-
110
+ * installed progress-start),
111
+ * 3. returns one `{ matcher, sentinel }` row per entry, taking the
112
+ * FIRST matching sentinel per entry (entries have a single command
113
+ * handler in practice, but the loop tolerates multi-handler
114
+ * entries by taking the first match).
115
+ *
116
+ * Pre-#014 settings.json files that have a stale progress-start entry
117
+ * will see it surface in the result. This is intentional: the status
118
+ * command is the user's tool for "what is on disk right now", and
119
+ * surfacing a stale entry is the only way the user can know to run
120
+ * `peaks hooks install` (which now strips it) or `peaks hooks
121
+ * uninstall` (which removes it).
122
+ */
123
+ export function readInstalledEntriesFromSettings(settings, ide) {
124
+ const sentinels = resolveLegacySentinels(ide);
125
+ // Walk every event key the settings file has, not just the
126
+ // adapter-declared one. A pre-#014 install could have left a
127
+ // progress-start entry on a different event than the gate-enforce
128
+ // entry (Trae: both on beforeToolCall, but a stale install on a
129
+ // future IDE could split them).
130
+ const hooksRoot = settings.hooks;
131
+ if (!hooksRoot || typeof hooksRoot !== 'object' || Array.isArray(hooksRoot))
132
+ return [];
133
+ const result = [];
134
+ for (const eventKey of Object.keys(hooksRoot)) {
135
+ const entries = readHookEntriesFromHooks(hooksRoot, eventKey);
136
+ for (const entry of entries) {
137
+ if (!entryIsPeaksManaged(entry, sentinels))
138
+ continue;
139
+ const matcher = typeof entry.matcher === 'string' ? entry.matcher : '';
140
+ // Find the first matching sentinel inside the entry's command
141
+ // handlers. For each handler, find the first sentinel substring
142
+ // it contains. We pick the first handler's first matching
143
+ // sentinel (entries have a single command in practice).
144
+ const firstHandler = Array.isArray(entry.hooks) ? entry.hooks[0] : undefined;
145
+ const cmd = typeof firstHandler?.command === 'string' ? firstHandler.command : '';
146
+ const sentinel = sentinels.find((s) => cmd.includes(s));
147
+ if (matcher === '' || sentinel === undefined)
148
+ continue;
149
+ result.push({ matcher, sentinel });
150
+ }
151
+ }
152
+ return result;
153
+ }
104
154
  /**
105
155
  * Compute the per-IDE peaks hook entries to merge into the settings file.
106
156
  * Replaces the slice #1 hardcoded `PEAKS_HOOK_ENTRIES` constant; the constant
107
157
  * remains exported (computed for claude-code) for backward compat.
158
+ *
159
+ * Slice #013 (`--no-progress` flag): when `skipProgress` is true, the
160
+ * progress-start entry is omitted from the returned list. Install with this
161
+ * flag will (a) NOT emit the progress hook entry, and (b) will idempotently
162
+ * remove any previously-installed progress entry (sentinel-based merge).
163
+ *
164
+ * Slice #014 (refactor — full removal): only the gate-enforce entry is
165
+ * ever emitted. The `skipProgress` parameter is kept for API stability
166
+ * but is a no-op — the returned list is always single-entry. The progress
167
+ * entry's sentinel is included in the legacy sentinel set so uninstall
168
+ * can find + remove any stale progress-start entry that an older
169
+ * `peaks hooks install` may have written before this slice.
108
170
  */
109
- function resolveHookEntries(ide) {
171
+ function resolveHookEntries(ide, _skipProgress = false) {
110
172
  const spec = resolveHookSpec(ide);
111
173
  return [
112
- { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent },
113
- { sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
174
+ { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
114
175
  ];
115
176
  }
116
- /** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. */
177
+ /**
178
+ * Legacy sentinel set used by uninstall + status to find and remove stale
179
+ * progress-start entries written by pre-#014 installs. The progress-start
180
+ * sentinel is the literal substring that older installs emitted.
181
+ */
182
+ const LEGACY_PROGRESS_START_SENTINEL = 'peaks progress start';
183
+ function resolveLegacySentinels(ide) {
184
+ if (ide === 'trae') {
185
+ return ['peaks hook handle', LEGACY_PROGRESS_START_SENTINEL];
186
+ }
187
+ return [HOOK_ENFORCE_SENTINEL, LEGACY_PROGRESS_START_SENTINEL];
188
+ }
189
+ /** Default (claude-code) peaks-managed hook entries — kept as a stable export for tests. Slice #014: only the gate-enforce entry. */
117
190
  export const PEAKS_HOOK_ENTRIES = (() => {
118
191
  const spec = resolveHookSpec('claude-code');
119
192
  return [
120
- { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent },
121
- { sentinel: spec.hookProgressSentinel, matcher: spec.hookProgressMatcher, command: spec.hookProgressCommand, event: spec.hookProgressEvent }
193
+ { sentinel: spec.hookEnforceSentinel, matcher: spec.hookEnforceMatcher, command: spec.hookEnforceCommand, event: spec.hookEnforceEvent }
122
194
  ];
123
195
  })();
124
196
  function isInstalledForIde(settings, ide) {
@@ -133,8 +205,43 @@ function isInstalledForIde(settings, ide) {
133
205
  }
134
206
  return false;
135
207
  }
208
+ /**
209
+ * Slice #014: detect the "stale progress entry after pre-#014 install"
210
+ * case. The desired shape is gate-enforce-only. If the file currently
211
+ * has a peaks-managed progress-start entry (left behind by a pre-#014
212
+ * install), the install is NOT a no-op — it must strip the stale
213
+ * entry. This helper returns true exactly when the desired shape is
214
+ * fully reflected on disk: gate-enforce present AND no legacy
215
+ * progress-start present.
216
+ */
217
+ function shapeMatchesDesired(settings, ide) {
218
+ const desiredEntries = resolveHookEntries(ide);
219
+ const desiredSentinels = new Set(desiredEntries.map((e) => e.sentinel));
220
+ const allPeaksSentinels = resolveLegacySentinels(ide);
221
+ const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
222
+ for (const eventKey of eventKeys) {
223
+ const present = readHookEventEntries(settings, eventKey);
224
+ const peaksPresent = present.filter((e) => entryIsPeaksManaged(e, allPeaksSentinels));
225
+ // (a) every peaks-managed entry currently on disk must match the
226
+ // desired sentinel set (no stale entries the caller wants removed).
227
+ for (const entry of peaksPresent) {
228
+ const entrySentinels = (entry.hooks ?? []).map((h) => allPeaksSentinels.find((s) => String(h.command ?? '').includes(s))).filter((s) => Boolean(s));
229
+ if (entrySentinels.some((s) => !desiredSentinels.has(s))) {
230
+ return false;
231
+ }
232
+ }
233
+ // (b) every desired entry must be on disk.
234
+ for (const sentinel of desiredSentinels) {
235
+ const has = peaksPresent.some((entry) => (entry.hooks ?? []).some((h) => String(h.command ?? '').includes(sentinel)));
236
+ if (!has)
237
+ return false;
238
+ }
239
+ }
240
+ return true;
241
+ }
136
242
  export function planHookInstall(scope, projectRoot, options) {
137
243
  const ide = resolveIde(options);
244
+ const _skipProgress = resolveSkipProgress(options);
138
245
  const root = resolveSettingsRoot(scope, projectRoot);
139
246
  const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
140
247
  assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
@@ -152,24 +259,29 @@ export function planHookInstall(scope, projectRoot, options) {
152
259
  };
153
260
  }
154
261
  /** Merge all peaks-managed hook entries into settings, preserving all other keys and hooks. */
155
- function withHooksInstalledForIde(settings, ide) {
262
+ function withHooksInstalledForIde(settings, ide, _skipProgress = false) {
156
263
  const existingHooks = (settings.hooks && typeof settings.hooks === 'object' && !Array.isArray(settings.hooks))
157
264
  ? settings.hooks
158
265
  : {};
159
- // Per-IDE entries may map to different events (Trae: both on beforeToolCall;
160
- // Claude: both on PreToolUse). Group by event so each event array is
161
- // independently merged.
266
+ // Per-IDE entries may map to different events (Trae: gate-enforce on
267
+ // beforeToolCall; Claude: gate-enforce on PreToolUse). Group by event
268
+ // so each event array is independently merged.
162
269
  const ourByEvent = new Map();
163
270
  for (const spec of resolveHookEntries(ide)) {
164
271
  const list = ourByEvent.get(spec.event) ?? [];
165
272
  list.push(spec);
166
273
  ourByEvent.set(spec.event, list);
167
274
  }
168
- const sentinels = resolveHookEntries(ide).map((e) => e.sentinel);
275
+ // Slice #014: the legacy sentinel set includes the progress-start
276
+ // sentinel so a pre-#014 install's stale progress-start entry is
277
+ // stripped by the filter (the file converges on the new
278
+ // gate-enforce-only shape, idempotently). The desired set (passed
279
+ // in below) only contains the gate-enforce sentinel.
280
+ const allSentinels = resolveLegacySentinels(ide);
169
281
  const nextHooks = { ...existingHooks };
170
282
  for (const [eventKey, ourEntries] of ourByEvent) {
171
283
  const existing = readHookEntriesFromHooks(nextHooks, eventKey);
172
- const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry, sentinels));
284
+ const nonPeaks = existing.filter((entry) => !entryIsPeaksManaged(entry, allSentinels));
173
285
  const ourFormatted = ourEntries.map((spec) => ({
174
286
  matcher: spec.matcher,
175
287
  hooks: [{ type: 'command', command: spec.command }]
@@ -189,17 +301,25 @@ function withHooksInstalledForIde(settings, ide) {
189
301
  }
190
302
  export function applyHookInstall(scope, projectRoot, options) {
191
303
  const ide = resolveIde(options);
304
+ const _skipProgress = resolveSkipProgress(options);
192
305
  const root = resolveSettingsRoot(scope, projectRoot);
193
306
  const settingsPath = resolveSettingsPath(scope, ide, projectRoot);
194
307
  assertSafeSettingsPathCompat(scope, ide, root, settingsPath);
195
308
  const exists = existsSync(settingsPath);
196
309
  const settings = exists ? readJsonObjectFile(settingsPath) : {};
197
310
  const spec = resolveHookSpec(ide);
311
+ // Slice #014: `alreadyInstalled` reflects the FULL desired shape
312
+ // (gate-enforce-only + no stale progress-start entry). Pre-#014
313
+ // installs that left a progress-start entry behind will be treated
314
+ // as not-yet-installed, so the merge strips the stale entry on the
315
+ // next install call. This is the only path that converges the file
316
+ // on the new shape; pure presence-checks are insufficient.
317
+ const alreadyInstalled = shapeMatchesDesired(settings, ide);
198
318
  const baseResult = {
199
319
  scope,
200
320
  settingsPath,
201
321
  exists,
202
- alreadyInstalled: isInstalledForIde(settings, ide),
322
+ alreadyInstalled,
203
323
  desiredCommand: spec.hookEnforceCommand,
204
324
  sentinel: spec.hookEnforceSentinel,
205
325
  matcher: spec.hookEnforceMatcher
@@ -220,7 +340,12 @@ export function removeHookInstall(scope, projectRoot, options) {
220
340
  }
221
341
  const settings = readJsonObjectFile(settingsPath);
222
342
  const existingHooks = settings.hooks ?? {};
223
- const sentinels = resolveHookEntries(ide).map((e) => e.sentinel);
343
+ // Slice #014: uninstall must remove the gate-enforce entry AND any
344
+ // legacy progress-start entry that a pre-#014 install left behind.
345
+ // The legacy sentinel set covers both shapes so the uninstall
346
+ // converges the file on "no peaks-managed entries", regardless of
347
+ // what shape the file was in when the user ran uninstall.
348
+ const sentinels = resolveLegacySentinels(ide);
224
349
  const eventKeys = new Set(resolveHookEntries(ide).map((e) => e.event));
225
350
  let removedAny = false;
226
351
  const nextHooks = { ...existingHooks };
@@ -11,14 +11,6 @@
11
11
  * @returns Next available number (1, 2, 3, ...)
12
12
  */
13
13
  export declare function getNextNumber(dirPath: string): number;
14
- /**
15
- * Build a numbered filename from a number and description.
16
- * Format: 001-description-slug.md
17
- *
18
- * @param number - The file number (will be zero-padded to 3 digits)
19
- * @param description - Human-readable description (converted to kebab-case slug)
20
- * @returns Formatted filename like "001-feature-name.md"
21
- */
22
14
  export declare function buildNumberedFilename(number: number, description: string): string;
23
15
  /**
24
16
  * Get the next numbered file path in a directory.
@@ -34,13 +34,23 @@ export function getNextNumber(dirPath) {
34
34
  * @param description - Human-readable description (converted to kebab-case slug)
35
35
  * @returns Formatted filename like "001-feature-name.md"
36
36
  */
37
+ // Windows supports up to 255 chars per filename component (and 260 for the
38
+ // full path). Pre-#015 the slug was silently truncated to 50 chars, which
39
+ // produced orphaned artefacts (the on-disk file no longer matched the
40
+ // request-id and the state machine could not find it). 255 is the OS-level
41
+ // ceiling; if a requestId exceeds that, `mkdir` / `writeFile` will surface a
42
+ // real ENAMETOOLONG error instead of a silent mismatch. We reserve 4 chars
43
+ // for the `<NNN>-` numeric prefix and 3 chars for the `.md` suffix, so
44
+ // the slug may be at most 248 chars (giving 4 + 248 + 3 = 255 total).
45
+ const MAX_FILENAME_LENGTH = 255;
46
+ const MAX_FILENAME_SLUG_LENGTH = MAX_FILENAME_LENGTH - 7;
37
47
  export function buildNumberedFilename(number, description) {
38
48
  const padded = String(number).padStart(3, '0');
39
49
  const slug = description
40
50
  .toLowerCase()
41
51
  .replace(/[^a-z0-9]+/g, '-')
42
52
  .replace(/^-|-$/g, '')
43
- .slice(0, 50); // Limit slug length
53
+ .slice(0, MAX_FILENAME_SLUG_LENGTH);
44
54
  return `${padded}-${slug}.md`;
45
55
  }
46
56
  /**
@@ -1 +1 @@
1
- export declare const CLI_VERSION = "1.3.4";
1
+ export declare const CLI_VERSION = "1.3.5";
@@ -1 +1 @@
1
- export const CLI_VERSION = "1.3.4";
1
+ export const CLI_VERSION = "1.3.5";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-cli",
3
- "version": "1.3.4",
3
+ "version": "1.3.5",
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",
@@ -40,7 +40,7 @@ issues 3 dispatch calls in a **single message**:
40
40
  peaks sub-agent dispatch qa-business \
41
41
  --prompt "<qa-business contract below, plus runtime args: project=<repo>,
42
42
  session-id=<sid>, request-id=<rid>.
43
- Write your evidence at .peaks/<sid>/qa/test-reports/<rid>.md
43
+ Write your evidence at .peaks/_runtime/<sid>/qa/test-reports/<rid>.md
44
44
  and return ONLY the path. While running, call
45
45
  peaks sub-agent heartbeat --record <dispatchRecordPath>
46
46
  --status running --progress <pct> --note '<text>' at least every 30s;
@@ -48,11 +48,11 @@ peaks sub-agent dispatch qa-business \
48
48
  --request-id <rid> --session-id <sid> --project <repo> --json
49
49
 
50
50
  peaks sub-agent dispatch qa-perf \
51
- --prompt "<qa-perf contract below, plus runtime args; output .peaks/<sid>/qa/performance-findings.md>" \
51
+ --prompt "<qa-perf contract below, plus runtime args; output .peaks/_runtime/<sid>/qa/performance-findings.md>" \
52
52
  --request-id <rid> --session-id <sid> --project <repo> --json
53
53
 
54
54
  peaks sub-agent dispatch qa-security \
55
- --prompt "<qa-security contract below, plus runtime args; output .peaks/<sid>/qa/security-findings.md>" \
55
+ --prompt "<qa-security contract below, plus runtime args; output .peaks/_runtime/<sid>/qa/security-findings.md>" \
56
56
  --request-id <rid> --session-id <sid> --project <repo> --json
57
57
  ```
58
58
 
@@ -80,9 +80,9 @@ string. The recommended names above are hints, not a hard list.
80
80
 
81
81
  | Sub-agent | Reads | Writes | Must not depend on |
82
82
  |---|---|---|---|
83
- | `qa-business` (or subdivisions) | PRD body, RD planning, codegraph, project scan, existing system | `qa/test-reports/<rid>.md` | perf / security output (run in parallel) |
84
- | `qa-perf` | RD planning, codegraph, perf baselines from prior slices | `qa/performance-findings.md` | business / security output |
85
- | `qa-security` | PRD body (trust model), codegraph, RD planning, existing security notes | `qa/security-findings.md` | business / perf output |
83
+ | `qa-business` (or subdivisions) | PRD body, RD planning, codegraph, project scan, existing system | `.peaks/_runtime/<sid>/qa/test-reports/<rid>.md` | perf / security output (run in parallel) |
84
+ | `qa-perf` | RD planning, codegraph, perf baselines from prior slices | `.peaks/_runtime/<sid>/qa/performance-findings.md` | business / security output |
85
+ | `qa-security` | PRD body (trust model), codegraph, RD planning, existing security notes | `.peaks/_runtime/<sid>/qa/security-findings.md` | business / perf output |
86
86
 
87
87
  The reducer merges all 3 outputs into the final QA verdict. None of
88
88
  the 3 sub-agents reads another sub-agent's output (no peer-to-peer
@@ -119,21 +119,6 @@ On the first presence:set in a project, ensure the out-of-band status bar is ins
119
119
  peaks statusline install --project <repo> # idempotent; skips if already installed
120
120
  ```
121
121
 
122
- **Auto-spawn a progress watch terminal once per slice (BLOCKING on the first phase transition).** The user opens a fresh VSCode window per slice, not per Bash call. Without a separate progress terminal the user has no live signal that the sub-agent is alive — the only signal they have is the static statusline. So at the first phase transition of every slice, fire `peaks progress start` ONCE. The CLI auto-spawns a new terminal tab running `peaks progress watch` and the user can close the new tab at any time. Do NOT re-invoke on every phase change — one per slice is the contract. The LLM-side cost of this one invocation is one Bash call plus a small JSON envelope; the watch side is a 1s file poll that does not consume LLM tokens.
123
-
124
- ```bash
125
- # At the first phase transition of a slice (after the first
126
- # peaks progress step), fire the watch:
127
- peaks progress start --project <repo> --reason "rd-implementing for <rid>"
128
-
129
- # On every subsequent phase transition, only update the
130
- # progress file — the watch is already running in another tab:
131
- peaks progress step --project <repo> --request-id <rid> --role rd \
132
- --step "running pnpm test" --phase running
133
- ```
134
-
135
- If `peaks progress start` is unsupported on the current platform (no terminal emulator, headless container, etc.) it returns a recoverable error envelope. Surface that in the RD handoff so the user knows the auto-spawn failed; the sub-agent can still emit `peaks progress step` writes that the user reads from the on-disk file. The auto-spawn is convenience, not a gate.
136
-
137
122
  Read persistent project memory via CLI (durable, LLM-authored memories):
138
123
 
139
124
  ```bash
@@ -20,12 +20,12 @@ peaks skill runbook peaks-solo --json
20
20
  peaks workspace init --project <repo> --json
21
21
  peaks workspace reconcile --project <repo> --json
22
22
  peaks scan archetype --project <repo> --json
23
- # → copy archetype, frontendOnly, signals into .peaks/<session-id>/rd/project-scan.md (Peaks-Cli Gate A)
24
- # → copy libraries[] into .peaks/<session-id>/rd/project-scan.md under `## Library versions`
23
+ # → copy archetype, frontendOnly, signals into .peaks/_runtime/<session-id>/rd/project-scan.md (Peaks-Cli Gate A)
24
+ # → copy libraries[] into .peaks/_runtime/<session-id>/rd/project-scan.md under `## Library versions`
25
25
  peaks scan libraries --project <repo> --json
26
26
  # → if archetype != greenfield AND archetype != unknown:
27
27
  peaks scan existing-system --project <repo> --json
28
- # → copy tokens, sources, conventions, inconsistencies into .peaks/<session-id>/system/existing-system.md (Peaks-Cli Gate A.5)
28
+ # → copy tokens, sources, conventions, inconsistencies into .peaks/_runtime/<session-id>/system/existing-system.md (Peaks-Cli Gate A.5)
29
29
 
30
30
  # 1. Peaks-Cli Standards preflight + apply
31
31
  # Run dry-run first to inspect deltas, then APPLY. In full-auto and swarm modes,
@@ -55,7 +55,7 @@ peaks request transition <rid> --role prd --state handed-off --project <repo> --
55
55
 
56
56
  # 3. Peaks-Cli Swarm parallel — sub-agent fan-out (peaks sub-agent dispatch, NOT Skill tool)
57
57
  # Solo computes the swarm plan from --type + frontendOnly + frontend-keyword scan,
58
- # writes it to .peaks/<sid>/sc/swarm-plan.json, then launches one
58
+ # writes it to .peaks/_runtime/<sid>/sc/swarm-plan.json, then launches one
59
59
  # `peaks sub-agent dispatch <role>` call per sub-agent in the same message.
60
60
  # See "Peaks-Cli Swarm parallel phase" above for the full decision table and the
61
61
  # prompt template; the role's required artefact paths are listed there.
@@ -80,36 +80,36 @@ peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate swarm-
80
80
  # Step 0 / presence, plus the runtime args (rid / sid / mode / type / paths).
81
81
  # 3c. After fan-out, Solo restores presence once and runs Gate B (ls checks):
82
82
  peaks skill presence:set peaks-solo --project <repo> --mode <mode> --gate swarm-converged
83
- ls .peaks/<sid>/prd/requests/<rid>.md # PRD artefact must exist (Gate B hard)
84
- # feature / refactor → ls .peaks/<sid>/rd/tech-doc.md
85
- # bugfix → ls .peaks/<sid>/rd/bug-analysis.md
86
- ls .peaks/<sid>/qa/test-cases/<rid>.md # QA test-cases (skipped for docs|chore)
83
+ ls .peaks/_runtime/<sid>/prd/requests/<rid>.md # PRD artefact must exist (Gate B hard)
84
+ # feature / refactor → ls .peaks/_runtime/<sid>/rd/tech-doc.md
85
+ # bugfix → ls .peaks/_runtime/<sid>/rd/bug-analysis.md
86
+ ls .peaks/_runtime/<sid>/qa/test-cases/<rid>.md # QA test-cases (skipped for docs|chore)
87
87
  # ui (only when in plan):
88
- ls .peaks/<sid>/ui/design-draft.md 2>&1 # non-blocking (Gate B info)
88
+ ls .peaks/_runtime/<sid>/ui/design-draft.md 2>&1 # non-blocking (Gate B info)
89
89
  # Apply the degradation rules in the main SKILL.md if any artefact is missing.
90
90
  # → Peaks-Cli Gate B convergence check. Assisted/Strict: [CONFIRM]
91
91
 
92
92
  # 4. Peaks-Cli RD planning artifact (the file required by the prerequisite gate)
93
- # feature / refactor → write .peaks/<id>/rd/tech-doc.md
94
- # bugfix → write .peaks/<id>/rd/bug-analysis.md
93
+ # feature / refactor → write .peaks/_runtime/<id>/rd/tech-doc.md
94
+ # bugfix → write .peaks/_runtime/<id>/rd/bug-analysis.md
95
95
  # config → no planning artifact required at this state
96
96
  # docs / chore → no planning artifact required
97
97
  peaks request transition <rid> --role rd --state implemented --project <repo> --json
98
98
 
99
99
  # 5. Peaks-Cli Code review + security review BEFORE qa-handoff transition.
100
100
  # Produce the evidence files the CLI gate enforces:
101
- # - .peaks/<id>/rd/code-review.md (CRITICAL/HIGH findings + fixes; required for feature/bugfix/refactor)
102
- # - .peaks/<id>/rd/security-review.md (required for feature/bugfix/refactor/config)
101
+ # - .peaks/_runtime/<id>/rd/code-review.md (CRITICAL/HIGH findings + fixes; required for feature/bugfix/refactor)
102
+ # - .peaks/_runtime/<id>/rd/security-review.md (required for feature/bugfix/refactor/config)
103
103
  # Then transition. If --type is docs/chore the gate is empty and the transition is unguarded.
104
104
  peaks request transition <rid> --role rd --state qa-handoff --project <repo> --json
105
105
 
106
106
  # 6. Peaks-Cli QA validation (AUTO-PROCEED from RD in full-auto)
107
107
  # Before each QA transition, produce the evidence files the CLI gate enforces:
108
- # Before qa:running → .peaks/<id>/qa/test-cases/<rid>.md
108
+ # Before qa:running → .peaks/_runtime/<id>/qa/test-cases/<rid>.md
109
109
  peaks request transition <rid> --role qa --state running --project <repo> --json
110
- # Before qa:verdict-issued → .peaks/<id>/qa/test-reports/<rid>.md
111
- # + .peaks/<id>/qa/security-findings.md
112
- # + .peaks/<id>/qa/performance-findings.md (feature/refactor only)
110
+ # Before qa:verdict-issued → .peaks/_runtime/<id>/qa/test-reports/<rid>.md
111
+ # + .peaks/_runtime/<id>/qa/security-findings.md
112
+ # + .peaks/_runtime/<id>/qa/performance-findings.md (feature/refactor only)
113
113
  peaks request transition <rid> --role qa --state verdict-issued --project <repo> --json
114
114
  # → Peaks-Cli Gate D check. Assisted/Strict: [CONFIRM]
115
115
 
@@ -135,12 +135,12 @@ peaks openspec archive <cid> --project <repo> --apply --json
135
135
  peaks workspace reconcile --project <repo> --apply --older-than 7
136
136
 
137
137
  # 10. Peaks-Cli TXT handoff — invoke peaks-txt which embeds memory markers and extracts
138
- # peaks-txt writes the handoff capsule to .peaks/<id>/txt/handoff.md. Inside the
138
+ # peaks-txt writes the handoff capsule to .peaks/_runtime/<id>/txt/handoff.md. Inside the
139
139
  # capsule body, peaks-txt embeds <!-- peaks-memory:start --> blocks for every
140
140
  # stable project fact surfaced this session.
141
141
  #
142
142
  # 10a. Skill-side scan (do this BEFORE the AskUserQuestion below):
143
- # grep -n "peaks-memory:start" .peaks/<id>/txt/handoff.md
143
+ # grep -n "peaks-memory:start" .peaks/_runtime/<id>/txt/handoff.md
144
144
  # Record the count. This is the skill doing the work, not a CLI command —
145
145
  # we deliberately do not ship a `peaks memory scan` because the LLM is
146
146
  # the only consumer and the LLM has grep.
@@ -148,7 +148,7 @@ peaks workspace reconcile --project <repo> --apply --older-than 7
148
148
  # 10b. AskUserQuestion (only if 10a returned count >= 1):
149
149
  # "The TXT handoff has N peaks-memory:start blocks. Persist to .peaks/memory/?
150
150
  # (a) Apply all — `peaks memory extract --project <repo>
151
- # --artifact .peaks/<id>/txt/handoff.md --apply --json`
151
+ # --artifact .peaks/_runtime/<id>/txt/handoff.md --apply --json`
152
152
  # (b) Apply selectively — re-edit handoff.md first, then re-apply
153
153
  # (c) Skip for now — blocks stay in the handoff only, no .peaks/memory/ write"
154
154
  # If 10a returned 0 AND the session surfaced a stable project fact
@@ -156,7 +156,7 @@ peaks workspace reconcile --project <repo> --apply --older-than 7
156
156
  # back and embed at least one block before Solo can advance.
157
157
 
158
158
  # 10c. After the user picks (a) or (b), run:
159
- peaks memory extract --project <repo> --artifact .peaks/<id>/txt/handoff.md --apply --json
159
+ peaks memory extract --project <repo> --artifact .peaks/_runtime/<id>/txt/handoff.md --apply --json
160
160
  # --apply is REQUIRED to write .peaks/memory/; without it the command only
161
161
  # previews. The extract regenerates index.json in the same call.
162
162
 
@@ -16,7 +16,7 @@ Return a compact JSON envelope — do not write prose.
16
16
  ## Hard prohibitions
17
17
  - Do NOT call Skill(skill="..."). You are the role.
18
18
  - Do NOT call `peaks skill presence:set` — the main loop owns .peaks/.active-skill.json.
19
- If you need to record state, write to .peaks/<session-id>/system/sub-agent-<role>.json.
19
+ If you need to record state, write to .peaks/_runtime/<session-id>/system/sub-agent-<role>.json.
20
20
  - Do NOT commit, push, install hooks, or apply settings.json mutations.
21
21
  - Do NOT ask the user interactive questions. If you need clarification, return
22
22
  {"status":"blocked","blockedReason":"<text>"} and let the main loop handle it.
@@ -47,14 +47,14 @@ Steps:
47
47
  3. Read <project-scan-path> for component library / CSS framework.
48
48
  4. Run the prototype fidelity gate: Figma file? PRD visuals? Headed browser?
49
49
  5. Write TWO artefacts:
50
- - .peaks/<sid>/ui/design-draft.md
51
- - .peaks/<sid>/ui/requests/<rid>.md
50
+ - .peaks/_runtime/<sid>/ui/design-draft.md
51
+ - .peaks/_runtime/<sid>/ui/requests/<rid>.md
52
52
  6. Return:
53
53
  {
54
54
  "role": "ui",
55
55
  "rid": "<rid>",
56
56
  "status": "ok" | "blocked" | "skipped",
57
- "artefacts": [".peaks/<sid>/ui/design-draft.md", ".peaks/<sid>/ui/requests/<rid>.md"],
57
+ "artefacts": [".peaks/_runtime/<sid>/ui/design-draft.md", ".peaks/_runtime/<sid>/ui/requests/<rid>.md"],
58
58
  "warnings": [],
59
59
  "blockedReason": null
60
60
  }
@@ -80,15 +80,15 @@ Steps:
80
80
  into rd/project-scan.md.
81
81
  5. Read <existing-system-path> if archetype is legacy-*.
82
82
  6. Write the type-appropriate planning artefact:
83
- - feature | refactor → .peaks/<sid>/rd/tech-doc.md
84
- - bugfix → .peaks/<sid>/rd/bug-analysis.md
83
+ - feature | refactor → .peaks/_runtime/<sid>/rd/tech-doc.md
84
+ - bugfix → .peaks/_runtime/<sid>/rd/bug-analysis.md
85
85
  - config | docs | chore → no planning artefact required. Return skipped.
86
86
  7. Return:
87
87
  {
88
88
  "role": "rd-planning",
89
89
  "rid": "<rid>",
90
90
  "status": "ok" | "blocked" | "skipped",
91
- "artefacts": [".peaks/<sid>/rd/tech-doc.md"], // or [] when skipped
91
+ "artefacts": [".peaks/_runtime/<sid>/rd/tech-doc.md"], // or [] when skipped
92
92
  "warnings": [],
93
93
  "blockedReason": null
94
94
  }
@@ -106,14 +106,14 @@ Steps:
106
106
  2. peaks request show <rid> --role prd --project <repo> --json
107
107
  3. peaks request show <rid> --role rd --project <repo> --json
108
108
  4. Read <project-scan-path>.
109
- 5. Write .peaks/<sid>/qa/test-cases/<rid>.md with test cases linked to PRD
109
+ 5. Write .peaks/_runtime/<sid>/qa/test-cases/<rid>.md with test cases linked to PRD
110
110
  acceptance items (use **Acceptance:** A1, A2 style).
111
111
  6. Return:
112
112
  {
113
113
  "role": "qa-test-cases",
114
114
  "rid": "<rid>",
115
115
  "status": "ok" | "blocked" | "skipped",
116
- "artefacts": [".peaks/<sid>/qa/test-cases/<rid>.md"],
116
+ "artefacts": [".peaks/_runtime/<sid>/qa/test-cases/<rid>.md"],
117
117
  "warnings": [],
118
118
  "blockedReason": null
119
119
  }
@@ -1,51 +0,0 @@
1
- /**
2
- * Best-effort close of a spawned `peaks progress watch`
3
- * window. Used by `peaks progress close` (manual escape
4
- * hatch) and by the watch-side auto-exit when the sub-agent
5
- * hits a terminal phase.
6
- *
7
- * The close is best-effort by design: we never throw from
8
- * individual signals. One failed close primitive is a UX
9
- * paper cut, not a correctness bug — the caller still clears
10
- * the spawn record after this returns.
11
- *
12
- * Cross-platform strategy:
13
- *
14
- * - macOS: pkill the watch process by command pattern
15
- * (matches the project path, so we never close the
16
- * wrong window), then send AppleScript to Terminal.app
17
- * to close the window by `custom title`. Terminal.app
18
- * is the dominant macOS terminal, and `custom title` is
19
- * the only stable identifier we can target from outside
20
- * the running shell.
21
- * - Linux: pkill the watch process, then try `wmctrl -c
22
- * peaks-cli-progress` to close the terminal window by
23
- * WM class (set in `progress start` for alacritty /
24
- * kitty; gnome-terminal / konsole / xfce4-terminal
25
- * close on their own when the child exits). wmctrl is
26
- * not always installed; we silently no-op on
27
- * "command not found" (exit 127) and surface other
28
- * errors as warnings.
29
- * - Windows: `taskkill /F /FI "WINDOWTITLE eq
30
- * peaks-cli:*"` to kill the cmd.exe wrapper. We use
31
- * the title prefix because the exact title includes the
32
- * `--reason` suffix which we do not know here.
33
- *
34
- * The kill is intentionally not a single primitive (e.g.
35
- * `process.kill(-pid, 'SIGTERM')` on the process group).
36
- * The launcher's PID is the spawn-time PID (osascript on
37
- * macOS, gnome-terminal on Linux), not the long-lived
38
- * watch process — and the long-lived process is the one we
39
- * actually need to terminate to make the terminal close.
40
- * Targeting by command pattern (pkill) + window title
41
- * (AppleScript / wmctrl / taskkill) is more reliable than
42
- * PID chasing across detached children.
43
- */
44
- import type { ProgressSpawnRecord } from '../../services/progress/progress-service.js';
45
- export type KillSpawnedTerminalResult = {
46
- /** Each signal that was successfully sent. */
47
- signals: string[];
48
- /** Soft failures (e.g. pkill matched no process, wmctrl missing). */
49
- warnings: string[];
50
- };
51
- export declare function killSpawnedTerminal(record: ProgressSpawnRecord, canonicalProjectRoot: string, currentPlatform: NodeJS.Platform): Promise<KillSpawnedTerminalResult>;