@hunter-harness/workflow-harness 0.2.19 → 0.2.21
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/CONTEXT.md +20 -0
- package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-run/checklist.md +6 -0
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/checklist.md +8 -0
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/checklist.md +8 -0
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/CONTEXT.md +20 -0
- package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-run/checklist.md +6 -0
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/checklist.md +8 -0
- package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/general/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/CONTEXT.md +20 -0
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-run/checklist.md +6 -0
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/checklist.md +8 -0
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/general/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/general/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/general/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +149 -9
- package/harness/bundles/general/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/CONTEXT.md +20 -0
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-submit/checklist.md +8 -0
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/claude-code/scripts/harness_events.py +159 -1
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/claude-code/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/CONTEXT.md +20 -0
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-submit/checklist.md +8 -0
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codebuddy/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/codebuddy/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/CONTEXT.md +20 -0
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-submit/checklist.md +8 -0
- package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/codex/scripts/harness_events.py +159 -1
- package/harness/bundles/java/codex/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/codex/scripts/harness_migration_head.py +220 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/CONTEXT.md +20 -0
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-submit/checklist.md +8 -0
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/protocols/model-routing-protocol.md +22 -0
- package/harness/bundles/java/cursor/scripts/harness_events.py +159 -1
- package/harness/bundles/java/cursor/scripts/harness_integration.py +34 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +149 -9
- package/harness/bundles/java/cursor/scripts/harness_migration_head.py +220 -0
- package/harness/manifests/general/claude-code.json +25 -17
- package/harness/manifests/general/codebuddy.json +25 -17
- package/harness/manifests/general/codex.json +25 -17
- package/harness/manifests/general/cursor.json +25 -17
- package/harness/manifests/java/claude-code.json +26 -18
- package/harness/manifests/java/codebuddy.json +26 -18
- package/harness/manifests/java/codex.json +26 -18
- package/harness/manifests/java/cursor.json +26 -18
- package/hunter-workflow-family.json +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,220 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Migration head single-source consistency check (Wave-2 H-7).
|
|
3
|
+
|
|
4
|
+
Canonical file (default ``.harness/config/migration-head.json``)::
|
|
5
|
+
|
|
6
|
+
{
|
|
7
|
+
"schemaVersion": 1,
|
|
8
|
+
"head": "005_example",
|
|
9
|
+
"sources": [
|
|
10
|
+
{"path": "backend/app/config.py", "pattern": "EXPECTED_ALEMBIC_HEAD\\s*=\\s*[\\\"']([^\\\"']+)[\\\"']"},
|
|
11
|
+
{"path": "integration/manifest.json", "jsonPointer": "/migrationHead"}
|
|
12
|
+
]
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
Each source must resolve to the same ``head`` value. This harness repo may not
|
|
16
|
+
ship alembic; consumer projects declare the canonical file and run::
|
|
17
|
+
|
|
18
|
+
python harness_migration_head.py check --project . --json
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from __future__ import annotations
|
|
22
|
+
|
|
23
|
+
import argparse
|
|
24
|
+
import json
|
|
25
|
+
import re
|
|
26
|
+
import sys
|
|
27
|
+
from pathlib import Path
|
|
28
|
+
from typing import Any
|
|
29
|
+
|
|
30
|
+
DEFAULT_REL = ".harness/config/migration-head.json"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def emit_json(payload: dict[str, Any], *, as_json: bool) -> None:
|
|
34
|
+
if as_json:
|
|
35
|
+
sys.stdout.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
36
|
+
else:
|
|
37
|
+
code = payload.get("code") or ("OK" if payload.get("ok") else "FAIL")
|
|
38
|
+
sys.stdout.write(f"{code}\n")
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def emit_error(code: str, message: str, *, as_json: bool, extra: dict[str, Any] | None = None) -> int:
|
|
42
|
+
payload: dict[str, Any] = {"ok": False, "code": code, "message": message}
|
|
43
|
+
if extra:
|
|
44
|
+
payload.update(extra)
|
|
45
|
+
stream = sys.stdout if as_json else sys.stderr
|
|
46
|
+
if as_json:
|
|
47
|
+
stream.write(json.dumps(payload, ensure_ascii=False, indent=2) + "\n")
|
|
48
|
+
else:
|
|
49
|
+
stream.write(f"{code}: {message}\n")
|
|
50
|
+
return 1
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _read_json_pointer(data: Any, pointer: str) -> Any:
|
|
54
|
+
if not pointer.startswith("/"):
|
|
55
|
+
raise ValueError(f"jsonPointer must start with /: {pointer}")
|
|
56
|
+
cur = data
|
|
57
|
+
for part in pointer.lstrip("/").split("/"):
|
|
58
|
+
part = part.replace("~1", "/").replace("~0", "~")
|
|
59
|
+
if isinstance(cur, dict) and part in cur:
|
|
60
|
+
cur = cur[part]
|
|
61
|
+
else:
|
|
62
|
+
raise KeyError(pointer)
|
|
63
|
+
return cur
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def _extract_source(project: Path, source: dict[str, Any]) -> str:
|
|
67
|
+
rel = str(source.get("path") or "").strip().replace("\\", "/")
|
|
68
|
+
if not rel or rel.startswith("/") or ".." in rel.split("/"):
|
|
69
|
+
raise ValueError(f"invalid source path: {rel}")
|
|
70
|
+
target = (project / rel).resolve()
|
|
71
|
+
try:
|
|
72
|
+
target.relative_to(project.resolve())
|
|
73
|
+
except ValueError as exc:
|
|
74
|
+
raise ValueError(f"source escapes project: {rel}") from exc
|
|
75
|
+
if not target.is_file():
|
|
76
|
+
raise FileNotFoundError(rel)
|
|
77
|
+
text = target.read_text(encoding="utf-8-sig")
|
|
78
|
+
if "jsonPointer" in source and source["jsonPointer"]:
|
|
79
|
+
data = json.loads(text)
|
|
80
|
+
value = _read_json_pointer(data, str(source["jsonPointer"]))
|
|
81
|
+
if value is None or (isinstance(value, str) and not value.strip()):
|
|
82
|
+
raise ValueError(f"empty value at {source['jsonPointer']} in {rel}")
|
|
83
|
+
return str(value).strip()
|
|
84
|
+
pattern = str(source.get("pattern") or "").strip()
|
|
85
|
+
if not pattern:
|
|
86
|
+
raise ValueError(f"source {rel} requires pattern or jsonPointer")
|
|
87
|
+
match = re.search(pattern, text, flags=re.MULTILINE)
|
|
88
|
+
if not match:
|
|
89
|
+
raise ValueError(f"pattern did not match in {rel}")
|
|
90
|
+
return str(match.group(1) if match.lastindex else match.group(0)).strip()
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def check_migration_head(
|
|
94
|
+
project: Path,
|
|
95
|
+
*,
|
|
96
|
+
config_rel: str = DEFAULT_REL,
|
|
97
|
+
) -> dict[str, Any]:
|
|
98
|
+
project = project.resolve()
|
|
99
|
+
config_path = (project / config_rel).resolve()
|
|
100
|
+
try:
|
|
101
|
+
config_path.relative_to(project)
|
|
102
|
+
except ValueError:
|
|
103
|
+
return {
|
|
104
|
+
"ok": False,
|
|
105
|
+
"code": "MIGRATION_HEAD_CONFIG_ESCAPE",
|
|
106
|
+
"message": f"config path escapes project: {config_rel}",
|
|
107
|
+
}
|
|
108
|
+
if not config_path.is_file():
|
|
109
|
+
return {
|
|
110
|
+
"ok": False,
|
|
111
|
+
"code": "MIGRATION_HEAD_MISSING",
|
|
112
|
+
"message": f"canonical migration-head file missing: {config_rel}",
|
|
113
|
+
"configPath": str(config_path),
|
|
114
|
+
}
|
|
115
|
+
try:
|
|
116
|
+
data = json.loads(config_path.read_text(encoding="utf-8-sig"))
|
|
117
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
118
|
+
return {
|
|
119
|
+
"ok": False,
|
|
120
|
+
"code": "MIGRATION_HEAD_UNREADABLE",
|
|
121
|
+
"message": str(exc),
|
|
122
|
+
"configPath": str(config_path),
|
|
123
|
+
}
|
|
124
|
+
if not isinstance(data, dict) or data.get("schemaVersion") != 1:
|
|
125
|
+
return {
|
|
126
|
+
"ok": False,
|
|
127
|
+
"code": "MIGRATION_HEAD_SCHEMA_INVALID",
|
|
128
|
+
"message": "migration-head.json must be schemaVersion 1 object",
|
|
129
|
+
"configPath": str(config_path),
|
|
130
|
+
}
|
|
131
|
+
head = str(data.get("head") or "").strip()
|
|
132
|
+
sources = data.get("sources")
|
|
133
|
+
if not head:
|
|
134
|
+
return {
|
|
135
|
+
"ok": False,
|
|
136
|
+
"code": "MIGRATION_HEAD_EMPTY",
|
|
137
|
+
"message": "head must be a non-empty string",
|
|
138
|
+
"configPath": str(config_path),
|
|
139
|
+
}
|
|
140
|
+
if not isinstance(sources, list) or not sources:
|
|
141
|
+
return {
|
|
142
|
+
"ok": False,
|
|
143
|
+
"code": "MIGRATION_HEAD_SOURCES_EMPTY",
|
|
144
|
+
"message": "sources must be a non-empty array",
|
|
145
|
+
"configPath": str(config_path),
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
observed: list[dict[str, Any]] = []
|
|
149
|
+
drifts: list[dict[str, Any]] = []
|
|
150
|
+
for index, source in enumerate(sources):
|
|
151
|
+
if not isinstance(source, dict):
|
|
152
|
+
return {
|
|
153
|
+
"ok": False,
|
|
154
|
+
"code": "MIGRATION_HEAD_SOURCE_INVALID",
|
|
155
|
+
"message": f"sources[{index}] must be an object",
|
|
156
|
+
}
|
|
157
|
+
try:
|
|
158
|
+
value = _extract_source(project, source)
|
|
159
|
+
except (OSError, ValueError, KeyError, json.JSONDecodeError) as exc:
|
|
160
|
+
return {
|
|
161
|
+
"ok": False,
|
|
162
|
+
"code": "MIGRATION_HEAD_SOURCE_UNREADABLE",
|
|
163
|
+
"message": str(exc),
|
|
164
|
+
"index": index,
|
|
165
|
+
"path": source.get("path"),
|
|
166
|
+
}
|
|
167
|
+
item = {"path": source.get("path"), "value": value}
|
|
168
|
+
observed.append(item)
|
|
169
|
+
if value != head:
|
|
170
|
+
drifts.append({**item, "expected": head})
|
|
171
|
+
|
|
172
|
+
if drifts:
|
|
173
|
+
return {
|
|
174
|
+
"ok": False,
|
|
175
|
+
"code": "MIGRATION_HEAD_DRIFT",
|
|
176
|
+
"message": "one or more sources disagree with canonical head",
|
|
177
|
+
"head": head,
|
|
178
|
+
"drifts": drifts,
|
|
179
|
+
"observed": observed,
|
|
180
|
+
"configPath": str(config_path),
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
"ok": True,
|
|
184
|
+
"code": "MIGRATION_HEAD_OK",
|
|
185
|
+
"head": head,
|
|
186
|
+
"observed": observed,
|
|
187
|
+
"configPath": str(config_path),
|
|
188
|
+
"sourceCount": len(observed),
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
193
|
+
parser = argparse.ArgumentParser(prog="harness_migration_head.py")
|
|
194
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
195
|
+
check = sub.add_parser("check", help="verify sources match canonical head")
|
|
196
|
+
check.add_argument("--project", required=True)
|
|
197
|
+
check.add_argument("--config", default=DEFAULT_REL)
|
|
198
|
+
check.add_argument("--json", action="store_true")
|
|
199
|
+
return parser
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def main(argv: list[str] | None = None) -> int:
|
|
203
|
+
args = build_parser().parse_args(argv)
|
|
204
|
+
as_json = bool(getattr(args, "json", False))
|
|
205
|
+
if args.command == "check":
|
|
206
|
+
result = check_migration_head(Path(args.project), config_rel=str(args.config))
|
|
207
|
+
if not result.get("ok"):
|
|
208
|
+
return emit_error(
|
|
209
|
+
str(result.get("code") or "MIGRATION_HEAD_FAIL"),
|
|
210
|
+
str(result.get("message") or "check failed"),
|
|
211
|
+
as_json=as_json,
|
|
212
|
+
extra={k: v for k, v in result.items() if k not in {"ok", "code", "message"}},
|
|
213
|
+
)
|
|
214
|
+
emit_json(result, as_json=as_json)
|
|
215
|
+
return 0
|
|
216
|
+
return emit_error("UNKNOWN_COMMAND", f"unknown command: {args.command}", as_json=as_json)
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
if __name__ == "__main__":
|
|
220
|
+
raise SystemExit(main())
|
|
@@ -48,6 +48,26 @@ _Avoid_: 每次重新探测, skill 内硬编码项目参数
|
|
|
48
48
|
一项验证(compile/install/unitTest/apiTest)所依赖源文件集合的内容哈希。指纹一致且证据完整才允许 🔁REUSED;无法证明即保守重跑。
|
|
49
49
|
_Avoid_: 仅凭 diffHash 复用, 无证据跳过
|
|
50
50
|
|
|
51
|
+
**验证图(verification graph)**:
|
|
52
|
+
`evidence/verification-graph.json` 按 canonical identity 持久化验证节点;`can-reuse` 返回 `invalidationCode`(如 `INPUT_CHANGED`);profile `mergeVerification.requiredOnMerge` 进入 integration verify。详见 ledger / integration 脚本。
|
|
53
|
+
_Avoid_: 冻结后无因重跑全量
|
|
54
|
+
|
|
55
|
+
**Migration head 单源**:
|
|
56
|
+
消费项目用 `.harness/config/migration-head.json` 声明 canonical head 与联动 sources;`harness_migration_head.py check` 做静态一致性门禁。
|
|
57
|
+
_Avoid_: config/CI/manifest 多处手写 head
|
|
58
|
+
|
|
59
|
+
**事件批处理**:
|
|
60
|
+
同阶段多事件用 `harness_events.py batch-append`(单锁多行);`phase.end` 仍走单次 append。
|
|
61
|
+
_Avoid_: 机械事件逐条起进程
|
|
62
|
+
|
|
63
|
+
**能力级模型路由**:
|
|
64
|
+
任务用 `economy` / `balanced` / `frontier` 表述;禁止在通用 harness 规则硬编码供应商模型名。详见 `protocols/model-routing-protocol.md`。
|
|
65
|
+
_Avoid_: skill 内写死 claude-/gpt- 模型 id
|
|
66
|
+
|
|
67
|
+
**Overlay 所有权**:
|
|
68
|
+
Bundle 安装目标由安装器管理;项目自有配置放 `.harness/project-overlay/`(或 project.yaml 侧)。`--force-managed` 必须配合 `--yes`/`--confirmed`,冲突 fail closed。
|
|
69
|
+
_Avoid_: 静默 force-managed
|
|
70
|
+
|
|
51
71
|
**服务会话**:
|
|
52
72
|
`runtime/service-session.json` 记录的 AI 启动服务生命周期。指纹匹配静默复用、不匹配自动重启、归档收尾统一停止;只有用户自启服务占端口才询问。
|
|
53
73
|
_Avoid_: 每轮测试后必杀服务
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-apidoc — 前端接口文档(Java overlay)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-archive — 变更归档
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-codebase-map — 代码库地图
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-ingest
|
|
25
25
|
|
|
26
26
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-knowledge-query
|
|
25
25
|
|
|
26
26
|
并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-package — 增量模块打包(Java overlay)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-plan — 需求规划
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
- Bash(codegraph *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-review — 代码审查
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -21,7 +21,7 @@ disallowed-tools:
|
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
- Bash(codegraph *)
|
|
23
23
|
---
|
|
24
|
-
<!-- generated by harness_deploy.py; core=
|
|
24
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
25
25
|
# harness-run — 需求编码
|
|
26
26
|
|
|
27
27
|
## Purpose
|
|
@@ -20,7 +20,7 @@ disallowed-tools:
|
|
|
20
20
|
- Bash(awk *)
|
|
21
21
|
- Bash(curl *)
|
|
22
22
|
---
|
|
23
|
-
<!-- generated by harness_deploy.py; core=
|
|
23
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
24
24
|
# harness-submit — 最终提交(含 worktree 合并)
|
|
25
25
|
|
|
26
26
|
## Purpose
|
|
@@ -369,6 +369,8 @@ powershell.exe -Command "python harness/scripts/harness_check_gate.py --write"
|
|
|
369
369
|
|
|
370
370
|
### 步骤 M5:push
|
|
371
371
|
|
|
372
|
+
**硬门槛(eslint × feature worktree)**:integration / 主仓 `pre-push` 跑 `npm run check` 时,eslint 不得把 sibling `.worktrees/<change>/` 当成第二工程根(双 `tsconfigRootDir`)。本仓以 `eslint.config.mjs` 的 `.worktrees/**` ignore 为准;若自定义 eslint 未 ignore,须先 `move_agent_to_root(<projectRoot>)` 再清 feature worktree,再 push。
|
|
373
|
+
|
|
372
374
|
```powershell
|
|
373
375
|
python harness/scripts/harness_integration.py push --change <change-name> --run-id <run-id> --feature-branch harness/<change-name> --target-branch <主分支> --temp-root <task-temp>
|
|
374
376
|
```
|
|
@@ -418,3 +420,9 @@ ledger 顶层写入 `"mergeFinalHash": "<journal pushedHead>"`;经 `harness_ga
|
|
|
418
420
|
- worktree: 已清理(迁根后)
|
|
419
421
|
- 下一步: `/harness-archive`
|
|
420
422
|
```
|
|
423
|
+
|
|
424
|
+
|
|
425
|
+
## Wave-2 — 验证复用与 merge 面(H-9)
|
|
426
|
+
|
|
427
|
+
- [ ] 代码冻结后,同 identity 的 unitTestFull 须 can-reuse;禁止无 invalidationCode 整套重跑
|
|
428
|
+
- [ ] integration verify 会合并 profile mergeVerification.requiredOnMerge(若配置)
|
|
@@ -19,7 +19,7 @@ disallowed-tools:
|
|
|
19
19
|
- Bash(awk *)
|
|
20
20
|
- Bash(curl *)
|
|
21
21
|
---
|
|
22
|
-
<!-- generated by harness_deploy.py; core=
|
|
22
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
23
23
|
# harness-sync — 元数据同步
|
|
24
24
|
|
|
25
25
|
## Purpose
|
|
@@ -22,7 +22,7 @@ disallowed-tools:
|
|
|
22
22
|
- Bash(node *)
|
|
23
23
|
- Bash(codegraph *)
|
|
24
24
|
---
|
|
25
|
-
<!-- generated by harness_deploy.py; core=
|
|
25
|
+
<!-- generated by harness_deploy.py; core=a9c096220b88ca56; overlay=java; agent=claude-code; do not edit -->
|
|
26
26
|
# harness-test — 测试执行
|
|
27
27
|
|
|
28
28
|
## Purpose
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# model-routing-protocol — 能力级模型路由(Wave-2 H-16)
|
|
2
|
+
|
|
3
|
+
> 本协议定义 **economy / balanced / frontier** 三档能力级。通用 Harness 规则与 skill **禁止**硬编码供应商模型名(如 `claude-*`、`gpt-*`、`gemini-*`)。宿主把能力级映射到具体模型;映射表属于项目/宿主配置,不属于本协议正文。
|
|
4
|
+
|
|
5
|
+
## 能力级
|
|
6
|
+
|
|
7
|
+
| 级别 | 适用任务 | 示例 |
|
|
8
|
+
|------|----------|------|
|
|
9
|
+
| `economy` | 机械提取、日志压缩、等待监控、格式化摘要 | 解析测试输出行、压缩长日志、轮询服务就绪 |
|
|
10
|
+
| `balanced` | 常规实现、常规修复、常规文档修订 | 按 plan 实现 CRUD、修 YELLOW、补 checklist |
|
|
11
|
+
| `frontier` | 架构取舍、安全/并发/迁移、最终审查、高风险判断 | 设计审批、RED 安全项、schema 迁移方案、merge 前终审 |
|
|
12
|
+
|
|
13
|
+
## 路由规则
|
|
14
|
+
|
|
15
|
+
1. Skill/任务描述用能力级(`--capability economy|balanced|frontier` 或文案「本任务=frontier」),不写供应商模型 id。
|
|
16
|
+
2. 宿主或 `HUNTER_HARNESS_MODEL` 仅作**遥测**(`executor_model`),不得反过来驱动通用规则分支。
|
|
17
|
+
3. **降级**(frontier→balanced→economy)必须由确定性命令结果或更高能力审查验收;不得用更低能力级的模型输出替代测试/门禁证据。
|
|
18
|
+
4. 无法映射某能力级时:fail closed,列出可用级别,禁止静默落到未知模型。
|
|
19
|
+
|
|
20
|
+
## 与验证的关系
|
|
21
|
+
|
|
22
|
+
能力级不改变 ledger / can-reuse / verification graph 合同。模型便宜不能成为跳过 `unitTestFull` 或忽略 `requiredOnMerge` 的理由。
|
|
@@ -24,7 +24,7 @@ import tempfile
|
|
|
24
24
|
import time
|
|
25
25
|
import uuid
|
|
26
26
|
from pathlib import Path
|
|
27
|
-
from typing import Any
|
|
27
|
+
from typing import Any, Sequence
|
|
28
28
|
|
|
29
29
|
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
30
30
|
if str(SCRIPTS_DIR) not in sys.path:
|
|
@@ -595,6 +595,132 @@ def append_event(
|
|
|
595
595
|
return {"ok": True, "event": event, "events_path": str(events_path_obj), "rendered": False}
|
|
596
596
|
|
|
597
597
|
|
|
598
|
+
def batch_append_events(
|
|
599
|
+
change_dir: Path,
|
|
600
|
+
events: Sequence[dict[str, Any]],
|
|
601
|
+
) -> dict[str, Any]:
|
|
602
|
+
"""Validate then append many events under one lock (Wave-2 H-15).
|
|
603
|
+
|
|
604
|
+
Any invalid item aborts the whole batch before mutation. ``phase.end`` items
|
|
605
|
+
are rejected here — use single ``append`` so render semantics stay explicit.
|
|
606
|
+
"""
|
|
607
|
+
if not isinstance(events, (list, tuple)) or not events:
|
|
608
|
+
return {
|
|
609
|
+
"ok": False,
|
|
610
|
+
"code": "BATCH_EMPTY",
|
|
611
|
+
"message": "batch-append requires a non-empty events array",
|
|
612
|
+
}
|
|
613
|
+
archived = archived_change_dir(change_dir)
|
|
614
|
+
if archived is not None:
|
|
615
|
+
return {
|
|
616
|
+
"ok": False,
|
|
617
|
+
"code": "ARCHIVED_CHANGE_IMMUTABLE",
|
|
618
|
+
"message": str(archived),
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
built: list[dict[str, Any]] = []
|
|
622
|
+
for index, raw in enumerate(events):
|
|
623
|
+
if not isinstance(raw, dict):
|
|
624
|
+
return {
|
|
625
|
+
"ok": False,
|
|
626
|
+
"code": "BATCH_ITEM_INVALID",
|
|
627
|
+
"message": f"events[{index}] must be an object",
|
|
628
|
+
"index": index,
|
|
629
|
+
}
|
|
630
|
+
type_ = str(raw.get("type") or "").strip()
|
|
631
|
+
phase = str(raw.get("phase") or "").strip()
|
|
632
|
+
if not type_ or type_ not in EVENT_TYPES:
|
|
633
|
+
return {
|
|
634
|
+
"ok": False,
|
|
635
|
+
"code": "EVENT_TYPE_INVALID",
|
|
636
|
+
"message": f"events[{index}]: unsupported type: {type_}",
|
|
637
|
+
"index": index,
|
|
638
|
+
}
|
|
639
|
+
if not phase:
|
|
640
|
+
return {
|
|
641
|
+
"ok": False,
|
|
642
|
+
"code": "EVENT_REQUIRED_FIELD",
|
|
643
|
+
"message": f"events[{index}]: phase required",
|
|
644
|
+
"index": index,
|
|
645
|
+
}
|
|
646
|
+
if type_ == "phase.end":
|
|
647
|
+
return {
|
|
648
|
+
"ok": False,
|
|
649
|
+
"code": "BATCH_PHASE_END_FORBIDDEN",
|
|
650
|
+
"message": f"events[{index}]: phase.end must use single append",
|
|
651
|
+
"index": index,
|
|
652
|
+
}
|
|
653
|
+
args = argparse.Namespace(
|
|
654
|
+
change_dir=str(change_dir),
|
|
655
|
+
phase=phase,
|
|
656
|
+
type=type_,
|
|
657
|
+
note=raw.get("note"),
|
|
658
|
+
kind=raw.get("kind"),
|
|
659
|
+
path=raw.get("path"),
|
|
660
|
+
run_id=raw.get("run_id"),
|
|
661
|
+
executor_tool=raw.get("executor_tool"),
|
|
662
|
+
command=raw.get("command"),
|
|
663
|
+
exit_code=raw.get("exit_code"),
|
|
664
|
+
duration_ms=raw.get("duration_ms"),
|
|
665
|
+
status=raw.get("status"),
|
|
666
|
+
name=raw.get("name"),
|
|
667
|
+
code=raw.get("code"),
|
|
668
|
+
severity=raw.get("severity"),
|
|
669
|
+
message=raw.get("message"),
|
|
670
|
+
decision=raw.get("decision"),
|
|
671
|
+
reason=raw.get("reason"),
|
|
672
|
+
issue_id=raw.get("issue_id"),
|
|
673
|
+
scope=raw.get("scope"),
|
|
674
|
+
target_event_id=raw.get("target_event_id"),
|
|
675
|
+
target_field=raw.get("target_field"),
|
|
676
|
+
old_value_hash=raw.get("old_value_hash"),
|
|
677
|
+
new_value_json=raw.get("new_value_json"),
|
|
678
|
+
renamed_from=raw.get("renamed_from"),
|
|
679
|
+
renamed_to=raw.get("renamed_to"),
|
|
680
|
+
change_uuid=raw.get("change_uuid"),
|
|
681
|
+
attempt=raw.get("attempt"),
|
|
682
|
+
executor_agent=raw.get("executor_agent"),
|
|
683
|
+
executor_model=raw.get("executor_model"),
|
|
684
|
+
handoff_from_tool=raw.get("handoff_from_tool"),
|
|
685
|
+
handoff_reason=raw.get("handoff_reason"),
|
|
686
|
+
trace_id=raw.get("trace_id"),
|
|
687
|
+
span_id=raw.get("span_id"),
|
|
688
|
+
parent_span_id=raw.get("parent_span_id"),
|
|
689
|
+
runner_ms=raw.get("runner_ms"),
|
|
690
|
+
orchestration_active_ms=raw.get("orchestration_active_ms"),
|
|
691
|
+
wall_clock_ms=raw.get("wall_clock_ms"),
|
|
692
|
+
user_wait_ms=raw.get("user_wait_ms"),
|
|
693
|
+
legacy_lenient=False,
|
|
694
|
+
json=True,
|
|
695
|
+
)
|
|
696
|
+
validation = validate_append_event(args)
|
|
697
|
+
if validation:
|
|
698
|
+
error_code, message = validation
|
|
699
|
+
return {
|
|
700
|
+
"ok": False,
|
|
701
|
+
"code": error_code,
|
|
702
|
+
"message": f"events[{index}]: {message}",
|
|
703
|
+
"index": index,
|
|
704
|
+
}
|
|
705
|
+
built.append(build_event(args, []))
|
|
706
|
+
|
|
707
|
+
events_path_obj = events_path(change_dir)
|
|
708
|
+
lock_path = events_path_obj.with_name(events_path_obj.name + ".lock")
|
|
709
|
+
with event_file_lock(lock_path):
|
|
710
|
+
for event in built:
|
|
711
|
+
atomic_append_line(
|
|
712
|
+
events_path_obj, json.dumps(event, ensure_ascii=False, separators=(",", ":"))
|
|
713
|
+
)
|
|
714
|
+
return {
|
|
715
|
+
"ok": True,
|
|
716
|
+
"action": "batch-append",
|
|
717
|
+
"count": len(built),
|
|
718
|
+
"events": built,
|
|
719
|
+
"events_path": str(events_path_obj),
|
|
720
|
+
"rendered": False,
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
|
|
598
724
|
def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dict[str, Any]:
|
|
599
725
|
event: dict[str, Any] = {
|
|
600
726
|
"schema_version": SCHEMA_VERSION,
|
|
@@ -1295,6 +1421,25 @@ def cmd_append(args: argparse.Namespace) -> int:
|
|
|
1295
1421
|
return 0
|
|
1296
1422
|
|
|
1297
1423
|
|
|
1424
|
+
def cmd_batch_append(args: argparse.Namespace) -> int:
|
|
1425
|
+
as_json = bool(args.json)
|
|
1426
|
+
change_dir = resolve_change_dir(args.change_dir)
|
|
1427
|
+
batch_path = Path(str(args.file)).expanduser()
|
|
1428
|
+
try:
|
|
1429
|
+
raw = json.loads(batch_path.read_text(encoding="utf-8-sig"))
|
|
1430
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
1431
|
+
return emit_error(f"batch-append failed to read --file: {exc}", as_json=as_json)
|
|
1432
|
+
result = batch_append_events(change_dir, raw if isinstance(raw, list) else [])
|
|
1433
|
+
if not result.get("ok"):
|
|
1434
|
+
return emit_error(
|
|
1435
|
+
str(result.get("message") or "batch-append failed"),
|
|
1436
|
+
as_json=as_json,
|
|
1437
|
+
error_code=str(result.get("code") or "BATCH_APPEND_FAILED"),
|
|
1438
|
+
)
|
|
1439
|
+
emit_json(result, as_json=as_json)
|
|
1440
|
+
return 0
|
|
1441
|
+
|
|
1442
|
+
|
|
1298
1443
|
def cmd_render(args: argparse.Namespace) -> int:
|
|
1299
1444
|
as_json = bool(args.json)
|
|
1300
1445
|
change_dir = resolve_change_dir(args.change_dir)
|
|
@@ -1395,6 +1540,19 @@ def build_parser() -> argparse.ArgumentParser:
|
|
|
1395
1540
|
)
|
|
1396
1541
|
p_append.set_defaults(func=cmd_append)
|
|
1397
1542
|
|
|
1543
|
+
p_batch = sub.add_parser(
|
|
1544
|
+
"batch-append",
|
|
1545
|
+
parents=[common],
|
|
1546
|
+
help="validate and append many events under one lock (H-15)",
|
|
1547
|
+
)
|
|
1548
|
+
p_batch.add_argument("--change-dir", required=True)
|
|
1549
|
+
p_batch.add_argument(
|
|
1550
|
+
"--file",
|
|
1551
|
+
required=True,
|
|
1552
|
+
help="JSON file containing a non-empty array of event objects",
|
|
1553
|
+
)
|
|
1554
|
+
p_batch.set_defaults(func=cmd_batch_append)
|
|
1555
|
+
|
|
1398
1556
|
p_render = sub.add_parser(
|
|
1399
1557
|
"render",
|
|
1400
1558
|
parents=[common],
|
|
@@ -862,8 +862,42 @@ class IntegrationTransaction:
|
|
|
862
862
|
|
|
863
863
|
return self._run_step("merge", action)
|
|
864
864
|
|
|
865
|
+
def _required_on_merge_commands(self) -> list[list[str]]:
|
|
866
|
+
"""Load build-profile mergeVerification.requiredOnMerge (Wave-2 H-9)."""
|
|
867
|
+
import harness_profile
|
|
868
|
+
|
|
869
|
+
profile = harness_profile.load_profile(self.project_root)
|
|
870
|
+
if not isinstance(profile, dict):
|
|
871
|
+
return []
|
|
872
|
+
merge = profile.get("mergeVerification")
|
|
873
|
+
if not isinstance(merge, dict):
|
|
874
|
+
return []
|
|
875
|
+
required = merge.get("requiredOnMerge")
|
|
876
|
+
if not isinstance(required, list):
|
|
877
|
+
return []
|
|
878
|
+
commands: list[list[str]] = []
|
|
879
|
+
for index, item in enumerate(required):
|
|
880
|
+
if isinstance(item, str) and item.strip():
|
|
881
|
+
commands.append(shlex.split(item.strip()))
|
|
882
|
+
continue
|
|
883
|
+
if not isinstance(item, dict):
|
|
884
|
+
raise ValueError(f"requiredOnMerge[{index}] must be string or object")
|
|
885
|
+
raw = item.get("command")
|
|
886
|
+
if isinstance(raw, list) and raw and all(isinstance(x, str) and x for x in raw):
|
|
887
|
+
commands.append([str(x) for x in raw])
|
|
888
|
+
elif isinstance(raw, str) and raw.strip():
|
|
889
|
+
commands.append(shlex.split(raw.strip()))
|
|
890
|
+
else:
|
|
891
|
+
raise ValueError(f"requiredOnMerge[{index}].command missing")
|
|
892
|
+
return commands
|
|
893
|
+
|
|
865
894
|
def verify(self, commands: Sequence[Sequence[str]] | None = None) -> dict[str, Any]:
|
|
866
895
|
commands = list(commands or [])
|
|
896
|
+
# Wave-2 H-9: append profile mergeVerification.requiredOnMerge commands.
|
|
897
|
+
try:
|
|
898
|
+
commands.extend(self._required_on_merge_commands())
|
|
899
|
+
except Exception as exc: # noqa: BLE001 — surface as verify plan error
|
|
900
|
+
raise VerifyPlanMissingError(f"mergeVerification load failed: {exc}") from exc
|
|
867
901
|
|
|
868
902
|
def action(journal: dict[str, Any]) -> None:
|
|
869
903
|
if not commands or any(
|