peaks-loop 4.0.0-beta.5 → 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 (39) hide show
  1. package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
  2. package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
  3. package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
  4. package/dist/src/cli/commands/polyrepo-commands.js +155 -0
  5. package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
  6. package/dist/src/cli/commands/runtime-commands.js +150 -0
  7. package/dist/src/cli/program.js +16 -0
  8. package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
  9. package/dist/src/services/adapter/adapter-registry.js +186 -0
  10. package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
  11. package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
  12. package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
  13. package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
  14. package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
  15. package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
  16. package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
  17. package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
  18. package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
  19. package/dist/src/services/runtime/runtime-detector.js +62 -0
  20. package/dist/src/services/runtime/runtime-service.d.ts +59 -0
  21. package/dist/src/services/runtime/runtime-service.js +51 -0
  22. package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
  23. package/dist/src/services/runtime/vendor-adapter.js +1 -0
  24. package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
  25. package/dist/src/services/runtime/vendors/claude-code.js +39 -0
  26. package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
  27. package/dist/src/services/runtime/vendors/codex.js +43 -0
  28. package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
  29. package/dist/src/services/runtime/vendors/copilot.js +40 -0
  30. package/dist/src/shared/version.d.ts +1 -1
  31. package/dist/src/shared/version.js +1 -1
  32. package/package.json +1 -1
  33. package/skills/peaks-code/SKILL.md +1 -8
  34. package/skills/peaks-code/references/external-references.md +1 -3
  35. package/skills/peaks-code/references/external-skill-invocation.md +1 -1
  36. package/skills/peaks-code/references/runbook.md +1 -3
  37. package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
  38. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
  39. package/skills/peaks-code/references/openspec-workflow.md +0 -81
