@hunter-harness/workflow-harness 0.2.4 → 0.2.6
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 +5 -2
- package/harness/bundles/general/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/general/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/claude-code/harness-run/checklist.md +7 -3
- package/harness/bundles/general/claude-code/harness-run/reference.md +4 -10
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/claude-code/harness-test/checklist.md +4 -0
- package/harness/bundles/general/claude-code/harness-test/reference.md +5 -5
- package/harness/bundles/general/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/claude-code/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/claude-code/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/claude-code/scripts/harness_state.py +148 -0
- 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/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codebuddy/harness-run/reference.md +4 -10
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/codebuddy/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codebuddy/harness-test/reference.md +5 -5
- package/harness/bundles/general/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codebuddy/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codebuddy/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/codebuddy/scripts/harness_state.py +148 -0
- 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/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/general/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/codex/harness-review/SKILL.md +5 -2
- package/harness/bundles/general/codex/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/codex/harness-run/checklist.md +7 -3
- package/harness/bundles/general/codex/harness-run/reference.md +4 -10
- package/harness/bundles/general/codex/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/codex/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/codex/harness-test/checklist.md +4 -0
- package/harness/bundles/general/codex/harness-test/reference.md +5 -5
- package/harness/bundles/general/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/codex/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/codex/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/general/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/codex/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/codex/scripts/harness_state.py +148 -0
- 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/harness-archive/SKILL.md +5 -2
- package/harness/bundles/general/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/general/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/general/cursor/harness-review/SKILL.md +5 -2
- package/harness/bundles/general/cursor/harness-run/SKILL.md +19 -4
- package/harness/bundles/general/cursor/harness-run/checklist.md +7 -3
- package/harness/bundles/general/cursor/harness-run/reference.md +4 -10
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +9 -4
- package/harness/bundles/general/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/general/cursor/harness-test/SKILL.md +18 -3
- package/harness/bundles/general/cursor/harness-test/checklist.md +4 -0
- package/harness/bundles/general/cursor/harness-test/reference.md +5 -5
- package/harness/bundles/general/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/general/cursor/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/general/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/general/cursor/scripts/harness_archive.py +190 -32
- package/harness/bundles/general/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/general/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/general/cursor/scripts/harness_profile.py +31 -3
- package/harness/bundles/general/cursor/scripts/harness_state.py +148 -0
- 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/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-archive/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/claude-code/harness-run/reference.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/claude-code/harness-test/reference.md +1 -1
- package/harness/bundles/java/claude-code/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/claude-code/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/claude-code/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/claude-code/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/claude-code/scripts/harness_events.py +134 -40
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/claude-code/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/claude-code/scripts/harness_state.py +148 -0
- 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/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/codebuddy/harness-run/reference.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/codebuddy/harness-test/reference.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codebuddy/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/codebuddy/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codebuddy/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/codebuddy/scripts/harness_state.py +148 -0
- 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/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-archive/reference.md +1 -1
- package/harness/bundles/java/codex/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/codex/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/codex/harness-review/SKILL.md +5 -2
- package/harness/bundles/java/codex/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/codex/harness-run/reference.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/codex/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/codex/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/codex/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/codex/harness-test/reference.md +1 -1
- package/harness/bundles/java/codex/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/codex/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/codex/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/codex/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/codex/scripts/harness_events.py +134 -40
- package/harness/bundles/java/codex/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/codex/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/codex/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/codex/scripts/harness_state.py +148 -0
- 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/harness-apidoc/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-archive/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/templates/render-summary.mjs +78 -155
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +83 -6
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +4 -2
- package/harness/bundles/java/cursor/harness-package/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
- package/harness/bundles/java/cursor/harness-review/SKILL.md +5 -2
- package/harness/bundles/java/cursor/harness-run/SKILL.md +22 -5
- package/harness/bundles/java/cursor/harness-run/reference.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +9 -4
- package/harness/bundles/java/cursor/harness-submit/checklist.md +10 -1
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +5 -3
- package/harness/bundles/java/cursor/harness-test/SKILL.md +20 -3
- package/harness/bundles/java/cursor/harness-test/reference.md +1 -1
- package/harness/bundles/java/cursor/protocols/archive-report-protocol.md +2 -4
- package/harness/bundles/java/cursor/protocols/ledger-protocol.md +8 -16
- package/harness/bundles/java/cursor/protocols/report-pipeline-protocol.md +11 -4
- package/harness/bundles/java/cursor/scripts/harness_archive.py +190 -32
- package/harness/bundles/java/cursor/scripts/harness_events.py +134 -40
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +111 -5
- package/harness/bundles/java/cursor/scripts/harness_preflight.py +46 -3
- package/harness/bundles/java/cursor/scripts/harness_profile.py +31 -3
- package/harness/bundles/java/cursor/scripts/harness_state.py +148 -0
- package/harness/bundles/java/cursor/scripts/harness_test_guard.py +502 -0
- package/harness/manifests/general/claude-code.json +33 -29
- package/harness/manifests/general/codebuddy.json +33 -29
- package/harness/manifests/general/codex.json +33 -29
- package/harness/manifests/general/cursor.json +33 -29
- package/harness/manifests/java/claude-code.json +33 -29
- package/harness/manifests/java/codebuddy.json +33 -29
- package/harness/manifests/java/codex.json +33 -29
- package/harness/manifests/java/cursor.json +33 -29
- 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())
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: harness-apidoc
|
|
3
3
|
description: 从 Java 后端代码生成结构化前端 API 文档(Controller/VO/ErrorCode)。使用场景:接口文档、前端接口文档、API 文档生成
|
|
4
4
|
---
|
|
5
|
-
<!-- generated by harness_deploy.py; core=
|
|
5
|
+
<!-- generated by harness_deploy.py; core=a7decc4da50d0b7f; overlay=java; agent=cursor; do not edit -->
|
|
6
6
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
7
7
|
|
|
8
8
|
## Purpose
|
|
@@ -24,6 +24,7 @@ description: 从 Java 后端代码生成结构化前端 API 文档(Controller/
|
|
|
24
24
|
6. **implementation-detail** — 自适应执行参考;legacy 缺失 🟡WARN,不阻断
|
|
25
25
|
7. **test-scenarios** — 测试真相源:`plans/<change>-test-scenarios.md`
|
|
26
26
|
8. **禁止读取 `docs/superpowers/` 作为正式输入** — 旧草稿仅人工线索
|
|
27
|
+
9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
|
|
27
28
|
|
|
28
29
|
状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
|
|
29
30
|
|
|
@@ -57,7 +58,7 @@ description: 从 Java 后端代码生成结构化前端 API 文档(Controller/
|
|
|
57
58
|
|
|
58
59
|
## 执行日志
|
|
59
60
|
|
|
60
|
-
`events.ndjson` 为唯一事实源(schema_version
|
|
61
|
+
`events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
61
62
|
|
|
62
63
|
```powershell
|
|
63
64
|
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
@@ -66,3 +67,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
|
|
|
66
67
|
> **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
|
|
67
68
|
|
|
68
69
|
> **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
|
|
70
|
+
|
|
71
|
+
每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
@@ -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=a7decc4da50d0b7f; overlay=java; agent=cursor; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -49,6 +49,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
|
|
|
49
49
|
6. **implementation-detail** — 自适应执行参考;legacy 缺失 🟡WARN,不阻断
|
|
50
50
|
7. **test-scenarios** — 测试真相源:`plans/<change>-test-scenarios.md`
|
|
51
51
|
8. **禁止读取 `docs/superpowers/` 作为正式输入** — 旧草稿仅人工线索
|
|
52
|
+
9. **每个 skill 边界只刷新一次状态快照** — 先运行 `python <skills-root>/scripts/harness_state.py capture --project . --change-dir ".harness/changes/<change-name>" --base <baseCommit> --json`;`changedSegments=[]` 时复用已有 profile/rules/map/knowledge/change/code 指纹,不再重复跑全量 `harness-sync`、知识重建或代码库扫描。只有对应 segment 变化或缺失时才定向刷新;快照不能替代源码读取和验证门禁。
|
|
52
53
|
|
|
53
54
|
状态目录分层:新路径优先,旧路径兼容 → [[../protocols/state-layout-protocol.md|state-layout-protocol]]
|
|
54
55
|
|
|
@@ -185,7 +186,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
|
|
|
185
186
|
|
|
186
187
|
## 执行日志
|
|
187
188
|
|
|
188
|
-
`events.ndjson` 为唯一事实源(schema_version
|
|
189
|
+
`events.ndjson` 为唯一事实源(schema_version 3,兼容读取 v1/v2;`note` 承载人类可读摘要);`logs/execution-log.md` 由 `harness_events.py` 渲染,**禁止用 Write/Edit 直接维护**。直接修改的内容会在 `phase.end` 或 finalize 时被完整重建覆盖,属于数据丢失;需要保留的详情必须进入事件 `note`。结构 → [[../protocols/report-pipeline-protocol.md|report-pipeline-protocol]]
|
|
189
190
|
|
|
190
191
|
```powershell
|
|
191
192
|
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
@@ -194,3 +195,5 @@ python <skills-root>/scripts/harness_events.py append --change-dir ".harness/cha
|
|
|
194
195
|
> **脚本接线**:`harness_events.py append`;`harness_archive.py finalize`;`harness_preflight.py check`;`harness_ledger.py can-reuse`;`harness_service.py ensure/stop`(须 `--files`/`serviceStart.inputFiles`)。JSON 输出按 D13 护栏解读。
|
|
195
196
|
|
|
196
197
|
> **Task 4 §6.1 写入契约**:普通 `append` = 加锁 -> 追加一行 -> fsync -> 解锁,**不 load 历史、不渲染**(O(1),跨进程锁 `events.ndjson.lock`,UUID 用完整 `uuid4().hex` 无需去重扫描)。仅 `--type phase.end` append 在追加成功后渲染一次 `execution-log.md`;显式 `harness_events.py render` 随时从完整 events 重建;`harness_archive.py finalize` 在 collect 前强制 render 一次。高频 command append 期间 log 可能滞后,phase 边界保持最新。
|
|
198
|
+
|
|
199
|
+
每次阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`;重试同一阶段时增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
@@ -92,4 +92,4 @@ powershell.exe -NoProfile -Command "& '<node-path>' 'harness-skills/harness-arch
|
|
|
92
92
|
|
|
93
93
|
## 执行日志记录
|
|
94
94
|
|
|
95
|
-
归档只向 `events.ndjson` 追加事件(schema_version
|
|
95
|
+
归档只向 `events.ndjson` 追加事件(schema_version 3,兼容读取 v1/v2);`logs/execution-log.md` 由 `harness_events.py append` 自动渲染,禁止手工 Edit。事件类型与脚本用法见 SKILL.md `## 执行日志` 与 `../protocols/report-pipeline-protocol.md`。
|