plan-governance-cli 0.3.2 → 0.3.4

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
@@ -188,36 +188,14 @@ plan-governance-cli graph code impact --repo modelpad --file Sources/ModelPadCor
188
188
 
189
189
  `plan impact` 是计划治理的前置只读查询:输入文件声明 `graph_scope`、`change_kind`,默认只查询功能层;API 契约、数据、安全、外部边界或明确代码定位才升级到架构层或代码候选。输出查询层级、升级原因、行动分级、测试映射和未查询层级;失败不伪装成“无影响”,也不会触发 `gitnexus analyze`。
190
190
 
191
- ## 自主连续执行(可选)
191
+ ## 持续推进
192
192
 
193
- 适合目标明确、步骤边界清楚、希望执行者连续推进的计划。用户可以直接说“推进到完成计划”,执行者不必每一步请求继续,但必须逐项执行、记录证据、运行验证,并在失败、阶段门、独立复核或其他治理边界处暂停。
194
-
195
- 自主模式默认关闭。`plan-governance-cli init` 生成的计划仍按普通计划运行;只有在计划当前阶段明确启用以下结构后,才会进入步骤校验和下一步骤查询:
196
-
197
- ```markdown
198
- execution_mode: autonomous-continuous
199
- execution_policy: serial
200
-
201
- ### 执行清单
202
-
203
- | 步骤 ID | 前置步骤 | 动作 | 证据 | 完成条件 | 状态 | 分支记录 |
204
- |---|---|---|---|---|---|---|
205
- | `S1` | `-` | 建立基线 | `tests/baseline.log` | 基线命令通过 | `未开始` | `-` |
206
- ```
207
-
208
- 启用后建议先运行:
209
-
210
- ```bash
211
- plan-governance-cli plan steps validate <plan> --json
212
- plan-governance-cli plan next <plan> --json
213
- ```
214
-
215
- `plan next` 只读返回 `ready`、`blocked`、`phase_gate`、`not_enabled` 或 `complete`。它不会执行 shell、修改计划或状态、自动批准独立复核,也不会把 `complete` 当作验收通过;`blocked`、`phase_gate`、`complete` 和 `not_enabled` 都是停止或等待信号。
193
+ 在 Codex 中需要跨轮持续推进时使用 `goal`。`plan-governance` skill 和 CLI 只负责计划、证据、阶段准入与检查,不提供自主连续执行模式或步骤级下一步查询。
216
194
 
217
195
  也可以不全局安装,直接使用锁定版本:
218
196
 
219
197
  ```bash
220
- npx --yes --package plan-governance-cli@0.3.1 plan-governance-cli check . --strict-readiness
198
+ npx --yes --package plan-governance-cli@0.3.3 plan-governance-cli check . --strict-readiness
221
199
  ```
222
200
 
223
201
  npm 包内部仍使用版本化的 Python 检查器,但用户不需要直接调用或复制该脚本。旧项目已有本地脚本时仍可保留作为兼容或回滚路径。
@@ -189,11 +189,11 @@ function main() {
189
189
  if (command === "init") return runPython(initializer, args.slice(1));
190
190
  if (command === "hook") return runPython(hookRuntime, args.slice(1));
191
191
  if (command === "graph") return runNode(graphRuntime, args.slice(1));
192
- if (command === "plan" && args[1] === "steps" && args[2] === "validate") {
193
- return runPython(checker, ["--validate-steps", ...args.slice(3)]);
194
- }
195
192
  if (command === "plan" && args[1] === "next") {
196
- return runPython(checker, ["--next-plan", args[2], ...args.slice(3)]);
193
+ return fail("plan next 已移除;在 Codex 中请使用 goal 管理跨轮持续推进。");
194
+ }
195
+ if (command === "plan" && args[1] === "steps" && args[2] === "validate") {
196
+ return fail("自主执行步骤校验已移除;在 Codex 中请使用 goal 管理跨轮持续推进。");
197
197
  }
198
198
  if (command === "plan") return runNode(graphRuntime, args);
199
199
  if (command === "check") return runPython(checker, args.slice(1));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "plan-governance-cli",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
4
4
  "description": "统一入口的计划治理检查 CLI",
5
5
  "private": false,
6
6
  "type": "module",
@@ -23,7 +23,8 @@
23
23
  "access": "public"
24
24
  },
25
25
  "scripts": {
26
- "test": "node --test tests/npm_cli.test.mjs tests/graph_cli.test.mjs tests/architecture_graph_cli.test.mjs tests/plan_impact_step0.test.mjs tests/plan_impact_cli.test.mjs"
26
+ "test": "node --test tests/npm_cli.test.mjs tests/graph_cli.test.mjs tests/architecture_graph_cli.test.mjs tests/plan_impact_step0.test.mjs tests/plan_impact_cli.test.mjs",
27
+ "release:npm": "node scripts/release_npm.mjs"
27
28
  },
