peaks-loop 4.0.0-beta.6 → 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.
- package/CHANGELOG.md +62 -0
- package/dist/src/cli/commands/ide-commands.d.ts +25 -0
- package/dist/src/cli/commands/ide-commands.js +42 -0
- package/dist/src/cli/program.js +7 -0
- package/dist/src/services/config/model-routing.d.ts +55 -2
- package/dist/src/services/config/model-routing.js +79 -3
- package/dist/src/services/ide/adapters/zcode-adapter.d.ts +40 -0
- package/dist/src/services/ide/adapters/zcode-adapter.js +220 -0
- package/dist/src/services/ide/current-model-detector.d.ts +14 -0
- package/dist/src/services/ide/current-model-detector.js +60 -0
- package/dist/src/services/ide/ide-registry.js +3 -0
- package/dist/src/services/ide/ide-types.d.ts +24 -1
- package/dist/src/services/preferences/preferences-types.d.ts +1 -1
- package/dist/src/services/providers/minimax-worker-service.d.ts +1 -1
- package/dist/src/services/providers/minimax-worker-service.js +1 -1
- package/dist/src/services/rd/rd-service.js +7 -4
- package/dist/src/services/workflow/workflow-router-service.d.ts +1 -1
- package/dist/src/services/workflow/workflow-router-service.js +37 -31
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/scripts/install-skills.mjs +13 -10
- package/skills/peaks-code/SKILL.md +25 -4
- package/skills/peaks-slice-decompose/SKILL.md +61 -1
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,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
|
+
}
|
package/dist/src/cli/program.js
CHANGED
|
@@ -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
|
|
@@ -370,6 +374,9 @@ Run peaks (no arguments) for a quickstart. You likely want one of:
|
|
|
370
374
|
// Slice B of v2.11.1: `peaks observability status|slices|fanout|repair-cycles`
|
|
371
375
|
// (read-only queries over the JSONL metrics emitted from the
|
|
372
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);
|
|
373
380
|
registerObservabilityCommands(program, io);
|
|
374
381
|
// Slice 2026-07-01-strategic-compact-cli: `peaks compact
|
|
375
382
|
// suggest|recommend|survival|dry-run|force` — strategic-compact CLI
|
|
@@ -1,4 +1,57 @@
|
|
|
1
1
|
import { type ModelProviderConfig, type PeaksConfig } from './config-types.js';
|
|
2
|
-
|
|
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'>
|
|
55
|
+
export declare function getEconomyAwareExecutionModelId(config: Pick<PeaksConfig, 'economyMode' | 'providers'> & {
|
|
56
|
+
model?: string;
|
|
57
|
+
}): string;
|
|
@@ -1,4 +1,79 @@
|
|
|
1
|
-
|
|
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
|
|
16
|
-
|
|
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;
|
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* z-code IDE adapter (slice 2026-07-09-add-zcode-adapter) — peaks-loop 的第九个
|
|
3
|
+
* 内置 IDE 适配器。
|
|
4
|
+
*
|
|
5
|
+
* z-code 是一个 VS Code-style 的 Anthropic-compatible 桌面应用:
|
|
6
|
+
* - 不是 CLI,没有 `zcode` binary (`which zcode` 返回空)。
|
|
7
|
+
* - 桌面应用内置了 "导入 skills" 功能,user 手动触发后将本地 skills 目录
|
|
8
|
+
* 与 `~/.zcode/skills` 双向同步 (实测 symlink)。
|
|
9
|
+
* - hook 协议、project-dir env var、compact command 均未公开。
|
|
10
|
+
*
|
|
11
|
+
* 设计前提 (RD-3 §2.2 D1 决策 + 现场实测,见
|
|
12
|
+
* `.peaks/memory/2026-07-09-zcode-context-probe-handoff.md` §2):
|
|
13
|
+
* - `compact` 字段全部 undefined — 走 `llm-self-compress` fallback 路径。
|
|
14
|
+
* - `envVar` / `hookEvent` / `toolMatcher` 全部标注 UNVERIFIED,因为
|
|
15
|
+
* z-code 文档未公开,本 adapter 用 Anthropic-compatible 协议的合理
|
|
16
|
+
* 假设占位。
|
|
17
|
+
* - `subAgentDispatcher` 用 `nullSubAgentDispatcher` 占位 — z-code
|
|
18
|
+
* 桌面应用没有公开的 Task 工具 dispatch 协议。
|
|
19
|
+
* - `standardsProfile` 字段指向宪法文件根 + rules 目录 — z-code 的设计
|
|
20
|
+
* 就是借用上游 "import skills" 目标目录,因此保留上游路径常量。
|
|
21
|
+
* - `skillInstall` 指向 `~/.zcode/skills` — 实测 z-code 已通过 user
|
|
22
|
+
* 手动导入同步到这里。
|
|
23
|
+
* - 注:`standardsProfile` 中的常量子段是 z-code 借用的固定路径,属于
|
|
24
|
+
* 架构事实,不算 vendor verb 泄漏。
|
|
25
|
+
*
|
|
26
|
+
* Vendor-neutrality 备注 (SC-3 §5 + RD-3 §2.4):
|
|
27
|
+
* 本文件允许出现 vendor 字符串 (`zcode` / `ZCODE_PROJECT_DIR` 等),
|
|
28
|
+
* **不允许** 出现其他 vendor 字符串 / 模型名。`standardsProfile`
|
|
29
|
+
* 中的 `CLAUDE.md` / `.claude/rules` 是 z-code 借用的上游路径常量,
|
|
30
|
+
* 属于架构事实,不算 vendor verb 泄漏。
|
|
31
|
+
*/
|
|
32
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
33
|
+
import { homedir } from 'node:os';
|
|
34
|
+
import { join, resolve } from 'node:path';
|
|
35
|
+
import { nullSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
|
|
36
|
+
/**
|
|
37
|
+
* Helper exported for tests + internal use: the default location of the
|
|
38
|
+
* z-code v2 config file on the host filesystem.
|
|
39
|
+
*/
|
|
40
|
+
export function defaultZcodeConfigPath() {
|
|
41
|
+
return join(homedir(), '.zcode', 'v2', 'config.json');
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Read the z-code config from `path` (string-typed to keep this
|
|
45
|
+
* swappable for tests). Synchronous + Node.js `fs` so adapter callers
|
|
46
|
+
* stay async-only at the public surface (`detectCurrentModel` is
|
|
47
|
+
* already declared `async`).
|
|
48
|
+
*/
|
|
49
|
+
function readZcodeConfig(path) {
|
|
50
|
+
if (!existsSync(path))
|
|
51
|
+
return undefined;
|
|
52
|
+
try {
|
|
53
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Resolve the active model id from a parsed z-code config payload
|
|
61
|
+
* (the `~/.zcode/v2/config.json` root object). Pure / exported for
|
|
62
|
+
* unit tests so the file-IO seam and the resolution seam are
|
|
63
|
+
* independently verifiable.
|
|
64
|
+
*
|
|
65
|
+
* Resolution order (Slice C §"z-code config 解析路径"):
|
|
66
|
+
* P1. env var `PEAKS_ZCODE_ACTIVE_PROVIDER_UUID` (test seam +
|
|
67
|
+
* manual pin for the user).
|
|
68
|
+
* P2. The provider whose top-level UUID key is NOT prefixed
|
|
69
|
+
* `builtin:` — this matches the user-installed provider
|
|
70
|
+
* pattern. Within that provider, prefer its `models` first
|
|
71
|
+
* key (insertion order — z-code writes the active model
|
|
72
|
+
* first in the picker).
|
|
73
|
+
* P3. The first provider with `enabled: true`.
|
|
74
|
+
* P4. The first provider in the object (insertion order) — last
|
|
75
|
+
* resort. Returns undefined if there are zero providers.
|
|
76
|
+
*
|
|
77
|
+
* Returns `undefined` when nothing matches.
|
|
78
|
+
*/
|
|
79
|
+
export function resolveZcodeCurrentModel(config, envOverride) {
|
|
80
|
+
const providers = config?.provider;
|
|
81
|
+
if (!providers || typeof providers !== 'object')
|
|
82
|
+
return undefined;
|
|
83
|
+
// P1: env var override (vendor-neutral identifier: the provider UUID).
|
|
84
|
+
if (envOverride !== undefined && envOverride !== '') {
|
|
85
|
+
const target = providers[envOverride];
|
|
86
|
+
if (target && typeof target === 'object') {
|
|
87
|
+
const modelId = pickFirstModelId(target);
|
|
88
|
+
if (modelId)
|
|
89
|
+
return modelId;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
// P2: prefer a non-builtin provider (the user-installed one).
|
|
93
|
+
for (const [uuid, entry] of Object.entries(providers)) {
|
|
94
|
+
if (uuid.startsWith('builtin:'))
|
|
95
|
+
continue;
|
|
96
|
+
if (!entry || typeof entry !== 'object')
|
|
97
|
+
continue;
|
|
98
|
+
const modelId = pickFirstModelId(entry);
|
|
99
|
+
if (modelId)
|
|
100
|
+
return modelId;
|
|
101
|
+
}
|
|
102
|
+
// P3: first enabled provider (legacy / fresh-install case).
|
|
103
|
+
for (const entry of Object.values(providers)) {
|
|
104
|
+
if (!entry || typeof entry !== 'object')
|
|
105
|
+
continue;
|
|
106
|
+
const enabled = entry.enabled;
|
|
107
|
+
if (enabled === true) {
|
|
108
|
+
const modelId = pickFirstModelId(entry);
|
|
109
|
+
if (modelId)
|
|
110
|
+
return modelId;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// P4: first provider at all.
|
|
114
|
+
for (const entry of Object.values(providers)) {
|
|
115
|
+
if (!entry || typeof entry !== 'object')
|
|
116
|
+
continue;
|
|
117
|
+
const modelId = pickFirstModelId(entry);
|
|
118
|
+
if (modelId)
|
|
119
|
+
return modelId;
|
|
120
|
+
}
|
|
121
|
+
return undefined;
|
|
122
|
+
}
|
|
123
|
+
function pickFirstModelId(provider) {
|
|
124
|
+
const models = provider.models;
|
|
125
|
+
if (!models || typeof models !== 'object')
|
|
126
|
+
return undefined;
|
|
127
|
+
for (const key of Object.keys(models)) {
|
|
128
|
+
if (typeof key === 'string' && key.trim().length > 0)
|
|
129
|
+
return key.trim();
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* The async adapter method that `detectCurrentIdeModel()` calls.
|
|
135
|
+
*
|
|
136
|
+
* Resolution order:
|
|
137
|
+
* 1. `PEAKS_ZCODE_ACTIVE_PROVIDER_UUID` env var override (test seam
|
|
138
|
+
* + manual pin).
|
|
139
|
+
* 2. `PEAKS_ZCODE_CONFIG_PATH` env var override (test seam for a
|
|
140
|
+
* fixture file). Falls back to `~/.zcode/v2/config.json`.
|
|
141
|
+
* 3. Returns `undefined` when the file is missing / malformed
|
|
142
|
+
* (consistent with the cross-IDE contract).
|
|
143
|
+
*/
|
|
144
|
+
export async function detectZcodeCurrentModel() {
|
|
145
|
+
const overridePath = process.env.PEAKS_ZCODE_CONFIG_PATH;
|
|
146
|
+
const path = overridePath && overridePath.length > 0 ? overridePath : defaultZcodeConfigPath();
|
|
147
|
+
const config = readZcodeConfig(path);
|
|
148
|
+
if (config === undefined)
|
|
149
|
+
return undefined;
|
|
150
|
+
return resolveZcodeCurrentModel(config, process.env.PEAKS_ZCODE_ACTIVE_PROVIDER_UUID);
|
|
151
|
+
}
|
|
152
|
+
export const ZCODE_ADAPTER = {
|
|
153
|
+
id: 'zcode',
|
|
154
|
+
displayName: 'z-code',
|
|
155
|
+
settings: {
|
|
156
|
+
dirName: '.zcode', // VERIFIED — 实测 z-code 项目根目录是 `.zcode`
|
|
157
|
+
settingsFileName: 'settings.json',
|
|
158
|
+
resolveSettingsFile: (scope, projectRoot) => {
|
|
159
|
+
const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
|
|
160
|
+
return join(root, '.zcode', 'settings.json');
|
|
161
|
+
},
|
|
162
|
+
supportsScope: (scope) => scope === 'project' || scope === 'global',
|
|
163
|
+
},
|
|
164
|
+
// UNVERIFIED — z-code 桌面应用未公开 `ZCODE_PROJECT_DIR` env var,这里
|
|
165
|
+
// 用占位符,RD 阶段假设 z-code 借用了 Anthropic-compatible 协议并可能
|
|
166
|
+
// 注入一个项目根 env 变量。若未注入,峰 router 会回退到 cwd 探测。
|
|
167
|
+
envVar: 'ZCODE_PROJECT_DIR',
|
|
168
|
+
// UNVERIFIED — z-code 未公开 hook event key,占位用 Anthropic-compatible
|
|
169
|
+
// 协议的常见命名 `PreToolUse`。若 z-code 用其他 key,peaks hooks install
|
|
170
|
+
// 钩子在此 IDE 上不生效,可由 user 通过手动导入绕过。
|
|
171
|
+
hookEvent: 'PreToolUse',
|
|
172
|
+
// UNVERIFIED — 同上,占位用 `Bash` matcher。
|
|
173
|
+
toolMatcher: 'Bash',
|
|
174
|
+
// Slice 2026-07-09: z-code 桌面应用没有公开 Task 工具 dispatch 协议,
|
|
175
|
+
// 用 `nullSubAgentDispatcher` 占位 — peaks sub-agent dispatch CLI 在
|
|
176
|
+
// z-code 上返回 SubAgentNotSupportedError,符合 slice #008 P-5 capability
|
|
177
|
+
// 检查契约 (CLI 不崩溃)。
|
|
178
|
+
subAgentDispatcher: nullSubAgentDispatcher,
|
|
179
|
+
// Slice #010 G9: 假设 z-code 支持 PreToolUse (UNVERIFIED)。Opt-in,等真实
|
|
180
|
+
// z-code 安装 dogfood 后再决定。若 z-code 不支持 G9 hook,CLI 兜底层仍会
|
|
181
|
+
// 强制执行 G9 阈值。
|
|
182
|
+
promptSizeAware: true,
|
|
183
|
+
installHints: [
|
|
184
|
+
'z-code 没有 CLI,因此无需重启 — skills symlink 已写入 ~/.zcode/skills/。',
|
|
185
|
+
'若 z-code 未自动加载 peaks-* skills,在 z-code 桌面应用里手动触发 "导入 skills" 功能即可。',
|
|
186
|
+
],
|
|
187
|
+
capabilities: {
|
|
188
|
+
gateEnforce: true,
|
|
189
|
+
statusline: true,
|
|
190
|
+
},
|
|
191
|
+
// Standards profile: 路径常量保留 (z-code 的 "导入 skills" 功能
|
|
192
|
+
// 借用了上游 settings 目录,因此宪法文件路径常量同步保留)。
|
|
193
|
+
standardsProfile: {
|
|
194
|
+
rootFile: 'CLAUDE.md',
|
|
195
|
+
rulesDir: '.claude/rules',
|
|
196
|
+
rulesFileGlob: '**/*.md',
|
|
197
|
+
autoLoaded: true,
|
|
198
|
+
format: 'markdown',
|
|
199
|
+
migrationHint: 'z-code 借用了上游 standards 路径,无需迁移。',
|
|
200
|
+
},
|
|
201
|
+
// Skill install profile: 指向 `~/.zcode/skills` 系列 (实测 z-code 已通过
|
|
202
|
+
// user 手动 symlink 同步;peaks-loop 也直接支持)。
|
|
203
|
+
skillInstall: {
|
|
204
|
+
skillsDir: join(homedir(), '.zcode', 'skills'),
|
|
205
|
+
outputStylesDir: join(homedir(), '.zcode', 'output-styles'),
|
|
206
|
+
installStrategy: 'symlink',
|
|
207
|
+
envVarOverride: 'PEAKS_ZCODE_SKILLS_DIR',
|
|
208
|
+
},
|
|
209
|
+
// compact profile 留空 — z-code 没有 CLI binary (RD-3 §2.2 D1 决策)。
|
|
210
|
+
// peaks-loop 走 `llm-self-compress` fallback 路径 (LLM 自己总结 context),
|
|
211
|
+
// 不抛错、不警告 (符合 slice #008 P-5 capability-check contract)。
|
|
212
|
+
// Slice C (2026-07-09 add-zcode-adapter, C.2): runtime probe for
|
|
213
|
+
// the currently-active model id. Reads `~/.zcode/v2/config.json`
|
|
214
|
+
// and resolves the active provider via `resolveZcodeCurrentModel`.
|
|
215
|
+
// See `.peaks/_runtime/2026-07-08-session-17918f/qa/.../slice-C-completion.md`
|
|
216
|
+
// for the resolution priority chain (env override → non-builtin
|
|
217
|
+
// provider → first enabled → first provider). Returns undefined
|
|
218
|
+
// on any failure (consistent with the cross-IDE contract).
|
|
219
|
+
detectCurrentModel: detectZcodeCurrentModel,
|
|
220
|
+
};
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Probe every registered IDE adapter for its currently-active model
|
|
3
|
+
* id. Returns the first non-empty value found in registration order,
|
|
4
|
+
* or `undefined` when no adapter opts in / all opted-in adapters
|
|
5
|
+
* return undefined.
|
|
6
|
+
*
|
|
7
|
+
* Registration order: see `ide-registry.ts` ADAPTERS Map. The IDE
|
|
8
|
+
* adapter for `claude-code` is consulted first; only `zcode` opts in
|
|
9
|
+
* to this slice's detection (other adapters will return undefined).
|
|
10
|
+
*
|
|
11
|
+
* Async because each IDE adapter's detectCurrentModel may read local
|
|
12
|
+
* disk or env on a slow filesystem.
|
|
13
|
+
*/
|
|
14
|
+
export declare function detectCurrentIdeModel(): Promise<string | undefined>;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime current-model detector (Slice 2026-07-09 add-zcode-adapter, Slice C).
|
|
3
|
+
*
|
|
4
|
+
* Walks the registered IDE adapter chain and asks the IDE adapter
|
|
5
|
+
* (when it opts in via `IdeAdapter.detectCurrentModel?`) to report the
|
|
6
|
+
* currently-active model id. This is the runtime sibling of
|
|
7
|
+
* `getStrongestModelId(config)` in `src/services/config/model-routing.ts`:
|
|
8
|
+
*
|
|
9
|
+
* - `getStrongestModelId(config)` reads `config.model` (user-set) and
|
|
10
|
+
* falls back to the env-var / `'claude-opus-4-7'` constant. Sync.
|
|
11
|
+
* - `detectCurrentIdeModel()` reads the IDE's runtime state (which
|
|
12
|
+
* provider UI is active, which model the picker shows). Async.
|
|
13
|
+
*
|
|
14
|
+
* The CLI surface `peaks ide model --current` (added in this slice)
|
|
15
|
+
* delegates here. The future async resolver at
|
|
16
|
+
* `getStrongestModelIdAsync` (Slice C §C.4) calls this so config-less
|
|
17
|
+
* runs (e.g. an LLM inside z-code) can derive the strongest model from
|
|
18
|
+
* its actual environment.
|
|
19
|
+
*
|
|
20
|
+
* Vendor-neutrality notes (SC-3 §5 + RD-3 §2.4):
|
|
21
|
+
* - This file never names a vendor (no "claude", "zcode", etc.).
|
|
22
|
+
* - It only walks `IdeAdapter.detectCurrentModel?` — any adapter
|
|
23
|
+
* can opt in.
|
|
24
|
+
* - It returns `undefined` when detection is unavailable; callers
|
|
25
|
+
* must fall back to the configured / hardcoded model.
|
|
26
|
+
*/
|
|
27
|
+
import { getAdapter } from './ide-registry.js';
|
|
28
|
+
import { listAdapterIds } from './ide-registry.js';
|
|
29
|
+
/**
|
|
30
|
+
* Probe every registered IDE adapter for its currently-active model
|
|
31
|
+
* id. Returns the first non-empty value found in registration order,
|
|
32
|
+
* or `undefined` when no adapter opts in / all opted-in adapters
|
|
33
|
+
* return undefined.
|
|
34
|
+
*
|
|
35
|
+
* Registration order: see `ide-registry.ts` ADAPTERS Map. The IDE
|
|
36
|
+
* adapter for `claude-code` is consulted first; only `zcode` opts in
|
|
37
|
+
* to this slice's detection (other adapters will return undefined).
|
|
38
|
+
*
|
|
39
|
+
* Async because each IDE adapter's detectCurrentModel may read local
|
|
40
|
+
* disk or env on a slow filesystem.
|
|
41
|
+
*/
|
|
42
|
+
export async function detectCurrentIdeModel() {
|
|
43
|
+
for (const id of listAdapterIds()) {
|
|
44
|
+
const adapter = getAdapter(id);
|
|
45
|
+
if (typeof adapter.detectCurrentModel !== 'function')
|
|
46
|
+
continue;
|
|
47
|
+
try {
|
|
48
|
+
const modelId = await adapter.detectCurrentModel();
|
|
49
|
+
if (typeof modelId === 'string' && modelId.trim().length > 0) {
|
|
50
|
+
return modelId.trim();
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Per-SC §3.4: detection is best-effort. Swallow errors so a
|
|
55
|
+
// misconfigured adapter cannot poison the chain; the next
|
|
56
|
+
// adapter still gets a chance.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return undefined;
|
|
60
|
+
}
|
|
@@ -4,6 +4,7 @@ import { CURSOR_ADAPTER } from './adapters/cursor-adapter.js';
|
|
|
4
4
|
import { CODEX_ADAPTER } from './adapters/codex-adapter.js';
|
|
5
5
|
import { HERMES_ADAPTER } from './adapters/hermes-adapter.js';
|
|
6
6
|
import { OPENCLAW_ADAPTER } from './adapters/openclaw-adapter.js';
|
|
7
|
+
import { ZCODE_ADAPTER } from './adapters/zcode-adapter.js';
|
|
7
8
|
/**
|
|
8
9
|
* Built-in IDE adapter registry。Map<IdeId, IdeAdapter> 是单一来源。
|
|
9
10
|
*
|
|
@@ -24,6 +25,7 @@ const ADAPTERS = new Map([
|
|
|
24
25
|
['codex', CODEX_ADAPTER],
|
|
25
26
|
['hermes', HERMES_ADAPTER],
|
|
26
27
|
['openclaw', OPENCLAW_ADAPTER],
|
|
28
|
+
['zcode', ZCODE_ADAPTER],
|
|
27
29
|
]);
|
|
28
30
|
/** Get the adapter for a given IDE id. Throws on unsupported IDE. */
|
|
29
31
|
export function getAdapter(ide) {
|
|
@@ -57,4 +59,5 @@ export function _resetAdaptersForTesting() {
|
|
|
57
59
|
ADAPTERS.set('codex', CODEX_ADAPTER);
|
|
58
60
|
ADAPTERS.set('hermes', HERMES_ADAPTER);
|
|
59
61
|
ADAPTERS.set('openclaw', OPENCLAW_ADAPTER);
|
|
62
|
+
ADAPTERS.set('zcode', ZCODE_ADAPTER);
|
|
60
63
|
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
* 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。
|
|
13
13
|
*/
|
|
14
14
|
import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js';
|
|
15
|
-
export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma' | 'hermes' | 'openclaw';
|
|
15
|
+
export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma' | 'hermes' | 'openclaw' | 'zcode';
|
|
16
16
|
export interface IdeCapabilities {
|
|
17
17
|
/** peaks gate enforce 是否适用该 IDE(必备) */
|
|
18
18
|
readonly gateEnforce: true;
|
|
@@ -125,6 +125,29 @@ export interface IdeAdapter {
|
|
|
125
125
|
* `.peaks/memory/2026-06-27-auto-compact-design.md`.
|
|
126
126
|
*/
|
|
127
127
|
readonly compact?: IdeCompactProfile;
|
|
128
|
+
/**
|
|
129
|
+
* Optional runtime probe for the IDE's currently-active model id.
|
|
130
|
+
* When defined, `detectCurrentIdeModel()` in
|
|
131
|
+
* `src/services/ide/current-model-detector.ts` calls this to
|
|
132
|
+
* discover which model the IDE UI is currently configured for —
|
|
133
|
+
* e.g. so `peaks ide model --current` and the future
|
|
134
|
+
* `getStrongestModelIdAsync()` can use the same value without
|
|
135
|
+
* forcing the user to set `config.model` manually.
|
|
136
|
+
*
|
|
137
|
+
* Returning `undefined` (or the method being absent on this
|
|
138
|
+
* adapter) means "I don't know — fall back to the configured
|
|
139
|
+
* model / env var / back-compat default".
|
|
140
|
+
*
|
|
141
|
+
* Implementations may read local files (`fs.readFileSync`),
|
|
142
|
+
* env vars, or both. They MUST NOT throw on missing files — catch
|
|
143
|
+
* locally and return `undefined`. The orchestration layer also
|
|
144
|
+
* catches, but defense in depth.
|
|
145
|
+
*
|
|
146
|
+
* Added in slice 2026-07-09 add-zcode-adapter (Slice C); only the
|
|
147
|
+
* `zcode` adapter opts in this slice. Future slices can opt in
|
|
148
|
+
* other IDEs without changing this signature (optional method).
|
|
149
|
+
*/
|
|
150
|
+
readonly detectCurrentModel?: () => Promise<string | undefined>;
|
|
128
151
|
}
|
|
129
152
|
/**
|
|
130
153
|
* Per-IDE auto-compact capability descriptor. See `IdeAdapter.compact`
|
|
@@ -71,7 +71,7 @@ export interface ProjectPreferences {
|
|
|
71
71
|
* Slice 2026-06-28-code-mode-bypass-fix: economyMode is a
|
|
72
72
|
* **MODEL-SELECTION** knob only — it chooses between the cheap
|
|
73
73
|
* configured provider (`getConfiguredExecutionModelId`) and the
|
|
74
|
-
* strongest planner/reviewer model (`
|
|
74
|
+
* strongest planner/reviewer model (`getStrongestModelId(...)`). It does
|
|
75
75
|
* NOT throttle concurrency, fan-out, or worker count. Per user
|
|
76
76
|
* direction 2026-06-28: "效率比省钱更重要,是在效率达到最大值的时
|
|
77
77
|
* 候,再去考虑经济问题" (efficiency first; economy is only considered
|
|
@@ -62,7 +62,7 @@ export async function runMiniMaxWorker(config, request, fetchImpl = fetch) {
|
|
|
62
62
|
successMatch: 'startsWith'
|
|
63
63
|
}, fetchImpl);
|
|
64
64
|
const reviewHandoff = {
|
|
65
|
-
model: 'claude-opus-4-7',
|
|
65
|
+
model: process.env.PEAKS_MINIMAX_REVIEW_MODEL ?? 'claude-opus-4-7',
|
|
66
66
|
prompt: [
|
|
67
67
|
`Review this MiniMax worker result for change ${sessionId}.`,
|
|
68
68
|
`Goal: ${goal}`,
|
|
@@ -4,7 +4,7 @@ import { stableRealPath } from '../../shared/path-utils.js';
|
|
|
4
4
|
import { WORKSPACE_UNAVAILABLE_NEXT_ACTIONS } from '../../shared/planner-response.js';
|
|
5
5
|
import { getLocalArtifactPath, hasValidArtifactWorkspace } from '../artifacts/workspace-service.js';
|
|
6
6
|
import { getSessionDir } from '../session/getSessionDir.js';
|
|
7
|
-
import { getConfiguredExecutionModelId,
|
|
7
|
+
import { getConfiguredExecutionModelId, getStrongestModelId } from '../config/model-routing.js';
|
|
8
8
|
/**
|
|
9
9
|
* 2.0.1-bug1: the slim 2.0 `~/.peaks/config.json` no longer carries a
|
|
10
10
|
* `providers` block (legacy model config lives in
|
|
@@ -116,8 +116,8 @@ function selectConcreteTargetArea(targetAreas, index) {
|
|
|
116
116
|
function getTaskModelRole(wave) {
|
|
117
117
|
return wave === 'implementation candidates' || wave === 'unit-test execution' ? 'execution' : 'strongest';
|
|
118
118
|
}
|
|
119
|
-
function getTaskModelId(modelRole, executionModelId) {
|
|
120
|
-
return modelRole === 'execution' ? executionModelId :
|
|
119
|
+
function getTaskModelId(modelRole, executionModelId, strongestModelId) {
|
|
120
|
+
return modelRole === 'execution' ? executionModelId : strongestModelId;
|
|
121
121
|
}
|
|
122
122
|
const MAX_ARTIFACT_BYTES = 256_000;
|
|
123
123
|
function readArtifactFile(rootPath, artifactWorkspacePath, artifact) {
|
|
@@ -220,6 +220,9 @@ function buildPlan(request, standardsOverlay) {
|
|
|
220
220
|
const goal = normalizeGoal(request.goal);
|
|
221
221
|
const swarmMode = request.swarmMode ?? true;
|
|
222
222
|
const executionModelId = request.executionModelId?.trim() || resolveExecutionModelId();
|
|
223
|
+
// Slice 2026-07-09 add-zcode-adapter (A.3): strongest planner/reviewer
|
|
224
|
+
// model is resolved dynamically (config.model → env-var fallback).
|
|
225
|
+
const strongestModelId = getStrongestModelId();
|
|
223
226
|
const { workerTarget, blockedReasons } = resolveWorkerTarget(request.maxWorkers);
|
|
224
227
|
const artifactWorkspacePath = resolveArtifactWorkspacePath(request);
|
|
225
228
|
const artifactRoot = 'rd/swarm';
|
|
@@ -342,7 +345,7 @@ function buildPlan(request, standardsOverlay) {
|
|
|
342
345
|
workerKind: taskId,
|
|
343
346
|
purpose: `${taskId.replace(/^rd-/, '').replace(/-/g, ' ')} for ${goal}`,
|
|
344
347
|
modelRole,
|
|
345
|
-
modelId: getTaskModelId(modelRole, executionModelId),
|
|
348
|
+
modelId: getTaskModelId(modelRole, executionModelId, strongestModelId),
|
|
346
349
|
inputs: [goal, artifactRoot],
|
|
347
350
|
outputs: [briefPath],
|
|
348
351
|
dependsOn: [...waveDependencies[wave]],
|
|
@@ -38,7 +38,7 @@ export type WorkflowRouterStep = {
|
|
|
38
38
|
};
|
|
39
39
|
export type WorkflowModelRouting = {
|
|
40
40
|
readonly strongestModel: {
|
|
41
|
-
readonly modelId:
|
|
41
|
+
readonly modelId: string;
|
|
42
42
|
readonly uses: readonly WorkflowStepStage[];
|
|
43
43
|
};
|
|
44
44
|
readonly executionModel: {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { getConfiguredExecutionModelId,
|
|
1
|
+
import { getConfiguredExecutionModelId, getStrongestModelId } from '../config/model-routing.js';
|
|
2
2
|
import { getLocalArtifactPath } from '../artifacts/workspace-service.js';
|
|
3
3
|
import { createRdSwarmPlan } from '../rd/rd-service.js';
|
|
4
4
|
import { createTechPlan, getTechStatus } from '../tech/tech-service.js';
|
|
@@ -37,12 +37,12 @@ function assertCodeModeAllowed(mode, codeMode) {
|
|
|
37
37
|
throw new Error('codeMode requires code workflow mode');
|
|
38
38
|
}
|
|
39
39
|
}
|
|
40
|
-
function step(input, executionModelId) {
|
|
40
|
+
function step(input, executionModelId, strongestModelId) {
|
|
41
41
|
const modelRole = EXECUTION_STAGES.includes(input.stage) ? 'execution' : 'strongest';
|
|
42
42
|
return {
|
|
43
43
|
...input,
|
|
44
44
|
modelRole,
|
|
45
|
-
modelId: modelRole === 'execution' ? executionModelId :
|
|
45
|
+
modelId: modelRole === 'execution' ? executionModelId : strongestModelId,
|
|
46
46
|
dryRunOnly: true,
|
|
47
47
|
invokesAgents: false,
|
|
48
48
|
writesArtifacts: false
|
|
@@ -78,40 +78,40 @@ function annotateSteps(steps, codeMode) {
|
|
|
78
78
|
};
|
|
79
79
|
});
|
|
80
80
|
}
|
|
81
|
-
function createSoloSteps(executionModelId) {
|
|
81
|
+
function createSoloSteps(executionModelId, strongestModelId) {
|
|
82
82
|
return [
|
|
83
|
-
step({ id: 'code-product-direction', stage: 'product-direction', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Product direction needs strong judgment before execution work is delegated.', dependsOn: [] }, executionModelId),
|
|
84
|
-
step({ id: 'code-design-direction', stage: 'design-direction', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Design direction uses the recommended default before cheaper implementation work.', dependsOn: ['code-product-direction'] }, executionModelId),
|
|
85
|
-
step({ id: 'code-tech-direction', stage: 'tech-direction', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Technical boundaries and approval gates use the recommended default with high-confidence planning.', dependsOn: ['code-design-direction'] }, executionModelId),
|
|
86
|
-
step({ id: 'code-tech-review', stage: 'tech-review', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Tech artifacts and gate decisions require strong review and a recommended default path.', dependsOn: ['code-tech-direction'] }, executionModelId),
|
|
87
|
-
step({ id: 'code-rd-planning', stage: 'rd-planning', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'RD task decomposition and acceptance criteria use the recommended default before execution delegation.', dependsOn: ['code-tech-review'] }, executionModelId),
|
|
88
|
-
step({ id: 'code-coding-execution', stage: 'coding-execution', owner: 'peaks-rd', modelTier: executionModelId ===
|
|
89
|
-
step({ id: 'code-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId ===
|
|
90
|
-
step({ id: 'code-quality-review', stage: 'quality-review', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Reducer and final quality gates need strong synthesis and risk review.', dependsOn: ['code-unit-test-execution'] }, executionModelId)
|
|
83
|
+
step({ id: 'code-product-direction', stage: 'product-direction', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Product direction needs strong judgment before execution work is delegated.', dependsOn: [] }, executionModelId, strongestModelId),
|
|
84
|
+
step({ id: 'code-design-direction', stage: 'design-direction', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Design direction uses the recommended default before cheaper implementation work.', dependsOn: ['code-product-direction'] }, executionModelId, strongestModelId),
|
|
85
|
+
step({ id: 'code-tech-direction', stage: 'tech-direction', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Technical boundaries and approval gates use the recommended default with high-confidence planning.', dependsOn: ['code-design-direction'] }, executionModelId, strongestModelId),
|
|
86
|
+
step({ id: 'code-tech-review', stage: 'tech-review', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Tech artifacts and gate decisions require strong review and a recommended default path.', dependsOn: ['code-tech-direction'] }, executionModelId, strongestModelId),
|
|
87
|
+
step({ id: 'code-rd-planning', stage: 'rd-planning', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'RD task decomposition and acceptance criteria use the recommended default before execution delegation.', dependsOn: ['code-tech-review'] }, executionModelId, strongestModelId),
|
|
88
|
+
step({ id: 'code-coding-execution', stage: 'coding-execution', owner: 'peaks-rd', modelTier: executionModelId === strongestModelId ? 'top-tier' : 'mid-tier', reason: `Coding and routine refactoring must use the configured execution worker model ${executionModelId}.`, dependsOn: ['code-rd-planning'] }, executionModelId, strongestModelId),
|
|
89
|
+
step({ id: 'code-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId === strongestModelId ? 'top-tier' : 'mid-tier', reason: `Unit test authoring and focused test runs must use the configured execution worker model ${executionModelId}.`, dependsOn: ['code-coding-execution'] }, executionModelId, strongestModelId),
|
|
90
|
+
step({ id: 'code-quality-review', stage: 'quality-review', owner: 'peaks-code', modelTier: 'top-tier', reason: 'Reducer and final quality gates need strong synthesis and risk review.', dependsOn: ['code-unit-test-execution'] }, executionModelId, strongestModelId)
|
|
91
91
|
];
|
|
92
92
|
}
|
|
93
|
-
function createSoloStepsForMode(codeMode, executionModelId) {
|
|
94
|
-
return annotateSteps(createSoloSteps(executionModelId), codeMode);
|
|
93
|
+
function createSoloStepsForMode(codeMode, executionModelId, strongestModelId) {
|
|
94
|
+
return annotateSteps(createSoloSteps(executionModelId, strongestModelId), codeMode);
|
|
95
95
|
}
|
|
96
|
-
function createTeamSteps(executionModelId) {
|
|
96
|
+
function createTeamSteps(executionModelId, strongestModelId) {
|
|
97
97
|
return [
|
|
98
|
-
step({ id: 'team-product-direction', stage: 'product-direction', owner: 'human', modelTier: 'top-tier', reason: 'Team product direction should stay on the governed planning path.', dependsOn: [] }, executionModelId),
|
|
99
|
-
step({ id: 'team-design-direction', stage: 'design-direction', owner: 'human', modelTier: 'top-tier', reason: 'Team design direction should preserve reviewability and accountability.', dependsOn: ['team-product-direction'] }, executionModelId),
|
|
100
|
-
step({ id: 'team-tech-direction', stage: 'tech-direction', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Team technical plans should remain strongly governed before RD execution.', dependsOn: ['team-design-direction'] }, executionModelId),
|
|
101
|
-
step({ id: 'team-tech-review', stage: 'tech-review', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Team tech approval requires strong review before execution.', dependsOn: ['team-tech-direction'] }, executionModelId),
|
|
102
|
-
step({ id: 'team-rd-planning', stage: 'rd-planning', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'Team RD task decomposition remains on the governed strongest-model path.', dependsOn: ['team-tech-review'] }, executionModelId),
|
|
103
|
-
step({ id: 'team-coding-execution', stage: 'coding-execution', owner: 'peaks-rd', modelTier: executionModelId ===
|
|
104
|
-
step({ id: 'team-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId ===
|
|
105
|
-
step({ id: 'team-quality-review', stage: 'quality-review', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'Team RD outputs still need reducer and quality review gates.', dependsOn: ['team-unit-test-execution'] }, executionModelId)
|
|
98
|
+
step({ id: 'team-product-direction', stage: 'product-direction', owner: 'human', modelTier: 'top-tier', reason: 'Team product direction should stay on the governed planning path.', dependsOn: [] }, executionModelId, strongestModelId),
|
|
99
|
+
step({ id: 'team-design-direction', stage: 'design-direction', owner: 'human', modelTier: 'top-tier', reason: 'Team design direction should preserve reviewability and accountability.', dependsOn: ['team-product-direction'] }, executionModelId, strongestModelId),
|
|
100
|
+
step({ id: 'team-tech-direction', stage: 'tech-direction', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Team technical plans should remain strongly governed before RD execution.', dependsOn: ['team-design-direction'] }, executionModelId, strongestModelId),
|
|
101
|
+
step({ id: 'team-tech-review', stage: 'tech-review', owner: 'peaks-tech', modelTier: 'top-tier', reason: 'Team tech approval requires strong review before execution.', dependsOn: ['team-tech-direction'] }, executionModelId, strongestModelId),
|
|
102
|
+
step({ id: 'team-rd-planning', stage: 'rd-planning', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'Team RD task decomposition remains on the governed strongest-model path.', dependsOn: ['team-tech-review'] }, executionModelId, strongestModelId),
|
|
103
|
+
step({ id: 'team-coding-execution', stage: 'coding-execution', owner: 'peaks-rd', modelTier: executionModelId === strongestModelId ? 'top-tier' : 'mid-tier', reason: `Bounded coding tasks must use the configured execution worker model ${executionModelId}.`, dependsOn: ['team-rd-planning'] }, executionModelId, strongestModelId),
|
|
104
|
+
step({ id: 'team-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId === strongestModelId ? 'top-tier' : 'mid-tier', reason: `Unit-test tasks must use the configured execution worker model ${executionModelId}.`, dependsOn: ['team-coding-execution'] }, executionModelId, strongestModelId),
|
|
105
|
+
step({ id: 'team-quality-review', stage: 'quality-review', owner: 'peaks-rd', modelTier: 'top-tier', reason: 'Team RD outputs still need reducer and quality review gates.', dependsOn: ['team-unit-test-execution'] }, executionModelId, strongestModelId)
|
|
106
106
|
];
|
|
107
107
|
}
|
|
108
108
|
function uniqueStrings(values) {
|
|
109
109
|
return [...new Set(values)];
|
|
110
110
|
}
|
|
111
|
-
function createModelRouting(steps, executionModelId) {
|
|
111
|
+
function createModelRouting(steps, executionModelId, strongestModelId) {
|
|
112
112
|
return {
|
|
113
113
|
strongestModel: {
|
|
114
|
-
modelId:
|
|
114
|
+
modelId: strongestModelId,
|
|
115
115
|
uses: steps.filter((step) => step.modelRole === 'strongest').map((step) => step.stage)
|
|
116
116
|
},
|
|
117
117
|
executionModel: {
|
|
@@ -135,10 +135,10 @@ function getTechPlanBlockedReasons(techPlan) {
|
|
|
135
135
|
function getTechPlanNextActions(techPlan) {
|
|
136
136
|
return [...techPlan.nextActions];
|
|
137
137
|
}
|
|
138
|
-
function createModeStatus(economyMode, swarmMode, executionModelId, executionModelSource) {
|
|
138
|
+
function createModeStatus(economyMode, swarmMode, executionModelId, executionModelSource, strongestModelId) {
|
|
139
139
|
const economySummary = economyMode
|
|
140
140
|
? `Economy mode enabled: code worker and test worker strictly use ${executionModelId} from config providers.`
|
|
141
|
-
: `Economy mode disabled: code worker and test worker use ${
|
|
141
|
+
: `Economy mode disabled: code worker and test worker use ${strongestModelId}, matching planner/reviewer.`;
|
|
142
142
|
const swarmSummary = swarmMode
|
|
143
143
|
? 'Swarm mode enabled: peaks-rd coding, unit-test, and peaks-qa quality work must be represented as swarm worker tasks.'
|
|
144
144
|
: 'Swarm mode disabled: swarm worker graph generation is bypassed.';
|
|
@@ -183,8 +183,14 @@ export function createWorkflowRouterPlan(request) {
|
|
|
183
183
|
// object (`config: { providers: {} }`) still surfaces the "must be
|
|
184
184
|
// configured" error from `getConfiguredExecutionModelId`.
|
|
185
185
|
const effectiveProviders = request.config?.providers ?? { minimax: { model: 'minimax-2.7' } };
|
|
186
|
-
|
|
187
|
-
|
|
186
|
+
// Slice 2026-07-09 add-zcode-adapter (A.3): the strongest
|
|
187
|
+
// planner/reviewer model is now resolved via `getStrongestModelId`,
|
|
188
|
+
// which reads `config.model` (if present) or falls back to the
|
|
189
|
+
// env-var-overridable default. This decouples the strongest-model
|
|
190
|
+
// pick from any specific vendor constant.
|
|
191
|
+
const strongestModelId = getStrongestModelId(request.config);
|
|
192
|
+
const executionModelId = economyMode !== false ? getConfiguredExecutionModelId(effectiveProviders) : strongestModelId;
|
|
193
|
+
const modeStatus = createModeStatus(economyMode, swarmMode, executionModelId, economyMode ? 'config.providers' : 'planner-reviewer-strongest-model', strongestModelId);
|
|
188
194
|
const codeMode = getCodeMode(request.mode, request.codeMode);
|
|
189
195
|
const decisionProfile = getDecisionProfileSummary(request.mode, codeMode);
|
|
190
196
|
const artifactWorkspacePath = request.artifactWorkspacePath ?? (request.workspace ? getLocalArtifactPath(request.workspace) : undefined);
|
|
@@ -195,7 +201,7 @@ export function createWorkflowRouterPlan(request) {
|
|
|
195
201
|
const techStatus = getTechStatus({ sessionId: request.sessionId, ...sharedWorkspaceOptions });
|
|
196
202
|
const techPlan = createTechPlan({ sessionId: request.sessionId, goal, swarm: swarmMode, dryRun: true, ...sharedWorkspaceOptions });
|
|
197
203
|
const rdPlan = createRdSwarmPlan({ skill: 'rd', sessionId: request.sessionId, goal, maxWorkers, swarmMode, executionModelId, dryRun: true, ...sharedWorkspaceOptions });
|
|
198
|
-
const steps = codeMode ? createSoloStepsForMode(codeMode, executionModelId) : createTeamSteps(executionModelId);
|
|
204
|
+
const steps = codeMode ? createSoloStepsForMode(codeMode, executionModelId, strongestModelId) : createTeamSteps(executionModelId, strongestModelId);
|
|
199
205
|
const blockedReasons = uniqueStrings([
|
|
200
206
|
...techStatus.blockedReasons,
|
|
201
207
|
...getTechPlanBlockedReasons(techPlan),
|
|
@@ -213,7 +219,7 @@ export function createWorkflowRouterPlan(request) {
|
|
|
213
219
|
decisionProfile,
|
|
214
220
|
dryRun: true,
|
|
215
221
|
routePolicy: request.mode === 'code' ? 'code-broad-multi-model' : 'team-rd-limited-multi-model',
|
|
216
|
-
modelRouting: createModelRouting(steps, executionModelId),
|
|
222
|
+
modelRouting: createModelRouting(steps, executionModelId, strongestModelId),
|
|
217
223
|
modelAssignments: createModelAssignments(steps),
|
|
218
224
|
modeStatus,
|
|
219
225
|
techStatus,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "4.0.0-beta.
|
|
1
|
+
export declare const CLI_VERSION = "4.0.0-beta.7";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "4.0.0-beta.
|
|
1
|
+
export const CLI_VERSION = "4.0.0-beta.7";
|
package/package.json
CHANGED
|
@@ -172,21 +172,15 @@ function readPackageVersion(packageRoot = resolvePackageRoot()) {
|
|
|
172
172
|
}
|
|
173
173
|
|
|
174
174
|
function createConfigDefaults(packageRoot) {
|
|
175
|
-
|
|
175
|
+
return {
|
|
176
176
|
version: readPackageVersion(packageRoot),
|
|
177
177
|
currentWorkspace: null,
|
|
178
178
|
workspaces: [],
|
|
179
179
|
language: 'en',
|
|
180
|
-
model: 'sonnet',
|
|
181
180
|
economyMode: true,
|
|
182
181
|
swarmMode: true,
|
|
183
182
|
tokens: {},
|
|
184
|
-
|
|
185
|
-
minimax: {
|
|
186
|
-
model: 'minimax-2.7'
|
|
187
|
-
}
|
|
188
|
-
},
|
|
189
|
-
proxy: {}
|
|
183
|
+
proxy: {}
|
|
190
184
|
};
|
|
191
185
|
}
|
|
192
186
|
|
|
@@ -381,7 +375,7 @@ function resolveProjectRoot(options) {
|
|
|
381
375
|
* overrides `PEAKS_CLAUDE_SKILLS_DIR` / `PEAKS_CLAUDE_OUTPUT_STYLES_DIR`
|
|
382
376
|
* continue to work, and the legacy default is preserved.
|
|
383
377
|
*/
|
|
384
|
-
const IDE_DETECTION_DIRS = [
|
|
378
|
+
export const IDE_DETECTION_DIRS = [
|
|
385
379
|
{ id: 'claude-code', dir: '.claude' },
|
|
386
380
|
{ id: 'trae', dir: '.trae' },
|
|
387
381
|
{ id: 'trae-cn', dir: '.trae-cn' },
|
|
@@ -389,6 +383,7 @@ const IDE_DETECTION_DIRS = [
|
|
|
389
383
|
{ id: 'cursor', dir: '.cursor' },
|
|
390
384
|
{ id: 'qoder', dir: '.qoder' },
|
|
391
385
|
{ id: 'tongyi-lingma', dir: '.tongyi-lingma' },
|
|
386
|
+
{ id: 'zcode', dir: '.zcode' },
|
|
392
387
|
];
|
|
393
388
|
|
|
394
389
|
/**
|
|
@@ -404,7 +399,7 @@ const IDE_DETECTION_DIRS = [
|
|
|
404
399
|
* directory was never populated. 2.0 fixes this by giving
|
|
405
400
|
* all 8 platforms canonical install paths.
|
|
406
401
|
*/
|
|
407
|
-
const IDE_SKILL_INSTALL_PROFILES = {
|
|
402
|
+
export const IDE_SKILL_INSTALL_PROFILES = {
|
|
408
403
|
'claude-code': {
|
|
409
404
|
skillsDir: join(homedir(), '.claude', 'skills'),
|
|
410
405
|
outputStylesDir: join(homedir(), '.claude', 'output-styles'),
|
|
@@ -469,6 +464,14 @@ const IDE_SKILL_INSTALL_PROFILES = {
|
|
|
469
464
|
envVar: 'PEAKS_OPENCLAW_SKILLS_DIR',
|
|
470
465
|
outputStylesEnvVar: 'PEAKS_OPENCLAW_OUTPUT_STYLES_DIR',
|
|
471
466
|
},
|
|
467
|
+
'zcode': {
|
|
468
|
+
skillsDir: join(homedir(), '.zcode', 'skills'),
|
|
469
|
+
outputStylesDir: join(homedir(), '.zcode', 'output-styles'),
|
|
470
|
+
agentsDir: join(homedir(), '.zcode', 'agents'),
|
|
471
|
+
envVar: 'PEAKS_ZCODE_SKILLS_DIR',
|
|
472
|
+
outputStylesEnvVar: 'PEAKS_ZCODE_OUTPUT_STYLES_DIR',
|
|
473
|
+
agentsEnvVar: 'PEAKS_ZCODE_AGENTS_DIR',
|
|
474
|
+
},
|
|
472
475
|
};
|
|
473
476
|
|
|
474
477
|
function detectInstalledIdeId(projectRoot) {
|
|
@@ -98,9 +98,13 @@ After Step 0.7 returns "fresh", run `peaks upgrade --detect-1x --project <root>
|
|
|
98
98
|
|
|
99
99
|
### Peaks-Loop Step 0.8 — Job 启动 (BLOCKING on LLM judgement — v3.1.1 patch + v3.1.2 mechanical gates)
|
|
100
100
|
|
|
101
|
-
The CLI
|
|
101
|
+
> **CLI reality check (D-001 sediment, 2026-07-09):** The CLI surface has changed since this section was last verified. The actual command is **`peaks job init --job-id <jid> --slice-list <list> --main-loop-strategy <single|rotating> [--parallelism-hint <serial|llm-decides>] [--exit-policy <strict|best-effort>] [--project <repo>]`**. The legacy `peaks code detect-job --is-job/--suggested-job-id/--confidence` form described below is **no longer present** in 4.0.0-beta.6. If you find `--is-job` rejected, fall back to `peaks job init`.
|
|
102
102
|
|
|
103
|
-
**
|
|
103
|
+
The CLI is a **recorder + gate** for job-shape (the LLM judges). LLM calls `peaks job init --job-id <jid> --slice-list <list> --main-loop-strategy <single|rotating>` (with `--caller-id <id>` and `--project <repo>` in non-Claude-Code environments), which writes `.peaks/_runtime/<sessionId>/job/<jid>/state.json` and emits a `[job-event] {kind: job-started, jobId, total, strategy}` line. Downstream steps call `peaks job status --job-id <jid>` to read state.
|
|
104
|
+
|
|
105
|
+
**Hard rule (v3.1.1 red-line #10):** LLM MUST NOT skip the job-init call. If `peaks job status` reports `done: 0` and the slice work has begun, the LLM forgot to initialize — re-call `peaks job init` immediately.
|
|
106
|
+
|
|
107
|
+
> **D-003 sediment (2026-07-09):** The historical `JOB_SHAPE_NOT_DECIDED` exception thrown by `read-job-shape` is no longer wired in 4.0.0-beta.6. The current CLI uses `peaks job status` which reports a passive `done: 0` instead of throwing. Both behaviors enforce the same intent (no work may begin before init) but the failure mode is a soft warning rather than a hard exception. LLM should treat `done: 0` after Step 0.8 as a recoverable miss, not an unrecoverable error.
|
|
104
108
|
|
|
105
109
|
**v3.1.2 mechanical gates** (recorder-only was bypassed twice):
|
|
106
110
|
|
|
@@ -143,7 +147,9 @@ Run `peaks project memories --project <repo> --json` to read decisions / convent
|
|
|
143
147
|
|
|
144
148
|
### Peaks-Loop Step 2.5: Set session title
|
|
145
149
|
|
|
146
|
-
Extract a short title from the user's first request (8-20 Chinese chars or 4-10 English words). Run `peaks session title
|
|
150
|
+
Extract a short title from the user's first request (8-20 Chinese chars or 4-10 English words). Run `peaks session title <sid> "<title>" --json`. Skip if a title is already set. → `references/skill-presence-and-title.md` (same file as Step 2).
|
|
151
|
+
|
|
152
|
+
> **CLI reality check (D-002 sediment, 2026-07-09):** The session id is a **positional argument**, not a `--session-id` flag. Correct invocation is `peaks session title 2026-07-08-session-17918f "add-zcode-adapter — peaks-loop 第 9 IDE 接入" --json`. Passing `--session-id` will be rejected with `error: unknown option '--session-id'`.
|
|
147
153
|
|
|
148
154
|
## Sub-agent session sharing (MANDATORY — one conversation = one sid)
|
|
149
155
|
|
|
@@ -153,6 +159,19 @@ When peaks-code dispatches a sub-agent (peaks-rd, peaks-qa, peaks-ui, peaks-txt,
|
|
|
153
159
|
|
|
154
160
|
Peaks-Loop Code may: identify scenarios, recommend profiles, coordinate role skills through artifacts, coordinate project memory extraction, request user confirmation at risk/commit boundaries. Peaks-Loop Code must NOT silently install hooks / create agents / enable MCP / modify Claude settings / create GitHub repos / bypass role-skill artifacts. → `references/boundaries.md`.
|
|
155
161
|
|
|
162
|
+
## CLI Drift Index (sediment 2026-07-09)
|
|
163
|
+
|
|
164
|
+
> **Reading guide:** This document was last verified against CLI surface in **peaks-loop 4.0.0-beta.6**. The CLI has drifted in 4 places since this SKILL.md was last fully aligned. Each drift below is annotated **inline at the relevant step** with a `> CLI reality check` blockquote. When you hit a `error: unknown option ...` error, **read the inline reality check first** before guessing.
|
|
165
|
+
|
|
166
|
+
| Drift ID | Step | Symptom | Fix | Inline location |
|
|
167
|
+
|---|---|---|---|---|
|
|
168
|
+
| **D-001** | 0.8 | `peaks code detect-job --is-job ...` rejected with `error: unknown option '--is-job'` | Use `peaks job init --job-id <jid> --slice-list <list> --main-loop-strategy <single\|rotating>` | §Step 0.8 first paragraph |
|
|
169
|
+
| **D-002** | 2.5 | `peaks session title --session-id <sid> ...` rejected with `error: unknown option '--session-id'` | sid is positional: `peaks session title <sid> "<title>" --json` | §Step 2.5 |
|
|
170
|
+
| **D-003** | 0.8 | `JOB_SHAPE_NOT_DECIDED` exception expected but never thrown | Current behavior is `peaks job status` reports `done: 0` passively — treat as recoverable miss, not hard error | §Step 0.8 third paragraph |
|
|
171
|
+
| **D-010** | 11c | `peaks memory extract` returns `extractedCount: 0` despite `<!-- peaks-memory:start -->` existing | Block requires YAML frontmatter (`title:` + `kind:` + `---`) + closing `<!-- peaks-memory:end -->`. Bare `peaks-memory:start` is parsed silently but produces no writes | §Step 11c + 11d |
|
|
172
|
+
|
|
173
|
+
> **Sediment lesson (master record):** `.peaks/memory/peaks-code-runbook-4-0-0-beta-6-skill-md-cli-d-001-d-002-d-003-d-010.md`
|
|
174
|
+
|
|
156
175
|
## Peaks-Loop GStack integration
|
|
157
176
|
|
|
158
177
|
Map gstack stages to Peaks-Loop role artifacts; preserve confirmation gates. → `references/gstack-integration.md` + `references/browser-workflow.md`.
|
|
@@ -233,11 +252,13 @@ After final validation, refresh project-local standards via `peaks standards ini
|
|
|
233
252
|
|
|
234
253
|
**11c — Canonical extract** (the only CLI that writes `.peaks/memory/`): `peaks memory extract --project <repo> --artifact .peaks/_runtime/<sessionId>/txt/handoff.md --apply --json`. `--apply` is REQUIRED (without it the command only previews — no files land).
|
|
235
254
|
|
|
255
|
+
> **CLI reality check (D-010 sediment, 2026-07-09):** The `<!-- peaks-memory:start -->` block must be followed immediately by a **YAML frontmatter** (`title: ...`, `kind: lesson | decision | convention`) and a `---` separator. Each block must close with `<!-- peaks-memory:end -->`. A bare `peaks-memory:start` without the YAML fields is parsed but produces no `plannedWrites` — the CLI silently returns `extractedCount: 0`. Full format reference: see any successful extract output (e.g. `.peaks/_runtime/2026-07-08-session-17918f/txt/handoff-003-add-zcode-adapter.md`).
|
|
256
|
+
|
|
236
257
|
**11d — Gate C (zero-write outcome):** If `extractedCount === 0` after 11c, fire AskUserQuestion:
|
|
237
258
|
|
|
238
259
|
> "本次 code 未沉淀任何 `.peaks/memory` 文件。可选: (a) 回去在 handoff.md 嵌入至少 1 个 `peaks-memory:start` block 后重试; (b) 显式接受 no-sediment 并记录为 lesson; (c) 取消完成。"
|
|
239
260
|
|
|
240
|
-
Default option = (a). Code MUST NOT silently accept (b) without user pick. Full bash + flow at `references/runbook.md` §Step 11.
|
|
261
|
+
> **D-010 fix root cause check:** When firing 11d, first inspect whether the block has the YAML frontmatter (`title:` + `kind:` + `---`). If the `<!-- peaks-memory:start -->` exists but no `title:` line follows, fix the block format and re-run 11c — don't ask the user yet. Default option = (a). Code MUST NOT silently accept (b) without user pick. Full bash + flow at `references/runbook.md` §Step 11.
|
|
241
262
|
|
|
242
263
|
**Why this step exists:** audit 2026-07-03 confirmed 2 consecutive sessions produced zero `.peaks/memory/` files despite completing RD + QA + handoff artifacts; `assisted` mode silently skipped runbook Step 10 (no STOP condition). **Why `peaks memory extract` (not `peaks project memories:extract`):** the artifact-scoped extract is canonical; the batch-scoped sibling is for non-handoff flows. Always use `peaks memory extract --apply`.
|
|
243
264
|
|
|
@@ -108,4 +108,64 @@ This skill does NOT modify source code. It documents the v2 envelope produced by
|
|
|
108
108
|
|---|---|
|
|
109
109
|
| `references/v2-schema.md` | Field-by-field table for `DecompositionResultV2`, `PassResult`, `SliceV2`, `CrossPassEdge.arbitratedBy`, `LlmArbitration`. |
|
|
110
110
|
| `references/granularity-decision.md` | Decision tree for the 4 `--granularity` modes; `shouldSubdivide()` thresholds + tie-break semantics. |
|
|
111
|
-
| `references/cross-pass-edge-interpretation.md` | The 4 edge kinds and how `peaks-rd` should use them for dispatch ordering; `LlmArbitration` trace reader. |
|
|
111
|
+
| `references/cross-pass-edge-interpretation.md` | The 4 edge kinds and how `peaks-rd` should use them for dispatch ordering; `LlmArbitration` trace reader. |
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
## SC template hard rules (sediment 2026-07-09)
|
|
116
|
+
|
|
117
|
+
These rules are added in response to **D-009a + D-009c** discovered while running RID 003-add-zcode-adapter. They prevent the same class of mistake from re-occurring in future SC drafts.
|
|
118
|
+
|
|
119
|
+
### Rule 1 — Pre-flight: detect ESM vs CJS before writing export syntax (D-009c)
|
|
120
|
+
|
|
121
|
+
Before specifying `--caller-id` / export guard syntax in any SC task, run:
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
head -3 package.json | grep '"type"'
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
- `"type": "module"` → ESM. Use `export const FOO = ...` for any constant that tests need to import. **Do NOT** write `if (typeof module !== 'undefined' && module.exports) { module.exports = { FOO }; }` — that pattern is CJS-only.
|
|
128
|
+
- No `"type"` field (or `"type": "commonjs"`) → CJS. Use the `module.exports` guard.
|
|
129
|
+
|
|
130
|
+
Why: RID-003 SC §3.5 originally specified the CJS guard for `scripts/install-skills.mjs`. The script is actually ESM (`"type": "module"`). The RD agent had to backtrack and the SC lost 5 minutes of spec clarity.
|
|
131
|
+
|
|
132
|
+
### Rule 2 — Whitelist fixture sync is NOT collateral (D-009a)
|
|
133
|
+
|
|
134
|
+
When a slice adds a new entry to a **whitelist fixture** (e.g. `tests/unit/ide/ide-registry.test.ts` asserting `expect(adapters).toHaveLength(7)` after adding the 8th adapter), that fixture sync is a **primary task**, not a "by the way" footnote.
|
|
135
|
+
|
|
136
|
+
Mandatory structure for any slice that adds to a known set:
|
|
137
|
+
|
|
138
|
+
```markdown
|
|
139
|
+
### Tasks
|
|
140
|
+
|
|
141
|
+
| Task | File | Change |
|
|
142
|
+
|---|---|---|
|
|
143
|
+
| T-1 | `src/services/ide/ide-types.ts` | Add `'zcode'` to IdeId |
|
|
144
|
+
| T-2 | `src/services/ide/ide-registry.ts` | Register ZCODE_ADAPTER |
|
|
145
|
+
| T-3 | `tests/unit/ide/ide-registry.test.ts` | Update whitelist: 7 → 8 adapters, add 'zcode' |
|
|
146
|
+
| T-4 | `tests/unit/install-skills-script.test.ts` | Update whitelist: 5 → 6 platforms, add 'zcode' |
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
If you find a whitelist fixture during execution but didn't pre-list it as a task, treat it as a **NEW TASK** and run the SC ↔ QA repair loop, not as "necessary collateral". Pre-listing prevents surprises at Gate 1.
|
|
150
|
+
|
|
151
|
+
### Rule 3 — Adapter interface optional fields (D-009b reciprocal)
|
|
152
|
+
|
|
153
|
+
When extending `IdeAdapter` (or any plugin-interface in `src/services/**/ide/`), prefer **optional fields** over required fields so existing implementations don't need stub updates:
|
|
154
|
+
|
|
155
|
+
```ts
|
|
156
|
+
// ✅ Optional — back-compat safe
|
|
157
|
+
interface IdeAdapter {
|
|
158
|
+
readonly detectCurrentModel?: () => Promise<string | undefined>;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// ❌ Required — every existing adapter must add a stub
|
|
162
|
+
interface IdeAdapter {
|
|
163
|
+
readonly detectCurrentModel: () => Promise<string | undefined>;
|
|
164
|
+
}
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
If a field MUST be required, document in the SC why each existing implementation must be updated inline, and pre-list those updates as primary tasks (Rule 2 pattern).
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
**Sediment source:** `.peaks/_runtime/2026-07-08-session-17918f/qa/003-add-zcode-adapter/{slice-A-completion,slice-B-completion}.md` D-009a/b/c sections. Master lesson: `.peaks/memory/peaks-code-runbook-4-0-0-beta-6-skill-md-cli-d-001-d-002-d-003-d-010.md`.
|