@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.
Files changed (50) hide show
  1. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  2. package/harness/bundles/general/claude-code/harness-plan/checklist.md +2 -0
  3. package/harness/bundles/general/claude-code/harness-plan/reference.md +6 -0
  4. package/harness/bundles/general/claude-code/scripts/harness_gate.py +8 -19
  5. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +179 -58
  6. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  7. package/harness/bundles/general/codebuddy/harness-plan/checklist.md +2 -0
  8. package/harness/bundles/general/codebuddy/harness-plan/reference.md +6 -0
  9. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +8 -19
  10. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +179 -58
  11. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  12. package/harness/bundles/general/codex/harness-plan/checklist.md +2 -0
  13. package/harness/bundles/general/codex/harness-plan/reference.md +6 -0
  14. package/harness/bundles/general/codex/scripts/harness_gate.py +8 -19
  15. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +179 -58
  16. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  17. package/harness/bundles/general/cursor/harness-plan/checklist.md +2 -0
  18. package/harness/bundles/general/cursor/harness-plan/reference.md +6 -0
  19. package/harness/bundles/general/cursor/scripts/harness_gate.py +8 -19
  20. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +179 -58
  21. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  22. package/harness/bundles/java/claude-code/harness-plan/checklist.md +2 -0
  23. package/harness/bundles/java/claude-code/harness-plan/reference.md +6 -0
  24. package/harness/bundles/java/claude-code/scripts/harness_gate.py +8 -19
  25. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +179 -58
  26. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  27. package/harness/bundles/java/codebuddy/harness-plan/checklist.md +2 -0
  28. package/harness/bundles/java/codebuddy/harness-plan/reference.md +6 -0
  29. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +8 -19
  30. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +179 -58
  31. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  32. package/harness/bundles/java/codex/harness-plan/checklist.md +2 -0
  33. package/harness/bundles/java/codex/harness-plan/reference.md +6 -0
  34. package/harness/bundles/java/codex/scripts/harness_gate.py +8 -19
  35. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +179 -58
  36. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  37. package/harness/bundles/java/cursor/harness-plan/checklist.md +2 -0
  38. package/harness/bundles/java/cursor/harness-plan/reference.md +6 -0
  39. package/harness/bundles/java/cursor/scripts/harness_gate.py +8 -19
  40. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +179 -58
  41. package/harness/manifests/general/claude-code.json +7 -7
  42. package/harness/manifests/general/codebuddy.json +7 -7
  43. package/harness/manifests/general/codex.json +7 -7
  44. package/harness/manifests/general/cursor.json +7 -7
  45. package/harness/manifests/java/claude-code.json +7 -7
  46. package/harness/manifests/java/codebuddy.json +7 -7
  47. package/harness/manifests/java/codex.json +7 -7
  48. package/harness/manifests/java/cursor.json +7 -7
  49. package/hunter-workflow-family.json +4 -4
  50. 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
