peaks-cli 1.3.1 → 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 (111) hide show
  1. package/README.md +6 -2
  2. package/bin/peaks.js +0 -0
  3. package/dist/src/cli/commands/core-artifact-commands.js +49 -11
  4. package/dist/src/cli/commands/gate-commands.js +28 -19
  5. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  6. package/dist/src/cli/commands/hook-handle.js +111 -0
  7. package/dist/src/cli/commands/hooks-commands.js +72 -21
  8. package/dist/src/cli/commands/progress-commands.js +9 -2
  9. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  10. package/dist/src/cli/commands/slice-commands.js +4 -2
  11. package/dist/src/cli/commands/statusline-commands.js +75 -17
  12. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  13. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  14. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  15. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  16. package/dist/src/cli/commands/workspace-commands.js +70 -14
  17. package/dist/src/cli/program.js +9 -0
  18. package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
  19. package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
  20. package/dist/src/services/artifacts/artifact-prerequisites.d.ts +12 -0
  21. package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
  22. package/dist/src/services/artifacts/request-artifact-service.js +116 -76
  23. package/dist/src/services/config/config-types.d.ts +1 -1
  24. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  25. package/dist/src/services/context/artifact-meta.js +105 -0
  26. package/dist/src/services/context/context-guard.d.ts +49 -0
  27. package/dist/src/services/context/context-guard.js +91 -0
  28. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  29. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  30. package/dist/src/services/context/headroom-client.d.ts +34 -0
  31. package/dist/src/services/context/headroom-client.js +117 -0
  32. package/dist/src/services/context/shared-channel.d.ts +92 -0
  33. package/dist/src/services/context/shared-channel.js +285 -0
  34. package/dist/src/services/context/threshold.d.ts +35 -0
  35. package/dist/src/services/context/threshold.js +76 -0
  36. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  37. package/dist/src/services/dispatch/batch-counter.js +85 -0
  38. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  39. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  40. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  41. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  42. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  43. package/dist/src/services/dispatch/leak-detector.js +72 -0
  44. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  45. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  46. package/dist/src/services/doctor/doctor-service.d.ts +62 -0
  47. package/dist/src/services/doctor/doctor-service.js +276 -1
  48. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  49. package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
  50. package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
  51. package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
  52. package/dist/src/services/ide/hook-protocol.d.ts +44 -0
  53. package/dist/src/services/ide/hook-protocol.js +71 -0
  54. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  55. package/dist/src/services/ide/hook-translator.js +128 -0
  56. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  57. package/dist/src/services/ide/ide-detector.js +19 -0
  58. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  59. package/dist/src/services/ide/ide-registry.js +45 -0
  60. package/dist/src/services/ide/ide-types.d.ts +120 -0
  61. package/dist/src/services/ide/ide-types.js +2 -0
  62. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  63. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  64. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  65. package/dist/src/services/ide/shared/safe-path.js +29 -0
  66. package/dist/src/services/progress/progress-service.d.ts +1 -1
  67. package/dist/src/services/progress/progress-service.js +18 -14
  68. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  69. package/dist/src/services/security/safe-settings-path.js +104 -0
  70. package/dist/src/services/session/session-manager.d.ts +22 -1
  71. package/dist/src/services/session/session-manager.js +137 -28
  72. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  73. package/dist/src/services/signal/cancel-handler.js +76 -0
  74. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  75. package/dist/src/services/skill/resume-detector.js +334 -0
  76. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  77. package/dist/src/services/skill/skill-scheduler.js +53 -0
  78. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  79. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  80. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  81. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  82. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  83. package/dist/src/services/slice/slice-archive-service.js +111 -0
  84. package/dist/src/services/slice/slice-check-service.js +20 -1
  85. package/dist/src/services/slice/slice-check-types.d.ts +9 -0
  86. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  87. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  88. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  89. package/dist/src/services/solo/status-line-renderer.js +55 -0
  90. package/dist/src/services/workspace/migrate-service.js +124 -2
  91. package/dist/src/services/workspace/migrate-types.d.ts +50 -7
  92. package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
  93. package/dist/src/services/workspace/reconcile-service.js +267 -48
  94. package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
  95. package/dist/src/services/workspace/workspace-service.js +29 -62
  96. package/dist/src/shared/version.d.ts +1 -1
  97. package/dist/src/shared/version.js +1 -1
  98. package/package.json +2 -1
  99. package/schemas/doctor-report.schema.json +2 -2
  100. package/skills/peaks-ide/SKILL.md +159 -0
  101. package/skills/peaks-qa/SKILL.md +58 -1
  102. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  103. package/skills/peaks-rd/SKILL.md +52 -9
  104. package/skills/peaks-solo/SKILL.md +83 -20
  105. package/skills/peaks-solo/references/context-governance.md +144 -0
  106. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  107. package/skills/peaks-solo/references/runbook.md +3 -3
  108. package/skills/peaks-solo/references/sub-agent-dispatch.md +218 -0
  109. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  110. package/skills/peaks-txt/SKILL.md +19 -0
  111. package/skills/peaks-ui/SKILL.md +28 -1
