/** * peaks 自有 hook 协议 + slim IDE adapter 接口。 * * peaks-loop 不再适配 IDE 私有的 hook 协议;反之 peaks 定义自己的 canonical * schema,每个 IDE 只需要填 4 字符串 + 1 settings 函数,新 IDE 适配变成"填表"。 * * 不可消除的 per-IDE 字段(诚实交代,见 PRD R-1..R-4): * - settings.json 物理位置 * - 项目根 env 变量名 * - hook 事件名 + matcher 名 * * 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。 */ import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js'; import type { ContextPercentFallbackInput, IdeCompactProfile } from './ide-compact-types.js'; import type { IdeMcpInstallProfile } from './ide-mcp-install-types.js'; /** * Re-exported so every importer of `./ide-types.js` keeps seeing the whole * adapter type surface from the original path (b1 file-size campaign: the two * declarations now live in `./ide-compact-types.js`). */ export type { ContextPercentFallbackInput, IdeCompactProfile, IdeMcpInstallProfile }; export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma' | 'hermes' | 'openclaw' | 'zcode'; export interface IdeCapabilities { /** peaks gate enforce 是否适用该 IDE(必备) */ readonly gateEnforce: true; /** peaks statusline 状态栏是否适用 */ readonly statusline: boolean; } export interface IdeSettingsLocation { /** 项目根下的 settings 目录名,例如 '.claude' / '.trae' / '.cursor' */ readonly dirName: string; /** settings 文件名(部分 IDE 叫 settings.json / mcp.json) */ readonly settingsFileName: string; /** * 该 IDE 的**机器本地** settings 层文件名(Claude Code = `settings.local.json`), * 与 `settingsFileName` 同目录、通常被 gitignore,IDE 读取时叠加在其上。 * `undefined` = 该 IDE 没有这一层;调用方此时回落到自己的默认值。 * * peaks-loop 只在"写进去的东西是机器相关"时用这一层 —— 例如 per-OS 的 * hook shell shim —— 绝不写进共享的 `settingsFileName`。写 hook 的调用方 * (auto-compact 的 `ide-native` 通路)因此不需要知道任何 IDE 的目录/文件名, * 只读适配器声明的这两个字段。 * * 可选:既有适配器无需改动;声明了 `compactPathway: 'ide-native'` 的适配器 * 应当一并声明此字段,否则会落到调用方的 claude-code 默认路径。 */ readonly localSettingsFileName?: string; /** 解析出 settings.json 绝对路径 */ resolveSettingsFile(scope: 'project' | 'global', projectRoot: string | undefined): string; } /** * Slim IDE adapter 描述。每 IDE 一个静态常量(无需 DI)。 * 字段故意保持少:让 adapter 的添加是"填表"而非"重写"。 */ export interface IdeAdapter { readonly id: IdeId; /** 人类可读名,出现在 CLI help / 命令输出 */ readonly displayName: string; readonly settings: IdeSettingsLocation; /** IDE 注入的项目根 env 变量名;`peaks gate enforce` 等命令模板会引用此 env */ readonly envVar: string; /** settings.json 里 hook 数组的 key,例如 'PreToolUse' / 'beforeToolCall' */ readonly hookEvent: string; /** hook 数组元素的 matcher 字段(工具名匹配),例如 'Bash' / 'Task' / 'terminal' */ readonly toolMatcher: string; /** * Per-IDE sub-agent dispatcher. The `peaks sub-agent dispatch` CLI reads * this field, calls `supportsRole` + `buildToolCall`, and returns the * resulting tool-call descriptor in the JSON envelope. Encapsulates the per-IDE sub-agent dispatch surface (slice #009). The dispatcher's `buildToolCall` returns the IDE-native tool-call descriptor at runtime. * */ readonly subAgentDispatcher: SubAgentDispatcher; /** * Per-IDE opt-in to the G9 prompt-size gate. When `true`, the * `peaks hooks install` command registers the G9 PreToolUse hook * (`peaks sub-agent-dispatch-guard`) for this IDE. When `false`, * the hook is NOT installed (the IDE either doesn't support the * PreToolUse event in a useful form, or the user has opted out). * * The CLI 兜底 layer in `peaks sub-agent dispatch` still enforces * the threshold regardless of this field — `promptSizeAware` only * controls the hook layer (R-15: G9 hook is LLM-platform-specific). * * `peaks hooks install` output byte-stability. */ readonly promptSizeAware: boolean; /** install / uninstall 后展示给用户的提示文本(各 IDE 不同,例如 Claude 提示重启窗口) */ readonly installHints: readonly string[]; /** 该 IDE 在 peaks 上可启用的能力(用于在不支持的 IDE 上软警告) */ readonly capabilities: IdeCapabilities; /** * Where this IDE reads its project-level agent instructions from. * When undefined, the postinstall + `peaks standards init` codepath falls * back to the legacy Claude Code path (CLAUDE.md + .claude/rules/**) * AND emits a stderr warning. Adapters in slice 1.3.2 declare this * value (Claude Code), are annotated UNVERIFIED for future slices * (Trae, slice #012+), or omit it entirely (not-yet-registered IDEs). * */ readonly standardsProfile?: IdeStandardsProfile; /** * Where `scripts/install-skills.mjs` symlinks the bundled skills + * output styles. When undefined, the postinstall falls back to * `~/.claude/skills` + `~/.claude/output-styles` (legacy) AND emits * a stderr warning. Adapters that opt into the dispatch layer fill * this; adapters that don't (Trae in slice 1.3.2) leave it undefined * and follow the legacy path with a warning. * */ readonly skillInstall?: IdeSkillInstall; /** * descriptor. When `undefined`, peaks-loop falls back to the * `llm-self-compress` pathway (the LLM summarizes its own context). * Adapters that fill this expose a 4-stage protocol so peaks-loop * can drive compact autonomously: * * 1. `envVarForContextPercent` — env-var the IDE sets per turn * 2. `compactCommand` — slash command or shell-call the IDE * accepts to trigger compact (e.g. `/compact`). Dispatched * 3. `compactPathway` — `'shell-exec' | 'ide-native' | * 'llm-self-compress' | 'noop'`. `shell-exec` means peaks-loop * spawns `compactCommand` via `child_process.spawn` (zero IDE * hook required). `ide-native` means the IDE exposes a hook * surface peaks-loop can write to. `llm-self-compress` is the * fallback when no compact capability is registered. * 4. `postCompactDetectCommand` (optional) — command the LLM * runner can invoke after compact to confirm ratio dropped. * * Claude Code is the MVP implementation; trae / codex / cursor / * qoder / tongyi-lingma / hermes / openclaw ship with * `compactPathway: 'llm-self-compress'` until L2-dogfood verifies * each IDE's actual compact surface. * * `.peaks/memory/2026-06-27-auto-compact-design.md`. */ readonly compact?: IdeCompactProfile; /** * Optional runtime probe for the IDE's currently-active model id. * When defined, `detectCurrentIdeModel()` in * `src/services/ide/current-model-detector.ts` calls this to * discover which model the IDE UI is currently configured for — * e.g. so `peaks ide model --current` and the future * `getStrongestModelIdAsync()` can use the same value without * forcing the user to set `config.model` manually. * * Returning `undefined` (or the method being absent on this * adapter) means "I don't know — fall back to the configured * model / env var / back-compat default". * * Implementations may read local files (`fs.readFileSync`), * env vars, or both. They MUST NOT throw on missing files — catch * locally and return `undefined`. The orchestration layer also * catches, but defense in depth. * * `zcode` adapter opts in this slice. Future slices can opt in * other IDEs without changing this signature (optional method). */ readonly detectCurrentModel?: () => Promise; /** * Slice 4.0.8 presence-lease-graph (RD §5): per-IDE caller-id resolution. * * Returns the calling window's `callerId` derived from the IDE's own * session signal (e.g. `CLAUDE_CODE_SESSION_ID` for Claude Code). The * adapter owns the priority rules (vendor-neutral override first, then * the IDE-declared variable). Empty / missing / unsupported values * resolve to a thrown `PEAKS_CALLER_NOT_RESOLVED` error so the CLI * boundary can surface a typed envelope. * * Core presence / graph services MUST accept `callerId` as an argument * and never inspect vendor env vars. Vendor neutrality is preserved by * funnelling all vendor signal through this method. */ readonly resolveCallerId: (env?: NodeJS.ProcessEnv) => string; /** * How this harness SPELLS the MCP tool names of a peaks server. * * Optional, like `skillInstall?` / `standardsProfile?` / `compact?`: an adapter * that leaves it undefined declares "this harness has no MCP tool surface I can * name", and every consumer treats that as "nothing to match" rather than as an * error. * * It exists because one tool has SEVERAL spellings. A server registered * directly and the same server arriving through a plugin produce different * tool names, and an enforcement matcher written against one of them does not * fire on the other — silently. Declaring them here is what lets the * pre-tool branch and the installed hook entry agree on the full set. * * `resolveCallerId` above is the precedent for the placement rule: a vendor's * naming convention is vendor signal, and vendor signal is funnelled through * the adapter so no other module has to know it. */ readonly mcp?: IdeMcpProfile; /** * How this harness REGISTERS an MCP server (spec §8.3), consumed by * `peaks mcp install/uninstall`. Optional, like `mcp` above: leaving it * undefined declares "no registration entry peaks may call", and the install * reads that as "nothing to do". See `ide-mcp-install-types.ts`. */ readonly mcpInstall?: IdeMcpInstallProfile; } /** * The MCP naming profile of one harness. * * Deliberately ONE field. Templates both MATCH a concrete tool name and * CONSTRUCT one, which is what lets a conformance test build a call for a * channel it has never seen (spec §6.3) instead of restating the harness's * naming in the test — a restatement that would go stale the moment a channel * is added. */ export interface IdeMcpProfile { /** * Tool-name templates. `*` is replaced by the bare MCP tool id, so * `mcp__peaks__*` describes every tool of a server registered under the name * `peaks` on the direct channel. */ readonly toolNameTemplates: readonly string[]; } /** * Per-IDE standards-file location + format profile. Used by the * project-level standards files at the IDE-specific path, not the * Claude Code hardcoded one. Adapters that omit this field trigger * the legacy Claude Code path with a stderr warning. */ export interface IdeStandardsProfile { /** Filename for the project-root constitution (e.g. 'CLAUDE.md'), or null if the IDE has no equivalent. */ readonly rootFile: string | null; /** Directory for module-level rules (e.g. '.claude/rules'), or null if the IDE has no equivalent. */ readonly rulesDir: string | null; /** Glob under rulesDir to enumerate rule files. */ readonly rulesFileGlob: string; /** True if the IDE auto-loads these files at session start. */ readonly autoLoaded: boolean; /** Output format. markdown = plain text; markdown+frontmatter = adds YAML frontmatter to each rule file. */ readonly format: 'markdown' | 'markdown+frontmatter'; /** Human-readable hint surfaced in the fallback warning. */ readonly migrationHint?: string; } /** * Per-IDE postinstall target roots. The `scripts/install-skills.mjs` * script consumes this to symlink the bundled skills + output styles * to the IDE-specific install location, with back-compat for the * legacy `PEAKS_CLAUDE_SKILLS_DIR` / `PEAKS_CLAUDE_OUTPUT_STYLES_DIR` * env vars (precedence: explicit option > env var > IDE profile > legacy default). */ export interface IdeSkillInstall { /** Absolute path under which the postinstall script symlinks the bundled `skills/` directory. */ readonly skillsDir: string; /** Absolute path under which the postinstall script writes the bundled `output-styles/`. Null if the IDE has no equivalent. */ readonly outputStylesDir: string | null; /** Symlink strategy. */ readonly installStrategy: 'symlink' | 'copy'; /** Back-compat env var name (e.g. PEAKS_CLAUDE_SKILLS_DIR). Null if no env var is supported. */ readonly envVarOverride: string | null; } /** peaks canonical hook schema 版本标识 */ export declare const PEAKS_HOOK_SCHEMA: "peaks-hook/v1"; /** peaks canonical hook 形态 —— 单一协议,所有 IDE 经 hook-translator 归一化到此 */ export interface PeaksCanonicalHook { readonly schema: typeof PEAKS_HOOK_SCHEMA; readonly event: 'pre-tool-use' | 'post-tool-use' | 'sub-agent-start'; readonly toolName: string; readonly toolInput: Record; /** 解析自 env 变量或 --project 的项目根 */ readonly projectRoot: string; /** 选输出格式 */ readonly rawIdeFormat: IdeId; /** 原始 stdin,留作回退 */ readonly rawPayload: unknown; } /** peaks 决策发回形态枚举(按 IDE 期望的"发回"形式) */ export type PeaksDecisionTransport = { kind: 'stdout-json'; denyShape: Record; } | { kind: 'exit-code'; denyCode: number; } | { kind: 'both'; denyShape: Record; denyCode: number; };