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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,64 @@
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
+
48
+ ## 4.0.18 — 2026-08-10 (statusline 24h overlay)
49
+
50
+ **Bug fix — statusline doesn't reflect 24h mode substate after transition**:
51
+ - `src/services/skills/skill-statusline-service.ts`: adds `read24hOverlay` helper (name-distinct from canonical `src/services/24h-mode/store.ts:108 read24hState` so the two readers with divergent null vs empty-snapshot semantics are not confused); adds `TwentyFourHourOverlay` type (minimal `{ state: string }`); adds `twentyFourHourState: TwentyFourHourOverlay | null` field to `StatusLineModel`; populates the field in all 5 return paths of `buildStatusLineModel` (lines for projectRoot-null, invalid-presence, idle, outer-mismatch idle, and final active/stale — only the active branch calls `read24hOverlay`).
52
+ - `src/services/skills/skill-statusline-renderer.ts`: adds `format24hSuffix(overlay, palette, capability, noColor)` exported helper; extends `renderActive` signature to a 7th arg `twentyFourHourState`; appends `[24h-<state.toLowerCase()>]` suffix in 3 active-return branches (attention-gate / activeLeaf / normal); updates the call site at line 822.
53
+ - 12 new vitest cases in `tests/unit/skills/skill-statusline-sid-only-marker.test.ts` (3 service-layer `read24hOverlay` + 1 `buildStatusLineModel` integration + 3 `format24hSuffix` helper-level + 4 renderer-integration AC-1..AC-4 + 1 malformed-shape hardening).
54
+ - 5 typed-fixture sites updated to include `twentyFourHourState: null` (`tests/unit/services/skills/skill-statusline-renderer.test.ts` 5 factories + `tests/unit/skills/skill-statusline-sid-only-marker.test.ts` 3 directly-constructed literals).
55
+
56
+ **Safety semantics preserved**:
57
+ - v2.15.0 `presence:check-stale --project . --json` still returns `stale: true` on outer-mismatch. `skill-presence-service.ts` / `presence-lease-service.ts` / `workspace-service.ts` / `session/**` / `audit/**` / `src/services/24h-mode/**` / `src/cli/commands/session-24h-mode.ts` are FORBIDDEN files in this slice — zero edits.
58
+ - No new dependencies. No enum/API/command-surface changes.
59
+
60
+ **Migration**: zero changes required for existing users. Active 24h mode sessions automatically pick up the new `[24h-<state>]` suffix on next statusline render after the user runs `peaks session 24h-mode transition`. Sessions without a 24h-state.json file render unchanged (back-compat).
61
+
3
62
  ## 4.0.17 — 2026-08-07 (Vitest worker cap + statusline SIGTERM + 9-slice shipped)
4
63
 
5
64
  **Core fix — vitest worker concurrency cap (root-cause)**:
