peaks-loop 4.0.17 → 4.0.19

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 (26) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/cli/commands/doctor/invoke-from-code.d.ts +10 -0
  3. package/dist/cli/commands/doctor/invoke-from-code.js +30 -0
  4. package/dist/cli/commands/primer-command.d.ts +59 -0
  5. package/dist/cli/commands/primer-command.js +160 -0
  6. package/dist/cli/commands/sub-agent/detached.d.ts +29 -0
  7. package/dist/cli/commands/sub-agent/detached.js +60 -0
  8. package/dist/cli/commands/vendor-detect.d.ts +10 -0
  9. package/dist/cli/commands/vendor-detect.js +16 -0
  10. package/dist/cli/program.js +8 -0
  11. package/dist/services/config/config-safety.d.ts +23 -0
  12. package/dist/services/config/config-safety.js +62 -0
  13. package/dist/services/dispatch/dispatch-record-writer.d.ts +86 -9
  14. package/dist/services/dispatch/dispatch-record-writer.js +58 -10
  15. package/dist/services/skills/hooks-settings-service.js +21 -2
  16. package/dist/services/skills/{outer-cache-hook-constants.d.ts → session-start-hook-constants.d.ts} +20 -0
  17. package/dist/services/skills/{outer-cache-hook-constants.js → session-start-hook-constants.js} +20 -0
  18. package/dist/services/skills/skill-statusline-renderer.d.ts +50 -1
  19. package/dist/services/skills/skill-statusline-renderer.js +96 -8
  20. package/dist/services/skills/skill-statusline-service.d.ts +41 -0
  21. package/dist/services/skills/skill-statusline-service.js +56 -6
  22. package/package.json +5 -4
  23. package/skills/bee/peaks-qa/SKILL.md +4 -0
  24. package/skills/bee/peaks-rd/SKILL.md +4 -0
  25. package/skills/peaks-code/SKILL.md +3 -0
  26. package/skills/peaks-code/references/sub-agent-dispatch.md +16 -0
@@ -1,4 +1,5 @@
1
- import { resolve } from 'node:path';
1
+ import { readFileSync } from 'node:fs';
2
+ import { join, resolve } from 'node:path';
2
3
  import { findProjectRoot } from '../config/config-safety.js';
3
4
  import { decideCompactStatusline } from '../compact-statusline/compact-statusline-service.js';
4
5
  import { getSessionIdCanonical } from '../session/session-manager.js';
@@ -30,6 +31,48 @@ import { readActiveDispatchIndex } from '../dispatch/dispatch-record-writer.js';
30
31
  * documented back-compat path.
31
32
  */
32
33
  const STALE_THRESHOLD_MS = 24 * 60 * 60 * 1000;
34
+ /**
35
+ * Slice rid-statusline-24h-overlay (2026-08-10): name-distinct from
36
+ * the canonical `read24hState` in `src/services/24h-mode/store.ts:108`.
37
+ * The canonical reader calls `coerceSnapshot` (which throws on
38
+ * malformed shapes via `24H_STATE_INVALID`); this overlay reader
39
+ * returns `null` for ANY malformed shape (per PRD AC-3 — never
40
+ * throw across the statusline boundary).
41
+ *
42
+ * Returns null when:
43
+ * - `projectRoot` or `sessionId` is empty
44
+ * - the file does not exist (ENOENT)
45
+ * - JSON.parse fails (corrupt file)
46
+ * - root is not a non-array object
47
+ * - `state` is missing, non-string, or empty string
48
+ */
49
+ export function read24hOverlay(projectRoot, sessionId) {
50
+ if (!projectRoot || !sessionId)
51
+ return null;
52
+ const path = join(projectRoot, '.peaks', '_runtime', sessionId, '24h-state.json');
53
+ let raw;
54
+ try {
55
+ raw = readFileSync(path, 'utf8');
56
+ }
57
+ catch {
58
+ // ENOENT, EACCES, EISDIR — all treated as "no overlay"
59
+ return null;
60
+ }
61
+ let parsed;
62
+ try {
63
+ parsed = JSON.parse(raw);
64
+ }
65
+ catch {
66
+ // corrupt JSON — graceful null (PRD AC-3)
67
+ return null;
68
+ }
69
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
70
+ return null;
71
+ const obj = parsed;
72
+ if (typeof obj['state'] !== 'string' || obj['state'].length === 0)
73
+ return null;
74
+ return { state: obj['state'] };
75
+ }
33
76
  function resolveCwdFromStdin(stdin) {
34
77
  const fromWorkspace = stdin?.workspace?.current_dir ?? stdin?.workspace?.project_dir;
35
78
  if (typeof fromWorkspace === 'string' && fromWorkspace.length > 0) {
@@ -234,16 +277,16 @@ export function buildStatusLineModel(stdin, nowMs) {
234
277
  }
235
278
  }
236
279
  if (projectRoot === null) {
237
- return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null, sessionId: null };
280
+ return { state: 'idle', projectRoot: null, presence: null, ageMs: null, compact, activeLeaf: null, sessionId: null, twentyFourHourState: null };
238
281
  }
