@luckychen1993/pm-mcp 0.1.4 → 0.3.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/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
+ 用户仍然直接说“继续这个任务”“保存进度”“完成后帮我验收”。49 个工具全部保留,由 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,19 +68,41 @@ AI 编码项目的典型失控(本工具逐一给出机制):
32
68
 
33
69
  ## 一个命令自动添加到 AI 编程助手
34
70
 
71
+ 本页安装示例使用 npm 上最新的**已发布**版本(当前为 **0.1.5**)。0.1.4 不支持 `setup --project`;验证这些能力时请确认实际运行版本为 0.1.5 或更新版本。仓库源码当前为 **0.3.0**(新增任务游标分页、完成证据分级、受控升级与 WorkBuddy 客户端等),**尚未发布到 npm**——需要这些能力时请从源码构建,或见下方各客户端的本地入口用法。
72
+
35
73
  要求:Node.js ≥ 22.18。Windows、macOS 和 Linux 使用同一条命令:
36
74
 
37
75
  ```bash
38
76
  npx -y @luckychen1993/pm-mcp@latest setup
39
77
  ```
40
78
 
41
- 安装器自动检测本机的 Codex、Claude Code、ZCode、Cursor 和 VS Code,并配置所有检测到的客户端。JSON 配置会先备份;已经存在的 Codex/Claude 配置默认保留,用 `setup --force` 才替换。先预览、不写配置:
79
+ 安装器自动检测本机的 Codex、Claude Code、ZCode、Cursor、VS Code 和 WorkBuddy,并配置所有检测到的客户端。JSON 配置会先备份;已经存在的 Codex/Claude 配置默认保留,用 `setup --force` 才替换。先预览、不写配置:
42
80
 
43
81
  ```bash
44
82
  npx -y @luckychen1993/pm-mcp@latest setup --dry-run
45
83
  ```
46
84
 
47
- 非标准安装路径无法自动发现时,可在同一命令末尾指定 `--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-<目录名>-<路径摘要>`)。不同中文目录和同名目录独立注册;重复执行会核对项目路径,匹配的旧版条目仍可复用。
96
+
97
+ ### 新项目一条命令上手(Codex 必用)
98
+
99
+ Codex 桌面版启动 MCP 服务时不带工作区路径,逐项目钉根才能用。在项目目录里跑一条命令,自动完成「注册钉定条目 + 初始化 .pm + 写 AGENTS.md 工作规矩」:
100
+
101
+ ```bash
102
+ npx -y @luckychen1993/pm-mcp@0.1.5 setup --client codex --project
103
+ ```
104
+
105
+ 跑完完全重启 Codex,新会话即可用(工具名前缀 `pm-mcp-<目录名>-<路径摘要>`)。不同中文目录和同名目录独立注册;重复执行会核对项目路径,匹配的旧版条目仍可复用。
48
106
 
49
107
  ### 客户端官方命令(自动检测失败时备用)
50
108
 
@@ -53,7 +111,7 @@ npx -y @luckychen1993/pm-mcp@latest setup --dry-run
53
111
  Codex 的本地客户端共享同一份 MCP 配置。复制执行:
54
112
 
55
113
  ```bash
56
- codex mcp add pm-mcp -- npx -y @luckychen1993/pm-mcp@0.1.4
114
+ codex mcp add pm-mcp -- npx -y @luckychen1993/pm-mcp@0.1.5
57
115
  ```
58
116
 
