@hunter-harness/workflow-harness 0.2.21 → 0.2.23
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 +12 -3
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/claude-code/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +1 -0
- package/harness/bundles/general/claude-code/harness-plan/reference.md +17 -15
- 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-submit/SKILL.md +3 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +7 -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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +1168 -28
- package/harness/bundles/general/claude-code/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +93 -14
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/codebuddy/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +1 -0
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +17 -15
- 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-submit/SKILL.md +3 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +7 -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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +1168 -28
- package/harness/bundles/general/codebuddy/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +93 -14
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/codex/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/codex/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/general/codex/harness-plan/checklist.md +1 -0
- package/harness/bundles/general/codex/harness-plan/reference.md +17 -15
- 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-submit/SKILL.md +3 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +7 -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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/codex/scripts/harness_archive.py +1168 -28
- package/harness/bundles/general/codex/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +93 -14
- package/harness/bundles/general/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +30 -8
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +12 -3
- package/harness/bundles/general/cursor/harness-archive/checklist.md +14 -0
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/general/cursor/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/general/cursor/harness-plan/checklist.md +1 -0
- package/harness/bundles/general/cursor/harness-plan/reference.md +17 -15
- 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-submit/SKILL.md +3 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +7 -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/harness-test/checklist.md +6 -0
- package/harness/bundles/general/cursor/scripts/harness_archive.py +1168 -28
- package/harness/bundles/general/cursor/scripts/harness_environment.py +459 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +93 -14
- package/harness/bundles/general/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/general/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +30 -8
- 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 +12 -3
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +1 -0
- package/harness/bundles/java/claude-code/harness-plan/reference.md +17 -15
- 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 +3 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +7 -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 +1168 -28
- package/harness/bundles/java/claude-code/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +93 -14
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +30 -8
- 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 +12 -3
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/codebuddy/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +1 -0
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +17 -15
- 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 +3 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +7 -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 +1168 -28
- package/harness/bundles/java/codebuddy/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +93 -14
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +30 -8
- 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 +12 -3
- package/harness/bundles/java/codex/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/codex/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/java/codex/harness-plan/checklist.md +1 -0
- package/harness/bundles/java/codex/harness-plan/reference.md +17 -15
- 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 +3 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +7 -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 +1168 -28
- package/harness/bundles/java/codex/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +93 -14
- package/harness/bundles/java/codex/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/codex/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/codex/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +30 -8
- 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 +12 -3
- package/harness/bundles/java/cursor/harness-archive/checklist.md +14 -0
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +24 -3
- package/harness/bundles/java/cursor/harness-archive/templates/summary-data-template.json +11 -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 +2 -1
- package/harness/bundles/java/cursor/harness-plan/checklist.md +1 -0
- package/harness/bundles/java/cursor/harness-plan/reference.md +17 -15
- 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 +3 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +7 -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 +1168 -28
- package/harness/bundles/java/cursor/scripts/harness_environment.py +459 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +93 -14
- package/harness/bundles/java/cursor/scripts/harness_integration.py +126 -14
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +30 -6
- package/harness/bundles/java/cursor/scripts/harness_profile.py +33 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +30 -8
- package/harness/manifests/general/claude-code.json +29 -25
- package/harness/manifests/general/codebuddy.json +29 -25
- package/harness/manifests/general/codex.json +29 -25
- package/harness/manifests/general/cursor.json +29 -25
- package/harness/manifests/java/claude-code.json +30 -26
- package/harness/manifests/java/codebuddy.json +30 -26
- package/harness/manifests/java/codex.json +30 -26
- package/harness/manifests/java/cursor.json +30 -26
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -43,6 +43,7 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
43
43
|
sys.stderr.reconfigure(encoding="utf-8")
|
|
44
44
|
|
|
45
45
|
STEP_ORDER = ["preflight", "prepare", "merge", "verify", "push", "cleanup"]
|
|
46
|
+
DEFAULT_VERIFY_TIMEOUT_SECONDS = 1800
|
|
46
47
|
HEAVY_WORKTREE_ROOTS = (
|
|
47
48
|
"node_modules", ".venv", "venv", "build", "dist", "target", ".cache", "__pycache__"
|
|
48
49
|
)
|
|
@@ -56,6 +57,48 @@ def now_iso() -> str:
|
|
|
56
57
|
return dt.datetime.now().astimezone().isoformat(timespec="milliseconds")
|
|
57
58
|
|
|
58
59
|
|
|
60
|
+
def _split_verify_command(raw: str, *, platform: str = sys.platform) -> list[str]:
|
|
61
|
+
# Harness profile/CLI commands use one portable quoting contract. Keep
|
|
62
|
+
# POSIX tokenization on Windows too; executable normalization happens later.
|
|
63
|
+
return shlex.split(raw, posix=True)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _dedupe_verify_commands(
|
|
67
|
+
commands: Sequence[Sequence[str]],
|
|
68
|
+
) -> list[list[str]]:
|
|
69
|
+
"""Preserve order while preventing explicit/profile duplicate execution."""
|
|
70
|
+
result: list[list[str]] = []
|
|
71
|
+
seen: set[tuple[str, ...]] = set()
|
|
72
|
+
for command in commands:
|
|
73
|
+
normalized = tuple(str(item) for item in command)
|
|
74
|
+
if normalized in seen:
|
|
75
|
+
continue
|
|
76
|
+
seen.add(normalized)
|
|
77
|
+
result.append(list(normalized))
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _normalize_verify_command(
|
|
82
|
+
command: Sequence[str],
|
|
83
|
+
*,
|
|
84
|
+
platform: str = sys.platform,
|
|
85
|
+
resolver: Callable[[str], str | None] = shutil.which,
|
|
86
|
+
) -> list[str]:
|
|
87
|
+
"""Resolve Windows package-manager wrappers without enabling shell mode."""
|
|
88
|
+
result = [str(item) for item in command]
|
|
89
|
+
if not result or not platform.lower().startswith("win"):
|
|
90
|
+
return result
|
|
91
|
+
executable = Path(result[0]).name.lower()
|
|
92
|
+
if Path(executable).suffix:
|
|
93
|
+
return result
|
|
94
|
+
if executable not in {"npm", "npx", "pnpm", "yarn", "corepack"}:
|
|
95
|
+
return result
|
|
96
|
+
resolved = resolver(executable + ".cmd") or resolver(executable)
|
|
97
|
+
if resolved:
|
|
98
|
+
result[0] = resolved
|
|
99
|
+
return result
|
|
100
|
+
|
|
101
|
+
|
|
59
102
|
class IntegrationError(Exception):
|
|
60
103
|
code = "INTEGRATION_ERROR"
|
|
61
104
|
|
|
@@ -224,6 +267,11 @@ def load_journal(project_root: Path, transaction_id: str) -> dict[str, Any]:
|
|
|
224
267
|
def _pid_alive(pid: int) -> bool:
|
|
225
268
|
if pid <= 0:
|
|
226
269
|
return False
|
|
270
|
+
# On Windows, os.kill(pid, 0) can transiently report EINVAL for the
|
|
271
|
+
# current low-priority/background process. The current process is
|
|
272
|
+
# unambiguously alive, so avoid reclaiming its lock on that platform race.
|
|
273
|
+
if pid == os.getpid():
|
|
274
|
+
return True
|
|
227
275
|
try:
|
|
228
276
|
os.kill(pid, 0)
|
|
229
277
|
except ProcessLookupError:
|
|
@@ -878,7 +926,7 @@ class IntegrationTransaction:
|
|
|
878
926
|
commands: list[list[str]] = []
|
|
879
927
|
for index, item in enumerate(required):
|
|
880
928
|
if isinstance(item, str) and item.strip():
|
|
881
|
-
commands.append(
|
|
929
|
+
commands.append(_split_verify_command(item.strip()))
|
|
882
930
|
continue
|
|
883
931
|
if not isinstance(item, dict):
|
|
884
932
|
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
@@ -886,18 +934,26 @@ class IntegrationTransaction:
|
|
|
886
934
|
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
935
|
commands.append([str(x) for x in raw])
|
|
888
936
|
elif isinstance(raw, str) and raw.strip():
|
|
889
|
-
commands.append(
|
|
937
|
+
commands.append(_split_verify_command(raw.strip()))
|
|
890
938
|
else:
|
|
891
939
|
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
940
|
return commands
|
|
893
941
|
|
|
894
|
-
def verify(
|
|
942
|
+
def verify(
|
|
943
|
+
self,
|
|
944
|
+
commands: Sequence[Sequence[str]] | None = None,
|
|
945
|
+
*,
|
|
946
|
+
timeout_seconds: float = DEFAULT_VERIFY_TIMEOUT_SECONDS,
|
|
947
|
+
) -> dict[str, Any]:
|
|
895
948
|
commands = list(commands or [])
|
|
896
949
|
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
950
|
try:
|
|
898
951
|
commands.extend(self._required_on_merge_commands())
|
|
899
952
|
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
953
|
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
954
|
+
commands = _dedupe_verify_commands(commands)
|
|
955
|
+
if timeout_seconds <= 0:
|
|
956
|
+
raise VerifyPlanMissingError("verification timeout must be greater than zero")
|
|
901
957
|
|
|
902
958
|
def action(journal: dict[str, Any]) -> None:
|
|
903
959
|
if not commands or any(
|
|
@@ -910,21 +966,67 @@ class IntegrationTransaction:
|
|
|
910
966
|
intg_root = Path(journal["integrationRoot"]).resolve()
|
|
911
967
|
results: list[dict[str, Any]] = []
|
|
912
968
|
for command in commands:
|
|
969
|
+
executable_command = _normalize_verify_command(command)
|
|
913
970
|
started = dt.datetime.now().astimezone()
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
971
|
+
try:
|
|
972
|
+
proc = subprocess.run(
|
|
973
|
+
executable_command,
|
|
974
|
+
cwd=str(intg_root),
|
|
975
|
+
capture_output=True,
|
|
976
|
+
text=True,
|
|
977
|
+
encoding="utf-8",
|
|
978
|
+
errors="replace",
|
|
979
|
+
check=False,
|
|
980
|
+
timeout=timeout_seconds,
|
|
981
|
+
)
|
|
982
|
+
except subprocess.TimeoutExpired:
|
|
983
|
+
finished = dt.datetime.now().astimezone()
|
|
984
|
+
results.append(
|
|
985
|
+
{
|
|
986
|
+
"command": list(command),
|
|
987
|
+
"resolvedCommand": executable_command,
|
|
988
|
+
"exitCode": None,
|
|
989
|
+
"timedOut": True,
|
|
990
|
+
"timeoutSeconds": timeout_seconds,
|
|
991
|
+
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
992
|
+
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
993
|
+
"durationMs": int(
|
|
994
|
+
(finished - started).total_seconds() * 1000
|
|
995
|
+
),
|
|
996
|
+
}
|
|
997
|
+
)
|
|
998
|
+
journal["verifyResults"] = results
|
|
999
|
+
self._save(journal)
|
|
1000
|
+
raise VerificationFailedError(
|
|
1001
|
+
f"command timed out after {timeout_seconds}s: {' '.join(command)}"
|
|
1002
|
+
)
|
|
1003
|
+
except FileNotFoundError as exc:
|
|
1004
|
+
finished = dt.datetime.now().astimezone()
|
|
1005
|
+
results.append(
|
|
1006
|
+
{
|
|
1007
|
+
"command": list(command),
|
|
1008
|
+
"resolvedCommand": executable_command,
|
|
1009
|
+
"exitCode": None,
|
|
1010
|
+
"error": str(exc),
|
|
1011
|
+
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
1012
|
+
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
1013
|
+
"durationMs": int(
|
|
1014
|
+
(finished - started).total_seconds() * 1000
|
|
1015
|
+
),
|
|
1016
|
+
}
|
|
1017
|
+
)
|
|
1018
|
+
journal["verifyResults"] = results
|
|
1019
|
+
self._save(journal)
|
|
1020
|
+
raise VerifyPlanMissingError(
|
|
1021
|
+
f"verification executable not found: {executable_command[0]}"
|
|
1022
|
+
) from exc
|
|
923
1023
|
finished = dt.datetime.now().astimezone()
|
|
924
1024
|
results.append(
|
|
925
1025
|
{
|
|
926
1026
|
"command": list(command),
|
|
1027
|
+
"resolvedCommand": executable_command,
|
|
927
1028
|
"exitCode": proc.returncode,
|
|
1029
|
+
"timedOut": False,
|
|
928
1030
|
"startedAt": started.isoformat(timespec="milliseconds"),
|
|
929
1031
|
"finishedAt": finished.isoformat(timespec="milliseconds"),
|
|
930
1032
|
"durationMs": int(
|
|
@@ -1449,11 +1551,16 @@ def _parse_verify_commands(values: Sequence[str] | None) -> list[list[str]]:
|
|
|
1449
1551
|
POSIX shlex semantics: single/double quotes group tokens, backslash
|
|
1450
1552
|
escapes the next character (quote Windows paths).
|
|
1451
1553
|
"""
|
|
1452
|
-
return [
|
|
1554
|
+
return [_split_verify_command(value) for value in (values or [])]
|
|
1453
1555
|
|
|
1454
1556
|
|
|
1455
1557
|
def cmd_verify(args: argparse.Namespace) -> int:
|
|
1456
|
-
return _emit(
|
|
1558
|
+
return _emit(
|
|
1559
|
+
_txn_from_args(args).verify(
|
|
1560
|
+
commands=_parse_verify_commands(args.command),
|
|
1561
|
+
timeout_seconds=float(args.timeout_seconds),
|
|
1562
|
+
)
|
|
1563
|
+
)
|
|
1457
1564
|
|
|
1458
1565
|
|
|
1459
1566
|
def cmd_push(args: argparse.Namespace) -> int:
|
|
@@ -1543,6 +1650,11 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1543
1650
|
p_verify.add_argument("--feature-branch", required=True)
|
|
1544
1651
|
p_verify.add_argument("--temp-root", required=True)
|
|
1545
1652
|
p_verify.add_argument("--command", action="append")
|
|
1653
|
+
p_verify.add_argument(
|
|
1654
|
+
"--timeout-seconds",
|
|
1655
|
+
type=float,
|
|
1656
|
+
default=DEFAULT_VERIFY_TIMEOUT_SECONDS,
|
|
1657
|
+
)
|
|
1546
1658
|
p_verify.set_defaults(func=cmd_verify)
|
|
1547
1659
|
|
|
1548
1660
|
return parser
|
|
@@ -219,14 +219,30 @@ def sha256_file(path: Path) -> str:
|
|
|
219
219
|
return digest.hexdigest()
|
|
220
220
|
|
|
221
221
|
|
|
222
|
-
def compute_inputs_hash(
|
|
222
|
+
def compute_inputs_hash(
|
|
223
|
+
file_paths: list[str], *, project_root: Path | str | None = None
|
|
224
|
+
) -> tuple[str, list[str]]:
|
|
223
225
|
"""Per-file content sha256 → sort digests → hash again (order-independent)."""
|
|
226
|
+
root = (
|
|
227
|
+
Path(project_root).expanduser().resolve()
|
|
228
|
+
if project_root is not None
|
|
229
|
+
else None
|
|
230
|
+
)
|
|
224
231
|
by_path: dict[str, str] = {}
|
|
225
232
|
for raw in file_paths:
|
|
226
233
|
path = resolve_path(raw)
|
|
227
234
|
if not path.is_file():
|
|
228
235
|
raise FileNotFoundError(f"file not found: {raw}")
|
|
229
|
-
|
|
236
|
+
if root is not None:
|
|
237
|
+
try:
|
|
238
|
+
logical_path = path.relative_to(root).as_posix()
|
|
239
|
+
except ValueError as exc:
|
|
240
|
+
raise ValueError(
|
|
241
|
+
f"INPUT_OUTSIDE_PROJECT: {path} is outside {root}"
|
|
242
|
+
) from exc
|
|
243
|
+
else:
|
|
244
|
+
logical_path = path.as_posix()
|
|
245
|
+
by_path[logical_path] = sha256_file(path)
|
|
230
246
|
|
|
231
247
|
# Stable file list for callers; bind every resolved path to its digest.
|
|
232
248
|
# Hashing only the content multiset let a path swap incorrectly reuse a
|
|
@@ -1056,6 +1072,7 @@ def decide_can_reuse(
|
|
|
1056
1072
|
requested_profile_hash: str | None = None,
|
|
1057
1073
|
requested_environment_hash: str | None = None,
|
|
1058
1074
|
requested_db_schema_hash: str | None = None,
|
|
1075
|
+
project_root: Path | str | None = None,
|
|
1059
1076
|
) -> dict[str, Any]:
|
|
1060
1077
|
ledger, ledger_path = load_ledger(change_dir)
|
|
1061
1078
|
if ledger is None:
|
|
@@ -1220,8 +1237,10 @@ def decide_can_reuse(
|
|
|
1220
1237
|
}
|
|
1221
1238
|
|
|
1222
1239
|
try:
|
|
1223
|
-
current_hash, current_files = compute_inputs_hash(
|
|
1224
|
-
|
|
1240
|
+
current_hash, current_files = compute_inputs_hash(
|
|
1241
|
+
files, project_root=project_root
|
|
1242
|
+
)
|
|
1243
|
+
except (FileNotFoundError, ValueError) as exc:
|
|
1225
1244
|
return {
|
|
1226
1245
|
"ok": True,
|
|
1227
1246
|
"reuse": False,
|
|
@@ -1351,7 +1370,9 @@ def cmd_hash(args: argparse.Namespace) -> int:
|
|
|
1351
1370
|
files, project_root = input_files_from_args(args)
|
|
1352
1371
|
if not files:
|
|
1353
1372
|
return emit_error("hash requires --files or --files-from", as_json=as_json)
|
|
1354
|
-
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1373
|
+
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1374
|
+
files, project_root=project_root
|
|
1375
|
+
)
|
|
1355
1376
|
except (OSError, FileNotFoundError) as exc:
|
|
1356
1377
|
return emit_error(str(exc), as_json=as_json)
|
|
1357
1378
|
|
|
@@ -1418,6 +1439,7 @@ def cmd_can_reuse(args: argparse.Namespace) -> int:
|
|
|
1418
1439
|
requested_profile_hash=getattr(args, "profile_hash", None),
|
|
1419
1440
|
requested_environment_hash=getattr(args, "environment_hash", None),
|
|
1420
1441
|
requested_db_schema_hash=getattr(args, "db_schema_hash", None),
|
|
1442
|
+
project_root=project_root,
|
|
1421
1443
|
)
|
|
1422
1444
|
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
1423
1445
|
return emit_error(f"can-reuse failed: {exc}", as_json=as_json)
|
|
@@ -1470,7 +1492,9 @@ def cmd_record(args: argparse.Namespace) -> int:
|
|
|
1470
1492
|
|
|
1471
1493
|
try:
|
|
1472
1494
|
status = normalize_status(args.status)
|
|
1473
|
-
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1495
|
+
inputs_hash, inputs_files = compute_inputs_hash(
|
|
1496
|
+
files, project_root=project_root
|
|
1497
|
+
)
|
|
1474
1498
|
ledger, _existing_path = load_ledger(change_dir)
|
|
1475
1499
|
if ledger is None:
|
|
1476
1500
|
ledger = {
|
|
@@ -66,6 +66,16 @@ DEFAULT_EXCLUDED_ROOTS: tuple[str, ...] = (
|
|
|
66
66
|
".cache",
|
|
67
67
|
)
|
|
68
68
|
|
|
69
|
+
|
|
70
|
+
def profile_defaults_fingerprint() -> str:
|
|
71
|
+
"""Fingerprint generated defaults so old profiles are refreshed safely."""
|
|
72
|
+
payload = {
|
|
73
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
74
|
+
"excludedRoots": list(DEFAULT_EXCLUDED_ROOTS),
|
|
75
|
+
}
|
|
76
|
+
encoded = json.dumps(payload, ensure_ascii=False, sort_keys=True).encode("utf-8")
|
|
77
|
+
return f"sha256:{hashlib.sha256(encoded).hexdigest()}"
|
|
78
|
+
|
|
69
79
|
# 覆盖层级(spec §3.2):unitTest ⊂ unitTestFull;package 独立;submit 复用 unitTestFull。
|
|
70
80
|
VERIFICATION_KEYS: tuple[str, ...] = (
|
|
71
81
|
"compile",
|
|
@@ -330,6 +340,7 @@ def _node_commands(project: Path) -> dict[str, Any]:
|
|
|
330
340
|
def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, Any]:
|
|
331
341
|
return {
|
|
332
342
|
"schemaVersion": SCHEMA_VERSION,
|
|
343
|
+
"defaultsFingerprint": profile_defaults_fingerprint(),
|
|
333
344
|
"detectedAt": "",
|
|
334
345
|
"projectType": "unknown",
|
|
335
346
|
"toolPaths": {"node": "", "mvn": ""},
|
|
@@ -596,6 +607,28 @@ def check(project: Path) -> dict[str, Any]:
|
|
|
596
607
|
issues: list[str] = []
|
|
597
608
|
stale = False
|
|
598
609
|
|
|
610
|
+
expected_defaults_fingerprint = profile_defaults_fingerprint()
|
|
611
|
+
if profile.get("defaultsFingerprint") != expected_defaults_fingerprint:
|
|
612
|
+
stale = True
|
|
613
|
+
issues.append(
|
|
614
|
+
"defaultsFingerprint is missing or outdated; run detect to refresh generated defaults"
|
|
615
|
+
)
|
|
616
|
+
|
|
617
|
+
excluded_roots = {
|
|
618
|
+
str(item).replace("\\", "/").strip("/")
|
|
619
|
+
for item in (profile.get("excludedRoots") or [])
|
|
620
|
+
if isinstance(item, str) and item.strip("/")
|
|
621
|
+
}
|
|
622
|
+
missing_excluded_roots = [
|
|
623
|
+
item for item in DEFAULT_EXCLUDED_ROOTS if item not in excluded_roots
|
|
624
|
+
]
|
|
625
|
+
if missing_excluded_roots:
|
|
626
|
+
stale = True
|
|
627
|
+
issues.append(
|
|
628
|
+
"excludedRoots missing generated defaults: "
|
|
629
|
+
+ ", ".join(missing_excluded_roots)
|
|
630
|
+
)
|
|
631
|
+
|
|
599
632
|
fp = profile.get("fingerprint") or {}
|
|
600
633
|
stored_pom = fp.get("pomHash") or "" if isinstance(fp, dict) else ""
|
|
601
634
|
current_pom = sha256_file(project / "pom.xml") if (project / "pom.xml").is_file() else ""
|
|
@@ -32,6 +32,24 @@ MODE = "force-track-touched"
|
|
|
32
32
|
MANIFEST_REL = Path("evidence") / "test-tracking.json"
|
|
33
33
|
SNAPSHOT_REL = Path("evidence") / "test-guard-snapshot.json"
|
|
34
34
|
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
35
|
+
MANDATORY_EXCLUDED_ROOTS = (
|
|
36
|
+
".git",
|
|
37
|
+
".harness",
|
|
38
|
+
".worktrees",
|
|
39
|
+
".claude/worktrees",
|
|
40
|
+
".codex/worktrees",
|
|
41
|
+
".cursor/worktrees",
|
|
42
|
+
".codebuddy/worktrees",
|
|
43
|
+
".codeium/worktrees",
|
|
44
|
+
"target",
|
|
45
|
+
"build",
|
|
46
|
+
"dist",
|
|
47
|
+
"node_modules",
|
|
48
|
+
".gradle",
|
|
49
|
+
"__pycache__",
|
|
50
|
+
".pytest_cache",
|
|
51
|
+
".cache",
|
|
52
|
+
)
|
|
35
53
|
REASONS = ("tdd-created", "stale-test-repair", "test-updated")
|
|
36
54
|
|
|
37
55
|
|
|
@@ -213,11 +231,14 @@ def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
|
|
|
213
231
|
if not isinstance(paths, list):
|
|
214
232
|
return [], []
|
|
215
233
|
excluded = profile.get("excludedRoots") if isinstance(profile, dict) else None
|
|
216
|
-
|
|
234
|
+
configured_roots = (
|
|
217
235
|
[item.replace("\\", "/").strip("/") for item in excluded if isinstance(item, str) and item]
|
|
218
236
|
if isinstance(excluded, list)
|
|
219
237
|
else []
|
|
220
238
|
)
|
|
239
|
+
excluded_roots = list(
|
|
240
|
+
dict.fromkeys([*configured_roots, *MANDATORY_EXCLUDED_ROOTS])
|
|
241
|
+
)
|
|
221
242
|
patterns = [
|
|
222
243
|
item.replace("\\", "/") for item in paths if isinstance(item, str) and item
|
|
223
244
|
]
|
|
@@ -254,11 +275,8 @@ def _allowed_test_path(project: Path, rel: str) -> bool:
|
|
|
254
275
|
if config is None:
|
|
255
276
|
return _standard_test_path(rel)
|
|
256
277
|
patterns, excluded_roots = config
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
excluded_parts = tuple(os.path.normcase(part) for part in excluded.split("/"))
|
|
260
|
-
if rel_parts[: len(excluded_parts)] == excluded_parts:
|
|
261
|
-
return False
|
|
278
|
+
if _path_is_excluded(rel, excluded_roots):
|
|
279
|
+
return False
|
|
262
280
|
return any(_matches_pattern(rel, pattern) for pattern in patterns)
|
|
263
281
|
|
|
264
282
|
|
|
@@ -820,8 +838,12 @@ def _path_is_excluded(rel: str, excluded_roots: list[str]) -> bool:
|
|
|
820
838
|
excluded_parts = tuple(
|
|
821
839
|
os.path.normcase(part) for part in excluded.split("/") if part
|
|
822
840
|
)
|
|
823
|
-
if
|
|
824
|
-
|
|
841
|
+
if not excluded_parts:
|
|
842
|
+
continue
|
|
843
|
+
width = len(excluded_parts)
|
|
844
|
+
for index in range(len(rel_parts) - width + 1):
|
|
845
|
+
if rel_parts[index : index + width] == excluded_parts:
|
|
846
|
+
return True
|
|
825
847
|
return False
|
|
826
848
|
|
|
827
849
|
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-archive — 变更归档
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -56,6 +56,11 @@ disallowed-tools:
|
|
|
56
56
|
- `evidence/verification-ledger.json`
|
|
57
57
|
- 至少一个 test 或 review 报告/证据
|
|
58
58
|
- 缺失任一项 → `archivable=false`;可从 `.harness/cache/change-snapshots/<change>/` 恢复正式层后再重试
|
|
59
|
+
- **产品候选验证**:
|
|
60
|
+
- 有远端 CI:保留 `product-candidate-ci.json` 兼容证据;归档会把有效的 legacy schema v1 证据迁移为 `remote-claimed` 的 schema v2 `product-candidate-verification.json`,不会冒充 `remote-attested`。
|
|
61
|
+
- `remote-attested` 必须同时带远端运行 URL 与 `verification.attestationDigest`;只有 URL、旧 JSON 哈希或 ledger 哈希只能达到 `remote-claimed`。
|
|
62
|
+
- 无 CI:先运行 `harness_archive.py certify-local --change-dir ... --project . --json`,只复用身份一致且完整的 `unitTestFull` ledger 证据,不重复执行测试。
|
|
63
|
+
- `release-candidate` 默认要求候选验证通过;仅封存未完成事实时显式使用 `--intent record-only`,归档结果必须为 `releaseEligible=false`,不得用于发布。
|
|
59
64
|
|
|
60
65
|
## Inputs
|
|
61
66
|
|
|
@@ -109,9 +114,9 @@ blocking user confirmation 让用户确认归档操作。**用户拒绝 → 终
|
|
|
109
114
|
|
|
110
115
|
### Phase 3:执行归档
|
|
111
116
|
|
|
112
|
-
1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --json`
|
|
117
|
+
1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --intent release-candidate --json` 前置检查。无 CI 项目若已有完整全量 ledger,先运行 `certify-local`;若用户明确只封存事实,则 status/finalize 均使用 `--intent record-only`。
|
|
113
118
|
2. `meta/archive-meta.md` **由 `harness_archive.py finalize` 自动生成**(与 summary-data `finalStatus` 同源);**禁止 agent 手写**该文件,手写视为数据丢失。维护者结论写入 events(decision/issue)即可,finalize 会汇总到 summary / archive-meta。
|
|
114
|
-
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
|
|
119
|
+
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --intent <release-candidate|record-only> --json`;读 JSON(cleanup、事件、移动、collect、render、validate、manifest 比对、archive-meta)。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 不再同步执行知识维护**(§8.2):它写一个 `pending` maintenance-outbox 项即返回,`knowledgeMaintenance=QUEUED`;写 outbox 失败时 `NOT_QUEUED`(warning,不回滚 archive,总状态 CONDITIONAL)。后续由 `harness-sync` / `harness_knowledge.py maintain` 异步推进 outbox。**finalize 失败或 validate 报错时不删除原目录**。
|
|
115
120
|
|
|
116
121
|
- **Read `reference.md`** — finalize 输出字段、archive-meta 格式、CONDITIONAL_OK 规则
|
|
117
122
|
- **Read `templates/summary-data-template.json`** — summary-data 数据结构
|
|
@@ -187,6 +192,10 @@ finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,并在移
|
|
|
187
192
|
|
|
188
193
|
git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 final-summary.html 不得含明文 token/密码/密钥;归档报告必须区分 ✅真实成功 / 🟡跳过·静态验证 / ❌失败。
|
|
189
194
|
|
|
195
|
+
### 十三、归档完整性与发布资格分离
|
|
196
|
+
|
|
197
|
+
`archiveIntegrity` 表示归档内容是否完整;`candidateVerification` 表示候选验证证据及保证级别;`releaseEligible` 只有两者均满足发布策略时才为 true。`record-only` 允许保留证据不足的历史事实,但绝不等价于 CI 绿灯或可发布。
|
|
198
|
+
|
|
190
199
|
## Output Format
|
|
191
200
|
|
|
192
201
|
> 归档元数据格式见 `reference.md` 的 archive-meta 模板;最终报告由 `templates/summary-data-template.json` 数据结构 + `templates/render-summary.mjs` 固定脚本渲染。
|
|
@@ -14,6 +14,20 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
|
|
|
14
14
|
- before/after checksum 不一致时,不得删除原目录。
|
|
15
15
|
- 默认渲染器 `templates/render-summary.mjs`(finalize 内嵌调用)。
|
|
16
16
|
|
|
17
|
+
### Wave-A 状态机与身份(IA-1 / IA-4)
|
|
18
|
+
|
|
19
|
+
硬顺序:feature frozen → local gates → product candidate CI → **CI green** → merge → authoritative CI → archive product candidate → archive-only governance → release。
|
|
20
|
+
|
|
21
|
+
- [ ] `evidence/product-candidate-ci.json`(或等价 ledger 字段)`conclusion=success`,含 `runUrl` + `commit`;否则 `PRODUCT_CI_NOT_GREEN` 阻断
|
|
22
|
+
- [ ] summary/identity 含 `productCommit` / `productTreeHash` / `archiveCommit`;`productTreeHash` 排除 `.harness/**`
|
|
23
|
+
- [ ] 产品输入在 archive 后变化 → `ARCHIVE_EVIDENCE_REOPEN_REQUIRED`,旧 archive 不可作发布证据
|
|
24
|
+
- [ ] Manifest:coverage 字节在最终 snapshot 后不得静默漂移;后写报告必须 `exclusionReasons`,禁止假绿 `checksumStatus=OK`
|
|
25
|
+
|
|
26
|
+
### Environment 合同(IA-3)
|
|
27
|
+
|
|
28
|
+
- [ ] 使用可写环境栈前:`harness_environment.py fingerprint` → `acquire` lease → run → `release`
|
|
29
|
+
- [ ] 租约落在主仓 `.harness/runtime/env-leases/`;跨 change 默认不得共享可写 volume
|
|
30
|
+
|
|
17
31
|
## 归档前检查(Phase 1)
|
|
18
32
|
|
|
19
33
|
> ⚠️ **单一所有权**:finalize 内部负责且仅负责一次 `phase.start` / `phase.end`。归档前检查不得自行追加 archive 阶段边界。
|
|
@@ -42,6 +42,16 @@ const duration = (minutes) => {
|
|
|
42
42
|
if (value < 60) return `${Math.round(value * 10) / 10} 分钟`;
|
|
43
43
|
return `${Math.floor(value / 60)} 小时 ${Math.round(value % 60)} 分钟`;
|
|
44
44
|
};
|
|
45
|
+
const durationMs = (ms) => {
|
|
46
|
+
const value = number(ms);
|
|
47
|
+
if (value <= 0) return "0 秒";
|
|
48
|
+
if (value < 1000) return `${value}ms`;
|
|
49
|
+
const seconds = value / 1000;
|
|
50
|
+
if (seconds < 60) return `${Math.round(seconds * 10) / 10} 秒`;
|
|
51
|
+
const minutes = seconds / 60;
|
|
52
|
+
if (minutes < 60) return `${Math.round(minutes * 10) / 10} 分钟`;
|
|
53
|
+
return `${Math.floor(minutes / 60)} 小时 ${Math.round(minutes % 60)} 分钟`;
|
|
54
|
+
};
|
|
45
55
|
|
|
46
56
|
const stages = Object.entries(record(data.stageStatus));
|
|
47
57
|
const verification = record(data.verification);
|
|
@@ -49,6 +59,17 @@ const unit = record(verification.unitTests);
|
|
|
49
59
|
const api = record(verification.apiTests);
|
|
50
60
|
const durations = record(data.durations);
|
|
51
61
|
const durationStages = list(durations.stages);
|
|
62
|
+
const timing = record(data.timing);
|
|
63
|
+
const wallLabel = timing.workflowWallClockMs != null
|
|
64
|
+
? durationMs(timing.workflowWallClockMs)
|
|
65
|
+
: (durations.totalLabel || duration(durations.totalMinutes));
|
|
66
|
+
const timingColumnsHtml = `
|
|
67
|
+
<article class="card" id="timingColumns"><h2>时长(三列)</h2>
|
|
68
|
+
<div class="row"><div><strong>活动执行</strong><small>stageActiveExecution</small></div><span>${esc(durationMs(timing.stageActiveExecutionMs))}</span></div>
|
|
69
|
+
<div class="row"><div><strong>阶段墙钟</strong><small>stageWallClockSpan</small></div><span>${esc(durationMs(timing.stageWallClockSpanMs))}</span></div>
|
|
70
|
+
<div class="row"><div><strong>全流程墙钟</strong><small>workflowWallClock</small></div><span>${esc(durationMs(timing.workflowWallClockMs))}</span></div>
|
|
71
|
+
<p><small>reportCutoffAt=<code id="reportCutoffAt">${esc(timing.reportCutoffAt || "N/A")}</code> · durations.totalMinutes 仅为活动执行,不冒充墙钟</small></p>
|
|
72
|
+
</article>`;
|
|
52
73
|
const maxMinutes = Math.max(1, ...durationStages.map((item) => number(item.minutes)));
|
|
53
74
|
const diff = record(data.diffStat);
|
|
54
75
|
const risks = list(data.knownRisks);
|
|
@@ -86,12 +107,12 @@ const html = `<!doctype html><html lang="zh-CN"><head><meta charset="utf-8"><met
|
|
|
86
107
|
@media(max-width:800px){.metrics{grid-template-columns:1fr 1fr}.grid,.risk{grid-template-columns:1fr}}@media(max-width:480px){main{width:calc(100% - 18px);margin-top:9px}.metrics{grid-template-columns:1fr}.hero{padding:19px}h1{font-size:23px}dl{grid-template-columns:1fr}}
|
|
87
108
|
</style></head><body><main>
|
|
88
109
|
<section class="hero"><div class="eyebrow">HARNESS EXECUTION REPORT</div><h1>${esc(data.changeName || "未命名变更")}</h1><p class="goal">${esc(data.businessGoal || "未记录业务目标")}</p><div class="status">${pill(data.finalStatus)}<span>基于事件、验证账本与 Git 证据生成${data.riskTier ? ` · riskTier=${esc(data.riskTier)}` : ""}</span></div>${reasonHtml ? `<div class="status">${reasonHtml}</div>` : ""}</section>
|
|
89
|
-
<section class="metrics"><article class="metric"><small
|
|
90
|
-
<section class="grid"><div><article class="card"><h2>验证结论</h2>${verificationHtml}</article><article class="card"><h2
|
|
110
|
+
<section class="metrics"><article class="metric"><small>产品提交</small><strong><code title="${esc(data.productCommit || data.finalCommit)}">${esc(shortHash(data.productCommit || data.finalCommit))}</code></strong><small>archive=${esc(shortHash(data.archiveCommit || data.finalCommit))}</small></article><article class="metric"><small>代码范围</small><strong>${number(diff.filesChanged)} 个文件</strong><small><span class="plus">+${number(diff.insertions)}</span> · <span class="minus">-${number(diff.deletions)}</span></small></article><article class="metric"><small>全流程墙钟</small><strong>${esc(wallLabel)}</strong><small>活动=${esc(durationMs(timing.stageActiveExecutionMs))} · ${durationStages.length} 阶段</small></article><article class="metric"><small>归档完整性</small><strong>${esc(record(data.archiveManifest).checksumStatus || "UNKNOWN")}</strong><small>${number(record(data.archiveManifest).totalArchiveFiles)} 个归档文件</small></article></section>
|
|
111
|
+
<section class="grid"><div><article class="card"><h2>验证结论</h2>${verificationHtml}</article><article class="card"><h2>阶段耗时(活动)</h2>${durationHtml}</article>${timingColumnsHtml}</div><div><article class="card"><h2>阶段状态</h2>${stageHtml}</article><article class="card"><h2>风险与后续</h2><div class="risk"><div><h3>已知风险</h3><ul>${riskHtml}</ul></div><div><h3>人工动作</h3><ul>${actionHtml}</ul></div></div></article></div></section>
|
|
91
112
|
<details><summary>变更文件(${files.length})</summary><div><table><thead><tr><th>文件</th><th>新增</th><th>删除</th></tr></thead><tbody>${fileRows}</tbody></table></div></details>
|
|
92
113
|
<details><summary>执行时间线与工具交接(${timeline.length})</summary><div><table><thead><tr><th>阶段</th><th>尝试</th><th>状态</th><th>来源 / 摘要</th></tr></thead><tbody>${timelineRows}</tbody></table></div></details>
|
|
93
114
|
<details><summary>命令证据(${commands.length})</summary><div><table><thead><tr><th>阶段</th><th>命令</th><th>结果</th></tr></thead><tbody>${commandRows}</tbody></table></div></details>
|
|
94
|
-
<details><summary>技术元数据</summary><div><dl><dt
|
|
115
|
+
<details><summary>技术元数据</summary><div><dl><dt>产品提交</dt><dd><code>${esc(data.productCommit || data.finalCommit || "N/A")}</code></dd><dt>产品树哈希</dt><dd><code>${esc(data.productTreeHash || "N/A")}</code></dd><dt>归档提交</dt><dd><code>${esc(data.archiveCommit || data.finalCommit || "N/A")}</code></dd><dt>基线提交</dt><dd><code>${esc(data.baseCommit || "N/A")}</code></dd><dt>Git 范围</dt><dd><code>${esc(diff.range || "N/A")}</code></dd><dt>报告数据版本</dt><dd>${esc(data.schemaVersion || "N/A")}</dd><dt>事实来源</dt><dd>${esc(list(record(data.reportPipeline).sources).join(" · ") || "N/A")}</dd></dl></div></details>
|
|
95
116
|
</main></body></html>`;
|
|
96
117
|
|
|
97
118
|
fs.mkdirSync(path.dirname(outPath), { recursive: true });
|
package/harness/bundles/java/claude-code/harness-archive/templates/summary-data-template.json
CHANGED
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
"changeName": "<change-name>",
|
|
4
4
|
"businessGoal": "本次变更为了做什么",
|
|
5
5
|
"finalStatus": "OK | WARN | CONDITIONAL_OK | FAIL",
|
|
6
|
+
"archiveIntent": "release-candidate | record-only",
|
|
7
|
+
"archiveIntegrity": {
|
|
8
|
+
"ok": true
|
|
9
|
+
},
|
|
10
|
+
"candidateVerification": {
|
|
11
|
+
"ok": false,
|
|
12
|
+
"code": "PRODUCT_CANDIDATE_VERIFIED | PRODUCT_CANDIDATE_NOT_VERIFIED | PRODUCT_CI_GREEN | PRODUCT_CI_NOT_GREEN",
|
|
13
|
+
"assurance": "local-reproducible | remote-claimed | remote-attested",
|
|
14
|
+
"message": ""
|
|
15
|
+
},
|
|
16
|
+
"releaseEligible": false,
|
|
6
17
|
"finalCommit": "<commit-hash>",
|
|
7
18
|
"finalCommitBranch": "<origin/branch>",
|
|
8
19
|
"baseCommit": "<base-commit-hash>",
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-ingest
|
|
25
25
|
|
|
26
26
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-query
|
|
25
25
|
|
|
26
26
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-package — 增量模块打包(Java overlay)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -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=506c7d9df54c0f80; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-plan — 需求规划
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -109,6 +109,7 @@ change-name 范围变更 → 提示重命名或记 🟡WARN(→ `reference.md`
|
|
|
109
109
|
| 歧义优先检查 | 否定、对比、动作对象或范围存在多种合理解释时,最小取证后先给推荐理解并一次一问;确认前不深挖错误方向 |
|
|
110
110
|
| 简单修复探索预算 | 预计不超过 2 个代码文件、且不涉及认证/安全/迁移/并发/API 契约重设时,最多 1 次合并 CodeGraph 查询 + 1 次定向补查、1 个用户澄清问题;无关发现只记非阻断说明 |
|
|
111
111
|
| 精简产物 | 简单修复只保留实现所需的设计、任务、边界和测试;禁止在 spec/plan/detail/scenarios 中重复同一背景和结论 |
|
|
112
|
+
| 测试执行成本 | 场景表必须设计快速反馈层级、预计时长、资源预算、超时和可复用证据;默认先跑受影响测试,再跑模块门禁,候选验证只复用身份一致的全量证据 |
|
|
112
113
|
| state snapshot | 读取 `state-snapshot.json`(`harness_state.py` / state-layout-protocol §state-snapshot.json)了解 project/worktree root、HEAD/base、profile/rules/map/knowledge 指纹;失效由脚本刷新,**不得仅凭缓存跳过代码探索或验证门禁**(design §3.6) |
|
|
113
114
|
| 协议 | sensitive-info / evidence-based-reporting / state-layout |
|
|
114
115
|
|