@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
|
|