peaks-cli 1.2.6 → 1.2.8

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 (30) hide show
  1. package/README.md +108 -122
  2. package/dist/src/cli/commands/core-artifact-commands.js +36 -1
  3. package/dist/src/cli/commands/perf-commands.d.ts +3 -0
  4. package/dist/src/cli/commands/perf-commands.js +41 -0
  5. package/dist/src/cli/commands/progress-close-kill.d.ts +51 -0
  6. package/dist/src/cli/commands/progress-close-kill.js +152 -0
  7. package/dist/src/cli/commands/progress-commands.d.ts +3 -0
  8. package/dist/src/cli/commands/progress-commands.js +348 -0
  9. package/dist/src/cli/commands/progress-start-spawn.d.ts +59 -0
  10. package/dist/src/cli/commands/progress-start-spawn.js +114 -0
  11. package/dist/src/cli/commands/progress-watch-render.d.ts +80 -0
  12. package/dist/src/cli/commands/progress-watch-render.js +308 -0
  13. package/dist/src/cli/program.js +4 -0
  14. package/dist/src/services/config/config-types.d.ts +20 -0
  15. package/dist/src/services/config/config-types.js +5 -1
  16. package/dist/src/services/perf/perf-baseline-service.d.ts +70 -0
  17. package/dist/src/services/perf/perf-baseline-service.js +213 -0
  18. package/dist/src/services/progress/progress-service.d.ts +179 -0
  19. package/dist/src/services/progress/progress-service.js +276 -0
  20. package/dist/src/services/session/index.d.ts +1 -1
  21. package/dist/src/services/session/index.js +1 -1
  22. package/dist/src/services/session/session-manager.d.ts +53 -8
  23. package/dist/src/services/session/session-manager.js +150 -3
  24. package/dist/src/services/skills/skill-presence-service.d.ts +27 -1
  25. package/dist/src/services/skills/skill-presence-service.js +112 -9
  26. package/dist/src/shared/version.d.ts +1 -1
  27. package/dist/src/shared/version.js +1 -1
  28. package/package.json +6 -2
  29. package/skills/peaks-qa/SKILL.md +13 -0
  30. package/skills/peaks-rd/SKILL.md +76 -0
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # Peaks
2
2
 
3
- Peaks 是一个面向 Claude Code CLI 工具和技能族,把项目治理、工作流规划、受控执行、QA 验证、变更追踪组织成可复用的工程流程。
3
+ Peaks 是一组跑在 Claude Code 里的 **技能(SKILL)家族** ——把项目治理、工作流规划、受控执行、QA 验证、变更追踪组织成可复用的工程流程。
4
+ CLI 是这些技能在背后调用的引擎,负责「门禁 + JSON 契约 + 不可逆动作」。
5
+
6
+ > **一句话定位**:你**用技能(SKILL)工作**,CLI 只是技能用来在 hook、CI、结构化判断等场景下提供机器层保障的底层。
4
7
 
5
8
  ## 安装
6
9
 
@@ -8,170 +11,153 @@ Peaks 是一个面向 Claude Code 的 CLI 工具和技能族,把项目治理
8
11
  npm install -g peaks-cli
9
12
  ```
10
13
 
11
- 安装后,Peaks 会把内置 skills 注册到 Claude Code,你可以在对话里直接调用。
12
-
13
- 验证安装,跑这三条:
14
+ 安装后,Peaks 会把内置的 8 `peaks-*` 技能注册到 Claude Code,会话里直接通过技能名调用即可。
14
15
 
15
- ```bash
16
- peaks -V # prints the version
17
- peaks # shows a quickstart with installed-skill count
18
- peaks doctor # checks skills, config, env in one glance
19
- ```
16
+ ## 5 分钟上手
20
17
 
21
- ## 80 秒上手
18
+ Claude Code 对话里,**直接对 Claude 说「用 X 技能做 Y」** 即可,技能会接管剩下的所有流程:
22
19
 
23
- 1. `peaks` 看一眼有哪些东西
24
- 2. `peaks doctor` 确认环境 OK
25
- 3. `peaks sop init --id my-flow --apply` 创建你的第一个 SOP 骨架
26
- 4. 编辑 `~/.peaks/sops/my-flow/sop.json` 写上你的流程和门禁
27
- 5. `peaks sop register --id my-flow` 注册它
28
- 6. 声明 guard 把不可逆动作绑到 phase,`peaks hooks install` 装上强制 hook
20
+ ```text
21
+ peaks-solo 用全自动模式治理 /path/to/your-project
22
+ peaks-prd 为会员邀请功能整理产品目标、非目标和验收标准
23
+ peaks-rd 分析这次重构的最小实现切片和风险
24
+ peaks-qa 为这次改动设计测试和回归验证清单
25
+ peaks-ui 设计登录页的交互和视觉方案
26
+ peaks-sc 记录这次变更的影响范围、artifact 留存和 commit 边界
27
+ peaks-txt 为当前模块生成上下文胶囊,保留关键决策
28
+ peaks-sop 帮我把"内容发布"流程变成带门禁的 SOP
29
+ ```
29
30
 
30
- 然后让 Claude 在对话里改文件、打算跑被 guard 的命令——它会被当场拦住。详见 [自定义 SOP](#自定义-sop用户自创流程门禁) 一节。如果用自然语言描述流程更顺手,直接用 `peaks-sop` 技能,它全程走上面的 CLI 链。
31
+ 第一次使用?照这 4 步走:
31
32
 
32
- ## 使用 Skills
33
+ 1. Claude Code 里对 Claude 说:**`peaks-solo 分析 /path/to/your-project`**
34
+ 2. 技能会自动跑:`peaks workspace init` → `peaks scan archetype` → 生成 `.peaks/<session-id>/rd/project-scan.md`
35
+ 3. 接着说你要做的需求,技能会按 PRD → RD → UI → QA → SC → TXT 的顺序把流程走完
36
+ 4. 工作流结束时,技能会把所有中间产物留在 `.peaks/<session-id>/`,并把"该记住的事实"写进 `.peaks/memory/`
33
37
 
34
- Claude Code 对话里,直接用 `skill名称 + 自然语言描述` 发起工作流:
38
+ 想要随时确认状态?让 Claude 跑一下:
35
39
 
36
- ```text
37
- peaks-solo 使用全自动模式治理 /path/to/your-project
38
- peaks-prd 为会员邀请功能整理产品目标、非目标和验收标准
39
- peaks-rd 分析这次重构的最小实现切片和风险
40
- peaks-qa 为这次改动设计测试和回归验证清单
41
- peaks-ui 设计登录页面的交互和视觉方案
42
- peaks-sc 记录这次变更的影响范围、artifact 留存和 commit 边界
43
- peaks-txt 为当前模块生成上下文胶囊,保留关键决策
40
+ ```bash
41
+ peaks -V # 版本号
42
+ peaks # 当前 quickstart + 已安装技能数
43
+ peaks doctor --json # 环境/技能/配置一键体检
44
+ peaks skill doctor --json
45
+ peaks project dashboard --project . --json # 当前项目 dashboard
44
46
  ```