- row_index += 1
131
- continue
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
- row_index += 1
136
- continue
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": cells[scenario_index].strip(),
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 len(cells) < len(headers):
223
- row_index += 1
224
- continue
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 not number or not task_text or set(number) <= {"-", ":"}:
228
- row_index += 1
229
- continue
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 index is not None and cells[index].strip():
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.is_symlink():
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
- tasks = parse_plan_tasks(plan_path)
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
- scenarios = parse_test_scenarios(scenarios_path)
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 rel_text in files_list:
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
- expected_tasks = parse_plan_tasks(plan_path)
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
- actual_tasks = checkpoints.get("tasks") if isinstance(checkpoints, dict) else None
656
- expected_task_ids = [str(item.get("num") or "") for item in expected_tasks]
657
- actual_task_ids = (
658
- [str(item.get("num") or "") for item in actual_tasks if isinstance(item, dict)]
659
- if isinstance(actual_tasks, list)
660
- else []
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 task IDs do not match the finalized plan: "
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
- expected_scenarios = parse_test_scenarios(scenarios_path)
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
- def scenario_identity(item: dict[str, Any]) -> tuple[str, str, str, str]:
698
- return (
699
- str(item.get("id") or ""),
700
- str(item.get("priority") or ""),
701
- str(item.get("ownerPhase") or ""),
702
- str(item.get("requiredEvidenceKind") or ""),
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 identities do not match the finalized scenario table",
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 / "events.ndjson"
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.23",
5
+ "bundle_version": "0.2.25",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.33",
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": "005f6af01b829771cbec4135c65391964873b022cd2120c80e91a6554ec29cfd"
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": "8bfb369c958b4cb61021dc8604773a87e52a7dc5da62a059c0338aa3db252d4b"
159
+ "sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
160
160
  },
161
161
  {
162
162
  "path": "harness-plan/SKILL.md",
163
- "sha256": "990b0ee13acd0f7151ddd85acd9db70864b91a60a05482368f7466133f74cc33"
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": "be5bc122703efb1c4f37ca21c2eadfe78a8325d3420fee4d050a5662024faa76"
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": "4144e186c618bdb7576bdc2a5ade1f0bfb776b14b338022bd9bee4b413b05878"
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.23",
5
+ "bundle_version": "0.2.25",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.33",
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": "005f6af01b829771cbec4135c65391964873b022cd2120c80e91a6554ec29cfd"
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": "8bfb369c958b4cb61021dc8604773a87e52a7dc5da62a059c0338aa3db252d4b"
159
+ "sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
160
160
  },
161
161
  {
162
162
  "path": "harness-plan/SKILL.md",
163
- "sha256": "8fa4f34d26aa1e841d13054dd26bbdc163f98fb70ed41c3c829286708fa7bc9b"
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": "be5bc122703efb1c4f37ca21c2eadfe78a8325d3420fee4d050a5662024faa76"
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": "4144e186c618bdb7576bdc2a5ade1f0bfb776b14b338022bd9bee4b413b05878"
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.23",
5
+ "bundle_version": "0.2.25",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.33",
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": "005f6af01b829771cbec4135c65391964873b022cd2120c80e91a6554ec29cfd"
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": "8bfb369c958b4cb61021dc8604773a87e52a7dc5da62a059c0338aa3db252d4b"
147
+ "sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
148
148
  },
149
149
  {
150
150
  "path": "harness-plan/SKILL.md",
151
- "sha256": "7856d3fe5ff457e5256ca86924e8b17a2343bcfe9ff5bf260f374682919ac478"
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": "be5bc122703efb1c4f37ca21c2eadfe78a8325d3420fee4d050a5662024faa76"
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": "4144e186c618bdb7576bdc2a5ade1f0bfb776b14b338022bd9bee4b413b05878"
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.23",
5
+ "bundle_version": "0.2.25",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.33",
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": "005f6af01b829771cbec4135c65391964873b022cd2120c80e91a6554ec29cfd"
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": "8bfb369c958b4cb61021dc8604773a87e52a7dc5da62a059c0338aa3db252d4b"
147
+ "sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
148
148
  },
149
149
  {
150
150
  "path": "harness-plan/SKILL.md",
151
- "sha256": "1ade01ac8eb0afc8a40d0eff90bd4a8f9d6dc93646ca9b3cd73e0fb90b1d36b0"
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": "be5bc122703efb1c4f37ca21c2eadfe78a8325d3420fee4d050a5662024faa76"
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": "4144e186c618bdb7576bdc2a5ade1f0bfb776b14b338022bd9bee4b413b05878"
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.23",
5
+ "bundle_version": "0.2.25",
6
6
  "requires": {
7
- "minimumCliVersion": "0.2.33",
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": "005f6af01b829771cbec4135c65391964873b022cd2120c80e91a6554ec29cfd"
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": "8bfb369c958b4cb61021dc8604773a87e52a7dc5da62a059c0338aa3db252d4b"
183
+ "sha256": "a763a7a92ffaf35a0b0ea5a866ac84447245b02bfc53406f24e9470897ca2d61"
184
184
  },
185
185
  {
186
186
  "path": "harness-plan/SKILL.md",
187
- "sha256": "57101fbad56bb5b58c81a116dae408726d13510c7074fe1003010047bc3e9a43"
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": "be5bc122703efb1c4f37ca21c2eadfe78a8325d3420fee4d050a5662024faa76"
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": "4144e186c618bdb7576bdc2a5ade1f0bfb776b14b338022bd9bee4b413b05878"
359
+ "sha256": "1e9b81af4ed3166c3847b6668899e9b7bf32a27f9b879657fe92ee402bf9da4c"
360
360
  },
361
361
  {
362
362
  "path": "scripts/harness_preflight.py",