@hunter-harness/workflow-harness 0.2.6 → 0.2.8
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/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +9 -6
- package/harness/bundles/general/claude-code/harness-run/checklist.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/reference.md +5 -2
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +7 -5
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +8 -2
- package/harness/bundles/general/claude-code/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +41 -16
- package/harness/bundles/general/claude-code/scripts/harness_change.py +872 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +26 -0
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/claude-code/scripts/harness_service.py +34 -1
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/claude-code/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +9 -6
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/reference.md +5 -2
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +7 -5
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +8 -2
- package/harness/bundles/general/codebuddy/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +41 -16
- package/harness/bundles/general/codebuddy/scripts/harness_change.py +872 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +26 -0
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +34 -1
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/codebuddy/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +9 -6
- package/harness/bundles/general/codex/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codex/harness-run/reference.md +5 -2
- package/harness/bundles/general/codex/harness-submit/SKILL.md +7 -5
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +8 -2
- package/harness/bundles/general/codex/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +41 -16
- package/harness/bundles/general/codex/scripts/harness_change.py +872 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +26 -0
- package/harness/bundles/general/codex/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/codex/scripts/harness_service.py +34 -1
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/codex/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/contracts/workflow-policy.json +169 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +9 -6
- package/harness/bundles/general/cursor/harness-run/checklist.md +2 -2
- package/harness/bundles/general/cursor/harness-run/reference.md +5 -2
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +7 -5
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +8 -2
- package/harness/bundles/general/cursor/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +41 -16
- package/harness/bundles/general/cursor/scripts/harness_change.py +872 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +26 -0
- package/harness/bundles/general/cursor/scripts/harness_gate.py +1038 -0
- package/harness/bundles/general/cursor/scripts/harness_service.py +34 -1
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/general/cursor/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +7 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +9 -6
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +7 -5
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +8 -2
- package/harness/bundles/java/claude-code/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +41 -16
- package/harness/bundles/java/claude-code/scripts/harness_change.py +872 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +26 -0
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/claude-code/scripts/harness_service.py +34 -1
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/claude-code/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +7 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +9 -6
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +7 -5
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +8 -2
- package/harness/bundles/java/codebuddy/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +41 -16
- package/harness/bundles/java/codebuddy/scripts/harness_change.py +872 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +26 -0
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +34 -1
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/codebuddy/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +7 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +6 -3
- package/harness/bundles/java/codex/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +9 -6
- package/harness/bundles/java/codex/harness-submit/SKILL.md +7 -5
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +8 -2
- package/harness/bundles/java/codex/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +41 -16
- package/harness/bundles/java/codex/scripts/harness_change.py +872 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +26 -0
- package/harness/bundles/java/codex/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/codex/scripts/harness_service.py +34 -1
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/codex/scripts/harness_workflow_policy.py +160 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/contracts/workflow-policy.json +169 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +7 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -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-ingest/scripts/harness_knowledge.py +26 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +2 -0
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +6 -3
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +9 -6
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +7 -5
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +8 -2
- package/harness/bundles/java/cursor/scripts/harness_apidoc.py +53 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +41 -16
- package/harness/bundles/java/cursor/scripts/harness_change.py +872 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +26 -0
- package/harness/bundles/java/cursor/scripts/harness_gate.py +1038 -0
- package/harness/bundles/java/cursor/scripts/harness_service.py +34 -1
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +210 -14
- package/harness/bundles/java/cursor/scripts/harness_workflow_policy.py +160 -0
- package/harness/manifests/general/claude-code.json +40 -20
- package/harness/manifests/general/codebuddy.json +40 -20
- package/harness/manifests/general/codex.json +40 -20
- package/harness/manifests/general/cursor.json +40 -20
- package/harness/manifests/java/claude-code.json +40 -20
- package/harness/manifests/java/codebuddy.json +40 -20
- package/harness/manifests/java/codex.json +40 -20
- package/harness/manifests/java/cursor.json +40 -20
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -0,0 +1,1038 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Harness deterministic phase gate: begin/close, checkpoints, classify, lint.
|
|
3
|
+
|
|
4
|
+
Subcommands:
|
|
5
|
+
begin — claim lease, optional identity capture, append phase.start
|
|
6
|
+
close — validate ledger/test-guard/policy, append phase.end, release lease
|
|
7
|
+
classify — plan/post-run risk tier stub
|
|
8
|
+
checkpoint — status|approve foundation-gate (and future checkpoints)
|
|
9
|
+
lint-skills — forbid hand-written ledger patterns in skill trees
|
|
10
|
+
|
|
11
|
+
foundation-gate must block task>=6 until approved (API-012).
|
|
12
|
+
Python 3.10+, stdlib only.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import argparse
|
|
18
|
+
import hashlib
|
|
19
|
+
import json
|
|
20
|
+
import os
|
|
21
|
+
import re
|
|
22
|
+
import subprocess
|
|
23
|
+
import sys
|
|
24
|
+
import uuid
|
|
25
|
+
from pathlib import Path
|
|
26
|
+
from typing import Any
|
|
27
|
+
|
|
28
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
29
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
30
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
31
|
+
|
|
32
|
+
import harness_change as hc # noqa: E402
|
|
33
|
+
import harness_events as he # noqa: E402
|
|
34
|
+
import harness_ledger as hl # noqa: E402
|
|
35
|
+
import harness_workflow_policy as hwp # noqa: E402
|
|
36
|
+
import harness_test_guard as htg # noqa: E402
|
|
37
|
+
|
|
38
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
39
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
40
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
41
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
CHECKPOINTS_REL = Path("meta") / "implementation-checkpoints.json"
|
|
45
|
+
FORBIDDEN_SKILL_PATTERNS = (
|
|
46
|
+
re.compile(r"Write\s+verification-ledger\.json", re.IGNORECASE),
|
|
47
|
+
re.compile(r"Edit\s+verification-ledger", re.IGNORECASE),
|
|
48
|
+
re.compile(r"hand-?write.*verification-ledger", re.IGNORECASE),
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
LEDGER_V2_REQUIRED_ENTRY_FIELDS = (
|
|
52
|
+
"algorithmVersion",
|
|
53
|
+
"coverage",
|
|
54
|
+
"inputsHash",
|
|
55
|
+
"inputsFiles",
|
|
56
|
+
"status",
|
|
57
|
+
"command",
|
|
58
|
+
"evidence",
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
CAPABILITY_MARKERS: dict[str, tuple[str, ...]] = {
|
|
62
|
+
"risk-classify-plan": ("harness_gate.py classify",),
|
|
63
|
+
"gate-begin": ("harness_gate.py begin",),
|
|
64
|
+
"gate-close": ("harness_gate.py close",),
|
|
65
|
+
"ledger-record": ("harness_ledger.py record",),
|
|
66
|
+
"ledger-can-reuse": ("harness_ledger.py can-reuse",),
|
|
67
|
+
"test-guard": ("harness_test_guard.py begin", "harness_test_guard.py close"),
|
|
68
|
+
"test-guard-stage": ("harness_test_guard.py stage",),
|
|
69
|
+
"integration-lock": ("harness_change.py integration-lock",),
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _load_workflow_policy(
|
|
74
|
+
*, project: Path | None = None, skills_root: Path | None = None
|
|
75
|
+
) -> dict[str, Any]:
|
|
76
|
+
candidates = []
|
|
77
|
+
if project is not None:
|
|
78
|
+
candidates.append(project / "harness" / "contracts" / "workflow-policy.json")
|
|
79
|
+
if skills_root is not None:
|
|
80
|
+
candidates.append(skills_root / "contracts" / "workflow-policy.json")
|
|
81
|
+
candidates.append(SCRIPTS_DIR.parent / "contracts" / "workflow-policy.json")
|
|
82
|
+
for path in candidates:
|
|
83
|
+
if path.is_file():
|
|
84
|
+
raw = json.loads(path.read_text(encoding="utf-8"))
|
|
85
|
+
return hwp.validate_policy(raw)
|
|
86
|
+
raise FileNotFoundError("workflow-policy.json not found beside project or skills")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def emit(payload: dict[str, Any], *, as_json: bool) -> None:
|
|
90
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def emit_error(
|
|
94
|
+
code: str,
|
|
95
|
+
message: str,
|
|
96
|
+
*,
|
|
97
|
+
as_json: bool,
|
|
98
|
+
extra: dict[str, Any] | None = None,
|
|
99
|
+
) -> int:
|
|
100
|
+
payload: dict[str, Any] = {"ok": False, "code": code, "message": message}
|
|
101
|
+
if extra:
|
|
102
|
+
payload.update(extra)
|
|
103
|
+
if as_json:
|
|
104
|
+
sys.stderr.write(json.dumps(payload, ensure_ascii=False) + "\n")
|
|
105
|
+
else:
|
|
106
|
+
sys.stderr.write(f"error: {message} ({code})\n")
|
|
107
|
+
return 1
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def _read_json(path: Path) -> Any:
|
|
111
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _write_json(path: Path, data: Any) -> None:
|
|
115
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
116
|
+
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
117
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
118
|
+
try:
|
|
119
|
+
tmp.write_text(text, encoding="utf-8", newline="\n")
|
|
120
|
+
os.replace(tmp, path)
|
|
121
|
+
except BaseException:
|
|
122
|
+
tmp.unlink(missing_ok=True)
|
|
123
|
+
raise
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def load_checkpoints(change_dir: Path) -> dict[str, Any] | None:
|
|
127
|
+
path = change_dir / CHECKPOINTS_REL
|
|
128
|
+
if not path.is_file():
|
|
129
|
+
return None
|
|
130
|
+
try:
|
|
131
|
+
data = _read_json(path)
|
|
132
|
+
except (OSError, json.JSONDecodeError):
|
|
133
|
+
return None
|
|
134
|
+
return data if isinstance(data, dict) else None
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
def checkpoint_status(checkpoints: dict[str, Any] | None, checkpoint_id: str) -> str:
|
|
138
|
+
if not checkpoints:
|
|
139
|
+
return "missing"
|
|
140
|
+
items = checkpoints.get("checkpoints")
|
|
141
|
+
if not isinstance(items, list):
|
|
142
|
+
return "missing"
|
|
143
|
+
for item in items:
|
|
144
|
+
if isinstance(item, dict) and item.get("id") == checkpoint_id:
|
|
145
|
+
return str(item.get("status") or "pending")
|
|
146
|
+
return "missing"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def foundation_gate_blocks(task_number: int | None, change_dir: Path) -> dict[str, Any] | None:
|
|
150
|
+
checkpoints = load_checkpoints(change_dir)
|
|
151
|
+
status = checkpoint_status(checkpoints, "foundation-gate")
|
|
152
|
+
if status == "approved":
|
|
153
|
+
return None
|
|
154
|
+
if task_number is None:
|
|
155
|
+
return {
|
|
156
|
+
"ok": False,
|
|
157
|
+
"code": "TASK_NUMBER_REQUIRED",
|
|
158
|
+
"message": "--task is required while foundation-gate is pending",
|
|
159
|
+
"checkpointId": "foundation-gate",
|
|
160
|
+
"checkpointStatus": status,
|
|
161
|
+
}
|
|
162
|
+
if task_number < 6:
|
|
163
|
+
return None
|
|
164
|
+
return {
|
|
165
|
+
"ok": False,
|
|
166
|
+
"code": "FOUNDATION_GATE_PENDING",
|
|
167
|
+
"message": (
|
|
168
|
+
"foundation-gate is not approved; tasks 6+ are blocked until "
|
|
169
|
+
"reports/review/foundation-gate-review.md is reviewed and checkpoint approved"
|
|
170
|
+
),
|
|
171
|
+
"checkpointId": "foundation-gate",
|
|
172
|
+
"checkpointStatus": status,
|
|
173
|
+
"task": task_number,
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> list[str]:
|
|
178
|
+
missing: list[str] = []
|
|
179
|
+
for field in LEDGER_V2_REQUIRED_ENTRY_FIELDS:
|
|
180
|
+
value = entry.get(field)
|
|
181
|
+
if field == "inputsFiles":
|
|
182
|
+
if not isinstance(value, list):
|
|
183
|
+
missing.append(field)
|
|
184
|
+
elif verification == "unitTestFull" and not value:
|
|
185
|
+
missing.append("inputsFiles(non-empty)")
|
|
186
|
+
elif field == "status":
|
|
187
|
+
if value != "OK":
|
|
188
|
+
missing.append("status=OK")
|
|
189
|
+
elif not (isinstance(value, str) and value.strip()):
|
|
190
|
+
missing.append(field)
|
|
191
|
+
elif field == "coverage" and str(value).strip() not in hl.COVERAGE_RANK:
|
|
192
|
+
missing.append("coverage(valid)")
|
|
193
|
+
elif field == "algorithmVersion" and str(value).strip() != hl.LEDGER_VERSION:
|
|
194
|
+
missing.append("algorithmVersion(harness-ledger-2)")
|
|
195
|
+
return missing
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def validate_ledger_for_phase_close(
|
|
199
|
+
change_dir: Path,
|
|
200
|
+
phase: str,
|
|
201
|
+
policy: dict[str, Any],
|
|
202
|
+
) -> dict[str, Any]:
|
|
203
|
+
"""Validate ledger v2 fields required for phase close (UT-026)."""
|
|
204
|
+
required = policy.get("requiredValidations", {}).get(phase, [])
|
|
205
|
+
if not required:
|
|
206
|
+
return {"ok": True, "code": "LEDGER_NOT_REQUIRED", "phase": phase}
|
|
207
|
+
|
|
208
|
+
ledger, ledger_path = hl.load_ledger(change_dir)
|
|
209
|
+
if ledger is None:
|
|
210
|
+
return {
|
|
211
|
+
"ok": False,
|
|
212
|
+
"code": "LEDGER_MISSING",
|
|
213
|
+
"message": "verification ledger missing",
|
|
214
|
+
"phase": phase,
|
|
215
|
+
"required": required,
|
|
216
|
+
}
|
|
217
|
+
validations = ledger.get("validations")
|
|
218
|
+
if not isinstance(validations, dict):
|
|
219
|
+
return {
|
|
220
|
+
"ok": False,
|
|
221
|
+
"code": "VALIDATIONS_MISSING",
|
|
222
|
+
"message": "ledger validations missing",
|
|
223
|
+
"phase": phase,
|
|
224
|
+
"ledgerPath": str(ledger_path) if ledger_path else None,
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
problems: list[dict[str, Any]] = []
|
|
228
|
+
for verification in required:
|
|
229
|
+
entry = validations.get(verification)
|
|
230
|
+
if not isinstance(entry, dict):
|
|
231
|
+
problems.append(
|
|
232
|
+
{
|
|
233
|
+
"verification": verification,
|
|
234
|
+
"missing": ["entry"],
|
|
235
|
+
"code": "VALIDATION_MISSING",
|
|
236
|
+
}
|
|
237
|
+
)
|
|
238
|
+
continue
|
|
239
|
+
missing = validate_ledger_entry_v2(entry, verification)
|
|
240
|
+
if missing:
|
|
241
|
+
code = (
|
|
242
|
+
"MISSING_V2_FIELDS"
|
|
243
|
+
if any(
|
|
244
|
+
field in missing
|
|
245
|
+
for field in (
|
|
246
|
+
"algorithmVersion",
|
|
247
|
+
"algorithmVersion(harness-ledger-2)",
|
|
248
|
+
"coverage",
|
|
249
|
+
"coverage(valid)",
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
else "MISSING_FIELDS"
|
|
253
|
+
)
|
|
254
|
+
problems.append(
|
|
255
|
+
{
|
|
256
|
+
"verification": verification,
|
|
257
|
+
"missing": missing,
|
|
258
|
+
"code": code,
|
|
259
|
+
}
|
|
260
|
+
)
|
|
261
|
+
|
|
262
|
+
if problems:
|
|
263
|
+
return {
|
|
264
|
+
"ok": False,
|
|
265
|
+
"code": problems[0]["code"],
|
|
266
|
+
"message": "ledger validation failed for phase close",
|
|
267
|
+
"phase": phase,
|
|
268
|
+
"problems": problems,
|
|
269
|
+
"ledgerPath": str(ledger_path) if ledger_path else None,
|
|
270
|
+
"detail": "natural-language override is not permitted",
|
|
271
|
+
}
|
|
272
|
+
return {
|
|
273
|
+
"ok": True,
|
|
274
|
+
"code": "LEDGER_OK",
|
|
275
|
+
"phase": phase,
|
|
276
|
+
"validated": required,
|
|
277
|
+
"ledgerPath": str(ledger_path) if ledger_path else None,
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
|
|
281
|
+
def _sha256_file(path: Path) -> str:
|
|
282
|
+
digest = hashlib.sha256()
|
|
283
|
+
with path.open("rb") as handle:
|
|
284
|
+
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
285
|
+
digest.update(chunk)
|
|
286
|
+
return digest.hexdigest()
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def validate_identity(
|
|
290
|
+
project: Path,
|
|
291
|
+
skills_root: Path,
|
|
292
|
+
executor_tool: str | None,
|
|
293
|
+
) -> dict[str, Any]:
|
|
294
|
+
skills_root = skills_root.resolve()
|
|
295
|
+
build_path = skills_root / ".harness-build.json"
|
|
296
|
+
if not build_path.is_file():
|
|
297
|
+
raise ValueError("BUNDLE_IDENTITY_MISSING: refresh the selected Harness adapter")
|
|
298
|
+
build = _read_json(build_path)
|
|
299
|
+
if not isinstance(build, dict) or build.get("schemaVersion") != 1:
|
|
300
|
+
raise ValueError("BUNDLE_IDENTITY_INVALID: .harness-build.json schema")
|
|
301
|
+
agent = str(build.get("agent") or "").strip()
|
|
302
|
+
core_hash = str(build.get("coreHash") or "").strip()
|
|
303
|
+
if not agent or not core_hash:
|
|
304
|
+
raise ValueError("BUNDLE_IDENTITY_INVALID: agent/coreHash is required")
|
|
305
|
+
if executor_tool and executor_tool != agent:
|
|
306
|
+
raise ValueError(f"BUNDLE_IDENTITY_MISMATCH: executor {executor_tool} uses {agent} bundle")
|
|
307
|
+
|
|
308
|
+
context = _read_json(project / ".harness" / "context-index.json")
|
|
309
|
+
installed = _read_json(
|
|
310
|
+
project / ".harness" / "state" / "local" / "installed-harness-bundle.json"
|
|
311
|
+
)
|
|
312
|
+
if not isinstance(context, dict) or not isinstance(installed, dict):
|
|
313
|
+
raise ValueError("BUNDLE_IDENTITY_INVALID: context or installed state")
|
|
314
|
+
adapters = context.get("project", {}).get("adapters", {})
|
|
315
|
+
adapter = adapters.get(agent) if isinstance(adapters, dict) else None
|
|
316
|
+
if not isinstance(adapter, dict):
|
|
317
|
+
raise ValueError(f"BUNDLE_IDENTITY_MISMATCH: adapter {agent} is not configured")
|
|
318
|
+
configured_root = (project / str(adapter.get("skills_root") or "")).resolve()
|
|
319
|
+
if configured_root != skills_root:
|
|
320
|
+
raise ValueError("BUNDLE_IDENTITY_MISMATCH: skills root differs from context-index")
|
|
321
|
+
bundle = context.get("skill_bundles", {}).get(agent)
|
|
322
|
+
if not isinstance(bundle, dict):
|
|
323
|
+
raise ValueError("BUNDLE_IDENTITY_INVALID: context bundle metadata missing")
|
|
324
|
+
registry_version = str(bundle.get("registry_version") or "")
|
|
325
|
+
bundle_hash = str(bundle.get("bundle_hash") or "")
|
|
326
|
+
profile = str((installed.get("profiles") or {}).get(agent) or "")
|
|
327
|
+
manifests = installed.get("manifests")
|
|
328
|
+
manifest = next((item for item in manifests if isinstance(item, dict)
|
|
329
|
+
and item.get("adapter") == agent and item.get("profile") == profile), None) \
|
|
330
|
+
if isinstance(manifests, list) else None
|
|
331
|
+
if not isinstance(manifest, dict) or \
|
|
332
|
+
str(manifest.get("bundle_version") or "") != registry_version or \
|
|
333
|
+
str(manifest.get("bundle_manifest_hash") or "") != bundle_hash:
|
|
334
|
+
raise ValueError("BUNDLE_IDENTITY_MISMATCH: installed manifest differs from context-index")
|
|
335
|
+
try:
|
|
336
|
+
marker_target = build_path.relative_to(project).as_posix()
|
|
337
|
+
except ValueError as exc:
|
|
338
|
+
raise ValueError("BUNDLE_IDENTITY_MISMATCH: skills root is outside project") from exc
|
|
339
|
+
files = installed.get("files")
|
|
340
|
+
marker = next((item for item in files if isinstance(item, dict)
|
|
341
|
+
and item.get("owner") == agent
|
|
342
|
+
and str(item.get("target_path") or "").replace("\\", "/") == marker_target), None) \
|
|
343
|
+
if isinstance(files, list) else None
|
|
344
|
+
actual_hash = _sha256_file(build_path)
|
|
345
|
+
if not isinstance(marker, dict) or str(marker.get("sha256") or "") != actual_hash:
|
|
346
|
+
raise ValueError("BUNDLE_IDENTITY_MISMATCH: installed build marker hash drifted; refresh required")
|
|
347
|
+
return {
|
|
348
|
+
"skillsRoot": str(skills_root),
|
|
349
|
+
"registryVersion": registry_version,
|
|
350
|
+
"bundleHash": bundle_hash,
|
|
351
|
+
"coreHash": core_hash,
|
|
352
|
+
"overlay": str(build.get("overlay") or "none"),
|
|
353
|
+
"profile": profile,
|
|
354
|
+
"adapter": agent,
|
|
355
|
+
"buildMarkerHash": actual_hash,
|
|
356
|
+
"contextIndexPresent": True,
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
def read_identity(skills_root: Path) -> dict[str, Any]:
|
|
361
|
+
"""Backward-compatible marker reader used by callers that only need display data."""
|
|
362
|
+
identity: dict[str, Any] = {"skillsRoot": str(skills_root.resolve())}
|
|
363
|
+
build_path = skills_root / ".harness-build.json"
|
|
364
|
+
if build_path.is_file():
|
|
365
|
+
try:
|
|
366
|
+
build = _read_json(build_path)
|
|
367
|
+
if isinstance(build, dict):
|
|
368
|
+
for key in (
|
|
369
|
+
"registryVersion",
|
|
370
|
+
"bundleHash",
|
|
371
|
+
"coreHash",
|
|
372
|
+
"overlayHash",
|
|
373
|
+
"profile",
|
|
374
|
+
"adapter",
|
|
375
|
+
):
|
|
376
|
+
if key in build:
|
|
377
|
+
identity[key] = build[key]
|
|
378
|
+
except (OSError, json.JSONDecodeError):
|
|
379
|
+
identity["buildReadError"] = True
|
|
380
|
+
context_path = skills_root / ".harness" / "context-index.json"
|
|
381
|
+
if not context_path.is_file():
|
|
382
|
+
alt = skills_root.parent / ".harness" / "context-index.json"
|
|
383
|
+
if alt.is_file():
|
|
384
|
+
context_path = alt
|
|
385
|
+
if context_path.is_file():
|
|
386
|
+
try:
|
|
387
|
+
context = _read_json(context_path)
|
|
388
|
+
if isinstance(context, dict):
|
|
389
|
+
identity["contextIndexPresent"] = True
|
|
390
|
+
except (OSError, json.JSONDecodeError):
|
|
391
|
+
identity["contextIndexPresent"] = False
|
|
392
|
+
return identity
|
|
393
|
+
|
|
394
|
+
|
|
395
|
+
def change_code_root(change_dir: Path) -> Path:
|
|
396
|
+
project = change_dir.parents[2]
|
|
397
|
+
metadata = (
|
|
398
|
+
(change_dir / "meta" / "change-context.json", ("worktreeRoot",)),
|
|
399
|
+
(change_dir / "meta" / "worktree.json", ("worktreePath", "path")),
|
|
400
|
+
)
|
|
401
|
+
for context_path, fields in metadata:
|
|
402
|
+
if not context_path.is_file():
|
|
403
|
+
continue
|
|
404
|
+
try:
|
|
405
|
+
context = json.loads(context_path.read_text(encoding="utf-8"))
|
|
406
|
+
raw_root = next(
|
|
407
|
+
(str(context.get(field) or "").strip() for field in fields if context.get(field)),
|
|
408
|
+
"",
|
|
409
|
+
)
|
|
410
|
+
if raw_root:
|
|
411
|
+
raw_path = Path(raw_root).expanduser()
|
|
412
|
+
candidate = (raw_path if raw_path.is_absolute() else project / raw_path).resolve()
|
|
413
|
+
if candidate.is_dir():
|
|
414
|
+
return candidate
|
|
415
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
416
|
+
pass
|
|
417
|
+
return project
|
|
418
|
+
|
|
419
|
+
|
|
420
|
+
def classify_risk(change_dir: Path, stage: str) -> dict[str, Any]:
|
|
421
|
+
tier = "full"
|
|
422
|
+
source = "default-full"
|
|
423
|
+
plan_path = change_dir / "plans"
|
|
424
|
+
for candidate in sorted(plan_path.glob("*.md")) if plan_path.is_dir() else []:
|
|
425
|
+
text = candidate.read_text(encoding="utf-8", errors="replace")
|
|
426
|
+
match = re.search(r"风险等级[::]\s*(fast|standard|full)", text, re.IGNORECASE)
|
|
427
|
+
if match:
|
|
428
|
+
tier = match.group(1).lower()
|
|
429
|
+
source = f"plan:{candidate.name}"
|
|
430
|
+
break
|
|
431
|
+
match = re.search(r"risk[^:]*:\s*(fast|standard|full)", text, re.IGNORECASE)
|
|
432
|
+
if match:
|
|
433
|
+
tier = match.group(1).lower()
|
|
434
|
+
source = f"plan:{candidate.name}"
|
|
435
|
+
break
|
|
436
|
+
signals: list[str] = []
|
|
437
|
+
if stage == "post-run":
|
|
438
|
+
project = change_code_root(change_dir)
|
|
439
|
+
proc = subprocess.run(
|
|
440
|
+
["git", "status", "--porcelain", "--untracked-files=all"],
|
|
441
|
+
cwd=project,
|
|
442
|
+
capture_output=True,
|
|
443
|
+
text=True,
|
|
444
|
+
encoding="utf-8",
|
|
445
|
+
errors="replace",
|
|
446
|
+
check=False,
|
|
447
|
+
)
|
|
448
|
+
changed: list[str] = []
|
|
449
|
+
for line in proc.stdout.splitlines():
|
|
450
|
+
raw = line[3:].strip().strip('"').replace("\\", "/")
|
|
451
|
+
if not raw or raw.startswith(".harness/"):
|
|
452
|
+
continue
|
|
453
|
+
changed.append(raw)
|
|
454
|
+
lowered = "\n".join(changed).lower()
|
|
455
|
+
full_markers = {
|
|
456
|
+
"auth": ("auth", "token", "credential", "permission"),
|
|
457
|
+
"security": ("security", "secret", "crypto"),
|
|
458
|
+
"migration": ("migration", "migrate", "/sql/", ".sql"),
|
|
459
|
+
"concurrency": ("concurr", "lock", "lease", "transaction"),
|
|
460
|
+
"artifact-protocol": ("artifact", "protocol", "manifest", "baseline"),
|
|
461
|
+
"shared-state": ("shared", "state/", "workflow-policy"),
|
|
462
|
+
"delete": ("delete", "purge", "archive"),
|
|
463
|
+
}
|
|
464
|
+
for signal, markers in full_markers.items():
|
|
465
|
+
if any(marker in lowered for marker in markers):
|
|
466
|
+
signals.append(signal)
|
|
467
|
+
if signals:
|
|
468
|
+
observed = "full"
|
|
469
|
+
elif changed and all(
|
|
470
|
+
path.lower().endswith((".md", ".txt", ".rst"))
|
|
471
|
+
or path.lower().startswith("docs/")
|
|
472
|
+
for path in changed
|
|
473
|
+
):
|
|
474
|
+
observed = "fast"
|
|
475
|
+
signals.append("docs-only")
|
|
476
|
+
elif changed:
|
|
477
|
+
observed = "standard"
|
|
478
|
+
signals.append("production-code")
|
|
479
|
+
else:
|
|
480
|
+
observed = tier
|
|
481
|
+
signals.append("no-code-diff")
|
|
482
|
+
rank = {"fast": 0, "standard": 1, "full": 2}
|
|
483
|
+
if rank[observed] > rank[tier]:
|
|
484
|
+
tier = observed
|
|
485
|
+
source = f"{source}+post-run"
|
|
486
|
+
main_project = change_dir.parents[2]
|
|
487
|
+
workflow = _load_workflow_policy(project=main_project)
|
|
488
|
+
tier_policy = workflow["riskTiers"][tier]
|
|
489
|
+
unique_signals = sorted(set(signals))
|
|
490
|
+
stage_decisions: dict[str, dict[str, Any]] = {}
|
|
491
|
+
for stage_name, stage_policy in workflow["conditionalStages"].items():
|
|
492
|
+
default_required = stage_name in tier_policy["defaultPhases"]
|
|
493
|
+
matched = sorted(set(unique_signals) & set(stage_policy["signals"]))
|
|
494
|
+
signal_required = tier in stage_policy["tiers"] and bool(matched)
|
|
495
|
+
stage_decisions[stage_name] = {
|
|
496
|
+
"required": default_required or signal_required,
|
|
497
|
+
"reason": (
|
|
498
|
+
"tier-default" if default_required
|
|
499
|
+
else "signal:" + ",".join(matched) if signal_required
|
|
500
|
+
else "not-triggered"
|
|
501
|
+
),
|
|
502
|
+
"matchedSignals": matched,
|
|
503
|
+
}
|
|
504
|
+
payload = {
|
|
505
|
+
"ok": True,
|
|
506
|
+
"code": "CLASSIFIED",
|
|
507
|
+
"stage": stage,
|
|
508
|
+
"tier": tier,
|
|
509
|
+
"source": source,
|
|
510
|
+
"changeId": change_dir.name,
|
|
511
|
+
"signals": unique_signals,
|
|
512
|
+
"defaultPhases": list(tier_policy["defaultPhases"]),
|
|
513
|
+
"requiredValidations": list(tier_policy["requiredValidations"]),
|
|
514
|
+
"conditionalStages": list(tier_policy["conditionalStages"]),
|
|
515
|
+
"stageDecisions": stage_decisions,
|
|
516
|
+
}
|
|
517
|
+
if stage == "post-run":
|
|
518
|
+
_write_json(change_dir / "meta" / "risk-classification.json", payload)
|
|
519
|
+
return payload
|
|
520
|
+
|
|
521
|
+
|
|
522
|
+
def lint_skill_tree(skills_root: Path) -> dict[str, Any]:
|
|
523
|
+
violations: list[dict[str, Any]] = []
|
|
524
|
+
if not skills_root.is_dir():
|
|
525
|
+
return {
|
|
526
|
+
"ok": False,
|
|
527
|
+
"code": "SKILLS_ROOT_MISSING",
|
|
528
|
+
"message": f"skills root not found: {skills_root}",
|
|
529
|
+
}
|
|
530
|
+
for path in sorted(skills_root.rglob("*.md")):
|
|
531
|
+
try:
|
|
532
|
+
text = path.read_text(encoding="utf-8", errors="replace")
|
|
533
|
+
except OSError:
|
|
534
|
+
continue
|
|
535
|
+
for line_no, line in enumerate(text.splitlines(), start=1):
|
|
536
|
+
for pattern in FORBIDDEN_SKILL_PATTERNS:
|
|
537
|
+
if pattern.search(line):
|
|
538
|
+
violations.append(
|
|
539
|
+
{
|
|
540
|
+
"file": str(path.relative_to(skills_root)),
|
|
541
|
+
"line": line_no,
|
|
542
|
+
"pattern": pattern.pattern,
|
|
543
|
+
"text": line.strip(),
|
|
544
|
+
}
|
|
545
|
+
)
|
|
546
|
+
try:
|
|
547
|
+
workflow = _load_workflow_policy(skills_root=skills_root)
|
|
548
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
549
|
+
violations.append(
|
|
550
|
+
{
|
|
551
|
+
"file": "contracts/workflow-policy.json",
|
|
552
|
+
"line": 0,
|
|
553
|
+
"pattern": "valid workflow policy",
|
|
554
|
+
"text": str(exc),
|
|
555
|
+
}
|
|
556
|
+
)
|
|
557
|
+
workflow = {"skills": {}}
|
|
558
|
+
active_profile: str | None = None
|
|
559
|
+
build_marker = skills_root / ".harness-build.json"
|
|
560
|
+
if build_marker.is_file():
|
|
561
|
+
try:
|
|
562
|
+
marker = json.loads(build_marker.read_text(encoding="utf-8"))
|
|
563
|
+
active_profile = "java" if marker.get("overlay") == "java" else "general"
|
|
564
|
+
except (OSError, json.JSONDecodeError):
|
|
565
|
+
active_profile = None
|
|
566
|
+
for skill_name, contract in sorted(workflow["skills"].items()):
|
|
567
|
+
if active_profile is not None and active_profile not in contract.get(
|
|
568
|
+
"profiles", ["general", "java"]
|
|
569
|
+
):
|
|
570
|
+
continue
|
|
571
|
+
skill_files = sorted(skills_root.rglob(f"{skill_name}/SKILL.md"))
|
|
572
|
+
if not skill_files:
|
|
573
|
+
violations.append(
|
|
574
|
+
{
|
|
575
|
+
"file": skill_name,
|
|
576
|
+
"line": 0,
|
|
577
|
+
"pattern": "policy skill exists",
|
|
578
|
+
"text": f"missing {skill_name}/SKILL.md",
|
|
579
|
+
}
|
|
580
|
+
)
|
|
581
|
+
continue
|
|
582
|
+
required = tuple(
|
|
583
|
+
marker
|
|
584
|
+
for capability in contract.get("capabilities", [])
|
|
585
|
+
for marker in CAPABILITY_MARKERS.get(capability, ())
|
|
586
|
+
)
|
|
587
|
+
for skill_file in skill_files:
|
|
588
|
+
text = skill_file.read_text(encoding="utf-8", errors="replace")
|
|
589
|
+
for marker in required:
|
|
590
|
+
if marker not in text:
|
|
591
|
+
violations.append(
|
|
592
|
+
{
|
|
593
|
+
"file": str(skill_file.relative_to(skills_root)),
|
|
594
|
+
"line": 0,
|
|
595
|
+
"pattern": marker,
|
|
596
|
+
"text": f"missing capability command: {marker}",
|
|
597
|
+
}
|
|
598
|
+
)
|
|
599
|
+
return {
|
|
600
|
+
"ok": len(violations) == 0,
|
|
601
|
+
"code": "LINT_OK" if not violations else "SKILL_CONTRACT_VIOLATION",
|
|
602
|
+
"violations": violations,
|
|
603
|
+
"skillsRoot": str(skills_root.resolve()),
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
def append_phase_event(
|
|
608
|
+
change_dir: Path,
|
|
609
|
+
*,
|
|
610
|
+
phase: str,
|
|
611
|
+
type_: str,
|
|
612
|
+
status: str | None = None,
|
|
613
|
+
note: str = "",
|
|
614
|
+
identity: dict[str, Any] | None = None,
|
|
615
|
+
run_id: str | None = None,
|
|
616
|
+
executor_tool: str | None = None,
|
|
617
|
+
executor_agent: str | None = None,
|
|
618
|
+
executor_model: str | None = None,
|
|
619
|
+
) -> dict[str, Any]:
|
|
620
|
+
events_file = he.events_path(change_dir)
|
|
621
|
+
existing = he.load_events(events_file)
|
|
622
|
+
args = argparse.Namespace(
|
|
623
|
+
phase=phase,
|
|
624
|
+
type=type_,
|
|
625
|
+
status=status,
|
|
626
|
+
note=note,
|
|
627
|
+
command=None,
|
|
628
|
+
exit_code=None,
|
|
629
|
+
duration_ms=None,
|
|
630
|
+
name=None,
|
|
631
|
+
path=None,
|
|
632
|
+
kind=None,
|
|
633
|
+
code=None,
|
|
634
|
+
severity=None,
|
|
635
|
+
message=None,
|
|
636
|
+
decision=None,
|
|
637
|
+
reason=None,
|
|
638
|
+
run_id=run_id,
|
|
639
|
+
attempt=None,
|
|
640
|
+
executor_tool=executor_tool,
|
|
641
|
+
executor_agent=executor_agent,
|
|
642
|
+
executor_model=executor_model,
|
|
643
|
+
handoff_from_tool=None,
|
|
644
|
+
handoff_reason=None,
|
|
645
|
+
)
|
|
646
|
+
event = he.build_event(args, existing)
|
|
647
|
+
if identity:
|
|
648
|
+
for key, value in identity.items():
|
|
649
|
+
if key not in event and value is not None:
|
|
650
|
+
event[key] = value
|
|
651
|
+
line = json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
652
|
+
lock_path = change_dir / "events.ndjson.lock"
|
|
653
|
+
with he.event_file_lock(lock_path):
|
|
654
|
+
he.atomic_append_line(events_file, line)
|
|
655
|
+
rendered = False
|
|
656
|
+
log_path = None
|
|
657
|
+
if type_ == "phase.end":
|
|
658
|
+
events = he.load_events(events_file)
|
|
659
|
+
content = he.render_execution_log(events)
|
|
660
|
+
log_path = he.write_execution_log(change_dir, content)
|
|
661
|
+
rendered = True
|
|
662
|
+
return {
|
|
663
|
+
"ok": True,
|
|
664
|
+
"eventId": event.get("id"),
|
|
665
|
+
"path": str(events_file),
|
|
666
|
+
"rendered": rendered,
|
|
667
|
+
"executionLogPath": str(log_path) if log_path else None,
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
|
|
671
|
+
def _phase_event_exists(change_dir: Path, phase: str, type_: str, run_id: str) -> bool:
|
|
672
|
+
return any(
|
|
673
|
+
event.get("phase") == phase
|
|
674
|
+
and event.get("type") == type_
|
|
675
|
+
and event.get("run_id") == run_id
|
|
676
|
+
for event in he.load_events(he.events_path(change_dir))
|
|
677
|
+
)
|
|
678
|
+
|
|
679
|
+
|
|
680
|
+
def cmd_begin(args: argparse.Namespace) -> int:
|
|
681
|
+
as_json = bool(args.json)
|
|
682
|
+
project = hc.resolve_main_project_root()
|
|
683
|
+
resolved = hc.resolve_change(project, args.change)
|
|
684
|
+
if not resolved.get("ok"):
|
|
685
|
+
return emit_error(
|
|
686
|
+
str(resolved.get("code", "RESOLVE_FAILED")),
|
|
687
|
+
str(resolved.get("message", "change resolve failed")),
|
|
688
|
+
as_json=as_json,
|
|
689
|
+
extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
|
|
690
|
+
)
|
|
691
|
+
change_dir = Path(resolved["changeDir"])
|
|
692
|
+
blocked = foundation_gate_blocks(getattr(args, "task", None), change_dir)
|
|
693
|
+
if blocked:
|
|
694
|
+
return emit_error(
|
|
695
|
+
blocked["code"],
|
|
696
|
+
blocked["message"],
|
|
697
|
+
as_json=as_json,
|
|
698
|
+
extra={k: v for k, v in blocked.items() if k not in {"ok", "message", "code"}},
|
|
699
|
+
)
|
|
700
|
+
|
|
701
|
+
try:
|
|
702
|
+
policy = _load_workflow_policy(project=project)
|
|
703
|
+
except (OSError, ValueError, hwp.PolicyValidationError) as exc:
|
|
704
|
+
return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
|
|
705
|
+
|
|
706
|
+
executor_tool = args.executor_tool or os.environ.get("HUNTER_HARNESS_TOOL")
|
|
707
|
+
if not args.skills_root:
|
|
708
|
+
return emit_error(
|
|
709
|
+
"BUNDLE_IDENTITY_REQUIRED",
|
|
710
|
+
"--skills-root is required; refresh the selected Harness adapter if identity is missing",
|
|
711
|
+
as_json=as_json,
|
|
712
|
+
)
|
|
713
|
+
try:
|
|
714
|
+
identity = validate_identity(
|
|
715
|
+
project, Path(args.skills_root).expanduser().resolve(), executor_tool
|
|
716
|
+
)
|
|
717
|
+
except (OSError, json.JSONDecodeError, ValueError) as exc:
|
|
718
|
+
return emit_error("BUNDLE_IDENTITY_INVALID", str(exc), as_json=as_json)
|
|
719
|
+
executor_tool = executor_tool or str(identity.get("adapter") or "") or None
|
|
720
|
+
explicit_run_id = args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID")
|
|
721
|
+
current_lease = hc.inspect_lease(project, resolved["changeId"])
|
|
722
|
+
run_id = explicit_run_id or (
|
|
723
|
+
str(current_lease.get("runId"))
|
|
724
|
+
if isinstance(current_lease, dict) and current_lease.get("phase") == args.phase
|
|
725
|
+
else "run-" + uuid.uuid4().hex
|
|
726
|
+
)
|
|
727
|
+
claim = hc.claim_lease(
|
|
728
|
+
project,
|
|
729
|
+
change_id=resolved["changeId"],
|
|
730
|
+
phase=args.phase,
|
|
731
|
+
run_id=run_id,
|
|
732
|
+
ttl_seconds=int(args.ttl_seconds),
|
|
733
|
+
)
|
|
734
|
+
if not claim.get("ok"):
|
|
735
|
+
return emit_error(
|
|
736
|
+
str(claim.get("code", "LEASE_CONFLICT")),
|
|
737
|
+
str(claim.get("message", "lease claim failed")),
|
|
738
|
+
as_json=as_json,
|
|
739
|
+
extra={k: v for k, v in claim.items() if k not in {"ok", "message", "code"}},
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
try:
|
|
743
|
+
event_result = {"ok": True, "skipped": True, "reason": "already-recorded"} \
|
|
744
|
+
if _phase_event_exists(change_dir, args.phase, "phase.start", run_id) \
|
|
745
|
+
else append_phase_event(
|
|
746
|
+
change_dir,
|
|
747
|
+
phase=args.phase,
|
|
748
|
+
type_="phase.start",
|
|
749
|
+
note=args.note or "",
|
|
750
|
+
identity=identity,
|
|
751
|
+
run_id=run_id,
|
|
752
|
+
executor_tool=executor_tool,
|
|
753
|
+
executor_agent=args.executor_agent or os.environ.get("HUNTER_HARNESS_AGENT"),
|
|
754
|
+
executor_model=args.executor_model or os.environ.get("HUNTER_HARNESS_MODEL"),
|
|
755
|
+
)
|
|
756
|
+
except BaseException:
|
|
757
|
+
hc.release_lease(
|
|
758
|
+
project, change_id=resolved["changeId"], phase=args.phase, run_id=run_id
|
|
759
|
+
)
|
|
760
|
+
raise
|
|
761
|
+
|
|
762
|
+
payload = {
|
|
763
|
+
"ok": True,
|
|
764
|
+
"code": "PHASE_BEGUN",
|
|
765
|
+
"phase": args.phase,
|
|
766
|
+
"changeId": resolved["changeId"],
|
|
767
|
+
"changeDir": str(change_dir),
|
|
768
|
+
"projectRoot": str(project),
|
|
769
|
+
"lease": claim.get("lease"),
|
|
770
|
+
"identity": identity,
|
|
771
|
+
"event": event_result,
|
|
772
|
+
"policySchemaVersion": policy.get("schemaVersion"),
|
|
773
|
+
}
|
|
774
|
+
emit(payload, as_json=as_json)
|
|
775
|
+
return 0
|
|
776
|
+
|
|
777
|
+
|
|
778
|
+
def cmd_close(args: argparse.Namespace) -> int:
|
|
779
|
+
as_json = bool(args.json)
|
|
780
|
+
project = hc.resolve_main_project_root()
|
|
781
|
+
resolved = hc.resolve_change(project, args.change)
|
|
782
|
+
if not resolved.get("ok"):
|
|
783
|
+
return emit_error(
|
|
784
|
+
str(resolved.get("code", "RESOLVE_FAILED")),
|
|
785
|
+
str(resolved.get("message", "change resolve failed")),
|
|
786
|
+
as_json=as_json,
|
|
787
|
+
extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
|
|
788
|
+
)
|
|
789
|
+
change_dir = Path(resolved["changeDir"])
|
|
790
|
+
blocked = foundation_gate_blocks(getattr(args, "task", None), change_dir)
|
|
791
|
+
if blocked:
|
|
792
|
+
return emit_error(
|
|
793
|
+
blocked["code"],
|
|
794
|
+
blocked["message"],
|
|
795
|
+
as_json=as_json,
|
|
796
|
+
extra={k: v for k, v in blocked.items() if k not in {"ok", "message", "code"}},
|
|
797
|
+
)
|
|
798
|
+
|
|
799
|
+
try:
|
|
800
|
+
policy = _load_workflow_policy(project=project)
|
|
801
|
+
except (OSError, ValueError, hwp.PolicyValidationError) as exc:
|
|
802
|
+
return emit_error("POLICY_LOAD_FAILED", str(exc), as_json=as_json)
|
|
803
|
+
|
|
804
|
+
explicit_run_id = args.run_id or os.environ.get("HUNTER_HARNESS_RUN_ID")
|
|
805
|
+
current_lease = hc.inspect_lease(project, resolved["changeId"])
|
|
806
|
+
if current_lease is None:
|
|
807
|
+
return emit_error("LEASE_ABSENT", "no active lease for phase close", as_json=as_json)
|
|
808
|
+
run_id = explicit_run_id or str(current_lease.get("runId") or "")
|
|
809
|
+
if str(current_lease.get("runId")) != run_id or str(current_lease.get("phase")) != args.phase:
|
|
810
|
+
return emit_error(
|
|
811
|
+
"LEASE_OWNER_MISMATCH",
|
|
812
|
+
"active lease does not match close phase/run-id",
|
|
813
|
+
as_json=as_json,
|
|
814
|
+
extra={"holder": current_lease},
|
|
815
|
+
)
|
|
816
|
+
|
|
817
|
+
ledger_result = validate_ledger_for_phase_close(change_dir, args.phase, policy)
|
|
818
|
+
if not ledger_result.get("ok") and args.phase in {"run", "test", "package"}:
|
|
819
|
+
return emit_error(
|
|
820
|
+
str(ledger_result.get("code", "LEDGER_INVALID")),
|
|
821
|
+
str(ledger_result.get("message", "ledger validation failed")),
|
|
822
|
+
as_json=as_json,
|
|
823
|
+
extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
|
|
824
|
+
)
|
|
825
|
+
|
|
826
|
+
guard_result = None
|
|
827
|
+
if args.phase in {"run", "test"}:
|
|
828
|
+
project_root = args.project or str(project)
|
|
829
|
+
guard_result = htg.close(project_root, change_dir)
|
|
830
|
+
if not guard_result.get("ok"):
|
|
831
|
+
return emit_error(
|
|
832
|
+
str(guard_result.get("code", "TEST_GUARD_CLOSE_FAILED")),
|
|
833
|
+
"test guard close failed",
|
|
834
|
+
as_json=as_json,
|
|
835
|
+
extra=guard_result,
|
|
836
|
+
)
|
|
837
|
+
|
|
838
|
+
event_result = append_phase_event(
|
|
839
|
+
change_dir,
|
|
840
|
+
phase=args.phase,
|
|
841
|
+
type_="phase.end",
|
|
842
|
+
status=args.status,
|
|
843
|
+
note=args.note or "",
|
|
844
|
+
run_id=run_id,
|
|
845
|
+
) if not _phase_event_exists(change_dir, args.phase, "phase.end", run_id) else {
|
|
846
|
+
"ok": True, "skipped": True, "reason": "already-recorded"
|
|
847
|
+
}
|
|
848
|
+
|
|
849
|
+
release = hc.release_lease(
|
|
850
|
+
project,
|
|
851
|
+
change_id=resolved["changeId"],
|
|
852
|
+
phase=args.phase,
|
|
853
|
+
run_id=run_id,
|
|
854
|
+
)
|
|
855
|
+
if not release.get("ok"):
|
|
856
|
+
return emit_error(
|
|
857
|
+
str(release.get("code", "LEASE_RELEASE_FAILED")),
|
|
858
|
+
str(release.get("message", "lease release failed")),
|
|
859
|
+
as_json=as_json,
|
|
860
|
+
extra={k: v for k, v in release.items() if k not in {"ok", "message", "code"}},
|
|
861
|
+
)
|
|
862
|
+
|
|
863
|
+
payload = {
|
|
864
|
+
"ok": True,
|
|
865
|
+
"code": "PHASE_CLOSED",
|
|
866
|
+
"phase": args.phase,
|
|
867
|
+
"status": args.status,
|
|
868
|
+
"changeId": resolved["changeId"],
|
|
869
|
+
"ledger": ledger_result,
|
|
870
|
+
"testGuard": guard_result,
|
|
871
|
+
"event": event_result,
|
|
872
|
+
"lease": release,
|
|
873
|
+
}
|
|
874
|
+
emit(payload, as_json=as_json)
|
|
875
|
+
return 0
|
|
876
|
+
|
|
877
|
+
|
|
878
|
+
def cmd_classify(args: argparse.Namespace) -> int:
|
|
879
|
+
as_json = bool(args.json)
|
|
880
|
+
project = hc.resolve_main_project_root()
|
|
881
|
+
resolved = hc.resolve_change(project, args.change)
|
|
882
|
+
if not resolved.get("ok"):
|
|
883
|
+
return emit_error(
|
|
884
|
+
str(resolved.get("code", "RESOLVE_FAILED")),
|
|
885
|
+
str(resolved.get("message", "change resolve failed")),
|
|
886
|
+
as_json=as_json,
|
|
887
|
+
extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
|
|
888
|
+
)
|
|
889
|
+
payload = classify_risk(Path(resolved["changeDir"]), args.stage)
|
|
890
|
+
emit(payload, as_json=as_json)
|
|
891
|
+
return 0
|
|
892
|
+
|
|
893
|
+
|
|
894
|
+
def cmd_checkpoint(args: argparse.Namespace) -> int:
|
|
895
|
+
as_json = bool(args.json)
|
|
896
|
+
project = hc.resolve_main_project_root()
|
|
897
|
+
resolved = hc.resolve_change(project, args.change)
|
|
898
|
+
if not resolved.get("ok"):
|
|
899
|
+
return emit_error(
|
|
900
|
+
str(resolved.get("code", "RESOLVE_FAILED")),
|
|
901
|
+
str(resolved.get("message", "change resolve failed")),
|
|
902
|
+
as_json=as_json,
|
|
903
|
+
extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
|
|
904
|
+
)
|
|
905
|
+
change_dir = Path(resolved["changeDir"])
|
|
906
|
+
path = change_dir / CHECKPOINTS_REL
|
|
907
|
+
if args.checkpoint_action == "status":
|
|
908
|
+
checkpoints = load_checkpoints(change_dir)
|
|
909
|
+
status = checkpoint_status(checkpoints, args.id)
|
|
910
|
+
payload = {
|
|
911
|
+
"ok": True,
|
|
912
|
+
"code": "CHECKPOINT_STATUS",
|
|
913
|
+
"checkpointId": args.id,
|
|
914
|
+
"status": status,
|
|
915
|
+
"path": str(path) if path.is_file() else None,
|
|
916
|
+
}
|
|
917
|
+
emit(payload, as_json=as_json)
|
|
918
|
+
return 0
|
|
919
|
+
|
|
920
|
+
if args.checkpoint_action != "approve":
|
|
921
|
+
return emit_error("INVALID_CHECKPOINT_ACTION", args.checkpoint_action, as_json=as_json)
|
|
922
|
+
|
|
923
|
+
checkpoints = load_checkpoints(change_dir)
|
|
924
|
+
items = checkpoints.get("checkpoints") if isinstance(checkpoints, dict) else None
|
|
925
|
+
item = next((candidate for candidate in items if isinstance(candidate, dict)
|
|
926
|
+
and candidate.get("id") == args.id), None) if isinstance(items, list) else None
|
|
927
|
+
if item is None:
|
|
928
|
+
return emit_error("CHECKPOINT_NOT_FOUND", f"checkpoint not found: {args.id}", as_json=as_json)
|
|
929
|
+
expected_reviewer = str(item.get("reviewerTool") or "")
|
|
930
|
+
if not args.reviewer or (expected_reviewer and args.reviewer != expected_reviewer):
|
|
931
|
+
return emit_error(
|
|
932
|
+
"CHECKPOINT_REVIEWER_MISMATCH",
|
|
933
|
+
f"checkpoint requires reviewer {expected_reviewer or 'explicit reviewer'}",
|
|
934
|
+
as_json=as_json,
|
|
935
|
+
)
|
|
936
|
+
required_report = str(item.get("requiredReport") or "")
|
|
937
|
+
report_path = change_dir / required_report
|
|
938
|
+
if not required_report or not report_path.is_file() or report_path.stat().st_size == 0:
|
|
939
|
+
return emit_error(
|
|
940
|
+
"CHECKPOINT_REPORT_MISSING",
|
|
941
|
+
f"required report is missing: {required_report or '<unset>'}",
|
|
942
|
+
as_json=as_json,
|
|
943
|
+
)
|
|
944
|
+
report_text = report_path.read_text(encoding="utf-8", errors="replace")
|
|
945
|
+
if not re.search(r"(?im)^foundation-gate:\s*approved\s*$", report_text):
|
|
946
|
+
return emit_error(
|
|
947
|
+
"CHECKPOINT_REPORT_NOT_APPROVED",
|
|
948
|
+
"required report does not contain 'foundation-gate: approved'",
|
|
949
|
+
as_json=as_json,
|
|
950
|
+
)
|
|
951
|
+
item["status"] = "approved"
|
|
952
|
+
item["approvedAt"] = hc.now_iso()
|
|
953
|
+
item["approvedBy"] = args.reviewer
|
|
954
|
+
_write_json(path, checkpoints)
|
|
955
|
+
payload = {
|
|
956
|
+
"ok": True,
|
|
957
|
+
"code": "CHECKPOINT_APPROVED",
|
|
958
|
+
"checkpointId": args.id,
|
|
959
|
+
"status": "approved",
|
|
960
|
+
"path": str(path),
|
|
961
|
+
}
|
|
962
|
+
emit(payload, as_json=as_json)
|
|
963
|
+
return 0
|
|
964
|
+
|
|
965
|
+
|
|
966
|
+
def cmd_lint_skills(args: argparse.Namespace) -> int:
|
|
967
|
+
as_json = bool(args.json)
|
|
968
|
+
root = Path(args.skills_root).expanduser().resolve()
|
|
969
|
+
payload = lint_skill_tree(root)
|
|
970
|
+
if payload.get("ok"):
|
|
971
|
+
emit(payload, as_json=as_json)
|
|
972
|
+
return 0
|
|
973
|
+
return emit_error(
|
|
974
|
+
str(payload.get("code", "LINT_FAILED")),
|
|
975
|
+
str(payload.get("message", "skill lint failed")),
|
|
976
|
+
as_json=as_json,
|
|
977
|
+
extra={k: v for k, v in payload.items() if k not in {"ok", "message", "code"}},
|
|
978
|
+
)
|
|
979
|
+
|
|
980
|
+
|
|
981
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
982
|
+
parser = argparse.ArgumentParser(prog="harness_gate.py")
|
|
983
|
+
parser.add_argument("--json", action="store_true")
|
|
984
|
+
sub = parser.add_subparsers(dest="command_name", required=True)
|
|
985
|
+
shared = argparse.ArgumentParser(add_help=False)
|
|
986
|
+
shared.add_argument("--json", action="store_true", default=argparse.SUPPRESS)
|
|
987
|
+
|
|
988
|
+
p_begin = sub.add_parser("begin", parents=[shared])
|
|
989
|
+
p_begin.add_argument("--phase", required=True)
|
|
990
|
+
p_begin.add_argument("--change", default=None)
|
|
991
|
+
p_begin.add_argument("--project", default=None)
|
|
992
|
+
p_begin.add_argument("--skills-root", default=None)
|
|
993
|
+
p_begin.add_argument("--run-id", default=None)
|
|
994
|
+
p_begin.add_argument("--ttl-seconds", type=int, default=3600)
|
|
995
|
+
p_begin.add_argument("--task", type=int, default=None)
|
|
996
|
+
p_begin.add_argument("--note", default="")
|
|
997
|
+
p_begin.add_argument("--executor-tool", default=None)
|
|
998
|
+
p_begin.add_argument("--executor-agent", default=None)
|
|
999
|
+
p_begin.add_argument("--executor-model", default=None)
|
|
1000
|
+
p_begin.set_defaults(func=cmd_begin)
|
|
1001
|
+
|
|
1002
|
+
p_close = sub.add_parser("close", parents=[shared])
|
|
1003
|
+
p_close.add_argument("--phase", required=True)
|
|
1004
|
+
p_close.add_argument("--change", default=None)
|
|
1005
|
+
p_close.add_argument("--project", default=None)
|
|
1006
|
+
p_close.add_argument("--status", required=True)
|
|
1007
|
+
p_close.add_argument("--run-id", default=None)
|
|
1008
|
+
p_close.add_argument("--task", type=int, default=None)
|
|
1009
|
+
p_close.add_argument("--note", default="")
|
|
1010
|
+
p_close.set_defaults(func=cmd_close)
|
|
1011
|
+
|
|
1012
|
+
p_classify = sub.add_parser("classify", parents=[shared])
|
|
1013
|
+
p_classify.add_argument("--change", default=None)
|
|
1014
|
+
p_classify.add_argument("--stage", required=True, choices=["plan", "post-run"])
|
|
1015
|
+
p_classify.set_defaults(func=cmd_classify)
|
|
1016
|
+
|
|
1017
|
+
p_checkpoint = sub.add_parser("checkpoint", parents=[shared])
|
|
1018
|
+
p_checkpoint.add_argument("checkpoint_action", choices=["status", "approve"])
|
|
1019
|
+
p_checkpoint.add_argument("--id", required=True)
|
|
1020
|
+
p_checkpoint.add_argument("--change", default=None)
|
|
1021
|
+
p_checkpoint.add_argument("--reviewer", default=None)
|
|
1022
|
+
p_checkpoint.set_defaults(func=cmd_checkpoint)
|
|
1023
|
+
|
|
1024
|
+
p_lint = sub.add_parser("lint-skills", parents=[shared])
|
|
1025
|
+
p_lint.add_argument("--skills-root", required=True)
|
|
1026
|
+
p_lint.set_defaults(func=cmd_lint_skills)
|
|
1027
|
+
|
|
1028
|
+
return parser
|
|
1029
|
+
|
|
1030
|
+
|
|
1031
|
+
def main(argv: list[str] | None = None) -> int:
|
|
1032
|
+
parser = build_parser()
|
|
1033
|
+
args = parser.parse_args(argv)
|
|
1034
|
+
return int(args.func(args))
|
|
1035
|
+
|
|
1036
|
+
|
|
1037
|
+
if __name__ == "__main__":
|
|
1038
|
+
raise SystemExit(main())
|