@hunter-harness/workflow-harness 0.2.83 → 0.2.85
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 +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-push/SKILL.md +1 -1
- 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/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/general/claude-code/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-push/SKILL.md +1 -1
- 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/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codebuddy/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-push/SKILL.md +1 -1
- 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/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/general/codex/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/codex/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/general/codex/scripts/harness_runtime.py +125 -10
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-push/SKILL.md +1 -1
- 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/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/general/cursor/scripts/harness_archive.py +314 -11
- package/harness/bundles/general/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/general/cursor/scripts/harness_gate.py +10 -0
- package/harness/bundles/general/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/general/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/general/cursor/scripts/harness_runtime.py +125 -10
- 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 +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-push/SKILL.md +1 -1
- 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-run/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/claude-code/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/claude-code/scripts/harness_review.py +43 -5
- package/harness/bundles/java/claude-code/scripts/harness_runtime.py +125 -10
- 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 +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-push/SKILL.md +1 -1
- 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-run/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/codebuddy/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codebuddy/scripts/harness_runtime.py +125 -10
- 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 +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-push/SKILL.md +1 -1
- 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-run/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/java/codex/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/codex/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/codex/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/codex/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/codex/scripts/harness_review.py +43 -5
- package/harness/bundles/java/codex/scripts/harness_runtime.py +125 -10
- 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 +39 -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-pull/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-push/SKILL.md +1 -1
- 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-run/protocols.md +60 -0
- 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 +1 -1
- package/harness/bundles/java/cursor/scripts/harness_archive.py +314 -11
- package/harness/bundles/java/cursor/scripts/harness_fixback.py +451 -37
- package/harness/bundles/java/cursor/scripts/harness_gate.py +10 -0
- package/harness/bundles/java/cursor/scripts/harness_integration.py +156 -39
- package/harness/bundles/java/cursor/scripts/harness_review.py +43 -5
- package/harness/bundles/java/cursor/scripts/harness_runtime.py +125 -10
- 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
|
@@ -378,6 +378,121 @@ def _normalize_verify_command(
|
|
|
378
378
|
return result
|
|
379
379
|
|
|
380
380
|
|
|
381
|
+
# 合并态验证的兜底优先级:先跑覆盖面最广的,再退到窄的。profile 里这些命令
|
|
382
|
+
# 本来就是 ledger 一直在用的规范命令,没有理由让调用方在合并时另行手敲一遍。
|
|
383
|
+
MERGE_VERIFICATION_FALLBACK_ORDER = ("unitTestFull", "unitTest", "apiTest", "compile")
|
|
384
|
+
|
|
385
|
+
|
|
386
|
+
def verify_plan_recovery_action() -> str:
|
|
387
|
+
"""把 VERIFY_PLAN_MISSING 补成一句能照做的话。
|
|
388
|
+
|
|
389
|
+
此前它只说 "requires at least one executable command"。于是调用方要么去读
|
|
390
|
+
实现,要么随手塞一条命令让门过去——两条路都走过,后者更糟:它让"验证通过"
|
|
391
|
+
这句话失去含义。
|
|
392
|
+
"""
|
|
393
|
+
return (
|
|
394
|
+
"合并态验证没有可执行计划。两条正规出路:"
|
|
395
|
+
"(1) 在 .harness/config/build-profile.json 增加 mergeVerification.requiredOnMerge "
|
|
396
|
+
"声明合并时必跑的命令(推荐,一次配置长期生效);"
|
|
397
|
+
"(2) 本次用 --command 传入。注意 --command 可重复,每次只接**一条**命令,"
|
|
398
|
+
"且按 argv 直接执行(不经过 shell)——`&&`、`|`、`cd x && y` 都不成立,"
|
|
399
|
+
"需要拆成多个 --command。"
|
|
400
|
+
"示例:--command \"npm test\" --command \"npm run lint\"。"
|
|
401
|
+
)
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
def _fallback_merge_commands(profile: Any) -> list[dict[str, Any]]:
|
|
405
|
+
"""从 profile 已有的验证目标推导合并态计划。"""
|
|
406
|
+
commands = profile.get("commands") if isinstance(profile, dict) else None
|
|
407
|
+
if not isinstance(commands, dict):
|
|
408
|
+
return []
|
|
409
|
+
for name in MERGE_VERIFICATION_FALLBACK_ORDER:
|
|
410
|
+
entry = commands.get(name)
|
|
411
|
+
if not isinstance(entry, dict):
|
|
412
|
+
continue
|
|
413
|
+
argv = entry.get("argvTemplate")
|
|
414
|
+
if isinstance(argv, list) and argv and all(
|
|
415
|
+
isinstance(item, str) and item for item in argv
|
|
416
|
+
):
|
|
417
|
+
return [{
|
|
418
|
+
"command": [str(item) for item in argv],
|
|
419
|
+
"kind": "heavyweight",
|
|
420
|
+
"verification": name,
|
|
421
|
+
"source": "profile-fallback",
|
|
422
|
+
"reuseContext": {},
|
|
423
|
+
}]
|
|
424
|
+
raw = entry.get("command")
|
|
425
|
+
if isinstance(raw, str) and raw.strip():
|
|
426
|
+
return [{
|
|
427
|
+
"command": _split_verify_command(raw.strip()),
|
|
428
|
+
"kind": "heavyweight",
|
|
429
|
+
"verification": name,
|
|
430
|
+
"source": "profile-fallback",
|
|
431
|
+
"reuseContext": {},
|
|
432
|
+
}]
|
|
433
|
+
return []
|
|
434
|
+
|
|
435
|
+
|
|
436
|
+
def merge_verification_plan(profile: Any) -> list[dict[str, Any]]:
|
|
437
|
+
"""声明优先,推导兜底;两者都没有才返回空计划。"""
|
|
438
|
+
merge = profile.get("mergeVerification") if isinstance(profile, dict) else None
|
|
439
|
+
required = merge.get("requiredOnMerge") if isinstance(merge, dict) else None
|
|
440
|
+
if isinstance(required, list) and required:
|
|
441
|
+
plan: list[dict[str, Any]] = []
|
|
442
|
+
for index, item in enumerate(required):
|
|
443
|
+
if isinstance(item, str) and item.strip():
|
|
444
|
+
plan.append({
|
|
445
|
+
"command": _split_verify_command(item.strip()),
|
|
446
|
+
"kind": "smoke",
|
|
447
|
+
"verification": None,
|
|
448
|
+
"source": "profile",
|
|
449
|
+
"reuseContext": {},
|
|
450
|
+
})
|
|
451
|
+
continue
|
|
452
|
+
if not isinstance(item, dict):
|
|
453
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
454
|
+
raw = item.get("command")
|
|
455
|
+
if isinstance(raw, list) and raw and all(
|
|
456
|
+
isinstance(value, str) and value for value in raw
|
|
457
|
+
):
|
|
458
|
+
command = [str(value) for value in raw]
|
|
459
|
+
elif isinstance(raw, str) and raw.strip():
|
|
460
|
+
command = _split_verify_command(raw.strip())
|
|
461
|
+
else:
|
|
462
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
463
|
+
kind = str(item.get("kind") or "smoke")
|
|
464
|
+
plan.append({
|
|
465
|
+
"command": command,
|
|
466
|
+
"kind": kind,
|
|
467
|
+
"verification": item.get("verification"),
|
|
468
|
+
"source": "profile",
|
|
469
|
+
"reuseContext": {
|
|
470
|
+
field: str(item.get(field)).strip()
|
|
471
|
+
for field in (
|
|
472
|
+
"environmentHash",
|
|
473
|
+
"toolchainHash",
|
|
474
|
+
"lockHash",
|
|
475
|
+
"dbSchemaHash",
|
|
476
|
+
)
|
|
477
|
+
if str(item.get(field) or "").strip()
|
|
478
|
+
},
|
|
479
|
+
})
|
|
480
|
+
return plan
|
|
481
|
+
return _fallback_merge_commands(profile)
|
|
482
|
+
|
|
483
|
+
|
|
484
|
+
def verification_depth(plan: Sequence[dict[str, Any]]) -> str:
|
|
485
|
+
"""一条临时探测命令通过,不该和跑完整条链读起来一样。
|
|
486
|
+
|
|
487
|
+
2026-08-19 那次合并,verify 只跑了一个测试文件就 DONE,报告里看不出任何差别。
|
|
488
|
+
真正的全链验证是人工在事务外补跑的——账面与事实脱节,正是这种静默造成的。
|
|
489
|
+
"""
|
|
490
|
+
entries = [item for item in plan if isinstance(item, dict)]
|
|
491
|
+
if len(entries) == 1 and entries[0].get("source") not in {"profile", "profile-fallback"}:
|
|
492
|
+
return "thin"
|
|
493
|
+
return "declared"
|
|
494
|
+
|
|
495
|
+
|
|
381
496
|
class IntegrationError(Exception):
|
|
382
497
|
code = "INTEGRATION_ERROR"
|
|
383
498
|
|
|
@@ -1330,47 +1445,16 @@ class IntegrationTransaction:
|
|
|
1330
1445
|
return commands
|
|
1331
1446
|
|
|
1332
1447
|
def _required_on_merge_plan(self) -> list[dict[str, Any]]:
|
|
1333
|
-
"""
|
|
1448
|
+
"""合并态验证计划:profile 声明优先,缺声明时从既有验证目标推导。
|
|
1449
|
+
|
|
1450
|
+
此前只读 mergeVerification.requiredOnMerge,没配就是空计划 → VERIFY_PLAN_MISSING。
|
|
1451
|
+
但 profile 里本来就有 ledger 一直在用的规范命令;让调用方在合并时另行手敲一遍,
|
|
1452
|
+
换来的通常是一条随手写的探测命令。
|
|
1453
|
+
"""
|
|
1334
1454
|
import harness_profile
|
|
1335
1455
|
|
|
1336
1456
|
profile = harness_profile.load_profile(self.project_root)
|
|
1337
|
-
|
|
1338
|
-
required = merge.get("requiredOnMerge") if isinstance(merge, dict) else []
|
|
1339
|
-
if not isinstance(required, list):
|
|
1340
|
-
return []
|
|
1341
|
-
plan: list[dict[str, Any]] = []
|
|
1342
|
-
for index, item in enumerate(required):
|
|
1343
|
-
if isinstance(item, str) and item.strip():
|
|
1344
|
-
plan.append({"command": _split_verify_command(item.strip()), "kind": "smoke"})
|
|
1345
|
-
continue
|
|
1346
|
-
if not isinstance(item, dict):
|
|
1347
|
-
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
1348
|
-
raw = item.get("command")
|
|
1349
|
-
if isinstance(raw, list) and raw and all(isinstance(value, str) and value for value in raw):
|
|
1350
|
-
command = [str(value) for value in raw]
|
|
1351
|
-
elif isinstance(raw, str) and raw.strip():
|
|
1352
|
-
command = _split_verify_command(raw.strip())
|
|
1353
|
-
else:
|
|
1354
|
-
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
1355
|
-
kind, verification = _merge_command_kind(item)
|
|
1356
|
-
plan.append(
|
|
1357
|
-
{
|
|
1358
|
-
"command": command,
|
|
1359
|
-
"kind": kind,
|
|
1360
|
-
"verification": verification,
|
|
1361
|
-
"reuseContext": {
|
|
1362
|
-
field: str(item.get(field)).strip()
|
|
1363
|
-
for field in (
|
|
1364
|
-
"environmentHash",
|
|
1365
|
-
"toolchainHash",
|
|
1366
|
-
"lockHash",
|
|
1367
|
-
"dbSchemaHash",
|
|
1368
|
-
)
|
|
1369
|
-
if str(item.get(field) or "").strip()
|
|
1370
|
-
},
|
|
1371
|
-
}
|
|
1372
|
-
)
|
|
1373
|
-
return plan
|
|
1457
|
+
return merge_verification_plan(profile)
|
|
1374
1458
|
|
|
1375
1459
|
def verify(
|
|
1376
1460
|
self,
|
|
@@ -1384,7 +1468,12 @@ class IntegrationTransaction:
|
|
|
1384
1468
|
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
1385
1469
|
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
1386
1470
|
plan = [
|
|
1387
|
-
{
|
|
1471
|
+
{
|
|
1472
|
+
"command": list(command),
|
|
1473
|
+
"kind": "smoke",
|
|
1474
|
+
"verification": None,
|
|
1475
|
+
"source": "cli",
|
|
1476
|
+
}
|
|
1388
1477
|
for command in commands
|
|
1389
1478
|
]
|
|
1390
1479
|
plan.extend(profile_plan)
|
|
@@ -1399,7 +1488,21 @@ class IntegrationTransaction:
|
|
|
1399
1488
|
):
|
|
1400
1489
|
raise VerifyPlanMissingError(
|
|
1401
1490
|
"integration verification requires at least one executable command"
|
|
1491
|
+
" — " + verify_plan_recovery_action()
|
|
1402
1492
|
)
|
|
1493
|
+
# 计划先落盘:verify 失败时也要看得见"打算跑什么",
|
|
1494
|
+
# 而不是只剩一个 FAILED 状态。
|
|
1495
|
+
journal["verifyPlan"] = [
|
|
1496
|
+
{
|
|
1497
|
+
"command": list(item["command"]),
|
|
1498
|
+
"kind": item.get("kind"),
|
|
1499
|
+
"verification": item.get("verification"),
|
|
1500
|
+
"source": item.get("source"),
|
|
1501
|
+
}
|
|
1502
|
+
for item in plan
|
|
1503
|
+
]
|
|
1504
|
+
journal["verificationDepth"] = verification_depth(plan)
|
|
1505
|
+
self._save(journal)
|
|
1403
1506
|
evidence_change = self._bound_evidence_change(journal)
|
|
1404
1507
|
intg_root = Path(journal["integrationRoot"]).resolve()
|
|
1405
1508
|
results: list[dict[str, Any]] = []
|
|
@@ -2253,6 +2356,20 @@ class IntegrationTransaction:
|
|
|
2253
2356
|
"steps": journal["steps"],
|
|
2254
2357
|
"mergeCommit": journal.get("mergeCommit"),
|
|
2255
2358
|
"pushedHead": journal.get("pushedHead"),
|
|
2359
|
+
# "verify = DONE" 单独一句什么也没说明。把计划、来源和退出码一起回显,
|
|
2360
|
+
# 调用方才看得出这次合并到底验证了什么。
|
|
2361
|
+
"verifyPlan": journal.get("verifyPlan") or [],
|
|
2362
|
+
"verificationDepth": journal.get("verificationDepth"),
|
|
2363
|
+
"verifyResults": [
|
|
2364
|
+
{
|
|
2365
|
+
"command": item.get("command"),
|
|
2366
|
+
"exitCode": item.get("exitCode"),
|
|
2367
|
+
"reused": bool(item.get("reused")),
|
|
2368
|
+
"durationMs": item.get("durationMs"),
|
|
2369
|
+
}
|
|
2370
|
+
for item in (journal.get("verifyResults") or [])
|
|
2371
|
+
if isinstance(item, dict)
|
|
2372
|
+
],
|
|
2256
2373
|
}
|
|
2257
2374
|
|
|
2258
2375
|
|
|
@@ -515,13 +515,42 @@ def cmd_validate_codegraph_identity(args: argparse.Namespace) -> int:
|
|
|
515
515
|
return _emit(payload, as_json=True)
|
|
516
516
|
|
|
517
517
|
|
|
518
|
+
def _input_document(args: argparse.Namespace) -> Any:
|
|
519
|
+
"""--input <file> 或 --stdin,二选一。
|
|
520
|
+
|
|
521
|
+
为了把一段 JSON 交给命令而先在 runtime/ 落一个临时文件,既多一次往返,
|
|
522
|
+
也给 runtime/ 又添一件没人清的草稿。
|
|
523
|
+
"""
|
|
524
|
+
use_stdin = bool(getattr(args, "stdin", False))
|
|
525
|
+
raw_input_path = getattr(args, "input", None)
|
|
526
|
+
if use_stdin and raw_input_path:
|
|
527
|
+
raise ValueError("--input 与 --stdin 只能二选一")
|
|
528
|
+
if use_stdin:
|
|
529
|
+
text = getattr(args, "_stdin_text", None)
|
|
530
|
+
if text is None:
|
|
531
|
+
text = sys.stdin.read()
|
|
532
|
+
return json.loads(text)
|
|
533
|
+
if not raw_input_path:
|
|
534
|
+
raise ValueError("需要 --input <file> 或 --stdin")
|
|
535
|
+
return _read_json(Path(raw_input_path))
|
|
536
|
+
|
|
537
|
+
|
|
518
538
|
def cmd_write_findings(args: argparse.Namespace) -> int:
|
|
519
|
-
|
|
539
|
+
try:
|
|
540
|
+
doc = _input_document(args)
|
|
541
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
542
|
+
return _emit({"ok": False, "code": "FINDINGS_INPUT_INVALID", "error": str(exc)}, as_json=True)
|
|
520
543
|
return _emit(write_findings(Path(args.change_dir), doc), as_json=True)
|
|
521
544
|
|
|
522
545
|
|
|
523
546
|
def cmd_write_dispositions(args: argparse.Namespace) -> int:
|
|
524
|
-
|
|
547
|
+
try:
|
|
548
|
+
doc = _input_document(args)
|
|
549
|
+
except (ValueError, json.JSONDecodeError) as exc:
|
|
550
|
+
return _emit(
|
|
551
|
+
{"ok": False, "code": "DISPOSITIONS_INPUT_INVALID", "error": str(exc)},
|
|
552
|
+
as_json=True,
|
|
553
|
+
)
|
|
525
554
|
return _emit(write_dispositions(Path(args.change_dir), doc), as_json=True)
|
|
526
555
|
|
|
527
556
|
|
|
@@ -547,12 +576,18 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
547
576
|
|
|
548
577
|
p_findings = sub.add_parser("write-findings")
|
|
549
578
|
p_findings.add_argument("--change-dir", required=True)
|
|
550
|
-
p_findings.add_argument("--input",
|
|
579
|
+
p_findings.add_argument("--input", help="findings JSON 文件路径")
|
|
580
|
+
p_findings.add_argument(
|
|
581
|
+
"--stdin", action="store_true", help="从标准输入读 JSON,免落临时文件"
|
|
582
|
+
)
|
|
551
583
|
p_findings.set_defaults(func=cmd_write_findings)
|
|
552
584
|
|
|
553
585
|
p_dispositions = sub.add_parser("write-dispositions")
|
|
554
586
|
p_dispositions.add_argument("--change-dir", required=True)
|
|
555
|
-
p_dispositions.add_argument("--input",
|
|
587
|
+
p_dispositions.add_argument("--input", help="dispositions JSON 文件路径")
|
|
588
|
+
p_dispositions.add_argument(
|
|
589
|
+
"--stdin", action="store_true", help="从标准输入读 JSON,免落临时文件"
|
|
590
|
+
)
|
|
556
591
|
p_dispositions.set_defaults(func=cmd_write_dispositions)
|
|
557
592
|
|
|
558
593
|
p_status = sub.add_parser("status")
|
|
@@ -562,9 +597,12 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
562
597
|
return parser
|
|
563
598
|
|
|
564
599
|
|
|
565
|
-
def main(argv: list[str] | None = None) -> int:
|
|
600
|
+
def main(argv: list[str] | None = None, *, stdin_text: str | None = None) -> int:
|
|
566
601
|
parser = build_parser()
|
|
567
602
|
args = parser.parse_args(argv)
|
|
603
|
+
if stdin_text is not None:
|
|
604
|
+
# 测试注入用;正式路径仍然读真正的 stdin。
|
|
605
|
+
args._stdin_text = stdin_text
|
|
568
606
|
return int(args.func(args))
|
|
569
607
|
|
|
570
608
|
|
|
@@ -18,6 +18,8 @@ import threading
|
|
|
18
18
|
import time
|
|
19
19
|
import uuid
|
|
20
20
|
from pathlib import Path
|
|
21
|
+
import fnmatch
|
|
22
|
+
from collections.abc import Sequence
|
|
21
23
|
from typing import Any, Callable
|
|
22
24
|
|
|
23
25
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
@@ -35,6 +37,11 @@ RUN_TERMINAL_STATUSES = {"OK", "FAIL", "INCOMPLETE", "CANCELLED"}
|
|
|
35
37
|
_CHANGE_ID = re.compile(r"^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$")
|
|
36
38
|
SECRET_SCAN_RULES_VERSION = "secret-scan-v2"
|
|
37
39
|
SECRET_SCAN_RECEIPT_REL = Path("meta") / "secret-scan-receipt.json"
|
|
40
|
+
# 变更目录里只有一部分内容会被发布:归档包成员是 reports/final/summary-data.json、
|
|
41
|
+
# spec/**.md、plans/**.md、candidates/knowledge.json、meta/archive-meta.md、
|
|
42
|
+
# meta/change-context.json。runtime/ 是各阶段的草稿(review 的 diff、临时校验
|
|
43
|
+
# 脚本、命令输出重定向),从不入包——拿它挡发布,挡的是永远不会离开本机的字节。
|
|
44
|
+
PUBLICATION_EXCLUDED_DIRS: tuple[str, ...] = ("runtime",)
|
|
38
45
|
_SENSITIVE_ASSIGNMENT = re.compile(
|
|
39
46
|
rb"(?i)[\"']?(?:password|passwd|token|secret|cookie|database[_-]?url)"
|
|
40
47
|
rb"[\"']?\s*[:=]\s*[\"']?(?P<value>[^\s\"']{4,})"
|
|
@@ -126,7 +133,15 @@ def _sha256_json(value: Any) -> str:
|
|
|
126
133
|
return "sha256:" + hashlib.sha256(encoded).hexdigest()
|
|
127
134
|
|
|
128
135
|
|
|
129
|
-
def
|
|
136
|
+
def _is_excluded(relative: str, exclude_dirs: Sequence[str]) -> bool:
|
|
137
|
+
"""Path is inside one of the excluded top-level directories."""
|
|
138
|
+
head = relative.split("/", 1)[0]
|
|
139
|
+
return head in exclude_dirs
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _publishable_tree_digest(
|
|
143
|
+
root: Path, *, exclude_dirs: Sequence[str] = ()
|
|
144
|
+
) -> str:
|
|
130
145
|
"""Hash publishable bytes while excluding the self-referential scan receipt."""
|
|
131
146
|
root = root.expanduser().resolve()
|
|
132
147
|
digest = hashlib.sha256()
|
|
@@ -136,6 +151,10 @@ def _publishable_tree_digest(root: Path) -> str:
|
|
|
136
151
|
for path in sorted(root.rglob("*")):
|
|
137
152
|
if not path.is_file() or path.resolve() == receipt:
|
|
138
153
|
continue
|
|
154
|
+
if exclude_dirs and _is_excluded(
|
|
155
|
+
path.relative_to(root).as_posix(), exclude_dirs
|
|
156
|
+
):
|
|
157
|
+
continue
|
|
139
158
|
relative = path.relative_to(root).as_posix()
|
|
140
159
|
data = path.read_bytes()
|
|
141
160
|
digest.update(relative.encode("utf-8"))
|
|
@@ -215,8 +234,73 @@ def _secure_private_path(path: Path, *, directory: bool) -> str:
|
|
|
215
234
|
return "WINDOWS_ACL_UNVERIFIED"
|
|
216
235
|
|
|
217
236
|
|
|
218
|
-
|
|
219
|
-
|
|
237
|
+
# runtime/ 下哪些是**过程草稿**。白名单式判断:只删明确列举的形态,
|
|
238
|
+
# 其余一律保留——门禁、租约、恢复和证据都住在这棵树里,宁可留垃圾也不能误删。
|
|
239
|
+
SCRATCH_PATTERNS: tuple[str, ...] = (
|
|
240
|
+
"*.patch",
|
|
241
|
+
"*.diff",
|
|
242
|
+
"*-input.json",
|
|
243
|
+
"*-input[0-9].json",
|
|
244
|
+
"findings-rereview.json",
|
|
245
|
+
"archive-out*.json",
|
|
246
|
+
"fixback-*-check.*",
|
|
247
|
+
)
|
|
248
|
+
# 即便命中上面的形态也必须留下的名字:它们是运行态,不是草稿。
|
|
249
|
+
SCRATCH_KEEP: frozenset[str] = frozenset({
|
|
250
|
+
"context-lease.json",
|
|
251
|
+
"fixback-session.json",
|
|
252
|
+
"preflight.json",
|
|
253
|
+
})
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def sweep_scratch(change_root: Path, *, dry_run: bool = False) -> dict[str, Any]:
|
|
257
|
+
"""清掉 runtime/ 顶层的过程草稿,保留运行态、会话、证据与报告。
|
|
258
|
+
|
|
259
|
+
一轮流程下来 runtime/ 会堆满 diff、临时校验脚本和命令输出重定向。没人清,
|
|
260
|
+
而且它们会被其他门禁读到——2026-08-19 那次,review 自己生成的两个 diff
|
|
261
|
+
直接把归档挡住了。
|
|
262
|
+
|
|
263
|
+
只扫 runtime/ 顶层文件:run-sessions/ 等子目录是托管会话证据,不进扫描。
|
|
264
|
+
"""
|
|
265
|
+
change_root = change_root.expanduser().resolve()
|
|
266
|
+
runtime_dir = change_root / "runtime"
|
|
267
|
+
removed: list[str] = []
|
|
268
|
+
if not runtime_dir.is_dir():
|
|
269
|
+
return {"ok": True, "code": "SCRATCH_SWEPT", "removed": [], "dryRun": dry_run}
|
|
270
|
+
errors: list[dict[str, str]] = []
|
|
271
|
+
for path in sorted(runtime_dir.iterdir()):
|
|
272
|
+
if not path.is_file() or path.name in SCRATCH_KEEP:
|
|
273
|
+
continue
|
|
274
|
+
if not any(fnmatch.fnmatch(path.name, pattern) for pattern in SCRATCH_PATTERNS):
|
|
275
|
+
continue
|
|
276
|
+
relative = path.relative_to(change_root).as_posix()
|
|
277
|
+
if dry_run:
|
|
278
|
+
removed.append(relative)
|
|
279
|
+
continue
|
|
280
|
+
try:
|
|
281
|
+
path.unlink()
|
|
282
|
+
except OSError as exc:
|
|
283
|
+
errors.append({"path": relative, "error": str(exc)})
|
|
284
|
+
continue
|
|
285
|
+
removed.append(relative)
|
|
286
|
+
return {
|
|
287
|
+
"ok": not errors,
|
|
288
|
+
"code": "SCRATCH_SWEPT" if not errors else "SCRATCH_SWEEP_PARTIAL",
|
|
289
|
+
"removed": removed,
|
|
290
|
+
"errors": errors,
|
|
291
|
+
"dryRun": dry_run,
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
|
|
295
|
+
def _sensitive_candidates(
|
|
296
|
+
root: Path, *, exclude_dirs: Sequence[str] = ()
|
|
297
|
+
) -> list[dict[str, Any]]:
|
|
298
|
+
"""Find high-confidence plaintext assignments without treating prose as a secret.
|
|
299
|
+
|
|
300
|
+
``exclude_dirs`` scopes the scan to what a caller actually publishes. The
|
|
301
|
+
default stays whole-tree: the quarantine flow depends on seeing legacy
|
|
302
|
+
plaintext anywhere under the change, including scratch directories.
|
|
303
|
+
"""
|
|
220
304
|
# Compare by relative path, not by resolve() per file: this loop covers the
|
|
221
305
|
# whole change tree, and on Windows each resolve() is a realpath syscall.
|
|
222
306
|
receipt_rel = SECRET_SCAN_RECEIPT_REL.as_posix()
|
|
@@ -224,7 +308,10 @@ def _sensitive_candidates(root: Path) -> list[dict[str, Any]]:
|
|
|
224
308
|
for path in sorted(root.rglob("*")):
|
|
225
309
|
if not path.is_file():
|
|
226
310
|
continue
|
|
227
|
-
|
|
311
|
+
relative = path.relative_to(root).as_posix()
|
|
312
|
+
if relative == receipt_rel:
|
|
313
|
+
continue
|
|
314
|
+
if exclude_dirs and _is_excluded(relative, exclude_dirs):
|
|
228
315
|
continue
|
|
229
316
|
try:
|
|
230
317
|
raw = path.read_bytes()
|
|
@@ -263,17 +350,21 @@ def _is_sensitive_placeholder(value: bytes) -> bool:
|
|
|
263
350
|
)
|
|
264
351
|
|
|
265
352
|
|
|
266
|
-
def publishable_tree_digest(
|
|
353
|
+
def publishable_tree_digest(
|
|
354
|
+
root: Path, *, exclude_dirs: Sequence[str] = ()
|
|
355
|
+
) -> str:
|
|
267
356
|
"""Public digest helper shared by the archive publication gate."""
|
|
268
|
-
return _publishable_tree_digest(root)
|
|
357
|
+
return _publishable_tree_digest(root, exclude_dirs=exclude_dirs)
|
|
269
358
|
|
|
270
359
|
|
|
271
360
|
def sensitive_evidence_receipt_path(change_root: Path) -> Path:
|
|
272
361
|
return _sensitive_evidence_receipt_path(change_root)
|
|
273
362
|
|
|
274
363
|
|
|
275
|
-
def sensitive_evidence_candidates(
|
|
276
|
-
|
|
364
|
+
def sensitive_evidence_candidates(
|
|
365
|
+
root: Path, *, exclude_dirs: Sequence[str] = ()
|
|
366
|
+
) -> list[dict[str, Any]]:
|
|
367
|
+
return _sensitive_candidates(root, exclude_dirs=exclude_dirs)
|
|
277
368
|
|
|
278
369
|
|
|
279
370
|
def _build_sensitive_scan_receipt(
|
|
@@ -282,6 +373,7 @@ def _build_sensitive_scan_receipt(
|
|
|
282
373
|
entries: list[dict[str, Any]],
|
|
283
374
|
unresolved: list[dict[str, Any]],
|
|
284
375
|
status: str,
|
|
376
|
+
exclude_dirs: Sequence[str] = (),
|
|
285
377
|
) -> dict[str, Any]:
|
|
286
378
|
change_root = change_root.expanduser().resolve()
|
|
287
379
|
return {
|
|
@@ -291,7 +383,12 @@ def _build_sensitive_scan_receipt(
|
|
|
291
383
|
"status": status,
|
|
292
384
|
"unresolvedFailures": unresolved,
|
|
293
385
|
"entries": entries,
|
|
294
|
-
"publishableTreeDigest": _publishable_tree_digest(
|
|
386
|
+
"publishableTreeDigest": _publishable_tree_digest(
|
|
387
|
+
change_root, exclude_dirs=exclude_dirs
|
|
388
|
+
),
|
|
389
|
+
# 收据自述扫描范围:复算摘要的一方据此排除同样的目录,
|
|
390
|
+
# 否则两侧口径不同会误报 digest 漂移。
|
|
391
|
+
"publicationExcludedDirs": list(exclude_dirs),
|
|
295
392
|
"publicationExcluded": True,
|
|
296
393
|
"recordedAt": now_iso(),
|
|
297
394
|
}
|
|
@@ -303,6 +400,7 @@ def _write_sensitive_scan_receipt(
|
|
|
303
400
|
entries: list[dict[str, Any]],
|
|
304
401
|
unresolved: list[dict[str, Any]],
|
|
305
402
|
status: str,
|
|
403
|
+
exclude_dirs: Sequence[str] = (),
|
|
306
404
|
) -> dict[str, Any]:
|
|
307
405
|
change_root = change_root.expanduser().resolve()
|
|
308
406
|
receipt = _build_sensitive_scan_receipt(
|
|
@@ -310,6 +408,7 @@ def _write_sensitive_scan_receipt(
|
|
|
310
408
|
entries=entries,
|
|
311
409
|
unresolved=unresolved,
|
|
312
410
|
status=status,
|
|
411
|
+
exclude_dirs=exclude_dirs,
|
|
313
412
|
)
|
|
314
413
|
atomic_write_json(_sensitive_evidence_receipt_path(change_root), receipt)
|
|
315
414
|
return receipt
|
|
@@ -463,6 +562,7 @@ def refresh_sensitive_evidence_scan_receipt(
|
|
|
463
562
|
change_root: Path,
|
|
464
563
|
*,
|
|
465
564
|
persist: bool = True,
|
|
565
|
+
exclude_dirs: Sequence[str] = (),
|
|
466
566
|
) -> dict[str, Any]:
|
|
467
567
|
"""Rescan current bytes while preserving valid quarantine audit entries."""
|
|
468
568
|
change_root = change_root.expanduser().resolve()
|
|
@@ -508,7 +608,7 @@ def refresh_sensitive_evidence_scan_receipt(
|
|
|
508
608
|
}
|
|
509
609
|
entries = list(prior_entries)
|
|
510
610
|
|
|
511
|
-
unresolved = _sensitive_candidates(change_root)
|
|
611
|
+
unresolved = _sensitive_candidates(change_root, exclude_dirs=exclude_dirs)
|
|
512
612
|
status = "FAIL" if unresolved else "QUARANTINED" if entries else "OK"
|
|
513
613
|
if persist:
|
|
514
614
|
try:
|
|
@@ -517,6 +617,7 @@ def refresh_sensitive_evidence_scan_receipt(
|
|
|
517
617
|
entries=entries,
|
|
518
618
|
unresolved=unresolved,
|
|
519
619
|
status=status,
|
|
620
|
+
exclude_dirs=exclude_dirs,
|
|
520
621
|
)
|
|
521
622
|
except OSError as exc:
|
|
522
623
|
return {
|
|
@@ -531,6 +632,7 @@ def refresh_sensitive_evidence_scan_receipt(
|
|
|
531
632
|
entries=entries,
|
|
532
633
|
unresolved=unresolved,
|
|
533
634
|
status=status,
|
|
635
|
+
exclude_dirs=exclude_dirs,
|
|
534
636
|
)
|
|
535
637
|
return {
|
|
536
638
|
"ok": not unresolved,
|
|
@@ -2019,6 +2121,15 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
2019
2121
|
help="override the private root; must be outside the project root and on its drive",
|
|
2020
2122
|
)
|
|
2021
2123
|
p_quarantine.add_argument("--json", action="store_true")
|
|
2124
|
+
p_sweep = sub.add_parser(
|
|
2125
|
+
"sweep-scratch",
|
|
2126
|
+
help="删除 runtime/ 顶层的过程草稿(diff、临时输入、命令输出)",
|
|
2127
|
+
)
|
|
2128
|
+
p_sweep.add_argument("--change-dir", required=True)
|
|
2129
|
+
p_sweep.add_argument(
|
|
2130
|
+
"--dry-run", action="store_true", help="只列出会删什么,不真删"
|
|
2131
|
+
)
|
|
2132
|
+
p_sweep.add_argument("--json", action="store_true")
|
|
2022
2133
|
p_adapter = sub.add_parser("adapter")
|
|
2023
2134
|
p_adapter.add_argument("--agent", choices=sorted(_ADAPTERS), required=True)
|
|
2024
2135
|
p_adapter.add_argument("--change", required=True)
|
|
@@ -2069,6 +2180,10 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
2069
2180
|
result = doctor(Path(args.project), Path(args.change_dir), agent=args.agent)
|
|
2070
2181
|
elif args.command == "quarantine-evidence":
|
|
2071
2182
|
result = cmd_quarantine_evidence(args)
|
|
2183
|
+
elif args.command == "sweep-scratch":
|
|
2184
|
+
result = sweep_scratch(
|
|
2185
|
+
Path(args.change_dir), dry_run=bool(args.dry_run)
|
|
2186
|
+
)
|
|
2072
2187
|
elif args.command == "adapter":
|
|
2073
2188
|
result = {"ok": True, "action": "adapter", **adapter_worktree(args.agent, args.change)}
|
|
2074
2189
|
elif args.command == "_worker":
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=123134be5333129c; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(awk *)
|
|
22
22
|
- Bash(curl *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=123134be5333129c; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-archive — 变更归档
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -151,6 +151,44 @@ disallowed-tools:
|
|
|
151
151
|
| `SENSITIVE_EVIDENCE_UNQUARANTINED`(默认只告警,`HUNTER_HARNESS_SENSITIVE_SCAN=block` 时才阻断) | `python <skills-root>/scripts/harness_runtime.py quarantine-evidence --project . --change-dir ".harness/changes/<cn>" --file "<相对 change-dir 的路径>" --reason "<为什么是敏感证据>" --json`(`--file` 可重复)。私有根默认已与项目同盘、且在项目根之外——**不要**手工指定项目内的路径,归档的密钥扫描会以 `SECRET_SCAN_PRIVATE_PATH_IN_COPY_ROOT` 拒绝 |
|
|
152
152
|
| `DIFF_ZERO_WITH_NONEMPTY_COMMIT`(提交范围非空但 filesChanged=0) | 契约缺 `ownership.productPaths`,全部改动被判为 `foreignPaths`。用 `python <skills-root>/scripts/harness_change.py declare-ownership --change <cn> --product-path "<目录前缀或精确文件>" --json` 按计划的实际改动范围声明(可重复;只收精确路径,不支持通配)。**不要**手改 `change-context.json` |
|
|
153
153
|
|
|
154
|
+
### 二·A·1、发布内容预检与服务端 422
|
|
155
|
+
|
|
156
|
+
`status`(`execute` 的预检)会用**与服务端同源的规则**(`packages/core` 的
|
|
157
|
+
`scanSensitiveFiles`)扫描**真正会进 ZIP 的那批文件**——`reports/final/summary-data.json`、
|
|
158
|
+
`spec/**.md`、`plans/**.md`、`candidates/knowledge.json`、`meta/archive-meta.md`、
|
|
159
|
+
`meta/change-context.json`。`runtime/` 是过程草稿,从不入包,也不进这道扫描。
|
|
160
|
+
|
|
161
|
+
结果在 `checks.publication_content_scan`:
|
|
162
|
+
|
|
163
|
+
- `blocked=false` → 上传不会因内容被拒
|
|
164
|
+
- `blocked=true` → **服务端会以 HTTP 422 `archive contains sensitive content` 拒收**。
|
|
165
|
+
归档不阻断(服务端策略不归本地裁决),但必须在此时处理,别等跑完全流程才发现
|
|
166
|
+
- `reasonCode=PUBLICATION_CONTENT_SCAN_UNAVAILABLE` → CLI 不可用,预检没跑;归档继续,
|
|
167
|
+
上传仍可能 422
|
|
168
|
+
|
|
169
|
+
每条 finding 都带 `rule_id / path / line / column / overridable / recovery_action`。
|
|
170
|
+
|
|
171
|
+
**处置:**
|
|
172
|
+
|
|
173
|
+
| 命中类型 | 出路 |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `overridable=true`(medium/low,如 `HH_INTERNAL_ADDRESS` 内网地址、`HH_WINDOWS_ABSOLUTE_PATH`) | 若确属设计固有内容,在源文件该行附近加行内标注:`<!-- hunter-harness-ignore: <RULE_ID> reason=<简短理由> -->`,然后 `republish` 重建重传 |
|
|
176
|
+
| `overridable=false`(high,如私钥、真实 token) | **不提供豁免。** 必须真正脱敏后重新打包 |
|
|
177
|
+
|
|
178
|
+
> ⚠️ 行内标注是**申报**,不是绕过:它把"这是设计决策"写进文档本身,可审计、可追溯。
|
|
179
|
+
> **不要**为了过扫描去删改设计文档的事实内容——那是篡改证据。
|
|
180
|
+
>
|
|
181
|
+
> ✅ 行内标注**服务端确认生效**:平台归档入口跑的是同一个 `scanSensitiveFiles`,
|
|
182
|
+
> 它直接从文件内容里解析 `hunter-harness-ignore`,命中转为 `overridden`、不再阻断。
|
|
183
|
+
> 上传端点只收裸 ZIP、没有独立申报通道,靠的就是这条写在文档里的标注。
|
|
184
|
+
>
|
|
185
|
+
> ⚠️ 一处已知的服务端滞后:**不带凭据的连接串**(如 `postgres://dbhost:5432/appdb`)
|
|
186
|
+
> 本地预检判 `low`/可豁免,而线上服务端目前仍判 `high`——high 不接受行内标注,
|
|
187
|
+
> 这类设计文档暂时无法上传。平台侧修复已提交(见 hunter-platform
|
|
188
|
+
> `fix(archive): 归档 422 给出可执行出路`),**部署后**两侧一致。在那之前遇到这一条:
|
|
189
|
+
> 等平台部署,不要为了过扫描删掉连接串。带凭据的连接串(`user:pass@host`)两侧都是
|
|
190
|
+
> `high`,必须真正脱敏。
|
|
191
|
+
|
|
154
192
|
### 二·B、归档补传(上传失败或历史归档缺条目)
|
|
155
193
|
|
|
156
194
|
归档上传成功后 ZIP 与回执会被清理,旧版本产生的归档从未入 outbox——所以
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=123134be5333129c; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=123134be5333129c; overlay=java; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-ingest
|
|
13
13
|
|
|
14
14
|
知识 ingest 完全由 Hunter Platform 负责。归档完成时,客户端生成一个确定性 ZIP;
|
|
@@ -8,7 +8,7 @@ disallowed-tools:
|
|
|
8
8
|
- Bash(git *)
|
|
9
9
|
- Bash(curl *)
|
|
10
10
|
---
|
|
11
|
-
<!-- generated by harness_deploy.py; core=
|
|
11
|
+
<!-- generated by harness_deploy.py; core=123134be5333129c; overlay=java; agent=claude-code; do not edit -->
|
|
12
12
|
# harness-knowledge-query
|
|
13
13
|
|
|
14
14
|
项目知识以 Hunter Platform 的服务端索引为唯一真源。客户端只提交查询并消费结果:
|