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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
  3. package/dist/src/cli/commands/adapter-commands-s2a.js +92 -0
  4. package/dist/src/cli/commands/ide-commands.d.ts +25 -0
  5. package/dist/src/cli/commands/ide-commands.js +42 -0
  6. package/dist/src/cli/commands/polyrepo-commands.d.ts +37 -0
  7. package/dist/src/cli/commands/polyrepo-commands.js +155 -0
  8. package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
  9. package/dist/src/cli/commands/runtime-commands.js +150 -0
  10. package/dist/src/cli/program.js +23 -0
  11. package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
  12. package/dist/src/services/adapter/adapter-registry.js +186 -0
  13. package/dist/src/services/config/model-routing.d.ts +55 -2
  14. package/dist/src/services/config/model-routing.js +79 -3
  15. package/dist/src/services/ide/adapters/zcode-adapter.d.ts +40 -0
  16. package/dist/src/services/ide/adapters/zcode-adapter.js +220 -0
  17. package/dist/src/services/ide/current-model-detector.d.ts +14 -0
  18. package/dist/src/services/ide/current-model-detector.js +60 -0
  19. package/dist/src/services/ide/ide-registry.js +3 -0
  20. package/dist/src/services/ide/ide-types.d.ts +24 -1
  21. package/dist/src/services/polyrepo/polyrepo-dispatcher.d.ts +34 -0
  22. package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
  23. package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
  24. package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
  25. package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
  26. package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
  27. package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
  28. package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
  29. package/dist/src/services/preferences/preferences-types.d.ts +1 -1
  30. package/dist/src/services/providers/minimax-worker-service.d.ts +1 -1
  31. package/dist/src/services/providers/minimax-worker-service.js +1 -1
  32. package/dist/src/services/rd/rd-service.js +7 -4
  33. package/dist/src/services/runtime/runtime-detector.d.ts +15 -0
  34. package/dist/src/services/runtime/runtime-detector.js +62 -0
  35. package/dist/src/services/runtime/runtime-service.d.ts +59 -0
  36. package/dist/src/services/runtime/runtime-service.js +51 -0
  37. package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
  38. package/dist/src/services/runtime/vendor-adapter.js +1 -0
  39. package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
  40. package/dist/src/services/runtime/vendors/claude-code.js +39 -0
  41. package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
  42. package/dist/src/services/runtime/vendors/codex.js +43 -0
  43. package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
  44. package/dist/src/services/runtime/vendors/copilot.js +40 -0
  45. package/dist/src/services/workflow/workflow-router-service.d.ts +1 -1
  46. package/dist/src/services/workflow/workflow-router-service.js +37 -31
  47. package/dist/src/shared/version.d.ts +1 -1
  48. package/dist/src/shared/version.js +1 -1
  49. package/package.json +1 -1
  50. package/scripts/install-skills.mjs +13 -10
  51. package/skills/peaks-code/SKILL.md +26 -12
  52. package/skills/peaks-code/references/external-references.md +1 -3
  53. package/skills/peaks-code/references/external-skill-invocation.md +1 -1
  54. package/skills/peaks-code/references/runbook.md +1 -3
  55. package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
  56. package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
  57. package/skills/peaks-slice-decompose/SKILL.md +61 -1
  58. package/skills/peaks-code/references/openspec-workflow.md +0 -81
@@ -0,0 +1,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 };
@@ -73,6 +73,10 @@ import { registerMutCommands } from './commands/mut-commands.js';
73
73
  import { registerFixtureCommands } from './commands/fixture-commands.js';
74
74
  import { registerReviewerCommands } from './commands/reviewer-commands.js';
75
75
  import { registerObservabilityCommands } from './commands/observability-commands.js';
76
+ // Slice 2026-07-09 add-zcode-adapter (Slice C): `peaks ide model --current` —
77
+ // read-only runtime model probe surfaced via the registered IDE adapter
78
+ // chain. See src/services/ide/current-model-detector.ts.
79
+ import { registerIdeCommands } from './commands/ide-commands.js';
76
80
  // Slice 2026-07-01-strategic-compact-cli: peaks compact
77
81
  // (suggest | recommend | survival | dry-run | force) — strategic-compact
78
82
  // CLI primitives. See src/services/compact/ and
@@ -85,6 +89,15 @@ import { registerJobCommands } from './commands/job-commands.js';
85
89
  // 15b / 15c / 15d will extend sediment-commands.ts with the remaining 14 verbs.
