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.
- package/.claude-plugin/marketplace.json +5 -0
- package/CHANGELOG.md +16 -0
- package/README-en.md +4 -0
- package/README.md +4 -0
- package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
- package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
- package/dist/src/cli/commands/core/skill-command.js +22 -0
- package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
- package/dist/src/cli/commands/polyrepo-commands.js +155 -0
- package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
- package/dist/src/cli/commands/runtime-commands.js +150 -0
- package/dist/src/cli/commands/skill-search-commands.d.ts +25 -0
- package/dist/src/cli/commands/skill-search-commands.js +68 -0
- package/dist/src/cli/program.js +16 -0
- package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
- package/dist/src/services/adapter/adapter-registry.js +186 -0
- package/dist/src/services/context/context-schema.d.ts +6 -6
- package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
- package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
- package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
- package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
- package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
- package/dist/src/services/runtime/runtime-detector.js +62 -0
- package/dist/src/services/runtime/runtime-service.d.ts +59 -0
- package/dist/src/services/runtime/runtime-service.js +51 -0
- package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
- package/dist/src/services/runtime/vendor-adapter.js +1 -0
- package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
- package/dist/src/services/runtime/vendors/claude-code.js +39 -0
- package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
- package/dist/src/services/runtime/vendors/codex.js +43 -0
- package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
- package/dist/src/services/runtime/vendors/copilot.js +40 -0
- package/dist/src/services/skill/skill-search-service.d.ts +88 -0
- package/dist/src/services/skill/skill-search-service.js +228 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-code/SKILL.md +1 -8
- package/skills/peaks-code/references/external-references.md +1 -3
- package/skills/peaks-code/references/external-skill-invocation.md +1 -1
- package/skills/peaks-code/references/runbook.md +1 -3
- package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
- package/skills/peaks-solo/SKILL.md +197 -0
- package/skills/peaks-solo/references/fallback-tool-inventory.md +46 -0
- package/skills/peaks-solo/references/sediment-prompt-template.md +62 -0
- package/skills/peaks-solo/references/triage-decision-table.md +31 -0
- 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 };
|