@@ -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;
@@ -0,0 +1,58 @@
1
+ import { closeSync, constants, existsSync, mkdirSync, openSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { dirname, join } from 'node:path';
4
+ export const ATOMIC_JSON_FILE_MODE = 0o600;
5
+ /**
6
+ * Read a JSON object file using a no-follow open. Returns an empty object when
7
+ * the file does not exist or is empty. Throws when the file exists but does not
8
+ * contain a JSON object (so callers can distinguish "no settings" from
9
+ * "malformed settings").
10
+ */
11
+ export function readJsonObjectFile(filePath) {
12
+ if (!existsSync(filePath))
13
+ return {};
14
+ const fd = openSync(filePath, constants.O_RDONLY | constants.O_NOFOLLOW);
15
+ try {
16
+ const raw = readFileSync(fd, 'utf8').trim();
17
+ if (raw.length === 0)
18
+ return {};
19
+ const parsed = JSON.parse(raw);
20
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
21
+ throw new Error('settings file must contain a JSON object');
22
+ }
23
+ return parsed;
24
+ }
25
+ finally {
26
+ closeSync(fd);
27
+ }
28
+ }
29
+ /**
30
+ * Atomically write a JSON file: create a unique temp file in the same
31
+ * directory, fsync its contents via close, then `rename` over the target. A
32
+ * failure during rename removes the temp file (best effort). The target is
33
+ * created with 0o600 permissions.
34
+ */
35
+ export function atomicWriteJson(filePath, value) {
36
+ const dir = dirname(filePath);
37
+ mkdirSync(dir, { recursive: true });
38
+ const tempPath = join(dir, `.settings.${randomUUID()}.tmp`);
39
+ const fd = openSync(tempPath, constants.O_WRONLY | constants.O_CREAT | constants.O_EXCL | constants.O_NOFOLLOW, ATOMIC_JSON_FILE_MODE);
40
+ try {
41
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`, 'utf8');
42
+ }
43
+ finally {
44
+ closeSync(fd);
45
+ }
46
+ try {
47
+ renameSync(tempPath, filePath);
48
+ }
49
+ catch (error) {
50
+ try {
51
+ unlinkSync(tempPath);
52
+ }
53
+ catch {
54
+ // best effort cleanup
55
+ }
56
+ throw error;
57
+ }
58
+ }
@@ -0,0 +1,11 @@
1
+ export type HookScope = 'project' | 'global';
2
+ /** True iff `childPath` resolves to `parentPath` or any path nested inside it. */
3
+ export declare function isInsidePath(childPath: string, parentPath: string): boolean;
4
+ /**
5
+ * Reject settings targets that are symlinked or escape the configured root.
6
+ * Used by the hook / statusline / MCP install paths to keep the project root the
7
+ * sole owner of writable settings files.
8
+ */
9
+ export declare function assertSafeSettingsFile(scope: HookScope, root: string, dirName: string, settingsFileName: string): {
10
+ settingsPath: string;
11
+ };
@@ -0,0 +1,29 @@
1
+ import { existsSync, lstatSync, realpathSync } from 'node:fs';
2
+ import { isAbsolute, join, relative } from 'node:path';
3
+ /** True iff `childPath` resolves to `parentPath` or any path nested inside it. */
4
+ export function isInsidePath(childPath, parentPath) {
5
+ const rel = relative(parentPath, childPath);
6
+ return rel === '' || (!rel.startsWith('..') && !isAbsolute(rel));
7
+ }
8
+ /**
9
+ * Reject settings targets that are symlinked or escape the configured root.
10
+ * Used by the hook / statusline / MCP install paths to keep the project root the
11
+ * sole owner of writable settings files.
12
+ */
13
+ export function assertSafeSettingsFile(scope, root, dirName, settingsFileName) {
14
+ const settingsPath = join(root, dirName, settingsFileName);
15
+ const dirPath = join(root, dirName);
16
+ if (existsSync(dirPath) && lstatSync(dirPath).isSymbolicLink()) {
17
+ throw new Error(`${dirName} directory must not be a symlink`);
18
+ }
19
+ if (existsSync(settingsPath)) {
20
+ if (lstatSync(settingsPath).isSymbolicLink()) {
21
+ throw new Error(`${settingsFileName} must not be a symlink`);
22
+ }
23
+ const realRoot = realpathSync(root);
24
+ if (!isInsidePath(realpathSync(settingsPath), realRoot)) {
25
+ throw new Error(`${settingsFileName} must stay inside the ${scope} root`);
26
+ }
27
+ }
28
+ return { settingsPath };
29
+ }
@@ -2,7 +2,7 @@
2
2
  * Sub-agent progress surfacing for the RD/QA sub-agents in
3
3
  * `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
4
4
  * `peaks progress step` CLI) writes a stable JSON file at
5
- * `.peaks/<sid>/system/subagent-progress.json`. The user-side
5
+ * `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
6
6
  * `peaks progress watch` CLI polls this file in a separate
7
7
  * terminal tab and renders elapsed / spinner / sub-step. The
8
8
  * `peaks progress start` CLI auto-spawns the watch in a new
@@ -2,7 +2,7 @@
2
2
  * Sub-agent progress surfacing for the RD/QA sub-agents in
3
3
  * `peaks-solo`'s Swarm phase. A sub-agent (or the LLM via the
4
4
  * `peaks progress step` CLI) writes a stable JSON file at
5
- * `.peaks/<sid>/system/subagent-progress.json`. The user-side
5
+ * `.peaks/_sub_agents/<sid>/subagent-progress.json`. The user-side
6
6
  * `peaks progress watch` CLI polls this file in a separate
7
7
  * terminal tab and renders elapsed / spinner / sub-step. The
8
8
  * `peaks progress start` CLI auto-spawns the watch in a new
@@ -34,20 +34,24 @@ import { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync } from '
34
34
  import { dirname, join, resolve } from 'node:path';
35
35
  import { getSessionIdCanonical } from '../session/session-manager.js';
36
36
  import { findProjectRoot } from '../config/config-safety.js';
37
- const PROGRESS_REL_PATH = 'system/subagent-progress.json';
38
- const SPAWN_REL_PATH = 'system/progress-spawn.json';
37
+ // As of slice 2026-06-06-sub-agent-spawn-bug-and-decouple, the per-session
38
+ // sub-agent state files live under `.peaks/_sub_agents/<sid>/`, NOT under
39
+ // `.peaks/<sid>/system/`. The new path mirrors the existing `_runtime/`
40
+ // and `_dogfood/` convention (leading underscore = meta-classification, not
41
+ // a per-session artifact). The previous `<sid>/system/...` locations are
42
+ // migrated to the new path on first run of `peaks workspace reconcile
43
+ // --apply` (see `migrateSubAgentState` in reconcile-service.ts).
44
+ const SUB_AGENTS_DIR = '_sub_agents';
45
+ const PROGRESS_FILE_NAME = 'subagent-progress.json';
46
+ const SPAWN_FILE_NAME = 'progress-spawn.json';
39
47
  function progressPath(projectRoot) {
40
- // The progress file lives under the *session* directory, not
41
- // directly under .peaks/. Every other per-slice artefact
42
- // (rd/tech-doc.md, qa/test-cases/<rid>.md, prd/requests/<rid>.md,
43
- // memory/, openspec/) lives under .peaks/<sid>/, so progress
44
- // should too. Without the session prefix, a session rotation
45
- // would orphan the file in the project root, and switching
46
- // sessions would have the watch reading the wrong slice's
47
- // progress.
48
+ // The progress file lives at `.peaks/_sub_agents/<sid>/subagent-progress.json`.
49
+ // The leading `_sub_agents/` is a meta-classification (mirrors `_runtime/`,
50
+ // `_dogfood/`) — the SID is the per-session discriminator inside that meta
51
+ // dir. Without the SID, sessions would collide on the same file.
48
52
  const sessionId = getSessionIdCanonical(projectRoot);
49
53
  const subDir = sessionId ?? 'unbound';
50
- return join(projectRoot, '.peaks', subDir, PROGRESS_REL_PATH);
54
+ return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, PROGRESS_FILE_NAME);
51
55
  }
