@luckychen1993/pm-mcp 0.1.5 → 0.4.0
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/LICENSE +21 -21
- package/README.md +114 -11
- package/dist/agents-md.js +3 -3
- package/dist/audit-tools.js +17 -0
- package/dist/audit.js +25 -21
- package/dist/cli.js +54 -0
- package/dist/dashboard.js +6 -4
- package/dist/doctor.js +148 -0
- package/dist/duplicates.js +228 -0
- package/dist/git-state.js +75 -0
- package/dist/governance-audit.js +98 -1
- package/dist/governance-tools.js +21 -3
- package/dist/index-db.js +40 -0
- package/dist/index-store.js +35 -44
- package/dist/index.js +15 -8
- package/dist/knowledge-tools.js +6 -15
- package/dist/language-adapters.js +11 -10
- package/dist/license.js +28 -2
- package/dist/osv.js +139 -0
- package/dist/probe.js +141 -0
- package/dist/project-fingerprint.js +9 -5
- package/dist/project-tools.js +13 -2
- package/dist/quality-evidence.js +56 -0
- package/dist/quality-store.js +5 -25
- package/dist/registry.js +28 -2
- package/dist/scan-policy.js +34 -0
- package/dist/scan.js +5 -25
- package/dist/search.js +4 -0
- package/dist/security.js +2 -0
- package/dist/semantic-evidence-store.js +22 -0
- package/dist/semantic-graph.js +2 -0
- package/dist/semantic-parsers.js +18 -0
- package/dist/session-log.js +14 -0
- package/dist/setup-project.js +146 -0
- package/dist/setup.js +92 -94
- package/dist/task-evidence.js +110 -0
- package/dist/task-pagination.js +64 -0
- package/dist/task-tools.js +47 -37
- package/dist/tool-base.js +7 -5
- package/dist/types.js +5 -0
- package/dist/ui.js +191 -0
- package/dist/version.js +10 -0
- package/package.json +10 -4
package/LICENSE
CHANGED
|
@@ -1,21 +1,21 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 pm-mcp contributors
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 pm-mcp contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -8,6 +8,42 @@
|
|
|
8
8
|
|
|
9
9
|
> 核心理念:**把恶化从「无声发生」变成「显式记账」,把上下文从「模型记忆」搬到「仓库文件」。**
|
|
10
10
|
|
|
11
|
+
当前源码为 **0.3.0 本地候选版本,尚未由本轮工作发布**。下方公共安装示例仍使用已发布的 0.1.5。候选版可用 `setup --local` 绑定已构建的本地入口;未发布前不要配置依赖注册表中 0.3.0 的 npx 启动项。
|
|
12
|
+
|
|
13
|
+
## 0.3.0:升级生效、任务取全与证据分级
|
|
14
|
+
|
|
15
|
+
- `get_status` 同时显示当前进程的运行版本、项目根和结构化 runtime,避免把项目 phase 中的旧文字当作实际版本。
|
|
16
|
+
- `list_tasks` 默认每页 25 条,可用 `page_size`(1–100,受输出预算限制)和 `next_cursor` 连续读取;保持原过滤条件,数据变化会使游标失效。文字及结构化结果都给出本页范围、总数和后续游标。长标题/下一步可能缩略,但编号与分页条目完整。`pm://tasks` 的大列表也提供 continuation。
|
|
17
|
+
- 质量摘要标明 `execution_only`、`tests_observed`、`coverage_observed` 等观测等级及计数来源。支持 Node、Jest、Vitest、pytest 的常见英文摘要;无法识别时明确保留未知计数,不猜测为零。等级不等于测试充分性、通过结果或防篡改认证。
|
|
18
|
+
- feature/fix 默认必须具有可识别的非零测试及完整状态计数。需要兼容无计数运行器时,由项目负责人明确设置 `update_project({completion_evidence:{minimum:"execution_only",reason:"说明至少八个字的具体理由"}})`;完成记录仍标出弱证据等级。已知零测试、失败、跳过、截断或过期证据始终拒绝。
|
|
19
|
+
- 质量子进程不会继承 Node 宿主的内部 `NODE_TEST_CONTEXT`,避免嵌套 `node --test` 跳过文件却退出 0。
|
|
20
|
+
|
|
21
|
+
本地候选的 Codex 项目升级在构建后按以下步骤执行。先预览,再替换该项目的 command/args,保留其他设置与工具预算:
|
|
22
|
+
|
|
23
|
+
```powershell
|
|
24
|
+
npm run build
|
|
25
|
+
node dist/cli.js setup --client codex --project --local --force --dry-run
|
|
26
|
+
node dist/cli.js setup --client codex --project --local --force
|
|
27
|
+
node dist/cli.js doctor --root .
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`doctor` 会核对配置并建立短期本地 MCP 读连接,检查实际版本、项目根、48 工具与新参数契约;不会把探测新进程冒充已经重连的客户端。探测不复制自定义凭据环境;结果会产生既有运行计量。显式 `--entry <本地入口>` 可用于安装包检查。配置更新后仍需在实际客户端重新连接,再调用 get_status 确认版本。复杂或歧义配置会拒绝自动替换,已有配置与升级前备份保留。
|
|
31
|
+
|
|
32
|
+
验收脚本从 `package.json` 的 `pm_mcp.acceptance_baseline` 统一读取基线 ID/版本,命令行可显式覆盖。批准记录不自动升版、不选择猜测的“最新”文件;本仓选择 1.1.0,原 1.0.0 保留。新版评价使用对应版本的计划与失败记录。
|
|
33
|
+
|
|
34
|
+
## 已有管理流程
|
|
35
|
+
|
|
36
|
+
用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。52 个工具全部保留,由 AI 组合调用,无需用户记工具名。
|
|
37
|
+
|
|
38
|
+
- `update_task` 可同时更新进度、保存 `checkpoint:{note,next_step}` 并记录会话;完成时给 `record_session:true`,返回会话编号后不再另调 `log_session`。省略该参数仍兼容旧的手动记会话流程。同一幂等键重放不会重复落账;跨多个文件的异常仍遵守原有“不确定状态需核对”规则,不宣称事务原子性。
|
|
39
|
+
- `feature/fix` 转 `done` 必须关联实际文件,并先通过 `run_quality_matrix` 执行相应测试。系统自动选择最新报告,核对执行成功、测试前后与当前源码摘要、测试单元目录和项目证据策略。`verification_run` 可明确指向最新报告,文字 `verification` 只作补充说明。报告存于 `.pm/quality-runs/`,任务保存报告与源码 SHA-256。目录范围匹配不等于代码覆盖率证明;第一方报告也不构成防篡改认证。
|
|
40
|
+
- Git 对账按所有会话的文件内容摘要核对,保留删除、重命名和中文路径。旧版只有路径的记录标为“未验证”;同一路径内容再次变化仍会告警。这里只核对当前工作区内容,不代替暂存区审查或提交验收。
|
|
41
|
+
- 扫描、索引、搜索、语言发现和源码指纹共用排除规则,默认排除 `.runtime`、`.zcode`、`EBWebView` 等缓存。含义不明确的 `release`、`userdata` 不默认隐藏;确认是产物后由 AI 用 `update_project({scan_ignore:["release/**"]})` 配置,传 `[]` 恢复。报告列明自定义排除范围,规则变动会刷新索引并使旧质量证据失效。
|
|
42
|
+
|
|
43
|
+
旧账本保持可读,历史已完成任务不会自动重开;新增字段为可选字段。所有读写同一项目的 MCP 进程应统一升级并重启,旧版本写入可能丢弃新元数据。已有 `AGENTS.md` 保留原文;新初始化项目和内置 `end-session` 提示使用合并流程。
|
|
44
|
+
|
|
45
|
+
仓库测试默认同时运行两个测试文件,避免文件级并发与内部进程压力叠加;20 进程竞争等测试的内部并发、锁超时和覆盖率门槛保持原设定。这是测试资源控制,不是产品并发能力或生产负载的证明。
|
|
46
|
+
|
|
11
47
|
## 为什么需要它
|
|
12
48
|
|
|
13
49
|
AI 编码项目的典型失控(本工具逐一给出机制):
|
|
@@ -32,7 +68,7 @@ AI 编码项目的典型失控(本工具逐一给出机制):
|
|
|
32
68
|
|
|
33
69
|
## 一个命令自动添加到 AI 编程助手
|
|
34
70
|
|
|
35
|
-
|
|
71
|
+
本页安装示例使用 npm 上最新的**已发布**版本(当前为 **0.1.5**)。0.1.4 不支持 `setup --project`;验证这些能力时请确认实际运行版本为 0.1.5 或更新版本。仓库源码当前为 **0.3.0**(新增任务游标分页、完成证据分级、受控升级与 WorkBuddy 客户端等),**尚未发布到 npm**——需要这些能力时请从源码构建,或见下方各客户端的本地入口用法。
|
|
36
72
|
|
|
37
73
|
要求:Node.js ≥ 22.18。Windows、macOS 和 Linux 使用同一条命令:
|
|
38
74
|
|
|
@@ -40,13 +76,23 @@ AI 编码项目的典型失控(本工具逐一给出机制):
|
|
|
40
76
|
npx -y @luckychen1993/pm-mcp@latest setup
|
|
41
77
|
```
|
|
42
78
|
|
|
43
|
-
安装器自动检测本机的 Codex、Claude Code、ZCode、Cursor 和
|
|
79
|
+
安装器自动检测本机的 Codex、Claude Code、ZCode、Cursor、VS Code 和 WorkBuddy,并配置所有检测到的客户端。JSON 配置会先备份;已经存在的 Codex/Claude 配置默认保留,用 `setup --force` 才替换。先预览、不写配置:
|
|
44
80
|
|
|
45
81
|
```bash
|
|
46
82
|
npx -y @luckychen1993/pm-mcp@latest setup --dry-run
|
|
47
83
|
```
|
|
48
84
|
|
|
49
|
-
非标准安装路径无法自动发现时,可在同一命令末尾指定 `--client codex|claude|zcode|cursor|vscode`;`--client print` 输出通用 MCP JSON。首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
|
|
85
|
+
非标准安装路径无法自动发现时,可在同一命令末尾指定 `--client codex|claude|zcode|cursor|vscode|workbuddy`;`--client print` 输出通用 MCP JSON。首次启动由 `npx` 下载并在本机运行,pm-mcp 本身不需要 API Key,也不会调用远程模型。
|
|
86
|
+
|
|
87
|
+
### 新项目一条命令上手(Codex 必用)
|
|
88
|
+
|
|
89
|
+
Codex 桌面版启动 MCP 服务时不带工作区路径,逐项目钉根才能用。在项目目录里跑一条命令,自动完成「注册钉定条目 + 初始化 .pm + 写 AGENTS.md 工作规矩」:
|
|
90
|
+
|
|
91
|
+
```bash
|
|
92
|
+
npx -y @luckychen1993/pm-mcp@0.1.5 setup --client codex --project
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
跑完完全重启 Codex,新会话即可用(工具名前缀 `pm-mcp-<目录名>-<路径摘要>`)。不同中文目录和同名目录独立注册;重复执行会核对项目路径,匹配的旧版条目仍可复用。
|
|
50
96
|
|
|
51
97
|
### 新项目一条命令上手(Codex 必用)
|
|
52
98
|
|
|
@@ -86,6 +132,37 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.5/install.ps1'; $f=
|
|
|
86
132
|
|
|
87
133
|
脚本只写对应客户端的 MCP 配置;已有 JSON 配置会先生成带时间戳的备份。建议执行前先打开 [`install.ps1`](install.ps1) 审阅。ZCode 默认写入 `$ZCODE_HOME/cli/config.json` 或 `~/.zcode/cli/config.json`,Cursor 默认写入 `~/.cursor/mcp.json`;也可用 `-ConfigPath` 指定路径。
|
|
88
134
|
|
|
135
|
+
#### WorkBuddy 桌面版
|
|
136
|
+
|
|
137
|
+
> **要求 pm-mcp ≥ 0.3.0**:`--client workbuddy` 是 0.3.0 新增的客户端;npm 上当前的 0.1.5 **不支持**该参数,直接跑会报 `--client must be auto, all, codex, claude, zcode, cursor, vscode, or print`。
|
|
138
|
+
|
|
139
|
+
0.3.0 发布后:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
npx -y @luckychen1993/pm-mcp@latest setup --client workbuddy
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
0.3.0 发布前,从源码构建后用本地入口接入(`--local` 绑定本包已构建的 `dist/index.js`):
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
npm ci && npm run build
|
|
149
|
+
node dist/cli.js setup --client workbuddy --local
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
写入 `<WorkBuddy 数据目录>/mcp.json`,结构与下方通用 JSON 一致。数据目录按 `WORKBUDDY_CONFIG_DIR` → `~/.workbuddy-ai`(当前版本)→ `~/.workbuddy`(旧版)顺序解析,写入前会生成带时间戳的备份,未知顶层字段与既有 server 全部保留。
|
|
153
|
+
|
|
154
|
+
与其他客户端的关键差别:**WorkBuddy 的条目刻意不写 `--root`**。它的 MCP 子进程由会话工作区拉起,cwd 即当前项目,服务端按 cwd 解析项目根——因此一条配置同时服务所有工作区,不需要逐项目钉根;未初始化的目录只返回「未初始化」,不会写入任何文件。
|
|
155
|
+
|
|
156
|
+
⚠️ 注意该设计的边界:**跨项目操作会失效**。若在 A 项目的会话里让 pm-mcp 去改 B 项目,它读到的始终是 A(MCP 子进程的 cwd 就是会话工作区)。跨项目时请在目标项目的会话内操作,或改用显式钉根的配置。
|
|
157
|
+
|
|
158
|
+
接入后用 `doctor` 核验(它会读取该 `mcp.json` 的 pm-mcp 条目并做真实握手):
|
|
159
|
+
|
|
160
|
+
```bash
|
|
161
|
+
node dist/cli.js doctor --root . --client workbuddy # 核验 48 工具与项目根
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`doctor` 会读取该 `mcp.json` 的 pm-mcp 条目,只认可识别的本地 pm-mcp 启动项(npx 发布版或 node + 本包 `dist/index.js`),被 `disabled` 的条目与未知启动器一律拒绝,且不读取或转发自定义凭据环境。配置完成后需在 WorkBuddy 的「连接器管理 → 自定义连接器」中确认信任 pm-mcp,再重开会话调用 `get_status` 核验。
|
|
165
|
+
|
|
89
166
|
#### 其他支持 stdio MCP 的客户端
|
|
90
167
|
|
|
91
168
|
```json
|
|
@@ -110,6 +187,32 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.5/install.ps1'; $f=
|
|
|
110
187
|
| `PM_ROOT` | 项目根(次优先级) |
|
|
111
188
|
| `PM_MCP_HOME` | 全局注册表重定向(默认 `~`;多环境/测试隔离用) |
|
|
112
189
|
|
|
190
|
+
### pm-mcp probe:标准 raw tools/list 直连探针
|
|
191
|
+
|
|
192
|
+
宿主(ZCode/Codex/Cursor 等)是唯一 MCP client 持有者,不会向模型透传原始 `tools/list`;要拿服务器在协议线上的真实工具清单,用 `probe` 绕过宿主直连:
|
|
193
|
+
|
|
194
|
+
```bash
|
|
195
|
+
node dist/cli.js probe -- node dist/index.js --root .
|
|
196
|
+
node dist/cli.js probe --timeout 10000 --expect get_status,list_tasks,search_code -- node dist/index.js --root .
|
|
197
|
+
node dist/cli.js probe --expect-file 上一轮probe报告.json -- node 其他服务器/dist/index.js
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
- 输出原始工具清单 JSON(`tools` 含 name/description/inputSchema,自动翻页取全)与 `serverInfo`;退出码 0/1。
|
|
201
|
+
- `--expect` 与期望工具表比对(如从 Codex `config.toml` 的 `[mcp_servers.X.tools.*]` 提取),缺失/多余都会列进 `issues` 并置 `ok:false`。
|
|
202
|
+
- `--expect-file` 接受上一轮 probe 报告、字符串数组或 `{tools:[...]}`,便于跨项目基线比对。
|
|
203
|
+
- 探测进程与服务器子进程在结束时都会被清理,不会残留。
|
|
204
|
+
|
|
205
|
+
### pm-mcp ui:本地只读 Web 仪表盘
|
|
206
|
+
|
|
207
|
+
```bash
|
|
208
|
+
node dist/cli.js ui --root . # 端口自动分配
|
|
209
|
+
node dist/cli.js ui --root . --port 7781
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
- 仅绑定 `127.0.0.1`、纯 GET 只读、未纳管项目拒绝启动;概览(任务/功能/安全/会话/索引/质量门禁)+ 里程碑进度 + 最近任务。
|
|
213
|
+
- `/api/state` 输出同源 JSON,`/healthz` 健康检查;所有插值 HTML 转义,CSP 禁止脚本。
|
|
214
|
+
- Ctrl+C 退出。不新增 MCP 工具,不改变工具目录契约。
|
|
215
|
+
|
|
113
216
|
## 推荐工作流(写给 AI 的规矩)
|
|
114
217
|
|
|
115
218
|
把下面这段放进目标项目的 `AGENTS.md`,任何 AI 会话自动遵守:
|
|
@@ -126,16 +229,16 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.5/install.ps1'; $f=
|
|
|
126
229
|
4. 陈述本次计划:做哪个任务、动哪些文件、验收标准
|
|
127
230
|
|
|
128
231
|
## 做事过程
|
|
129
|
-
- 长任务:add_task 时给 steps
|
|
232
|
+
- 长任务:add_task 时给 steps;update_task 附 checkpoint 一次更新进度与会话
|
|
130
233
|
- 走捷径:立刻 add_task(type=debt) 登记债务,不许无声欠债
|
|
131
234
|
- 修 bug:完成后 log_debug 记录症状/根因/修法/验证
|
|
132
235
|
- 陈述"某功能在某文件"之前,先 search_code 求证
|
|
133
236
|
|
|
134
237
|
## 收工(缺一不可)
|
|
135
|
-
1.
|
|
238
|
+
1. feature/fix 先 run_quality_matrix 执行测试;update_task 置 done + result_note + files,record_session:true 一并归档
|
|
136
239
|
2. 未完成任务 checkpoint(进展 + 下一步)
|
|
137
240
|
3. 落地的功能 register_feature(入口文件 + 测试文件)
|
|
138
|
-
4. log_session
|
|
241
|
+
4. update_task 已返回自动会话编号时不重复 log_session;否则如实记录改动文件清单。纯问答无需新建任务或重复记账
|
|
139
242
|
|
|
140
243
|
## 每个里程碑节点
|
|
141
244
|
snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit_license 许可证审计;audit_governance 模块/语言边界审计;plan_quality_matrix 生成真实质量矩阵
|
|
@@ -143,16 +246,16 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
|
|
|
143
246
|
|
|
144
247
|
也可以直接用内置 prompts:`start-session` / `end-session` / `onboard` / `architecture-review` / `acceptance-review`;其中 `onboard` 会引导客户端读取状态并生成新人/AI 入职简报。
|
|
145
248
|
|
|
146
|
-
## 工具清单(
|
|
249
|
+
## 工具清单(52 个)
|
|
147
250
|
|
|
148
251
|
| 域 | 工具 | 说明 |
|
|
149
252
|
|---|---|---|
|
|
150
253
|
| 状态 | `init_project` / `get_status` / `update_project` / `regenerate_dashboard` | 初始化、一站式「我在哪」(`since` 汇总任务/会话变化)、改元信息与预算、重生成仪表盘 |
|
|
151
254
|
| 路线图 | `add_milestone` / `update_milestone` / `get_roadmap` | 里程碑生命周期;depth=1 单行摘要 / depth=2 展开活跃任务 |
|
|
152
|
-
| 任务 | `add_task` / `list_tasks` / `update_task` / `checkpoint` |
|
|
255
|
+
| 任务 | `add_task` / `list_tasks` / `update_task` / `checkpoint` | 进度/断点/会话合并更新;feature/fix 完成强制关联文件与最新真实测试报告 |
|
|
153
256
|
| 功能 | `register_feature` / `list_features` | 功能地图(入口文件 = 漂移对账锚点;测试文件 = 测试背书) |
|
|
154
257
|
| 决策 | `record_decision` / `list_decisions` | ADR 架构决策记录(不可变编号) |
|
|
155
|
-
| 会话 | `log_session` |
|
|
258
|
+
| 会话 | `log_session` | 摘要 + 变更足迹/内容摘要 + 下一步(churn 与 Git 内容对账来源) |
|
|
156
259
|
| 调试 | `log_debug` | 调试知识账:症状/根因/修法/验证 |
|
|
157
260
|
| 求证 | `search_code` / `search_knowledge` / `annotate_file` | 代码检索(file:line)、七类知识源检索(含模块/接口/仓库治理)、文件用途索引 |
|
|
158
261
|
| 审计 | `snapshot_codebase` / `audit_structure` | 结构快照;完整性/增长/漂移/债务/churn/复杂度/索引/足迹/测试/Git 十节对账 |
|
|
@@ -161,8 +264,8 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
|
|
|
161
264
|
| 可观测 | `get_usage_log` / `get_runtime_log` | 使用日志(工具调用统计、错误率、耗时、输出与折叠省下的 token 估算——验证省不省 token)与运行日志(server/watcher/工具报错);只记计量不记参数内容 |
|
|
162
265
|
| 注册表 | `list_projects` | 本机所有被管理项目 |
|
|
163
266
|
| 治理模型 | `init_governance` / `get_governance` / `upsert_module` / `upsert_interface` / `upsert_repository` / `set_governance_policies` | 结构化模块根、owner、语言、公开接口、允许/禁止依赖、跨仓版本约束与强制策略 |
|
|
164
|
-
| 语义治理 | `discover_languages` / `audit_governance` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` | 编译器/Tree-sitter AST 关系、hash-bound
|
|
165
|
-
| 质量与组合 | `plan_quality_matrix` / `run_quality_matrix` / `get_portfolio` | plan 与真实执行严格分离;shell=false;跨仓阶段/债务/安全/版本/cycle
|
|
267
|
+
| 语义治理 | `discover_languages` / `audit_governance` / `dependency_graph` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` / `replace_semantic_evidence_for_file` | 编译器/Tree-sitter AST 关系、hash-bound 原生分析器/运行时证据、模块与文件级循环、依赖邻域/枢纽/unresolved/覆盖率与变更反向闭包 |
|
|
268
|
+
| 质量与组合 | `plan_quality_matrix` / `run_quality_matrix` / `get_portfolio` / `find_duplicates` / `audit_osv` | plan 与真实执行严格分离;shell=false;跨仓阶段/债务/安全/版本/cycle 聚合;行指纹聚类跨文件重复块;OSV 联网漏洞比对(默认关,须显式 confirm=true) |
|
|
166
269
|
| 标准化验收 | `list_acceptance_baselines` / `get_acceptance_baseline` / `save_acceptance_baseline_draft` / `approve_acceptance_baseline` / `evaluate_acceptance` | ISO/SQuaRE 对齐的版本化质量基线、冻结证据指针、需求—风险—测试追踪、机器判定报告与 SHA-256 manifest |
|
|
167
270
|
|
|
168
271
|
Resources:`pm://dashboard`、`pm://roadmap`、`pm://tasks`、`pm://changelog`、`pm://architecture`、`pm://portfolio`、`pm://acceptance`。
|
package/dist/agents-md.js
CHANGED
|
@@ -19,16 +19,16 @@ const SECTION = `${BEGIN}
|
|
|
19
19
|
3. 陈述本次计划:做哪个任务、动哪些文件、验收标准
|
|
20
20
|
|
|
21
21
|
## 做事过程
|
|
22
|
-
- 长任务:add_task 时给 steps
|
|
22
|
+
- 长任务:add_task 时给 steps;用 update_task 附 checkpoint 一次更新进度与会话;保留独立 checkpoint 兼容旧流程
|
|
23
23
|
- 走捷径:立刻 add_task(type=debt) 登记债务,不许无声欠债
|
|
24
24
|
- 修 bug:完成后 log_debug 记录症状/根因/修法/验证
|
|
25
25
|
- 陈述「某功能在某文件」之前,先 search_code 求证
|
|
26
26
|
|
|
27
27
|
## 收工(缺一不可)
|
|
28
|
-
1.
|
|
28
|
+
1. feature/fix 先执行相应测试并生成当前源码的质量报告;update_task 置 done + result_note + files,设 record_session:true 一并归档
|
|
29
29
|
2. 未完成任务 checkpoint(进展 + 下一步具体动作)
|
|
30
30
|
3. 落地的功能 register_feature(入口文件 + 测试文件)
|
|
31
|
-
4. log_session
|
|
31
|
+
4. update_task 已返回自动会话编号时不重复 log_session;否则用 log_session 如实记录改动文件清单。纯问答无需新建任务或重复记账
|
|
32
32
|
|
|
33
33
|
## 每个里程碑节点
|
|
34
34
|
snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit_license 许可证审计
|
package/dist/audit-tools.js
CHANGED
|
@@ -8,6 +8,8 @@ import { listRegistry } from "./registry.js";
|
|
|
8
8
|
import { auditSecurity, listFindings, prepareSecurityAudit, resolveFinding } from "./security.js";
|
|
9
9
|
import { budgetLines, toolR, toolW } from "./tool-base.js";
|
|
10
10
|
import { walkRefresh } from "./index-store.js";
|
|
11
|
+
import { renderDuplicates } from "./duplicates.js";
|
|
12
|
+
import { renderOsv, auditOsv } from "./osv.js";
|
|
11
13
|
export function registerAuditTools(server, root) {
|
|
12
14
|
toolW(server, root, "snapshot_codebase", "给代码结构拍快照(文件数/行数/目录分布/测试数/skip 标记/依赖清单)。之后 audit_structure 会与上次快照 diff。建议每个会话或每个里程碑结束时拍一次。", {}, () => {
|
|
13
15
|
requireInitialized(root);
|
|
@@ -19,6 +21,21 @@ export function registerAuditTools(server, root) {
|
|
|
19
21
|
requireInitialized(root);
|
|
20
22
|
return auditStructure(root, budgetLines(root), true);
|
|
21
23
|
});
|
|
24
|
+
toolR(server, root, "find_duplicates", "重复代码检测:注释剥离/字符串占位/空白归一化的行指纹滑动窗口聚类,跨文件重复块按长度降序列出。", {
|
|
25
|
+
min_lines: z.number().int().min(4).max(20).optional().describe("最小重复行数(有效代码行),默认 6"),
|
|
26
|
+
limit: z.number().int().min(1).max(100).optional().describe("最多列出的重复组数,默认 20"),
|
|
27
|
+
}, (args) => {
|
|
28
|
+
requireInitialized(root);
|
|
29
|
+
return renderDuplicates(root, args ?? {}, budgetLines(root));
|
|
30
|
+
});
|
|
31
|
+
toolR(server, root, "audit_osv", "OSV.dev 已知漏洞查询(联网!默认关):把 manifest 声明的依赖(仅包名/版本/生态)批量发给 api.osv.dev 比对;必须显式 confirm=true 才发起网络请求。", {
|
|
32
|
+
confirm: z.literal(true).describe("显式确认联网:本工具会把依赖名与版本发送到 api.osv.dev"),
|
|
33
|
+
timeout_ms: z.number().int().min(5000).max(120000).optional().describe("每批请求超时毫秒数,默认 30000"),
|
|
34
|
+
}, async (args) => {
|
|
35
|
+
requireInitialized(root);
|
|
36
|
+
const scan = await auditOsv(root, { timeoutMs: args.timeout_ms });
|
|
37
|
+
return renderOsv(scan, budgetLines(root));
|
|
38
|
+
});
|
|
22
39
|
toolW(server, root, "audit_security", "安全体检(只扫描本地、不联网,结果写入安全台账):密钥泄露 / 危险模式(eval、SQL 拼接、禁用证书校验等)/ 依赖风险。修复后重扫自动关闭;接受风险必须留理由。", {}, (_args, prepared) => {
|
|
23
40
|
requireInitialized(root);
|
|
24
41
|
const report = auditSecurity(root, prepared);
|
package/dist/audit.js
CHANGED
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
import fs from "node:fs";
|
|
2
2
|
import path from "node:path";
|
|
3
|
-
import { spawnSync } from "node:child_process";
|
|
4
3
|
import { loadFeatures, loadFileNotes, loadProject, loadRoadmap, loadSessions, loadTasks, saveJson, } from "./store.js";
|
|
5
4
|
import { decisionsDir, pmPath, snapshotsDir } from "./paths.js";
|
|
6
|
-
import {
|
|
5
|
+
import { foldLines } from "./budget.js";
|
|
7
6
|
import { readDirectDeps } from "./scan.js";
|
|
8
7
|
import { aggregates, ensureFresh, freshness, getIndex, getMeta, walkRefresh } from "./index-store.js";
|
|
9
8
|
import { quotaWarnings, milestoneStats } from "./roadmap.js";
|
|
10
9
|
import { blastRadiusWarning, churnStats, debtAging, footprint } from "./health.js";
|
|
11
10
|
import { now } from "./types.js";
|
|
11
|
+
import { changedGitFiles, captureFileHashes } from "./git-state.js";
|
|
12
|
+
import { scanExclusionNote } from "./scan-policy.js";
|
|
12
13
|
/* --------------------------------- 快照 ---------------------------------- */
|
|
13
14
|
/** 用途索引覆盖:登记过用途且真实存在的文件数 */
|
|
14
15
|
export function annotatedCount(root, db) {
|
|
@@ -130,7 +131,7 @@ export function pmIntegrity(root) {
|
|
|
130
131
|
}
|
|
131
132
|
/** git 对账:自报变更足迹 vs 真实工作区状态 */
|
|
132
133
|
export function gitAudit(root) {
|
|
133
|
-
const out = { available: false, unaccounted: [], ignoresPm: false, error: null };
|
|
134
|
+
const out = { available: false, unaccounted: [], unverified: [], ignoresPm: false, error: null };
|
|
134
135
|
if (!fs.existsSync(path.join(root, ".git")))
|
|
135
136
|
return out;
|
|
136
137
|
out.available = true;
|
|
@@ -148,23 +149,23 @@ export function gitAudit(root) {
|
|
|
148
149
|
}
|
|
149
150
|
}
|
|
150
151
|
// -uall:未跟踪目录展开为具体文件(默认只给 "?? src/" 目录缩写,抓不到文件)
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
152
|
+
try {
|
|
153
|
+
const changed = changedGitFiles(root);
|
|
154
|
+
const hashes = captureFileHashes(root, changed);
|
|
155
|
+
const sessions = loadSessions(root).sessions;
|
|
156
|
+
for (const rel of changed) {
|
|
157
|
+
const receipts = sessions.filter((session) => session.files.includes(rel) && typeof session.file_hashes?.[rel] === "string");
|
|
158
|
+
if (receipts.some((session) => session.file_hashes[rel] === hashes[rel]))
|
|
159
|
+
continue;
|
|
160
|
+
if (receipts.length === 0 && sessions.some((session) => session.files.includes(rel)))
|
|
161
|
+
out.unverified.push(rel);
|
|
162
|
+
else
|
|
163
|
+
out.unaccounted.push(rel);
|
|
164
|
+
}
|
|
155
165
|
}
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const rel = normSep(line.slice(3).trim().split(" -> ").pop() ?? "");
|
|
159
|
-
if (!rel)
|
|
160
|
-
continue;
|
|
161
|
-
if (rel.startsWith(".pm/") || rel === "PROJECT.md" || rel === ".gitignore")
|
|
162
|
-
continue;
|
|
163
|
-
changed.add(rel);
|
|
166
|
+
catch (error) {
|
|
167
|
+
out.error = error.message;
|
|
164
168
|
}
|
|
165
|
-
const sessions = loadSessions(root).sessions;
|
|
166
|
-
const lastFiles = new Set(sessions[sessions.length - 1]?.files ?? []);
|
|
167
|
-
out.unaccounted = [...changed].filter((f) => !lastFiles.has(f));
|
|
168
169
|
return out;
|
|
169
170
|
}
|
|
170
171
|
/** 完整结构对账:九项报告 */
|
|
@@ -343,6 +344,7 @@ export function auditStructure(root, maxLines = 150, forceFresh = false, indexPr
|
|
|
343
344
|
const git = gitAudit(root);
|
|
344
345
|
L.push("");
|
|
345
346
|
L.push("## ⑨ git 对账(自报足迹 vs 真实工作区)");
|
|
347
|
+
L.push(...scanExclusionNote(root));
|
|
346
348
|
if (git.available) {
|
|
347
349
|
if (git.error) {
|
|
348
350
|
L.push(`- 🚩 git status 对账失败(fail-closed): ${git.error}`);
|
|
@@ -351,10 +353,12 @@ export function auditStructure(root, maxLines = 150, forceFresh = false, indexPr
|
|
|
351
353
|
L.push("- 🚩 .gitignore 把 .pm/ 排除出版本控制——团队与多 AI 共享失效,状态只活在你本机。请移除该规则并提交 .pm/。");
|
|
352
354
|
}
|
|
353
355
|
if (git.unaccounted.length > 0) {
|
|
354
|
-
L.push(`- 🚩 ${git.unaccounted.length}
|
|
356
|
+
L.push(`- 🚩 ${git.unaccounted.length} 个文件的当前 git 变更未被会话内容证据覆盖: ${git.unaccounted.slice(0, 5).join(", ")}`);
|
|
355
357
|
}
|
|
356
|
-
if (
|
|
357
|
-
L.push("
|
|
358
|
+
if (git.unverified.length)
|
|
359
|
+
L.push(`- ⚠️ ${git.unverified.length} 个文件已有旧版路径记录,但缺少当前内容摘要,尚未验证: ${git.unverified.slice(0, 5).join(", ")}`);
|
|
360
|
+
if (!git.error && !git.ignoresPm && git.unaccounted.length === 0 && git.unverified.length === 0) {
|
|
361
|
+
L.push("✅ 工作区变更与已记录的会话内容证据一致。");
|
|
358
362
|
}
|
|
359
363
|
}
|
|
360
364
|
else {
|
package/dist/cli.js
CHANGED
|
@@ -9,6 +9,60 @@ if (process.argv[2] === "setup") {
|
|
|
9
9
|
process.exitCode = 1;
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
|
+
else if (process.argv[2] === "doctor") {
|
|
13
|
+
try {
|
|
14
|
+
const { parseDoctorArgs, runDoctor } = await import("./doctor.js");
|
|
15
|
+
if (process.argv.includes("--help") || process.argv.includes("-h"))
|
|
16
|
+
console.log("pm-mcp doctor --root <项目> [--client codex|workbuddy] [--config <配置文件> | --entry <本地入口>]");
|
|
17
|
+
else {
|
|
18
|
+
const result = await runDoctor(parseDoctorArgs(process.argv.slice(3)));
|
|
19
|
+
console.log(JSON.stringify(result, null, 2));
|
|
20
|
+
process.exitCode = result.ok ? 0 : 1;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
catch (error) {
|
|
24
|
+
console.error(`[pm-mcp doctor] ERROR: ${error.message}`);
|
|
25
|
+
process.exitCode = 1;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
else if (process.argv[2] === "ui") {
|
|
29
|
+
try {
|
|
30
|
+
const { startUiServer } = await import("./ui.js");
|
|
31
|
+
const args = process.argv.slice(3);
|
|
32
|
+
const value = (flag) => {
|
|
33
|
+
const index = args.indexOf(flag);
|
|
34
|
+
return index >= 0 ? args[index + 1] : undefined;
|
|
35
|
+
};
|
|
36
|
+
const root = value("--root") ?? process.cwd();
|
|
37
|
+
const portArg = value("--port");
|
|
38
|
+
const { url, close } = await startUiServer(root, { port: portArg ? Number(portArg) : 0 });
|
|
39
|
+
console.log(`pm-mcp ui 已启动(只读、仅绑定 127.0.0.1):${url} 按 Ctrl+C 退出`);
|
|
40
|
+
const shutdown = () => { void close().finally(() => process.exit(0)); };
|
|
41
|
+
process.on("SIGINT", shutdown);
|
|
42
|
+
process.on("SIGTERM", shutdown);
|
|
43
|
+
await new Promise(() => { });
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
console.error(`[pm-mcp ui] ERROR: ${error.message}`);
|
|
47
|
+
process.exitCode = 1;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
else if (process.argv[2] === "probe") {
|
|
51
|
+
try {
|
|
52
|
+
const { PROBE_USAGE, parseProbeArgs, runProbe } = await import("./probe.js");
|
|
53
|
+
if (process.argv.includes("--help") || process.argv.includes("-h"))
|
|
54
|
+
console.log(PROBE_USAGE);
|
|
55
|
+
else {
|
|
56
|
+
const report = await runProbe(parseProbeArgs(process.argv.slice(3)));
|
|
57
|
+
console.log(JSON.stringify(report, null, 2));
|
|
58
|
+
process.exitCode = report.ok ? 0 : 1;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
console.error(`[pm-mcp probe] ERROR: ${error.message}`);
|
|
63
|
+
process.exitCode = 1;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
12
66
|
else {
|
|
13
67
|
await import("./index.js");
|
|
14
68
|
}
|
package/dist/dashboard.js
CHANGED
|
@@ -4,7 +4,7 @@ import { atomicWrite, loadDebugLog, loadFeatures, loadProject, loadRoadmap, load
|
|
|
4
4
|
import { decisionsDir, pmPath, dashboardFile } from "./paths.js";
|
|
5
5
|
import { mermaidRoadmap, quotaWarnings, renderRoadmap, checkpointSuffix } from "./roadmap.js";
|
|
6
6
|
import { churnStats, debtAging } from "./health.js";
|
|
7
|
-
import { featureVerifyMark } from "./audit.js";
|
|
7
|
+
import { detectDrift, featureVerifyMark } from "./audit.js";
|
|
8
8
|
import { loadGovernance } from "./governance-model.js";
|
|
9
9
|
import { touchRegistry } from "./registry.js";
|
|
10
10
|
/** 生成 PROJECT.md 全文 */
|
|
@@ -100,17 +100,19 @@ export function buildDashboard(root) {
|
|
|
100
100
|
const highFindings = openFindings.filter((f) => f.severity === "high");
|
|
101
101
|
const churn = churnStats(sessions);
|
|
102
102
|
const implemented = features.filter((f) => f.status === "implemented");
|
|
103
|
-
|
|
103
|
+
// 与 audit_structure 的「漂移对账」共用同一实现,避免两处口径不一致:
|
|
104
|
+
// detectDrift 覆盖 implemented 功能的 entry_files 与 done 任务的 files。
|
|
105
|
+
const drift = detectDrift(root);
|
|
104
106
|
L.push(`| 账本 | 状态 |`);
|
|
105
107
|
L.push(`|---|---|`);
|
|
106
|
-
L.push(`| 漂移(防幻觉) | ${
|
|
108
|
+
L.push(`| 漂移(防幻觉) | ${drift.length === 0 ? "✅ 无" : `⚠️ ${drift.length} 项(功能入口文件或 done 任务关联文件缺失)`} |`);
|
|
107
109
|
L.push(`| 债务(反挤出) | ${aging.openDebt === 0 ? "✅ 无未清债务" : `⚠️ ${aging.openDebt} 条,最老 ${aging.oldestDays} 天`} |`);
|
|
108
110
|
L.push(`| churn(变更率) | ${churn.hotspots.length === 0 ? "✅ 无热点" : `⚠️ 热点 ${churn.hotspots.slice(0, 3).map((h) => `${h.file}(${h.count})`).join(", ")}`} |`);
|
|
109
111
|
L.push(`| 安全 | ${openFindings.length === 0 ? "✅ 无未处理发现" : `⚠️ ${openFindings.length} 个未处理(高危 ${highFindings.length})`} |`);
|
|
110
112
|
L.push(`| 调试知识 | ${debugEntries.length} 条记录 |`);
|
|
111
113
|
L.push(`| 测试背书 | ${implemented.length === 0 ? "—" : `${implemented.filter((f) => f.test_files.length > 0).length}/${implemented.length} 个功能带测试`} |`);
|
|
112
114
|
L.push(`| 语义治理 | ${governance ? `${governance.modules.length} 模块 / ${governance.interfaces.length} 接口 / ${governance.repositories.length} 仓库` : "⚠️ 未初始化"} |`);
|
|
113
|
-
L.push(`| 质量矩阵 | ${latestQuality ? `${latestQuality.ok ? "✅" : "🚩"} ${latestQuality.run_at.slice(0, 16).replace("T", " ")}
|
|
115
|
+
L.push(`| 质量矩阵 | ${latestQuality ? `${latestQuality.ok ? "✅" : "🚩"} ${latestQuality.run_at.slice(0, 16).replace("T", " ")}(命令 ${latestQuality.results.filter((item) => item.status === "passed").length}/${latestQuality.results.length};证据 ${latestQuality.evidence_level ?? "旧格式未标级"})` : "⚠️ 尚无真实执行记录"} |`);
|
|
114
116
|
L.push(`| 标准化验收 | ${latestAcceptance ? `${latestAcceptance.verdict === "accepted" ? "✅" : "🚩"} ${latestAcceptance.report_generated_at.slice(0, 16).replace("T", " ")}(需求 ${latestAcceptance.summary.requirements_passed}/${latestAcceptance.summary.requirements_total},errors ${latestAcceptance.summary.errors})` : "⚠️ 尚无正式评价报告"} |`);
|
|
115
117
|
L.push("");
|
|
116
118
|
/* 跨模块/语言治理(这里只读账本,不在每次写操作时重跑全仓语义图) */
|
package/dist/doctor.js
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
import os from "node:os";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
|
5
|
+
import { StdioClientTransport } from "@modelcontextprotocol/sdk/client/stdio.js";
|
|
6
|
+
import { PACKAGE_SPEC, RUNTIME_CONTRACT, SERVER_NAME, TOOL_CATALOG_SIZE, VERSION } from "./version.js";
|
|
7
|
+
import { canonicalProjectRoot, findCodexProjectEntry, isLocalLaunch, isLocalServerEntry, isPackageLaunch, readCodexConfig } from "./setup-project.js";
|
|
8
|
+
import { localProjectLaunch, workbuddyConfigPath } from "./setup.js";
|
|
9
|
+
export function parseDoctorArgs(argv) {
|
|
10
|
+
const options = { root: process.cwd() };
|
|
11
|
+
for (let index = 0; index < argv.length; index++) {
|
|
12
|
+
const arg = argv[index], value = argv[index + 1];
|
|
13
|
+
if (!["--root", "--entry", "--config", "--client"].includes(arg) || !value || value.startsWith("--"))
|
|
14
|
+
throw new Error(`doctor 参数无效: ${arg}`);
|
|
15
|
+
if (arg === "--root")
|
|
16
|
+
options.root = path.resolve(value);
|
|
17
|
+
if (arg === "--entry")
|
|
18
|
+
options.entry = path.resolve(value);
|
|
19
|
+
if (arg === "--config")
|
|
20
|
+
options.config = path.resolve(value);
|
|
21
|
+
if (arg === "--client") {
|
|
22
|
+
if (value !== "codex" && value !== "workbuddy")
|
|
23
|
+
throw new Error("doctor 支持 --client codex|workbuddy,其他客户端可用 --entry 验证本地入口。");
|
|
24
|
+
options.client = value;
|
|
25
|
+
}
|
|
26
|
+
index++;
|
|
27
|
+
}
|
|
28
|
+
if (options.entry && options.config)
|
|
29
|
+
throw new Error("--entry 与 --config 不能同时使用。");
|
|
30
|
+
return options;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* 从 WorkBuddy 的 `mcp.json` 中取出 pm-mcp 条目。
|
|
34
|
+
*
|
|
35
|
+
* 只读取启动定义(command/args),不复制也不输出任何自定义 env / 凭据。
|
|
36
|
+
* 文件必须是**有界普通文件**:链接、目录、超 2MB 一律拒绝。
|
|
37
|
+
*/
|
|
38
|
+
function readWorkbuddyEntry(file) {
|
|
39
|
+
if (!fs.existsSync(file))
|
|
40
|
+
return undefined;
|
|
41
|
+
const stat = fs.lstatSync(file);
|
|
42
|
+
if (!stat.isFile() || stat.isSymbolicLink() || stat.size > 2 * 1024 * 1024)
|
|
43
|
+
throw new Error("WorkBuddy 配置必须为有界普通文件,禁止链接。");
|
|
44
|
+
let parsed;
|
|
45
|
+
try {
|
|
46
|
+
parsed = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
47
|
+
}
|
|
48
|
+
catch (error) {
|
|
49
|
+
throw new Error(`WorkBuddy 配置不是合法 JSON:${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
const servers = parsed?.mcpServers;
|
|
52
|
+
const entry = servers?.[SERVER_NAME];
|
|
53
|
+
if (!entry)
|
|
54
|
+
return undefined;
|
|
55
|
+
if (entry.disabled === true)
|
|
56
|
+
throw new Error("WorkBuddy 中 pm-mcp 条目被禁用,现有客户端不会启动它。");
|
|
57
|
+
if (typeof entry.command !== "string" || entry.command.length === 0)
|
|
58
|
+
throw new Error("WorkBuddy 中 pm-mcp 条目缺少可识别的 command。");
|
|
59
|
+
const args = entry.args === undefined ? [] : entry.args;
|
|
60
|
+
if (!Array.isArray(args) || !args.every((value) => typeof value === "string"))
|
|
61
|
+
throw new Error("WorkBuddy 中 pm-mcp 条目的 args 不是字符串数组。");
|
|
62
|
+
return { command: entry.command, args: args };
|
|
63
|
+
}
|
|
64
|
+
/** 短期本地读连接;不把探测新进程冒充已重新连接的客户端。 */
|
|
65
|
+
export async function runDoctor(options) {
|
|
66
|
+
const root = path.resolve(options.root);
|
|
67
|
+
const report = {
|
|
68
|
+
ok: false, expected_version: VERSION, root, configuration: options.entry ? "explicit_entry" : (options.client ?? "codex"), issues: [],
|
|
69
|
+
existing_client: "not_observed", note: "这是新建的本地探测连接。现有客户端须重新连接,并在其中调用 get_status 核对运行版本;未读取或转发自定义凭据环境。",
|
|
70
|
+
};
|
|
71
|
+
let launch;
|
|
72
|
+
try {
|
|
73
|
+
if (options.entry)
|
|
74
|
+
launch = { command: process.execPath, args: [options.entry, "--root", root] };
|
|
75
|
+
else if (options.client === "workbuddy") {
|
|
76
|
+
const file = options.config ?? workbuddyConfigPath(process.env, os.homedir(), fs.existsSync);
|
|
77
|
+
const entry = readWorkbuddyEntry(file);
|
|
78
|
+
if (!entry)
|
|
79
|
+
throw new Error("未在 WorkBuddy 的 mcp.json 中找到 pm-mcp 条目。");
|
|
80
|
+
report.configured_server = SERVER_NAME;
|
|
81
|
+
if (!isLocalServerEntry(entry))
|
|
82
|
+
throw new Error("WorkBuddy 中的 pm-mcp 不是可识别的本地 pm-mcp 启动项;未执行未知启动命令。");
|
|
83
|
+
// 配置通常不钉根(由会话工作区决定);探测需要明确目标根,故补上 --root。
|
|
84
|
+
launch = entry.args.includes("--root") ? entry : { command: entry.command, args: [...entry.args, "--root", root] };
|
|
85
|
+
report.configuration = "workbuddy_local_probe";
|
|
86
|
+
}
|
|
87
|
+
else {
|
|
88
|
+
if (!options.config && fs.existsSync(path.join(root, ".codex", "config.toml")))
|
|
89
|
+
throw new Error("存在项目级 Codex 配置,可能覆盖全局设置;请用 --config 明确核验的配置文件并检查实际客户端。");
|
|
90
|
+
const file = options.config ?? path.join(process.env.CODEX_HOME || path.join(os.homedir(), ".codex"), "config.toml");
|
|
91
|
+
const entry = findCodexProjectEntry(readCodexConfig(file), root);
|
|
92
|
+
if (!entry)
|
|
93
|
+
throw new Error("未找到已明确绑定此项目的 Codex pm-mcp 配置。");
|
|
94
|
+
report.configured_server = entry.name;
|
|
95
|
+
if (!entry.enabled)
|
|
96
|
+
throw new Error("该服务在配置中被禁用,现有客户端不会启动它。");
|
|
97
|
+
if (entry.toolFilters)
|
|
98
|
+
throw new Error(`存在工具过滤设置,请核对后再确认全部${TOOL_CATALOG_SIZE}工具可用。`);
|
|
99
|
+
if (isPackageLaunch(entry)) {
|
|
100
|
+
if (entry.args[1] !== PACKAGE_SPEC)
|
|
101
|
+
throw new Error(`启动配置版本与本包 ${PACKAGE_SPEC} 不一致,请先预览并升级配置。`);
|
|
102
|
+
launch = localProjectLaunch(root);
|
|
103
|
+
report.configuration = "codex_package_matches_local_probe";
|
|
104
|
+
}
|
|
105
|
+
else
|
|
106
|
+
launch = entry;
|
|
107
|
+
}
|
|
108
|
+
if (!isLocalLaunch(launch))
|
|
109
|
+
throw new Error("探测入口不是可识别的本地 pm-mcp 包;不执行未知启动命令。");
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
report.issues.push(error.message);
|
|
113
|
+
return report;
|
|
114
|
+
}
|
|
115
|
+
const client = new Client({ name: "pm-mcp-doctor", version: VERSION });
|
|
116
|
+
const transport = new StdioClientTransport({ command: launch.command, args: launch.args,
|
|
117
|
+
env: { PM_MCP_HOME: path.join(root, ".pm", ".runtime", "doctor-home") }, stderr: "pipe" });
|
|
118
|
+
const timer = setTimeout(() => { void client.close(); }, 20_000);
|
|
119
|
+
timer.unref();
|
|
120
|
+
try {
|
|
121
|
+
await client.connect(transport, { timeout: 10_000 });
|
|
122
|
+
transport.stderr?.on("data", () => undefined);
|
|
123
|
+
const info = client.getServerVersion();
|
|
124
|
+
const inventory = await client.listTools({}, { timeout: 10_000 });
|
|
125
|
+
const status = await client.callTool({ name: "get_status", arguments: {} }, undefined, { timeout: 10_000 });
|
|
126
|
+
const runtime = status.structuredContent?.runtime;
|
|
127
|
+
report.probe = { server: info?.name, version: info?.version, tools: inventory.tools.length, runtime };
|
|
128
|
+
if (info?.name !== SERVER_NAME || info.version !== VERSION)
|
|
129
|
+
report.issues.push("握手返回的服务名或运行版本不匹配。");
|
|
130
|
+
if (status.isError || typeof runtime?.root !== "string" || canonicalProjectRoot(runtime.root) !== canonicalProjectRoot(root))
|
|
131
|
+
report.issues.push("实际状态读取未确认目标项目根。");
|
|
132
|
+
if (runtime?.version !== VERSION || runtime?.contract !== RUNTIME_CONTRACT)
|
|
133
|
+
report.issues.push("实际运行契约不支持本轮升级,不能仅凭配置宣称生效。");
|
|
134
|
+
const list = inventory.tools.find((tool) => tool.name === "list_tasks");
|
|
135
|
+
const project = inventory.tools.find((tool) => tool.name === "update_project");
|
|
136
|
+
if (inventory.tools.length !== TOOL_CATALOG_SIZE || !list?.inputSchema.properties?.cursor || !project?.inputSchema.properties?.completion_evidence)
|
|
137
|
+
report.issues.push("工具目录或新参数契约不完整。");
|
|
138
|
+
report.ok = report.issues.length === 0;
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
report.issues.push(`本地连接验证失败: ${error.message}`);
|
|
142
|
+
}
|
|
143
|
+
finally {
|
|
144
|
+
clearTimeout(timer);
|
|
145
|
+
await client.close();
|
|
146
|
+
}
|
|
147
|
+
return report;
|
|
148
|
+
}
|