@harmonyos-arkts/d2h 0.1.26 → 0.1.27
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/.claude-plugin/plugin.json +1 -1
- package/agents/d2h.md +13 -14
- package/dist/bin/install-opencode.js +11 -2
- package/dist/src/install-tui-extension.js +133 -0
- package/dist/src/toolchain.js +2 -2
- package/package.json +1 -1
- package/skills/d2h-architecture-plan/SKILL.md +16 -7
- package/skills/d2h-architecture-plan/references/architecture-plan.example.json +357 -0
- package/skills/d2h-architecture-plan/references/directory-creation.md +11 -6
- package/skills/d2h-architecture-plan/references/json-contract.md +111 -0
- package/skills/d2h-architecture-plan/references/plan-contract.md +16 -14
- package/skills/d2h-architecture-plan/references/selection-flow.md +71 -0
- package/skills/d2h-architecture-plan/scripts/architecture_plan.py +281 -0
- package/skills/d2h-architecture-plan/scripts/test_architecture_plan.py +378 -0
- package/skills/d2h-dashboard/SKILL.md +12 -1
- package/skills/d2h-spec-develop/SKILL.md +51 -42
- package/skills/d2h-spec-generate/SKILL.md +30 -29
- package/skills/d2h-spec-plan/SKILL.md +20 -16
- package/skills/d2h-spec-plan/agents/openai.yaml +3 -3
- package/skills/d2h-spec-plan/references/plan-contract.md +32 -7
- package/skills/d2h-spec-plan/references/planning-method.md +12 -62
- package/skills/d2h-spec-plan/references/planning-review.md +9 -21
- package/skills/d2h-spec-plan/references/round-scope-assessment.md +12 -39
- package/skills/d2h-spec-plan/references/work-packets.md +6 -28
|
@@ -0,0 +1,378 @@
|
|
|
1
|
+
"""Behavior checks for the dashboard selection boundary; no real project is touched."""
|
|
2
|
+
|
|
3
|
+
import copy
|
|
4
|
+
import contextlib
|
|
5
|
+
import io
|
|
6
|
+
import json
|
|
7
|
+
import os
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
import subprocess
|
|
10
|
+
import sys
|
|
11
|
+
import tempfile
|
|
12
|
+
import unittest
|
|
13
|
+
from unittest.mock import patch
|
|
14
|
+
|
|
15
|
+
import architecture_plan as helper
|
|
16
|
+
from architecture_plan import ContractError, apply_selection, read_selection, validate_plan
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
SKILL_ROOT = Path(__file__).resolve().parents[1]
|
|
20
|
+
HELPER = SKILL_ROOT / "scripts" / "architecture_plan.py"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ArchitecturePlanTests(unittest.TestCase):
|
|
24
|
+
def setUp(self):
|
|
25
|
+
self.plan = json.loads((SKILL_ROOT / "references" / "architecture-plan.example.json").read_text(encoding="utf-8"))
|
|
26
|
+
self.decision = {
|
|
27
|
+
"requestId": "request-1", "planId": self.plan["planId"],
|
|
28
|
+
"selectedProposalId": "android-mirror",
|
|
29
|
+
"action": "select-and-create", "source": "dashboard", "notes": "",
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def test_example_has_three_independently_valid_trees_and_no_user_selection(self):
|
|
33
|
+
validate_plan(self.plan)
|
|
34
|
+
self.assertEqual({p["id"] for p in self.plan["proposals"]}, {"android-mirror", "harmony-practice", "hybrid"})
|
|
35
|
+
self.assertIsNone(self.plan["selectedProposalId"])
|
|
36
|
+
|
|
37
|
+
def test_minimal_content_needs_no_execution_paths_or_architecture_relationship_models(self):
|
|
38
|
+
self.plan["androidAnalysis"]["risks"] = []
|
|
39
|
+
validate_plan(self.plan)
|
|
40
|
+
self.assertEqual(set(self.plan["androidAnalysis"]), {"summary", "techStack", "risks"})
|
|
41
|
+
self.assertEqual(set(self.plan["project"]), {"name", "summary"})
|
|
42
|
+
for proposal in self.plan["proposals"]:
|
|
43
|
+
self.assertEqual(set(proposal), {"id", "name", "summary", "directoryTree"})
|
|
44
|
+
updated, result = apply_selection(self.plan, self.decision)
|
|
45
|
+
self.assertEqual(result["nextAction"], "create-directory-shell")
|
|
46
|
+
self.assertEqual(updated["androidAnalysis"], self.plan["androidAnalysis"])
|
|
47
|
+
|
|
48
|
+
def test_selecting_nonrecommended_scheme_does_not_change_recommendation_or_trees(self):
|
|
49
|
+
original = copy.deepcopy(self.plan)
|
|
50
|
+
updated, result = apply_selection(self.plan, self.decision)
|
|
51
|
+
self.assertEqual(self.plan, original)
|
|
52
|
+
self.assertEqual(updated["recommendedProposalId"], "hybrid")
|
|
53
|
+
self.assertEqual(updated["selectedProposalId"], "android-mirror")
|
|
54
|
+
self.assertEqual(updated["proposals"], original["proposals"])
|
|
55
|
+
self.assertNotIn("selection", updated)
|
|
56
|
+
proposal, confirmation = read_selection(updated, self.decision)
|
|
57
|
+
self.assertEqual(proposal, updated["proposals"][0])
|
|
58
|
+
self.assertEqual(confirmation["requestId"], self.decision["requestId"])
|
|
59
|
+
self.assertEqual(result["nextAction"], "create-directory-shell")
|
|
60
|
+
validate_plan(updated)
|
|
61
|
+
|
|
62
|
+
def test_select_only_does_not_instruct_creation(self):
|
|
63
|
+
self.decision["action"] = "select"
|
|
64
|
+
updated, result = apply_selection(self.plan, self.decision)
|
|
65
|
+
self.assertEqual(result["nextAction"], "selection-recorded")
|
|
66
|
+
self.assertEqual(updated["selectedProposalId"], "android-mirror")
|
|
67
|
+
|
|
68
|
+
def test_exact_retry_does_not_change_plan(self):
|
|
69
|
+
updated, _ = apply_selection(self.plan, self.decision)
|
|
70
|
+
retry, result = apply_selection(updated, self.decision, self.decision)
|
|
71
|
+
self.assertEqual(retry, updated)
|
|
72
|
+
self.assertEqual(result["status"], "already-applied")
|
|
73
|
+
|
|
74
|
+
def test_request_id_cannot_be_reused_with_changed_content(self):
|
|
75
|
+
updated, _ = apply_selection(self.plan, self.decision)
|
|
76
|
+
for key, value in (("selectedProposalId", "hybrid"), ("action", "select"), ("notes", "changed")):
|
|
77
|
+
changed = {**self.decision, key: value}
|
|
78
|
+
with self.subTest(key=key), self.assertRaisesRegex(ContractError, "requestId"):
|
|
79
|
+
apply_selection(updated, changed, self.decision)
|
|
80
|
+
|
|
81
|
+
def test_new_request_can_change_the_selected_scheme(self):
|
|
82
|
+
updated, _ = apply_selection(self.plan, self.decision)
|
|
83
|
+
changed = {**self.decision, "requestId": "request-2", "selectedProposalId": "harmony-practice"}
|
|
84
|
+
final, result = apply_selection(updated, changed, self.decision)
|
|
85
|
+
self.assertEqual(result["status"], "applied")
|
|
86
|
+
self.assertEqual(final["selectedProposalId"], "harmony-practice")
|
|
87
|
+
_, confirmation = read_selection(final, changed)
|
|
88
|
+
self.assertEqual(confirmation["selectedProposalId"], "harmony-practice")
|
|
89
|
+
|
|
90
|
+
def test_wrong_plan_scheme_action_and_extra_paths_are_rejected(self):
|
|
91
|
+
for key, value in (
|
|
92
|
+
("planId", "another-project"), ("planId", True),
|
|
93
|
+
("selectedProposalId", "unknown"), ("action", "run-command"),
|
|
94
|
+
("targetRoot", "E:/other"), ("directoryTree", {}),
|
|
95
|
+
):
|
|
96
|
+
with self.subTest(key=key), self.assertRaises(ContractError):
|
|
97
|
+
apply_selection(self.plan, {**self.decision, key: value})
|
|
98
|
+
self.assertIsNone(self.plan["selectedProposalId"])
|
|
99
|
+
|
|
100
|
+
def test_plan_and_callback_only_need_content_and_selection_metadata(self):
|
|
101
|
+
self.assertEqual(set(self.plan), {
|
|
102
|
+
"planId", "project", "androidAnalysis", "proposals", "recommendedProposalId",
|
|
103
|
+
"recommendationReason", "selectedProposalId",
|
|
104
|
+
})
|
|
105
|
+
self.assertEqual(set(self.decision), {
|
|
106
|
+
"requestId", "planId", "selectedProposalId", "action", "source", "notes",
|
|
107
|
+
})
|
|
108
|
+
validate_plan(self.plan)
|
|
109
|
+
updated, _ = apply_selection(self.plan, self.decision)
|
|
110
|
+
self.assertEqual(set(updated), set(self.plan))
|
|
111
|
+
|
|
112
|
+
def test_fixed_analysis_fields_and_concise_risk_cards_are_validated(self):
|
|
113
|
+
stack = self.plan["androidAnalysis"]["techStack"]
|
|
114
|
+
invalid = (
|
|
115
|
+
{"techStack": "Kotlin"}, {"techStack": [""]},
|
|
116
|
+
{"techStack": {key: value for key, value in stack.items() if key != "storage"}},
|
|
117
|
+
{"techStack": {**stack, "ui": ""}},
|
|
118
|
+
{"techStack": {**stack, "language": ["Kotlin", "Java"]}},
|
|
119
|
+
{"techStack": {**stack, "frameworks": "extra category"}},
|
|
120
|
+
{"risks": ["long unstructured risk"]},
|
|
121
|
+
{"risks": [{"title": "存储适配", "impact": ""}]},
|
|
122
|
+
{"risks": [{"title": "存储适配"}]},
|
|
123
|
+
{"risks": [{"title": "存储适配", "impact": "需核验", "answerRequired": True}]},
|
|
124
|
+
{"risks": [{"title": str(index), "impact": "影响"} for index in range(6)]},
|
|
125
|
+
)
|
|
126
|
+
for fields in invalid:
|
|
127
|
+
with self.subTest(fields=fields), self.assertRaises(ContractError):
|
|
128
|
+
validate_plan({**self.plan, "androidAnalysis": {**self.plan["androidAnalysis"], **fields}})
|
|
129
|
+
|
|
130
|
+
def test_removed_fields_are_not_reintroduced_into_compact_contract(self):
|
|
131
|
+
cases = (("project", "targetRoot"), ("project", "scope"), ("androidAnalysis", "regions"),
|
|
132
|
+
("androidAnalysis", "structure"), ("androidAnalysis", "openQuestions"),
|
|
133
|
+
("androidAnalysis", "dependencies"), ("proposal", "mappings"),
|
|
134
|
+
("proposal", "dependencies"), ("proposal", "sharedOwners"),
|
|
135
|
+
("proposal", "benefits"), ("proposal", "costs"), ("proposal", "risks"))
|
|
136
|
+
for section, field in cases:
|
|
137
|
+
broken = copy.deepcopy(self.plan)
|
|
138
|
+
value = broken["proposals"][0] if section == "proposal" else broken[section]
|
|
139
|
+
value[field] = []
|
|
140
|
+
with self.subTest(section=section, field=field), self.assertRaisesRegex(ContractError, "unknown fields"):
|
|
141
|
+
validate_plan(broken)
|
|
142
|
+
|
|
143
|
+
def test_invalid_paths_case_collisions_and_skipped_levels_are_rejected(self):
|
|
144
|
+
for path in ("../escape", "/absolute", "E:/absolute", "a\\b", "a//b", "a/./b", "a/../b", "NUL", "x.", "{pages,state}", "a/b"):
|
|
145
|
+
broken = copy.deepcopy(self.plan)
|
|
146
|
+
leaf = broken["proposals"][0]["directoryTree"]["children"][0]
|
|
147
|
+
leaf["path"] = path
|
|
148
|
+
leaf["name"] = path.split("/")[-1]
|
|
149
|
+
with self.subTest(path=path), self.assertRaises(ContractError):
|
|
150
|
+
validate_plan(broken)
|
|
151
|
+
root = self.plan["proposals"][0]["directoryTree"]
|
|
152
|
+
root["children"].append({"name": "ENTRY", "path": "ENTRY", "description": "collision", "kind": "directory", "children": []})
|
|
153
|
+
with self.assertRaisesRegex(ContractError, "case-conflicting"):
|
|
154
|
+
validate_plan(self.plan)
|
|
155
|
+
|
|
156
|
+
def test_nested_module_and_missing_child_array_are_rejected(self):
|
|
157
|
+
entry = self.plan["proposals"][0]["directoryTree"]["children"][1]
|
|
158
|
+
entry["children"][0]["kind"] = "HAR"
|
|
159
|
+
with self.assertRaisesRegex(ContractError, "nested module"):
|
|
160
|
+
validate_plan(self.plan)
|
|
161
|
+
entry["children"][0]["kind"] = "directory"
|
|
162
|
+
del entry["children"][0]["children"]
|
|
163
|
+
with self.assertRaises(ContractError):
|
|
164
|
+
validate_plan(self.plan)
|
|
165
|
+
|
|
166
|
+
def test_empty_tree_or_tree_without_application_module_is_rejected(self):
|
|
167
|
+
for mode in ("empty", "no-hap", "hap-without-source"):
|
|
168
|
+
broken = copy.deepcopy(self.plan)
|
|
169
|
+
tree = broken["proposals"][0]["directoryTree"]
|
|
170
|
+
if mode == "empty":
|
|
171
|
+
tree["children"] = []
|
|
172
|
+
elif mode == "no-hap":
|
|
173
|
+
tree["children"][1].pop("kind")
|
|
174
|
+
else:
|
|
175
|
+
tree["children"][1]["children"] = []
|
|
176
|
+
with self.subTest(mode=mode), self.assertRaises(ContractError):
|
|
177
|
+
validate_plan(broken)
|
|
178
|
+
|
|
179
|
+
def test_read_preserves_user_notes_for_implementation_review(self):
|
|
180
|
+
answer = {**self.decision, "notes": "保留现有目录,只补充风险说明。"}
|
|
181
|
+
selected, _ = apply_selection(self.plan, answer)
|
|
182
|
+
_, result = read_selection(selected, answer)
|
|
183
|
+
self.assertEqual(result["notes"], answer["notes"])
|
|
184
|
+
|
|
185
|
+
def test_new_submission_cannot_overwrite_the_confirmation_input_in_place(self):
|
|
186
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
187
|
+
root = Path(directory)
|
|
188
|
+
plan_file, saved_file = root / "architecture-plan.json", root / "architecture-selection.json"
|
|
189
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
190
|
+
saved_file.write_text(json.dumps(self.decision), encoding="utf-8")
|
|
191
|
+
original = plan_file.read_bytes()
|
|
192
|
+
result = self.run_cli("apply-selection", plan_file, saved_file)
|
|
193
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
194
|
+
self.assertEqual(plan_file.read_bytes(), original)
|
|
195
|
+
|
|
196
|
+
def test_required_content_scheme_identity_and_selection_consistency_are_checked(self):
|
|
197
|
+
mutations = (
|
|
198
|
+
lambda p: p["project"].pop("summary"),
|
|
199
|
+
lambda p: p["androidAnalysis"].pop("risks"),
|
|
200
|
+
lambda p: p["androidAnalysis"].pop("techStack"),
|
|
201
|
+
lambda p: p["androidAnalysis"].update(risks="unknown"),
|
|
202
|
+
lambda p: p["proposals"][1].pop("directoryTree"),
|
|
203
|
+
lambda p: p["proposals"].pop(),
|
|
204
|
+
lambda p: p["proposals"][0].update(id="hybrid"),
|
|
205
|
+
lambda p: p.update(selectedProposalId="unknown"),
|
|
206
|
+
lambda p: p.update(selection=None),
|
|
207
|
+
)
|
|
208
|
+
for index, mutate in enumerate(mutations):
|
|
209
|
+
broken = copy.deepcopy(self.plan)
|
|
210
|
+
mutate(broken)
|
|
211
|
+
with self.subTest(index=index), self.assertRaises(ContractError):
|
|
212
|
+
validate_plan(broken)
|
|
213
|
+
|
|
214
|
+
def run_cli(self, *args):
|
|
215
|
+
return subprocess.run([sys.executable, str(HELPER), *map(str, args)],
|
|
216
|
+
capture_output=True, text=True, encoding="utf-8", timeout=10)
|
|
217
|
+
|
|
218
|
+
def test_dashboard_and_question_use_the_same_durable_confirmation_and_read_step(self):
|
|
219
|
+
for source in ("dashboard", "question"):
|
|
220
|
+
with self.subTest(source=source), tempfile.TemporaryDirectory() as directory:
|
|
221
|
+
root = Path(directory)
|
|
222
|
+
plan_file, answer_file = root / "architecture-plan.json", root / "tool-answer.json"
|
|
223
|
+
saved_file = root / "architecture-selection.json"
|
|
224
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
225
|
+
answer = {**self.decision, "source": source}
|
|
226
|
+
answer_file.write_text(json.dumps(answer), encoding="utf-8")
|
|
227
|
+
applied = self.run_cli("apply-selection", plan_file, answer_file)
|
|
228
|
+
self.assertEqual(applied.returncode, 0, applied.stdout + applied.stderr)
|
|
229
|
+
self.assertEqual(json.loads(saved_file.read_text(encoding="utf-8")), answer)
|
|
230
|
+
saved_plan = json.loads(plan_file.read_text(encoding="utf-8"))
|
|
231
|
+
self.assertEqual(saved_plan["selectedProposalId"], answer["selectedProposalId"])
|
|
232
|
+
self.assertNotIn("selection", saved_plan)
|
|
233
|
+
resumed = self.run_cli("read-selection", plan_file)
|
|
234
|
+
self.assertEqual(resumed.returncode, 0, resumed.stdout + resumed.stderr)
|
|
235
|
+
output = json.loads(resumed.stdout)
|
|
236
|
+
self.assertEqual(output["source"], source)
|
|
237
|
+
self.assertEqual(output["nextAction"], "create-directory-shell")
|
|
238
|
+
self.assertEqual(output["selectedProposalId"], "android-mirror")
|
|
239
|
+
self.assertEqual({p.name for p in root.iterdir()}, {plan_file.name, answer_file.name, saved_file.name})
|
|
240
|
+
|
|
241
|
+
def test_read_rejects_unconfirmed_or_mismatched_selection(self):
|
|
242
|
+
with self.assertRaisesRegex(ContractError, "no confirmed"):
|
|
243
|
+
read_selection(self.plan, self.decision)
|
|
244
|
+
updated, _ = apply_selection(self.plan, self.decision)
|
|
245
|
+
for key, value in (("planId", "other-plan"), ("selectedProposalId", "hybrid")):
|
|
246
|
+
with self.subTest(key=key), self.assertRaisesRegex(ContractError, "does not match"):
|
|
247
|
+
read_selection(updated, {**self.decision, key: value})
|
|
248
|
+
|
|
249
|
+
def test_read_select_only_never_requests_creation(self):
|
|
250
|
+
answer = {**self.decision, "action": "select", "source": "question"}
|
|
251
|
+
updated, _ = apply_selection(self.plan, answer)
|
|
252
|
+
_, result = read_selection(updated, answer)
|
|
253
|
+
self.assertEqual(result["nextAction"], "selection-recorded")
|
|
254
|
+
|
|
255
|
+
def test_missing_confirmation_file_prevents_directory_implementation(self):
|
|
256
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
257
|
+
plan_file = Path(directory) / "architecture-plan.json"
|
|
258
|
+
selected, _ = apply_selection(self.plan, self.decision)
|
|
259
|
+
plan_file.write_text(json.dumps(selected), encoding="utf-8")
|
|
260
|
+
result = self.run_cli("read-selection", plan_file)
|
|
261
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
262
|
+
self.assertEqual(json.loads(result.stdout)["status"], "error")
|
|
263
|
+
|
|
264
|
+
def test_recovery_applies_saved_question_answer_before_reading_implementation_input(self):
|
|
265
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
266
|
+
root = Path(directory)
|
|
267
|
+
plan_file, saved_file = root / "architecture-plan.json", root / "architecture-selection.json"
|
|
268
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
269
|
+
answer = {**self.decision, "source": "question"}
|
|
270
|
+
saved_file.write_text(json.dumps(answer), encoding="utf-8")
|
|
271
|
+
pending = self.run_cli("read-selection", plan_file)
|
|
272
|
+
self.assertEqual(pending.returncode, 1)
|
|
273
|
+
recovered = self.run_cli("apply-selection", plan_file, saved_file, "--recover")
|
|
274
|
+
self.assertEqual(recovered.returncode, 0, recovered.stdout + recovered.stderr)
|
|
275
|
+
ready = self.run_cli("read-selection", plan_file)
|
|
276
|
+
self.assertEqual(ready.returncode, 0, ready.stdout + ready.stderr)
|
|
277
|
+
self.assertEqual(json.loads(ready.stdout)["source"], "question")
|
|
278
|
+
|
|
279
|
+
def test_cli_retries_without_writing_and_never_creates_target(self):
|
|
280
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
281
|
+
root = Path(directory)
|
|
282
|
+
target = root / "not-created"
|
|
283
|
+
plan_file, decision_file = root / "architecture-plan.json", root / "incoming-selection.json"
|
|
284
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
285
|
+
decision_file.write_text(json.dumps(self.decision), encoding="utf-8-sig")
|
|
286
|
+
result = self.run_cli("apply-selection", plan_file, decision_file)
|
|
287
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
288
|
+
self.assertEqual(json.loads(result.stdout)["nextAction"], "create-directory-shell")
|
|
289
|
+
first_write = plan_file.stat().st_mtime_ns
|
|
290
|
+
retry = self.run_cli("apply-selection", plan_file, decision_file)
|
|
291
|
+
self.assertEqual(retry.returncode, 0, retry.stdout + retry.stderr)
|
|
292
|
+
self.assertEqual(json.loads(retry.stdout)["status"], "already-applied")
|
|
293
|
+
self.assertEqual(plan_file.stat().st_mtime_ns, first_write)
|
|
294
|
+
self.assertFalse(target.exists())
|
|
295
|
+
self.assertEqual({p.name for p in root.iterdir()}, {plan_file.name, decision_file.name, "architecture-selection.json"})
|
|
296
|
+
|
|
297
|
+
def test_changed_retry_is_rejected_without_replacing_confirmation(self):
|
|
298
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
299
|
+
root = Path(directory)
|
|
300
|
+
plan_file, incoming = root / "architecture-plan.json", root / "incoming.json"
|
|
301
|
+
saved = root / "architecture-selection.json"
|
|
302
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
303
|
+
incoming.write_text(json.dumps(self.decision), encoding="utf-8")
|
|
304
|
+
self.assertEqual(self.run_cli("apply-selection", plan_file, incoming).returncode, 0)
|
|
305
|
+
plan_bytes, confirmed_bytes = plan_file.read_bytes(), saved.read_bytes()
|
|
306
|
+
incoming.write_text(json.dumps({**self.decision, "action": "select"}), encoding="utf-8")
|
|
307
|
+
rejected = self.run_cli("apply-selection", plan_file, incoming)
|
|
308
|
+
self.assertEqual(rejected.returncode, 1, rejected.stdout)
|
|
309
|
+
self.assertIn("requestId", json.loads(rejected.stdout)["error"])
|
|
310
|
+
self.assertEqual(plan_file.read_bytes(), plan_bytes)
|
|
311
|
+
self.assertEqual(saved.read_bytes(), confirmed_bytes)
|
|
312
|
+
|
|
313
|
+
def test_recovery_flag_requires_the_script_owned_confirmation(self):
|
|
314
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
315
|
+
root = Path(directory)
|
|
316
|
+
plan_file, incoming = root / "architecture-plan.json", root / "incoming.json"
|
|
317
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
318
|
+
incoming.write_text(json.dumps(self.decision), encoding="utf-8")
|
|
319
|
+
rejected = self.run_cli("apply-selection", plan_file, incoming, "--recover")
|
|
320
|
+
self.assertEqual(rejected.returncode, 1)
|
|
321
|
+
self.assertFalse((root / "architecture-selection.json").exists())
|
|
322
|
+
|
|
323
|
+
def test_failed_plan_write_cannot_enter_implementation_and_can_recover(self):
|
|
324
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
325
|
+
root = Path(directory)
|
|
326
|
+
plan_file, incoming = root / "architecture-plan.json", root / "incoming.json"
|
|
327
|
+
saved = root / "architecture-selection.json"
|
|
328
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
329
|
+
incoming.write_text(json.dumps(self.decision), encoding="utf-8")
|
|
330
|
+
original = plan_file.read_bytes()
|
|
331
|
+
save_json = helper.save_json
|
|
332
|
+
|
|
333
|
+
def fail_plan_write(path, value):
|
|
334
|
+
if path == plan_file:
|
|
335
|
+
raise OSError("simulated plan write failure")
|
|
336
|
+
save_json(path, value)
|
|
337
|
+
|
|
338
|
+
arguments = [str(HELPER), "apply-selection", str(plan_file), str(incoming)]
|
|
339
|
+
with patch.object(sys, "argv", arguments), patch.object(helper, "save_json", side_effect=fail_plan_write):
|
|
340
|
+
with contextlib.redirect_stdout(io.StringIO()):
|
|
341
|
+
self.assertEqual(helper.main(), 1)
|
|
342
|
+
self.assertEqual(plan_file.read_bytes(), original)
|
|
343
|
+
self.assertEqual(json.loads(saved.read_text(encoding="utf-8")), self.decision)
|
|
344
|
+
self.assertEqual(self.run_cli("read-selection", plan_file).returncode, 1)
|
|
345
|
+
recovered = self.run_cli("apply-selection", plan_file, saved, "--recover")
|
|
346
|
+
self.assertEqual(recovered.returncode, 0, recovered.stdout + recovered.stderr)
|
|
347
|
+
self.assertEqual(self.run_cli("read-selection", plan_file).returncode, 0)
|
|
348
|
+
|
|
349
|
+
def test_reloaded_notes_are_utf8_even_with_an_ascii_console(self):
|
|
350
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
351
|
+
root = Path(directory)
|
|
352
|
+
plan_file, incoming = root / "architecture-plan.json", root / "incoming.json"
|
|
353
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
354
|
+
answer = {**self.decision, "notes": "保留目录,补充说明 📝"}
|
|
355
|
+
incoming.write_text(json.dumps(answer, ensure_ascii=False), encoding="utf-8")
|
|
356
|
+
self.assertEqual(self.run_cli("apply-selection", plan_file, incoming).returncode, 0)
|
|
357
|
+
result = subprocess.run([sys.executable, str(HELPER), "read-selection", str(plan_file)],
|
|
358
|
+
capture_output=True, timeout=10,
|
|
359
|
+
env={**os.environ, "PYTHONIOENCODING": "ascii"})
|
|
360
|
+
self.assertEqual(result.returncode, 0, result.stdout + result.stderr)
|
|
361
|
+
self.assertEqual(json.loads(result.stdout.decode("utf-8"))["notes"], answer["notes"])
|
|
362
|
+
|
|
363
|
+
def test_cli_error_leaves_plan_bytes_unchanged_and_returns_structured_error(self):
|
|
364
|
+
with tempfile.TemporaryDirectory() as directory:
|
|
365
|
+
root = Path(directory)
|
|
366
|
+
plan_file, decision_file = root / "plan.json", root / "decision.json"
|
|
367
|
+
plan_file.write_text(json.dumps(self.plan), encoding="utf-8")
|
|
368
|
+
original = plan_file.read_bytes()
|
|
369
|
+
for content in (json.dumps({**self.decision, "planId": "other-plan"}), '{"action":"select","action":"select-and-create"}', '[]', '{invalid'):
|
|
370
|
+
decision_file.write_text(content, encoding="utf-8")
|
|
371
|
+
result = self.run_cli("apply-selection", plan_file, decision_file)
|
|
372
|
+
self.assertEqual(result.returncode, 1, result.stdout + result.stderr)
|
|
373
|
+
self.assertEqual(json.loads(result.stdout)["status"], "error")
|
|
374
|
+
self.assertEqual(plan_file.read_bytes(), original)
|
|
375
|
+
|
|
376
|
+
|
|
377
|
+
if __name__ == "__main__":
|
|
378
|
+
unittest.main()
|
|
@@ -19,6 +19,7 @@ npx --yes d2h-dashboard@latest --dataDir <项目>/.migration [--port 8080] [--ho
|
|
|
19
19
|
|
|
20
20
|
- `.migration/progress.json` 出现(`progress init` 后)或恢复会话已存在 → 自动启动看板并用默认浏览器打开 URL,不等用户请求;每轮会话执行一次,复用实例不重复启动、不重复打开。
|
|
21
21
|
- 启动或打开失败只记录不阻塞迁移,下次阶段更新重试一次。
|
|
22
|
+
- 确认 `$url` 后按「TUI 常驻横幅」一节更新横幅;端口漂移(重启落到新端口)同样触发更新。
|
|
22
23
|
|
|
23
24
|
## 后台启动与验证(必做)
|
|
24
25
|
|
|
@@ -53,7 +54,17 @@ do {
|
|
|
53
54
|
|
|
54
55
|
**URL 告知规范**:URL 单独成行或用反引号包裹,其后不得紧跟任何文字(含中文括号说明)——否则 markdown 会把 URL+后缀渲染成单个失效链接,说明放下一行。
|
|
55
56
|
|
|
57
|
+
## TUI 常驻横幅
|
|
58
|
+
|
|
59
|
+
- 确认 `$url` 后(启动/复用/端口漂移)执行,10s 超时防卡死:
|
|
60
|
+
```powershell
|
|
61
|
+
Start-Process cmd.exe -ArgumentList '/c opencode-tui-extension.cmd set "<url>"' -NoNewWindow -PassThru | Wait-Process -Timeout 10 -ErrorAction SilentlyContinue
|
|
62
|
+
```
|
|
63
|
+
bash:`timeout 10 opencode-tui-extension set "<url>"`
|
|
64
|
+
- 停止看板后同法执行 `clear`
|
|
65
|
+
- 失败/超时/命令不存在:只记录不阻塞,降级为 URL 单独成行输出;安装注册由 `d2h install` 负责,本 skill 不做初始化
|
|
66
|
+
|
|
56
67
|
## 复用与停止
|
|
57
68
|
|
|
58
69
|
- **复用**:启动前探测 8080–8090 `GET /api/data/manifest`(1s 超时),`root` == 目标 dataDir 且 `GET /` 返回 200 才复用该 URL;仅 root 匹配但 `GET /` 非 200 属损坏实例(资产被删/版本切换残留),按停止规则杀掉后重新启动。root 不匹配跳过(其他流程的服务器)。服务器常驻,会话结束不回收,残留进程属预期。
|
|
59
|
-
- **停止**:仅用户显式要求(「关闭看板」「关闭 XX 项目看板」)才执行,且必须 root 归属校验通过:Windows `Get-NetTCPConnection -LocalPort <port> -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess }`(杀 node 真实 PID,cmd 包装 PID 无效);bash `kill <pid>`。未找到 → 报告未在运行;root 不匹配 →
|
|
70
|
+
- **停止**:仅用户显式要求(「关闭看板」「关闭 XX 项目看板」)才执行,且必须 root 归属校验通过:Windows `Get-NetTCPConnection -LocalPort <port> -State Listen | ForEach-Object { Stop-Process -Id $_.OwningProcess }`(杀 node 真实 PID,cmd 包装 PID 无效);bash `kill <pid>`。未找到 → 报告未在运行;root 不匹配 → 跳过并说明属于其他流程。停止成功后按「TUI 常驻横幅」执行 `clear`。
|
|
@@ -1,50 +1,52 @@
|
|
|
1
1
|
---
|
|
2
|
-
name: d2h-
|
|
3
|
-
description: Implement ordered Android-to-HarmonyOS page tasks from pages.json, batch_report.
|
|
2
|
+
name: d2h-spec-develop
|
|
3
|
+
description: Implement ordered Android-to-HarmonyOS page tasks from pages.json, batch_report.json, and page-scoped spec.md files. Use during page development when a dedicated page-worker must complete one page's ArkTS implementation and managed build; the parent agent owns testing and fixes, rebuilding, evidence, commits, and final acceptance before the next page starts.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Page development loop
|
|
7
7
|
|
|
8
|
-
|
|
8
|
+
Each page-worker implements only the selected page and brings its build to a pass. After development is complete, the parent agent takes over testing, defect fixes, and acceptance for the current task.
|
|
9
9
|
|
|
10
10
|
## Inputs and boundaries
|
|
11
11
|
|
|
12
|
-
- Use
|
|
12
|
+
- Use `batch_report.json` for task identities, dependencies, planning and execution state. Read included tasks and execution order from its bound planning field; legacy ledgers without planning use tasks order only when no selected scope is supplied. Do not read plan.json for execution or recovery. Keep task IDs and page packages unchanged.
|
|
13
13
|
- Use the selected page package's `spec.md` as the authority for page scope, behavior, acceptance sequences, Android evidence, and expected HarmonyOS files.
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
14
|
+
- When a development scope is supplied, require bound ledger planning and execute only its included tasks. Never select an excluded task because its Spec exists or its ledger state is pending. Return mixed selected/unselected behavior in one Spec as a scope conflict before implementation.
|
|
15
|
+
- Read `.migration/utg/page.json` when the Spec references a UTG node or the arrival path needs clarification. Read cited Android source before implementing or fixing behavior.
|
|
16
|
+
- Each `page_NNNN_<name>-<id>/` directory contains only `spec.md`. Do not add `ui-design.md`, copied screenshots, copied `view.xml`, or test/build evidence to page packages. Keep evidence in the resolved task evidence directory.
|
|
17
|
+
- Do not regenerate `pages.json`, page Specs, or `batch_report.json` during ordinary development. Correct an owning artifact only when current source proves it wrong, record the reason, and preserve execution history.
|
|
17
18
|
- Use `droid2hmos` managed build and test entry points. Do not use task-planning commands or static scripts to choose or execute page work.
|
|
18
|
-
- Preserve unrelated user changes. Never reset, discard, or include them in a page commit.
|
|
19
|
+
- Preserve unrelated user changes throughout implementation, parent fixes, and commits. Never reset, discard, or include them in a page commit.
|
|
19
20
|
|
|
20
21
|
## Parent-agent mode
|
|
21
22
|
|
|
22
|
-
1. Read `pages.json` and `batch_report.
|
|
23
|
-
2. Select the earliest
|
|
23
|
+
1. Read `pages.json` and `batch_report.json`; verify task identities, Spec paths and dependencies against pages.json, and use ledger planning for development scope and execution order. Verify planning references, counts and states. Reject missing, stale or invalid planning instead of falling back to all tasks when a selected scope or planning is present.
|
|
24
|
+
2. Select the earliest eligible task in planning milestone/wave order (or legacy ledger tasks order when allowed) whose development/build/validation states are not `implemented/passed/passed` and whose declared dependencies are satisfied.
|
|
24
25
|
3. Resolve the page package, Android root, HarmonyOS root, repository root, current `HEAD`, Git status, device target, and task evidence directory.
|
|
25
|
-
4. Dispatch exactly one page-worker with absolute paths, selected task ID, dispatch baseline, device target, and evidence directory. Do not run page-workers in parallel
|
|
26
|
-
5.
|
|
27
|
-
6.
|
|
28
|
-
7. Only after the
|
|
29
|
-
8. Only when the user explicitly requests it, enumerate every Python Hypium test list and run one managed batch regression. Append the batch evidence to `batch_report.md` and the migration report.
|
|
26
|
+
4. Dispatch exactly one page-worker with absolute paths, selected task ID, dispatch baseline, device target, and evidence directory. Do not run page-workers in parallel. Wait for its result before the parent begins testing and fixes.
|
|
27
|
+
5. After the worker returns, the parent takes ownership of the current page and executes testing and all fixes according to **Managed page tests and fixes** below until current build and required tests pass.
|
|
28
|
+
6. Execute **Parent report and commits** below.
|
|
29
|
+
7. Apply **Parent verification** below. Only after the page reaches `implemented/passed/passed`, evidence is current, and all page-owned changes are committed may the parent select the next page and dispatch a new worker.
|
|
30
30
|
|
|
31
31
|
Use this worker prompt contract:
|
|
32
32
|
|
|
33
33
|
```text
|
|
34
|
-
Use $d2h-
|
|
34
|
+
Use $d2h-spec-develop in page-worker mode. Complete the initial implementation and managed build for exactly <TASK_ID>.
|
|
35
|
+
Skill path: <absolute path to this SKILL.md>
|
|
35
36
|
Migration root: <absolute path>
|
|
36
37
|
Page package: <absolute path>
|
|
37
38
|
Android root: <absolute path>
|
|
38
39
|
HarmonyOS root: <absolute path>
|
|
39
40
|
Repository root: <absolute path>
|
|
40
41
|
Dispatch baseline: <HEAD and recorded Git status>
|
|
42
|
+
Execution ledger: <absolute batch_report.json path; planning revision/hash, milestone ID, wave ID when present>
|
|
41
43
|
Device target: <target>
|
|
42
44
|
Evidence directory: <absolute task/run directory>
|
|
43
45
|
|
|
44
46
|
Own the complete page loop: read spec.md, UTG evidence and cited Android source;
|
|
45
|
-
implement the page; run managed build;
|
|
46
|
-
|
|
47
|
-
|
|
47
|
+
implement the page; run managed build; update the migration report and batch_report.json in place;
|
|
48
|
+
create scoped commits; Do not start another page.
|
|
49
|
+
After handoff, the parent owns all testing, defect fixes, rebuilding, and acceptance.
|
|
48
50
|
```
|
|
49
51
|
|
|
50
52
|
If independent page-worker execution is unavailable, return that the required mode cannot run. Do not silently implement multiple pages in the parent agent.
|
|
@@ -53,9 +55,9 @@ If independent page-worker execution is unavailable, return that the required mo
|
|
|
53
55
|
|
|
54
56
|
### 1. Claim and understand the page
|
|
55
57
|
|
|
56
|
-
- Confirm the task is the earliest incomplete eligible
|
|
58
|
+
- Confirm the task is the earliest incomplete eligible task in ledger planning (or legacy task order when allowed) and no other page is being implemented or awaiting parent acceptance.
|
|
57
59
|
- Compare the checkout with the dispatch baseline. Preserve and avoid unrelated dirty files; if ownership conflicts with a build-affecting file, return the exact conflict instead of overwriting it.
|
|
58
|
-
- Mark only the selected task `in_progress` in
|
|
60
|
+
- Mark only the selected task `in_progress` in `batch_report.json`.
|
|
59
61
|
- Read the complete `spec.md`, the referenced `.migration/utg/page.json` node, every cited Fact, and every cited Android source file. Follow handlers, state transitions, repositories, navigation, persistence, error paths, and platform side effects far enough to establish the observable result.
|
|
60
62
|
- Inspect the HarmonyOS architecture, routes, resources, shared components, tests, and current implementation. Load `arkts-mvvm-architecture` before planning or writing ArkTS.
|
|
61
63
|
- Build a private checklist mapping every `F-xxx` to reachable code, a stable semantic locator, an executed test assertion, and an observable result. Do not create another planning file.
|
|
@@ -63,10 +65,11 @@ If independent page-worker execution is unavailable, return that the required mo
|
|
|
63
65
|
|
|
64
66
|
### 2. Implement
|
|
65
67
|
|
|
66
|
-
- Implement all page-scoped UI, navigation, overlays, state transitions, persistence/data effects, error paths,
|
|
68
|
+
- Implement all page-scoped UI and UI resources, navigation, overlays, state transitions, persistence/data effects, error paths, and required shared integration.
|
|
67
69
|
- Use the Android source and observable behavior for functional truth. Use HarmonyOS-native architecture and components for the target implementation.
|
|
68
70
|
- Give tested interactive elements stable, unique semantic locators. Do not add controls solely to make testing easier.
|
|
69
|
-
- Load the applicable domain Skill for identity, i18n, drawable conversion, icon sizing, or third-party substitution
|
|
71
|
+
- Load the applicable domain Skill for identity, i18n, drawable conversion, icon sizing, or third-party substitution.
|
|
72
|
+
- Before implementing platform capabilities or uncertain HarmonyOS APIs (`@kit.*` Kit imports or `@ohos.*` modules), and when build or test failures point to API signatures or behavior, load `d2h-cloud-knowledge`.
|
|
70
73
|
- Keep shared changes limited to capabilities this page actually needs and record downstream consumers.
|
|
71
74
|
- Do not leave stubs, fixed success values, swallowed errors, unreachable pages, placeholder controls, or TODO implementations.
|
|
72
75
|
|
|
@@ -74,12 +77,20 @@ If independent page-worker execution is unavailable, return that the required mo
|
|
|
74
77
|
|
|
75
78
|
- Run a fresh, non-interactive, bounded build through `droid2hmos build harmony <ANDROID_PROJECT>` using the resolved project configuration.
|
|
76
79
|
- Save the exact command, exit status, timestamps, relevant log, produced HAP path, and HAP SHA-256 in the task evidence directory.
|
|
77
|
-
-
|
|
80
|
+
- Fix compilation errors caused or exposed by the implementation within this worker until the build passes.
|
|
78
81
|
- An older HAP, static analysis result, direct Hvigor run, or previous log is not current build evidence.
|
|
79
82
|
|
|
80
|
-
### 4.
|
|
83
|
+
### 4. Worker report and commits
|
|
81
84
|
|
|
82
|
-
- Load `
|
|
85
|
+
- Load `maintaining-migration-report` while evidence is fresh. Record real HarmonyOS files, Android source provenance, current managed evidence, differences, and source-proven discoveries; run report validation.
|
|
86
|
+
- After a successful build, inspect Git status and diff and create a scoped initial implementation commit containing only this page's implementation, resources, required shared files, and source-proven Spec corrections. Do not commit unrelated changes.
|
|
87
|
+
- Update the existing `batch_report.json` task object, summary, evidence, files, implementation commit, timestamps, blockers, and append-only history in place. Do not rerun `build_batch_report.py`. When planning is present, record planning_ref with revision, sha256, milestone_id and wave_id in this run's task history. Preserve planning and planning_history when updating task results.
|
|
88
|
+
- Set `implemented/passed/not_run` only when all three states are supported by current evidence. Use AI inspection to confirm task/summary/history consistency and preserve all prior history.
|
|
89
|
+
- Return the handoff and stop modifying this page. Do not generate or execute tests or select the next page.
|
|
90
|
+
|
|
91
|
+
## Managed page tests and fixes
|
|
92
|
+
|
|
93
|
+
- The parent agent loads `d2h-hypium-forge` to perform testing. Add or update executable Python Hypium tests for every `F-xxx`, with paired `.py` and `.json` test-list assets and stable semantic locators.
|
|
83
94
|
- Cover navigation or overlay lifecycle, state changes, data effects, and error behavior required by the Spec. Assertions must prove the declared result, not merely that a page or button exists.
|
|
84
95
|
- Run only the current page's test lists (the `pageNNNN_*` lists owned by this task) through the managed entry point. Do not append regression lists from other pages.
|
|
85
96
|
|
|
@@ -92,35 +103,33 @@ If independent page-worker execution is unavailable, return that the required mo
|
|
|
92
103
|
|
|
93
104
|
- Save the exact command, result, native report/log, device target, timestamps, hashes, and returned `managedEvidence` in the task evidence directory.
|
|
94
105
|
- A written-but-unexecuted test, screenshot-only check, coordinate-only replay, direct local runner, old log, or zero-test result is not a pass.
|
|
95
|
-
-
|
|
106
|
+
- After application fixes, repeat managed build, installation, and managed tests until current build and required tests pass. Only persistent external environment, signing, or device failures may become `blocked` after bounded retries and recorded diagnostics.
|
|
96
107
|
|
|
97
|
-
|
|
108
|
+
## Parent report and commits
|
|
98
109
|
|
|
99
110
|
- Load `maintaining-migration-report` while evidence is fresh. Record real HarmonyOS files, Android source provenance, current managed evidence, differences, and source-proven discoveries; run report validation.
|
|
100
|
-
- Inspect
|
|
101
|
-
-
|
|
102
|
-
-
|
|
103
|
-
- Update the existing `batch_report.md` queue row, detail section, counts, evidence, files, implementation commit, timestamp, blocker, and chronological notes in place. Do not rerun `build_batch_report.py`.
|
|
104
|
-
- Set `implemented/passed/passed` only when all three states are supported by current evidence. Use AI inspection to confirm queue/detail/count consistency and preserve all prior history.
|
|
111
|
+
- Inspect the final diff. After current build and tests pass, commit the parent's page fixes, test files, and necessary shared changes.
|
|
112
|
+
- Update the existing `batch_report.json` task object, summary, evidence, files, implementation commit, timestamps, blockers, and append-only history in place. Do not rerun `build_batch_report.py`. When planning is present, record planning_ref with revision, sha256, milestone_id and wave_id in this run's task history. Preserve planning and planning_history when updating task results.
|
|
113
|
+
- Set `implemented/passed/passed` only when all three states are supported by current evidence. Use AI inspection to confirm task/summary/history consistency and preserve all prior history.
|
|
105
114
|
- Commit the ledger and immutable task evidence separately from the implementation commit. Do not create a self-referential commit field.
|
|
106
115
|
|
|
107
116
|
## Parent verification
|
|
108
117
|
|
|
109
|
-
Before
|
|
118
|
+
Before dispatching a worker for the next page, the parent confirms:
|
|
110
119
|
|
|
111
|
-
- every `F-xxx` has reachable implementation, a stable locator, and an actually executed assertion;
|
|
120
|
+
- every dispatched `F-xxx` has reachable implementation, a stable locator, and an actually executed assertion;
|
|
112
121
|
- the page is reachable from the real app entry without a test-only route or replacement control;
|
|
113
|
-
- build logs, HAP hash, test reports, managed evidence,
|
|
114
|
-
- `batch_report.
|
|
115
|
-
-
|
|
116
|
-
- user changes and other pages were not reset or included;
|
|
122
|
+
- final build logs, installed HAP hash, source snapshot, test reports, managed evidence, migration-report refs, and recorded implementation commit correspond to the final tested application;
|
|
123
|
+
- `batch_report.json` is internally consistent and the selected task row is `implemented/passed/passed`;
|
|
124
|
+
- all page-owned implementation, tests, report, ledger, and evidence changes are committed, while unrelated user changes remain untouched;
|
|
117
125
|
- no implementation defect was reclassified as a platform difference or external blocker.
|
|
118
126
|
|
|
119
|
-
|
|
127
|
+
If any check fails, the parent continues fixing the issues and repeats the necessary build/install/test and reporting steps.
|
|
120
128
|
|
|
121
129
|
## Batch regression
|
|
122
130
|
|
|
123
|
-
Run the batch regression only when the user explicitly requests it. During ordinary page development, run only the current page's test lists
|
|
131
|
+
Run the batch regression only when the user explicitly requests it. During ordinary page development, run only the current page's test lists.
|
|
124
132
|
|
|
125
133
|
## Worker result
|
|
126
|
-
|
|
134
|
+
|
|
135
|
+
Return the task ID and page, final states, implementation and test files, managed build command/HAP/evidence, implementation and ledger commit IDs, unrelated Git changes left untouched, and any genuine blocker or unresolved item.
|