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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
  3. package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
  4. package/dist/src/cli/commands/ide-commands.d.ts +25 -0
  5. package/dist/src/cli/commands/ide-commands.js +42 -0
  6. package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
  7. package/dist/src/cli/commands/polyrepo-commands.js +155 -0
  8. package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
  9. package/dist/src/cli/commands/runtime-commands.js +150 -0
  10. package/dist/src/cli/program.js +23 -0
  11. package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
  12. package/dist/src/services/adapter/adapter-registry.js +186 -0
  13. package/dist/src/services/config/model-routing.d.ts +55 -2
  14. package/dist/src/services/config/model-routing.js +79 -3
  15. package/dist/src/services/ide/adapters/zcode-adapter.d.ts +40 -0
  16. package/dist/src/services/ide/adapters/zcode-adapter.js +220 -0
  17. package/dist/src/services/ide/current-model-detector.d.ts +14 -0
  18. package/dist/src/services/ide/current-model-detector.js +60 -0
  19. package/dist/src/services/ide/ide-registry.js +3 -0
  20. package/dist/src/services/ide/ide-types.d.ts +24 -1
  21. package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
  22. package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
  23. package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
  24. package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
  25. package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
  26. package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
  27. package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
  28. package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
  29. package/dist/src/services/preferences/preferences-types.d.ts +1 -1
  30. package/dist/src/services/providers/minimax-worker-service.d.ts +1 -1
  31. package/dist/src/services/providers/minimax-worker-service.js +1 -1
  32. package/dist/src/services/rd/rd-service.js +7 -4
  33. package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
  34. package/dist/src/services/runtime/runtime-detector.js +62 -0
  35. package/dist/src/services/runtime/runtime-service.d.ts +59 -0
  36. package/dist/src/services/runtime/runtime-service.js +51 -0
  37. package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
  38. package/dist/src/services/runtime/vendor-adapter.js +1 -0
  39. package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
  40. package/dist/src/services/runtime/vendors/claude-code.js +39 -0
  41. package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
  42. package/dist/src/services/runtime/vendors/codex.js +43 -0
  43. package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
  44. package/dist/src/services/runtime/vendors/copilot.js +40 -0
  45. package/dist/src/services/workflow/workflow-router-service.d.ts +1 -1
  46. package/dist/src/services/workflow/workflow-router-service.js +37 -31
  47. package/dist/src/shared/version.d.ts +1 -1
  48. package/dist/src/shared/version.js +1 -1
  49. package/package.json +1 -1
  50. package/scripts/install-skills.mjs +13 -10
  51. package/skills/peaks-code/SKILL.md +26 -12
  52. package/skills/peaks-code/references/external-references.md +1 -3
  53. package/skills/peaks-code/references/external-skill-invocation.md +1 -1
  54. package/skills/peaks-code/references/runbook.md +1 -3
  55. package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
  56. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
  57. package/skills/peaks-slice-decompose/SKILL.md +61 -1
  58. package/skills/peaks-code/references/openspec-workflow.md +0 -81
package/CHANGELOG.md CHANGED
@@ -2,6 +2,68 @@
2
2
 
3
3
  ## [Unreleased]
4
4
 
