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 +45 -0
- package/dist/cli/commands/doctor/invoke-from-code.d.ts +10 -0
- package/dist/cli/commands/doctor/invoke-from-code.js +30 -0
- package/dist/cli/commands/sub-agent/detached.d.ts +29 -0
- package/dist/cli/commands/sub-agent/detached.js +60 -0
- package/dist/cli/commands/vendor-detect.d.ts +10 -0
- package/dist/cli/commands/vendor-detect.js +16 -0
- package/dist/services/dispatch/dispatch-record-writer.d.ts +86 -9
- package/dist/services/dispatch/dispatch-record-writer.js +58 -10
- package/package.json +5 -4
- package/skills/bee/peaks-qa/SKILL.md +4 -0
- package/skills/bee/peaks-rd/SKILL.md +4 -0
- package/skills/peaks-code/SKILL.md +3 -0
- package/skills/peaks-code/references/sub-agent-dispatch.md +16 -0
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,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,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
|
-
*
|
|
38
|
-
* is
|
|
39
|
-
* `
|
|
40
|
-
*
|
|
41
|
-
*
|
|
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.
|
|
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.
|
|
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.
|
|
667
|
-
// to '4.
|
|
668
|
-
// accepted transparently and
|
|
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.
|
|
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.
|
|
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.
|
|
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-
|
|
105
|
-
"peaks-loop-
|
|
106
|
-
"peaks-loop-shared
|
|
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.
|