59
117
  重启客户端后可用 `codex mcp list` 检查。该命令遵循 [OpenAI 官方 MCP CLI 格式](https://learn.chatgpt.com/docs/extend/mcp?surface=cli)。
@@ -61,7 +119,7 @@ codex mcp add pm-mcp -- npx -y @luckychen1993/pm-mcp@0.1.4
61
119
  #### Claude Code
62
120
 
63
121
  ```bash
64
- claude mcp add pm-mcp --scope user -- npx -y @luckychen1993/pm-mcp@0.1.4
122
+ claude mcp add pm-mcp --scope user -- npx -y @luckychen1993/pm-mcp@0.1.5
65
123
  ```
66
124
 
67
125
  #### ZCode、Cursor、VS Code(Windows 备用脚本)
@@ -69,11 +127,42 @@ claude mcp add pm-mcp --scope user -- npx -y @luckychen1993/pm-mcp@0.1.4
69
127
  将 `<client>` 替换为 `zcode`、`cursor` 或 `vscode`,整行复制到 PowerShell:
70
128
 
71
129
  ```powershell
72
- $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.4/install.ps1'; $f=Join-Path $env:TEMP 'install-pm-mcp.ps1'; Invoke-WebRequest $u -OutFile $f; & $f -Client <client>
130
+ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.5/install.ps1'; $f=Join-Path $env:TEMP 'install-pm-mcp.ps1'; Invoke-WebRequest $u -OutFile $f; & $f -Client <client>
73
131
  ```
74
132
 
75
133
  脚本只写对应客户端的 MCP 配置;已有 JSON 配置会先生成带时间戳的备份。建议执行前先打开 [`install.ps1`](install.ps1) 审阅。ZCode 默认写入 `$ZCODE_HOME/cli/config.json` 或 `~/.zcode/cli/config.json`,Cursor 默认写入 `~/.cursor/mcp.json`;也可用 `-ConfigPath` 指定路径。
76
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
+
77
166
  #### 其他支持 stdio MCP 的客户端
78
167
 
79
168
  ```json
@@ -81,7 +170,7 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.4/install.ps1'; $f=
81
170
  "mcpServers": {
82
171
  "pm-mcp": {
83
172
  "command": "npx",
84
- "args": ["-y", "@luckychen1993/pm-mcp@0.1.4"],
173
+ "args": ["-y", "@luckychen1993/pm-mcp@0.1.5"],
85
174
  "env": {}
86
175
  }
87
176
  }
@@ -98,6 +187,21 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.4/install.ps1'; $f=
98
187
  | `PM_ROOT` | 项目根(次优先级) |
99
188
  | `PM_MCP_HOME` | 全局注册表重定向(默认 `~`;多环境/测试隔离用) |
100
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
+
101
205
  ## 推荐工作流(写给 AI 的规矩)
102
206
 
103
207
  把下面这段放进目标项目的 `AGENTS.md`,任何 AI 会话自动遵守:
@@ -114,16 +218,16 @@ $u='https://raw.githubusercontent.com/happy520ai/pm-mcp/v0.1.4/install.ps1'; $f=
114
218
  4. 陈述本次计划:做哪个任务、动哪些文件、验收标准
115
219
 
116
220
  ## 做事过程
117
- - 长任务:add_task 时给 steps;上下文快满或中断前 checkpoint 存断点
221
+ - 长任务:add_task 时给 steps;update_task 附 checkpoint 一次更新进度与会话
118
222
  - 走捷径:立刻 add_task(type=debt) 登记债务,不许无声欠债
119
223
  - 修 bug:完成后 log_debug 记录症状/根因/修法/验证
120
224
  - 陈述"某功能在某文件"之前,先 search_code 求证
121
225
 
122
226
  ## 收工(缺一不可)
123
- 1. 完成的任务 update_task 置 done + result_note;feature/fix 补 verification
227
+ 1. feature/fix 先 run_quality_matrix 执行测试;update_task 置 done + result_note + files,record_session:true 一并归档
124
228
  2. 未完成任务 checkpoint(进展 + 下一步)
125
229
  3. 落地的功能 register_feature(入口文件 + 测试文件)
126
- 4. log_session 如实记录改动文件清单
230
+ 4. update_task 已返回自动会话编号时不重复 log_session;否则如实记录改动文件清单。纯问答无需新建任务或重复记账
127
231
 
128
232
  ## 每个里程碑节点
129
233
  snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit_license 许可证审计;audit_governance 模块/语言边界审计;plan_quality_matrix 生成真实质量矩阵
@@ -131,24 +235,25 @@ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit
131
235
 
132
236
  也可以直接用内置 prompts:`start-session` / `end-session` / `onboard` / `architecture-review` / `acceptance-review`;其中 `onboard` 会引导客户端读取状态并生成新人/AI 入职简报。
133
237
 
134
- ## 工具清单(46 个)
238
+ ## 工具清单(49 个)
135
239
 
136
240
  | 域 | 工具 | 说明 |
137
241
  |---|---|---|
138
242
  | 状态 | `init_project` / `get_status` / `update_project` / `regenerate_dashboard` | 初始化、一站式「我在哪」(`since` 汇总任务/会话变化)、改元信息与预算、重生成仪表盘 |
139
243
  | 路线图 | `add_milestone` / `update_milestone` / `get_roadmap` | 里程碑生命周期;depth=1 单行摘要 / depth=2 展开活跃任务 |
140
- | 任务 | `add_task` / `list_tasks` / `update_task` / `checkpoint` | 类型 feature/refactor/fix/chore/debt;转 done 强制 result_note;断点存档 |
244
+ | 任务 | `add_task` / `list_tasks` / `update_task` / `checkpoint` | 进度/断点/会话合并更新;feature/fix 完成强制关联文件与最新真实测试报告 |
141
245
  | 功能 | `register_feature` / `list_features` | 功能地图(入口文件 = 漂移对账锚点;测试文件 = 测试背书) |
142
246
  | 决策 | `record_decision` / `list_decisions` | ADR 架构决策记录(不可变编号) |
143
- | 会话 | `log_session` | 收工仪式:摘要 + 变更足迹 + 下一步(churn 数据源) |
247
+ | 会话 | `log_session` | 摘要 + 变更足迹/内容摘要 + 下一步(churn 与 Git 内容对账来源) |
144
248
  | 调试 | `log_debug` | 调试知识账:症状/根因/修法/验证 |
145
249
  | 求证 | `search_code` / `search_knowledge` / `annotate_file` | 代码检索(file:line)、七类知识源检索(含模块/接口/仓库治理)、文件用途索引 |
146
250
  | 审计 | `snapshot_codebase` / `audit_structure` | 结构快照;完整性/增长/漂移/债务/churn/复杂度/索引/足迹/测试/Git 十节对账 |
147
251
  | 安全 | `audit_security` / `list_findings` / `resolve_finding` | 密钥+危险模式+新增/通配依赖提示;扫描命中原文不落盘,处置 note 拒绝内置密钥形态 |
148
252
  | 法律 | `audit_license` | 依赖许可证清单、copyleft 冲突、GPL 头检测、LICENSE 检查、来源登记 |
253
+ | 可观测 | `get_usage_log` / `get_runtime_log` | 使用日志(工具调用统计、错误率、耗时、输出与折叠省下的 token 估算——验证省不省 token)与运行日志(server/watcher/工具报错);只记计量不记参数内容 |
149
254
  | 注册表 | `list_projects` | 本机所有被管理项目 |
150
255
  | 治理模型 | `init_governance` / `get_governance` / `upsert_module` / `upsert_interface` / `upsert_repository` / `set_governance_policies` | 结构化模块根、owner、语言、公开接口、允许/禁止依赖、跨仓版本约束与强制策略 |
151
- | 语义治理 | `discover_languages` / `audit_governance` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` | 编译器/Tree-sitter AST 关系、hash-bound 原生分析器/运行时证据、循环/越界/unresolved/覆盖率与变更反向闭包 |
256
+ | 语义治理 | `discover_languages` / `audit_governance` / `impact_analysis` / `list_semantic_evidence` / `save_semantic_evidence` / `replace_semantic_evidence_for_file` | 编译器/Tree-sitter AST 关系、hash-bound 原生分析器/运行时证据、循环/越界/unresolved/覆盖率与变更反向闭包 |
152
257
  | 质量与组合 | `plan_quality_matrix` / `run_quality_matrix` / `get_portfolio` | plan 与真实执行严格分离;shell=false;跨仓阶段/债务/安全/版本/cycle 聚合 |
153
258
  | 标准化验收 | `list_acceptance_baselines` / `get_acceptance_baseline` / `save_acceptance_baseline_draft` / `approve_acceptance_baseline` / `evaluate_acceptance` | ISO/SQuaRE 对齐的版本化质量基线、冻结证据指针、需求—风险—测试追踪、机器判定报告与 SHA-256 manifest |
154
259
 
@@ -312,7 +417,7 @@ npm run gate # 完整本地门禁
312
417
  | 单元 | budget/store/roadmap/health/security/license/audit/search/dashboard | 各账本与机制的正确性(含伪造密钥/危险模式 fixture、已识别命中原文不落盘断言) |
313
418
  | 治理 | governance-model/language-adapters/semantic-graph/semantic-evidence/polyglot-ast/portfolio/governance-audit | 模块 schema、八语言 AST、symbol-bound call、原生/runtime证据、私有接口、循环、影响闭包、semver 与跨仓 fail-closed |
314
419
  | 标准化验收 | acceptance-evaluator/acceptance-tools/acceptance-gate/quality-evidence | 九特性/五阶段、冻结指标与断言、双向追踪、风险接受、源码/证据/报告 SHA-256、伪造 PASS 失败 |
315
- | 集成 | integration/governance-mcp/quality-gate | 真实拉起 stdio server 全链路:46 工具、7 资源、5 提示词;真实执行安全 argv 质量命令 |
420
+ | 集成 | integration/governance-mcp/quality-gate | 真实拉起 stdio server 全链路:48 工具、7 资源、5 提示词;真实执行安全 argv 质量命令 |
316
421
  | 场景 | scenario.test.ts | **对 dist 产物**跑三段会话生命周期:中文+空格路径、CRLF、断点跨进程恢复、故障注入(删测试/加 skip/删入口文件/新增依赖/伪造密钥/GPL 头)、三种根解析方式(cwd / PM_ROOT / --root)各用一遍 |
317
422
  | 完整性 + 真实仓库 | integrity.test.ts / realrepo.test.ts | 状态不变式(全账本 schema、派生逐字节一致、原子性、损坏恢复、500 任务规模折叠、并发双进程零丢失);并以本仓库真实 `.pm` 为被测对象(漂移为零、PROJECT.md 同步、done 纪律、全仓拷贝审计不误报) |
318
423
  | 钻空(红队) | exploit.test.ts | 每个反制机制配一个「先钻、再断言钻不进」的测试(见上表) |
@@ -0,0 +1,51 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ /**
4
+ * AGENTS.md 工作规矩:init_project 时自动写入/合并("项目开启即有规矩")。
5
+ * - 文件不存在 → 创建
6
+ * - 已存在且无标记段 → 末尾追加(绝不覆盖你的内容)
7
+ * - 已存在且有标记段 → 原样保留(幂等;模板升级时用新版本号标记替换)
8
+ */
9
+ const BEGIN = "<!-- pm-mcp:agents-md:v1:begin -->";
10
+ const END = "<!-- pm-mcp:agents-md:v1:end -->";
11
+ const SECTION = `${BEGIN}
12
+ # 项目管理规矩(pm-mcp 自动维护段落,可整体删除但不建议)
13
+
14
+ 本项目由 pm-mcp 管理,状态以 .pm/ 目录为准,禁止凭记忆陈述项目状态。
15
+
16
+ ## 开工(每次会话第一件事)
17
+ 1. get_status:了解阶段、里程碑、进行中任务与其 checkpoint 下一步
18
+ 2. 不清楚的模块先 search_code 定位到 file:line 再读文件,不要全量读代码
19
+ 3. 陈述本次计划:做哪个任务、动哪些文件、验收标准
20
+
21
+ ## 做事过程
22
+ - 长任务:add_task 时给 steps;用 update_task 附 checkpoint 一次更新进度与会话;保留独立 checkpoint 兼容旧流程
23
+ - 走捷径:立刻 add_task(type=debt) 登记债务,不许无声欠债
24
+ - 修 bug:完成后 log_debug 记录症状/根因/修法/验证
25
+ - 陈述「某功能在某文件」之前,先 search_code 求证
26
+
27
+ ## 收工(缺一不可)
28
+ 1. feature/fix 先执行相应测试并生成当前源码的质量报告;update_task 置 done + result_note + files,设 record_session:true 一并归档
29
+ 2. 未完成任务 checkpoint(进展 + 下一步具体动作)
30
+ 3. 落地的功能 register_feature(入口文件 + 测试文件)
31
+ 4. update_task 已返回自动会话编号时不重复 log_session;否则用 log_session 如实记录改动文件清单。纯问答无需新建任务或重复记账
32
+
33
+ ## 每个里程碑节点
34
+ snapshot_codebase + audit_structure 对账;audit_security 安全体检;audit_license 许可证审计
35
+ ${END}`;
36
+ /** 幂等 upsert:返回发生了什么(skipped = 调用方显式关闭) */
37
+ export function upsertAgentsMd(root, enabled = true) {
38
+ if (!enabled)
39
+ return "skipped";
40
+ const file = path.join(root, "AGENTS.md");
41
+ if (!fs.existsSync(file)) {
42
+ fs.writeFileSync(file, SECTION + "\n", "utf8");
43
+ return "created";
44
+ }
45
+ const content = fs.readFileSync(file, "utf8");
46
+ if (content.includes(BEGIN))
47
+ return "unchanged";
48
+ const separator = content.endsWith("\n") ? "" : "\n";
49
+ fs.writeFileSync(file, content + separator + "\n" + SECTION + "\n", "utf8");
50
+ return "appended";
51
+ }
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 { normSep, foldLines } from "./budget.js";
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
- const status = spawnSync("git", ["status", "--porcelain", "-uall"], { cwd: root, encoding: "utf8", timeout: 10_000 });
152
- if (status.error || status.status !== 0) {
153
- out.error = status.error?.message ?? `git status 退出码 ${status.status ?? "未知"}: ${(status.stderr ?? "").trim().slice(0, 200)}`;
154
- return out;
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
- const changed = new Set();
157
- for (const line of status.stdout.split("\n")) {
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} 个文件有 git 变更但未在最近一次会话入账(log_session 漏报会被这里抓到): ${git.unaccounted.slice(0, 5).join(", ")}`);
356
+ L.push(`- 🚩 ${git.unaccounted.length} 个文件的当前 git 变更未被会话内容证据覆盖: ${git.unaccounted.slice(0, 5).join(", ")}`);
355
357
  }
356
- if (!git.error && !git.ignoresPm && git.unaccounted.length === 0) {
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/budget.js CHANGED
@@ -1,4 +1,32 @@
1
1
  import { relative as pathRelative } from "node:path";
2
+ import { estimateTokens } from "./usage-log.js";
3
+ /**
4
+ * token 经济:读类工具输出硬预算。
5
+ * 超出预算自动折叠为 "Top N + 另有 M 项已折叠",引导模型用过滤参数缩小范围。
6
+ * 折叠/截断省下的字符与 token 会记账,供使用日志统计「省了多少 token」。
7
+ */
8
+ let foldSavedChars = 0;
9
+ let foldSavedTokens = 0;
10
+ function noteFoldSavings(full, out) {
11
+ const savedChars = full.length - out.length;
12
+ if (savedChars <= 0)
13
+ return;
14
+ foldSavedChars += savedChars;
15
+ foldSavedTokens += Math.max(0, estimateTokens(full) - estimateTokens(out));
16
+ }
17
+ /**
18
+ * 取走自上次调用以来折叠/截断省下的字符与 token(估算)。
19
+ * 单进程内计数:并发异步调用之间的归因可能漂移,但总量精确(每次折叠恰好记一次)。
20
+ * 工具包装器在业务函数返回后调用,把省量归到本次工具调用。
21
+ */
22
+ export function drainFoldSavings() {
23
+ if (foldSavedChars <= 0)
24
+ return null;
25
+ const out = { chars: foldSavedChars, tokens: foldSavedTokens };
26
+ foldSavedChars = 0;
27
+ foldSavedTokens = 0;
28
+ return out;
29
+ }
2
30
  /** 限制字符串单行长度,超长截断(防止一行巨长文件吃 token) */
3
31
  export function capLine(line, maxChars = 300) {
4
32
  if (line.length <= maxChars)
@@ -12,8 +40,11 @@ export function capLine(line, maxChars = 300) {
12
40
  export function foldLines(lines, opts) {
13
41
  const { maxLines, hint } = opts;
14
42
  const capped = lines.map((l) => capLine(l));
15
- if (capped.length <= maxLines)
16
- return capped.join("\n");
43
+ if (capped.length <= maxLines) {
44
+ const out = capped.join("\n");
45
+ noteFoldSavings(lines.join("\n"), out);
46
+ return out;
47
+ }
17
48
  const keep = Math.max(1, Math.floor(maxLines * 0.8));
18
49
  const tail = Math.max(0, maxLines - keep - 1);
19
50
  const hidden = capped.length - keep - tail;
@@ -21,7 +52,9 @@ export function foldLines(lines, opts) {
21
52
  parts.push(`…(另有 ${hidden} 项已折叠${hint ? "," + hint : ""},请用过滤参数缩小范围)`);
22
53
  if (tail > 0)
23
54
  parts.push(...capped.slice(-tail));
24
- return parts.join("\n");
55
+ const out = parts.join("\n");
56
+ noteFoldSavings(lines.join("\n"), out);
57
+ return out;
25
58
  }
26
59
  /** 对整段文本按行折叠 */
27
60
  export function foldText(text, maxLines, hint) {
package/dist/cli.js CHANGED
@@ -9,6 +9,38 @@ 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] === "probe") {
29
+ try {
30
+ const { PROBE_USAGE, parseProbeArgs, runProbe } = await import("./probe.js");
31
+ if (process.argv.includes("--help") || process.argv.includes("-h"))
32
+ console.log(PROBE_USAGE);
33
+ else {
34
+ const report = await runProbe(parseProbeArgs(process.argv.slice(3)));
35
+ console.log(JSON.stringify(report, null, 2));
36
+ process.exitCode = report.ok ? 0 : 1;
37
+ }
38
+ }
39
+ catch (error) {
40
+ console.error(`[pm-mcp probe] ERROR: ${error.message}`);
41
+ process.exitCode = 1;
42
+ }
43
+ }
12
44
  else {
13
45
  await import("./index.js");
14
46
  }
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
- const drifted = implemented.filter((f) => f.entry_files.some((p) => !fs.existsSync(path.join(root, p))));
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(`| 漂移(防幻觉) | ${drifted.length === 0 ? "✅ 无" : `⚠️ ${drifted.length} 个功能入口文件缺失`} |`);
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", " ")}(${latestQuality.results.filter((item) => item.status === "passed").length}/${latestQuality.results.length})` : "⚠️ 尚无真实执行记录"} |`);
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
  /* 跨模块/语言治理(这里只读账本,不在每次写操作时重跑全仓语义图) */