plan-governance-cli 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +30 -24
- package/bin/plan-governance-cli.mjs +65 -22
- package/package.json +2 -1
- package/resources/manifest.json +14 -2
- package/resources/migration-skill/SKILL.md +14 -0
- package/resources/migration-skill/agents/openai.yaml +4 -0
- package/resources/skill/SKILL.md +2 -2
- package/resources/skill/assets/PLAN_MAP.template.md +1 -1
- package/resources/skill/references/cli.md +3 -3
- package/resources/skill/references/planning.md +3 -1
- package/resources/skill/references/verification.md +1 -1
- package/scripts/check_plan_governance.py +62 -17
- package/scripts/init_plan_governance.py +31 -44
package/README.md
CHANGED
|
@@ -55,12 +55,12 @@ docs/
|
|
|
55
55
|
| 文档 | 权威内容 |
|
|
56
56
|
|---|---|
|
|
57
57
|
| `docs/PLAN_MAP.md` | 计划索引、状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项、证据链接 |
|
|
58
|
-
| `docs/plans/*.md` |
|
|
58
|
+
| `docs/plans/YYYYMMDD/*.md` | 新计划的目标、范围、行为差异、阶段、Step 0、验证与完成条件;旧式 `docs/plans/*.md` 继续兼容 |
|
|
59
59
|
| 已有 Schema/OpenAPI 或按需的 `docs/specs/*.md` | 跨迭代的现行契约;优先复用已有来源,文字 spec 补充行为/失败语义,不重复字段定义 |
|
|
60
60
|
| `docs/adr/*.md` | 关键架构决策、备选方案和后果 |
|
|
61
61
|
| `docs/migrations/*.md` | 兼容策略、迁移步骤、回滚方式和旧行为保留窗口 |
|
|
62
62
|
|
|
63
|
-
`docs/plans
|
|
63
|
+
新计划按创建时的本地日期存放在 `docs/plans/YYYYMMDD/<plan>.md`;修改时不改变日期目录。根目录旧计划继续可读,工具升级不会自动迁移。用户明确要求迁移存量计划时,单独调用 `plan-governance-migration` skill。其他目录按真实内容增加:`docs/reviews/` 放独立复核,`docs/fixtures/` 放人工样本/回放说明,`tests/fixtures/` 放可执行输入,`docs/attestations/` 放按需快照。init 只创建最小结构;可选 [spec 模板](resources/skill/assets/spec.template.md) 随 skill 分发,不自动创建目标项目 spec 或空目录。
|
|
64
64
|
|
|
65
65
|
## 任务分流与验收
|
|
66
66
|
|
|
@@ -85,7 +85,7 @@ docs/
|
|
|
85
85
|
|
|
86
86
|
启用治理后,已有草案、历史设计、归档计划、临时分析文档等默认只作为背景材料,不再作为规范事实源。
|
|
87
87
|
|
|
88
|
-
- 新发生的目标、范围、公共契约、字段、Schema、状态语义、阶段、验证方式、完成条件、风险和回滚,应写入 `docs/plans/*.md`、ADR、migration 或正式 spec。
|
|
88
|
+
- 新发生的目标、范围、公共契约、字段、Schema、状态语义、阶段、验证方式、完成条件、风险和回滚,应写入 `docs/plans/YYYYMMDD/*.md`、ADR、migration 或正式 spec。
|
|
89
89
|
- 计划状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接,应写入或同步 `docs/PLAN_MAP.md`。
|
|
90
90
|
- 不要为了“保持一致”而修改草案或历史文档;除非用户明确点名该文件,否则草案只在治理文档中作为背景材料引用。
|
|
91
91
|
- 验收时应搜索并修正“草案为准”“以草案为事实源”“详见草案”等表达。
|
|
@@ -110,8 +110,7 @@ plan-governance-cli init \
|
|
|
110
110
|
```text
|
|
111
111
|
.git/
|
|
112
112
|
docs/PLAN_MAP.md
|
|
113
|
-
docs/plans/api-compat-migration.md
|
|
114
|
-
CLAUDE.md
|
|
113
|
+
docs/plans/20260926/api-compat-migration.md
|
|
115
114
|
AGENTS.md
|
|
116
115
|
```
|
|
117
116
|
|
|
@@ -119,12 +118,11 @@ AGENTS.md
|
|
|
119
118
|
|
|
120
119
|
如果目标目录还不是 Git 仓库,初始化流程会先执行 `git init`;已有 `.git/` 时会跳过,不重复初始化。
|
|
121
120
|
|
|
122
|
-
`--update-agent-rules` 会创建或更新 `
|
|
121
|
+
`--update-agent-rules` 会创建或更新 `AGENTS.md` 中带标记的计划治理章节,只写稳定执行规则,不写具体计划内容。具体计划仍以 `docs/PLAN_MAP.md` 中的真实链接为准,新计划默认位于 `docs/plans/YYYYMMDD/`。
|
|
123
122
|
|
|
124
|
-
|
|
123
|
+
也可以使用含义更直接的别名:
|
|
125
124
|
|
|
126
125
|
```bash
|
|
127
|
-
--update-claude-md
|
|
128
126
|
--update-agents-md
|
|
129
127
|
```
|
|
130
128
|
|
|
@@ -136,7 +134,7 @@ AGENTS.md
|
|
|
136
134
|
|
|
137
135
|
## 更新已有项目
|
|
138
136
|
|
|
139
|
-
如果项目已经有 `docs/PLAN_MAP.md`
|
|
137
|
+
如果项目已经有 `docs/PLAN_MAP.md` 和计划文件,不要重新初始化计划文档。只更新代理执行规则时运行:
|
|
140
138
|
|
|
141
139
|
```bash
|
|
142
140
|
plan-governance-cli init \
|
|
@@ -144,7 +142,7 @@ plan-governance-cli init \
|
|
|
144
142
|
--update-agent-rules-only
|
|
145
143
|
```
|
|
146
144
|
|
|
147
|
-
|
|
145
|
+
`--update-agents-md-only` 与上述兼容别名作用相同,也只更新 `AGENTS.md`。
|
|
148
146
|
|
|
149
147
|
如果要升级已有项目的辅助文件,刷新检查脚本并更新代理规则,但不覆盖 `docs/`,运行:
|
|
150
148
|
|
|
@@ -157,8 +155,8 @@ plan-governance-cli init \
|
|
|
157
155
|
`--upgrade-existing` 会:
|
|
158
156
|
|
|
159
157
|
- 按兼容模式覆盖更新项目中的 `scripts/check_plan_governance.py`
|
|
160
|
-
- 创建或更新 `
|
|
161
|
-
- 保留已有 `docs/PLAN_MAP.md`
|
|
158
|
+
- 创建或更新 `AGENTS.md` 中带标记的计划治理章节,既有其他代理入口保持原样
|
|
159
|
+
- 保留已有 `docs/PLAN_MAP.md` 和所有现存计划文件;兼容读取平铺和日期目录计划
|
|
162
160
|
- 提示缺失的治理文档
|
|
163
161
|
|
|
164
162
|
新项目和日常升级不需要使用该兼容模式;优先使用全局 npm CLI。
|
|
@@ -209,13 +207,20 @@ plan-governance-cli graph code impact --repo modelpad --file Sources/ModelPadCor
|
|
|
209
207
|
|
|
210
208
|
[验证规范](resources/skill/references/verification.md)统一维护单次复核、任务复用、证据复用、修复自验与用户验收;其他会话从同一 skill 读取。需要独立复核且授权明确时自动安排只读 subagent,复用已有同范围任务/结果,等待异常先核准任务 ID 和状态,且复核者不得再委派复核;不为普通小改逐次派发。技术完成待用户验收的适用功能计划仍保持实施中。
|
|
211
209
|
|
|
212
|
-
|
|
210
|
+
恢复已有计划时可读取派生工作集:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
plan-governance-cli workset . --json
|
|
214
|
+
plan-governance-cli workset . --json --strict-readiness
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
工作集返回活跃计划、阶段、阻塞、下一动作、并行提示和最近证据,不写计划或自动执行动作;严格模式让活跃计划的机械准入缺陷返回非零。需要缩小恢复上下文时使用最近记录窗口:
|
|
213
218
|
|
|
214
219
|
```bash
|
|
215
220
|
plan-governance-cli workset . --json --evidence-limit 3
|
|
216
221
|
```
|
|
217
222
|
|
|
218
|
-
每个计划保留末尾三条记录,并给出省略数量和原文位置;默认完整输出和门禁判断保持。窗口用于恢复阅读,必要时去掉参数回查完整记录,详见 [CLI 参考](resources/skill/references/cli.md#当前工作集)
|
|
223
|
+
每个计划保留末尾三条记录,并给出省略数量和原文位置;默认完整输出和门禁判断保持。窗口用于恢复阅读,必要时去掉参数回查完整记录,详见 [CLI 参考](resources/skill/references/cli.md#当前工作集)。
|
|
219
224
|
|
|
220
225
|
## 按需读取规则
|
|
221
226
|
|
|
@@ -230,10 +235,10 @@ plan-governance-cli guide cli # CLI 操作
|
|
|
230
235
|
|
|
231
236
|
SKILL 保留任务边界、自主执行原则和按需入口,三份 references 按需读取并随 manifest/setup/npm 分发。已有项目只刷新受管入口,不迁移旧计划、不改用户块外内容。选择同一版本的规范来源,安装副本与仓库同版本号也可能有不同内容。
|
|
232
237
|
|
|
233
|
-
|
|
238
|
+
也可以不全局安装,直接使用当前公开版本:
|
|
234
239
|
|
|
235
240
|
```bash
|
|
236
|
-
npx --yes --package plan-governance-cli@
|
|
241
|
+
npx --yes --package plan-governance-cli@latest plan-governance-cli check . --strict-readiness
|
|
237
242
|
```
|
|
238
243
|
|
|
239
244
|
npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接调用或复制该脚本。旧项目已有本地脚本时仍可保留作为兼容或回滚路径。
|
|
@@ -252,11 +257,10 @@ npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接
|
|
|
252
257
|
|
|
253
258
|
```bash
|
|
254
259
|
plan-governance-cli setup --target codex --dry-run
|
|
255
|
-
plan-governance-cli setup --target
|
|
256
|
-
plan-governance-cli setup --target all
|
|
260
|
+
plan-governance-cli setup --target codex --force
|
|
257
261
|
```
|
|
258
262
|
|
|
259
|
-
`setup`
|
|
263
|
+
`setup` 只向 Codex 同步 npm 包 manifest 指定的主治理 skill、独立迁移 skill、代理元数据和模板文件,必须恰好指定一次 `--target codex`。旧版或重复 target、混合 help、缺失 target 都会在写入前失败;目标之外的用户目录不会被读取、修改或删除。默认先用 `--dry-run` 查看差异,目标文件有本地修改时不会静默覆盖。使用 `--destination DIR` 指定主 skill 临时目标时,迁移 skill 会写到同一 skills 目录下的 `plan-governance-migration`。检查器、初始化器和 hook runtime 由 npm 包内部调用,不复制到项目或 skill 目录;项目初始化器只维护 `AGENTS.md`,当前实现不自动安装 hook 配置。
|
|
260
264
|
|
|
261
265
|
初始化项目时使用包内初始化器:
|
|
262
266
|
|
|
@@ -268,7 +272,7 @@ plan-governance-cli init --root . --plan api-compat-migration --title "API 兼
|
|
|
268
272
|
|
|
269
273
|
## Hook runtime
|
|
270
274
|
|
|
271
|
-
本仓库提供只读 hook runtime,供 Codex
|
|
275
|
+
本仓库提供只读 hook runtime,供 Codex 项目级 hooks 手动接入。脚本只输出短提示和检查结果,不修改治理文档,不更新 `最后更新`,不安装或修改全局配置。
|
|
272
276
|
|
|
273
277
|
```bash
|
|
274
278
|
plan-governance-cli hook --event session-start
|
|
@@ -295,7 +299,7 @@ plan-governance-cli init --root . --migrate-plan-map-last-updated --last-updated
|
|
|
295
299
|
当前检查项包括:
|
|
296
300
|
|
|
297
301
|
- `PLAN_MAP.md` 引用的计划文件是否存在。
|
|
298
|
-
- `docs/plans/*.md` 是否存在未登记到 `PLAN_MAP.md` 的孤立计划;孤立计划以 `WARNING` 提示,不阻断检查。
|
|
302
|
+
- 根目录平铺计划及 `docs/plans/YYYYMMDD/*.md` 是否存在未登记到 `PLAN_MAP.md` 的孤立计划;孤立计划以 `WARNING` 提示,不阻断检查。
|
|
299
303
|
- 计划状态是否合法。
|
|
300
304
|
- 计划依赖是否存在环。
|
|
301
305
|
- 活跃计划正文中的计划引用是否与 `PLAN_MAP.md` 依赖列一致;不一致以 `WARNING` 提示。
|
|
@@ -332,7 +336,7 @@ plan-governance-cli check . --check-attestations --strict-readiness
|
|
|
332
336
|
|
|
333
337
|
这是可选模式:快照绑定显式文件、目标与必要上游计划的实际内容,以及相关地图行。无关计划日期、地图注释或 HEAD 变化不触发复核;相关内容变化、删除或绑定无效会显示 `needs_review`,默认警告,显式严格检查失败。旧快照继续按原有全文 hash 规则检查,不自动回填;CI/发布不会自动开启证据检查。
|
|
334
338
|
|
|
335
|
-
文件须为仓库内普通文件,拒绝重复、目录、glob、越界或 symlink。它记录工作树字节,HEAD 只作定位;未列出的新文件不会自动被发现,hash 不代表独立验收。完成新的复核后,用带绑定的新快照和 `--supersedes <旧快照相对路径>` 替代;旧格式后继不能解除绑定快照的复核责任。具体投影、兼容和失败边界见[范围绑定契约](docs/plans/iterative-governance-reliability.md#阶段-4-行为契约)。
|
|
339
|
+
文件须为仓库内普通文件,拒绝重复、目录、glob、越界或 symlink。它记录工作树字节,HEAD 只作定位;未列出的新文件不会自动被发现,hash 不代表独立验收。完成新的复核后,用带绑定的新快照和 `--supersedes <旧快照相对路径>` 替代;旧格式后继不能解除绑定快照的复核责任。具体投影、兼容和失败边界见[范围绑定契约](docs/plans/20260906/iterative-governance-reliability.md#阶段-4-行为契约)。
|
|
336
340
|
|
|
337
341
|
## 测试覆盖率
|
|
338
342
|
|
|
@@ -366,7 +370,7 @@ python3 -m pytest
|
|
|
366
370
|
| `设计中` | 正在明确范围、契约和门禁 |
|
|
367
371
|
| `待实施` | 当前阶段门禁已通过,但尚未开始 |
|
|
368
372
|
| `实施中` | 当前阶段正在修改代码或文档 |
|
|
369
|
-
| `已完成` |
|
|
373
|
+
| `已完成` | 实现、测试、证据、文档及适用用户验收均已完成 |
|
|
370
374
|
| `已替代` | 被另一个计划取代 |
|
|
371
375
|
| `已合并` | 并入另一个计划 |
|
|
372
376
|
| `已废弃` | 明确不再推进 |
|
|
@@ -377,6 +381,8 @@ python3 -m pytest
|
|
|
377
381
|
|
|
378
382
|
新旧计划统一采用单次复核行为:普通改动自验,高风险或高影响独立检查一次后由实施者修复自验,原发现保留。旧 `风险分流` 标签和缺省六列格式只作为兼容记录形式,不恢复多次独立门禁;固定字段、历史保护和严格检查语义以[阶段门规范](resources/skill/references/verification.md#step-0-与阶段门)为准。
|
|
379
383
|
|
|
384
|
+
派发独立复核时显式传入推理强度,不依赖全局 subagent 默认值:安全、权限、隐私、资金、数据完整性等高风险复核使用 `high`,其他独立复核使用 `medium`。该分层只影响本次复核的思考强度,不改变是否复核或单次复核规则。
|
|
385
|
+
|
|
380
386
|
使用 `check --strict-readiness` 做准入/CI/发布机械检查;默认检查兼容告警,机械通过不代替真实风险判断、业务验收或用户接受。
|
|
381
387
|
|
|
382
388
|
## Codex 使用方式
|
|
@@ -390,7 +396,7 @@ $plan-governance 为这个项目初始化计划治理,计划名是 api-compat-
|
|
|
390
396
|
或继续推进已有计划:
|
|
391
397
|
|
|
392
398
|
```text
|
|
393
|
-
$plan-governance 继续推进 docs/plans/api-compat-migration.md 的当前阶段,完成后记录验证证据。
|
|
399
|
+
$plan-governance 继续推进 docs/plans/20260926/api-compat-migration.md 的当前阶段,完成后记录验证证据。
|
|
394
400
|
```
|
|
395
401
|
|
|
396
402
|
skill 只负责让 Codex 按流程工作;真实计划状态仍然保存在项目仓库的 `docs/` 中。
|
|
@@ -10,7 +10,7 @@ import {
|
|
|
10
10
|
writeFileSync,
|
|
11
11
|
} from "node:fs";
|
|
12
12
|
import { homedir, tmpdir } from "node:os";
|
|
13
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
13
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, sep } from "node:path";
|
|
14
14
|
import { spawnSync } from "node:child_process";
|
|
15
15
|
import { fileURLToPath } from "node:url";
|
|
16
16
|
|
|
@@ -79,11 +79,23 @@ function loadManifest() {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
function parseSetupArgs(args) {
|
|
82
|
+
if (args.length === 1 && ["-h", "--help"].includes(args[0])) {
|
|
83
|
+
console.log("用法:plan-governance-cli setup --target codex [--destination DIR] [--dry-run] [--force]\n同步主治理 skill 和独立计划迁移 skill;自定义 destination 时,其他 skill 写入同一 skills 目录。");
|
|
84
|
+
return null;
|
|
85
|
+
}
|
|
86
|
+
|
|
82
87
|
const options = { target: null, destination: null, dryRun: false, force: false };
|
|
83
88
|
for (let index = 0; index < args.length; index += 1) {
|
|
84
89
|
const arg = args[index];
|
|
85
90
|
if (arg === "--target") {
|
|
86
|
-
options.target
|
|
91
|
+
if (options.target !== null) {
|
|
92
|
+
throw new Error("setup 的 --target 只能指定一次");
|
|
93
|
+
}
|
|
94
|
+
const value = args[++index];
|
|
95
|
+
if (!value || value.startsWith("-")) {
|
|
96
|
+
throw new Error("setup 的 --target 缺少值");
|
|
97
|
+
}
|
|
98
|
+
options.target = value;
|
|
87
99
|
} else if (arg === "--destination") {
|
|
88
100
|
options.destination = args[++index];
|
|
89
101
|
} else if (arg === "--dry-run") {
|
|
@@ -91,18 +103,14 @@ function parseSetupArgs(args) {
|
|
|
91
103
|
} else if (arg === "--force") {
|
|
92
104
|
options.force = true;
|
|
93
105
|
} else if (arg === "-h" || arg === "--help") {
|
|
94
|
-
|
|
95
|
-
return null;
|
|
106
|
+
throw new Error("setup 的 --help 必须单独使用");
|
|
96
107
|
} else {
|
|
97
108
|
throw new Error(`setup 不支持参数:${arg}`);
|
|
98
109
|
}
|
|
99
110
|
}
|
|
100
111
|
|
|
101
|
-
if (
|
|
102
|
-
throw new Error("setup
|
|
103
|
-
}
|
|
104
|
-
if (options.destination && options.target === "all") {
|
|
105
|
-
throw new Error("--destination 只能与单个 setup target 一起使用");
|
|
112
|
+
if (options.target !== "codex") {
|
|
113
|
+
throw new Error("setup 仅支持 --target codex");
|
|
106
114
|
}
|
|
107
115
|
return options;
|
|
108
116
|
}
|
|
@@ -112,17 +120,15 @@ function expandTarget(target) {
|
|
|
112
120
|
return target.replace(/^~(?=\/|$)/, home);
|
|
113
121
|
}
|
|
114
122
|
|
|
115
|
-
function
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
}));
|
|
123
|
+
function targetRoot(manifest, options) {
|
|
124
|
+
return {
|
|
125
|
+
name: "codex",
|
|
126
|
+
root: resolve(options.destination ? options.destination : expandTarget(manifest.skill.targets.codex)),
|
|
127
|
+
};
|
|
121
128
|
}
|
|
122
129
|
|
|
123
|
-
function resourceFiles(
|
|
124
|
-
|
|
125
|
-
return manifest.skill.files.map((source) => {
|
|
130
|
+
function resourceFiles(skill, skillPrefix) {
|
|
131
|
+
return skill.files.map((source) => {
|
|
126
132
|
if (!source.startsWith(skillPrefix)) {
|
|
127
133
|
throw new Error(`skill 资源不在受支持的目录中:${source}`);
|
|
128
134
|
}
|
|
@@ -133,23 +139,60 @@ function resourceFiles(manifest) {
|
|
|
133
139
|
});
|
|
134
140
|
}
|
|
135
141
|
|
|
142
|
+
function pathsOverlap(left, right) {
|
|
143
|
+
const contains = (parent, child) => {
|
|
144
|
+
const childRelative = relative(parent, child);
|
|
145
|
+
return childRelative === ""
|
|
146
|
+
|| (childRelative !== ".." && !childRelative.startsWith(`..${sep}`) && !isAbsolute(childRelative));
|
|
147
|
+
};
|
|
148
|
+
return contains(left, right) || contains(right, left);
|
|
149
|
+
}
|
|
150
|
+
|
|
136
151
|
function setup(args) {
|
|
137
152
|
const options = parseSetupArgs(args);
|
|
138
153
|
if (options === null) return 0;
|
|
139
154
|
|
|
140
155
|
let manifest;
|
|
141
|
-
let
|
|
156
|
+
let skills;
|
|
142
157
|
try {
|
|
143
158
|
manifest = loadManifest();
|
|
144
|
-
|
|
159
|
+
skills = [
|
|
160
|
+
{ name: "plan-governance", definition: manifest.skill, prefix: "resources/skill/" },
|
|
161
|
+
...(manifest.additionalSkills ?? []).map((definition) => ({
|
|
162
|
+
name: definition.name,
|
|
163
|
+
definition,
|
|
164
|
+
prefix: definition.prefix,
|
|
165
|
+
})),
|
|
166
|
+
];
|
|
145
167
|
} catch (error) {
|
|
146
168
|
return fail(error.message);
|
|
147
169
|
}
|
|
148
170
|
|
|
149
171
|
const plans = [];
|
|
150
172
|
try {
|
|
151
|
-
|
|
152
|
-
|
|
173
|
+
const primaryTarget = targetRoot(manifest, options);
|
|
174
|
+
const targets = skills.map((skill) => {
|
|
175
|
+
const configuredTarget = skill.definition.targets.codex;
|
|
176
|
+
return skill.name === "plan-governance"
|
|
177
|
+
? primaryTarget
|
|
178
|
+
: {
|
|
179
|
+
name: skill.name,
|
|
180
|
+
root: resolve(options.destination
|
|
181
|
+
? join(dirname(primaryTarget.root), basename(expandTarget(configuredTarget)))
|
|
182
|
+
: expandTarget(configuredTarget)),
|
|
183
|
+
};
|
|
184
|
+
});
|
|
185
|
+
for (let index = 0; index < targets.length; index += 1) {
|
|
186
|
+
for (let other = index + 1; other < targets.length; other += 1) {
|
|
187
|
+
if (pathsOverlap(targets[index].root, targets[other].root)) {
|
|
188
|
+
throw new Error(`setup 的 skill 目标目录重叠:${targets[index].root} 与 ${targets[other].root}`);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
for (let index = 0; index < skills.length; index += 1) {
|
|
193
|
+
const skill = skills[index];
|
|
194
|
+
const target = targets[index];
|
|
195
|
+
for (const file of resourceFiles(skill.definition, skill.prefix)) {
|
|
153
196
|
accessSync(file.source, constants.R_OK);
|
|
154
197
|
const destination = resolve(target.root, file.relative);
|
|
155
198
|
const existing = existsSync(destination) ? readFileSync(destination, "utf8") : null;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "plan-governance-cli",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "统一入口的计划治理检查 CLI",
|
|
5
5
|
"private": false,
|
|
6
6
|
"type": "module",
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"bin/plan-governance-cli.mjs",
|
|
12
12
|
"resources/manifest.json",
|
|
13
13
|
"resources/skill",
|
|
14
|
+
"resources/migration-skill",
|
|
14
15
|
"scripts/check_plan_governance.py",
|
|
15
16
|
"scripts/init_plan_governance.py",
|
|
16
17
|
"scripts/plan_governance_hook.py",
|
package/resources/manifest.json
CHANGED
|
@@ -2,8 +2,7 @@
|
|
|
2
2
|
"manifestVersion": 1,
|
|
3
3
|
"skill": {
|
|
4
4
|
"targets": {
|
|
5
|
-
"codex": "~/.agents/skills/plan-governance"
|
|
6
|
-
"claude": "~/.claude/skills/plan-governance"
|
|
5
|
+
"codex": "~/.agents/skills/plan-governance"
|
|
7
6
|
},
|
|
8
7
|
"files": [
|
|
9
8
|
"resources/skill/SKILL.md",
|
|
@@ -18,6 +17,19 @@
|
|
|
18
17
|
"resources/skill/references/cli.md"
|
|
19
18
|
]
|
|
20
19
|
},
|
|
20
|
+
"additionalSkills": [
|
|
21
|
+
{
|
|
22
|
+
"name": "plan-governance-migration",
|
|
23
|
+
"targets": {
|
|
24
|
+
"codex": "~/.agents/skills/plan-governance-migration"
|
|
25
|
+
},
|
|
26
|
+
"prefix": "resources/migration-skill/",
|
|
27
|
+
"files": [
|
|
28
|
+
"resources/migration-skill/SKILL.md",
|
|
29
|
+
"resources/migration-skill/agents/openai.yaml"
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
],
|
|
21
33
|
"runtime": [
|
|
22
34
|
"scripts/check_plan_governance.py",
|
|
23
35
|
"scripts/init_plan_governance.py",
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plan-governance-migration
|
|
3
|
+
description: Migrate existing flat docs/plans/*.md files into date directories when the user explicitly requests migration, preserving plan history and repairing references.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# 计划日期目录迁移
|
|
7
|
+
|
|
8
|
+
仅在用户明确要求迁移存量计划时使用。计划目录存在或主治理 skill 已升级,都不构成迁移授权。日常治理只读取索引路径,不主动搬动计划。
|
|
9
|
+
|
|
10
|
+
迁移前读取项目 `AGENTS.md`、`docs/PLAN_MAP.md`、受影响计划和适用的 plan-governance verification 规则。先检查工作区差异、地图登记、所有平铺计划、目标路径占用、Markdown 引用和 attestation;建立源路径、目标路径、创建日期及引用影响清单,并先报告无法确定的日期或歧义。日期优先取 Git 首次添加记录;仅当正文明确记录创建日期时才可回退使用该日期。不得用修改时间或最后更新日期猜测。路径冲突、未登记文件、无法解析的日期或会丢失的本地修改未解决前,不移动文件。
|
|
11
|
+
|
|
12
|
+
迁移时保留计划内容、文件名、身份、状态、阶段和历史证据。按核对后的清单移动到 `docs/plans/YYYYMMDD/<slug>.md`,同步 `PLAN_MAP.md`、指向这些计划的文档链接,以及因源文件层级变化而失效的相对 Markdown 链接。保留锚点;计划链接按原目标重新计算相对路径。代码块中的示例路径不视为引用。不要改写旧 attestation;检查其计划路径、地图 hash 和替代关系。若不能用项目 CLI 建立可信后继快照,保留旧快照并报告 `needs_review`,不要手改 JSON 来制造通过状态。
|
|
13
|
+
|
|
14
|
+
每批迁移都先保存可逆的源/目标映射。完成后确认源路径已清空、目标文件各出现一次、索引路径准确、所有受影响本地链接可达,且 checker、workset 和 attestation 检查符合项目规则。任一检查失败时停止,并依据映射恢复文件和引用;报告实际恢复范围及仍未解决的项目。只处理用户授权的仓库与计划范围,不安装依赖、不发布、不触碰其他项目。
|
package/resources/skill/SKILL.md
CHANGED
|
@@ -13,7 +13,7 @@ description: 维护已有项目计划,或为需要跨阶段、跨会话协调
|
|
|
13
13
|
|
|
14
14
|
首次接手读取地图、当前相关计划及必要契约;恢复只核对当前阶段、实际差异和最近证据。只实施已授权且准入的范围;实质边界变化时调整受影响计划,日常进展随工作同步。事实源与格式见 planning。
|
|
15
15
|
|
|
16
|
-
按 verification
|
|
16
|
+
按 verification 验证:普通改动自验,高风险或高影响的同一交付范围独立检查一次,修复由实施者自验;派发独立复核时显式传入推理强度,高风险用 `high`,其余用 `medium`。旧策略名和旧记录格式不改变此行为。适用检查通过后完成交付;未解决问题如实保留。交付说明完成结果、必要证据和剩余事项,明确约定的用户体验验收继续有效。
|
|
17
17
|
|
|
18
18
|
## 按需读取
|
|
19
19
|
|
|
@@ -23,4 +23,4 @@ description: 维护已有项目计划,或为需要跨阶段、跨会话协调
|
|
|
23
23
|
| 阶段准入、风险判断、验证或完成 | [verification](references/verification.md) |
|
|
24
24
|
| 查询工作集、运行检查、初始化或分发 | [cli](references/cli.md) |
|
|
25
25
|
|
|
26
|
-
开发或审查本 skill 时使用仓库源及对应 CLI;其他项目使用实际发现的安装版,参考按所选 skill 目录解析。仅 CLI 可用时用 `guide`
|
|
26
|
+
开发或审查本 skill 时使用仓库源及对应 CLI;其他项目使用实际发现的安装版,参考按所选 skill 目录解析。仅 CLI 可用时用 `guide` 和对应主题读取。来源一次选定后复用,只有内容变化或能力不符时重新核对;不要混读副本。缺少所需能力时说明具体缺口,继续不依赖它的工作,安装与升级按已有授权处理。已有计划批量迁移由独立 `plan-governance-migration` skill 在用户明确要求时执行,日常治理不会自动搬迁计划。
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
## 分发与资源
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
主治理 skill 和独立 migration skill 由同一 npm 包的 manifest 分别分发。`plan-governance-cli setup --target codex --dry-run` 查看两个 skill 的同步差异;`--destination DIR` 将主 skill 放入指定目录,并将迁移 skill 放在其同级目录。实际覆盖需既有用户授权并按 setup 冲突提示处理,不自动清理目标之外的用户目录。setup 只支持 Codex 单目标同步;项目初始化器只维护 `AGENTS.md`。项目升级不迁移旧计划,受管块外内容必须保持。
|
|
22
22
|
|
|
23
23
|
仅更新计划格式不等于更新项目入口。已有项目在任务交接、没有进行中的实施或复核时更新受管入口;升级不批量回写历史,旧策略名和六列格式按 verification 解释为统一单次行为。安装副本与仓库开发规则有差异时说明本次选用来源,不能因版本号相同就混读正文。
|
|
24
24
|
|
|
@@ -28,7 +28,7 @@ SKILL、三个 references、agents 元数据与 assets 由同一 npm 包的 mani
|
|
|
28
28
|
- `../assets/plan.template.md`:计划文档模板。
|
|
29
29
|
- `../assets/adr.template.md`:可选 ADR 模板。
|
|
30
30
|
- `../assets/migration.template.md`:可选迁移模板。
|
|
31
|
-
- `scripts/init_plan_governance.py`:初始化 Git 仓库、`docs/PLAN_MAP.md` 和首个 `docs/plans/<plan>.md
|
|
31
|
+
- `scripts/init_plan_governance.py`:初始化 Git 仓库、`docs/PLAN_MAP.md` 和首个 `docs/plans/YYYYMMDD/<plan>.md`;检查器仍读取旧式根目录计划。也可升级已有项目的代理规则和检查脚本。
|
|
32
32
|
- `scripts/check_plan_governance.py`:检查状态、最后更新、计划链接、依赖、阻塞项、完成证据、测试覆盖率证据,并提供可选漂移、pre-commit、停滞和 attestation 生命周期检查。
|
|
33
33
|
- `scripts/plan_governance_hook.py`:只读 hook runtime,可由项目级 Agent hooks 手动调用;只输出短提示和检查结果,不修改治理文档、不更新状态、不安装 hooks。
|
|
34
34
|
|
|
@@ -54,7 +54,7 @@ plan-governance-cli init \
|
|
|
54
54
|
--update-agent-rules-only
|
|
55
55
|
```
|
|
56
56
|
|
|
57
|
-
升级已有项目的辅助文件,刷新 `scripts/check_plan_governance.py
|
|
57
|
+
升级已有项目的辅助文件,刷新 `scripts/check_plan_governance.py` 和 `AGENTS.md`,但不覆盖 `docs/` 或其他代理入口:
|
|
58
58
|
|
|
59
59
|
```bash
|
|
60
60
|
plan-governance-cli init \
|
|
@@ -15,11 +15,13 @@
|
|
|
15
15
|
| 文档 | 职责 |
|
|
16
16
|
|---|---|
|
|
17
17
|
| `docs/PLAN_MAP.md` | 计划状态、阶段、关系、阻塞及证据入口 |
|
|
18
|
-
| `docs/plans/*.md` |
|
|
18
|
+
| `docs/plans/YYYYMMDD/*.md` | 新计划的目标、范围、决策、差异与验证证据;旧式 `docs/plans/*.md` 计划继续兼容读取 |
|
|
19
19
|
| Schema/OpenAPI 或 `docs/specs/` | 现行公共契约,优先复用已有来源 |
|
|
20
20
|
| `docs/adr/`、`docs/migrations/` | 确有需要时记录长期架构决策或迁移步骤 |
|
|
21
21
|
| `docs/reviews/`、`docs/fixtures/` | 必要复核及可复验样本,代码 fixture 放 `tests/fixtures/` |
|
|
22
22
|
|
|
23
|
+
新建计划使用 `docs/plans/YYYYMMDD/<plan>.md`,日期是创建时的本地日期,之后更新不改变目录日期。`PLAN_MAP.md` 链接真实路径。根目录中的旧式 `docs/plans/<plan>.md` 仍受支持;工具升级不会自动迁移它们。已有计划要批量迁移时,只有用户明确要求才调用独立 `plan-governance-migration` skill。
|
|
24
|
+
|
|
23
25
|
## 多文档同步规则
|
|
24
26
|
|
|
25
27
|
同一事实保留一个权威来源,其他文档引用。决策或结果改变后,同步地图及实际受影响的索引/引用;同一组已收敛变化可合并同步,不为每次保存增加流程。用 `rg` 定向检查相关计划名、关键字段和链接,不重扫无关历史。
|
|
@@ -22,7 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
同一交付范围只检查一次,通常在实现和自验收敛后、交付或高影响外部动作前;需要设计准入时将这一次前置,后续实施与完成自验。已审范围内的增量和修复合并处理,确有新增高风险范围才另行判断,不把每个修复拆成新范围;后续独立通过不能清除这一次检查留下的发现。
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
授权明确时自动安排一名未参与实现或测试编写的只读复核者,提供范围、差异、风险和证据入口,并要求其自行完成、不得再委派复核。创建复核 subagent 时必须显式传入推理强度,不依赖全局默认:涉及安全、权限、隐私、资金、数据完整性或同类高风险后果时用 `high`,其他独立复核用 `medium`;推理强度不改变是否复核、复核次数或完成判定。派发前复用已有同范围任务或结果;等待失败先核对准确任务 ID 与实际状态,只有确认原任务不存在或不可恢复才补发。冻结受审范围并告知变化。独立者给出发现后,实施者修复并验证原问题及受影响回归,不再送审,未解决则保持未完成。检查不可用/超时直接记录状态,不能伪填已完成结论;业务缺陷描述中的“超时”不代表检查未完成。复核不代替外部授权。
|
|
26
26
|
|
|
27
27
|
## Step 0 与阶段门
|
|
28
28
|
|
|
@@ -793,6 +793,27 @@ def extract_plan_link(cell):
|
|
|
793
793
|
return None
|
|
794
794
|
|
|
795
795
|
|
|
796
|
+
def plan_link_issue(link):
|
|
797
|
+
"""Validate the canonical YYYYMMDD bucket while preserving legacy/custom paths."""
|
|
798
|
+
parts = Path(link).parts
|
|
799
|
+
if len(parts) < 2 or parts[0] != "plans":
|
|
800
|
+
return f"计划路径必须位于 docs/plans 下:{link}"
|
|
801
|
+
bucket = parts[1]
|
|
802
|
+
if len(parts) == 2 and bucket.endswith(".md"):
|
|
803
|
+
return None
|
|
804
|
+
if not bucket[:1].isdigit():
|
|
805
|
+
return None
|
|
806
|
+
if not re.fullmatch(r"\d{8}", bucket):
|
|
807
|
+
return f"计划日期目录必须为 YYYYMMDD:{link}"
|
|
808
|
+
try:
|
|
809
|
+
datetime.strptime(bucket, "%Y%m%d")
|
|
810
|
+
except ValueError:
|
|
811
|
+
return f"计划日期目录不是有效日期:{link}"
|
|
812
|
+
if len(parts) != 3 or not parts[2].endswith(".md"):
|
|
813
|
+
return f"计划日期目录必须直接包含 Markdown 文件:{link}"
|
|
814
|
+
return None
|
|
815
|
+
|
|
816
|
+
|
|
796
817
|
def extract_declared_dependencies(depends_cell):
|
|
797
818
|
return [d.strip("` ") for d in re.split(r",|<br>|、", depends_cell) if d.strip("` -")]
|
|
798
819
|
|
|
@@ -846,13 +867,19 @@ def extract_plan_references(plan_text, known_plans, current_name):
|
|
|
846
867
|
if name in known_plans and name != current_name:
|
|
847
868
|
references.add(name)
|
|
848
869
|
|
|
849
|
-
for match in re.finditer(
|
|
870
|
+
for match in re.finditer(
|
|
871
|
+
r"(?:docs/)?plans/(?:\d{8}/)?([A-Za-z0-9\u4e00-\u9fff._-]+)\.md",
|
|
872
|
+
plan_text,
|
|
873
|
+
):
|
|
850
874
|
name = Path(match.group(1).strip()).stem
|
|
851
875
|
if name in known_plans and name != current_name:
|
|
852
876
|
references.add(name)
|
|
853
877
|
|
|
854
|
-
for match in re.finditer(r"\]\(([
|
|
855
|
-
|
|
878
|
+
for match in re.finditer(r"\]\(([^)#]+\.md)(?:#[^)]+)?\)", plan_text):
|
|
879
|
+
target = match.group(1).strip()
|
|
880
|
+
if re.match(r"^[a-zA-Z][a-zA-Z0-9+.-]*://", target):
|
|
881
|
+
continue
|
|
882
|
+
name = Path(target).stem
|
|
856
883
|
if name in known_plans and name != current_name:
|
|
857
884
|
references.add(name)
|
|
858
885
|
|
|
@@ -918,9 +945,16 @@ def find_orphan_plans(docs, plans):
|
|
|
918
945
|
if not plans_dir.exists():
|
|
919
946
|
return []
|
|
920
947
|
indexed_paths = {data["path"].resolve() for data in plans.values()}
|
|
948
|
+
candidates = set(plans_dir.glob("*.md"))
|
|
949
|
+
for bucket in plans_dir.iterdir():
|
|
950
|
+
if not bucket.is_dir() or not bucket.name[:1].isdigit():
|
|
951
|
+
continue
|
|
952
|
+
# Numeric buckets are reserved for dated plans. Include malformed buckets
|
|
953
|
+
# and deeper files so they cannot disappear from orphan diagnostics.
|
|
954
|
+
candidates.update(bucket.rglob("*.md"))
|
|
921
955
|
return sorted(
|
|
922
956
|
plan_file
|
|
923
|
-
for plan_file in
|
|
957
|
+
for plan_file in candidates
|
|
924
958
|
if plan_file.resolve() not in indexed_paths
|
|
925
959
|
)
|
|
926
960
|
|
|
@@ -1630,32 +1664,33 @@ def warn_attestation_drift(warnings, root, plans, errors=None, strict=False, rep
|
|
|
1630
1664
|
plan_map_path = root / plan_map_path_rel
|
|
1631
1665
|
if scoped_checks:
|
|
1632
1666
|
try:
|
|
1633
|
-
binding_file(root, plan_path_rel)
|
|
1667
|
+
binding_file(root, plan_path_rel, require_file=plan_path.exists())
|
|
1634
1668
|
binding_file(root, plan_map_path_rel)
|
|
1635
1669
|
except (ValueError, OSError) as exc:
|
|
1636
1670
|
attestation_issue(warnings, errors, strict, f"{path}: 混合快照路径读取预检失败:{exc}")
|
|
1637
1671
|
continue
|
|
1638
|
-
if not plan_path.exists():
|
|
1639
|
-
warn(warnings, f"{path}: 快照引用的计划文件不存在:{plan_path}")
|
|
1640
|
-
continue
|
|
1641
1672
|
if not plan_map_path.exists():
|
|
1642
1673
|
warn(warnings, f"{path}: 快照引用的 PLAN_MAP.md 不存在:{plan_map_path}")
|
|
1643
1674
|
continue
|
|
1644
1675
|
|
|
1645
|
-
|
|
1676
|
+
# A moved plan leaves the historical snapshot's recorded path behind.
|
|
1677
|
+
# Keep that legacy record in the supersedes graph; an explicit valid
|
|
1678
|
+
# successor can retire it, while an unretired record remains needs_review.
|
|
1679
|
+
missing_plan = not plan_path.exists()
|
|
1680
|
+
drift_reasons = []
|
|
1681
|
+
if missing_plan:
|
|
1682
|
+
drift_reasons.append(f"快照引用的计划文件不存在:{plan_path}")
|
|
1646
1683
|
try:
|
|
1647
|
-
if sha256_file(plan_path) != attestation.get("plan_sha256"):
|
|
1648
|
-
|
|
1649
|
-
drifted = True
|
|
1684
|
+
if not missing_plan and sha256_file(plan_path) != attestation.get("plan_sha256"):
|
|
1685
|
+
drift_reasons.append(f"{plan_name} 计划文件 hash 已变化,需要人工复核")
|
|
1650
1686
|
if sha256_file(plan_map_path) != attestation.get("plan_map_sha256"):
|
|
1651
|
-
|
|
1652
|
-
drifted = True
|
|
1687
|
+
drift_reasons.append("PLAN_MAP.md hash 已变化,需要人工复核")
|
|
1653
1688
|
except OSError as exc:
|
|
1654
1689
|
if not scoped_checks:
|
|
1655
1690
|
raise
|
|
1656
1691
|
# 内容不可读不使已存在的替代目标消失,也不阻止后续绑定报告。
|
|
1657
1692
|
attestation_issue(warnings, errors, strict, f"{path}: 混合快照内容读取失败:{exc}")
|
|
1658
|
-
|
|
1693
|
+
drift_reasons.append(f"快照内容读取失败:{exc}")
|
|
1659
1694
|
records.append(
|
|
1660
1695
|
{
|
|
1661
1696
|
"path": path,
|
|
@@ -1664,10 +1699,12 @@ def warn_attestation_drift(warnings, root, plans, errors=None, strict=False, rep
|
|
|
1664
1699
|
"purpose": purpose,
|
|
1665
1700
|
"review_status": review_status,
|
|
1666
1701
|
"supersedes": normalized_supersedes,
|
|
1667
|
-
"drifted":
|
|
1702
|
+
"drifted": bool(drift_reasons),
|
|
1668
1703
|
"legacy": legacy,
|
|
1669
1704
|
"invalid_structure": invalid_structure,
|
|
1670
1705
|
"bound": False,
|
|
1706
|
+
"plan_path": plan_path,
|
|
1707
|
+
"drift_reasons": drift_reasons,
|
|
1671
1708
|
}
|
|
1672
1709
|
)
|
|
1673
1710
|
|
|
@@ -1739,6 +1776,9 @@ def warn_attestation_drift(warnings, root, plans, errors=None, strict=False, rep
|
|
|
1739
1776
|
key = (record["plan"], record["purpose"])
|
|
1740
1777
|
current_by_key.setdefault(key, []).append(record["relative_path"])
|
|
1741
1778
|
record["effective_status"] = effective
|
|
1779
|
+
if effective != "superseded":
|
|
1780
|
+
for reason in record.get("drift_reasons", []):
|
|
1781
|
+
warn(warnings, f"{record['path']}: {reason}")
|
|
1742
1782
|
|
|
1743
1783
|
for key, paths in current_by_key.items():
|
|
1744
1784
|
if len(paths) > 1:
|
|
@@ -1835,6 +1875,9 @@ def plan_index_issues(plan_map_text):
|
|
|
1835
1875
|
if not link:
|
|
1836
1876
|
issues.append(f"计划索引第 {row_index} 行缺少计划链接,无法安全派生工作集")
|
|
1837
1877
|
continue
|
|
1878
|
+
path_issue = plan_link_issue(link)
|
|
1879
|
+
if path_issue:
|
|
1880
|
+
issues.append(f"计划索引第 {row_index} 行路径无效:{path_issue}")
|
|
1838
1881
|
name = Path(link).stem
|
|
1839
1882
|
if name in seen:
|
|
1840
1883
|
issues.append(f"计划索引存在重复计划 ID:{name}")
|
|
@@ -2128,7 +2171,9 @@ def validate_relation_evidence(evidence, plans, row_label):
|
|
|
2128
2171
|
"""仅校验 Markdown 链接形式的相对证据路径;纯文本运行记录保持兼容。"""
|
|
2129
2172
|
if not evidence or not plans:
|
|
2130
2173
|
return []
|
|
2131
|
-
|
|
2174
|
+
first_path = next(iter(plans.values()))["path"]
|
|
2175
|
+
plans_dir = next((parent for parent in first_path.parents if parent.name == "plans"), None)
|
|
2176
|
+
docs_root = plans_dir.parent if plans_dir else first_path.parent.parent
|
|
2132
2177
|
errors = []
|
|
2133
2178
|
for match in re.finditer(r"\]\(([^)#]+)(?:#[^)]+)?\)", evidence):
|
|
2134
2179
|
target = match.group(1).strip()
|
|
@@ -4,7 +4,7 @@ import re
|
|
|
4
4
|
import shutil
|
|
5
5
|
import subprocess
|
|
6
6
|
import sys
|
|
7
|
-
from datetime import date
|
|
7
|
+
from datetime import date, datetime
|
|
8
8
|
from pathlib import Path
|
|
9
9
|
|
|
10
10
|
|
|
@@ -19,8 +19,6 @@ VALID_STATUSES = {
|
|
|
19
19
|
"已废弃",
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
CLAUDE_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
23
|
-
CLAUDE_SECTION_END = "<!-- plan-governance:end -->"
|
|
24
22
|
AGENTS_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
25
23
|
AGENTS_SECTION_END = "<!-- plan-governance:end -->"
|
|
26
24
|
|
|
@@ -62,7 +60,7 @@ def agent_rules_body():
|
|
|
62
60
|
|
|
63
61
|
- 普通一次性任务直接处理;已有计划按共享 verification 验证。新旧计划均为普通改动自验,高风险或高影响同范围独立复核一次,修复后自验。
|
|
64
62
|
- 只实施已授权、已准入的当前阶段。每个阶段有自身 Step 0、验证/完成和失败边界;未解决问题不能记为完成,不可用或超时不能冒充已检查。已有授权不重复索取,新增高影响外部动作按实际授权处理。
|
|
65
|
-
- 首次读取 `docs/PLAN_MAP.md
|
|
63
|
+
- 首次读取 `docs/PLAN_MAP.md`、索引登记的相关 `docs/plans/` 计划及适用契约/ADR/migration;恢复先核对当前工作集、最近证据和实际 diff,缺失或冲突再展开。
|
|
66
64
|
- 地图维护状态、阶段、关系、阻塞与证据入口;计划记录本次差异及验证,现行契约优先复用 Schema/OpenAPI。新事实不写回历史草案,变化同步地图及相关引用。
|
|
67
65
|
- 完成时按共享规范记录实际验证与适用用户验收,运行 `plan-governance-cli check .`;准入/CI/发布显式使用 `--strict-readiness`,机械通过不等于业务验收。
|
|
68
66
|
"""
|
|
@@ -72,10 +70,6 @@ def managed_section(begin, end):
|
|
|
72
70
|
return f"{begin}\n{agent_rules_body()}{end}\n"
|
|
73
71
|
|
|
74
72
|
|
|
75
|
-
def claude_md_section():
|
|
76
|
-
return managed_section(CLAUDE_SECTION_BEGIN, CLAUDE_SECTION_END)
|
|
77
|
-
|
|
78
|
-
|
|
79
73
|
def agents_md_section():
|
|
80
74
|
return managed_section(AGENTS_SECTION_BEGIN, AGENTS_SECTION_END)
|
|
81
75
|
|
|
@@ -105,16 +99,6 @@ def update_managed_file(root, filename, section, begin, end):
|
|
|
105
99
|
return target
|
|
106
100
|
|
|
107
101
|
|
|
108
|
-
def update_claude_md(root):
|
|
109
|
-
return update_managed_file(
|
|
110
|
-
root,
|
|
111
|
-
"CLAUDE.md",
|
|
112
|
-
claude_md_section(),
|
|
113
|
-
CLAUDE_SECTION_BEGIN,
|
|
114
|
-
CLAUDE_SECTION_END,
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
|
|
118
102
|
def update_agents_md(root):
|
|
119
103
|
return update_managed_file(
|
|
120
104
|
root,
|
|
@@ -126,12 +110,14 @@ def update_agents_md(root):
|
|
|
126
110
|
|
|
127
111
|
|
|
128
112
|
def update_agent_rules(root):
|
|
129
|
-
return [
|
|
113
|
+
return [update_agents_md(root)]
|
|
130
114
|
|
|
131
115
|
|
|
132
|
-
def plan_map_content(plan_slug, title, status, phase):
|
|
133
|
-
|
|
134
|
-
|
|
116
|
+
def plan_map_content(plan_slug, title, status, phase, created_on=None):
|
|
117
|
+
created_on = created_on or date.today()
|
|
118
|
+
today = created_on.isoformat()
|
|
119
|
+
plan_relative = f"plans/{created_on:%Y%m%d}/{plan_slug}.md"
|
|
120
|
+
row = f"| [{title}]({plan_relative}) | {status} | {phase} | {today} | - | - |"
|
|
135
121
|
unfinished_row = row if status in {"候选", "设计中", "待实施", "实施中"} else ""
|
|
136
122
|
completed_row = row if status == "已完成" else ""
|
|
137
123
|
deprecated_row = row if status in {"已废弃", "已替代", "已合并"} else ""
|
|
@@ -144,13 +130,13 @@ def plan_map_content(plan_slug, title, status, phase):
|
|
|
144
130
|
## 文档权责
|
|
145
131
|
|
|
146
132
|
- `docs/PLAN_MAP.md` 是状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
|
|
147
|
-
- `docs/plans/*.md`
|
|
133
|
+
- `docs/plans/YYYYMMDD/*.md` 默认记录新计划的行为差异、阶段、Step 0 和验收;旧式 `docs/plans/*.md` 计划继续兼容读取。现行契约优先引用已有 Schema/OpenAPI,无合适来源且需长期维护时才按需建立 `docs/specs/*.md`。同一事实不重复维护。
|
|
148
134
|
- 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
|
|
149
135
|
- 当专项计划变化时,必须同步所有引用该计划的路线图、优先级计划或索引。
|
|
150
136
|
- 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
|
|
151
137
|
- `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;阶段 N 完成后,阶段 N+1 默认保持 `设计中`。
|
|
152
138
|
- 阶段准入摘要、样本矩阵和独立复核记录只写入专项计划,不复制到本索引。
|
|
153
|
-
- 启用治理后,已有草案、历史设计、归档计划和临时分析文档默认只作为背景材料,不再作为规范事实源;后续新规范默认进入 `docs/plans/*.md`、ADR、migration、正式 spec 或 `docs/PLAN_MAP.md`。
|
|
139
|
+
- 启用治理后,已有草案、历史设计、归档计划和临时分析文档默认只作为背景材料,不再作为规范事实源;后续新规范默认进入 `docs/plans/YYYYMMDD/*.md`、ADR、migration、正式 spec 或 `docs/PLAN_MAP.md`。
|
|
154
140
|
- 计划索引固定分为 `未完成`、`已完成`、`已废弃` 三张表;`已替代`、`已合并`等不再推进的终态归入 `已废弃` 表,但保留真实状态值。
|
|
155
141
|
|
|
156
142
|
## 计划索引
|
|
@@ -239,8 +225,20 @@ def docs_warnings(root):
|
|
|
239
225
|
if not (root / "docs" / "PLAN_MAP.md").exists():
|
|
240
226
|
warnings.append("缺少 docs/PLAN_MAP.md")
|
|
241
227
|
plans_dir = root / "docs" / "plans"
|
|
242
|
-
|
|
243
|
-
|
|
228
|
+
dated_plans = False
|
|
229
|
+
if plans_dir.exists():
|
|
230
|
+
for child in plans_dir.iterdir():
|
|
231
|
+
if not (child.is_dir() and re.fullmatch(r"\d{8}", child.name)):
|
|
232
|
+
continue
|
|
233
|
+
try:
|
|
234
|
+
datetime.strptime(child.name, "%Y%m%d")
|
|
235
|
+
except ValueError:
|
|
236
|
+
continue
|
|
237
|
+
if any(child.glob("*.md")):
|
|
238
|
+
dated_plans = True
|
|
239
|
+
break
|
|
240
|
+
if not plans_dir.exists() or (not any(plans_dir.glob("*.md")) and not dated_plans):
|
|
241
|
+
warnings.append("缺少 docs/plans 下的平铺或 YYYYMMDD 日期目录计划")
|
|
244
242
|
return warnings
|
|
245
243
|
|
|
246
244
|
|
|
@@ -303,19 +301,16 @@ def parse_args(argv):
|
|
|
303
301
|
parser.add_argument("--status", default="设计中", choices=sorted(VALID_STATUSES), help="初始状态。")
|
|
304
302
|
parser.add_argument("--phase", default="阶段 0", help="当前阶段名称。")
|
|
305
303
|
parser.add_argument("--copy-checker", action="store_true", help="复制检查脚本到目标仓库 scripts/。")
|
|
306
|
-
parser.add_argument("--update-claude-md", action="store_true", help="创建或更新目标仓库 CLAUDE.md 中的计划治理规则。")
|
|
307
304
|
parser.add_argument("--update-agents-md", action="store_true", help="创建或更新目标仓库 AGENTS.md 中的计划治理规则。")
|
|
308
|
-
parser.add_argument("--update-agent-rules", action="store_true", help="
|
|
309
|
-
parser.add_argument("--update-claude-md-only", action="store_true", help="只创建或更新 CLAUDE.md,不初始化或覆盖 docs/。")
|
|
305
|
+
parser.add_argument("--update-agent-rules", action="store_true", help="创建或更新目标仓库 AGENTS.md 中的计划治理规则。")
|
|
310
306
|
parser.add_argument("--update-agents-md-only", action="store_true", help="只创建或更新 AGENTS.md,不初始化或覆盖 docs/。")
|
|
311
|
-
parser.add_argument("--update-agent-rules-only", action="store_true", help="只创建或更新
|
|
307
|
+
parser.add_argument("--update-agent-rules-only", action="store_true", help="只创建或更新 AGENTS.md,不初始化或覆盖 docs/。")
|
|
312
308
|
parser.add_argument("--upgrade-existing", action="store_true", help="升级已有项目的辅助文件:刷新检查脚本和代理规则,不覆盖 docs/。")
|
|
313
309
|
parser.add_argument("--migrate-plan-map-last-updated", action="store_true", help="将旧五列表 PLAN_MAP.md 迁移为包含最后更新的六列表。")
|
|
314
310
|
parser.add_argument("--last-updated-date", default=date.today().isoformat(), help="迁移 PLAN_MAP.md 时填入的最后更新日期,默认今天。")
|
|
315
311
|
parser.add_argument("--force", action="store_true", help="允许覆盖已存在的治理文件。")
|
|
316
312
|
args = parser.parse_args(argv)
|
|
317
313
|
only_modes = [
|
|
318
|
-
args.update_claude_md_only,
|
|
319
314
|
args.update_agents_md_only,
|
|
320
315
|
args.update_agent_rules_only,
|
|
321
316
|
args.upgrade_existing,
|
|
@@ -332,12 +327,6 @@ def main(argv=None):
|
|
|
332
327
|
args = parse_args(argv or sys.argv[1:])
|
|
333
328
|
root = Path(args.root).expanduser().resolve()
|
|
334
329
|
|
|
335
|
-
if args.update_claude_md_only:
|
|
336
|
-
target = update_claude_md(root)
|
|
337
|
-
print(f"已写入:{target}")
|
|
338
|
-
print("CLAUDE.md 已更新;未修改 docs/。")
|
|
339
|
-
return 0
|
|
340
|
-
|
|
341
330
|
if args.update_agents_md_only:
|
|
342
331
|
target = update_agents_md(root)
|
|
343
332
|
print(f"已写入:{target}")
|
|
@@ -374,14 +363,15 @@ def main(argv=None):
|
|
|
374
363
|
|
|
375
364
|
docs = root / "docs"
|
|
376
365
|
plan_map = docs / "PLAN_MAP.md"
|
|
377
|
-
|
|
366
|
+
created_on = date.today()
|
|
367
|
+
plan_file = docs / "plans" / created_on.strftime("%Y%m%d") / f"{plan_slug}.md"
|
|
378
368
|
|
|
379
369
|
created = []
|
|
380
370
|
git_dir = init_git(root)
|
|
381
371
|
if git_dir is not None:
|
|
382
372
|
created.append(git_dir)
|
|
383
373
|
|
|
384
|
-
write_file(plan_map, plan_map_content(plan_slug, title, args.status, args.phase), args.force)
|
|
374
|
+
write_file(plan_map, plan_map_content(plan_slug, title, args.status, args.phase, created_on), args.force)
|
|
385
375
|
created.append(plan_map)
|
|
386
376
|
write_file(plan_file, plan_content(plan_slug, title, args.status, args.phase, args.goal), args.force)
|
|
387
377
|
created.append(plan_file)
|
|
@@ -390,11 +380,8 @@ def main(argv=None):
|
|
|
390
380
|
created.append(copy_checker(root, args.force))
|
|
391
381
|
if args.update_agent_rules:
|
|
392
382
|
created.extend(update_agent_rules(root))
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
created.append(update_claude_md(root))
|
|
396
|
-
if args.update_agents_md:
|
|
397
|
-
created.append(update_agents_md(root))
|
|
383
|
+
elif args.update_agents_md:
|
|
384
|
+
created.append(update_agents_md(root))
|
|
398
385
|
|
|
399
386
|
for path in created:
|
|
400
387
|
print(f"已写入:{path}")
|