86
90
  import { registerSedimentCommands } from './commands/sediment-commands.js';
87
91
  import { registerAdapterCommands } from './commands/adapter-commands.js';
92
+ // Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
93
+ // `peaks adapter *` (list / register). See src/services/runtime/ and
94
+ // src/services/adapter/adapter-registry.ts. Coexists with the older
95
+ // `peaks skill adapter` surface (slice 2026-07-04-cli-15a) which is
96
+ // for skill materialization, not vendor-adapter registry.
97
+ import { registerRuntimeCommands } from './commands/runtime-commands.js';
98
+ import { registerAdapterS2ACommands } from './commands/adapter-commands-s2a.js';
99
+ // Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
100
+ import { registerPolyrepoCommands } from './commands/polyrepo-commands.js';
88
101
  import { registerSkillVisibilityCommand } from './commands/skill-visibility.js';
89
102
  import { applyRetention } from '../services/log/retention.js';
90
103
  import { writeLogEntry, maybeWriteStderr } from '../services/log/logger.js';
@@ -361,6 +374,9 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
361
374
  // Slice B of v2.11.1: `peaks observability status|slices|fanout|repair-cycles`
362
375
  // (read-only queries over the JSONL metrics emitted from the
363
376
  // peaks request transition hook in Slice A).
377
+ // Slice 2026-07-09 add-zcode-adapter (C.6): `peaks ide model --current`
378
+ // (read-only runtime model probe via the registered IDE adapter chain).
379
+ registerIdeCommands(program, io);
364
380
  registerObservabilityCommands(program, io);
365
381
  // Slice 2026-07-01-strategic-compact-cli: `peaks compact
366
382
  // suggest|recommend|survival|dry-run|force` — strategic-compact CLI
@@ -375,6 +391,13 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
375
391
  registerSedimentCommands(program, io);
376
392
  // Slice 2026-07-04-cli-15a: `peaks skill adapter <verb>` — adapter list / set-active.
377
393
  registerAdapterCommands(program, io);
394
+ // Slice RD-2 S2-a: `peaks runtime *` (detect / list / compact) +
395
+ // `peaks adapter *` (list / register). Vendor verbs live ONLY in
396
+ // src/services/runtime/vendors/<vendor>.ts (AC-1 enforcement).
397
+ registerRuntimeCommands(program, io);
398
+ registerAdapterS2ACommands(program, io);
399
+ // Slice RD-2 S2-b: `peaks polyrepo *` (init / status / dispatch).
400
+ registerPolyrepoCommands(program, io);
378
401
  // Slice 2026-07-05-peaks-code-to-peaks-code (Task 1): `peaks skill:visibility --list --json`
379
402
  // — read .claude-plugin/marketplace.json and report each skill's
380
403
  // 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
