plan-governance-cli 0.3.2 → 0.3.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 +3 -25
- package/bin/plan-governance-cli.mjs +4 -4
- package/package.json +1 -1
- package/resources/skill/SKILL.md +2 -18
- package/resources/skill/agents/openai.yaml +1 -1
- package/resources/skill/assets/plan.template.md +0 -17
- package/scripts/check_plan_governance.py +2 -499
- package/scripts/plan_governance_hook.py +1 -29
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.
|
|
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
|
|
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
package/resources/skill/SKILL.md
CHANGED
|
@@ -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` 和相关计划。
|
|
@@ -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("--
|
|
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"
|
|
@@ -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
|
-
|
|
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):
|