peaks-loop 4.0.0-beta.5 → 4.0.0-beta.7

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 (58) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
  3. package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
  4. package/dist/src/cli/commands/ide-commands.d.ts +25 -0
  5. package/dist/src/cli/commands/ide-commands.js +42 -0
  6. package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
  7. package/dist/src/cli/commands/polyrepo-commands.js +155 -0
  8. package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
  9. package/dist/src/cli/commands/runtime-commands.js +150 -0
  10. package/dist/src/cli/program.js +23 -0
  11. package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
  12. package/dist/src/services/adapter/adapter-registry.js +186 -0
  13. package/dist/src/services/config/model-routing.d.ts +55 -2
  14. package/dist/src/services/config/model-routing.js +79 -3
  15. package/dist/src/services/ide/adapters/zcode-adapter.d.ts +40 -0
  16. package/dist/src/services/ide/adapters/zcode-adapter.js +220 -0
  17. package/dist/src/services/ide/current-model-detector.d.ts +14 -0
  18. package/dist/src/services/ide/current-model-detector.js +60 -0
  19. package/dist/src/services/ide/ide-registry.js +3 -0
  20. package/dist/src/services/ide/ide-types.d.ts +24 -1
  21. package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
  22. package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
  23. package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
  24. package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
  25. package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
  26. package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
  27. package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
  28. package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
  29. package/dist/src/services/preferences/preferences-types.d.ts +1 -1
  30. package/dist/src/services/providers/minimax-worker-service.d.ts +1 -1
  31. package/dist/src/services/providers/minimax-worker-service.js +1 -1
  32. package/dist/src/services/rd/rd-service.js +7 -4
  33. package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
  34. package/dist/src/services/runtime/runtime-detector.js +62 -0
  35. package/dist/src/services/runtime/runtime-service.d.ts +59 -0
  36. package/dist/src/services/runtime/runtime-service.js +51 -0
  37. package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
  38. package/dist/src/services/runtime/vendor-adapter.js +1 -0
  39. package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
  40. package/dist/src/services/runtime/vendors/claude-code.js +39 -0
  41. package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
  42. package/dist/src/services/runtime/vendors/codex.js +43 -0
  43. package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
  44. package/dist/src/services/runtime/vendors/copilot.js +40 -0
  45. package/dist/src/services/workflow/workflow-router-service.d.ts +1 -1
  46. package/dist/src/services/workflow/workflow-router-service.js +37 -31
  47. package/dist/src/shared/version.d.ts +1 -1
  48. package/dist/src/shared/version.js +1 -1
  49. package/package.json +1 -1
  50. package/scripts/install-skills.mjs +13 -10
  51. package/skills/peaks-code/SKILL.md +26 -12
  52. package/skills/peaks-code/references/external-references.md +1 -3
  53. package/skills/peaks-code/references/external-skill-invocation.md +1 -1
  54. package/skills/peaks-code/references/runbook.md +1 -3
  55. package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
  56. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
  57. package/skills/peaks-slice-decompose/SKILL.md +61 -1
  58. package/skills/peaks-code/references/openspec-workflow.md +0 -81
