peaks-loop 4.0.0-beta.5 → 4.0.0-beta.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +62 -0
- package/dist/src/cli/commands/adapter-commands-s2a.d.ts +34 -0
- package/dist/src/cli/commands/adapter-commands-s2a.js +92 -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/commands/polyrepo-commands.d.ts +37 -0
- package/dist/src/cli/commands/polyrepo-commands.js +155 -0
- package/dist/src/cli/commands/runtime-commands.d.ts +52 -0
- package/dist/src/cli/commands/runtime-commands.js +150 -0
- package/dist/src/cli/program.js +23 -0
- package/dist/src/services/adapter/adapter-registry.d.ts +48 -0
- package/dist/src/services/adapter/adapter-registry.js +186 -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/polyrepo/polyrepo-dispatcher.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-dispatcher.js +119 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.d.ts +9 -0
- package/dist/src/services/polyrepo/polyrepo-scanner.js +94 -0
- package/dist/src/services/polyrepo/polyrepo-service.d.ts +34 -0
- package/dist/src/services/polyrepo/polyrepo-service.js +49 -0
- package/dist/src/services/polyrepo/polyrepo-types.d.ts +55 -0
- package/dist/src/services/polyrepo/polyrepo-types.js +1 -0
- 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/runtime/runtime-detector.d.ts +15 -0
- package/dist/src/services/runtime/runtime-detector.js +62 -0
- package/dist/src/services/runtime/runtime-service.d.ts +59 -0
- package/dist/src/services/runtime/runtime-service.js +51 -0
- package/dist/src/services/runtime/vendor-adapter.d.ts +59 -0
- package/dist/src/services/runtime/vendor-adapter.js +1 -0
- package/dist/src/services/runtime/vendors/claude-code.d.ts +7 -0
- package/dist/src/services/runtime/vendors/claude-code.js +39 -0
- package/dist/src/services/runtime/vendors/codex.d.ts +7 -0
- package/dist/src/services/runtime/vendors/codex.js +43 -0
- package/dist/src/services/runtime/vendors/copilot.d.ts +7 -0
- package/dist/src/services/runtime/vendors/copilot.js +40 -0
- 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 +26 -12
- package/skills/peaks-code/references/external-references.md +1 -3
- package/skills/peaks-code/references/external-skill-invocation.md +1 -1
- package/skills/peaks-code/references/runbook.md +1 -3
- package/skills/peaks-code/references/step-0-55-1x-detection.md +2 -1
- package/skills/peaks-code/references/workflow-gates-and-types.md +1 -1
- package/skills/peaks-slice-decompose/SKILL.md +61 -1
- package/skills/peaks-code/references/openspec-workflow.md +0 -81
|
@@ -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`
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PolyrepoDispatch, PolyrepoManifest } from './polyrepo-types.js';
|
|
2
|
+
export interface DispatchOptions {
|
|
3
|
+
/** Manifest returned by scanPolyrepo (root + children list). */
|
|
4
|
+
readonly manifest: PolyrepoManifest;
|
|
5
|
+
/** Session id this dispatch belongs to. */
|
|
6
|
+
readonly sid: string;
|
|
7
|
+
/** Request id (used in the on-disk filename). */
|
|
8
|
+
readonly rid: string;
|
|
9
|
+
/** Target child ids. Empty = root-only dispatch. Unknown ids are
|
|
10
|
+
* recorded as warnings in the result, NOT raised as errors. */
|
|
11
|
+
readonly targets: readonly string[];
|
|
12
|
+
/** Artifact body — the role + source path on the parent side. */
|
|
13
|
+
readonly artifact: PolyrepoDispatch['artifact'];
|
|
14
|
+
}
|
|
15
|
+
export interface ChildDispatchResult {
|
|
16
|
+
readonly childId: string;
|
|
17
|
+
readonly path: string;
|
|
18
|
+
readonly ok: boolean;
|
|
19
|
+
readonly error?: string;
|
|
20
|
+
/** Where the artifact was mirrored to (child-side path). */
|
|
21
|
+
readonly mirroredTo?: string;
|
|
22
|
+
}
|
|
23
|
+
export interface DispatchResult {
|
|
24
|
+
readonly dispatch: PolyrepoDispatch;
|
|
25
|
+
readonly perChild: ChildDispatchResult[];
|
|
26
|
+
readonly warnings: string[];
|
|
27
|
+
}
|
|
28
|
+
export declare function dispatchArtifact(opts: DispatchOptions): DispatchResult;
|
|
29
|
+
/** Default location of the polyrepo manifest on disk. */
|
|
30
|
+
export declare function manifestPath(root: string): string;
|
|
31
|
+
/** Read a persisted manifest from disk. Returns null when missing. */
|
|
32
|
+
export declare function readManifest(root: string): PolyrepoManifest | null;
|
|
33
|
+
/** Write a manifest atomically. */
|
|
34
|
+
export declare function writeManifest(root: string, manifest: PolyrepoManifest): void;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyrepo dispatcher — slice S2-b of RD-2.
|
|
3
|
+
*
|
|
4
|
+
* Takes a parent root + a list of target child ids + a PRD/RD/QA
|
|
5
|
+
* artifact, and mirrors the artifact into each target child's
|
|
6
|
+
* `.peaks/_runtime/<sid>/<role>/` directory. Mirroring is
|
|
7
|
+
* best-effort + atomic per child: a failure in one child does NOT
|
|
8
|
+
* block the others (the dispatcher collects per-child results and
|
|
9
|
+
* returns them all so the caller can surface partial successes).
|
|
10
|
+
*
|
|
11
|
+
* Design notes:
|
|
12
|
+
* - Per the project's two-axis convention, the parent's reviewable
|
|
13
|
+
* artifact lives under `.peaks/_runtime/<sid>/<role>/`, not as a
|
|
14
|
+
* sibling of `.peaks/_runtime/`. We mirror the same shape into
|
|
15
|
+
* each child (the child does NOT inherit the parent's session
|
|
16
|
+
* id verbatim — it gets the same one, which is fine because the
|
|
17
|
+
* sid is the project's binding, not the parent's transient
|
|
18
|
+
* pid).
|
|
19
|
+
* - The dispatcher does NOT spawn any vendor verb — vendor
|
|
20
|
+
* neutrality is preserved. If a child has no peaks-loop binary
|
|
21
|
+
* on PATH, the dispatcher writes the artifact to disk and notes
|
|
22
|
+
* a `peaksInstalled: false` warning; the user / a future slice
|
|
23
|
+
* can pick it up. Per PRD §5 mitigation for "child has no
|
|
24
|
+
* peaks-loop installed".
|
|
25
|
+
*/
|
|
26
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
27
|
+
import { dirname, join } from 'node:path';
|
|
28
|
+
/** Compute the child-side path where the artifact should be mirrored.
|
|
29
|
+
* Mirrors the parent shape exactly: `.peaks/_runtime/<sid>/<role>/`. */
|
|
30
|
+
function childArtifactPath(childRoot, sid, role, sourcePath) {
|
|
31
|
+
const filename = sourcePath.split(/[\\/]/).pop() ?? 'artifact.md';
|
|
32
|
+
return join(childRoot, '.peaks', '_runtime', sid, role, filename);
|
|
33
|
+
}
|
|
34
|
+
function ensureDirFor(file) {
|
|
35
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
36
|
+
}
|
|
37
|
+
export function dispatchArtifact(opts) {
|
|
38
|
+
const warnings = [];
|
|
39
|
+
const perChild = [];
|
|
40
|
+
// Validate targets against the manifest.
|
|
41
|
+
const knownIds = new Set(opts.manifest.children.map((c) => c.id));
|
|
42
|
+
const validTargets = [];
|
|
43
|
+
for (const t of opts.targets) {
|
|
44
|
+
if (knownIds.has(t)) {
|
|
45
|
+
validTargets.push(t);
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
warnings.push(`unknown target id "${t}" — ignored`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
// Read the source artifact body once (so we can mirror to N children).
|
|
52
|
+
let sourceBody;
|
|
53
|
+
try {
|
|
54
|
+
sourceBody = readFileSync(opts.artifact.path, 'utf8');
|
|
55
|
+
}
|
|
56
|
+
catch (err) {
|
|
57
|
+
throw new Error(`source artifact unreadable at ${opts.artifact.path}: ${err.message}`);
|
|
58
|
+
}
|
|
59
|
+
// Per-child write.
|
|
60
|
+
for (const targetId of validTargets) {
|
|
61
|
+
const child = opts.manifest.children.find((c) => c.id === targetId);
|
|
62
|
+
if (child === undefined)
|
|
63
|
+
continue; // already filtered above
|
|
64
|
+
const dest = childArtifactPath(child.path, opts.sid, opts.artifact.role, opts.artifact.path);
|
|
65
|
+
try {
|
|
66
|
+
ensureDirFor(dest);
|
|
67
|
+
writeFileSync(dest, sourceBody, 'utf8');
|
|
68
|
+
perChild.push({
|
|
69
|
+
childId: targetId,
|
|
70
|
+
path: child.path,
|
|
71
|
+
ok: true,
|
|
72
|
+
mirroredTo: dest
|
|
73
|
+
});
|
|
74
|
+
if (!child.peaksInstalled) {
|
|
75
|
+
warnings.push(`child "${targetId}" has no peaks-loop install — artifact mirrored but not picked up by a running peaks process`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch (err) {
|
|
79
|
+
perChild.push({
|
|
80
|
+
childId: targetId,
|
|
81
|
+
path: child.path,
|
|
82
|
+
ok: false,
|
|
83
|
+
error: err.message
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
const dispatch = {
|
|
88
|
+
rid: opts.rid,
|
|
89
|
+
sid: opts.sid,
|
|
90
|
+
root: opts.manifest.root,
|
|
91
|
+
targets: validTargets,
|
|
92
|
+
dispatchedAt: new Date().toISOString(),
|
|
93
|
+
artifact: opts.artifact
|
|
94
|
+
};
|
|
95
|
+
return { dispatch, perChild, warnings };
|
|
96
|
+
}
|
|
97
|
+
/** Default location of the polyrepo manifest on disk. */
|
|
98
|
+
export function manifestPath(root) {
|
|
99
|
+
return join(root, '.peaks', 'polyrepo.json');
|
|
100
|
+
}
|
|
101
|
+
/** Read a persisted manifest from disk. Returns null when missing. */
|
|
102
|
+
export function readManifest(root) {
|
|
103
|
+
const p = manifestPath(root);
|
|
104
|
+
if (!existsSync(p))
|
|
105
|
+
return null;
|
|
106
|
+
try {
|
|
107
|
+
const raw = readFileSync(p, 'utf8');
|
|
108
|
+
return JSON.parse(raw);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
/** Write a manifest atomically. */
|
|
115
|
+
export function writeManifest(root, manifest) {
|
|
116
|
+
const p = manifestPath(root);
|
|
117
|
+
ensureDirFor(p);
|
|
118
|
+
writeFileSync(p, JSON.stringify(manifest, null, 2), 'utf8');
|
|
119
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { PolyrepoManifest } from './polyrepo-types.js';
|
|
2
|
+
export interface ScanOptions {
|
|
3
|
+
/** Explicit list of child directory names (relative to root). When
|
|
4
|
+
* provided, the scanner uses these instead of auto-discovery.
|
|
5
|
+
* Matches the `peaks polyrepo init --children <dir1,dir2>` shape. */
|
|
6
|
+
readonly explicitChildren?: readonly string[];
|
|
7
|
+
}
|
|
8
|
+
/** Walk the parent root at depth 1 and return discovered children. */
|
|
9
|
+
export declare function scanPolyrepo(root: string, opts?: ScanOptions): PolyrepoManifest;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Polyrepo scanner — slice S2-b of RD-2.
|
|
3
|
+
*
|
|
4
|
+
* Walks a parent directory looking for child git repos. A "child
|
|
5
|
+
* git repo" is defined as: a directory that contains a `.git`
|
|
6
|
+
* entry (file or directory — worktrees + submodules both qualify),
|
|
7
|
+
* is NOT the root itself, and sits at depth 1 from the root.
|
|
8
|
+
*
|
|
9
|
+
* Depth is intentionally bounded to 1 (per the PRD §4.3 model):
|
|
10
|
+
* parent/
|
|
11
|
+
* .peaks/
|
|
12
|
+
* frontend/.git
|
|
13
|
+
* backend/.git
|
|
14
|
+
* We do not recurse into nested monorepos in this slice — that is
|
|
15
|
+
* a future-slice concern.
|
|
16
|
+
*/
|
|
17
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
18
|
+
import { join, basename, resolve } from 'node:path';
|
|
19
|
+
function isGitRoot(dir) {
|
|
20
|
+
// A `.git` directory (normal repo) OR a `.git` file (worktree +
|
|
21
|
+
// submodule pointer) both count. statSync follows the pointer for
|
|
22
|
+
// a `.git` file, so the existence check is the same.
|
|
23
|
+
return existsSync(join(dir, '.git'));
|
|
24
|
+
}
|
|
25
|
+
function childHasPeaks(dir) {
|
|
26
|
+
return existsSync(join(dir, '.peaks'));
|
|
27
|
+
}
|
|
28
|
+
/** Sanitize a candidate child id: lowercase, dash-joined, no
|
|
29
|
+
* leading dots or dashes. Used when the dir name itself is the id
|
|
30
|
+
* (the common case). */
|
|
31
|
+
function sanitizeId(name) {
|
|
32
|
+
return name.toLowerCase().replace(/[^a-z0-9._-]+/g, '-').replace(/^[-.]+|[-.]+$/g, '');
|
|
33
|
+
}
|
|
34
|
+
/** Walk the parent root at depth 1 and return discovered children. */
|
|
35
|
+
export function scanPolyrepo(root, opts = {}) {
|
|
36
|
+
const absRoot = resolve(root);
|
|
37
|
+
if (!existsSync(absRoot)) {
|
|
38
|
+
throw new Error(`polyrepo root does not exist: ${absRoot}`);
|
|
39
|
+
}
|
|
40
|
+
const rootStat = statSync(absRoot);
|
|
41
|
+
if (!rootStat.isDirectory()) {
|
|
42
|
+
throw new Error(`polyrepo root is not a directory: ${absRoot}`);
|
|
43
|
+
}
|
|
44
|
+
const children = [];
|
|
45
|
+
if (opts.explicitChildren !== undefined && opts.explicitChildren.length > 0) {
|
|
46
|
+
// Explicit mode: only consider the named subdirs. This matches
|
|
47
|
+
// `peaks polyrepo init --children ./frontend,./backend`.
|
|
48
|
+
for (const name of opts.explicitChildren) {
|
|
49
|
+
const childPath = resolve(absRoot, name);
|
|
50
|
+
if (!existsSync(childPath)) {
|
|
51
|
+
throw new Error(`polyrepo child directory does not exist: ${childPath}`);
|
|
52
|
+
}
|
|
53
|
+
const s = statSync(childPath);
|
|
54
|
+
if (!s.isDirectory()) {
|
|
55
|
+
throw new Error(`polyrepo child path is not a directory: ${childPath}`);
|
|
56
|
+
}
|
|
57
|
+
children.push({
|
|
58
|
+
id: sanitizeId(basename(childPath)),
|
|
59
|
+
path: childPath,
|
|
60
|
+
gitRoot: isGitRoot(childPath),
|
|
61
|
+
peaksScope: childHasPeaks(childPath) ? 'root+child' : 'child-only',
|
|
62
|
+
peaksInstalled: childHasPeaks(childPath)
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
else {
|
|
67
|
+
// Auto-discovery mode: list immediate entries, filter to dirs with
|
|
68
|
+
// `.git`. This is intentionally narrow — see the file-level
|
|
69
|
+
// rationale at the top of this file.
|
|
70
|
+
const entries = readdirSync(absRoot, { withFileTypes: true });
|
|
71
|
+
for (const entry of entries) {
|
|
72
|
+
if (!entry.isDirectory())
|
|
73
|
+
continue;
|
|
74
|
+
if (entry.name.startsWith('.'))
|
|
75
|
+
continue; // skip .git, .peaks, .vscode, etc.
|
|
76
|
+
const childPath = join(absRoot, entry.name);
|
|
77
|
+
if (!isGitRoot(childPath))
|
|
78
|
+
continue;
|
|
79
|
+
children.push({
|
|
80
|
+
id: sanitizeId(entry.name),
|
|
81
|
+
path: childPath,
|
|
82
|
+
gitRoot: true,
|
|
83
|
+
peaksScope: childHasPeaks(childPath) ? 'root+child' : 'child-only',
|
|
84
|
+
peaksInstalled: childHasPeaks(childPath)
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return {
|
|
89
|
+
version: 1,
|
|
90
|
+
root: absRoot,
|
|
91
|
+
detectedAt: new Date().toISOString(),
|
|
92
|
+
children
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { PolyrepoChild, PolyrepoManifest } from './polyrepo-types.js';
|
|
2
|
+
export interface PolyrepoInitOptions {
|
|
3
|
+
readonly root: string;
|
|
4
|
+
readonly children?: readonly string[];
|
|
5
|
+
}
|
|
6
|
+
export interface PolyrepoInitResult {
|
|
7
|
+
readonly manifest: PolyrepoManifest;
|
|
8
|
+
readonly created: boolean;
|
|
9
|
+
}
|
|
10
|
+
export interface PolyrepoStatusResult {
|
|
11
|
+
readonly root: string;
|
|
12
|
+
readonly manifestExists: boolean;
|
|
13
|
+
readonly children: PolyrepoChild[];
|
|
14
|
+
readonly detectedAt: string | null;
|
|
15
|
+
}
|
|
16
|
+
export declare class PolyrepoService {
|
|
17
|
+
/** Run a scan and persist the manifest. Returns the manifest + a
|
|
18
|
+
* `created` flag indicating whether a new manifest was written
|
|
19
|
+
* (false = existing manifest was overwritten). */
|
|
20
|
+
init(opts: PolyrepoInitOptions): PolyrepoInitResult;
|
|
21
|
+
/** Read the persisted manifest + report children. */
|
|
22
|
+
status(root: string): PolyrepoStatusResult;
|
|
23
|
+
/** Dispatch an artifact to the named child targets. Wrapper
|
|
24
|
+
* around dispatchArtifact that resolves the manifest from disk
|
|
25
|
+
* first. Throws when no manifest exists (caller should run init
|
|
26
|
+
* first). */
|
|
27
|
+
dispatch(root: string, params: {
|
|
28
|
+
sid: string;
|
|
29
|
+
rid: string;
|
|
30
|
+
targets: readonly string[];
|
|
31
|
+
role: 'prd' | 'rd' | 'qa';
|
|
32
|
+
artifactPath: string;
|
|
33
|
+
}): import("./polyrepo-dispatcher.js").DispatchResult;
|
|
34
|
+
}
|