28
29
  "dependencies": {
29
30
  "yaml": "^2.9.0"
@@ -7,6 +7,8 @@ description: 轻量计划治理工作流。Use when 用户要求管理路线图
7
7
 
8
8
  使用这个 skill 管理跨阶段、会漂移、需要证据闭环的工作。普通一次性 bugfix、小范围本地修改、互不依赖的待办列表,不要启用治理,除非用户明确要求。
9
9
 
10
+ 跨轮持续推进由 Codex 的 `goal` 机制管理;本 skill 不定义自主连续执行模式、执行清单或下一步骤查询。
11
+
10
12
  ## 启用判定
11
13
 
12
14
  开始前先给任务打分:
@@ -127,24 +129,6 @@ docs/
127
129
 
128
130
  阶段准入检查命令为 `--strict-readiness`。默认治理检查保持兼容,面向历史文档的准入缺陷先以 `WARNING` 提示;严格模式才将机械准入缺陷提升为 `ERROR`。该检查只判断结构化准入条件,不替代业务验收。
129
131
 
130
- ## 自主连续执行
131
-
132
- 当用户明确说“按计划自主推进至完成”“推进到完成计划”或“自主执行且不跳步”时,将其视为当前计划的持续执行指令:无需逐步请求用户继续,但必须从当前阶段的第一项开始,按计划顺序实际执行并记录每一项的证据。
133
-
134
- - 不得跳过、合并或仅凭结果声明 Step 0、实施步骤、样本验证、独立复核、状态同步、完成证据或反向引用检查已经完成。
135
- - 阶段 N 完成仍不自动准入阶段 N+1。只有阶段 N+1 已完成自己的准入条件,且最新独立准入复核明确通过,才可继续执行该阶段。
136
- - 计划明确定义“不适用”或失败分支时,必须执行该分支并记录原因和证据;不得静默跳过。
137
- - 目标、范围、公共契约、兼容策略、失败/回滚边界或计划顺序发生实质变化时,先更新治理文档;需要新的用户取舍、外部授权或超出既定范围的操作时暂停并请求方向。
138
- - 验证失败时应在既定范围内诊断和修复;无法按计划解决、当前阶段被阻塞或后续阶段尚未准入时暂停,并一次性报告已执行步骤、证据、阻塞原因和推荐下一步。
139
- - 计划完成后统一报告变更摘要、逐项验证结果、计划地图同步情况、独立验收结论和剩余风险;不需要为了例行步骤逐条汇报。
140
-
141
- ### 模板与只读查询协同
142
-
143
- - 自主模式是计划级可选项。只有计划正文明确写出 `execution_mode: autonomous-continuous`,并提供七列表 `执行清单`,才使用步骤级解析;普通计划保持兼容,不要求迁移。
144
- - 对已启用计划,开始当前步骤前先运行 `plan-governance-cli plan steps validate <plan> --json` 和 `plan-governance-cli plan next <plan> --json`;只有 `ready_steps` 中的步骤才可推进。
145
- - 对 `blocked`、`phase_gate`、`complete` 或 `not_enabled`,按输出原因停止当前连续推进;这些状态都不代表自动执行、自动验收或下一阶段已获准入。
146
- - 模板中的自主示例默认位于代码围栏内,不会自动启用;复制并去掉围栏后,必须补齐真实动作、证据、完成条件、状态和分支记录。
147
-
148
132
  ## 工作流
149
133
 
150
134
  1. 检查仓库是否已有 `docs/PLAN_MAP.md` 和相关计划。
@@ -193,6 +177,14 @@ docs/
193
177
 
194
178
  `PLAN_MAP.md` 的计划索引应包含 `最后更新` 列,日期格式为 `YYYY-MM-DD`。
195
179
 
180
+ 计划索引必须在同一个 `## 计划索引` 章节下,按固定顺序拆成三张六列表格:`### 未完成`、`### 已完成`、`### 已废弃`。状态归类规则如下:
181
+
182
+ - `未完成`:`候选`、`设计中`、`待实施`、`实施中`。
183
+ - `已完成`:`已完成`。
184
+ - `已废弃`:`已废弃`、`已替代`、`已合并`等不再推进的终态;表格标题不改变行内真实状态。
185
+
186
+ 每个计划只能出现一次;没有对应计划的分类保留表头但不添加占位行。必须使用三级标题,以便治理 CLI 将三张表作为同一个计划索引读取。
187
+
196
188
  推荐表头:
197
189
 
198
190
  ```markdown
@@ -1,4 +1,4 @@
1
1
  interface:
2
2
  display_name: "Plan Governance"
3
3
  short_description: "用计划、证据和检查管理分阶段工作"
4
- default_prompt: "使用 $plan-governance 管理这个跨阶段变更,维护计划文档、Step 0 证据和完成检查;如果用户要求推进到完成,逐项执行计划步骤,不跳步,仅在异常或治理门禁处暂停。"
4
+ default_prompt: "使用 $plan-governance 管理这个跨阶段变更,维护计划文档、Step 0 证据和完成检查。"
@@ -11,13 +11,26 @@
11
11
  - 阶段准入摘要、样本矩阵和独立复核记录只写入专项计划,不复制到本索引。
12
12
  - 专项计划可在当前阶段声明阶段证据和追加式实施/验证记录;完成快照默认保持旧 JSON 兼容格式,发布/合规场景再显式使用 purpose 关系。
13
13
  - `--drift`/`--pre-commit` 的关闭窗口只在已完成计划自身也发生变更时覆盖该计划自身和显式阶段证据,不复用已完成计划的完整影响范围。
14
+ - 计划索引固定分为 `未完成`、`已完成`、`已废弃` 三张表;`已替代`、`已合并`等不再推进的终态归入 `已废弃` 表,但保留真实状态值。
14
15
 
15
16
  ## 计划索引
16
17
 
18
+ ### 未完成
19
+
17
20
  | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
18
21
  |---|---|---|---|---|---|
19
22
  | [example-plan](plans/example-plan.md) | 候选 | 阶段 0 | <YYYY-MM-DD> | - | - |
20
23
 
24
+ ### 已完成
25
+
26
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
27
+ |---|---|---|---|---|---|
28
+
29
+ ### 已废弃
30
+
31
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
32
+ |---|---|---|---|---|---|
33
+
21
34
  允许状态:`候选`、`设计中`、`待实施`、`实施中`、`已完成`、`已替代`、`已合并`、`已废弃`。
22
35
 
23
36
  ## 推荐顺序
@@ -12,23 +12,6 @@
12
12
 
13
13
  - 待补充。
14
14
 
15
- ## 自主连续执行(可选)
16
-
17
- 默认不启用自主连续执行。只有当计划明确选择“推进到完成计划”并且每一步都需要真实执行时,才复制下面的示例到当前阶段,去掉代码围栏并替换所有占位内容。未启用时,旧计划和普通计划继续使用原有流程,不需要补填步骤表。
18
-
19
- 只读查询入口为 `plan-governance-cli plan steps validate <plan> --json` 和 `plan-governance-cli plan next <plan> --json`;查询不会执行动作、写回状态或替代独立验收。
20
-
21
- ```markdown
22
- execution_mode: autonomous-continuous
23
- execution_policy: serial
24
-
25
- ### 执行清单
26
-
27
- | 步骤 ID | 前置步骤 | 动作 | 证据 | 完成条件 | 状态 | 分支记录 |
28
- |---|---|---|---|---|---|---|
29
- | `S1` | `-` | `<动作>` | `<可复现证据定位>` | `<可验证完成条件>` | `未开始` | `-` |
30
- ```
31
-
32
15
  <!-- 仅在触发 grilling 且用户确认结构化结论后保留本节;否则删除本节和本注释。 -->
33
16
  ## 需求探索
34
17
 
@@ -1405,170 +1405,6 @@ def print_workset_text(payload, strict=False):
1405
1405
  print(f"{'ERROR' if strict else 'WARNING'}: {warning}")
1406
1406
 
1407
1407
 
1408
- STEP_COLUMNS = ["步骤 ID", "前置步骤", "动作", "证据", "完成条件", "状态", "分支记录"]
1409
- STEP_STATES = {"未开始", "执行中", "已完成", "阻塞", "不适用"}
1410
- CONSTRAINT_COLUMNS = ["约束 ID", "类型", "步骤", "共享目标", "建议顺序", "说明"]
1411
-
1412
-
1413
- def split_prerequisites(value):
1414
- if not value or value.strip("` -") == "":
1415
- return []
1416
- return [item for item in re.split(r"[,,、\s]+", value.strip("` ")) if item and item != "-"]
1417
-
1418
-
1419
- def branch_record_has_value(branch_record, marker):
1420
- match = re.search(rf"{re.escape(marker)}\s*[::]\s*([^;;,,\n]+)", branch_record)
1421
- return bool(match and not is_placeholder(match.group(1).strip()))
1422
-
1423
-
1424
- def find_execution_rows(plan_text):
1425
- section = markdown_section(plan_text, ["执行清单"])
1426
- if section is None:
1427
- return None, []
1428
- rows = markdown_table_rows(section)
1429
- header_index = next((index for index, row in enumerate(rows) if row[: len(STEP_COLUMNS)] == STEP_COLUMNS), None)
1430
- if header_index is None:
1431
- return [], []
1432
- return STEP_COLUMNS, rows[header_index + 1 :]
1433
-
1434
-
1435
- def validate_step_plan(root, plan_name, strict=False):
1436
- errors = []
1437
- _, _, plans = load_plan_records(root, errors)
1438
- if errors:
1439
- payload = {
1440
- "schema_version": 1,
1441
- "plan": plan_name,
1442
- "phase": "",
1443
- "enabled": False,
1444
- "status": "invalid",
1445
- "steps": [],
1446
- "errors": errors,
1447
- "warnings": [],
1448
- }
1449
- return payload, 1
1450
-
1451
- data = plans.get(plan_name)
1452
- if data is None or not data["path"].exists():
1453
- payload = {
1454
- "schema_version": 1,
1455
- "plan": plan_name,
1456
- "phase": data["phase"] if data else "",
1457
- "enabled": False,
1458
- "status": "invalid",
1459
- "steps": [],
1460
- "errors": [f"{plan_name}: 未登记计划或计划文件不存在"],
1461
- "warnings": [],
1462
- }
1463
- return payload, 1
1464
-
1465
- plan_errors = []
1466
- plan_text = read_utf8(data["path"], plan_errors)
1467
- if plan_errors:
1468
- return {
1469
- "schema_version": 1,
1470
- "plan": plan_name,
1471
- "phase": data["phase"],
1472
- "enabled": False,
1473
- "status": "invalid",
1474
- "steps": [],
1475
- "errors": plan_errors,
1476
- "warnings": [],
1477
- }, 1
1478
- parse_text = mask_fenced_code(plan_text)
1479
- if not re.search(r"^\s*execution_mode\s*:\s*autonomous-continuous\s*$", parse_text, re.MULTILINE):
1480
- return {
1481
- "schema_version": 1,
1482
- "plan": plan_name,
1483
- "phase": data["phase"],
1484
- "enabled": False,
1485
- "status": "not_enabled",
1486
- "steps": [],
1487
- "errors": [],
1488
- "warnings": [],
1489
- }, 0
1490
-
1491
- errors = []
1492
- policy_match = re.search(r"^\s*execution_policy\s*:\s*(\S+)\s*$", parse_text, re.MULTILINE)
1493
- policy = policy_match.group(1).strip("`") if policy_match else "serial"
1494
- if policy not in {"serial", "parallel"}:
1495
- errors.append(f"{plan_name}: 字段 execution_policy 值非法:{policy}")
1496
-
1497
- headers, rows = find_execution_rows(plan_text)
1498
- if headers is None:
1499
- errors.append(f"{plan_name}: 缺少 `### 执行清单` 章节")
1500
- rows = []
1501
- elif not headers:
1502
- errors.append(f"{plan_name}: 执行清单缺少固定七列表头")
1503
- elif not rows:
1504
- errors.append(f"{plan_name}: 执行清单至少需要一个步骤")
1505
-
1506
- steps = []
1507
- ids = set()
1508
- dependencies = {}
1509
- for row_index, row in enumerate(rows, start=1):
1510
- if len(row) < len(STEP_COLUMNS):
1511
- errors.append(f"{plan_name}: 执行清单第 {row_index} 行字段不足")
1512
- continue
1513
- step = dict(zip(STEP_COLUMNS, row[: len(STEP_COLUMNS)]))
1514
- step_id = step["步骤 ID"].strip("` ")
1515
- if not step_id:
1516
- errors.append(f"{plan_name}: 执行清单第 {row_index} 行缺少步骤 ID")
1517
- continue
1518
- if step_id in ids:
1519
- errors.append(f"{plan_name}: 步骤 {step_id} 重复")
1520
- ids.add(step_id)
1521
- dependencies[step_id] = split_prerequisites(step["前置步骤"])
1522
- for field in ["动作", "证据", "完成条件"]:
1523
- if not step[field].strip("` ") or is_placeholder(step[field]):
1524
- errors.append(f"{plan_name}: 步骤 {step_id} 缺少字段 {field}")
1525
- state = step["状态"].strip("` ")
1526
- if state not in STEP_STATES:
1527
- errors.append(f"{plan_name}: 步骤 {step_id} 状态非法:{state}")
1528
- if state == "不适用":
1529
- branch_record = step["分支记录"].strip("` ")
1530
- if is_placeholder(branch_record):
1531
- errors.append(f"{plan_name}: 步骤 {step_id} 为不适用但缺少分支记录")
1532
- else:
1533
- for marker in ["触发条件", "理由", "替代证据"]:
1534
- if not branch_record_has_value(branch_record, marker):
1535
- errors.append(f"{plan_name}: 步骤 {step_id} 的不适用分支记录缺少有效{marker}")
1536
- if state in {"已完成", "不适用"} and is_placeholder(step["证据"]):
1537
- errors.append(f"{plan_name}: 步骤 {step_id} 为 {state} 但缺少证据")
1538
- steps.append(
1539
- {
1540
- "id": step_id,
1541
- "preconditions": dependencies[step_id],
1542
- "action": step["动作"],
1543
- "evidence": step["证据"],
1544
- "completion": step["完成条件"],
1545
- "state": state,
1546
- "branch_record": step["分支记录"],
1547
- }
1548
- )
1549
-
1550
- for step_id, prerequisites in dependencies.items():
1551
- for prerequisite in prerequisites:
1552
- if prerequisite not in ids:
1553
- errors.append(f"{plan_name}: 步骤 {step_id} 引用了未知前置步骤 {prerequisite}")
1554
-
1555
- graph = {step_id: [item for item in prerequisites if item in ids] for step_id, prerequisites in dependencies.items()}
1556
- errors.extend(f"{plan_name}: 执行清单存在环依赖:{cycle}" for cycle in detect_dependency_cycles(graph))
1557
- status = "invalid" if errors else "valid"
1558
- warnings = list(errors) if errors and not strict else []
1559
- return {
1560
- "schema_version": 1,
1561
- "plan": plan_name,
1562
- "phase": data["phase"],
1563
- "enabled": True,
1564
- "status": status,
1565
- "execution_policy": policy,
1566
- "steps": steps if not errors else [],
1567
- "errors": errors,
1568
- "warnings": warnings,
1569
- }, 1 if errors and strict else 0
1570
-
1571
-
1572
1408
  def run_workset(root, as_json=False, include_history=False, strict=False):
1573
1409
  payload, status = workset_payload(root, include_history=include_history, strict=strict)
1574
1410
  if as_json:
@@ -1578,337 +1414,13 @@ def run_workset(root, as_json=False, include_history=False, strict=False):
1578
1414
  return status
1579
1415
 
1580
1416
 
1581
- def run_validate_steps(root, plan_name, as_json=False, strict=False):
1582
- payload, status = validate_step_plan(root, plan_name, strict=strict)
1583
- if as_json:
1584
- print(json.dumps(payload, ensure_ascii=False, indent=2))
1585
- else:
1586
- print(f"步骤校验:{payload['plan']} | {payload['phase']} | {payload['status']}")
1587
- for error in payload["errors"]:
1588
- print(f"{'ERROR' if strict else 'WARNING'}: {error}")
1589
- for warning in payload["warnings"]:
1590
- if warning not in payload["errors"]:
1591
- print(f"WARNING: {warning}")
1592
- return status
1593
-
1594
-
1595
- def split_constraint_values(value):
1596
- if not value or value.strip("` -") == "":
1597
- return []
1598
- return [
1599
- item.strip("` ")
1600
- for item in re.split(r"\s*->\s*|[,,、\s]+", value.strip("` "))
1601
- if item and item != "-"
1602
- ]
1603
-
1604
-
1605
- def parse_execution_constraints(plan_text, step_ids):
1606
- section = markdown_section(plan_text, ["执行约束"])
1607
- if section is None:
1608
- return [], []
1609
- rows = markdown_table_rows(section)
1610
- header_index = next(
1611
- (index for index, row in enumerate(rows) if row[: len(CONSTRAINT_COLUMNS)] == CONSTRAINT_COLUMNS),
1612
- None,
1613
- )
1614
- if header_index is None:
1615
- return [], ["执行约束缺少固定六列表头"]
1616
-
1617
- constraints = []
1618
- errors = []
1619
- seen = set()
1620
- for row_index, row in enumerate(rows[header_index + 1 :], start=1):
1621
- if len(row) < len(CONSTRAINT_COLUMNS):
1622
- errors.append(f"执行约束第 {row_index} 行字段不足")
1623
- continue
1624
- constraint = dict(zip(CONSTRAINT_COLUMNS, row[: len(CONSTRAINT_COLUMNS)]))
1625
- constraint_id = constraint["约束 ID"].strip("` ")
1626
- kind = constraint["类型"].strip("` ")
1627
- step_values = split_constraint_values(constraint["步骤"])
1628
- targets = split_constraint_values(constraint["共享目标"])
1629
- recommended_order = split_constraint_values(constraint["建议顺序"])
1630
- if not constraint_id or constraint_id in seen:
1631
- errors.append(f"执行约束第 {row_index} 行约束 ID 缺失或重复:{constraint_id or '-'}")
1632
- seen.add(constraint_id)
1633
- if kind != "shared_write_risk":
1634
- errors.append(f"执行约束 {constraint_id or row_index} 类型非法:{kind}")
1635
- if not step_values:
1636
- errors.append(f"执行约束 {constraint_id or row_index} 缺少步骤")
1637
- unknown_steps = [item for item in step_values if item not in step_ids]
1638
- if unknown_steps:
1639
- errors.append(
1640
- f"执行约束 {constraint_id or row_index} 引用了未知步骤:{', '.join(unknown_steps)}"
1641
- )
1642
- if not targets:
1643
- errors.append(f"执行约束 {constraint_id or row_index} 缺少共享目标")
1644
- if recommended_order and any(item not in step_values for item in recommended_order):
1645
- errors.append(f"执行约束 {constraint_id or row_index} 建议顺序引用了约束外步骤")
1646
- constraints.append(
1647
- {
1648
- "id": constraint_id,
1649
- "kind": kind,
1650
- "step_ids": step_values,
1651
- "targets": targets,
1652
- "recommended_order": recommended_order,
1653
- "description": constraint["说明"],
1654
- }
1655
- )
1656
- return constraints, errors
1657
-
1658
-
1659
- def phase_gate_is_blocked(plan_text, current_phase):
1660
- roadmap = phase_roadmap_rows(plan_text)
1661
- current_index = next(
1662
- (index for index, row in enumerate(roadmap) if row and row[0] == current_phase),
1663
- None,
1664
- )
1665
- if current_index is None:
1666
- return False
1667
- current_row = roadmap[current_index]
1668
- current_status = current_row[4].strip() if len(current_row) > 4 else ""
1669
- if current_status != "已完成":
1670
- return False
1671
- following = roadmap[current_index + 1 :]
1672
- if not following:
1673
- return False
1674
- next_status = following[0][4].strip() if len(following[0]) > 4 else ""
1675
- return next_status not in {"待实施", "实施中", "已完成"}
1676
-
1677
-
1678
- def plan_next_payload(root, plan_name):
1679
- errors = []
1680
- _, _, plans = load_plan_records(root, errors)
1681
- data = plans.get(plan_name)
1682
- if errors or data is None or not data["path"].exists():
1683
- return {
1684
- "schema_version": 1,
1685
- "plan": plan_name,
1686
- "phase": data["phase"] if data else "",
1687
- "execution_mode": None,
1688
- "status": "blocked",
1689
- "ready_steps": [],
1690
- "blocked_steps": [
1691
- {
1692
- "id": "__plan__",
1693
- "reasons": [{"kind": "plan_not_found", "detail": f"未登记计划或计划文件不存在:{plan_name}"}],
1694
- }
1695
- ],
1696
- "constraints": [],
1697
- "next_action": {"kind": "resolve_blocked", "reason": "plan_not_found"},
1698
- }, 2
1699
-
1700
- plan_errors = []
1701
- plan_text = read_utf8(data["path"], plan_errors)
1702
- if plan_errors:
1703
- return {
1704
- "schema_version": 1,
1705
- "plan": plan_name,
1706
- "phase": data["phase"],
1707
- "execution_mode": None,
1708
- "status": "blocked",
1709
- "ready_steps": [],
1710
- "blocked_steps": [
1711
- {"id": "__plan__", "reasons": [{"kind": "invalid_structure", "detail": item} for item in plan_errors]},
1712
- ],
1713
- "constraints": [],
1714
- "next_action": {"kind": "resolve_blocked", "reason": "invalid_structure"},
1715
- }, 1
1716
-
1717
- validated, validation_status = validate_step_plan(root, plan_name, strict=True)
1718
- if validated["status"] == "not_enabled":
1719
- return {
1720
- "schema_version": 1,
1721
- "plan": plan_name,
1722
- "phase": data["phase"],
1723
- "execution_mode": None,
1724
- "status": "not_enabled",
1725
- "ready_steps": [],
1726
- "blocked_steps": [],
1727
- "constraints": [],
1728
- "next_action": {"kind": "none", "reason": "plan_not_enabled"},
1729
- }, 0
1730
-
1731
- if validated["status"] != "valid":
1732
- evidence_errors = [
1733
- error
1734
- for error in validated.get("errors", [])
1735
- if "缺少证据" in error or "缺少字段 证据" in error
1736
- ]
1737
- other_errors = [
1738
- error
1739
- for error in validated.get("errors", [])
1740
- if "缺少证据" not in error and "缺少字段 证据" not in error
1741
- ]
1742
- if evidence_errors and not other_errors:
1743
- blocked_steps = []
1744
- for error in evidence_errors:
1745
- match = re.search(r"步骤\s+([^: ]+)", error)
1746
- step_id = match.group(1).strip("` ") if match else "__plan__"
1747
- blocked_steps.append(
1748
- {
1749
- "id": step_id,
1750
- "reasons": [{"kind": "missing_evidence", "detail": error}],
1751
- }
1752
- )
1753
- return {
1754
- "schema_version": 1,
1755
- "plan": plan_name,
1756
- "phase": data["phase"],
1757
- "execution_mode": "autonomous-continuous",
1758
- "execution_policy": validated.get("execution_policy", "serial"),
1759
- "status": "blocked",
1760
- "ready_steps": [],
1761
- "blocked_steps": blocked_steps,
1762
- "constraints": [],
1763
- "next_action": {"kind": "resolve_blocked", "reason": "missing_evidence"},
1764
- }, 0
1765
- reasons = [
1766
- {"kind": "invalid_structure", "detail": error}
1767
- for error in validated.get("errors", [])
1768
- ] or [{"kind": "invalid_structure", "detail": "步骤表无法安全解析"}]
1769
- return {
1770
- "schema_version": 1,
1771
- "plan": plan_name,
1772
- "phase": data["phase"],
1773
- "execution_mode": "autonomous-continuous",
1774
- "execution_policy": validated.get("execution_policy", "serial"),
1775
- "status": "blocked",
1776
- "ready_steps": [],
1777
- "blocked_steps": [{"id": "__plan__", "reasons": reasons}],
1778
- "constraints": [],
1779
- "next_action": {"kind": "resolve_blocked", "reason": "invalid_structure"},
1780
- }, 1
1781
-
1782
- steps = validated["steps"]
1783
- step_ids = {step["id"] for step in steps}
1784
- constraints, constraint_errors = parse_execution_constraints(plan_text, step_ids)
1785
- if constraint_errors:
1786
- return {
1787
- "schema_version": 1,
1788
- "plan": plan_name,
1789
- "phase": data["phase"],
1790
- "execution_mode": "autonomous-continuous",
1791
- "execution_policy": validated.get("execution_policy", "serial"),
1792
- "status": "blocked",
1793
- "ready_steps": [],
1794
- "blocked_steps": [
1795
- {
1796
- "id": "__plan__",
1797
- "reasons": [{"kind": "invalid_structure", "detail": error} for error in constraint_errors],
1798
- }
1799
- ],
1800
- "constraints": constraints,
1801
- "next_action": {"kind": "resolve_blocked", "reason": "invalid_structure"},
1802
- }, 1
1803
-
1804
- terminal_states = {"已完成", "不适用"}
1805
- terminal_ids = {step["id"] for step in steps if step["state"] in terminal_states}
1806
- ready = []
1807
- blocked = []
1808
- for step in steps:
1809
- if step["state"] in terminal_states:
1810
- continue
1811
- if step["state"] == "阻塞":
1812
- blocked.append(
1813
- {"id": step["id"], "reasons": [{"kind": "failed_step", "detail": "步骤处于阻塞状态"}]}
1814
- )
1815
- continue
1816
- if step["state"] == "执行中":
1817
- blocked.append(
1818
- {"id": step["id"], "reasons": [{"kind": "step_in_progress", "detail": "步骤已处于执行中"}]}
1819
- )
1820
- continue
1821
- missing = [item for item in step["preconditions"] if item not in terminal_ids]
1822
- if missing:
1823
- blocked.append(
1824
- {
1825
- "id": step["id"],
1826
- "reasons": [
1827
- {
1828
- "kind": "missing_predecessor",
1829
- "step_ids": missing,
1830
- }
1831
- ],
1832
- }
1833
- )
1834
- continue
1835
- ready.append(
1836
- {
1837
- "id": step["id"],
1838
- "reason": {"kind": "dependencies_satisfied"},
1839
- }
1840
- )
1841
-
1842
- policy = validated.get("execution_policy", "serial")
1843
- if policy == "serial" and ready:
1844
- ready = ready[:1]
1845
-
1846
- all_terminal = all(step["state"] in terminal_states for step in steps)
1847
- if ready:
1848
- status = "ready"
1849
- action_kind = "review_shared_write_order" if constraints else "run_ready_steps"
1850
- action_reason = "shared_write_risk_present" if constraints else "steps_ready"
1851
- exit_status = 0
1852
- elif all_terminal and phase_gate_is_blocked(plan_text, data["phase"]):
1853
- status = "phase_gate"
1854
- action_kind = "await_phase_gate"
1855
- action_reason = "next_phase_not_admitted"
1856
- exit_status = 0
1857
- elif all_terminal:
1858
- status = "complete"
1859
- action_kind = "await_independent_acceptance"
1860
- action_reason = "implementation_complete_acceptance_pending"
1861
- exit_status = 0
1862
- else:
1863
- status = "blocked"
1864
- action_kind = "resolve_blocked"
1865
- action_reason = blocked[0]["reasons"][0]["kind"] if blocked else "no_ready_steps"
1866
- exit_status = 0
1867
-
1868
- return {
1869
- "schema_version": 1,
1870
- "plan": plan_name,
1871
- "phase": data["phase"],
1872
- "execution_mode": "autonomous-continuous",
1873
- "execution_policy": policy,
1874
- "status": status,
1875
- "ready_steps": ready,
1876
- "blocked_steps": blocked,
1877
- "constraints": constraints,
1878
- "next_action": {"kind": action_kind, "reason": action_reason},
1879
- }, exit_status
1880
-
1881
-
1882
- def print_plan_next_text(payload):
1883
- print(f"下一步骤:{payload['plan']} | {payload['phase']} | {payload['status']}")
1884
- if payload.get("ready_steps"):
1885
- print(" ready: " + ", ".join(item["id"] for item in payload["ready_steps"]))
1886
- if payload.get("blocked_steps"):
1887
- print(" blocked: " + ", ".join(item["id"] for item in payload["blocked_steps"]))
1888
- if payload.get("constraints"):
1889
- print(" constraints: " + ", ".join(item["id"] for item in payload["constraints"]))
1890
- action = payload["next_action"]
1891
- print(f" next: {action['kind']} ({action['reason']})")
1892
-
1893
-
1894
- def run_plan_next(root, plan_name, as_json=False):
1895
- payload, status = plan_next_payload(root, plan_name)
1896
- if as_json:
1897
- print(json.dumps(payload, ensure_ascii=False, indent=2))
1898
- else:
1899
- print_plan_next_text(payload)
1900
- return status
1901
-
1902
-
1903
1417
  def parse_args(argv):
1904
1418
  parser = argparse.ArgumentParser(description="检查计划治理文档的一致性。")
1905
1419
  parser.add_argument("root", nargs="?", default=".", help="仓库根目录,默认当前目录。")
1906
1420
  parser.add_argument("--workset", action="store_true", help="输出只读当前工作集。")
1907
- parser.add_argument("--validate-steps", action="store_true", help="只读校验指定计划的自主执行步骤表。")
1908
- parser.add_argument("--next-plan", metavar="PLAN", help="只读查询指定计划的下一步骤集合。")
1909
- parser.add_argument("--json", action="store_true", help="以 JSON 输出工作集或步骤校验结果。")
1421
+ parser.add_argument("--json", action="store_true", help="以 JSON 输出工作集。")
1910
1422
  parser.add_argument("--include-history", action="store_true", help="工作集包含历史计划。")
1911
- parser.add_argument("--root", dest="mode_root", help="工作集/步骤校验的仓库根目录。")
1423
+ parser.add_argument("--root", dest="mode_root", help="工作集的仓库根目录。")
1912
1424
  parser.add_argument("--drift", action="store_true", help="检查工作区变更是否被活跃计划影响范围覆盖。")
1913
1425
  parser.add_argument("--pre-commit", action="store_true", help="检查 staged 变更是否被活跃计划影响范围覆盖。")
1914
1426
  parser.add_argument(
@@ -1950,15 +1462,6 @@ def main(argv=None):
1950
1462
  include_history=args.include_history,
1951
1463
  strict=args.strict_readiness,
1952
1464
  )
1953
- if args.validate_steps:
1954
- return run_validate_steps(
1955
- Path(args.mode_root or "."),
1956
- args.root,
1957
- as_json=args.json,
1958
- strict=args.strict_readiness,
1959
- )
1960
- if args.next_plan:
1961
- return run_plan_next(Path(args.mode_root or "."), args.next_plan, as_json=args.json)
1962
1465
  root = Path(args.root)
1963
1466
  docs = root / "docs"
1964
1467
  plan_map = docs / "PLAN_MAP.md"
@@ -192,6 +192,10 @@ def update_agent_rules(root):
192
192
 
193
193
  def plan_map_content(plan_slug, title, status, phase):
194
194
  today = date.today().isoformat()
195
+ row = f"| [{title}](plans/{plan_slug}.md) | {status} | {phase} | {today} | - | - |"
196
+ unfinished_row = row if status in {"候选", "设计中", "待实施", "实施中"} else ""
197
+ completed_row = row if status == "已完成" else ""
198
+ deprecated_row = row if status in {"已废弃", "已替代", "已合并"} else ""
195
199
  return f"""# PLAN_MAP
196
200
 
197
201
  ## 治理范围
@@ -208,12 +212,27 @@ def plan_map_content(plan_slug, title, status, phase):
208
212
  - `PLAN_MAP.md` 的 `状态` 是计划级生命周期,`当前阶段` 是阶段身份指针;阶段 N 完成后,阶段 N+1 默认保持 `设计中`。
209
213
  - 阶段准入摘要、样本矩阵和独立复核记录只写入专项计划,不复制到本索引。
210
214
  - 启用治理后,已有草案、历史设计、归档计划和临时分析文档默认只作为背景材料,不再作为规范事实源;后续新规范默认进入 `docs/plans/*.md`、ADR、migration、正式 spec 或 `docs/PLAN_MAP.md`。
215
+ - 计划索引固定分为 `未完成`、`已完成`、`已废弃` 三张表;`已替代`、`已合并`等不再推进的终态归入 `已废弃` 表,但保留真实状态值。
211
216
 
212
217
  ## 计划索引
213
218
 
219
+ ### 未完成
220
+
221
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
222
+ |---|---|---|---|---|---|
223
+ {unfinished_row}
224
+
225
+ ### 已完成
226
+
227
+ | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
228
+ |---|---|---|---|---|---|
229
+ {completed_row}
230
+
231
+ ### 已废弃
232
+
214
233
  | 计划 | 状态 | 当前阶段 | 最后更新 | 依赖 | 证据 |
215
234
  |---|---|---|---|---|---|
216
- | [{title}](plans/{plan_slug}.md) | {status} | {phase} | {today} | - | - |
235
+ {deprecated_row}
217
236
 
218
237
  允许状态:`候选`、`设计中`、`待实施`、`实施中`、`已完成`、`已替代`、`已合并`、`已废弃`。
219
238
 
@@ -1,6 +1,5 @@
1
1
  #!/usr/bin/env python3
2
2
  import argparse
3
- import importlib.util
4
3
  import re
5
4
  import subprocess
6
5
  import sys
@@ -135,16 +134,6 @@ def active_plans(root):
135
134
  return [plan for plan in plans if plan["status"] in ACTIVE_STATUSES], blockers
136
135
 
137
136
 
138
- def load_next_checker():
139
- checker_path = Path(__file__).with_name("check_plan_governance.py")
140
- spec = importlib.util.spec_from_file_location("plan_governance_next_checker", checker_path)
141
- if spec is None or spec.loader is None:
142
- return None
143
- module = importlib.util.module_from_spec(spec)
144
- spec.loader.exec_module(module)
145
- return module
146
-
147
-
148
137
  def target_matches_path(target, path):
149
138
  normalized_target = normalize_scope_path(target)
150
139
  normalized_path = normalize_scope_path(path)
@@ -190,24 +179,7 @@ def print_session_start(root):
190
179
  else:
191
180
  print("[plan-governance] 当前阻塞项:无。")
192
181
 
193
- checker = load_next_checker()
194
- if checker is None:
195
- return 0
196
- hook_status = 0
197
- for plan in active:
198
- payload, status = checker.plan_next_payload(root, plan["name"])
199
- if payload["status"] == "not_enabled":
200
- continue
201
- ready = ",".join(item["id"] for item in payload.get("ready_steps", [])) or "-"
202
- blocked = ",".join(item["id"] for item in payload.get("blocked_steps", [])) or "-"
203
- action = payload["next_action"]
204
- print(
205
- f"[plan-governance] autonomous next: {plan['name']} | "
206
- f"{payload['status']} | ready={ready} | blocked={blocked} | "
207
- f"next={action['kind']}:{action['reason']}"
208
- )
209
- hook_status = max(hook_status, status)
210
- return hook_status
182
+ return 0
211
183
 
212
184
 
213
185
  def print_pre_write(root, paths):