45
47
 
46
- 按任务选择对应技能:
48
+ ## 技能家族速查
47
49
 
48
- | 技能 | 用途 | 典型场景 |
50
+ | 技能 | 你用它做什么 | 典型场景 |
49
51
  |------|------|----------|
50
- | `peaks-solo` | 端到端编排入口 | 全流程开发、从需求到上线 |
51
- | `peaks-prd` | 产品目标、非目标、验收标准 | 需求整理、重构目标定义 |
52
- | `peaks-ui` | UI/UX、交互和视觉约束 | 页面设计、交互方案、原型 |
53
- | `peaks-rd` | 研发分析、重构规划、执行契约 | 工程分析、最小实现切片、风险评估 |
54
- | `peaks-qa` | 测试、覆盖率、回归验证 | 测试设计、回归矩阵、验收检查 |
55
- | `peaks-sc` | 变更追踪、commit 边界、artifact 留存 | 影响范围记录、回滚证据 |
56
- | `peaks-txt` | 上下文胶囊、决策记录、知识压缩 | 模块理解、关键决策留存 |
57
-
58
- ### 常用工作流
59
-
60
- **从零到一的新功能:**
61
-
62
- 1. `peaks-prd` 输出功能目标、用户价值、验收标准和非目标
63
- 2. `peaks-rd` 找到最小实现切片和受影响模块
64
- 3. `peaks-ui` 补充交互和视觉方案(UI 相关任务)
65
- 4. `peaks-qa` 定义新增测试和回归测试
66
- 5. `peaks-solo` 端到端编排执行
52
+ | `peaks-solo` | **端到端编排入口**。从需求到上线的全流程,自动协调 `prd/rd/ui/qa/sc/txt` | 全流程开发、从产品文档/PRD 开始到上线、跨多个子任务的批量迭代 |
53
+ | `peaks-prd` | 把模糊的产品意图变成**可验收的 PRD**:目标、非目标、行为保留、验收标准 | 需求整理、PRD 撰写、重构目标定义 |
54
+ | `peaks-rd` | 工程分析 + 重构规划 + 执行契约(覆盖门、规格、风险) | 工程分析、最小实现切片、风险评估、重构规划 |
55
+ | `peaks-ui` | UI/UX 交互和视觉约束、视觉方向、设计系统约束 | 页面设计、交互方案、原型、UI 回归 |
56
+ | `peaks-qa` | 测试设计 + 覆盖率 + 回归验证 + 验收证据 | 测试用例、回归矩阵、验收检查、浏览器 E2E |
57
+ | `peaks-sc` | 变更追踪、commit 边界、artifact 留存、回滚证据 | 影响范围记录、回滚证据、变更控制 |
58
+ | `peaks-txt` | 上下文胶囊、决策记录、知识压缩 | 模块理解、关键决策留存、复盘 |
59
+ | `peaks-sop` | **把你的工作流变成带门禁的 SOP**(不是研发专属) | 内容发布、合规清单、数据 pipeline、运维 runbook、个人流程 |
67
60
 
68
- **既有项目重构:**
61
+ ### 三个常用工作流
69
62
 