@@ -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,59 @@
1
+ /**
2
+ * Slice rid-statusline-stale-ux AC-2: `peaks session primer --project <path>`.
3
+ *
4
+ * Lightweight SessionStart primer that fires rotation + presence
5
+ * cleanup BEFORE the first statusline render of a fresh session.
6
+ * Independent subcommand (NOT a flag on `peaks workspace init` per
7
+ * task regulation: "don't add a flag to workspace init"). Cleaner
8
+ * permission boundary — primer cannot be mistaken for init by users.
9
+ *
10
+ * Performs only 3 things:
11
+ * 1. ensureSessionWithRotation(projectRoot) — the real rotation
12
+ * entry point (verified at
13
+ * src/services/session/session-binding-bridge.ts:461-527).
14
+ * 2. If outer-mismatch rotation occurred: call
15
+ * clearStalePresenceOnRotation with the verified single-options-
16
+ * object signature (mirrors init-command.ts:324-329 pattern with
17
+ * all 3 fields populated). Note: on 4.0.11-A sid-scoped leases
18
+ * this is a legacy-compat no-op; the real cleanup mechanism is
19
+ * rotation itself (rebinding changes the bound sid so the
20
+ * reader looks in the new sid's empty lease dir).
21
+ * 3. gcStalePresenceLeases({ projectRoot, trigger: 'manual' }) —
22
+ * sync; documented as legacy-compat no-op on 4.0.11-A sid-scoped
23
+ * data.
24
+ *
25
+ * SKIPS (per RD §4.2.1):
26
+ * - bootstrapProjectScan
27
+ * - materializeClaudeSettingsLocal
28
+ * - resolveFirstTimeHooksInstall
29
+ * - applyHookInstall (prevents accidental first-launch hook install)
30
+ *
31
+ * Mounted as a CHILD of the existing `session` commander group at
32
+ * `src/cli/commands/core/session-command.ts:32` (verified pattern:
33
+ * `session.command('list')...`). NOT `program.command('session primer')`.
34
+ *
35
+ * The action handler is exported as `runPrimerAction` so the
36
+ * integration test suite can drive it directly without going through
37
+ * Commander's argv-parsing wrapper (which would mangle empty /
38
+ * whitespace / NUL-byte inputs before reaching the action body).
39
+ */
40
+ import type { Command } from 'commander';
41
+ import type { ProgramIO } from '../cli-helpers.js';
42
+ export type PrimerOptions = {
43
+ project: string;
44
+ json?: boolean;
45
+ };
46
+ /**
47
+ * Pure action body for `peaks session primer`. Exported so
48
+ * integration tests can drive it directly with crafted inputs (empty
49
+ * string, whitespace, NUL byte) that would otherwise be rejected by
50
+ * Commander's argv parser.
51
+ *
52
+ * Returns `{ exitCode: 0 | 1 }` so callers (CLI path or test path)
53
+ * can apply the exit code uniformly. NEVER calls `process.exit` —
54
+ * exits are the responsibility of the CLI wrapper.
55
+ */
56
+ export declare function runPrimerAction(opts: PrimerOptions, io: ProgramIO): Promise<{
57
+ exitCode: 0 | 1;
58
+ }>;
59
+ export declare function registerPrimerCommand(program: Command, io: ProgramIO): void;
@@ -0,0 +1,160 @@
1
+ /**
2
+ * Slice rid-statusline-stale-ux AC-2: `peaks session primer --project <path>`.
3
+ *
4
+ * Lightweight SessionStart primer that fires rotation + presence
5
+ * cleanup BEFORE the first statusline render of a fresh session.
6
+ * Independent subcommand (NOT a flag on `peaks workspace init` per
7
+ * task regulation: "don't add a flag to workspace init"). Cleaner
8
+ * permission boundary — primer cannot be mistaken for init by users.
9
+ *
10
+ * Performs only 3 things:
11
+ * 1. ensureSessionWithRotation(projectRoot) — the real rotation
12
+ * entry point (verified at
13
+ * src/services/session/session-binding-bridge.ts:461-527).
14
+ * 2. If outer-mismatch rotation occurred: call
15
+ * clearStalePresenceOnRotation with the verified single-options-
16
+ * object signature (mirrors init-command.ts:324-329 pattern with
17
+ * all 3 fields populated). Note: on 4.0.11-A sid-scoped leases
18
+ * this is a legacy-compat no-op; the real cleanup mechanism is
19
+ * rotation itself (rebinding changes the bound sid so the
20
+ * reader looks in the new sid's empty lease dir).
21
+ * 3. gcStalePresenceLeases({ projectRoot, trigger: 'manual' }) —
22
+ * sync; documented as legacy-compat no-op on 4.0.11-A sid-scoped
23
+ * data.
24
+ *
25
+ * SKIPS (per RD §4.2.1):
26
+ * - bootstrapProjectScan
27
+ * - materializeClaudeSettingsLocal
28
+ * - resolveFirstTimeHooksInstall
29
+ * - applyHookInstall (prevents accidental first-launch hook install)
30
+ *
31
+ * Mounted as a CHILD of the existing `session` commander group at
32
+ * `src/cli/commands/core/session-command.ts:32` (verified pattern:
33
+ * `session.command('list')...`). NOT `program.command('session primer')`.
34
+ *
35
+ * The action handler is exported as `runPrimerAction` so the
36
+ * integration test suite can drive it directly without going through
37
+ * Commander's argv-parsing wrapper (which would mangle empty /
38
+ * whitespace / NUL-byte inputs before reaching the action body).
39
+ */
40
+ import { printResult } from '../cli-helpers.js';
41
+ import { fail, ok } from 'peaks-loop-shared/result';
42
+ import { resolveCanonicalProjectRootStrict, InvalidProjectRootError } from '../../services/config/config-safety.js';
43
+ import { ensureSessionWithRotation } from '../../services/session/session-manager.js';
44
+ import { clearStalePresenceOnRotation } from '../../services/skills/skill-presence-service.js';
45
+ import { gcStalePresenceLeases } from '../../services/skills/presence-lease-service.js';
46
+ /**
47
+ * Pure action body for `peaks session primer`. Exported so
48
+ * integration tests can drive it directly with crafted inputs (empty
49
+ * string, whitespace, NUL byte) that would otherwise be rejected by
50
+ * Commander's argv parser.
51
+ *
52
+ * Returns `{ exitCode: 0 | 1 }` so callers (CLI path or test path)
53
+ * can apply the exit code uniformly. NEVER calls `process.exit` —
54
+ * exits are the responsibility of the CLI wrapper.
55
+ */
56
+ export async function runPrimerAction(opts, io) {
57
+ // Explicit empty / whitespace-only guard BEFORE the strict
58
+ // resolver. Commander's requiredOption checks presence, not
59
+ // emptiness; node's `resolve('')` returns cwd (silent
60
+ // fall-through), so we must short-circuit explicitly.
61
+ if (!opts.project || opts.project.trim() === '') {
62
+ printResult(io, fail('session.primer', 'PRIMER_EMPTY_PROJECT', '--project <path> required and must be non-empty', { project: opts.project }, ['Pass a non-empty absolute path to --project']), opts.json);
63
+ return { exitCode: 1 };
64
+ }
65
+ // P1 H1 option A: use NEW strict helper. Throws on NUL /
66
+ // non-canonical / non-existent path. The fail-open
67
+ // `resolveCanonicalProjectRoot` is not safe here because primer
68
+ // is invoked from a SessionStart hook on a path pulled from
69
+ // `${CLAUDE_PROJECT_DIR}` — the env var is user-controlled and
70
+ // may contain path traversal payloads.
71
+ let projectRoot;
72
+ try {
73
+ projectRoot = resolveCanonicalProjectRootStrict(opts.project);
74
+ }
75
+ catch (error) {
76
+ if (error instanceof InvalidProjectRootError) {
77
+ printResult(io, fail('session.primer', `PRIMER_INVALID_PROJECT_ROOT_${error.reason.replace(/-/g, '_').toUpperCase()}`, error.message, { project: opts.project, reason: error.reason }, ['Pass a non-empty absolute canonical path to --project']), opts.json);
78
+ return { exitCode: 1 };
79
+ }
80
+ throw error;
81
+ }
82
+ // Real rotation entry point (verified
83
+ // src/services/session/session-binding-bridge.ts:461-527).
84
+ // NOT the invented `runRotationCheck` from cycle-2 RD.
85
+ const rotation = await ensureSessionWithRotation(projectRoot);
86
+ const rotationOccurred = rotation.previousSessionId !== null
87
+ && rotation.rotationReason === 'outer-session-mismatch';
88
+ let clearOutcome = null;
89
+ if (rotationOccurred) {
90
+ // VERIFIED single-options-object signature (see
91
+ // skill-presence-service.ts:584-588). Mirror the real caller
92
+ // pattern at init-command.ts:324-329 with all 3 fields
93
+ // populated. NOTE: on 4.0.11-A sid-scoped leases this is a
94
+ // legacy-compat no-op (clearSkillPresence only unlinks legacy
95
+ // single-slot files; the real cleanup mechanism is rotation
96
+ // itself).
97
+ clearOutcome = clearStalePresenceOnRotation({
98
+ projectRootOverride: projectRoot,
99
+ currentOuterSessionId: process.env.PEAKS_OUTER_SESSION_ID
100
+ ?? process.env.CLAUDE_CODE_SESSION_ID,
101
+ rotatedOutSessionId: rotation.previousSessionId
102
+ });
103
+ }
104
+ // Sync call (verified presence-lease-service.ts:383-414) —
105
+ // documented as legacy-compat no-op on 4.0.11-A sid-scoped
106
+ // data; iterates `input.leases ?? []` and gets `[]` on the
107
+ // real init-command.ts:463-466 caller.
108
+ const gcOutcome = gcStalePresenceLeases({
109
+ projectRoot,
110
+ trigger: 'manual'
111
+ });
112
+ printResult(io, ok('session.primer', {
113
+ projectRoot,
114
+ sessionId: rotation.sessionId,
115
+ rotationOccurred,
116
+ ...(rotation.previousSessionId !== null
117
+ ? { previousSessionId: rotation.previousSessionId }
118
+ : {}),
119
+ clearOutcome,
120
+ gcOutcome: {
121
+ removed: gcOutcome.removed,
122
+ retained: gcOutcome.retained
123
+ }
124
+ }), opts.json);
125
+ return { exitCode: 0 };
126
+ }
127
+ export function registerPrimerCommand(program, io) {
128
+ // Reuse the existing `session` commander group registered earlier
129
+ // in `createProgram` (via autoRegisterAllCommands → session-command.ts:32).
130
+ // Creating a new `program.command('session')` here throws
131
+ // `cannot add command 'session' as already have command 'session'`
132
+ // because commander disallows duplicate top-level command names.
133
+ // This regression was caught in publish.yml gate-changeset step
134
+ // (run #134, exit 1) on the first 4.0.18 release attempt.
135
+ const session = program.commands.find((c) => c.name() === 'session');
136
+ if (!session) {
137
+ throw new Error('registerPrimerCommand: session group not registered; ' +
138
+ 'autoRegisterAllCommands must run before registerPrimerCommand');
139
+ }
140
+ session
141
+ .command('primer')
142
+ .description('Lightweight SessionStart primer: rotation + presence cleanup. ' +
143
+ 'Does NOT bootstrap project scan / materialize settings / install hooks. ' +
144
+ 'Idempotent; safe to run on every SessionStart.')
145
+ .requiredOption('--project <path>', 'Project root (must be non-empty canonical path)')
146
+ .option('--json', 'emit a JSON envelope { ok, data } to stdout')
147
+ .action(async (opts) => {
148
+ try {
149
+ const { exitCode } = await runPrimerAction(opts, io);
150
+ if (exitCode !== 0) {
151
+ process.exitCode = exitCode;
152
+ }
153
+ }
154
+ catch (error) {
155
+ const message = error instanceof Error ? error.message : String(error);
156
+ printResult(io, fail('session.primer', 'PRIMER_FAILED', message, { project: opts.project }, ['Verify the project path exists, is writable, and is canonical']), opts.json);
157
+ process.exitCode = 1;
158
+ }
159
+ });
160
+ }
@@ -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
+ }
@@ -16,6 +16,7 @@ import { registerCronSchedulerCommand } from './commands/cron-scheduler-commands
16
16
  import { registerWorkspaceCommands } from './commands/workspace-commands.js';