5
+ ## 4.0.0-beta.7 — 2026-07-09
6
+
7
+ ### Added — zcode adapter (9th IDE) + runtime model detection
8
+
9
+ - **`peaks ide model --current` CLI** — `src/cli/commands/ide-commands.ts` (80 lines) + register in `src/cli/program.ts`. Outputs `{ modelId, detected, registeredAdapters }`. Available as a top-level primitive (not adapter-specific).
10
+ - **`detectCurrentIdeModel()` service** — `src/services/ide/current-model-detector.ts` (43 lines). Walks the adapter registry in insertion order, calls each adapter's optional `detectCurrentModel()` with try/catch isolation — one bad adapter cannot poison the chain.
11
+ - **zcode-adapter `detectCurrentModel()` method** — `src/services/ide/adapters/zcode-adapter.ts`. Reads `~/.zcode/v2/config.json`, resolves active provider via 4-tier priority chain (env override → non-`builtin:` provider → first enabled → first provider), returns the model's id. Live test: `peaks ide model --current` against a real z-code installation → `"modelId": "M3"`.
12
+ - **`IdeAdapter` interface optional field** — `readonly detectCurrentModel?: () => Promise<string | undefined>` in `src/services/ide/ide-types.ts`. Back-compat: 8 existing adapters without the field are unaffected.
13
+ - **`getStrongestModelIdAsync()` async variant** — `src/services/config/model-routing.ts`. Sync `getStrongestModelId()` is unchanged (rd-service compat); new async variant lets future async callers fall back to runtime probe instead of `'claude-opus-4-7'`.
14
+
15
+ ### Changed — install no longer writes hardcoded default model
16
+
17
+ - **`scripts/install-skills.mjs`** — removed `model: 'sonnet'` default and `providers.minimax.model` default from `createConfigDefaults()`. After install, `~/.peaks/config.json` no longer contains a Claude model recommendation; users set their own via `peaks config set model <id>` (or leave unset and the back-compat fallback applies).
18
+ - **`STRONGEST_MODEL_ID` constant removed** — replaced by `getStrongestModelId(config?)` function in `src/services/config/model-routing.ts`. Resolution order: `config.model` → `PEAKS_STRONGEST_MODEL_DEFAULT` env var → back-compat `'claude-opus-4-7'`.
19
+ - **`workflow-router-service.ts` / `minimax-worker-service.ts` / `rd-service.ts`** — threaded dynamic modelId through 16 call sites in `workflow-router-service.ts`, replaced literal `'claude-opus-4-7'` in `minimax-worker-service.ts` (×2) and `rd-service.ts`.
20
+
21
+ ### Added — zcode IDE (peaks-loop's 9th)
22
+
23
+ - **`IdeId` union type** — `'zcode'` added in `src/services/ide/ide-types.ts`.
24
+ - **`ZCODE_ADAPTER`** — new `src/services/ide/adapters/zcode-adapter.ts` (98 lines). Template: `claude-code-adapter.ts` with `compact.compactCommand` / `hookEvent` / `toolMatcher` / `envVar` degraded (z-code is a desktop application, no CLI binary; UNVERIFIED sentinel strings used where the interface requires non-optional types).
25
+ - **`IDE_DETECTION_DIRS` + `IDE_SKILL_INSTALL_PROFILES`** — `scripts/install-skills.mjs`. New `{ id: 'zcode', dir: '.zcode' }` detection entry + zcode profile (skillsDir `~/.zcode/skills`, env override `PEAKS_ZCODE_SKILLS_DIR`).
26
+ - **`ide-registry.ts`** — registers `ZCODE_ADAPTER` alongside the existing 8 adapters (claude-code / trae / trae-cn / codex / cursor / qoder / tongyi-lingma / hermes / openclaw).
27
+ - **`tests/unit/ide/zcode-adapter.test.ts`** — 10 cases (T-1 id, T-2 dirName, T-3 standards rootFile = `CLAUDE.md`, T-4 rulesDir = `.claude/rules`, T-5 skillsDir, T-6 compactCommand undefined, T-7 hookEvent is string, T-8 type test, T-9 IDE_DETECTION_DIRS contains, T-10 IDE_SKILL_INSTALL_PROFILES contains).
28
+ - **`tests/unit/ide/zcode-adapter-detect-model.test.ts`** — 14 cases (pure resolver + env-fixture file IO + path helper).
29
+ - **`tests/unit/cli/ide-commands.test.ts`** — 5 cases (CLI + top-level command registration).
30
+ - **Fixture sync** — `tests/unit/install-skills-script.test.ts` + `tests/unit/cli-program.workflow.test.ts` (Strategy A: env-var override, no hardcoded literals) + `tests/unit/ide/ide-registry.test.ts` (whitelist 6 → 7 adapters).
31
+
32
+ ### Documentation — SKILL.md sync with 4.0.0-beta.6 CLI reality
33
+
34
+ - **`skills/peaks-code/SKILL.md`** — added inline `> CLI reality check` blocks at §Step 0.8 (D-001 `peaks code detect-job` is `peaks job init`), §Step 0.8 third paragraph (D-003 `JOB_SHAPE_NOT_DECIDED` is now passive `done: 0` warning, not exception), §Step 2.5 (D-002 `peaks session title` takes positional sid, not `--session-id` flag), §Step 11c + 11d (D-010 `<!-- peaks-memory:start -->` block requires YAML frontmatter `title:` + `kind:` + `---` + closing `<!-- peaks-memory:end -->`, otherwise `extractedCount: 0` silently). New **§CLI Drift Index** section after Boundaries provides single landing page for D-001/002/003/010 with inline-anchor table.
35
+ - **`skills/peaks-slice-decompose/SKILL.md`** — new **§SC template hard rules (sediment 2026-07-09)** section with 3 rules: Rule 1 ESM/CJS pre-flight before specifying export syntax (D-009c), Rule 2 whitelist fixture sync is a primary task not collateral (D-009a), Rule 3 adapter-interface optional fields > required (D-009b reciprocal).
36
+
37
+ ### Verification (4.0.0-beta.7)
38
+
39
+ - `pnpm tsc -p tsconfig.json --noEmit` — 0 errors
40
+ - `pnpm vitest run tests/unit/ide/ tests/unit/cli/ide-commands.test.ts` — 15 files / 211 tests / 0 failed
41
+ - `peaks workflow verify-pipeline --rid 003-add-zcode-adapter` — `ok=true, complete=true`, 9/9 gate PASS
42
+ - `node bin/peaks.js ide model --current` (against real z-code installation) — `modelId: "M3"`, `detected: true`
43
+
44
+ ### Sediment (Step 11) — 7 lessons in `.peaks/memory/`
45
+
46
+ | File | Topic |
47
+ |---|---|
48
+ | `z-code-peaks-loop-9-ide-adapter-vendor-neutrality-adapter.md` | z-code 9th IDE + vendor-neutrality adapter pattern |
49
+ | `peaks-loop-install-model-getstrongestmodelid-fallback.md` | install no longer hardcodes default model |
50
+ | `desktop-application-ide-adapter-z-code-cli.md` | desktop-app IDE adapter field-degradation decision |
51
+ | `peaks-code-runbook-4-0-0-beta-6-skill-md-cli-d-001-d-002-d-003-d-010.md` | 4 SKILL.md / CLI drift points |
52
+ | `2026-07-09-zcode-adapter-overview.md` | RID 003 overall summary (project record) |
53
+ | `peaks-ide-runtime-detect-zcode-only.md` | `peaks ide model --current` z-code 4-tier priority chain |
54
+ | `ide-adapter-detectcurrentmodel-optional-interface-pattern.md` | optional interface field extension pattern |
55
+
56
+ ### Known follow-ups (S3-cleanup backlog)
57
+
58
+ - **Slice C extension** — other 8 IDEs' `detectCurrentModel()` (claude-code / trae / cursor / codex / qoder / tongyi-lingma / hermes / openclaw). Currently only z-code has a working detector; all 8 still fall through to the back-compat `'claude-opus-4-7'`.
59
+ - **zai / GLM provider entry** — `D-008`: peaks-loop has no built-in provider entry for z-code's default `builtin:zai` (GLM) or `builtin:bigmodel` providers. Users on those providers must add custom `provider.<uuid>` entries to their `~/.peaks/config.json` themselves.
60
+ - **zcode-adapter UNVERIFIED sentinel values** — `hookEvent: 'PreToolUse'`, `toolMatcher: 'Bash'`, `envVar: 'ZCODE_PROJECT_DIR'` are placeholder guesses based on Anthropic-compatible protocol. Real z-code desktop application dogfood will reveal the actual values; replace once known.
61
+ - **`IdeAdapter` interface optional fields** — `D-009b` proposes converting `hookEvent` / `toolMatcher` / `envVar` to optional types, or adding an `unverified: boolean` flag, so future adapters with unknown protocols don't need to fabricate sentinel values.
62
+
63
+ ### Breaking changes
64
+
65
+ None. All 8 pre-existing adapters continue to load without changes. CLI subcommands `peaks ide *` and `peaks ide model --current` are additive.
66
+
5
67
  ## 4.0.0-beta.5 — 2026-07-08
