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.
- package/README.md +6 -2
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/core-artifact-commands.js +49 -11
- 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/slice-commands.js +4 -2
- 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/workspace-commands.js +70 -14
- 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/artifacts/artifact-prerequisites.d.ts +12 -0
- package/dist/src/services/artifacts/artifact-prerequisites.js +39 -8
- package/dist/src/services/artifacts/request-artifact-service.js +116 -76
- 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/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/doctor/doctor-service.d.ts +62 -0
- package/dist/src/services/doctor/doctor-service.js +276 -1
- package/dist/src/services/ide/adapters/claude-code-adapter.d.ts +18 -0
- package/dist/src/services/ide/adapters/claude-code-adapter.js +53 -0
- package/dist/src/services/ide/adapters/trae-adapter.d.ts +34 -0
- package/dist/src/services/ide/adapters/trae-adapter.js +70 -0
- package/dist/src/services/ide/hook-protocol.d.ts +44 -0
- package/dist/src/services/ide/hook-protocol.js +71 -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 +120 -0
- package/dist/src/services/ide/ide-types.js +2 -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/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/session-manager.d.ts +22 -1
- package/dist/src/services/session/session-manager.js +137 -28
- 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/slice/slice-check-service.js +20 -1
- package/dist/src/services/slice/slice-check-types.d.ts +9 -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/workspace/migrate-service.js +124 -2
- package/dist/src/services/workspace/migrate-types.d.ts +50 -7
- package/dist/src/services/workspace/reconcile-service.d.ts +69 -0
- package/dist/src/services/workspace/reconcile-service.js +267 -48
- package/dist/src/services/workspace/reconcile-types.d.ts +37 -0
- package/dist/src/services/workspace/workspace-service.js +29 -62
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +2 -1
- package/schemas/doctor-report.schema.json +2 -2
- package/skills/peaks-ide/SKILL.md +159 -0
- package/skills/peaks-qa/SKILL.md +58 -1
- package/skills/peaks-qa/references/qa-fanout-contract.md +150 -0
- package/skills/peaks-rd/SKILL.md +52 -9
- package/skills/peaks-solo/SKILL.md +83 -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 +218 -0
- package/skills/peaks-solo/references/swarm-dispatch-contract.md +3 -37
- package/skills/peaks-txt/SKILL.md +19 -0
- package/skills/peaks-ui/SKILL.md +28 -1
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 **不是日常使用的主要入口**。它的存在有三个理由,全都是机器层保障:
|
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -8,7 +8,7 @@ import { runDoctor } from '../../services/doctor/doctor-service.js';
|
|
|
8
8
|
import { listSkills } from '../../services/skills/skill-registry.js';
|
|
9
9
|
import { inspectSkillRunbook } from '../../services/skills/skill-runbook-service.js';
|
|
10
10
|
import { setSkillPresence, clearSkillPresence, getSkillPresence, isSkillPresenceMode, touchSkillHeartbeat } from '../../services/skills/skill-presence-service.js';
|
|
11
|
-
import {
|
|
11
|
+
import { getSessionId, getSessionMeta, rotateSessionBinding, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
|
|
12
12
|
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
13
13
|
import { findProjectRoot } from '../../services/config/config-safety.js';
|
|
14
14
|
import { generateProjectContext } from '../../services/memory/project-context-service.js';
|
|
@@ -120,7 +120,7 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
120
120
|
.description('Set the currently active Peaks skill for session-wide visibility')
|
|
121
121
|
.option('--mode <mode>', 'execution mode')
|
|
122
122
|
.option('--gate <gate>', 'current gate')
|
|
123
|
-
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action(
|
|
123
|
+
.option('--project <path>', 'project root path (auto-detected from cwd when omitted)')).action((name, options) => {
|
|
124
124
|
const projectRoot = options.project ?? findProjectRoot(process.cwd()) ?? process.cwd();
|
|
125
125
|
if (options.mode !== undefined && !isSkillPresenceMode(options.mode)) {
|
|
126
126
|
printResult(io, fail('skill.presence:set', 'INVALID_MODE', `Invalid mode: ${options.mode} (expected one of: full-auto, assisted, swarm, strict)`, { name, mode: options.mode }, ['Use a valid mode: full-auto, assisted, swarm, or strict']), options.json);
|
|
@@ -128,13 +128,26 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
128
128
|
return;
|
|
129
129
|
}
|
|
130
130
|
const presence = setSkillPresence(name, options.mode, options.gate, options.project);
|
|
131
|
-
//
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
131
|
+
// As of slice 003-2026-06-06-session-layout-canonicalize we do NOT
|
|
132
|
+
// call `ensureSession` here. The CLI wrapper previously spawned a
|
|
133
|
+
// new session on every presence call, which made the canonical
|
|
134
|
+
// session binding drift (the LLM saw the session id change every
|
|
135
|
+
// turn). The presence now reuses the session bound at
|
|
136
|
+
// `.peaks/_runtime/session.json` (or the legacy `.peaks/.session.json`
|
|
137
|
+
// during the back-compat window). If no session is bound, the
|
|
138
|
+
// presence still writes the active-skill marker — downstream code
|
|
139
|
+
// can `peaks workspace init` separately to create the session.
|
|
140
|
+
//
|
|
141
|
+
// Session metadata is updated when a session is bound (read-only
|
|
142
|
+
// path: `getSessionId`). We do not auto-spawn a session.
|
|
143
|
+
const boundSessionId = getSessionId(projectRoot);
|
|
144
|
+
if (boundSessionId !== null) {
|
|
145
|
+
setSessionMeta(projectRoot, boundSessionId, {
|
|
146
|
+
skill: name,
|
|
147
|
+
...(options.mode ? { mode: options.mode } : {}),
|
|
148
|
+
...(options.gate ? { gate: options.gate } : {})
|
|
149
|
+
});
|
|
150
|
+
}
|
|
138
151
|
printResult(io, ok('skill.presence:set', { active: true, ...presence }), options.json);
|
|
139
152
|
});
|
|
140
153
|
addJsonOption(skill
|
|
@@ -191,9 +204,34 @@ export function registerCoreAndArtifactCommands(program, io) {
|
|
|
191
204
|
printResult(io, ok('session.list', { sessions: metas, total: metas.length }), options.json);
|
|
192
205
|
});
|
|
193
206
|
addJsonOption(session
|
|
194
|
-
.command('info
|
|
195
|
-
.description('Show full metadata for a session directory
|
|
207
|
+
.command('info [sessionId]')
|
|
208
|
+
.description('Show full metadata for a session directory. Pass --active to resolve the canonical binding from .peaks/_runtime/session.json (the "one command a sub-agent runs to find the parent\'s sid" primitive).')
|
|
209
|
+
.option('--active', 'resolve the canonical session id from .peaks/_runtime/session.json (ignores [sessionId] when set)')).action(async (sessionId, options) => {
|
|
196
210
|
const projectRoot = findProjectRoot(process.cwd()) ?? process.cwd();
|
|
211
|
+
// Slice 007 — sub-agent session sharing. A sub-agent that does
|
|
212
|
+
// not know the parent's sid reads it from the binding via
|
|
213
|
+
// `peaks session info --active`. The call uses the
|
|
214
|
+
// canonicalize-on-read path so a stored "projectRoot: '.'" and a
|
|
215
|
+
// caller-passed absolute realpath both resolve to the same
|
|
216
|
+
// binding. Without this primitive the sub-agent has no way to
|
|
217
|
+
// discover the parent sid short of scanning the filesystem.
|
|
218
|
+
if (options.active === true) {
|
|
219
|
+
// Import lazily to avoid a cycle with workspace-commands.
|
|
220
|
+
const { getSessionIdCanonical } = await import('../../services/session/session-manager.js');
|
|
221
|
+
const activeSid = getSessionIdCanonical(projectRoot);
|
|
222
|
+
if (activeSid === null) {
|
|
223
|
+
printResult(io, fail('session.info', 'NO_ACTIVE_BINDING', 'No canonical session binding at .peaks/_runtime/session.json', { projectRoot }, ['Run `peaks workspace init` or `peaks skill presence:set` to anchor a session']), options.json);
|
|
224
|
+
process.exitCode = 1;
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
printResult(io, ok('session.info', { active: true, sessionId: activeSid, source: '.peaks/_runtime/session.json' }), options.json);
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
if (sessionId === undefined) {
|
|
231
|
+
printResult(io, fail('session.info', 'SESSION_ID_REQUIRED', 'session.info requires a <sessionId> or --active', {}, ['Pass a <sessionId> argument, or use --active to resolve the canonical binding']), options.json);
|
|
232
|
+
process.exitCode = 1;
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
197
235
|
const meta = getSessionMeta(projectRoot, sessionId);
|
|
198
236
|
if (meta === null) {
|
|
199
237
|
printResult(io, fail('session.info', 'SESSION_NOT_FOUND', `Session "${sessionId}" not found or has no metadata`, { sessionId }, ['Use `peaks session list` to see available sessions']), options.json);
|
|
@@ -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 };
|
|
@@ -16,14 +16,16 @@ export function registerSliceCommands(program, io) {
|
|
|
16
16
|
.option('--project <path>', 'target project root', '.')
|
|
17
17
|
.option('--rid <rid>', 'request id; defaults to the active current-change binding')
|
|
18
18
|
.option('--refresh-fanout', 're-run the 3-way review fan-out (peaks-rd) even if the review files already exist', false)
|
|
19
|
-
.option('--skip-tests', 'skip the unit-test stage (e.g. docs-only slices)', false)
|
|
19
|
+
.option('--skip-tests', 'skip the unit-test stage (e.g. docs-only slices)', false)
|
|
20
|
+
.option('--allow-pre-existing-failures', 'opt-in: if the unit-test stage fails, report it as `skipped` with a reason naming the failure count (useful when the repo has unrelated pre-existing failures; the long-term fix is to .skip or coverage.exclude those tests)', false)).action(async (options) => {
|
|
20
21
|
try {
|
|
21
22
|
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
22
23
|
const result = await sliceCheck({
|
|
23
24
|
projectRoot,
|
|
24
25
|
...(options.rid ? { rid: options.rid } : {}),
|
|
25
26
|
refreshFanout: options.refreshFanout === true,
|
|
26
|
-
skipTests: options.skipTests === true
|
|
27
|
+
skipTests: options.skipTests === true,
|
|
28
|
+
allowPreExistingFailures: options.allowPreExistingFailures === true
|
|
27
29
|
});
|
|
28
30
|
const warnings = [];
|
|
29
31
|
if (result.stages.some((s) => s.status === 'fail')) {
|