peaks-cli 1.3.2 → 1.3.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (95) hide show
  1. package/README.md +6 -2
  2. package/dist/src/cli/commands/gate-commands.js +28 -19
  3. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  4. package/dist/src/cli/commands/hook-handle.js +111 -0
  5. package/dist/src/cli/commands/hooks-commands.js +72 -21
  6. package/dist/src/cli/commands/progress-commands.js +9 -2
  7. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  8. package/dist/src/cli/commands/statusline-commands.js +75 -17
  9. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  10. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  11. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  12. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  13. package/dist/src/cli/commands/workspace-commands.js +3 -0
  14. package/dist/src/cli/program.js +9 -0
  15. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  16. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  17. package/dist/src/services/config/config-types.d.ts +1 -1
  18. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  19. package/dist/src/services/context/artifact-meta.js +105 -0
  20. package/dist/src/services/context/context-guard.d.ts +49 -0
  21. package/dist/src/services/context/context-guard.js +91 -0
  22. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  23. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  24. package/dist/src/services/context/headroom-client.d.ts +34 -0
  25. package/dist/src/services/context/headroom-client.js +117 -0
  26. package/dist/src/services/context/shared-channel.d.ts +92 -0
  27. package/dist/src/services/context/shared-channel.js +285 -0
  28. package/dist/src/services/context/threshold.d.ts +35 -0
  29. package/dist/src/services/context/threshold.js +76 -0
  30. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  31. package/dist/src/services/dispatch/batch-counter.js +85 -0
  32. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  33. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  34. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  35. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  36. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  37. package/dist/src/services/dispatch/leak-detector.js +72 -0
  38. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  39. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  40. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  41. package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
  42. package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
  43. package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
  44. package/dist/src/services/ide/hook-protocol.d.ts +44 -0
  45. package/dist/src/services/ide/hook-protocol.js +71 -0
  46. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  47. package/dist/src/services/ide/hook-translator.js +128 -0
  48. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  49. package/dist/src/services/ide/ide-detector.js +19 -0
  50. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  51. package/dist/src/services/ide/ide-registry.js +45 -0
  52. package/dist/src/services/ide/ide-types.d.ts +120 -0
  53. package/dist/src/services/ide/ide-types.js +2 -0
  54. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  55. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  56. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  57. package/dist/src/services/ide/shared/safe-path.js +29 -0
  58. package/dist/src/services/progress/progress-service.d.ts +1 -1
  59. package/dist/src/services/progress/progress-service.js +18 -14
  60. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  61. package/dist/src/services/security/safe-settings-path.js +104 -0
  62. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  63. package/dist/src/services/signal/cancel-handler.js +76 -0
  64. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  65. package/dist/src/services/skill/resume-detector.js +334 -0
  66. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  67. package/dist/src/services/skill/skill-scheduler.js +53 -0
  68. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  69. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  70. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  71. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  72. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  73. package/dist/src/services/slice/slice-archive-service.js +111 -0
  74. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  75. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  76. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  77. package/dist/src/services/solo/status-line-renderer.js +55 -0
  78. package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
  79. package/dist/src/services/workspace/reconcile-service.js +107 -6
  80. package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
  81. package/dist/src/shared/version.d.ts +1 -1
  82. package/dist/src/shared/version.js +1 -1
  83. package/package.json +2 -1
  84. package/skills/peaks-ide/SKILL.md +159 -0
  85. package/skills/peaks-qa/SKILL.md +57 -1
  86. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  87. package/skills/peaks-rd/SKILL.md +50 -8
  88. package/skills/peaks-solo/SKILL.md +77 -20
  89. package/skills/peaks-solo/references/context-governance.md +144 -0
  90. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  91. package/skills/peaks-solo/references/runbook.md +3 -3
  92. package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
  93. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  94. package/skills/peaks-txt/SKILL.md +17 -0
  95. package/skills/peaks-ui/SKILL.md +27 -1
