@hunter-harness/workflow-harness 0.2.19 → 0.2.21
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/CONTEXT.md +20 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/CONTEXT.md +20 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/CONTEXT.md +20 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/general/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/CONTEXT.md +20 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/CONTEXT.md +20 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/CONTEXT.md +20 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- 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 +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +8 -0
- 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/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/java/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/manifests/general/claude-code.json +25 -17
- package/harness/manifests/general/codebuddy.json +25 -17
- package/harness/manifests/general/codex.json +25 -17
- package/harness/manifests/general/cursor.json +25 -17
- package/harness/manifests/java/claude-code.json +26 -18
- package/harness/manifests/java/codebuddy.json +26 -18
- package/harness/manifests/java/codex.json +26 -18
- package/harness/manifests/java/cursor.json +26 -18
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
import time
|
|
25
25
|
import uuid
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any
|
|
27
|
+
from typing import Any, Sequence
|
|
28
28
|
|
|
29
29
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
30
30
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -595,6 +595,132 @@ def append_event(
|
|
|
595
595
|
return {"ok": True, "event": event, "events_path": str(events_path_obj), "rendered": False}
|
|
596
596
|
|
|
597
597
|
|
|
598
|
+
def batch_append_events(
|
|
599
|
+
change_dir: Path,
|
|
600
|
+
events: Sequence[dict[str, Any]],
|
|
601
|
+
) -> dict[str, Any]:
|
|
602
|
+
"""Validate then append many events under one lock (Wave-2 H-15).
|
|
603
|
+
|
|
604
|
+
Any invalid item aborts the whole batch before mutation. ``phase.end`` items
|
|
605
|
+
are rejected here — use single ``append`` so render semantics stay explicit.
|
|
606
|
+
"""
|
|
607
|
+
if not isinstance(events, (list, tuple)) or not events:
|
|
608
|
+
return {
|
|
609
|
+
"ok": False,
|
|
610
|
+
"code": "BATCH_EMPTY",
|
|
611
|
+
"message": "batch-append requires a non-empty events array",
|
|
612
|
+
}
|
|
613
|
+
archived = archived_change_dir(change_dir)
|
|
614
|
+
if archived is not None:
|
|
615
|
+
return {
|
|
616
|
+
"ok": False,
|
|
617
|
+
"code": "ARCHIVED_CHANGE_IMMUTABLE",
|
|
618
|
+
"message": str(archived),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
built: list[dict[str, Any]] = []
|
|
622
|
+
for index, raw in enumerate(events):
|
|
623
|
+
if not isinstance(raw, dict):
|
|
624
|
+
return {
|
|
625
|
+
"ok": False,
|
|
626
|
+
"code": "BATCH_ITEM_INVALID",
|
|
627
|
+
"message": f"events[{index}] must be an object",
|
|
628
|
+
"index": index,
|
|
629
|
+
}
|
|
630
|
+
type_ = str(raw.get("type") or "").strip()
|
|
631
|
+
phase = str(raw.get("phase") or "").strip()
|
|
632
|
+
if not type_ or type_ not in EVENT_TYPES:
|
|
633
|
+
return {
|
|
634
|
+
"ok": False,
|
|
635
|
+
"code": "EVENT_TYPE_INVALID",
|
|
636
|
+
"message": f"events[{index}]: unsupported type: {type_}",
|
|
637
|
+
"index": index,
|
|
638
|
+
}
|
|
639
|
+
if not phase:
|
|
640
|
+
return {
|
|
641
|
+
"ok": False,
|
|
642
|
+
"code": "EVENT_REQUIRED_FIELD",
|
|
643
|
+
"message": f"events[{index}]: phase required",
|
|
644
|
+
"index": index,
|
|
645
|
+
}
|
|
646
|
+
if type_ == "phase.end":
|
|
647
|
+
return {
|
|
648
|
+
"ok": False,
|
|
649
|
+
"code": "BATCH_PHASE_END_FORBIDDEN",
|
|
650
|
+
"message": f"events[{index}]: phase.end must use single append",
|
|
651
|
+
"index": index,
|
|
652
|
+
}
|
|
653
|
+
args = argparse.Namespace(
|
|
654
|
+
change_dir=str(change_dir),
|
|
655
|
+
phase=phase,
|
|
656
|
+
type=type_,
|
|
657
|
+
note=raw.get("note"),
|
|
658
|
+
kind=raw.get("kind"),
|
|
659
|
+
path=raw.get("path"),
|
|
660
|
+
run_id=raw.get("run_id"),
|
|
661
|
+
executor_tool=raw.get("executor_tool"),
|
|
662
|
+
command=raw.get("command"),
|
|
663
|
+
exit_code=raw.get("exit_code"),
|
|
664
|
+
duration_ms=raw.get("duration_ms"),
|
|
665
|
+
status=raw.get("status"),
|
|
666
|
+
name=raw.get("name"),
|
|
667
|
+
code=raw.get("code"),
|
|
668
|
+
severity=raw.get("severity"),
|
|
669
|
+
message=raw.get("message"),
|
|
670
|
+
decision=raw.get("decision"),
|
|
671
|
+
reason=raw.get("reason"),
|
|
672
|
+
issue_id=raw.get("issue_id"),
|
|
673
|
+
scope=raw.get("scope"),
|
|
674
|
+
target_event_id=raw.get("target_event_id"),
|
|
675
|
+
target_field=raw.get("target_field"),
|
|
676
|
+
old_value_hash=raw.get("old_value_hash"),
|
|
677
|
+
new_value_json=raw.get("new_value_json"),
|
|
678
|
+
renamed_from=raw.get("renamed_from"),
|
|
679
|
+
renamed_to=raw.get("renamed_to"),
|
|
680
|
+
change_uuid=raw.get("change_uuid"),
|
|
681
|
+
attempt=raw.get("attempt"),
|
|
682
|
+
executor_agent=raw.get("executor_agent"),
|
|
683
|
+
executor_model=raw.get("executor_model"),
|
|
684
|
+
handoff_from_tool=raw.get("handoff_from_tool"),
|
|
685
|
+
handoff_reason=raw.get("handoff_reason"),
|
|
686
|
+
trace_id=raw.get("trace_id"),
|
|
687
|
+
span_id=raw.get("span_id"),
|
|
688
|
+
parent_span_id=raw.get("parent_span_id"),
|
|
689
|
+
runner_ms=raw.get("runner_ms"),
|
|
690
|
+
orchestration_active_ms=raw.get("orchestration_active_ms"),
|
|
691
|
+
wall_clock_ms=raw.get("wall_clock_ms"),
|
|
692
|
+
user_wait_ms=raw.get("user_wait_ms"),
|
|
693
|
+
legacy_lenient=False,
|
|
694
|
+
json=True,
|
|
695
|
+
)
|
|
696
|
+
validation = validate_append_event(args)
|
|
697
|
+
if validation:
|
|
698
|
+
error_code, message = validation
|
|
699
|
+
return {
|
|
700
|
+
"ok": False,
|
|
701
|
+
"code": error_code,
|
|
702
|
+
"message": f"events[{index}]: {message}",
|
|
703
|
+
"index": index,
|
|
704
|
+
}
|
|
705
|
+
built.append(build_event(args, []))
|
|
706
|
+
|
|
707
|
+
events_path_obj = events_path(change_dir)
|
|
708
|
+
lock_path = events_path_obj.with_name(events_path_obj.name + ".lock")
|
|
709
|
+
with event_file_lock(lock_path):
|
|
710
|
+
for event in built:
|
|
711
|
+
atomic_append_line(
|
|
712
|
+
events_path_obj, json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
713
|
+
)
|
|
714
|
+
return {
|
|
715
|
+
"ok": True,
|
|
716
|
+
"action": "batch-append",
|
|
717
|
+
"count": len(built),
|
|
718
|
+
"events": built,
|
|
719
|
+
"events_path": str(events_path_obj),
|
|
720
|
+
"rendered": False,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
598
724
|
def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dict[str, Any]:
|
|
599
725
|
event: dict[str, Any] = {
|
|
600
726
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -1295,6 +1421,25 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1295
1421
|
return 0
|
|
1296
1422
|
|
|
1297
1423
|
|
|
1424
|
+
def cmd_batch_append(args: argparse.Namespace) -> int:
|
|
1425
|
+
as_json = bool(args.json)
|
|
1426
|
+
change_dir = resolve_change_dir(args.change_dir)
|
|
1427
|
+
batch_path = Path(str(args.file)).expanduser()
|
|
1428
|
+
try:
|
|
1429
|
+
raw = json.loads(batch_path.read_text(encoding="utf-8-sig"))
|
|
1430
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1431
|
+
return emit_error(f"batch-append failed to read --file: {exc}", as_json=as_json)
|
|
1432
|
+
result = batch_append_events(change_dir, raw if isinstance(raw, list) else [])
|
|
1433
|
+
if not result.get("ok"):
|
|
1434
|
+
return emit_error(
|
|
1435
|
+
str(result.get("message") or "batch-append failed"),
|
|
1436
|
+
as_json=as_json,
|
|
1437
|
+
error_code=str(result.get("code") or "BATCH_APPEND_FAILED"),
|
|
1438
|
+
)
|
|
1439
|
+
emit_json(result, as_json=as_json)
|
|
1440
|
+
return 0
|
|
1441
|
+
|
|
1442
|
+
|
|
1298
1443
|
def cmd_render(args: argparse.Namespace) -> int:
|
|
1299
1444
|
as_json = bool(args.json)
|
|
1300
1445
|
change_dir = resolve_change_dir(args.change_dir)
|
|
@@ -1395,6 +1540,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1395
1540
|
)
|
|
1396
1541
|
p_append.set_defaults(func=cmd_append)
|
|
1397
1542
|
|
|
1543
|
+
p_batch = sub.add_parser(
|
|
1544
|
+
"batch-append",
|
|
1545
|
+
parents=[common],
|
|
1546
|
+
help="validate and append many events under one lock (H-15)",
|
|
1547
|
+
)
|
|
1548
|
+
p_batch.add_argument("--change-dir", required=True)
|
|
1549
|
+
p_batch.add_argument(
|
|
1550
|
+
"--file",
|
|
1551
|
+
required=True,
|
|
1552
|
+
help="JSON file containing a non-empty array of event objects",
|
|
1553
|
+
)
|
|
1554
|
+
p_batch.set_defaults(func=cmd_batch_append)
|
|
1555
|
+
|
|
1398
1556
|
p_render = sub.add_parser(
|
|
1399
1557
|
"render",
|
|
1400
1558
|
parents=[common],
|
|
@@ -862,8 +862,42 @@ class IntegrationTransaction:
|
|
|
862
862
|
|
|
863
863
|
return self._run_step("merge", action)
|
|
864
864
|
|
|
865
|
+
def _required_on_merge_commands(self) -> list[list[str]]:
|
|
866
|
+
"""Load build-profile mergeVerification.requiredOnMerge (Wave-2 H-9)."""
|
|
867
|
+
import harness_profile
|
|
868
|
+
|
|
869
|
+
profile = harness_profile.load_profile(self.project_root)
|
|
870
|
+
if not isinstance(profile, dict):
|
|
871
|
+
return []
|
|
872
|
+
merge = profile.get("mergeVerification")
|
|
873
|
+
if not isinstance(merge, dict):
|
|
874
|
+
return []
|
|
875
|
+
required = merge.get("requiredOnMerge")
|
|
876
|
+
if not isinstance(required, list):
|
|
877
|
+
return []
|
|
878
|
+
commands: list[list[str]] = []
|
|
879
|
+
for index, item in enumerate(required):
|
|
880
|
+
if isinstance(item, str) and item.strip():
|
|
881
|
+
commands.append(shlex.split(item.strip()))
|
|
882
|
+
continue
|
|
883
|
+
if not isinstance(item, dict):
|
|
884
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
885
|
+
raw = item.get("command")
|
|
886
|
+
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
|
+
commands.append([str(x) for x in raw])
|
|
888
|
+
elif isinstance(raw, str) and raw.strip():
|
|
889
|
+
commands.append(shlex.split(raw.strip()))
|
|
890
|
+
else:
|
|
891
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
|
+
return commands
|
|
893
|
+
|
|
865
894
|
def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
|
|
866
895
|
commands = list(commands or [])
|
|
896
|
+
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
|
+
try:
|
|
898
|
+
commands.extend(self._required_on_merge_commands())
|
|
899
|
+
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
|
+
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
867
901
|
|
|
868
902
|
def action(journal: dict[str, Any]) -> None:
|
|
869
903
|
if not commands or any(
|
|
@@ -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_profile # noqa: E402
|
|
31
32
|
|
|
32
33
|
|
|
33
34
|
if hasattr(sys.stdout, "reconfigure"):
|
|
@@ -534,18 +535,30 @@ def expand_profile_input_files(
|
|
|
534
535
|
) -> tuple[list[str], str | None]:
|
|
535
536
|
"""Expand verificationInputs[profile_input] globs from build-profile.json.
|
|
536
537
|
|
|
537
|
-
|
|
538
|
-
|
|
538
|
+
Profile is loaded via ``harness_profile.load_profile`` (C7: common_root then
|
|
539
|
+
execution overlay) so linked worktrees without a local build-profile still
|
|
540
|
+
reuse the main checkout profile. Globs remain relative to the execution
|
|
541
|
+
``project`` root (not common_root) so inputsHash tracks the tree under test.
|
|
542
|
+
|
|
543
|
+
Returns (files, error); error is None on success.
|
|
539
544
|
profile 缺失 / key 缺失 / glob 无匹配 / 结果为空 → 返回 ([], "<reason>"),
|
|
540
545
|
调用方据此返回 insufficient-evidence,执行全量测试但不允许缓存复用。
|
|
541
546
|
"""
|
|
542
|
-
|
|
543
|
-
if
|
|
547
|
+
profile = harness_profile.load_profile(Path(project))
|
|
548
|
+
if profile is None:
|
|
549
|
+
# load_profile swallows JSON errors; restore actionable unreadable diag
|
|
550
|
+
# when a profile file exists but cannot be parsed (review YELLOW-1).
|
|
551
|
+
project_root = Path(project).resolve()
|
|
552
|
+
common = harness_paths.common_root(project_root)
|
|
553
|
+
for root in (common, project_root):
|
|
554
|
+
candidate = root / ".harness" / "config" / "build-profile.json"
|
|
555
|
+
if not candidate.is_file():
|
|
556
|
+
continue
|
|
557
|
+
try:
|
|
558
|
+
json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
559
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
560
|
+
return [], f"build-profile.json unreadable: {exc}"
|
|
544
561
|
return [], "build-profile.json missing; run harness_preflight.py detect"
|
|
545
|
-
try:
|
|
546
|
-
profile = json.loads(profile_path.read_text(encoding="utf-8-sig"))
|
|
547
|
-
except (OSError, json.JSONDecodeError) as exc:
|
|
548
|
-
return [], f"build-profile.json unreadable: {exc}"
|
|
549
562
|
if not isinstance(profile, dict):
|
|
550
563
|
return [], "build-profile.json is not an object"
|
|
551
564
|
inputs = profile.get("verificationInputs")
|
|
@@ -555,7 +568,7 @@ def expand_profile_input_files(
|
|
|
555
568
|
if not isinstance(patterns, list) or not patterns:
|
|
556
569
|
return [], f"verificationInputs.{profile_input} is empty or invalid"
|
|
557
570
|
|
|
558
|
-
base = project.resolve()
|
|
571
|
+
base = Path(project).resolve()
|
|
559
572
|
seen: set[str] = set()
|
|
560
573
|
for pat in patterns:
|
|
561
574
|
if not isinstance(pat, str) or not pat.strip():
|
|
@@ -1042,6 +1055,7 @@ def decide_can_reuse(
|
|
|
1042
1055
|
requested_toolchain_hash: str | None = None,
|
|
1043
1056
|
requested_profile_hash: str | None = None,
|
|
1044
1057
|
requested_environment_hash: str | None = None,
|
|
1058
|
+
requested_db_schema_hash: str | None = None,
|
|
1045
1059
|
) -> dict[str, Any]:
|
|
1046
1060
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1047
1061
|
if ledger is None:
|
|
@@ -1170,6 +1184,7 @@ def decide_can_reuse(
|
|
|
1170
1184
|
("toolchainHash", requested_toolchain_hash, "TOOLCHAIN_CHANGED"),
|
|
1171
1185
|
("profileHash", requested_profile_hash, "PROFILE_CHANGED"),
|
|
1172
1186
|
("environmentHash", requested_environment_hash, "ENVIRONMENT_CHANGED"),
|
|
1187
|
+
("dbSchemaHash", requested_db_schema_hash, "DB_SCHEMA_CHANGED"),
|
|
1173
1188
|
):
|
|
1174
1189
|
stored = entry.get(field)
|
|
1175
1190
|
if (
|
|
@@ -1236,6 +1251,7 @@ def decide_can_reuse(
|
|
|
1236
1251
|
"reuse": True,
|
|
1237
1252
|
"reason": "reuse",
|
|
1238
1253
|
"code": "REUSED",
|
|
1254
|
+
"invalidationCode": None,
|
|
1239
1255
|
"verification": verification,
|
|
1240
1256
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
1241
1257
|
"inputsHash": stored_hash,
|
|
@@ -1245,6 +1261,90 @@ def decide_can_reuse(
|
|
|
1245
1261
|
}
|
|
1246
1262
|
|
|
1247
1263
|
|
|
1264
|
+
_INVALIDATION_ALIASES = {
|
|
1265
|
+
"INPUTS_HASH_CHANGED": "INPUT_CHANGED",
|
|
1266
|
+
"ENVIRONMENT_CHANGED": "ENV_CHANGED",
|
|
1267
|
+
"MISSING_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1268
|
+
"MISSING_V2_FIELDS": "MISSING_IDENTITY_FIELD",
|
|
1269
|
+
"LEDGER_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1270
|
+
"VALIDATIONS_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1271
|
+
"VALIDATION_MISSING": "MISSING_IDENTITY_FIELD",
|
|
1272
|
+
}
|
|
1273
|
+
|
|
1274
|
+
|
|
1275
|
+
def _attach_invalidation_code(payload: dict[str, Any]) -> dict[str, Any]:
|
|
1276
|
+
"""Wave-2 H-9: expose stable invalidationCode alongside legacy code."""
|
|
1277
|
+
if payload.get("reuse") is True:
|
|
1278
|
+
payload.setdefault("invalidationCode", None)
|
|
1279
|
+
return payload
|
|
1280
|
+
code = str(payload.get("code") or "").strip()
|
|
1281
|
+
if code:
|
|
1282
|
+
payload["invalidationCode"] = _INVALIDATION_ALIASES.get(code, code)
|
|
1283
|
+
return payload
|
|
1284
|
+
|
|
1285
|
+
|
|
1286
|
+
def verification_graph_path(change_dir: Path) -> Path:
|
|
1287
|
+
return Path(change_dir) / "evidence" / "verification-graph.json"
|
|
1288
|
+
|
|
1289
|
+
|
|
1290
|
+
def upsert_verification_graph_node(
|
|
1291
|
+
change_dir: Path,
|
|
1292
|
+
*,
|
|
1293
|
+
verification: str,
|
|
1294
|
+
status: str,
|
|
1295
|
+
inputs_hash: str,
|
|
1296
|
+
command: str | None = None,
|
|
1297
|
+
toolchain_hash: str | None = None,
|
|
1298
|
+
environment_hash: str | None = None,
|
|
1299
|
+
db_schema_hash: str | None = None,
|
|
1300
|
+
diff_hash: str | None = None,
|
|
1301
|
+
) -> dict[str, Any]:
|
|
1302
|
+
"""Persist a verification graph node keyed by canonical identity (H-9)."""
|
|
1303
|
+
path = verification_graph_path(change_dir)
|
|
1304
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
1305
|
+
graph: dict[str, Any]
|
|
1306
|
+
if path.is_file():
|
|
1307
|
+
try:
|
|
1308
|
+
loaded = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
1309
|
+
graph = loaded if isinstance(loaded, dict) else {"schemaVersion": 1, "nodes": []}
|
|
1310
|
+
except (OSError, json.JSONDecodeError):
|
|
1311
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1312
|
+
else:
|
|
1313
|
+
graph = {"schemaVersion": 1, "nodes": []}
|
|
1314
|
+
nodes = graph.get("nodes")
|
|
1315
|
+
if not isinstance(nodes, list):
|
|
1316
|
+
nodes = []
|
|
1317
|
+
identity = {
|
|
1318
|
+
"verification": verification,
|
|
1319
|
+
"inputsHash": inputs_hash,
|
|
1320
|
+
"command": command,
|
|
1321
|
+
"toolchainHash": toolchain_hash,
|
|
1322
|
+
"environmentHash": environment_hash,
|
|
1323
|
+
"dbSchemaHash": db_schema_hash,
|
|
1324
|
+
"diffHash": diff_hash,
|
|
1325
|
+
}
|
|
1326
|
+
node = {
|
|
1327
|
+
"identity": identity,
|
|
1328
|
+
"status": status,
|
|
1329
|
+
"recordedAt": dt.datetime.now().astimezone().isoformat(timespec="milliseconds"),
|
|
1330
|
+
}
|
|
1331
|
+
replaced = False
|
|
1332
|
+
for index, existing in enumerate(nodes):
|
|
1333
|
+
if not isinstance(existing, dict):
|
|
1334
|
+
continue
|
|
1335
|
+
existing_id = existing.get("identity")
|
|
1336
|
+
if isinstance(existing_id, dict) and existing_id == identity:
|
|
1337
|
+
nodes[index] = node
|
|
1338
|
+
replaced = True
|
|
1339
|
+
break
|
|
1340
|
+
if not replaced:
|
|
1341
|
+
nodes.append(node)
|
|
1342
|
+
graph["schemaVersion"] = 1
|
|
1343
|
+
graph["nodes"] = nodes
|
|
1344
|
+
write_ledger(path, graph) # atomic json write helper already used for ledger
|
|
1345
|
+
return {"ok": True, "path": str(path), "nodeCount": len(nodes), "replaced": replaced}
|
|
1346
|
+
|
|
1347
|
+
|
|
1248
1348
|
def cmd_hash(args: argparse.Namespace) -> int:
|
|
1249
1349
|
as_json = bool(args.json)
|
|
1250
1350
|
try:
|
|
@@ -1317,10 +1417,12 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1317
1417
|
requested_toolchain_hash=getattr(args, "toolchain_hash", None),
|
|
1318
1418
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1319
1419
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
|
+
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1320
1421
|
)
|
|
1321
1422
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1322
1423
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
1323
1424
|
|
|
1425
|
+
payload = _attach_invalidation_code(payload)
|
|
1324
1426
|
emit_compact_or_verbose(
|
|
1325
1427
|
payload, as_json=as_json, verbose=verbose,
|
|
1326
1428
|
compact_fn=_compact_can_reuse_payload,
|
|
@@ -1457,6 +1559,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1457
1559
|
("toolchainHash", "toolchain_hash"),
|
|
1458
1560
|
("profileHash", "profile_hash"),
|
|
1459
1561
|
("environmentHash", "environment_hash"),
|
|
1562
|
+
("dbSchemaHash", "db_schema_hash"),
|
|
1460
1563
|
):
|
|
1461
1564
|
val = getattr(args, attr, None)
|
|
1462
1565
|
if _nonempty_str(val):
|
|
@@ -1535,6 +1638,37 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1535
1638
|
extra={"missing": missing},
|
|
1536
1639
|
)
|
|
1537
1640
|
write_ledger(out_path, ledger)
|
|
1641
|
+
try:
|
|
1642
|
+
upsert_verification_graph_node(
|
|
1643
|
+
change_dir,
|
|
1644
|
+
verification=verification,
|
|
1645
|
+
status=str(status),
|
|
1646
|
+
inputs_hash=str(inputs_hash),
|
|
1647
|
+
command=str(entry.get("command") or "") or None,
|
|
1648
|
+
toolchain_hash=(
|
|
1649
|
+
str(entry.get("toolchainHash")).strip()
|
|
1650
|
+
if _nonempty_str(entry.get("toolchainHash"))
|
|
1651
|
+
else None
|
|
1652
|
+
),
|
|
1653
|
+
environment_hash=(
|
|
1654
|
+
str(entry.get("environmentHash")).strip()
|
|
1655
|
+
if _nonempty_str(entry.get("environmentHash"))
|
|
1656
|
+
else None
|
|
1657
|
+
),
|
|
1658
|
+
db_schema_hash=(
|
|
1659
|
+
str(entry.get("dbSchemaHash")).strip()
|
|
1660
|
+
if _nonempty_str(entry.get("dbSchemaHash"))
|
|
1661
|
+
else None
|
|
1662
|
+
),
|
|
1663
|
+
diff_hash=(
|
|
1664
|
+
str(ledger.get("diffHash")).strip()
|
|
1665
|
+
if _nonempty_str(ledger.get("diffHash"))
|
|
1666
|
+
else None
|
|
1667
|
+
),
|
|
1668
|
+
)
|
|
1669
|
+
except (OSError, ValueError, TypeError):
|
|
1670
|
+
# Graph is advisory persistence; ledger write already succeeded.
|
|
1671
|
+
pass
|
|
1538
1672
|
except (OSError, ValueError, FileNotFoundError, json.JSONDecodeError) as exc:
|
|
1539
1673
|
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
1540
1674
|
|
|
@@ -1654,6 +1788,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1654
1788
|
default=None,
|
|
1655
1789
|
help="optional environment hash to compare against ledger entry (UT-017)",
|
|
1656
1790
|
)
|
|
1791
|
+
p_reuse.add_argument(
|
|
1792
|
+
"--db-schema-hash",
|
|
1793
|
+
default=None,
|
|
1794
|
+
help="optional DB schema hash to compare against ledger entry (H-9)",
|
|
1795
|
+
)
|
|
1657
1796
|
p_reuse.add_argument(
|
|
1658
1797
|
"--verbose",
|
|
1659
1798
|
action="store_true",
|
|
@@ -1695,6 +1834,7 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1695
1834
|
p_record.add_argument("--toolchain-hash", default=None)
|
|
1696
1835
|
p_record.add_argument("--profile-hash", default=None)
|
|
1697
1836
|
p_record.add_argument("--environment-hash", default=None)
|
|
1837
|
+
p_record.add_argument("--db-schema-hash", default=None)
|
|
1698
1838
|
p_record.add_argument("--deploy-artifact", default=None, help="package: built artifact path")
|
|
1699
1839
|
p_record.add_argument("--artifact-hash", default=None, help="package: artifact sha256")
|
|
1700
1840
|
p_record.add_argument(
|