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.
- package/README.md +6 -2
- package/dist/src/cli/commands/core-artifact-commands.js +6 -3
- package/dist/src/cli/commands/gate-commands.js +28 -19
- package/dist/src/cli/commands/hook-handle.d.ts +17 -0
- package/dist/src/cli/commands/hook-handle.js +111 -0
- package/dist/src/cli/commands/hooks-commands.js +72 -21
- package/dist/src/cli/commands/progress-commands.js +9 -2
- package/dist/src/cli/commands/progress-start-spawn.js +30 -4
- package/dist/src/cli/commands/project-commands.js +8 -4
- package/dist/src/cli/commands/statusline-commands.js +75 -17
- package/dist/src/cli/commands/sub-agent-commands.d.ts +5 -0
- package/dist/src/cli/commands/sub-agent-commands.js +488 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.d.ts +55 -0
- package/dist/src/cli/commands/sub-agent-dispatch-guard.js +57 -0
- package/dist/src/cli/commands/workflow-commands.js +2 -1
- package/dist/src/cli/commands/workspace-commands.js +3 -0
- package/dist/src/cli/program.js +9 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.d.ts +28 -0
- package/dist/src/hooks/pre-tool-use-sub-agent.js +105 -0
- package/dist/src/services/config/config-types.d.ts +1 -1
- package/dist/src/services/context/artifact-meta.d.ts +72 -0
- package/dist/src/services/context/artifact-meta.js +105 -0
- package/dist/src/services/context/context-guard.d.ts +49 -0
- package/dist/src/services/context/context-guard.js +91 -0
- package/dist/src/services/context/dispatch-context-guard.d.ts +27 -0
- package/dist/src/services/context/dispatch-context-guard.js +192 -0
- package/dist/src/services/context/headroom-client.d.ts +34 -0
- package/dist/src/services/context/headroom-client.js +117 -0
- package/dist/src/services/context/shared-channel.d.ts +92 -0
- package/dist/src/services/context/shared-channel.js +285 -0
- package/dist/src/services/context/threshold.d.ts +35 -0
- package/dist/src/services/context/threshold.js +76 -0
- package/dist/src/services/dashboard/project-dashboard-service.d.ts +23 -0
- package/dist/src/services/dashboard/project-dashboard-service.js +21 -0
- package/dist/src/services/dispatch/batch-counter.d.ts +27 -0
- package/dist/src/services/dispatch/batch-counter.js +85 -0
- package/dist/src/services/dispatch/dispatch-record-writer.d.ts +93 -0
- package/dist/src/services/dispatch/dispatch-record-writer.js +261 -0
- package/dist/src/services/dispatch/heartbeat-truncator.d.ts +26 -0
- package/dist/src/services/dispatch/heartbeat-truncator.js +13 -0
- package/dist/src/services/dispatch/leak-detector.d.ts +11 -0
- package/dist/src/services/dispatch/leak-detector.js +72 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.d.ts +127 -0
- package/dist/src/services/dispatch/sub-agent-dispatcher.js +98 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +80 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +42 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +98 -0
- package/dist/src/services/ide/hook-protocol.d.ts +47 -0
- package/dist/src/services/ide/hook-protocol.js +74 -0
- package/dist/src/services/ide/hook-translator.d.ts +72 -0
- package/dist/src/services/ide/hook-translator.js +128 -0
- package/dist/src/services/ide/ide-detector.d.ts +10 -0
- package/dist/src/services/ide/ide-detector.js +19 -0
- package/dist/src/services/ide/ide-registry.d.ts +14 -0
- package/dist/src/services/ide/ide-registry.js +45 -0
- package/dist/src/services/ide/ide-types.d.ts +180 -0
- package/dist/src/services/ide/ide-types.js +2 -0
- package/dist/src/services/ide/resource-profile.d.ts +52 -0
- package/dist/src/services/ide/resource-profile.js +33 -0
- package/dist/src/services/ide/shared/atomic-json.d.ts +15 -0
- package/dist/src/services/ide/shared/atomic-json.js +58 -0
- package/dist/src/services/ide/shared/safe-path.d.ts +11 -0
- package/dist/src/services/ide/shared/safe-path.js +29 -0
- package/dist/src/services/memory/project-context-service.js +2 -1
- package/dist/src/services/memory/project-memory-service.js +4 -3
- package/dist/src/services/perf/perf-baseline-service.js +2 -1
- package/dist/src/services/progress/progress-service.d.ts +1 -1
- package/dist/src/services/progress/progress-service.js +18 -14
- package/dist/src/services/security/safe-settings-path.d.ts +12 -0
- package/dist/src/services/security/safe-settings-path.js +104 -0
- package/dist/src/services/session/getSessionDir.d.ts +1 -0
- package/dist/src/services/session/getSessionDir.js +27 -0
- package/dist/src/services/session/index.d.ts +1 -0
- package/dist/src/services/session/index.js +1 -0
- package/dist/src/services/signal/cancel-handler.d.ts +14 -0
- package/dist/src/services/signal/cancel-handler.js +76 -0
- package/dist/src/services/skill/resume-detector.d.ts +54 -0
- package/dist/src/services/skill/resume-detector.js +334 -0
- package/dist/src/services/skill/skill-scheduler.d.ts +40 -0
- package/dist/src/services/skill/skill-scheduler.js +53 -0
- package/dist/src/services/skills/hooks-settings-service.d.ts +47 -29
- package/dist/src/services/skills/hooks-settings-service.js +190 -144
- package/dist/src/services/skills/statusline-settings-service.d.ts +33 -6
- package/dist/src/services/skills/statusline-settings-service.js +31 -34
- package/dist/src/services/slice/slice-archive-service.d.ts +20 -0
- package/dist/src/services/slice/slice-archive-service.js +111 -0
- package/dist/src/services/solo/batch-heartbeat-poller.d.ts +51 -0
- package/dist/src/services/solo/batch-heartbeat-poller.js +88 -0
- package/dist/src/services/solo/status-line-renderer.d.ts +34 -0
- package/dist/src/services/solo/status-line-renderer.js +55 -0
- package/dist/src/services/standards/ide-aware-standards-service.d.ts +94 -0
- package/dist/src/services/standards/ide-aware-standards-service.js +89 -0
- package/dist/src/services/standards/project-standards-service.d.ts +1 -2
- package/dist/src/services/workspace/reconcile-service.d.ts +36 -0
- package/dist/src/services/workspace/reconcile-service.js +107 -6
- package/dist/src/services/workspace/reconcile-types.d.ts +12 -0
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/scripts/install-skills.mjs +112 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-ide/references/audit-log-helper.md +52 -0
- package/skills/peaks-qa/SKILL.md +153 -55
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +134 -62
- package/skills/peaks-solo/SKILL.md +124 -37
- package/skills/peaks-solo/references/browser-workflow.md +22 -20
- package/skills/peaks-solo/references/context-governance.md +144 -0
- package/skills/peaks-solo/references/headroom-integration.md +107 -0
- package/skills/peaks-solo/references/runbook.md +3 -3
- package/skills/peaks-solo/references/sub-agent-dispatch.md +261 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +17 -0
- 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
|
|
12
|
-
> -
|
|
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 {
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
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
|
|
45
|
-
let toolName;
|
|
41
|
+
let parsedStdin = null;
|
|
46
42
|
if (raw.trim().length > 0) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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(
|
|
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
|
|
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:
|
|
31
|
-
}, [], [`would install ${
|
|
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
|
|
38
|
-
`Installed: ${
|
|
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:
|
|
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(
|
|
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)')
|
|
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
|
|
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)')
|
|
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
|
-
|
|
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>/
|
|
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
|
-
|
|
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
|
-
/**
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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')
|
|
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({
|
|
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', '
|
|
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
|
}
|