17
17
  import { registerSopCommands } from './commands/sop-commands.js';
18
18
  import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
19
+ import { registerPrimerCommand } from './commands/primer-command.js';
19
20
  import { applyRetention, cleanupEccCache } from '../services/log/retention.js';
20
21
  import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
21
22
  import { printSuperCommandCatalog } from './cli-helpers.js';
@@ -209,5 +210,12 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
209
210
  // registerCoreAndArtifactCommands instead of being created twice.
210
211
  autoRegisterAllCommands(program, io);
211
212
  registerSkillVisibilityCommand(program, repoRoot);
213
+ // Slice rid-statusline-stale-ux AC-2: register `peaks session primer`
214
+ // so it appears in `peaks session --help` for LLM `<TAB>`-discovery.
215
+ // Mounted as a CHILD of the existing `session` commander group
216
+ // (verified at src/cli/commands/core/session-command.ts:32). NOT
217
+ // `program.command('session primer')` (that registers a single
218
+ // literal command name, not a child of the session group).
219
+ registerPrimerCommand(program, io);
212
220
  return program;
213
221
  }
@@ -28,6 +28,29 @@ export declare function resolveProjectRootForConfig(startPath: string): string;
28
28
  * `git rev-parse` exit; both fall through to the heuristic.
29
29
  */