239
282
  // callerId resolves the read-side isolation; back-compat is `null`.
240
283
  const callerId = resolveCallerId(stdin);
241
284
  const { presence, invalid } = readPresenceReadOnly(projectRoot, callerId);
242
285
  if (invalid) {
243
- return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
286
+ return { state: 'invalid-presence', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId, twentyFourHourState: null };
244
287
  }
245
288
  if (presence === null) {
246
- return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
289
+ return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId, twentyFourHourState: null };
247
290
  }
248
291
  // Session binding: when the presence was stamped with a Claude session id and
249
292
  // the live session (from stdin) is a different one, the recorded skill belongs
@@ -252,7 +295,7 @@ export function buildStatusLineModel(stdin, nowMs) {
252
295
  // fall back to the time-based behavior below for backward compatibility.
253
296
  const liveSessionId = typeof stdin?.session_id === 'string' && stdin.session_id.length > 0 ? stdin.session_id : null;
254
297
  if (presence.claudeSessionId && liveSessionId && presence.claudeSessionId !== liveSessionId) {
255
- return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId };
298
+ return { state: 'idle', projectRoot, presence: null, ageMs: null, compact, activeLeaf: null, sessionId, twentyFourHourState: null };
256
299
  }
257
300
  const setAtMs = presence.setAt ? Date.parse(presence.setAt) : Number.NaN;
258
301
  const ageMs = Number.isNaN(setAtMs) ? null : nowMs - setAtMs;
@@ -268,7 +311,14 @@ export function buildStatusLineModel(stdin, nowMs) {
268
311
  catch {
269
312
  activeLeaf = null;
270
313
  }
271
- return { state, projectRoot, presence, ageMs, compact, activeLeaf, sessionId };
314
+ // Slice rid-statusline-24h-overlay (2026-08-10): read the 24h-mode
315
+ // overlay snapshot ONLY when state === 'active'. Stale / idle /
316
+ // invalid-presence / idle-via-outer-mismatch never carry the suffix
317
+ // (per PRD §Non-goals.6 — "不在 idle / stale / invalid-presence
318
+ // 状态下 overlay 24h suffix"). When projectRoot or sessionId is
319
+ // missing, return null without re-reading disk.
320
+ const twentyFourHourState = state === 'active' ? read24hOverlay(projectRoot, sessionId ?? '') : null;
321
+ return { state, projectRoot, presence, ageMs, compact, activeLeaf, sessionId, twentyFourHourState };
272
322
  }
