peaks-cli 1.3.2 → 1.3.4

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 (115) hide show
  1. package/README.md +6 -2
  2. package/dist/src/cli/commands/core-artifact-commands.js +6 -3
  3. package/dist/src/cli/commands/gate-commands.js +28 -19
  4. package/dist/src/cli/commands/hook-handle.d.ts +17 -0
  5. package/dist/src/cli/commands/hook-handle.js +111 -0
  6. package/dist/src/cli/commands/hooks-commands.js +72 -21
  7. package/dist/src/cli/commands/progress-commands.js +9 -2
  8. package/dist/src/cli/commands/progress-start-spawn.js +30 -4
  9. package/dist/src/cli/commands/project-commands.js +8 -4
  10. package/dist/src/cli/commands/statusline-commands.js +75 -17
  11. package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
  12. package/dist/src/cli/commands/sub-agent-commands.js +488 -0
  13. package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
  14. package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
  15. package/dist/src/cli/commands/workflow-commands.js +2 -1
  16. package/dist/src/cli/commands/workspace-commands.js +3 -0
  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/config/config-types.d.ts +1 -1
  21. package/dist/src/services/context/artifact-meta.d.ts +72 -0
  22. package/dist/src/services/context/artifact-meta.js +105 -0
  23. package/dist/src/services/context/context-guard.d.ts +49 -0
  24. package/dist/src/services/context/context-guard.js +91 -0
  25. package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
  26. package/dist/src/services/context/dispatch-context-guard.js +192 -0
  27. package/dist/src/services/context/headroom-client.d.ts +34 -0
  28. package/dist/src/services/context/headroom-client.js +117 -0
  29. package/dist/src/services/context/shared-channel.d.ts +92 -0
  30. package/dist/src/services/context/shared-channel.js +285 -0
  31. package/dist/src/services/context/threshold.d.ts +35 -0
  32. package/dist/src/services/context/threshold.js +76 -0
  33. package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
  34. package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
  35. package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
  36. package/dist/src/services/dispatch/batch-counter.js +85 -0
  37. package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
  38. package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
  39. package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
  40. package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
  41. package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
  42. package/dist/src/services/dispatch/leak-detector.js +72 -0
  43. package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
  44. package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
  45. package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
  46. package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
  47. package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
  48. package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
  49. package/dist/src/services/ide/hook-protocol.d.ts +47 -0
  50. package/dist/src/services/ide/hook-protocol.js +74 -0
  51. package/dist/src/services/ide/hook-translator.d.ts +72 -0
  52. package/dist/src/services/ide/hook-translator.js +128 -0
  53. package/dist/src/services/ide/ide-detector.d.ts +10 -0
  54. package/dist/src/services/ide/ide-detector.js +19 -0
  55. package/dist/src/services/ide/ide-registry.d.ts +14 -0
  56. package/dist/src/services/ide/ide-registry.js +45 -0
  57. package/dist/src/services/ide/ide-types.d.ts +180 -0
  58. package/dist/src/services/ide/ide-types.js +2 -0
  59. package/dist/src/services/ide/resource-profile.d.ts +52 -0
  60. package/dist/src/services/ide/resource-profile.js +33 -0
  61. package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
  62. package/dist/src/services/ide/shared/atomic-json.js +58 -0
  63. package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
  64. package/dist/src/services/ide/shared/safe-path.js +29 -0
  65. package/dist/src/services/memory/project-context-service.js +2 -1
  66. package/dist/src/services/memory/project-memory-service.js +4 -3
  67. package/dist/src/services/perf/perf-baseline-service.js +2 -1
  68. package/dist/src/services/progress/progress-service.d.ts +1 -1
  69. package/dist/src/services/progress/progress-service.js +18 -14
  70. package/dist/src/services/security/safe-settings-path.d.ts +12 -0
  71. package/dist/src/services/security/safe-settings-path.js +104 -0
  72. package/dist/src/services/session/getSessionDir.d.ts +1 -0
  73. package/dist/src/services/session/getSessionDir.js +27 -0
  74. package/dist/src/services/session/index.d.ts +1 -0
  75. package/dist/src/services/session/index.js +1 -0
  76. package/dist/src/services/signal/cancel-handler.d.ts +14 -0
  77. package/dist/src/services/signal/cancel-handler.js +76 -0
  78. package/dist/src/services/skill/resume-detector.d.ts +54 -0
  79. package/dist/src/services/skill/resume-detector.js +334 -0
  80. package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
  81. package/dist/src/services/skill/skill-scheduler.js +53 -0
  82. package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
  83. package/dist/src/services/skills/hooks-settings-service.js +190 -144
  84. package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
  85. package/dist/src/services/skills/statusline-settings-service.js +31 -34
  86. package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
  87. package/dist/src/services/slice/slice-archive-service.js +111 -0
  88. package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
  89. package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
  90. package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
  91. package/dist/src/services/solo/status-line-renderer.js +55 -0
  92. package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
  93. package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
  94. package/dist/src/services/standards/project-standards-service.d.ts +1 -2
  95. package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
  96. package/dist/src/services/workspace/reconcile-service.js +107 -6
  97. package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
  98. package/dist/src/shared/version.d.ts +1 -1
  99. package/dist/src/shared/version.js +1 -1
  100. package/package.json +2 -1
  101. package/scripts/install-skills.mjs +112 -2
  102. package/skills/peaks-ide/SKILL.md +159 -0
  103. package/skills/peaks-ide/references/audit-log-helper.md +52 -0
  104. package/skills/peaks-qa/SKILL.md +153 -55
  105. package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
  106. package/skills/peaks-rd/SKILL.md +134 -62
  107. package/skills/peaks-solo/SKILL.md +124 -37
  108. package/skills/peaks-solo/references/browser-workflow.md +22 -20
  109. package/skills/peaks-solo/references/context-governance.md +144 -0
  110. package/skills/peaks-solo/references/headroom-integration.md +107 -0
  111. package/skills/peaks-solo/references/runbook.md +3 -3
  112. package/skills/peaks-solo/references/sub-agent-dispatch.md +261 -0
  113. package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
  114. package/skills/peaks-txt/SKILL.md +17 -0
  115. package/skills/peaks-ui/SKILL.md +45 -10
