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 CHANGED
@@ -166,7 +166,7 @@ plan-governance-cli . --strict-readiness
166
166
  也可以不全局安装,直接使用锁定版本:
167
167
 
168
168
  ```bash
169
- npx --yes plan-governance-cli@0.1.0 . --strict-readiness
169
+ npx --yes --package plan-governance-cli@0.2.0 plan-governance-cli . --strict-readiness
170
170
  ```
171
171
 
172
172
  npm CLI 只是统一入口,实际检查逻辑仍由包内版本化的 Python 检查器执行;迁移期间项目原有 Python 命令仍可作为回滚路径。
@@ -181,6 +181,24 @@ npm CLI 只是统一入口,实际检查逻辑仍由包内版本化的 Python
181
181
 
182
182
  `--stale-days` 会检查活跃计划的 `最后更新` 日期是否超过阈值;省略数值时默认 10 天。停滞检测只输出 `WARNING`,不自动改变计划状态。
183
183
 
184
+ 安装后的资源管理:
185
+
186
+ ```bash
187
+ plan-governance-cli setup --target codex --dry-run
188
+ plan-governance-cli setup --target claude --dry-run
189
+ plan-governance-cli setup --target all
190
+ ```
191
+
192
+ `setup` 只同步 npm 包 manifest 指定的 skill、代理元数据和模板文件;默认先用 `--dry-run` 查看差异,目标文件有本地修改时不会静默覆盖。检查器、初始化器和 hook runtime 由 npm 包内部调用,不复制到项目或 skill 目录。当前版本不自动安装 hook 配置。
193
+
194
+ 初始化项目时使用包内初始化器:
195
+
196
+ ```bash
197
+ plan-governance-cli init --root . --plan api-compat-migration --title "API 兼容性迁移" --goal "分阶段完成 API 兼容性迁移"
198
+ ```
199
+
200
+ `init` 默认不复制项目本地 `scripts/check_plan_governance.py`;如确有兼容需要,才显式传递底层 `--copy-checker` 参数。
201
+
184
202
  ## Hook runtime
185
203
 
186
204
  本仓库提供只读 hook runtime,供 Codex、Claude Code 或其他 Agent 的项目级 hooks 手动接入。脚本只输出短提示和检查结果,不修改治理文档,不更新 `最后更新`,不安装或修改全局配置。
@@ -1,39 +1,179 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- import { accessSync, constants } from "node:fs";
4
- import { fileURLToPath } from "node:url";
5
- import { dirname, resolve } from "node:path";
3
+ import {
4
+ accessSync,
5
+ constants,
6
+ existsSync,
7
+ mkdirSync,
8
+ readFileSync,
9
+ renameSync,
10
+ writeFileSync,
11
+ } from "node:fs";
12
+ import { homedir, tmpdir } from "node:os";
13
+ import { dirname, join, relative, resolve } from "node:path";
6
14
  import { spawnSync } from "node:child_process";
15
+ import { fileURLToPath } from "node:url";
7
16
 
8
17
  const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
9
18
  const checker = resolve(packageRoot, "scripts", "check_plan_governance.py");
19
+ const initializer = resolve(packageRoot, "scripts", "init_plan_governance.py");
20
+ const hookRuntime = resolve(packageRoot, "scripts", "plan_governance_hook.py");
21
+ const manifestPath = resolve(packageRoot, "resources", "manifest.json");
10
22
 