@@ -0,0 +1,34 @@
1
+ /**
2
+ * `peaks adapter *` — vendor adapter registry CLI surface.
3
+ *
4
+ * Slice S2-a of RD-2 (2026-07-08 session). Two subcommands:
5
+ *
6
+ * list — list registered user adapters (persisted at
7
+ * `.peaks/runtime/adapters.json`).
8
+ * register — register a new adapter. Vendor verb strings live in
9
+ * the binary the user picks, NOT in peaks-loop source.
10
+ *
11
+ * This file coexists with the older `adapter-commands.ts` (slice
12
+ * 2026-07-04-cli-15a) which exposes `peaks skill adapter <verb>` —
13
+ * a different surface for skill materialization (resolveScratchDir /
14
+ * materialize / publish). The two surfaces do NOT collide because
15
+ * one is `peaks skill adapter` and the other is `peaks adapter`.
16
+ */
17
+ import type { Command } from 'commander';
18
+ import { type ProgramIO } from '../cli-helpers.js';
19
+ export interface AdapterListOptions {
20
+ json?: boolean;
21
+ project?: string;
22
+ }
23
+ export interface AdapterRegisterOptions {
24
+ json?: boolean;
25
+ id?: string;
26
+ /** Display name override (defaults to the id). */
27
+ name?: string;
28
+ binary?: string;
29
+ /** Repeated --arg values. */
30
+ arg?: string[];
31
+ force?: boolean;
32
+ project?: string;
33
+ }
34
+ export declare function registerAdapterS2ACommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,92 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { addJsonOption, printResult } from '../cli-helpers.js';
3
+ import { fail, ok, getErrorMessage } from '../../shared/result.js';
4
+ import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
5
+ import { registryFileFor } from './runtime-commands.js';
6
+ function resolveProjectRoot(explicit) {
7
+ if (explicit !== undefined && explicit.length > 0)
8
+ return explicit;
9
+ return process.cwd();
10
+ }
11
+ export function registerAdapterS2ACommands(program, io) {
12
+ const adapter = program
13
+ .command('adapter')
14
+ .description('Vendor adapter registry (user-registered, persisted to .peaks/runtime/adapters.json)');
15
+ // -----------------------------------------------------------------
16
+ // 1. peaks adapter list [--json] [--project <root>]
17
+ // -----------------------------------------------------------------
18
+ addJsonOption(adapter
19
+ .command('list')
20
+ .description('List user-registered vendor adapters')
21
+ .option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
22
+ try {
23
+ const projectRoot = resolveProjectRoot(options.project);
24
+ const file = registryFileFor(projectRoot);
25
+ const reg = new AdapterRegistry();
26
+ if (existsSync(file))
27
+ reg.load(file);
28
+ const records = reg.list();
29
+ printResult(io, ok('adapter.list', { projectRoot, file, records, count: records.length }, [], [
30
+ records.length === 0
31
+ ? 'No adapters registered yet. Run `peaks adapter register --id <vendor> --binary <cmd>` to add one.'
32
+ : `Run \`peaks runtime compact --via <id>\` to invoke a registered adapter.`
33
+ ]), options.json);
34
+ }
35
+ catch (error) {
36
+ printResult(io, fail('adapter.list', 'ADAPTER_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
37
+ process.exitCode = 1;
38
+ }
39
+ });
40
+ // -----------------------------------------------------------------
41
+ // 2. peaks adapter register --id <id> --binary <cmd> [--arg <a>]... [--force]
42
+ // -----------------------------------------------------------------
43
+ addJsonOption(adapter
44
+ .command('register')
45
+ .description('Register a new vendor adapter; persists to .peaks/runtime/adapters.json')
46
+ .requiredOption('--id <id>', 'vendor adapter id (lowercase, e.g. my-cli)')
47
+ .option('--name <name>', 'human-readable display name (defaults to <id>)')
48
+ .requiredOption('--binary <cmd>', 'binary name to invoke for compact (e.g. my-cli)')
49
+ .option('--arg <value>', 'extra arg appended before --force (repeatable)', (value, previous = []) => [...previous, value])
50
+ .option('--force', 'overwrite an existing adapter with the same id')
51
+ .option('--project <path>', 'project root (defaults to cwd)')).action((options) => {
52
+ try {
53
+ const projectRoot = resolveProjectRoot(options.project);
54
+ const file = registryFileFor(projectRoot);
55
+ const reg = new AdapterRegistry();
56
+ if (existsSync(file))
57
+ reg.load(file);
58
+ const id = options.id ?? '';
59
+ const record = options.arg !== undefined
60
+ ? { id, displayName: options.name ?? id, binary: options.binary ?? '', args: options.arg }
61
+ : { id, displayName: options.name ?? id, binary: options.binary ?? '' };
62
+ let result;
63
+ try {
64
+ result = reg.register(record, { force: options.force === true });
65
+ }
66
+ catch (validationError) {
67
+ printResult(io, fail('adapter.register', 'INVALID_ADAPTER_RECORD', getErrorMessage(validationError), { id }, [
68
+ 'Use --id with /^[a-z0-9][a-z0-9._-]*$/, --binary as a binary name (no path separators).'
69
+ ]), options.json);
70
+ process.exitCode = 1;
71
+ return;
72
+ }
73
+ reg.persist(file);
74
+ const warnings = [];
75
+ if (!result.created && options.force !== true) {
76
+ warnings.push(`adapter "${id}" already registered; pass --force to overwrite`);
77
+ }
78
+ printResult(io, ok('adapter.register', {
79
+ projectRoot,
80
+ file,
81
+ adapter: result.record,
82
+ created: result.created
83
+ }, warnings, [
84
+ `Run \`peaks runtime compact --via ${id}\` to invoke it.`
85
+ ]), options.json);
86
+ }
87
+ catch (error) {
88
+ printResult(io, fail('adapter.register', 'ADAPTER_REGISTER_FAILED', getErrorMessage(error), {}, []), options.json);
89
+ process.exitCode = 1;
90
+ }
91
+ });
92
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * `peaks polyrepo *` — polyrepo workflow CLI surface.
3
+ *
4
+ * Slice S2-b of RD-2 (2026-07-08 session). Three subcommands:
5
+ *
6
+ * init — scan the parent dir for child git repos + write a
7
+ * polyrepo manifest to `.peaks/polyrepo.json`.
8
+ * status — read the persisted manifest + report children.
9
+ * dispatch — mirror a PRD/RD/QA artifact from the parent into
10
+ * each target child's
11
+ * `.peaks/_runtime/<sid>/<role>/` directory.
12
+ *
13
+ * Per the project's two-axis convention, all reviewable artifacts
14
+ * live under `.peaks/_runtime/<sid>/<role>/` — never as top-level
15
+ * siblings of `.peaks/_runtime/`.
16
+ */
17
+ import type { Command } from 'commander';
18
+ import { type ProgramIO } from '../cli-helpers.js';
19
+ export interface PolyrepoInitOptions {
20
+ json?: boolean;
21
+ root?: string;
22
+ children?: string;
23
+ }
24
+ export interface PolyrepoStatusOptions {
25
+ json?: boolean;
26
+ root?: string;
27
+ }
28
+ export interface PolyrepoDispatchOptions {
29
+ json?: boolean;
30
+ root?: string;
31
+ rid?: string;
32
+ to?: string;
33
+ role?: 'prd' | 'rd' | 'qa';
34
+ artifact?: string;
35
+ sid?: string;
36
+ }
37
+ export declare function registerPolyrepoCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,155 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { addJsonOption, printResult } from '../cli-helpers.js';
3
+ import { fail, ok, getErrorMessage } from '../../shared/result.js';
4
+ import { PolyrepoService } from '../../services/polyrepo/polyrepo-service.js';
5
+ function parseChildren(raw) {
6
+ if (raw === undefined || raw.length === 0)
7
+ return undefined;
8
+ return raw.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
9
+ }
10
+ function resolveRoot(explicit) {
11
+ if (explicit !== undefined && explicit.length > 0)
12
+ return explicit;
13
+ return process.cwd();
14
+ }
15
+ export function registerPolyrepoCommands(program, io) {
16
+ const polyrepo = program
17
+ .command('polyrepo')
18
+ .description('Polyrepo workflow (parent dir + child git repos with their own .peaks/ subsets)');
19
+ // -----------------------------------------------------------------
20
+ // 1. peaks polyrepo init [--children <dir1,dir2>] [--root <dir>]
21
+ // -----------------------------------------------------------------
22
+ addJsonOption(polyrepo
23
+ .command('init')
24
+ .description('Scan the parent dir for child git repos and write .peaks/polyrepo.json')
25
+ .option('--children <list>', 'comma-separated child directory names (defaults to auto-discovery)')
26
+ .option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
27
+ try {
28
+ const root = resolveRoot(options.root);
29
+ const svc = new PolyrepoService();
30
+ const children = parseChildren(options.children);
31
+ const result = children === undefined
32
+ ? svc.init({ root })
33
+ : svc.init({ root, children });
34
+ printResult(io, ok('polyrepo.init', {
35
+ root: result.manifest.root,
36
+ detectedAt: result.manifest.detectedAt,
37
+ children: result.manifest.children,
38
+ manifestPath: `${result.manifest.root}/.peaks/polyrepo.json`,
39
+ created: result.created
40
+ }, [], [
41
+ result.manifest.children.length === 0
42
+ ? 'No child git repos detected. Pass --children <dir1,dir2> to override.'
43
+ : `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact into one or more children.`
44
+ ]), options.json);
45
+ }
46
+ catch (error) {
47
+ printResult(io, fail('polyrepo.init', 'POLYREPO_INIT_FAILED', getErrorMessage(error), {}, ['Verify --root exists and is a directory']), options.json);
48
+ process.exitCode = 1;
49
+ }
50
+ });
51
+ // -----------------------------------------------------------------
52
+ // 2. peaks polyrepo status [--root <dir>]
53
+ // -----------------------------------------------------------------
54
+ addJsonOption(polyrepo
55
+ .command('status')
56
+ .description('Report the persisted polyrepo manifest + children')
57
+ .option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
58
+ try {
59
+ const root = resolveRoot(options.root);
60
+ const svc = new PolyrepoService();
61
+ const result = svc.status(root);
62
+ if (!result.manifestExists) {
63
+ printResult(io, ok('polyrepo.status', { ...result, initialized: false }, [], [
64
+ 'No polyrepo manifest found. Run `peaks polyrepo init [--children <list>]` first.'
65
+ ]), options.json);
66
+ return;
67
+ }
68
+ printResult(io, ok('polyrepo.status', { ...result, initialized: true }, [], [
69
+ result.children.length === 0
70
+ ? 'Manifest exists but has no children. Re-run `peaks polyrepo init --children <list>`.'
71
+ : `Run \`peaks polyrepo dispatch --rid <rid> --to <child-id>\` to mirror an artifact.`
72
+ ]), options.json);
73
+ }
74
+ catch (error) {
75
+ printResult(io, fail('polyrepo.status', 'POLYREPO_STATUS_FAILED', getErrorMessage(error), {}, []), options.json);
76
+ process.exitCode = 1;
77
+ }
78
+ });
79
+ // -----------------------------------------------------------------
80
+ // 3. peaks polyrepo dispatch --rid <rid> [--to <child-id>] --role <prd|rd|qa> --artifact <path>
81
+ // -----------------------------------------------------------------
82
+ addJsonOption(polyrepo
83
+ .command('dispatch')
84
+ .description('Mirror a PRD/RD/QA artifact from the parent into one or more children')
85
+ .requiredOption('--rid <rid>', 'request id (used in the on-disk filename)')
86
+ .option('--to <list>', 'comma-separated child ids (defaults to all children in the manifest)')
87
+ .requiredOption('--role <role>', 'artifact role: prd | rd | qa')
88
+ .requiredOption('--artifact <path>', 'source artifact path (relative to --root or absolute)')
89
+ .option('--sid <sid>', 'session id override (defaults to the canonical binding)')
90
+ .option('--root <path>', 'parent root (defaults to cwd)')).action((options) => {
91
+ try {
92
+ const root = resolveRoot(options.root);
93
+ const sid = options.sid ?? 'default';
94
+ const rid = options.rid ?? '';
95
+ const role = options.role;
96
+ if (role !== 'prd' && role !== 'rd' && role !== 'qa') {
97
+ printResult(io, fail('polyrepo.dispatch', 'INVALID_ROLE', `--role must be prd | rd | qa (got "${role}")`, {}, [
98
+ 'Re-run with --role prd (or rd / qa).'
99
+ ]), options.json);
100
+ process.exitCode = 1;
101
+ return;
102
+ }
103
+ const artifact = options.artifact ?? '';
104
+ if (!existsSync(artifact)) {
105
+ printResult(io, fail('polyrepo.dispatch', 'ARTIFACT_NOT_FOUND', `source artifact does not exist: ${artifact}`, {}, [
106
+ 'Verify the path or pass an absolute path.'
107
+ ]), options.json);
108
+ process.exitCode = 1;
109
+ return;
110
+ }
111
+ // Resolve targets from the manifest first so we can default to
112
+ // "all children" when --to is omitted.
113
+ const svc = new PolyrepoService();
114
+ const status = svc.status(root);
115
+ if (!status.manifestExists) {
116
+ printResult(io, fail('polyrepo.dispatch', 'NO_MANIFEST', `no polyrepo manifest at ${root}/.peaks/polyrepo.json`, {}, [
117
+ 'Run `peaks polyrepo init` first.'
118
+ ]), options.json);
119
+ process.exitCode = 1;
120
+ return;
121
+ }
122
+ let targets;
123
+ if (options.to !== undefined && options.to.length > 0) {
124
+ targets = options.to.split(',').map((s) => s.trim()).filter((s) => s.length > 0);
125
+ }
126
+ else {
127
+ targets = status.children.map((c) => c.id);
128
+ }
129
+ const result = svc.dispatch(root, {
130
+ sid, rid, targets, role, artifactPath: artifact
131
+ });
132
+ const allOk = result.perChild.every((c) => c.ok);
133
+ printResult(io, ok('polyrepo.dispatch', {
134
+ root,
135
+ sid,
136
+ rid,
137
+ role,
138
+ artifact,
139
+ dispatch: result.dispatch,
140
+ perChild: result.perChild,
141
+ allOk
142
+ }, result.warnings, [
143
+ allOk
144
+ ? `Mirrored artifact to ${result.perChild.length} child(ren).`
145
+ : `One or more child mirrors failed; inspect perChild[].ok.`
146
+ ]), options.json);
147
+ if (!allOk)
148
+ process.exitCode = 1;
149
+ }
150
+ catch (error) {
151
+ printResult(io, fail('polyrepo.dispatch', 'POLYREPO_DISPATCH_FAILED', getErrorMessage(error), {}, []), options.json);
152
+ process.exitCode = 1;
153
+ }
154
+ });
155
+ }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `peaks runtime *` — vendor-runtime detection + compact CLI surface.
3
+ *
4
+ * Slice S2-a of RD-2 (2026-07-08 session). Four subcommands:
5
+ *
6
+ * detect — print which vendor is the active AI runtime
7
+ * (claude-code / codex / copilot / unknown).
8
+ * list — list the built-in vendor adapters registered by
9
+ * RuntimeService (Claude Code / Codex / Copilot).
10
+ * compact — invoke a vendor's compact verb via the adapter. The
11
+ * adapter (NOT this file) chooses the vendor verb;
12
+ * see src/services/runtime/vendors/<vendor>.ts.
13
+ *
14
+ * Vendor verb strings (`claude --compact`, `codex --compact`,
15
+ * `copilot compact`) MUST live ONLY in `src/services/runtime/vendors/`
16
+ * — verified by AC-1: `rg -n "claude --compact|codex --compact|copilot
17
+ * compact" src/services/code/` returns 0 matches.
18
+ *
19
+ * `peaks runtime compact --via <id>` first consults the user
20
+ * adapter registry (`.peaks/runtime/adapters.json`) for the id, then
21
+ * falls back to the built-in adapter list. Missing vendor CLI
22
+ * surfaces as exitCode=127 + warning, NOT as a fatal error — vendor
23
+ * neutrality demands peaks-loop stay alive.
24
+ */
25
+ import type { Command } from 'commander';
26
+ import { join } from 'node:path';
27
+ import { type ProgramIO } from '../cli-helpers.js';
28
+ export interface RuntimeDetectOptions {
29
+ json?: boolean;
30
+ }
31
+ export interface RuntimeListOptions {
32
+ json?: boolean;
33
+ }
34
+ export interface RuntimeCompactOptions {
35
+ json?: boolean;
36
+ /** Vendor adapter id (built-in or registered). */
37
+ via?: string;
38
+ force?: boolean;
39
+ /** Override the project root (defaults to .peaks runtime lookup). */
40
+ project?: string;
41
+ }
42
+ export declare function registerRuntimeCommands(program: Command, io: ProgramIO): void;
43
+ /** Internal helper exposed for tests + future programmatic callers:
44
+ * resolve the registry file location, ensuring the parent dir
45
+ * exists. Idempotent. */
46
+ export declare function ensureRegistryDir(registryFile: string): void;
47
+ /** Internal helper for tests: locate the registry file under the
48
+ * given project root. Thin wrapper so tests can stub the resolution. */
49
+ export declare function registryFileFor(projectRoot: string): string;
50
+ /** Re-export the path join helper for completeness; not strictly
51
+ * needed by callers but keeps the module self-contained. */
52
+ export { join };
@@ -0,0 +1,150 @@
1
+ import { existsSync, mkdirSync } from 'node:fs';
2
+ import { join, dirname } from 'node:path';
3
+ import { addJsonOption, printResult } from '../cli-helpers.js';
4
+ import { fail, ok } from '../../shared/result.js';
5
+ import { getErrorMessage } from '../../shared/result.js';
6
+ import { detectRuntime } from '../../services/runtime/runtime-detector.js';
7
+ import { RuntimeService } from '../../services/runtime/runtime-service.js';
8
+ import { AdapterRegistry } from '../../services/adapter/adapter-registry.js';
9
+ function resolveProjectRoot(explicit) {
10
+ if (explicit !== undefined && explicit.length > 0)
11
+ return explicit;
12
+ return process.cwd();
13
+ }
14
+ export function registerRuntimeCommands(program, io) {
15
+ const runtime = program
16
+ .command('runtime')
17
+ .description('Vendor runtime detection + compact via adapter (--via <vendor-id>)');
18
+ // -----------------------------------------------------------------
19
+ // 1. peaks runtime detect [--json]
20
+ // -----------------------------------------------------------------
21
+ addJsonOption(runtime
22
+ .command('detect')
23
+ .description('Print the active AI runtime vendor (claude-code / codex / copilot / unknown)')).action((options) => {
24
+ try {
25
+ const result = detectRuntime();
26
+ printResult(io, ok('runtime.detect', result, [], [
27
+ result.vendor === 'unknown'
28
+ ? 'No vendor sentinel detected. Use `peaks adapter register --id <vendor> --binary <cmd>` to wire a custom one.'
29
+ : `Run \`peaks runtime compact --via ${result.vendor}\` to invoke its compact verb.`
30
+ ]), options.json);
31
+ }
32
+ catch (error) {
33
+ printResult(io, fail('runtime.detect', 'RUNTIME_DETECT_FAILED', getErrorMessage(error), {}, ['Retry with a clean env']), options.json);
34
+ process.exitCode = 1;
35
+ }
36
+ });
37
+ // -----------------------------------------------------------------
38
+ // 2. peaks runtime list [--json]
39
+ // -----------------------------------------------------------------
40
+ addJsonOption(runtime
41
+ .command('list')
42
+ .description('List the built-in vendor adapters registered by RuntimeService')).action((options) => {
43
+ try {
44
+ const svc = new RuntimeService();
45
+ const adapters = svc.listBuiltInAdapters().map((a) => ({ id: a.id, displayName: a.displayName }));
46
+ printResult(io, ok('runtime.list', { builtIn: adapters }, [], [
47
+ `Run \`peaks adapter register --id <vendor> --binary <cmd>\` to add a custom adapter (persists to .peaks/runtime/adapters.json).`
48
+ ]), options.json);
49
+ }
50
+ catch (error) {
51
+ printResult(io, fail('runtime.list', 'RUNTIME_LIST_FAILED', getErrorMessage(error), {}, []), options.json);
52
+ process.exitCode = 1;
53
+ }
54
+ });
55
+ // -----------------------------------------------------------------
56
+ // 3. peaks runtime compact [--via <id>] [--force] [--json] [--project <root>]
57
+ // -----------------------------------------------------------------
58
+ addJsonOption(runtime
59
+ .command('compact')
60
+ .description('Compact the active vendor via the chosen adapter (--via <vendor-id>); vendor verbs live ONLY in adapter files')
61
+ .option('--via <id>', 'vendor adapter id (built-in or user-registered)')
62
+ .option('--force', 'ask the vendor to compact unconditionally')
63
+ .option('--project <path>', 'project root (defaults to cwd)')).action(async (options) => {
64
+ try {
65
+ const projectRoot = resolveProjectRoot(options.project);
66
+ const requested = options.via;
67
+ if (requested === undefined || requested.length === 0) {
68
+ // No explicit --via: try to detect first, then fall back.
69
+ const detected = detectRuntime();
70
+ if (detected.vendor === 'unknown') {
71
+ printResult(io, fail('runtime.compact', 'NO_VENDOR_SPECIFIED', 'No --via <id> provided and no vendor detected.', { detected }, [
72
+ 'Pass --via <vendor-id>, or run `peaks runtime detect` to see what was detected.'
73
+ ]), options.json);
74
+ process.exitCode = 1;
75
+ return;
76
+ }
77
+ await runCompact(projectRoot, detected.vendor, options.force === true, io, options.json);
78
+ return;
79
+ }
80
+ await runCompact(projectRoot, requested, options.force === true, io, options.json);
81
+ }
82
+ catch (error) {
83
+ printResult(io, fail('runtime.compact', 'RUNTIME_COMPACT_FAILED', getErrorMessage(error), {}, []), options.json);
84
+ process.exitCode = 1;
85
+ }
86
+ });
87
+ }
88
+ async function runCompact(projectRoot, viaId, force, io, asJson) {
89
+ // Layer 1: user-registered adapter (via AdapterRegistry).
90
+ const registryFile = AdapterRegistry.defaultFile(projectRoot);
91
+ const registry = new AdapterRegistry();
92
+ if (existsSync(registryFile)) {
93
+ try {
94
+ registry.load(registryFile);
95
+ }
96
+ catch {
97
+ // Treat corrupt registry as empty — vendor-neutrality: a corrupt
98
+ // .peaks/runtime/adapters.json must NOT block peaks runtime.
99
+ // We still try the built-in path below.
100
+ }
101
+ }
102
+ const registered = registry.resolve(viaId);
103
+ if (registered !== undefined) {
104
+ const r = await registered.compact({ force });
105
+ const warnings = [];
106
+ if (r.exitCode === 127) {
107
+ warnings.push(`binary for adapter "${viaId}" not found on PATH; compact is a no-op`);
108
+ }
109
+ printResult(io, ok('runtime.compact', { via: viaId, source: 'registry', compact: r }, warnings, [
110
+ r.exitCode === 0
111
+ ? 'Compact completed.'
112
+ : `Compact exited with code ${r.exitCode}. Check stderr above.`
113
+ ]), asJson);
114
+ if (r.exitCode !== 0)
115
+ process.exitCode = 1;
116
+ return;
117
+ }
118
+ // Layer 2: built-in adapter (via RuntimeService).
119
+ const svc = new RuntimeService();
120
+ const r = await svc.compactVia(viaId, force);
121
+ const warnings = [];
122
+ if (r.warning !== undefined)
123
+ warnings.push(r.warning);
124
+ if (r.exitCode === 127) {
125
+ warnings.push(`built-in adapter "${viaId}" binary not found on PATH; compact is a no-op`);
126
+ }
127
+ printResult(io, ok('runtime.compact', { via: viaId, source: 'built-in', compact: { exitCode: r.exitCode, stdout: r.stdout, stderr: r.stderr } }, warnings, [
128
+ r.exitCode === 0
129
+ ? 'Compact completed.'
130
+ : `Compact exited with code ${r.exitCode}. Check stderr above.`
131
+ ]), asJson);
132
+ if (r.exitCode !== 0)
133
+ process.exitCode = 1;
134
+ }
135
+ /** Internal helper exposed for tests + future programmatic callers:
136
+ * resolve the registry file location, ensuring the parent dir
137
+ * exists. Idempotent. */
138
+ export function ensureRegistryDir(registryFile) {
139
+ mkdirSync(dirname(registryFile), { recursive: true });
140
+ }
141
+ /** Internal helper for tests: locate the registry file under the
142
+ * given project root. Thin wrapper so tests can stub the resolution. */
143
+ export function registryFileFor(projectRoot) {
144
+ const f = AdapterRegistry.defaultFile(projectRoot);
145
+ ensureRegistryDir(f);
146
+ return f;
147
+ }
148
+ /** Re-export the path join helper for completeness; not strictly
149
+ * needed by callers but keeps the module self-contained. */
150
+ export { join };
@@ -85,6 +85,15 @@ import { registerJobCommands } from './commands/job-commands.js';
85
85
  // 15b / 15c / 15d will extend sediment-commands.ts with the remaining 14 verbs.