+ }
@@ -0,0 +1,186 @@
1
+ /**
2
+ * Adapter registry — slice S2-a of RD-2.
3
+ *
4
+ * Persists user-registered vendor adapters to
5
+ * `.peaks/runtime/adapters.json` and resolves them by id. The
6
+ * registry is layered on top of `RuntimeService` (built-in adapters
7
+ * live there); a caller asks "do you have an adapter for id=X?" by
8
+ * first asking the registry, then falling back to built-ins. The
9
+ * runtime service itself does NOT consult the registry (kept
10
+ * decoupled so the registry can be swapped for a different
11
+ * persistence backend without touching runtime-service).
12
+ *
13
+ * Design notes:
14
+ * - Persistence uses atomic write (write to tmp + rename) so a
15
+ * crash mid-write does not corrupt the registry file.
16
+ * - `register()` defaults to fail-if-exists to prevent silent
17
+ * overwrites of user customizations; pass `force: true` to
18
+ * overwrite (matches the CLI surface from PRD §4.4).
19
+ * - VendorAdapter implementations are NOT directly serializable
20
+ * (they hold closures over `home`, child_process handles, etc.).
21
+ * Instead the registry persists a minimal JSON shape:
22
+ * `{id, displayName, binary, args}` and reconstructs a
23
+ * ProcessVendorAdapter wrapper on load. This keeps the JSON
24
+ * file human-readable + diff-friendly.
25
+ */
26
+ import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from 'node:fs';
27
+ import { dirname, join } from 'node:path';
28
+ import { spawn } from 'node:child_process';
29
+ /** A registry-built adapter: wraps a persisted record + invokes the
30
+ * recorded binary with the recorded args. Used so `peaks runtime
31
+ * compact --via <id>` works against user-registered adapters without
32
+ * peaks-loop needing to ship a vendor-specific implementation. */
33
+ class ProcessVendorAdapter {
34
+ id;
35
+ displayName;
36
+ binary;
37
+ extraArgs;
38
+ constructor(record) {
39
+ this.id = record.id;
40
+ this.displayName = record.displayName;
41
+ this.binary = record.binary;
42
+ this.extraArgs = record.args ?? [];
43
+ }
44
+ async detect() {
45
+ // Heuristic: process-spawned adapters are always "detectable" once
46
+ // registered; their actual availability is checked at compact()
47
+ // time. This avoids probing every registered binary on every
48
+ // detect() call.
49
+ return true;
50
+ }
51
+ async compact(args = {}) {
52
+ const argv = [...this.extraArgs];
53
+ if (args.force === true)
54
+ argv.push('--force');
55
+ return new Promise((resolveRun) => {
56
+ const proc = spawn(this.binary, argv, { stdio: ['ignore', 'pipe', 'pipe'] });
57
+ let stdout = '';
58
+ let stderr = '';
59
+ proc.stdout.on('data', (chunk) => { stdout += chunk.toString('utf8'); });
60
+ proc.stderr.on('data', (chunk) => { stderr += chunk.toString('utf8'); });
61
+ proc.on('error', (err) => {
62
+ resolveRun({ exitCode: 127, stdout, stderr: stderr + (stderr.length > 0 ? '\n' : '') + err.message });
63
+ });
64
+ proc.on('close', (code) => {
65
+ resolveRun({ exitCode: code ?? 0, stdout, stderr });
66
+ });
67
+ });
68
+ }
69
+ }
70
+ /** Validate an adapter record. Used by register() to fail fast on
71
+ * malformed CLI input. Returns an error message or undefined. */
72
+ function validateRecord(record) {
73
+ if (record.id.length === 0)
74
+ return 'adapter id must be non-empty';
75
+ if (!/^[a-z0-9][a-z0-9._-]*$/.test(record.id)) {
76
+ return `adapter id "${record.id}" must match /^[a-z0-9][a-z0-9._-]*$/`;
77
+ }
78
+ if (record.displayName.length === 0)
79
+ return 'adapter displayName must be non-empty';
80
+ if (record.binary.length === 0)
81
+ return 'adapter binary must be non-empty';
82
+ if (record.binary.includes('/') || record.binary.includes('\\')) {
83
+ return `adapter binary "${record.binary}" must be a name, not a path`;
84
+ }
85
+ return undefined;
86
+ }
87
+ /** In-memory + on-disk adapter registry. The registry holds USER
88
+ * adapters (those registered via `peaks adapter register`); built-in
89
+ * adapters live in RuntimeService and are NOT registered here. */
90
+ export class AdapterRegistry {
91
+ records = new Map();
92
+ /** Register a new adapter. When an adapter with the same id already
93
+ * exists, returns the existing record unless `force: true`. */
94
+ register(record, opts = {}) {
95
+ const validationError = validateRecord(record);
96
+ if (validationError !== undefined) {
97
+ throw new Error(`invalid adapter record: ${validationError}`);
98
+ }
99
+ const existed = this.records.has(record.id);
100
+ if (existed && opts.force !== true) {
101
+ return { record: this.records.get(record.id), created: false };
102
+ }
103
+ this.records.set(record.id, record);
104
+ return { record, created: !existed };
105
+ }
106
+ /** Resolve a registered adapter by id, materializing it as a
107
+ * runnable VendorAdapter. Returns undefined when not registered. */
108
+ resolve(id) {
109
+ const rec = this.records.get(id);
110
+ if (rec === undefined)
111
+ return undefined;
112
+ return new ProcessVendorAdapter(rec);
113
+ }
114
+ /** List registered adapter records (serializable form). */
115
+ list() {
116
+ return Array.from(this.records.values());
117
+ }
118
+ /** Remove a registered adapter. Returns true if it existed. */
119
+ unregister(id) {
120
+ return this.records.delete(id);
121
+ }
122
+ /** Persist the current registry to disk as JSON. Writes atomically
123
+ * (write to `<file>.tmp` then rename) so a crash mid-write does
124
+ * not corrupt the registry file. */
125
+ persist(file) {
126
+ const payload = {
127
+ version: 1,
128
+ adapters: this.list()
129
+ };
130
+ mkdirSync(dirname(file), { recursive: true });
131
+ const tmp = `${file}.tmp`;
132
+ writeFileSync(tmp, JSON.stringify(payload, null, 2), 'utf8');
133
+ renameSync(tmp, file);
134
+ }
135
+ /** Load a registry from disk. Missing file = empty registry.
136
+ * Corrupt file = throw (caller decides whether to bail or
137
+ * fall back to empty). */
138
+ load(file) {
139
+ if (!existsSync(file))
140
+ return;
141
+ const raw = readFileSync(file, 'utf8');
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(raw);
145
+ }
146
+ catch (err) {
147
+ throw new Error(`adapter registry at ${file} is not valid JSON: ${err.message}`);
148
+ }
149
+ if (!isPersistedRegistry(parsed)) {
150
+ throw new Error(`adapter registry at ${file} has unexpected shape (expected version=1 + adapters[])`);
151
+ }
152
+ this.records.clear();
153
+ for (const rec of parsed.adapters) {
154
+ const validationError = validateRecord(rec);
155
+ if (validationError !== undefined) {
156
+ // Skip + continue — partial recovery is better than throwing on
157
+ // a single bad row. The CLI surfaces the count of skipped
158
+ // records via the load() return value below.
159
+ continue;
160
+ }
161
+ this.records.set(rec.id, rec);
162
+ }
163
+ }
164
+ /** Default location for the on-disk registry file. */
165
+ static defaultFile(projectRoot) {
166
+ return join(projectRoot, '.peaks', 'runtime', 'adapters.json');
167
+ }
168
+ }
169
+ function isPersistedRegistry(value) {
170
+ if (typeof value !== 'object' || value === null)
171
+ return false;
172
+ const v = value;
173
+ if (v.version !== 1)
174
+ return false;
175
+ if (!Array.isArray(v.adapters))
176
+ return false;
177
+ return v.adapters.every((a) => {
178
+ if (typeof a !== 'object' || a === null)
179
+ return false;
180
+ const r = a;
181
+ return typeof r.id === 'string'
182
+ && typeof r.displayName === 'string'
183
+ && typeof r.binary === 'string'
184
+ && (r.args === undefined || (Array.isArray(r.args) && r.args.every((x) => typeof x === 'string')));
185
+ });
186
+ }
@@ -1,4 +1,57 @@
1
1
  import { type ModelProviderConfig, type PeaksConfig } from './config-types.js';