11
- try {
12
- accessSync(checker, constants.R_OK);
13
- } catch (error) {
14
- console.error(`plan-governance-cli: 找不到包内检查器:${checker}`);
15
- process.exit(1);
23
+ function fail(message) {
24
+ console.error(`plan-governance-cli: ${message}`);
25
+ return 1;
26
+ }
27
+
28
+ function runPython(script, args) {
29
+ try {
30
+ accessSync(script, constants.R_OK);
31
+ } catch {
32
+ return fail(`找不到包内脚本:${script}`);
33
+ }
34
+
35
+ const candidates = process.env.PYTHON ? [process.env.PYTHON] : ["python3", "python"];
36
+ let result;
37
+ for (const command of candidates) {
38
+ result = spawnSync(command, [script, ...args], { stdio: "inherit" });
39
+ if (!result.error || result.error.code !== "ENOENT") {
40
+ break;
41
+ }
42
+ }
43
+
44
+ if (result?.error) {
45
+ return fail(`无法启动 Python 脚本:${result.error.message}`);
46
+ }
47
+ if (result?.signal) {
48
+ console.error(`plan-governance-cli: 脚本被信号 ${result.signal} 终止`);
49
+ return 1;
50
+ }
51
+ return result?.status ?? 1;
16
52
  }
17
53
 
18
- const args = [checker, ...process.argv.slice(2)];
19
- const candidates = process.env.PYTHON ? [process.env.PYTHON] : ["python3", "python"];
54
+ function loadManifest() {
55
+ try {
56
+ return JSON.parse(readFileSync(manifestPath, "utf8"));
57
+ } catch (error) {
58
+ throw new Error(`无法读取包内资源清单:${error.message}`);
59
+ }
60
+ }
61
+
62
+ function parseSetupArgs(args) {
63
+ const options = { target: null, destination: null, dryRun: false, force: false };
64
+ for (let index = 0; index < args.length; index += 1) {
65
+ const arg = args[index];
66
+ if (arg === "--target") {
67
+ options.target = args[++index];
68
+ } else if (arg === "--destination") {
69
+ options.destination = args[++index];
70
+ } else if (arg === "--dry-run") {
71
+ options.dryRun = true;
72
+ } else if (arg === "--force") {
73
+ options.force = true;
74
+ } else if (arg === "-h" || arg === "--help") {
75
+ console.log("用法:plan-governance-cli setup --target codex|claude|all [--destination DIR] [--dry-run] [--force]");
76
+ return null;
77
+ } else {
78
+ throw new Error(`setup 不支持参数:${arg}`);
79
+ }
80
+ }
20
81
 
21
- let result;
22
- for (const command of candidates) {
23
- result = spawnSync(command, args, { stdio: "inherit" });
24
- if (!result.error || result.error.code !== "ENOENT") {
25
- break;
82
+ if (!options.target || !["codex", "claude", "all"].includes(options.target)) {
83
+ throw new Error("setup 必须指定 --target codex、claude 或 all");
26
84
  }
85
+ if (options.destination && options.target === "all") {
86
+ throw new Error("--destination 只能与单个 setup target 一起使用");
87
+ }
88
+ return options;
89
+ }
90
+
91
+ function expandTarget(target) {
92
+ const home = process.env.HOME || homedir();
93
+ return target.replace(/^~(?=\/|$)/, home);
94
+ }
95
+
96
+ function targetRoots(manifest, options) {
97
+ const names = options.target === "all" ? ["codex", "claude"] : [options.target];
98
+ return names.map((name) => ({
99
+ name,
100
+ root: resolve(options.destination ? options.destination : expandTarget(manifest.skill.targets[name])),
101
+ }));
102
+ }
103
+
104
+ function resourceFiles(manifest) {
105
+ const skillPrefix = "resources/skill/";
106
+ return manifest.skill.files.map((source) => {
107
+ if (!source.startsWith(skillPrefix)) {
108
+ throw new Error(`skill 资源不在受支持的目录中:${source}`);
109
+ }
110
+ return {
111
+ source: resolve(packageRoot, source),
112
+ relative: source.slice(skillPrefix.length),
113
+ };
114
+ });
27
115
  }
28
116
 
29
- if (result?.error) {
30
- console.error(`plan-governance-cli: 无法启动 Python 检查器:${result.error.message}`);
31
- process.exit(1);
117
+ function setup(args) {
118
+ const options = parseSetupArgs(args);
119
+ if (options === null) return 0;
120
+
121
+ let manifest;
122
+ let files;
123
+ try {
124
+ manifest = loadManifest();
125
+ files = resourceFiles(manifest);
126
+ } catch (error) {
127
+ return fail(error.message);
128
+ }
129
+
130
+ const plans = [];
131
+ try {
132
+ for (const target of targetRoots(manifest, options)) {
133
+ for (const file of files) {
134
+ accessSync(file.source, constants.R_OK);
135
+ const destination = resolve(target.root, file.relative);
136
+ const existing = existsSync(destination) ? readFileSync(destination, "utf8") : null;
137
+ const sourceContent = readFileSync(file.source, "utf8");
138
+ if (existing !== null && existing !== sourceContent && !options.force) {
139
+ throw new Error(`目标文件存在本地差异,未覆盖:${destination}(如确认覆盖请加 --force)`);
140
+ }
141
+ plans.push({ target: target.name, destination, existing, sourceContent });
142
+ }
143
+ }
144
+ } catch (error) {
145
+ return fail(error.message);
146
+ }
147
+
148
+ for (const plan of plans) {
149
+ if (plan.existing === plan.sourceContent) {
150
+ console.log(`已是最新:${plan.destination}`);
151
+ } else if (options.dryRun) {
152
+ console.log(`将写入:${plan.destination}`);
153
+ } else {
154
+ mkdirSync(dirname(plan.destination), { recursive: true });
155
+ const temporary = join(tmpdir(), `plan-governance-${process.pid}-${Date.now()}-${Math.random().toString(16).slice(2)}`);
156
+ writeFileSync(temporary, plan.sourceContent, "utf8");
157
+ renameSync(temporary, plan.destination);
158
+ console.log(`已同步:${plan.destination}`);
159
+ }
160
+ }
161
+ if (options.dryRun) console.log("setup dry-run 完成,未修改目标目录。");
162
+ return 0;
32
163
  }
33
164
 
34
- if (result?.signal) {
35
- console.error(`plan-governance-cli: 检查器被信号 ${result.signal} 终止`);
36
- process.exit(1);
165
+ function main() {
166
+ const args = process.argv.slice(2);
167
+ const command = args[0];
168
+ if (command === "setup") return setup(args.slice(1));
169
+ if (command === "init") return runPython(initializer, args.slice(1));
170
+ if (command === "hook") return runPython(hookRuntime, args.slice(1));
171
+ if (command === "check") return runPython(checker, args.slice(1));
172
+ return runPython(checker, args);
37
173
  }
38
174
 
39
- process.exit(result?.status ?? 1);
175
+ try {
176
+ process.exitCode = main();
177
+ } catch (error) {
178
+ process.exitCode = fail(error.message);
179
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plan-governance-cli",
3
- "version": "0.1.0",
3
+ "version": "0.2.0",
4
4
  "description": "统一入口的计划治理检查 CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -9,7 +9,11 @@
9
9
  },
10
10
  "files": [
11
11
  "bin/plan-governance-cli.mjs",
12
- "scripts/check_plan_governance.py"
12
+ "resources/manifest.json",
13
+ "resources/skill",
14
+ "scripts/check_plan_governance.py",
15
+ "scripts/init_plan_governance.py",
16
+ "scripts/plan_governance_hook.py"
13
17
  ],
14
18
  "engines": {
15
19
  "node": ">=20"
@@ -0,0 +1,24 @@
1
+ {
2
+ "manifestVersion": 1,
3
+ "skill": {
4
+ "targets": {
5
+ "codex": "~/.codex/skills/plan-governance",
6
+ "claude": "~/.claude/skills/plan-governance"
7
+ },
8
+ "files": [
9
+ "resources/skill/SKILL.md",
10
+ "resources/skill/agents/openai.yaml",
11
+ "resources/skill/assets/PLAN_MAP.template.md",
12
+ "resources/skill/assets/adr.template.md",
13
+ "resources/skill/assets/migration.template.md",
14
+ "resources/skill/assets/plan.template.md"
15
+ ]
16
+ },
17
+ "runtime": [
18
+ "scripts/check_plan_governance.py",
19
+ "scripts/init_plan_governance.py",
20
+ "scripts/plan_governance_hook.py"
21
+ ],
22
+ "hooks": []
23
+ }
24
+
@@ -0,0 +1,269 @@
1
+ ---
2
+ name: plan-governance
3
+ description: 轻量计划治理工作流。Use when 用户要求管理路线图、分阶段实施、迁移、架构变更、公共 API 或 Schema 变更、长期任务、计划漂移、ADR、Step 0 证据、PLAN_MAP.md、docs/plans 文件,或希望 Codex 协调多个相关计划,同时避免把普通小任务流程化。
4
+ ---
5
+
6
+ # Plan Governance
7
+
8
+ 使用这个 skill 管理跨阶段、会漂移、需要证据闭环的工作。普通一次性 bugfix、小范围本地修改、互不依赖的待办列表,不要启用治理,除非用户明确要求。
9
+
10
+ ## 启用判定
11
+
12
+ 开始前先给任务打分:
13
+
14
+ | 信号 | 分数 |
15
+ |---|---:|
16
+ | 跨阶段、里程碑或多轮会话 | 1 |
17
+ | 涉及架构、公共 API、Schema、兼容性或迁移行为 | 1 |
18
+ | 依赖真实运行报告、样本或反馈 | 1 |
19
+ | 多个计划之间存在依赖、替代、重叠或冲突 | 1 |
20
+ | 同一模块会被后续计划反复修改 | 1 |
21
+
22
+ - `>=3`:初始化或更新持续计划治理。
23
+ - `=2`:先检查已有治理文档,但不主动新建结构,除非确有必要。
24
+ - `<=1`:按普通任务处理。
25
+ - 大型公共契约迁移可以直接启用治理。
26
+
27
+ ## 仓库模型
28
+
29
+ 使用项目内这些文件作为事实来源:
30
+
31
+ ```text
32
+ docs/
33
+ PLAN_MAP.md
34
+ plans/
35
+ <plan-name>.md
36
+ adr/
37
+ 0001-<decision>.md
38
+ migrations/
39
+ <migration-name>.md
40
+ ```
41
+
42
+ 只创建有真实内容的文件。不要预建空的 `adr/` 或 `migrations/` 目录。
43
+
44
+ 文档权责:
45
+
46
+ - `PLAN_MAP.md`:计划索引、状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、当前阻塞项、证据链接。
47
+ - `docs/plans/*.md`:单个计划的目标、范围、阶段、当前阶段步骤、Step 0 证据、验证方式、完成条件、风险、回滚、未决问题。
48
+ - `docs/adr/*.md`:持久架构决策及其后果。
49
+ - `docs/migrations/*.md`:兼容策略、迁移步骤、回滚方式、旧行为保留窗口。
50
+ - spec 或 Schema:精确、可机器校验的契约。
51
+
52
+ 同一事实只定义一次,其他地方通过链接引用。
53
+
54
+ ## 多文档同步规则
55
+
56
+ - `docs/plans/*.md` 是专项计划的实施细节事实源,记录字段方案、Schema、枚举、Step 0 证据、验证方式和完成条件。
57
+ - `PLAN_MAP.md` 是状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接的事实源。
58
+ - 总路线图、优先级计划和索引只记录顺序、状态摘要和专项计划链接,不复制字段级方案、枚举、Step 0 细节或完成定义。
59
+ - 当专项计划的状态、当前阶段、最后更新、字段方案、完成条件或验证结果变化时,必须同步 `PLAN_MAP.md` 和所有引用该计划的路线图、优先级计划或索引。
60
+ - 验收治理文档时,必须用 `rg` 搜索同名计划、P 编号、状态名和关键字段,检查是否存在重复定义或漂移。
61
+ - 如果同一事实在多个文档中重复,保留一个事实源,其他文档改为链接引用。
62
+ - 实施者填写的计划状态、完成证据文字和文档格式只代表实施声明,不等于验收结论;验收者必须基于当前仓库内容、可复现验证命令和反向引用检查独立确认。
63
+
64
+ ## 草案和历史文档处理
65
+
66
+ 启用计划治理后,已有草案、历史设计、归档计划、superpowers 记录、临时分析文档等默认只作为背景材料,不再作为规范事实源。
67
+
68
+ - 除非用户明确点名要求更新这些文档,否则不要继续修改它们来承载新规范。
69
+ - 新发生的目标、范围、非目标、公共契约、字段、Schema、状态语义、阶段、验证方式、完成条件、风险和回滚,应写入 `docs/plans/*.md`、ADR、migration 或正式 spec。
70
+ - 计划状态、当前阶段、最后更新、依赖、替代/合并/废弃关系、推荐顺序、阻塞项和证据链接,应写入或同步 `docs/PLAN_MAP.md`。
71
+ - 历史草案可以在治理文档中作为背景材料链接引用,但不应承载新的规范事实。
72
+ - 如果治理文档出现“以草案为事实源”“详见草案中的字段定义”“草案为准”等表述,应改为由治理文档自身承载事实源,或链接到正式 spec/Schema。
73
+ - 当用户说“补充一下”“记录一下”“把刚才的结论写进去”时,如果治理已启用,默认写入治理文档;不要写入草案、README 或历史设计文档,除非用户明确点名该文件。
74
+
75
+ ## 推荐状态
76
+
77
+ 统一使用中文状态:
78
+
79
+ | 状态 | 含义 |
80
+ |---|---|
81
+ | `候选` | 记录了想法,但尚未承诺实施 |
82
+ | `设计中` | 正在明确范围、契约和门禁 |
83
+ | `待实施` | 当前阶段门禁已通过,但尚未开始 |
84
+ | `实施中` | 当前阶段正在修改代码或文档 |
85
+ | `已完成` | 实现、测试、证据和文档已同步 |
86
+ | `已替代` | 被另一个计划取代 |
87
+ | `已合并` | 并入另一个计划 |
88
+ | `已废弃` | 明确不再推进 |
89
+
90
+ ### 阶段转换规则
91
+
92
+ - 阶段 N 完成只关闭阶段 N,不自动改变阶段 N+1 的准入状态。
93
+ - 阶段 N+1 默认保持 `设计中`,直到完成该阶段自己的 Step 0、验证方式、完成条件和独立准入复核。
94
+ - 只有最新的独立准入复核明确写出“达到 `待实施` 标准”,阶段才可标记为 `待实施`。
95
+ - 不得以阶段 N 的完成证据、全量测试通过、治理脚本通过或实施者声明替代阶段 N+1 的 Step 0。
96
+ - `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;专项计划只承载该阶段的实施细节和准入证据。
97
+
98
+ ### `待实施` 最低准入条件
99
+
100
+ 当前阶段进入 `待实施` 前,必须同时具备:
101
+
102
+ 1. 当前阶段目标、范围和非目标。
103
+ 2. 明确基线类型的 Step 0 证据。
104
+ 3. 样本/fixture 矩阵,包含输入或基线、可执行命令、预期结果、失败判定和输出位置;不适合固定样本时必须记录可观察的替代基线。
105
+ 4. 验证方式、完成条件、失败策略和回滚或安全边界。
106
+ 5. 没有未解决的当前阶段阻塞项,且 `PLAN_MAP.md` 已同步。
107
+ 6. 最新独立准入复核明确为“通过”。
108
+
109
+ 阶段准入摘要应使用固定字段:`准入状态`、`Step 0`、`样本矩阵`、`验证方式`、`失败/回滚边界`、`当前阻塞项`、`最新独立准入复核`。阶段身份以 `PLAN_MAP.md` 为准,不在计划正文中另定义阶段编号。
110
+
111
+ ### 独立复核记录
112
+
113
+ 专项计划应保留追加式的独立复核记录,并显式维护最新结论的日期、阶段、结论、证据和复核者。历史记录不得覆盖;新的“未通过”覆盖此前的“通过”,修复后的新“通过”必须追加新记录。`PLAN_MAP.md` 只链接最新有效结论,不复制历史表格。
114
+
115
+ 阶段准入检查命令为 `--strict-readiness`。默认治理检查保持兼容,面向历史文档的准入缺陷先以 `WARNING` 提示;严格模式才将机械准入缺陷提升为 `ERROR`。该检查只判断结构化准入条件,不替代业务验收。
116
+
117
+ ## 工作流
118
+
119
+ 1. 检查仓库是否已有 `docs/PLAN_MAP.md` 和相关计划。
120
+ 2. 按启用判定决定是否需要治理。
121
+ 3. 如果需要初始化,优先运行源仓库脚本 `/Users/jafish/Documents/work/plan-governance/scripts/init_plan_governance.py` 生成最小结构;只有需要特殊改写时才手动复制 `assets/` 模板。已有项目不要重新初始化计划文档,使用 `--update-agent-rules-only` 或 `--upgrade-existing`。升级其他项目时必须从这个源仓库运行脚本,不要调用目标项目里的旧 `scripts/init_plan_governance.py`。旧项目若 `PLAN_MAP.md` 还是五列表,使用 `--migrate-plan-map-last-updated` 显式迁移。
122
+ 4. 初始化治理文档后,立即执行事实源切换:识别已有 README、草案、旧计划、归档设计文档,将它们视为背景材料或历史记录;从此刻起,新规范只写入 `docs/plans/*.md`、`PLAN_MAP.md`、ADR、migration 或正式 spec,除非用户明确要求,不再编辑旧草案。
123
+ 5. 实施前让当前阶段可执行:
124
+ - 目标、范围、非目标明确
125
+ - 必要的不变量和信任边界明确
126
+ - 必要的公共契约和失败策略明确
127
+ - Step 0 证据存在
128
+ - 验证方式可运行
129
+ - 当前阶段没有未解决阻塞项
130
+ 6. 只实施当前阶段。后续阶段保持粗粒度。
131
+ 7. 新信息改变假设时,先更新 `PLAN_MAP.md` 和相关计划,再继续实施;计划被推进、恢复、暂停或复核时同步 `最后更新`。
132
+ 8. 完成时记录验证证据、测试覆盖率证据并更新状态。
133
+ 9. 验收治理文档时先做独立复核和反向引用检查:不得仅依据计划状态、完成证据文字或文档格式判定完成;必须核对当前仓库内容、可复现验证命令、测试或 CI 输出、覆盖率证据,以及用 `rg` 搜索同名计划、P 编号、状态名、关键字段和 `草案为准|以草案为事实源|详见草案|draft is source|source of truth.*draft|以.*draft.*为准`,确认所有路线图、优先级计划和索引已经同步,且旧草案没有重新成为事实源。
134
+ 10. 运行项目内检查脚本;如果项目没有脚本或怀疑项目内脚本过旧,可使用源仓库脚本 `/Users/jafish/Documents/work/plan-governance/scripts/check_plan_governance.py`。需要检查活跃计划停滞时运行 `--stale-days`,默认 10 天。
135
+
136
+ ## Step 0 证据
137
+
138
+ 实施前必须先固定可观察基线:
139
+
140
+ | 任务类型 | Step 0 证据 |
141
+ |---|---|
142
+ | 缺陷修复 | 失败回归测试或最小复现 |
143
+ | 行为迁移 | 现状快照、兼容样本或契约测试 |
144
+ | 新项目 | 可执行验收 fixture、协议样本或提交图 |
145
+ | 架构探索 | 最小原型、风险实验或关键假设验证 |
146
+
147
+ 如果 Step 0 不现实,记录原因,并定义替代基线后再改实现。
148
+
149
+ ## 漂移检查
150
+
151
+ 当决策、实现发现或运行报告改变以下内容时,暂停实施并更新治理文档:
152
+
153
+ - 既有 ADR 或核心原则
154
+ - 计划顺序、依赖、替代关系或有效性
155
+ - 公共 API、Schema、状态语义、兼容承诺或失败模式
156
+ - 多个计划共享的重复修改点
157
+ - 用来证明完成的测试或完成条件
158
+ - 专项计划与路线图、优先级计划或索引之间的状态、字段方案、完成条件或验证结果
159
+
160
+ ## 停滞检查
161
+
162
+ `PLAN_MAP.md` 的计划索引应包含 `最后更新` 列,日期格式为 `YYYY-MM-DD`。
163
+
164
+ 推荐表头:
165
+
166
+ ```markdown
167
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
168
+ ```
169
+
170
+ 规则:
171
+
172
+ - `最后更新` 是显式治理元数据,不从 Git 历史或文件 mtime 推断。
173
+ - 当计划状态、当前阶段、阻塞项、依赖、完成条件、验证结果或实施范围变化时,同步 `最后更新`。
174
+ - `--stale-days` 只对 `候选`、`设计中`、`待实施`、`实施中` 等活跃计划输出 `WARNING`,不自动改变状态。
175
+ - `--stale-days` 省略数值时默认 10 天,可显式传入 N。
176
+
177
+ ## 初始化、模板和检查脚本
178
+
179
+ - `assets/PLAN_MAP.template.md`:治理入口模板。
180
+ - `assets/plan.template.md`:计划文档模板。
181
+ - `assets/adr.template.md`:可选 ADR 模板。
182
+ - `assets/migration.template.md`:可选迁移模板。
183
+ - `scripts/init_plan_governance.py`:初始化 Git 仓库、`docs/PLAN_MAP.md` 和首个 `docs/plans/<plan>.md`;也可升级已有项目的代理规则和检查脚本。
184
+ - `scripts/check_plan_governance.py`:检查状态、最后更新、计划链接、依赖、阻塞项、完成证据、测试覆盖率证据,并提供可选漂移、pre-commit 和停滞检查。
185
+ - `scripts/plan_governance_hook.py`:只读 hook runtime,可由项目级 Agent hooks 手动调用;只输出短提示和检查结果,不修改治理文档、不更新状态、不安装 hooks。
186
+
187
+ 初始化一个项目:
188
+
189
+ ```bash
190
+ plan-governance-cli init \
191
+ --root . \
192
+ --plan api-compat-migration \
193
+ --title "API 兼容性迁移" \
194
+ --goal "分阶段完成 API 兼容性迁移" \
195
+ --copy-checker \
196
+ --update-agent-rules
197
+ ```
198
+
199
+ 如果目标目录还不是 Git 仓库,初始化流程会先执行 `git init`;已有 `.git/` 时跳过。
200
+
201
+ 只更新已有项目的代理规则,不修改 `docs/`:
202
+
203
+ ```bash
204
+ plan-governance-cli init \
205
+ --root . \
206
+ --update-agent-rules-only
207
+ ```
208
+
209
+ 升级已有项目的辅助文件,刷新 `scripts/check_plan_governance.py`、`CLAUDE.md` 和 `AGENTS.md`,但不覆盖 `docs/`:
210
+
211
+ ```bash
212
+ plan-governance-cli init \
213
+ --root . \
214
+ --upgrade-existing
215
+ ```
216
+
217
+ 旧项目如果 `docs/PLAN_MAP.md` 仍是五列表,显式迁移为包含 `最后更新` 的六列表:
218
+
219
+ ```bash
220
+ plan-governance-cli init \
221
+ --root . \
222
+ --migrate-plan-map-last-updated \
223
+ --last-updated-date 2026-07-05
224
+ ```
225
+
226
+ 不传 `--last-updated-date` 时使用当天日期。该迁移只修改 `docs/PLAN_MAP.md` 的计划索引表,不自动改变计划状态。
227
+
228
+ 在仓库根目录运行:
229
+
230
+ ```bash
231
+ plan-governance-cli check .
232
+ ```
233
+
234
+ 可选检查:
235
+
236
+ ```bash
237
+ plan-governance-cli check . --drift
238
+ plan-governance-cli check . --pre-commit
239
+ plan-governance-cli check . --stale-days
240
+ plan-governance-cli check . --stale-days 10
241
+ ```
242
+
243
+ - `--drift`:检查工作区变更是否被活跃计划的 `影响模块或文件` 覆盖。
244
+ - `--pre-commit`:检查 staged 变更是否被活跃计划的 `影响模块或文件` 覆盖,可由用户手动接入 Git hook。
245
+ - `--stale-days`:检查活跃计划是否超过阈值未更新;默认 10 天。
246
+ - 这些可选检查输出 `WARNING`,不改变退出码。
247
+ - `--attest <plan-name>`:为已登记计划创建或覆盖 `docs/attestations/<plan-name>.json` 完成快照。
248
+ - `--check-attestations`:检查完成快照中的计划文件和 `PLAN_MAP.md` hash 是否漂移;漂移、缺失或 JSON 损坏只输出 `WARNING`,不改变退出码。
249
+
250
+ `影响模块或文件` 的作用域匹配规则:
251
+
252
+ - `--drift`、`--pre-commit` 和 `plan_governance_hook.py --event pre-write` 使用同一语义。
253
+ - 优先提取列表项中的第一个反引号路径,例如 ``- `./scripts/`: 检查脚本``。
254
+ - 没有反引号时提取第一个纯文本 token,例如 `- README.md`。
255
+ - 匹配前归一化前导 `./`、尾随 `/` 和重复斜杠。
256
+ - 支持文件精确匹配和目录前缀匹配。
257
+ - 不支持 glob、正则、否定规则或自然语言推断。
258
+
259
+ 只读 hook runtime 可手动接入项目级 Agent hooks:
260
+
261
+ ```bash
262
+ plan-governance-cli hook --event session-start
263
+ plan-governance-cli hook --event pre-write --paths scripts/check_plan_governance.py
264
+ plan-governance-cli hook --event post-write --paths docs/PLAN_MAP.md
265
+ plan-governance-cli hook --event stop
266
+ ```
267
+
268
+ 该脚本不自动安装 `.codex/hooks.json` 或修改全局配置。`stop` 事件只做非阻塞提示,不实现强制 gate。
269
+
@@ -0,0 +1,5 @@
1
+ interface:
2
+ display_name: "Plan Governance"
3
+ short_description: "用计划、证据和检查管理分阶段工作"
4
+ default_prompt: "使用 $plan-governance 管理这个跨阶段变更,维护计划文档、Step 0 证据和完成检查。"
5
+
@@ -0,0 +1,48 @@
1
+ # PLAN_MAP
2
+
3
+ ## 治理范围
4
+
5
+ 本文件只跟踪跨阶段、影响公共契约、依赖真实反馈,或会与其他计划发生关系的计划。普通一次性任务不要加入这里。
6
+
7
+ ## 文档权责
8
+
9
+ - `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针。
10
+ - 阶段 N 完成后,阶段 N+1 默认保持 `设计中`;专项计划自己的准入复核通过后才能进入 `待实施`。
11
+ - 阶段准入摘要、样本矩阵和独立复核记录只写入专项计划,不复制到本索引。
12
+
13
+ ## 计划索引
14
+
15
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
16
+ |---|---|---|---|---|---|
17
+ | [example-plan](plans/example-plan.md) | 候选 | 阶段 0 | <YYYY-MM-DD> | - | - |
18
+
19
+ 允许状态:`候选`、`设计中`、`待实施`、`实施中`、`已完成`、`已替代`、`已合并`、`已废弃`。
20
+
21
+ ## 推荐顺序
22
+
23
+ 1. `example-plan`
24
+
25
+ ## 依赖关系
26
+
27
+ | 计划 | 依赖 | 原因 |
28
+ |---|---|---|
29
+ | example-plan | - | - |
30
+
31
+ ## 替代、合并和废弃
32
+
33
+ | 计划 | 关系 | 目标 | 原因 |
34
+ |---|---|---|---|
35
+ | - | - | - | - |
36
+
37
+ ## 当前阻塞项
38
+
39
+ | 问题 | 推荐方案 | 影响范围 | 是否阻塞当前阶段 | 状态 |
40
+ |---|---|---|---|---|
41
+ | - | - | - | 否 | 已延后 |
42
+
43
+ ## 完成证据
44
+
45
+ | 计划 | 阶段 | 证据 |
46
+ |---|---|---|
47
+ | - | - | - |
48
+
@@ -0,0 +1,18 @@
1
+ # ADR <number>:<decision>
2
+
3
+ ## 状态
4
+
5
+ 提议中
6
+
7
+ ## 背景
8
+
9
+ ## 决策
10
+
11
+ ## 备选方案
12
+
13
+ ## 后果
14
+
15
+ ## 关联计划
16
+
17
+ -
18
+
@@ -0,0 +1,20 @@
1
+ # 迁移:<name>
2
+
3
+ ## 背景
4
+
5
+ ## 兼容策略
6
+
7
+ ## 迁移步骤
8
+
9
+ 1.
10
+
11
+ ## 回滚方式
12
+
13
+ ## 旧行为保留窗口
14
+
15
+ ## 验证方式
16
+
17
+ ## 关联计划或 ADR
18
+
19
+ -
20
+