plan-governance-cli 0.1.0 → 0.2.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 +19 -1
- package/bin/plan-governance-cli.mjs +162 -22
- package/package.json +6 -2
- package/resources/manifest.json +24 -0
- package/resources/skill/SKILL.md +269 -0
- package/resources/skill/agents/openai.yaml +5 -0
- package/resources/skill/assets/PLAN_MAP.template.md +48 -0
- package/resources/skill/assets/adr.template.md +18 -0
- package/resources/skill/assets/migration.template.md +20 -0
- package/resources/skill/assets/plan.template.md +80 -0
- package/scripts/init_plan_governance.py +541 -0
- package/scripts/plan_governance_hook.py +271 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# 计划:<name>
|
|
2
|
+
|
|
3
|
+
## 背景
|
|
4
|
+
|
|
5
|
+
## 目标
|
|
6
|
+
|
|
7
|
+
## 非目标
|
|
8
|
+
|
|
9
|
+
## 不变量
|
|
10
|
+
|
|
11
|
+
## 影响模块或文件
|
|
12
|
+
|
|
13
|
+
## 公共契约变化
|
|
14
|
+
|
|
15
|
+
## 阶段路线图
|
|
16
|
+
|
|
17
|
+
| 阶段 | 目标 | 进入条件 | 验证方向 | 状态 |
|
|
18
|
+
|---|---|---|---|---|
|
|
19
|
+
| 阶段 0 | 建立基线 | - | Step 0 证据存在 | 设计中 |
|
|
20
|
+
|
|
21
|
+
## 当前阶段
|
|
22
|
+
|
|
23
|
+
### 范围
|
|
24
|
+
|
|
25
|
+
### 阶段准入摘要
|
|
26
|
+
|
|
27
|
+
| 字段 | 内容 |
|
|
28
|
+
|---|---|
|
|
29
|
+
| 准入状态 | 设计中 |
|
|
30
|
+
| Step 0 | 当前阶段 Step 0 证据的链接或锚点 |
|
|
31
|
+
| 样本矩阵 | 样本/fixture 或替代基线的链接或锚点 |
|
|
32
|
+
| 验证方式 | 可执行命令和输出位置的链接或锚点 |
|
|
33
|
+
| 失败/回滚边界 | 失败判定和安全边界的链接或锚点 |
|
|
34
|
+
| 当前阻塞项 | 无,或指向未决问题表中的具体行 |
|
|
35
|
+
| 最新独立准入复核 | 尚未进行 |
|
|
36
|
+
|
|
37
|
+
### 实施步骤
|
|
38
|
+
|
|
39
|
+
1.
|
|
40
|
+
|
|
41
|
+
### Step 0 证据
|
|
42
|
+
|
|
43
|
+
### 验证方式
|
|
44
|
+
|
|
45
|
+
### 测试覆盖率
|
|
46
|
+
|
|
47
|
+
待补充。完成后需要记录测试覆盖率报告,例如 pytest-cov 输出、覆盖率百分比、关键模块覆盖情况等。
|
|
48
|
+
|
|
49
|
+
### 完成条件
|
|
50
|
+
|
|
51
|
+
## 独立复核记录
|
|
52
|
+
|
|
53
|
+
### 最新独立准入复核
|
|
54
|
+
|
|
55
|
+
| 字段 | 内容 |
|
|
56
|
+
|---|---|
|
|
57
|
+
| 日期 | 待补充 |
|
|
58
|
+
| 阶段 | 由 `PLAN_MAP.md` 当前阶段确定 |
|
|
59
|
+
| 结论 | 通过 / 未通过 |
|
|
60
|
+
| 证据 | 命令、样本或 CI 链接 |
|
|
61
|
+
| 复核者 | 姓名或 Agent |
|
|
62
|
+
|
|
63
|
+
### 历史记录
|
|
64
|
+
|
|
65
|
+
| 日期 | 类型 | 阶段 | 结论 | 证据 | 复核者 |
|
|
66
|
+
|---|---|---|---|---|---|
|
|
67
|
+
| - | - | - | - | - | - |
|
|
68
|
+
|
|
69
|
+
## 未决问题
|
|
70
|
+
|
|
71
|
+
| 问题 | 推荐方案 | 是否阻塞当前阶段 | 状态 |
|
|
72
|
+
|---|---|---|---|
|
|
73
|
+
| - | - | 否 | 已延后 |
|
|
74
|
+
|
|
75
|
+
## 风险和回滚
|
|
76
|
+
|
|
77
|
+
## 关联 ADR、迁移、spec 或 issue
|
|
78
|
+
|
|
79
|
+
-
|
|
80
|
+
|
|
@@ -0,0 +1,541 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
import argparse
|
|
3
|
+
import re
|
|
4
|
+
import shutil
|
|
5
|
+
import subprocess
|
|
6
|
+
import sys
|
|
7
|
+
from datetime import date
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
VALID_STATUSES = {
|
|
12
|
+
"候选",
|
|
13
|
+
"设计中",
|
|
14
|
+
"待实施",
|
|
15
|
+
"实施中",
|
|
16
|
+
"已完成",
|
|
17
|
+
"已替代",
|
|
18
|
+
"已合并",
|
|
19
|
+
"已废弃",
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
CLAUDE_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
23
|
+
CLAUDE_SECTION_END = "<!-- plan-governance:end -->"
|
|
24
|
+
AGENTS_SECTION_BEGIN = "<!-- plan-governance:start -->"
|
|
25
|
+
AGENTS_SECTION_END = "<!-- plan-governance:end -->"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def slugify(value):
|
|
29
|
+
value = value.strip().lower()
|
|
30
|
+
value = re.sub(r"\s+", "-", value)
|
|
31
|
+
value = re.sub(r"[^a-z0-9\u4e00-\u9fff._-]+", "-", value)
|
|
32
|
+
value = re.sub(r"-+", "-", value).strip("-._")
|
|
33
|
+
if not value:
|
|
34
|
+
raise ValueError("计划名称不能为空")
|
|
35
|
+
return value
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def write_file(path, content, force):
|
|
39
|
+
if path.exists() and not force:
|
|
40
|
+
raise FileExistsError(f"{path} 已存在;如需覆盖请加 --force")
|
|
41
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
42
|
+
path.write_text(content, encoding="utf-8")
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def init_git(root):
|
|
46
|
+
if (root / ".git").exists():
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
root.mkdir(parents=True, exist_ok=True)
|
|
50
|
+
subprocess.run(["git", "init"], cwd=root, check=True)
|
|
51
|
+
return root / ".git"
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def agent_rules_body():
|
|
55
|
+
return """## 计划治理
|
|
56
|
+
|
|
57
|
+
本项目使用轻量计划治理。处理跨阶段、架构、公共 API、Schema、迁移、兼容性或长期任务时,必须遵循以下规则。
|
|
58
|
+
|
|
59
|
+
实施前必须读取:
|
|
60
|
+
|
|
61
|
+
- `docs/PLAN_MAP.md`
|
|
62
|
+
- 当前相关的 `docs/plans/*.md`
|
|
63
|
+
- 相关 `docs/adr/*.md`
|
|
64
|
+
- 相关 `docs/migrations/*.md`
|
|
65
|
+
|
|
66
|
+
事实源规则:
|
|
67
|
+
|
|
68
|
+
- 专项计划是实施细节事实源,记录字段方案、Schema、枚举、Step 0 证据、验证方式和完成条件。
|
|
69
|
+
- `docs/PLAN_MAP.md` 是状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
|
|
70
|
+
- 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
|
|
71
|
+
- 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
|
|
72
|
+
|
|
73
|
+
阶段转换规则:
|
|
74
|
+
|
|
75
|
+
- 阶段 N 完成只关闭阶段 N,不自动改变阶段 N+1 的准入状态。
|
|
76
|
+
- 阶段 N+1 默认保持 `设计中`,直到完成该阶段自己的 Step 0、验证方式、完成条件和独立准入复核。
|
|
77
|
+
- 只有最新的独立准入复核明确写出“达到 `待实施` 标准”,阶段才可标记为 `待实施`。
|
|
78
|
+
- 不得以阶段 N 的完成证据、全量测试通过、治理脚本通过或实施者声明替代阶段 N+1 的 Step 0。
|
|
79
|
+
- `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;专项计划只承载该阶段的实施细节和准入证据。
|
|
80
|
+
|
|
81
|
+
阶段准入最低条件:
|
|
82
|
+
|
|
83
|
+
- 当前阶段目标、范围和非目标明确。
|
|
84
|
+
- Step 0 记录明确基线类型;样本/fixture 矩阵包含输入或基线、可执行命令、预期结果、失败判定和输出位置。
|
|
85
|
+
- 验证方式、完成条件、失败策略和回滚或安全边界明确。
|
|
86
|
+
- 没有未解决的当前阶段阻塞项,且 `PLAN_MAP.md` 已同步。
|
|
87
|
+
- 最新独立准入复核明确为“通过”。
|
|
88
|
+
|
|
89
|
+
专项计划应保留追加式独立复核记录,并显式维护最新结论的日期、阶段、结论、证据和复核者。历史记录不得覆盖;`PLAN_MAP.md` 只链接最新有效结论。
|
|
90
|
+
|
|
91
|
+
阶段准入严格检查命令为 `--strict-readiness`。默认治理检查保持兼容,准入缺陷先以 `WARNING` 提示,严格模式才提升为 `ERROR`;该检查只判断结构化准入条件,不替代业务验收。
|
|
92
|
+
|
|
93
|
+
草案和历史文档规则:
|
|
94
|
+
|
|
95
|
+
- 启用治理后,已有草案、历史设计、归档计划、临时分析文档等默认只作为背景材料,不再作为规范事实源。
|
|
96
|
+
- 除非用户明确点名要求更新这些文档,否则不要继续修改它们来承载新规范。
|
|
97
|
+
- 新发生的目标、范围、非目标、公共契约、字段、Schema、状态语义、阶段、验证方式、完成条件、风险和回滚,应写入 `docs/plans/*.md`、ADR、migration 或正式 spec。
|
|
98
|
+
- 计划状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接,应写入或同步 `docs/PLAN_MAP.md`。
|
|
99
|
+
- 当用户说“补充一下”“记录一下”“把刚才的结论写进去”时,如果治理已启用,默认写入治理文档;不要写入草案、README 或历史设计文档,除非用户明确点名该文件。
|
|
100
|
+
|
|
101
|
+
实施规则:
|
|
102
|
+
|
|
103
|
+
1. 只实施当前计划文档中明确的当前阶段。
|
|
104
|
+
2. 当前阶段必须具备 Step 0 证据、验证方式、完成条件,且没有未解决阻塞项。
|
|
105
|
+
3. 如果缺少 Step 0 证据,不要直接改实现;先补充复现、基线、样本、契约测试,或记录替代基线。
|
|
106
|
+
4. 如果实现过程中发现假设变化、计划顺序变化、公共契约变化、兼容策略变化、完成条件变化,或专项计划的状态、当前阶段、最后更新、字段方案、完成条件、验证结果变化,先更新治理文档,再继续实现。
|
|
107
|
+
5. 当专项计划变化时,必须同步 `docs/PLAN_MAP.md` 和所有引用该计划的路线图、优先级计划或索引。
|
|
108
|
+
6. 验收治理文档时,必须用 `rg` 搜索同名计划、P 编号、状态名、关键字段,以及 `草案为准|以草案为事实源|详见草案|draft is source|source of truth.*draft|以.*draft.*为准`,检查是否存在重复定义、漂移或旧草案重新成为事实源。
|
|
109
|
+
7. 完成后必须更新计划状态、验证证据、测试覆盖证据。
|
|
110
|
+
8. 完成后运行:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
python3 scripts/check_plan_governance.py .
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
9. 如需检查活跃计划停滞,运行 `python3 scripts/check_plan_governance.py . --stale-days`;默认阈值为 10 天。
|
|
117
|
+
10. 旧项目如果 `PLAN_MAP.md` 仍是五列表,先运行 `python3 scripts/init_plan_governance.py --root . --migrate-plan-map-last-updated` 显式迁移。
|
|
118
|
+
11. 普通小范围 bugfix 或一次性修改不需要强制新建治理文档,除非已有计划覆盖它。
|
|
119
|
+
|
|
120
|
+
验收独立性:
|
|
121
|
+
|
|
122
|
+
- 实施者可以更新计划状态、填写验证证据和同步治理文档,但这些内容只代表实施声明,不是验收结论。
|
|
123
|
+
- 验收者不得仅依据计划状态、完成证据文字或文档格式判定完成;必须基于当前仓库内容、可复现验证命令和反向引用检查独立确认。
|
|
124
|
+
- 验收时应逐条核对计划的完成条件、实际代码或文档变更、测试或 CI 输出、覆盖率证据,以及是否存在计划外行为变化。
|
|
125
|
+
- 如果实现偏离计划,必须更新相关计划、ADR、migration 或 spec 后再判定完成。
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def managed_section(begin, end):
|
|
130
|
+
return f"{begin}\n{agent_rules_body()}{end}\n"
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def claude_md_section():
|
|
134
|
+
return managed_section(CLAUDE_SECTION_BEGIN, CLAUDE_SECTION_END)
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def agents_md_section():
|
|
138
|
+
return managed_section(AGENTS_SECTION_BEGIN, AGENTS_SECTION_END)
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def update_managed_file(root, filename, section, begin, end):
|
|
142
|
+
target = root / filename
|
|
143
|
+
if not target.exists():
|
|
144
|
+
write_file(target, section, force=False)
|
|
145
|
+
return target
|
|
146
|
+
|
|
147
|
+
current = target.read_text(encoding="utf-8")
|
|
148
|
+
pattern = re.compile(
|
|
149
|
+
rf"{re.escape(begin)}.*?{re.escape(end)}",
|
|
150
|
+
re.DOTALL,
|
|
151
|
+
)
|
|
152
|
+
if pattern.search(current):
|
|
153
|
+
updated = pattern.sub(section.rstrip(), current)
|
|
154
|
+
else:
|
|
155
|
+
separator = "\n\n" if current.rstrip() else ""
|
|
156
|
+
updated = f"{current.rstrip()}{separator}{section}"
|
|
157
|
+
target.write_text(updated, encoding="utf-8")
|
|
158
|
+
return target
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def update_claude_md(root):
|
|
162
|
+
return update_managed_file(
|
|
163
|
+
root,
|
|
164
|
+
"CLAUDE.md",
|
|
165
|
+
claude_md_section(),
|
|
166
|
+
CLAUDE_SECTION_BEGIN,
|
|
167
|
+
CLAUDE_SECTION_END,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def update_agents_md(root):
|
|
172
|
+
return update_managed_file(
|
|
173
|
+
root,
|
|
174
|
+
"AGENTS.md",
|
|
175
|
+
agents_md_section(),
|
|
176
|
+
AGENTS_SECTION_BEGIN,
|
|
177
|
+
AGENTS_SECTION_END,
|
|
178
|
+
)
|
|
179
|
+
|
|
180
|
+
|
|
181
|
+
def update_agent_rules(root):
|
|
182
|
+
return [update_claude_md(root), update_agents_md(root)]
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
def plan_map_content(plan_slug, title, status, phase):
|
|
186
|
+
today = date.today().isoformat()
|
|
187
|
+
return f"""# PLAN_MAP
|
|
188
|
+
|
|
189
|
+
## 治理范围
|
|
190
|
+
|
|
191
|
+
本文件只跟踪跨阶段、影响公共契约、依赖真实反馈,或会与其他计划发生关系的计划。普通一次性任务不要加入这里。
|
|
192
|
+
|
|
193
|
+
## 文档权责
|
|
194
|
+
|
|
195
|
+
- `docs/PLAN_MAP.md` 是状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
|
|
196
|
+
- `docs/plans/*.md` 是专项计划的实施细节事实源,记录字段方案、Schema、枚举、Step 0 证据、验证方式和完成条件。
|
|
197
|
+
- 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
|
|
198
|
+
- 当专项计划变化时,必须同步所有引用该计划的路线图、优先级计划或索引。
|
|
199
|
+
- 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
|
|
200
|
+
- `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;阶段 N 完成后,阶段 N+1 默认保持 `设计中`。
|
|
201
|
+
- 阶段准入摘要、样本矩阵和独立复核记录只写入专项计划,不复制到本索引。
|
|
202
|
+
- 启用治理后,已有草案、历史设计、归档计划和临时分析文档默认只作为背景材料,不再作为规范事实源;后续新规范默认进入 `docs/plans/*.md`、ADR、migration、正式 spec 或 `docs/PLAN_MAP.md`。
|
|
203
|
+
|
|
204
|
+
## 计划索引
|
|
205
|
+
|
|
206
|
+
| 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
|
|
207
|
+
|---|---|---|---|---|---|
|
|
208
|
+
| [{title}](plans/{plan_slug}.md) | {status} | {phase} | {today} | - | - |
|
|
209
|
+
|
|
210
|
+
允许状态:`候选`、`设计中`、`待实施`、`实施中`、`已完成`、`已替代`、`已合并`、`已废弃`。
|
|
211
|
+
|
|
212
|
+
## 推荐顺序
|
|
213
|
+
|
|
214
|
+
1. `{plan_slug}`
|
|
215
|
+
|
|
216
|
+
## 依赖关系
|
|
217
|
+
|
|
218
|
+
| 计划 | 依赖 | 原因 |
|
|
219
|
+
|---|---|---|
|
|
220
|
+
| {plan_slug} | - | - |
|
|
221
|
+
|
|
222
|
+
## 替代、合并和废弃
|
|
223
|
+
|
|
224
|
+
| 计划 | 关系 | 目标 | 原因 |
|
|
225
|
+
|---|---|---|---|
|
|
226
|
+
| - | - | - | - |
|
|
227
|
+
|
|
228
|
+
## 当前阻塞项
|
|
229
|
+
|
|
230
|
+
| 问题 | 推荐方案 | 影响范围 | 是否阻塞当前阶段 | 状态 |
|
|
231
|
+
|---|---|---|---|---|
|
|
232
|
+
| - | - | - | 否 | 已延后 |
|
|
233
|
+
|
|
234
|
+
## 完成证据
|
|
235
|
+
|
|
236
|
+
| 计划 | 阶段 | 证据 |
|
|
237
|
+
|---|---|---|
|
|
238
|
+
| - | - | - |
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def plan_content(plan_slug, title, status, phase, goal):
|
|
243
|
+
goal_text = goal or "待补充。"
|
|
244
|
+
return f"""# 计划:{title}
|
|
245
|
+
|
|
246
|
+
## 背景
|
|
247
|
+
|
|
248
|
+
待补充。
|
|
249
|
+
|
|
250
|
+
## 目标
|
|
251
|
+
|
|
252
|
+
{goal_text}
|
|
253
|
+
|
|
254
|
+
## 非目标
|
|
255
|
+
|
|
256
|
+
- 待补充。
|
|
257
|
+
|
|
258
|
+
## 不变量
|
|
259
|
+
|
|
260
|
+
- 当前阶段写细,后续阶段写粗。
|
|
261
|
+
- 完成时必须记录可验证证据。
|
|
262
|
+
|
|
263
|
+
## 影响模块或文件
|
|
264
|
+
|
|
265
|
+
- 待补充。
|
|
266
|
+
|
|
267
|
+
## 公共契约变化
|
|
268
|
+
|
|
269
|
+
待确认。若涉及 API、Schema、兼容性或迁移行为,需要在这里明确。
|
|
270
|
+
|
|
271
|
+
## 阶段路线图
|
|
272
|
+
|
|
273
|
+
| 阶段 | 目标 | 进入条件 | 验证方向 | 状态 |
|
|
274
|
+
|---|---|---|---|---|
|
|
275
|
+
| {phase} | 建立当前阶段基线 | 治理文档已初始化 | Step 0 证据存在 | {status} |
|
|
276
|
+
|
|
277
|
+
## 当前阶段
|
|
278
|
+
|
|
279
|
+
### 范围
|
|
280
|
+
|
|
281
|
+
待补充。
|
|
282
|
+
|
|
283
|
+
### 阶段准入摘要
|
|
284
|
+
|
|
285
|
+
| 字段 | 内容 |
|
|
286
|
+
|---|---|
|
|
287
|
+
| 准入状态 | {status} |
|
|
288
|
+
| Step 0 | 当前阶段 Step 0 证据的链接或锚点 |
|
|
289
|
+
| 样本矩阵 | 样本/fixture 或替代基线的链接或锚点 |
|
|
290
|
+
| 验证方式 | 可执行命令和输出位置的链接或锚点 |
|
|
291
|
+
| 失败/回滚边界 | 失败判定和安全边界的链接或锚点 |
|
|
292
|
+
| 当前阻塞项 | 无,或指向未决问题表中的具体行 |
|
|
293
|
+
| 最新独立准入复核 | 尚未进行 |
|
|
294
|
+
|
|
295
|
+
### 实施步骤
|
|
296
|
+
|
|
297
|
+
1. 补充 Step 0 证据。
|
|
298
|
+
2. 明确当前阶段完成条件。
|
|
299
|
+
3. 实施当前阶段。
|
|
300
|
+
4. 运行验证并记录证据。
|
|
301
|
+
|
|
302
|
+
### Step 0 证据
|
|
303
|
+
|
|
304
|
+
待补充。实施前需要记录失败测试、最小复现、现状快照、兼容样本、fixture 或关键假设验证。
|
|
305
|
+
|
|
306
|
+
### 验证方式
|
|
307
|
+
|
|
308
|
+
待补充。
|
|
309
|
+
|
|
310
|
+
### 测试覆盖率
|
|
311
|
+
|
|
312
|
+
待补充。完成后需要记录测试覆盖率报告,例如 pytest-cov 输出、覆盖率百分比、关键模块覆盖情况等。
|
|
313
|
+
|
|
314
|
+
### 完成条件
|
|
315
|
+
|
|
316
|
+
- Step 0 证据已记录。
|
|
317
|
+
- 当前阶段验证通过。
|
|
318
|
+
- `docs/PLAN_MAP.md` 状态和证据已同步。
|
|
319
|
+
|
|
320
|
+
## 独立复核记录
|
|
321
|
+
|
|
322
|
+
### 最新独立准入复核
|
|
323
|
+
|
|
324
|
+
| 字段 | 内容 |
|
|
325
|
+
|---|---|
|
|
326
|
+
| 日期 | 待补充 |
|
|
327
|
+
| 阶段 | 由 `PLAN_MAP.md` 当前阶段确定 |
|
|
328
|
+
| 结论 | 通过 / 未通过 |
|
|
329
|
+
| 证据 | 命令、样本或 CI 链接 |
|
|
330
|
+
| 复核者 | 姓名或 Agent |
|
|
331
|
+
|
|
332
|
+
### 历史记录
|
|
333
|
+
|
|
334
|
+
| 日期 | 类型 | 阶段 | 结论 | 证据 | 复核者 |
|
|
335
|
+
|---|---|---|---|---|---|
|
|
336
|
+
| - | - | - | - | - | - |
|
|
337
|
+
|
|
338
|
+
## 未决问题
|
|
339
|
+
|
|
340
|
+
| 问题 | 推荐方案 | 是否阻塞当前阶段 | 状态 |
|
|
341
|
+
|---|---|---|---|
|
|
342
|
+
| 当前阶段 Step 0 证据是什么? | 在实施前补充最小可观察基线。 | 是 | 待确认 |
|
|
343
|
+
|
|
344
|
+
## 风险和回滚
|
|
345
|
+
|
|
346
|
+
待补充。
|
|
347
|
+
|
|
348
|
+
## 关联 ADR、迁移、spec 或 issue
|
|
349
|
+
|
|
350
|
+
-
|
|
351
|
+
"""
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
def copy_checker(root, force):
|
|
355
|
+
source = Path(__file__).with_name("check_plan_governance.py")
|
|
356
|
+
target = root / "scripts" / "check_plan_governance.py"
|
|
357
|
+
if source.resolve() == target.resolve():
|
|
358
|
+
target.chmod(0o755)
|
|
359
|
+
return target
|
|
360
|
+
if target.exists() and not force:
|
|
361
|
+
raise FileExistsError(f"{target} 已存在;如需覆盖请加 --force")
|
|
362
|
+
target.parent.mkdir(parents=True, exist_ok=True)
|
|
363
|
+
shutil.copy2(source, target)
|
|
364
|
+
target.chmod(0o755)
|
|
365
|
+
return target
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def docs_warnings(root):
|
|
369
|
+
warnings = []
|
|
370
|
+
if not (root / "docs" / "PLAN_MAP.md").exists():
|
|
371
|
+
warnings.append("缺少 docs/PLAN_MAP.md")
|
|
372
|
+
plans_dir = root / "docs" / "plans"
|
|
373
|
+
if not plans_dir.exists() or not any(plans_dir.glob("*.md")):
|
|
374
|
+
warnings.append("缺少 docs/plans/*.md")
|
|
375
|
+
return warnings
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def migrate_plan_map_last_updated(root, last_updated):
|
|
379
|
+
plan_map = root / "docs" / "PLAN_MAP.md"
|
|
380
|
+
if not plan_map.exists():
|
|
381
|
+
raise FileNotFoundError("缺少 docs/PLAN_MAP.md")
|
|
382
|
+
|
|
383
|
+
text = plan_map.read_text(encoding="utf-8")
|
|
384
|
+
lines = text.splitlines()
|
|
385
|
+
migrated = []
|
|
386
|
+
in_plan_index = False
|
|
387
|
+
changed = False
|
|
388
|
+
|
|
389
|
+
for line in lines:
|
|
390
|
+
if re.match(r"^##\s+计划索引\s*$", line):
|
|
391
|
+
in_plan_index = True
|
|
392
|
+
migrated.append(line)
|
|
393
|
+
continue
|
|
394
|
+
if in_plan_index and re.match(r"^##\s+", line):
|
|
395
|
+
in_plan_index = False
|
|
396
|
+
|
|
397
|
+
if in_plan_index and line.strip().startswith("|"):
|
|
398
|
+
cells = [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
399
|
+
if cells == ["计划", "状态", "当前阶段", "依赖", "证据"]:
|
|
400
|
+
migrated.append("| 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |")
|
|
401
|
+
changed = True
|
|
402
|
+
continue
|
|
403
|
+
if len(cells) == 5 and set(cells) == {"---"}:
|
|
404
|
+
migrated.append("|---|---|---|---|---|---|")
|
|
405
|
+
changed = True
|
|
406
|
+
continue
|
|
407
|
+
if len(cells) == 5:
|
|
408
|
+
cells.insert(3, last_updated)
|
|
409
|
+
migrated.append("| " + " | ".join(cells) + " |")
|
|
410
|
+
changed = True
|
|
411
|
+
continue
|
|
412
|
+
|
|
413
|
+
migrated.append(line)
|
|
414
|
+
|
|
415
|
+
if changed:
|
|
416
|
+
plan_map.write_text("\n".join(migrated) + ("\n" if text.endswith("\n") else ""), encoding="utf-8")
|
|
417
|
+
return plan_map, changed
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def upgrade_existing(root):
|
|
421
|
+
written = [
|
|
422
|
+
copy_checker(root, force=True),
|
|
423
|
+
*update_agent_rules(root),
|
|
424
|
+
]
|
|
425
|
+
return written, docs_warnings(root)
|
|
426
|
+
|
|
427
|
+
|
|
428
|
+
def parse_args(argv):
|
|
429
|
+
parser = argparse.ArgumentParser(description="初始化中文 plan-governance 文档。")
|
|
430
|
+
parser.add_argument("--root", default=".", help="目标仓库根目录,默认当前目录。")
|
|
431
|
+
parser.add_argument("--plan", help="计划文件名或计划标识,例如 api-compat-migration。")
|
|
432
|
+
parser.add_argument("--title", help="计划显示名称,默认使用 --plan。")
|
|
433
|
+
parser.add_argument("--goal", help="计划目标,会写入计划文档。")
|
|
434
|
+
parser.add_argument("--status", default="设计中", choices=sorted(VALID_STATUSES), help="初始状态。")
|
|
435
|
+
parser.add_argument("--phase", default="阶段 0", help="当前阶段名称。")
|
|
436
|
+
parser.add_argument("--copy-checker", action="store_true", help="复制检查脚本到目标仓库 scripts/。")
|
|
437
|
+
parser.add_argument("--update-claude-md", action="store_true", help="创建或更新目标仓库 CLAUDE.md 中的计划治理规则。")
|
|
438
|
+
parser.add_argument("--update-agents-md", action="store_true", help="创建或更新目标仓库 AGENTS.md 中的计划治理规则。")
|
|
439
|
+
parser.add_argument("--update-agent-rules", action="store_true", help="同时创建或更新 CLAUDE.md 和 AGENTS.md 中的计划治理规则。")
|
|
440
|
+
parser.add_argument("--update-claude-md-only", action="store_true", help="只创建或更新 CLAUDE.md,不初始化或覆盖 docs/。")
|
|
441
|
+
parser.add_argument("--update-agents-md-only", action="store_true", help="只创建或更新 AGENTS.md,不初始化或覆盖 docs/。")
|
|
442
|
+
parser.add_argument("--update-agent-rules-only", action="store_true", help="只创建或更新 CLAUDE.md 和 AGENTS.md,不初始化或覆盖 docs/。")
|
|
443
|
+
parser.add_argument("--upgrade-existing", action="store_true", help="升级已有项目的辅助文件:刷新检查脚本和代理规则,不覆盖 docs/。")
|
|
444
|
+
parser.add_argument("--migrate-plan-map-last-updated", action="store_true", help="将旧五列表 PLAN_MAP.md 迁移为包含最后更新的六列表。")
|
|
445
|
+
parser.add_argument("--last-updated-date", default=date.today().isoformat(), help="迁移 PLAN_MAP.md 时填入的最后更新日期,默认今天。")
|
|
446
|
+
parser.add_argument("--force", action="store_true", help="允许覆盖已存在的治理文件。")
|
|
447
|
+
args = parser.parse_args(argv)
|
|
448
|
+
only_modes = [
|
|
449
|
+
args.update_claude_md_only,
|
|
450
|
+
args.update_agents_md_only,
|
|
451
|
+
args.update_agent_rules_only,
|
|
452
|
+
args.upgrade_existing,
|
|
453
|
+
args.migrate_plan_map_last_updated,
|
|
454
|
+
]
|
|
455
|
+
if sum(bool(mode) for mode in only_modes) > 1:
|
|
456
|
+
parser.error("--update-*-only、--upgrade-existing 和 --migrate-plan-map-last-updated 不能同时使用")
|
|
457
|
+
if not any(only_modes) and not args.plan:
|
|
458
|
+
parser.error("正常初始化必须提供 --plan;已有项目可使用 --update-*-only、--upgrade-existing 或 --migrate-plan-map-last-updated")
|
|
459
|
+
return args
|
|
460
|
+
|
|
461
|
+
|
|
462
|
+
def main(argv=None):
|
|
463
|
+
args = parse_args(argv or sys.argv[1:])
|
|
464
|
+
root = Path(args.root).expanduser().resolve()
|
|
465
|
+
|
|
466
|
+
if args.update_claude_md_only:
|
|
467
|
+
target = update_claude_md(root)
|
|
468
|
+
print(f"已写入:{target}")
|
|
469
|
+
print("CLAUDE.md 已更新;未修改 docs/。")
|
|
470
|
+
return 0
|
|
471
|
+
|
|
472
|
+
if args.update_agents_md_only:
|
|
473
|
+
target = update_agents_md(root)
|
|
474
|
+
print(f"已写入:{target}")
|
|
475
|
+
print("AGENTS.md 已更新;未修改 docs/。")
|
|
476
|
+
return 0
|
|
477
|
+
|
|
478
|
+
if args.update_agent_rules_only:
|
|
479
|
+
for target in update_agent_rules(root):
|
|
480
|
+
print(f"已写入:{target}")
|
|
481
|
+
print("代理规则已更新;未修改 docs/。")
|
|
482
|
+
return 0
|
|
483
|
+
|
|
484
|
+
if args.upgrade_existing:
|
|
485
|
+
written, warnings = upgrade_existing(root)
|
|
486
|
+
for path in written:
|
|
487
|
+
print(f"已写入:{path}")
|
|
488
|
+
for warning in warnings:
|
|
489
|
+
print(f"WARNING: {warning}")
|
|
490
|
+
print("已有项目升级完成;未覆盖 docs/。下一步请运行 python3 scripts/check_plan_governance.py .")
|
|
491
|
+
return 0
|
|
492
|
+
|
|
493
|
+
if args.migrate_plan_map_last_updated:
|
|
494
|
+
parse_date = date.fromisoformat(args.last_updated_date)
|
|
495
|
+
target, changed = migrate_plan_map_last_updated(root, parse_date.isoformat())
|
|
496
|
+
print(f"已检查:{target}")
|
|
497
|
+
if changed:
|
|
498
|
+
print("PLAN_MAP.md 已迁移为包含最后更新的六列表。")
|
|
499
|
+
else:
|
|
500
|
+
print("PLAN_MAP.md 已是六列表;无需迁移。")
|
|
501
|
+
return 0
|
|
502
|
+
|
|
503
|
+
plan_slug = slugify(args.plan)
|
|
504
|
+
title = args.title or plan_slug
|
|
505
|
+
|
|
506
|
+
docs = root / "docs"
|
|
507
|
+
plan_map = docs / "PLAN_MAP.md"
|
|
508
|
+
plan_file = docs / "plans" / f"{plan_slug}.md"
|
|
509
|
+
|
|
510
|
+
created = []
|
|
511
|
+
git_dir = init_git(root)
|
|
512
|
+
if git_dir is not None:
|
|
513
|
+
created.append(git_dir)
|
|
514
|
+
|
|
515
|
+
write_file(plan_map, plan_map_content(plan_slug, title, args.status, args.phase), args.force)
|
|
516
|
+
created.append(plan_map)
|
|
517
|
+
write_file(plan_file, plan_content(plan_slug, title, args.status, args.phase, args.goal), args.force)
|
|
518
|
+
created.append(plan_file)
|
|
519
|
+
|
|
520
|
+
if args.copy_checker:
|
|
521
|
+
created.append(copy_checker(root, args.force))
|
|
522
|
+
if args.update_agent_rules:
|
|
523
|
+
created.extend(update_agent_rules(root))
|
|
524
|
+
else:
|
|
525
|
+
if args.update_claude_md:
|
|
526
|
+
created.append(update_claude_md(root))
|
|
527
|
+
if args.update_agents_md:
|
|
528
|
+
created.append(update_agents_md(root))
|
|
529
|
+
|
|
530
|
+
for path in created:
|
|
531
|
+
print(f"已写入:{path}")
|
|
532
|
+
print("初始化完成。下一步请补充计划文档中的 Step 0 证据、验证方式和完成条件。")
|
|
533
|
+
return 0
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
if __name__ == "__main__":
|
|
537
|
+
try:
|
|
538
|
+
raise SystemExit(main())
|
|
539
|
+
except (FileExistsError, FileNotFoundError, ValueError) as exc:
|
|
540
|
+
print(f"ERROR: {exc}", file=sys.stderr)
|
|
541
|
+
raise SystemExit(1)
|