273
323
  /**
274
324
  * Read-only compact state resolver. Resolves the canonical session id for the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.17",
3
+ "version": "4.0.19",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "keywords": [
@@ -101,9 +101,10 @@
101
101
  "headroom-ai": "0.22.4",
102
102
  "yaml": "^2.9.0",
103
103
  "zod": "^3.25.76",
104
- "peaks-loop-mut": "0.1.20",
105
- "peaks-loop-shared-channel": "0.0.24",
106
- "peaks-loop-shared": "0.0.48"
104
+ "peaks-loop-internal-runtime": "4.0.0",
105
+ "peaks-loop-mut": "0.1.22",
106
+ "peaks-loop-shared": "0.0.50",
107
+ "peaks-loop-shared-channel": "0.0.26"
107
108
  },
108
109
  "devDependencies": {
109
110
  "@changesets/cli": "2.31.1",
@@ -259,3 +259,7 @@ QA contracts to assert on the L2 surface (a minimal acceptance test set):
259
259
  2. `peaks worktree list` reports 0 active leases after a clean run.
260
260
  3. `peaks lease-metrics --rate` after a 1-spawn / 0-release test reports `estimatedLeaked: 1, completedLifetimes: 0`.
261
261
  4. `peaks audit red-lines` reports `proseOnly: 0` and `partial: 0` for any shipped L2 surface.
262
+
263
+ ## Sub-role detached mode (Phase C, slice 2026-08-10)
264
+
265
+ QA sub-roles (`qa-business`, `qa-perf`, `qa-security`, `qa-business-api`, `qa-business-frontend`, `qa-business-regression`) accept `--mode detached --vendor <vendor>` for parallel test-case execution. Detached mode spawns a real OS process via `peaks sub-agent dispatch --mode detached`, isolated from the orchestrator session. Use detached mode when the test-case writer's expected runtime exceeds 60s OR it processes ≥ 20 source files. The default remains `in-process` (backward compat; existing 106+ dispatch tests untouched). When `--mode detached` is used, the child QA agent receives the G8 infinite-context auto-compact marker (0.85 / 0.95 thresholds; unlimited spend authorized). See `peaks-code/references/sub-agent-dispatch.md` §"Detached Mode" for the full contract.
@@ -151,6 +151,10 @@ Both audit skills consume the immutable peaks-prd handoff (`prd/handoff.md`) and
151
151
 
152
152
  Full dispatch contract (when-to-fan-out rules, dispatch template, prereq gates) lives in **`references/parallel-review-fanout.md`**. Read that file before issuing any 3-way fan-out.
153
153
 
154
+ ## Reviewer fan-out detached mode (Phase C, slice 2026-08-10)
155
+
156
+ Reviewer fan-out may run in detached mode for any of the 3-way fan-out roles (`code-reviewer`, `qa-test-cases-writer`, `karpathy-reviewer`). Detached mode spawns a real OS process via `peaks sub-agent dispatch --mode detached --vendor <vendor>`, isolated from the orchestrator session. Use detached mode when the reviewer's expected runtime exceeds 60s OR processes ≥ 20 source files. Pass `--mode detached` explicitly per reviewer role; the default remains `in-process` (backward compat). The karpathy-reviewer's G8 infinite-context marker (auto-compact at 0.85/0.95, unlimited spend) is honored automatically when `--mode detached` is used. See `peaks-code/references/sub-agent-dispatch.md` §"Detached Mode" for the full contract.
157
+
154
158
  ## Refactor hard gates
155
159
 
156
160
  If a request is refactor, cleanup, architecture adjustment, module split, or technical debt work: scan project structure and existing standards; locate or run UT coverage; block implementation unless coverage is >= 95%; treat missing, unknown, or unverifiable coverage as failing; generate intermediate artifacts before implementation; call or consume peaks-prd and peaks-qa artifacts even in direct RD mode; require strict slice spec before each slice; require 100% acceptance for the slice; require code changes and intermediate artifacts to be traceable in local `.peaks/_runtime/<sessionId>/` storage before continuing.
@@ -3,6 +3,9 @@ name: peaks-code
3
3
  description: Code-domain loop engineering orchestrator for the Peaks-Loop skill family. Use when the user asks Peaks-Loop to handle a code-repo workflow end-to-end (端到端/全流程/需求开发), especially from a product document (PRD/飞书文档/Feishu doc) through implementation and validation. Coordinates peaks-prd, peaks-rd, peaks-qa, peaks-ui, peaks-sc, and peaks-txt while preserving user confirmation gates. Triggers on `/peaks-code`, "peaks code", "全流程开发", "端到端迭代". General primitives (peaks-resume / peaks-status / peaks-test) are sibling skills, not children.
4
4
  ---
5
5
 
6
+ > **Detached sub-agent mode (Phase A, slice 2026-08-10).** When the orchestrator requires true parallelism with isolated context windows and survives orchestrator session exit, dispatch sub-agents with `--mode detached --vendor <claude|codex|copilot>`. The CLI spawns a real OS process via `ProcessSupervisor` (Windows `DETACHED_PROCESS` + `CREATE_NEW_PROCESS_GROUP`, POSIX `setsid` + `nohup`); the child vendor LLM receives a 5–8KB minimum prompt slice (no orchestrator session history) and self-compacts at 0.85 / 0.95 against the vendor window via the `<peaks-auto-compact>` marker (G8 — unlimited spend authorized). Orchestrator MUST emit one line of prose before every detached dispatch: `⏳ Spawning detached sub-agent via <vendor>: rid=<rid> (ETA ~60s)`. Status is read from `.peaks/_runtime/<sid>/detached/<rid>/status.json`; `LifecycleOwner` enforces 100% cleanup of `pid` / `log.txt` / `status.json` / `owner-session` on every exit path. `--no-throttle --max-concurrent <N>` bypasses `ResourceBudgetGuard` (user accepts risk; default max=8). See `references/sub-agent-dispatch.md` §"Detached Mode" for the full contract. Default mode remains `in-process` for backward compat (existing 106+ dispatch tests untouched).
7
+ ---
8
+
6
9
  ## Scope (RL-8 — red line, locked 2026-07-08)
7
10
 
8
11
  `peaks-code` is a **code-domain long-task loop engineering orchestrator; not a general-purpose orchestrator.**
@@ -423,3 +423,19 @@ After every sub-agent dispatch returns, Code **restores presence** once (not per
423
423
  ```bash
424
424
  peaks skill presence:set peaks-code --project <repo> --mode <mode> --gate swarm-converged
425
425
  ```
426
+
427
+ ---
428
+
429
+ ## Detached Mode (Phase A, slice 2026-08-10)
430
+
431
+ `peaks sub-agent dispatch <role> --prompt <text> --request-id <rid> --mode detached --vendor claude|codex|copilot [--no-throttle --max-concurrent <N>] --json` spawns a real OS process independent of the orchestrator IDE session.
432
+
433
+ - **Cross-platform spawn**: Windows uses `DETACHED_PROCESS` + `CREATE_NEW_PROCESS_GROUP`; POSIX uses `setsid` + `nohup`. Implementation: `packages/peaks-loop-internal-runtime/src/process-supervisor.ts`.
434
+ - **Minimum-context prompt**: `PromptBuilder` emits a 5–8KB slice `{rid, role, vendor, files, refs}` plus the verbatim `<peaks-auto-compact>` marker. The forbidden marker `@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@` MUST NOT appear in any prompt (unit-tested; regression fails vitest).
435
+ - **G8 infinite context**: the child LLM self-monitors context; on ≥0.85 it compacts and writes `.peaks/_runtime/<sid>/detached/<rid>/compact/<n>.json`; on ≥0.95 it writes `status.json` note `'compact-emergency'`. peaks runtime does NOT throttle on token cost — user authorized unlimited spend for G8 (recorded as `tokenUsage` on the dispatch record for audit visibility only).
436
+ - **Lifecycle closure invariant**: `pid` / `log.txt` / `status.json` / `owner-session` 100% cleaned on every exit path (success / crash / OOM / SIGTERM). `peaks sub-agent cleanup --orphan` is the only orphan-killing path (RL-15: user-only decision; default is "do nothing").
437
+ - **Resource budget guard**: `ResourceBudgetGuard` enforces `runtime RSS ≤ 200MB / idle CPU ≤ 5% / fan-out ≤ 8 (default)`. `--no-throttle` bypasses (user accepts risk; adds warning to envelope). `--max-concurrent <N>` overrides the 8 default.
438
+ - **Vendor-neutral**: `VendorAdapterRegistry` registers `ClaudeAdapter` (Phase A) / `CodexAdapter` + `CopilotAdapter` (Phase B). Missing vendor CLI → fallback to default with `dispatchRecord.warning = 'vendor fallback: <id>→claude'`.
439
+ - **Orchestrator prose obligation (G11.5)**: before each detached dispatch, emit `⏳ Spawning detached sub-agent via <vendor>: rid=<rid> (ETA ~60s)`. Envelope field `data.orchestratorVisibleHint` carries the same string for tooling.
440
+
441
+ Default mode remains `in-process` (backward compat). The dispatch CLI accepts `--mode in-process|detached`; existing 106+ tests dispatching without `--mode` continue to use the in-process IDE-internal Task path.