70
- 1. `peaks-txt` 生成上下文胶囊,理解当前模块
71
- 2. `peaks-prd` 明确重构目标、非目标和验收标准
72
- 3. `peaks-rd` 分析项目结构、测试、脚本、关键模块和风险
73
- 4. `peaks-qa` 定义回归矩阵和覆盖率门禁
74
- 5. `peaks-solo` 端到端编排执行
75
- 6. `peaks-sc` 记录 impact、retention、boundary
63
+ **新功能(端到端)**
76
64
 
77
- **修 bug:**
65
+ ```text
66
+ peaks-prd → peaks-ui(如果涉及 UI) → peaks-rd → peaks-qa → peaks-sc
67
+ ```
78
68
 
79
- 1. 先复现或定位 bug
80
- 2. `peaks-rd` 生成 root cause、修复策略和回归风险
81
- 3. `peaks-qa` 定义失败用例和验收条件
82
- 4. 先补失败测试,再做最小修复
83
- 5. `peaks-sc` 记录影响范围和边界
69
+ **重构既有项目**
84
70
 
85
- ### 环境检查
71
+ ```text
72
+ peaks-txt(先压缩现状) → peaks-prd(明确目标) →
73
+ peaks-rd(拆最小切片) → peaks-qa(回归矩阵) →
74
+ peaks-solo(编排执行) → peaks-sc(变更证据)
75
+ ```
86
76
 
87
- 使用 skill 之前,建议先确认环境:
77
+ **修 bug**
88
78
 
89
- ```bash
90
- peaks doctor --json
91
- peaks skill doctor --json
79
+ ```text
80
+ peaks-rd(复现 + 根因) → peaks-qa(失败用例 + 验收) → 改代码(先补失败测试) → peaks-sc
92
81
  ```
93
82
 
94
- ## 自定义 SOP(用户自创流程门禁)
83
+ ## 怎么用:技能优先,CLI 是门禁
95
84
 
96
- 除了内置的 `peaks-*` 技能家族,你还能用 `peaks sop` 命令族定义**自己的 SOP**:一组有序阶段(phases)加上绑定在阶段上的门禁(gates)。门禁不通过,就推不进对应阶段——把"流程不丢环节"落到你自己的工作流上。
85
+ Peaks 里的 `peaks <cmd>` CLI **不是日常使用的主要入口**。它的存在有三个理由,全都是机器层保障:
97
86
 
98
- **这是一个通用的流程门禁工具,不限于研发。** 凡是"有先后阶段、进入下一步前必须满足某些可检查条件"的流程都适用——内容发布、合规/审批清单、数据校验管线、入职流程、运维 runbook、个人可重复流程……研发发布只是其中一个例子,往往非研发场景更有价值。
87
+ 1. **不可逆动作的显式 opt-in**(例如 `peaks sop init --apply`、`peaks openspec archive --apply`)—— 这一刀不能靠 LLM"自觉"挥下。
88
+ 2. **结构化 JSON 契约**(`peaks request show ... --json`、`peaks scan archetype ... --json`)—— 让技能读回一个可机读的判决,作为下游决策的输入。
89
+ 3. **hook / CI / 脚本场景下能被程序化调用**(`peaks hooks install`、`peaks gate enforce`)—— 这层机器保障在对话里你看不到,但它把"必须满足门禁才能做 X"这件事从纸面规则变成可执行规则。
99
90
 
100
- > 更顺手的用法:直接用 **`peaks-sop` 技能**——在对话里用自然语言描述你的流程,由 LLM 帮你生成、调试、注册 SOP,无需手写 JSON 或记命令。下面的 CLI 是它底层调用的引擎。
91
+ 技能和 CLI 的关系可以记成一句话:**技能 = 流程的大脑**;**CLI = 流程的骨节**。
101
92
 
102
- ### 不可绕过的门禁(杀手锏)
93
+ ### 你**会**用到的几条 CLI 命令
103
94
 
104
- CI 只能在**合并时**拦,`CLAUDE.md` 里的规矩靠 agent **自觉**。Peaks 能做到 CI 和提示词都做不到的事:在**对话流程中途、面向 agent 本身**把不可逆动作摁住。
105
-
106
- 给某个 phase 声明 **guard**(把一个 Bash 命令绑到该 phase),再装一个 PreToolUse hook:
95
+ 虽然主要工作在技能里完成,但这些 CLI 命令在技能驱动下你也会经常看到被调用,概念上知道有它们就够了:
107
96
 
108
97
  ```bash