2
- export declare const STRONGEST_MODEL_ID: "claude-opus-4-7";
2
+ /**
3
+ * Slice 2026-07-09 add-zcode-adapter (A.3): replaces the legacy
4
+ * `STRONGEST_MODEL_ID = 'claude-opus-4-7'` constant (removed) with a dynamic
5
+ * resolver. The strongest planner/reviewer model is now derived
6
+ * from the active config instead of being hardcoded to a specific
7
+ * vendor. This unlocks running peaks-loop under non-Claude IDEs
8
+ * (z-code, Cursor, Trae, ...).
9
+ *
10
+ * Resolution order:
11
+ * 1. Explicit `config.model` (user override via `peaks config set model ...`)
12
+ * 2. Stable fallback for back-compat (env-var override; defaults to
13
+ * `claude-opus-4-7` so legacy fixtures keep compiling per
14
+ * SC §3.4 strategy A)
15
+ *
16
+ * Vendor-neutrality note: the default `'claude-opus-4-7'` is a
17
+ * *back-compat fallback*, NOT a hardcoded "the strongest model is
18
+ * always Claude" assumption. Production callers should set
19
+ * `config.model` (or override the env var) to declare their strongest
20
+ * model. Adapter-level runtime probing is wired in Slice C
21
+ * (`getStrongestModelIdAsync` below).
22
+ */
23
+ export declare function getStrongestModelId(config?: {
24
+ model?: unknown;
25
+ }): string;
26
+ /**
27
+ * Slice 2026-07-09 add-zcode-adapter (Slice C, C.4): async variant
28
+ * of `getStrongestModelId`. Same precedence for layers 1 + 2, but
29
+ * layer 2.5 is the IDE's runtime probe (`detectCurrentIdeModel`)
30
+ * which is consulted BEFORE the env-var back-compat fallback.
31
+ *
32
+ * Why this exists:
33
+ * - `rd-service.buildPlan` (and other sync callers) still call the
34
+ * sync `getStrongestModelId` — we do NOT change those signatures.
35
+ * - New async callers (the future Slice C-extension of
36
+ * `workflow-router-service`, plus any LLM-inside-z-code path)
37
+ * resolve the strongest model from the IDE's actual state,
38
+ * without forcing the user to set `config.model`.
39
+ *
40
+ * Resolution order:
41
+ * 1. Explicit `config.model`.
42
+ * 2. `PEAKS_STRONGEST_MODEL_DEFAULT` env var override.
43
+ * 3. Runtime probe via `detectCurrentIdeModel()` (only when the
44
+ * registered IDE adapter opts in via `detectCurrentModel?` —
45
+ * only `zcode` does in Slice C; other IDEs skip this layer).
46
+ * 4. Hardcoded back-compat default `'claude-opus-4-7'`.
47
+ *
48
+ * Each layer is isolated so a failure in the probe does not break
49
+ * the resolver.
50
+ */
51
+ export declare function getStrongestModelIdAsync(config?: {
52
+ model?: unknown;
53
+ }, runtimeProbeDisabled?: boolean): Promise<string>;
3
54
  export declare function getConfiguredExecutionModelId(providers: ModelProviderConfig | undefined): string;