@@ -0,0 +1,49 @@
1
+ /**
2
+ * Polyrepo service — slice S2-b of RD-2.
3
+ *
4
+ * Thin orchestrator wrapping the scanner + dispatcher. CLI commands
5
+ * call into this service so they don't need to know about the
6
+ * individual helpers.
7
+ */
8
+ import { dispatchArtifact, readManifest, writeManifest, manifestPath } from './polyrepo-dispatcher.js';
9
+ import { scanPolyrepo } from './polyrepo-scanner.js';
10
+ export class PolyrepoService {
11
+ /** Run a scan and persist the manifest. Returns the manifest + a
12
+ * `created` flag indicating whether a new manifest was written
13
+ * (false = existing manifest was overwritten). */
14
+ init(opts) {
15
+ const existing = readManifest(opts.root);
16
+ const manifest = opts.children === undefined
17
+ ? scanPolyrepo(opts.root)
18
+ : scanPolyrepo(opts.root, { explicitChildren: opts.children });
19
+ writeManifest(opts.root, manifest);
20
+ return { manifest, created: existing === null };
21
+ }
22
+ /** Read the persisted manifest + report children. */
23
+ status(root) {
24
+ const m = readManifest(root);
25
+ return {
26
+ root,
27
+ manifestExists: m !== null,
28
+ children: m?.children ?? [],
29
+ detectedAt: m?.detectedAt ?? null
30
+ };
31
+ }
32
+ /** Dispatch an artifact to the named child targets. Wrapper
33
+ * around dispatchArtifact that resolves the manifest from disk
34
+ * first. Throws when no manifest exists (caller should run init
35
+ * first). */
36
+ dispatch(root, params) {
37
+ const manifest = readManifest(root);
38
+ if (manifest === null) {
39
+ throw new Error(`no polyrepo manifest at ${manifestPath(root)} — run \`peaks polyrepo init\` first`);
40
+ }
41
+ return dispatchArtifact({
42
+ manifest,
43
+ sid: params.sid,
44
+ rid: params.rid,
45
+ targets: params.targets,
46
+ artifact: { role: params.role, path: params.artifactPath }
47
+ });
48
+ }
49
+ }
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Polyrepo types — slice S2-b of RD-2.
3
+ *
4
+ * Models a parent directory that hosts one or more child git repos,
5
+ * each of which may carry its own `.peaks/` state. The root `.peaks/`
6
+ * is the source of truth for cross-child orchestration; child
7
+ * `.peaks/` are subsets that mirror only the artifacts the child
8
+ * needs (PRD §4.3).
9
+ */
10
+ export type PeaksScope = 'root+child' | 'child-only';
11
+ /** A single child repo discovered under the parent root. */
12
+ export interface PolyrepoChild {
13
+ /** Stable id derived from the basename (e.g. `frontend`, `backend`). */
14
+ readonly id: string;
15
+ /** Absolute path to the child directory. */
16
+ readonly path: string;
17
+ /** Whether `.git` exists at the child root (true git repo, not just a subdir). */
18
+ readonly gitRoot: boolean;
19
+ /** Whether the child carries its own `.peaks/` artifact subset. */
20
+ readonly peaksScope: PeaksScope;
21
+ /** Whether the child has peaks-loop installed (peaks binary on PATH
22
+ * is NOT checked here — that's done at dispatch time). */
23
+ readonly peaksInstalled: boolean;
24
+ }
25
+ /** The polyrepo manifest, written to `.peaks/polyrepo.json` on init. */
26
+ export interface PolyrepoManifest {
27
+ /** Schema version — increment on breaking changes. */
28
+ readonly version: 1;
29
+ /** Absolute path to the parent (root) directory. */
30
+ readonly root: string;
31
+ /** ISO-8601 timestamp of when this manifest was generated. */
32
+ readonly detectedAt: string;
33
+ /** Children discovered under the root. */
34
+ readonly children: PolyrepoChild[];
35
+ }
36
+ /** A dispatch record — written to `.peaks/_runtime/<sid>/sc/dispatch/<rid>.json`
37
+ * AND mirrored to each target child's
38
+ * `.peaks/_runtime/<sid>/sc/dispatch/<rid>.json`. The two-axis
39
+ * convention from .peaks/standards/ is preserved: reviewable
40
+ * artifacts live under `.peaks/_runtime/<sid>/<role>/`, never as
41
+ * top-level siblings of `.peaks/_runtime/`. */
42
+ export interface PolyrepoDispatch {
43
+ readonly rid: string;
44
+ readonly sid: string;
45
+ readonly root: string;
46
+ /** Child ids targeted by this dispatch. Empty array = root-only. */
47
+ readonly targets: string[];
48
+ /** ISO timestamp. */
49
+ readonly dispatchedAt: string;
50
+ /** Source-of-truth for the artifact body. */
51
+ readonly artifact: {
52
+ readonly role: 'prd' | 'rd' | 'qa';
53
+ readonly path: string;
54
+ };
55
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -71,7 +71,7 @@ export interface ProjectPreferences {
71
71
  * Slice 2026-06-28-code-mode-bypass-fix: economyMode is a
72
72
  * **MODEL-SELECTION** knob only — it chooses between the cheap
73
73
  * configured provider (`getConfiguredExecutionModelId`) and the
74
- * strongest planner/reviewer model (`STRONGEST_MODEL_ID`). It does
74
+ * strongest planner/reviewer model (`getStrongestModelId(...)`). It does
75
75
  * NOT throttle concurrency, fan-out, or worker count. Per user
76
76
  * direction 2026-06-28: "效率比省钱更重要,是在效率达到最大值的时
77
77
  * 候,再去考虑经济问题" (efficiency first; economy is only considered
@@ -10,7 +10,7 @@ export type MiniMaxWorkerRequest = {
10
10
  export type MiniMaxWorkerResult = {
11
11
  provider: MiniMaxProviderSmokeResult;
12
12
  reviewHandoff: {
13
- model: 'claude-opus-4-7';
13
+ model: string;
14
14
  prompt: string;
15
15
  };
16
16
  constraints: {
@@ -62,7 +62,7 @@ export async function runMiniMaxWorker(config, request, fetchImpl = fetch) {
62
62
  successMatch: 'startsWith'
63
63
  }, fetchImpl);
64
64
  const reviewHandoff = {
65
- model: 'claude-opus-4-7',
65
+ model: process.env.PEAKS_MINIMAX_REVIEW_MODEL ?? 'claude-opus-4-7',
66
66
  prompt: [
67
67
  `Review this MiniMax worker result for change ${sessionId}.`,
68
68
  `Goal: ${goal}`,
@@ -4,7 +4,7 @@ import { stableRealPath } from '../../shared/path-utils.js';
4
4
  import { WORKSPACE_UNAVAILABLE_NEXT_ACTIONS } from '../../shared/planner-response.js';
5
5
  import { getLocalArtifactPath, hasValidArtifactWorkspace } from '../artifacts/workspace-service.js';
6
6
  import { getSessionDir } from '../session/getSessionDir.js';
7
- import { getConfiguredExecutionModelId, STRONGEST_MODEL_ID } from '../config/model-routing.js';
7
+ import { getConfiguredExecutionModelId, getStrongestModelId } from '../config/model-routing.js';
8
8
  /**
9
9
  * 2.0.1-bug1: the slim 2.0 `~/.peaks/config.json` no longer carries a
10
10
  * `providers` block (legacy model config lives in
@@ -116,8 +116,8 @@ function selectConcreteTargetArea(targetAreas, index) {
116
116
  function getTaskModelRole(wave) {
117
117
  return wave === 'implementation candidates' || wave === 'unit-test execution' ? 'execution' : 'strongest';
118
118
  }
119
- function getTaskModelId(modelRole, executionModelId) {
120
- return modelRole === 'execution' ? executionModelId : STRONGEST_MODEL_ID;
119
+ function getTaskModelId(modelRole, executionModelId, strongestModelId) {
120
+ return modelRole === 'execution' ? executionModelId : strongestModelId;
121
121
  }
122
122
  const MAX_ARTIFACT_BYTES = 256_000;
123
123
  function readArtifactFile(rootPath, artifactWorkspacePath, artifact) {
@@ -220,6 +220,9 @@ function buildPlan(request, standardsOverlay) {
220
220
  const goal = normalizeGoal(request.goal);
221
221
  const swarmMode = request.swarmMode ?? true;
222
222
  const executionModelId = request.executionModelId?.trim() || resolveExecutionModelId();
223
+ // Slice 2026-07-09 add-zcode-adapter (A.3): strongest planner/reviewer
224
+ // model is resolved dynamically (config.model → env-var fallback).
225
+ const strongestModelId = getStrongestModelId();
223
226
  const { workerTarget, blockedReasons } = resolveWorkerTarget(request.maxWorkers);
224
227
  const artifactWorkspacePath = resolveArtifactWorkspacePath(request);
225
228
  const artifactRoot = 'rd/swarm';
@@ -342,7 +345,7 @@ function buildPlan(request, standardsOverlay) {
342
345
  workerKind: taskId,
343
346
  purpose: `${taskId.replace(/^rd-/, '').replace(/-/g, ' ')} for ${goal}`,
344
347
  modelRole,
345
- modelId: getTaskModelId(modelRole, executionModelId),
348
+ modelId: getTaskModelId(modelRole, executionModelId, strongestModelId),
346
349
  inputs: [goal, artifactRoot],
347
350
  outputs: [briefPath],
348
351
  dependsOn: [...waveDependencies[wave]],
@@ -0,0 +1,15 @@
1
+ export type DetectedVendor = 'claude-code' | 'codex' | 'copilot' | 'unknown';
2
+ export interface RuntimeDetectorOptions {
3
+ /** Override for env (used by tests). */
4
+ readonly env?: NodeJS.ProcessEnv;
5
+ /** Override for home directory (used by tests). */
6
+ readonly home?: string;
7
+ }
8
+ export interface DetectionResult {
9
+ readonly vendor: DetectedVendor;
10
+ /** Why this vendor was chosen — useful for diagnostics and tests. */
11
+ readonly reason: string;
12
+ }
13
+ /** Detect the active vendor. Order: claude-code → codex → copilot →
14
+ * unknown. The first match wins. */
15
+ export declare function detectRuntime(opts?: RuntimeDetectorOptions): DetectionResult;
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Runtime detector — slice S2-a of RD-2.
3
+ *
4
+ * Given the current process environment, decide which vendor (if any)
5
+ * is the active AI runtime. Detection order matters: Claude Code is
6
+ * checked first because it is the default runtime for peaks-loop
7
+ * (the project literally runs inside Claude Code). Codex + Copilot
8
+ * are checked second; if none match we report `unknown`.
9
+ *
10
+ * Detectors MUST be cheap (no process spawn, no network). They look
11
+ * at env vars + filesystem sentinels + home-dir shapes only. This
12
+ * keeps `peaks runtime detect` sub-100ms even on cold paths.
13
+ */
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ /** Heuristic: Claude Code sets CLAUDE_CODE_ENTRYPOINT or has a
17
+ * .claude/skills directory under HOME (peaks-loop installs there). */
18
+ function detectClaudeCode(opts) {
19
+ const env = opts.env ?? process.env;
20
+ if (env.CLAUDE_CODE === '1' || env.CLAUDE_CODE === 'true')
21
+ return true;
22
+ if (env.CLAUDE_CODE_ENTRYPOINT !== undefined && env.CLAUDE_CODE_ENTRYPOINT.length > 0)
23
+ return true;
24
+ const home = opts.home ?? process.env.HOME ?? process.env.USERPROFILE ?? '';
25
+ if (home.length > 0 && existsSync(join(home, '.claude')))
26
+ return true;
27
+ return false;
28
+ }
29
+ /** Heuristic: Codex sets CODEX_HOME or has a .codex dir under HOME. */
30
+ function detectCodex(opts) {
31
+ const env = opts.env ?? process.env;
32
+ if (env.CODEX_HOME !== undefined && env.CODEX_HOME.length > 0)
33
+ return true;
34
+ const home = opts.home ?? process.env.HOME ?? process.env.USERPROFILE ?? '';
35
+ if (home.length > 0 && existsSync(join(home, '.codex')))
36
+ return true;
37
+ return false;
38
+ }
39
+ /** Heuristic: Copilot sets GITHUB_COPILOT or has a .copilot dir. */
40
+ function detectCopilot(opts) {
41
+ const env = opts.env ?? process.env;
42
+ if (env.GITHUB_COPILOT === '1' || env.GITHUB_COPILOT === 'true')
43
+ return true;
44
+ const home = opts.home ?? process.env.HOME ?? process.env.USERPROFILE ?? '';
45
+ if (home.length > 0 && existsSync(join(home, '.copilot')))
46
+ return true;
47
+ return false;
48
+ }
49
+ /** Detect the active vendor. Order: claude-code → codex → copilot →
50
+ * unknown. The first match wins. */
51
+ export function detectRuntime(opts = {}) {
52
+ if (detectClaudeCode(opts)) {
53
+ return { vendor: 'claude-code', reason: 'CLAUDE_CODE_ENTRYPOINT / CLAUDE_CODE / ~/.claude matched' };
54
+ }
55
+ if (detectCodex(opts)) {
56
+ return { vendor: 'codex', reason: 'CODEX_HOME / ~/.codex matched' };
57
+ }
58
+ if (detectCopilot(opts)) {
59
+ return { vendor: 'copilot', reason: 'GITHUB_COPILOT / ~/.copilot matched' };
60
+ }
61
+ return { vendor: 'unknown', reason: 'no vendor sentinel detected' };
62
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Runtime service — slice S2-a of RD-2.
3
+ *
4
+ * Orchestrator that wires the runtime detector + vendor adapter
5
+ * registry into a small surface that the CLI commands call. The
6
+ * service owns the canonical list of built-in adapters (claude-code /
7
+ * codex / copilot) and decides which adapter handles a given
8
+ * `--via <id>` request.
9
+ *
10
+ * Design notes (Karpathy #2 Simplicity First):
11
+ *
12
+ * - The service is a thin orchestrator — it does NOT contain any
13
+ * vendor verb strings (claude / codex / copilot). All vendor verbs
14
+ * live in `src/services/runtime/vendors/<vendor>.ts`. Verified by
15
+ * AC-1.
16
+ * - When `compactVia(id)` is called with an unknown id, the service
17
+ * returns a "no-op" result with exitCode=0 and a warning instead of
18
+ * throwing — vendor-neutrality means peaks-loop MUST keep working
19
+ * even when the requested vendor adapter isn't registered.
20
+ * - The service is the SOLE owner of the built-in adapter list. Tests
21
+ * and CLI commands should never instantiate adapters directly;
22
+ * they should call `listBuiltInAdapters()` or `getAdapter(id)`.
23
+ */
24
+ import type { VendorAdapter, VendorCompactResult } from './vendor-adapter.js';
25
+ export interface RuntimeServiceOptions {
26
+ /** Override the built-in adapter list (used by tests + the
27
+ * adapter-registry's `register()` flow). */
28
+ readonly builtIns?: VendorAdapter[];
29
+ /** Override the home dir used by adapter detect() helpers. */
30
+ readonly home?: string;
31
+ }
32
+ /** Pure orchestrator. The runtime service holds built-in adapters as
33
+ * the baseline registry; the AdapterRegistry layer (`src/services/
34
+ * adapter/adapter-registry.ts`) extends this baseline at runtime via
35
+ * user-registered adapters persisted to `.peaks/runtime/adapters.json`.
36
+ * The two layers are intentionally separate:
37
+ * - runtime-service: built-in vendor adapters for the runtime-detect
38
+ * compact flow (S2-a).
39
+ * - adapter-registry: persisted user-registered adapters (S2-a).
40
+ * runtime-service itself does NOT consult the adapter registry —
41
+ * callers (`peaks runtime compact --via <id>`) layer them: first look
42
+ * up the id in the persisted registry, fall back to the built-in list.
43
+ */
44
+ export declare class RuntimeService {
45
+ private readonly builtIns;
46
+ constructor(opts?: RuntimeServiceOptions);
47
+ /** Return all built-in adapters. Order: claude-code, codex, copilot. */
48
+ listBuiltInAdapters(): VendorAdapter[];
49
+ /** Resolve a built-in adapter by id. Returns undefined when not
50
+ * found — caller should fall back to adapter registry or unknown. */
51
+ getBuiltInAdapter(id: string): VendorAdapter | undefined;
52
+ /** Compact via a built-in adapter. When the id is unknown, returns
53
+ * a no-op result with a warning rather than throwing — vendor
54
+ * neutrality demands peaks-loop stay alive even when a vendor is
55
+ * not wired up. */
56
+ compactVia(id: string, force: boolean | undefined): Promise<VendorCompactResult & {
57
+ warning?: string;
58
+ }>;
59
+ }
@@ -0,0 +1,51 @@
1
+ import { ClaudeCodeAdapter } from './vendors/claude-code.js';
2
+ import { CodexAdapter } from './vendors/codex.js';
3
+ import { CopilotAdapter } from './vendors/copilot.js';
4
+ const DEFAULT_BUILT_INS = [
5
+ new ClaudeCodeAdapter(),
6
+ new CodexAdapter(),
7
+ new CopilotAdapter()
8
+ ];
9
+ /** Pure orchestrator. The runtime service holds built-in adapters as
10
+ * the baseline registry; the AdapterRegistry layer (`src/services/
11
+ * adapter/adapter-registry.ts`) extends this baseline at runtime via
12
+ * user-registered adapters persisted to `.peaks/runtime/adapters.json`.
13
+ * The two layers are intentionally separate:
14
+ * - runtime-service: built-in vendor adapters for the runtime-detect
15
+ * compact flow (S2-a).
16
+ * - adapter-registry: persisted user-registered adapters (S2-a).
17
+ * runtime-service itself does NOT consult the adapter registry —
18
+ * callers (`peaks runtime compact --via <id>`) layer them: first look
19
+ * up the id in the persisted registry, fall back to the built-in list.
20
+ */
21
+ export class RuntimeService {
22
+ builtIns;
23
+ constructor(opts = {}) {
24
+ this.builtIns = opts.builtIns ?? [...DEFAULT_BUILT_INS];
25
+ }
26
+ /** Return all built-in adapters. Order: claude-code, codex, copilot. */
27
+ listBuiltInAdapters() {
28
+ return [...this.builtIns];
29
+ }
30
+ /** Resolve a built-in adapter by id. Returns undefined when not
31
+ * found — caller should fall back to adapter registry or unknown. */
32
+ getBuiltInAdapter(id) {
33
+ return this.builtIns.find((a) => a.id === id);
34
+ }
35
+ /** Compact via a built-in adapter. When the id is unknown, returns
36
+ * a no-op result with a warning rather than throwing — vendor
37
+ * neutrality demands peaks-loop stay alive even when a vendor is
38
+ * not wired up. */
39
+ async compactVia(id, force) {
40
+ const adapter = this.getBuiltInAdapter(id);
41
+ if (adapter === undefined) {
42
+ return {
43
+ exitCode: 0,
44
+ stdout: '',
45
+ stderr: '',
46
+ warning: `no built-in adapter registered for id="${id}" — falling back to no-op; use \`peaks adapter register --id ${id} --binary <cmd>\` to wire one`
47
+ };
48
+ }
49
+ return adapter.compact({ force: force === true });
50
+ }
51
+ }
@@ -0,0 +1,59 @@
1
+ /**
2
+ * Vendor adapter contract — slice S2-a of RD-2 (2026-07-08 session).
3
+ *
4
+ * Each `VendorAdapter` is a thin wrapper around ONE AI runtime (Claude
5
+ * Code / Codex / Copilot / custom) that knows how to:
6
+ *
7
+ * 1. Detect whether the current environment is that vendor
8
+ * (env vars, sentinel binaries, environment shape).
9
+ * 2. Issue a compact command through that vendor without forcing
10
+ * peaks-loop's core code to know vendor-specific verbs.
11
+ *
12
+ * The interface is intentionally MINIMAL (Karpathy #2 Simplicity First):
13
+ * two methods (`detect`, `compact`) plus an `id` + `displayName`.
14
+ * New verbs (status, refactor, etc.) are NOT in scope — they would
15
+ * bloat the surface and force every vendor to implement every verb
16
+ * whether the vendor supports it or not. When a future verb is needed
17
+ * it can either be added to the interface (with all vendors forced to
18
+ * implement) or — preferred — handled through a separate
19
+ * verb-specific adapter family.
20
+ *
21
+ * Vendor-specific verb strings (`claude --compact`, `codex --compact`,
22
+ * `copilot compact`) MUST live ONLY in adapter implementations under
23
+ * `src/services/runtime/vendors/<vendor>.ts`. The runtime service that
24
+ * orchestrates these adapters (runtime-service.ts) and the CLI commands
25
+ * (`peaks runtime compact --via <id>`) MUST NOT contain vendor verbs
26
+ * — verified by AC-1: `rg -n "claude --compact|codex --compact|copilot
27
+ * compact" src/services/code/` must return 0 matches.
28
+ *
29
+ * See `.peaks/_runtime/2026-07-08-session-17918f/prd/002-adapter-runtime-and-polyrepo.md`
30
+ * for the source PRD.
31
+ */
32
+ export interface VendorCompactArgs {
33
+ /** When `true`, the vendor is asked to compact unconditionally even
34
+ * if its auto-detector says it would not normally do so. */
35
+ readonly force?: boolean;
36
+ }
37
+ export interface VendorCompactResult {
38
+ readonly exitCode: number;
39
+ readonly stdout: string;
40
+ readonly stderr: string;
41
+ }
42
+ export interface VendorAdapter {
43
+ /** Stable identifier used by `peaks runtime compact --via <id>` and
44
+ * by the adapter registry. Convention: lowercase, dash-separated,
45
+ * e.g. `claude-code`, `codex`, `copilot`. */
46
+ readonly id: string;
47
+ /** Human-readable name shown in `peaks runtime list` output. */
48
+ readonly displayName: string;
49
+ /** Whether the CURRENT environment is this vendor. Detectors MUST
50
+ * be pure functions of env + filesystem; they MUST NOT spawn
51
+ * processes or call vendor CLIs (a detect that itself runs the
52
+ * vendor would defeat the purpose of asking whether the vendor is
53
+ * present). */
54
+ detect(): Promise<boolean>;
55
+ /** Issue a compact command for this vendor. The adapter chooses
56
+ * which binary + args + env to invoke; callers MUST NOT pass
57
+ * vendor verbs through this method. */
58
+ compact(args?: VendorCompactArgs): Promise<VendorCompactResult>;
59
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ import type { VendorAdapter, VendorCompactArgs, VendorCompactResult } from '../vendor-adapter.js';
2
+ export declare class ClaudeCodeAdapter implements VendorAdapter {
3
+ readonly id = "claude-code";
4
+ readonly displayName = "Claude Code";
5
+ detect(): Promise<boolean>;
6
+ compact(args?: VendorCompactArgs): Promise<VendorCompactResult>;
7
+ }
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Claude Code vendor adapter — slice S2-a.
3
+ *
4
+ * Stubs the compact verb to `claude --compact` (the canonical vendor
5
+ * verb for Claude Code). The actual binary is invoked via spawn so
6
+ * the adapter does NOT block peaks-loop. When the `claude` binary is
7
+ * not on PATH the adapter returns exitCode=127 + stderr explaining
8
+ * the missing binary, which the runtime service surfaces as a warning
9
+ * + no-op rather than a fatal error (vendor-neutrality: peaks-loop
10
+ * should still work even when the host vendor CLI is absent).
11
+ */
12
+ import { spawn } from 'node:child_process';
13
+ export class ClaudeCodeAdapter {
14
+ id = 'claude-code';
15
+ displayName = 'Claude Code';
16
+ async detect() {
17
+ return process.env.CLAUDE_CODE === '1'
18
+ || process.env.CLAUDE_CODE === 'true'
19
+ || (process.env.CLAUDE_CODE_ENTRYPOINT?.length ?? 0) > 0;
20
+ }
21
+ async compact(args = {}) {
22
+ const argv = ['--compact'];
23
+ if (args.force === true)
24
+ argv.push('--force');
25
+ return new Promise((resolveRun) => {
26
+ const proc = spawn('claude', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
27
+ let stdout = '';
28
+ let stderr = '';
29
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
30
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
31
+ proc.on('error', (err) => {
32
+ resolveRun({ exitCode: 127, stdout, stderr: stderr + (stderr.length > 0 ? '\n' : '') + err.message });
33
+ });
34
+ proc.on('close', (code) => {
35
+ resolveRun({ exitCode: code ?? 0, stdout, stderr });
36
+ });
37
+ });
38
+ }
39
+ }
@@ -0,0 +1,7 @@
1
+ import type { VendorAdapter, VendorCompactArgs, VendorCompactResult } from '../vendor-adapter.js';
2
+ export declare class CodexAdapter implements VendorAdapter {
3
+ readonly id = "codex";
4
+ readonly displayName = "Codex";
5
+ detect(): Promise<boolean>;
6
+ compact(args?: VendorCompactArgs): Promise<VendorCompactResult>;
7
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * Codex vendor adapter — slice S2-a.
3
+ *
4
+ * Stub adapter (PRD out-of-scope: full implementation lands in a
5
+ * future slice). The detect() heuristic is intentionally permissive:
6
+ * if CODEX_HOME or ~/.codex is present we report detected=true so
7
+ * `peaks runtime detect` exercises the codex branch. compact() uses
8
+ * the canonical `codex --compact` invocation; missing-binary is
9
+ * reported as exitCode=127, not a thrown error (the runtime service
10
+ * surfaces that as a warning + no-op so a missing vendor CLI does
11
+ * not break peaks-loop).
12
+ */
13
+ import { spawn } from 'node:child_process';
14
+ import { existsSync } from 'node:fs';
15
+ import { join } from 'node:path';
16
+ export class CodexAdapter {
17
+ id = 'codex';
18
+ displayName = 'Codex';
19
+ async detect() {
20
+ if (process.env.CODEX_HOME !== undefined && process.env.CODEX_HOME.length > 0)
21
+ return true;
22
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
23
+ return home.length > 0 && existsSync(join(home, '.codex'));
24
+ }
25
+ async compact(args = {}) {
26
+ const argv = ['--compact'];
27
+ if (args.force === true)
28
+ argv.push('--force');
29
+ return new Promise((resolveRun) => {
30
+ const proc = spawn('codex', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
31
+ let stdout = '';
32
+ let stderr = '';
33
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
34
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
35
+ proc.on('error', (err) => {
36
+ resolveRun({ exitCode: 127, stdout, stderr: stderr + (stderr.length > 0 ? '\n' : '') + err.message });
37
+ });
38
+ proc.on('close', (code) => {
39
+ resolveRun({ exitCode: code ?? 0, stdout, stderr });
40
+ });
41
+ });
42
+ }
43
+ }
@@ -0,0 +1,7 @@
1
+ import type { VendorAdapter, VendorCompactArgs, VendorCompactResult } from '../vendor-adapter.js';
2
+ export declare class CopilotAdapter implements VendorAdapter {
3
+ readonly id = "copilot";
4
+ readonly displayName = "GitHub Copilot";
5
+ detect(): Promise<boolean>;
6
+ compact(args?: VendorCompactArgs): Promise<VendorCompactResult>;
7
+ }
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Copilot vendor adapter — slice S2-a.
3
+ *
4
+ * Stub adapter (PRD out-of-scope: full implementation lands in a
5
+ * future slice). Copilot's compact verb is `copilot compact`
6
+ * (subcommand form, not `--compact` flag — different from Claude
7
+ * Code + Codex). The adapter encapsulates that verb here so the
8
+ * runtime service + CLI never see it.
9
+ */
10
+ import { spawn } from 'node:child_process';
11
+ import { existsSync } from 'node:fs';
12
+ import { join } from 'node:path';
13
+ export class CopilotAdapter {
14
+ id = 'copilot';
15
+ displayName = 'GitHub Copilot';
16
+ async detect() {
17
+ if (process.env.GITHUB_COPILOT === '1' || process.env.GITHUB_COPILOT === 'true')
18
+ return true;
19
+ const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
20
+ return home.length > 0 && existsSync(join(home, '.copilot'));
21
+ }
22
+ async compact(args = {}) {
23
+ const argv = ['compact'];
24
+ if (args.force === true)
25
+ argv.push('--force');
26
+ return new Promise((resolveRun) => {
27
+ const proc = spawn('copilot', argv, { stdio: ['ignore', 'pipe', 'pipe'] });
28
+ let stdout = '';
29
+ let stderr = '';
30
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
31
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
32
+ proc.on('error', (err) => {
33
+ resolveRun({ exitCode: 127, stdout, stderr: stderr + (stderr.length > 0 ? '\n' : '') + err.message });
34
+ });
35
+ proc.on('close', (code) => {
36
+ resolveRun({ exitCode: code ?? 0, stdout, stderr });
37
+ });
38
+ });
39
+ }
40
+ }
@@ -38,7 +38,7 @@ export type WorkflowRouterStep = {
38
38
  };
39
39
  export type WorkflowModelRouting = {
40
40
  readonly strongestModel: {
41
- readonly modelId: 'claude-opus-4-7';
41
+ readonly modelId: string;
42
42
  readonly uses: readonly WorkflowStepStage[];
43
43
  };
44
44
  readonly executionModel: {