109
- # sop.json 里:把「发布」绑定到 git push,并要求正文无 TODO
110
- # "gates": [{ "id":"no-todo","phase":"publish",
111
- # "check":{ "type":"grep","file":"posts/current.md","pattern":"TODO","absent":true } }]
112
- # "guards": [{ "phase":"publish","bash":"git +push" }]
113
- peaks hooks install --project . # 显式 opt-in,只写一条 PreToolUse 条目
98
+ peaks workspace init --project <repo> --json # 创建 .peaks/ 工作区(每个 session 一次)
99
+ peaks scan archetype --project <repo> --json # 探测项目原型(greenfield/legacy-frontend/...)
100
+ peaks request init/show/transition # PRD/RD/QA/SC 的请求状态机
101
+ peaks sop init/lint/check/advance/register # 你的自定义 SOP 生命周期
102
+ peaks hooks install --project <repo> # 装门禁的 PreToolUse hook
103
+ peaks project dashboard --project <repo> --json # 整个项目一眼看完
104
+ peaks project memories --project <repo> --json # 读取 .peaks/memory/ 里的历史决策
114
105
  ```
115
106
 
116
- 此后 agent 正文里还留着 TODO 却想 `git push` 时,Claude Code 收到 `permissionDecision:"deny"`,命令**在任何权限检查之前被拦下——连 `--dangerously-skip-permissions` 都绕不过**。满足门禁后自动放行;紧急情况用 `peaks gate bypass --sop <id> --phase <phase> --reason "<原因>"` 一次性放行(每项目每 SOP 有上限、记原因)。
107
+ 完整命令列表跑 `peaks --help` 即可。
117
108
 
118
- 强制层**故障即放行**(fail-open):Peaks 自身任何内部错误都放行命令,绝不会卡死你的 Claude Code,只有真实门禁失败才拦。装 hook 是显式用户命令,skill 自己永不写 `settings.json`。`peaks hooks status` / `peaks hooks uninstall` 管理它。
109
+ ## 自定义 SOP(把你的流程变成带门禁的工作流)
119
110
 
120
- **团队强制**:把 SOP 用 `peaks sop init/register --project <repo>` 落到**仓库里**(`<repo>/.peaks/sops/`,随仓库提交)。队友 clone 后——哪怕全局 `~/.peaks` 是空的——装上 hook 就被同一套门禁强制。SOP 定义分两层:仓库层(团队共享、随 PR 评审)优先,全局层(你个人跨项目复用)兜底。只放在全局的 SOP 只对你本机生效。
111
+ > **技能入口**:`peaks-sop` 技能
112
+ > 告诉 Claude "帮我把『内容发布』做成一个 SOP",它会引导你定义阶段、设定门禁、调试、注册,全程不用手写 JSON。
121
113
 
122
- **两层定义、执行按项目**:SOP 定义(`sop.json` + 可注册的 `SKILL.md`)可放在**全局** `~/.peaks/sops/`(个人跨项目复用,`init`/`lint`/`register` 默认层)或**仓库** `<repo>/.peaks/sops/`(随仓库提交、团队共享,加 `--project <repo>`);同 id 时**仓库层优先**。运行态(当前阶段、历史)始终按项目落在 `<project>/.peaks/sop-state/<sop-id>/`,各项目独立进度。`check`/`advance` 带 `--project`(默认当前目录)指定对哪个项目执行、用哪层定义。
114
+ 内置的 `peaks-*` 技能家族解决"开箱即用"的需求。但很多工作流是**领域特定的、有先后阶段、进入下一步前必须满足某些可检查条件**的——这种流程用 SOP(Standard Operating Procedure)来表达。
123
115
 
124
- ```bash
125
- # 1. 创建 SOP 骨架到 ~/.peaks/sops(默认预览不落盘,--apply 才写入)
126
- peaks sop init --id team-release --name "Team Release" --apply --json
116
+ `peaks-sop` 技能可以把任何这样的流程变成**带门禁的工作流**:
117
+
118
+ | 领域 | 阶段举例 | 门禁思路 |
119
+ |------|---------|---------|
120
+ | 内容 / 发布 | draft → edit → publish | `file-exists` 草稿;`grep` 没有 `TODO`/`TKTK`;`command` 跑字数/拼写检查 |
121
+ | 合规 / 审批 | prepare → review → sign-off | `file-exists` `approval.md`;`grep` 包含 "Approved" |
122
+ | 数据 pipeline | raw → cleaned → validated | `command` 跑校验脚本,退出码 0 |
123
+ | 运维 / 入职 | request → provision → done | `file-exists` 每个清单产物;`command` 校验配置 |
124
+ | 研发发布(典型但非唯一) | draft → review → ship | `file-exists` CHANGELOG;`grep` 源码里没有 `FIXME`;`command` 跑测试 |
125
+ | 个人流程 | 任何"不要忘步骤 X"的流程 | 把"判断"重新物化成一个文件/文本/退出码 |
127
126
 
128
- # 2. 校验 manifest(门禁 id 唯一、阶段合法、check 字段完整)
129
- peaks sop lint --id team-release --json
127
+ ### 门禁类型
130
128
 
131
- # 3. 注册进全局门禁注册表(--dry-run 预览)
132
- peaks sop register --id team-release --json
129
+ | 类型 | 含义 | 例子 |
130
+ |------|------|------|
131
+ | `file-exists` | 文件存在 → pass | `CHANGELOG.md` 存在 |
132
+ | `grep`(含 `absent`) | 文件内正则匹配 → pass;加 `absent: true` 反转("不准有 X") | "正文里没有 `TODO`" |
133
+ | `command` | 跑命令并按退出码判定(默认拒绝,需 `--allow-commands`) | 跑 `npm test` |
133
134
 
134
- # 4. 列出注册表里所有自定义门禁(内置 peaks-* 门禁永不出现)
135
- peaks sop registry --json
135
+ ### 杀手锏:不可绕过的门禁
136
136
 
