peaks-loop 4.0.0-beta.4 → 4.0.0-beta.6

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 (53) hide show
  1. package/.claude-plugin/marketplace.json +5 -0
  2. package/CHANGELOG.md +16 -0
  3. package/README-en.md +4 -0
  4. package/README.md +4 -0
  5. package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
  6. package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
  7. package/dist/src/cli/commands/core/skill-command.js +22 -0
  8. package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
  9. package/dist/src/cli/commands/polyrepo-commands.js +155 -0
  10. package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
  11. package/dist/src/cli/commands/runtime-commands.js +150 -0
  12. package/dist/src/cli/commands/skill-search-commands.d.ts +25 -0
  13. package/dist/src/cli/commands/skill-search-commands.js +68 -0
  14. package/dist/src/cli/program.js +16 -0
  15. package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
  16. package/dist/src/services/adapter/adapter-registry.js +186 -0
  17. package/dist/src/services/context/context-schema.d.ts +6 -6
  18. package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
  19. package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
  20. package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
  21. package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
  22. package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
  23. package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
  24. package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
  25. package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
  26. package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
  27. package/dist/src/services/runtime/runtime-detector.js +62 -0
  28. package/dist/src/services/runtime/runtime-service.d.ts +59 -0
  29. package/dist/src/services/runtime/runtime-service.js +51 -0
  30. package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
  31. package/dist/src/services/runtime/vendor-adapter.js +1 -0
  32. package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
  33. package/dist/src/services/runtime/vendors/claude-code.js +39 -0
  34. package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
  35. package/dist/src/services/runtime/vendors/codex.js +43 -0
  36. package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
  37. package/dist/src/services/runtime/vendors/copilot.js +40 -0
  38. package/dist/src/services/skill/skill-search-service.d.ts +88 -0
  39. package/dist/src/services/skill/skill-search-service.js +228 -0
  40. package/dist/src/shared/version.d.ts +1 -1
  41. package/dist/src/shared/version.js +1 -1
  42. package/package.json +1 -1
  43. package/skills/peaks-code/SKILL.md +1 -8
  44. package/skills/peaks-code/references/external-references.md +1 -3
  45. package/skills/peaks-code/references/external-skill-invocation.md +1 -1
  46. package/skills/peaks-code/references/runbook.md +1 -3
  47. package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
  48. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
  49. package/skills/peaks-solo/SKILL.md +197 -0
  50. package/skills/peaks-solo/references/fallback-tool-inventory.md +46 -0
  51. package/skills/peaks-solo/references/sediment-prompt-template.md +62 -0
  52. package/skills/peaks-solo/references/triage-decision-table.md +31 -0
  53. package/skills/peaks-code/references/openspec-workflow.md +0 -81