4
- export declare function getEconomyAwareExecutionModelId(config: Pick<PeaksConfig, 'economyMode' | 'providers'>): string;
55
+ export declare function getEconomyAwareExecutionModelId(config: Pick<PeaksConfig, 'economyMode' | 'providers'> & {
56
+ model?: string;
57
+ }): string;
@@ -1,4 +1,79 @@
1
- export const STRONGEST_MODEL_ID = 'claude-opus-4-7';
1
+ import { detectCurrentIdeModel } from '../ide/current-model-detector.js';
2
+ /**
3
+ * Slice 2026-07-09 add-zcode-adapter (A.3): replaces the legacy
4
+ * `STRONGEST_MODEL_ID = 'claude-opus-4-7'` constant (removed) with a dynamic
5
+ * resolver. The strongest planner/reviewer model is now derived
6
+ * from the active config instead of being hardcoded to a specific
7
+ * vendor. This unlocks running peaks-loop under non-Claude IDEs
8
+ * (z-code, Cursor, Trae, ...).
9
+ *
10
+ * Resolution order:
11
+ * 1. Explicit `config.model` (user override via `peaks config set model ...`)
12
+ * 2. Stable fallback for back-compat (env-var override; defaults to
13
+ * `claude-opus-4-7` so legacy fixtures keep compiling per
14
+ * SC §3.4 strategy A)
15
+ *
16
+ * Vendor-neutrality note: the default `'claude-opus-4-7'` is a
17
+ * *back-compat fallback*, NOT a hardcoded "the strongest model is
18
+ * always Claude" assumption. Production callers should set
19
+ * `config.model` (or override the env var) to declare their strongest
20
+ * model. Adapter-level runtime probing is wired in Slice C
21
+ * (`getStrongestModelIdAsync` below).
22
+ */
23
+ export function getStrongestModelId(config) {
24
+ const raw = config?.model;
25
+ const fromConfig = typeof raw === 'string' ? raw.trim() : undefined;
26
+ if (fromConfig)
27
+ return fromConfig;
28
+ // Slice 2026-07-09: back-compat fallback for test fixtures that
29
+ // pre-date this slice (SC §3.4 strategy A env-var override).
30
+ return process.env.PEAKS_STRONGEST_MODEL_DEFAULT ?? 'claude-opus-4-7';
31
+ }
32
+ /**
33
+ * Slice 2026-07-09 add-zcode-adapter (Slice C, C.4): async variant
34
+ * of `getStrongestModelId`. Same precedence for layers 1 + 2, but
35
+ * layer 2.5 is the IDE's runtime probe (`detectCurrentIdeModel`)
36
+ * which is consulted BEFORE the env-var back-compat fallback.
37
+ *
38
+ * Why this exists:
39
+ * - `rd-service.buildPlan` (and other sync callers) still call the
40
+ * sync `getStrongestModelId` — we do NOT change those signatures.
41
+ * - New async callers (the future Slice C-extension of
42
+ * `workflow-router-service`, plus any LLM-inside-z-code path)
43
+ * resolve the strongest model from the IDE's actual state,
44
+ * without forcing the user to set `config.model`.
45
+ *
46
+ * Resolution order:
47
+ * 1. Explicit `config.model`.
48
+ * 2. `PEAKS_STRONGEST_MODEL_DEFAULT` env var override.
49
+ * 3. Runtime probe via `detectCurrentIdeModel()` (only when the
50
+ * registered IDE adapter opts in via `detectCurrentModel?` —
51
+ * only `zcode` does in Slice C; other IDEs skip this layer).
52
+ * 4. Hardcoded back-compat default `'claude-opus-4-7'`.
53
+ *
54
+ * Each layer is isolated so a failure in the probe does not break
55
+ * the resolver.
56
+ */
57
+ export async function getStrongestModelIdAsync(config, runtimeProbeDisabled = false) {
58
+ const raw = config?.model;
59
+ const fromConfig = typeof raw === 'string' ? raw.trim() : undefined;
60
+ if (fromConfig)
61
+ return fromConfig;
62
+ const envDefault = process.env.PEAKS_STRONGEST_MODEL_DEFAULT;
63
+ if (envDefault && envDefault.length > 0)
64
+ return envDefault;
65
+ if (!runtimeProbeDisabled) {
66
+ try {
67
+ const probed = await detectCurrentIdeModel();
68
+ if (probed && probed.length > 0)
69
+ return probed;
70
+ }
71
+ catch {
72
+ // best-effort — fall through to back-compat default
73
+ }
74
+ }
75
+ return 'claude-opus-4-7';
76
+ }
2
77
  export function getConfiguredExecutionModelId(providers) {
3
78
  const providerConfigs = Object.values(providers ?? {});
4
79
  const configuredModel = providerConfigs
@@ -12,6 +87,7 @@ export function getConfiguredExecutionModelId(providers) {
12
87
  export function getEconomyAwareExecutionModelId(config) {
13
88
  // Slice 2.0.1-bug1 round 3: economy is the project default. Treat undefined as enabled
14
89
  // (matches the pre-slice implicit default from DEFAULT_CONFIG.economyMode = true). Only an
15
- // explicit `economyMode === false` switches execution to STRONGEST_MODEL_ID.
16
- return config.economyMode !== false ? getConfiguredExecutionModelId(config.providers) : STRONGEST_MODEL_ID;
90
+ // explicit `economyMode === false` switches execution to the strongest planner/reviewer
91
+ // model (resolved dynamically per Slice 2026-07-09 add-zcode-adapter A.3).
92
+ return config.economyMode !== false ? getConfiguredExecutionModelId(config.providers) : getStrongestModelId(config);
17
93
  }
@@ -0,0 +1,40 @@
1
+ import type { IdeAdapter } from '../ide-types.js';
2
+ /**
3
+ * Helper exported for tests + internal use: the default location of the
4
+ * z-code v2 config file on the host filesystem.
5
+ */
6
+ export declare function defaultZcodeConfigPath(): string;
7
+ /**
8
+ * Resolve the active model id from a parsed z-code config payload
9
+ * (the `~/.zcode/v2/config.json` root object). Pure / exported for
10
+ * unit tests so the file-IO seam and the resolution seam are
11
+ * independently verifiable.
12
+ *
13
+ * Resolution order (Slice C §"z-code config 解析路径"):
14
+ * P1. env var `PEAKS_ZCODE_ACTIVE_PROVIDER_UUID` (test seam +
15
+ * manual pin for the user).
16
+ * P2. The provider whose top-level UUID key is NOT prefixed
17
+ * `builtin:` — this matches the user-installed provider
18
+ * pattern. Within that provider, prefer its `models` first
19
+ * key (insertion order — z-code writes the active model
20
+ * first in the picker).
21
+ * P3. The first provider with `enabled: true`.
22
+ * P4. The first provider in the object (insertion order) — last
23
+ * resort. Returns undefined if there are zero providers.
24
+ *
25
+ * Returns `undefined` when nothing matches.
26
+ */
27
+ export declare function resolveZcodeCurrentModel(config: unknown, envOverride?: string | undefined): string | undefined;
28
+ /**
29
+ * The async adapter method that `detectCurrentIdeModel()` calls.
30
+ *
31
+ * Resolution order:
32
+ * 1. `PEAKS_ZCODE_ACTIVE_PROVIDER_UUID` env var override (test seam
33
+ * + manual pin).
34
+ * 2. `PEAKS_ZCODE_CONFIG_PATH` env var override (test seam for a
35
+ * fixture file). Falls back to `~/.zcode/v2/config.json`.
36
+ * 3. Returns `undefined` when the file is missing / malformed
37
+ * (consistent with the cross-IDE contract).
38
+ */
39
+ export declare function detectZcodeCurrentModel(): Promise<string | undefined>;
40
+ export declare const ZCODE_ADAPTER: IdeAdapter;