@hunter-harness/workflow-harness 0.2.30 → 0.2.31
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 +3 -3
- package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -6
- package/harness/bundles/general/claude-code/harness-plan/checklist.md +10 -4
- package/harness/bundles/general/claude-code/harness-plan/reference.md +17 -17
- package/harness/bundles/general/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/claude-code/harness-run/checklist.md +2 -2
- package/harness/bundles/general/claude-code/harness-run/reference.md +3 -12
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-sync/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/harness-test/SKILL.md +3 -3
- package/harness/bundles/general/claude-code/scripts/harness_gate.py +97 -14
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -6
- package/harness/bundles/general/codebuddy/harness-plan/checklist.md +10 -4
- package/harness/bundles/general/codebuddy/harness-plan/reference.md +17 -17
- package/harness/bundles/general/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codebuddy/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -12
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/harness-test/SKILL.md +3 -3
- package/harness/bundles/general/codebuddy/scripts/harness_gate.py +97 -14
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- package/harness/bundles/general/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -6
- package/harness/bundles/general/codex/harness-plan/checklist.md +10 -4
- package/harness/bundles/general/codex/harness-plan/reference.md +17 -17
- package/harness/bundles/general/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/codex/harness-run/checklist.md +2 -2
- package/harness/bundles/general/codex/harness-run/reference.md +3 -12
- package/harness/bundles/general/codex/harness-submit/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-sync/SKILL.md +3 -3
- package/harness/bundles/general/codex/harness-test/SKILL.md +3 -3
- package/harness/bundles/general/codex/scripts/harness_gate.py +97 -14
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- package/harness/bundles/general/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -6
- package/harness/bundles/general/cursor/harness-plan/checklist.md +10 -4
- package/harness/bundles/general/cursor/harness-plan/reference.md +17 -17
- package/harness/bundles/general/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/general/cursor/harness-run/checklist.md +2 -2
- package/harness/bundles/general/cursor/harness-run/reference.md +3 -12
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-sync/SKILL.md +3 -3
- package/harness/bundles/general/cursor/harness-test/SKILL.md +3 -3
- package/harness/bundles/general/cursor/scripts/harness_gate.py +97 -14
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -6
- package/harness/bundles/java/claude-code/harness-plan/checklist.md +10 -4
- package/harness/bundles/java/claude-code/harness-plan/reference.md +17 -17
- package/harness/bundles/java/claude-code/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/claude-code/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-sync/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +3 -3
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +97 -14
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -6
- package/harness/bundles/java/codebuddy/harness-plan/checklist.md +10 -4
- package/harness/bundles/java/codebuddy/harness-plan/reference.md +17 -17
- package/harness/bundles/java/codebuddy/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +3 -3
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +97 -14
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- package/harness/bundles/java/codex/harness-apidoc/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -6
- package/harness/bundles/java/codex/harness-plan/checklist.md +10 -4
- package/harness/bundles/java/codex/harness-plan/reference.md +17 -17
- package/harness/bundles/java/codex/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/codex/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-sync/SKILL.md +3 -3
- package/harness/bundles/java/codex/harness-test/SKILL.md +3 -3
- package/harness/bundles/java/codex/scripts/harness_gate.py +97 -14
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +364 -74
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-archive/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-package/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -6
- package/harness/bundles/java/cursor/harness-plan/checklist.md +10 -4
- package/harness/bundles/java/cursor/harness-plan/reference.md +17 -17
- package/harness/bundles/java/cursor/harness-review/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-run/SKILL.md +4 -4
- package/harness/bundles/java/cursor/harness-submit/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-sync/SKILL.md +3 -3
- package/harness/bundles/java/cursor/harness-test/SKILL.md +3 -3
- package/harness/bundles/java/cursor/scripts/harness_gate.py +97 -14
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +364 -74
- package/harness/manifests/general/claude-code.json +19 -19
- package/harness/manifests/general/codebuddy.json +19 -19
- package/harness/manifests/general/codex.json +19 -19
- package/harness/manifests/general/cursor.json +19 -19
- package/harness/manifests/java/claude-code.json +19 -19
- package/harness/manifests/java/codebuddy.json +19 -19
- package/harness/manifests/java/codex.json +19 -19
- package/harness/manifests/java/cursor.json +19 -19
- package/hunter-workflow-family.json +4 -4
- package/package.json +1 -1
|
@@ -51,6 +51,28 @@ def _result_error(code: str, message: str) -> dict[str, Any]:
|
|
|
51
51
|
return {"ok": False, "code": code, "error": message}
|
|
52
52
|
|
|
53
53
|
|
|
54
|
+
def _normalize_header(value: str) -> str:
|
|
55
|
+
return re.sub(r"[\s_-]+", "", value).lower()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _column_index(
|
|
59
|
+
headers: list[str],
|
|
60
|
+
*aliases: str,
|
|
61
|
+
) -> int | None:
|
|
62
|
+
normalized = {
|
|
63
|
+
_normalize_header(header): index for index, header in enumerate(headers)
|
|
64
|
+
}
|
|
65
|
+
for alias in aliases:
|
|
66
|
+
index = normalized.get(_normalize_header(alias))
|
|
67
|
+
if index is not None:
|
|
68
|
+
return index
|
|
69
|
+
return None
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def _table_cells(line: str) -> list[str]:
|
|
73
|
+
return [cell.strip() for cell in line.strip().strip("|").split("|")]
|
|
74
|
+
|
|
75
|
+
|
|
54
76
|
def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
55
77
|
"""C9: parse test-scenarios.md tables, extracting scenario rows.
|
|
56
78
|
|
|
@@ -60,42 +82,100 @@ def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
|
60
82
|
text = Path(scenarios_path).read_text(encoding="utf-8-sig")
|
|
61
83
|
lines = text.splitlines()
|
|
62
84
|
scenarios: list[dict[str, str]] = []
|
|
63
|
-
# Find all tables whose header includes "ID" and "优先级" (or "priority").
|
|
64
85
|
i = 0
|
|
65
86
|
while i < len(lines):
|
|
66
87
|
line = lines[i].strip()
|
|
67
|
-
if line.startswith("|")
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
88
|
+
if line.startswith("|"):
|
|
89
|
+
headers = _table_cells(line)
|
|
90
|
+
id_index = _column_index(headers, "ID", "#", "编号", "场景 ID")
|
|
91
|
+
scenario_index = _column_index(
|
|
92
|
+
headers,
|
|
93
|
+
"场景",
|
|
94
|
+
"场景描述",
|
|
95
|
+
"scenario",
|
|
96
|
+
"description",
|
|
97
|
+
)
|
|
98
|
+
if id_index is None or scenario_index is None:
|
|
99
|
+
i += 1
|
|
100
|
+
continue
|
|
101
|
+
priority_index = _column_index(headers, "优先级", "priority")
|
|
102
|
+
category_index = _column_index(headers, "分类", "category")
|
|
103
|
+
verification_index = _column_index(
|
|
104
|
+
headers,
|
|
105
|
+
"验证方式",
|
|
106
|
+
"verification",
|
|
107
|
+
"可复用证据",
|
|
108
|
+
)
|
|
109
|
+
owner_phase_index = _column_index(
|
|
110
|
+
headers,
|
|
111
|
+
"owner phase",
|
|
112
|
+
"ownerPhase",
|
|
113
|
+
"负责阶段",
|
|
114
|
+
)
|
|
115
|
+
execution_tier_index = _column_index(
|
|
116
|
+
headers,
|
|
117
|
+
"执行层级",
|
|
118
|
+
"execution tier",
|
|
119
|
+
"tier",
|
|
120
|
+
)
|
|
121
|
+
expected_index = _column_index(headers, "预期", "expected")
|
|
122
|
+
|
|
123
|
+
row_index = i + 2
|
|
124
|
+
while row_index < len(lines):
|
|
125
|
+
row = lines[row_index].strip()
|
|
78
126
|
if not row.startswith("|"):
|
|
79
127
|
break
|
|
80
|
-
cells =
|
|
81
|
-
if
|
|
82
|
-
|
|
128
|
+
cells = _table_cells(row)
|
|
129
|
+
if max(id_index, scenario_index) >= len(cells):
|
|
130
|
+
row_index += 1
|
|
83
131
|
continue
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
(
|
|
132
|
+
complete_row = len(cells) == len(headers)
|
|
133
|
+
scenario_id = cells[id_index].strip()
|
|
134
|
+
if not scenario_id or set(scenario_id) <= {"-", ":"}:
|
|
135
|
+
row_index += 1
|
|
136
|
+
continue
|
|
137
|
+
priority = (
|
|
138
|
+
cells[priority_index].strip().upper()
|
|
139
|
+
if priority_index is not None
|
|
140
|
+
and priority_index < len(cells)
|
|
141
|
+
and cells[priority_index].strip()
|
|
142
|
+
else "P1"
|
|
143
|
+
)
|
|
144
|
+
scenario = {
|
|
145
|
+
"id": scenario_id,
|
|
146
|
+
"priority": priority,
|
|
147
|
+
"scenario": cells[scenario_index].strip(),
|
|
148
|
+
"ownerPhase": (
|
|
149
|
+
cells[owner_phase_index].strip()
|
|
150
|
+
if owner_phase_index is not None
|
|
151
|
+
and complete_row
|
|
152
|
+
and owner_phase_index < len(cells)
|
|
153
|
+
and cells[owner_phase_index].strip()
|
|
154
|
+
else "test"
|
|
155
|
+
),
|
|
156
|
+
"requiredEvidenceKind": PRIORITY_EVIDENCE_KIND.get(
|
|
157
|
+
priority,
|
|
158
|
+
"advisory",
|
|
159
|
+
),
|
|
160
|
+
}
|
|
161
|
+
if category_index is not None and category_index < len(cells):
|
|
162
|
+
if cells[category_index].strip():
|
|
163
|
+
scenario["category"] = cells[category_index].strip()
|
|
164
|
+
for key, index in (
|
|
165
|
+
("verification", verification_index),
|
|
166
|
+
("executionTier", execution_tier_index),
|
|
167
|
+
("expected", expected_index),
|
|
91
168
|
):
|
|
92
|
-
if
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
169
|
+
if (
|
|
170
|
+
complete_row
|
|
171
|
+
and index is not None
|
|
172
|
+
and index < len(cells)
|
|
173
|
+
and cells[index].strip()
|
|
174
|
+
):
|
|
175
|
+
scenario[key] = cells[index].strip()
|
|
97
176
|
scenarios.append(scenario)
|
|
98
|
-
|
|
177
|
+
row_index += 1
|
|
178
|
+
i = row_index
|
|
99
179
|
continue
|
|
100
180
|
i += 1
|
|
101
181
|
return scenarios
|
|
@@ -110,40 +190,52 @@ def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
|
110
190
|
"""
|
|
111
191
|
text = Path(plan_path).read_text(encoding="utf-8-sig")
|
|
112
192
|
lines = text.splitlines()
|
|
113
|
-
# Find the task table: a header row starting with "| #" followed by a separator row.
|
|
114
|
-
header_idx = -1
|
|
115
|
-
for i, line in enumerate(lines):
|
|
116
|
-
stripped = line.strip()
|
|
117
|
-
if stripped.startswith("|") and "#" in stripped and "任务" in stripped:
|
|
118
|
-
header_idx = i
|
|
119
|
-
break
|
|
120
|
-
if header_idx < 0:
|
|
121
|
-
return []
|
|
122
|
-
header_line = lines[header_idx]
|
|
123
|
-
headers = [h.strip() for h in header_line.strip().strip("|").split("|")]
|
|
124
|
-
# Map header names to column indices (case-insensitive, English keys).
|
|
125
|
-
col_map: dict[str, int] = {}
|
|
126
|
-
for idx, name in enumerate(headers):
|
|
127
|
-
key = name.lower()
|
|
128
|
-
col_map[key] = idx
|
|
129
193
|
tasks: list[dict[str, str]] = []
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
194
|
+
i = 0
|
|
195
|
+
while i < len(lines):
|
|
196
|
+
line = lines[i].strip()
|
|
197
|
+
if not line.startswith("|"):
|
|
198
|
+
i += 1
|
|
199
|
+
continue
|
|
200
|
+
headers = _table_cells(line)
|
|
201
|
+
number_index = _column_index(headers, "#", "任务编号", "task ID")
|
|
202
|
+
task_index = _column_index(headers, "任务", "task")
|
|
203
|
+
if number_index is None or task_index is None:
|
|
204
|
+
i += 1
|
|
136
205
|
continue
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
206
|
+
cluster_index = _column_index(headers, "簇", "cluster")
|
|
207
|
+
optional_indices = {
|
|
208
|
+
name: _column_index(headers, name)
|
|
209
|
+
for name in (
|
|
210
|
+
"ownerPhase",
|
|
211
|
+
"implementationDoneWhen",
|
|
212
|
+
"verificationPhase",
|
|
213
|
+
"requiresExplicitAuthority",
|
|
214
|
+
)
|
|
215
|
+
}
|
|
216
|
+
row_index = i + 2
|
|
217
|
+
while row_index < len(lines):
|
|
218
|
+
row = lines[row_index].strip()
|
|
219
|
+
if not row.startswith("|"):
|
|
220
|
+
break
|
|
221
|
+
cells = _table_cells(row)
|
|
222
|
+
if len(cells) < len(headers):
|
|
223
|
+
row_index += 1
|
|
224
|
+
continue
|
|
225
|
+
number = cells[number_index].strip()
|
|
226
|
+
task_text = cells[task_index].strip()
|
|
227
|
+
if not number or not task_text or set(number) <= {"-", ":"}:
|
|
228
|
+
row_index += 1
|
|
229
|
+
continue
|
|
230
|
+
task: dict[str, str] = {"num": number, "task": task_text}
|
|
231
|
+
if cluster_index is not None and cells[cluster_index].strip():
|
|
232
|
+
task["cluster"] = cells[cluster_index].strip()
|
|
233
|
+
for name, index in optional_indices.items():
|
|
234
|
+
if index is not None and cells[index].strip():
|
|
235
|
+
task[name] = cells[index].strip()
|
|
236
|
+
tasks.append(task)
|
|
237
|
+
row_index += 1
|
|
238
|
+
i = row_index
|
|
147
239
|
return tasks
|
|
148
240
|
|
|
149
241
|
|
|
@@ -325,6 +417,20 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
325
417
|
# C8: validate ownerPhase values in plan.md task table.
|
|
326
418
|
plan_path = staging / "plans" / f"{change_name}-plan.md"
|
|
327
419
|
tasks = parse_plan_tasks(plan_path)
|
|
420
|
+
if not tasks:
|
|
421
|
+
return _result_error(
|
|
422
|
+
"PLAN_TASKS_EMPTY",
|
|
423
|
+
f"{plan_path.relative_to(staging).as_posix()}: no task rows were parsed",
|
|
424
|
+
)
|
|
425
|
+
task_ids = [str(task.get("num") or "").strip() for task in tasks]
|
|
426
|
+
duplicate_task_ids = sorted(
|
|
427
|
+
{task_id for task_id in task_ids if task_ids.count(task_id) > 1}
|
|
428
|
+
)
|
|
429
|
+
if duplicate_task_ids:
|
|
430
|
+
return _result_error(
|
|
431
|
+
"PLAN_TASK_ID_DUPLICATE",
|
|
432
|
+
"duplicate task IDs: " + ", ".join(duplicate_task_ids),
|
|
433
|
+
)
|
|
328
434
|
for task in tasks:
|
|
329
435
|
owner = task.get("ownerPhase")
|
|
330
436
|
if owner is not None and owner != "" and owner not in VALID_OWNER_PHASES:
|
|
@@ -339,6 +445,37 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
339
445
|
# C9: parse test-scenarios.md for scenario manifest.
|
|
340
446
|
scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
|
|
341
447
|
scenarios = parse_test_scenarios(scenarios_path)
|
|
448
|
+
if not scenarios:
|
|
449
|
+
return _result_error(
|
|
450
|
+
"PLAN_SCENARIOS_EMPTY",
|
|
451
|
+
f"{scenarios_path.relative_to(staging).as_posix()}: no scenario rows were parsed",
|
|
452
|
+
)
|
|
453
|
+
scenario_ids = [str(item.get("id") or "").strip() for item in scenarios]
|
|
454
|
+
duplicate_scenario_ids = sorted(
|
|
455
|
+
{
|
|
456
|
+
scenario_id
|
|
457
|
+
for scenario_id in scenario_ids
|
|
458
|
+
if scenario_ids.count(scenario_id) > 1
|
|
459
|
+
}
|
|
460
|
+
)
|
|
461
|
+
if duplicate_scenario_ids:
|
|
462
|
+
return _result_error(
|
|
463
|
+
"PLAN_SCENARIO_ID_DUPLICATE",
|
|
464
|
+
"duplicate scenario IDs: " + ", ".join(duplicate_scenario_ids),
|
|
465
|
+
)
|
|
466
|
+
for scenario in scenarios:
|
|
467
|
+
priority = str(scenario.get("priority") or "").upper()
|
|
468
|
+
if priority not in PRIORITY_EVIDENCE_KIND:
|
|
469
|
+
return _result_error(
|
|
470
|
+
"PLAN_SCENARIO_PRIORITY_INVALID",
|
|
471
|
+
f"scenario {scenario.get('id', '?')}: unsupported priority '{priority}'",
|
|
472
|
+
)
|
|
473
|
+
owner = str(scenario.get("ownerPhase") or "")
|
|
474
|
+
if owner not in VALID_OWNER_PHASES:
|
|
475
|
+
return _result_error(
|
|
476
|
+
"PLAN_SCENARIO_OWNER_PHASE_INVALID",
|
|
477
|
+
f"scenario {scenario.get('id', '?')}: unsupported ownerPhase '{owner}'",
|
|
478
|
+
)
|
|
342
479
|
|
|
343
480
|
digest = hashlib.sha256()
|
|
344
481
|
artifact_names: list[str] = []
|
|
@@ -374,6 +511,37 @@ def _terminal_exists(change_dir: Path, run_id: str, attempt: int) -> bool:
|
|
|
374
511
|
)
|
|
375
512
|
|
|
376
513
|
|
|
514
|
+
def _validate_plan_start(
|
|
515
|
+
change_dir: Path,
|
|
516
|
+
run_id: str,
|
|
517
|
+
attempt: int,
|
|
518
|
+
) -> dict[str, Any]:
|
|
519
|
+
events_path = harness_events.events_path(change_dir)
|
|
520
|
+
try:
|
|
521
|
+
events = harness_events.load_events(events_path)
|
|
522
|
+
except (OSError, ValueError) as exc:
|
|
523
|
+
return _result_error("EVENTS_PARSE_ERROR", str(exc))
|
|
524
|
+
matching = [
|
|
525
|
+
event
|
|
526
|
+
for event in events
|
|
527
|
+
if event.get("phase") == "plan"
|
|
528
|
+
and event.get("type") == "phase.start"
|
|
529
|
+
and event.get("run_id") == run_id
|
|
530
|
+
and event.get("attempt") == attempt
|
|
531
|
+
]
|
|
532
|
+
if not matching:
|
|
533
|
+
return _result_error(
|
|
534
|
+
"PHASE_START_MISSING",
|
|
535
|
+
"no matching plan phase.start event found for finalizer runId/attempt",
|
|
536
|
+
)
|
|
537
|
+
if len(matching) > 1:
|
|
538
|
+
return _result_error(
|
|
539
|
+
"PHASE_START_DUPLICATE",
|
|
540
|
+
f"found {len(matching)} matching plan phase.start events",
|
|
541
|
+
)
|
|
542
|
+
return {"ok": True, "phaseStartCount": 1}
|
|
543
|
+
|
|
544
|
+
|
|
377
545
|
def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
378
546
|
"""Read-only verification of a finalized plan (retro §5.8).
|
|
379
547
|
|
|
@@ -405,6 +573,18 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
405
573
|
files_list = receipt.get("files")
|
|
406
574
|
if not isinstance(files_list, list) or not files_list:
|
|
407
575
|
return _result_error("RECEIPT_INVALID", "receipt files list missing or empty")
|
|
576
|
+
if receipt.get("status") != "finalized":
|
|
577
|
+
return _result_error(
|
|
578
|
+
"RECEIPT_NOT_FINALIZED",
|
|
579
|
+
f"receipt status is {receipt.get('status')!r}, expected 'finalized'",
|
|
580
|
+
)
|
|
581
|
+
receipt_run_id = str(receipt.get("runId") or "").strip()
|
|
582
|
+
receipt_attempt = receipt.get("attempt")
|
|
583
|
+
if not receipt_run_id or not isinstance(receipt_attempt, int):
|
|
584
|
+
return _result_error(
|
|
585
|
+
"RECEIPT_INVALID",
|
|
586
|
+
"receipt runId/attempt identity is missing or malformed",
|
|
587
|
+
)
|
|
408
588
|
|
|
409
589
|
# Recompute artifacts hash from published files.
|
|
410
590
|
digest = hashlib.sha256()
|
|
@@ -453,19 +633,105 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
453
633
|
"status": fm.get("status"),
|
|
454
634
|
}
|
|
455
635
|
|
|
636
|
+
plan_path = change_dir / "plans" / f"{change_name}-plan.md"
|
|
637
|
+
expected_tasks = parse_plan_tasks(plan_path)
|
|
638
|
+
if not expected_tasks:
|
|
639
|
+
return _result_error(
|
|
640
|
+
"PLAN_TASKS_EMPTY",
|
|
641
|
+
f"no task rows parsed from {plan_path.relative_to(change_dir).as_posix()}",
|
|
642
|
+
)
|
|
643
|
+
checkpoints_path = change_dir / "meta" / "implementation-checkpoints.json"
|
|
644
|
+
if not checkpoints_path.is_file():
|
|
645
|
+
return _result_error(
|
|
646
|
+
"IMPLEMENTATION_CHECKPOINTS_MISSING",
|
|
647
|
+
f"derived task metadata missing: {checkpoints_path}",
|
|
648
|
+
)
|
|
649
|
+
try:
|
|
650
|
+
checkpoints = json.loads(
|
|
651
|
+
checkpoints_path.read_text(encoding="utf-8-sig")
|
|
652
|
+
)
|
|
653
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
654
|
+
return _result_error("IMPLEMENTATION_CHECKPOINTS_INVALID", str(exc))
|
|
655
|
+
actual_tasks = checkpoints.get("tasks") if isinstance(checkpoints, dict) else None
|
|
656
|
+
expected_task_ids = [str(item.get("num") or "") for item in expected_tasks]
|
|
657
|
+
actual_task_ids = (
|
|
658
|
+
[str(item.get("num") or "") for item in actual_tasks if isinstance(item, dict)]
|
|
659
|
+
if isinstance(actual_tasks, list)
|
|
660
|
+
else []
|
|
661
|
+
)
|
|
662
|
+
if expected_task_ids != actual_task_ids:
|
|
663
|
+
return _result_error(
|
|
664
|
+
"IMPLEMENTATION_CHECKPOINTS_DRIFT",
|
|
665
|
+
"derived task IDs do not match the finalized plan: "
|
|
666
|
+
f"expected={expected_task_ids}, actual={actual_task_ids}",
|
|
667
|
+
)
|
|
668
|
+
|
|
669
|
+
scenarios_path = (
|
|
670
|
+
change_dir / "plans" / f"{change_name}-test-scenarios.md"
|
|
671
|
+
)
|
|
672
|
+
expected_scenarios = parse_test_scenarios(scenarios_path)
|
|
673
|
+
if not expected_scenarios:
|
|
674
|
+
return _result_error(
|
|
675
|
+
"PLAN_SCENARIOS_EMPTY",
|
|
676
|
+
f"no scenario rows parsed from {scenarios_path.relative_to(change_dir).as_posix()}",
|
|
677
|
+
)
|
|
678
|
+
scenario_manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
679
|
+
if not scenario_manifest_path.is_file():
|
|
680
|
+
return _result_error(
|
|
681
|
+
"SCENARIO_MANIFEST_MISSING",
|
|
682
|
+
f"derived scenario metadata missing: {scenario_manifest_path}",
|
|
683
|
+
)
|
|
684
|
+
try:
|
|
685
|
+
manifest = json.loads(
|
|
686
|
+
scenario_manifest_path.read_text(encoding="utf-8-sig")
|
|
687
|
+
)
|
|
688
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
689
|
+
return _result_error("SCENARIO_MANIFEST_INVALID", str(exc))
|
|
690
|
+
actual_scenarios = manifest.get("scenarios") if isinstance(manifest, dict) else None
|
|
691
|
+
if not isinstance(actual_scenarios, list) or not actual_scenarios:
|
|
692
|
+
return _result_error(
|
|
693
|
+
"SCENARIO_MANIFEST_EMPTY",
|
|
694
|
+
"scenario-manifest.json must contain at least one scenario",
|
|
695
|
+
)
|
|
696
|
+
|
|
697
|
+
def scenario_identity(item: dict[str, Any]) -> tuple[str, str, str, str]:
|
|
698
|
+
return (
|
|
699
|
+
str(item.get("id") or ""),
|
|
700
|
+
str(item.get("priority") or ""),
|
|
701
|
+
str(item.get("ownerPhase") or ""),
|
|
702
|
+
str(item.get("requiredEvidenceKind") or ""),
|
|
703
|
+
)
|
|
704
|
+
|
|
705
|
+
expected_scenario_identities = [
|
|
706
|
+
scenario_identity(item) for item in expected_scenarios
|
|
707
|
+
]
|
|
708
|
+
actual_scenario_identities = [
|
|
709
|
+
scenario_identity(item)
|
|
710
|
+
for item in actual_scenarios
|
|
711
|
+
if isinstance(item, dict)
|
|
712
|
+
]
|
|
713
|
+
if expected_scenario_identities != actual_scenario_identities:
|
|
714
|
+
return _result_error(
|
|
715
|
+
"SCENARIO_MANIFEST_DRIFT",
|
|
716
|
+
"derived scenario identities do not match the finalized scenario table",
|
|
717
|
+
)
|
|
718
|
+
|
|
456
719
|
# Validate gate-policy.json is parseable JSON.
|
|
457
720
|
gate_policy_path = change_dir / "meta" / "gate-policy.json"
|
|
721
|
+
if not gate_policy_path.is_file():
|
|
722
|
+
return _result_error(
|
|
723
|
+
"GATE_POLICY_MISSING",
|
|
724
|
+
f"gate policy missing: {gate_policy_path}",
|
|
725
|
+
)
|
|
726
|
+
try:
|
|
727
|
+
json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
|
|
728
|
+
except (OSError, json.JSONDecodeError) as exc:
|
|
729
|
+
return _result_error("GATE_POLICY_INVALID", str(exc))
|
|
458
730
|
gate_policy_consistent = True
|
|
459
|
-
if gate_policy_path.is_file():
|
|
460
|
-
try:
|
|
461
|
-
json.loads(gate_policy_path.read_text(encoding="utf-8-sig"))
|
|
462
|
-
except (OSError, json.JSONDecodeError):
|
|
463
|
-
gate_policy_consistent = False
|
|
464
|
-
else:
|
|
465
|
-
gate_policy_consistent = False
|
|
466
731
|
|
|
467
|
-
# Validate events.ndjson:
|
|
732
|
+
# Validate events.ndjson: require one matching start/end lifecycle.
|
|
468
733
|
events_path = change_dir / "events.ndjson"
|
|
734
|
+
phase_start_count = 0
|
|
469
735
|
phase_end_count = 0
|
|
470
736
|
phase_end_status: str | None = None
|
|
471
737
|
events_parse_errors: list[str] = []
|
|
@@ -481,10 +747,16 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
481
747
|
if not isinstance(event, dict):
|
|
482
748
|
events_parse_errors.append(f"line {line_no}: not an object")
|
|
483
749
|
continue
|
|
750
|
+
if event.get("phase") != "plan":
|
|
751
|
+
continue
|
|
484
752
|
if (
|
|
485
|
-
event.get("
|
|
486
|
-
|
|
753
|
+
str(event.get("run_id") or "") != receipt_run_id
|
|
754
|
+
or event.get("attempt") != receipt_attempt
|
|
487
755
|
):
|
|
756
|
+
continue
|
|
757
|
+
if event.get("type") == "phase.start":
|
|
758
|
+
phase_start_count += 1
|
|
759
|
+
elif event.get("type") == "phase.end":
|
|
488
760
|
phase_end_count += 1
|
|
489
761
|
phase_end_status = str(event.get("status") or "").upper()
|
|
490
762
|
else:
|
|
@@ -495,15 +767,27 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
495
767
|
"EVENTS_PARSE_ERROR", "; ".join(events_parse_errors[:5])
|
|
496
768
|
)
|
|
497
769
|
|
|
770
|
+
if phase_start_count == 0:
|
|
771
|
+
return _result_error(
|
|
772
|
+
"PHASE_START_MISSING",
|
|
773
|
+
"no matching plan phase.start event found",
|
|
774
|
+
)
|
|
775
|
+
if phase_start_count > 1:
|
|
776
|
+
return _result_error(
|
|
777
|
+
"PHASE_START_DUPLICATE",
|
|
778
|
+
f"found {phase_start_count} matching plan phase.start events",
|
|
779
|
+
)
|
|
498
780
|
if phase_end_count == 0:
|
|
499
781
|
return _result_error("PHASE_END_MISSING", "no plan phase.end event found")
|
|
500
782
|
if phase_end_count > 1:
|
|
501
783
|
return _result_error(
|
|
502
784
|
"PHASE_END_DUPLICATE", f"found {phase_end_count} phase.end events"
|
|
503
785
|
)
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
786
|
+
if phase_end_status != "OK":
|
|
787
|
+
return _result_error(
|
|
788
|
+
"PHASE_END_NOT_OK",
|
|
789
|
+
f"plan phase.end status is {phase_end_status!r}, expected 'OK'",
|
|
790
|
+
)
|
|
507
791
|
|
|
508
792
|
return {
|
|
509
793
|
"ok": True,
|
|
@@ -511,11 +795,14 @@ def verify_plan(change_dir: Path) -> dict[str, Any]:
|
|
|
511
795
|
"changeDir": str(change_dir),
|
|
512
796
|
"changeName": change_name,
|
|
513
797
|
"artifactsHash": actual_hash,
|
|
798
|
+
"phaseStartCount": phase_start_count,
|
|
514
799
|
"phaseEndCount": phase_end_count,
|
|
515
800
|
"phaseEndStatus": phase_end_status,
|
|
516
801
|
"frontmatter": frontmatter_results,
|
|
517
802
|
"gatePolicyConsistent": gate_policy_consistent,
|
|
518
|
-
"receiptConsistent":
|
|
803
|
+
"receiptConsistent": True,
|
|
804
|
+
"taskCount": len(expected_tasks),
|
|
805
|
+
"scenarioCount": len(expected_scenarios),
|
|
519
806
|
"files": artifact_names,
|
|
520
807
|
}
|
|
521
808
|
|
|
@@ -645,6 +932,9 @@ def finalize_plan(
|
|
|
645
932
|
validation = validate_staging(staging, change_name)
|
|
646
933
|
if not validation["ok"]:
|
|
647
934
|
return validation
|
|
935
|
+
start_validation = _validate_plan_start(change_dir, run_id, attempt)
|
|
936
|
+
if not start_validation["ok"]:
|
|
937
|
+
return start_validation
|
|
648
938
|
|
|
649
939
|
receipt_path = change_dir / "meta" / "plan-finalization.json"
|
|
650
940
|
lock_path = change_dir / "meta" / "plan-finalize.lock"
|
|
@@ -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=19c747d5d8746337; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-archive — 变更归档
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -208,12 +208,12 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
|
|
|
208
208
|
`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]]
|
|
209
209
|
|
|
210
210
|
```powershell
|
|
211
|
-
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
211
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --run-id <phase-run-id> --attempt <n> --note "<触发指令>"
|
|
212
212
|
```
|
|
213
213
|
|
|
214
214
|
> **脚本接线**:`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 护栏解读。
|
|
215
215
|
|
|
216
216
|
> **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 边界保持最新。
|
|
217
217
|
|
|
218
|
-
|
|
218
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
219
219
|
> 片段:logging · phase=`archive`
|
|
@@ -3,7 +3,7 @@ name: harness-codebase-map
|
|
|
3
3
|
description: 分析现有代码库并生成 .harness/codebase/map/
|
|
4
4
|
下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=19c747d5d8746337; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-codebase-map — 代码库地图
|
|
8
8
|
|
|
9
9
|
## Purpose
|
|
@@ -89,12 +89,12 @@ description: 分析现有代码库并生成 .harness/codebase/map/
|
|
|
89
89
|
`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]]
|
|
90
90
|
|
|
91
91
|
```powershell
|
|
92
|
-
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
92
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --run-id <phase-run-id> --attempt <n> --note "<触发指令>"
|
|
93
93
|
```
|
|
94
94
|
|
|
95
95
|
> **脚本接线**:`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 护栏解读。
|
|
96
96
|
|
|
97
97
|
> **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 边界保持最新。
|
|
98
98
|
|
|
99
|
-
|
|
99
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
100
100
|
> 片段:logging · phase=`codebase-map`;默认写 reports,有未归档变更时另写 change events
|
|
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
|
|
|
3
3
|
description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
|
|
4
4
|
knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
|
|
5
5
|
---
|
|
6
|
-
<!-- generated by harness_deploy.py; core=
|
|
6
|
+
<!-- generated by harness_deploy.py; core=19c747d5d8746337; overlay=none; agent=codex; do not edit -->
|
|
7
7
|
# harness-knowledge-ingest
|
|
8
8
|
|
|
9
9
|
从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
|
|
@@ -238,12 +238,12 @@ powershell.exe -Command "python '<skill-dir>\scripts\harness_knowledge.py' sync
|
|
|
238
238
|
`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]]
|
|
239
239
|
|
|
240
240
|
```powershell
|
|
241
|
-
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --note "<触发指令>"
|
|
241
|
+
python <skills-root>/scripts/harness_events.py append --change-dir ".harness/changes/<change-name>" --phase <phase> --type phase.start --run-id <phase-run-id> --attempt <n> --note "<触发指令>"
|
|
242
242
|
```
|
|
243
243
|
|
|
244
244
|
> **脚本接线**:`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 护栏解读。
|
|
245
245
|
|
|
246
246
|
> **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 边界保持最新。
|
|
247
247
|
|
|
248
|
-
|
|
248
|
+
每个阶段的 `phase.start` 与对应 `phase.end` 必须复用同一 `--run-id` / `--attempt`;阶段结束必须写 `--status OK|WARN|FAIL|BLOCKED`。重试同一阶段时生成新的 run-id 并增加 `--attempt <n>`,不得覆盖或伪装成一次执行。跨工具继续执行时写 `--executor-tool <codex|claude-code|codebuddy|cursor>`,并在接棒事件写 `--handoff-from-tool` / `--handoff-reason`;也可由 `HUNTER_HARNESS_TOOL/AGENT/MODEL/RUN_ID` 环境变量统一注入。
|
|
249
249
|
> 片段:logging · phase=`knowledge-ingest` · 默认控制台报告;变更上下文写 phase/decision/issue/artifact
|