@hunter-harness/workflow-harness 0.2.39 → 0.2.41
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/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/reference.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/general/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/general/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/general/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/general/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/general/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/general/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/reference.md +16 -16
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/claude-code/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/claude-code/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/claude-code/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/claude-code/scripts/harness_service.py +291 -22
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codebuddy/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codebuddy/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codebuddy/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/reference.md +16 -16
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/codex/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/codex/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/codex/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/codex/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/codex/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/codex/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/codex/scripts/harness_service.py +291 -22
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +405 -25
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/checklist.md +3 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +22 -0
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/reference.md +16 -16
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +10 -0
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +12 -0
- package/harness/bundles/java/cursor/protocols/powershell-protocol.md +21 -0
- package/harness/bundles/java/cursor/protocols/state-layout-protocol.md +11 -0
- package/harness/bundles/java/cursor/scripts/harness_archive.py +413 -2
- package/harness/bundles/java/cursor/scripts/harness_deploy.py +10 -51
- package/harness/bundles/java/cursor/scripts/harness_environment.py +556 -39
- package/harness/bundles/java/cursor/scripts/harness_gate.py +145 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +581 -55
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -6
- package/harness/bundles/java/cursor/scripts/harness_service.py +291 -22
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +405 -25
- package/harness/manifests/general/claude-code.json +29 -29
- package/harness/manifests/general/codebuddy.json +29 -29
- package/harness/manifests/general/codex.json +29 -29
- package/harness/manifests/general/cursor.json +29 -29
- package/harness/manifests/java/claude-code.json +30 -30
- package/harness/manifests/java/codebuddy.json +30 -30
- package/harness/manifests/java/codex.json +30 -30
- package/harness/manifests/java/cursor.json +30 -30
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -24,6 +24,12 @@ from dataclasses import dataclass
|
|
|
24
24
|
from pathlib import Path
|
|
25
25
|
from typing import Mapping, Sequence
|
|
26
26
|
|
|
27
|
+
SCRIPT_DIR = Path(__file__).resolve().parent
|
|
28
|
+
if str(SCRIPT_DIR) not in sys.path:
|
|
29
|
+
sys.path.insert(0, str(SCRIPT_DIR))
|
|
30
|
+
|
|
31
|
+
import harness_environment as henv
|
|
32
|
+
|
|
27
33
|
DEFAULT_MAX_WORKERS = 2
|
|
28
34
|
DEFAULT_TIMEOUT_SECONDS = 300.0
|
|
29
35
|
RESOURCE_INTENSIVE_PROFILES = frozenset({"system", "full"})
|
|
@@ -161,6 +167,115 @@ def _pid_is_running(pid: int) -> bool:
|
|
|
161
167
|
return True
|
|
162
168
|
|
|
163
169
|
|
|
170
|
+
def classify_test_lock(path: Path) -> dict[str, object]:
|
|
171
|
+
"""Classify one lock without mutating it or terminating a process."""
|
|
172
|
+
try:
|
|
173
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
174
|
+
except FileNotFoundError:
|
|
175
|
+
return {"classification": "ABSENT", "path": str(path)}
|
|
176
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
177
|
+
return {
|
|
178
|
+
"classification": "LOCK_UNREADABLE",
|
|
179
|
+
"path": str(path),
|
|
180
|
+
"message": str(exc),
|
|
181
|
+
}
|
|
182
|
+
if not isinstance(payload, dict):
|
|
183
|
+
return {"classification": "LOCK_UNREADABLE", "path": str(path)}
|
|
184
|
+
owner = payload.get("owner")
|
|
185
|
+
expires = payload.get("expiresAtUnix")
|
|
186
|
+
if not isinstance(expires, (int, float)) or isinstance(expires, bool):
|
|
187
|
+
return {
|
|
188
|
+
"classification": "OWNER_IDENTITY_INCOMPLETE",
|
|
189
|
+
"path": str(path),
|
|
190
|
+
"token": payload.get("token"),
|
|
191
|
+
}
|
|
192
|
+
if time.time() < float(expires):
|
|
193
|
+
return {
|
|
194
|
+
"classification": "ACTIVE",
|
|
195
|
+
"path": str(path),
|
|
196
|
+
"token": payload.get("token"),
|
|
197
|
+
"owner": owner,
|
|
198
|
+
}
|
|
199
|
+
if not isinstance(owner, dict) or any(
|
|
200
|
+
not owner.get(field) for field in ("pid", "executable", "startedAt")
|
|
201
|
+
):
|
|
202
|
+
return {
|
|
203
|
+
"classification": "OWNER_IDENTITY_INCOMPLETE",
|
|
204
|
+
"path": str(path),
|
|
205
|
+
"token": payload.get("token"),
|
|
206
|
+
}
|
|
207
|
+
try:
|
|
208
|
+
owner_pid = int(owner["pid"])
|
|
209
|
+
except (TypeError, ValueError):
|
|
210
|
+
return {
|
|
211
|
+
"classification": "OWNER_IDENTITY_INCOMPLETE",
|
|
212
|
+
"path": str(path),
|
|
213
|
+
"token": payload.get("token"),
|
|
214
|
+
}
|
|
215
|
+
if not _pid_is_running(owner_pid):
|
|
216
|
+
return {
|
|
217
|
+
"classification": "RECLAIMABLE",
|
|
218
|
+
"path": str(path),
|
|
219
|
+
"token": payload.get("token"),
|
|
220
|
+
"owner": owner,
|
|
221
|
+
}
|
|
222
|
+
verified = henv.hservice.verify_process_identity(
|
|
223
|
+
{
|
|
224
|
+
"pid": owner_pid,
|
|
225
|
+
"startedAt": owner["startedAt"],
|
|
226
|
+
"processIdentity": {"executable": owner["executable"]},
|
|
227
|
+
}
|
|
228
|
+
)
|
|
229
|
+
classification = (
|
|
230
|
+
"OWNER_ACTIVE"
|
|
231
|
+
if verified is True
|
|
232
|
+
else (
|
|
233
|
+
"OWNER_IDENTITY_UNCONFIRMED"
|
|
234
|
+
if verified is None
|
|
235
|
+
else "OWNER_IDENTITY_MISMATCH"
|
|
236
|
+
)
|
|
237
|
+
)
|
|
238
|
+
return {
|
|
239
|
+
"classification": classification,
|
|
240
|
+
"path": str(path),
|
|
241
|
+
"token": payload.get("token"),
|
|
242
|
+
"owner": owner,
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def reap_test_lock(path: Path) -> dict[str, object]:
|
|
247
|
+
"""Reap only an expired lock whose exact recorded owner is absent."""
|
|
248
|
+
classification = classify_test_lock(path)
|
|
249
|
+
reaped = classification.get("classification") == "RECLAIMABLE"
|
|
250
|
+
if reaped:
|
|
251
|
+
try:
|
|
252
|
+
path.unlink()
|
|
253
|
+
except FileNotFoundError:
|
|
254
|
+
pass
|
|
255
|
+
receipt = {
|
|
256
|
+
"schemaVersion": 1,
|
|
257
|
+
"action": "test-run-lock-reap",
|
|
258
|
+
"status": "REAPED",
|
|
259
|
+
"completedAtUnix": time.time(),
|
|
260
|
+
"lockPath": str(path),
|
|
261
|
+
"prior": classification,
|
|
262
|
+
}
|
|
263
|
+
receipt_dir = path.parent / "receipts"
|
|
264
|
+
receipt_dir.mkdir(parents=True, exist_ok=True)
|
|
265
|
+
target = receipt_dir / f"{path.stem}-reap-{uuid.uuid4().hex}.json"
|
|
266
|
+
target.write_text(
|
|
267
|
+
json.dumps(receipt, ensure_ascii=False, indent=2) + "\n",
|
|
268
|
+
encoding="utf-8",
|
|
269
|
+
newline="\n",
|
|
270
|
+
)
|
|
271
|
+
return {
|
|
272
|
+
"ok": True,
|
|
273
|
+
"code": "TEST_RUN_LOCK_REAPED" if reaped else "TEST_RUN_LOCK_REPORT_ONLY",
|
|
274
|
+
"reaped": reaped,
|
|
275
|
+
"lock": classification,
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
|
|
164
279
|
class TestRunLock:
|
|
165
280
|
"""Atomic per-project single-instance lock with stale-owner recovery."""
|
|
166
281
|
|
|
@@ -182,6 +297,7 @@ class TestRunLock:
|
|
|
182
297
|
self.path = root / f"{digest}.lock"
|
|
183
298
|
self.token = uuid.uuid4().hex
|
|
184
299
|
self._owned = False
|
|
300
|
+
self._ttl_seconds = 3600.0
|
|
185
301
|
|
|
186
302
|
def _owner(self) -> dict[str, object]:
|
|
187
303
|
try:
|
|
@@ -199,34 +315,42 @@ class TestRunLock:
|
|
|
199
315
|
0o600,
|
|
200
316
|
)
|
|
201
317
|
except FileExistsError:
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
owner_pid = int(owner.get("pid", 0))
|
|
205
|
-
except (TypeError, ValueError):
|
|
206
|
-
owner_pid = 0
|
|
207
|
-
try:
|
|
208
|
-
age_seconds = time.time() - self.path.stat().st_mtime
|
|
209
|
-
except OSError:
|
|
210
|
-
age_seconds = 0
|
|
211
|
-
if _pid_is_running(owner_pid) or age_seconds < 30:
|
|
318
|
+
classification = classify_test_lock(self.path)
|
|
319
|
+
if classification.get("classification") != "RECLAIMABLE":
|
|
212
320
|
raise TestRunAlreadyActive(
|
|
213
321
|
"TEST_RUN_ALREADY_ACTIVE: "
|
|
214
|
-
f"lock={self.path}
|
|
322
|
+
f"lock={self.path} "
|
|
323
|
+
f"classification={classification.get('classification')}"
|
|
215
324
|
)
|
|
216
|
-
|
|
217
|
-
self.path.unlink()
|
|
218
|
-
except FileNotFoundError:
|
|
219
|
-
pass
|
|
325
|
+
reap_test_lock(self.path)
|
|
220
326
|
if attempt == 0:
|
|
221
327
|
continue
|
|
222
328
|
raise TestRunAlreadyActive(
|
|
223
329
|
f"TEST_RUN_ALREADY_ACTIVE: could not reclaim {self.path}"
|
|
224
330
|
)
|
|
225
331
|
else:
|
|
332
|
+
now = time.time()
|
|
333
|
+
created = henv.hservice.get_process_create_time(os.getpid())
|
|
334
|
+
executable = (
|
|
335
|
+
henv.hservice.get_process_executable(os.getpid())
|
|
336
|
+
or str(Path(sys.executable).resolve())
|
|
337
|
+
)
|
|
226
338
|
payload = {
|
|
227
|
-
"
|
|
339
|
+
"schemaVersion": 2,
|
|
228
340
|
"token": self.token,
|
|
229
|
-
"
|
|
341
|
+
"status": "ACTIVE",
|
|
342
|
+
"createdAtUnix": now,
|
|
343
|
+
"heartbeatAtUnix": now,
|
|
344
|
+
"expiresAtUnix": now + self._ttl_seconds,
|
|
345
|
+
"owner": {
|
|
346
|
+
"pid": os.getpid(),
|
|
347
|
+
"executable": executable,
|
|
348
|
+
"startedAt": (
|
|
349
|
+
created.isoformat(timespec="seconds")
|
|
350
|
+
if created is not None
|
|
351
|
+
else ""
|
|
352
|
+
),
|
|
353
|
+
},
|
|
230
354
|
}
|
|
231
355
|
with os.fdopen(descriptor, "w", encoding="utf-8") as stream:
|
|
232
356
|
json.dump(payload, stream, ensure_ascii=False)
|
|
@@ -235,11 +359,49 @@ class TestRunLock:
|
|
|
235
359
|
return
|
|
236
360
|
raise TestRunAlreadyActive(f"TEST_RUN_ALREADY_ACTIVE: {self.path}")
|
|
237
361
|
|
|
238
|
-
def
|
|
362
|
+
def heartbeat(self) -> None:
|
|
363
|
+
if not self._owned:
|
|
364
|
+
return
|
|
365
|
+
owner = self._owner()
|
|
366
|
+
if owner.get("token") != self.token:
|
|
367
|
+
raise TestRunAlreadyActive(
|
|
368
|
+
f"TEST_RUN_LOCK_OWNERSHIP_LOST: lock={self.path}"
|
|
369
|
+
)
|
|
370
|
+
now = time.time()
|
|
371
|
+
owner["heartbeatAtUnix"] = now
|
|
372
|
+
owner["expiresAtUnix"] = now + self._ttl_seconds
|
|
373
|
+
with self.path.open("w", encoding="utf-8", newline="\n") as stream:
|
|
374
|
+
json.dump(owner, stream, ensure_ascii=False)
|
|
375
|
+
stream.flush()
|
|
376
|
+
os.fsync(stream.fileno())
|
|
377
|
+
|
|
378
|
+
def _write_closeout_receipt(self, *, status: str) -> None:
|
|
379
|
+
receipt = {
|
|
380
|
+
"schemaVersion": 1,
|
|
381
|
+
"action": "test-run-lock-closeout",
|
|
382
|
+
"status": status,
|
|
383
|
+
"token": self.token,
|
|
384
|
+
"lockPath": str(self.path),
|
|
385
|
+
"closedAtUnix": time.time(),
|
|
386
|
+
"ownerPid": os.getpid(),
|
|
387
|
+
}
|
|
388
|
+
receipt_dir = self.path.parent / "receipts"
|
|
389
|
+
receipt_dir.mkdir(parents=True, exist_ok=True)
|
|
390
|
+
target = receipt_dir / f"{self.path.stem}-{self.token}.json"
|
|
391
|
+
temporary = target.with_name(f".{target.name}.{uuid.uuid4().hex}.tmp")
|
|
392
|
+
temporary.write_text(
|
|
393
|
+
json.dumps(receipt, ensure_ascii=False, indent=2) + "\n",
|
|
394
|
+
encoding="utf-8",
|
|
395
|
+
newline="\n",
|
|
396
|
+
)
|
|
397
|
+
os.replace(temporary, target)
|
|
398
|
+
|
|
399
|
+
def release(self, *, status: str = "COMPLETED") -> None:
|
|
239
400
|
if not self._owned:
|
|
240
401
|
return
|
|
241
402
|
owner = self._owner()
|
|
242
403
|
if owner.get("token") == self.token:
|
|
404
|
+
self._write_closeout_receipt(status=status)
|
|
243
405
|
try:
|
|
244
406
|
self.path.unlink()
|
|
245
407
|
except FileNotFoundError:
|
|
@@ -251,7 +413,7 @@ class TestRunLock:
|
|
|
251
413
|
return self
|
|
252
414
|
|
|
253
415
|
def __exit__(self, exc_type, exc, traceback) -> None:
|
|
254
|
-
self.release()
|
|
416
|
+
self.release(status="ABNORMAL" if exc_type is not None else "COMPLETED")
|
|
255
417
|
|
|
256
418
|
|
|
257
419
|
def lower_process_priority() -> bool:
|
|
@@ -709,6 +871,81 @@ def _execution_environment(profile: str, max_workers: int) -> dict[str, str]:
|
|
|
709
871
|
}
|
|
710
872
|
|
|
711
873
|
|
|
874
|
+
def load_argv_envelope(path: Path) -> tuple[list[str], dict[str, str]]:
|
|
875
|
+
"""Load a shell-neutral UTF-8 argument file without reparsing command text."""
|
|
876
|
+
try:
|
|
877
|
+
raw = json.loads(path.read_text(encoding="utf-8-sig"))
|
|
878
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
879
|
+
raise ValueError(f"argument file is unreadable: {exc}") from exc
|
|
880
|
+
argv = raw.get("argv") if isinstance(raw, dict) else None
|
|
881
|
+
powershell = raw.get("powershell") if isinstance(raw, dict) else None
|
|
882
|
+
if (
|
|
883
|
+
not isinstance(raw, dict)
|
|
884
|
+
or raw.get("schemaVersion") != 1
|
|
885
|
+
or raw.get("transport") != "utf8-json-argument-file"
|
|
886
|
+
or not isinstance(argv, list)
|
|
887
|
+
or not argv
|
|
888
|
+
or any(not isinstance(item, str) or "\0" in item for item in argv)
|
|
889
|
+
or not isinstance(powershell, dict)
|
|
890
|
+
):
|
|
891
|
+
raise ValueError(
|
|
892
|
+
"argument file must declare schemaVersion=1, "
|
|
893
|
+
"transport=utf8-json-argument-file, argv[], and powershell metadata"
|
|
894
|
+
)
|
|
895
|
+
shell = {
|
|
896
|
+
field: str(powershell.get(field) or "").strip()
|
|
897
|
+
for field in ("edition", "version")
|
|
898
|
+
}
|
|
899
|
+
if not all(shell.values()):
|
|
900
|
+
raise ValueError("argument file powershell edition/version are required")
|
|
901
|
+
return list(argv), shell
|
|
902
|
+
|
|
903
|
+
|
|
904
|
+
def _is_persistent_service_command(command: Sequence[str]) -> bool:
|
|
905
|
+
"""Recognize formal service-manager launches that must outlive this runner."""
|
|
906
|
+
normalized = [str(item).replace("\\", "/").lower() for item in command]
|
|
907
|
+
return "ensure" in normalized and any(
|
|
908
|
+
item.rsplit("/", 1)[-1] == "harness_service.py" for item in normalized
|
|
909
|
+
)
|
|
910
|
+
|
|
911
|
+
|
|
912
|
+
def _write_exec_runtime_receipt(
|
|
913
|
+
path: Path,
|
|
914
|
+
*,
|
|
915
|
+
command: Sequence[str],
|
|
916
|
+
powershell: Mapping[str, str],
|
|
917
|
+
result: CommandResult,
|
|
918
|
+
) -> None:
|
|
919
|
+
canonical_argv = json.dumps(
|
|
920
|
+
list(command),
|
|
921
|
+
ensure_ascii=False,
|
|
922
|
+
separators=(",", ":"),
|
|
923
|
+
).encode("utf-8")
|
|
924
|
+
payload = {
|
|
925
|
+
"schemaVersion": 1,
|
|
926
|
+
"action": "managed-exec",
|
|
927
|
+
"transport": "utf8-json-argument-file",
|
|
928
|
+
"completedAtEpochSeconds": int(time.time()),
|
|
929
|
+
"argvHash": "sha256:" + hashlib.sha256(canonical_argv).hexdigest(),
|
|
930
|
+
"argumentCount": len(command),
|
|
931
|
+
"powershell": {
|
|
932
|
+
"edition": str(powershell["edition"]),
|
|
933
|
+
"version": str(powershell["version"]),
|
|
934
|
+
},
|
|
935
|
+
"returncode": result.returncode,
|
|
936
|
+
"timedOut": result.timed_out,
|
|
937
|
+
"processTreeIsolated": result.process_tree_isolated,
|
|
938
|
+
}
|
|
939
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
940
|
+
temporary = path.with_name(f".{path.name}.{uuid.uuid4().hex}.tmp")
|
|
941
|
+
temporary.write_text(
|
|
942
|
+
json.dumps(payload, ensure_ascii=False, indent=2) + "\n",
|
|
943
|
+
encoding="utf-8",
|
|
944
|
+
newline="\n",
|
|
945
|
+
)
|
|
946
|
+
os.replace(temporary, path)
|
|
947
|
+
|
|
948
|
+
|
|
712
949
|
def _print_plan(profile: str, plan: Sequence[TestModule]) -> None:
|
|
713
950
|
payload = {
|
|
714
951
|
"profile": profile,
|
|
@@ -757,8 +994,9 @@ def _run_unittest(args: argparse.Namespace) -> int:
|
|
|
757
994
|
detached_service_modules = set(args.detached_service_module)
|
|
758
995
|
detached_capability_checked = False
|
|
759
996
|
try:
|
|
760
|
-
with TestRunLock(project):
|
|
997
|
+
with TestRunLock(project) as run_lock:
|
|
761
998
|
for index, module in enumerate(plan, start=1):
|
|
999
|
+
run_lock.heartbeat()
|
|
762
1000
|
if (
|
|
763
1001
|
module.name in detached_service_modules
|
|
764
1002
|
and not detached_capability_checked
|
|
@@ -800,6 +1038,7 @@ def _run_unittest(args: argparse.Namespace) -> int:
|
|
|
800
1038
|
capture_output=args.verbosity == 0,
|
|
801
1039
|
allow_detached_processes=module.name in detached_service_modules,
|
|
802
1040
|
)
|
|
1041
|
+
run_lock.heartbeat()
|
|
803
1042
|
executed_count += 1
|
|
804
1043
|
module_failure_count = len(failures)
|
|
805
1044
|
if result.timed_out:
|
|
@@ -837,11 +1076,45 @@ def _run_unittest(args: argparse.Namespace) -> int:
|
|
|
837
1076
|
|
|
838
1077
|
def _run_exec(args: argparse.Namespace) -> int:
|
|
839
1078
|
command = list(args.command)
|
|
840
|
-
|
|
1079
|
+
powershell: dict[str, str] | None = None
|
|
1080
|
+
if args.runtime_receipt and not args.argv_file:
|
|
1081
|
+
print(
|
|
1082
|
+
"RUNTIME_RECEIPT_REQUIRES_ARGUMENT_FILE: "
|
|
1083
|
+
"--runtime-receipt requires --argv-file",
|
|
1084
|
+
file=sys.stderr,
|
|
1085
|
+
)
|
|
1086
|
+
return 2
|
|
1087
|
+
if args.argv_file:
|
|
1088
|
+
if command:
|
|
1089
|
+
print(
|
|
1090
|
+
"ARGUMENT_TRANSPORT_CONFLICT: use either --argv-file or a command",
|
|
1091
|
+
file=sys.stderr,
|
|
1092
|
+
)
|
|
1093
|
+
return 2
|
|
1094
|
+
argv_file = Path(args.argv_file)
|
|
1095
|
+
project_for_path = Path(args.project).resolve()
|
|
1096
|
+
if not argv_file.is_absolute():
|
|
1097
|
+
argv_file = project_for_path / argv_file
|
|
1098
|
+
try:
|
|
1099
|
+
command, powershell = load_argv_envelope(argv_file)
|
|
1100
|
+
except ValueError as exc:
|
|
1101
|
+
print(f"ARGUMENT_FILE_INVALID: {exc}", file=sys.stderr)
|
|
1102
|
+
return 2
|
|
1103
|
+
elif command and command[0] == "--":
|
|
841
1104
|
command = command[1:]
|
|
842
1105
|
if not command:
|
|
843
|
-
print(
|
|
1106
|
+
print(
|
|
1107
|
+
"managed exec requires a command after -- or --argv-file",
|
|
1108
|
+
file=sys.stderr,
|
|
1109
|
+
)
|
|
844
1110
|
return 2
|
|
1111
|
+
if _is_persistent_service_command(command):
|
|
1112
|
+
print(
|
|
1113
|
+
"PERSISTENT_SERVICE_MODE_REQUIRED: invoke harness_service.py ensure "
|
|
1114
|
+
"outside the bounded test runner",
|
|
1115
|
+
file=sys.stderr,
|
|
1116
|
+
)
|
|
1117
|
+
return 7
|
|
845
1118
|
if not resource_profile_allowed(
|
|
846
1119
|
args.profile,
|
|
847
1120
|
args.confirm_resource_intensive,
|
|
@@ -855,8 +1128,46 @@ def _run_exec(args: argparse.Namespace) -> int:
|
|
|
855
1128
|
return 2
|
|
856
1129
|
project = Path(args.project).resolve()
|
|
857
1130
|
environment = _execution_environment(args.profile, args.max_workers)
|
|
1131
|
+
required_environment = list(args.required_environment_field or [])
|
|
1132
|
+
if required_environment and not args.environment_receipt:
|
|
1133
|
+
print(
|
|
1134
|
+
json.dumps(
|
|
1135
|
+
{
|
|
1136
|
+
"ok": False,
|
|
1137
|
+
"code": "VERIFICATION_ENVIRONMENT_INCOMPLETE",
|
|
1138
|
+
"missing": sorted(set(required_environment)),
|
|
1139
|
+
"changed": [],
|
|
1140
|
+
},
|
|
1141
|
+
ensure_ascii=False,
|
|
1142
|
+
),
|
|
1143
|
+
file=sys.stderr,
|
|
1144
|
+
)
|
|
1145
|
+
return 6
|
|
1146
|
+
if args.environment_receipt:
|
|
1147
|
+
receipt_path = Path(args.environment_receipt)
|
|
1148
|
+
if not receipt_path.is_absolute():
|
|
1149
|
+
receipt_path = project / receipt_path
|
|
1150
|
+
try:
|
|
1151
|
+
receipt = json.loads(receipt_path.read_text(encoding="utf-8-sig"))
|
|
1152
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1153
|
+
print(
|
|
1154
|
+
"VERIFICATION_ENVIRONMENT_INCOMPLETE: "
|
|
1155
|
+
f"environment receipt is unreadable: {exc}",
|
|
1156
|
+
file=sys.stderr,
|
|
1157
|
+
)
|
|
1158
|
+
return 6
|
|
1159
|
+
resolved = henv.resolve_verification_environment(
|
|
1160
|
+
receipt,
|
|
1161
|
+
required_fields=required_environment,
|
|
1162
|
+
source_environment=dict(os.environ),
|
|
1163
|
+
)
|
|
1164
|
+
if not resolved.get("ok"):
|
|
1165
|
+
print(json.dumps(resolved, ensure_ascii=False), file=sys.stderr)
|
|
1166
|
+
return 6
|
|
1167
|
+
environment.update(resolved["environment"])
|
|
858
1168
|
try:
|
|
859
|
-
with TestRunLock(project):
|
|
1169
|
+
with TestRunLock(project) as run_lock:
|
|
1170
|
+
run_lock.heartbeat()
|
|
860
1171
|
if args.allow_detached_processes:
|
|
861
1172
|
available, detail = detached_process_capability(project)
|
|
862
1173
|
if not available:
|
|
@@ -874,6 +1185,7 @@ def _run_exec(args: argparse.Namespace) -> int:
|
|
|
874
1185
|
environ=environment,
|
|
875
1186
|
allow_detached_processes=args.allow_detached_processes,
|
|
876
1187
|
)
|
|
1188
|
+
run_lock.heartbeat()
|
|
877
1189
|
except TestRunAlreadyActive as exc:
|
|
878
1190
|
print(str(exc), file=sys.stderr)
|
|
879
1191
|
return 3
|
|
@@ -886,9 +1198,42 @@ def _run_exec(args: argparse.Namespace) -> int:
|
|
|
886
1198
|
file=sys.stderr,
|
|
887
1199
|
)
|
|
888
1200
|
return 124
|
|
1201
|
+
if args.runtime_receipt:
|
|
1202
|
+
assert powershell is not None
|
|
1203
|
+
receipt_path = Path(args.runtime_receipt)
|
|
1204
|
+
if not receipt_path.is_absolute():
|
|
1205
|
+
receipt_path = project / receipt_path
|
|
1206
|
+
_write_exec_runtime_receipt(
|
|
1207
|
+
receipt_path,
|
|
1208
|
+
command=command,
|
|
1209
|
+
powershell=powershell,
|
|
1210
|
+
result=result,
|
|
1211
|
+
)
|
|
889
1212
|
return result.returncode
|
|
890
1213
|
|
|
891
1214
|
|
|
1215
|
+
def _run_lock_status(args: argparse.Namespace) -> int:
|
|
1216
|
+
lock = TestRunLock(Path(args.project).resolve())
|
|
1217
|
+
print(
|
|
1218
|
+
json.dumps(
|
|
1219
|
+
{
|
|
1220
|
+
"ok": True,
|
|
1221
|
+
"action": "lock-status",
|
|
1222
|
+
"lock": classify_test_lock(lock.path),
|
|
1223
|
+
},
|
|
1224
|
+
ensure_ascii=False,
|
|
1225
|
+
indent=2,
|
|
1226
|
+
)
|
|
1227
|
+
)
|
|
1228
|
+
return 0
|
|
1229
|
+
|
|
1230
|
+
|
|
1231
|
+
def _run_lock_reap(args: argparse.Namespace) -> int:
|
|
1232
|
+
lock = TestRunLock(Path(args.project).resolve())
|
|
1233
|
+
print(json.dumps(reap_test_lock(lock.path), ensure_ascii=False, indent=2))
|
|
1234
|
+
return 0
|
|
1235
|
+
|
|
1236
|
+
|
|
892
1237
|
def build_parser() -> argparse.ArgumentParser:
|
|
893
1238
|
parser = argparse.ArgumentParser(
|
|
894
1239
|
description="Run tests with bounded workers, locking, timeout, and cleanup."
|
|
@@ -951,21 +1296,56 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
951
1296
|
help="Run one arbitrary test command under the same safety controls.",
|
|
952
1297
|
)
|
|
953
1298
|
exec_parser.add_argument("command", nargs=argparse.REMAINDER)
|
|
1299
|
+
exec_parser.add_argument(
|
|
1300
|
+
"--argv-file",
|
|
1301
|
+
help=(
|
|
1302
|
+
"Read argv from a UTF-8 JSON parameter file so PowerShell 5.1 and 7 "
|
|
1303
|
+
"use identical native-process argument semantics."
|
|
1304
|
+
),
|
|
1305
|
+
)
|
|
1306
|
+
exec_parser.add_argument(
|
|
1307
|
+
"--runtime-receipt",
|
|
1308
|
+
help="Write a secret-free argv hash and PowerShell runtime receipt.",
|
|
1309
|
+
)
|
|
1310
|
+
exec_parser.add_argument(
|
|
1311
|
+
"--environment-receipt",
|
|
1312
|
+
help="Typed environment receipt created by harness_environment.py prepare.",
|
|
1313
|
+
)
|
|
1314
|
+
exec_parser.add_argument(
|
|
1315
|
+
"--required-environment-field",
|
|
1316
|
+
action="append",
|
|
1317
|
+
default=[],
|
|
1318
|
+
help="Dynamic environment field required by the command; may be repeated.",
|
|
1319
|
+
)
|
|
954
1320
|
exec_parser.add_argument(
|
|
955
1321
|
"--allow-detached-processes",
|
|
956
1322
|
action="store_true",
|
|
957
1323
|
help="Use exact Windows PID-lineage tracking instead of a Job Object.",
|
|
958
1324
|
)
|
|
959
1325
|
exec_parser.set_defaults(handler=_run_exec)
|
|
1326
|
+
|
|
1327
|
+
lock_status = subparsers.add_parser(
|
|
1328
|
+
"lock-status",
|
|
1329
|
+
help="Classify the project test lock without mutating it.",
|
|
1330
|
+
)
|
|
1331
|
+
lock_status.add_argument("--project", default=".")
|
|
1332
|
+
lock_status.set_defaults(handler=_run_lock_status)
|
|
1333
|
+
|
|
1334
|
+
lock_reap = subparsers.add_parser(
|
|
1335
|
+
"lock-reap",
|
|
1336
|
+
help="Reap only an expired lock whose exact owner is absent.",
|
|
1337
|
+
)
|
|
1338
|
+
lock_reap.add_argument("--project", default=".")
|
|
1339
|
+
lock_reap.set_defaults(handler=_run_lock_reap)
|
|
960
1340
|
return parser
|
|
961
1341
|
|
|
962
1342
|
|
|
963
1343
|
def main(argv: Sequence[str] | None = None) -> int:
|
|
964
1344
|
parser = build_parser()
|
|
965
1345
|
args = parser.parse_args(argv)
|
|
966
|
-
if args
|
|
1346
|
+
if getattr(args, "timeout_seconds", DEFAULT_TIMEOUT_SECONDS) <= 0:
|
|
967
1347
|
parser.error("--timeout-seconds must be positive")
|
|
968
|
-
if args
|
|
1348
|
+
if getattr(args, "max_workers", DEFAULT_MAX_WORKERS) <= 0:
|
|
969
1349
|
parser.error("--max-workers must be positive")
|
|
970
1350
|
return int(args.handler(args))
|
|
971
1351
|
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-apidoc
|
|
3
3
|
description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
|
|
6
6
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
|
|
4
4
|
.harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -13,6 +13,9 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
|
|
|
13
13
|
- final-summary 的统计只能来自 summary-data 或 manifest。
|
|
14
14
|
- before/after checksum 不一致时,不得删除原目录。
|
|
15
15
|
- 默认渲染器 `templates/render-summary.mjs`(finalize 内嵌调用)。
|
|
16
|
+
- 需要跨工作区恢复时传 `--durable-root <独立存储>`;成功必须为 `ARCHIVED_DURABLE`,并验证 content-addressed object 与 durable receipt 可读回。
|
|
17
|
+
- `restore-durable` 只恢复到不存在的目标;目标已存在或对象 digest 不符时 fail closed,禁止覆盖。
|
|
18
|
+
- 未配置 durable root 时显式记录 `ARCHIVED_LOCAL_ONLY` 风险,不得把同工作区 move 宣称为独立备份。
|
|
16
19
|
|
|
17
20
|
### Wave-A 状态机与身份(IA-1 / IA-4)
|
|
18
21
|
|
|
@@ -48,6 +48,8 @@ const identity = Object.keys(record(normalized.identity)).length
|
|
|
48
48
|
const verification = Object.keys(record(normalized.verification)).length
|
|
49
49
|
? record(normalized.verification)
|
|
50
50
|
: record(data.verification);
|
|
51
|
+
const scenarioCoverage = record(data.scenarioCoverage);
|
|
52
|
+
const unexecutedScenarios = list(scenarioCoverage.unexecuted);
|
|
51
53
|
const timing = Object.keys(record(normalized.timing)).length
|
|
52
54
|
? record(normalized.timing)
|
|
53
55
|
: record(data.timing);
|
|
@@ -200,6 +202,21 @@ const remoteCost = record(measurements.remoteCost);
|
|
|
200
202
|
const remoteCostTotals = record(remoteCost.totals);
|
|
201
203
|
const storage = record(measurements.artifactStorage);
|
|
202
204
|
const projection = record(data.projection);
|
|
205
|
+
const durability = record(data.archiveDurability);
|
|
206
|
+
const durabilityStatus = durability.status || "ARCHIVED_LOCAL_ONLY";
|
|
207
|
+
const scenarioCoverageStatus = scenarioCoverage.skipped
|
|
208
|
+
? "SKIPPED"
|
|
209
|
+
: scenarioCoverage.ok
|
|
210
|
+
? "OK"
|
|
211
|
+
: scenarioCoverage.code || "NOT_RUN";
|
|
212
|
+
const scenarioCoverageHtml = `
|
|
213
|
+
<article class="card"><h2>场景执行闭环</h2>
|
|
214
|
+
<div class="fact"><span>结构化运行回执</span>${pill(scenarioCoverageStatus)}</div>
|
|
215
|
+
<div class="fact"><span>已执行并通过</span><strong>${list(scenarioCoverage.passed).length}</strong></div>
|
|
216
|
+
<div class="fact"><span>未执行 / 未通过</span><span>${unexecutedScenarios.length
|
|
217
|
+
? `<code>${esc(unexecutedScenarios.join(", "))}</code>`
|
|
218
|
+
: "无"}</span></div>
|
|
219
|
+
</article>`;
|
|
203
220
|
|
|
204
221
|
const html = `<!doctype html>
|
|
205
222
|
<html lang="zh-CN"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
@@ -227,12 +244,14 @@ const html = `<!doctype html>
|
|
|
227
244
|
<article class="metric"><small>代码范围</small><strong>${esc(record(data.diffStat).filesChanged ?? files.length)} 个文件</strong><small class="positive">+${esc(record(data.diffStat).insertions ?? 0)} · <span class="negative">-${esc(record(data.diffStat).deletions ?? 0)}</span></small></article>
|
|
228
245
|
</section>
|
|
229
246
|
<article class="card"><h2>验证概览</h2><div class="verification-grid">${groupHtml}</div></article>
|
|
247
|
+
${scenarioCoverageHtml}
|
|
230
248
|
<section class="two-column">
|
|
231
249
|
<article class="card"><h2>风险与动作</h2><div class="risk-actions"><section><h3>当前风险</h3><ul>${risksHtml}</ul></section><section><h3>人工动作</h3><ul>${actionsHtml}</ul></section></div></article>
|
|
232
250
|
<div>
|
|
233
251
|
${releaseHtml}
|
|
234
252
|
<article class="card"><h2>事实完整性</h2>
|
|
235
253
|
<div class="fact"><span>归档完整性</span>${pill(record(data.archiveIntegrity).ok === false ? "FAIL" : record(data.archiveManifest).checksumStatus || "UNKNOWN")}</div>
|
|
254
|
+
<div class="fact"><span>归档持久性</span><span>${pill(durabilityStatus === "ARCHIVED_DURABLE" ? "OK" : "WARN")} <code>${esc(durabilityStatus)}</code></span></div>
|
|
236
255
|
<div class="fact"><span>时间守恒</span>${pill(numeric(timing.conservationDeltaMs) === 0 ? "OK" : "WARN")}</div>
|
|
237
256
|
</article>
|
|
238
257
|
</div>
|
|
@@ -259,6 +278,9 @@ const html = `<!doctype html>
|
|
|
259
278
|
<dt>基线提交</dt><dd><code>${esc(identity.baseCommit || data.baseCommit || "N/A")}</code></dd>
|
|
260
279
|
<dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd>
|
|
261
280
|
<dt>归档意图</dt><dd title="${esc(release.intent)}">${recordOnly ? "仅记录(未请求发布)" : esc(release.intent || "未声明")}</dd>
|
|
281
|
+
<dt>归档持久性</dt><dd><code>${esc(durabilityStatus)}</code>${durability.risk ? ` · ${esc(durability.risk)}` : ""}</dd>
|
|
282
|
+
<dt>保留策略</dt><dd>${esc(durability.retentionPolicy || "N/A")}</dd>
|
|
283
|
+
<dt>场景回执状态</dt><dd><code>${esc(scenarioCoverage.code || scenarioCoverageStatus)}</code></dd>
|
|
262
284
|
<dt>尝试记录</dt><dd>${attempts.length}</dd>
|
|
263
285
|
<dt>历史质量</dt><dd>${numeric(timing.unclosedAttemptCount) === 0 ? "无未闭合尝试" : `${esc(timing.unclosedAttemptCount ?? "N/A")} 个未闭合尝试`}</dd>
|
|
264
286
|
<dt>投影状态</dt><dd title="Projection / Fallback"><code>${esc(projection.code || projection.mode || "N/A")}</code></dd>
|
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
|
|
|
3
3
|
description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
|
|
4
4
|
knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=3a5923039708d7bb; overlay=java; agent=codebuddy; do not edit -->
|
|
7
7
|
# harness-knowledge-ingest
|
|
8
8
|
|
|
9
9
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|