30
30
  export declare function resolveCanonicalProjectRoot(startPath: string): string;
31
+ /**
32
+ * Slice rid-statusline-stale-ux AC-2 + P1 H1 option A: strict
33
+ * canonicalization for use in trust-boundary paths (e.g.
34
+ * `peaks session primer`).
35
+ *
36
+ * Unlike `resolveCanonicalProjectRoot` (fail-open, returns `start`
37
+ * on any resolution failure), this helper THROWS on:
38
+ * - empty / whitespace-only input
39
+ * - NUL byte in input
40
+ * - non-existent path (realpathSync ENOENT)
41
+ * - non-canonical (symlink) input (when the input path and its
42
+ * realpath differ in a way that suggests the user passed a
43
+ * symlinked path through a security-sensitive surface)
44
+ *
45
+ * Existing fail-open `resolveCanonicalProjectRoot` is retained for
46
+ * back-compat — many callers depend on the fail-open behavior
47
+ * (e.g. `init-command.ts:32`). The strict variant is opt-in.
48
+ */
49
+ export declare class InvalidProjectRootError extends Error {
50
+ readonly reason: 'empty' | 'nul-byte' | 'non-existent' | 'non-canonical';
51
+ constructor(reason: 'empty' | 'nul-byte' | 'non-existent' | 'non-canonical', input: string);
52
+ }
53
+ export declare function resolveCanonicalProjectRootStrict(startPath: string): string;
31
54
  export declare function getProjectConfigPath(projectRoot: string | null): string | null;
