@hunter-harness/workflow-harness 0.2.86 → 0.3.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/harness/bundles/general/claude-code/.harness-build.json +1 -1
- package/harness/bundles/general/claude-code/contracts/workflow-policy.json +46 -17
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-execute/SKILL.md +189 -0
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +10 -10
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -13
- package/harness/bundles/general/claude-code/harness-plan/reference.md +43 -81
- package/harness/bundles/general/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-review/protocols.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +8 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +11 -2
- package/harness/bundles/general/claude-code/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +23 -14
- package/harness/bundles/general/claude-code/scripts/harness_context.py +94 -43
- package/harness/bundles/general/claude-code/scripts/harness_events.py +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +10 -10
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +92 -25
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +4 -2
- package/harness/bundles/general/claude-code/scripts/harness_paths.py +120 -5
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +41 -17
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +46 -17
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-execute/SKILL.md +170 -0
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +10 -10
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -13
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +43 -81
- package/harness/bundles/general/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-review/protocols.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +8 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +11 -2
- package/harness/bundles/general/codebuddy/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +23 -14
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +94 -43
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +10 -10
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +92 -25
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +4 -2
- package/harness/bundles/general/codebuddy/scripts/harness_paths.py +120 -5
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +41 -17
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +46 -17
- package/harness/bundles/general/codex/harness-archive/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-execute/SKILL.md +170 -0
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-plan/SKILL.md +10 -10
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -13
- package/harness/bundles/general/codex/harness-plan/reference.md +43 -81
- package/harness/bundles/general/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-review/protocols.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +8 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +11 -2
- package/harness/bundles/general/codex/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +23 -14
- package/harness/bundles/general/codex/scripts/harness_context.py +94 -43
- package/harness/bundles/general/codex/scripts/harness_events.py +1 -1
- package/harness/bundles/general/codex/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/general/codex/scripts/harness_fixback.py +10 -10
- package/harness/bundles/general/codex/scripts/harness_gate.py +92 -25
- package/harness/bundles/general/codex/scripts/harness_ledger.py +4 -2
- package/harness/bundles/general/codex/scripts/harness_paths.py +120 -5
- package/harness/bundles/general/codex/scripts/harness_phase.py +41 -17
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +46 -17
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-execute/SKILL.md +170 -0
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +10 -10
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -13
- package/harness/bundles/general/cursor/harness-plan/reference.md +43 -81
- package/harness/bundles/general/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-review/protocols.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +8 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +11 -2
- package/harness/bundles/general/cursor/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +23 -14
- package/harness/bundles/general/cursor/scripts/harness_context.py +94 -43
- package/harness/bundles/general/cursor/scripts/harness_events.py +1 -1
- package/harness/bundles/general/cursor/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +10 -10
- package/harness/bundles/general/cursor/scripts/harness_gate.py +92 -25
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +4 -2
- package/harness/bundles/general/cursor/scripts/harness_paths.py +120 -5
- package/harness/bundles/general/cursor/scripts/harness_phase.py +41 -17
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +46 -17
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-execute/SKILL.md +189 -0
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +10 -10
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -13
- package/harness/bundles/java/claude-code/harness-plan/reference.md +43 -81
- package/harness/bundles/java/claude-code/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-review/protocols.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +8 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +11 -2
- package/harness/bundles/java/claude-code/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +23 -14
- package/harness/bundles/java/claude-code/scripts/harness_context.py +94 -43
- package/harness/bundles/java/claude-code/scripts/harness_events.py +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +10 -10
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +92 -25
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +4 -2
- package/harness/bundles/java/claude-code/scripts/harness_paths.py +120 -5
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +41 -17
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +46 -17
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-execute/SKILL.md +170 -0
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +10 -10
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -13
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +43 -81
- package/harness/bundles/java/codebuddy/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-review/protocols.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +8 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +11 -2
- package/harness/bundles/java/codebuddy/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +23 -14
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +94 -43
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +10 -10
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +92 -25
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +4 -2
- package/harness/bundles/java/codebuddy/scripts/harness_paths.py +120 -5
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +41 -17
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +46 -17
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-execute/SKILL.md +170 -0
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +10 -10
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -13
- package/harness/bundles/java/codex/harness-plan/reference.md +43 -81
- package/harness/bundles/java/codex/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-review/protocols.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +8 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +11 -2
- package/harness/bundles/java/codex/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +23 -14
- package/harness/bundles/java/codex/scripts/harness_context.py +94 -43
- package/harness/bundles/java/codex/scripts/harness_events.py +1 -1
- package/harness/bundles/java/codex/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/java/codex/scripts/harness_fixback.py +10 -10
- package/harness/bundles/java/codex/scripts/harness_gate.py +92 -25
- package/harness/bundles/java/codex/scripts/harness_ledger.py +4 -2
- package/harness/bundles/java/codex/scripts/harness_paths.py +120 -5
- package/harness/bundles/java/codex/scripts/harness_phase.py +41 -17
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +16 -873
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +46 -17
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-execute/SKILL.md +170 -0
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +10 -10
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -13
- package/harness/bundles/java/cursor/harness-plan/reference.md +43 -81
- package/harness/bundles/java/cursor/harness-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-review/protocols.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +8 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +11 -2
- package/harness/bundles/java/cursor/scripts/harness_adoption_metrics.py +391 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +23 -14
- package/harness/bundles/java/cursor/scripts/harness_context.py +94 -43
- package/harness/bundles/java/cursor/scripts/harness_events.py +1 -1
- package/harness/bundles/java/cursor/scripts/harness_events_sync.py +1 -0
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +10 -10
- package/harness/bundles/java/cursor/scripts/harness_gate.py +92 -25
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +4 -2
- package/harness/bundles/java/cursor/scripts/harness_paths.py +120 -5
- package/harness/bundles/java/cursor/scripts/harness_phase.py +41 -17
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +16 -873
- package/harness/manifests/general/claude-code.json +36 -28
- package/harness/manifests/general/codebuddy.json +36 -28
- package/harness/manifests/general/codex.json +36 -28
- package/harness/manifests/general/cursor.json +36 -28
- package/harness/manifests/java/claude-code.json +38 -30
- package/harness/manifests/java/codebuddy.json +38 -30
- package/harness/manifests/java/codex.json +38 -30
- package/harness/manifests/java/cursor.json +38 -30
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -1,19 +1,27 @@
|
|
|
1
1
|
#!/usr/bin/env python3
|
|
2
|
-
"""
|
|
2
|
+
"""已发布计划的只读验收与 v2 清单解包(read-side only)。
|
|
3
|
+
|
|
4
|
+
legacy staging/finalize/republish 写侧已于 2026-08 移除(roadmap 14;采纳度
|
|
5
|
+
gating 由用户决策解除)。新 change 的发布走 `hunter-harness plan evidence-pack`
|
|
6
|
+
+ `plan finalize`(v2);计划修订走 evidence-pack 重跑(expected_baseline
|
|
7
|
+
= present + 新 attempt)。本模块保留:
|
|
8
|
+
- `verify_plan`:已发布计划的只读验收(legacy receipt 与 v2 事务/journal 两种
|
|
9
|
+
证据形状),供 `harness_gate.validate_plan_handoff` 与 `verify` 子命令使用;
|
|
10
|
+
- v2 manifest / checkpoints 解包与 plan/scenario 表解析:供 gate 与 ledger
|
|
11
|
+
在只读侧消费(哈希校验后)。
|
|
12
|
+
|
|
13
|
+
历史 change 的 legacy 产物必须保持可读;删除的是写入路径,不是读路径。
|
|
14
|
+
"""
|
|
3
15
|
|
|
4
16
|
from __future__ import annotations
|
|
5
17
|
|
|
6
18
|
import argparse
|
|
7
|
-
import contextlib
|
|
8
19
|
import hashlib
|
|
9
|
-
import io
|
|
10
20
|
import json
|
|
11
21
|
import os
|
|
12
22
|
import re
|
|
13
|
-
import shutil
|
|
14
23
|
import stat
|
|
15
24
|
import sys
|
|
16
|
-
import tempfile
|
|
17
25
|
from pathlib import Path, PurePosixPath
|
|
18
26
|
from typing import Any
|
|
19
27
|
|
|
@@ -44,7 +52,7 @@ _BUILD_FILE_NAMES = {
|
|
|
44
52
|
}
|
|
45
53
|
|
|
46
54
|
# C8: valid ownerPhase values (lifecycle phases that can own tasks).
|
|
47
|
-
VALID_OWNER_PHASES = {"plan", "
|
|
55
|
+
VALID_OWNER_PHASES = {"plan", "execute", "review", "submit"}
|
|
48
56
|
SLICE_CANDIDATE_TYPES = {
|
|
49
57
|
"independent-release-candidate",
|
|
50
58
|
"child-of-aggregate",
|
|
@@ -210,7 +218,7 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, Any]]:
|
|
|
210
218
|
and complete_row
|
|
211
219
|
and owner_phase_index < len(cells)
|
|
212
220
|
and cells[owner_phase_index].strip()
|
|
213
|
-
else "
|
|
221
|
+
else "execute"
|
|
214
222
|
),
|
|
215
223
|
"requiredEvidenceKind": PRIORITY_EVIDENCE_KIND.get(
|
|
216
224
|
priority,
|
|
@@ -455,21 +463,6 @@ def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
|
455
463
|
return tasks
|
|
456
464
|
|
|
457
465
|
|
|
458
|
-
def _atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
459
|
-
path.parent.mkdir(parents=True, exist_ok=True)
|
|
460
|
-
fd, raw_tmp = tempfile.mkstemp(
|
|
461
|
-
prefix=f".{path.name}.", suffix=".tmp", dir=str(path.parent)
|
|
462
|
-
)
|
|
463
|
-
tmp = Path(raw_tmp)
|
|
464
|
-
try:
|
|
465
|
-
with os.fdopen(fd, "w", encoding="utf-8", newline="\n") as handle:
|
|
466
|
-
json.dump(payload, handle, ensure_ascii=False, indent=2)
|
|
467
|
-
handle.write("\n")
|
|
468
|
-
handle.flush()
|
|
469
|
-
os.fsync(handle.fileno())
|
|
470
|
-
os.replace(tmp, path)
|
|
471
|
-
finally:
|
|
472
|
-
tmp.unlink(missing_ok=True)
|
|
473
466
|
|
|
474
467
|
|
|
475
468
|
def _frontmatter(text: str) -> dict[str, str] | None:
|
|
@@ -509,362 +502,17 @@ def _is_link_or_reparse(path: Path) -> bool:
|
|
|
509
502
|
return bool(attributes & _FILE_ATTRIBUTE_REPARSE_POINT)
|
|
510
503
|
|
|
511
504
|
|
|
512
|
-
def lint_slice_plan(plan_path: Path) -> dict[str, Any]:
|
|
513
|
-
"""Validate the identity, reuse, and storage contract for a slice plan."""
|
|
514
|
-
text = Path(plan_path).read_text(encoding="utf-8-sig")
|
|
515
|
-
frontmatter = _frontmatter(text)
|
|
516
|
-
if frontmatter is None:
|
|
517
|
-
return _result_error(
|
|
518
|
-
"SLICE_PLAN_CONTRACT_INVALID",
|
|
519
|
-
f"{plan_path.name}: frontmatter is required",
|
|
520
|
-
)
|
|
521
|
-
enabled = (
|
|
522
|
-
str(frontmatter.get("slice-plan") or "").strip().lower()
|
|
523
|
-
in {"true", "yes", "1"}
|
|
524
|
-
or "candidate-type" in frontmatter
|
|
525
|
-
)
|
|
526
|
-
if not enabled:
|
|
527
|
-
return {
|
|
528
|
-
"ok": True,
|
|
529
|
-
"configured": False,
|
|
530
|
-
"code": "SLICE_PLAN_NOT_CONFIGURED",
|
|
531
|
-
}
|
|
532
505
|
|
|
533
|
-
issues: list[str] = []
|
|
534
|
-
candidate_type = str(frontmatter.get("candidate-type") or "").strip()
|
|
535
|
-
aggregate_parent = str(frontmatter.get("aggregate-parent") or "").strip()
|
|
536
|
-
evidence_reuse = str(frontmatter.get("evidence-reuse") or "").strip()
|
|
537
|
-
raw_budget = str(frontmatter.get("artifact-budget-bytes") or "").strip()
|
|
538
|
-
if candidate_type not in SLICE_CANDIDATE_TYPES:
|
|
539
|
-
issues.append(
|
|
540
|
-
"candidate-type must be one of "
|
|
541
|
-
+ ", ".join(sorted(SLICE_CANDIDATE_TYPES))
|
|
542
|
-
)
|
|
543
|
-
if not aggregate_parent:
|
|
544
|
-
issues.append(
|
|
545
|
-
"aggregate-parent is required (use 'none' for an independent candidate)"
|
|
546
|
-
)
|
|
547
|
-
if not evidence_reuse:
|
|
548
|
-
issues.append(
|
|
549
|
-
"evidence-reuse is required (use 'none' when no evidence is reused)"
|
|
550
|
-
)
|
|
551
|
-
try:
|
|
552
|
-
artifact_budget = int(raw_budget)
|
|
553
|
-
if artifact_budget <= 0:
|
|
554
|
-
raise ValueError
|
|
555
|
-
except ValueError:
|
|
556
|
-
artifact_budget = 0
|
|
557
|
-
issues.append("artifact-budget-bytes must be a positive integer")
|
|
558
|
-
if issues:
|
|
559
|
-
return {
|
|
560
|
-
"ok": False,
|
|
561
|
-
"code": "SLICE_PLAN_CONTRACT_INVALID",
|
|
562
|
-
"error": f"{plan_path.name}: " + "; ".join(issues),
|
|
563
|
-
"issues": issues,
|
|
564
|
-
}
|
|
565
|
-
return {
|
|
566
|
-
"ok": True,
|
|
567
|
-
"configured": True,
|
|
568
|
-
"code": "SLICE_PLAN_CONTRACT_OK",
|
|
569
|
-
"candidateType": candidate_type,
|
|
570
|
-
"aggregateParent": aggregate_parent,
|
|
571
|
-
"evidenceReuse": evidence_reuse,
|
|
572
|
-
"artifactBudgetBytes": artifact_budget,
|
|
573
|
-
}
|
|
574
506
|
|
|
575
507
|
|
|
576
|
-
def _artifact_files(staging: Path) -> list[Path]:
|
|
577
|
-
files: list[Path] = []
|
|
578
|
-
for path in staging.rglob("*"):
|
|
579
|
-
if _is_link_or_reparse(path):
|
|
580
|
-
raise ValueError(f"PLAN_ARTIFACT_SYMLINK: {path}")
|
|
581
|
-
if path.is_file():
|
|
582
|
-
files.append(path)
|
|
583
|
-
return sorted(files, key=lambda item: item.relative_to(staging).as_posix())
|
|
584
508
|
|
|
585
509
|
|
|
586
|
-
def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
587
|
-
staging = staging.resolve()
|
|
588
|
-
required = {Path(name) for name in _required_artifact_names(change_name)}
|
|
589
|
-
try:
|
|
590
|
-
files = _artifact_files(staging)
|
|
591
|
-
except ValueError as exc:
|
|
592
|
-
code = str(exc).split(":", 1)[0]
|
|
593
|
-
return _result_error(code, str(exc))
|
|
594
|
-
rel_files = {path.relative_to(staging) for path in files}
|
|
595
|
-
missing = sorted(path.as_posix() for path in required - rel_files)
|
|
596
|
-
if missing:
|
|
597
|
-
return _result_error(
|
|
598
|
-
"PLAN_ARTIFACT_MISSING", "missing required artifacts: " + ", ".join(missing)
|
|
599
|
-
)
|
|
600
510
|
|
|
601
|
-
for path in files:
|
|
602
|
-
rel = path.relative_to(staging)
|
|
603
|
-
if rel.parts[0] not in {"spec", "plans", "meta"}:
|
|
604
|
-
return _result_error(
|
|
605
|
-
"PLAN_ARTIFACT_PATH_INVALID", f"unexpected artifact root: {rel.as_posix()}"
|
|
606
|
-
)
|
|
607
|
-
if path.suffix.lower() == ".json":
|
|
608
|
-
try:
|
|
609
|
-
payload = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
610
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
611
|
-
return _result_error(
|
|
612
|
-
"PLAN_ARTIFACT_INVALID_JSON", f"{rel.as_posix()}: {exc}"
|
|
613
|
-
)
|
|
614
|
-
if not isinstance(payload, dict):
|
|
615
|
-
return _result_error(
|
|
616
|
-
"PLAN_ARTIFACT_INVALID_JSON",
|
|
617
|
-
f"{rel.as_posix()}: top-level JSON must be an object",
|
|
618
|
-
)
|
|
619
|
-
elif path.suffix.lower() == ".md":
|
|
620
|
-
try:
|
|
621
|
-
text = path.read_text(encoding="utf-8-sig")
|
|
622
|
-
except OSError as exc:
|
|
623
|
-
return _result_error("PLAN_ARTIFACT_UNREADABLE", str(exc))
|
|
624
|
-
frontmatter = _frontmatter(text)
|
|
625
|
-
if frontmatter is None or frontmatter.get("change-name") != change_name:
|
|
626
|
-
return _result_error(
|
|
627
|
-
"PLAN_ARTIFACT_FRONTMATTER_INVALID",
|
|
628
|
-
f"{rel.as_posix()}: change-name frontmatter mismatch",
|
|
629
|
-
)
|
|
630
|
-
for raw_link in _LINK.findall(text):
|
|
631
|
-
link = raw_link.strip().split("#", 1)[0]
|
|
632
|
-
if not link or "://" in link or link.startswith("mailto:"):
|
|
633
|
-
continue
|
|
634
|
-
target = (path.parent / link).resolve()
|
|
635
|
-
try:
|
|
636
|
-
target.relative_to(staging)
|
|
637
|
-
except ValueError:
|
|
638
|
-
return _result_error(
|
|
639
|
-
"PLAN_ARTIFACT_REFERENCE_INVALID",
|
|
640
|
-
f"{rel.as_posix()}: reference escapes staging: {raw_link}",
|
|
641
|
-
)
|
|
642
|
-
if not target.is_file():
|
|
643
|
-
return _result_error(
|
|
644
|
-
"PLAN_ARTIFACT_REFERENCE_MISSING",
|
|
645
|
-
f"{rel.as_posix()}: missing reference {raw_link}",
|
|
646
|
-
)
|
|
647
511
|
|
|
648
|
-
# C8: validate ownerPhase values in plan.md task table.
|
|
649
|
-
plan_path = staging / "plans" / f"{change_name}-plan.md"
|
|
650
|
-
try:
|
|
651
|
-
tasks = parse_plan_tasks(plan_path)
|
|
652
|
-
except PlanParseError as exc:
|
|
653
|
-
return _result_error(exc.code, str(exc))
|
|
654
|
-
if not tasks:
|
|
655
|
-
return _result_error(
|
|
656
|
-
"PLAN_TASKS_EMPTY",
|
|
657
|
-
f"{plan_path.relative_to(staging).as_posix()}: no task rows were parsed",
|
|
658
|
-
)
|
|
659
|
-
task_ids = [str(task.get("num") or "").strip() for task in tasks]
|
|
660
|
-
duplicate_task_ids = sorted(
|
|
661
|
-
{task_id for task_id in task_ids if task_ids.count(task_id) > 1}
|
|
662
|
-
)
|
|
663
|
-
if duplicate_task_ids:
|
|
664
|
-
return _result_error(
|
|
665
|
-
"PLAN_TASK_ID_DUPLICATE",
|
|
666
|
-
"duplicate task IDs: " + ", ".join(duplicate_task_ids),
|
|
667
|
-
)
|
|
668
|
-
for task in tasks:
|
|
669
|
-
owner = task.get("ownerPhase")
|
|
670
|
-
if owner is not None and owner != "" and owner not in VALID_OWNER_PHASES:
|
|
671
|
-
return _result_error(
|
|
672
|
-
"PLAN_OWNER_PHASE_INVALID",
|
|
673
|
-
f"task {task.get('num', '?')}: ownerPhase '{owner}' not in {sorted(VALID_OWNER_PHASES)}",
|
|
674
|
-
)
|
|
675
|
-
slice_plan = lint_slice_plan(plan_path)
|
|
676
|
-
if not slice_plan.get("ok"):
|
|
677
|
-
return slice_plan
|
|
678
512
|
|
|
679
|
-
# C9: parse test-scenarios.md for scenario manifest.
|
|
680
|
-
scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
|
|
681
|
-
try:
|
|
682
|
-
scenarios = parse_test_scenarios(scenarios_path)
|
|
683
|
-
except PlanParseError as exc:
|
|
684
|
-
return _result_error(exc.code, str(exc))
|
|
685
|
-
if not scenarios:
|
|
686
|
-
return _result_error(
|
|
687
|
-
"PLAN_SCENARIOS_EMPTY",
|
|
688
|
-
f"{scenarios_path.relative_to(staging).as_posix()}: no scenario rows were parsed",
|
|
689
|
-
)
|
|
690
|
-
scenario_ids = [str(item.get("id") or "").strip() for item in scenarios]
|
|
691
|
-
duplicate_scenario_ids = sorted(
|
|
692
|
-
{
|
|
693
|
-
scenario_id
|
|
694
|
-
for scenario_id in scenario_ids
|
|
695
|
-
if scenario_ids.count(scenario_id) > 1
|
|
696
|
-
}
|
|
697
|
-
)
|
|
698
|
-
if duplicate_scenario_ids:
|
|
699
|
-
return _result_error(
|
|
700
|
-
"PLAN_SCENARIO_ID_DUPLICATE",
|
|
701
|
-
"duplicate scenario IDs: " + ", ".join(duplicate_scenario_ids),
|
|
702
|
-
)
|
|
703
|
-
for scenario in scenarios:
|
|
704
|
-
priority = str(scenario.get("priority") or "").upper()
|
|
705
|
-
if priority not in PRIORITY_EVIDENCE_KIND:
|
|
706
|
-
return _result_error(
|
|
707
|
-
"PLAN_SCENARIO_PRIORITY_INVALID",
|
|
708
|
-
f"scenario {scenario.get('id', '?')}: unsupported priority '{priority}'",
|
|
709
|
-
)
|
|
710
|
-
owner = str(scenario.get("ownerPhase") or "")
|
|
711
|
-
if owner not in VALID_OWNER_PHASES:
|
|
712
|
-
return _result_error(
|
|
713
|
-
"PLAN_SCENARIO_OWNER_PHASE_INVALID",
|
|
714
|
-
f"scenario {scenario.get('id', '?')}: unsupported ownerPhase '{owner}'",
|
|
715
|
-
)
|
|
716
|
-
if (
|
|
717
|
-
scenario.get("requiredEvidenceKind") == "ledger"
|
|
718
|
-
and scenario.get("executableMappingDeclared")
|
|
719
|
-
):
|
|
720
|
-
missing_mapping = [
|
|
721
|
-
field
|
|
722
|
-
for field in ("executableTestId", "testFile", "testTitle")
|
|
723
|
-
if not str(scenario.get(field) or "").strip()
|
|
724
|
-
]
|
|
725
|
-
if missing_mapping:
|
|
726
|
-
return _result_error(
|
|
727
|
-
"PLAN_SCENARIO_EXECUTABLE_MAPPING_MISSING",
|
|
728
|
-
f"scenario {scenario.get('id', '?')}: missing executable mapping fields: "
|
|
729
|
-
+ ", ".join(missing_mapping),
|
|
730
|
-
)
|
|
731
513
|
|
|
732
|
-
digest = hashlib.sha256()
|
|
733
|
-
artifact_names: list[str] = []
|
|
734
|
-
for path in files:
|
|
735
|
-
rel = path.relative_to(staging).as_posix()
|
|
736
|
-
artifact_names.append(rel)
|
|
737
|
-
digest.update(rel.encode("utf-8"))
|
|
738
|
-
digest.update(b"\0")
|
|
739
|
-
digest.update(path.read_bytes())
|
|
740
|
-
digest.update(b"\0")
|
|
741
|
-
return {
|
|
742
|
-
"ok": True,
|
|
743
|
-
"files": artifact_names,
|
|
744
|
-
"artifactsHash": "sha256:" + digest.hexdigest(),
|
|
745
|
-
"tasks": tasks,
|
|
746
|
-
"scenarios": scenarios,
|
|
747
|
-
"slicePlan": slice_plan,
|
|
748
|
-
}
|
|
749
514
|
|
|
750
515
|
|
|
751
|
-
def validate_product_ownership(change_dir: Path, staging: Path) -> dict[str, Any]:
|
|
752
|
-
"""Ensure Plan declares stable prefix ownership for explicit code/test/build paths."""
|
|
753
|
-
context_path = change_dir / "meta" / "change-context.json"
|
|
754
|
-
if not context_path.is_file():
|
|
755
|
-
return {"ok": True, "code": "PLAN_PRODUCT_PATHS_LEGACY_UNDECLARED"}
|
|
756
|
-
try:
|
|
757
|
-
context = json.loads(context_path.read_text(encoding="utf-8-sig"))
|
|
758
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
759
|
-
return _result_error("PLAN_CHANGE_CONTEXT_INVALID", str(exc))
|
|
760
|
-
ownership = context.get("ownership") if isinstance(context, dict) else None
|
|
761
|
-
raw_paths = ownership.get("productPaths") if isinstance(ownership, dict) else None
|
|
762
|
-
product_paths = [
|
|
763
|
-
str(item).strip().replace("\\", "/").removeprefix("./")
|
|
764
|
-
for item in raw_paths or []
|
|
765
|
-
if isinstance(item, str) and item.strip()
|
|
766
|
-
]
|
|
767
|
-
unsupported = sorted(path for path in product_paths if any(char in path for char in "*?[]"))
|
|
768
|
-
if unsupported:
|
|
769
|
-
result = _result_error(
|
|
770
|
-
"PLAN_PRODUCT_PATHS_GLOB_UNSUPPORTED",
|
|
771
|
-
"productPaths 只接受精确文件或目录前缀,不支持通配:" + ", ".join(unsupported),
|
|
772
|
-
)
|
|
773
|
-
result["unsupportedPaths"] = unsupported
|
|
774
|
-
return result
|
|
775
|
-
|
|
776
|
-
planned_paths: set[str] = set()
|
|
777
|
-
for document in sorted(staging.rglob("*.md")):
|
|
778
|
-
text = document.read_text(encoding="utf-8-sig")
|
|
779
|
-
for raw in _BACKTICK.findall(text):
|
|
780
|
-
candidate = raw.strip().strip("'\"").replace("\\", "/")
|
|
781
|
-
if not candidate or " " in candidate or "://" in candidate or "<" in candidate:
|
|
782
|
-
continue
|
|
783
|
-
candidate = re.sub(r":\d+(?::\d+)?$", "", candidate)
|
|
784
|
-
candidate = candidate.removeprefix("./").strip("/.,;,;")
|
|
785
|
-
if not candidate or candidate.startswith("../") or candidate.startswith(".harness/"):
|
|
786
|
-
continue
|
|
787
|
-
pure = PurePosixPath(candidate)
|
|
788
|
-
if pure.name.lower() in _BUILD_FILE_NAMES or pure.suffix.lower() in _CODE_PATH_SUFFIXES:
|
|
789
|
-
planned_paths.add(candidate)
|
|
790
|
-
|
|
791
|
-
def covered(path: str) -> bool:
|
|
792
|
-
return any(
|
|
793
|
-
path == owned.rstrip("/")
|
|
794
|
-
or path.startswith(owned.rstrip("/") + "/")
|
|
795
|
-
for owned in product_paths
|
|
796
|
-
if owned.rstrip("/")
|
|
797
|
-
)
|
|
798
|
-
|
|
799
|
-
missing = sorted(path for path in planned_paths if not covered(path))
|
|
800
|
-
if missing:
|
|
801
|
-
result = _result_error(
|
|
802
|
-
"PLAN_PRODUCT_PATHS_INCOMPLETE",
|
|
803
|
-
"productPaths 未覆盖计划明确引用的产品文件:" + ", ".join(missing),
|
|
804
|
-
)
|
|
805
|
-
result["missingPaths"] = missing
|
|
806
|
-
result["recoveryAction"] = "在 Plan 阶段补充精确文件或目录前缀后重新 finalize。"
|
|
807
|
-
return result
|
|
808
|
-
return {
|
|
809
|
-
"ok": True,
|
|
810
|
-
"code": "PLAN_PRODUCT_PATHS_COMPLETE",
|
|
811
|
-
"plannedPaths": sorted(planned_paths),
|
|
812
|
-
"productPaths": product_paths,
|
|
813
|
-
}
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
def _terminal_exists(change_dir: Path, run_id: str, attempt: int) -> bool:
|
|
817
|
-
try:
|
|
818
|
-
events = harness_events.load_events(harness_events.events_path(change_dir))
|
|
819
|
-
except (OSError, ValueError):
|
|
820
|
-
return False
|
|
821
|
-
return any(
|
|
822
|
-
event.get("phase") == "plan"
|
|
823
|
-
and event.get("type") == "phase.end"
|
|
824
|
-
and event.get("run_id") == run_id
|
|
825
|
-
and event.get("attempt") == attempt
|
|
826
|
-
and str(event.get("status") or "").upper() == "OK"
|
|
827
|
-
for event in events
|
|
828
|
-
)
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
def _validate_plan_start(
|
|
832
|
-
change_dir: Path,
|
|
833
|
-
run_id: str,
|
|
834
|
-
attempt: int,
|
|
835
|
-
) -> dict[str, Any]:
|
|
836
|
-
events_path = harness_events.events_path(change_dir)
|
|
837
|
-
try:
|
|
838
|
-
events = harness_events.load_events(events_path)
|
|
839
|
-
except (OSError, ValueError) as exc:
|
|
840
|
-
return _result_error("EVENTS_PARSE_ERROR", str(exc))
|
|
841
|
-
matching = [
|
|
842
|
-
event
|
|
843
|
-
for event in events
|
|
844
|
-
if event.get("phase") == "plan"
|
|
845
|
-
and event.get("type") == "phase.start"
|
|
846
|
-
and event.get("run_id") == run_id
|
|
847
|
-
and event.get("attempt") == attempt
|
|
848
|
-
]
|
|
849
|
-
if not matching:
|
|
850
|
-
return _result_error(
|
|
851
|
-
"PHASE_START_MISSING",
|
|
852
|
-
"no matching plan phase.start event found for finalizer runId/attempt; "
|
|
853
|
-
"append one with the same run id and attempt before finalizing, or use "
|
|
854
|
-
"the `republish` subcommand which allocates and appends both for you",
|
|
855
|
-
)
|
|
856
|
-
if len(matching) > 1:
|
|
857
|
-
return _result_error(
|
|
858
|
-
"PHASE_START_DUPLICATE",
|
|
859
|
-
f"found {len(matching)} matching plan phase.start events for run id "
|
|
860
|
-
f"{run_id}. `harness_gate.py begin` already appends phase.start; a "
|
|
861
|
-
"second manual append of the same run id creates this state. Recover "
|
|
862
|
-
"with a fresh run id and the next attempt number (attempts are global "
|
|
863
|
-
"per phase, not per run id): append phase.start with both, then "
|
|
864
|
-
"finalize with the same pair.",
|
|
865
|
-
)
|
|
866
|
-
return {"ok": True, "phaseStartCount": 1}
|
|
867
|
-
|
|
868
516
|
|
|
869
517
|
def _receipt_artifact_targets(
|
|
870
518
|
change_dir: Path,
|
|
@@ -1326,515 +974,27 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
1326
974
|
}
|
|
1327
975
|
|
|
1328
976
|
|
|
1329
|
-
def _read_design_capabilities(staging: Path, change_name: str) -> list[str]:
|
|
1330
|
-
"""Read capabilities from design frontmatter (retro §5.4)."""
|
|
1331
|
-
design_path = staging / "spec" / f"{change_name}-design.md"
|
|
1332
|
-
if not design_path.is_file():
|
|
1333
|
-
return []
|
|
1334
|
-
try:
|
|
1335
|
-
text = design_path.read_text(encoding="utf-8-sig")
|
|
1336
|
-
except OSError:
|
|
1337
|
-
return []
|
|
1338
|
-
frontmatter = _frontmatter(text)
|
|
1339
|
-
if not frontmatter:
|
|
1340
|
-
return []
|
|
1341
|
-
raw = frontmatter.get("capabilities") or ""
|
|
1342
|
-
if not raw:
|
|
1343
|
-
return []
|
|
1344
|
-
# capabilities may be comma-separated or YAML list
|
|
1345
|
-
if raw.startswith("["):
|
|
1346
|
-
try:
|
|
1347
|
-
parsed = json.loads(raw)
|
|
1348
|
-
if isinstance(parsed, list):
|
|
1349
|
-
return [str(c) for c in parsed if isinstance(c, str)]
|
|
1350
|
-
except json.JSONDecodeError:
|
|
1351
|
-
pass
|
|
1352
|
-
return [c.strip() for c in raw.split(",") if c.strip()]
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
def _reclassify_gate_policy(
|
|
1356
|
-
staging: Path,
|
|
1357
|
-
change_name: str,
|
|
1358
|
-
) -> dict[str, Any]:
|
|
1359
|
-
"""Reclassify gate policy based on approved design capabilities (retro §5.4).
|
|
1360
|
-
|
|
1361
|
-
Reads design frontmatter `capabilities`, invokes harness_gate.py classify
|
|
1362
|
-
to recompute the gate DAG, and updates staging/meta/gate-policy.json.
|
|
1363
|
-
Returns {"ok": bool, "capabilities": [...], "drift": bool}.
|
|
1364
|
-
"""
|
|
1365
|
-
capabilities = _read_design_capabilities(staging, change_name)
|
|
1366
|
-
if not capabilities:
|
|
1367
|
-
return {"ok": True, "capabilities": [], "drift": False, "updated": False}
|
|
1368
977
|
|
|
1369
|
-
gate_policy_path = staging / "meta" / "gate-policy.json"
|
|
1370
|
-
try:
|
|
1371
|
-
existing = json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
|
|
1372
|
-
except (OSError, json.JSONDecodeError):
|
|
1373
|
-
existing = {}
|
|
1374
978
|
|
|
1375
|
-
existing_caps = set(existing.get("capabilities") or [])
|
|
1376
|
-
design_caps = set(capabilities)
|
|
1377
|
-
drift = existing_caps != design_caps
|
|
1378
979
|
|
|
1379
|
-
if drift:
|
|
1380
|
-
# Update gate-policy.json with design capabilities
|
|
1381
|
-
existing["capabilities"] = sorted(design_caps)
|
|
1382
|
-
try:
|
|
1383
|
-
gate_policy_path.write_text(
|
|
1384
|
-
json.dumps(existing, ensure_ascii=False, indent=2) + "\n",
|
|
1385
|
-
encoding="utf-8",
|
|
1386
|
-
newline="\n",
|
|
1387
|
-
)
|
|
1388
|
-
except OSError as exc:
|
|
1389
|
-
return {
|
|
1390
|
-
"ok": False,
|
|
1391
|
-
"error": f"failed to update gate-policy.json: {exc}",
|
|
1392
|
-
"capabilities": capabilities,
|
|
1393
|
-
"drift": True,
|
|
1394
|
-
"updated": False,
|
|
1395
|
-
}
|
|
1396
980
|
|
|
1397
|
-
return {
|
|
1398
|
-
"ok": True,
|
|
1399
|
-
"capabilities": sorted(design_caps),
|
|
1400
|
-
"drift": drift,
|
|
1401
|
-
"updated": drift,
|
|
1402
|
-
}
|
|
1403
981
|
|
|
1404
982
|
|
|
1405
|
-
def _append_terminal(change_dir: Path, run_id: str, attempt: int) -> tuple[int, str]:
|
|
1406
|
-
stdout = io.StringIO()
|
|
1407
|
-
stderr = io.StringIO()
|
|
1408
|
-
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
1409
|
-
code = harness_events.main(
|
|
1410
|
-
[
|
|
1411
|
-
"append",
|
|
1412
|
-
"--change-dir",
|
|
1413
|
-
str(change_dir),
|
|
1414
|
-
"--phase",
|
|
1415
|
-
"plan",
|
|
1416
|
-
"--type",
|
|
1417
|
-
"phase.end",
|
|
1418
|
-
"--status",
|
|
1419
|
-
"OK",
|
|
1420
|
-
"--reason",
|
|
1421
|
-
"plan artifacts validated and published",
|
|
1422
|
-
"--run-id",
|
|
1423
|
-
run_id,
|
|
1424
|
-
"--attempt",
|
|
1425
|
-
str(attempt),
|
|
1426
|
-
"--json",
|
|
1427
|
-
]
|
|
1428
|
-
)
|
|
1429
|
-
return code, stderr.getvalue().strip()
|
|
1430
|
-
|
|
1431
983
|
|
|
1432
|
-
def _rollback_publish(
|
|
1433
|
-
created: list[Path], replaced: list[tuple[Path, bytes]]
|
|
1434
|
-
) -> None:
|
|
1435
|
-
"""Undo a failed publish: delete new files, restore overwritten ones."""
|
|
1436
|
-
for target in reversed(created):
|
|
1437
|
-
target.unlink(missing_ok=True)
|
|
1438
|
-
for target, original in reversed(replaced):
|
|
1439
|
-
try:
|
|
1440
|
-
target.write_bytes(original)
|
|
1441
|
-
except OSError:
|
|
1442
|
-
# Best effort: the caller already reports the publish failure.
|
|
1443
|
-
pass
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
def _plan_attempts(change_dir: Path) -> list[int]:
|
|
1447
|
-
"""Every attempt number already used by a plan lifecycle event."""
|
|
1448
|
-
try:
|
|
1449
|
-
events = harness_events.load_events(harness_events.events_path(change_dir))
|
|
1450
|
-
except (OSError, ValueError):
|
|
1451
|
-
return []
|
|
1452
|
-
attempts: list[int] = []
|
|
1453
|
-
for event in events:
|
|
1454
|
-
if event.get("phase") != "plan":
|
|
1455
|
-
continue
|
|
1456
|
-
value = event.get("attempt")
|
|
1457
|
-
if isinstance(value, int) and not isinstance(value, bool):
|
|
1458
|
-
attempts.append(value)
|
|
1459
|
-
return attempts
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
def _run_id_in_use(change_dir: Path, run_id: str) -> bool:
|
|
1463
|
-
try:
|
|
1464
|
-
events = harness_events.load_events(harness_events.events_path(change_dir))
|
|
1465
|
-
except (OSError, ValueError):
|
|
1466
|
-
return False
|
|
1467
|
-
return any(event.get("run_id") == run_id for event in events)
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
def _append_plan_start(
|
|
1471
|
-
change_dir: Path, run_id: str, attempt: int, reason: str
|
|
1472
|
-
) -> tuple[int, str]:
|
|
1473
|
-
stdout = io.StringIO()
|
|
1474
|
-
stderr = io.StringIO()
|
|
1475
|
-
with contextlib.redirect_stdout(stdout), contextlib.redirect_stderr(stderr):
|
|
1476
|
-
code = harness_events.main(
|
|
1477
|
-
[
|
|
1478
|
-
"append",
|
|
1479
|
-
"--change-dir",
|
|
1480
|
-
str(change_dir),
|
|
1481
|
-
"--phase",
|
|
1482
|
-
"plan",
|
|
1483
|
-
"--type",
|
|
1484
|
-
"phase.start",
|
|
1485
|
-
# phase.start accepts only `note` beyond provenance fields.
|
|
1486
|
-
"--note",
|
|
1487
|
-
reason,
|
|
1488
|
-
"--run-id",
|
|
1489
|
-
run_id,
|
|
1490
|
-
"--attempt",
|
|
1491
|
-
str(attempt),
|
|
1492
|
-
"--json",
|
|
1493
|
-
]
|
|
1494
|
-
)
|
|
1495
|
-
return code, stderr.getvalue().strip()
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
def finalize_plan(
|
|
1499
|
-
change_dir: Path,
|
|
1500
|
-
staging: Path,
|
|
1501
|
-
*,
|
|
1502
|
-
change_name: str,
|
|
1503
|
-
run_id: str,
|
|
1504
|
-
attempt: int,
|
|
1505
|
-
allow_amend: bool = False,
|
|
1506
|
-
amend_reason: str | None = None,
|
|
1507
|
-
) -> dict[str, Any]:
|
|
1508
|
-
change_dir = change_dir.resolve()
|
|
1509
|
-
staging = staging.resolve()
|
|
1510
|
-
|
|
1511
|
-
# C2 (retro §5.4): reclassify gate policy based on approved design capabilities.
|
|
1512
|
-
# This updates staging/meta/gate-policy.json before validation, so the hash
|
|
1513
|
-
# reflects the final gate policy. Drift between design capabilities and
|
|
1514
|
-
# gate-policy capabilities is resolved in favor of the approved design.
|
|
1515
|
-
reclassify = _reclassify_gate_policy(staging, change_name)
|
|
1516
|
-
if not reclassify.get("ok"):
|
|
1517
|
-
return _result_error("CAPABILITY_GATE_DRIFT", reclassify.get("error", "reclassify failed"))
|
|
1518
|
-
|
|
1519
|
-
validation = validate_staging(staging, change_name)
|
|
1520
|
-
if not validation["ok"]:
|
|
1521
|
-
return validation
|
|
1522
|
-
ownership_validation = validate_product_ownership(change_dir, staging)
|
|
1523
|
-
if not ownership_validation["ok"]:
|
|
1524
|
-
return ownership_validation
|
|
1525
|
-
start_validation = _validate_plan_start(change_dir, run_id, attempt)
|
|
1526
|
-
if not start_validation["ok"]:
|
|
1527
|
-
return start_validation
|
|
1528
|
-
|
|
1529
|
-
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
1530
|
-
lock_path = change_dir / "meta" / "plan-finalize.lock"
|
|
1531
|
-
receipt: dict[str, Any] | None = None
|
|
1532
|
-
supersedes: dict[str, Any] | None = None
|
|
1533
|
-
if receipt_path.is_file():
|
|
1534
|
-
try:
|
|
1535
|
-
loaded = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
1536
|
-
receipt = loaded if isinstance(loaded, dict) else None
|
|
1537
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
1538
|
-
return _result_error("PLAN_FINALIZATION_RECEIPT_INVALID", str(exc))
|
|
1539
|
-
if receipt and receipt.get("artifactsHash") != validation["artifactsHash"]:
|
|
1540
|
-
if not allow_amend:
|
|
1541
|
-
return _result_error(
|
|
1542
|
-
"PLAN_FINALIZATION_HASH_CONFLICT",
|
|
1543
|
-
"finalizer was already invoked with a different artifact set; "
|
|
1544
|
-
"to publish an approved amendment run: harness_plan_finalize.py "
|
|
1545
|
-
"republish --change-dir <dir> --staging-dir <dir> --change "
|
|
1546
|
-
f"{change_name} --run-id <fresh-run-id> --reason \"<why>\"",
|
|
1547
|
-
)
|
|
1548
|
-
# Authorized amendment: keep the superseded identity in the new
|
|
1549
|
-
# receipt so the published history stays auditable.
|
|
1550
|
-
supersedes = {
|
|
1551
|
-
"artifactsHash": receipt.get("artifactsHash"),
|
|
1552
|
-
"runId": receipt.get("runId"),
|
|
1553
|
-
"attempt": receipt.get("attempt"),
|
|
1554
|
-
}
|
|
1555
|
-
elif (
|
|
1556
|
-
receipt
|
|
1557
|
-
and receipt.get("status") == "finalized"
|
|
1558
|
-
and _terminal_exists(change_dir, run_id, attempt)
|
|
1559
|
-
):
|
|
1560
|
-
return {
|
|
1561
|
-
"ok": True,
|
|
1562
|
-
"action": "finalize",
|
|
1563
|
-
"idempotent": True,
|
|
1564
|
-
"artifactsHash": validation["artifactsHash"],
|
|
1565
|
-
"files": validation["files"],
|
|
1566
|
-
"receiptPath": str(receipt_path),
|
|
1567
|
-
"artifactRef": "meta/plan-finalization.json",
|
|
1568
|
-
}
|
|
1569
|
-
|
|
1570
|
-
lock_path.parent.mkdir(parents=True, exist_ok=True)
|
|
1571
|
-
try:
|
|
1572
|
-
lock_fd = os.open(str(lock_path), os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
1573
|
-
os.close(lock_fd)
|
|
1574
|
-
except FileExistsError:
|
|
1575
|
-
return _result_error("PLAN_FINALIZATION_LOCKED", f"lock exists: {lock_path}")
|
|
1576
|
-
|
|
1577
|
-
created: list[Path] = []
|
|
1578
|
-
# Amendment overwrites replace already-published bytes. Keep the originals
|
|
1579
|
-
# so a failed publish restores them instead of deleting the published plan.
|
|
1580
|
-
replaced: list[tuple[Path, bytes]] = []
|
|
1581
|
-
terminal_committed = False
|
|
1582
|
-
try:
|
|
1583
|
-
for rel_text in validation["files"]:
|
|
1584
|
-
source = staging / rel_text
|
|
1585
|
-
target = change_dir / rel_text
|
|
1586
|
-
if (
|
|
1587
|
-
not allow_amend
|
|
1588
|
-
and target.exists()
|
|
1589
|
-
and target.read_bytes() != source.read_bytes()
|
|
1590
|
-
):
|
|
1591
|
-
return _result_error(
|
|
1592
|
-
"PLAN_TARGET_CONFLICT",
|
|
1593
|
-
f"refusing to overwrite {rel_text}; publish an approved "
|
|
1594
|
-
"amendment with the `republish` subcommand instead",
|
|
1595
|
-
)
|
|
1596
984
|
|
|
1597
|
-
for rel_text in validation["files"]:
|
|
1598
|
-
source = staging / rel_text
|
|
1599
|
-
target = change_dir / rel_text
|
|
1600
|
-
existed = target.exists()
|
|
1601
|
-
if existed:
|
|
1602
|
-
original = target.read_bytes()
|
|
1603
|
-
if not allow_amend or original == source.read_bytes():
|
|
1604
|
-
continue
|
|
1605
|
-
target.parent.mkdir(parents=True, exist_ok=True)
|
|
1606
|
-
fd, raw_tmp = tempfile.mkstemp(
|
|
1607
|
-
prefix=f".{target.name}.", suffix=".tmp", dir=str(target.parent)
|
|
1608
|
-
)
|
|
1609
|
-
os.close(fd)
|
|
1610
|
-
tmp = Path(raw_tmp)
|
|
1611
|
-
try:
|
|
1612
|
-
shutil.copyfile(source, tmp)
|
|
1613
|
-
os.replace(tmp, target)
|
|
1614
|
-
if existed:
|
|
1615
|
-
replaced.append((target, original))
|
|
1616
|
-
else:
|
|
1617
|
-
created.append(target)
|
|
1618
|
-
finally:
|
|
1619
|
-
tmp.unlink(missing_ok=True)
|
|
1620
|
-
|
|
1621
|
-
# C8: write implementation-checkpoints.json with parsed task ownerPhase.
|
|
1622
|
-
tasks = validation.get("tasks") or []
|
|
1623
|
-
if tasks:
|
|
1624
|
-
checkpoints_path = change_dir / "meta" / "implementation-checkpoints.json"
|
|
1625
|
-
checkpoints_payload = {
|
|
1626
|
-
"schemaVersion": 1,
|
|
1627
|
-
"changeName": change_name,
|
|
1628
|
-
"tasks": tasks,
|
|
1629
|
-
"foundationGate": "approved",
|
|
1630
|
-
}
|
|
1631
|
-
if checkpoints_path.is_file():
|
|
1632
|
-
replaced.append((checkpoints_path, checkpoints_path.read_bytes()))
|
|
1633
|
-
else:
|
|
1634
|
-
created.append(checkpoints_path)
|
|
1635
|
-
_atomic_write_json(checkpoints_path, checkpoints_payload)
|
|
1636
|
-
|
|
1637
|
-
# C9: write scenario-manifest.json with parsed scenarios.
|
|
1638
|
-
scenarios = validation.get("scenarios") or []
|
|
1639
|
-
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
1640
|
-
manifest_payload = {
|
|
1641
|
-
"schemaVersion": scenario_manifest_schema_version(scenarios),
|
|
1642
|
-
"changeName": change_name,
|
|
1643
|
-
"scenarios": [
|
|
1644
|
-
{
|
|
1645
|
-
key: value
|
|
1646
|
-
for key, value in scenario.items()
|
|
1647
|
-
if key != "executableMappingDeclared"
|
|
1648
|
-
}
|
|
1649
|
-
for scenario in scenarios
|
|
1650
|
-
],
|
|
1651
|
-
}
|
|
1652
|
-
if manifest_path.is_file():
|
|
1653
|
-
replaced.append((manifest_path, manifest_path.read_bytes()))
|
|
1654
|
-
else:
|
|
1655
|
-
created.append(manifest_path)
|
|
1656
|
-
_atomic_write_json(manifest_path, manifest_payload)
|
|
1657
|
-
|
|
1658
|
-
pending_receipt = {
|
|
1659
|
-
"schemaVersion": SCHEMA_VERSION,
|
|
1660
|
-
"changeName": change_name,
|
|
1661
|
-
"status": "publishing",
|
|
1662
|
-
"artifactsHash": validation["artifactsHash"],
|
|
1663
|
-
"files": validation["files"],
|
|
1664
|
-
"runId": run_id,
|
|
1665
|
-
"attempt": attempt,
|
|
1666
|
-
}
|
|
1667
|
-
if supersedes is not None:
|
|
1668
|
-
pending_receipt["supersedes"] = supersedes
|
|
1669
|
-
pending_receipt["amendReason"] = amend_reason or ""
|
|
1670
|
-
_atomic_write_json(receipt_path, pending_receipt)
|
|
1671
|
-
terminal_code, terminal_error = _append_terminal(change_dir, run_id, attempt)
|
|
1672
|
-
terminal_committed = _terminal_exists(change_dir, run_id, attempt)
|
|
1673
|
-
if terminal_code != 0 and not terminal_committed:
|
|
1674
|
-
_rollback_publish(created, replaced)
|
|
1675
|
-
receipt_path.unlink(missing_ok=True)
|
|
1676
|
-
return _result_error(
|
|
1677
|
-
"PLAN_TERMINAL_APPEND_FAILED",
|
|
1678
|
-
terminal_error or "phase.end append failed",
|
|
1679
|
-
)
|
|
1680
|
-
pending_receipt["status"] = "finalized"
|
|
1681
|
-
_atomic_write_json(receipt_path, pending_receipt)
|
|
1682
|
-
result: dict[str, Any] = {
|
|
1683
|
-
"ok": True,
|
|
1684
|
-
"action": "republish" if supersedes is not None else "finalize",
|
|
1685
|
-
"idempotent": False,
|
|
1686
|
-
"artifactsHash": validation["artifactsHash"],
|
|
1687
|
-
"files": validation["files"],
|
|
1688
|
-
"receiptPath": str(receipt_path),
|
|
1689
|
-
"artifactRef": "meta/plan-finalization.json",
|
|
1690
|
-
"executionLogPath": str(harness_events.execution_log_path(change_dir)),
|
|
1691
|
-
}
|
|
1692
|
-
if supersedes is not None:
|
|
1693
|
-
result["supersedes"] = supersedes
|
|
1694
|
-
result["scenarioCount"] = len(scenarios)
|
|
1695
|
-
result["taskCount"] = len(tasks)
|
|
1696
|
-
return result
|
|
1697
|
-
except OSError as exc:
|
|
1698
|
-
if terminal_committed or _terminal_exists(change_dir, run_id, attempt):
|
|
1699
|
-
return _result_error(
|
|
1700
|
-
"PLAN_FINALIZATION_RECOVERY_REQUIRED",
|
|
1701
|
-
f"terminal committed; retry finalization to complete receipt: {exc}",
|
|
1702
|
-
)
|
|
1703
|
-
_rollback_publish(created, replaced)
|
|
1704
|
-
receipt_path.unlink(missing_ok=True)
|
|
1705
|
-
return _result_error("PLAN_FINALIZATION_IO_ERROR", str(exc))
|
|
1706
|
-
finally:
|
|
1707
|
-
lock_path.unlink(missing_ok=True)
|
|
1708
985
|
|
|
1709
986
|
|
|
1710
|
-
def republish_plan(
|
|
1711
|
-
change_dir: Path,
|
|
1712
|
-
staging: Path,
|
|
1713
|
-
*,
|
|
1714
|
-
change_name: str,
|
|
1715
|
-
run_id: str,
|
|
1716
|
-
reason: str,
|
|
1717
|
-
) -> dict[str, Any]:
|
|
1718
|
-
"""Publish an approved amendment to an already-finalized plan.
|
|
1719
|
-
|
|
1720
|
-
Amending a published plan (adding a scenario, correcting a task) used to
|
|
1721
|
-
require reverse-engineering a five-step lifecycle dance across three
|
|
1722
|
-
modules — new run id, incremented attempt, stale receipt removal, dangling
|
|
1723
|
-
attempt sealing — and every wrong turn produced a different error code
|
|
1724
|
-
(PLAN_FINALIZATION_HASH_CONFLICT / PHASE_START_MISSING /
|
|
1725
|
-
EVENT_ATTEMPT_CONFLICT / PHASE_ALREADY_CLOSED). Agents that gave up
|
|
1726
|
-
hand-edited meta/scenario-manifest.json, creating exactly the drift the
|
|
1727
|
-
hash guard exists to prevent.
|
|
1728
|
-
|
|
1729
|
-
This performs the whole sequence atomically and keeps the superseded
|
|
1730
|
-
identity in the receipt, so amendment stays auditable rather than silent.
|
|
1731
|
-
"""
|
|
1732
|
-
change_dir = change_dir.resolve()
|
|
1733
|
-
staging = staging.resolve()
|
|
1734
987
|
|
|
1735
|
-
if not str(reason).strip():
|
|
1736
|
-
return _result_error(
|
|
1737
|
-
"PLAN_AMEND_REASON_REQUIRED",
|
|
1738
|
-
"--reason is required: an amendment to a published plan must record why",
|
|
1739
|
-
)
|
|
1740
988
|
|
|
1741
|
-
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
1742
|
-
if not receipt_path.is_file():
|
|
1743
|
-
return _result_error(
|
|
1744
|
-
"PLAN_NOT_FINALIZED",
|
|
1745
|
-
"no plan-finalization.json to amend; use `finalize` for the first "
|
|
1746
|
-
"publication",
|
|
1747
|
-
)
|
|
1748
989
|
|
|
1749
|
-
if _run_id_in_use(change_dir, run_id):
|
|
1750
|
-
return _result_error(
|
|
1751
|
-
"PLAN_AMEND_RUN_ID_IN_USE",
|
|
1752
|
-
f"run id {run_id} already appears in this change's events; "
|
|
1753
|
-
"an amendment needs a fresh run id",
|
|
1754
|
-
)
|
|
1755
990
|
|
|
1756
|
-
# Nothing to amend is a no-op, not a new attempt: probe the staged hash
|
|
1757
|
-
# before writing any lifecycle event.
|
|
1758
|
-
reclassify = _reclassify_gate_policy(staging, change_name)
|
|
1759
|
-
if not reclassify.get("ok"):
|
|
1760
|
-
return _result_error(
|
|
1761
|
-
"CAPABILITY_GATE_DRIFT", reclassify.get("error", "reclassify failed")
|
|
1762
|
-
)
|
|
1763
|
-
probe = validate_staging(staging, change_name)
|
|
1764
|
-
if not probe["ok"]:
|
|
1765
|
-
return probe
|
|
1766
|
-
try:
|
|
1767
|
-
published = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
1768
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
1769
|
-
return _result_error("PLAN_FINALIZATION_RECEIPT_INVALID", str(exc))
|
|
1770
|
-
if (
|
|
1771
|
-
isinstance(published, dict)
|
|
1772
|
-
and published.get("status") == "finalized"
|
|
1773
|
-
and published.get("artifactsHash") == probe["artifactsHash"]
|
|
1774
|
-
):
|
|
1775
|
-
return {
|
|
1776
|
-
"ok": True,
|
|
1777
|
-
"action": "republish",
|
|
1778
|
-
"idempotent": True,
|
|
1779
|
-
"artifactsHash": probe["artifactsHash"],
|
|
1780
|
-
"files": probe["files"],
|
|
1781
|
-
"receiptPath": str(receipt_path),
|
|
1782
|
-
"artifactRef": "meta/plan-finalization.json",
|
|
1783
|
-
"note": "published plan already matches staging; nothing to amend",
|
|
1784
|
-
}
|
|
1785
991
|
|
|
1786
|
-
attempt = max(_plan_attempts(change_dir), default=0) + 1
|
|
1787
|
-
start_code, start_error = _append_plan_start(
|
|
1788
|
-
change_dir, run_id, attempt, f"plan amendment: {reason}"
|
|
1789
|
-
)
|
|
1790
|
-
if start_code != 0:
|
|
1791
|
-
return _result_error(
|
|
1792
|
-
"PLAN_AMEND_START_FAILED",
|
|
1793
|
-
start_error or "failed to append plan phase.start for the amendment",
|
|
1794
|
-
)
|
|
1795
992
|
|
|
1796
|
-
result = finalize_plan(
|
|
1797
|
-
change_dir,
|
|
1798
|
-
staging,
|
|
1799
|
-
change_name=change_name,
|
|
1800
|
-
run_id=run_id,
|
|
1801
|
-
attempt=attempt,
|
|
1802
|
-
allow_amend=True,
|
|
1803
|
-
amend_reason=reason,
|
|
1804
|
-
)
|
|
1805
|
-
if isinstance(result, dict):
|
|
1806
|
-
result.setdefault("runId", run_id)
|
|
1807
|
-
result.setdefault("attempt", attempt)
|
|
1808
|
-
return result
|
|
1809
993
|
|
|
1810
994
|
|
|
1811
995
|
def build_parser() -> argparse.ArgumentParser:
|
|
1812
996
|
parser = argparse.ArgumentParser(prog="harness_plan_finalize.py")
|
|
1813
997
|
sub = parser.add_subparsers(dest="command", required=True)
|
|
1814
|
-
republish = sub.add_parser(
|
|
1815
|
-
"republish",
|
|
1816
|
-
help=(
|
|
1817
|
-
"publish an approved amendment to an already-finalized plan "
|
|
1818
|
-
"(allocates a fresh attempt and records what it supersedes)"
|
|
1819
|
-
),
|
|
1820
|
-
)
|
|
1821
|
-
republish.add_argument("--change-dir", required=True)
|
|
1822
|
-
republish.add_argument("--staging-dir", required=True)
|
|
1823
|
-
republish.add_argument("--change", required=True)
|
|
1824
|
-
republish.add_argument(
|
|
1825
|
-
"--run-id", required=True, help="a fresh run id, unused by this change"
|
|
1826
|
-
)
|
|
1827
|
-
republish.add_argument(
|
|
1828
|
-
"--reason", required=True, help="why the published plan is being amended"
|
|
1829
|
-
)
|
|
1830
|
-
republish.add_argument("--json", action="store_true")
|
|
1831
|
-
finalize = sub.add_parser("finalize")
|
|
1832
|
-
finalize.add_argument("--change-dir", required=True)
|
|
1833
|
-
finalize.add_argument("--staging-dir", required=True)
|
|
1834
|
-
finalize.add_argument("--change", required=True)
|
|
1835
|
-
finalize.add_argument("--run-id", required=True)
|
|
1836
|
-
finalize.add_argument("--attempt", required=True, type=int)
|
|
1837
|
-
finalize.add_argument("--json", action="store_true")
|
|
1838
998
|
verify = sub.add_parser("verify")
|
|
1839
999
|
verify.add_argument("--change-dir", required=True)
|
|
1840
1000
|
verify.add_argument("--json", action="store_true")
|
|
@@ -1843,24 +1003,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1843
1003
|
|
|
1844
1004
|
def main(argv: list[str] | None = None) -> int:
|
|
1845
1005
|
args = build_parser().parse_args(argv)
|
|
1846
|
-
|
|
1847
|
-
result = verify_plan(Path(args.change_dir))
|
|
1848
|
-
elif args.command == "republish":
|
|
1849
|
-
result = republish_plan(
|
|
1850
|
-
Path(args.change_dir),
|
|
1851
|
-
Path(args.staging_dir),
|
|
1852
|
-
change_name=args.change,
|
|
1853
|
-
run_id=args.run_id,
|
|
1854
|
-
reason=args.reason,
|
|
1855
|
-
)
|
|
1856
|
-
else:
|
|
1857
|
-
result = finalize_plan(
|
|
1858
|
-
Path(args.change_dir),
|
|
1859
|
-
Path(args.staging_dir),
|
|
1860
|
-
change_name=args.change,
|
|
1861
|
-
run_id=args.run_id,
|
|
1862
|
-
attempt=args.attempt,
|
|
1863
|
-
)
|
|
1006
|
+
result = verify_plan(Path(args.change_dir))
|
|
1864
1007
|
stream = sys.stdout if result["ok"] else sys.stderr
|
|
1865
1008
|
if args.json:
|
|
1866
1009
|
stream.write(json.dumps(result, ensure_ascii=False, indent=2) + "\n")
|