@hunter-harness/workflow-harness 0.2.31 → 0.2.32
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/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/general/codex/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/general/cursor/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/java/codex/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +138 -49
- package/harness/bundles/java/cursor/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +138 -49
- package/harness/manifests/general/claude-code.json +4 -4
- package/harness/manifests/general/codebuddy.json +4 -4
- package/harness/manifests/general/codex.json +4 -4
- package/harness/manifests/general/cursor.json +4 -4
- package/harness/manifests/java/claude-code.json +4 -4
- package/harness/manifests/java/codebuddy.json +4 -4
- package/harness/manifests/java/codex.json +4 -4
- package/harness/manifests/java/cursor.json +4 -4
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -1635,31 +1635,27 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
1635
1635
|
for s in scenarios
|
|
1636
1636
|
if isinstance(s, dict)
|
|
1637
1637
|
and (
|
|
1638
|
-
s.get("
|
|
1639
|
-
or (
|
|
1640
|
-
not s.get("requiredEvidenceKind")
|
|
1641
|
-
and str(s.get("priority", "")).upper() in {"P0", "P1"}
|
|
1642
|
-
)
|
|
1638
|
+
str(s.get("priority", "")).upper() in {"P0", "P1"}
|
|
1639
|
+
or s.get("requiredEvidenceKind") == "ledger"
|
|
1643
1640
|
)
|
|
1644
1641
|
}
|
|
1645
1642
|
if not required_ids:
|
|
1646
1643
|
return {"ok": True, "code": "NO_LEDGER_REQUIRED_SCENARIOS"}
|
|
1647
1644
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1645
|
+
try:
|
|
1646
|
+
ledger, ledger_path = hl.load_ledger(change_dir)
|
|
1647
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1650
1648
|
return {
|
|
1651
1649
|
"ok": False,
|
|
1652
1650
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
1653
|
-
"message": "ledger
|
|
1651
|
+
"message": f"ledger unreadable: {exc}",
|
|
1654
1652
|
"missing": sorted(required_ids),
|
|
1655
1653
|
}
|
|
1656
|
-
|
|
1657
|
-
ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
|
|
1658
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
1654
|
+
if ledger is None or ledger_path is None:
|
|
1659
1655
|
return {
|
|
1660
1656
|
"ok": False,
|
|
1661
1657
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
1662
|
-
"message":
|
|
1658
|
+
"message": "ledger missing; cannot verify required scenario coverage",
|
|
1663
1659
|
"missing": sorted(required_ids),
|
|
1664
1660
|
}
|
|
1665
1661
|
covered: set[str] = set()
|
|
@@ -1728,13 +1724,6 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1728
1724
|
or plan_verification.get("message")
|
|
1729
1725
|
or "finalized plan verification failed"
|
|
1730
1726
|
)
|
|
1731
|
-
record_blocked_attempt(
|
|
1732
|
-
change_dir,
|
|
1733
|
-
phase=args.phase,
|
|
1734
|
-
code=code,
|
|
1735
|
-
message=message,
|
|
1736
|
-
run_id=args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID"),
|
|
1737
|
-
)
|
|
1738
1727
|
return emit_error(
|
|
1739
1728
|
code,
|
|
1740
1729
|
message,
|
|
@@ -13,7 +13,7 @@ import re
|
|
|
13
13
|
import shutil
|
|
14
14
|
import sys
|
|
15
15
|
import tempfile
|
|
16
|
-
from pathlib import Path
|
|
16
|
+
from pathlib import Path, PurePosixPath
|
|
17
17
|
from typing import Any
|
|
18
18
|
|
|
19
19
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
@@ -51,6 +51,12 @@ def _result_error(code: str, message: str) -> dict[str, Any]:
|
|
|
51
51
|
return {"ok": False, "code": code, "error": message}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
class PlanParseError(ValueError):
|
|
55
|
+
def __init__(self, code: str, message: str) -> None:
|
|
56
|
+
super().__init__(message)
|
|
57
|
+
self.code = code
|
|
58
|
+
|
|
59
|
+
|
|
54
60
|
def _normalize_header(value: str) -> str:
|
|
55
61
|
return re.sub(r"[\s_-]+", "", value).lower()
|
|
56
62
|
|
|
@@ -127,13 +133,23 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
127
133
|
break
|
|
128
134
|
cells = _table_cells(row)
|
|
129
135
|
if max(id_index, scenario_index) >= len(cells):
|
|
130
|
-
|
|
131
|
-
|
|
136
|
+
raise PlanParseError(
|
|
137
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
138
|
+
f"{scenarios_path.name}: line {row_index + 1} is missing scenario ID or description",
|
|
139
|
+
)
|
|
132
140
|
complete_row = len(cells) == len(headers)
|
|
133
141
|
scenario_id = cells[id_index].strip()
|
|
134
142
|
if not scenario_id or set(scenario_id) <= {"-", ":"}:
|
|
135
|
-
|
|
136
|
-
|
|
143
|
+
raise PlanParseError(
|
|
144
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
145
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario ID",
|
|
146
|
+
)
|
|
147
|
+
scenario_text = cells[scenario_index].strip()
|
|
148
|
+
if not scenario_text or set(scenario_text) <= {"-", ":"}:
|
|
149
|
+
raise PlanParseError(
|
|
150
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
151
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario description",
|
|
152
|
+
)
|
|
137
153
|
priority = (
|
|
138
154
|
cells[priority_index].strip().upper()
|
|
139
155
|
if priority_index is not None
|
|
@@ -144,7 +160,7 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
144
160
|
scenario = {
|
|
145
161
|
"id": scenario_id,
|
|
146
162
|
"priority": priority,
|
|
147
|
-
"scenario":
|
|
163
|
+
"scenario": scenario_text,
|
|
148
164
|
"ownerPhase": (
|
|
149
165
|
cells[owner_phase_index].strip()
|
|
150
166
|
if owner_phase_index is not None
|
|
@@ -219,19 +235,34 @@ def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
|
219
235
|
if not row.startswith("|"):
|
|
220
236
|
break
|
|
221
237
|
cells = _table_cells(row)
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
238
|
+
if max(number_index, task_index) >= len(cells):
|
|
239
|
+
raise PlanParseError(
|
|
240
|
+
"PLAN_TASK_ROW_INVALID",
|
|
241
|
+
f"{plan_path.name}: line {row_index + 1} is missing task ID or description",
|
|
242
|
+
)
|
|
243
|
+
complete_row = len(cells) == len(headers)
|
|
225
244
|
number = cells[number_index].strip()
|
|
226
245
|
task_text = cells[task_index].strip()
|
|
227
|
-
if
|
|
228
|
-
|
|
229
|
-
|
|
246
|
+
if (
|
|
247
|
+
not number
|
|
248
|
+
or not task_text
|
|
249
|
+
or set(number) <= {"-", ":"}
|
|
250
|
+
or set(task_text) <= {"-", ":"}
|
|
251
|
+
):
|
|
252
|
+
raise PlanParseError(
|
|
253
|
+
"PLAN_TASK_ROW_INVALID",
|
|
254
|
+
f"{plan_path.name}: line {row_index + 1} has an empty task ID or description",
|
|
255
|
+
)
|
|
230
256
|
task: dict[str, str] = {"num": number, "task": task_text}
|
|
231
257
|
if cluster_index is not None and cells[cluster_index].strip():
|
|
232
258
|
task["cluster"] = cells[cluster_index].strip()
|
|
233
259
|
for name, index in optional_indices.items():
|
|
234
|
-
if
|
|
260
|
+
if (
|
|
261
|
+
complete_row
|
|
262
|
+
and index is not None
|
|
263
|
+
and index < len(cells)
|
|
264
|
+
and cells[index].strip()
|
|
265
|
+
):
|
|
235
266
|
task[name] = cells[index].strip()
|
|
236
267
|
tasks.append(task)
|
|
237
268
|
row_index += 1
|
|
@@ -416,7 +447,10 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
416
447
|
|
|
417
448
|
# C8: validate ownerPhase values in plan.md task table.
|
|
418
449
|
plan_path = staging / "plans" / f"{change_name}-plan.md"
|
|
419
|
-
|
|
450
|
+
try:
|
|
451
|
+
tasks = parse_plan_tasks(plan_path)
|
|
452
|
+
except PlanParseError as exc:
|
|
453
|
+
return _result_error(exc.code, str(exc))
|
|
420
454
|
if not tasks:
|
|
421
455
|
return _result_error(
|
|
422
456
|
"PLAN_TASKS_EMPTY",
|
|
@@ -444,7 +478,10 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
444
478
|
|
|
445
479
|
# C9: parse test-scenarios.md for scenario manifest.
|
|
446
480
|
scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
|
|
447
|
-
|
|
481
|
+
try:
|
|
482
|
+
scenarios = parse_test_scenarios(scenarios_path)
|
|
483
|
+
except PlanParseError as exc:
|
|
484
|
+
return _result_error(exc.code, str(exc))
|
|
448
485
|
if not scenarios:
|
|
449
486
|
return _result_error(
|
|
450
487
|
"PLAN_SCENARIOS_EMPTY",
|
|
@@ -542,6 +579,58 @@ def _validate_plan_start(
|
|
|
542
579
|
return {"ok": True, "phaseStartCount": 1}
|
|
543
580
|
|
|
544
581
|
|
|
582
|
+
def _receipt_artifact_targets(
|
|
583
|
+
change_dir: Path,
|
|
584
|
+
files_list: list[Any],
|
|
585
|
+
) -> tuple[list[tuple[str, Path]] | None, dict[str, Any] | None]:
|
|
586
|
+
targets: list[tuple[str, Path]] = []
|
|
587
|
+
seen: set[str] = set()
|
|
588
|
+
for index, value in enumerate(files_list):
|
|
589
|
+
if not isinstance(value, str):
|
|
590
|
+
return None, _result_error(
|
|
591
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
592
|
+
f"receipt files[{index}] must be a string",
|
|
593
|
+
)
|
|
594
|
+
raw = value
|
|
595
|
+
segments = raw.split("/")
|
|
596
|
+
rel = PurePosixPath(raw)
|
|
597
|
+
invalid = (
|
|
598
|
+
not raw
|
|
599
|
+
or raw != raw.strip()
|
|
600
|
+
or "\\" in raw
|
|
601
|
+
or ":" in raw
|
|
602
|
+
or rel.is_absolute()
|
|
603
|
+
or any(segment in {"", ".", ".."} for segment in segments)
|
|
604
|
+
or any(segment.endswith((".", " ")) for segment in segments)
|
|
605
|
+
or not rel.parts
|
|
606
|
+
or rel.parts[0] not in {"spec", "plans", "meta"}
|
|
607
|
+
)
|
|
608
|
+
if invalid:
|
|
609
|
+
return None, _result_error(
|
|
610
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
611
|
+
f"receipt files[{index}] is not a safe artifact-relative path: {raw!r}",
|
|
612
|
+
)
|
|
613
|
+
normalized = rel.as_posix()
|
|
614
|
+
if normalized in seen:
|
|
615
|
+
return None, _result_error(
|
|
616
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
617
|
+
f"receipt contains duplicate artifact path: {normalized}",
|
|
618
|
+
)
|
|
619
|
+
seen.add(normalized)
|
|
620
|
+
target = change_dir.joinpath(*rel.parts)
|
|
621
|
+
cursor = change_dir
|
|
622
|
+
for part in rel.parts:
|
|
623
|
+
cursor = cursor / part
|
|
624
|
+
is_junction = getattr(cursor, "is_junction", lambda: False)
|
|
625
|
+
if cursor.is_symlink() or is_junction():
|
|
626
|
+
return None, _result_error(
|
|
627
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
628
|
+
f"receipt artifact path traverses a link: {normalized}",
|
|
629
|
+
)
|
|
630
|
+
targets.append((normalized, target))
|
|
631
|
+
return targets, None
|
|
632
|
+
|
|
633
|
+
|
|
545
634
|
def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
546
635
|
"""Read-only verification of a finalized plan (retro §5.8).
|
|
547
636
|
|
|
@@ -565,6 +654,11 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
565
654
|
change_name = str(receipt.get("changeName") or "").strip()
|
|
566
655
|
if not change_name:
|
|
567
656
|
return _result_error("RECEIPT_INVALID", "receipt missing changeName")
|
|
657
|
+
if change_name != change_dir.name:
|
|
658
|
+
return _result_error(
|
|
659
|
+
"RECEIPT_CHANGE_NAME_INVALID",
|
|
660
|
+
f"receipt changeName {change_name!r} does not match {change_dir.name!r}",
|
|
661
|
+
)
|
|
568
662
|
|
|
569
663
|
expected_hash = str(receipt.get("artifactsHash") or "").strip()
|
|
570
664
|
if not expected_hash.startswith("sha256:"):
|
|
@@ -573,6 +667,10 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
573
667
|
files_list = receipt.get("files")
|
|
574
668
|
if not isinstance(files_list, list) or not files_list:
|
|
575
669
|
return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
|
|
670
|
+
artifact_targets, path_error = _receipt_artifact_targets(change_dir, files_list)
|
|
671
|
+
if path_error is not None:
|
|
672
|
+
return path_error
|
|
673
|
+
assert artifact_targets is not None
|
|
576
674
|
if receipt.get("status") != "finalized":
|
|
577
675
|
return _result_error(
|
|
578
676
|
"RECEIPT_NOT_FINALIZED",
|
|
@@ -589,9 +687,7 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
589
687
|
# Recompute artifacts hash from published files.
|
|
590
688
|
digest = hashlib.sha256()
|
|
591
689
|
artifact_names: list[str] = []
|
|
592
|
-
for
|
|
593
|
-
rel = rel_text.as_posix() if hasattr(rel_text, "as_posix") else str(rel_text)
|
|
594
|
-
target = change_dir / rel
|
|
690
|
+
for rel, target in artifact_targets:
|
|
595
691
|
if not target.is_file():
|
|
596
692
|
return _result_error(
|
|
597
693
|
"ARTIFACT_MISSING", f"published artifact missing: {rel}"
|
|
@@ -634,7 +730,10 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
634
730
|
}
|
|
635
731
|
|
|
636
732
|
plan_path = change_dir / "plans" / f"{change_name}-plan.md"
|
|
637
|
-
|
|
733
|
+
try:
|
|
734
|
+
expected_tasks = parse_plan_tasks(plan_path)
|
|
735
|
+
except PlanParseError as exc:
|
|
736
|
+
return _result_error(exc.code, str(exc))
|
|
638
737
|
if not expected_tasks:
|
|
639
738
|
return _result_error(
|
|
640
739
|
"PLAN_TASKS_EMPTY",
|
|
@@ -652,24 +751,25 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
652
751
|
)
|
|
653
752
|
except (OSError, json.JSONDecodeError) as exc:
|
|
654
753
|
return _result_error("IMPLEMENTATION_CHECKPOINTS_INVALID", str(exc))
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if expected_task_ids != actual_task_ids:
|
|
754
|
+
expected_checkpoints = {
|
|
755
|
+
"schemaVersion": 1,
|
|
756
|
+
"changeName": change_name,
|
|
757
|
+
"tasks": expected_tasks,
|
|
758
|
+
"foundationGate": "approved",
|
|
759
|
+
}
|
|
760
|
+
if checkpoints != expected_checkpoints:
|
|
663
761
|
return _result_error(
|
|
664
762
|
"IMPLEMENTATION_CHECKPOINTS_DRIFT",
|
|
665
|
-
"derived
|
|
666
|
-
f"expected={expected_task_ids}, actual={actual_task_ids}",
|
|
763
|
+
"derived implementation checkpoints do not match the finalized plan",
|
|
667
764
|
)
|
|
668
765
|
|
|
669
766
|
scenarios_path = (
|
|
670
767
|
change_dir / "plans" / f"{change_name}-test-scenarios.md"
|
|
671
768
|
)
|
|
672
|
-
|
|
769
|
+
try:
|
|
770
|
+
expected_scenarios = parse_test_scenarios(scenarios_path)
|
|
771
|
+
except PlanParseError as exc:
|
|
772
|
+
return _result_error(exc.code, str(exc))
|
|
673
773
|
if not expected_scenarios:
|
|
674
774
|
return _result_error(
|
|
675
775
|
"PLAN_SCENARIOS_EMPTY",
|
|
@@ -694,26 +794,15 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
694
794
|
"scenario-manifest.json must contain at least one scenario",
|
|
695
795
|
)
|
|
696
796
|
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
expected_scenario_identities = [
|
|
706
|
-
scenario_identity(item) for item in expected_scenarios
|
|
707
|
-
]
|
|
708
|
-
actual_scenario_identities = [
|
|
709
|
-
scenario_identity(item)
|
|
710
|
-
for item in actual_scenarios
|
|
711
|
-
if isinstance(item, dict)
|
|
712
|
-
]
|
|
713
|
-
if expected_scenario_identities != actual_scenario_identities:
|
|
797
|
+
expected_manifest = {
|
|
798
|
+
"schemaVersion": 1,
|
|
799
|
+
"changeName": change_name,
|
|
800
|
+
"scenarios": expected_scenarios,
|
|
801
|
+
}
|
|
802
|
+
if manifest != expected_manifest:
|
|
714
803
|
return _result_error(
|
|
715
804
|
"SCENARIO_MANIFEST_DRIFT",
|
|
716
|
-
"derived scenario
|
|
805
|
+
"derived scenario manifest does not match the finalized scenario table",
|
|
717
806
|
)
|
|
718
807
|
|
|
719
808
|
# Validate gate-policy.json is parseable JSON.
|
|
@@ -730,7 +819,7 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
730
819
|
gate_policy_consistent = True
|
|
731
820
|
|
|
732
821
|
# Validate events.ndjson: require one matching start/end lifecycle.
|
|
733
|
-
events_path = change_dir
|
|
822
|
+
events_path = harness_events.events_path(change_dir)
|
|
734
823
|
phase_start_count = 0
|
|
735
824
|
phase_end_count = 0
|
|
736
825
|
phase_end_status: str | None = None
|
|
@@ -1635,31 +1635,27 @@ def _validate_scenario_coverage(change_dir: Path) -> dict[str, Any]:
|
|
|
1635
1635
|
for s in scenarios
|
|
1636
1636
|
if isinstance(s, dict)
|
|
1637
1637
|
and (
|
|
1638
|
-
s.get("
|
|
1639
|
-
or (
|
|
1640
|
-
not s.get("requiredEvidenceKind")
|
|
1641
|
-
and str(s.get("priority", "")).upper() in {"P0", "P1"}
|
|
1642
|
-
)
|
|
1638
|
+
str(s.get("priority", "")).upper() in {"P0", "P1"}
|
|
1639
|
+
or s.get("requiredEvidenceKind") == "ledger"
|
|
1643
1640
|
)
|
|
1644
1641
|
}
|
|
1645
1642
|
if not required_ids:
|
|
1646
1643
|
return {"ok": True, "code": "NO_LEDGER_REQUIRED_SCENARIOS"}
|
|
1647
1644
|
|
|
1648
|
-
|
|
1649
|
-
|
|
1645
|
+
try:
|
|
1646
|
+
ledger, ledger_path = hl.load_ledger(change_dir)
|
|
1647
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1650
1648
|
return {
|
|
1651
1649
|
"ok": False,
|
|
1652
1650
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
1653
|
-
"message": "ledger
|
|
1651
|
+
"message": f"ledger unreadable: {exc}",
|
|
1654
1652
|
"missing": sorted(required_ids),
|
|
1655
1653
|
}
|
|
1656
|
-
|
|
1657
|
-
ledger = json.loads(ledger_path.read_text(encoding="utf-8-sig"))
|
|
1658
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
1654
|
+
if ledger is None or ledger_path is None:
|
|
1659
1655
|
return {
|
|
1660
1656
|
"ok": False,
|
|
1661
1657
|
"code": "SCENARIO_COVERAGE_FAILED",
|
|
1662
|
-
"message":
|
|
1658
|
+
"message": "ledger missing; cannot verify required scenario coverage",
|
|
1663
1659
|
"missing": sorted(required_ids),
|
|
1664
1660
|
}
|
|
1665
1661
|
covered: set[str] = set()
|
|
@@ -1728,13 +1724,6 @@ def cmd_begin(args: argparse.Namespace) -> int:
|
|
|
1728
1724
|
or plan_verification.get("message")
|
|
1729
1725
|
or "finalized plan verification failed"
|
|
1730
1726
|
)
|
|
1731
|
-
record_blocked_attempt(
|
|
1732
|
-
change_dir,
|
|
1733
|
-
phase=args.phase,
|
|
1734
|
-
code=code,
|
|
1735
|
-
message=message,
|
|
1736
|
-
run_id=args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID"),
|
|
1737
|
-
)
|
|
1738
1727
|
return emit_error(
|
|
1739
1728
|
code,
|
|
1740
1729
|
message,
|