peaks-loop 4.0.18 → 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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # Changelog
2
2
 
3
+ ## 4.0.19 — 2026-08-11 (detached sub-agent + G8 infinite-context — single-ship)
4
+
5
+ **New feature — detached sub-agent mode (Phase A-E single-ship)**:
6
+ - New monorepo package `peaks-loop-internal-runtime` (npm name `peaks-loop-internal-runtime`; sibling of `peaks-loop-shared`; private; consumed via `workspace:*`).
7
+ - 11 new modules: `ProcessSupervisor` (Windows `DETACHED_PROCESS` + POSIX `setsid`), `LifecycleOwner` (closure invariant — 100% cleanup of pid / log.txt / status.json / owner-session on every exit path), `VendorAdapter` interface + `ClaudeAdapter` / `CodexAdapter` / `CopilotAdapter` + `VendorAdapterRegistry`, `PromptBuilder` (5-8KB minimum-context slice + forbidden-marker guard `@@@ORCHESTRATOR_SESSION_HISTORY_BOUNDARY@@@`), `StatusProtocol` (heartbeat merge + stale detection + autoCompact events), `AutoCompactAdapter` (G8 marker `<peaks-auto-compact threshold="0.85|0.95">` + scratch file parser), `ResourceBudgetGuard` (RSS ≤ 200MB / CPU ≤ 5% / fan-out ≤ 8 / `--no-throttle` user override), `dispatchDetached` orchestrator (wires all collaborators), `peaks-loop-internal-runtime/src/index.ts` (public re-exports).
8
+ - 2 new CLI handlers: `src/cli/commands/sub-agent/detached.ts` (`peaks sub-agent dispatch <role> --mode detached --vendor claude|codex|copilot --no-throttle --max-concurrent <N>`), `src/cli/commands/vendor-detect.ts` (`peaks vendor-detect` — reports installed vendor CLIs + recommends default), `src/cli/commands/doctor/invoke-from-code.ts` (`peaks doctor invoke --from-code` — writes proposal stub for peaks-doctor bridge).
9
+ - New bench: `benchmarks/runtime-detached/{baseline.ts, resource-budget-bench.ts}`. Idle sample: rssMb=79, cpuPct=0.34 (passes §5.3 ceiling rails).
10
+
11
+ **G8 infinite-context auto-compact protocol (Phase A critical path)**:
12
+ - Child vendor LLM receives `<peaks-auto-compact>` marker in minimum-context prompt.
13
+ - At ≥ 0.85 of vendor window: child writes scratch file `.peaks/_runtime/<sid>/detached/<rid>/compact/<n>.json` (summary + decisionsKept + discardedOptions) + calls `peaks runtime write-compact-event` to append `autoCompactEvents[]` to dispatch record.
14
+ - At ≥ 0.95: synchronous compact + status.json `note: 'compact-emergency'` + peaks main process notification.
15
+ - **Unlimited spend authorized**: G8 red line; `tokenUsage` recorded on dispatch record for audit visibility only (no alert, no kill, no user ask).
16
+
17
+ **LifecycleOwner closure invariant — core red line**:
18
+ - Every exit path (success / crash / OOM-killed / SIGTERM / orchestrator session exit) MUST remove `pid` + `log.txt` + `status.json` + `owner-session` (archived to `log-archive.txt` + `status-final.json` for forensics).
19
+ - Orphan reaper (RL-15: user-only decision; peaks does NOT auto-kill).
20
+ - Unit + integration test coverage (4 lifecycle + 3 integration closure audit).
21
+
22
+ **Performance ceiling rails (spec §5.3)**:
23
+ - peaks runtime RSS ≤ 200 MB idle / CPU ≤ 5% idle / fan-out ≤ 8 (default).
24
+ - `--no-throttle --max-concurrent <N>` bypasses (user accepts risk; adds warning to envelope).
25
+ - `peaks sub-agent cleanup --orphan` is the only orphan-killing path.
26
+
27
+ **Publish lockstep 3 packages (gate-cli-version extension)**:
28
+ - `gate-cli-version` now verifies 3 lockstep dimensions: root `package.json#version` ↔ peaks-loop-shared `dist/version.js#CLI_VERSION` ↔ peaks-loop-internal-runtime `src/index.ts#RUNTIME_VERSION` (private package — NOT in publish list; runtime is consumed via workspace:* only).
29
+ - 2/2 lockstep test passes (`tests/unit/publish/lockstep-three-packages.test.ts`).
30
+
31
+ **Doc updates**:
32
+ - `skills/peaks-code/SKILL.md` + `skills/peaks-code/references/sub-agent-dispatch.md` — Detached mode section + G11.5 orchestrator prose obligation.
33
+ - `skills/bee/peaks-rd/SKILL.md` + `skills/bee/peaks-qa/SKILL.md` — reviewer / sub-role `--mode detached` paragraphs.
34
+ - `skills/peaks-code/references/lease-dashboard.html` — `detachedGraphView` empty container hook (Phase E render deferred).
35
+ - Spec: `docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md`.
36
+ - Plan: `docs/superpowers/plans/2026-08-10-peaks-detached-sub-agent-plan.md`.
37
+
38
+ **Tests added**: 11 vitest files (15 unit + 3 integration suites): process-supervisor, lifecycle, vendor/{claude,codex,copilot}-adapter, prompt-builder, status-protocol, auto-compact-adapter, resource-budget, dispatch, sub-agent-detached, vendor-detect, doctor-invoke-from-code, lockstep-three-packages. 0 regressions in existing 106+ dispatch tests.
39
+
40
+ **CI fix (publish #142)**:
41
+ - Explicit `.js` extensions in all relative imports under `packages/peaks-loop-internal-runtime/src` (peaks-loop uses `module: NodeNext`; vitest hides this but `tsc -p tsconfig.build.json` requires `.js`).
42
+ - Added `"peaks-loop-internal-runtime": "workspace:*"` to root `package.json` (CLI handlers consume runtime via npm name import, not relative path into `packages/`).
43
+
44
+ **Backwards compat**: 100%. Default dispatch mode remains `in-process`; existing 106+ dispatch tests untouched; peak-loop-shared retains its 0.0.x SemVer.
45
+
46
+ **For full design context**: `docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md` §0 + §3.2 + §3.5 + §5.3 + §6.3. Closure sediment: `.peaks/memory/2026-08-11-runtime-detached-4-0-19-ship-pending.md`.
47
+
3
48
  ## 4.0.18 — 2026-08-10 (statusline 24h overlay)
4
49
 
5
50
  **Bug fix — statusline doesn't reflect 24h mode substate after transition**:
@@ -0,0 +1,10 @@
1
+ export declare function doctorInvokeFromCode(opts: {
2
+ sid: string;
3
+ json: boolean;
4
+ }): Promise<{
5
+ ok: boolean;
6
+ command: string;
7
+ data: {
8
+ proposalPath: string;
9
+ };
10
+ }>;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Phase D Task 24: peaks doctor invoke --from-code CLI.
3
+ * Contract surface for peaks-code Step 11 → peaks-doctor bridge.
4
+ * Writes proposal stub to .peaks/_runtime/<sid>/doctor/proposal.md.
5
+ * Real LLM call is delegated to peaks-doctor (Phase D Task 24 detail).
6
+ * Spec: docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md §3.6
7
+ */
8
+ import { mkdirSync, writeFileSync } from 'node:fs';
9
+ import { join } from 'node:path';
10
+ export async function doctorInvokeFromCode(opts) {
11
+ const dir = join('.peaks', '_runtime', opts.sid, 'doctor');
12
+ mkdirSync(dir, { recursive: true });
13
+ const proposalPath = join(dir, 'proposal.md');
14
+ // Stub: real implementation invokes peaks-doctor sub-skill
15
+ // (LLM-driven analysis of .peaks/_runtime/<sid>/txt/handoff.md
16
+ // + dispatch records + autoCompactEvents; emits OpenSpec proposals).
17
+ writeFileSync(proposalPath, [
18
+ '# doctor proposal (stub)',
19
+ '',
20
+ '## capability: <TBD>',
21
+ '## kind: <TBD>',
22
+ '',
23
+ 'Real implementation: peaks-doctor LLM-driven analysis of',
24
+ '.peaks/_runtime/<sid>/txt/handoff.md + dispatch records.',
25
+ ].join('\n'));
26
+ // Normalize to POSIX-style separators so callers (and tests) can rely
27
+ // on a forward-slash contract regardless of host OS.
28
+ const normalizedPath = proposalPath.replaceAll('\\', '/');
29
+ return { ok: true, command: 'doctor.invoke.from-code', data: { proposalPath: normalizedPath } };
30
+ }
@@ -0,0 +1,29 @@
1
+ export interface DispatchFlags {
2
+ role: string;
3
+ prompt: string;
4
+ requestId: string;
5
+ mode?: 'in-process' | 'detached';
6
+ vendor?: 'claude' | 'codex' | 'copilot';
7
+ project: string;
8
+ json: boolean;
9
+ /** Task 11.5: bypass ResourceBudgetGuard (user accepts risk) */
10
+ noThrottle?: boolean;
11
+ /** Task 11.5: override max concurrent (default 8) */
12
+ maxConcurrent?: number;
13
+ }
14
+ export declare function dispatch(f: DispatchFlags): Promise<{
15
+ ok: boolean;
16
+ command: string;
17
+ data: {
18
+ mode: string;
19
+ vendor: "claude" | "codex" | "copilot" | undefined;
20
+ pid: number;
21
+ dispatchRecordPath: string;
22
+ maxConcurrent: number;
23
+ noThrottle: boolean;
24
+ orchestratorVisibleHint: string;
25
+ expectedCompletionSeconds: number;
26
+ };
27
+ warnings: string[];
28
+ nextActions: string[];
29
+ }>;
@@ -0,0 +1,60 @@
1
+ /**
2
+ * Phase A Task 11 + 11.5: peaks sub-agent dispatch --mode detached CLI handler.
3
+ * Vendor-neutral detached sub-agent dispatch. Spawns real OS process via
4
+ * peaks-loop-internal-runtime/dispatch.dispatchDetached. --no-throttle and
5
+ * --max-concurrent flags bypass / scope ResourceBudgetGuard (Task 11.5).
6
+ *
7
+ * Default mode is in-process (backward compat — existing 106+ tests untouched).
8
+ * This handler only fires when the user explicitly passes --mode detached.
9
+ * Spec: docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md §3.1 §5.3
10
+ */
11
+ import { dispatchDetached, ResourceBudgetGuard } from 'peaks-loop-internal-runtime';
12
+ export async function dispatch(f) {
13
+ if (f.mode !== 'detached') {
14
+ throw new Error('src/cli/commands/sub-agent/detached.ts only handles --mode detached; ' +
15
+ 'peaks sub-agent dispatch default mode remains in-process (backward compat)');
16
+ }
17
+ // Task 11.5: ResourceBudgetGuard gate
18
+ const maxConcurrent = f.maxConcurrent ?? 8;
19
+ const guard = new ResourceBudgetGuard({ maxRssMb: 200, maxCpuPct: 5 });
20
+ const enforce = guard.enforce({ active: 1 }, { maxConcurrent });
21
+ const warnings = [];
22
+ if (enforce.throttle && !f.noThrottle) {
23
+ throw new Error('RESOURCE_BUDGET_THROTTLED: concurrent fan-out > max-concurrent; pass --no-throttle to bypass');
24
+ }
25
+ if (f.noThrottle) {
26
+ warnings.push('user-overrode: --no-throttle (peak runtime may exceed performance ceiling)');
27
+ }
28
+ const sid = process.env.PEAKS_SESSION_ID ?? 'local';
29
+ const r = await dispatchDetached({
30
+ sid,
31
+ rid: f.requestId,
32
+ role: f.role,
33
+ vendor: (f.vendor ?? 'claude'),
34
+ userTask: f.prompt,
35
+ files: [],
36
+ refs: [],
37
+ runtimeDir: `.peaks/_runtime/${sid}/detached`,
38
+ subAgentsDir: `.peaks/_sub_agents/${sid}`,
39
+ });
40
+ return {
41
+ ok: true,
42
+ command: 'sub-agent.dispatch.detached',
43
+ data: {
44
+ mode: 'detached',
45
+ vendor: f.vendor,
46
+ pid: r.pid,
47
+ dispatchRecordPath: r.dispatchRecordPath,
48
+ maxConcurrent,
49
+ noThrottle: f.noThrottle ?? false,
50
+ orchestratorVisibleHint: `⏳ Spawning detached sub-agent via ${f.vendor ?? 'claude'}: rid=${f.requestId} (ETA ~60s)`,
51
+ expectedCompletionSeconds: 60,
52
+ },
53
+ warnings,
54
+ nextActions: [
55
+ 'Sub-agent runs as detached OS process. Status at .peaks/_runtime/<sid>/detached/<rid>/status.json',
56
+ 'Use `peaks sub-agent list --mode detached` to monitor.',
57
+ 'Run `peaks sub-agent cleanup --orphan` to reap orphan processes (RL-15: user-only decision).',
58
+ ],
59
+ };
60
+ }
@@ -0,0 +1,10 @@
1
+ export declare function vendorDetect(opts: {
2
+ json: boolean;
3
+ }): Promise<{
4
+ ok: boolean;
5
+ command: string;
6
+ data: {
7
+ installed: string[];
8
+ recommended: string | null;
9
+ };
10
+ }>;
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Phase B Task 20: peaks vendor-detect CLI.
3
+ * Reports which vendor CLIs are installed on PATH + recommends default.
4
+ * Spec: docs/superpowers/specs/2026-08-10-peaks-detached-sub-agent-design.md §3.3
5
+ */
6
+ import { defaultRegistry } from 'peaks-loop-internal-runtime';
7
+ export async function vendorDetect(opts) {
8
+ const reg = defaultRegistry();
9
+ const list = reg.list();
10
+ const installed = [];
11
+ for (const a of list)
12
+ if (await a.detectInstalled())
13
+ installed.push(a.id);
14
+ const recommended = installed[0] ?? null;
15
+ return { ok: true, command: 'vendor-detect', data: { installed, recommended } };
16
+ }
@@ -34,16 +34,13 @@ export interface DispatchRecord {
34
34
  * defaulted to `null` on read so v3 records upgrade cleanly.
35
35
  */
36
36
  /**
37
- * Slice 2026-07-29-rid-prose-only-sweep Part 34: schema v3.1
38
- * is the explicit minor bump that records the
39
- * `isolationStartedAt` (Part 7) and `leaseId` (Part 3.A.1 +
40
- * Part 4.C) fields as part of the canonical schema. v3
41
- * records on disk upgrade transparently — see `upgradeRecord`
42
- * in this file. The literal type ('3.1') is the source of
43
- * truth for "this record is v3.1-form"; readers check
44
- * `version === '3.1'` for forward-compatible dispatching.
37
+ * Phase A Task 8: schema bumped to v4.1.0 (additive). The bump
38
+ * is purely additive — new fields (`mode`, `vendor`,
39
+ * `autoCompactEvents`, `tokenUsage`) all default safely on
40
+ * read for legacy v4.0.0 / v3.2 / v3.1 / v3 / v2 / v1 records.
41
+ * No existing field semantics changed.
45
42
  */
46
- readonly version: '4.0.0';
43
+ readonly version: '4.1.0';
47
44
  readonly createdAt: string;
48
45
  readonly completedAt: string | null;
49
46
  readonly outcome: DispatchOutcome;
@@ -136,6 +133,49 @@ export interface DispatchRecord {
136
133
  readonly workflowId: string | null;
137
134
  readonly graphNodeId: string | null;
138
135
  readonly graphRef: string | null;
136
+ /**
137
+ * Phase A Task 8: dispatch execution mode. `in-process` is the
138
+ * current behavior (LLM-side runner, no separate OS process).
139
+ * `detached` is the new path: `peaks sub-agent dispatch --mode
140
+ * detached` spawns a real child OS process running a different
141
+ * LLM vendor (claude / codex / copilot) and reports back via
142
+ * the dispatch record. v4.1.0 is the additive bump; legacy v4.0.0
143
+ * records upgrade to `in-process` on read.
144
+ */
145
+ readonly mode: 'in-process' | 'detached';
146
+ /**
147
+ * Phase A Task 8: vendor id when `mode='detached'`. `null` when
148
+ * the dispatch is in-process. Reserved for future use; current
149
+ * detached sub-agents use `claude` but the schema also accepts
150
+ * `codex` and `copilot` for the vendor-neutral adapter layer.
151
+ */
152
+ readonly vendor: 'claude' | 'codex' | 'copilot' | null;
153
+ /**
154
+ * Phase A Task 8: G8 autoCompact events accumulated by the child
155
+ * LLM during a detached run. Each event records the threshold
156
+ * that fired (0.85 = first warning, 0.95 = second warning) plus
157
+ * token counts before/after. Empty for in-process dispatches
158
+ * and for legacy records upgraded on read.
159
+ */
160
+ readonly autoCompactEvents: ReadonlyArray<{
161
+ readonly at: number;
162
+ readonly threshold: '0.85' | '0.95';
163
+ readonly tokensBefore: number;
164
+ readonly tokensAfter: number;
165
+ readonly scratchFile?: string;
166
+ }>;
167
+ /**
168
+ * Phase A Task 8: G8 token-usage accounting for detached sub-agents.
169
+ * Detached runs have "unlimited spend but recorded" semantics —
170
+ * the cost is recorded for audit but not enforced. `null` when
171
+ * the dispatch is in-process (no detached accounting) or for
172
+ * legacy records upgraded on read.
173
+ */
174
+ readonly tokenUsage: {
175
+ readonly promptTokens: number;
176
+ readonly completionTokens: number;
177
+ readonly totalCostUsd?: number;
178
+ } | null;
139
179
  }
140
180
  /** Input for the initial write. */
141
181
  export type WriteInitialDispatchInput = {
@@ -172,6 +212,43 @@ export type WriteInitialDispatchInput = {
172
212
  workflowId?: string | null;
173
213
  graphNodeId?: string | null;
174
214
  graphRef?: string | null;
215
+ /**
216
+ * Phase A Task 8: dispatch execution mode. Default `'in-process'`
217
+ * preserves the current LLM-side runner behavior. `'detached'`
218
+ * triggers the new real-OS-process path via
219
+ * `peaks sub-agent dispatch --mode detached`.
220
+ */
221
+ mode?: 'in-process' | 'detached';
222
+ /**
223
+ * Phase A Task 8: vendor id when `mode='detached'`. Required by
224
+ * the adapter layer to know which CLI / runtime to spawn. The
225
+ * schema accepts the three vendors peaks-loop has adapters for
226
+ * (claude / codex / copilot). Ignored when `mode='in-process'`.
227
+ */
228
+ vendor?: 'claude' | 'codex' | 'copilot';
229
+ /**
230
+ * Phase A Task 8: G8 autoCompact events accumulated by the child
231
+ * LLM. Optional on input — most dispatches start with an empty
232
+ * array and the detached runner appends events as they fire.
233
+ */
234
+ autoCompactEvents?: Array<{
235
+ at: number;
236
+ threshold: '0.85' | '0.95';
237
+ tokensBefore: number;
238
+ tokensAfter: number;
239
+ scratchFile?: string;
240
+ }>;
241
+ /**
242
+ * Phase A Task 8: G8 token-usage accounting. Detached runs
243
+ * record spend for audit (unlimited, but persisted). Optional
244
+ * on input; the detached runner fills this in as it streams
245
+ * usage from the vendor API.
246
+ */
247
+ tokenUsage?: {
248
+ promptTokens: number;
249
+ completionTokens: number;
250
+ totalCostUsd?: number;
251
+ };
175
252
  };
176
253
  /** Heartbeat write input. */
177
254
  export type AppendHeartbeatInput = {
@@ -86,7 +86,7 @@ export function writeInitialDispatchRecord(input) {
86
86
  function buildInitialDispatchRecord(input, now) {
87
87
  const { role, requestId, sessionId, prompt, toolCall, batchId } = input;
88
88
  return {
89
- version: '4.0.0',
89
+ version: '4.1.0',
90
90
  createdAt: now().toISOString(),
91
91
  completedAt: null,
92
92
  outcome: 'no-execution',
@@ -141,6 +141,22 @@ function buildInitialDispatchRecord(input, now) {
141
141
  workflowId: typeof input.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.workflowId) ? input.workflowId : null,
142
142
  graphNodeId: typeof input.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(input.graphNodeId) ? input.graphNodeId : null,
143
143
  graphRef: typeof input.graphRef === 'string' && input.graphRef.length > 0 ? input.graphRef : null,
144
+ // Phase A Task 8: detached sub-agent mode (default in-process).
145
+ mode: input.mode === 'detached' ? 'detached' : 'in-process',
146
+ vendor: input.vendor === 'claude' || input.vendor === 'codex' || input.vendor === 'copilot' ? input.vendor : null,
147
+ autoCompactEvents: Array.isArray(input.autoCompactEvents)
148
+ ? input.autoCompactEvents.filter((e) => typeof e?.at === 'number' &&
149
+ (e?.threshold === '0.85' || e?.threshold === '0.95') &&
150
+ typeof e?.tokensBefore === 'number' &&
151
+ typeof e?.tokensAfter === 'number')
152
+ : [],
153
+ tokenUsage: typeof input.tokenUsage === 'object' && input.tokenUsage !== null && typeof input.tokenUsage.promptTokens === 'number' && typeof input.tokenUsage.completionTokens === 'number'
154
+ ? {
155
+ promptTokens: input.tokenUsage.promptTokens,
156
+ completionTokens: input.tokenUsage.completionTokens,
157
+ ...(typeof input.tokenUsage.totalCostUsd === 'number' ? { totalCostUsd: input.tokenUsage.totalCostUsd } : {}),
158
+ }
159
+ : null,
144
160
  };
145
161
  }
146
162
  function activeDispatchIndexPath(projectRoot, sessionId) {
@@ -663,18 +679,19 @@ function upgradeRecord(parsed) {
663
679
  throw new Error('Dispatch record root must be an object');
664
680
  }
665
681
  const obj = parsed;
666
- // Slice 4.0.8: 3.2 → 4.0.0 schema bump. The literal type narrows
667
- // to '4.0.0' but legacy v3.2 / v3.1 / 3 / 2 / 1 records are
668
- // accepted transparently and upgraded on read.
682
+ // Slice 4.0.8: 3.2 → 4.0.0 schema bump. Phase A Task 8: 4.0.0 → 4.1.0
683
+ // (additive). The literal type narrows to '4.1.0' but legacy v4.0.0 /
684
+ // v3.2 / v3.1 / 3 / 2 / 1 records are accepted transparently and
685
+ // upgraded on read.
669
686
  const rawVersion = obj.version;
670
- if (rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
671
- throw new Error(`Dispatch record version mismatch: expected '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
672
- 'The v1 → v4.0.0 migration is in-file; records from much older or newer builds must be regenerated.');
687
+ if (rawVersion !== '4.1.0' && rawVersion !== '4.0.0' && rawVersion !== '3.2' && rawVersion !== '3.1' && rawVersion !== 3 && rawVersion !== 2 && rawVersion !== 1) {
688
+ throw new Error(`Dispatch record version mismatch: expected '4.1.0', '4.0.0', '3.2', '3.1', 3, 2, or 1, got ${JSON.stringify(rawVersion)}. ` +
689
+ 'The v1 → v4.1.0 migration is in-file; records from much older or newer builds must be regenerated.');
673
690
  }
674
691
  const legacy = parseUpgradeRecordLegacyFields(obj);
675
692
  const migration = parseUpgradeRecordMigrationFields(obj);
676
693
  return {
677
- version: '4.0.0',
694
+ version: '4.1.0',
678
695
  createdAt: legacy.createdAt,
679
696
  completedAt: legacy.completedAt,
680
697
  outcome: legacy.outcome,
@@ -697,7 +714,16 @@ function upgradeRecord(parsed) {
697
714
  mergeBackAttempts: migration.mergeBackAttempts,
698
715
  workflowId: migration.workflowId,
699
716
  graphNodeId: migration.graphNodeId,
700
- graphRef: migration.graphRef
717
+ graphRef: migration.graphRef,
718
+ // Phase A Task 8: detached sub-agent fields. Legacy records
719
+ // (pre-4.1.0) default mode='in-process', vendor=null,
720
+ // autoCompactEvents=[], tokenUsage=null. See
721
+ // parseUpgradeRecordMigrationFields for the per-field
722
+ // validation rules.
723
+ mode: migration.mode,
724
+ vendor: migration.vendor,
725
+ autoCompactEvents: migration.autoCompactEvents,
726
+ tokenUsage: migration.tokenUsage
701
727
  };
702
728
  }
703
729
  /**
@@ -812,7 +838,29 @@ function parseUpgradeRecordMigrationFields(obj) {
812
838
  // fields to `null` so a legacy record upgrades transparently.
813
839
  workflowId: typeof obj.workflowId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.workflowId) ? obj.workflowId : null,
814
840
  graphNodeId: typeof obj.graphNodeId === 'string' && /^[a-zA-Z0-9._-]{1,200}$/.test(obj.graphNodeId) ? obj.graphNodeId : null,
815
- graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null
841
+ graphRef: typeof obj.graphRef === 'string' ? obj.graphRef : null,
842
+ // Phase A Task 8: 4.0.0 → 4.1.0 migration. Pre-4.1.0 records
843
+ // have no mode / vendor / autoCompactEvents / tokenUsage
844
+ // fields. Default to safe in-process / null / [] / null so
845
+ // legacy records upgrade transparently without breaking
846
+ // consumers (e.g. the dashboard, the merge-back-runner).
847
+ mode: obj.mode === 'detached' ? 'detached' : 'in-process',
848
+ vendor: obj.vendor === 'claude' || obj.vendor === 'codex' || obj.vendor === 'copilot' ? obj.vendor : null,
849
+ autoCompactEvents: Array.isArray(obj.autoCompactEvents)
850
+ ? obj.autoCompactEvents.filter((e) => typeof e?.at === 'number' &&
851
+ (e?.threshold === '0.85' || e?.threshold === '0.95') &&
852
+ typeof e?.tokensBefore === 'number' &&
853
+ typeof e?.tokensAfter === 'number')
854
+ : [],
855
+ tokenUsage: typeof obj.tokenUsage === 'object' && obj.tokenUsage !== null && typeof obj.tokenUsage.promptTokens === 'number' && typeof obj.tokenUsage.completionTokens === 'number'
856
+ ? {
857
+ promptTokens: obj.tokenUsage.promptTokens,
858
+ completionTokens: obj.tokenUsage.completionTokens,
859
+ ...(typeof obj.tokenUsage.totalCostUsd === 'number'
860
+ ? { totalCostUsd: obj.tokenUsage.totalCostUsd }
861
+ : {}),
862
+ }
863
+ : null
816
864
  };
817
865
  }
818
866
  function isObject(v) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.18",
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.21",
105
- "peaks-loop-shared": "0.0.49",
106
- "peaks-loop-shared-channel": "0.0.25"
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.