@hunter-harness/workflow-harness 0.2.3 → 0.2.5
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/README.md +12 -11
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +7 -9
- package/harness/bundles/general/claude-code/harness-archive/checklist.md +4 -4
- package/harness/bundles/general/claude-code/harness-archive/reference.md +3 -5
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +14 -10
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +28 -13
- package/harness/bundles/general/claude-code/harness-plan/protocols.md +10 -3
- package/harness/bundles/general/claude-code/harness-plan/reference.md +9 -23
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +16 -4
- package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
- package/harness/bundles/general/claude-code/harness-run/protocols.md +3 -2
- package/harness/bundles/general/claude-code/harness-run/reference.md +3 -9
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +6 -4
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +15 -3
- package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +4 -4
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/README.md +12 -11
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +7 -9
- package/harness/bundles/general/codebuddy/harness-archive/checklist.md +4 -4
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +3 -5
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +13 -9
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +28 -13
- package/harness/bundles/general/codebuddy/harness-plan/protocols.md +10 -3
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +9 -23
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +16 -4
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codebuddy/harness-run/protocols.md +3 -2
- package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -9
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +6 -4
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +15 -3
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +4 -4
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/README.md +12 -11
- package/harness/bundles/general/codex/harness-archive/SKILL.md +7 -9
- package/harness/bundles/general/codex/harness-archive/checklist.md +4 -4
- package/harness/bundles/general/codex/harness-archive/reference.md +3 -5
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/general/codex/harness-plan/SKILL.md +13 -9
- package/harness/bundles/general/codex/harness-plan/checklist.md +28 -13
- package/harness/bundles/general/codex/harness-plan/protocols.md +10 -3
- package/harness/bundles/general/codex/harness-plan/reference.md +9 -23
- package/harness/bundles/general/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +16 -4
- package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codex/harness-run/protocols.md +3 -2
- package/harness/bundles/general/codex/harness-run/reference.md +3 -9
- package/harness/bundles/general/codex/harness-submit/SKILL.md +6 -4
- package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/codex/harness-test/SKILL.md +15 -3
- package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codex/harness-test/reference.md +4 -4
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codex/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/README.md +12 -11
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +7 -9
- package/harness/bundles/general/cursor/harness-archive/checklist.md +4 -4
- package/harness/bundles/general/cursor/harness-archive/reference.md +3 -5
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +13 -9
- package/harness/bundles/general/cursor/harness-plan/checklist.md +28 -13
- package/harness/bundles/general/cursor/harness-plan/protocols.md +10 -3
- package/harness/bundles/general/cursor/harness-plan/reference.md +9 -23
- package/harness/bundles/general/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +16 -4
- package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
- package/harness/bundles/general/cursor/harness-run/protocols.md +3 -2
- package/harness/bundles/general/cursor/harness-run/reference.md +3 -9
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +6 -4
- package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +2 -2
- package/harness/bundles/general/cursor/harness-test/SKILL.md +15 -3
- package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +4 -4
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/cursor/scripts/harness_profile.py +29 -1
- package/harness/bundles/general/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/README.md +12 -11
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +7 -9
- package/harness/bundles/java/claude-code/harness-archive/checklist.md +4 -4
- package/harness/bundles/java/claude-code/harness-archive/reference.md +3 -5
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +14 -10
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +28 -13
- package/harness/bundles/java/claude-code/harness-plan/protocols.md +10 -3
- package/harness/bundles/java/claude-code/harness-plan/reference.md +9 -23
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +19 -5
- package/harness/bundles/java/claude-code/harness-run/protocols.md +3 -2
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +6 -4
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +17 -3
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/README.md +12 -11
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +7 -9
- package/harness/bundles/java/codebuddy/harness-archive/checklist.md +4 -4
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +3 -5
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +13 -9
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +28 -13
- package/harness/bundles/java/codebuddy/harness-plan/protocols.md +10 -3
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +9 -23
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +19 -5
- package/harness/bundles/java/codebuddy/harness-run/protocols.md +3 -2
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +6 -4
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +17 -3
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/README.md +12 -11
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +7 -9
- package/harness/bundles/java/codex/harness-archive/checklist.md +4 -4
- package/harness/bundles/java/codex/harness-archive/reference.md +3 -5
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +13 -9
- package/harness/bundles/java/codex/harness-plan/checklist.md +28 -13
- package/harness/bundles/java/codex/harness-plan/protocols.md +10 -3
- package/harness/bundles/java/codex/harness-plan/reference.md +9 -23
- package/harness/bundles/java/codex/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +19 -5
- package/harness/bundles/java/codex/harness-run/protocols.md +3 -2
- package/harness/bundles/java/codex/harness-submit/SKILL.md +6 -4
- package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/codex/harness-test/SKILL.md +17 -3
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codex/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/codex/scripts/harness_test_guard.py +502 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/README.md +12 -11
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +7 -9
- package/harness/bundles/java/cursor/harness-archive/checklist.md +4 -4
- package/harness/bundles/java/cursor/harness-archive/reference.md +3 -5
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +10 -16
- package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +13 -9
- package/harness/bundles/java/cursor/harness-plan/checklist.md +28 -13
- package/harness/bundles/java/cursor/harness-plan/protocols.md +10 -3
- package/harness/bundles/java/cursor/harness-plan/reference.md +9 -23
- package/harness/bundles/java/cursor/harness-review/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +19 -5
- package/harness/bundles/java/cursor/harness-run/protocols.md +3 -2
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +6 -4
- package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +2 -2
- package/harness/bundles/java/cursor/harness-test/SKILL.md +17 -3
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +7 -15
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/cursor/scripts/harness_profile.py +29 -1
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/manifests/general/claude-code.json +32 -28
- package/harness/manifests/general/codebuddy.json +32 -28
- package/harness/manifests/general/codex.json +32 -28
- package/harness/manifests/general/cursor.json +32 -28
- 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
|
@@ -0,0 +1,502 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Record and force-stage only explicitly touched test files.
|
|
3
|
+
|
|
4
|
+
Python 3.10+ stdlib only. Git is always invoked with an argv list.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import argparse
|
|
10
|
+
import contextlib
|
|
11
|
+
import fnmatch
|
|
12
|
+
import hashlib
|
|
13
|
+
import json
|
|
14
|
+
import os
|
|
15
|
+
import re
|
|
16
|
+
import shutil
|
|
17
|
+
import subprocess
|
|
18
|
+
import sys
|
|
19
|
+
import tempfile
|
|
20
|
+
import time
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
SCHEMA_VERSION = 1
|
|
25
|
+
MODE = "force-track-touched"
|
|
26
|
+
MANIFEST_REL = Path("evidence") / "test-tracking.json"
|
|
27
|
+
PROFILE_REL = Path(".harness") / "config" / "build-profile.json"
|
|
28
|
+
REASONS = ("tdd-created", "stale-test-repair", "test-updated")
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class LockUnavailable(RuntimeError):
|
|
32
|
+
"""A compatible lock file is already held by another process."""
|
|
33
|
+
|
|
34
|
+
if hasattr(sys.stdout, "reconfigure"):
|
|
35
|
+
sys.stdout.reconfigure(encoding="utf-8")
|
|
36
|
+
if hasattr(sys.stderr, "reconfigure"):
|
|
37
|
+
sys.stderr.reconfigure(encoding="utf-8")
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _result(ok: bool, action: str, code: str, files: list[str], **extra: Any) -> dict[str, Any]:
|
|
41
|
+
return {"ok": ok, "action": action, "code": code, "files": files, **extra}
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def _write_json(path: Path, data: Any) -> None:
|
|
45
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
46
|
+
text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
|
|
47
|
+
tmp = path.with_name(f".{path.name}.{os.getpid()}.tmp")
|
|
48
|
+
try:
|
|
49
|
+
tmp.write_text(text, encoding="utf-8", newline="\n")
|
|
50
|
+
os.replace(tmp, path)
|
|
51
|
+
except BaseException:
|
|
52
|
+
tmp.unlink(missing_ok=True)
|
|
53
|
+
raise
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def _read_json(path: Path) -> Any:
|
|
57
|
+
return json.loads(path.read_text(encoding="utf-8-sig"))
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _sha256(path: Path) -> str:
|
|
61
|
+
digest = hashlib.sha256()
|
|
62
|
+
with path.open("rb") as handle:
|
|
63
|
+
for chunk in iter(lambda: handle.read(65536), b""):
|
|
64
|
+
digest.update(chunk)
|
|
65
|
+
return "sha256:" + digest.hexdigest()
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def _git(
|
|
69
|
+
project: Path,
|
|
70
|
+
*args: str,
|
|
71
|
+
index_file: Path | None = None,
|
|
72
|
+
) -> subprocess.CompletedProcess[str]:
|
|
73
|
+
env = os.environ.copy()
|
|
74
|
+
if index_file is not None:
|
|
75
|
+
env["GIT_INDEX_FILE"] = str(index_file)
|
|
76
|
+
return subprocess.run(
|
|
77
|
+
[
|
|
78
|
+
"git",
|
|
79
|
+
"--literal-pathspecs",
|
|
80
|
+
"-c",
|
|
81
|
+
"core.quotepath=false",
|
|
82
|
+
"-C",
|
|
83
|
+
str(project),
|
|
84
|
+
*args,
|
|
85
|
+
],
|
|
86
|
+
capture_output=True,
|
|
87
|
+
text=True,
|
|
88
|
+
encoding="utf-8",
|
|
89
|
+
errors="replace",
|
|
90
|
+
timeout=30,
|
|
91
|
+
check=False,
|
|
92
|
+
env=env,
|
|
93
|
+
)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _is_ignored(project: Path, rel: str) -> bool:
|
|
97
|
+
status = _git(
|
|
98
|
+
project,
|
|
99
|
+
"status",
|
|
100
|
+
"--ignored=matching",
|
|
101
|
+
"--untracked-files=all",
|
|
102
|
+
"--porcelain=v1",
|
|
103
|
+
"--",
|
|
104
|
+
rel,
|
|
105
|
+
)
|
|
106
|
+
return status.returncode == 0 and any(
|
|
107
|
+
line.startswith("!! ") for line in status.stdout.splitlines()
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _inside(path: Path, root: Path) -> bool:
|
|
112
|
+
try:
|
|
113
|
+
normalized_path = os.path.normcase(str(path))
|
|
114
|
+
normalized_root = os.path.normcase(str(root))
|
|
115
|
+
return os.path.commonpath((normalized_path, normalized_root)) == normalized_root
|
|
116
|
+
except ValueError:
|
|
117
|
+
return False
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
@contextlib.contextmanager
|
|
121
|
+
def _exclusive_lock(path: Path, *, wait_seconds: float) -> Any:
|
|
122
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
123
|
+
deadline = time.monotonic() + wait_seconds
|
|
124
|
+
descriptor: int | None = None
|
|
125
|
+
while descriptor is None:
|
|
126
|
+
try:
|
|
127
|
+
descriptor = os.open(path, os.O_CREAT | os.O_EXCL | os.O_WRONLY, 0o600)
|
|
128
|
+
except FileExistsError as exc:
|
|
129
|
+
if time.monotonic() >= deadline:
|
|
130
|
+
raise LockUnavailable(str(path)) from exc
|
|
131
|
+
time.sleep(0.01)
|
|
132
|
+
try:
|
|
133
|
+
os.write(descriptor, f"{os.getpid()}\n".encode("ascii"))
|
|
134
|
+
yield
|
|
135
|
+
finally:
|
|
136
|
+
os.close(descriptor)
|
|
137
|
+
path.unlink(missing_ok=True)
|
|
138
|
+
|
|
139
|
+
|
|
140
|
+
def _change_dir(project: Path, change_dir: Path | str) -> Path | None:
|
|
141
|
+
candidate = Path(change_dir)
|
|
142
|
+
if not candidate.is_absolute():
|
|
143
|
+
candidate = project / candidate
|
|
144
|
+
resolved = candidate.resolve()
|
|
145
|
+
return resolved if _inside(resolved, project) else None
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def _manifest_path(project: Path, change_root: Path) -> Path | None:
|
|
149
|
+
evidence = change_root / MANIFEST_REL.parent
|
|
150
|
+
manifest = change_root / MANIFEST_REL
|
|
151
|
+
expected_evidence = evidence.absolute()
|
|
152
|
+
resolved_evidence = evidence.resolve()
|
|
153
|
+
if (
|
|
154
|
+
not _inside(change_root.resolve(), project)
|
|
155
|
+
or not _inside(resolved_evidence, project)
|
|
156
|
+
or os.path.normcase(str(resolved_evidence))
|
|
157
|
+
!= os.path.normcase(str(expected_evidence))
|
|
158
|
+
):
|
|
159
|
+
return None
|
|
160
|
+
return manifest
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
def _manifest_target_inside(project: Path, manifest: Path) -> bool:
|
|
164
|
+
resolved = manifest.resolve()
|
|
165
|
+
return _inside(resolved, project) and _inside(resolved, manifest.parent.absolute())
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
def _profile_config(project: Path) -> tuple[list[str], list[str]] | None:
|
|
169
|
+
profile_path = project / PROFILE_REL
|
|
170
|
+
if not profile_path.is_file():
|
|
171
|
+
return None
|
|
172
|
+
try:
|
|
173
|
+
profile = _read_json(profile_path)
|
|
174
|
+
except (OSError, json.JSONDecodeError):
|
|
175
|
+
return [], []
|
|
176
|
+
tracking = profile.get("testTracking") if isinstance(profile, dict) else None
|
|
177
|
+
paths = tracking.get("paths") if isinstance(tracking, dict) else None
|
|
178
|
+
if not isinstance(paths, list):
|
|
179
|
+
return [], []
|
|
180
|
+
excluded = profile.get("excludedRoots") if isinstance(profile, dict) else None
|
|
181
|
+
excluded_roots = (
|
|
182
|
+
[item.replace("\\", "/").strip("/") for item in excluded if isinstance(item, str) and item]
|
|
183
|
+
if isinstance(excluded, list)
|
|
184
|
+
else []
|
|
185
|
+
)
|
|
186
|
+
patterns = [
|
|
187
|
+
item.replace("\\", "/") for item in paths if isinstance(item, str) and item
|
|
188
|
+
]
|
|
189
|
+
return patterns, excluded_roots
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _matches_pattern(rel: str, pattern: str) -> bool:
|
|
193
|
+
# fnmatch does not give **/ its usual zero-or-more-directory meaning, so
|
|
194
|
+
# also try the form with every **/ removed.
|
|
195
|
+
if fnmatch.fnmatchcase(rel, pattern):
|
|
196
|
+
return True
|
|
197
|
+
compact = pattern
|
|
198
|
+
while "**/" in compact:
|
|
199
|
+
compact = compact.replace("**/", "", 1)
|
|
200
|
+
if fnmatch.fnmatchcase(rel, compact):
|
|
201
|
+
return True
|
|
202
|
+
if pattern.endswith("/**"):
|
|
203
|
+
prefix = pattern[:-3].rstrip("/")
|
|
204
|
+
return rel == prefix or rel.startswith(prefix + "/")
|
|
205
|
+
return False
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def _standard_test_path(rel: str) -> bool:
|
|
209
|
+
parts = rel.split("/")
|
|
210
|
+
if len(parts) >= 2 and parts[0] in ("test", "tests"):
|
|
211
|
+
return True
|
|
212
|
+
if any(parts[index:index + 2] == ["src", "test"] for index in range(len(parts) - 1)):
|
|
213
|
+
return True
|
|
214
|
+
return False
|
|
215
|
+
|
|
216
|
+
|
|
217
|
+
def _allowed_test_path(project: Path, rel: str) -> bool:
|
|
218
|
+
config = _profile_config(project)
|
|
219
|
+
if config is None:
|
|
220
|
+
return _standard_test_path(rel)
|
|
221
|
+
patterns, excluded_roots = config
|
|
222
|
+
rel_parts = tuple(os.path.normcase(part) for part in rel.split("/"))
|
|
223
|
+
for excluded in excluded_roots:
|
|
224
|
+
excluded_parts = tuple(os.path.normcase(part) for part in excluded.split("/"))
|
|
225
|
+
if rel_parts[: len(excluded_parts)] == excluded_parts:
|
|
226
|
+
return False
|
|
227
|
+
return any(_matches_pattern(rel, pattern) for pattern in patterns)
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
def _validate_file(project: Path, raw: str) -> tuple[Path | None, str | None, str | None]:
|
|
231
|
+
candidate = Path(raw)
|
|
232
|
+
if not candidate.is_absolute():
|
|
233
|
+
candidate = project / candidate
|
|
234
|
+
resolved = candidate.resolve()
|
|
235
|
+
if not _inside(resolved, project):
|
|
236
|
+
return None, None, "PATH_OUTSIDE_PROJECT"
|
|
237
|
+
if not resolved.exists():
|
|
238
|
+
return None, None, "FILE_NOT_FOUND"
|
|
239
|
+
if not resolved.is_file():
|
|
240
|
+
return None, None, "NOT_REGULAR_FILE"
|
|
241
|
+
rel = resolved.relative_to(project).as_posix()
|
|
242
|
+
if not _allowed_test_path(project, rel):
|
|
243
|
+
return None, rel, "TEST_PATH_NOT_ALLOWED"
|
|
244
|
+
return resolved, rel, None
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
def _entry_shape_valid(item: Any) -> bool:
|
|
248
|
+
return (
|
|
249
|
+
isinstance(item, dict)
|
|
250
|
+
and isinstance(item.get("path"), str)
|
|
251
|
+
and bool(item["path"])
|
|
252
|
+
and isinstance(item.get("sha256"), str)
|
|
253
|
+
and re.fullmatch(r"sha256:[0-9a-f]{64}", item["sha256"]) is not None
|
|
254
|
+
and item.get("reason") in REASONS
|
|
255
|
+
and type(item.get("ignored")) is bool
|
|
256
|
+
and type(item.get("trackedBefore")) is bool
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def _validate_existing_manifest(
|
|
261
|
+
project: Path,
|
|
262
|
+
manifest: Any,
|
|
263
|
+
*,
|
|
264
|
+
allow_hash_drift: set[str] | None = None,
|
|
265
|
+
require_files: bool,
|
|
266
|
+
) -> tuple[str | None, dict[str, dict[str, Any]]]:
|
|
267
|
+
if (
|
|
268
|
+
not isinstance(manifest, dict)
|
|
269
|
+
or manifest.get("schemaVersion") != SCHEMA_VERSION
|
|
270
|
+
or manifest.get("mode") != MODE
|
|
271
|
+
):
|
|
272
|
+
return "MANIFEST_INVALID", {}
|
|
273
|
+
if manifest.get("projectRoot") != str(project):
|
|
274
|
+
return "MANIFEST_PROJECT_MISMATCH", {}
|
|
275
|
+
entries = manifest.get("files")
|
|
276
|
+
if not isinstance(entries, list):
|
|
277
|
+
return "MANIFEST_INVALID", {}
|
|
278
|
+
if require_files and not entries:
|
|
279
|
+
return "EMPTY_MANIFEST", {}
|
|
280
|
+
|
|
281
|
+
allowed_drift = allow_hash_drift or set()
|
|
282
|
+
validated: dict[str, dict[str, Any]] = {}
|
|
283
|
+
for item in entries:
|
|
284
|
+
if not _entry_shape_valid(item):
|
|
285
|
+
return "MANIFEST_INVALID", {}
|
|
286
|
+
rel = item["path"]
|
|
287
|
+
if rel in validated:
|
|
288
|
+
return "MANIFEST_INVALID", {}
|
|
289
|
+
path, normalized, error = _validate_file(project, rel)
|
|
290
|
+
if error or normalized != rel or path is None:
|
|
291
|
+
return error or "MANIFEST_INVALID", {}
|
|
292
|
+
if item["sha256"] != _sha256(path) and rel not in allowed_drift:
|
|
293
|
+
return "HASH_DRIFT" if require_files else "MANIFEST_INVALID", {}
|
|
294
|
+
ignored_now = _is_ignored(project, rel)
|
|
295
|
+
tracked_now = _git(
|
|
296
|
+
project, "ls-files", "--error-unmatch", "--", rel
|
|
297
|
+
).returncode == 0
|
|
298
|
+
if item["ignored"] != ignored_now or item["trackedBefore"] != tracked_now:
|
|
299
|
+
return "MANIFEST_INVALID", {}
|
|
300
|
+
validated[rel] = dict(item)
|
|
301
|
+
return None, validated
|
|
302
|
+
|
|
303
|
+
|
|
304
|
+
def record(
|
|
305
|
+
project: Path | str,
|
|
306
|
+
change_dir: Path | str,
|
|
307
|
+
files: list[str],
|
|
308
|
+
reason: str,
|
|
309
|
+
) -> dict[str, Any]:
|
|
310
|
+
action = "record"
|
|
311
|
+
project_root = Path(project).resolve()
|
|
312
|
+
if not files:
|
|
313
|
+
return _result(False, action, "EMPTY_FILES", [])
|
|
314
|
+
if reason not in REASONS:
|
|
315
|
+
return _result(False, action, "INVALID_REASON", [])
|
|
316
|
+
change_root = _change_dir(project_root, change_dir)
|
|
317
|
+
if change_root is None:
|
|
318
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
319
|
+
|
|
320
|
+
validated: list[tuple[Path, str]] = []
|
|
321
|
+
for raw in files:
|
|
322
|
+
path, rel, error = _validate_file(project_root, raw)
|
|
323
|
+
if error:
|
|
324
|
+
return _result(False, action, error, [rel or str(raw)])
|
|
325
|
+
assert path is not None and rel is not None
|
|
326
|
+
validated.append((path, rel))
|
|
327
|
+
|
|
328
|
+
manifest_path = _manifest_path(project_root, change_root)
|
|
329
|
+
if manifest_path is None:
|
|
330
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
331
|
+
lock_path = manifest_path.with_name(manifest_path.name + ".lock")
|
|
332
|
+
try:
|
|
333
|
+
with _exclusive_lock(lock_path, wait_seconds=5.0):
|
|
334
|
+
if not _manifest_target_inside(project_root, manifest_path):
|
|
335
|
+
return _result(
|
|
336
|
+
False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", []
|
|
337
|
+
)
|
|
338
|
+
existing_files: dict[str, dict[str, Any]] = {}
|
|
339
|
+
if manifest_path.is_file():
|
|
340
|
+
try:
|
|
341
|
+
existing = _read_json(manifest_path)
|
|
342
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
343
|
+
return _result(
|
|
344
|
+
False, action, "MANIFEST_INVALID", [], error=str(exc)
|
|
345
|
+
)
|
|
346
|
+
error, existing_files = _validate_existing_manifest(
|
|
347
|
+
project_root,
|
|
348
|
+
existing,
|
|
349
|
+
allow_hash_drift={rel for _, rel in validated},
|
|
350
|
+
require_files=False,
|
|
351
|
+
)
|
|
352
|
+
if error:
|
|
353
|
+
return _result(False, action, error, [])
|
|
354
|
+
|
|
355
|
+
for path, rel in validated:
|
|
356
|
+
ignored = _is_ignored(project_root, rel)
|
|
357
|
+
tracked = _git(
|
|
358
|
+
project_root, "ls-files", "--error-unmatch", "--", rel
|
|
359
|
+
).returncode == 0
|
|
360
|
+
existing_files[rel] = {
|
|
361
|
+
"path": rel,
|
|
362
|
+
"sha256": _sha256(path),
|
|
363
|
+
"reason": reason,
|
|
364
|
+
"ignored": ignored,
|
|
365
|
+
"trackedBefore": tracked,
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
manifest = {
|
|
369
|
+
"schemaVersion": SCHEMA_VERSION,
|
|
370
|
+
"mode": MODE,
|
|
371
|
+
"projectRoot": str(project_root),
|
|
372
|
+
"files": [existing_files[key] for key in sorted(existing_files)],
|
|
373
|
+
}
|
|
374
|
+
_write_json(manifest_path, manifest)
|
|
375
|
+
except LockUnavailable:
|
|
376
|
+
return _result(False, action, "MANIFEST_LOCKED", [])
|
|
377
|
+
return _result(True, action, "RECORDED", [rel for _, rel in validated], manifestPath=str(manifest_path))
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def _stage_locked(
|
|
381
|
+
project_root: Path, manifest_path: Path, index_path: Path
|
|
382
|
+
) -> dict[str, Any]:
|
|
383
|
+
action = "stage"
|
|
384
|
+
if not _manifest_target_inside(project_root, manifest_path):
|
|
385
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
386
|
+
try:
|
|
387
|
+
manifest = _read_json(manifest_path)
|
|
388
|
+
except FileNotFoundError:
|
|
389
|
+
return _result(False, action, "MANIFEST_MISSING", [])
|
|
390
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
391
|
+
return _result(False, action, "MANIFEST_INVALID", [], error=str(exc))
|
|
392
|
+
error, entries = _validate_existing_manifest(
|
|
393
|
+
project_root, manifest, require_files=True
|
|
394
|
+
)
|
|
395
|
+
if error:
|
|
396
|
+
return _result(False, action, error, [])
|
|
397
|
+
rels = list(entries)
|
|
398
|
+
|
|
399
|
+
before_cached_result = _git(project_root, "diff", "--cached", "--name-only")
|
|
400
|
+
if before_cached_result.returncode != 0:
|
|
401
|
+
return _result(False, action, "CACHED_DIFF_FAILED", rels)
|
|
402
|
+
before_cached = set(before_cached_result.stdout.splitlines())
|
|
403
|
+
|
|
404
|
+
handle, temp_name = tempfile.mkstemp(
|
|
405
|
+
prefix=f".{index_path.name}.test-guard.", dir=index_path.parent
|
|
406
|
+
)
|
|
407
|
+
os.close(handle)
|
|
408
|
+
temp_index = Path(temp_name)
|
|
409
|
+
temp_index.unlink(missing_ok=True)
|
|
410
|
+
try:
|
|
411
|
+
if index_path.is_file():
|
|
412
|
+
shutil.copy2(index_path, temp_index)
|
|
413
|
+
added = _git(project_root, "add", "-f", "--", *rels, index_file=temp_index)
|
|
414
|
+
if added.returncode != 0:
|
|
415
|
+
return _result(
|
|
416
|
+
False,
|
|
417
|
+
action,
|
|
418
|
+
"GIT_ADD_FAILED",
|
|
419
|
+
rels,
|
|
420
|
+
error=added.stderr.strip(),
|
|
421
|
+
)
|
|
422
|
+
cached = _git(
|
|
423
|
+
project_root,
|
|
424
|
+
"diff",
|
|
425
|
+
"--cached",
|
|
426
|
+
"--name-only",
|
|
427
|
+
index_file=temp_index,
|
|
428
|
+
)
|
|
429
|
+
if cached.returncode != 0:
|
|
430
|
+
return _result(False, action, "CACHED_DIFF_FAILED", rels)
|
|
431
|
+
after_cached = set(cached.stdout.splitlines())
|
|
432
|
+
missing = [rel for rel in rels if rel not in after_cached]
|
|
433
|
+
unexpected = sorted((after_cached - before_cached) - set(rels))
|
|
434
|
+
if missing or unexpected:
|
|
435
|
+
return _result(
|
|
436
|
+
False,
|
|
437
|
+
action,
|
|
438
|
+
"CACHED_DIFF_MISMATCH",
|
|
439
|
+
missing or unexpected,
|
|
440
|
+
)
|
|
441
|
+
os.replace(temp_index, index_path)
|
|
442
|
+
return _result(True, action, "STAGED", rels)
|
|
443
|
+
finally:
|
|
444
|
+
temp_index.unlink(missing_ok=True)
|
|
445
|
+
|
|
446
|
+
|
|
447
|
+
def stage(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
|
|
448
|
+
action = "stage"
|
|
449
|
+
project_root = Path(project).resolve()
|
|
450
|
+
change_root = _change_dir(project_root, change_dir)
|
|
451
|
+
if change_root is None:
|
|
452
|
+
return _result(False, action, "CHANGE_DIR_OUTSIDE_PROJECT", [])
|
|
453
|
+
manifest_path = _manifest_path(project_root, change_root)
|
|
454
|
+
if manifest_path is None:
|
|
455
|
+
return _result(False, action, "MANIFEST_PATH_OUTSIDE_PROJECT", [])
|
|
456
|
+
|
|
457
|
+
index_result = _git(project_root, "rev-parse", "--git-path", "index")
|
|
458
|
+
if index_result.returncode != 0 or not index_result.stdout.strip():
|
|
459
|
+
return _result(False, action, "GIT_INDEX_NOT_FOUND", [])
|
|
460
|
+
index_path = Path(index_result.stdout.strip())
|
|
461
|
+
if not index_path.is_absolute():
|
|
462
|
+
index_path = (project_root / index_path).resolve()
|
|
463
|
+
index_path.parent.mkdir(parents=True, exist_ok=True)
|
|
464
|
+
|
|
465
|
+
index_lock = index_path.with_name(index_path.name + ".lock")
|
|
466
|
+
try:
|
|
467
|
+
with _exclusive_lock(index_lock, wait_seconds=0.0):
|
|
468
|
+
manifest_lock = manifest_path.with_name(manifest_path.name + ".lock")
|
|
469
|
+
try:
|
|
470
|
+
with _exclusive_lock(manifest_lock, wait_seconds=0.0):
|
|
471
|
+
return _stage_locked(project_root, manifest_path, index_path)
|
|
472
|
+
except LockUnavailable:
|
|
473
|
+
return _result(False, action, "MANIFEST_LOCKED", [])
|
|
474
|
+
except LockUnavailable:
|
|
475
|
+
return _result(False, action, "INDEX_LOCKED", [])
|
|
476
|
+
|
|
477
|
+
|
|
478
|
+
def main(argv: list[str] | None = None) -> int:
|
|
479
|
+
parser = argparse.ArgumentParser(description=__doc__)
|
|
480
|
+
sub = parser.add_subparsers(dest="action", required=True)
|
|
481
|
+
record_parser = sub.add_parser("record")
|
|
482
|
+
record_parser.add_argument("--project", required=True)
|
|
483
|
+
record_parser.add_argument("--change-dir", required=True)
|
|
484
|
+
record_parser.add_argument("--files", required=True)
|
|
485
|
+
record_parser.add_argument("--reason", required=True, choices=REASONS)
|
|
486
|
+
record_parser.add_argument("--json", action="store_true")
|
|
487
|
+
stage_parser = sub.add_parser("stage")
|
|
488
|
+
stage_parser.add_argument("--project", required=True)
|
|
489
|
+
stage_parser.add_argument("--change-dir", required=True)
|
|
490
|
+
stage_parser.add_argument("--json", action="store_true")
|
|
491
|
+
args = parser.parse_args(argv)
|
|
492
|
+
if args.action == "record":
|
|
493
|
+
files = [item.strip() for item in args.files.split(",") if item.strip()]
|
|
494
|
+
result = record(args.project, args.change_dir, files, args.reason)
|
|
495
|
+
else:
|
|
496
|
+
result = stage(args.project, args.change_dir)
|
|
497
|
+
print(json.dumps(result, ensure_ascii=False, indent=2 if args.json else None))
|
|
498
|
+
return 0 if result["ok"] else 1
|
|
499
|
+
|
|
500
|
+
|
|
501
|
+
if __name__ == "__main__":
|
|
502
|
+
raise SystemExit(main())
|
|
@@ -135,12 +135,12 @@
|
|
|
135
135
|
|
|
136
136
|
## 执行日志与结构化事件机制
|
|
137
137
|
|
|
138
|
-
每个关联具体变更的 skill
|
|
138
|
+
每个关联具体变更的 skill 只维护一个实时事实源:
|
|
139
139
|
|
|
140
|
-
- `.harness/changes/<change-name>/
|
|
141
|
-
- `.harness/changes/<change-name>/
|
|
140
|
+
- `.harness/changes/<change-name>/events.ndjson`:程序化事件层;人类可读上下文、降级原因和解释性文字写入事件 `note`。
|
|
141
|
+
- `.harness/changes/<change-name>/logs/execution-log.md`:由 `harness_events.py` 在 `phase.end` 自动重建的人类审计投影,禁止 Write/Edit。
|
|
142
142
|
|
|
143
|
-
`execution-log.md`
|
|
143
|
+
渲染后的 `execution-log.md` 阶段格式统一:
|
|
144
144
|
|
|
145
145
|
```markdown
|
|
146
146
|
### [<序号>] harness-<skill> — YYYY-MM-DD HH:MM
|
|
@@ -152,16 +152,17 @@
|
|
|
152
152
|
- **摘要**: <一两句话描述主要产出或问题>
|
|
153
153
|
```
|
|
154
154
|
|
|
155
|
-
- harness-plan
|
|
156
|
-
- 后续每个 skill
|
|
157
|
-
-
|
|
155
|
+
- harness-plan 确定 change-name 后先追加 `phase.start`,脚本自动建立事件文件
|
|
156
|
+
- 后续每个 skill 开始时追加 `phase.start` 事件,结束时追加包含耗时/结果的 `phase.end` 事件
|
|
157
|
+
- **任何代码修改前必须先追加 `phase.start`**(不得等执行完才补记录)
|
|
158
158
|
- **降级时必须记录明确原因**(如"Agent 不可用,降级为主会话只读探索"),不可仅写"完成"
|
|
159
|
-
-
|
|
159
|
+
- **禁止末尾一次性补写**——各阶段实时 append 结构化事件,渲染日志不得直接编辑
|
|
160
160
|
- **Bash 拒绝、PowerShell 重试、降级、跳过、用户确认都必须记录**
|
|
161
161
|
- 日志状态统一使用 `✅OK / 🟡WARN(原因) / ❌FAIL(原因)`
|
|
162
162
|
- harness-archive 归档时从日志汇总:时间线、总用时、Skill 调用统计
|
|
163
163
|
- `harness_archive.py finalize/replay` 优先从 `events.ndjson` 汇总命令、验证、artifact、问题和决策;旧 archive 缺少 events 时才回放 execution-log/ledger/manifest
|
|
164
164
|
- sync 默认不关联具体变更目录,仅在已有变更目录时追加日志和 events
|
|
165
|
+
- archive 是例外:finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加阶段边界
|
|
165
166
|
|
|
166
167
|
## Skill 目录结构
|
|
167
168
|
|
|
@@ -259,10 +260,10 @@ harness-skills/
|
|
|
259
260
|
| 阶段 | 强制检查 |
|
|
260
261
|
|------|---------|
|
|
261
262
|
| **plan 阶段 0** | 检查工作区是否有未提交业务代码变更,已有则询问用户处理方式(继续/暂存/回滚/取消),不得假装"编码前规划" |
|
|
262
|
-
| **plan 阶段 3** | 委派前 `harness_preflight.py check-agents --agent harness-explorer`;`usable=false` 或无效返回 → 主会话只读探索,不 retry |
|
|
263
|
+
| **plan 阶段 3** | 委派前 `python <skills-root>/scripts/harness_preflight.py check-agents --skills-root <skills-root> --agent harness-explorer --json`;`usable=false` 或无效返回 → 主会话只读探索,不 retry |
|
|
263
264
|
| **plan 阶段 4/6** | 原生规划协议必须记录风险/复用/替代方案/推荐方案/关键决策,以及任务拆分摘要 |
|
|
264
265
|
| **plan 阶段 5** | 设计文档自审结果必须展示给用户;测试场景表未覆盖维度必须标记为 ⚠️ 缺口,不得全部 ✅ |
|
|
265
|
-
| **run 步骤 0** |
|
|
266
|
+
| **run 步骤 0** | 任何代码修改前必须先向 `events.ndjson` 追加 `phase.start`;执行日志由阶段结束事件自动渲染 |
|
|
266
267
|
| **run 轻量验证** | `/harness-run` 默认只做开发反馈:TDD RED/GREEN + REFACTOR + 构建命令增量编译(Java 的 `mvn compile -pl <module>` 等)+ 关门检查 + 写 verification-ledger;除非改了公共模块/数据访问层/sql/权限认证/接口层/数据契约 或用户要求 full-run-validation 或不打算继续 `/harness-test`,否则不默认跑全量测试命令(Java 的 `mvn test` 等)。若跑了全量测试必须写入 ledger 供 test/submit 复用 |
|
|
267
268
|
| **run TDD 降级** | 输出必须写"🟡 静态逻辑验证通过,未执行真实单元测试",**禁止写"测试全部通过"**;降级标注写在执行日志和覆盖报告中,**不污染业务代码注释**;记录三项:降级原因、静态验证场景列表、待部署后验证场景列表 |
|
|
268
269
|
| **test ledger 复用** | Phase 1 单元测试前先读 `verification-ledger.json`:若 run 阶段已对同一 diffHash/module/profile 跑过单元测试命令(Java 的 `mvn test` 等)且测试通过(Java 的 `Tests run: N, Failures: 0, Errors: 0` / 前端 N passing),可跳过重跑并标记"✅ 复用 harness-run 单元测试结果";diffHash 不一致 / profile 不一致 / 命令范围更窄 / run 后有行为性修改则不得复用 |
|
|
@@ -276,7 +277,7 @@ harness-skills/
|
|
|
276
277
|
| **submit 步骤 4** | 提交前必须展示四项:实际 staged 文件列表、diff stat、commit message、是否 push |
|
|
277
278
|
| **submit push 前** | `git fetch` 后检查远程是否有新提交;有则**不得直接 pull 后 push**,必须 pull/rebase + 重新 compile/test |
|
|
278
279
|
| **submit hash 记录** | pre-pull local hash + final pushed hash 双标注(主目录);worktree 模式 submit 段只本地 commit,合并段产生 `mergeFinalHash`,archive 以 `mergeFinalHash` 为准(无则回退 final pushed hash) |
|
|
279
|
-
| **archive 阶段 1** |
|
|
280
|
+
| **archive 阶段 1** | 归档前确认 commit 已 push、hash 与 submit/merge 记录一致、test/review 报告状态;阶段边界由 finalize 单进程维护,调用者不额外 append |
|
|
280
281
|
| **archive 文件移动** | 只用 PowerShell 或 Read+Write+验证,**禁止 Bash mv/cp/rm**;移动失败时不删除原目录 |
|
|
281
282
|
| **archive final-summary.html** | 默认运行 `harness_archive.py finalize`:由 events/ledger/log/manifest 生成 `summary-data.json`,再由 `templates/render-summary.mjs` 渲染 `final-summary.html`,内嵌 validate。无测试或无 review 时必须在 JSON 中标记 `NOT_RUN` / `ADVISORY_NOT_RUN`,禁止伪造 100% 通过率。必须真实展示状态演进(✅OK / 🟡WARN / 🔁REUSED / 🔁RETESTED / 📝ADVISORY / 🧹NON_BEHAVIORAL_CLEANUP) |
|
|
282
283
|
|
|
@@ -3,7 +3,7 @@ name: harness-archive
|
|
|
3
3
|
description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
|
|
4
4
|
.harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=2df0efbebf1f603a; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -82,11 +82,9 @@ AskUserQuestion 让用户确认归档操作。**用户拒绝 → 终止流程,
|
|
|
82
82
|
|
|
83
83
|
### Phase 3:执行归档
|
|
84
84
|
|
|
85
|
-
1.
|
|
86
|
-
2.
|
|
87
|
-
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON
|
|
88
|
-
4. 模型补写 `meta/archive-meta.md` 的维护者结论 / knownRisks / manualActions(脚本占位字段)。
|
|
89
|
-
5. append `phase.end` 事件。**finalize 失败或 validate 报错时不删除原目录**。
|
|
85
|
+
1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ".harness/changes/<change-name>" --json` 前置检查。
|
|
86
|
+
2. 如需维护者结论,在移动前补全 `meta/archive-meta.md`;不得在 finalize 后修改已校验的归档文件。
|
|
87
|
+
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ".harness/changes/<change-name>" --archive-root ".harness/archive" --json`;读 JSON(事件、移动、collect、render、validate、manifest 比对)。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 报错时不删除原目录**。
|
|
90
88
|
|
|
91
89
|
- **Read `reference.md`** — finalize 输出字段、archive-meta 格式、CONDITIONAL_OK 规则
|
|
92
90
|
- **Read `templates/summary-data-template.json`** — summary-data 数据结构
|
|
@@ -153,9 +151,9 @@ API 测试 `USER_SKIPPED` 或 DB 兼容 `BLOCKED_BY_DBA` 时,最终状态必
|
|
|
153
151
|
|
|
154
152
|
未提交但用于验证的测试文件必须归档到 `backups/uncommitted-tests/` 并在 final-summary 中展示。
|
|
155
153
|
|
|
156
|
-
###
|
|
154
|
+
### 十一、归档事件单一所有权
|
|
157
155
|
|
|
158
|
-
|
|
156
|
+
finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,并在移动后继续向归档目录中的同一事件流追加。调用者不得在 finalize 前后重复追加阶段边界,否则会造成重复阶段或在原 changes 路径生成幽灵目录。归档后 events.ndjson 与自动渲染的 execution-log.md 一起位于 archive。
|
|
159
157
|
|
|
160
158
|
### 十二、Shell 安全 / 敏感信息 / 证据化报告
|
|
161
159
|
|
|
@@ -187,7 +185,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
|
|
|
187
185
|
|
|
188
186
|
## 执行日志
|
|
189
187
|
|
|
190
|
-
`events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py`
|
|
188
|
+
`events.ndjson` 为唯一事实源(schema_version 2,`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
191
189
|
|
|
192
190
|
```powershell
|
|
193
191
|
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
@@ -16,12 +16,12 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
|
|
|
16
16
|
|
|
17
17
|
## 归档前检查(Phase 1)
|
|
18
18
|
|
|
19
|
-
> ⚠️
|
|
19
|
+
> ⚠️ **单一所有权**:finalize 内部负责且仅负责一次 `phase.start` / `phase.end`。归档前检查不得自行追加 archive 阶段边界。
|
|
20
20
|
|
|
21
|
-
- [ ]
|
|
21
|
+
- [ ] 未在调用 finalize 前手工追加 archive 阶段边界
|
|
22
22
|
- [ ] 只有一个未归档变更目录(多个时终止或让用户选择)
|
|
23
23
|
- [ ] 变更目录下有 plans/ 子目录(至少有计划文件)
|
|
24
|
-
- [ ] `
|
|
24
|
+
- [ ] `events.ndjson` 存在;执行日志允许由 finalize 从事件流重新渲染(旧 archive 才兼容根目录 `execution-log.md`)
|
|
25
25
|
- [ ] 准备生成 `archive-manifest-before.json`(path/size/sha256)
|
|
26
26
|
- [ ] 准备生成 `summary-data.json`(业务目标、阶段状态、验证、产物、维护者结论)
|
|
27
27
|
- [ ] git status 无未提交的重要变更(归档应对应已提交的代码)
|
|
@@ -40,7 +40,7 @@ description: harness-archive 的归档前检查项和归档后验证项。仅在
|
|
|
40
40
|
- [ ] `.harness/archive/YYYY-MM-DD-<change-name>/` 目录存在(通过 Glob 实际扫描确认)
|
|
41
41
|
- [ ] 所有子目录(plans/, tests/, reviews/, sqls/)已完整移入(通过 Glob 实际扫描确认,不仅看预期路径)
|
|
42
42
|
- [ ] before/after manifest 校验通过(排除 `logs/execution-log.md`——归档追加结束记录预期 sha256 变化;其他 moved 文件 sha256 必须一致,missing/mismatch=0)
|
|
43
|
-
- [ ] archive-meta.md
|
|
43
|
+
- [ ] 如需 archive-meta.md,已在 finalize 前创建且 frontmatter 字段完整;finalize 后未再改动归档文件
|
|
44
44
|
- [ ] summary-data.json 已生成,且为合法 JSON
|
|
45
45
|
- [ ] final-summary.html 已由 `templates/render-summary.mjs` 渲染生成
|
|
46
46
|
- [ ] **final-summary.html 真实性检查**:
|
|
@@ -10,11 +10,9 @@ description: harness-archive 的归档流程、manifest、summary-data、final-s
|
|
|
10
10
|
- **Phase 1 确认归档对象**:Glob `.harness/changes/*/plans/*-plan.md`(排除 archive),展示概要;多变更让用户选择或终止。
|
|
11
11
|
- **Phase 2 确认归档(强制阻断)**:AskUserQuestion 确认,拒绝即终止。
|
|
12
12
|
- **Phase 3 执行归档**:
|
|
13
|
-
1.
|
|
14
|
-
2.
|
|
15
|
-
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ... --archive-root ".harness/archive" --json`;读 JSON 结果。
|
|
16
|
-
4. 模型补写 `meta/archive-meta.md` 的 `maintenanceNotes` / `knownRisks` / `manualActions`(脚本留空占位)。
|
|
17
|
-
5. append `phase.end` 事件。**finalize 报错或 validate 失败时不删除原 changes 目录**。
|
|
13
|
+
1. 运行 `python <skills-root>/scripts/harness_archive.py status --change-dir ... --json` 做前置检查。
|
|
14
|
+
2. 需要维护者结论时,在移动前补全 `meta/archive-meta.md` 的 `maintenanceNotes` / `knownRisks` / `manualActions`;finalize 后不再修改归档内容。
|
|
15
|
+
3. 运行 `python <skills-root>/scripts/harness_archive.py finalize --change-dir ... --archive-root ".harness/archive" --json`;读 JSON 结果。finalize 内部负责且仅负责一次 `phase.start` / `phase.end`,调用者不得重复追加。**finalize 报错或 validate 失败时不删除原 changes 目录**。
|
|
18
16
|
- **Phase 4 验证与提示**:见 `checklist.md` 归档后验证项。
|
|
19
17
|
|
|
20
18
|
## manifest 生成
|