@hunter-harness/workflow-harness 0.2.57 → 0.2.59
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-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-review/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-review/protocols.md +2 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/general/claude-code/harness-test/reference.md +2 -0
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/claude-code/scripts/harness_context.py +423 -77
- package/harness/bundles/general/claude-code/scripts/harness_events.py +102 -12
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +529 -1
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +161 -6
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +81 -8
- package/harness/bundles/general/claude-code/scripts/harness_review.py +25 -2
- package/harness/bundles/general/claude-code/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-review/protocols.md +2 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/general/codebuddy/harness-test/reference.md +2 -0
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/codebuddy/scripts/harness_context.py +423 -77
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +102 -12
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +529 -1
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +161 -6
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +81 -8
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +25 -2
- package/harness/bundles/general/codebuddy/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-review/protocols.md +2 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/general/codex/harness-test/reference.md +2 -0
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/codex/scripts/harness_context.py +423 -77
- package/harness/bundles/general/codex/scripts/harness_events.py +102 -12
- package/harness/bundles/general/codex/scripts/harness_fixback.py +529 -1
- package/harness/bundles/general/codex/scripts/harness_gate.py +161 -6
- package/harness/bundles/general/codex/scripts/harness_ledger.py +81 -8
- package/harness/bundles/general/codex/scripts/harness_review.py +25 -2
- package/harness/bundles/general/codex/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-review/protocols.md +2 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/general/cursor/harness-test/reference.md +2 -0
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/general/cursor/scripts/harness_context.py +423 -77
- package/harness/bundles/general/cursor/scripts/harness_events.py +102 -12
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +529 -1
- package/harness/bundles/general/cursor/scripts/harness_gate.py +161 -6
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +81 -8
- package/harness/bundles/general/cursor/scripts/harness_review.py +25 -2
- package/harness/bundles/general/cursor/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-review/protocols.md +2 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/claude-code/scripts/harness_context.py +423 -77
- package/harness/bundles/java/claude-code/scripts/harness_events.py +102 -12
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +529 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +161 -6
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +81 -8
- package/harness/bundles/java/claude-code/scripts/harness_review.py +25 -2
- package/harness/bundles/java/claude-code/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-review/protocols.md +2 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/codebuddy/scripts/harness_context.py +423 -77
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +102 -12
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +529 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +161 -6
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +81 -8
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +25 -2
- package/harness/bundles/java/codebuddy/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +3 -1
- package/harness/bundles/java/codex/harness-review/protocols.md +2 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/codex/scripts/harness_context.py +423 -77
- package/harness/bundles/java/codex/scripts/harness_events.py +102 -12
- package/harness/bundles/java/codex/scripts/harness_fixback.py +529 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +161 -6
- package/harness/bundles/java/codex/scripts/harness_ledger.py +81 -8
- package/harness/bundles/java/codex/scripts/harness_review.py +25 -2
- package/harness/bundles/java/codex/scripts/harness_test_runner.py +155 -1
- 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-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-review/SKILL.md +3 -1
- package/harness/bundles/java/cursor/harness-review/protocols.md +2 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- 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 +5 -3
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +2 -0
- package/harness/bundles/java/cursor/scripts/harness_context.py +423 -77
- package/harness/bundles/java/cursor/scripts/harness_events.py +102 -12
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +529 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +161 -6
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +81 -8
- package/harness/bundles/java/cursor/scripts/harness_review.py +25 -2
- package/harness/bundles/java/cursor/scripts/harness_test_runner.py +155 -1
- package/harness/manifests/general/claude-code.json +23 -23
- package/harness/manifests/general/codebuddy.json +23 -23
- package/harness/manifests/general/codex.json +23 -23
- package/harness/manifests/general/cursor.json +23 -23
- package/harness/manifests/java/claude-code.json +24 -24
- package/harness/manifests/java/codebuddy.json +24 -24
- package/harness/manifests/java/codex.json +24 -24
- package/harness/manifests/java/cursor.json +24 -24
- package/hunter-workflow-family.json +5 -5
- package/package.json +1 -1
|
@@ -36,6 +36,7 @@ import harness_events_sync as hes # noqa: E402
|
|
|
36
36
|
import harness_ledger as hl # noqa: E402
|
|
37
37
|
import harness_paths as hp # noqa: E402
|
|
38
38
|
import harness_plan_finalize as hpf # noqa: E402
|
|
39
|
+
import harness_review as hr # noqa: E402
|
|
39
40
|
import harness_workflow_policy as hwp # noqa: E402
|
|
40
41
|
import harness_test_guard as htg # noqa: E402
|
|
41
42
|
|
|
@@ -1319,11 +1320,62 @@ def classify_risk(
|
|
|
1319
1320
|
changed: list[str] = []
|
|
1320
1321
|
for line in proc.stdout.splitlines():
|
|
1321
1322
|
raw = line[3:].strip().strip('"').replace("\\", "/")
|
|
1322
|
-
if not raw
|
|
1323
|
+
if not raw:
|
|
1323
1324
|
continue
|
|
1325
|
+
if " -> " in raw:
|
|
1326
|
+
raw = raw.rsplit(" -> ", 1)[-1].strip().strip('"')
|
|
1324
1327
|
changed.append(raw)
|
|
1325
|
-
|
|
1326
|
-
|
|
1328
|
+
try:
|
|
1329
|
+
contract = hp.load_change_contract(change_dir)
|
|
1330
|
+
except (OSError, ValueError, json.JSONDecodeError):
|
|
1331
|
+
contract = {}
|
|
1332
|
+
ownership = contract.get("ownership") if isinstance(contract, dict) else None
|
|
1333
|
+
modern_scope = isinstance(ownership, dict) and bool(
|
|
1334
|
+
ownership.get("productPaths") or ownership.get("staticEvidencePaths")
|
|
1335
|
+
)
|
|
1336
|
+
product_paths: list[str] = []
|
|
1337
|
+
static_paths: list[str] = []
|
|
1338
|
+
excluded_paths: list[str] = []
|
|
1339
|
+
maintenance_paths: list[str] = []
|
|
1340
|
+
foreign_paths: list[str] = []
|
|
1341
|
+
|
|
1342
|
+
def is_harness_maintenance(path: str) -> bool:
|
|
1343
|
+
normalized = path.replace("\\", "/")
|
|
1344
|
+
return normalized.startswith(
|
|
1345
|
+
(
|
|
1346
|
+
".cursor/skills/",
|
|
1347
|
+
".claude/skills/",
|
|
1348
|
+
".agents/skills/",
|
|
1349
|
+
".codebuddy/skills/",
|
|
1350
|
+
)
|
|
1351
|
+
) or normalized in {
|
|
1352
|
+
".harness/context-index.json",
|
|
1353
|
+
".harness/config/build-profile.json",
|
|
1354
|
+
}
|
|
1355
|
+
|
|
1356
|
+
for path in changed:
|
|
1357
|
+
if modern_scope:
|
|
1358
|
+
category = hl._classify_ownership_path(
|
|
1359
|
+
path, change_dir.name, ownership
|
|
1360
|
+
)
|
|
1361
|
+
if category == "owned":
|
|
1362
|
+
product_paths.append(path)
|
|
1363
|
+
elif category == "staticEvidence":
|
|
1364
|
+
static_paths.append(path)
|
|
1365
|
+
elif category == "excludedRuntime":
|
|
1366
|
+
excluded_paths.append(path)
|
|
1367
|
+
elif is_harness_maintenance(path):
|
|
1368
|
+
maintenance_paths.append(path)
|
|
1369
|
+
else:
|
|
1370
|
+
foreign_paths.append(path)
|
|
1371
|
+
elif path.startswith(".harness/") or is_harness_maintenance(path):
|
|
1372
|
+
maintenance_paths.append(path)
|
|
1373
|
+
else:
|
|
1374
|
+
product_paths.append(path)
|
|
1375
|
+
capabilities = sorted(
|
|
1376
|
+
set(capabilities) | set(_diff_capabilities(product_paths))
|
|
1377
|
+
)
|
|
1378
|
+
lowered = "\n".join(product_paths).lower()
|
|
1327
1379
|
full_markers = {
|
|
1328
1380
|
"auth": ("auth", "token", "credential", "permission"),
|
|
1329
1381
|
"security": ("security", "secret", "crypto"),
|
|
@@ -1338,14 +1390,14 @@ def classify_risk(
|
|
|
1338
1390
|
signals.append(signal)
|
|
1339
1391
|
if signals:
|
|
1340
1392
|
observed = "full"
|
|
1341
|
-
elif
|
|
1393
|
+
elif product_paths and all(
|
|
1342
1394
|
path.lower().endswith((".md", ".txt", ".rst"))
|
|
1343
1395
|
or path.lower().startswith("docs/")
|
|
1344
|
-
for path in
|
|
1396
|
+
for path in product_paths
|
|
1345
1397
|
):
|
|
1346
1398
|
observed = "fast"
|
|
1347
1399
|
signals.append("docs-only")
|
|
1348
|
-
elif
|
|
1400
|
+
elif product_paths:
|
|
1349
1401
|
observed = "standard"
|
|
1350
1402
|
signals.append("production-code")
|
|
1351
1403
|
else:
|
|
@@ -1373,6 +1425,14 @@ def classify_risk(
|
|
|
1373
1425
|
"conditionalStages": list(tier_policy["conditionalStages"]),
|
|
1374
1426
|
"stageDecisions": _stage_decisions_for_tier(workflow, tier, unique_signals),
|
|
1375
1427
|
}
|
|
1428
|
+
if stage == "post-run":
|
|
1429
|
+
payload["workspaceBreakdown"] = {
|
|
1430
|
+
"productPaths": sorted(set(product_paths)),
|
|
1431
|
+
"staticEvidencePaths": sorted(set(static_paths)),
|
|
1432
|
+
"excludedRuntimePaths": sorted(set(excluded_paths)),
|
|
1433
|
+
"harnessMaintenancePaths": sorted(set(maintenance_paths)),
|
|
1434
|
+
"foreignPaths": sorted(set(foreign_paths)),
|
|
1435
|
+
}
|
|
1376
1436
|
if stage == "post-run":
|
|
1377
1437
|
_write_json(change_dir / "meta" / "risk-classification.json", payload)
|
|
1378
1438
|
result = _apply_required_gate_contract(payload, workflow, capabilities)
|
|
@@ -2158,6 +2218,72 @@ def validate_plan_handoff(change_dir: Path) -> dict[str, Any]:
|
|
|
2158
2218
|
return hpf.verify_plan(change_dir)
|
|
2159
2219
|
|
|
2160
2220
|
|
|
2221
|
+
def validate_review_outputs_for_close(
|
|
2222
|
+
change_dir: Path,
|
|
2223
|
+
run_id: str,
|
|
2224
|
+
) -> dict[str, Any]:
|
|
2225
|
+
"""Require complete, run-bound structured Review sidecars before close."""
|
|
2226
|
+
|
|
2227
|
+
findings_path = hr.findings_path(change_dir)
|
|
2228
|
+
dispositions_path = hr.dispositions_path(change_dir)
|
|
2229
|
+
missing = [
|
|
2230
|
+
str(path)
|
|
2231
|
+
for path in (findings_path, dispositions_path)
|
|
2232
|
+
if not path.is_file()
|
|
2233
|
+
]
|
|
2234
|
+
if missing:
|
|
2235
|
+
return {
|
|
2236
|
+
"ok": False,
|
|
2237
|
+
"code": "REVIEW_OUTPUTS_INCOMPLETE",
|
|
2238
|
+
"message": "评审的结构化发现或处置记录缺失,尚不能结束评审阶段。",
|
|
2239
|
+
"missing": missing,
|
|
2240
|
+
"recoveryAction": "补写 review-findings.json 与 fixback-dispositions.json 后重试关门",
|
|
2241
|
+
}
|
|
2242
|
+
try:
|
|
2243
|
+
findings = json.loads(findings_path.read_text(encoding="utf-8-sig"))
|
|
2244
|
+
dispositions = json.loads(
|
|
2245
|
+
dispositions_path.read_text(encoding="utf-8-sig")
|
|
2246
|
+
)
|
|
2247
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
2248
|
+
return {
|
|
2249
|
+
"ok": False,
|
|
2250
|
+
"code": "REVIEW_OUTPUTS_INVALID",
|
|
2251
|
+
"message": f"评审结构化记录无法读取:{exc}",
|
|
2252
|
+
}
|
|
2253
|
+
finding_problems = hr.validate_findings(findings, require_ids=True)
|
|
2254
|
+
known_ids = {
|
|
2255
|
+
str(item.get("id"))
|
|
2256
|
+
for item in findings.get("findings", [])
|
|
2257
|
+
if isinstance(item, dict) and item.get("id")
|
|
2258
|
+
}
|
|
2259
|
+
disposition_problems = hr.validate_dispositions(dispositions, known_ids)
|
|
2260
|
+
disposition_ids = {
|
|
2261
|
+
str(item.get("findingId"))
|
|
2262
|
+
for item in dispositions.get("dispositions", [])
|
|
2263
|
+
if isinstance(item, dict) and item.get("findingId")
|
|
2264
|
+
}
|
|
2265
|
+
problems = [*finding_problems, *disposition_problems]
|
|
2266
|
+
if findings.get("runId") != run_id or dispositions.get("runId") != run_id:
|
|
2267
|
+
problems.append("review sidecars must match the active review runId")
|
|
2268
|
+
missing_dispositions = sorted(known_ids - disposition_ids)
|
|
2269
|
+
if missing_dispositions:
|
|
2270
|
+
problems.append(
|
|
2271
|
+
"findings without dispositions: " + ",".join(missing_dispositions)
|
|
2272
|
+
)
|
|
2273
|
+
if problems:
|
|
2274
|
+
return {
|
|
2275
|
+
"ok": False,
|
|
2276
|
+
"code": "REVIEW_OUTPUTS_INVALID",
|
|
2277
|
+
"message": "评审结构化记录不完整或与当前轮次不一致。",
|
|
2278
|
+
"problems": problems,
|
|
2279
|
+
}
|
|
2280
|
+
return {
|
|
2281
|
+
"ok": True,
|
|
2282
|
+
"code": "REVIEW_OUTPUTS_READY",
|
|
2283
|
+
"findingCount": len(known_ids),
|
|
2284
|
+
}
|
|
2285
|
+
|
|
2286
|
+
|
|
2161
2287
|
def cmd_begin(args: argparse.Namespace) -> int:
|
|
2162
2288
|
as_json = bool(args.json)
|
|
2163
2289
|
project = hc.resolve_main_project_root()
|
|
@@ -2738,6 +2864,35 @@ def cmd_close(args: argparse.Namespace) -> int:
|
|
|
2738
2864
|
extra={k: v for k, v in ledger_result.items() if k not in {"ok", "message", "code"}},
|
|
2739
2865
|
)
|
|
2740
2866
|
|
|
2867
|
+
if args.phase == "review":
|
|
2868
|
+
review_outputs = validate_review_outputs_for_close(change_dir, run_id)
|
|
2869
|
+
if not review_outputs.get("ok"):
|
|
2870
|
+
persist_close_failure(
|
|
2871
|
+
change_dir,
|
|
2872
|
+
args.phase,
|
|
2873
|
+
run_id,
|
|
2874
|
+
capsule,
|
|
2875
|
+
status="REVIEW_OUTPUTS_INVALID",
|
|
2876
|
+
error=review_outputs,
|
|
2877
|
+
)
|
|
2878
|
+
record_gate_blocked(
|
|
2879
|
+
change_dir,
|
|
2880
|
+
phase=args.phase,
|
|
2881
|
+
code=str(review_outputs.get("code") or "REVIEW_OUTPUTS_INVALID"),
|
|
2882
|
+
message=str(review_outputs.get("message") or "评审记录不完整。"),
|
|
2883
|
+
run_id=run_id,
|
|
2884
|
+
)
|
|
2885
|
+
return emit_error(
|
|
2886
|
+
str(review_outputs.get("code") or "REVIEW_OUTPUTS_INVALID"),
|
|
2887
|
+
str(review_outputs.get("message") or "评审记录不完整。"),
|
|
2888
|
+
as_json=as_json,
|
|
2889
|
+
extra={
|
|
2890
|
+
key: value
|
|
2891
|
+
for key, value in review_outputs.items()
|
|
2892
|
+
if key not in {"ok", "code", "message"}
|
|
2893
|
+
},
|
|
2894
|
+
)
|
|
2895
|
+
|
|
2741
2896
|
# C9: scenario coverage check — all P0 scenarios must have a ledger entry.
|
|
2742
2897
|
if args.phase in {"run", "test"}:
|
|
2743
2898
|
coverage = _validate_scenario_coverage(change_dir)
|
|
@@ -808,6 +808,50 @@ def expand_profile_input_files(
|
|
|
808
808
|
return sorted(seen), None
|
|
809
809
|
|
|
810
810
|
|
|
811
|
+
def ensure_profile_input_target(
|
|
812
|
+
project: Path,
|
|
813
|
+
profile_input: str,
|
|
814
|
+
) -> tuple[dict[str, Any] | None, str | None]:
|
|
815
|
+
"""Refresh a missing/stale detected profile before recording evidence."""
|
|
816
|
+
|
|
817
|
+
project_root = Path(project).resolve()
|
|
818
|
+
profile = harness_profile.load_profile(project_root)
|
|
819
|
+
inputs = profile.get("verificationInputs") if isinstance(profile, dict) else None
|
|
820
|
+
graph = profile.get("verificationGraph") if isinstance(profile, dict) else None
|
|
821
|
+
targets = graph.get("targets") if isinstance(graph, dict) else None
|
|
822
|
+
if (
|
|
823
|
+
isinstance(inputs, dict)
|
|
824
|
+
and profile_input in inputs
|
|
825
|
+
and isinstance(targets, dict)
|
|
826
|
+
and isinstance(targets.get(profile_input), dict)
|
|
827
|
+
):
|
|
828
|
+
return profile, None
|
|
829
|
+
|
|
830
|
+
# An unreadable profile needs an explicit repair; silently replacing it can
|
|
831
|
+
# discard user-owned overrides.
|
|
832
|
+
common = harness_paths.common_root(project_root)
|
|
833
|
+
for root in dict.fromkeys((common, project_root)):
|
|
834
|
+
candidate = root / ".harness" / "config" / "build-profile.json"
|
|
835
|
+
if not candidate.is_file():
|
|
836
|
+
continue
|
|
837
|
+
try:
|
|
838
|
+
json.loads(candidate.read_text(encoding="utf-8-sig"))
|
|
839
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
840
|
+
return None, f"build-profile.json unreadable: {exc}"
|
|
841
|
+
|
|
842
|
+
detected = harness_profile.detect(project_root)
|
|
843
|
+
if not detected.get("ok"):
|
|
844
|
+
return None, str(
|
|
845
|
+
detected.get("message")
|
|
846
|
+
or detected.get("code")
|
|
847
|
+
or "build profile detection failed"
|
|
848
|
+
)
|
|
849
|
+
refreshed = harness_profile.load_profile(project_root)
|
|
850
|
+
if not isinstance(refreshed, dict):
|
|
851
|
+
return None, "build-profile.json missing after detection"
|
|
852
|
+
return refreshed, None
|
|
853
|
+
|
|
854
|
+
|
|
811
855
|
def _state_dir(change_dir: Path) -> Path:
|
|
812
856
|
return Path(harness_paths.resolve_state_dir_for_contract(change_dir))
|
|
813
857
|
|
|
@@ -1728,6 +1772,7 @@ def decide_can_reuse(
|
|
|
1728
1772
|
"reuse": False,
|
|
1729
1773
|
"reason": "insufficient-evidence",
|
|
1730
1774
|
"code": "LEDGER_MISSING",
|
|
1775
|
+
"executionNeed": "first-run",
|
|
1731
1776
|
"verification": verification,
|
|
1732
1777
|
"detail": "ledger missing",
|
|
1733
1778
|
}
|
|
@@ -1787,6 +1832,7 @@ def decide_can_reuse(
|
|
|
1787
1832
|
"reuse": False,
|
|
1788
1833
|
"reason": "insufficient-evidence",
|
|
1789
1834
|
"code": "VALIDATIONS_MISSING",
|
|
1835
|
+
"executionNeed": "first-run",
|
|
1790
1836
|
"verification": verification,
|
|
1791
1837
|
"detail": "validations missing",
|
|
1792
1838
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
@@ -1800,6 +1846,7 @@ def decide_can_reuse(
|
|
|
1800
1846
|
"reuse": False,
|
|
1801
1847
|
"reason": "insufficient-evidence",
|
|
1802
1848
|
"code": "VALIDATION_MISSING",
|
|
1849
|
+
"executionNeed": "first-run",
|
|
1803
1850
|
"verification": verification,
|
|
1804
1851
|
"detail": f"validation '{verification}' missing",
|
|
1805
1852
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
@@ -1821,6 +1868,7 @@ def decide_can_reuse(
|
|
|
1821
1868
|
"reuse": False,
|
|
1822
1869
|
"reason": "rerun",
|
|
1823
1870
|
"code": "EVIDENCE_INVALIDATED",
|
|
1871
|
+
"executionNeed": "rerun",
|
|
1824
1872
|
"verification": verification,
|
|
1825
1873
|
"detail": "recorded target was invalidated by workflow fixback",
|
|
1826
1874
|
"invalidation": entry.get("invalidation"),
|
|
@@ -1868,6 +1916,7 @@ def decide_can_reuse(
|
|
|
1868
1916
|
"reuse": False,
|
|
1869
1917
|
"reason": "insufficient-evidence",
|
|
1870
1918
|
"code": "MISSING_V2_FIELDS" if v2_missing else "MISSING_FIELDS",
|
|
1919
|
+
"executionNeed": "evidence-incomplete",
|
|
1871
1920
|
"verification": verification,
|
|
1872
1921
|
"detail": "missing or invalid: " + ", ".join(all_missing),
|
|
1873
1922
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
@@ -2017,6 +2066,7 @@ def decide_can_reuse(
|
|
|
2017
2066
|
"reuse": True,
|
|
2018
2067
|
"reason": "reuse",
|
|
2019
2068
|
"code": "REUSED",
|
|
2069
|
+
"executionNeed": "reuse",
|
|
2020
2070
|
"invalidationCode": None,
|
|
2021
2071
|
"verification": verification,
|
|
2022
2072
|
"ledger_path": str(ledger_path) if ledger_path else None,
|
|
@@ -2171,7 +2221,6 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
2171
2221
|
)
|
|
2172
2222
|
except (OSError, ValueError) as exc:
|
|
2173
2223
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
2174
|
-
explicit_files = list(files)
|
|
2175
2224
|
target = resolve_verification_target(verification, project_root)
|
|
2176
2225
|
if target is None:
|
|
2177
2226
|
return emit_error(
|
|
@@ -2180,6 +2229,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
2180
2229
|
"verificationGraph.targets",
|
|
2181
2230
|
as_json=as_json,
|
|
2182
2231
|
)
|
|
2232
|
+
explicit_files = list(files)
|
|
2183
2233
|
profile_input = getattr(args, "profile_input", None)
|
|
2184
2234
|
project_raw = getattr(args, "project", None)
|
|
2185
2235
|
if profile_input:
|
|
@@ -2297,6 +2347,20 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2297
2347
|
except (OSError, ValueError) as exc:
|
|
2298
2348
|
return emit_error(f"record failed: {exc}", as_json=as_json)
|
|
2299
2349
|
explicit_files = list(files)
|
|
2350
|
+
profile_input = getattr(args, "profile_input", None)
|
|
2351
|
+
project_raw = getattr(args, "project", None)
|
|
2352
|
+
if profile_input:
|
|
2353
|
+
if not project_raw:
|
|
2354
|
+
return emit_error("--profile-input requires --project", as_json=as_json)
|
|
2355
|
+
_profile, profile_error = ensure_profile_input_target(
|
|
2356
|
+
project_root, profile_input
|
|
2357
|
+
)
|
|
2358
|
+
if profile_error:
|
|
2359
|
+
return emit_error(
|
|
2360
|
+
f"record failed: {profile_error}",
|
|
2361
|
+
as_json=as_json,
|
|
2362
|
+
error_code="BUILD_PROFILE_REFRESH_FAILED",
|
|
2363
|
+
)
|
|
2300
2364
|
target = resolve_verification_target(verification, project_root)
|
|
2301
2365
|
if target is None:
|
|
2302
2366
|
return emit_error(
|
|
@@ -2305,11 +2369,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2305
2369
|
"verificationGraph.targets",
|
|
2306
2370
|
as_json=as_json,
|
|
2307
2371
|
)
|
|
2308
|
-
profile_input = getattr(args, "profile_input", None)
|
|
2309
|
-
project_raw = getattr(args, "project", None)
|
|
2310
2372
|
if profile_input:
|
|
2311
|
-
if not project_raw:
|
|
2312
|
-
return emit_error("--profile-input requires --project", as_json=as_json)
|
|
2313
2373
|
resolved_files, err = expand_profile_input_files(project_root, profile_input)
|
|
2314
2374
|
if err:
|
|
2315
2375
|
return emit_error(f"record failed: {err}", as_json=as_json)
|
|
@@ -2455,8 +2515,21 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2455
2515
|
)
|
|
2456
2516
|
except ValueError as exc:
|
|
2457
2517
|
return emit_error(str(exc), as_json=as_json)
|
|
2458
|
-
|
|
2459
|
-
|
|
2518
|
+
effective_scope = (
|
|
2519
|
+
str(args.scope).strip()
|
|
2520
|
+
if args.scope is not None and str(args.scope).strip()
|
|
2521
|
+
else (
|
|
2522
|
+
str(
|
|
2523
|
+
target.get("coverageLevel")
|
|
2524
|
+
or target.get("requiredCoverage")
|
|
2525
|
+
or ""
|
|
2526
|
+
).strip()
|
|
2527
|
+
if profile_input
|
|
2528
|
+
else ""
|
|
2529
|
+
)
|
|
2530
|
+
)
|
|
2531
|
+
if effective_scope:
|
|
2532
|
+
entry["scope"] = effective_scope
|
|
2460
2533
|
# No default scope: recording an incremental run as broad "module" scope
|
|
2461
2534
|
# would let can-reuse wrongly approve untested classes (D13 guardrail).
|
|
2462
2535
|
# Missing scope → can-reuse treats unitTest as insufficient-evidence.
|
|
@@ -2470,7 +2543,7 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
2470
2543
|
)
|
|
2471
2544
|
coverage = getattr(args, "coverage", None)
|
|
2472
2545
|
if not (_nonempty_str(coverage) and str(coverage).strip() in COVERAGE_RANK):
|
|
2473
|
-
coverage = derive_coverage(verification,
|
|
2546
|
+
coverage = derive_coverage(verification, effective_scope or None)
|
|
2474
2547
|
entry["coverage"] = coverage
|
|
2475
2548
|
for field, attr in (
|
|
2476
2549
|
("toolchainHash", "toolchain_hash"),
|
|
@@ -37,6 +37,7 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
37
37
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
38
38
|
|
|
39
39
|
SEVERITIES = {"RED", "YELLOW", "OK"}
|
|
40
|
+
FIXBACK_ACTIONS = {"code", "manual", "workflow"}
|
|
40
41
|
DISPOSITIONS = {
|
|
41
42
|
"OPEN",
|
|
42
43
|
"FIXED",
|
|
@@ -48,7 +49,14 @@ DISPOSITIONS = {
|
|
|
48
49
|
CURRENT_RISK_DISPOSITIONS = {"OPEN", "ACCEPTED_RISK", "DEFERRED", "UNKNOWN"}
|
|
49
50
|
FINDINGS_REL = Path("reports") / "review" / "review-findings.json"
|
|
50
51
|
DISPOSITIONS_REL = Path("reports") / "review" / "fixback-dispositions.json"
|
|
51
|
-
_REQUIRED_FINDING_FIELDS = (
|
|
52
|
+
_REQUIRED_FINDING_FIELDS = (
|
|
53
|
+
"dimension",
|
|
54
|
+
"severity",
|
|
55
|
+
"path",
|
|
56
|
+
"line",
|
|
57
|
+
"title",
|
|
58
|
+
"fixbackAction",
|
|
59
|
+
)
|
|
52
60
|
|
|
53
61
|
|
|
54
62
|
def _write_json_atomic(path: Path, data: Any) -> None:
|
|
@@ -95,7 +103,7 @@ def stable_finding_id(
|
|
|
95
103
|
return "f-" + hashlib.sha256(basis.encode("utf-8")).hexdigest()[:16]
|
|
96
104
|
|
|
97
105
|
|
|
98
|
-
def validate_findings(doc: Any) -> list[str]:
|
|
106
|
+
def validate_findings(doc: Any, *, require_ids: bool = False) -> list[str]:
|
|
99
107
|
problems: list[str] = []
|
|
100
108
|
if not isinstance(doc, dict):
|
|
101
109
|
return ["findings document must be an object"]
|
|
@@ -105,10 +113,19 @@ def validate_findings(doc: Any) -> list[str]:
|
|
|
105
113
|
if not isinstance(findings, list):
|
|
106
114
|
problems.append("findings must be a list")
|
|
107
115
|
return problems
|
|
116
|
+
seen_ids: set[str] = set()
|
|
108
117
|
for index, finding in enumerate(findings):
|
|
109
118
|
if not isinstance(finding, dict):
|
|
110
119
|
problems.append(f"findings[{index}] must be an object")
|
|
111
120
|
continue
|
|
121
|
+
if require_ids:
|
|
122
|
+
finding_id = finding.get("id")
|
|
123
|
+
if not isinstance(finding_id, str) or not finding_id.strip():
|
|
124
|
+
problems.append(f"findings[{index}].id is required")
|
|
125
|
+
elif finding_id in seen_ids:
|
|
126
|
+
problems.append(f"findings[{index}].id must be unique")
|
|
127
|
+
else:
|
|
128
|
+
seen_ids.add(finding_id)
|
|
112
129
|
for field in _REQUIRED_FINDING_FIELDS:
|
|
113
130
|
if field not in finding:
|
|
114
131
|
problems.append(f"findings[{index}].{field} is required")
|
|
@@ -120,6 +137,12 @@ def validate_findings(doc: Any) -> list[str]:
|
|
|
120
137
|
line = finding.get("line")
|
|
121
138
|
if line is not None and (not isinstance(line, int) or line < 0):
|
|
122
139
|
problems.append(f"findings[{index}].line must be a non-negative int")
|
|
140
|
+
action = finding.get("fixbackAction")
|
|
141
|
+
if action is not None and action not in FIXBACK_ACTIONS:
|
|
142
|
+
problems.append(
|
|
143
|
+
f"findings[{index}].fixbackAction must be one of "
|
|
144
|
+
f"{sorted(FIXBACK_ACTIONS)}"
|
|
145
|
+
)
|
|
123
146
|
return problems
|
|
124
147
|
|
|
125
148
|
|
|
@@ -14,6 +14,8 @@ import ctypes
|
|
|
14
14
|
import hashlib
|
|
15
15
|
import json
|
|
16
16
|
import os
|
|
17
|
+
import re
|
|
18
|
+
import shutil
|
|
17
19
|
import signal
|
|
18
20
|
import subprocess
|
|
19
21
|
import sys
|
|
@@ -68,6 +70,104 @@ class TestRunAlreadyActive(RuntimeError):
|
|
|
68
70
|
"""Raised when another resource-safe runner owns the project lock."""
|
|
69
71
|
|
|
70
72
|
|
|
73
|
+
class ManagedCommandNotFound(FileNotFoundError):
|
|
74
|
+
"""Raised when a structured command cannot be resolved without a shell."""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class ManagedCommandUnsafe(ValueError):
|
|
78
|
+
"""Raised when a Windows batch argument could be interpreted by cmd.exe."""
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
_WINDOWS_BATCH_SUFFIXES = {".bat", ".cmd"}
|
|
82
|
+
_WINDOWS_CMD_META = re.compile(r'[\r\n&|<>^%!\"]')
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
def validate_managed_argv(
|
|
86
|
+
argv: Sequence[str], *, platform_name: str | None = None
|
|
87
|
+
) -> None:
|
|
88
|
+
"""Fail closed when Windows would reinterpret a batch argv boundary."""
|
|
89
|
+
|
|
90
|
+
platform = platform_name or os.name
|
|
91
|
+
if (
|
|
92
|
+
platform != "nt"
|
|
93
|
+
or not argv
|
|
94
|
+
or Path(argv[0]).suffix.casefold() not in _WINDOWS_BATCH_SUFFIXES
|
|
95
|
+
):
|
|
96
|
+
return
|
|
97
|
+
for index, argument in enumerate(argv):
|
|
98
|
+
if _WINDOWS_CMD_META.search(str(argument)):
|
|
99
|
+
raise ManagedCommandUnsafe(
|
|
100
|
+
"MANAGED_BATCH_ARGUMENT_UNSAFE: "
|
|
101
|
+
f"Windows 批处理命令的第 {index} 个参数包含命令解释符;"
|
|
102
|
+
"请改用原生可执行文件或不含命令解释符的参数"
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def resolve_managed_argv(
|
|
107
|
+
argv: Sequence[str],
|
|
108
|
+
*,
|
|
109
|
+
environ: Mapping[str, str],
|
|
110
|
+
platform_name: str | None = None,
|
|
111
|
+
) -> list[str]:
|
|
112
|
+
"""Resolve argv[0] through PATH/PATHEXT while preserving argv boundaries."""
|
|
113
|
+
|
|
114
|
+
if not argv or not isinstance(argv[0], str) or not argv[0].strip():
|
|
115
|
+
raise ManagedCommandNotFound("MANAGED_COMMAND_NOT_FOUND: 命令不能为空")
|
|
116
|
+
command = argv[0].strip()
|
|
117
|
+
platform = platform_name or os.name
|
|
118
|
+
windows_extensions = [
|
|
119
|
+
item.strip() if item.strip().startswith(".") else f".{item.strip()}"
|
|
120
|
+
for item in str(
|
|
121
|
+
environ.get("PATHEXT") or ".COM;.EXE;.BAT;.CMD"
|
|
122
|
+
).split(";")
|
|
123
|
+
if item.strip()
|
|
124
|
+
] if platform == "nt" else []
|
|
125
|
+
windows_extension_set = {item.casefold() for item in windows_extensions}
|
|
126
|
+
has_path = Path(command).is_absolute() or "/" in command or "\\" in command
|
|
127
|
+
if has_path:
|
|
128
|
+
requested = Path(command).expanduser()
|
|
129
|
+
suffix = requested.suffix.casefold()
|
|
130
|
+
candidates = (
|
|
131
|
+
[Path(str(requested) + extension) for extension in windows_extensions]
|
|
132
|
+
+ [requested]
|
|
133
|
+
if platform == "nt" and suffix not in windows_extension_set
|
|
134
|
+
else [requested]
|
|
135
|
+
)
|
|
136
|
+
for candidate in candidates:
|
|
137
|
+
if candidate.is_file():
|
|
138
|
+
return [str(candidate.resolve()), *list(argv[1:])]
|
|
139
|
+
raise ManagedCommandNotFound(
|
|
140
|
+
f"MANAGED_COMMAND_NOT_FOUND: 找不到可执行命令 {command}"
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
path_value = str(environ.get("PATH") or "")
|
|
144
|
+
resolved: str | None = None
|
|
145
|
+
if platform == "nt":
|
|
146
|
+
suffix = Path(command).suffix.casefold()
|
|
147
|
+
names = (
|
|
148
|
+
[command]
|
|
149
|
+
if suffix and suffix in windows_extension_set
|
|
150
|
+
else [*(command + item for item in windows_extensions), command]
|
|
151
|
+
)
|
|
152
|
+
for raw_directory in path_value.split(";"):
|
|
153
|
+
directory = raw_directory.strip().strip('"') or "."
|
|
154
|
+
for name in names:
|
|
155
|
+
candidate = Path(directory) / name
|
|
156
|
+
if candidate.is_file():
|
|
157
|
+
resolved = str(candidate.resolve())
|
|
158
|
+
break
|
|
159
|
+
if resolved is not None:
|
|
160
|
+
break
|
|
161
|
+
else:
|
|
162
|
+
resolved = shutil.which(command, path=path_value or None)
|
|
163
|
+
|
|
164
|
+
if resolved is None:
|
|
165
|
+
raise ManagedCommandNotFound(
|
|
166
|
+
f"MANAGED_COMMAND_NOT_FOUND: 找不到可执行命令 {command}"
|
|
167
|
+
)
|
|
168
|
+
return [resolved, *list(argv[1:])]
|
|
169
|
+
|
|
170
|
+
|
|
71
171
|
def _truthy(value: object) -> bool:
|
|
72
172
|
return str(value or "").strip().lower() in TRUTHY_VALUES
|
|
73
173
|
|
|
@@ -819,6 +919,8 @@ def run_managed_command(
|
|
|
819
919
|
if environ:
|
|
820
920
|
child_env.update({str(key): str(value) for key, value in environ.items()})
|
|
821
921
|
child_env.setdefault("PYTHONUNBUFFERED", "1")
|
|
922
|
+
resolved_argv = resolve_managed_argv(argv, environ=child_env)
|
|
923
|
+
validate_managed_argv(resolved_argv)
|
|
822
924
|
lower_process_priority()
|
|
823
925
|
|
|
824
926
|
output_stream = tempfile.TemporaryFile(mode="w+b") if capture_output else None
|
|
@@ -828,7 +930,7 @@ def run_managed_command(
|
|
|
828
930
|
cleanup_complete = False
|
|
829
931
|
try:
|
|
830
932
|
spawned = hprocess.spawn_structured_argv(
|
|
831
|
-
|
|
933
|
+
resolved_argv,
|
|
832
934
|
cwd=Path(cwd).resolve(),
|
|
833
935
|
environment=child_env,
|
|
834
936
|
owner_token=f"test-runner:{uuid.uuid4().hex}",
|
|
@@ -1133,6 +1235,32 @@ def _run_unittest(args: argparse.Namespace) -> int:
|
|
|
1133
1235
|
except TestRunAlreadyActive as exc:
|
|
1134
1236
|
print(str(exc), file=sys.stderr)
|
|
1135
1237
|
return 3
|
|
1238
|
+
except ManagedCommandNotFound as exc:
|
|
1239
|
+
print(
|
|
1240
|
+
json.dumps(
|
|
1241
|
+
{
|
|
1242
|
+
"ok": False,
|
|
1243
|
+
"code": "MANAGED_COMMAND_NOT_FOUND",
|
|
1244
|
+
"message": str(exc).split(":", 1)[-1].strip(),
|
|
1245
|
+
},
|
|
1246
|
+
ensure_ascii=False,
|
|
1247
|
+
),
|
|
1248
|
+
file=sys.stderr,
|
|
1249
|
+
)
|
|
1250
|
+
return 127
|
|
1251
|
+
except OSError as exc:
|
|
1252
|
+
print(
|
|
1253
|
+
json.dumps(
|
|
1254
|
+
{
|
|
1255
|
+
"ok": False,
|
|
1256
|
+
"code": "MANAGED_COMMAND_START_FAILED",
|
|
1257
|
+
"message": f"命令无法启动:{exc}",
|
|
1258
|
+
},
|
|
1259
|
+
ensure_ascii=False,
|
|
1260
|
+
),
|
|
1261
|
+
file=sys.stderr,
|
|
1262
|
+
)
|
|
1263
|
+
return 126
|
|
1136
1264
|
|
|
1137
1265
|
duration = time.monotonic() - started
|
|
1138
1266
|
status = "PASS" if not failures else "FAIL"
|
|
@@ -1264,6 +1392,32 @@ def _run_exec(args: argparse.Namespace) -> int:
|
|
|
1264
1392
|
except TestRunAlreadyActive as exc:
|
|
1265
1393
|
print(str(exc), file=sys.stderr)
|
|
1266
1394
|
return 3
|
|
1395
|
+
except ManagedCommandNotFound as exc:
|
|
1396
|
+
print(
|
|
1397
|
+
json.dumps(
|
|
1398
|
+
{
|
|
1399
|
+
"ok": False,
|
|
1400
|
+
"code": "MANAGED_COMMAND_NOT_FOUND",
|
|
1401
|
+
"message": str(exc).split(":", 1)[-1].strip(),
|
|
1402
|
+
},
|
|
1403
|
+
ensure_ascii=False,
|
|
1404
|
+
),
|
|
1405
|
+
file=sys.stderr,
|
|
1406
|
+
)
|
|
1407
|
+
return 127
|
|
1408
|
+
except OSError as exc:
|
|
1409
|
+
print(
|
|
1410
|
+
json.dumps(
|
|
1411
|
+
{
|
|
1412
|
+
"ok": False,
|
|
1413
|
+
"code": "MANAGED_COMMAND_START_FAILED",
|
|
1414
|
+
"message": f"命令无法启动:{exc}",
|
|
1415
|
+
},
|
|
1416
|
+
ensure_ascii=False,
|
|
1417
|
+
),
|
|
1418
|
+
file=sys.stderr,
|
|
1419
|
+
)
|
|
1420
|
+
return 126
|
|
1267
1421
|
if not result.process_tree_isolated:
|
|
1268
1422
|
print("PROCESS_TREE_ISOLATION_UNAVAILABLE", file=sys.stderr)
|
|
1269
1423
|
return 4
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
|
|
4
4
|
/harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=0da35fd0d75cbbb3; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -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=0da35fd0d75cbbb3; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|