package/README.md CHANGED
@@ -8,8 +8,8 @@
8
8
  Peaks 是一个**跨 AI IDE 的工作流门禁 CLI + 技能家族**——把项目治理、工作流规划、受控执行、QA 验证、变更追踪组织成可复用的工程流程。CLI 是跨 IDE 稳定的核心(门禁 + JSON 契约 + 不可逆动作),技能 / 钩子 / 配置按各 IDE 的原生格式承载。
9
9
 
10
10
  > **支持的 IDE**:
11
- > - ✅ **Claude Code**(当前实现):11 个 `peaks-*` 技能 + `.claude/settings.json` PreToolUse hook
12
- > - 🚧 **Trae**(适配中):用 Trae 的原生配置承载相同的角色 / 状态机 / 门禁
11
+ > - ✅ **Claude Code**(shipped, 当前主用):11 个 `peaks-*` 技能 + `.claude/settings.json` PreToolUse hook;agent team 在本 IDE 已 dogfood
12
+ > - ⚠️ **Trae**(adapter shipped, real-Trae unverified):slim `IdeAdapter` 已注册到 slice #1 registry(`hookEvent` / `toolMatcher` / `envVar` 是 1.x 假设,**未在真实 Trae 上验证**);真实 Trae 集成 dogfood 留到后续切片
13
13
  > - 📋 **Codex / Cursor / Qoder / 通义灵码 等**(路线图)
14
14
 
15
15
  > **产品定位**:你**用技能工作**,CLI 是跨 IDE 的质量保障层。
@@ -115,6 +115,10 @@ peaks-solo strict 模式做 X # 显式 strict:最严格门禁
115
115
 
116
116
  **3 个 solo 包装 + 7 个角色技能 + 1 个 solo 编排 = 11 个技能家族。** 日常使用中,1 个(`peaks-solo`)覆盖 ≥90% 的需求。
117
117
 
118
+ ## Agent team
119
+
120
+ `peaks` 帮你调度一个 agent team——`peaks-solo` / `peaks-rd` / `peaks-qa` / `peaks-ui` 把 peer sub-agent 派到隔离沙箱里写 PRD / 做架构分析 / 跑测试 / 设计 UI,主 LLM 只看每个 sub-agent 的元数据(路径 + 大小 + 摘要)。
121
+
118
122
  ## 怎么用:技能优先,CLI 是门禁
119
123
 
120
124
  Peaks 里的 `peaks <cmd>` CLI **不是日常使用的主要入口**。它的存在有三个理由,全都是机器层保障:
@@ -1,7 +1,8 @@
1
1
  import { createArtifactInitPlan, getArtifactStatus, createGuidedArtifactSetup } from '../../services/artifacts/artifact-service.js';
2
2
  import { getArtifactWorkspaceStatus, planArtifactSync } from '../../services/artifacts/workspace-service.js';
3
3
  import { executeProjectMemoryBackup, executeProjectMemoryExtract, summarizeProjectMemoryBackupResult, summarizeProjectMemoryExtractResult } from '../../services/memory/project-memory-service.js';