86
86
  import { registerSedimentCommands } from './commands/sediment-commands.js';
87
87
  import { registerAdapterCommands } from './commands/adapter-commands.js';
88
+ // Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
89
+ // `peaks adapter *` (list / register). See src/services/runtime/ and
90
+ // src/services/adapter/adapter-registry.ts. Coexists with the older
91
+ // `peaks skill adapter` surface (slice 2026-07-04-cli-15a) which is
92
+ // for skill materialization, not vendor-adapter registry.
93
+ import { registerRuntimeCommands } from './commands/runtime-commands.js';
94
+ import { registerAdapterS2ACommands } from './commands/adapter-commands-s2a.js';
95
+ // Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
96
+ import { registerPolyrepoCommands } from './commands/polyrepo-commands.js';
88
97
  import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
89
98
  import { applyRetention } from '../services/log/retention.js';
90
99
  import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
@@ -375,6 +384,13 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
375
384
  registerSedimentCommands(program, io);
376
385
  // Slice 2026-07-04-cli-15a: `peaks skill adapter <verb>` — adapter list / set-active.
377
386
  registerAdapterCommands(program, io);
387
+ // Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
388
+ // `peaks adapter *` (list / register). Vendor verbs live ONLY in
389
+ // src/services/runtime/vendors/<vendor>.ts (AC-1 enforcement).
390
+ registerRuntimeCommands(program, io);
391
+ registerAdapterS2ACommands(program, io);
392
+ // Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
393
+ registerPolyrepoCommands(program, io);
378
394
  // Slice 2026-07-05-peaks-code-to-peaks-code (Task 1): `peaks skill:visibility --list --json`
