@hunter-harness/workflow-harness 0.2.22 → 0.2.24
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/README.md +2 -2
- package/harness/bundles/general/claude-code/agents/harness-evaluator.md +4 -4
- package/harness/bundles/general/claude-code/agents/harness-explorer.md +2 -2
- package/harness/bundles/general/claude-code/agents/harness-reviewer.md +2 -2
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/claude-code/harness-plan/reference.md +24 -34
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/protocols.md +3 -3
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +9 -3
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +574 -12
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +2 -2
- package/harness/bundles/general/codebuddy/agents/harness-evaluator.md +5 -6
- package/harness/bundles/general/codebuddy/agents/harness-explorer.md +3 -3
- package/harness/bundles/general/codebuddy/agents/harness-reviewer.md +3 -5
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +24 -34
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +3 -3
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +9 -3
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +574 -12
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +2 -2
- package/harness/bundles/general/codex/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/general/codex/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/codex/harness-plan/reference.md +24 -34
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/protocols.md +3 -3
- package/harness/bundles/general/codex/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +9 -3
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +574 -12
- package/harness/bundles/general/codex/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codex/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +2 -2
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/general/cursor/harness-plan/checklist.md +13 -17
- package/harness/bundles/general/cursor/harness-plan/reference.md +24 -34
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/protocols.md +3 -3
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +9 -3
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +574 -12
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +22 -3
- package/harness/bundles/general/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +50 -29
- package/harness/bundles/general/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +2 -2
- package/harness/bundles/java/claude-code/agents/harness-evaluator.md +4 -4
- package/harness/bundles/java/claude-code/agents/harness-explorer.md +2 -2
- package/harness/bundles/java/claude-code/agents/harness-reviewer.md +2 -2
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/claude-code/harness-plan/reference.md +24 -34
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/protocols.md +3 -3
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +9 -3
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +574 -12
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +2 -2
- package/harness/bundles/java/codebuddy/agents/harness-evaluator.md +5 -6
- package/harness/bundles/java/codebuddy/agents/harness-explorer.md +3 -3
- package/harness/bundles/java/codebuddy/agents/harness-reviewer.md +3 -5
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -4
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +24 -34
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +3 -3
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +9 -3
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +574 -12
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +2 -2
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/java/codex/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/codex/harness-plan/reference.md +24 -34
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/protocols.md +3 -3
- package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +9 -3
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +574 -12
- package/harness/bundles/java/codex/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codex/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +2 -2
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +12 -3
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +11 -0
- 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 +8 -5
- package/harness/bundles/java/cursor/harness-plan/checklist.md +13 -17
- package/harness/bundles/java/cursor/harness-plan/reference.md +24 -34
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/protocols.md +3 -3
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +9 -3
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +574 -12
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +22 -3
- package/harness/bundles/java/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +50 -29
- package/harness/bundles/java/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +30 -8
- package/harness/manifests/general/claude-code.json +28 -28
- package/harness/manifests/general/codebuddy.json +28 -28
- package/harness/manifests/general/codex.json +25 -25
- package/harness/manifests/general/cursor.json +25 -25
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +27 -27
- package/harness/manifests/java/cursor.json +27 -27
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -16,6 +16,7 @@ import re
|
|
|
16
16
|
import shutil
|
|
17
17
|
import subprocess
|
|
18
18
|
import sys
|
|
19
|
+
import time
|
|
19
20
|
import uuid
|
|
20
21
|
from pathlib import Path
|
|
21
22
|
from typing import Any, Iterable
|
|
@@ -38,6 +39,7 @@ HEADING_RE = re.compile(r"^(#{2,6})\s+(.+?)\s*$")
|
|
|
38
39
|
FRAGMENT_HINT_RE = re.compile(r"^>\s*片段:\[\[shared/[^\]]+\]\]\s*.*$")
|
|
39
40
|
|
|
40
41
|
BUILD_MARKER = ".harness-build.json"
|
|
42
|
+
_REPLACE_RETRY_DELAYS_SECONDS = (0.05, 0.1, 0.2, 0.4, 0.8)
|
|
41
43
|
# Install-side managed manifests record which files a prior install owned, so a
|
|
42
44
|
# later conservative install can delete removed managed files while preserving
|
|
43
45
|
# user-owned files (design §3.8 要点4). Skills manifest lives in the skills
|
|
@@ -76,6 +78,23 @@ def now_iso() -> str:
|
|
|
76
78
|
return dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
77
79
|
|
|
78
80
|
|
|
81
|
+
def replace_with_retry(source: Path, target: Path) -> None:
|
|
82
|
+
"""Bounded retry for transient directory locks during atomic swaps.
|
|
83
|
+
|
|
84
|
+
Windows indexers and antivirus scanners may briefly hold a newly generated
|
|
85
|
+
bundle directory. Retrying only ``PermissionError`` keeps real path and
|
|
86
|
+
contract failures fail-fast while avoiding an expensive full bundle rerun.
|
|
87
|
+
"""
|
|
88
|
+
for attempt in range(len(_REPLACE_RETRY_DELAYS_SECONDS) + 1):
|
|
89
|
+
try:
|
|
90
|
+
os.replace(source, target)
|
|
91
|
+
return
|
|
92
|
+
except PermissionError:
|
|
93
|
+
if attempt >= len(_REPLACE_RETRY_DELAYS_SECONDS):
|
|
94
|
+
raise
|
|
95
|
+
time.sleep(_REPLACE_RETRY_DELAYS_SECONDS[attempt])
|
|
96
|
+
|
|
97
|
+
|
|
79
98
|
def emit_json(payload: dict[str, Any], *, ok: bool = True) -> int:
|
|
80
99
|
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
81
100
|
return 0 if ok else 1
|
|
@@ -596,15 +615,15 @@ def cmd_build(
|
|
|
596
615
|
if out_dir.exists():
|
|
597
616
|
if (out_dir / BUILD_MARKER).is_file():
|
|
598
617
|
old_backup = out_dir.parent / f".{out_dir.name}.old-{uuid.uuid4().hex[:8]}"
|
|
599
|
-
|
|
618
|
+
replace_with_retry(out_dir, old_backup)
|
|
600
619
|
else:
|
|
601
620
|
shutil.rmtree(staging, ignore_errors=True)
|
|
602
621
|
raise ValueError(f"refusing to overwrite unmarked out_dir: {out_dir}")
|
|
603
622
|
try:
|
|
604
|
-
|
|
623
|
+
replace_with_retry(staging, out_dir)
|
|
605
624
|
except OSError:
|
|
606
625
|
if old_backup is not None and old_backup.exists():
|
|
607
|
-
|
|
626
|
+
replace_with_retry(old_backup, out_dir)
|
|
608
627
|
raise
|
|
609
628
|
if old_backup is not None:
|
|
610
629
|
shutil.rmtree(old_backup, ignore_errors=True)
|
|
@@ -43,6 +43,7 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
43
43
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
44
44
|
|
|
45
45
|
STEP_ORDER = ["preflight", "prepare", "merge", "verify", "push", "cleanup"]
|
|
46
|
+
DEFAULT_VERIFY_TIMEOUT_SECONDS = 1800
|
|
46
47
|
HEAVY_WORKTREE_ROOTS = (
|
|
47
48
|
"node_modules", ".venv", "venv", "build", "dist", "target", ".cache", "__pycache__"
|
|
48
49
|
)
|
|
@@ -56,6 +57,48 @@ def now_iso() -> str:
|
|
|
56
57
|
return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def _split_verify_command(raw: str, *, platform: str = sys.platform) -> list[str]:
|
|
61
|
+
# Harness profile/CLI commands use one portable quoting contract. Keep
|
|
62
|
+
# POSIX tokenization on Windows too; executable normalization happens later.
|
|
63
|
+
return shlex.split(raw, posix=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _dedupe_verify_commands(
|
|
67
|
+
commands: Sequence[Sequence[str]],
|
|
68
|
+
) -> list[list[str]]:
|
|
69
|
+
"""Preserve order while preventing explicit/profile duplicate execution."""
|
|
70
|
+
result: list[list[str]] = []
|
|
71
|
+
seen: set[tuple[str, ...]] = set()
|
|
72
|
+
for command in commands:
|
|
73
|
+
normalized = tuple(str(item) for item in command)
|
|
74
|
+
if normalized in seen:
|
|
75
|
+
continue
|
|
76
|
+
seen.add(normalized)
|
|
77
|
+
result.append(list(normalized))
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize_verify_command(
|
|
82
|
+
command: Sequence[str],
|
|
83
|
+
*,
|
|
84
|
+
platform: str = sys.platform,
|
|
85
|
+
resolver: Callable[[str], str | None] = shutil.which,
|
|
86
|
+
) -> list[str]:
|
|
87
|
+
"""Resolve Windows package-manager wrappers without enabling shell mode."""
|
|
88
|
+
result = [str(item) for item in command]
|
|
89
|
+
if not result or not platform.lower().startswith("win"):
|
|
90
|
+
return result
|
|
91
|
+
executable = Path(result[0]).name.lower()
|
|
92
|
+
if Path(executable).suffix:
|
|
93
|
+
return result
|
|
94
|
+
if executable not in {"npm", "npx", "pnpm", "yarn", "corepack"}:
|
|
95
|
+
return result
|
|
96
|
+
resolved = resolver(executable + ".cmd") or resolver(executable)
|
|
97
|
+
if resolved:
|
|
98
|
+
result[0] = resolved
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
59
102
|
class IntegrationError(Exception):
|
|
60
103
|
code = "INTEGRATION_ERROR"
|
|
61
104
|
|
|
@@ -224,6 +267,11 @@ def load_journal(project_root: Path, transaction_id: str) -> dict[str, Any]:
|
|
|
224
267
|
def _pid_alive(pid: int) -> bool:
|
|
225
268
|
if pid <= 0:
|
|
226
269
|
return False
|
|
270
|
+
# On Windows, os.kill(pid, 0) can transiently report EINVAL for the
|
|
271
|
+
# current low-priority/background process. The current process is
|
|
272
|
+
# unambiguously alive, so avoid reclaiming its lock on that platform race.
|
|
273
|
+
if pid == os.getpid():
|
|
274
|
+
return True
|
|
227
275
|
try:
|
|
228
276
|
os.kill(pid, 0)
|
|
229
277
|
except ProcessLookupError:
|
|
@@ -878,7 +926,7 @@ class IntegrationTransaction:
|
|
|
878
926
|
commands: list[list[str]] = []
|
|
879
927
|
for index, item in enumerate(required):
|
|
880
928
|
if isinstance(item, str) and item.strip():
|
|
881
|
-
commands.append(
|
|
929
|
+
commands.append(_split_verify_command(item.strip()))
|
|
882
930
|
continue
|
|
883
931
|
if not isinstance(item, dict):
|
|
884
932
|
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
@@ -886,18 +934,26 @@ class IntegrationTransaction:
|
|
|
886
934
|
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
935
|
commands.append([str(x) for x in raw])
|
|
888
936
|
elif isinstance(raw, str) and raw.strip():
|
|
889
|
-
commands.append(
|
|
937
|
+
commands.append(_split_verify_command(raw.strip()))
|
|
890
938
|
else:
|
|
891
939
|
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
940
|
return commands
|
|
893
941
|
|
|
894
|
-
def verify(
|
|
942
|
+
def verify(
|
|
943
|
+
self,
|
|
944
|
+
commands: Sequence[Sequence[str]] | None = None,
|
|
945
|
+
*,
|
|
946
|
+
timeout_seconds: float = DEFAULT_VERIFY_TIMEOUT_SECONDS,
|
|
947
|
+
) -> dict[str, Any]:
|
|
895
948
|
commands = list(commands or [])
|
|
896
949
|
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
950
|
try:
|
|
898
951
|
commands.extend(self._required_on_merge_commands())
|
|
899
952
|
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
953
|
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
954
|
+
commands = _dedupe_verify_commands(commands)
|
|
955
|
+
if timeout_seconds <= 0:
|
|
956
|
+
raise VerifyPlanMissingError("verification timeout must be greater than zero")
|
|
901
957
|
|
|
902
958
|
def action(journal: dict[str, Any]) -> None:
|
|
903
959
|
if not commands or any(
|
|
@@ -910,21 +966,67 @@ class IntegrationTransaction:
|
|
|
910
966
|
intg_root = Path(journal["integrationRoot"]).resolve()
|
|
911
967
|
results: list[dict[str, Any]] = []
|
|
912
968
|
for command in commands:
|
|
969
|
+
executable_command = _normalize_verify_command(command)
|
|
913
970
|
started = dt.datetime.now().astimezone()
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
971
|
+
try:
|
|
972
|
+
proc = subprocess.run(
|
|
973
|
+
executable_command,
|
|
974
|
+
cwd=str(intg_root),
|
|
975
|
+
capture_output=True,
|
|
976
|
+
text=True,
|
|
977
|
+
encoding="utf-8",
|
|
978
|
+
errors="replace",
|
|
979
|
+
check=False,
|
|
980
|
+
timeout=timeout_seconds,
|
|
981
|
+
)
|
|
982
|
+
except subprocess.TimeoutExpired:
|
|
983
|
+
finished = dt.datetime.now().astimezone()
|
|
984
|
+
results.append(
|
|
985
|
+
{
|
|
986
|
+
"command": list(command),
|
|
987
|
+
"resolvedCommand": executable_command,
|
|
988
|
+
"exitCode": None,
|
|
989
|
+
"timedOut": True,
|
|
990
|
+
"timeoutSeconds": timeout_seconds,
|
|
991
|
+
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
992
|
+
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
993
|
+
"durationMs": int(
|
|
994
|
+
(finished - started).total_seconds() * 1000
|
|
995
|
+
),
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
journal["verifyResults"] = results
|
|
999
|
+
self._save(journal)
|
|
1000
|
+
raise VerificationFailedError(
|
|
1001
|
+
f"command timed out after {timeout_seconds}s: {' '.join(command)}"
|
|
1002
|
+
)
|
|
1003
|
+
except FileNotFoundError as exc:
|
|
1004
|
+
finished = dt.datetime.now().astimezone()
|
|
1005
|
+
results.append(
|
|
1006
|
+
{
|
|
1007
|
+
"command": list(command),
|
|
1008
|
+
"resolvedCommand": executable_command,
|
|
1009
|
+
"exitCode": None,
|
|
1010
|
+
"error": str(exc),
|
|
1011
|
+
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
1012
|
+
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
1013
|
+
"durationMs": int(
|
|
1014
|
+
(finished - started).total_seconds() * 1000
|
|
1015
|
+
),
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
journal["verifyResults"] = results
|
|
1019
|
+
self._save(journal)
|
|
1020
|
+
raise VerifyPlanMissingError(
|
|
1021
|
+
f"verification executable not found: {executable_command[0]}"
|
|
1022
|
+
) from exc
|
|
923
1023
|
finished = dt.datetime.now().astimezone()
|
|
924
1024
|
results.append(
|
|
925
1025
|
{
|
|
926
1026
|
"command": list(command),
|
|
1027
|
+
"resolvedCommand": executable_command,
|
|
927
1028
|
"exitCode": proc.returncode,
|
|
1029
|
+
"timedOut": False,
|
|
928
1030
|
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
929
1031
|
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
930
1032
|
"durationMs": int(
|
|
@@ -1449,11 +1551,16 @@ def _parse_verify_commands(values: Sequence[str] | None) -> list[list[str]]:
|
|
|
1449
1551
|
POSIX shlex semantics: single/double quotes group tokens, backslash
|
|
1450
1552
|
escapes the next character (quote Windows paths).
|
|
1451
1553
|
"""
|
|
1452
|
-
return [
|
|
1554
|
+
return [_split_verify_command(value) for value in (values or [])]
|
|
1453
1555
|
|
|
1454
1556
|
|
|
1455
1557
|
def cmd_verify(args: argparse.Namespace) -> int:
|
|
1456
|
-
return _emit(
|
|
1558
|
+
return _emit(
|
|
1559
|
+
_txn_from_args(args).verify(
|
|
1560
|
+
commands=_parse_verify_commands(args.command),
|
|
1561
|
+
timeout_seconds=float(args.timeout_seconds),
|
|
1562
|
+
)
|
|
1563
|
+
)
|
|
1457
1564
|
|
|
1458
1565
|
|
|
1459
1566
|
def cmd_push(args: argparse.Namespace) -> int:
|
|
@@ -1543,6 +1650,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1543
1650
|
p_verify.add_argument("--feature-branch", required=True)
|
|
1544
1651
|
p_verify.add_argument("--temp-root", required=True)
|
|
1545
1652
|
p_verify.add_argument("--command", action="append")
|
|
1653
|
+
p_verify.add_argument(
|
|
1654
|
+
"--timeout-seconds",
|
|
1655
|
+
type=float,
|
|
1656
|
+
default=DEFAULT_VERIFY_TIMEOUT_SECONDS,
|
|
1657
|
+
)
|
|
1546
1658
|
p_verify.set_defaults(func=cmd_verify)
|
|
1547
1659
|
|
|
1548
1660
|
return parser
|
|
@@ -219,14 +219,30 @@ def sha256_file(path: Path) -> str:
|
|
|
219
219
|
return digest.hexdigest()
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def compute_inputs_hash(
|
|
222
|
+
def compute_inputs_hash(
|
|
223
|
+
file_paths: list[str], *, project_root: Path | str | None = None
|
|
224
|
+
) -> tuple[str, list[str]]:
|
|
223
225
|
"""Per-file content sha256 → sort digests → hash again (order-independent)."""
|
|
226
|
+
root = (
|
|
227
|
+
Path(project_root).expanduser().resolve()
|
|
228
|
+
if project_root is not None
|
|
229
|
+
else None
|
|
230
|
+
)
|
|
224
231
|
by_path: dict[str, str] = {}
|
|
225
232
|
for raw in file_paths:
|
|
226
233
|
path = resolve_path(raw)
|
|
227
234
|
if not path.is_file():
|
|
228
235
|
raise FileNotFoundError(f"file not found: {raw}")
|
|
229
|
-
|
|
236
|
+
if root is not None:
|
|
237
|
+
try:
|
|
238
|
+
logical_path = path.relative_to(root).as_posix()
|
|
239
|
+
except ValueError as exc:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"INPUT_OUTSIDE_PROJECT: {path} is outside {root}"
|
|
242
|
+
) from exc
|
|
243
|
+
else:
|
|
244
|
+
logical_path = path.as_posix()
|
|
245
|
+
by_path[logical_path] = sha256_file(path)
|
|
230
246
|
|
|
231
247
|
# Stable file list for callers; bind every resolved path to its digest.
|
|
232
248
|
# Hashing only the content multiset let a path swap incorrectly reuse a
|
|
@@ -1056,6 +1072,7 @@ def decide_can_reuse(
|
|
|
1056
1072
|
requested_profile_hash: str | None = None,
|
|
1057
1073
|
requested_environment_hash: str | None = None,
|
|
1058
1074
|
requested_db_schema_hash: str | None = None,
|
|
1075
|
+
project_root: Path | str | None = None,
|
|
1059
1076
|
) -> dict[str, Any]:
|
|
1060
1077
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1061
1078
|
if ledger is None:
|
|
@@ -1220,8 +1237,10 @@ def decide_can_reuse(
|
|
|
1220
1237
|
}
|
|
1221
1238
|
|
|
1222
1239
|
try:
|
|
1223
|
-
current_hash, current_files = compute_inputs_hash(
|
|
1224
|
-
|
|
1240
|
+
current_hash, current_files = compute_inputs_hash(
|
|
1241
|
+
files, project_root=project_root
|
|
1242
|
+
)
|
|
1243
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
1225
1244
|
return {
|
|
1226
1245
|
"ok": True,
|
|
1227
1246
|
"reuse": False,
|
|
@@ -1351,7 +1370,9 @@ def cmd_hash(args: argparse.Namespace) -> int:
|
|
|
1351
1370
|
files, project_root = input_files_from_args(args)
|
|
1352
1371
|
if not files:
|
|
1353
1372
|
return emit_error("hash requires --files or --files-from", as_json=as_json)
|
|
1354
|
-
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1373
|
+
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1374
|
+
files, project_root=project_root
|
|
1375
|
+
)
|
|
1355
1376
|
except (OSError, FileNotFoundError) as exc:
|
|
1356
1377
|
return emit_error(str(exc), as_json=as_json)
|
|
1357
1378
|
|
|
@@ -1418,6 +1439,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1418
1439
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1419
1440
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
1441
|
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1442
|
+
project_root=project_root,
|
|
1421
1443
|
)
|
|
1422
1444
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1423
1445
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
@@ -1470,7 +1492,9 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1470
1492
|
|
|
1471
1493
|
try:
|
|
1472
1494
|
status = normalize_status(args.status)
|
|
1473
|
-
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1495
|
+
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1496
|
+
files, project_root=project_root
|
|
1497
|
+
)
|
|
1474
1498
|
ledger, _existing_path = load_ledger(change_dir)
|
|
1475
1499
|
if ledger is None:
|
|
1476
1500
|
ledger = {
|
|
@@ -374,12 +374,14 @@ def _resolve_agents_root(skills_root: Path, agents_root: Path | None) -> tuple[P
|
|
|
374
374
|
return (skills_root / "agents").resolve(), None
|
|
375
375
|
|
|
376
376
|
|
|
377
|
-
def _read_host_capabilities(skills_root: Path) -> set[str]:
|
|
377
|
+
def _read_host_capabilities(skills_root: Path) -> tuple[set[str], bool]:
|
|
378
378
|
"""Read agent capabilities declared by the host adapter (retro §5.3).
|
|
379
379
|
|
|
380
380
|
Adapter installs may declare available agent roles in
|
|
381
381
|
`meta/runtime.json` under `agentCapabilities` (list of role names).
|
|
382
|
-
Returns
|
|
382
|
+
Returns ``(capabilities, manifest_present)``. ``manifest_present`` is true
|
|
383
|
+
only when a readable capability list exists; an unrelated runtime.json
|
|
384
|
+
must not turn an unknown host into a hard "agent missing" failure.
|
|
383
385
|
"""
|
|
384
386
|
# runtime.json lives in the change's meta/ dir, but agent capabilities are
|
|
385
387
|
# project-level. Check the adapter's runtime.json at the project root.
|
|
@@ -398,8 +400,8 @@ def _read_host_capabilities(skills_root: Path) -> set[str]:
|
|
|
398
400
|
continue
|
|
399
401
|
caps = data.get("agentCapabilities")
|
|
400
402
|
if isinstance(caps, list):
|
|
401
|
-
return {str(c) for c in caps if isinstance(c, str)}
|
|
402
|
-
return set()
|
|
403
|
+
return {str(c) for c in caps if isinstance(c, str)}, True
|
|
404
|
+
return set(), False
|
|
403
405
|
|
|
404
406
|
|
|
405
407
|
def cmd_check_agents(
|
|
@@ -420,7 +422,7 @@ def cmd_check_agents(
|
|
|
420
422
|
}
|
|
421
423
|
|
|
422
424
|
resolved_agents_root, _ = _resolve_agents_root(skills_root, agents_root)
|
|
423
|
-
host_capabilities = _read_host_capabilities(skills_root)
|
|
425
|
+
host_capabilities, capability_manifest_present = _read_host_capabilities(skills_root)
|
|
424
426
|
host_callable = agent_name in host_capabilities
|
|
425
427
|
|
|
426
428
|
# Three independent fields (retro §5.3):
|
|
@@ -437,32 +439,37 @@ def cmd_check_agents(
|
|
|
437
439
|
agent_path = resolved_agents_root / filename
|
|
438
440
|
definition_present = agent_path.is_file()
|
|
439
441
|
|
|
440
|
-
#
|
|
441
|
-
#
|
|
442
|
+
# Codex/Cursor bundles intentionally do not install fixed harness-* agent
|
|
443
|
+
# definitions. Missing host capabilities therefore select the normal
|
|
444
|
+
# inline path instead of reporting UNKNOWN/"subagent unavailable".
|
|
442
445
|
if not definition_present and not host_callable:
|
|
443
|
-
# Check if this adapter is known to not support custom agents
|
|
444
446
|
adapter_name = skills_root.parent.name
|
|
445
447
|
if adapter_name in (".agents", ".cursor"):
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
448
|
+
reason_code = (
|
|
449
|
+
"HOST_CAPABILITY_NOT_DECLARED"
|
|
450
|
+
if capability_manifest_present
|
|
451
|
+
else "INLINE_BY_ADAPTER"
|
|
452
|
+
)
|
|
453
|
+
return {
|
|
454
|
+
"ok": True,
|
|
455
|
+
"action": "check-agents",
|
|
456
|
+
"agent": agent_name,
|
|
457
|
+
"usable": False,
|
|
458
|
+
"executionMode": "inline",
|
|
459
|
+
"fallbackPolicy": "inline-no-retry",
|
|
460
|
+
"definitionPresent": False,
|
|
461
|
+
"hostCallable": False,
|
|
462
|
+
"toolContractValid": False,
|
|
463
|
+
"capabilityManifestPresent": capability_manifest_present,
|
|
464
|
+
"reasonCode": reason_code,
|
|
465
|
+
"reason": (
|
|
466
|
+
"host capability manifest does not declare this fixed agent; "
|
|
467
|
+
"use the normal inline path"
|
|
468
|
+
if capability_manifest_present
|
|
469
|
+
else "adapter uses inline execution unless the host explicitly "
|
|
470
|
+
"declares a callable agent capability"
|
|
471
|
+
),
|
|
472
|
+
}
|
|
466
473
|
return {
|
|
467
474
|
"ok": False,
|
|
468
475
|
"action": "check-agents",
|
|
@@ -470,9 +477,12 @@ def cmd_check_agents(
|
|
|
470
477
|
"path": str(agent_path) if agent_path else None,
|
|
471
478
|
"agentsRoot": str(resolved_agents_root) if resolved_agents_root else None,
|
|
472
479
|
"usable": False,
|
|
480
|
+
"executionMode": "unavailable",
|
|
481
|
+
"fallbackPolicy": "inline-no-retry",
|
|
473
482
|
"definitionPresent": False,
|
|
474
483
|
"hostCallable": False,
|
|
475
484
|
"toolContractValid": False,
|
|
485
|
+
"capabilityManifestPresent": capability_manifest_present,
|
|
476
486
|
"reasonCode": "AGENT_DEFINITION_NOT_FOUND",
|
|
477
487
|
"reason": f"agent file not found and host does not declare role: {agent_name}",
|
|
478
488
|
}
|
|
@@ -488,9 +498,13 @@ def cmd_check_agents(
|
|
|
488
498
|
"agent": agent_name,
|
|
489
499
|
"path": str(agent_path),
|
|
490
500
|
"usable": False,
|
|
501
|
+
"executionMode": "unavailable",
|
|
502
|
+
"fallbackPolicy": "inline-no-retry",
|
|
491
503
|
"definitionPresent": True,
|
|
492
504
|
"hostCallable": host_callable,
|
|
493
505
|
"toolContractValid": False,
|
|
506
|
+
"capabilityManifestPresent": capability_manifest_present,
|
|
507
|
+
"reasonCode": "AGENT_DEFINITION_UNREADABLE",
|
|
494
508
|
"reason": f"cannot read agent file: {exc}",
|
|
495
509
|
}
|
|
496
510
|
|
|
@@ -502,9 +516,13 @@ def cmd_check_agents(
|
|
|
502
516
|
"agent": agent_name,
|
|
503
517
|
"path": str(agent_path),
|
|
504
518
|
"usable": False,
|
|
519
|
+
"executionMode": "unavailable",
|
|
520
|
+
"fallbackPolicy": "inline-no-retry",
|
|
505
521
|
"definitionPresent": True,
|
|
506
522
|
"hostCallable": host_callable,
|
|
507
523
|
"toolContractValid": False,
|
|
524
|
+
"capabilityManifestPresent": capability_manifest_present,
|
|
525
|
+
"reasonCode": "AGENT_DEFINITION_INVALID",
|
|
508
526
|
"reason": f"frontmatter parse failed: {err or 'unknown'}",
|
|
509
527
|
}
|
|
510
528
|
|
|
@@ -525,7 +543,7 @@ def cmd_check_agents(
|
|
|
525
543
|
name_field = meta.get("name") if meta else None
|
|
526
544
|
tools_field = meta.get("tools") if meta else None
|
|
527
545
|
|
|
528
|
-
reason_code = "READY" if usable else "
|
|
546
|
+
reason_code = "READY" if usable else "TOOL_CONTRACT_INVALID"
|
|
529
547
|
if not definition_present and host_callable:
|
|
530
548
|
reason_code = "DEFINITION_NOT_FOUND_HOST_CAPABLE"
|
|
531
549
|
|
|
@@ -536,9 +554,12 @@ def cmd_check_agents(
|
|
|
536
554
|
"path": str(agent_path) if agent_path else None,
|
|
537
555
|
"agentsRoot": str(resolved_agents_root) if resolved_agents_root else None,
|
|
538
556
|
"usable": usable,
|
|
557
|
+
"executionMode": "delegated" if usable else "unavailable",
|
|
558
|
+
"fallbackPolicy": "inline-no-retry",
|
|
539
559
|
"definitionPresent": definition_present,
|
|
540
560
|
"hostCallable": host_callable,
|
|
541
561
|
"toolContractValid": tool_contract_valid,
|
|
562
|
+
"capabilityManifestPresent": capability_manifest_present,
|
|
542
563
|
"reasonCode": reason_code,
|
|
543
564
|
"reason": (
|
|
544
565
|
"agent available; "
|
|
@@ -66,6 +66,16 @@ DEFAULT_EXCLUDED_ROOTS: tuple[str, ...] = (
|
|
|
66
66
|
".cache",
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
|
|
70
|
+
def profile_defaults_fingerprint() -> str:
|
|
71
|
+
"""Fingerprint generated defaults so old profiles are refreshed safely."""
|
|
72
|
+
payload = {
|
|
73
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
74
|
+
"excludedRoots": list(DEFAULT_EXCLUDED_ROOTS),
|
|
75
|
+
}
|
|
76
|
+
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
|
77
|
+
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
78
|
+
|
|
69
79
|
# 覆盖层级(spec §3.2):unitTest ⊂ unitTestFull;package 独立;submit 复用 unitTestFull。
|
|
70
80
|
VERIFICATION_KEYS: tuple[str, ...] = (
|
|
71
81
|
"compile",
|
|
@@ -330,6 +340,7 @@ def _node_commands(project: Path) -> dict[str, Any]:
|
|
|
330
340
|
def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, Any]:
|
|
331
341
|
return {
|
|
332
342
|
"schemaVersion": SCHEMA_VERSION,
|
|
343
|
+
"defaultsFingerprint": profile_defaults_fingerprint(),
|
|
333
344
|
"detectedAt": "",
|
|
334
345
|
"projectType": "unknown",
|
|
335
346
|
"toolPaths": {"node": "", "mvn": ""},
|
|
@@ -596,6 +607,28 @@ def check(project: Path) -> dict[str, Any]:
|
|
|
596
607
|
issues: list[str] = []
|
|
597
608
|
stale = False
|
|
598
609
|
|
|
610
|
+
expected_defaults_fingerprint = profile_defaults_fingerprint()
|
|
611
|
+
if profile.get("defaultsFingerprint") != expected_defaults_fingerprint:
|
|
612
|
+
stale = True
|
|
613
|
+
issues.append(
|
|
614
|
+
"defaultsFingerprint is missing or outdated; run detect to refresh generated defaults"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
excluded_roots = {
|
|
618
|
+
str(item).replace("\\", "/").strip("/")
|
|
619
|
+
for item in (profile.get("excludedRoots") or [])
|
|
620
|
+
if isinstance(item, str) and item.strip("/")
|
|
621
|
+
}
|
|
622
|
+
missing_excluded_roots = [
|
|
623
|
+
item for item in DEFAULT_EXCLUDED_ROOTS if item not in excluded_roots
|
|
624
|
+
]
|
|
625
|
+
if missing_excluded_roots:
|
|
626
|
+
stale = True
|
|
627
|
+
issues.append(
|
|
628
|
+
"excludedRoots missing generated defaults: "
|
|
629
|
+
+ ", ".join(missing_excluded_roots)
|
|
630
|
+
)
|
|
631
|
+
|
|
599
632
|
fp = profile.get("fingerprint") or {}
|
|
600
633
|
stored_pom = fp.get("pomHash") or "" if isinstance(fp, dict) else ""
|
|
601
634
|
current_pom = sha256_file(project / "pom.xml") if (project / "pom.xml").is_file() else ""
|
|
@@ -32,6 +32,24 @@ MODE = "force-track-touched"
|
|
|
32
32
|
MANIFEST_REL = Path("evidence") / "test-tracking.json"
|
|
33
33
|
SNAPSHOT_REL = Path("evidence") / "test-guard-snapshot.json"
|
|
34
34
|
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
35
|
+
MANDATORY_EXCLUDED_ROOTS = (
|
|
36
|
+
".git",
|
|
37
|
+
".harness",
|
|
38
|
+
".worktrees",
|
|
39
|
+
".claude/worktrees",
|
|
40
|
+
".codex/worktrees",
|
|
41
|
+
".cursor/worktrees",
|
|
42
|
+
".codebuddy/worktrees",
|
|
43
|
+
".codeium/worktrees",
|
|
44
|
+
"target",
|
|
45
|
+
"build",
|
|
46
|
+
"dist",
|
|
47
|
+
"node_modules",
|
|
48
|
+
".gradle",
|
|
49
|
+
"__pycache__",
|
|
50
|
+
".pytest_cache",
|
|
51
|
+
".cache",
|
|
52
|
+
)
|
|
35
53
|
REASONS = ("tdd-created", "stale-test-repair", "test-updated")
|
|
36
54
|
|
|
37
55
|
|
|
@@ -213,11 +231,14 @@ def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
|
|
|
213
231
|
if not isinstance(paths, list):
|
|
214
232
|
return [], []
|
|
215
233
|
excluded = profile.get("excludedRoots") if isinstance(profile, dict) else None
|
|
216
|
-
|
|
234
|
+
configured_roots = (
|
|
217
235
|
[item.replace("\\", "/").strip("/") for item in excluded if isinstance(item, str) and item]
|
|
218
236
|
if isinstance(excluded, list)
|
|
219
237
|
else []
|
|
220
238
|
)
|
|
239
|
+
excluded_roots = list(
|
|
240
|
+
dict.fromkeys([*configured_roots, *MANDATORY_EXCLUDED_ROOTS])
|
|
241
|
+
)
|
|
221
242
|
patterns = [
|
|
222
243
|
item.replace("\\", "/") for item in paths if isinstance(item, str) and item
|
|
223
244
|
]
|
|
@@ -254,11 +275,8 @@ def _allowed_test_path(project: Path, rel: str) -> bool:
|
|
|
254
275
|
if config is None:
|
|
255
276
|
return _standard_test_path(rel)
|
|
256
277
|
patterns, excluded_roots = config
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
excluded_parts = tuple(os.path.normcase(part) for part in excluded.split("/"))
|
|
260
|
-
if rel_parts[: len(excluded_parts)] == excluded_parts:
|
|
261
|
-
return False
|
|
278
|
+
if _path_is_excluded(rel, excluded_roots):
|
|
279
|
+
return False
|
|
262
280
|
return any(_matches_pattern(rel, pattern) for pattern in patterns)
|
|
263
281
|
|
|
264
282
|
|
|
@@ -820,8 +838,12 @@ def _path_is_excluded(rel: str, excluded_roots: list[str]) -> bool:
|
|
|
820
838
|
excluded_parts = tuple(
|
|
821
839
|
os.path.normcase(part) for part in excluded.split("/") if part
|
|
822
840
|
)
|
|
823
|
-
if
|
|
824
|
-
|
|
841
|
+
if not excluded_parts:
|
|
842
|
+
continue
|
|
843
|
+
width = len(excluded_parts)
|
|
844
|
+
for index in range(len(rel_parts) - width + 1):
|
|
845
|
+
if rel_parts[index : index + width] == excluded_parts:
|
|
846
|
+
return True
|
|
825
847
|
return False
|
|
826
848
|
|
|
827
849
|
|