52
56
  function ensureParentDir(path) {
53
57
  const dir = dirname(path);
@@ -199,12 +203,12 @@ export function subAgentProgressPath(projectRoot) {
199
203
  export function subAgentSpawnPath(projectRoot) {
200
204
  const sessionId = getSessionIdCanonical(projectRoot);
201
205
  const subDir = sessionId ?? 'unbound';
202
- return join(projectRoot, '.peaks', subDir, SPAWN_REL_PATH);
206
+ return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
203
207
  }
204
208
  function spawnRecordPath(projectRoot) {
205
209
  const sessionId = getSessionIdCanonical(projectRoot);
206
210
  const subDir = sessionId ?? 'unbound';
207
- return join(projectRoot, '.peaks', subDir, SPAWN_REL_PATH);
211
+ return join(projectRoot, '.peaks', SUB_AGENTS_DIR, subDir, SPAWN_FILE_NAME);
208
212
  }
209
213
  export function writeSpawnRecord(options) {
210
214
  const sessionId = getSessionIdCanonical(options.projectRoot);
@@ -0,0 +1,12 @@
1
+ /** Build the canonical record path for a given session/rid/timestamp. */
2
+ export declare function dispatchRecordPath(projectRoot: string, sid: string, rid: string, ts?: Date): string;
3
+ /** The directory under which dispatch records live. */
4
+ export declare function dispatchRecordsDir(projectRoot: string, sid: string): string;
5
+ /**
6
+ * Assert that `recordPath` lives under `projectRoot/.peaks/_sub_agents/<sid>/`.
7
+ * Rejects symlink/junction escapes and `..` segments.
8
+ *
9
+ * Throws an Error with `.code = 'INVALID_RECORD_PATH'` on rejection so
10
+ * the CLI can map to `{ok: false, code: "INVALID_RECORD_PATH"}`.
11
+ */
12
+ export declare function assertSafeDispatchRecordPath(recordPath: string, projectRoot: string): string;