379
395
  // — read .claude-plugin/marketplace.json and report each skill's
380
396
  // visibility (public vs internal). Used to surface which skills are
@@ -0,0 +1,48 @@
1
+ import type { VendorAdapter } from '../runtime/vendor-adapter.js';
2
+ /** Serializable adapter record (the on-disk shape). */
3
+ export interface AdapterRecord {
4
+ readonly id: string;
5
+ readonly displayName: string;
6
+ /** Binary name or absolute path to invoke for compact. */
7
+ readonly binary: string;
8
+ /** Extra args appended after the compact verb. */
9
+ readonly args?: string[];
10
+ }
11
+ export interface PersistedAdapterRegistry {
12
+ readonly version: 1;
13
+ readonly adapters: AdapterRecord[];
14
+ }
15
+ export interface RegisterOptions {
16
+ /** Overwrite an existing record with the same id. Defaults to false
17
+ * to prevent silent overwrites of user customizations. */
18
+ readonly force?: boolean;
19
+ }
20
+ /** In-memory + on-disk adapter registry. The registry holds USER
21
+ * adapters (those registered via `peaks adapter register`); built-in
22
+ * adapters live in RuntimeService and are NOT registered here. */
23
+ export declare class AdapterRegistry {
24
+ private readonly records;
25
+ /** Register a new adapter. When an adapter with the same id already
26
+ * exists, returns the existing record unless `force: true`. */
27
+ register(record: AdapterRecord, opts?: RegisterOptions): {
28
+ record: AdapterRecord;
29
+ created: boolean;
30
+ };
31
+ /** Resolve a registered adapter by id, materializing it as a
32
+ * runnable VendorAdapter. Returns undefined when not registered. */
33
+ resolve(id: string): VendorAdapter | undefined;
34
+ /** List registered adapter records (serializable form). */
35
+ list(): AdapterRecord[];
36
+ /** Remove a registered adapter. Returns true if it existed. */
37
+ unregister(id: string): boolean;
38
+ /** Persist the current registry to disk as JSON. Writes atomically
39
+ * (write to `<file>.tmp` then rename) so a crash mid-write does
40
+ * not corrupt the registry file. */
41
+ persist(file: string): void;
42
+ /** Load a registry from disk. Missing file = empty registry.
43
+ * Corrupt file = throw (caller decides whether to bail or
44
+ * fall back to empty). */
45
+ load(file: string): void;
46
+ /** Default location for the on-disk registry file. */
47
+ static defaultFile(projectRoot: string): string;
48
+ }