@@ -0,0 +1,34 @@
1
+ import type { PolyrepoChild, PolyrepoManifest } from './polyrepo-types.js';
2
+ export interface PolyrepoInitOptions {
3
+ readonly root: string;
4
+ readonly children?: readonly string[];
5
+ }
6
+ export interface PolyrepoInitResult {
7
+ readonly manifest: PolyrepoManifest;
8
+ readonly created: boolean;
9
+ }
10
+ export interface PolyrepoStatusResult {
11
+ readonly root: string;
12
+ readonly manifestExists: boolean;
13
+ readonly children: PolyrepoChild[];
14
+ readonly detectedAt: string | null;
15
+ }
16
+ export declare class PolyrepoService {
17
+ /** Run a scan and persist the manifest. Returns the manifest + a
18
+ * `created` flag indicating whether a new manifest was written
19
+ * (false = existing manifest was overwritten). */
20
+ init(opts: PolyrepoInitOptions): PolyrepoInitResult;
21
+ /** Read the persisted manifest + report children. */
22
+ status(root: string): PolyrepoStatusResult;
23
+ /** Dispatch an artifact to the named child targets. Wrapper
24
+ * around dispatchArtifact that resolves the manifest from disk
25
+ * first. Throws when no manifest exists (caller should run init
26
+ * first). */
27
+ dispatch(root: string, params: {
28
+ sid: string;
29
+ rid: string;
30
+ targets: readonly string[];
31
+ role: 'prd' | 'rd' | 'qa';
32
+ artifactPath: string;
33
+ }): import("./polyrepo-dispatcher.js").DispatchResult;
34
+ }
@@ -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 {};
@@ -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
+ }
@@ -0,0 +1,88 @@
1
+ /**
2
+ * peaks skill search — service-layer primitive for S0 of the
3
+ * 4.0.0-beta.5 peaks-solo dispatcher release.
4
+ *
5
+ * Spec: docs/superpowers/specs/2026-07-08-peaks-solo-dispatcher-design.md §3.2
6
+ * Plan: docs/superpowers/plans/2026-07-08-peaks-solo-dispatcher/s0-skill-search-cli.md
7
+ *
8
+ * This service is the single source of truth for skill pool retrieval
9
+ * and is reused by the `peaks skill search` CLI. It is intentionally
10
+ * pure (synchronous frontmatter parse + substring scoring) so the
11
+ * dispatcher (`peaks-solo`, S1) can call it without paying for any
12
+ * async I/O during the triage decision flow.
13
+ *
14
+ * v1 scope (locked, see plan §"Match rules (v1)"):
15
+ * - --query: case-insensitive substring match on description + triggers
16
+ * - --tag: case-insensitive exact match on `metadata.tags`
17
+ * - --domain: exact match on `metadata.domain` (locked enum)
18
+ * - All three compose with AND; at least one filter is required
19
+ * - No match → `[]` (not error, not null)
20
+ * - --limit truncates to the top N by matchScore desc
21
+ *
22
+ * Out of v1 scope (deferred, see plan §"Goal"): fuzzy / FTS5 / semantic
23
+ * matching, ML-based ranking, pagination, i18n, caching.
24
+ */
25
+ import { z } from 'zod';
26
+ import { skillsDir } from '../../shared/paths.js';
27
+ export declare const SkillSearchInputSchema: z.ZodEffects<z.ZodObject<{
28
+ query: z.ZodOptional<z.ZodString>;
29
+ tag: z.ZodOptional<z.ZodString>;
30
+ domain: z.ZodOptional<z.ZodEnum<["code", "content", "doctor", "research", "triage", "sop", "audit", "final-review", "resume", "status", "test", "ide", "slice-decompose", "issue-fix-orchestrator", "perf-audit", "security-audit", "reviewer"]>>;
31
+ limit: z.ZodDefault<z.ZodOptional<z.ZodNumber>>;
32
+ }, "strip", z.ZodTypeAny, {
33
+ limit: number;
34
+ query?: string | undefined;
35
+ tag?: string | undefined;
36
+ domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
37
+ }, {
38
+ limit?: number | undefined;
39
+ query?: string | undefined;
40
+ tag?: string | undefined;
41
+ domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
42
+ }>, {
43
+ limit: number;
44
+ query?: string | undefined;
45
+ tag?: string | undefined;
46
+ domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
47
+ }, {
48
+ limit?: number | undefined;
49
+ query?: string | undefined;
50
+ tag?: string | undefined;
51
+ domain?: "code" | "status" | "content" | "test" | "doctor" | "resume" | "ide" | "research" | "triage" | "sop" | "audit" | "final-review" | "slice-decompose" | "issue-fix-orchestrator" | "perf-audit" | "security-audit" | "reviewer" | undefined;
52
+ }>;
53
+ export type SkillSearchInput = z.input<typeof SkillSearchInputSchema>;
54
+ export declare const SkillSearchResultSchema: z.ZodObject<{
55
+ name: z.ZodString;
56
+ description: z.ZodString;
57
+ triggers: z.ZodArray<z.ZodString, "many">;
58
+ tags: z.ZodArray<z.ZodString, "many">;
59
+ domain: z.ZodString;
60
+ matchScore: z.ZodNumber;
61
+ }, "strip", z.ZodTypeAny, {
62
+ name: string;
63
+ description: string;
64
+ domain: string;
65
+ triggers: string[];
66
+ tags: string[];
67
+ matchScore: number;
68
+ }, {
69
+ name: string;
70
+ description: string;
71
+ domain: string;
72
+ triggers: string[];
73
+ tags: string[];
74
+ matchScore: number;
75
+ }>;
76
+ export type SkillSearchResult = z.infer<typeof SkillSearchResultSchema>;
77
+ /**
78
+ * Search the in-tree skill pool. Validates `input` against
79
+ * `SkillSearchInputSchema`; the schema's refine rejects empty input
80
+ * with a ZodError, which is the contract test U-7 / I-3 rely on.
81
+ *
82
+ * @param input One of `query | tag | domain` is required.
83
+ * All provided filters compose with AND.
84
+ * @returns Matching skills sorted by matchScore desc, name asc.
85
+ * Empty array (not error, not null) when nothing matches.
86
+ */
87
+ export declare function searchSkills(rawInput: SkillSearchInput): Promise<SkillSearchResult[]>;
88
+ export { skillsDir as __skillsDirForTests };