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 CHANGED
@@ -55,12 +55,12 @@ docs/
55
55
  | 文档 | 权威内容 |
56
56
  |---|---|
57
57
  | `docs/PLAN_MAP.md` | 计划索引、状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项、证据链接 |
58
- | `docs/plans/*.md` | 本次目标、范围、行为差异、阶段、Step 0、验证与完成条件;链接现行契约 |
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/*.md` 保持平铺。目录按真实内容增加:`docs/reviews/` 放独立复核,`docs/fixtures/` 放人工样本/回放说明,`tests/fixtures/` 放可执行输入,`docs/attestations/` 放按需快照。init 只创建最小结构;可选 [spec 模板](resources/skill/assets/spec.template.md) 随 skill 分发,不自动创建目标项目 spec 或空目录。
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` 会创建或更新 `CLAUDE.md` 和 `AGENTS.md` 中带标记的计划治理章节,只写稳定执行规则,不写具体计划内容。具体计划仍以 `docs/PLAN_MAP.md` 和 `docs/plans/*.md` 为准。
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` 和 `docs/plans/*.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
- 如果只更新单个入口,可以使用 `--update-claude-md-only` 或 `--update-agents-md-only`。
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
- - 创建或更新 `CLAUDE.md` 和 `AGENTS.md` 中带标记的计划治理章节
161
- - 保留已有 `docs/PLAN_MAP.md` 和 `docs/plans/*.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
- 1.0.1 起支持可选的最近记录窗口:
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#当前工作集)。已发布的 1.0.0 尚不支持此参数。
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@1.0.1 plan-governance-cli check . --strict-readiness
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 claude --dry-run
256
- plan-governance-cli setup --target all
260
+ plan-governance-cli setup --target codex --force
257
261
  ```
258
262
 
259
- `setup` 只同步 npm 包 manifest 指定的 skill、代理元数据和模板文件;默认先用 `--dry-run` 查看差异,目标文件有本地修改时不会静默覆盖。检查器、初始化器和 hook runtime 由 npm 包内部调用,不复制到项目或 skill 目录。当前版本不自动安装 hook 配置。
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、Claude Code 或其他 Agent 的项目级 hooks 手动接入。脚本只输出短提示和检查结果,不修改治理文档,不更新 `最后更新`,不安装或修改全局配置。
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 = args[++index];
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
- console.log("用法:plan-governance-cli setup --target codex|claude|all [--destination DIR] [--dry-run] [--force]");
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 (!options.target || !["codex", "claude", "all"].includes(options.target)) {
102
- throw new Error("setup 必须指定 --target codex、claude 或 all");
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 targetRoots(manifest, options) {
116
- const names = options.target === "all" ? ["codex", "claude"] : [options.target];
117
- return names.map((name) => ({
118
- name,
119
- root: resolve(options.destination ? options.destination : expandTarget(manifest.skill.targets[name])),
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(manifest) {
124
- const skillPrefix = "resources/skill/";
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 files;
156
+ let skills;
142
157
  try {
143
158
  manifest = loadManifest();
144
- files = resourceFiles(manifest);
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
- for (const target of targetRoots(manifest, options)) {
152
- for (const file of files) {
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.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",
@@ -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 检查符合项目规则。任一检查失败时停止,并依据映射恢复文件和引用;报告实际恢复范围及仍未解决的项目。只处理用户授权的仓库与计划范围,不安装依赖、不发布、不触碰其他项目。
@@ -0,0 +1,4 @@
1
+ interface:
2
+ display_name: "Plan Governance Migration"
3
+ short_description: "按创建日期迁移存量计划并修复文档引用"
4
+ default_prompt: "使用 $plan-governance-migration 将这些存量计划迁入日期目录,先核对迁移清单和日期来源,再按授权执行并验证引用。"
@@ -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 在用户明确要求时执行,日常治理不会自动搬迁计划。
@@ -20,7 +20,7 @@
20
20
 
21
21
  | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
22
22
  |---|---|---|---|---|---|
23
- | [example-plan](plans/example-plan.md) | 候选 | 阶段 0 | <YYYY-MM-DD> | - | - |
23
+ | [example-plan](plans/YYYYMMDD/example-plan.md) | 候选 | 阶段 0 | <YYYY-MM-DD> | - | - |
24
24
 
25
25
  ### 已完成
26
26
 
@@ -18,7 +18,7 @@
18
18
 
19
19
  ## 分发与资源
20
20
 
21
- SKILL、三个 references、agents 元数据与 assets 由同一 npm 包的 manifest 分发。`plan-governance-cli setup --target codex|claude|all --dry-run` 查看同步差异;实际覆盖需既有用户授权并按 setup 冲突提示处理,不自动清理用户目录残留。项目升级不迁移旧计划,受管块外内容必须保持。
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`、`CLAUDE.md` 和 `AGENTS.md`,但不覆盖 `docs/`:
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
- 授权明确时自动安排一名未参与实现或测试编写的只读复核者,提供范围、差异、风险和证据入口,并要求其自行完成、不得再委派复核。派发前复用已有同范围任务或结果;等待失败先核对准确任务 ID 与实际状态,只有确认原任务不存在或不可恢复才补发。冻结受审范围并告知变化。独立者给出发现后,实施者修复并验证原问题及受影响回归,不再送审,未解决则保持未完成。检查不可用/超时直接记录状态,不能伪填已完成结论;业务缺陷描述中的“超时”不代表检查未完成。复核不代替外部授权。
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(r"(?:docs/)?plans/([A-Za-z0-9\u4e00-\u9fff._-]+)\.md", plan_text):
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"\]\(([A-Za-z0-9\u4e00-\u9fff._-]+)\.md(?:#[^)]+)?\)", plan_text):
855
- name = Path(match.group(1).strip()).stem
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 plans_dir.glob("*.md")
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
- drifted = False
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
- warn(warnings, f"{path}: {plan_name} 计划文件 hash 已变化,需要人工复核")
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
- warn(warnings, f"{path}: PLAN_MAP.md hash 已变化,需要人工复核")
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
- drifted = True
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": 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
- docs_root = next(iter(plans.values()))["path"].parent.parent
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`、当前相关 `docs/plans/*.md` 及适用契约/ADR/migration;恢复先核对当前工作集、最近证据和实际 diff,缺失或冲突再展开。
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 [update_claude_md(root), update_agents_md(root)]
113
+ return [update_agents_md(root)]
130
114
 
131
115
 
132
- def plan_map_content(plan_slug, title, status, phase):
133
- today = date.today().isoformat()
134
- row = f"| [{title}](plans/{plan_slug}.md) | {status} | {phase} | {today} | - | - |"
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` 记录本次行为差异、阶段、Step 0 和验收;现行契约优先引用已有 Schema/OpenAPI,无合适来源且需长期维护时才按需建立 `docs/specs/*.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
- if not plans_dir.exists() or not any(plans_dir.glob("*.md")):
243
- warnings.append("缺少 docs/plans/*.md")
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="同时创建或更新 CLAUDE.md 和 AGENTS.md 中的计划治理规则。")
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="只创建或更新 CLAUDE.md 和 AGENTS.md,不初始化或覆盖 docs/。")
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
- plan_file = docs / "plans" / f"{plan_slug}.md"
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
- else:
394
- if args.update_claude_md:
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}")