hunter-harness 0.1.0
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/LICENSE +21 -0
- package/README.md +17 -0
- package/dist/bin.js +3356 -0
- package/package.json +34 -0
- package/resources/bootstrap-ir/manifest.json +19 -0
- package/resources/bootstrap-ir/skills/harness-apidoc.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-archive.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-codebase-map.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-knowledge-ingest.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-package.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-plan.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-review.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-run.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-skill-optimizer.yaml +28 -0
- package/resources/bootstrap-ir/skills/harness-submit.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-sync.yaml +24 -0
- package/resources/bootstrap-ir/skills/harness-test.yaml +24 -0
- package/resources/bootstrap-ir/templates/claude-code-skill.md +12 -0
- package/resources/harness/general/.harness-build.json +5 -0
- package/resources/harness/general/CONTEXT.md +65 -0
- package/resources/harness/general/README.md +478 -0
- package/resources/harness/general/agents/harness-evaluator.md +96 -0
- package/resources/harness/general/agents/harness-explorer.md +72 -0
- package/resources/harness/general/agents/harness-reviewer.md +69 -0
- package/resources/harness/general/harness-archive/SKILL.md +215 -0
- package/resources/harness/general/harness-archive/checklist.md +52 -0
- package/resources/harness/general/harness-archive/reference.md +103 -0
- package/resources/harness/general/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/resources/harness/general/harness-archive/templates/render-summary.mjs +171 -0
- package/resources/harness/general/harness-archive/templates/summary-data-template.json +128 -0
- package/resources/harness/general/harness-codebase-map/SKILL.md +112 -0
- package/resources/harness/general/harness-codebase-map/checklist.md +126 -0
- package/resources/harness/general/harness-codebase-map/reference.md +364 -0
- package/resources/harness/general/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/resources/harness/general/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/resources/harness/general/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/resources/harness/general/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/resources/harness/general/harness-codebase-map/templates/STACK.md +55 -0
- package/resources/harness/general/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/resources/harness/general/harness-codebase-map/templates/TESTING.md +64 -0
- package/resources/harness/general/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/resources/harness/general/harness-codebase-map/templates/map-summary.md +64 -0
- package/resources/harness/general/harness-knowledge-ingest/SKILL.md +246 -0
- package/resources/harness/general/harness-knowledge-ingest/design.md +842 -0
- package/resources/harness/general/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
- package/resources/harness/general/harness-knowledge-ingest/mcp-config.example.json +10 -0
- package/resources/harness/general/harness-knowledge-ingest/reference.md +309 -0
- package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
- package/resources/harness/general/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-01-10-ledger-reconciliation/reports/final/summary-data.json +43 -0
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-02-14-ledger-snapshot-followup/reports/final/summary-data.json +38 -0
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-03-05-webhook-contract/reports/final/summary-data.json +36 -0
- package/resources/harness/general/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/project.yaml +1 -0
- package/resources/harness/general/harness-knowledge-ingest/tests/test_harness_knowledge.py +1792 -0
- package/resources/harness/general/harness-knowledge-query/SKILL.md +164 -0
- package/resources/harness/general/harness-plan/SKILL.md +127 -0
- package/resources/harness/general/harness-plan/checklist.md +243 -0
- package/resources/harness/general/harness-plan/protocols.md +124 -0
- package/resources/harness/general/harness-plan/reference.md +376 -0
- package/resources/harness/general/harness-review/SKILL.md +156 -0
- package/resources/harness/general/harness-review/checklist.md +124 -0
- package/resources/harness/general/harness-review/protocols.md +68 -0
- package/resources/harness/general/harness-review/reference.md +86 -0
- package/resources/harness/general/harness-run/SKILL.md +132 -0
- package/resources/harness/general/harness-run/checklist.md +259 -0
- package/resources/harness/general/harness-run/protocols.md +78 -0
- package/resources/harness/general/harness-run/reference.md +834 -0
- package/resources/harness/general/harness-submit/SKILL.md +159 -0
- package/resources/harness/general/harness-submit/checklist.md +407 -0
- package/resources/harness/general/harness-submit/reference.md +152 -0
- package/resources/harness/general/harness-sync/SKILL.md +82 -0
- package/resources/harness/general/harness-sync/reference.md +153 -0
- package/resources/harness/general/harness-test/SKILL.md +180 -0
- package/resources/harness/general/harness-test/checklist.md +319 -0
- package/resources/harness/general/harness-test/pitfalls.md +260 -0
- package/resources/harness/general/harness-test/reference.md +791 -0
- package/resources/harness/general/protocols/archive-report-protocol.md +175 -0
- package/resources/harness/general/protocols/evidence-based-reporting-protocol.md +134 -0
- package/resources/harness/general/protocols/ledger-protocol.md +270 -0
- package/resources/harness/general/protocols/powershell-protocol.md +181 -0
- package/resources/harness/general/protocols/report-pipeline-protocol.md +150 -0
- package/resources/harness/general/protocols/sensitive-info-protocol.md +92 -0
- package/resources/harness/general/protocols/state-layout-protocol.md +131 -0
- package/resources/harness/general/protocols/submit-protocol.md +78 -0
- package/resources/harness/general/scripts/harness_archive.py +2319 -0
- package/resources/harness/general/scripts/harness_deploy.py +613 -0
- package/resources/harness/general/scripts/harness_events.py +680 -0
- package/resources/harness/general/scripts/harness_ledger.py +671 -0
- package/resources/harness/general/scripts/harness_preflight.py +780 -0
- package/resources/harness/general/scripts/harness_service.py +1331 -0
- package/resources/harness/java/.harness-build.json +5 -0
- package/resources/harness/java/CONTEXT.md +65 -0
- package/resources/harness/java/README.md +478 -0
- package/resources/harness/java/agents/harness-evaluator.md +96 -0
- package/resources/harness/java/agents/harness-explorer.md +72 -0
- package/resources/harness/java/agents/harness-reviewer.md +69 -0
- package/resources/harness/java/harness-apidoc/SKILL.md +86 -0
- package/resources/harness/java/harness-apidoc/checklist.md +142 -0
- package/resources/harness/java/harness-apidoc/reference.md +233 -0
- package/resources/harness/java/harness-archive/SKILL.md +215 -0
- package/resources/harness/java/harness-archive/checklist.md +52 -0
- package/resources/harness/java/harness-archive/reference.md +103 -0
- package/resources/harness/java/harness-archive/scripts/gen-manifest.ps1 +31 -0
- package/resources/harness/java/harness-archive/templates/render-summary.mjs +171 -0
- package/resources/harness/java/harness-archive/templates/summary-data-template.json +128 -0
- package/resources/harness/java/harness-codebase-map/SKILL.md +112 -0
- package/resources/harness/java/harness-codebase-map/checklist.md +126 -0
- package/resources/harness/java/harness-codebase-map/reference.md +364 -0
- package/resources/harness/java/harness-codebase-map/templates/ARCHITECTURE.md +57 -0
- package/resources/harness/java/harness-codebase-map/templates/CONCERNS.md +49 -0
- package/resources/harness/java/harness-codebase-map/templates/CONVENTIONS.md +57 -0
- package/resources/harness/java/harness-codebase-map/templates/INTEGRATIONS.md +52 -0
- package/resources/harness/java/harness-codebase-map/templates/STACK.md +55 -0
- package/resources/harness/java/harness-codebase-map/templates/STRUCTURE.md +69 -0
- package/resources/harness/java/harness-codebase-map/templates/TESTING.md +64 -0
- package/resources/harness/java/harness-codebase-map/templates/map-manifest.schema.json +73 -0
- package/resources/harness/java/harness-codebase-map/templates/map-summary.md +64 -0
- package/resources/harness/java/harness-knowledge-ingest/SKILL.md +246 -0
- package/resources/harness/java/harness-knowledge-ingest/design.md +842 -0
- package/resources/harness/java/harness-knowledge-ingest/evaluations/harness_knowledge_evaluation.xml +42 -0
- package/resources/harness/java/harness-knowledge-ingest/mcp-config.example.json +10 -0
- package/resources/harness/java/harness-knowledge-ingest/reference.md +309 -0
- package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge.py +4082 -0
- package/resources/harness/java/harness-knowledge-ingest/scripts/harness_knowledge_mcp.py +273 -0
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-01-10-ledger-reconciliation/reports/final/summary-data.json +43 -0
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-02-14-ledger-snapshot-followup/reports/final/summary-data.json +38 -0
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/archive/2026-03-05-webhook-contract/reports/final/summary-data.json +36 -0
- package/resources/harness/java/harness-knowledge-ingest/tests/fixtures/mcp-eval-project/.harness/project.yaml +1 -0
- package/resources/harness/java/harness-knowledge-ingest/tests/test_harness_knowledge.py +1792 -0
- package/resources/harness/java/harness-knowledge-query/SKILL.md +164 -0
- package/resources/harness/java/harness-package/SKILL.md +87 -0
- package/resources/harness/java/harness-package/checklist.md +322 -0
- package/resources/harness/java/harness-package/reference.md +210 -0
- package/resources/harness/java/harness-plan/SKILL.md +127 -0
- package/resources/harness/java/harness-plan/checklist.md +243 -0
- package/resources/harness/java/harness-plan/protocols.md +124 -0
- package/resources/harness/java/harness-plan/reference.md +376 -0
- package/resources/harness/java/harness-review/SKILL.md +156 -0
- package/resources/harness/java/harness-review/checklist.md +124 -0
- package/resources/harness/java/harness-review/protocols.md +68 -0
- package/resources/harness/java/harness-review/reference.md +86 -0
- package/resources/harness/java/harness-run/SKILL.md +148 -0
- package/resources/harness/java/harness-run/checklist.md +250 -0
- package/resources/harness/java/harness-run/protocols.md +78 -0
- package/resources/harness/java/harness-run/reference.md +814 -0
- package/resources/harness/java/harness-submit/SKILL.md +166 -0
- package/resources/harness/java/harness-submit/checklist.md +407 -0
- package/resources/harness/java/harness-submit/reference.md +152 -0
- package/resources/harness/java/harness-sync/SKILL.md +82 -0
- package/resources/harness/java/harness-sync/reference.md +153 -0
- package/resources/harness/java/harness-test/SKILL.md +192 -0
- package/resources/harness/java/harness-test/checklist.md +317 -0
- package/resources/harness/java/harness-test/pitfalls-java.md +16 -0
- package/resources/harness/java/harness-test/pitfalls.md +260 -0
- package/resources/harness/java/harness-test/reference.md +788 -0
- package/resources/harness/java/protocols/archive-report-protocol.md +175 -0
- package/resources/harness/java/protocols/evidence-based-reporting-protocol.md +134 -0
- package/resources/harness/java/protocols/ledger-protocol.md +270 -0
- package/resources/harness/java/protocols/powershell-protocol.md +181 -0
- package/resources/harness/java/protocols/report-pipeline-protocol.md +150 -0
- package/resources/harness/java/protocols/sensitive-info-protocol.md +92 -0
- package/resources/harness/java/protocols/state-layout-protocol.md +131 -0
- package/resources/harness/java/protocols/submit-protocol.md +78 -0
- package/resources/harness/java/scripts/harness_archive.py +2319 -0
- package/resources/harness/java/scripts/harness_deploy.py +613 -0
- package/resources/harness/java/scripts/harness_events.py +680 -0
- package/resources/harness/java/scripts/harness_ledger.py +671 -0
- package/resources/harness/java/scripts/harness_preflight.py +780 -0
- package/resources/harness/java/scripts/harness_service.py +1331 -0
- package/resources/harness/manifests/general.json +296 -0
- package/resources/harness/manifests/java.json +324 -0
- package/resources/manifest.json +19 -0
- package/resources/skills/harness-apidoc/SKILL.md +50 -0
- package/resources/skills/harness-archive/SKILL.md +48 -0
- package/resources/skills/harness-codebase-map/SKILL.md +53 -0
- package/resources/skills/harness-knowledge-ingest/SKILL.md +48 -0
- package/resources/skills/harness-package/SKILL.md +48 -0
- package/resources/skills/harness-plan/SKILL.md +51 -0
- package/resources/skills/harness-review/SKILL.md +50 -0
- package/resources/skills/harness-run/SKILL.md +48 -0
- package/resources/skills/harness-skill-optimizer/SKILL.md +54 -0
- package/resources/skills/harness-submit/SKILL.md +47 -0
- package/resources/skills/harness-sync/SKILL.md +48 -0
- package/resources/skills/harness-test/SKILL.md +50 -0
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Harness preflight: build-profile detect/check + quirk recording + agent precheck.
|
|
3
|
+
|
|
4
|
+
Implements DESIGN.md D5 (build-profile) and D8 (subagent precheck).
|
|
5
|
+
Python 3.10+ stdlib only. UTF-8 without BOM. Windows path friendly.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import argparse
|
|
11
|
+
import datetime as dt
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
24
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
25
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
26
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
SCHEMA_VERSION = 1
|
|
30
|
+
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
31
|
+
PITFALLS_REL = Path(".harness") / "pitfalls.md"
|
|
32
|
+
PITFALLS_APPENDIX_HEADER = "## Preflight 附录(自动追加)"
|
|
33
|
+
|
|
34
|
+
DEFAULT_BUILD_COMMANDS: dict[str, str] = {
|
|
35
|
+
"compile": "",
|
|
36
|
+
"unitTest": "",
|
|
37
|
+
"unitTestFull": "",
|
|
38
|
+
"install": "",
|
|
39
|
+
"package": "",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
VALID_QUIRK_ACTIONS = {"skip-not-block", "fix-command"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def now_iso() -> str:
|
|
46
|
+
return dt.datetime.now().astimezone().isoformat(timespec="seconds")
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def emit_json(payload: dict[str, Any], *, ok: bool = True) -> int:
|
|
50
|
+
print(json.dumps(payload, ensure_ascii=False, indent=2))
|
|
51
|
+
return 0 if ok else 1
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def read_json(path: Path) -> Any:
|
|
55
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def write_json(path: Path, data: Any) -> None:
|
|
59
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
61
|
+
path.write_text(text, encoding="utf-8")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def sha256_file(path: Path) -> str | None:
|
|
65
|
+
if not path.is_file():
|
|
66
|
+
return None
|
|
67
|
+
h = hashlib.sha256()
|
|
68
|
+
with path.open("rb") as fh:
|
|
69
|
+
for chunk in iter(lambda: fh.read(65536), b""):
|
|
70
|
+
h.update(chunk)
|
|
71
|
+
return h.hexdigest()
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def find_root_pom(project: Path) -> Path | None:
|
|
75
|
+
candidates = [project / "pom.xml", project / "pom.xml.example"]
|
|
76
|
+
for c in candidates:
|
|
77
|
+
if c.is_file():
|
|
78
|
+
return c
|
|
79
|
+
return None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def which_tool(name: str) -> str | None:
|
|
83
|
+
found = shutil.which(name)
|
|
84
|
+
if found:
|
|
85
|
+
return str(Path(found).resolve())
|
|
86
|
+
# Windows: try .cmd / .bat / .exe explicitly
|
|
87
|
+
if os.name == "nt":
|
|
88
|
+
for ext in (".cmd", ".bat", ".exe"):
|
|
89
|
+
found = shutil.which(name + ext)
|
|
90
|
+
if found:
|
|
91
|
+
return str(Path(found).resolve())
|
|
92
|
+
return None
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def run_version(tool_path: str | None, args: list[str], timeout: float = 8.0) -> str:
|
|
96
|
+
"""Best-effort version probe. Empty string if unavailable."""
|
|
97
|
+
if not tool_path:
|
|
98
|
+
return ""
|
|
99
|
+
try:
|
|
100
|
+
completed = subprocess.run(
|
|
101
|
+
[tool_path, *args],
|
|
102
|
+
capture_output=True,
|
|
103
|
+
text=True,
|
|
104
|
+
encoding="utf-8",
|
|
105
|
+
errors="replace",
|
|
106
|
+
timeout=timeout,
|
|
107
|
+
check=False,
|
|
108
|
+
)
|
|
109
|
+
except (OSError, subprocess.TimeoutExpired):
|
|
110
|
+
return ""
|
|
111
|
+
text = (completed.stdout or "") + "\n" + (completed.stderr or "")
|
|
112
|
+
return _extract_version(text)
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _extract_version(text: str) -> str:
|
|
116
|
+
# node: v20.11.0 / Apache Maven 3.9.6
|
|
117
|
+
m = re.search(r"\bv?(\d+\.\d+\.\d+(?:[-+][\w.]+)?)\b", text)
|
|
118
|
+
if m:
|
|
119
|
+
return m.group(1)
|
|
120
|
+
m = re.search(r"version\s+([^\s,]+)", text, re.IGNORECASE)
|
|
121
|
+
if m:
|
|
122
|
+
return m.group(1).strip()
|
|
123
|
+
line = next((ln.strip() for ln in text.splitlines() if ln.strip()), "")
|
|
124
|
+
return line[:120]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def is_executable_path(path_str: str | None) -> bool:
|
|
128
|
+
if not path_str:
|
|
129
|
+
return False
|
|
130
|
+
p = Path(path_str)
|
|
131
|
+
if not p.exists():
|
|
132
|
+
return False
|
|
133
|
+
if p.is_file():
|
|
134
|
+
if os.name == "nt":
|
|
135
|
+
return True
|
|
136
|
+
return os.access(p, os.X_OK)
|
|
137
|
+
# On Windows, shutil.which may return .cmd wrappers that exist
|
|
138
|
+
return p.exists()
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def load_existing_profile(project: Path) -> dict[str, Any] | None:
|
|
142
|
+
path = project / PROFILE_REL
|
|
143
|
+
if not path.is_file():
|
|
144
|
+
return None
|
|
145
|
+
try:
|
|
146
|
+
data = read_json(path)
|
|
147
|
+
except (OSError, json.JSONDecodeError):
|
|
148
|
+
return None
|
|
149
|
+
return data if isinstance(data, dict) else None
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def parse_claude_hints(project: Path) -> dict[str, str]:
|
|
153
|
+
"""Extract optional build command hints from CLAUDE.md / AGENTS.md."""
|
|
154
|
+
hints: dict[str, str] = {}
|
|
155
|
+
for name in ("CLAUDE.md", "AGENTS.md", ".claude/CLAUDE.md"):
|
|
156
|
+
path = project / name
|
|
157
|
+
if not path.is_file():
|
|
158
|
+
continue
|
|
159
|
+
try:
|
|
160
|
+
text = path.read_text(encoding="utf-8")
|
|
161
|
+
except OSError:
|
|
162
|
+
continue
|
|
163
|
+
# Look for fenced or inline mvn/npm command examples
|
|
164
|
+
for key, pattern in (
|
|
165
|
+
("compile", r"(?:compile|编译)[^\n`]*`([^`]+)`"),
|
|
166
|
+
("unitTest", r"(?:unit.?test|单元测试)[^\n`]*`([^`]+)`"),
|
|
167
|
+
("package", r"(?:package|打包)[^\n`]*`([^`]+)`"),
|
|
168
|
+
("install", r"(?:install|安装依赖)[^\n`]*`([^`]+)`"),
|
|
169
|
+
):
|
|
170
|
+
m = re.search(pattern, text, re.IGNORECASE)
|
|
171
|
+
if m and key not in hints:
|
|
172
|
+
hints[key] = m.group(1).strip()
|
|
173
|
+
# Direct mvn lines
|
|
174
|
+
for m in re.finditer(r"`(mvn[^`]+)`", text):
|
|
175
|
+
cmd = m.group(1).strip()
|
|
176
|
+
if "compile" in cmd and "compile" not in hints:
|
|
177
|
+
hints["compile"] = cmd
|
|
178
|
+
elif re.search(r"\btest\b", cmd) and "unitTest" not in hints:
|
|
179
|
+
hints["unitTest"] = cmd
|
|
180
|
+
elif "package" in cmd and "package" not in hints:
|
|
181
|
+
hints["package"] = cmd
|
|
182
|
+
elif "install" in cmd and "install" not in hints:
|
|
183
|
+
hints["install"] = cmd
|
|
184
|
+
return hints
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def empty_profile_skeleton() -> dict[str, Any]:
|
|
188
|
+
return {
|
|
189
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
190
|
+
"detectedAt": "",
|
|
191
|
+
"toolPaths": {"node": "", "mvn": ""},
|
|
192
|
+
"buildCommands": dict(DEFAULT_BUILD_COMMANDS),
|
|
193
|
+
"verificationInputs": {},
|
|
194
|
+
"serviceStart": {
|
|
195
|
+
"command": "",
|
|
196
|
+
"healthUrl": "",
|
|
197
|
+
"startTimeoutSec": 120,
|
|
198
|
+
"inputFiles": [],
|
|
199
|
+
"profile": "",
|
|
200
|
+
"overlayPath": "",
|
|
201
|
+
},
|
|
202
|
+
"knownPreexistingErrors": [],
|
|
203
|
+
"shellQuirks": [],
|
|
204
|
+
"fingerprint": {"mvnVersion": "", "nodeVersion": "", "pomHash": ""},
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def merge_preserve_quirks(
|
|
209
|
+
base: dict[str, Any],
|
|
210
|
+
existing: dict[str, Any] | None,
|
|
211
|
+
) -> dict[str, Any]:
|
|
212
|
+
"""Keep human-curated quirks/errors/commands when re-detecting."""
|
|
213
|
+
if not existing:
|
|
214
|
+
return base
|
|
215
|
+
# Preserve knownPreexistingErrors / shellQuirks / filled buildCommands / serviceStart
|
|
216
|
+
if isinstance(existing.get("knownPreexistingErrors"), list):
|
|
217
|
+
base["knownPreexistingErrors"] = list(existing["knownPreexistingErrors"])
|
|
218
|
+
if isinstance(existing.get("shellQuirks"), list):
|
|
219
|
+
base["shellQuirks"] = list(existing["shellQuirks"])
|
|
220
|
+
existing_cmds = existing.get("buildCommands")
|
|
221
|
+
if isinstance(existing_cmds, dict):
|
|
222
|
+
for k, v in existing_cmds.items():
|
|
223
|
+
if isinstance(v, str) and v.strip():
|
|
224
|
+
base["buildCommands"][k] = v
|
|
225
|
+
existing_svc = existing.get("serviceStart")
|
|
226
|
+
if isinstance(existing_svc, dict):
|
|
227
|
+
merged_svc = dict(base.get("serviceStart") or {})
|
|
228
|
+
for k, v in existing_svc.items():
|
|
229
|
+
if v not in (None, ""):
|
|
230
|
+
merged_svc[k] = v
|
|
231
|
+
base["serviceStart"] = merged_svc
|
|
232
|
+
# 保留用户已配置的 verificationInputs(可能含 module 专属 glob),不覆盖。
|
|
233
|
+
if isinstance(existing.get("verificationInputs"), dict):
|
|
234
|
+
base["verificationInputs"] = dict(existing["verificationInputs"])
|
|
235
|
+
return base
|
|
236
|
+
|
|
237
|
+
|
|
238
|
+
def build_fingerprint(project: Path, node_path: str | None, mvn_path: str | None) -> dict[str, str]:
|
|
239
|
+
pom = find_root_pom(project)
|
|
240
|
+
pom_hash = sha256_file(pom) if pom else ""
|
|
241
|
+
return {
|
|
242
|
+
"mvnVersion": run_version(mvn_path, ["--version"]) if mvn_path else "",
|
|
243
|
+
"nodeVersion": run_version(node_path, ["--version"]) if node_path else "",
|
|
244
|
+
"pomHash": pom_hash or "",
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def current_pom_hash(project: Path) -> str:
|
|
249
|
+
pom = find_root_pom(project)
|
|
250
|
+
return (sha256_file(pom) if pom else "") or ""
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def cmd_detect(project: Path) -> dict[str, Any]:
|
|
254
|
+
project = project.resolve()
|
|
255
|
+
existing = load_existing_profile(project)
|
|
256
|
+
profile = empty_profile_skeleton()
|
|
257
|
+
|
|
258
|
+
node_path = which_tool("node")
|
|
259
|
+
mvn_path = which_tool("mvn")
|
|
260
|
+
profile["toolPaths"] = {
|
|
261
|
+
"node": node_path or "",
|
|
262
|
+
"mvn": mvn_path or "",
|
|
263
|
+
}
|
|
264
|
+
profile["fingerprint"] = build_fingerprint(project, node_path, mvn_path)
|
|
265
|
+
profile["detectedAt"] = now_iso()
|
|
266
|
+
|
|
267
|
+
hints = parse_claude_hints(project)
|
|
268
|
+
cmds = dict(DEFAULT_BUILD_COMMANDS)
|
|
269
|
+
cmds.update({k: v for k, v in hints.items() if k in cmds})
|
|
270
|
+
# Sensible Java placeholders when pom exists but no hints
|
|
271
|
+
if find_root_pom(project) and not any(cmds.values()):
|
|
272
|
+
cmds["compile"] = "mvn -f pom.xml compile -o -q"
|
|
273
|
+
cmds["unitTest"] = "mvn -f pom.xml test -Dtest={testClasses} -o"
|
|
274
|
+
cmds["unitTestFull"] = "mvn -f pom.xml test -o"
|
|
275
|
+
cmds["install"] = "mvn install -pl {modules} -am -DskipTests -nsu"
|
|
276
|
+
cmds["package"] = "mvn -f pom.xml package '-Dmaven.test.skip=true'"
|
|
277
|
+
profile["buildCommands"] = cmds
|
|
278
|
+
|
|
279
|
+
# Java 项目给 verificationInputs.unitTestFull 一个根级默认闭包
|
|
280
|
+
# (多 module 项目用户可改为 module/pom.xml + module/src/**)。通用/无 pom
|
|
281
|
+
# 项目保持空 {} —— can-reuse --profile-input unitTestFull 会返回
|
|
282
|
+
# insufficient-evidence,执行全量测试但不允许缓存复用,直到 profile 配置好。
|
|
283
|
+
if find_root_pom(project):
|
|
284
|
+
# A reactor-level Maven test can run every module, so its reuse
|
|
285
|
+
# fingerprint must include every module's pom and source roots.
|
|
286
|
+
module_poms = sorted(
|
|
287
|
+
pom.relative_to(project).as_posix()
|
|
288
|
+
for pom in project.rglob("pom.xml")
|
|
289
|
+
if pom.is_file() and ".harness" not in pom.parts and ".git" not in pom.parts
|
|
290
|
+
)
|
|
291
|
+
full_inputs: list[str] = []
|
|
292
|
+
for pom in module_poms:
|
|
293
|
+
full_inputs.append(pom)
|
|
294
|
+
parent = Path(pom).parent
|
|
295
|
+
prefix = "" if str(parent) == "." else f"{parent.as_posix()}/"
|
|
296
|
+
full_inputs.extend([f"{prefix}src/main/**", f"{prefix}src/test/**"])
|
|
297
|
+
profile["verificationInputs"] = {"unitTestFull": full_inputs}
|
|
298
|
+
|
|
299
|
+
profile = merge_preserve_quirks(profile, existing)
|
|
300
|
+
|
|
301
|
+
# Idempotent fingerprint/toolPaths overwrite from fresh detect;
|
|
302
|
+
# detectedAt updates each run (tests compare ignoring it).
|
|
303
|
+
out_path = project / PROFILE_REL
|
|
304
|
+
write_json(out_path, profile)
|
|
305
|
+
|
|
306
|
+
return {
|
|
307
|
+
"ok": True,
|
|
308
|
+
"action": "detect",
|
|
309
|
+
"project": str(project),
|
|
310
|
+
"profilePath": str(out_path),
|
|
311
|
+
"profile": profile,
|
|
312
|
+
"created": existing is None,
|
|
313
|
+
"updated": True,
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
|
|
317
|
+
def cmd_check(project: Path) -> dict[str, Any]:
|
|
318
|
+
"""Second-run fast check: existence + executability + fingerprint compare.
|
|
319
|
+
|
|
320
|
+
Does NOT run mvn/node full version probes (≤5s target).
|
|
321
|
+
"""
|
|
322
|
+
project = project.resolve()
|
|
323
|
+
profile_path = project / PROFILE_REL
|
|
324
|
+
issues: list[str] = []
|
|
325
|
+
stale = False
|
|
326
|
+
|
|
327
|
+
if not profile_path.is_file():
|
|
328
|
+
return {
|
|
329
|
+
"ok": False,
|
|
330
|
+
"hardFailure": True,
|
|
331
|
+
"action": "check",
|
|
332
|
+
"project": str(project),
|
|
333
|
+
"stale": True,
|
|
334
|
+
"issues": ["build-profile.json missing; run detect"],
|
|
335
|
+
"hint": "python harness_preflight.py detect --project <root> --json",
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
try:
|
|
339
|
+
profile = read_json(profile_path)
|
|
340
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
341
|
+
return {
|
|
342
|
+
"ok": False,
|
|
343
|
+
"hardFailure": True,
|
|
344
|
+
"action": "check",
|
|
345
|
+
"project": str(project),
|
|
346
|
+
"stale": True,
|
|
347
|
+
"issues": [f"build-profile.json unreadable: {exc}"],
|
|
348
|
+
"hint": "python harness_preflight.py detect --project <root> --json",
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if not isinstance(profile, dict):
|
|
352
|
+
return {
|
|
353
|
+
"ok": False,
|
|
354
|
+
"hardFailure": True,
|
|
355
|
+
"action": "check",
|
|
356
|
+
"project": str(project),
|
|
357
|
+
"stale": True,
|
|
358
|
+
"issues": ["build-profile.json is not an object"],
|
|
359
|
+
"hint": "python harness_preflight.py detect --project <root> --json",
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
tool_paths = profile.get("toolPaths") or {}
|
|
363
|
+
if not isinstance(tool_paths, dict):
|
|
364
|
+
tool_paths = {}
|
|
365
|
+
|
|
366
|
+
for tool_name in ("node", "mvn"):
|
|
367
|
+
path_str = tool_paths.get(tool_name) or ""
|
|
368
|
+
if not path_str:
|
|
369
|
+
# Empty is allowed (tool not present at detect time)
|
|
370
|
+
continue
|
|
371
|
+
if not is_executable_path(path_str):
|
|
372
|
+
stale = True
|
|
373
|
+
issues.append(f"toolPaths.{tool_name} missing or not executable: {path_str}")
|
|
374
|
+
|
|
375
|
+
fp = profile.get("fingerprint") or {}
|
|
376
|
+
if not isinstance(fp, dict):
|
|
377
|
+
fp = {}
|
|
378
|
+
stored_pom = fp.get("pomHash") or ""
|
|
379
|
+
current_pom = current_pom_hash(project)
|
|
380
|
+
if stored_pom != current_pom:
|
|
381
|
+
stale = True
|
|
382
|
+
issues.append(
|
|
383
|
+
f"fingerprint.pomHash changed: stored={stored_pom or '(empty)'} "
|
|
384
|
+
f"current={current_pom or '(empty)'}"
|
|
385
|
+
)
|
|
386
|
+
|
|
387
|
+
# If profile recorded a tool version but path is now gone → already covered.
|
|
388
|
+
# If pom appeared/disappeared relative to empty hash → covered above.
|
|
389
|
+
|
|
390
|
+
result = {
|
|
391
|
+
"ok": not stale,
|
|
392
|
+
"action": "check",
|
|
393
|
+
"project": str(project),
|
|
394
|
+
"stale": stale,
|
|
395
|
+
"issues": issues,
|
|
396
|
+
"fingerprint": {
|
|
397
|
+
"stored": {"pomHash": stored_pom, "mvnVersion": fp.get("mvnVersion", ""), "nodeVersion": fp.get("nodeVersion", "")},
|
|
398
|
+
"current": {"pomHash": current_pom},
|
|
399
|
+
},
|
|
400
|
+
}
|
|
401
|
+
if stale:
|
|
402
|
+
result["hint"] = "python harness_preflight.py detect --project <root> --json"
|
|
403
|
+
return result
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def ensure_pitfalls_appendix(project: Path) -> Path:
|
|
407
|
+
path = project / PITFALLS_REL
|
|
408
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
409
|
+
if not path.exists():
|
|
410
|
+
content = (
|
|
411
|
+
"# 项目避坑记录\n\n"
|
|
412
|
+
"> 由 harness_preflight.py record-quirk 自动维护附录。\n\n"
|
|
413
|
+
f"{PITFALLS_APPENDIX_HEADER}\n\n"
|
|
414
|
+
)
|
|
415
|
+
path.write_text(content, encoding="utf-8")
|
|
416
|
+
return path
|
|
417
|
+
text = path.read_text(encoding="utf-8")
|
|
418
|
+
if PITFALLS_APPENDIX_HEADER not in text:
|
|
419
|
+
if not text.endswith("\n"):
|
|
420
|
+
text += "\n"
|
|
421
|
+
text += f"\n{PITFALLS_APPENDIX_HEADER}\n\n"
|
|
422
|
+
path.write_text(text, encoding="utf-8")
|
|
423
|
+
return path
|
|
424
|
+
|
|
425
|
+
|
|
426
|
+
def append_pitfalls_line(project: Path, line: str) -> Path:
|
|
427
|
+
path = ensure_pitfalls_appendix(project)
|
|
428
|
+
text = path.read_text(encoding="utf-8")
|
|
429
|
+
if not text.endswith("\n"):
|
|
430
|
+
text += "\n"
|
|
431
|
+
# Append after appendix header block (end of file is fine for append-only)
|
|
432
|
+
text += line.rstrip() + "\n"
|
|
433
|
+
path.write_text(text, encoding="utf-8")
|
|
434
|
+
return path
|
|
435
|
+
|
|
436
|
+
|
|
437
|
+
def cmd_record_quirk(
|
|
438
|
+
project: Path,
|
|
439
|
+
pattern: str,
|
|
440
|
+
reason: str,
|
|
441
|
+
action: str,
|
|
442
|
+
fixed_command: str | None = None,
|
|
443
|
+
) -> dict[str, Any]:
|
|
444
|
+
project = project.resolve()
|
|
445
|
+
if action not in VALID_QUIRK_ACTIONS:
|
|
446
|
+
return {
|
|
447
|
+
"ok": False,
|
|
448
|
+
"action": "record-quirk",
|
|
449
|
+
"issues": [f"invalid action: {action}; expected one of {sorted(VALID_QUIRK_ACTIONS)}"],
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
profile_path = project / PROFILE_REL
|
|
453
|
+
if profile_path.is_file():
|
|
454
|
+
try:
|
|
455
|
+
profile = read_json(profile_path)
|
|
456
|
+
if not isinstance(profile, dict):
|
|
457
|
+
profile = empty_profile_skeleton()
|
|
458
|
+
except (OSError, json.JSONDecodeError):
|
|
459
|
+
profile = empty_profile_skeleton()
|
|
460
|
+
else:
|
|
461
|
+
profile = empty_profile_skeleton()
|
|
462
|
+
profile["detectedAt"] = now_iso()
|
|
463
|
+
|
|
464
|
+
profile.setdefault("knownPreexistingErrors", [])
|
|
465
|
+
profile.setdefault("shellQuirks", [])
|
|
466
|
+
profile.setdefault("buildCommands", dict(DEFAULT_BUILD_COMMANDS))
|
|
467
|
+
|
|
468
|
+
if not isinstance(profile["knownPreexistingErrors"], list):
|
|
469
|
+
profile["knownPreexistingErrors"] = []
|
|
470
|
+
if not isinstance(profile["shellQuirks"], list):
|
|
471
|
+
profile["shellQuirks"] = []
|
|
472
|
+
if not isinstance(profile["buildCommands"], dict):
|
|
473
|
+
profile["buildCommands"] = dict(DEFAULT_BUILD_COMMANDS)
|
|
474
|
+
|
|
475
|
+
changed: list[str] = []
|
|
476
|
+
|
|
477
|
+
if action == "skip-not-block":
|
|
478
|
+
entry = {"pattern": pattern, "reason": reason, "action": "skip-not-block"}
|
|
479
|
+
existing_patterns = {
|
|
480
|
+
e.get("pattern")
|
|
481
|
+
for e in profile["knownPreexistingErrors"]
|
|
482
|
+
if isinstance(e, dict)
|
|
483
|
+
}
|
|
484
|
+
if pattern not in existing_patterns:
|
|
485
|
+
profile["knownPreexistingErrors"].append(entry)
|
|
486
|
+
changed.append("knownPreexistingErrors")
|
|
487
|
+
else:
|
|
488
|
+
# Append-only: do not overwrite existing entry; still sync pitfalls
|
|
489
|
+
changed.append("knownPreexistingErrors(already-present)")
|
|
490
|
+
else: # fix-command
|
|
491
|
+
if pattern not in profile["shellQuirks"]:
|
|
492
|
+
profile["shellQuirks"].append(pattern)
|
|
493
|
+
changed.append("shellQuirks")
|
|
494
|
+
else:
|
|
495
|
+
changed.append("shellQuirks(already-present)")
|
|
496
|
+
if fixed_command:
|
|
497
|
+
# Known buildCommands key → update it; otherwise store under a
|
|
498
|
+
# namespaced custom key to avoid polluting the standard key space.
|
|
499
|
+
if pattern in DEFAULT_BUILD_COMMANDS:
|
|
500
|
+
key = pattern
|
|
501
|
+
else:
|
|
502
|
+
key = f"custom:{pattern}"
|
|
503
|
+
profile["buildCommands"][key] = fixed_command
|
|
504
|
+
changed.append(f"buildCommands.{key}")
|
|
505
|
+
|
|
506
|
+
write_json(profile_path, profile)
|
|
507
|
+
|
|
508
|
+
date_str = dt.date.today().isoformat()
|
|
509
|
+
pitfalls_line = f"- {date_str}: `{pattern}` — {reason} (action={action}"
|
|
510
|
+
if fixed_command:
|
|
511
|
+
pitfalls_line += f"; fixed-command=`{fixed_command}`"
|
|
512
|
+
pitfalls_line += ")"
|
|
513
|
+
pitfalls_path = append_pitfalls_line(project, pitfalls_line)
|
|
514
|
+
|
|
515
|
+
return {
|
|
516
|
+
"ok": True,
|
|
517
|
+
"action": "record-quirk",
|
|
518
|
+
"project": str(project),
|
|
519
|
+
"pattern": pattern,
|
|
520
|
+
"reason": reason,
|
|
521
|
+
"quirkAction": action,
|
|
522
|
+
"fixedCommand": fixed_command,
|
|
523
|
+
"changed": changed,
|
|
524
|
+
"profilePath": str(profile_path),
|
|
525
|
+
"pitfallsPath": str(pitfalls_path),
|
|
526
|
+
"pitfallsLine": pitfalls_line,
|
|
527
|
+
"profile": profile,
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
|
|
531
|
+
def parse_frontmatter(text: str) -> tuple[dict[str, Any] | None, str | None]:
|
|
532
|
+
"""Parse YAML-ish frontmatter between leading --- fences.
|
|
533
|
+
|
|
534
|
+
Returns (meta, error_reason). Supports simple scalars, lists, and
|
|
535
|
+
nested list items used by harness agent definitions.
|
|
536
|
+
"""
|
|
537
|
+
if not text.startswith("---"):
|
|
538
|
+
return None, "missing frontmatter start ---"
|
|
539
|
+
lines = text.splitlines()
|
|
540
|
+
if not lines or lines[0].strip() != "---":
|
|
541
|
+
return None, "missing frontmatter start ---"
|
|
542
|
+
end_idx = None
|
|
543
|
+
for i in range(1, len(lines)):
|
|
544
|
+
if lines[i].strip() == "---":
|
|
545
|
+
end_idx = i
|
|
546
|
+
break
|
|
547
|
+
if end_idx is None:
|
|
548
|
+
return None, "missing frontmatter end ---"
|
|
549
|
+
|
|
550
|
+
body = "\n".join(lines[1:end_idx])
|
|
551
|
+
meta: dict[str, Any] = {}
|
|
552
|
+
current_list_key: str | None = None
|
|
553
|
+
|
|
554
|
+
for raw in body.splitlines():
|
|
555
|
+
if not raw.strip():
|
|
556
|
+
continue
|
|
557
|
+
# list item under previous key
|
|
558
|
+
list_item = re.match(r"^-\s+(.*)$", raw)
|
|
559
|
+
if list_item and current_list_key:
|
|
560
|
+
meta.setdefault(current_list_key, [])
|
|
561
|
+
if not isinstance(meta[current_list_key], list):
|
|
562
|
+
meta[current_list_key] = []
|
|
563
|
+
meta[current_list_key].append(_parse_scalar(list_item.group(1).strip()))
|
|
564
|
+
continue
|
|
565
|
+
|
|
566
|
+
m = re.match(r"^([A-Za-z0-9_]+):\s*(.*)$", raw)
|
|
567
|
+
if not m:
|
|
568
|
+
current_list_key = None
|
|
569
|
+
continue
|
|
570
|
+
key, val = m.group(1), m.group(2).strip()
|
|
571
|
+
if val == "":
|
|
572
|
+
current_list_key = key
|
|
573
|
+
meta[key] = []
|
|
574
|
+
continue
|
|
575
|
+
current_list_key = None
|
|
576
|
+
if val.startswith("[") and val.endswith("]"):
|
|
577
|
+
inner = val[1:-1].strip()
|
|
578
|
+
if not inner:
|
|
579
|
+
meta[key] = []
|
|
580
|
+
else:
|
|
581
|
+
parts = _split_flow_list(inner)
|
|
582
|
+
meta[key] = [_parse_scalar(p.strip()) for p in parts]
|
|
583
|
+
else:
|
|
584
|
+
meta[key] = _parse_scalar(val)
|
|
585
|
+
return meta, None
|
|
586
|
+
|
|
587
|
+
|
|
588
|
+
def _split_flow_list(inner: str) -> list[str]:
|
|
589
|
+
"""Split YAML flow list respecting nested brackets like Bash(powershell.exe:*)."""
|
|
590
|
+
parts: list[str] = []
|
|
591
|
+
buf: list[str] = []
|
|
592
|
+
depth = 0
|
|
593
|
+
for ch in inner:
|
|
594
|
+
if ch == "[":
|
|
595
|
+
depth += 1
|
|
596
|
+
buf.append(ch)
|
|
597
|
+
elif ch == "]":
|
|
598
|
+
depth = max(0, depth - 1)
|
|
599
|
+
buf.append(ch)
|
|
600
|
+
elif ch == "(":
|
|
601
|
+
depth += 1
|
|
602
|
+
buf.append(ch)
|
|
603
|
+
elif ch == ")":
|
|
604
|
+
depth = max(0, depth - 1)
|
|
605
|
+
buf.append(ch)
|
|
606
|
+
elif ch == "," and depth == 0:
|
|
607
|
+
parts.append("".join(buf))
|
|
608
|
+
buf = []
|
|
609
|
+
else:
|
|
610
|
+
buf.append(ch)
|
|
611
|
+
if buf:
|
|
612
|
+
parts.append("".join(buf))
|
|
613
|
+
return parts
|
|
614
|
+
|
|
615
|
+
|
|
616
|
+
def _parse_scalar(val: str) -> Any:
|
|
617
|
+
if (val.startswith('"') and val.endswith('"')) or (val.startswith("'") and val.endswith("'")):
|
|
618
|
+
return val[1:-1]
|
|
619
|
+
if val.lower() in ("true", "false"):
|
|
620
|
+
return val.lower() == "true"
|
|
621
|
+
if re.fullmatch(r"-?\d+", val):
|
|
622
|
+
return int(val)
|
|
623
|
+
return val
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
def cmd_check_agents(skills_root: Path, agent: str) -> dict[str, Any]:
|
|
627
|
+
skills_root = skills_root.resolve()
|
|
628
|
+
agent_name = agent.strip()
|
|
629
|
+
if not agent_name:
|
|
630
|
+
return {
|
|
631
|
+
"ok": False,
|
|
632
|
+
"action": "check-agents",
|
|
633
|
+
"agent": agent,
|
|
634
|
+
"usable": False,
|
|
635
|
+
"reason": "agent name is empty",
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
# Accept bare name or with .md
|
|
639
|
+
filename = agent_name if agent_name.endswith(".md") else f"{agent_name}.md"
|
|
640
|
+
agent_path = skills_root / "agents" / filename
|
|
641
|
+
|
|
642
|
+
if not agent_path.is_file():
|
|
643
|
+
return {
|
|
644
|
+
"ok": False,
|
|
645
|
+
"action": "check-agents",
|
|
646
|
+
"agent": agent_name,
|
|
647
|
+
"path": str(agent_path),
|
|
648
|
+
"usable": False,
|
|
649
|
+
"reason": f"agent file not found: {agent_path}",
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
try:
|
|
653
|
+
text = agent_path.read_text(encoding="utf-8")
|
|
654
|
+
except OSError as exc:
|
|
655
|
+
return {
|
|
656
|
+
"ok": False,
|
|
657
|
+
"action": "check-agents",
|
|
658
|
+
"agent": agent_name,
|
|
659
|
+
"path": str(agent_path),
|
|
660
|
+
"usable": False,
|
|
661
|
+
"reason": f"cannot read agent file: {exc}",
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
meta, err = parse_frontmatter(text)
|
|
665
|
+
if err or meta is None:
|
|
666
|
+
return {
|
|
667
|
+
"ok": False,
|
|
668
|
+
"action": "check-agents",
|
|
669
|
+
"agent": agent_name,
|
|
670
|
+
"path": str(agent_path),
|
|
671
|
+
"usable": False,
|
|
672
|
+
"reason": f"frontmatter parse failed: {err or 'unknown'}",
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
tools = meta.get("tools")
|
|
676
|
+
if tools is None:
|
|
677
|
+
return {
|
|
678
|
+
"ok": False,
|
|
679
|
+
"action": "check-agents",
|
|
680
|
+
"agent": agent_name,
|
|
681
|
+
"path": str(agent_path),
|
|
682
|
+
"usable": False,
|
|
683
|
+
"reason": "tools declaration missing",
|
|
684
|
+
"frontmatter": meta,
|
|
685
|
+
}
|
|
686
|
+
if not isinstance(tools, list) or len(tools) == 0:
|
|
687
|
+
return {
|
|
688
|
+
"ok": False,
|
|
689
|
+
"action": "check-agents",
|
|
690
|
+
"agent": agent_name,
|
|
691
|
+
"path": str(agent_path),
|
|
692
|
+
"usable": False,
|
|
693
|
+
"reason": "tools declaration empty or invalid",
|
|
694
|
+
"frontmatter": meta,
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
name_field = meta.get("name")
|
|
698
|
+
return {
|
|
699
|
+
"ok": True,
|
|
700
|
+
"action": "check-agents",
|
|
701
|
+
"agent": agent_name,
|
|
702
|
+
"path": str(agent_path),
|
|
703
|
+
"usable": True,
|
|
704
|
+
"reason": "agent file exists; frontmatter parsed; tools declared",
|
|
705
|
+
"name": name_field,
|
|
706
|
+
"tools": tools,
|
|
707
|
+
"frontmatterKeys": sorted(meta.keys()),
|
|
708
|
+
}
|
|
709
|
+
|
|
710
|
+
|
|
711
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
712
|
+
parser = argparse.ArgumentParser(
|
|
713
|
+
description="Harness preflight: build-profile + agent availability checks.",
|
|
714
|
+
)
|
|
715
|
+
parser.add_argument(
|
|
716
|
+
"--json",
|
|
717
|
+
action="store_true",
|
|
718
|
+
help="Emit machine-readable JSON (always on for subcommands; kept for contract).",
|
|
719
|
+
)
|
|
720
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
721
|
+
|
|
722
|
+
p_detect = sub.add_parser("detect", help="Full probe and write build-profile.json")
|
|
723
|
+
p_detect.add_argument("--project", required=True, type=Path)
|
|
724
|
+
p_detect.add_argument("--json", action="store_true")
|
|
725
|
+
|
|
726
|
+
p_check = sub.add_parser("check", help="Fast stale check of build-profile")
|
|
727
|
+
p_check.add_argument("--project", required=True, type=Path)
|
|
728
|
+
p_check.add_argument("--json", action="store_true")
|
|
729
|
+
|
|
730
|
+
p_quirk = sub.add_parser("record-quirk", help="Append quirk without overwriting peers")
|
|
731
|
+
p_quirk.add_argument("--project", required=True, type=Path)
|
|
732
|
+
p_quirk.add_argument("--pattern", required=True)
|
|
733
|
+
p_quirk.add_argument("--reason", required=True)
|
|
734
|
+
p_quirk.add_argument(
|
|
735
|
+
"--action",
|
|
736
|
+
required=True,
|
|
737
|
+
choices=sorted(VALID_QUIRK_ACTIONS),
|
|
738
|
+
)
|
|
739
|
+
p_quirk.add_argument("--fixed-command", default=None)
|
|
740
|
+
p_quirk.add_argument("--json", action="store_true")
|
|
741
|
+
|
|
742
|
+
p_agents = sub.add_parser("check-agents", help="Validate agent definition usability")
|
|
743
|
+
p_agents.add_argument("--skills-root", required=True, type=Path)
|
|
744
|
+
p_agents.add_argument("--agent", required=True)
|
|
745
|
+
p_agents.add_argument("--json", action="store_true")
|
|
746
|
+
|
|
747
|
+
return parser
|
|
748
|
+
|
|
749
|
+
|
|
750
|
+
def main(argv: list[str] | None = None) -> int:
|
|
751
|
+
parser = build_parser()
|
|
752
|
+
args = parser.parse_args(argv)
|
|
753
|
+
|
|
754
|
+
if args.command == "detect":
|
|
755
|
+
result = cmd_detect(args.project)
|
|
756
|
+
return emit_json(result, ok=bool(result.get("ok", True)))
|
|
757
|
+
if args.command == "check":
|
|
758
|
+
result = cmd_check(args.project)
|
|
759
|
+
# stale=true with a readable profile still exits 0 (callers branch on JSON);
|
|
760
|
+
# hard failures (profile missing / unreadable / not an object) exit 1.
|
|
761
|
+
return emit_json(result, ok=not result.get("hardFailure", False))
|
|
762
|
+
if args.command == "record-quirk":
|
|
763
|
+
result = cmd_record_quirk(
|
|
764
|
+
args.project,
|
|
765
|
+
pattern=args.pattern,
|
|
766
|
+
reason=args.reason,
|
|
767
|
+
action=args.action,
|
|
768
|
+
fixed_command=args.fixed_command,
|
|
769
|
+
)
|
|
770
|
+
return emit_json(result, ok=bool(result.get("ok", False)))
|
|
771
|
+
if args.command == "check-agents":
|
|
772
|
+
result = cmd_check_agents(args.skills_root, args.agent)
|
|
773
|
+
return emit_json(result, ok=True)
|
|
774
|
+
|
|
775
|
+
parser.error(f"unknown command: {args.command}")
|
|
776
|
+
return 2
|
|
777
|
+
|
|
778
|
+
|
|
779
|
+
if __name__ == "__main__":
|
|
780
|
+
raise SystemExit(main())
|