@hunter-harness/workflow-harness 0.2.84 → 0.2.86
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/harness-archive/SKILL.md +11 -4
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -2
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +18 -79
- package/harness/bundles/general/claude-code/harness-plan/protocols.md +3 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +6 -4
- package/harness/bundles/general/claude-code/harness-submit/SKILL.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 +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +58 -25
- package/harness/bundles/general/claude-code/scripts/harness_change.py +25 -7
- package/harness/bundles/general/claude-code/scripts/harness_context.py +62 -19
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +4 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +208 -72
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +43 -0
- package/harness/bundles/general/claude-code/scripts/harness_paths.py +60 -0
- package/harness/bundles/general/claude-code/scripts/harness_phase.py +24 -6
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/general/claude-code/scripts/harness_review.py +25 -4
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +11 -4
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -2
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +18 -79
- package/harness/bundles/general/codebuddy/harness-plan/protocols.md +3 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +6 -4
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +58 -25
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +25 -7
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +62 -19
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +4 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +208 -72
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +43 -0
- package/harness/bundles/general/codebuddy/scripts/harness_paths.py +60 -0
- package/harness/bundles/general/codebuddy/scripts/harness_phase.py +24 -6
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +25 -4
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +11 -4
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -2
- package/harness/bundles/general/codex/harness-plan/checklist.md +18 -79
- package/harness/bundles/general/codex/harness-plan/protocols.md +3 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +6 -4
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +58 -25
- package/harness/bundles/general/codex/scripts/harness_change.py +25 -7
- package/harness/bundles/general/codex/scripts/harness_context.py +62 -19
- package/harness/bundles/general/codex/scripts/harness_fixback.py +4 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +208 -72
- package/harness/bundles/general/codex/scripts/harness_ledger.py +43 -0
- package/harness/bundles/general/codex/scripts/harness_paths.py +60 -0
- package/harness/bundles/general/codex/scripts/harness_phase.py +24 -6
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/general/codex/scripts/harness_review.py +25 -4
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +11 -4
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -2
- package/harness/bundles/general/cursor/harness-plan/checklist.md +18 -79
- package/harness/bundles/general/cursor/harness-plan/protocols.md +3 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +6 -4
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +58 -25
- package/harness/bundles/general/cursor/scripts/harness_change.py +25 -7
- package/harness/bundles/general/cursor/scripts/harness_context.py +62 -19
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +4 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +208 -72
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +43 -0
- package/harness/bundles/general/cursor/scripts/harness_paths.py +60 -0
- package/harness/bundles/general/cursor/scripts/harness_phase.py +24 -6
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/general/cursor/scripts/harness_review.py +25 -4
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +11 -4
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -2
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +18 -79
- package/harness/bundles/java/claude-code/harness-plan/protocols.md +3 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +6 -4
- package/harness/bundles/java/claude-code/harness-submit/SKILL.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 +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +58 -25
- package/harness/bundles/java/claude-code/scripts/harness_change.py +25 -7
- package/harness/bundles/java/claude-code/scripts/harness_context.py +62 -19
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +4 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +208 -72
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +43 -0
- package/harness/bundles/java/claude-code/scripts/harness_paths.py +60 -0
- package/harness/bundles/java/claude-code/scripts/harness_phase.py +24 -6
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/java/claude-code/scripts/harness_review.py +25 -4
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +11 -4
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -2
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +18 -79
- package/harness/bundles/java/codebuddy/harness-plan/protocols.md +3 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +6 -4
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +58 -25
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +25 -7
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +62 -19
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +4 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +208 -72
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +43 -0
- package/harness/bundles/java/codebuddy/scripts/harness_paths.py +60 -0
- package/harness/bundles/java/codebuddy/scripts/harness_phase.py +24 -6
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +25 -4
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +11 -4
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -2
- package/harness/bundles/java/codex/harness-plan/checklist.md +18 -79
- package/harness/bundles/java/codex/harness-plan/protocols.md +3 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +6 -4
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +58 -25
- package/harness/bundles/java/codex/scripts/harness_change.py +25 -7
- package/harness/bundles/java/codex/scripts/harness_context.py +62 -19
- package/harness/bundles/java/codex/scripts/harness_fixback.py +4 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +208 -72
- package/harness/bundles/java/codex/scripts/harness_ledger.py +43 -0
- package/harness/bundles/java/codex/scripts/harness_paths.py +60 -0
- package/harness/bundles/java/codex/scripts/harness_phase.py +24 -6
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/java/codex/scripts/harness_review.py +25 -4
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +11 -4
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -2
- package/harness/bundles/java/cursor/harness-plan/checklist.md +18 -79
- package/harness/bundles/java/cursor/harness-plan/protocols.md +3 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +37 -8
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +6 -4
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +58 -25
- package/harness/bundles/java/cursor/scripts/harness_change.py +25 -7
- package/harness/bundles/java/cursor/scripts/harness_context.py +62 -19
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +4 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +208 -72
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +43 -0
- package/harness/bundles/java/cursor/scripts/harness_paths.py +60 -0
- package/harness/bundles/java/cursor/scripts/harness_phase.py +24 -6
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +124 -1
- package/harness/bundles/java/cursor/scripts/harness_review.py +25 -4
- package/harness/manifests/general/claude-code.json +27 -27
- package/harness/manifests/general/codebuddy.json +27 -27
- package/harness/manifests/general/codex.json +27 -27
- package/harness/manifests/general/cursor.json +27 -27
- package/harness/manifests/java/claude-code.json +29 -29
- package/harness/manifests/java/codebuddy.json +29 -29
- package/harness/manifests/java/codex.json +29 -29
- package/harness/manifests/java/cursor.json +29 -29
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -157,6 +157,47 @@ GATE_WARNINGS_REL = Path("evidence") / "gate-warnings.ndjson"
|
|
|
157
157
|
# phase close. Must stay a superset-ordering of hpf.VALID_OWNER_PHASES.
|
|
158
158
|
SCENARIO_OWNER_PHASE_ORDER = ("plan", "run", "test", "review", "submit")
|
|
159
159
|
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# 阶段能力表:每个阶段在 begin/close 里额外做什么,集中在这里声明。
|
|
162
|
+
#
|
|
163
|
+
# 这些开关以前是 cmd_begin / cmd_close 里散落的 `if args.phase == ...` —— 两个
|
|
164
|
+
# 五百行的函数,想知道"test 阶段关门时到底跑哪几项"要通读全文。更要紧的是
|
|
165
|
+
# run/test/review 的差异被摊平在控制流里,看不出它们其实共用同一套生命周期。
|
|
166
|
+
#
|
|
167
|
+
# 键的含义:
|
|
168
|
+
# plan_handoff begin 时校验 plan 已 finalize(只有进入实现的第一个阶段需要)
|
|
169
|
+
# test_guard begin 建测试基线快照、close 比对(改测试的阶段才需要)
|
|
170
|
+
# scenario_coverage close 时跑 C9 场景覆盖
|
|
171
|
+
# ledger_blocking close 时 ledger 校验失败是否阻断(否则只进 payload)
|
|
172
|
+
# review_outputs close 时校验 review sidecar 与 runId 绑定
|
|
173
|
+
# head_may_advance capsule 校验允许 HEAD 前移(会产生 commit 的阶段)
|
|
174
|
+
# projection_drift close 时 projection receipt 变化即硬失败(外发边界)
|
|
175
|
+
PHASE_GATE_RULES: dict[str, frozenset[str]] = {
|
|
176
|
+
"plan": frozenset(),
|
|
177
|
+
"run": frozenset({
|
|
178
|
+
"plan_handoff", "test_guard", "scenario_coverage",
|
|
179
|
+
"ledger_blocking", "head_may_advance",
|
|
180
|
+
}),
|
|
181
|
+
"test": frozenset({"test_guard", "scenario_coverage", "ledger_blocking"}),
|
|
182
|
+
"review": frozenset({"review_outputs"}),
|
|
183
|
+
"package": frozenset({"ledger_blocking"}),
|
|
184
|
+
"apidoc": frozenset(),
|
|
185
|
+
"submit": frozenset({"head_may_advance", "projection_drift"}),
|
|
186
|
+
"merge": frozenset({"head_may_advance"}),
|
|
187
|
+
"archive": frozenset({"projection_drift"}),
|
|
188
|
+
}
|
|
189
|
+
# 不在 WORKFLOW_PHASES 里、但 projection 门禁按发布阶段对待的名字。
|
|
190
|
+
_EXTRA_PROJECTION_DRIFT_PHASES = frozenset({"release", "deploy"})
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def phase_gate_rule(phase: str | None, rule: str) -> bool:
|
|
194
|
+
"""这个阶段是否启用某项门禁能力。未知阶段一律不启用(fail-safe)。"""
|
|
195
|
+
name = str(phase or "")
|
|
196
|
+
if rule == "projection_drift" and name in _EXTRA_PROJECTION_DRIFT_PHASES:
|
|
197
|
+
return True
|
|
198
|
+
resolved = hp.resolve_phase_name(name) or name
|
|
199
|
+
return rule in PHASE_GATE_RULES.get(resolved, frozenset())
|
|
200
|
+
|
|
160
201
|
|
|
161
202
|
def gate_severity_mode(project: Path, change_dir: Path | None = None) -> str:
|
|
162
203
|
"""Resolve gate severity mode: env > change gate-policy > project config.
|
|
@@ -554,19 +595,39 @@ def load_checkpoints(change_dir: Path) -> dict[str, Any] | None:
|
|
|
554
595
|
data = _read_json(path)
|
|
555
596
|
except (OSError, json.JSONDecodeError):
|
|
556
597
|
return None
|
|
598
|
+
# 刻意返回**原样**文档:cmd_checkpoint approve 会把这里的返回值写回盘,
|
|
599
|
+
# 返回归一化结构会用合成内容覆盖 v2 的哈希绑定产物(ARTIFACT_HASH_DRIFT)。
|
|
600
|
+
# 形状差异在只读的 checkpoint_status 里消化。
|
|
557
601
|
return data if isinstance(data, dict) else None
|
|
558
602
|
|
|
559
603
|
|
|
560
604
|
def checkpoint_status(checkpoints: dict[str, Any] | None, checkpoint_id: str) -> str:
|
|
605
|
+
"""Read a checkpoint's status across the three shapes this file is written in.
|
|
606
|
+
|
|
607
|
+
`meta/implementation-checkpoints.json` 有三个写入者,形状各不相同:
|
|
608
|
+
`harness_change.migrate` 写 ``checkpoints[{id,status}]``;legacy plan finalize 写
|
|
609
|
+
顶层 ``foundationGate``;v2 plan finalize 写 artifact 包装体,状态在
|
|
610
|
+
``content.foundation_gate``。以前只认第一种,另外两种一律返回 "missing"
|
|
611
|
+
——checkpoint 的状态对整条 v2 路径和 legacy finalize 之后都是读不出来的。
|
|
612
|
+
|
|
613
|
+
只读归一化:调用方拿到的是状态,不是文档。写回路径(cmd_checkpoint approve)
|
|
614
|
+
仍然操作原始文档,不能用归一化结构覆盖哈希绑定的 v2 产物。
|
|
615
|
+
"""
|
|
561
616
|
if not checkpoints:
|
|
562
617
|
return "missing"
|
|
563
618
|
items = checkpoints.get("checkpoints")
|
|
564
|
-
if
|
|
619
|
+
if isinstance(items, list):
|
|
620
|
+
for item in items:
|
|
621
|
+
if isinstance(item, dict) and item.get("id") == checkpoint_id:
|
|
622
|
+
return str(item.get("status") or "pending")
|
|
565
623
|
return "missing"
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
624
|
+
if checkpoint_id != "foundation-gate":
|
|
625
|
+
return "missing"
|
|
626
|
+
unpacked = hpf.unpack_v2_implementation_checkpoints(checkpoints)
|
|
627
|
+
if unpacked is not None:
|
|
628
|
+
return str(unpacked["checkpoints"][0]["status"])
|
|
629
|
+
legacy = checkpoints.get("foundationGate")
|
|
630
|
+
return str(legacy).strip() if str(legacy or "").strip() else "missing"
|
|
570
631
|
|
|
571
632
|
|
|
572
633
|
def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[str, Any] | None:
|
|
@@ -901,8 +962,13 @@ def validate_identity(
|
|
|
901
962
|
core_hash = str(build.get("coreHash") or "").strip()
|
|
902
963
|
if not agent or not core_hash:
|
|
903
964
|
raise ValueError("BUNDLE_IDENTITY_INVALID: agent/coreHash is required")
|
|
904
|
-
|
|
905
|
-
|
|
965
|
+
|
|
966
|
+
# 「这个 bundle 是否可信」和「现在是哪个工具在跑」是两件事。以前
|
|
967
|
+
# executor_tool != agent 直接 BUNDLE_IDENTITY_MISMATCH,于是 Codex 换
|
|
968
|
+
# CodeBuddy 接手同一个 change 会被当成 bundle 身份不符挡下来。下面每一项
|
|
969
|
+
# 校验用的都是 bundle 自称的 agent,与当前工具无关;工具名降级为审计字段,
|
|
970
|
+
# 随 phase.start 事件留痕。
|
|
971
|
+
executor_matches_bundle = not executor_tool or executor_tool == agent
|
|
906
972
|
|
|
907
973
|
context = _read_json(project / ".harness" / "context-index.json")
|
|
908
974
|
installed = _read_json(
|
|
@@ -953,6 +1019,8 @@ def validate_identity(
|
|
|
953
1019
|
"adapter": agent,
|
|
954
1020
|
"buildMarkerHash": actual_hash,
|
|
955
1021
|
"contextIndexPresent": True,
|
|
1022
|
+
"executorTool": executor_tool or None,
|
|
1023
|
+
"executorMatchesBundle": executor_matches_bundle,
|
|
956
1024
|
}
|
|
957
1025
|
|
|
958
1026
|
|
|
@@ -1295,8 +1363,13 @@ def classify_risk(
|
|
|
1295
1363
|
stage: str,
|
|
1296
1364
|
workflow: dict[str, Any] | None = None,
|
|
1297
1365
|
) -> dict[str, Any]:
|
|
1298
|
-
|
|
1299
|
-
|
|
1366
|
+
# 无风险信号时的起步档。曾经是 full,于是每个普通变更都默认背上
|
|
1367
|
+
# plan→run→test→review→submit→archive 六阶段和 apiTest;风险信号推断只在
|
|
1368
|
+
# --stage post-run 下跑,而生产流程没有任何地方调用它,起步值实际就是终值。
|
|
1369
|
+
# standard 保留 compile/unitTest/unitTestFull 的证据闭环,只去掉默认的
|
|
1370
|
+
# review 阶段与 apiTest。下面的单调升级逻辑不变,有信号照样升到 full。
|
|
1371
|
+
tier = "standard"
|
|
1372
|
+
source = "default-standard"
|
|
1300
1373
|
capabilities = _design_capabilities(change_dir)
|
|
1301
1374
|
plan_path = change_dir / "plans"
|
|
1302
1375
|
for candidate in sorted(plan_path.glob("*.md")) if plan_path.is_dir() else []:
|
|
@@ -1551,8 +1624,9 @@ def classify_defaults(
|
|
|
1551
1624
|
change_id: str,
|
|
1552
1625
|
stage: str,
|
|
1553
1626
|
) -> dict[str, Any]:
|
|
1554
|
-
|
|
1555
|
-
|
|
1627
|
+
# change 目录不存在时的兜底,与 classify_risk 的起步档保持一致。
|
|
1628
|
+
tier = "standard"
|
|
1629
|
+
source = "default-standard"
|
|
1556
1630
|
tier_policy = workflow["riskTiers"][tier]
|
|
1557
1631
|
payload = {
|
|
1558
1632
|
"ok": True,
|
|
@@ -1668,6 +1742,7 @@ def append_phase_event(
|
|
|
1668
1742
|
executor_agent: str | None = None,
|
|
1669
1743
|
executor_model: str | None = None,
|
|
1670
1744
|
code: str | None = None,
|
|
1745
|
+
fixback: bool = False,
|
|
1671
1746
|
) -> dict[str, Any]:
|
|
1672
1747
|
"""Skill-facing phase lifecycle append (begin/close use this directly).
|
|
1673
1748
|
|
|
@@ -1720,8 +1795,17 @@ def append_phase_event(
|
|
|
1720
1795
|
execution_mode=None,
|
|
1721
1796
|
decision_reason_code=None,
|
|
1722
1797
|
fallback_reason_code=None,
|
|
1723
|
-
|
|
1724
|
-
|
|
1798
|
+
# 显式信号优先。以前只嗅探 note 文本,而真正的 fixback 启动路径
|
|
1799
|
+
# (harness_fixback._default_gate_begin)写的 note 是
|
|
1800
|
+
# "开始处理评审中确认需要修改的代码问题。",一个 "fixback" 字都没有
|
|
1801
|
+
# ——于是 fixback 的 run 事件从来没被打上 trigger/from_phase。
|
|
1802
|
+
# note 嗅探保留为兼容回退:手工传 --note 带 fixback 的老用法仍然有效。
|
|
1803
|
+
trigger=("fixback" if fixback or "fixback" in note.lower() else None),
|
|
1804
|
+
from_phase=(
|
|
1805
|
+
"review"
|
|
1806
|
+
if phase == "run" and (fixback or "fixback" in note.lower())
|
|
1807
|
+
else None
|
|
1808
|
+
),
|
|
1725
1809
|
result_status=None,
|
|
1726
1810
|
)
|
|
1727
1811
|
event = he.build_event(args, existing)
|
|
@@ -2068,52 +2152,6 @@ def _resolve_project(args: argparse.Namespace) -> Path:
|
|
|
2068
2152
|
return hc.resolve_main_project_root()
|
|
2069
2153
|
|
|
2070
2154
|
|
|
2071
|
-
# 门禁消费的场景字段。v2 plan finalize 派生的 scenario_manifest 一个都没有:
|
|
2072
|
-
# 它只带 scenario_id/coverage_dimension/execution_level/evidence_requirements/
|
|
2073
|
-
# risk_level/task_refs/requirement_refs。
|
|
2074
|
-
_V2_MANIFEST_REQUIRED_FIELDS = (
|
|
2075
|
-
"id",
|
|
2076
|
-
"priority",
|
|
2077
|
-
"requiredEvidenceKind",
|
|
2078
|
-
"ownerPhase",
|
|
2079
|
-
"executableTestId",
|
|
2080
|
-
"testFile",
|
|
2081
|
-
"testTitle",
|
|
2082
|
-
)
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
def _v2_artifact_manifest_gap(manifest: Any) -> dict[str, Any] | None:
|
|
2086
|
-
"""v2 plan artifact 包装的 scenario-manifest → 明确的不可消费错误。
|
|
2087
|
-
|
|
2088
|
-
不能"解包 + 字段改名"了事:包装体里既没有 priority 也没有
|
|
2089
|
-
requiredEvidenceKind,`required_ids` 会算成空集,C9 随即返回
|
|
2090
|
-
NO_LEDGER_REQUIRED_SCENARIOS——那是把证据门禁对所有 v2 计划静默关掉,
|
|
2091
|
-
比直接失败危险得多。所以这里点名缺口,让调用方回规划阶段补齐后重新发布。
|
|
2092
|
-
|
|
2093
|
-
v2 场景契约补齐这些字段后,解包逻辑加在这里。
|
|
2094
|
-
"""
|
|
2095
|
-
if not isinstance(manifest, dict) or manifest.get("artifact_type") != "scenario_manifest":
|
|
2096
|
-
return None
|
|
2097
|
-
content = manifest.get("content")
|
|
2098
|
-
present: set[str] = set()
|
|
2099
|
-
if isinstance(content, dict) and isinstance(content.get("scenarios"), list):
|
|
2100
|
-
for scenario in content["scenarios"]:
|
|
2101
|
-
if isinstance(scenario, dict):
|
|
2102
|
-
present.update(scenario.keys())
|
|
2103
|
-
return {
|
|
2104
|
-
"ok": False,
|
|
2105
|
-
"code": "SCENARIO_MANIFEST_V2_UNSUPPORTED",
|
|
2106
|
-
"message": (
|
|
2107
|
-
"meta/scenario-manifest.json 是 v2 plan artifact 包装体,缺少门禁消费的场景字段;"
|
|
2108
|
-
"请在规划阶段补齐后重新发布,不要手改派生产物"
|
|
2109
|
-
),
|
|
2110
|
-
"missingFields": [
|
|
2111
|
-
field for field in _V2_MANIFEST_REQUIRED_FIELDS if field not in present
|
|
2112
|
-
],
|
|
2113
|
-
"artifactType": "scenario_manifest",
|
|
2114
|
-
}
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
2155
|
def _validate_scenario_coverage(
|
|
2118
2156
|
change_dir: Path, phase: str | None = None
|
|
2119
2157
|
) -> dict[str, Any]:
|
|
@@ -2140,9 +2178,12 @@ def _validate_scenario_coverage(
|
|
|
2140
2178
|
"code": "SCENARIO_MANIFEST_INVALID",
|
|
2141
2179
|
"message": f"scenario-manifest.json unreadable: {exc}",
|
|
2142
2180
|
}
|
|
2143
|
-
|
|
2144
|
-
if
|
|
2145
|
-
|
|
2181
|
+
unpacked = hpf.unpack_v2_scenario_manifest(manifest)
|
|
2182
|
+
if unpacked is not None:
|
|
2183
|
+
if not unpacked.get("ok"):
|
|
2184
|
+
return unpacked
|
|
2185
|
+
# 解包成 legacy 形状后,下面的判定逻辑对 v2 与 legacy 完全一致。
|
|
2186
|
+
manifest = unpacked["manifest"]
|
|
2146
2187
|
scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
|
|
2147
2188
|
if not isinstance(scenarios, list):
|
|
2148
2189
|
return {
|
|
@@ -2514,7 +2555,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
2514
2555
|
severity_mode = gate_severity_mode(project, change_dir)
|
|
2515
2556
|
gate_warnings: list[dict[str, Any]] = []
|
|
2516
2557
|
plan_verification: dict[str, Any] | None = None
|
|
2517
|
-
if args.phase
|
|
2558
|
+
if phase_gate_rule(args.phase, "plan_handoff"):
|
|
2518
2559
|
plan_verification = validate_plan_handoff(change_dir)
|
|
2519
2560
|
if not plan_verification.get("ok"):
|
|
2520
2561
|
code = str(
|
|
@@ -2700,7 +2741,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
2700
2741
|
if capsule is not None:
|
|
2701
2742
|
guard_result = {"ok": True, "code": "SNAPSHOT_REUSED"}
|
|
2702
2743
|
else:
|
|
2703
|
-
if args.phase
|
|
2744
|
+
if phase_gate_rule(args.phase, "test_guard"):
|
|
2704
2745
|
guard_result = htg.begin(execution_root, change_dir)
|
|
2705
2746
|
if not guard_result.get("ok"):
|
|
2706
2747
|
if gate_soft_allowed(severity_mode, args.phase, "test-guard"):
|
|
@@ -2767,6 +2808,7 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
2767
2808
|
phase=args.phase,
|
|
2768
2809
|
type_="phase.start",
|
|
2769
2810
|
note=args.note or "",
|
|
2811
|
+
fixback=bool(getattr(args, "fixback", False)),
|
|
2770
2812
|
identity=identity,
|
|
2771
2813
|
run_id=run_id,
|
|
2772
2814
|
executor_tool=executor_tool,
|
|
@@ -2857,6 +2899,83 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2857
2899
|
|
|
2858
2900
|
explicit_run_id = args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID")
|
|
2859
2901
|
current_lease = hc.inspect_lease(project, resolved["changeId"])
|
|
2902
|
+
lease_lapsed: dict[str, Any] | None = None
|
|
2903
|
+
# 只有在"没有活跃租约"时才需要区分不存在/已过期/已损坏——三者要给的答案
|
|
2904
|
+
# 完全不同,而 inspect_lease 把它们一律压成 None。
|
|
2905
|
+
lease_state = (
|
|
2906
|
+
{"state": "active", "lease": current_lease}
|
|
2907
|
+
if current_lease is not None
|
|
2908
|
+
else hc.inspect_lease_state(project, resolved["changeId"])
|
|
2909
|
+
)
|
|
2910
|
+
|
|
2911
|
+
if lease_state["state"] == "corrupt":
|
|
2912
|
+
# 损坏的租约证明不了"没人抢占过",所以不自动恢复——那正是自动重取唯一
|
|
2913
|
+
# 的安全前提。
|
|
2914
|
+
return emit_error(
|
|
2915
|
+
"LEASE_INVALID",
|
|
2916
|
+
"phase lease file is unreadable; it cannot prove the phase was not taken over",
|
|
2917
|
+
as_json=as_json,
|
|
2918
|
+
extra={
|
|
2919
|
+
"phase": args.phase,
|
|
2920
|
+
"changeId": resolved["changeId"],
|
|
2921
|
+
"recoveryAction": (
|
|
2922
|
+
"租约文件已损坏,无法自动恢复。确认没有其他进程在跑同一个 change 后,"
|
|
2923
|
+
"用本阶段原 run-id 重新取得租约再重试 close:"
|
|
2924
|
+
"python <skills-root>/scripts/harness_change.py claim "
|
|
2925
|
+
f"--change {resolved['changeId']} --phase {args.phase} "
|
|
2926
|
+
"--run-id <原 run-id> --ttl-seconds 3600 --json"
|
|
2927
|
+
),
|
|
2928
|
+
},
|
|
2929
|
+
)
|
|
2930
|
+
|
|
2931
|
+
if lease_state["state"] == "expired":
|
|
2932
|
+
expired_lease = lease_state["lease"] or {}
|
|
2933
|
+
expired_run_id = str(expired_lease.get("runId") or "")
|
|
2934
|
+
expected_run_id = explicit_run_id or expired_run_id
|
|
2935
|
+
# 租约过期、runId 还是本轮的,恰好证明没有第三方抢占过:抢占会重写
|
|
2936
|
+
# 租约文件把 runId 换掉。所以过期只说明这个阶段跑得比 TTL 久,不说明
|
|
2937
|
+
# 所有权变了——harness_context.close_transition 早就是这么推理的
|
|
2938
|
+
# (只记 leaseLapsed 照常收尾),这里把 Gate 侧对齐过去,不再要求
|
|
2939
|
+
# 用户手工 claim 一遍再原样重试。
|
|
2940
|
+
if (
|
|
2941
|
+
expired_run_id
|
|
2942
|
+
and expired_run_id == expected_run_id
|
|
2943
|
+
and str(expired_lease.get("phase") or "") == args.phase
|
|
2944
|
+
):
|
|
2945
|
+
reacquired = hc.claim_lease(
|
|
2946
|
+
project,
|
|
2947
|
+
change_id=str(resolved["changeId"]),
|
|
2948
|
+
phase=args.phase,
|
|
2949
|
+
run_id=expired_run_id,
|
|
2950
|
+
ttl_seconds=int(getattr(args, "ttl_seconds", 0) or 3600),
|
|
2951
|
+
)
|
|
2952
|
+
if not reacquired.get("ok"):
|
|
2953
|
+
return emit_error(
|
|
2954
|
+
str(reacquired.get("code") or "LEASE_CONFLICT"),
|
|
2955
|
+
"expired phase lease could not be reacquired for close",
|
|
2956
|
+
as_json=as_json,
|
|
2957
|
+
extra={
|
|
2958
|
+
"phase": args.phase,
|
|
2959
|
+
"changeId": resolved["changeId"],
|
|
2960
|
+
"holder": reacquired.get("holder"),
|
|
2961
|
+
},
|
|
2962
|
+
)
|
|
2963
|
+
current_lease = reacquired.get("lease")
|
|
2964
|
+
lease_lapsed = {
|
|
2965
|
+
"acquiredAt": expired_lease.get("acquiredAt"),
|
|
2966
|
+
"expiresAt": expired_lease.get("expiresAt"),
|
|
2967
|
+
"reacquiredAt": (current_lease or {}).get("refreshedAt")
|
|
2968
|
+
or (current_lease or {}).get("acquiredAt"),
|
|
2969
|
+
"runId": expired_run_id,
|
|
2970
|
+
}
|
|
2971
|
+
else:
|
|
2972
|
+
return emit_error(
|
|
2973
|
+
"LEASE_OWNER_MISMATCH",
|
|
2974
|
+
"expired lease belongs to a different run or phase",
|
|
2975
|
+
as_json=as_json,
|
|
2976
|
+
extra={"holder": expired_lease},
|
|
2977
|
+
)
|
|
2978
|
+
|
|
2860
2979
|
if current_lease is None:
|
|
2861
2980
|
terminal = _latest_phase_end(change_dir, args.phase, explicit_run_id)
|
|
2862
2981
|
if (
|
|
@@ -2911,8 +3030,10 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2911
3030
|
"changeId": resolved["changeId"],
|
|
2912
3031
|
"resumeRunId": resume_run_id,
|
|
2913
3032
|
"recoveryAction": (
|
|
2914
|
-
"
|
|
2915
|
-
"
|
|
3033
|
+
"本阶段没有任何租约记录——过期租约现在由 close 自动重取,所以这里说明的是"
|
|
3034
|
+
"租约从未建立或已被释放。先确认 gate begin 是否真的跑过(events.ndjson 里"
|
|
3035
|
+
f"应有 phase={args.phase} 的 phase.start)。确有本轮 run-id 时用它重新取得"
|
|
3036
|
+
"租约再原样重试 close:"
|
|
2916
3037
|
"python <skills-root>/scripts/harness_change.py claim "
|
|
2917
3038
|
f"--change {resolved['changeId']} --phase {args.phase} "
|
|
2918
3039
|
f"--run-id {resume_run_id or '<原 run-id>'} --ttl-seconds 3600 --json"
|
|
@@ -2994,7 +3115,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2994
3115
|
run_id=run_id,
|
|
2995
3116
|
project=project,
|
|
2996
3117
|
execution_root=execution_root,
|
|
2997
|
-
allow_head_advance=args.phase
|
|
3118
|
+
allow_head_advance=phase_gate_rule(args.phase, "head_may_advance"),
|
|
2998
3119
|
)
|
|
2999
3120
|
except ValueError as exc:
|
|
3000
3121
|
if gate_soft_allowed(severity_mode, args.phase, "capsule"):
|
|
@@ -3049,7 +3170,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3049
3170
|
and captured_projection.get("receiptHash")
|
|
3050
3171
|
and captured_projection.get("receiptHash")
|
|
3051
3172
|
!= projection.get("receiptHash")
|
|
3052
|
-
and args.phase
|
|
3173
|
+
and phase_gate_rule(args.phase, "projection_drift")
|
|
3053
3174
|
):
|
|
3054
3175
|
mismatch = {
|
|
3055
3176
|
"code": "PROJECTION_CHANGED_DURING_PHASE",
|
|
@@ -3076,7 +3197,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3076
3197
|
change_dir, args.phase, policy, execution_root=execution_root,
|
|
3077
3198
|
phase_status=args.status,
|
|
3078
3199
|
)
|
|
3079
|
-
if not ledger_result.get("ok") and args.phase
|
|
3200
|
+
if not ledger_result.get("ok") and phase_gate_rule(args.phase, "ledger_blocking"):
|
|
3080
3201
|
persist_close_failure(
|
|
3081
3202
|
change_dir,
|
|
3082
3203
|
args.phase,
|
|
@@ -3102,7 +3223,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3102
3223
|
extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
|
|
3103
3224
|
)
|
|
3104
3225
|
|
|
3105
|
-
if args.phase
|
|
3226
|
+
if phase_gate_rule(args.phase, "review_outputs"):
|
|
3106
3227
|
review_outputs = validate_review_outputs_for_close(change_dir, run_id)
|
|
3107
3228
|
if not review_outputs.get("ok"):
|
|
3108
3229
|
persist_close_failure(
|
|
@@ -3134,7 +3255,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3134
3255
|
# C9: scenario coverage check — every P0/ledger scenario *due by this phase*
|
|
3135
3256
|
# must have a ledger entry. Scenarios with a later ownerPhase are deferred,
|
|
3136
3257
|
# not missing (see _validate_scenario_coverage).
|
|
3137
|
-
if args.phase
|
|
3258
|
+
if phase_gate_rule(args.phase, "scenario_coverage"):
|
|
3138
3259
|
coverage = _validate_scenario_coverage(change_dir, args.phase)
|
|
3139
3260
|
if not coverage.get("ok"):
|
|
3140
3261
|
if gate_soft_allowed(severity_mode, args.phase, "scenario-coverage"):
|
|
@@ -3192,7 +3313,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3192
3313
|
write_phase_capsule(change_dir, args.phase, run_id, capsule)
|
|
3193
3314
|
|
|
3194
3315
|
guard_result = None
|
|
3195
|
-
if args.phase
|
|
3316
|
+
if phase_gate_rule(args.phase, "test_guard"):
|
|
3196
3317
|
if close_transaction.get("guardClosed"):
|
|
3197
3318
|
guard_result = {"ok": True, "code": "ALREADY_CLOSED", "reused": True}
|
|
3198
3319
|
else:
|
|
@@ -3247,6 +3368,7 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3247
3368
|
type_="phase.end",
|
|
3248
3369
|
status=close_status,
|
|
3249
3370
|
note=args.note or "",
|
|
3371
|
+
fixback=bool(getattr(args, "fixback", False)),
|
|
3250
3372
|
run_id=run_id,
|
|
3251
3373
|
)
|
|
3252
3374
|
except BaseException as exc:
|
|
@@ -3364,6 +3486,10 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
3364
3486
|
"gateRecovery": recovery_result,
|
|
3365
3487
|
"scratchSwept": scratch_swept,
|
|
3366
3488
|
}
|
|
3489
|
+
if lease_lapsed is not None:
|
|
3490
|
+
# 与 context transition receipt 的 leaseLapsed 同名同义:记录这个阶段
|
|
3491
|
+
# 跑过了 TTL 且租约是自动重取的,不隐藏。
|
|
3492
|
+
payload["leaseLapsed"] = lease_lapsed
|
|
3367
3493
|
emit(payload, as_json=as_json)
|
|
3368
3494
|
return 0
|
|
3369
3495
|
|
|
@@ -3551,6 +3677,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3551
3677
|
p_begin.add_argument("--ttl-seconds", type=int, default=3600)
|
|
3552
3678
|
p_begin.add_argument("--task", type=int, default=None)
|
|
3553
3679
|
p_begin.add_argument("--note", default="")
|
|
3680
|
+
p_begin.add_argument(
|
|
3681
|
+
"--fixback",
|
|
3682
|
+
action="store_true",
|
|
3683
|
+
help="This phase is a review fixback round (tags the lifecycle event).",
|
|
3684
|
+
)
|
|
3554
3685
|
p_begin.add_argument("--executor-tool", default=None)
|
|
3555
3686
|
p_begin.add_argument("--executor-agent", default=None)
|
|
3556
3687
|
p_begin.add_argument("--executor-model", default=None)
|
|
@@ -3571,6 +3702,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
3571
3702
|
p_close.add_argument("--run-id", default=None)
|
|
3572
3703
|
p_close.add_argument("--task", type=int, default=None)
|
|
3573
3704
|
p_close.add_argument("--note", default="")
|
|
3705
|
+
p_close.add_argument(
|
|
3706
|
+
"--fixback",
|
|
3707
|
+
action="store_true",
|
|
3708
|
+
help="This phase is a review fixback round (tags the lifecycle event).",
|
|
3709
|
+
)
|
|
3574
3710
|
p_close.add_argument("--to-phase", default=None)
|
|
3575
3711
|
p_close.add_argument("--executor", default=None)
|
|
3576
3712
|
p_close.add_argument("--artifact", action="append", default=[])
|
|
@@ -28,6 +28,7 @@ if str(SCRIPTS_DIR) not in sys.path:
|
|
|
28
28
|
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
29
29
|
|
|
30
30
|
import harness_paths # noqa: E402
|
|
31
|
+
import harness_plan_finalize as hpf # noqa: E402
|
|
31
32
|
import harness_profile # noqa: E402
|
|
32
33
|
|
|
33
34
|
|
|
@@ -1629,6 +1630,24 @@ def _scenario_receipt_error(code: str, message: str) -> dict[str, Any]:
|
|
|
1629
1630
|
return {"ok": False, "code": code, "error": message}
|
|
1630
1631
|
|
|
1631
1632
|
|
|
1633
|
+
def resolve_scenario_manifest(manifest: Any) -> dict[str, Any]:
|
|
1634
|
+
"""把 v2 artifact 包装体解成 legacy 形状;legacy 输入原样返回。
|
|
1635
|
+
|
|
1636
|
+
解包规则住在 harness_plan_finalize(它定义了 legacy manifest 的 schema),
|
|
1637
|
+
门禁与本模块共用同一份,两边不会各推一套。
|
|
1638
|
+
|
|
1639
|
+
此前这里只判包装体、一律 fail-closed;更早的时候连判都不判——cmd_record
|
|
1640
|
+
的版本探测初值是 0,包装体探测不出 schemaVersion 就停在 0,
|
|
1641
|
+
`manifest_schema >= 2` 判假,--scenario-receipt-file 的强制要求被静默跳过。
|
|
1642
|
+
|
|
1643
|
+
返回 ``{"ok": True, "manifest": ...}`` 或 ``{"ok": False, "code": ...}``。
|
|
1644
|
+
"""
|
|
1645
|
+
unpacked = hpf.unpack_v2_scenario_manifest(manifest)
|
|
1646
|
+
if unpacked is None:
|
|
1647
|
+
return {"ok": True, "manifest": manifest}
|
|
1648
|
+
return unpacked
|
|
1649
|
+
|
|
1650
|
+
|
|
1632
1651
|
def _resolve_receipt_path(
|
|
1633
1652
|
raw: str, change_dir: Path
|
|
1634
1653
|
) -> tuple[Path | None, list[Path]]:
|
|
@@ -1694,6 +1713,12 @@ def validate_scenario_execution_receipt(
|
|
|
1694
1713
|
)
|
|
1695
1714
|
except (OSError, json.JSONDecodeError) as exc:
|
|
1696
1715
|
return _scenario_receipt_error("SCENARIO_MANIFEST_INVALID", str(exc))
|
|
1716
|
+
resolved = resolve_scenario_manifest(manifest)
|
|
1717
|
+
if not resolved["ok"]:
|
|
1718
|
+
return _scenario_receipt_error(
|
|
1719
|
+
str(resolved["code"]), str(resolved.get("message") or "")
|
|
1720
|
+
)
|
|
1721
|
+
manifest = resolved["manifest"]
|
|
1697
1722
|
if not isinstance(manifest, dict) or not isinstance(manifest.get("scenarios"), list):
|
|
1698
1723
|
return _scenario_receipt_error(
|
|
1699
1724
|
"SCENARIO_MANIFEST_INVALID",
|
|
@@ -2827,6 +2852,15 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2827
2852
|
as_json=as_json,
|
|
2828
2853
|
error_code="SCENARIO_MANIFEST_INVALID",
|
|
2829
2854
|
)
|
|
2855
|
+
resolved_probe = resolve_scenario_manifest(manifest_probe)
|
|
2856
|
+
if not resolved_probe["ok"]:
|
|
2857
|
+
return emit_error(
|
|
2858
|
+
str(resolved_probe.get("message") or ""),
|
|
2859
|
+
as_json=as_json,
|
|
2860
|
+
error_code=str(resolved_probe["code"]),
|
|
2861
|
+
extra={"artifactType": "scenario_manifest"},
|
|
2862
|
+
)
|
|
2863
|
+
manifest_probe = resolved_probe["manifest"]
|
|
2830
2864
|
raw_schema = (
|
|
2831
2865
|
manifest_probe.get("schemaVersion")
|
|
2832
2866
|
if isinstance(manifest_probe, dict)
|
|
@@ -3064,6 +3098,15 @@ def cmd_scenario_receipt_template(args: argparse.Namespace) -> int:
|
|
|
3064
3098
|
as_json=as_json,
|
|
3065
3099
|
error_code="SCENARIO_MANIFEST_INVALID",
|
|
3066
3100
|
)
|
|
3101
|
+
resolved = resolve_scenario_manifest(manifest)
|
|
3102
|
+
if not resolved["ok"]:
|
|
3103
|
+
return emit_error(
|
|
3104
|
+
str(resolved.get("message") or ""),
|
|
3105
|
+
as_json=as_json,
|
|
3106
|
+
error_code=str(resolved["code"]),
|
|
3107
|
+
extra={"artifactType": "scenario_manifest"},
|
|
3108
|
+
)
|
|
3109
|
+
manifest = resolved["manifest"]
|
|
3067
3110
|
scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
|
|
3068
3111
|
if not isinstance(scenarios, list):
|
|
3069
3112
|
return emit_error(
|
|
@@ -34,6 +34,66 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
34
34
|
LIFECYCLE_STATUSES = {"draft", "active", "superseded", "archived", "cancelled"}
|
|
35
35
|
CHANGE_CONTEXT_REL = Path("meta") / "change-context.json"
|
|
36
36
|
|
|
37
|
+
# 工作流阶段的唯一权威清单。这里是叶子模块(不 import 任何 harness_*),所以
|
|
38
|
+
# harness_context 与 harness_phase 都能引它而不成环。
|
|
39
|
+
#
|
|
40
|
+
# 此前这份清单在两个模块各写一遍,靠约定保持同步——直到 harness_phase 少了一个
|
|
41
|
+
# merge,worktree 变更走到 merge 阶段就在 target_required_dag 里直接 raise
|
|
42
|
+
# "unsupported reconcile target phase"。同一份事实不该有两个副本。
|
|
43
|
+
WORKFLOW_PHASES = (
|
|
44
|
+
"plan",
|
|
45
|
+
"run",
|
|
46
|
+
"test",
|
|
47
|
+
"review",
|
|
48
|
+
"package",
|
|
49
|
+
"apidoc",
|
|
50
|
+
"submit",
|
|
51
|
+
"merge",
|
|
52
|
+
"archive",
|
|
53
|
+
)
|
|
54
|
+
|
|
55
|
+
# 出现在阶段位置、但不是工作流阶段的名字。它们是真实存在的:
|
|
56
|
+
# workflow-policy.json 的 skills.*.phase 用 sync/codebase-map/knowledge-query 标注
|
|
57
|
+
# 技能归属,harness_gate.GATE_RELEASE_PHASES 含 release/deploy。把它们与"拼错的
|
|
58
|
+
# 阶段名"区分开,调用方才能既不误判也不硬崩。
|
|
59
|
+
KNOWN_NON_WORKFLOW_PHASES = frozenset({
|
|
60
|
+
"release",
|
|
61
|
+
"deploy",
|
|
62
|
+
"sync",
|
|
63
|
+
"codebase-map",
|
|
64
|
+
"knowledge-query",
|
|
65
|
+
"knowledge-ingest",
|
|
66
|
+
"push",
|
|
67
|
+
"pull",
|
|
68
|
+
})
|
|
69
|
+
|
|
70
|
+
# 旧阶段名 → 现阶段名。阶段合并/改名时在这里登记,读时映射即可让历史
|
|
71
|
+
# gate-policy.json 与 plannedPhases 继续可读,不必迁移已落盘的 change。
|
|
72
|
+
# 目前为空:还没有发生过阶段改名。
|
|
73
|
+
LEGACY_PHASE_ALIASES: dict[str, str] = {}
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def resolve_phase_name(phase: Any) -> str | None:
|
|
77
|
+
"""把可能是旧名的阶段解析成当前的工作流阶段名;不是工作流阶段则返回 None。"""
|
|
78
|
+
name = str(phase or "").strip()
|
|
79
|
+
if name in WORKFLOW_PHASES:
|
|
80
|
+
return name
|
|
81
|
+
mapped = LEGACY_PHASE_ALIASES.get(name)
|
|
82
|
+
return mapped if mapped in WORKFLOW_PHASES else None
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def classify_phase_name(phase: Any) -> str:
|
|
86
|
+
"""三态判定:``workflow`` | ``non_workflow`` | ``unknown``。
|
|
87
|
+
|
|
88
|
+
调用方需要区分这三者:``workflow`` 照常处理;``non_workflow`` 是别的子系统
|
|
89
|
+
的标注,跳过而不是报错;只有 ``unknown`` 才是真的有问题,值得让人看见。
|
|
90
|
+
"""
|
|
91
|
+
if resolve_phase_name(phase) is not None:
|
|
92
|
+
return "workflow"
|
|
93
|
+
if str(phase or "").strip() in KNOWN_NON_WORKFLOW_PHASES:
|
|
94
|
+
return "non_workflow"
|
|
95
|
+
return "unknown"
|
|
96
|
+
|
|
37
97
|
|
|
38
98
|
def _git_text(cwd: Path, *args: str) -> str | None:
|
|
39
99
|
proc = subprocess.run(
|
|
@@ -33,7 +33,9 @@ import harness_paths as hpaths # noqa: E402
|
|
|
33
33
|
|
|
34
34
|
SCHEMA_VERSION = 1
|
|
35
35
|
CI_METRICS_SCHEMA_VERSION = 1
|
|
36
|
-
|
|
36
|
+
# 唯一权威清单在 harness_paths,harness_context 引的是同一个对象。此前这里另有
|
|
37
|
+
# 一份拷贝,少了 merge,worktree 变更走到 merge 就在 target_required_dag 里硬 raise。
|
|
38
|
+
PHASE_ORDER = hpaths.WORKFLOW_PHASES
|
|
37
39
|
VALIDATION_PHASES = {
|
|
38
40
|
"compile": "run",
|
|
39
41
|
"unitTest": "run",
|
|
@@ -700,8 +702,18 @@ def target_required_dag(policy: dict[str, Any], target_phase: str) -> dict[str,
|
|
|
700
702
|
stage nodes for the target itself are deliberately excluded because their
|
|
701
703
|
``phase.end`` is the result of close, not evidence required before close.
|
|
702
704
|
"""
|
|
703
|
-
|
|
704
|
-
|
|
705
|
+
# 未知阶段名先过读时映射:阶段改名后,历史 change 的 gate-policy.json 与
|
|
706
|
+
# plannedPhases 里仍然是旧名,靠 LEGACY_PHASE_ALIASES 继续可读,不必迁移已落盘
|
|
707
|
+
# 的 change(本仓库没有 change 级 schema 迁移机制,硬 raise 等于让在途变更报废)。
|
|
708
|
+
resolved_target = hpaths.resolve_phase_name(target_phase)
|
|
709
|
+
if resolved_target is None:
|
|
710
|
+
raise ValueError(
|
|
711
|
+
f"unsupported reconcile target phase: {target_phase}; "
|
|
712
|
+
f"已知阶段 {', '.join(PHASE_ORDER)}。"
|
|
713
|
+
"若这是改名前的旧阶段,在 harness_paths.LEGACY_PHASE_ALIASES 登记映射;"
|
|
714
|
+
"若这份 gate-policy.json 本身过期,重跑 harness_gate.py classify 重建"
|
|
715
|
+
)
|
|
716
|
+
target_phase = resolved_target
|
|
705
717
|
dag = policy.get("requiredGateDag")
|
|
706
718
|
if not isinstance(dag, dict) or not isinstance(dag.get("nodes"), list):
|
|
707
719
|
raise ValueError("gate policy missing requiredGateDag; run harness_gate.py classify")
|
|
@@ -718,9 +730,15 @@ def target_required_dag(policy: dict[str, Any], target_phase: str) -> dict[str,
|
|
|
718
730
|
if node_kind == "validation"
|
|
719
731
|
else node_name
|
|
720
732
|
)
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
733
|
+
raw_phase = str(node.get("phase") or inferred_phase or "")
|
|
734
|
+
node_phase = hpaths.resolve_phase_name(raw_phase)
|
|
735
|
+
if node_phase is None:
|
|
736
|
+
raise ValueError(
|
|
737
|
+
f"requiredGateDag node has unsupported phase: {node_id} (phase={raw_phase!r});"
|
|
738
|
+
"旧阶段名在 harness_paths.LEGACY_PHASE_ALIASES 登记映射,"
|
|
739
|
+
"或重跑 harness_gate.py classify 重建这份 gate-policy.json"
|
|
740
|
+
)
|
|
741
|
+
node["phase"] = node_phase
|
|
724
742
|
node_rank = PHASE_ORDER.index(node_phase)
|
|
725
743
|
is_validation = node.get("kind") == "validation" or node_id.startswith(
|
|
726
744
|
"validation:"
|