4
- import { executeProjectStandardsInit, executeProjectStandardsUpdate, summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../services/standards/project-standards-service.js';
4
+ import { summarizeProjectStandardsInitResult, summarizeProjectStandardsUpdateResult } from '../../services/standards/project-standards-service.js';
5
+ import { executeProjectStandardsInitIdeAware, executeProjectStandardsUpdateIdeAware } from '../../services/standards/ide-aware-standards-service.js';
5
6
  import { listProfiles } from '../../services/profiles/profile-service.js';
6
7
  import { planProxyTest } from '../../services/proxy/proxy-service.js';
7
8
  import { runDoctor } from '../../services/doctor/doctor-service.js';
@@ -297,6 +298,7 @@ export function registerCoreAndArtifactCommands(program, io) {
297
298
  .description('Initialize project-local coding standards for Peaks skill preflight')
298
299
  .requiredOption('--project <path>', 'target project root')
299
300
  .option('--language <language>', 'standards language pack')
301
+ .option('--ide <id>', 'override IDE detection (e.g. claude-code, trae)')
300
302
  .option('--dry-run', 'preview writes without changing files')
301
303
  .option('--apply', 'write missing standards into the target project')).action((options) => {
302
304
  if (options.dryRun === true && options.apply === true) {
@@ -305,7 +307,7 @@ export function registerCoreAndArtifactCommands(program, io) {
305
307
  return;
306
308
  }
307
309
  try {
308
- const result = executeProjectStandardsInit({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), apply: options.apply === true });
310
+ const result = executeProjectStandardsInitIdeAware({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), ...(options.ide !== undefined ? { ideId: options.ide } : {}), apply: options.apply === true });
309
311
  printResult(io, ok('standards.init', summarizeProjectStandardsInitResult(result)), options.json);
310
312
  }
311
313
  catch (error) {
@@ -318,6 +320,7 @@ export function registerCoreAndArtifactCommands(program, io) {
318
320
  .description('Append managed standards metadata to an existing CLAUDE.md without rewriting the body')
319
321
  .requiredOption('--project <path>', 'target project root')
320
322
  .option('--language <language>', 'standards language pack')
323
+ .option('--ide <id>', 'override IDE detection (e.g. claude-code, trae)')
321
324
  .option('--dry-run', 'preview writes without changing files')
322
325
  .option('--apply', 'append managed metadata to the target project')).action((options) => {
323
326
  if (options.dryRun === true && options.apply === true) {
@@ -326,7 +329,7 @@ export function registerCoreAndArtifactCommands(program, io) {
326
329
  return;
327
330
  }
328
331
  try {
329
- const result = executeProjectStandardsUpdate({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), apply: options.apply === true });
332
+ const result = executeProjectStandardsUpdateIdeAware({ projectRoot: options.project, ...(options.language !== undefined ? { language: options.language } : {}), ...(options.ide !== undefined ? { ideId: options.ide } : {}), apply: options.apply === true });
330
333
  const summary = summarizeProjectStandardsUpdateResult(result);
331
334
  const response = summary.reviewSuggestions.length > 0
332
335
  ? fail('standards.update', 'STANDARDS_UPDATE_REVIEW_REQUIRED', 'Standards update requires manual review', summary, summary.reviewSuggestions)
@@ -1,7 +1,15 @@
1
1
  import { enforceBashCommand, recordGateBypass, GateBypassError } from '../../services/sop/gate-enforce-service.js';
2
2
  import { fail, ok } from '../../shared/result.js';
3
3
  import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
- /** Read the PreToolUse hook payload. `PEAKS_HOOK_STDIN` is a test seam; production reads stdin. */
4
+ import { detectIdeFromContext, parseClaudeShapeStdin } from '../../services/ide/hook-translator.js';
5
+ import { formatDecisionResponse } from '../../services/ide/hook-protocol.js';
6
+ import { getAdapter } from '../../services/ide/ide-registry.js';
7
+ /**
8
+ * Read the PreToolUse hook payload. `PEAKS_HOOK_STDIN` is a test seam; production
9
+ * reads stdin. The CLI-side stdin reader is intentionally kept here (not in
10
+ * `hook-translator.ts`) because it owns the `process.stdin` lifecycle and the
11
+ * test-seam env var. The translator operates on already-parsed payloads.
12
+ */
5
13
  async function readHookPayload() {
6
14
  const override = process.env.PEAKS_HOOK_STDIN;
7
15
  if (override !== undefined) {
@@ -20,17 +28,6 @@ async function readHookPayload() {
20
28
  process.stdin.on('error', () => resolveStdin(data));
21
29
  });
22
30
  }
23
- function emitDeny(io, reason) {
24
- // The exact, verified PreToolUse decision shape. permissionDecision:"deny"
25
- // blocks the tool call before Claude Code's permission checks (un-bypassable).
26
- io.stdout(JSON.stringify({
27
- hookSpecificOutput: {
28
- hookEventName: 'PreToolUse',
29
- permissionDecision: 'deny',
30
- permissionDecisionReason: reason
31
- }
32
- }));
33
- }
34
31
  export function registerGateCommands(program, io) {
35
32
  const gate = program.command('gate').description('SOP gate enforcement (PreToolUse hook handler and bypass)');
36
33
  addJsonOption(gate
@@ -41,14 +38,25 @@ export function registerGateCommands(program, io) {
41
38
  // decide must FAIL-OPEN (allow), never block the user's Claude Code.
42
39
  try {
43
40
  const raw = await readHookPayload();
44
- let command;
45
- let toolName;
41
+ let parsedStdin = null;
46
42
  if (raw.trim().length > 0) {
47
- const payload = JSON.parse(raw);
48
- toolName = payload.tool_name;
49
- command = payload.tool_input?.command;
43
+ try {
44
+ parsedStdin = JSON.parse(raw);
45
+ }
46
+ catch {
47
+ // Malformed JSON — fail-open. Detect + parse on null fall back to the
48
+ // default adapter and yield empty tool/command, which short-circuits
49
+ // to the "not a guarded surface" early exit below.
50
+ }
50
51
  }
51
- if (toolName !== 'Bash' || typeof command !== 'string' || command.trim().length === 0) {
52
+ const ide = detectIdeFromContext({ env: process.env, cwd: process.cwd(), parsedStdin });
53
+ const adapter = getAdapter(ide);
54
+ // For slice #1 only the Claude adapter is registered, so the parser is
55
+ // Claude-shaped. Future slices dispatch on `ide` to pick a per-adapter
56
+ // parser; the parser entry-point (`parseXxxShapeStdin`) is the only
57
+ // change required.
58
+ const { toolName, command } = parseClaudeShapeStdin(parsedStdin);
59
+ if (toolName !== adapter.toolMatcher || typeof command !== 'string' || command.trim().length === 0) {
52
60
  // Not a guarded surface — allow (no output = normal permission flow).
53
61
  if (options.json === true) {
54
62
  printResult(io, ok('gate.enforce', { decision: 'allow', skipped: true }), true);
@@ -57,7 +65,8 @@ export function registerGateCommands(program, io) {
57
65
  }
58
66
  const decision = await enforceBashCommand(options.project, command);
59
67
  if (decision.decision === 'deny') {
60
- emitDeny(io, decision.reason);
68
+ const { stdout } = formatDecisionResponse(ide, 'deny', decision.reason);
69
+ io.stdout(stdout);
61
70
  if (options.json === true) {
62
71
  io.stderr(JSON.stringify(ok('gate.enforce', decision)));
63
72
  }
@@ -0,0 +1,17 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ /**
4
+ * `peaks hook handle` —— peaks 自有 hook 协议的单一入口。
5
+ *
6
+ * 该命令是 peaks-cli 拥有的 hook 处理总入口。它:
7
+ * 1. 读 stdin
8
+ * 2. auto-detect 来源 IDE(env / stdin shape / cwd)
9
+ * 3. 归一化到 peaks canonical schema
10
+ * 4. dispatch 到内部 peaks 逻辑(目前:gate enforce 或 progress start)
11
+ * 5. 用 IDE 期望的格式发回决策
12
+ *
13
+ * Slice #1 阶段:peaks hook handle 与 peaks gate enforce / peaks progress start
14
+ * 并存(后者内部走 hook-translator)。Slice #2 把 IDE settings 改成调用
15
+ * peaks hook handle 即可。Slice #3 删除旧命令。
16
+ */
17
+ export declare function registerHookHandleCommand(program: Command, io: ProgramIO): void;
@@ -0,0 +1,111 @@
1
+ import { addJsonOption, printResult } from '../cli-helpers.js';
2
+ import { detectIdeFromContext, parseAdapterStdin, parseClaudeShapeStdin, pluckObject, pluckString } from '../../services/ide/hook-translator.js';
3
+ import { buildCanonicalHook, formatDecisionResponse } from '../../services/ide/hook-protocol.js';
4
+ import { getAdapter } from '../../services/ide/ide-registry.js';
5
+ import { fail, ok } from '../../shared/result.js';
6
+ /**
7
+ * Read the hook payload. `PEAKS_HOOK_STDIN` is a test seam (same convention as
8
+ * `gate-commands.ts`); production reads stdin. The TTY short-circuit means an
9
+ * interactive shell invocation is treated as an empty payload (allow).
10
+ */
11
+ async function readStdin() {
12
+ const override = process.env.PEAKS_HOOK_STDIN;
13
+ if (override !== undefined) {
14
+ return override;
15
+ }
16
+ if (process.stdin.isTTY)
17
+ return '';
18
+ return new Promise((resolveStdin) => {
19
+ let data = '';
20
+ process.stdin.setEncoding('utf8');
21
+ process.stdin.on('data', (chunk) => { data += chunk; });
22
+ process.stdin.on('end', () => resolveStdin(data));
23
+ process.stdin.on('error', () => resolveStdin(data));
24
+ });
25
+ }
26
+ /**
27
+ * `peaks hook handle` —— peaks 自有 hook 协议的单一入口。
28
+ *
29
+ * 该命令是 peaks-cli 拥有的 hook 处理总入口。它:
30
+ * 1. 读 stdin
31
+ * 2. auto-detect 来源 IDE(env / stdin shape / cwd)
32
+ * 3. 归一化到 peaks canonical schema
33
+ * 4. dispatch 到内部 peaks 逻辑(目前:gate enforce 或 progress start)
34
+ * 5. 用 IDE 期望的格式发回决策
35
+ *
36
+ * Slice #1 阶段:peaks hook handle 与 peaks gate enforce / peaks progress start
37
+ * 并存(后者内部走 hook-translator)。Slice #2 把 IDE settings 改成调用
38
+ * peaks hook handle 即可。Slice #3 删除旧命令。
39
+ */
40
+ export function registerHookHandleCommand(program, io) {
41
+ const hook = program.command('hook').description('Peaks 自有 hook 协议单一入口(slice #1 新增;后续 slice 将逐步替代 gate enforce / progress start)');
42
+ addJsonOption(hook
43
+ .command('handle')
44
+ .description('Read stdin hook payload, auto-detect IDE, dispatch to peaks gate/progress logic, output IDE-formatted decision')
45
+ .option('--project <path>', 'project the gates evaluate against (default: current directory)', '.')).action(async (options) => {
46
+ try {
47
+ const raw = await readStdin();
48
+ let parsed = null;
49
+ if (raw.trim().length > 0) {
50
+ try {
51
+ parsed = JSON.parse(raw);
52
+ }
53
+ catch {
54
+ // Malformed JSON — treat as empty payload (fail-open)
55
+ parsed = null;
56
+ }
57
+ }
58
+ const ide = detectIdeFromContext({ env: process.env, cwd: process.cwd(), parsedStdin: parsed });
59
+ const adapter = getAdapter(ide);
60
+ // Slice #3: per-adapter stdin parser dispatch. The detected IDE
61
+ // determines which parser runs; unknown IDEs fall back to the Claude
62
+ // shape (preserves slice #1's "fail-open to Claude" semantics).
63
+ const { toolName, command } = parseAdapterStdin(ide, parsed);
64
+ // Also try the Claude parser as a secondary fallback so a Trae user who
65
+ // accidentally pipes a Claude-shaped payload still gets the right fields.
66
+ const claudeShape = parseClaudeShapeStdin(parsed);
67
+ const fallbackToolName = toolName ?? claudeShape.toolName ?? pluckString(parsed, ['toolName']);
68
+ const fallbackCommand = command ?? claudeShape.command ?? pluckString(parsed, ['toolInput', 'command']);
69
+ const projectRoot = process.env[adapter.envVar] ?? options.project;
70
+ const hook = buildCanonicalHook({
71
+ toolName: fallbackToolName ?? '',
72
+ toolInput: pluckObject(parsed, ['tool_input']) ?? pluckObject(parsed, ['toolInput']) ?? {},
73
+ projectRoot,
74
+ rawIdeFormat: ide,
75
+ rawPayload: parsed
76
+ });
77
+ // Dispatch by toolName. For slice #1, we only handle Bash and Task.
78
+ // Other tools: allow (no-op; future events will be added here).
79
+ if (hook.toolName === 'Bash' && typeof fallbackCommand === 'string' && fallbackCommand.trim().length > 0) {
80
+ // Lazy import to avoid circular: peaks gate enforce logic
81
+ const { enforceBashCommand } = await import('../../services/sop/gate-enforce-service.js');
82
+ const decision = await enforceBashCommand(projectRoot, fallbackCommand);
83
+ if (decision.decision === 'deny') {
84
+ const formatted = formatDecisionResponse(ide, 'deny', decision.reason);
85
+ io.stdout(formatted.stdout);
86
+ if (options.json === true) {
87
+ io.stderr(JSON.stringify(ok('hook.handle', { ide, tool: hook.toolName, decision: 'deny', reason: decision.reason })));
88
+ }
89
+ return;
90
+ }
91
+ }
92
+ else if (hook.toolName === 'Task') {
93
+ // peaks progress start is a fire-and-forget; do not block hook.handle.
94
+ // Slice #1: simply acknowledge (no terminal spawn from hook handle itself;
95
+ // the legacy `peaks progress start` command still does that).
96
+ }
97
+ const allow = formatDecisionResponse(ide, 'allow');
98
+ io.stdout(allow.stdout);
99
+ if (options.json === true) {
100
+ printResult(io, ok('hook.handle', { ide, tool: hook.toolName, decision: 'allow' }), true);
101
+ }
102
+ }
103
+ catch (error) {
104
+ // Fail-open: a bug in hook.handle must not brick Claude Code.
105
+ io.stderr(`hook handle: internal error, allowing command (${error instanceof Error ? error.message : String(error)})`);
106
+ if (options.json === true) {
107
+ printResult(io, fail('hook.handle', 'HOOK_HANDLE_FAILED', error instanceof Error ? error.message : 'unknown', {}), true);
108
+ }
109
+ }
110
+ });
111
+ }
@@ -1,89 +1,140 @@
1
1
  import { fail, ok } from '../../shared/result.js';
2
2
  import { addJsonOption, printResult, getErrorMessage } from '../cli-helpers.js';
3
3
  import { findProjectRoot } from '../../services/config/config-safety.js';
4
- import { applyHookInstall, PEAKS_HOOK_ENTRIES, planHookInstall, readHookStatus, removeHookInstall } from '../../services/skills/hooks-settings-service.js';
4
+ import { applyHookInstall, planHookInstall, readHookStatus, removeHookInstall } from '../../services/skills/hooks-settings-service.js';
5
+ import { detectIdeFromContext } from '../../services/ide/hook-translator.js';
6
+ import { getAdapter } from '../../services/ide/ide-registry.js';
5
7
  function resolveScope(options) {
6
8
  return options.global ? 'global' : 'project';
7
9
  }
8
10
  function resolveProjectRoot(scope, project) {
9
11
  return scope === 'project' ? (project ?? findProjectRoot(process.cwd()) ?? process.cwd()) : undefined;
10
12
  }
13
+ /**
14
+ * Resolve the IDE the install should target. The CLI user can override with
15
+ * `--ide <id>`. Otherwise we delegate to `detectIdeFromContext` which checks
16
+ * `process.env[adapter.envVar]` → stdin shape → cwd `.trae`/`.claude` →
17
+ * fallback `'claude-code'`. Pass `parsedStdin: null` since `peaks hooks
18
+ * install` is not invoked from inside an IDE hook — there's no stdin payload.
19
+ */
20
+ function resolveIdeForCommand(options, projectRoot) {
21
+ if (options.ide !== undefined && options.ide.length > 0) {
22
+ return options.ide;
23
+ }
24
+ return detectIdeFromContext({ env: process.env, cwd: projectRoot ?? process.cwd(), parsedStdin: null });
25
+ }
26
+ // Slice #3: compute the per-IDE peaks hook entries for the CLI response
27
+ // summary. Replaces the slice #1 PEAKS_HOOK_ENTRIES constant which was
28
+ // hardcoded to claude-code values. Slice 2026-06-06-sub-agent-spawn-bug-
29
+ // and-decouple: the sub-agent progress matcher now reads from
30
+ // `adapter.subAgentToolMatcher` instead of being hardcoded to 'Task', so
31
+ // every IDE self-reports its sub-agent tool name (claude-code: 'Task',
32
+ // future adapters: whatever the adapter declares).
33
+ function listInstalledEntriesForIde(ide) {
34
+ const adapter = getAdapter(ide);
35
+ if (ide === 'trae') {
36
+ return [
37
+ { matcher: adapter.toolMatcher, sentinel: 'peaks hook handle' },
38
+ { matcher: adapter.subAgentToolMatcher, sentinel: 'peaks progress start' }
39
+ ];
40
+ }
41
+ // Default (claude-code) and any future registered adapters.
42
+ return [
43
+ { matcher: adapter.toolMatcher, sentinel: 'peaks gate enforce' },
44
+ { matcher: adapter.subAgentToolMatcher, sentinel: 'peaks progress start' }
45
+ ];
46
+ }
11
47
  export function registerHooksCommands(program, io) {
12
48
  const hooks = program
13
49
  .command('hooks')
14
- .description('Manage the Peaks PreToolUse hooks in .claude/settings.json: (1) Bash→peaks gate enforce (SOP gate), (2) Task→peaks progress start (auto-spawn sub-agent progress terminal). Both are installed / removed together.');
50
+ .description("Manage the Peaks-managed hook entries in the adapter's settings.json (default: .claude/settings.json for Claude, .trae/settings.json for Trae): (1) gate-enforce hook (SOP gate), (2) progress-start hook (auto-spawn sub-agent progress terminal). Both are installed / removed together. The IDE is auto-detected from env / cwd; override with --ide <id>.");
15
51
  addJsonOption(hooks
16
52
  .command('install')
17
- .description(`Install all peaks-managed PreToolUse hooks (${PEAKS_HOOK_ENTRIES.map((e) => e.matcher).join(', ')}) into the target .claude/settings.json. Idempotent: re-runs are no-ops. Project scope by default.`)
53
+ .description(`Install all peaks-managed hook entries into the adapter's settings.json. Idempotent: re-runs are no-ops. Project scope by default.`)
18
54
  .option('--global', 'install into the user-level ~/.claude/settings.json instead of the project')
19
55
  .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
56
+ .option('--ide <id>', "target adapter id (claude-code | trae); default: auto-detect from env/cwd")
20
57
  .option('--dry-run', 'show what would change without writing')).action((options) => {
21
58
  const scope = resolveScope(options);
22
59
  const projectRoot = resolveProjectRoot(scope, options.project);
60
+ const ide = resolveIdeForCommand(options, projectRoot);
23
61
  try {
24
62
  if (options.dryRun === true) {
25
- const plan = planHookInstall(scope, projectRoot);
63
+ const plan = planHookInstall(scope, projectRoot, { ide });
64
+ const dryRunEntries = listInstalledEntriesForIde(ide);
26
65
  printResult(io, ok('hooks.install', {
27
66
  ...plan,
67
+ ide,
28
68
  applied: false,
29
69
  dryRun: true,
30
- entries: PEAKS_HOOK_ENTRIES.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel }))
31
- }, [], [`would install ${PEAKS_HOOK_ENTRIES.length} peaks-managed hook entries`]), options.json);
70
+ entries: dryRunEntries
71
+ }, [], [`would install ${dryRunEntries.length} peaks-managed hook entries`]), options.json);
32
72
  return;
33
73
  }
34
- const result = applyHookInstall(scope, projectRoot);
74
+ const result = applyHookInstall(scope, projectRoot, { ide });
75
+ // Slice #3: build the per-IDE entries summary from the actual installed
76
+ // entries, not the slice #1 PEAKS_HOOK_ENTRIES constant (which is the
77
+ // claude-code default). The user's JSON envelope must reflect the IDE
78
+ // they targeted.
79
+ const installedEntries = listInstalledEntriesForIde(ide);
35
80
  const nextActions = result.applied
36
81
  ? [
37
- 'Restart Claude Code (or reload the window) so the PreToolUse hooks take effect',
38
- `Installed: ${PEAKS_HOOK_ENTRIES.map((e) => `${e.matcher}→${e.sentinel}`).join(', ')}`
82
+ 'Restart the IDE (or reload the workspace) so the hook entries take effect',
83
+ `Installed: ${installedEntries.map((e) => `${e.matcher}→${e.sentinel}`).join(', ')}`
39
84
  ]
40
85
  : [];
41
86
  printResult(io, ok('hooks.install', {
42
87
  ...result,
88
+ ide,
43
89
  dryRun: false,
44
- entries: PEAKS_HOOK_ENTRIES.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel }))
90
+ entries: installedEntries.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel }))
45
91
  }, [], nextActions), options.json);
46
92
  }
47
93
  catch (error) {
48
94
  const message = getErrorMessage(error);
49
- printResult(io, fail('hooks.install', 'HOOKS_INSTALL_FAILED', message, { scope, applied: false }, [message]), options.json);
95
+ printResult(io, fail('hooks.install', 'HOOKS_INSTALL_FAILED', message, { scope, ide, applied: false }, [message]), options.json);
50
96
  process.exitCode = 1;
51
97
  }
52
98
  });
53
99
  addJsonOption(hooks
54
100
  .command('uninstall')
55
- .description('Remove all peaks-managed PreToolUse hooks (gate-enforce + progress-start) from the target .claude/settings.json. Third-party hooks are preserved.')
101
+ .description("Remove all peaks-managed hook entries (gate-enforce + progress-start) from the target settings.json. Third-party hooks are preserved.")
56
102
  .option('--global', 'remove from the user-level ~/.claude/settings.json instead of the project')
57
- .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((options) => {
103
+ .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
104
+ .option('--ide <id>', 'target adapter id (claude-code | trae); default: auto-detect from env/cwd')).action((options) => {
58
105
  const scope = resolveScope(options);
59
106
  const projectRoot = resolveProjectRoot(scope, options.project);
107
+ const ide = resolveIdeForCommand(options, projectRoot);
60
108
  try {
61
- const result = removeHookInstall(scope, projectRoot);
62
- printResult(io, ok('hooks.uninstall', result), options.json);
109
+ const result = removeHookInstall(scope, projectRoot, { ide });
110
+ printResult(io, ok('hooks.uninstall', { ...result, ide }), options.json);
63
111
  }
64
112
  catch (error) {
65
113
  const message = getErrorMessage(error);
66
- printResult(io, fail('hooks.uninstall', 'HOOKS_UNINSTALL_FAILED', message, { scope, removed: false }, [message]), options.json);
114
+ printResult(io, fail('hooks.uninstall', 'HOOKS_UNINSTALL_FAILED', message, { scope, ide, removed: false }, [message]), options.json);
67
115
  process.exitCode = 1;
68
116
  }
69
117
  });
70
118
  addJsonOption(hooks
71
119
  .command('status')
72
- .description('Report which peaks-managed PreToolUse hooks are installed (gate-enforce + progress-start).')
120
+ .description('Report which peaks-managed hook entries are installed.')
73
121
  .option('--global', 'inspect the user-level ~/.claude/settings.json instead of the project')
74
- .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((options) => {
122
+ .option('--project <path>', 'project root path (auto-detected from cwd when omitted)')
123
+ .option('--ide <id>', 'target adapter id (claude-code | trae); default: auto-detect from env/cwd')).action((options) => {
75
124
  const scope = resolveScope(options);
76
125
  const projectRoot = resolveProjectRoot(scope, options.project);
126
+ const ide = resolveIdeForCommand(options, projectRoot);
77
127
  try {
78
- const status = readHookStatus(scope, projectRoot);
128
+ const status = readHookStatus(scope, projectRoot, { ide });
79
129
  printResult(io, ok('hooks.status', {
80
130
  ...status,
81
- entries: PEAKS_HOOK_ENTRIES.map((e) => ({ matcher: e.matcher, sentinel: e.sentinel }))
131
+ ide,
132
+ entries: listInstalledEntriesForIde(ide)
82
133
  }), options.json);
83
134
  }
84
135
  catch (error) {
85
136
  const message = getErrorMessage(error);
86
- printResult(io, fail('hooks.status', 'HOOKS_STATUS_FAILED', message, { scope }, [message]), options.json);
137
+ printResult(io, fail('hooks.status', 'HOOKS_STATUS_FAILED', message, { scope, ide }, [message]), options.json);
87
138
  process.exitCode = 1;
88
139
  }
89
140
  });
@@ -15,7 +15,7 @@ export function registerProgressCommands(program, io) {
15
15
  // peaks progress step
16
16
  // LLM-side: called by the LLM on phase transitions. Near-zero
17
17
  // token cost — one Bash call per phase change. Writes
18
- // `.peaks/<sid>/system/subagent-progress.json`. No auto-spawn
18
+ // `.peaks/_sub_agents/<sid>/subagent-progress.json`. No auto-spawn
19
19
  // here; the LLM invokes `peaks progress start` separately when
20
20
  // the user-visible window needs to open.
21
21
  // ─────────────────────────────────────────────────────────────────
@@ -193,7 +193,14 @@ export function registerProgressCommands(program, io) {
193
193
  // asked for visible "this is peaks-cli" branding so the
194
194
  // spawned terminal is identifiable at a glance; the title
195
195
  // also makes `peaks progress close` self-documenting.
196
- const windowTitle = `peaks-cli: sub-agent progress${reasonSuffix}`;
196
+ //
197
+ // Em-dash (U+2014) instead of a colon. On Windows, cmd /c's
198
+ // script parser interprets a `:` even inside quotes as a
199
+ // drive-letter prefix, so `peaks-cli: sub-agent progress`
200
+ // triggers the "Windows 找不到文件 'sub-agent'" dialog. The
201
+ // em-dash is a no-op for cmd / c, bash, and AppleScript
202
+ // string parsing — the visible branding is preserved.
203
+ const windowTitle = `peaks-cli — sub-agent progress${reasonSuffix}`;
197
204
  const watchCommand = `${peaksBin} progress watch --project "${canonical}"`;
198
205
  // Build the platform-specific spawn command + args. This
199
206
  // is extracted to ./progress-start-spawn.ts so the three
@@ -45,9 +45,25 @@ function buildPosixTitleCmd(windowTitle) {
45
45
  const escaped = windowTitle.replaceAll("'", "'\\''");
46
46
  return `printf '\\033]0;${escaped}\\007'`;
47
47
  }
48
- /** cmd.exe `title` builtin call. Quoting is intentionally bare. */
48
+ /**
49
+ * cmd.exe `title` builtin call. Quoting is REQUIRED: cmd /k parses the
50
+ * subsequent tokens as a command line, and a `:` in the unquoted title (the
51
+ * canonical peaks windowTitle starts with `peaks-cli:`) gets mis-read as a
52
+ * drive-letter prefix, triggering Windows' "找不到文件 'sub-agent'" dialog.
53
+ *
54
+ * The double-quote wrap makes the whole title one parameter to `title`.
55
+ * Defensive guard: reject embedded `"`, `\n`, or `\r` (the `cmd /k` parser
56
+ * will not survive a literal newline or an un-escaped quote in the title).
57
+ * Callers (progress-commands.ts:267) compose the title from CLI args; if
58
+ * the user passed a `--reason` containing one of these characters, the
59
+ * spawn attempt is abandoned with a tagged result so the CLI can surface
60
+ * a clear error envelope.
61
+ */
49
62
  function buildWinTitleCmd(windowTitle) {
50
- return `title ${windowTitle}`;
63
+ if (windowTitle.includes('"') || windowTitle.includes('\n') || windowTitle.includes('\r')) {
64
+ return { ok: false, unsupported: true };
65
+ }
66
+ return { ok: true, cmd: `title "${windowTitle}"` };
51
67
  }
52
68
  /** Shared helper: the shell command that runs the watch. */
53
69
  function buildWatchCommand(peaksBin, projectRoot) {
@@ -103,11 +119,21 @@ export function buildStartSpawn(options) {
103
119
  return { ok: true, command: terminal, args: ['-e', bannerShell] };
104
120
  }
105
121
  if (currentPlatform === 'win32') {
106
- const bannerCmd = `${winTitleCmd} && echo peaks-cli --- sub-agent progress && ${watchCommand}`;
122
+ if (!winTitleCmd.ok) {
123
+ return { ok: false, unsupported: true };
124
+ }
125
+ const bannerCmd = `${winTitleCmd.cmd} && echo peaks-cli --- sub-agent progress && ${watchCommand}`;
107
126
  return {
108
127
  ok: true,
109
128
  command: 'cmd',
110
- args: ['/c', 'start', `"${windowTitle}"`, 'cmd', '/k', bannerCmd]
129
+ // Wrap bannerCmd in an EXTRA pair of outer quotes so the OUTER
130
+ // `cmd /c`'s script parser sees the banner as a single arg
131
+ // (not a `&&`-chained script). The INNER `cmd /k` in the new
132
+ // window strips the extra outer quotes and parses the banner
133
+ // as a normal script. windowTitle is left unquoted: Node's
134
+ // spawn applies the correct Windows escaping naturally,
135
+ // which is more robust than pre-quoting.
136
+ args: ['/c', 'start', windowTitle, 'cmd', '/k', `"${bannerCmd}"`]
111
137
  };
112
138
  }
113
139
  return { ok: false, unsupported: true };
@@ -8,9 +8,13 @@ export function registerProjectCommands(program, io) {
8
8
  addJsonOption(project
9
9
  .command('dashboard')
10
10
  .description('One-call snapshot of doctor / MCP / OpenSpec / requests / Understand Anything / capabilities for a project')
11
- .requiredOption('--project <path>', 'target project root')).action(async (options) => {
11
+ .requiredOption('--project <path>', 'target project root')
12
+ .option('--strict', 'ok follows the doctor aggregate (legacy semantics). Default: workspace-only (ok tracks the runbook health)', false)).action(async (options) => {
12
13
  try {
13
- const dashboard = await loadProjectDashboard({ projectRoot: options.project });
14
+ const dashboard = await loadProjectDashboard({
15
+ projectRoot: options.project,
16
+ okPolicy: options.strict === true ? 'strict' : 'workspace-only'
17
+ });
14
18
  if (!dashboard.runbookHealth.ok) {
15
19
  const suggestions = [
16
20
  dashboard.runbookHealth.missingRunbook.length > 0
@@ -29,8 +33,8 @@ export function registerProjectCommands(program, io) {
29
33
  process.exitCode = 1;
30
34
  return;
31
35
  }
32
- if (!dashboard.doctor.ok) {
33
- printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_DOCTOR_FAILED', `Doctor reports ${dashboard.doctor.failed} failed check(s) (${dashboard.doctor.passed} passed)`, dashboard, ['Run `peaks doctor --json` and resolve the failing checks before re-running the dashboard']), options.json);
36
+ if (!dashboard.doctor.ok && options.strict === true) {
37
+ printResult(io, fail('project.dashboard', 'PROJECT_DASHBOARD_DOCTOR_STRICT_FAIL', `Doctor reports ${dashboard.doctor.failed} failed check(s) (${dashboard.doctor.passed} passed) — --strict mode requires the doctor aggregate to pass`, dashboard, ['Run `peaks doctor --json` and resolve the failing checks, or drop --strict to use the workspace-only policy']), options.json);
34
38
  process.exitCode = 1;
35
39
  return;
36
40
  }