32
55
  export declare function getProjectBootstrapConfigPath(projectRoot: string): string;
33
56
  export declare function validateProjectBootstrapConfigPathForWrite(projectRoot: string, configPath: string): void;
@@ -131,6 +131,68 @@ export function resolveCanonicalProjectRoot(startPath) {
131
131
  }
132
132
  return start;
133
133
  }
134
+ /**
135
+ * Slice rid-statusline-stale-ux AC-2 + P1 H1 option A: strict
136
+ * canonicalization for use in trust-boundary paths (e.g.
137
+ * `peaks session primer`).
138
+ *
139
+ * Unlike `resolveCanonicalProjectRoot` (fail-open, returns `start`
140
+ * on any resolution failure), this helper THROWS on:
141
+ * - empty / whitespace-only input
142
+ * - NUL byte in input
143
+ * - non-existent path (realpathSync ENOENT)
144
+ * - non-canonical (symlink) input (when the input path and its
145
+ * realpath differ in a way that suggests the user passed a
146
+ * symlinked path through a security-sensitive surface)
147
+ *
148
+ * Existing fail-open `resolveCanonicalProjectRoot` is retained for
149
+ * back-compat — many callers depend on the fail-open behavior
150
+ * (e.g. `init-command.ts:32`). The strict variant is opt-in.
151
+ */
152
+ export class InvalidProjectRootError extends Error {
153
+ reason;
154
+ constructor(reason, input) {
155
+ super(`Invalid project root (${reason}): "${input}"`);
156
+ this.reason = reason;
157
+ this.name = 'InvalidProjectRootError';
158
+ }
159
+ }
160
+ export function resolveCanonicalProjectRootStrict(startPath) {
161
+ if (!startPath || startPath.trim() === '') {
162
+ throw new InvalidProjectRootError('empty', startPath);
163
+ }
164
+ if (startPath.indexOf('\0') !== -1) {
165
+ throw new InvalidProjectRootError('nul-byte', startPath);
166
+ }
167
+ const start = resolve(startPath);
168
+ let realStart;
169
+ try {
170
+ realStart = realpathSync(start);
171
+ }
172
+ catch {
173
+ throw new InvalidProjectRootError('non-existent', startPath);
174
+ }
175
+ if (realStart !== start) {
176
+ // User passed a path through a symlink; reject as non-canonical
177
+ // for trust-boundary entry points.
178
+ throw new InvalidProjectRootError('non-canonical', startPath);
179
+ }
180
+ // Delegate to the existing canonicalization (git root → heuristic)
181
+ // AFTER the strict pre-checks pass.
182
+ const canonical = resolveCanonicalProjectRoot(startPath);
183
+ if (canonical === startPath || canonical === realStart) {
184
+ return canonical;
185
+ }
186
+ // Canonicalization moved the path (e.g. to a git root) — accept it
187
+ // but ensure it also exists.
188
+ try {
189
+ realpathSync(canonical);
190
+ return canonical;
191
+ }
192
+ catch {
193
+ throw new InvalidProjectRootError('non-existent', startPath);
194
+ }
195
+ }
134
196
  function resolveProjectRootFromGit(startPath) {
135
197
  // execFileSync (not execSync) so a malicious `startPath` cannot
136
198
  // inject argv into the spawned git invocation. The child only