plan-governance-cli 0.3.5 → 1.0.1
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 +58 -12
- package/bin/plan-governance-cli.mjs +27 -0
- package/package.json +3 -2
- package/resources/manifest.json +5 -1
- package/resources/skill/SKILL.md +22 -311
- package/resources/skill/agents/openai.yaml +1 -1
- package/resources/skill/assets/PLAN_MAP.template.md +1 -0
- package/resources/skill/assets/plan.template.md +28 -17
- package/resources/skill/assets/spec.template.md +32 -0
- package/resources/skill/references/cli.md +145 -0
- package/resources/skill/references/planning.md +128 -0
- package/resources/skill/references/verification.md +83 -0
- package/scripts/check_plan_governance.py +978 -69
- package/scripts/init_plan_governance.py +19 -89
- package/scripts/plan_governance_hook.py +32 -44
|
@@ -54,92 +54,17 @@ def init_git(root):
|
|
|
54
54
|
def agent_rules_body():
|
|
55
55
|
return """## 计划治理
|
|
56
56
|
|
|
57
|
-
|
|
57
|
+
本项目使用轻量计划治理。处理治理任务前,先读取当前环境发现的已安装 `plan-governance` skill 的 SKILL.md,并按任务读取其同源 references;不要假定用户安装目录。
|
|
58
58
|
|
|
59
|
-
|
|
59
|
+
只有 CLI 可用时,运行 `plan-governance-cli guide` 读取入口,再按需运行 `guide planning`、`guide verification` 或 `guide cli`。两者均不可用或所需能力缺失时说明前提,不自动安装,也不猜测/拼接不同版本的规则。
|
|
60
60
|
|
|
61
|
-
|
|
62
|
-
- 当前相关的 `docs/plans/*.md`
|
|
63
|
-
- 相关 `docs/adr/*.md`
|
|
64
|
-
- 相关 `docs/migrations/*.md`
|
|
61
|
+
最小执行边界:
|
|
65
62
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
-
|
|
70
|
-
-
|
|
71
|
-
- grilling 不替代 Step 0、样本矩阵、验证、失败/回滚边界或独立准入复核;`grill-me` 不可用时说明依赖不可用并保持在澄清/设计边界,不得伪造已完成 grilling。
|
|
72
|
-
|
|
73
|
-
事实源规则:
|
|
74
|
-
|
|
75
|
-
- 专项计划是实施细节事实源,记录字段方案、Schema、枚举、Step 0 证据、验证方式和完成条件。
|
|
76
|
-
- `docs/PLAN_MAP.md` 是状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
|
|
77
|
-
- 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
|
|
78
|
-
- 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
|
|
79
|
-
|
|
80
|
-
阶段转换规则:
|
|
81
|
-
|
|
82
|
-
- 阶段 N 完成只关闭阶段 N,不自动改变阶段 N+1 的准入状态。
|
|
83
|
-
- 阶段 N+1 默认保持 `设计中`,直到完成该阶段自己的 Step 0、验证方式、完成条件和独立准入复核。
|
|
84
|
-
- 只有最新的独立准入复核明确写出“达到 `待实施` 标准”,阶段才可标记为 `待实施`。
|
|
85
|
-
- 不得以阶段 N 的完成证据、全量测试通过、治理脚本通过或实施者声明替代阶段 N+1 的 Step 0。
|
|
86
|
-
- `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;专项计划只承载该阶段的实施细节和准入证据。
|
|
87
|
-
|
|
88
|
-
阶段准入最低条件:
|
|
89
|
-
|
|
90
|
-
- 当前阶段目标、范围和非目标明确。
|
|
91
|
-
- Step 0 记录明确基线类型;样本/fixture 矩阵包含输入或基线、可执行命令、预期结果、失败判定和输出位置。
|
|
92
|
-
- 验证方式、完成条件、失败策略和回滚或安全边界明确。
|
|
93
|
-
- 没有未解决的当前阶段阻塞项,且 `PLAN_MAP.md` 已同步。
|
|
94
|
-
- 最新独立准入复核明确为“通过”。
|
|
95
|
-
- 机器识别的结构化章节标题固定为 `阶段路线图`、`当前阶段`、`阶段准入摘要`、`最新独立准入复核`、`独立复核记录`;不要写成 `阶段 1 准入摘要` 等带编号变体,阶段编号以 `PLAN_MAP.md` 的当前阶段为准。
|
|
96
|
-
|
|
97
|
-
专项计划应保留追加式独立复核记录,并显式维护最新结论的日期、阶段、结论、证据和复核者。历史记录不得覆盖;`PLAN_MAP.md` 只链接最新有效结论。
|
|
98
|
-
|
|
99
|
-
阶段内独立复核调度:
|
|
100
|
-
|
|
101
|
-
- 阶段门复核只在阶段准入、阶段转换或计划明确的高影响边界触发;不为每个微小动作单独复核。
|
|
102
|
-
- 对目标、范围和授权边界已清楚且不涉及高影响外部动作的普通阶段门,执行者应自动启动独立只读 subagent 或等价独立复核者,不等待用户逐项确认。复核任务使用新上下文,不参与当前阶段实施,不修改工作区;输入至少包括计划、阶段、被审查 revision、范围、验证命令、完成条件和安全边界。
|
|
103
|
-
- 复核通过后,记录复核者、命令和证据位置,并继续当前计划/阶段推进;跨轮继续由 `goal` 保持上下文。
|
|
104
|
-
- 复核失败、复核入口不可用、超时或证据冲突时,保留阻塞并报告,不得自我批准或以安全替代动作单独放行。只有满足同等独立性和证据要求的等价复核者可以替代;没有复核者时必须停止并报告。
|
|
105
|
-
- 涉及外部授权、凭证、公开暴露、破坏性或不可逆操作、安全、隐私、合规或产品取舍的事项,必须请求用户确认;独立复核不能代替用户授权。
|
|
106
|
-
- 复核结果至少记录:计划、阶段、被审查 revision、复核范围、实际执行命令、证据位置、结论、阻塞项和复核者标识。
|
|
107
|
-
|
|
108
|
-
阶段准入严格检查命令为 `--strict-readiness`。默认治理检查保持兼容,准入缺陷先以 `WARNING` 提示,严格模式才提升为 `ERROR`;该检查只判断结构化准入条件,不替代业务验收。
|
|
109
|
-
|
|
110
|
-
草案和历史文档规则:
|
|
111
|
-
|
|
112
|
-
- 启用治理后,已有草案、历史设计、归档计划、临时分析文档等默认只作为背景材料,不再作为规范事实源。
|
|
113
|
-
- 除非用户明确点名要求更新这些文档,否则不要继续修改它们来承载新规范。
|
|
114
|
-
- 新发生的目标、范围、非目标、公共契约、字段、Schema、状态语义、阶段、验证方式、完成条件、风险和回滚,应写入 `docs/plans/*.md`、ADR、migration 或正式 spec。
|
|
115
|
-
- 计划状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接,应写入或同步 `docs/PLAN_MAP.md`。
|
|
116
|
-
- 当用户说“补充一下”“记录一下”“把刚才的结论写进去”时,如果治理已启用,默认写入治理文档;不要写入草案、README 或历史设计文档,除非用户明确点名该文件。
|
|
117
|
-
|
|
118
|
-
实施规则:
|
|
119
|
-
|
|
120
|
-
1. 只实施当前计划文档中明确的当前阶段。
|
|
121
|
-
2. 当前阶段必须具备 Step 0 证据、验证方式、完成条件,且没有未解决阻塞项。
|
|
122
|
-
3. 如果缺少 Step 0 证据,不要直接改实现;先补充复现、基线、样本、契约测试,或记录替代基线。
|
|
123
|
-
4. 如果实现过程中发现假设变化、计划顺序变化、公共契约变化、兼容策略变化、完成条件变化,或专项计划的状态、当前阶段、最后更新、字段方案、完成条件、验证结果变化,先更新治理文档,再继续实现。
|
|
124
|
-
5. 当专项计划变化时,必须同步 `docs/PLAN_MAP.md` 和所有引用该计划的路线图、优先级计划或索引。
|
|
125
|
-
6. 验收治理文档时,必须用 `rg` 搜索同名计划、P 编号、状态名、关键字段,以及 `草案为准|以草案为事实源|详见草案|draft is source|source of truth.*draft|以.*draft.*为准`,检查是否存在重复定义、漂移或旧草案重新成为事实源。
|
|
126
|
-
7. 完成后必须更新计划状态、验证证据、测试覆盖证据。
|
|
127
|
-
8. 完成后运行:
|
|
128
|
-
|
|
129
|
-
```bash
|
|
130
|
-
plan-governance-cli check .
|
|
131
|
-
```
|
|
132
|
-
|
|
133
|
-
9. 如需检查活跃计划停滞,运行 `plan-governance-cli check . --stale-days`;默认阈值为 10 天。
|
|
134
|
-
10. 旧项目如果 `PLAN_MAP.md` 仍是五列表,先运行 `plan-governance-cli init --root . --migrate-plan-map-last-updated` 显式迁移。
|
|
135
|
-
11. 普通小范围 bugfix 或一次性修改不需要强制新建治理文档,除非已有计划覆盖它。
|
|
136
|
-
|
|
137
|
-
验收独立性:
|
|
138
|
-
|
|
139
|
-
- 实施者可以更新计划状态、填写验证证据和同步治理文档,但这些内容只代表实施声明,不是验收结论。
|
|
140
|
-
- 验收者不得仅依据计划状态、完成证据文字或文档格式判定完成;必须基于当前仓库内容、可复现验证命令和反向引用检查独立确认。
|
|
141
|
-
- 验收时应逐条核对计划的完成条件、实际代码或文档变更、测试或 CI 输出、覆盖率证据,以及是否存在计划外行为变化。
|
|
142
|
-
- 如果实现偏离计划,必须更新相关计划、ADR、migration 或 spec 后再判定完成。
|
|
63
|
+
- 普通无计划覆盖的小改不强制治理;低风险且范围、后果与验证明确时由当前 AI 自验,高影响及有效独立失败按共享 verification 规范处理。已有计划未显式选择风险策略时保持原独立门禁。
|
|
64
|
+
- 只实施已授权、已准入的当前阶段。每个阶段有自身 Step 0、验证/完成和失败边界;失败、不可用或证据冲突不能自批。已有授权不重复索取,新增高影响外部动作按实际授权处理。
|
|
65
|
+
- 首次读取 `docs/PLAN_MAP.md`、当前相关 `docs/plans/*.md` 及适用契约/ADR/migration;恢复先核对当前工作集、最近证据和实际 diff,缺失或冲突再展开。
|
|
66
|
+
- 地图维护状态、阶段、关系、阻塞与证据入口;计划记录本次差异及验证,现行契约优先复用 Schema/OpenAPI。新事实不写回历史草案,变化同步地图及相关引用。
|
|
67
|
+
- 完成时按共享规范记录实际验证与适用用户验收,运行 `plan-governance-cli check .`;准入/CI/发布显式使用 `--strict-readiness`,机械通过不等于业务验收。
|
|
143
68
|
"""
|
|
144
69
|
|
|
145
70
|
|
|
@@ -161,17 +86,22 @@ def update_managed_file(root, filename, section, begin, end):
|
|
|
161
86
|
write_file(target, section, force=False)
|
|
162
87
|
return target
|
|
163
88
|
|
|
164
|
-
|
|
89
|
+
# Keep line endings and trailing whitespace outside the managed block intact.
|
|
90
|
+
current = target.read_bytes().decode("utf-8")
|
|
165
91
|
pattern = re.compile(
|
|
166
92
|
rf"{re.escape(begin)}.*?{re.escape(end)}",
|
|
167
93
|
re.DOTALL,
|
|
168
94
|
)
|
|
169
95
|
if pattern.search(current):
|
|
170
|
-
updated = pattern.sub(section.rstrip(), current)
|
|
96
|
+
updated = pattern.sub(lambda match: section.rstrip(), current)
|
|
171
97
|
else:
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
98
|
+
newline = "\r\n" if "\r\n" in current else "\n"
|
|
99
|
+
if not current or current.endswith(newline * 2):
|
|
100
|
+
separator = ""
|
|
101
|
+
else:
|
|
102
|
+
separator = newline if current.endswith(("\n", "\r")) else newline * 2
|
|
103
|
+
updated = f"{current}{separator}{section}"
|
|
104
|
+
target.write_bytes(updated.encode("utf-8"))
|
|
175
105
|
return target
|
|
176
106
|
|
|
177
107
|
|
|
@@ -214,7 +144,7 @@ def plan_map_content(plan_slug, title, status, phase):
|
|
|
214
144
|
## 文档权责
|
|
215
145
|
|
|
216
146
|
- `docs/PLAN_MAP.md` 是状态、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
|
|
217
|
-
- `docs/plans/*.md`
|
|
147
|
+
- `docs/plans/*.md` 记录本次行为差异、阶段、Step 0 和验收;现行契约优先引用已有 Schema/OpenAPI,无合适来源且需长期维护时才按需建立 `docs/specs/*.md`。同一事实不重复维护。
|
|
218
148
|
- 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
|
|
219
149
|
- 当专项计划变化时,必须同步所有引用该计划的路线图、优先级计划或索引。
|
|
220
150
|
- 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
|
|
@@ -1,11 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
2
|
import argparse
|
|
3
|
+
import importlib.util
|
|
3
4
|
import re
|
|
4
5
|
import subprocess
|
|
5
6
|
import sys
|
|
6
7
|
from pathlib import Path
|
|
7
8
|
|
|
8
9
|
|
|
10
|
+
# 从随包分发的同目录模块读取,避免误用目标仓库或全局安装的检查器。
|
|
11
|
+
_checker_spec = importlib.util.spec_from_file_location(
|
|
12
|
+
"plan_governance_checker", Path(__file__).with_name("check_plan_governance.py")
|
|
13
|
+
)
|
|
14
|
+
checker = importlib.util.module_from_spec(_checker_spec)
|
|
15
|
+
_checker_spec.loader.exec_module(checker)
|
|
16
|
+
|
|
17
|
+
|
|
9
18
|
ACTIVE_STATUSES = {"候选", "设计中", "待实施", "实施中"}
|
|
10
19
|
PLACEHOLDER_VALUES = {"-", "待补充", "待补充。", "待确认", "无", "N/A"}
|
|
11
20
|
|
|
@@ -17,32 +26,10 @@ def read_text(path):
|
|
|
17
26
|
return ""
|
|
18
27
|
|
|
19
28
|
|
|
20
|
-
|
|
21
|
-
pattern = re.compile(rf"^##\s+{re.escape(heading)}\s*$", re.MULTILINE)
|
|
22
|
-
match = pattern.search(text)
|
|
23
|
-
if not match:
|
|
24
|
-
return []
|
|
25
|
-
tail = text[match.end():]
|
|
26
|
-
next_heading = re.search(r"^##\s+", tail, re.MULTILINE)
|
|
27
|
-
section = tail[: next_heading.start()] if next_heading else tail
|
|
28
|
-
rows = []
|
|
29
|
-
for line in section.splitlines():
|
|
30
|
-
stripped = line.strip()
|
|
31
|
-
if not stripped.startswith("|") or "---" in stripped:
|
|
32
|
-
continue
|
|
33
|
-
cells = [cell.strip() for cell in stripped.strip("|").split("|")]
|
|
34
|
-
if cells and cells[0] not in {"计划", "问题"}:
|
|
35
|
-
rows.append(cells)
|
|
36
|
-
return rows
|
|
29
|
+
table_rows = checker.table_rows
|
|
37
30
|
|
|
38
31
|
|
|
39
|
-
|
|
40
|
-
match = re.search(r"\((plans/[^)]+\.md)\)", cell)
|
|
41
|
-
if match:
|
|
42
|
-
return match.group(1)
|
|
43
|
-
if cell.endswith(".md") and cell.startswith("plans/"):
|
|
44
|
-
return cell
|
|
45
|
-
return None
|
|
32
|
+
extract_plan_link = checker.extract_plan_link
|
|
46
33
|
|
|
47
34
|
|
|
48
35
|
def extract_plan_name(cell, link):
|
|
@@ -52,15 +39,7 @@ def extract_plan_name(cell, link):
|
|
|
52
39
|
return label.strip("` ")
|
|
53
40
|
|
|
54
41
|
|
|
55
|
-
|
|
56
|
-
heading_pattern = "|".join(re.escape(name) for name in heading_names)
|
|
57
|
-
pattern = re.compile(rf"^#+\s+({heading_pattern})\b.*$", re.MULTILINE)
|
|
58
|
-
match = pattern.search(text)
|
|
59
|
-
if not match:
|
|
60
|
-
return None
|
|
61
|
-
tail = text[match.end():]
|
|
62
|
-
next_heading = re.search(r"^#+\s+", tail, re.MULTILINE)
|
|
63
|
-
return tail[: next_heading.start()] if next_heading else tail
|
|
42
|
+
markdown_section = checker.markdown_section
|
|
64
43
|
|
|
65
44
|
|
|
66
45
|
def markdown_list_items(section):
|
|
@@ -158,7 +137,7 @@ def matching_plans(root, paths):
|
|
|
158
137
|
|
|
159
138
|
|
|
160
139
|
def print_session_start(root):
|
|
161
|
-
active,
|
|
140
|
+
active, _ = active_plans(root)
|
|
162
141
|
if not active:
|
|
163
142
|
print("[plan-governance] 未发现活跃计划。")
|
|
164
143
|
return 0
|
|
@@ -169,19 +148,27 @@ def print_session_start(root):
|
|
|
169
148
|
f"- {plan['name']}: {plan['status']},{plan['phase']},"
|
|
170
149
|
f"最后更新 {plan['last_updated']},证据 {plan['evidence']}"
|
|
171
150
|
)
|
|
172
|
-
|
|
173
|
-
row for row in blockers if len(row) >= 5 and row[3].strip() in {"是", "Yes"}
|
|
174
|
-
]
|
|
175
|
-
if open_blockers:
|
|
176
|
-
print("[plan-governance] 当前阻塞项:")
|
|
177
|
-
for row in open_blockers:
|
|
178
|
-
print(f"- {row[0]}: {row[4]}")
|
|
179
|
-
else:
|
|
180
|
-
print("[plan-governance] 当前阻塞项:无。")
|
|
181
|
-
|
|
151
|
+
print_gate_hints(root, {plan["name"] for plan in active})
|
|
182
152
|
return 0
|
|
183
153
|
|
|
184
154
|
|
|
155
|
+
def print_gate_hints(root, names):
|
|
156
|
+
payload, _ = checker.workset_payload(root)
|
|
157
|
+
has_blockers = False
|
|
158
|
+
for plan in payload["plans"]:
|
|
159
|
+
if plan["plan"] not in names:
|
|
160
|
+
continue
|
|
161
|
+
print(f" {plan['plan']}: {plan['readiness']},下一动作 {plan['next_action']['kind']}")
|
|
162
|
+
for blocker in plan["blockers"]:
|
|
163
|
+
has_blockers = True
|
|
164
|
+
print(f" 当前阻塞项:{blocker}")
|
|
165
|
+
for warning in payload["warnings"]:
|
|
166
|
+
print(f" WARNING: {warning}")
|
|
167
|
+
if not has_blockers:
|
|
168
|
+
print("[plan-governance] 当前阻塞项:无法确认,请核对诊断。" if payload["warnings"]
|
|
169
|
+
else "[plan-governance] 当前阻塞项:无。")
|
|
170
|
+
|
|
171
|
+
|
|
185
172
|
def print_pre_write(root, paths):
|
|
186
173
|
matches = matching_plans(root, paths)
|
|
187
174
|
if not matches:
|
|
@@ -192,6 +179,7 @@ def print_pre_write(root, paths):
|
|
|
192
179
|
for plan in matches:
|
|
193
180
|
print(f"- {plan['name']}: {plan['status']},{plan['phase']}")
|
|
194
181
|
print(" 当前阶段门禁:确认 Step 0 证据、验证方式、完成条件和公共契约约束。")
|
|
182
|
+
print_gate_hints(root, {plan["name"] for plan in matches})
|
|
195
183
|
return 0
|
|
196
184
|
|
|
197
185
|
|