@@ -0,0 +1,34 @@
1
+ import type { IdeAdapter } from '../ide-types.js';
2
+ /**
3
+ * Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
4
+ *
5
+ * 不可消除的 per-IDE 字段(slice #1 锁定):
6
+ * - settings.dirName = '.trae' : Trae 项目根下的配置目录
7
+ * - settings.settingsFileName = 'settings.json' (UNVERIFIED at slice time: Trae 实际叫什么待 Trae 1.x 文档确认,先按 Claude 风格)
8
+ * - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
9
+ * - hookEvent = 'beforeToolCall' : UNVERIFIED — Trae 的 hook 数组 key(待 Trae 文档确认,先假设与 Cursor 同名)
10
+ * - toolMatcher = 'terminal' : UNVERIFIED — Trae 的 bash 工具 matcher(待 Trae 文档确认)
11
+ *
12
+ * Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
13
+ * 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
14
+ * 简单复制粘贴就接入新 IDE。
15
+ *
16
+ * 与 slice #1 claude-code-adapter.ts 的区别(故意):
17
+ * - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(假设)
18
+ * - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(假设)
19
+ * - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同)
20
+ * - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
21
+ * - installHints 提示用户"重启 Trae"(同 Claude 风格)
22
+ *
23
+ * 等 Trae 真实文档/真实用户的 dogfood 之后,可能需要把 hookEvent /
24
+ * toolMatcher 替换为 Trae 实际值。slice #2 的 tech-doc 里要明确"此 adapter
25
+ * 是基于 1.x 假设,Trae 真实集成需要在 Trae 上 dogfood 验证"。
26
+ *
27
+ * Slice #3 refactor: the `peaks hooks install` command now dispatches on the
28
+ * IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
29
+ * a Trae install is run, the resulting `<root>/.trae/settings.json` will use
30
+ * the `beforeToolCall` event key and the `terminal` matcher from this adapter.
31
+ * Until a real Trae 1.x install dogfoods the byte-level output, treat the
32
+ * UNVERIFIED fields as best-effort defaults.
33
+ */
34
+ export declare const TRAE_ADAPTER: IdeAdapter;
@@ -0,0 +1,70 @@
1
+ import { homedir } from 'node:os';
2
+ import { join, resolve } from 'node:path';
3
+ import { traeSubAgentDispatcher } from '../../dispatch/sub-agent-dispatcher.js';
4
+ /**
5
+ * Trae IDE adapter —— peaks-cli 的第二个内置 IDE 适配器。
6
+ *
7
+ * 不可消除的 per-IDE 字段(slice #1 锁定):
8
+ * - settings.dirName = '.trae' : Trae 项目根下的配置目录
9
+ * - settings.settingsFileName = 'settings.json' (UNVERIFIED at slice time: Trae 实际叫什么待 Trae 1.x 文档确认,先按 Claude 风格)
10
+ * - envVar = 'TRAE_PROJECT_DIR' : Trae 注入的 env 变量(用于 ${...} 占位)
11
+ * - hookEvent = 'beforeToolCall' : UNVERIFIED — Trae 的 hook 数组 key(待 Trae 文档确认,先假设与 Cursor 同名)
12
+ * - toolMatcher = 'terminal' : UNVERIFIED — Trae 的 bash 工具 matcher(待 Trae 文档确认)
13
+ *
14
+ * Slice #1 的 slim `IdeAdapter` shape 在 slice #1 RD 中被锁为"填表"模式。
15
+ * 本文件是 slice #2 第一个真实客户,验证 slice #1 抽出的形状真的可以
16
+ * 简单复制粘贴就接入新 IDE。
17
+ *
18
+ * 与 slice #1 claude-code-adapter.ts 的区别(故意):
19
+ * - Trae 的 hookEvent 名是 `beforeToolCall` 而不是 `PreToolUse`(假设)
20
+ * - Trae 的 toolMatcher 是 `terminal` 而不是 `Bash`(假设)
21
+ * - Trae 的 settings 路径是 `.trae/settings.json`(同 Claude 风格,只是目录名不同)
22
+ * - Trae 的 envVar 是 `TRAE_PROJECT_DIR`
23
+ * - installHints 提示用户"重启 Trae"(同 Claude 风格)
24
+ *
25
+ * 等 Trae 真实文档/真实用户的 dogfood 之后,可能需要把 hookEvent /
26
+ * toolMatcher 替换为 Trae 实际值。slice #2 的 tech-doc 里要明确"此 adapter
27
+ * 是基于 1.x 假设,Trae 真实集成需要在 Trae 上 dogfood 验证"。
28
+ *
29
+ * Slice #3 refactor: the `peaks hooks install` command now dispatches on the
30
+ * IDE adapter (auto-detect from env / cwd, override with `--ide trae`). When
31
+ * a Trae install is run, the resulting `<root>/.trae/settings.json` will use
32
+ * the `beforeToolCall` event key and the `terminal` matcher from this adapter.
33
+ * Until a real Trae 1.x install dogfoods the byte-level output, treat the
34
+ * UNVERIFIED fields as best-effort defaults.
35
+ */
36
+ export const TRAE_ADAPTER = {
37
+ id: 'trae',
38
+ displayName: 'Trae',
39
+ settings: {
40
+ dirName: '.trae',
41
+ settingsFileName: 'settings.json', // UNVERIFIED — see slice #2 closeout code-review M-1
42
+ resolveSettingsFile: (scope, projectRoot) => {
43
+ const root = scope === 'global' ? homedir() : resolve(projectRoot ?? homedir());
44
+ return join(root, '.trae', 'settings.json');
45
+ },
46
+ supportsScope: (scope) => scope === 'project' || scope === 'global'
47
+ },
48
+ envVar: 'TRAE_PROJECT_DIR',
49
+ hookEvent: 'beforeToolCall', // UNVERIFIED — see slice #2 closeout code-review M-1; will be validated when a real Trae 1.x install dogfoods the install path
50
+ toolMatcher: 'terminal', // UNVERIFIED — see slice #2 closeout code-review M-1
51
+ subAgentToolMatcher: 'Task', // UNVERIFIED — Trae's sub-agent tool name is unknown; matches the prior hardcoded 'Task' literal so byte-level install output is unchanged. Will be dogfooded when a real Trae 1.x install dispatches a sub-agent.
52
+ // Slice #009: Trae's sub-agent dispatcher is UNVERIFIED — Trae sub-agent
53
+ // tool name TBD on real dogfood; byte-level identical to claude-code by
54
+ // design so the slice #008 `subAgentToolMatcher: 'Task'` install entry
55
+ // stays byte-stable. Awaiting real Trae 1.x dogfood to confirm/replace.
56
+ subAgentDispatcher: traeSubAgentDispatcher,
57
+ // Slice #010 G9: Trae supports `beforeToolCall` which can wrap
58
+ // `peaks sub-agent-dispatch-guard`. Opt in (matches the byte-stable
59
+ // slice #008 install entry shape).
60
+ promptSizeAware: true,
61
+ installHints: [
62
+ 'Restart Trae (or reload the workspace) so the beforeToolCall hooks take effect.'
63
+ ],
64
+ capabilities: {
65
+ gateEnforce: true,
66
+ progressStart: true,
67
+ statusline: true,
68
+ mcpInstall: false // Trae 的 MCP 集成尚未确定,先关掉避免误导
69
+ }
70
+ };
@@ -0,0 +1,44 @@
1
+ import { PEAKS_HOOK_SCHEMA, type IdeId, type PeaksCanonicalHook, type PeaksDecisionTransport } from './ide-types.js';
2
+ export { PEAKS_HOOK_SCHEMA };
3
+ export type { PeaksCanonicalHook, PeaksDecisionTransport };
4
+ /**
5
+ * Compute the deny decision shape for Claude Code (the only adapter registered
6
+ * in slice #1). The output is a JSON object that, when written to stdout, makes
7
+ * the Claude Code permission system block the tool call BEFORE the user's
8
+ * permission prompt — un-bypassable, even under --dangerously-skip-permissions.
9
+ */
10
+ export declare const CLAUDE_CODE_DENY_SHAPE: Record<string, unknown>;
11
+ export declare const CLAUDE_CODE_DENY_TRANSPORT: PeaksDecisionTransport;
12
+ /**
13
+ * Compute the deny decision shape for Trae (Cursor-style sibling IDE).
14
+ * UNVERIFIED — Trae 1.x's actual response envelope is a 1.x assumption
15
+ * (see src/services/ide/adapters/trae-adapter.ts). Slice #3 ships a
16
+ * Cursor-style envelope as the best-effort default; if a future slice
17
+ * confirms Trae's actual shape, update this constant and the related test.
18
+ */
19
+ export declare const TRAE_DENY_SHAPE: Record<string, unknown>;
20
+ export declare const TRAE_DENY_TRANSPORT: PeaksDecisionTransport;
21
+ /**
22
+ * Format a decision response for a given IDE. Slice #1 handles Claude Code;
23
+ * slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
24
+ * Future slices will add exit-code / both variants for IDEs that don't read
25
+ * stdout.
26
+ */
27
+ export declare function formatDecisionResponse(ide: IdeId, decision: 'allow' | 'deny', reason?: string): {
28
+ stdout: string;
29
+ exitCode: number;
30
+ };
31
+ /**
32
+ * Build a peaks canonical hook from a parsed stdin payload. Caller has already
33
+ * done stdin parsing + IDE auto-detection; this function normalizes to the
34
+ * canonical schema.
35
+ */
36
+ export interface BuildCanonicalHookInput {
37
+ readonly toolName: string;
38
+ readonly toolInput: Record<string, unknown>;
39
+ readonly projectRoot: string;
40
+ readonly rawIdeFormat: IdeId;
41
+ readonly rawPayload: unknown;
42
+ readonly event?: PeaksCanonicalHook['event'];
43
+ }
44
+ export declare function buildCanonicalHook(input: BuildCanonicalHookInput): PeaksCanonicalHook;
@@ -0,0 +1,71 @@
1
+ import { PEAKS_HOOK_SCHEMA } from './ide-types.js';
2
+ export { PEAKS_HOOK_SCHEMA };
3
+ /**
4
+ * Compute the deny decision shape for Claude Code (the only adapter registered
5
+ * in slice #1). The output is a JSON object that, when written to stdout, makes
6
+ * the Claude Code permission system block the tool call BEFORE the user's
7
+ * permission prompt — un-bypassable, even under --dangerously-skip-permissions.
8
+ */
9
+ export const CLAUDE_CODE_DENY_SHAPE = {
10
+ hookSpecificOutput: {
11
+ hookEventName: 'PreToolUse',
12
+ permissionDecision: 'deny',
13
+ permissionDecisionReason: '__REASON__' // replaced at format time
14
+ }
15
+ };
16
+ export const CLAUDE_CODE_DENY_TRANSPORT = {
17
+ kind: 'stdout-json',
18
+ denyShape: CLAUDE_CODE_DENY_SHAPE
19
+ };
20
+ /**
21
+ * Compute the deny decision shape for Trae (Cursor-style sibling IDE).
22
+ * UNVERIFIED — Trae 1.x's actual response envelope is a 1.x assumption
23
+ * (see src/services/ide/adapters/trae-adapter.ts). Slice #3 ships a
24
+ * Cursor-style envelope as the best-effort default; if a future slice
25
+ * confirms Trae's actual shape, update this constant and the related test.
26
+ */
27
+ export const TRAE_DENY_SHAPE = {
28
+ hookSpecificOutput: {
29
+ hookEventName: 'beforeToolCall',
30
+ permissionDecision: 'deny',
31
+ permissionDecisionReason: '__REASON__' // replaced at format time
32
+ }
33
+ };
34
+ export const TRAE_DENY_TRANSPORT = {
35
+ kind: 'stdout-json',
36
+ denyShape: TRAE_DENY_SHAPE
37
+ };
38
+ /**
39
+ * Format a decision response for a given IDE. Slice #1 handles Claude Code;
40
+ * slice #3 added Trae (1.x-assumption shape — see TRAE_DENY_SHAPE doc).
41
+ * Future slices will add exit-code / both variants for IDEs that don't read
42
+ * stdout.
43
+ */
44
+ export function formatDecisionResponse(ide, decision, reason) {
45
+ if (decision === 'allow') {
46
+ return { stdout: '', exitCode: 0 };
47
+ }
48
+ let shape;
49
+ if (ide === 'claude-code') {
50
+ shape = CLAUDE_CODE_DENY_SHAPE;
51
+ }
52
+ else if (ide === 'trae') {
53
+ shape = TRAE_DENY_SHAPE;
54
+ }
55
+ else {
56
+ throw new Error(`formatDecisionResponse: unsupported IDE ${ide} (not registered in adapter registry; future slice will add support)`);
57
+ }
58
+ const filled = JSON.stringify(shape).replace('"__REASON__"', JSON.stringify(reason ?? 'denied'));
59
+ return { stdout: filled, exitCode: 0 };
60
+ }
61
+ export function buildCanonicalHook(input) {
62
+ return {
63
+ schema: PEAKS_HOOK_SCHEMA,
64
+ event: input.event ?? 'pre-tool-use',
65
+ toolName: input.toolName,
66
+ toolInput: input.toolInput,
67
+ projectRoot: input.projectRoot,
68
+ rawIdeFormat: input.rawIdeFormat,
69
+ rawPayload: input.rawPayload
70
+ };
71
+ }
@@ -0,0 +1,72 @@
1
+ import type { IdeId } from './ide-types.js';
2
+ /**
3
+ * hook-translator —— peaks 自有 hook 协议的核心。
4
+ *
5
+ * 单一职责:把 IDE 私有 stdin 形态归一化到 peaks canonical schema;把 peaks 决策
6
+ * 格式化回 IDE 期望的 stdout/exit-code 形态。
7
+ *
8
+ * auto-detection 算法(优先级从高到低):
9
+ * 1. env 变量:CLAUDE_PROJECT_DIR / TRAE_PROJECT_DIR / CODEX_PROJECT_DIR / ...
10
+ * 2. stdin shape:`{ tool_name, tool_input }` 是 Claude / Trae;
11
+ * `{ toolName, toolInput }` 是 Cursor;
12
+ * `{ eventName, parameters }` 是 Trae 另形态
13
+ * 3. cwd 启发式:存在 .claude / .trae / .codex / .cursor 目录
14
+ * 4. fallback:`claude-code`(backward compat,见 PRD preserved behavior #10)
15
+ */
16
+ export interface DetectFromStdinInput {
17
+ readonly env: NodeJS.ProcessEnv;
18
+ readonly cwd: string;
19
+ /** 已解析的 stdin 形态;null = 空 stdin 或非 JSON */
20
+ readonly parsedStdin: unknown;
21
+ }
22
+ /**
23
+ * Detect the originating IDE from env / stdin shape / cwd heuristics.
24
+ * Falls back to 'claude-code' (PRD preserved behavior: backward compat).
25
+ */
26
+ export declare function detectIdeFromContext(input: DetectFromStdinInput): IdeId;
27
+ /**
28
+ * Pluck a string value at a nested path. Returns undefined if any segment is
29
+ * missing or non-object. Used by adapter-driven stdin parsers.
30
+ */
31
+ export declare function pluckString(obj: unknown, path: readonly string[]): string | undefined;
32
+ export declare function pluckObject(obj: unknown, path: readonly string[]): Record<string, unknown> | undefined;
33
+ /**
34
+ * Default stdin parser for adapters that follow the Claude Code shape
35
+ * (most LLM-style IDEs do: tool_name at top, tool_input.{command} inside).
36
+ */
37
+ export declare function parseClaudeShapeStdin(parsed: unknown): {
38
+ toolName?: string;
39
+ command?: string;
40
+ };
41
+ /**
42
+ * Trae stdin parser. UNVERIFIED — Trae 1.x's actual stdin envelope shape is
43
+ * assumed to be Cursor-style (eventName at top, parameters nested), based on
44
+ * slice #2's read of Trae as a "Cursor sibling". When a real Trae 1.x hook
45
+ * payload is dogfooded, this parser is the seam to update.
46
+ *
47
+ * Shape (assumed):
48
+ * { eventName: 'beforeToolCall', parameters: { command: 'rm -rf /' } }
49
+ *
50
+ * Returns the same `{ toolName, command }` shape as parseClaudeShapeStdin so
51
+ * downstream code (buildCanonicalHook, enforceBashCommand) does not need to
52
+ * branch on IDE. The `toolName` here is the IDE-specific name (e.g.
53
+ * 'terminal'); callers may normalize it to the canonical 'Bash' if needed.
54
+ */
55
+ export declare function parseTraeShapeStdin(parsed: unknown): {
56
+ toolName?: string;
57
+ command?: string;
58
+ };
59
+ /**
60
+ * Per-adapter stdin parser dispatch. Returns the `{ toolName, command }` pair
61
+ * for the given IDE, regardless of which stdin shape the IDE actually emits.
62
+ *
63
+ * The dispatch table is keyed on `IdeId` (not stdin shape) so an adapter can
64
+ * change its wire format without breaking the hook-handle flow. Unknown IDEs
65
+ * fall back to the Claude parser — that preserves the slice #1 "fail-open to
66
+ * Claude" behavior so a future adapter that has not yet been added does not
67
+ * crash the hook runtime.
68
+ */
69
+ export declare function parseAdapterStdin(ide: IdeId, parsed: unknown): {
70
+ toolName?: string;
71
+ command?: string;
72
+ };
@@ -0,0 +1,128 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { getAdapter, listAdapterIds } from './ide-registry.js';
4
+ /**
5
+ * Detect the originating IDE from env / stdin shape / cwd heuristics.
6
+ * Falls back to 'claude-code' (PRD preserved behavior: backward compat).
7
+ */
8
+ export function detectIdeFromContext(input) {
9
+ // 1. env 变量优先级最高。Iterate ONLY over currently-registered adapters
10
+ // so the function does not throw on unregistered IDEs while future slices
11
+ // progressively add trae / codex / cursor / qoder / tongyi-lingma.
12
+ for (const adapter of listAdapterIds()) {
13
+ const a = getAdapter(adapter);
14
+ if (input.env[a.envVar] !== undefined) {
15
+ return adapter;
16
+ }
17
+ }
18
+ // 2. stdin shape
19
+ if (isObject(input.parsedStdin)) {
20
+ if ('tool_name' in input.parsedStdin || 'tool_input' in input.parsedStdin) {
21
+ return 'claude-code';
22
+ }
23
+ if ('toolName' in input.parsedStdin || 'toolInput' in input.parsedStdin) {
24
+ return 'cursor';
25
+ }
26
+ if ('eventName' in input.parsedStdin || 'parameters' in input.parsedStdin) {
27
+ return 'trae';
28
+ }
29
+ }
30
+ // 3. cwd 启发式。Same registration-aware iteration as step 1.
31
+ for (const adapter of listAdapterIds()) {
32
+ const a = getAdapter(adapter);
33
+ if (existsSync(join(input.cwd, a.settings.dirName))) {
34
+ return adapter;
35
+ }
36
+ }
37
+ // 4. fallback
38
+ return 'claude-code';
39
+ }
40
+ /**
41
+ * Pluck a string value at a nested path. Returns undefined if any segment is
42
+ * missing or non-object. Used by adapter-driven stdin parsers.
43
+ */
44
+ export function pluckString(obj, path) {
45
+ let cur = obj;
46
+ for (const seg of path) {
47
+ if (!isObject(cur) || !(seg in cur))
48
+ return undefined;
49
+ cur = cur[seg];
50
+ }
51
+ return typeof cur === 'string' ? cur : undefined;
52
+ }
53
+ export function pluckObject(obj, path) {
54
+ let cur = obj;
55
+ for (const seg of path) {
56
+ if (!isObject(cur) || !(seg in cur))
57
+ return undefined;
58
+ cur = cur[seg];
59
+ }
60
+ return isObject(cur) ? cur : undefined;
61
+ }
62
+ function isObject(v) {
63
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
64
+ }
65
+ /**
66
+ * Default stdin parser for adapters that follow the Claude Code shape
67
+ * (most LLM-style IDEs do: tool_name at top, tool_input.{command} inside).
68
+ */
69
+ export function parseClaudeShapeStdin(parsed) {
70
+ if (!isObject(parsed))
71
+ return {};
72
+ const toolName = pluckString(parsed, ['tool_name']);
73
+ const command = pluckString(parsed, ['tool_input', 'command']);
74
+ const result = {};
75
+ if (toolName !== undefined)
76
+ result.toolName = toolName;
77
+ if (command !== undefined)
78
+ result.command = command;
79
+ return result;
80
+ }
81
+ /**
82
+ * Trae stdin parser. UNVERIFIED — Trae 1.x's actual stdin envelope shape is
83
+ * assumed to be Cursor-style (eventName at top, parameters nested), based on
84
+ * slice #2's read of Trae as a "Cursor sibling". When a real Trae 1.x hook
85
+ * payload is dogfooded, this parser is the seam to update.
86
+ *
87
+ * Shape (assumed):
88
+ * { eventName: 'beforeToolCall', parameters: { command: 'rm -rf /' } }
89
+ *
90
+ * Returns the same `{ toolName, command }` shape as parseClaudeShapeStdin so
91
+ * downstream code (buildCanonicalHook, enforceBashCommand) does not need to
92
+ * branch on IDE. The `toolName` here is the IDE-specific name (e.g.
93
+ * 'terminal'); callers may normalize it to the canonical 'Bash' if needed.
94
+ */
95
+ export function parseTraeShapeStdin(parsed) {
96
+ if (!isObject(parsed))
97
+ return {};
98
+ const eventName = pluckString(parsed, ['eventName']);
99
+ const command = pluckString(parsed, ['parameters', 'command']);
100
+ const result = {};
101
+ // Trae sends the event name in the payload (`eventName: 'beforeToolCall'`)
102
+ // and the tool name on the parameters (e.g. `parameters.tool: 'terminal'`).
103
+ // We expose the event name as the "toolName" for now since Trae has not
104
+ // been observed to carry a top-level tool field. Future slice can split
105
+ // event vs tool if the real shape requires it.
106
+ if (eventName !== undefined)
107
+ result.toolName = eventName;
108
+ if (command !== undefined)
109
+ result.command = command;
110
+ return result;
111
+ }
112
+ /**
113
+ * Per-adapter stdin parser dispatch. Returns the `{ toolName, command }` pair
114
+ * for the given IDE, regardless of which stdin shape the IDE actually emits.
115
+ *
116
+ * The dispatch table is keyed on `IdeId` (not stdin shape) so an adapter can
117
+ * change its wire format without breaking the hook-handle flow. Unknown IDEs
118
+ * fall back to the Claude parser — that preserves the slice #1 "fail-open to
119
+ * Claude" behavior so a future adapter that has not yet been added does not
120
+ * crash the hook runtime.
121
+ */
122
+ export function parseAdapterStdin(ide, parsed) {
123
+ if (ide === 'trae')
124
+ return parseTraeShapeStdin(parsed);
125
+ // Future adapters (codex, cursor, qoder, tongyi-lingma) — branch here when
126
+ // their adapters are registered. Until then, fall back to Claude shape.
127
+ return parseClaudeShapeStdin(parsed);
128
+ }
@@ -0,0 +1,10 @@
1
+ import type { IdeId } from './ide-types.js';
2
+ /**
3
+ * Detect which IDE a given project root is using, by looking for the IDE's
4
+ * settings directory (`.claude`, `.trae`, etc.). Returns the first match in
5
+ * adapter insertion order, or `null` if no adapter's directory is present.
6
+ *
7
+ * 启发式:基于 cwd 目录存在性。CLI 后续 slice 会扩展为 env 变量检测、settings
8
+ * 文件内容检测、显式 `--ide` flag 覆盖等。
9
+ */
10
+ export declare function detectInstalledIde(projectRoot: string): IdeId | null;
@@ -0,0 +1,19 @@
1
+ import { existsSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { listAdapters } from './ide-registry.js';
4
+ /**
5
+ * Detect which IDE a given project root is using, by looking for the IDE's
6
+ * settings directory (`.claude`, `.trae`, etc.). Returns the first match in
7
+ * adapter insertion order, or `null` if no adapter's directory is present.
8
+ *
9
+ * 启发式:基于 cwd 目录存在性。CLI 后续 slice 会扩展为 env 变量检测、settings
10
+ * 文件内容检测、显式 `--ide` flag 覆盖等。
11
+ */
12
+ export function detectInstalledIde(projectRoot) {
13
+ for (const adapter of listAdapters()) {
14
+ if (existsSync(join(projectRoot, adapter.settings.dirName))) {
15
+ return adapter.id;
16
+ }
17
+ }
18
+ return null;
19
+ }
@@ -0,0 +1,14 @@
1
+ import type { IdeAdapter, IdeId } from './ide-types.js';
2
+ /** Get the adapter for a given IDE id. Throws on unsupported IDE. */
3
+ export declare function getAdapter(ide: IdeId): IdeAdapter;
4
+ /** All registered adapter ids (insertion order). */
5
+ export declare function listAdapterIds(): readonly IdeId[];
6
+ /** All registered adapters (insertion order). */
7
+ export declare function listAdapters(): readonly IdeAdapter[];
8
+ /**
9
+ * Test seam: register or replace an adapter. Used by future slices when adding
10
+ * a new IDE. Caller is responsible for ensuring the adapter is well-formed.
11
+ */
12
+ export declare function _setAdapterForTesting(ide: IdeId, adapter: IdeAdapter): void;
13
+ /** Test seam: reset to built-in defaults. */
14
+ export declare function _resetAdaptersForTesting(): void;
@@ -0,0 +1,45 @@
1
+ import { CLAUDE_CODE_ADAPTER } from './adapters/claude-code-adapter.js';
2
+ import { TRAE_ADAPTER } from './adapters/trae-adapter.js';
3
+ /**
4
+ * Built-in IDE adapter registry。Map<IdeId, IdeAdapter> 是单一来源。
5
+ *
6
+ * Slice #1 注册 claude-code。
7
+ * Slice #2 注册 trae —— 这是 slice #1 抽出的 IdeAdapter 形状的
8
+ * 第一个真实客户,验证"填表"承诺。
9
+ * 后续 slice 注入 codex / cursor / qoder / tongyi-lingma 时,只需在此
10
+ * Map 加条目 —— 所有 adapter 使用方(hook-translator、hooks install、statusline
11
+ * install、mcp apply)通过 `getAdapter(ide)` 拿取,无需修改。
12
+ */
13
+ const ADAPTERS = new Map([
14
+ ['claude-code', CLAUDE_CODE_ADAPTER],
15
+ ['trae', TRAE_ADAPTER],
16
+ ]);
17
+ /** Get the adapter for a given IDE id. Throws on unsupported IDE. */
18
+ export function getAdapter(ide) {
19
+ const adapter = ADAPTERS.get(ide);
20
+ if (!adapter) {
21
+ throw new Error(`Unsupported IDE: ${ide}. Registered: ${listAdapterIds().join(', ') || '(none)'}`);
22
+ }
23
+ return adapter;
24
+ }
25
+ /** All registered adapter ids (insertion order). */
26
+ export function listAdapterIds() {
27
+ return Array.from(ADAPTERS.keys());
28
+ }
29
+ /** All registered adapters (insertion order). */
30
+ export function listAdapters() {
31
+ return Array.from(ADAPTERS.values());
32
+ }
33
+ /**
34
+ * Test seam: register or replace an adapter. Used by future slices when adding
35
+ * a new IDE. Caller is responsible for ensuring the adapter is well-formed.
36
+ */
37
+ export function _setAdapterForTesting(ide, adapter) {
38
+ ADAPTERS.set(ide, adapter);
39
+ }
40
+ /** Test seam: reset to built-in defaults. */
41
+ export function _resetAdaptersForTesting() {
42
+ ADAPTERS.clear();
43
+ ADAPTERS.set('claude-code', CLAUDE_CODE_ADAPTER);
44
+ ADAPTERS.set('trae', TRAE_ADAPTER);
45
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * peaks 自有 hook 协议 + slim IDE adapter 接口。
3
+ *
4
+ * peaks-cli 不再适配 IDE 私有的 hook 协议;反之 peaks 定义自己的 canonical
5
+ * schema,每个 IDE 只需要填 4 字符串 + 1 settings 函数,新 IDE 适配变成"填表"。
6
+ *
7
+ * 不可消除的 per-IDE 字段(诚实交代,见 PRD R-1..R-4):
8
+ * - settings.json 物理位置
9
+ * - 项目根 env 变量名
10
+ * - hook 事件名 + matcher 名
11
+ *
12
+ * 其他全部归一化到 peaks 内部模型(见 hook-protocol.ts)。
13
+ */
14
+ import type { SubAgentDispatcher } from '../dispatch/sub-agent-dispatcher.js';
15
+ export type IdeId = 'claude-code' | 'trae' | 'codex' | 'cursor' | 'qoder' | 'tongyi-lingma';
16
+ export interface IdeCapabilities {
17
+ /** peaks gate enforce 是否适用该 IDE(必备) */
18
+ readonly gateEnforce: true;
19
+ /** peaks progress start(sub-agent 派发)是否适用 */
20
+ readonly progressStart: boolean;
21
+ /** peaks statusline 状态栏是否适用 */
22
+ readonly statusline: boolean;
23
+ /** peaks mcp install 是否适用 */
24
+ readonly mcpInstall: boolean;
25
+ }
26
+ export interface IdeSettingsLocation {
27
+ /** 项目根下的 settings 目录名,例如 '.claude' / '.trae' / '.cursor' */
28
+ readonly dirName: string;
29
+ /** settings 文件名(部分 IDE 叫 settings.json / mcp.json) */
30
+ readonly settingsFileName: string;
31
+ /** 解析出 settings.json 绝对路径 */
32
+ resolveSettingsFile(scope: 'project' | 'global', projectRoot: string | undefined): string;
33
+ /** 该 IDE 是否支持此 scope(用于清晰报错) */
34
+ supportsScope(scope: 'project' | 'global'): boolean;
35
+ }
36
+ /**
37
+ * Slim IDE adapter 描述。每 IDE 一个静态常量(无需 DI)。
38
+ * 字段故意保持少:让 adapter 的添加是"填表"而非"重写"。
39
+ */
40
+ export interface IdeAdapter {
41
+ readonly id: IdeId;
42
+ /** 人类可读名,出现在 CLI help / 命令输出 */
43
+ readonly displayName: string;
44
+ readonly settings: IdeSettingsLocation;
45
+ /** IDE 注入的项目根 env 变量名;`peaks gate enforce` 等命令模板会引用此 env */
46
+ readonly envVar: string;
47
+ /** settings.json 里 hook 数组的 key,例如 'PreToolUse' / 'beforeToolCall' */
48
+ readonly hookEvent: string;
49
+ /** hook 数组元素的 matcher 字段(工具名匹配),例如 'Bash' / 'Task' / 'terminal' */
50
+ readonly toolMatcher: string;
51
+ /**
52
+ * The tool name used by this IDE to invoke a sub-agent (e.g. Claude Code
53
+ * uses 'Task' to dispatch a sub-agent, Trae may use a different name).
54
+ * Consumed by the `peaks progress start` hook entry so each IDE self-
55
+ * reports its sub-agent tool name. Additive on `toolMatcher`: the
56
+ * `toolMatcher` field still drives the gate-enforce hook entry, this
57
+ * one drives the sub-agent-progress hook entry.
58
+ *
59
+ * Added in slice 2026-06-06-sub-agent-spawn-bug-and-decouple.
60
+ */
61
+ readonly subAgentToolMatcher: string;
62
+ /**
63
+ * Per-IDE sub-agent dispatcher. The `peaks sub-agent dispatch` CLI reads
64
+ * this field, calls `supportsRole` + `buildToolCall`, and returns the
65
+ * resulting tool-call descriptor in the JSON envelope. Additive on
66
+ * `subAgentToolMatcher`: the matcher still drives the gate-enforce hook
67
+ * entry; this field drives the runtime sub-agent dispatch surface.
68
+ *
69
+ * Added in slice 2026-06-07-sub-agent-dispatch-decouple. See PRD #002
70
+ * G1 (AC-1, AC-2) + [[slim-ideadapter-shape-is-the-contract]].
71
+ */
72
+ readonly subAgentDispatcher: SubAgentDispatcher;
73
+ /**
74
+ * Per-IDE opt-in to the G9 prompt-size gate. When `true`, the
75
+ * `peaks hooks install` command registers the G9 PreToolUse hook
76
+ * (`peaks sub-agent-dispatch-guard`) for this IDE. When `false`,
77
+ * the hook is NOT installed (the IDE either doesn't support the
78
+ * PreToolUse event in a useful form, or the user has opted out).
79
+ *
80
+ * The CLI 兜底 layer in `peaks sub-agent dispatch` still enforces
81
+ * the threshold regardless of this field — `promptSizeAware` only
82
+ * controls the hook layer (R-15: G9 hook is LLM-platform-specific).
83
+ *
84
+ * Added in slice 2026-06-07-sub-agent-context-governance. See PRD
85
+ * #003 G9.2 + AC-56. Default `false` to preserve slice #009's
86
+ * `peaks hooks install` output byte-stability.
87
+ */
88
+ readonly promptSizeAware: boolean;
89
+ /** install / uninstall 后展示给用户的提示文本(各 IDE 不同,例如 Claude 提示重启窗口) */
90
+ readonly installHints: readonly string[];
91
+ /** 该 IDE 在 peaks 上可启用的能力(用于在不支持的 IDE 上软警告) */
92
+ readonly capabilities: IdeCapabilities;
93
+ }
94
+ /** peaks canonical hook schema 版本标识 */
95
+ export declare const PEAKS_HOOK_SCHEMA: "peaks-hook/v1";
96
+ /** peaks canonical hook 形态 —— 单一协议,所有 IDE 经 hook-translator 归一化到此 */
97
+ export interface PeaksCanonicalHook {
98
+ readonly schema: typeof PEAKS_HOOK_SCHEMA;
99
+ readonly event: 'pre-tool-use' | 'post-tool-use' | 'sub-agent-start';
100
+ readonly toolName: string;
101
+ readonly toolInput: Record<string, unknown>;
102
+ /** 解析自 env 变量或 --project 的项目根 */
103
+ readonly projectRoot: string;
104
+ /** 选输出格式 */
105
+ readonly rawIdeFormat: IdeId;
106
+ /** 原始 stdin,留作回退 */
107
+ readonly rawPayload: unknown;
108
+ }
109
+ /** peaks 决策发回形态枚举(按 IDE 期望的"发回"形式) */
110
+ export type PeaksDecisionTransport = {
111
+ kind: 'stdout-json';
112
+ denyShape: Record<string, unknown>;
113
+ } | {
114
+ kind: 'exit-code';
115
+ denyCode: number;
116
+ } | {
117
+ kind: 'both';
118
+ denyShape: Record<string, unknown>;
119
+ denyCode: number;
120
+ };
@@ -0,0 +1,2 @@
1
+ /** peaks canonical hook schema 版本标识 */
2
+ export const PEAKS_HOOK_SCHEMA = 'peaks-hook/v1';
@@ -0,0 +1,15 @@
1
+ export declare const ATOMIC_JSON_FILE_MODE = 384;
2
+ /**
3
+ * Read a JSON object file using a no-follow open. Returns an empty object when
4
+ * the file does not exist or is empty. Throws when the file exists but does not
5
+ * contain a JSON object (so callers can distinguish "no settings" from
6
+ * "malformed settings").
7
+ */
8
+ export declare function readJsonObjectFile<T extends Record<string, unknown> = Record<string, unknown>>(filePath: string): T;
9
+ /**
10
+ * Atomically write a JSON file: create a unique temp file in the same
11
+ * directory, fsync its contents via close, then `rename` over the target. A
12
+ * failure during rename removes the temp file (best effort). The target is
13
+ * created with 0o600 permissions.
14
+ */
15
+ export declare function atomicWriteJson(filePath: string, value: unknown): void;