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 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
+ }
@@ -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
- 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;
@@ -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 (`STRONGEST_MODEL_ID`). It does
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
@@ -10,7 +10,7 @@ export type MiniMaxWorkerRequest = {
10
10
  export type MiniMaxWorkerResult = {
11
11
  provider: MiniMaxProviderSmokeResult;
12
12
  reviewHandoff: {
13
- model: 'claude-opus-4-7';
13
+ model: string;
14
14
  prompt: string;
15
15
  };
16
16
  constraints: {
@@ -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, STRONGEST_MODEL_ID } from '../config/model-routing.js';
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 : STRONGEST_MODEL_ID;
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: 'claude-opus-4-7';
41
+ readonly modelId: string;
42
42
  readonly uses: readonly WorkflowStepStage[];
43
43
  };
44
44
  readonly executionModel: {
@@ -1,4 +1,4 @@
1
- import { getConfiguredExecutionModelId, STRONGEST_MODEL_ID } from '../config/model-routing.js';
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 : STRONGEST_MODEL_ID,
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 === STRONGEST_MODEL_ID ? 'top-tier' : 'mid-tier', reason: `Coding and routine refactoring must use the configured execution worker model ${executionModelId}.`, dependsOn: ['code-rd-planning'] }, executionModelId),
89
- step({ id: 'code-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId === STRONGEST_MODEL_ID ? '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),
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 === STRONGEST_MODEL_ID ? 'top-tier' : 'mid-tier', reason: `Bounded coding tasks must use the configured execution worker model ${executionModelId}.`, dependsOn: ['team-rd-planning'] }, executionModelId),
104
- step({ id: 'team-unit-test-execution', stage: 'unit-test-execution', owner: 'peaks-rd', modelTier: executionModelId === STRONGEST_MODEL_ID ? 'top-tier' : 'mid-tier', reason: `Unit-test tasks must use the configured execution worker model ${executionModelId}.`, dependsOn: ['team-coding-execution'] }, 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: STRONGEST_MODEL_ID,
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 ${STRONGEST_MODEL_ID}, matching planner/reviewer.`;
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
- const executionModelId = economyMode !== false ? getConfiguredExecutionModelId(effectiveProviders) : STRONGEST_MODEL_ID;
187
- const modeStatus = createModeStatus(economyMode, swarmMode, executionModelId, economyMode ? 'config.providers' : 'planner-reviewer-strongest-model');
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.6";
1
+ export declare const CLI_VERSION = "4.0.0-beta.7";
@@ -1 +1 @@
1
- export const CLI_VERSION = "4.0.0-beta.6";
1
+ export const CLI_VERSION = "4.0.0-beta.7";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "peaks-loop",
3
- "version": "4.0.0-beta.6",
3
+ "version": "4.0.0-beta.7",
4
4
  "description": "Loop Engineering CLI — workflow primitive / loop guards / evaluators / slice orchestration",
5
5
  "author": "SquabbyZ",
6
6
  "license": "MIT",
@@ -172,21 +172,15 @@ function readPackageVersion(packageRoot = resolvePackageRoot()) {
172
172
  }
173
173
 
174
174
  function createConfigDefaults(packageRoot) {
175
- return {
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
- providers: {
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 is a **recorder + gate** for job-shape (the LLM judges). LLM calls `peaks code detect-job --is-job <bool> --rationale <text> --suggested-job-id <jid> --suggested-strategy <single|rotating> --confidence <high|medium|low>`, which writes `.peaks/_runtime/<sessionId>/job-shape.json`. Downstream steps call `read-job-shape` and refuse if missing.
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
- **Hard rule (v3.1.1 red-line #10):** LLM MUST NOT skip `peaks code detect-job`. If `read-job-shape` throws `JOB_SHAPE_NOT_DECIDED`, record a decision before proceeding.
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` with the active sid. Skip if a title is already set. → `references/skill-presence-and-title.md` (same file as Step 2).
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`.