@hunter-harness/workflow-harness 0.2.31 → 0.2.33
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-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -0
- package/harness/bundles/general/claude-code/harness-plan/reference.md +6 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -0
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +6 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/checklist.md +2 -0
- package/harness/bundles/general/codex/harness-plan/reference.md +6 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -0
- package/harness/bundles/general/cursor/harness-plan/reference.md +6 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +8 -19
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -0
- package/harness/bundles/java/claude-code/harness-plan/reference.md +6 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -0
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +6 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/checklist.md +2 -0
- package/harness/bundles/java/codex/harness-plan/reference.md +6 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +179 -58
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -0
- package/harness/bundles/java/cursor/harness-plan/reference.md +6 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +8 -19
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +179 -58
- package/harness/manifests/general/claude-code.json +7 -7
- package/harness/manifests/general/codebuddy.json +7 -7
- package/harness/manifests/general/codex.json +7 -7
- package/harness/manifests/general/cursor.json +7 -7
- package/harness/manifests/java/claude-code.json +7 -7
- package/harness/manifests/java/codebuddy.json +7 -7
- package/harness/manifests/java/codex.json +7 -7
- package/harness/manifests/java/cursor.json +7 -7
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -11,9 +11,10 @@ import json
|
|
|
11
11
|
import os
|
|
12
12
|
import re
|
|
13
13
|
import shutil
|
|
14
|
+
import stat
|
|
14
15
|
import sys
|
|
15
16
|
import tempfile
|
|
16
|
-
from pathlib import Path
|
|
17
|
+
from pathlib import Path, PurePosixPath
|
|
17
18
|
from typing import Any
|
|
18
19
|
|
|
19
20
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
@@ -45,12 +46,19 @@ PRIORITY_EVIDENCE_KIND = {
|
|
|
45
46
|
"P1": "ledger",
|
|
46
47
|
"P2": "advisory",
|
|
47
48
|
}
|
|
49
|
+
_FILE_ATTRIBUTE_REPARSE_POINT = getattr(stat, "FILE_ATTRIBUTE_REPARSE_POINT", 0x400)
|
|
48
50
|
|
|
49
51
|
|
|
50
52
|
def _result_error(code: str, message: str) -> dict[str, Any]:
|
|
51
53
|
return {"ok": False, "code": code, "error": message}
|
|
52
54
|
|
|
53
55
|
|
|
56
|
+
class PlanParseError(ValueError):
|
|
57
|
+
def __init__(self, code: str, message: str) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.code = code
|
|
60
|
+
|
|
61
|
+
|
|
54
62
|
def _normalize_header(value: str) -> str:
|
|
55
63
|
return re.sub(r"[\s_-]+", "", value).lower()
|
|
56
64
|
|
|
@@ -127,13 +135,23 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
127
135
|
break
|
|
128
136
|
cells = _table_cells(row)
|
|
129
137
|
if max(id_index, scenario_index) >= len(cells):
|
|
130
|
-
|
|
131
|
-
|
|
138
|
+
raise PlanParseError(
|
|
139
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
140
|
+
f"{scenarios_path.name}: line {row_index + 1} is missing scenario ID or description",
|
|
141
|
+
)
|
|
132
142
|
complete_row = len(cells) == len(headers)
|
|
133
143
|
scenario_id = cells[id_index].strip()
|
|
134
144
|
if not scenario_id or set(scenario_id) <= {"-", ":"}:
|
|
135
|
-
|
|
136
|
-
|
|
145
|
+
raise PlanParseError(
|
|
146
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
147
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario ID",
|
|
148
|
+
)
|
|
149
|
+
scenario_text = cells[scenario_index].strip()
|
|
150
|
+
if not scenario_text or set(scenario_text) <= {"-", ":"}:
|
|
151
|
+
raise PlanParseError(
|
|
152
|
+
"PLAN_SCENARIO_ROW_INVALID",
|
|
153
|
+
f"{scenarios_path.name}: line {row_index + 1} has an empty scenario description",
|
|
154
|
+
)
|
|
137
155
|
priority = (
|
|
138
156
|
cells[priority_index].strip().upper()
|
|
139
157
|
if priority_index is not None
|
|
@@ -144,7 +162,7 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
144
162
|
scenario = {
|
|
145
163
|
"id": scenario_id,
|
|
146
164
|
"priority": priority,
|
|
147
|
-
"scenario":
|
|
165
|
+
"scenario": scenario_text,
|
|
148
166
|
"ownerPhase": (
|
|
149
167
|
cells[owner_phase_index].strip()
|
|
150
168
|
if owner_phase_index is not None
|
|
@@ -219,19 +237,34 @@ def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
|
219
237
|
if not row.startswith("|"):
|
|
220
238
|
break
|
|
221
239
|
cells = _table_cells(row)
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
|
|
240
|
+
if max(number_index, task_index) >= len(cells):
|
|
241
|
+
raise PlanParseError(
|
|
242
|
+
"PLAN_TASK_ROW_INVALID",
|
|
243
|
+
f"{plan_path.name}: line {row_index + 1} is missing task ID or description",
|
|
244
|
+
)
|
|
245
|
+
complete_row = len(cells) == len(headers)
|
|
225
246
|
number = cells[number_index].strip()
|
|
226
247
|
task_text = cells[task_index].strip()
|
|
227
|
-
if
|
|
228
|
-
|
|
229
|
-
|
|
248
|
+
if (
|
|
249
|
+
not number
|
|
250
|
+
or not task_text
|
|
251
|
+
or set(number) <= {"-", ":"}
|
|
252
|
+
or set(task_text) <= {"-", ":"}
|
|
253
|
+
):
|
|
254
|
+
raise PlanParseError(
|
|
255
|
+
"PLAN_TASK_ROW_INVALID",
|
|
256
|
+
f"{plan_path.name}: line {row_index + 1} has an empty task ID or description",
|
|
257
|
+
)
|
|
230
258
|
task: dict[str, str] = {"num": number, "task": task_text}
|
|
231
259
|
if cluster_index is not None and cells[cluster_index].strip():
|
|
232
260
|
task["cluster"] = cells[cluster_index].strip()
|
|
233
261
|
for name, index in optional_indices.items():
|
|
234
|
-
if
|
|
262
|
+
if (
|
|
263
|
+
complete_row
|
|
264
|
+
and index is not None
|
|
265
|
+
and index < len(cells)
|
|
266
|
+
and cells[index].strip()
|
|
267
|
+
):
|
|
235
268
|
task[name] = cells[index].strip()
|
|
236
269
|
tasks.append(task)
|
|
237
270
|
row_index += 1
|
|
@@ -271,6 +304,28 @@ def _frontmatter(text: str) -> dict[str, str] | None:
|
|
|
271
304
|
return None
|
|
272
305
|
|
|
273
306
|
|
|
307
|
+
def _required_artifact_names(change_name: str) -> set[str]:
|
|
308
|
+
return {
|
|
309
|
+
f"spec/{change_name}-design.md",
|
|
310
|
+
f"plans/{change_name}-plan.md",
|
|
311
|
+
f"plans/{change_name}-implementation-detail.md",
|
|
312
|
+
f"plans/{change_name}-test-scenarios.md",
|
|
313
|
+
"meta/gate-policy.json",
|
|
314
|
+
"meta/worktree.json",
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _is_link_or_reparse(path: Path) -> bool:
|
|
319
|
+
"""Detect symlinks and Windows reparse points on every supported Python."""
|
|
320
|
+
try:
|
|
321
|
+
if path.is_symlink():
|
|
322
|
+
return True
|
|
323
|
+
attributes = getattr(os.lstat(path), "st_file_attributes", 0)
|
|
324
|
+
except OSError:
|
|
325
|
+
return False
|
|
326
|
+
return bool(attributes & _FILE_ATTRIBUTE_REPARSE_POINT)
|
|
327
|
+
|
|
328
|
+
|
|
274
329
|
def lint_slice_plan(plan_path: Path) -> dict[str, Any]:
|
|
275
330
|
"""Validate the identity, reuse, and storage contract for a slice plan."""
|
|
276
331
|
text = Path(plan_path).read_text(encoding="utf-8-sig")
|
|
@@ -338,7 +393,7 @@ def lint_slice_plan(plan_path: Path) -> dict[str, Any]:
|
|
|
338
393
|
def _artifact_files(staging: Path) -> list[Path]:
|
|
339
394
|
files: list[Path] = []
|
|
340
395
|
for path in staging.rglob("*"):
|
|
341
|
-
if path
|
|
396
|
+
if _is_link_or_reparse(path):
|
|
342
397
|
raise ValueError(f"PLAN_ARTIFACT_SYMLINK: {path}")
|
|
343
398
|
if path.is_file():
|
|
344
399
|
files.append(path)
|
|
@@ -347,14 +402,7 @@ def _artifact_files(staging: Path) -> list[Path]:
|
|
|
347
402
|
|
|
348
403
|
def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
349
404
|
staging = staging.resolve()
|
|
350
|
-
required = {
|
|
351
|
-
Path("spec") / f"{change_name}-design.md",
|
|
352
|
-
Path("plans") / f"{change_name}-plan.md",
|
|
353
|
-
Path("plans") / f"{change_name}-implementation-detail.md",
|
|
354
|
-
Path("plans") / f"{change_name}-test-scenarios.md",
|
|
355
|
-
Path("meta") / "gate-policy.json",
|
|
356
|
-
Path("meta") / "worktree.json",
|
|
357
|
-
}
|
|
405
|
+
required = {Path(name) for name in _required_artifact_names(change_name)}
|
|
358
406
|
try:
|
|
359
407
|
files = _artifact_files(staging)
|
|
360
408
|
except ValueError as exc:
|
|
@@ -416,7 +464,10 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
416
464
|
|
|
417
465
|
# C8: validate ownerPhase values in plan.md task table.
|
|
418
466
|
plan_path = staging / "plans" / f"{change_name}-plan.md"
|
|
419
|
-
|
|
467
|
+
try:
|
|
468
|
+
tasks = parse_plan_tasks(plan_path)
|
|
469
|
+
except PlanParseError as exc:
|
|
470
|
+
return _result_error(exc.code, str(exc))
|
|
420
471
|
if not tasks:
|
|
421
472
|
return _result_error(
|
|
422
473
|
"PLAN_TASKS_EMPTY",
|
|
@@ -444,7 +495,10 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
444
495
|
|
|
445
496
|
# C9: parse test-scenarios.md for scenario manifest.
|
|
446
497
|
scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
|
|
447
|
-
|
|
498
|
+
try:
|
|
499
|
+
scenarios = parse_test_scenarios(scenarios_path)
|
|
500
|
+
except PlanParseError as exc:
|
|
501
|
+
return _result_error(exc.code, str(exc))
|
|
448
502
|
if not scenarios:
|
|
449
503
|
return _result_error(
|
|
450
504
|
"PLAN_SCENARIOS_EMPTY",
|
|
@@ -542,6 +596,64 @@ def _validate_plan_start(
|
|
|
542
596
|
return {"ok": True, "phaseStartCount": 1}
|
|
543
597
|
|
|
544
598
|
|
|
599
|
+
def _receipt_artifact_targets(
|
|
600
|
+
change_dir: Path,
|
|
601
|
+
files_list: list[Any],
|
|
602
|
+
) -> tuple[list[tuple[str, Path]] | None, dict[str, Any] | None]:
|
|
603
|
+
targets: list[tuple[str, Path]] = []
|
|
604
|
+
seen: set[str] = set()
|
|
605
|
+
for index, value in enumerate(files_list):
|
|
606
|
+
if not isinstance(value, str):
|
|
607
|
+
return None, _result_error(
|
|
608
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
609
|
+
f"receipt files[{index}] must be a string",
|
|
610
|
+
)
|
|
611
|
+
raw = value
|
|
612
|
+
segments = raw.split("/")
|
|
613
|
+
rel = PurePosixPath(raw)
|
|
614
|
+
invalid = (
|
|
615
|
+
not raw
|
|
616
|
+
or raw != raw.strip()
|
|
617
|
+
or "\\" in raw
|
|
618
|
+
or ":" in raw
|
|
619
|
+
or rel.is_absolute()
|
|
620
|
+
or any(segment in {"", ".", ".."} for segment in segments)
|
|
621
|
+
or any(segment.endswith((".", " ")) for segment in segments)
|
|
622
|
+
or not rel.parts
|
|
623
|
+
or rel.parts[0] not in {"spec", "plans", "meta"}
|
|
624
|
+
)
|
|
625
|
+
if invalid:
|
|
626
|
+
return None, _result_error(
|
|
627
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
628
|
+
f"receipt files[{index}] is not a safe artifact-relative path: {raw!r}",
|
|
629
|
+
)
|
|
630
|
+
normalized = rel.as_posix()
|
|
631
|
+
if normalized in seen:
|
|
632
|
+
return None, _result_error(
|
|
633
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
634
|
+
f"receipt contains duplicate artifact path: {normalized}",
|
|
635
|
+
)
|
|
636
|
+
seen.add(normalized)
|
|
637
|
+
target = change_dir.joinpath(*rel.parts)
|
|
638
|
+
cursor = change_dir
|
|
639
|
+
for part in rel.parts:
|
|
640
|
+
cursor = cursor / part
|
|
641
|
+
if _is_link_or_reparse(cursor):
|
|
642
|
+
return None, _result_error(
|
|
643
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
644
|
+
f"receipt artifact path traverses a link: {normalized}",
|
|
645
|
+
)
|
|
646
|
+
try:
|
|
647
|
+
target.resolve(strict=False).relative_to(change_dir)
|
|
648
|
+
except (OSError, RuntimeError, ValueError):
|
|
649
|
+
return None, _result_error(
|
|
650
|
+
"RECEIPT_FILE_PATH_INVALID",
|
|
651
|
+
f"receipt artifact path resolves outside the change directory: {normalized}",
|
|
652
|
+
)
|
|
653
|
+
targets.append((normalized, target))
|
|
654
|
+
return targets, None
|
|
655
|
+
|
|
656
|
+
|
|
545
657
|
def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
546
658
|
"""Read-only verification of a finalized plan (retro §5.8).
|
|
547
659
|
|
|
@@ -565,6 +677,11 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
565
677
|
change_name = str(receipt.get("changeName") or "").strip()
|
|
566
678
|
if not change_name:
|
|
567
679
|
return _result_error("RECEIPT_INVALID", "receipt missing changeName")
|
|
680
|
+
if change_name != change_dir.name:
|
|
681
|
+
return _result_error(
|
|
682
|
+
"RECEIPT_CHANGE_NAME_INVALID",
|
|
683
|
+
f"receipt changeName {change_name!r} does not match {change_dir.name!r}",
|
|
684
|
+
)
|
|
568
685
|
|
|
569
686
|
expected_hash = str(receipt.get("artifactsHash") or "").strip()
|
|
570
687
|
if not expected_hash.startswith("sha256:"):
|
|
@@ -573,6 +690,19 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
573
690
|
files_list = receipt.get("files")
|
|
574
691
|
if not isinstance(files_list, list) or not files_list:
|
|
575
692
|
return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
|
|
693
|
+
artifact_targets, path_error = _receipt_artifact_targets(change_dir, files_list)
|
|
694
|
+
if path_error is not None:
|
|
695
|
+
return path_error
|
|
696
|
+
assert artifact_targets is not None
|
|
697
|
+
artifact_name_set = {rel for rel, _ in artifact_targets}
|
|
698
|
+
missing_required = sorted(
|
|
699
|
+
_required_artifact_names(change_name) - artifact_name_set
|
|
700
|
+
)
|
|
701
|
+
if missing_required:
|
|
702
|
+
return _result_error(
|
|
703
|
+
"RECEIPT_FILES_INCOMPLETE",
|
|
704
|
+
"receipt omits required artifacts: " + ", ".join(missing_required),
|
|
705
|
+
)
|
|
576
706
|
if receipt.get("status") != "finalized":
|
|
577
707
|
return _result_error(
|
|
578
708
|
"RECEIPT_NOT_FINALIZED",
|
|
@@ -589,9 +719,7 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
589
719
|
# Recompute artifacts hash from published files.
|
|
590
720
|
digest = hashlib.sha256()
|
|
591
721
|
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
|
|
722
|
+
for rel, target in artifact_targets:
|
|
595
723
|
if not target.is_file():
|
|
596
724
|
return _result_error(
|
|
597
725
|
"ARTIFACT_MISSING", f"published artifact missing: {rel}"
|
|
@@ -634,7 +762,10 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
634
762
|
}
|
|
635
763
|
|
|
636
764
|
plan_path = change_dir / "plans" / f"{change_name}-plan.md"
|
|
637
|
-
|
|
765
|
+
try:
|
|
766
|
+
expected_tasks = parse_plan_tasks(plan_path)
|
|
767
|
+
except PlanParseError as exc:
|
|
768
|
+
return _result_error(exc.code, str(exc))
|
|
638
769
|
if not expected_tasks:
|
|
639
770
|
return _result_error(
|
|
640
771
|
"PLAN_TASKS_EMPTY",
|
|
@@ -652,24 +783,25 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
652
783
|
)
|
|
653
784
|
except (OSError, json.JSONDecodeError) as exc:
|
|
654
785
|
return _result_error("IMPLEMENTATION_CHECKPOINTS_INVALID", str(exc))
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
if expected_task_ids != actual_task_ids:
|
|
786
|
+
expected_checkpoints = {
|
|
787
|
+
"schemaVersion": 1,
|
|
788
|
+
"changeName": change_name,
|
|
789
|
+
"tasks": expected_tasks,
|
|
790
|
+
"foundationGate": "approved",
|
|
791
|
+
}
|
|
792
|
+
if checkpoints != expected_checkpoints:
|
|
663
793
|
return _result_error(
|
|
664
794
|
"IMPLEMENTATION_CHECKPOINTS_DRIFT",
|
|
665
|
-
"derived
|
|
666
|
-
f"expected={expected_task_ids}, actual={actual_task_ids}",
|
|
795
|
+
"derived implementation checkpoints do not match the finalized plan",
|
|
667
796
|
)
|
|
668
797
|
|
|
669
798
|
scenarios_path = (
|
|
670
799
|
change_dir / "plans" / f"{change_name}-test-scenarios.md"
|
|
671
800
|
)
|
|
672
|
-
|
|
801
|
+
try:
|
|
802
|
+
expected_scenarios = parse_test_scenarios(scenarios_path)
|
|
803
|
+
except PlanParseError as exc:
|
|
804
|
+
return _result_error(exc.code, str(exc))
|
|
673
805
|
if not expected_scenarios:
|
|
674
806
|
return _result_error(
|
|
675
807
|
"PLAN_SCENARIOS_EMPTY",
|
|
@@ -694,26 +826,15 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
694
826
|
"scenario-manifest.json must contain at least one scenario",
|
|
695
827
|
)
|
|
696
828
|
|
|
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:
|
|
829
|
+
expected_manifest = {
|
|
830
|
+
"schemaVersion": 1,
|
|
831
|
+
"changeName": change_name,
|
|
832
|
+
"scenarios": expected_scenarios,
|
|
833
|
+
}
|
|
834
|
+
if manifest != expected_manifest:
|
|
714
835
|
return _result_error(
|
|
715
836
|
"SCENARIO_MANIFEST_DRIFT",
|
|
716
|
-
"derived scenario
|
|
837
|
+
"derived scenario manifest does not match the finalized scenario table",
|
|
717
838
|
)
|
|
718
839
|
|
|
719
840
|
# Validate gate-policy.json is parseable JSON.
|
|
@@ -730,7 +851,7 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
730
851
|
gate_policy_consistent = True
|
|
731
852
|
|
|
732
853
|
# Validate events.ndjson: require one matching start/end lifecycle.
|
|
733
|
-
events_path = change_dir
|
|
854
|
+
events_path = harness_events.events_path(change_dir)
|
|
734
855
|
phase_start_count = 0
|
|
735
856
|
phase_end_count = 0
|
|
736
857
|
phase_end_status: str | None = None
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "claude-code",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.25",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.35",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@1",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"path": "harness-plan/checklist.md",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "e99853043c543ad4ae3bcd4d5f773f5e518177034444b86cac3348c62a003f7a"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "harness-plan/protocols.md",
|
|
@@ -156,11 +156,11 @@
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"path": "harness-plan/reference.md",
|
|
159
|
-
"sha256": "
|
|
159
|
+
"sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"path": "harness-plan/SKILL.md",
|
|
163
|
-
"sha256": "
|
|
163
|
+
"sha256": "f53144c983ee228ad30c8020d738c0a7ddfdad67d93002a61083c8758b09de67"
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"path": "harness-review/checklist.md",
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"path": "scripts/harness_gate.py",
|
|
311
|
-
"sha256": "
|
|
311
|
+
"sha256": "bd2a7e9025be45f53d0ca9e91028e6451728a34e528a881422437a5f5100c77a"
|
|
312
312
|
},
|
|
313
313
|
{
|
|
314
314
|
"path": "scripts/harness_integration.py",
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
"path": "scripts/harness_plan_finalize.py",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "scripts/harness_preflight.py",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "codebuddy",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.25",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.35",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@1",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"path": "harness-plan/checklist.md",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "e99853043c543ad4ae3bcd4d5f773f5e518177034444b86cac3348c62a003f7a"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "harness-plan/protocols.md",
|
|
@@ -156,11 +156,11 @@
|
|
|
156
156
|
},
|
|
157
157
|
{
|
|
158
158
|
"path": "harness-plan/reference.md",
|
|
159
|
-
"sha256": "
|
|
159
|
+
"sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
|
|
160
160
|
},
|
|
161
161
|
{
|
|
162
162
|
"path": "harness-plan/SKILL.md",
|
|
163
|
-
"sha256": "
|
|
163
|
+
"sha256": "0cb82bef71217827d40750ee132eb9f3592bc54bcf61550a833d3de09075c40c"
|
|
164
164
|
},
|
|
165
165
|
{
|
|
166
166
|
"path": "harness-review/checklist.md",
|
|
@@ -308,7 +308,7 @@
|
|
|
308
308
|
},
|
|
309
309
|
{
|
|
310
310
|
"path": "scripts/harness_gate.py",
|
|
311
|
-
"sha256": "
|
|
311
|
+
"sha256": "bd2a7e9025be45f53d0ca9e91028e6451728a34e528a881422437a5f5100c77a"
|
|
312
312
|
},
|
|
313
313
|
{
|
|
314
314
|
"path": "scripts/harness_integration.py",
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
"path": "scripts/harness_plan_finalize.py",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "scripts/harness_preflight.py",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "codex",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.25",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.35",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@1",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"path": "harness-plan/checklist.md",
|
|
139
|
-
"sha256": "
|
|
139
|
+
"sha256": "e99853043c543ad4ae3bcd4d5f773f5e518177034444b86cac3348c62a003f7a"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
"path": "harness-plan/protocols.md",
|
|
@@ -144,11 +144,11 @@
|
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"path": "harness-plan/reference.md",
|
|
147
|
-
"sha256": "
|
|
147
|
+
"sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"path": "harness-plan/SKILL.md",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "5459bb03b0a1099b47b672b736a81f742a7b73f7a027bc18314f7526b7ad1e34"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "harness-review/checklist.md",
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
"path": "scripts/harness_gate.py",
|
|
299
|
-
"sha256": "
|
|
299
|
+
"sha256": "bd2a7e9025be45f53d0ca9e91028e6451728a34e528a881422437a5f5100c77a"
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
"path": "scripts/harness_integration.py",
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
},
|
|
321
321
|
{
|
|
322
322
|
"path": "scripts/harness_plan_finalize.py",
|
|
323
|
-
"sha256": "
|
|
323
|
+
"sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
"path": "scripts/harness_preflight.py",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "cursor",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.25",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.35",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@1",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -136,7 +136,7 @@
|
|
|
136
136
|
},
|
|
137
137
|
{
|
|
138
138
|
"path": "harness-plan/checklist.md",
|
|
139
|
-
"sha256": "
|
|
139
|
+
"sha256": "e99853043c543ad4ae3bcd4d5f773f5e518177034444b86cac3348c62a003f7a"
|
|
140
140
|
},
|
|
141
141
|
{
|
|
142
142
|
"path": "harness-plan/protocols.md",
|
|
@@ -144,11 +144,11 @@
|
|
|
144
144
|
},
|
|
145
145
|
{
|
|
146
146
|
"path": "harness-plan/reference.md",
|
|
147
|
-
"sha256": "
|
|
147
|
+
"sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
|
|
148
148
|
},
|
|
149
149
|
{
|
|
150
150
|
"path": "harness-plan/SKILL.md",
|
|
151
|
-
"sha256": "
|
|
151
|
+
"sha256": "d722638a7c1143c8831ba64ca90caf68590273d2feccaf7a8a2407ae15c5cf51"
|
|
152
152
|
},
|
|
153
153
|
{
|
|
154
154
|
"path": "harness-review/checklist.md",
|
|
@@ -296,7 +296,7 @@
|
|
|
296
296
|
},
|
|
297
297
|
{
|
|
298
298
|
"path": "scripts/harness_gate.py",
|
|
299
|
-
"sha256": "
|
|
299
|
+
"sha256": "bd2a7e9025be45f53d0ca9e91028e6451728a34e528a881422437a5f5100c77a"
|
|
300
300
|
},
|
|
301
301
|
{
|
|
302
302
|
"path": "scripts/harness_integration.py",
|
|
@@ -320,7 +320,7 @@
|
|
|
320
320
|
},
|
|
321
321
|
{
|
|
322
322
|
"path": "scripts/harness_plan_finalize.py",
|
|
323
|
-
"sha256": "
|
|
323
|
+
"sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
|
|
324
324
|
},
|
|
325
325
|
{
|
|
326
326
|
"path": "scripts/harness_preflight.py",
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "java",
|
|
4
4
|
"adapter": "claude-code",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.25",
|
|
6
6
|
"requires": {
|
|
7
|
-
"minimumCliVersion": "0.2.
|
|
7
|
+
"minimumCliVersion": "0.2.35",
|
|
8
8
|
"capabilities": [
|
|
9
9
|
"sync@1",
|
|
10
10
|
"rules-sync@1",
|
|
@@ -172,7 +172,7 @@
|
|
|
172
172
|
},
|
|
173
173
|
{
|
|
174
174
|
"path": "harness-plan/checklist.md",
|
|
175
|
-
"sha256": "
|
|
175
|
+
"sha256": "e99853043c543ad4ae3bcd4d5f773f5e518177034444b86cac3348c62a003f7a"
|
|
176
176
|
},
|
|
177
177
|
{
|
|
178
178
|
"path": "harness-plan/protocols.md",
|
|
@@ -180,11 +180,11 @@
|
|
|
180
180
|
},
|
|
181
181
|
{
|
|
182
182
|
"path": "harness-plan/reference.md",
|
|
183
|
-
"sha256": "
|
|
183
|
+
"sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
|
|
184
184
|
},
|
|
185
185
|
{
|
|
186
186
|
"path": "harness-plan/SKILL.md",
|
|
187
|
-
"sha256": "
|
|
187
|
+
"sha256": "091ef17813ed1e92415c849d655054f7cf483bb37dc728a1871af26c225f2ea4"
|
|
188
188
|
},
|
|
189
189
|
{
|
|
190
190
|
"path": "harness-review/checklist.md",
|
|
@@ -332,7 +332,7 @@
|
|
|
332
332
|
},
|
|
333
333
|
{
|
|
334
334
|
"path": "scripts/harness_gate.py",
|
|
335
|
-
"sha256": "
|
|
335
|
+
"sha256": "bd2a7e9025be45f53d0ca9e91028e6451728a34e528a881422437a5f5100c77a"
|
|
336
336
|
},
|
|
337
337
|
{
|
|
338
338
|
"path": "scripts/harness_integration.py",
|
|
@@ -356,7 +356,7 @@
|
|
|
356
356
|
},
|
|
357
357
|
{
|
|
358
358
|
"path": "scripts/harness_plan_finalize.py",
|
|
359
|
-
"sha256": "
|
|
359
|
+
"sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
|
|
360
360
|
},
|
|
361
361
|
{
|
|
362
362
|
"path": "scripts/harness_preflight.py",
|