137
- # 5. 评估单个门禁(在某项目下,返回 pass / fail / blocked)
138
- peaks sop check --id team-release --gate changelog --project . --json
137
+ CI 只能在**合并时**拦,`CLAUDE.md` 里的规则靠 agent **自觉**。SOP 能做到 CI 和提示词都做不到的事:**在对话中途、面向 agent 本身**把不可逆动作摁住。
139
138
 
140
- # 6. 推进到某阶段——门禁须全过、且不能跳级,否则被真正阻断
141
- peaks sop advance --id team-release --to ship --project . --json
139
+ ```jsonc
140
+ // sop.json
141
+ "guards": [ { "phase": "publish", "bash": "git +push" } ]
142
142
  ```
143
143
 
144
- `sop.json` 示例:
145
-
146
- ```json
147
- {
148
- "id": "team-release",
149
- "name": "Team Release",
150
- "phases": ["draft", "review", "ship"],
151
- "gates": [
152
- { "id": "changelog", "phase": "ship", "check": { "type": "file-exists", "path": "CHANGELOG.md" } },
153
- { "id": "no-fixme", "phase": "review", "check": { "type": "grep", "file": "src/index.ts", "pattern": "FIXME", "absent": true } },
154
- { "id": "tests", "phase": "ship", "check": { "type": "command", "run": ["npm", "test"] } }
155
- ]
156
- }
144
+ ```bash
145
+ peaks hooks install --project <repo> # 显式 opt-in:装一条 PreToolUse 规则
157
146
  ```
158
147
 
159
- 门禁 check 支持三类:
148
+ 之后 agent 在 `publish` 阶段的门禁没全过时还想 `git push`,Claude Code 会收到 `permissionDecision: "deny"`,**在任何权限检查之前就被拦下——连 `--dangerously-skip-permissions` 都绕不过**。满足门禁后自动放行;紧急情况用 `peaks gate bypass --sop <id> --phase <phase> --reason "<原因>"` 一次性放行(每个项目每个 SOP 有上限、记原因)。
160
149
 
161
- | 类型 | 字段 | 含义 |
162
- |------|------|------|
163
- | `file-exists` | `path` | 文件存在 → pass |
164
- | `grep` | `file` + `pattern`(+ `absent`) | 文件内匹配到正则 → pass;加 `absent: true` 则反转——匹配不到才 pass(表达「不准有 X」,纯文本、免 `--allow-commands`、跨平台) |
165
- | `command` | `run`(参数数组)+ `expectExitZero` | 运行命令并按退出码判定 |
166
-
167
- 「不准有 TODO / 占位符 / 遗留项」这类最常见的诉求,优先用 `grep` + `absent: true`,不必动用 `command` 门禁。
168
-
169
- 安全约束:
170
- - `command` 类门禁运行用户定义的命令,**默认拒绝**,必须显式加 `--allow-commands` 才会评估;命令以参数数组执行(无 shell、无注入面)、有超时上限、工作目录锁定项目根。
171
- - `file-exists` / `grep` 的路径锁在项目根内,越界路径返回 `blocked`。
172
- - 有副作用的命令(init/register/advance)都支持 `--dry-run` 预览且不落盘。
173
- - `advance` 还会校验**阶段顺序**:可停留当前阶段、可回退,但不能越过下一个阶段跳级,跳级返回 `SOP_PHASE_SKIP`。
174
- - 被门禁阻断或被判跳级时,可用 `--allow-incomplete --reason "<原因>"` 显式绕过(同时绕过门禁与顺序校验);assisted/strict 模式下还需 `--confirm`,且每个项目内每个 SOP 的绕过次数有上限。
150
+ > **两层定义、执行按项目**:SOP 定义(`sop.json` + `SKILL.md`)可以放在**全局** `~/.peaks/sops/`(个人跨项目复用)或**仓库** `<repo>/.peaks/sops/`(随仓库提交、团队共享;`peaks sop init/register --project <repo>`)。**仓库层优先**于全局层。运行态(当前阶段、历史)按项目落在 `<project>/.peaks/sop-state/<sop-id>/`。
151
+
152
+ ## 工程结构(了解 peaks-cli 本身)
153
+
154
+ ```text
155
+ skills/ # 8 个 SKILL.md(peaks-solo / -prd / -rd / -qa / -ui / -sc / -txt / -sop)
156
+ src/cli/ # CLI 引擎(commands/、services/、hooks/、memory/、sop/、scan/、...)
157
+ bin/peaks.js # 入口
158
+ docs/ # 设计文档
159
+ openspec/ # 内部 OpenSpec 变更提案
160
+ ```
175
161
 
176
162
  ## 许可
177
163
 
@@ -8,7 +8,8 @@ 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 { ensureSession, getSessionMeta, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
11
+ import { ensureSession, getSessionMeta, rotateSessionBinding, setSessionMeta, setSessionTitle, listSessionMetas } from '../../services/session/session-manager.js';
12
+ import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
12
13
  import { findProjectRoot } from '../../services/config/config-safety.js';
13
14
  import { generateProjectContext } from '../../services/memory/project-context-service.js';
14
15
  import { fail, ok } from '../../shared/result.js';
@@ -214,6 +215,40 @@ export function registerCoreAndArtifactCommands(program, io) {
214
215
  process.exitCode = 1;
215
216
  }
216
217
  });
218
+ addJsonOption(session
219
+ .command('rotate')
220
+ .description('Drop the project-level session binding so the next peaks call auto-generates a fresh session id. The on-disk session directory is left intact — only .peaks/.session.json is removed.')
221
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
222
+ .option('--reason <text>', 'human-readable reason for the rotation, recorded in the response data')).action(async (options) => {
223
+ try {
224
+ // Canonicalise the project root before touching the binding.
225
+ // `peaks workspace init` writes the binding with the
226
+ // realpath-resolved projectRoot; if the caller passes a path
227
+ // through a symlink (notably /tmp on macOS, which is a
228
+ // symlink to /private/tmp) without canonicalising here,
229
+ // readSessionFile's strict projectRoot equality check fails
230
+ // and the rotate call reports "no prior binding" even
231
+ // though one exists. The same fix as `workspace init`
232
+ // (b193714): promote the path to the git root, falling back
233
+ // to the heuristic, falling back to cwd verbatim.
234
+ const projectRoot = options.project !== undefined
235
+ ? options.project
236
+ : (findProjectRoot(process.cwd()) ?? process.cwd());
237
+ const canonical = resolveCanonicalProjectRoot(projectRoot);
238
+ const previousSessionId = rotateSessionBinding(canonical);
239
+ printResult(io, ok('session.rotate', {
240
+ previousSessionId,
241
+ ...(options.reason !== undefined ? { reason: options.reason } : {}),
242
+ note: previousSessionId === null
243
+ ? 'No prior binding was present; the project is already unbound.'
244
+ : 'Next ensureSession() call will auto-generate a fresh id. The previous session directory is still on disk at .peaks/<previousSessionId>/.'
245
+ }), options.json);
246
+ }
247
+ catch (error) {
248
+ printResult(io, fail('session.rotate', 'SESSION_ROTATE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and is writable']), options.json);
249
+ process.exitCode = 1;
250
+ }
251
+ });
217
252
  const profile = program.command('profile').description('Manage runtime profiles');
218
253
  addJsonOption(profile.command('list').description('List available profiles')).action((options) => {
219
254
  printResult(io, ok('profile.list', { profiles: listProfiles() }), options.json);
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerPerfCommands(program: Command, io: ProgramIO): void;
@@ -0,0 +1,41 @@
1
+ import { executePerfBaselineInit, resolveProjectRootFromCwd } from '../../services/perf/perf-baseline-service.js';
2
+ import { fail, ok } from '../../shared/result.js';
3
+ import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
4
+ export function registerPerfCommands(program, io) {
5
+ const perf = program.command('perf').description('Manage performance baseline scaffolding for the RD stage');
6
+ addJsonOption(perf
7
+ .command('baseline')
8
+ .description('Scaffold .peaks/<sid>/rd/perf-baseline.md so the RD can record the slice\'s perf numbers in a stable place that QA Gate A4 can diff against. Default dry-run; pass --apply to write.')
9
+ .option('--project <path>', 'target project root (defaults to git root or cwd)')
10
+ .option('--apply', 'write the scaffold into the target project', false)
11
+ .option('--reason <text>', 'human-readable reason for the baseline (recorded in the response data)')).action(async (options) => {
12
+ try {
13
+ const projectRoot = options.project !== undefined
14
+ ? options.project
15
+ : resolveProjectRootFromCwd(process.cwd());
16
+ const result = await executePerfBaselineInit({
17
+ projectRoot,
18
+ apply: options.apply === true,
19
+ ...(options.reason !== undefined ? { reason: options.reason } : {})
20
+ });
21
+ const nextActions = [];
22
+ if (result.sessionId === null) {
23
+ nextActions.push('No peaks session is bound for this project yet. Run `peaks workspace init` (or any peaks skill) first so a session directory exists.');
24
+ }
25
+ else if (result.alreadyInitialized) {
26
+ nextActions.push(`perf-baseline.md already exists; no files were written. Re-run only after a re-measurement if you intend to overwrite.`);
27
+ }
28
+ else if (!result.apply) {
29
+ nextActions.push('Re-run with --apply to write the scaffold.');
30
+ }
31
+ else {
32
+ nextActions.push('Open the file and fill in the Results table — that is the input QA Gate A4 will diff against.');
33
+ }
34
+ printResult(io, ok('perf.baseline', result, [], nextActions), options.json);
35
+ }
36
+ catch (error) {
37
+ printResult(io, fail('perf.baseline', 'PERF_BASELINE_FAILED', getErrorMessage(error), { projectRoot: options.project }, ['Verify the project path exists and is writable']), options.json);
38
+ process.exitCode = 1;
39
+ }
40
+ });
41
+ }
@@ -0,0 +1,51 @@
1
+ /**
2
+ * Best-effort close of a spawned `peaks progress watch`
3
+ * window. Used by `peaks progress close` (manual escape
4
+ * hatch) and by the watch-side auto-exit when the sub-agent
5
+ * hits a terminal phase.
6
+ *
7
+ * The close is best-effort by design: we never throw from
8
+ * individual signals. One failed close primitive is a UX
9
+ * paper cut, not a correctness bug — the caller still clears
10
+ * the spawn record after this returns.
11
+ *
12
+ * Cross-platform strategy:
13
+ *
14
+ * - macOS: pkill the watch process by command pattern
15
+ * (matches the project path, so we never close the
16
+ * wrong window), then send AppleScript to Terminal.app
17
+ * to close the window by `custom title`. Terminal.app
18
+ * is the dominant macOS terminal, and `custom title` is
19
+ * the only stable identifier we can target from outside
20
+ * the running shell.
21
+ * - Linux: pkill the watch process, then try `wmctrl -c
22
+ * peaks-cli-progress` to close the terminal window by
23
+ * WM class (set in `progress start` for alacritty /
24
+ * kitty; gnome-terminal / konsole / xfce4-terminal
25
+ * close on their own when the child exits). wmctrl is
26
+ * not always installed; we silently no-op on
27
+ * "command not found" (exit 127) and surface other
28
+ * errors as warnings.
29
+ * - Windows: `taskkill /F /FI "WINDOWTITLE eq
30
+ * peaks-cli:*"` to kill the cmd.exe wrapper. We use
31
+ * the title prefix because the exact title includes the
32
+ * `--reason` suffix which we do not know here.
33
+ *
34
+ * The kill is intentionally not a single primitive (e.g.
35
+ * `process.kill(-pid, 'SIGTERM')` on the process group).
36
+ * The launcher's PID is the spawn-time PID (osascript on
37
+ * macOS, gnome-terminal on Linux), not the long-lived
38
+ * watch process — and the long-lived process is the one we
39
+ * actually need to terminate to make the terminal close.
40
+ * Targeting by command pattern (pkill) + window title
41
+ * (AppleScript / wmctrl / taskkill) is more reliable than
42
+ * PID chasing across detached children.
43
+ */
44
+ import type { ProgressSpawnRecord } from '../../services/progress/progress-service.js';
45
+ export type KillSpawnedTerminalResult = {
46
+ /** Each signal that was successfully sent. */
47
+ signals: string[];
48
+ /** Soft failures (e.g. pkill matched no process, wmctrl missing). */
49
+ warnings: string[];
50
+ };
51
+ export declare function killSpawnedTerminal(record: ProgressSpawnRecord, canonicalProjectRoot: string, currentPlatform: NodeJS.Platform): Promise<KillSpawnedTerminalResult>;
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Best-effort close of a spawned `peaks progress watch`
3
+ * window. Used by `peaks progress close` (manual escape
4
+ * hatch) and by the watch-side auto-exit when the sub-agent
5
+ * hits a terminal phase.
6
+ *
7
+ * The close is best-effort by design: we never throw from
8
+ * individual signals. One failed close primitive is a UX
9
+ * paper cut, not a correctness bug — the caller still clears
10
+ * the spawn record after this returns.
11
+ *
12
+ * Cross-platform strategy:
13
+ *
14
+ * - macOS: pkill the watch process by command pattern
15
+ * (matches the project path, so we never close the
16
+ * wrong window), then send AppleScript to Terminal.app
17
+ * to close the window by `custom title`. Terminal.app
18
+ * is the dominant macOS terminal, and `custom title` is
19
+ * the only stable identifier we can target from outside
20
+ * the running shell.
21
+ * - Linux: pkill the watch process, then try `wmctrl -c
22
+ * peaks-cli-progress` to close the terminal window by
23
+ * WM class (set in `progress start` for alacritty /
24
+ * kitty; gnome-terminal / konsole / xfce4-terminal
25
+ * close on their own when the child exits). wmctrl is
26
+ * not always installed; we silently no-op on
27
+ * "command not found" (exit 127) and surface other
28
+ * errors as warnings.
29
+ * - Windows: `taskkill /F /FI "WINDOWTITLE eq
30
+ * peaks-cli:*"` to kill the cmd.exe wrapper. We use
31
+ * the title prefix because the exact title includes the
32
+ * `--reason` suffix which we do not know here.
33
+ *
34
+ * The kill is intentionally not a single primitive (e.g.
35
+ * `process.kill(-pid, 'SIGTERM')` on the process group).
36
+ * The launcher's PID is the spawn-time PID (osascript on
37
+ * macOS, gnome-terminal on Linux), not the long-lived
38
+ * watch process — and the long-lived process is the one we
39
+ * actually need to terminate to make the terminal close.
40
+ * Targeting by command pattern (pkill) + window title
41
+ * (AppleScript / wmctrl / taskkill) is more reliable than
42
+ * PID chasing across detached children.
43
+ */
44
+ import { execFile } from 'node:child_process';
45
+ import { promisify } from 'node:util';
46
+ import { getErrorMessage } from '../cli-helpers.js';
47
+ const execFileAsync = promisify(execFile);
48
+ export async function killSpawnedTerminal(record, canonicalProjectRoot, currentPlatform) {
49
+ const signals = [];
50
+ const warnings = [];
51
+ // The watch command we spawned, escaped for use as a pkill
52
+ // pattern. We anchor on `progress watch` (NOT `peaks progress
53
+ // watch`) because the actual cmdline is `.../peaks.js progress
54
+ // watch --project /path` — the literal substring
55
+ // `peaks progress watch` does NOT appear in the cmdline
56
+ // (there is a `.js` between `peaks` and `progress`).
57
+ // Anchoring on the verb + the project path is specific
58
+ // enough to not hit any user-owned `progress watch` process
59
+ // for a different project.
60
+ const watchPattern = `progress watch.*--project ${canonicalProjectRoot.replace(/[\\"\s]/g, '\\$&')}`;
61
+ if (currentPlatform === 'darwin') {
62
+ // pkill exit codes: 0 = matched & signalled, 1 = no processes
63
+ // matched (silent miss), 2 = syntax error (warning), 3 = fatal
64
+ // (warning). macOS pkill writes nothing to stderr on a clean
65
+ // miss, so the exit code is the only signal we have.
66
+ await trySignal('pkill', ['-f', watchPattern], signals, 'pkill-watch', warnings, /no.*process/i, new Set([1]));
67
+ // AppleScript to close the Terminal.app window by
68
+ // custom title. We use `every window whose custom title
69
+ // is` so we only close the right tab. AppleScript returns
70
+ // a non-zero exit when the window is already gone, the
71
+ // app is not running, or the title does not match — all
72
+ // of which are silent misses from the user's perspective
73
+ // (the user-facing outcome is identical to the success
74
+ // case: the window is no longer visible). Treat any
75
+ // non-zero exit as silent.
76
+ try {
77
+ const escapedTitle = record.windowTitle.replaceAll('\\', '\\\\').replaceAll('"', '\\"');
78
+ await execFileAsync('osascript', [
79
+ '-e',
80
+ `tell application "Terminal" to close (every window whose custom title is "${escapedTitle}")`
81
+ ]);
82
+ signals.push('osascript-close-window');
83
+ }
84
+ catch {
85
+ // Silent miss. See comment above.
86
+ }
87
+ }
88
+ else if (currentPlatform === 'linux') {
89
+ // Same pkill exit code semantics as macOS.
90
+ await trySignal('pkill', ['-f', watchPattern], signals, 'pkill-watch', warnings, /no.*process/i, new Set([1]));
91
+ // wmctrl by WM class (set in `progress start`). Missing
92
+ // wmctrl is silent (exit 127) — most distros ship it but
93
+ // headless / minimal installs do not.
94
+ await trySignal('wmctrl', ['-c', 'peaks-cli-progress'], signals, 'wmctrl-close-class', warnings, /not found|No such file/i, new Set([127]));
95
+ }
96
+ else if (currentPlatform === 'win32') {
97
+ // Title prefix is set in `progress start` to `peaks-cli:`.
98
+ // We match the prefix because the full title includes
99
+ // the `--reason` suffix which we do not know here.
100
+ // taskkill exit codes: 0 = success, 1 = no tasks matched
101
+ // (silent miss — the window is already gone), 128 = error.
102
+ const titlePrefix = 'peaks-cli:';
103
+ await trySignal('taskkill', ['/F', '/FI', `WINDOWTITLE eq ${titlePrefix}*`], signals, 'taskkill-window-title', warnings, /no.*task/i, new Set([1]));
104
+ }
105
+ else {
106
+ warnings.push(`unsupported platform: ${currentPlatform}`);
107
+ }
108
+ return { signals, warnings };
109
+ }
110
+ /**
111
+ * Run a single close primitive. If it throws AND either
112
+ * (a) the error matches the "expected" stderr pattern
113
+ * (e.g. "no process matched" for pkill, "command not
114
+ * found" for wmctrl) — most platforms print this on
115
+ * stderr; or
116
+ * (b) the exit code is in `silentMissExitCodes` (pkill 1,
117
+ * wmctrl 127, taskkill 1) — the primitive ran, found
118
+ * nothing, and is not telling us via stderr,
119
+ * we silently no-op — that is the success case for the
120
+ * primitive. Other errors are appended to `warnings` for
121
+ * the caller to surface. On a clean resolve, the named
122
+ * signal is appended to `signals`.
123
+ */
124
+ async function trySignal(command, args, signals, signal, warnings, expectedFailurePattern, silentMissExitCodes) {
125
+ try {
126
+ await execFileAsync(command, args);
127
+ }
128
+ catch (error) {
129
+ // execFile's error object exposes `code` as either a
130
+ // numeric exit code (when the process ran) or a string
131
+ // system code like 'ENOENT' (when the binary itself
132
+ // is missing). Only numeric exit codes are candidates
133
+ // for silent-miss.
134
+ const execError = error;
135
+ if (typeof execError.code === 'number' && silentMissExitCodes.has(execError.code)) {
136
+ // Exit code says "ran, but found nothing to act on".
137
+ // The user-facing outcome is identical to the success
138
+ // case, so do not surface a warning.
139
+ return;
140
+ }
141
+ const message = getErrorMessage(error);
142
+ if (expectedFailurePattern.test(message)) {
143
+ return;
144
+ }
145
+ warnings.push(`${command}: ${message}`);
146
+ return;
147
+ }
148
+ // Reached only if execFile resolves (exit 0). All three
149
+ // primitives exit non-zero on a miss, so a clean resolve
150
+ // means the signal landed.
151
+ signals.push(signal);
152
+ }
@@ -0,0 +1,3 @@
1
+ import { Command } from 'commander';
2
+ import { type ProgramIO } from '../cli-helpers.js';
3
+ export declare function registerProgressCommands(program: Command, io: ProgramIO): void;