peaks-cli 1.2.5 → 1.2.7
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 +108 -122
- package/bin/peaks.js +0 -0
- package/dist/src/cli/commands/workspace-commands.js +14 -2
- package/dist/src/services/config/config-safety.d.ts +26 -0
- package/dist/src/services/config/config-safety.js +76 -0
- package/dist/src/services/config/config-service.d.ts +1 -1
- package/dist/src/services/config/config-service.js +2 -2
- package/dist/src/shared/version.d.ts +1 -1
- package/dist/src/shared/version.js +1 -1
- package/package.json +1 -1
- package/skills/peaks-solo/SKILL.md +1 -1
- package/skills/peaks-solo/references/a2a-artifact-mapping.md +115 -0
package/README.md
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
# Peaks
|
|
2
2
|
|
|
3
|
-
Peaks
|
|
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
|
|
12
|
-
|
|
13
|
-
验证安装,跑这三条:
|
|
14
|
+
安装后,Peaks 会把内置的 8 个 `peaks-*` 技能注册到 Claude Code,会话里直接通过技能名调用即可。
|
|
14
15
|
|
|
15
|
-
|
|
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
|
-
|
|
18
|
+
在 Claude Code 对话里,**直接对 Claude 说「用 X 技能做 Y」** 即可,技能会接管剩下的所有流程:
|
|
22
19
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
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
|
-
|
|
31
|
+
第一次使用?照这 4 步走:
|
|
31
32
|
|
|
32
|
-
|
|
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
|
-
|
|
38
|
+
想要随时确认状态?让 Claude 跑一下:
|
|
35
39
|
|
|
36
|
-
```
|
|
37
|
-
peaks-
|
|
38
|
-
peaks
|
|
39
|
-
peaks
|
|
40
|
-
peaks
|
|
41
|
-
peaks
|
|
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-
|
|
53
|
-
| `peaks-
|
|
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
|
-
|
|
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
|
-
|
|
65
|
+
```text
|
|
66
|
+
peaks-prd → peaks-ui(如果涉及 UI) → peaks-rd → peaks-qa → peaks-sc
|
|
67
|
+
```
|
|
78
68
|
|
|
79
|
-
|
|
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
|
-
|
|
77
|
+
**修 bug**
|
|
88
78
|
|
|
89
|
-
```
|
|
90
|
-
peaks
|
|
91
|
-
peaks skill doctor --json
|
|
79
|
+
```text
|
|
80
|
+
peaks-rd(复现 + 根因) → peaks-qa(失败用例 + 验收) → 改代码(先补失败测试) → peaks-sc
|
|
92
81
|
```
|
|
93
82
|
|
|
94
|
-
##
|
|
83
|
+
## 怎么用:技能优先,CLI 是门禁
|
|
95
84
|
|
|
96
|
-
|
|
85
|
+
Peaks 里的 `peaks <cmd>` CLI **不是日常使用的主要入口**。它的存在有三个理由,全都是机器层保障:
|
|
97
86
|
|
|
98
|
-
|
|
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
|
-
|
|
91
|
+
技能和 CLI 的关系可以记成一句话:**技能 = 流程的大脑**;**CLI = 流程的骨节**。
|
|
101
92
|
|
|
102
|
-
###
|
|
93
|
+
### 你**会**用到的几条 CLI 命令
|
|
103
94
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
给某个 phase 声明 **guard**(把一个 Bash 命令绑到该 phase),再装一个 PreToolUse hook:
|
|
95
|
+
虽然主要工作在技能里完成,但这些 CLI 命令在技能驱动下你也会经常看到被调用,概念上知道有它们就够了:
|
|
107
96
|
|
|
108
97
|
```bash
|
|
109
|
-
#
|
|
110
|
-
#
|
|
111
|
-
#
|
|
112
|
-
#
|
|
113
|
-
peaks hooks install --project
|
|
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
|
-
|
|
107
|
+
完整命令列表跑 `peaks --help` 即可。
|
|
117
108
|
|
|
118
|
-
|
|
109
|
+
## 自定义 SOP(把你的流程变成带门禁的工作流)
|
|
119
110
|
|
|
120
|
-
|
|
111
|
+
> **技能入口**:`peaks-sop` 技能
|
|
112
|
+
> 告诉 Claude "帮我把『内容发布』做成一个 SOP",它会引导你定义阶段、设定门禁、调试、注册,全程不用手写 JSON。
|
|
121
113
|
|
|
122
|
-
|
|
114
|
+
内置的 `peaks-*` 技能家族解决"开箱即用"的需求。但很多工作流是**领域特定的、有先后阶段、进入下一步前必须满足某些可检查条件**的——这种流程用 SOP(Standard Operating Procedure)来表达。
|
|
123
115
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
-
|
|
129
|
-
peaks sop lint --id team-release --json
|
|
127
|
+
### 门禁类型
|
|
130
128
|
|
|
131
|
-
|
|
132
|
-
|
|
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
|
-
|
|
135
|
-
peaks sop registry --json
|
|
135
|
+
### 杀手锏:不可绕过的门禁
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
peaks sop check --id team-release --gate changelog --project . --json
|
|
137
|
+
CI 只能在**合并时**拦,`CLAUDE.md` 里的规则靠 agent **自觉**。SOP 能做到 CI 和提示词都做不到的事:**在对话中途、面向 agent 本身**把不可逆动作摁住。
|
|
139
138
|
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
```jsonc
|
|
140
|
+
// sop.json
|
|
141
|
+
"guards": [ { "phase": "publish", "bash": "git +push" } ]
|
|
142
142
|
```
|
|
143
143
|
|
|
144
|
-
|
|
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
|
-
|
|
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
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
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
|
|
package/bin/peaks.js
CHANGED
|
File without changes
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { initWorkspace, InvalidSessionIdError, ConflictingSessionError } from '../../services/workspace/workspace-service.js';
|
|
2
2
|
import { ensureSession } from '../../services/session/session-manager.js';
|
|
3
|
+
import { resolveCanonicalProjectRoot } from '../../services/config/config-service.js';
|
|
3
4
|
import { fail, ok } from '../../shared/result.js';
|
|
4
5
|
import { addJsonOption, getErrorMessage, printResult } from '../cli-helpers.js';
|
|
5
6
|
export function registerWorkspaceCommands(program, io) {
|
|
@@ -18,15 +19,26 @@ export function registerWorkspaceCommands(program, io) {
|
|
|
18
19
|
// - omitted: defer to ensureSession(), which reuses an existing
|
|
19
20
|
// binding or auto-generates a fresh one. The init then writes
|
|
20
21
|
// .session.json so the binding sticks.
|
|
22
|
+
//
|
|
23
|
+
// Before that: canonicalise the project root. If the user (or the
|
|
24
|
+
// LLM via "$(pwd)") passed a sub-directory of a real git repo
|
|
25
|
+
// (e.g. prompt-project/prompt-project/ inside the outer
|
|
26
|
+
// prompt-project/.git), promote the path to the git root. Without
|
|
27
|
+
// this, peaks would build a parallel .peaks/ tree under the
|
|
28
|
+
// nested sub-folder and silently break the project-binding model
|
|
29
|
+
// (the same regression that produced prompt-project/.peaks/ in
|
|
30
|
+
// the 5/27-5/29 sessions). When startPath is not inside any
|
|
31
|
+
// git repo, the helper falls through to the cwd verbatim.
|
|
32
|
+
const projectRoot = resolveCanonicalProjectRoot(options.project);
|
|
21
33
|
let sessionId;
|
|
22
34
|
if (options.sessionId !== undefined && options.sessionId.length > 0) {
|
|
23
35
|
sessionId = options.sessionId;
|
|
24
36
|
}
|
|
25
37
|
else {
|
|
26
|
-
sessionId = await ensureSession(
|
|
38
|
+
sessionId = await ensureSession(projectRoot);
|
|
27
39
|
}
|
|
28
40
|
const report = await initWorkspace({
|
|
29
|
-
projectRoot
|
|
41
|
+
projectRoot,
|
|
30
42
|
sessionId,
|
|
31
43
|
allowSessionRebind: options.allowSessionRebind === true
|
|
32
44
|
});
|
|
@@ -2,6 +2,32 @@ export declare function getUserConfigPath(): string;
|
|
|
2
2
|
export declare function isInsidePath(childPath: string, parentPath: string): boolean;
|
|
3
3
|
export declare function findProjectRoot(startPath: string): string | null;
|
|
4
4
|
export declare function resolveProjectRootForConfig(startPath: string): string;
|
|
5
|
+
/**
|
|
6
|
+
* Canonicalise a user-supplied project root path against git's view of the
|
|
7
|
+
* repository root. This is the fix for the nested-directory regression
|
|
8
|
+
* where peaks-cli would write `.peaks/` under a nested sub-folder
|
|
9
|
+
* (e.g. `prompt-project/prompt-project/.peaks/`) because the LLM passed
|
|
10
|
+
* `$(pwd)` from inside a sub-directory of a real git repo. Without
|
|
11
|
+
* canonicalisation, peaks accepted the cwd as-is, built the .peaks/
|
|
12
|
+
* tree there, and left the team with two parallel state stores.
|
|
13
|
+
*
|
|
14
|
+
* Strategy:
|
|
15
|
+
* 1. If `startPath` (or any ancestor) is inside a git repo, return
|
|
16
|
+
* `git rev-parse --show-toplevel` from `startPath`. The git root
|
|
17
|
+
* is the *only* correct answer for "where does the .peaks/ tree
|
|
18
|
+
* belong?" — sub-folders of a git repo are not their own projects.
|
|
19
|
+
* 2. If `startPath` is not inside a git repo, fall back to
|
|
20
|
+
* `findProjectRoot` (the existing heuristic) so the CLI still
|
|
21
|
+
* works for non-git projects.
|
|
22
|
+
* 3. If both fail, return `startPath` unchanged — better to write
|
|
23
|
+
* to the cwd than to refuse the command.
|
|
24
|
+
*
|
|
25
|
+
* This is intentionally fail-open: it only *promotes* a path towards
|
|
26
|
+
* the git root, it never demotes one. A non-git user is unaffected.
|
|
27
|
+
* The function does NOT throw on a missing git binary or a non-zero
|
|
28
|
+
* `git rev-parse` exit; both fall through to the heuristic.
|
|
29
|
+
*/
|
|
30
|
+
export declare function resolveCanonicalProjectRoot(startPath: string): string;
|
|
5
31
|
export declare function getProjectConfigPath(projectRoot: string | null): string | null;
|
|
6
32
|
export declare function getProjectBootstrapConfigPath(projectRoot: string): string;
|
|
7
33
|
export declare function validateProjectBootstrapConfigPathForWrite(projectRoot: string, configPath: string): void;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { closeSync, constants, existsSync, fchmodSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
|
|
2
|
+
import { execFileSync } from 'node:child_process';
|
|
2
3
|
import { randomUUID } from 'node:crypto';
|
|
3
4
|
import { dirname, isAbsolute, relative, resolve } from 'node:path';
|
|
4
5
|
import { homedir } from 'node:os';
|
|
@@ -89,6 +90,81 @@ export function resolveProjectRootForConfig(startPath) {
|
|
|
89
90
|
}
|
|
90
91
|
return pkgRoot ?? start;
|
|
91
92
|
}
|
|
93
|
+
/**
|
|
94
|
+
* Canonicalise a user-supplied project root path against git's view of the
|
|
95
|
+
* repository root. This is the fix for the nested-directory regression
|
|
96
|
+
* where peaks-cli would write `.peaks/` under a nested sub-folder
|
|
97
|
+
* (e.g. `prompt-project/prompt-project/.peaks/`) because the LLM passed
|
|
98
|
+
* `$(pwd)` from inside a sub-directory of a real git repo. Without
|
|
99
|
+
* canonicalisation, peaks accepted the cwd as-is, built the .peaks/
|
|
100
|
+
* tree there, and left the team with two parallel state stores.
|
|
101
|
+
*
|
|
102
|
+
* Strategy:
|
|
103
|
+
* 1. If `startPath` (or any ancestor) is inside a git repo, return
|
|
104
|
+
* `git rev-parse --show-toplevel` from `startPath`. The git root
|
|
105
|
+
* is the *only* correct answer for "where does the .peaks/ tree
|
|
106
|
+
* belong?" — sub-folders of a git repo are not their own projects.
|
|
107
|
+
* 2. If `startPath` is not inside a git repo, fall back to
|
|
108
|
+
* `findProjectRoot` (the existing heuristic) so the CLI still
|
|
109
|
+
* works for non-git projects.
|
|
110
|
+
* 3. If both fail, return `startPath` unchanged — better to write
|
|
111
|
+
* to the cwd than to refuse the command.
|
|
112
|
+
*
|
|
113
|
+
* This is intentionally fail-open: it only *promotes* a path towards
|
|
114
|
+
* the git root, it never demotes one. A non-git user is unaffected.
|
|
115
|
+
* The function does NOT throw on a missing git binary or a non-zero
|
|
116
|
+
* `git rev-parse` exit; both fall through to the heuristic.
|
|
117
|
+
*/
|
|
118
|
+
export function resolveCanonicalProjectRoot(startPath) {
|
|
119
|
+
const start = resolve(startPath);
|
|
120
|
+
const gitRoot = resolveProjectRootFromGit(start);
|
|
121
|
+
if (gitRoot !== null) {
|
|
122
|
+
return gitRoot;
|
|
123
|
+
}
|
|
124
|
+
// Non-git fallback: walk the heuristic up to the home boundary.
|
|
125
|
+
// We do NOT call realpathSync on the heuristic result because the
|
|
126
|
+
// heuristic may legitimately return a path through a symlink that
|
|
127
|
+
// the caller passed in (no canonicalisation needed in that case).
|
|
128
|
+
const heuristicRoot = findProjectRoot(start);
|
|
129
|
+
if (heuristicRoot !== null) {
|
|
130
|
+
return heuristicRoot;
|
|
131
|
+
}
|
|
132
|
+
return start;
|
|
133
|
+
}
|
|
134
|
+
function resolveProjectRootFromGit(startPath) {
|
|
135
|
+
// execFileSync (not execSync) so a malicious `startPath` cannot
|
|
136
|
+
// inject argv into the spawned git invocation. The child only
|
|
137
|
+
// receives `startPath` as the cwd, never as a flag.
|
|
138
|
+
let rawRoot;
|
|
139
|
+
try {
|
|
140
|
+
const stdout = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
141
|
+
cwd: startPath,
|
|
142
|
+
encoding: 'utf8',
|
|
143
|
+
stdio: ['ignore', 'pipe', 'ignore']
|
|
144
|
+
});
|
|
145
|
+
const trimmed = stdout.trim();
|
|
146
|
+
if (trimmed.length === 0)
|
|
147
|
+
return null;
|
|
148
|
+
rawRoot = resolve(trimmed);
|
|
149
|
+
}
|
|
150
|
+
catch {
|
|
151
|
+
// git not on PATH, startPath is not in a repo, or some other
|
|
152
|
+
// benign failure — fall through to the heuristic.
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
// On macOS, /tmp is a symlink to /private/tmp; git returns the
|
|
156
|
+
// realpath. If the caller passed a path through the symlink, the
|
|
157
|
+
// two strings won't match byte-for-byte even though they refer
|
|
158
|
+
// to the same directory. realpathSync the git root and the
|
|
159
|
+
// startPath through the same lens so callers get a canonical
|
|
160
|
+
// answer that compares equal to the path they passed in.
|
|
161
|
+
try {
|
|
162
|
+
return realpathSync(rawRoot);
|
|
163
|
+
}
|
|
164
|
+
catch {
|
|
165
|
+
return rawRoot;
|
|
166
|
+
}
|
|
167
|
+
}
|
|
92
168
|
export function getProjectConfigPath(projectRoot) {
|
|
93
169
|
if (!projectRoot)
|
|
94
170
|
return null;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ConfigGetOptions, ConfigLayer, ConfigSetOptions, MiniMaxProviderConfig, PeaksConfig, TokenRef, WorkspaceConfig } from './config-types.js';
|
|
2
|
-
export { resolveProjectRootForConfig } from './config-safety.js';
|
|
2
|
+
export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './config-safety.js';
|
|
3
3
|
export declare function isConfigLayer(value: string): value is ConfigLayer;
|
|
4
4
|
export declare function isSensitiveConfigPath(path: string): boolean;
|
|
5
5
|
export declare function containsSensitiveConfigValue(value: unknown): boolean;
|
|
@@ -3,8 +3,8 @@ import { dirname, isAbsolute, resolve } from 'node:path';
|
|
|
3
3
|
import { DEFAULT_CONFIG } from './config-types.js';
|
|
4
4
|
import { stablePath } from '../../shared/path-utils.js';
|
|
5
5
|
import { findProjectRoot, getProjectBootstrapConfigPath, getProjectConfigPath, getUserConfigPath, isInsidePath, readConfigFileSafely, resolveProjectRootForConfig, validateArtifactWorkspaceMarkerPath, validateArtifactWorkspaceRoot, validateProjectBootstrapConfigPathForWrite, validateUserConfigPathForWrite, writeConfigFileSafely, writeProjectConfigFile, writeUserConfigFile } from './config-safety.js';
|
|
6
|
-
// Re-export resolveProjectRootForConfig for external consumers
|
|
7
|
-
export { resolveProjectRootForConfig } from './config-safety.js';
|
|
6
|
+
// Re-export resolveProjectRootForConfig and resolveCanonicalProjectRoot for external consumers
|
|
7
|
+
export { resolveProjectRootForConfig, resolveCanonicalProjectRoot } from './config-safety.js';
|
|
8
8
|
function readJsonFile(path, validateBeforeRead, errorMessage = 'Config path must stay inside the config root') {
|
|
9
9
|
if (!path || !existsSync(path))
|
|
10
10
|
return null;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const CLI_VERSION = "1.2.
|
|
1
|
+
export declare const CLI_VERSION = "1.2.7";
|
|
@@ -1 +1 @@
|
|
|
1
|
-
export const CLI_VERSION = "1.2.
|
|
1
|
+
export const CLI_VERSION = "1.2.7";
|
package/package.json
CHANGED
|
@@ -1065,4 +1065,4 @@ Do not run upstream installer flows, mutate agent settings, or commit `.codegrap
|
|
|
1065
1065
|
|
|
1066
1066
|
**MCP lifecycle**: `list → plan → apply --yes → call → rollback`. `apply` backs up settings and refuses non-peaks entries unless `--claim` is passed.
|
|
1067
1067
|
|
|
1068
|
-
Detailed rules: `references/external-skill-invocation.md`, `references/openspec-mcp-workflow.md`, `references/workflow.md`, `references/existing-system-extraction.md`.
|
|
1068
|
+
Detailed rules: `references/external-skill-invocation.md`, `references/openspec-mcp-workflow.md`, `references/workflow.md`, `references/existing-system-extraction.md`. For an informational mapping of peaks artefact paths to the A2A (Agent2Agent) protocol's Task / Artifact / Part / Message / AgentCard vocabulary (no A2A implementation, just a shared naming layer), see `references/a2a-artifact-mapping.md`.
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# A2A artifact mapping (informational)
|
|
2
|
+
|
|
3
|
+
> Reference for `peaks-solo` and any other peaks skill that produces durable artefacts in `.peaks/<session-id>/`. Maps peaks's on-disk artefact vocabulary onto the A2A (Agent2Agent) protocol's vocabulary so a future peaks consumer (e.g. an external LLM agent or a downstream peaks-cli extension) can read peaks output without having to learn a brand-new schema. This is a **documentation mapping**, not a protocol implementation: peaks-cli does not speak A2A over HTTP, does not host an AgentCard endpoint, and does not advertise its capabilities via A2A's discovery mechanism. It only uses A2A's *concepts* as a shared naming layer.
|
|
4
|
+
|
|
5
|
+
## 1. Why this reference exists
|
|
6
|
+
|
|
7
|
+
The A2A protocol (https://a2acn.com) defines five core concepts: **AgentCard**, **Task**, **Artifact**, **Message**, and **Part**. peaks-cli's session workspace is a parallel vocabulary that grew up independently: `prd/requests/<rid>.md`, `rd/tech-doc.md`, `qa/test-cases/<rid>.md`, etc. The two vocabularies are *not* identical (A2A is HTTP-shaped, peaks is filesystem-shaped), but the A2A concepts are close enough that aligning peaks artefact names with A2A terms in this reference:
|
|
8
|
+
|
|
9
|
+
- gives an external consumer a single translation table instead of two schemas to learn,
|
|
10
|
+
- lets a peaks operator talk about "the artifact" or "the task" in mixed conversations without losing precision,
|
|
11
|
+
- documents what peaks output is **not** (no SSE streaming, no remote AgentCard), so the limits are explicit.
|
|
12
|
+
|
|
13
|
+
This is the kind of borrowing that costs zero code and earns some interoperability. It is **not** an integration: peaks-cli does not implement A2A, does not run an A2A server, and does not depend on the a2a-protocol package. Adopting A2A concepts here is the same as adopting any other shared nomenclature (UML, OpenTelemetry, etc.): it improves the conversation, nothing more.
|
|
14
|
+
|
|
15
|
+
## 2. Concept-to-path mapping
|
|
16
|
+
|
|
17
|
+
The mapping below uses peaks's own paths verbatim. Each row also notes where peaks **diverges** from A2A, so a reader does not assume parity.
|
|
18
|
+
|
|
19
|
+
| A2A concept | peaks artefact | Path (under `.peaks/<session-id>/`) | Notes |
|
|
20
|
+
|---|---|---|---|
|
|
21
|
+
| **AgentCard** (capability advertisement) | `peaks-skill-output-style` + `.peaks/.active-skill.json` | `.peaks/.active-skill.json`, `.peaks/.session.json` | peaks is a *local* tool, not a service. The "card" is the active-skill file plus a peek at `.peaks/PROJECT.md` for human-readable history. There is no `/.well-known/agent-card.json` endpoint. |
|
|
22
|
+
| **Task** (stateful unit of work) | `peaks request` state machine for a single `<rid>` | `.peaks/<sid>/{prd,rd,qa,ui,sc}/requests/<rid>.md` (the request artefact); `.peaks/<sid>/<role>/session.json` (per-session metadata) | peaks's task lifecycle is `prd:confirmed-by-user → handed-off`, then per role `draft → spec-locked → implemented → qa-handoff`, then `qa:running → verdict-issued`. The full state graph is enforced by `peaks request transition`. A2A's Task object is JSON; peaks's task is **a set of files with a `state` field per role**. |
|
|
23
|
+
| **Artifact** (immutable output) | `rd/tech-doc.md`, `rd/code-review.md`, `rd/security-review.md`, `qa/test-cases/<rid>.md`, `qa/test-reports/<rid>.md`, `qa/security-findings.md`, `qa/performance-findings.md`, `sc/handoff.md` | as listed | peaks's artefacts are *append-once*, not strictly immutable: a `qa/test-reports/<rid>.md` may be re-emitted on repair cycles. The convention is "newest write wins; the file at the end of the workflow is the truth", which is close enough to A2A's immutable-Artifact semantics for translation purposes. |
|
|
24
|
+
| **Message** (non-artifact communication) | `peaks skill presence` heartbeat + transition `--reason` notes | `.peaks/.active-skill.json` (`lastHeartbeat`), transition notes in `.peaks/<sid>/<role>/requests/<rid>.md` | peaks does **not** separate Messages from Artifacts at the storage layer; a "message" is anything that is not the artefact body (the `<!-- peaks-memory:start -->` markers, the `state` field, the `--reason` text on a transition). Treat these as inline metadata of the artefact, not as separate objects. |
|
|
25
|
+
| **Part** (atomic content unit) | Markdown sections within an artefact, frontmatter fields | inline within the artefact | peaks's Artifacts are single Markdown files, so the "Part" concept maps to a heading or a frontmatter field. A `Part`'s `kind` in A2A terms is `text` (the prose), `file` (a `<!-- peaks-memory:start -->` block as a structured chunk), or `data` (the frontmatter). A2A's `form` / `iframe` / video `Part` kinds are not produced by peaks. |
|
|
26
|
+
|
|
27
|
+
## 3. Field-level mapping (A2A Part ↔ peaks frontmatter)
|
|
28
|
+
|
|
29
|
+
A2A `Part` has `kind` and `metadata` (free-form) plus `content` (typed by kind). peaks's per-artifact frontmatter carries a subset:
|
|
30
|
+
|
|
31
|
+
```yaml
|
|
32
|
+
---
|
|
33
|
+
name: <slug> # used in memory extraction; not a 1:1 A2A field
|
|
34
|
+
description: <title> # roughly the A2A Artifact.description
|
|
35
|
+
metadata:
|
|
36
|
+
type: <kind> # A2A Artifact.kind equivalent
|
|
37
|
+
sourceArtifact: <rel> # A2A Artifact.source / provenance equivalent
|
|
38
|
+
---
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
A consumer reading a peaks artefact and translating it to A2A can populate:
|
|
42
|
+
|
|
43
|
+
- `Artifact.name` ← peaks `name`
|
|
44
|
+
- `Artifact.description` ← peaks `description` (or the first H1)
|
|
45
|
+
- `Artifact.kind` ← peaks `metadata.type`
|
|
46
|
+
- `Artifact.parts[0]` ← the body text (A2A `Part{kind: "text"}`)
|
|
47
|
+
- `Artifact.metadata.sourcePath` ← peaks `metadata.sourceArtifact`
|
|
48
|
+
- `Artifact.metadata.sessionId` ← from `.peaks/.session.json`
|
|
49
|
+
|
|
50
|
+
The mapping is not 100% lossless: A2A's `Part` can carry structured forms or file references, peaks cannot. That is the explicit *non-goal* of this mapping; it would be over-claiming to assert parity where there is none.
|
|
51
|
+
|
|
52
|
+
## 4. State-graph mapping (A2A Task ↔ peaks request)
|
|
53
|
+
|
|
54
|
+
A2A's Task object has a small set of states (typically: `submitted`, `working`, `input-required`, `completed`, `failed`, `canceled`). peaks's per-role request state machine is richer and per-role:
|
|
55
|
+
|
|
56
|
+
| Role | peaks states (in order) | Closest A2A Task state |
|
|
57
|
+
|---|---|---|
|
|
58
|
+
| `prd` | `draft` → `confirmed-by-user` → `handed-off` | `submitted` → `working` → `input-required` (for the confirm gate) |
|
|
59
|
+
| `rd` | `draft` → `spec-locked` → `implemented` → `qa-handoff` | `working` |
|
|
60
|
+
| `qa` | `draft` → `running` → `verdict-issued` (verdict is `pass` / `return-to-rd` / `blocked`) | `working` → `completed` (pass) / `input-required` (return-to-rd) / `failed` (blocked) |
|
|
61
|
+
| `ui` | `draft` → `direction-locked` → `handed-off` | `working` → `completed` |
|
|
62
|
+
| `sc` | `draft` → `recorded` | `working` → `completed` |
|
|
63
|
+
|
|
64
|
+
A consumer translating peaks states to A2A should:
|
|
65
|
+
|
|
66
|
+
- collapse peaks's multi-role state machine to a *single* A2A Task state by taking the most progressed of any role,
|
|
67
|
+
- use the A2A `input-required` state to model **any** gate where peaks is waiting for a human (`confirmed-by-user`, `--confirm`, AskUserQuestion for a login wall, etc.),
|
|
68
|
+
- emit `completed` only when QA verdict is `pass` and SC has recorded the change,
|
|
69
|
+
- emit `failed` on `blocked` QA verdict or `blocked` handoff.
|
|
70
|
+
|
|
71
|
+
## 5. Worked example: a feature slice from start to finish
|
|
72
|
+
|
|
73
|
+
A user runs `peaks-solo` for a "add user authentication" feature. Mapping the resulting files to A2A concepts:
|
|
74
|
+
|
|
75
|
+
```
|
|
76
|
+
.peaks/<sid>/prd/requests/001.md → A2A Artifact (kind=proposal)
|
|
77
|
+
.peaks/<sid>/ui/requests/001.md → A2A Artifact (kind=design-direction)
|
|
78
|
+
.peaks/<sid>/ui/design-draft.md → A2A Artifact (kind=visual-spec)
|
|
79
|
+
.peaks/<sid>/rd/tech-doc.md → A2A Artifact (kind=implementation-plan)
|
|
80
|
+
.peaks/<sid>/qa/test-cases/001.md → A2A Artifact (kind=test-cases)
|
|
81
|
+
.peaks/<sid>/rd/code-review.md → A2A Artifact (kind=review, status=fixed)
|
|
82
|
+
.peaks/<sid>/rd/security-review.md → A2A Artifact (kind=security-review)
|
|
83
|
+
.peaks/<sid>/qa/test-reports/001.md → A2A Artifact (kind=test-report, verdict=pass)
|
|
84
|
+
.peaks/<sid>/qa/security-findings.md → A2A Artifact (kind=security-findings)
|
|
85
|
+
.peaks/<sid>/qa/performance-findings.md → A2A Artifact (kind=performance-findings)
|
|
86
|
+
.peaks/<sid>/sc/handoff.md → A2A Artifact (kind=change-record)
|
|
87
|
+
.peaks/<sid>/txt/handoff.md → A2A Artifact (kind=handoff-capsule)
|
|
88
|
+
.peaks/<sid>/system/sub-agent-*.json → A2A Message (sub-agent presence markers)
|
|
89
|
+
.peaks/<sid>/sc/swarm-plan.json → A2A Message (the dispatch plan)
|
|
90
|
+
.peaks/memory/*.md → A2A Artifact (kind=project-memory, persists across sessions)
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
A consumer wanting to render a single "feature" object in A2A terms picks the `test-reports/001.md` (verdict=pass) as the terminal Artifact and the rest as supporting Parts or sibling Artifacts. The mapping is intentionally loose: peaks's value is that *all of these files exist*, not that they fit A2A's object model exactly.
|
|
94
|
+
|
|
95
|
+
## 6. What peaks does NOT provide
|
|
96
|
+
|
|
97
|
+
To keep the mapping honest, peaks-cli **does not** currently provide the following A2A primitives, and consumers should not expect them:
|
|
98
|
+
|
|
99
|
+
- A2A **AgentCard** served over HTTP at `/.well-known/agent-card.json`. peaks-cli is a local CLI; its "card" is the on-disk `.peaks/.active-skill.json` plus `peaks skill doctor --json`.
|
|
100
|
+
- A2A **streaming** responses (SSE / WebSocket). peaks commands are synchronous and return a single JSON envelope.
|
|
101
|
+
- A2A **identity / auth** (OAuth, OIDC, mTLS). peaks assumes local-machine trust.
|
|
102
|
+
- A2A **cross-vendor discovery**. peaks has no A2A registry entry; it has `peaks mcp list --json` for MCP-compatible capabilities.
|
|
103
|
+
- A2A **Task delegation across the network**. peaks's "sub-agent" is a Claude Code `Task` tool call in the same process, not a remote A2A server.
|
|
104
|
+
|
|
105
|
+
These are *deliberate* omissions. peaks-cli solves a different problem (a local workflow-gating CLI for Claude Code), and adopting A2A's networking surface would add weight without addressing peaks's actual failure modes (which are around LLM bypassing gates, not around inter-agent discovery).
|
|
106
|
+
|
|
107
|
+
## 7. When to re-evaluate
|
|
108
|
+
|
|
109
|
+
Re-open this mapping in any of the following cases:
|
|
110
|
+
|
|
111
|
+
- a peaks user reports a real need to share workflow state with a non-peaks agent (e.g. an Autogen / LangChain agent that wants to read a peaks handoff capsule);
|
|
112
|
+
- peaks-cli ships a hosted / multi-user mode where AgentCard-style discovery becomes useful;
|
|
113
|
+
- the A2A protocol stabilises on a thin `Artifact` JSON schema that matches peaks's on-disk shape close enough to make translation a one-liner rather than a reference doc.
|
|
114
|
+
|
|
115
|
+
Until one of those fires, this reference doc is the entire A2A surface area of peaks-cli. Adding more is over-engineering.
|