6
68
 
7
69
  ### Added — peaks-solo dispatcher (分诊员)
@@ -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,25 @@
1
+ /**
2
+ * peaks ide * CLI (Slice 2026-07-09 add-zcode-adapter, Slice C).
3
+ *
4
+ * Slice C adds ONE verb:
5
+ *
6
+ * peaks ide model --current
7
+ *
8
+ * Outputs the currently-active model id detected from the host
9
+ * environment (so far: only the `zcode` adapter opts in via
10
+ * `IdeAdapter.detectCurrentModel?`). The CLI is the read-only
11
+ * debugging surface for runtime model probing. It does NOT set any
12
+ * state — `peaks config set model` remains the canonical write path.
13
+ *
14
+ * Future slices will extend this surface with `peaks ide list`,
15
+ * `peaks ide detect`, etc. — this slice keeps the surface minimal
16
+ * (per Karpathy guideline #2 — Simplicity First).
17
+ *
18
+ * Vendor-neutrality: this file delegates to
19
+ * `src/services/ide/current-model-detector.ts` which walks every
20
+ * registered adapter. This file does NOT import any specific
21
+ * adapter.
22
+ */
23
+ import { Command } from 'commander';
24
+ import { type ProgramIO } from '../cli-helpers.js';
25
+ export declare function registerIdeCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,42 @@
1
+ import { detectCurrentIdeModel } from '../../services/ide/current-model-detector.js';
2
+ import { addJsonOption, printResult } from '../cli-helpers.js';
3
+ import { fail, ok } from '../../shared/result.js';
4
+ import { listAdapterIds } from '../../services/ide/ide-registry.js';
5
+ export function registerIdeCommands(program, io) {
6
+ const ide = program.command('ide').description('Read-only introspection helpers for the registered IDE adapter layer (slice 2026-07-09 add-zcode-adapter, Slice C)');
7
+ addJsonOption(ide
8
+ .command('model')
9
+ .description('Inspect the IDE adapter model surface. Slice C supports `--current` only.')
10
+ .option('--current', 'Print the model id the active IDE adapter reports as currently configured')
11
+ .action(async (options) => {
12
+ if (options.current) {
13
+ await runIdeModelCurrent(io, options);
14
+ return;
15
+ }
16
+ // No verb supplied — Commander would normally show help; we
17
+ // emit a structured error instead so the JSON envelope stays
18
+ // consistent for LLM callers.
19
+ printResult(io, fail('ide.model', 'MISSING_VERB', 'Specify a verb: --current. Future slices may add --configured / --default.', { registeredAdapters: listAdapterIds() }, ['Rerun with --current to read the runtime-detected model id.']), options.json);
20
+ }));
21
+ }
22
+ async function runIdeModelCurrent(io, options) {
23
+ try {
24
+ const modelId = await detectCurrentIdeModel();
25
+ if (modelId === undefined) {
26
+ printResult(io, ok('ide.model.current', {
27
+ modelId: null,
28
+ detected: false,
29
+ registeredAdapters: listAdapterIds(),
30
+ }), options.json);
31
+ return;
32
+ }
33
+ printResult(io, ok('ide.model.current', {
34
+ modelId,
35
+ detected: true,
36
+ registeredAdapters: listAdapterIds(),
37
+ }), options.json);
38
+ }
39
+ catch (error) {
40
+ printResult(io, fail('ide.model.current', 'DETECTION_FAILED', 'Runtime model detection failed; falling back to configured model. See logs.', { error: error instanceof Error ? error.message : String(error) }, ['Check `PEAKS_LOG_LEVEL=debug peaks ide model --current` for the underlying error.']), options.json);
41
+ }
42
+ }
@@ -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 };