@hunter-harness/workflow-harness 0.2.15 → 0.2.16
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 +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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_gate.py +75 -0
- package/harness/bundles/general/claude-code/scripts/harness_integration.py +38 -0
- package/harness/bundles/general/claude-code/scripts/harness_ledger.py +70 -3
- package/harness/bundles/general/claude-code/scripts/harness_paths.py +17 -0
- package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/general/claude-code/scripts/harness_profile.py +58 -7
- package/harness/bundles/general/claude-code/scripts/harness_review.py +107 -0
- package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_gate.py +75 -0
- package/harness/bundles/general/codebuddy/scripts/harness_integration.py +38 -0
- package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +70 -3
- package/harness/bundles/general/codebuddy/scripts/harness_paths.py +17 -0
- package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/general/codebuddy/scripts/harness_profile.py +58 -7
- package/harness/bundles/general/codebuddy/scripts/harness_review.py +107 -0
- package/harness/bundles/general/codex/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_gate.py +75 -0
- package/harness/bundles/general/codex/scripts/harness_integration.py +38 -0
- package/harness/bundles/general/codex/scripts/harness_ledger.py +70 -3
- package/harness/bundles/general/codex/scripts/harness_paths.py +17 -0
- package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/general/codex/scripts/harness_profile.py +58 -7
- package/harness/bundles/general/codex/scripts/harness_review.py +107 -0
- package/harness/bundles/general/cursor/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
- package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
- 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/scripts/harness_gate.py +75 -0
- package/harness/bundles/general/cursor/scripts/harness_integration.py +38 -0
- package/harness/bundles/general/cursor/scripts/harness_ledger.py +70 -3
- package/harness/bundles/general/cursor/scripts/harness_paths.py +17 -0
- package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/general/cursor/scripts/harness_profile.py +58 -7
- package/harness/bundles/general/cursor/scripts/harness_review.py +107 -0
- package/harness/bundles/java/claude-code/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/claude-code/scripts/harness_gate.py +75 -0
- package/harness/bundles/java/claude-code/scripts/harness_integration.py +38 -0
- package/harness/bundles/java/claude-code/scripts/harness_ledger.py +70 -3
- package/harness/bundles/java/claude-code/scripts/harness_paths.py +17 -0
- package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/java/claude-code/scripts/harness_profile.py +58 -7
- package/harness/bundles/java/claude-code/scripts/harness_review.py +107 -0
- package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codebuddy/scripts/harness_gate.py +75 -0
- package/harness/bundles/java/codebuddy/scripts/harness_integration.py +38 -0
- package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +70 -3
- package/harness/bundles/java/codebuddy/scripts/harness_paths.py +17 -0
- package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/java/codebuddy/scripts/harness_profile.py +58 -7
- package/harness/bundles/java/codebuddy/scripts/harness_review.py +107 -0
- package/harness/bundles/java/codex/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/codex/scripts/harness_gate.py +75 -0
- package/harness/bundles/java/codex/scripts/harness_integration.py +38 -0
- package/harness/bundles/java/codex/scripts/harness_ledger.py +70 -3
- package/harness/bundles/java/codex/scripts/harness_paths.py +17 -0
- package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/java/codex/scripts/harness_profile.py +58 -7
- package/harness/bundles/java/codex/scripts/harness_review.py +107 -0
- package/harness/bundles/java/cursor/.harness-build.json +1 -1
- 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-ingest/scripts/harness_knowledge.py +16 -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-review/reference.md +13 -0
- 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-sync/SKILL.md +1 -1
- package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
- package/harness/bundles/java/cursor/scripts/harness_gate.py +75 -0
- package/harness/bundles/java/cursor/scripts/harness_integration.py +38 -0
- package/harness/bundles/java/cursor/scripts/harness_ledger.py +70 -3
- package/harness/bundles/java/cursor/scripts/harness_paths.py +17 -0
- package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +147 -0
- package/harness/bundles/java/cursor/scripts/harness_profile.py +58 -7
- package/harness/bundles/java/cursor/scripts/harness_review.py +107 -0
- package/harness/manifests/general/claude-code.json +21 -21
- package/harness/manifests/general/codebuddy.json +21 -21
- package/harness/manifests/general/codex.json +21 -21
- package/harness/manifests/general/cursor.json +21 -21
- package/harness/manifests/java/claude-code.json +23 -23
- package/harness/manifests/java/codebuddy.json +23 -23
- package/harness/manifests/java/codex.json +23 -23
- package/harness/manifests/java/cursor.json +23 -23
- package/hunter-workflow-family.json +2 -2
- package/package.json +1 -1
|
@@ -83,6 +83,23 @@ def _git_common_dir(cwd: Path) -> Path | None:
|
|
|
83
83
|
return common
|
|
84
84
|
|
|
85
85
|
|
|
86
|
+
def common_root(project_root: Path) -> Path:
|
|
87
|
+
"""Resolve the shared "common" root for a project.
|
|
88
|
+
|
|
89
|
+
For a linked worktree, ``git rev-parse --git-common-dir`` points at the
|
|
90
|
+
main repository's ``.git`` directory; its parent is the main project root.
|
|
91
|
+
For the main worktree, the common dir is ``<root>/.git`` so the parent is
|
|
92
|
+
the project root itself. Falls back to the resolved input when not in a
|
|
93
|
+
git repo (e.g. exploratory scratch dirs).
|
|
94
|
+
"""
|
|
95
|
+
cwd = Path(project_root).resolve()
|
|
96
|
+
common = _git_common_dir(cwd)
|
|
97
|
+
if common is None:
|
|
98
|
+
return cwd
|
|
99
|
+
# common dir is <root>/.git — its parent is the main project root.
|
|
100
|
+
return common.parent.resolve()
|
|
101
|
+
|
|
102
|
+
|
|
86
103
|
def _root_commit(cwd: Path) -> str | None:
|
|
87
104
|
raw = _git_text(cwd, "rev-list", "--max-parents=0", "HEAD")
|
|
88
105
|
if not raw:
|
|
@@ -30,11 +30,117 @@ if hasattr(sys.stderr, "reconfigure"):
|
|
|
30
30
|
SCHEMA_VERSION = 1
|
|
31
31
|
_LINK = re.compile(r"\[[^\]]+\]\(([^)]+)\)")
|
|
32
32
|
|
|
33
|
+
# C8: valid ownerPhase values (lifecycle phases that can own tasks).
|
|
34
|
+
VALID_OWNER_PHASES = {"plan", "run", "test", "review", "submit"}
|
|
35
|
+
|
|
36
|
+
# C9: map scenario priority to required evidence kind.
|
|
37
|
+
PRIORITY_EVIDENCE_KIND = {
|
|
38
|
+
"P0": "ledger",
|
|
39
|
+
"P1": "ledger",
|
|
40
|
+
"P2": "advisory",
|
|
41
|
+
}
|
|
42
|
+
|
|
33
43
|
|
|
34
44
|
def _result_error(code: str, message: str) -> dict[str, Any]:
|
|
35
45
|
return {"ok": False, "code": code, "error": message}
|
|
36
46
|
|
|
37
47
|
|
|
48
|
+
def parse_test_scenarios(scenarios_path: Path) -> list[dict[str, str]]:
|
|
49
|
+
"""C9: parse test-scenarios.md tables, extracting scenario rows.
|
|
50
|
+
|
|
51
|
+
Returns a list of dicts with keys: id / priority / scenario / verification /
|
|
52
|
+
ownerPhase / requiredEvidenceKind (last one derived from priority).
|
|
53
|
+
"""
|
|
54
|
+
text = Path(scenarios_path).read_text(encoding="utf-8-sig")
|
|
55
|
+
lines = text.splitlines()
|
|
56
|
+
scenarios: list[dict[str, str]] = []
|
|
57
|
+
# Find all tables whose header includes "ID" and "优先级" (or "priority").
|
|
58
|
+
i = 0
|
|
59
|
+
while i < len(lines):
|
|
60
|
+
line = lines[i].strip()
|
|
61
|
+
if line.startswith("|") and "ID" in line and ("优先级" in line or "priority" in line):
|
|
62
|
+
header_line = line
|
|
63
|
+
headers = [h.strip() for h in header_line.strip("|").split("|")]
|
|
64
|
+
col_map: dict[str, int] = {}
|
|
65
|
+
for idx, name in enumerate(headers):
|
|
66
|
+
key = name.lower()
|
|
67
|
+
col_map[key] = idx
|
|
68
|
+
# Skip separator row
|
|
69
|
+
i += 2
|
|
70
|
+
while i < len(lines):
|
|
71
|
+
row = lines[i].strip()
|
|
72
|
+
if not row.startswith("|"):
|
|
73
|
+
break
|
|
74
|
+
cells = [c.strip() for c in row.strip("|").split("|")]
|
|
75
|
+
if len(cells) < len(headers):
|
|
76
|
+
i += 1
|
|
77
|
+
continue
|
|
78
|
+
scenario: dict[str, str] = {}
|
|
79
|
+
for cn, en in (
|
|
80
|
+
("ID", "id"),
|
|
81
|
+
("优先级", "priority"),
|
|
82
|
+
("场景", "scenario"),
|
|
83
|
+
("验证方式", "verification"),
|
|
84
|
+
("owner phase", "ownerPhase"),
|
|
85
|
+
):
|
|
86
|
+
if cn.lower() in col_map:
|
|
87
|
+
scenario[en] = cells[col_map[cn.lower()]]
|
|
88
|
+
# Derive requiredEvidenceKind from priority
|
|
89
|
+
priority = scenario.get("priority", "")
|
|
90
|
+
scenario["requiredEvidenceKind"] = PRIORITY_EVIDENCE_KIND.get(priority, "advisory")
|
|
91
|
+
scenarios.append(scenario)
|
|
92
|
+
i += 1
|
|
93
|
+
continue
|
|
94
|
+
i += 1
|
|
95
|
+
return scenarios
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def parse_plan_tasks(plan_path: Path) -> list[dict[str, str]]:
|
|
99
|
+
"""C8: parse plan.md task table rows, extracting optional ownerPhase/implementationDoneWhen/verificationPhase columns.
|
|
100
|
+
|
|
101
|
+
Returns a list of dicts with keys: # / 簇 / 任务 / ownerPhase / implementationDoneWhen /
|
|
102
|
+
verificationPhase / requiresExplicitAuthority (last four optional, only present when
|
|
103
|
+
the column header includes them).
|
|
104
|
+
"""
|
|
105
|
+
text = Path(plan_path).read_text(encoding="utf-8-sig")
|
|
106
|
+
lines = text.splitlines()
|
|
107
|
+
# Find the task table: a header row starting with "| #" followed by a separator row.
|
|
108
|
+
header_idx = -1
|
|
109
|
+
for i, line in enumerate(lines):
|
|
110
|
+
stripped = line.strip()
|
|
111
|
+
if stripped.startswith("|") and "#" in stripped and "任务" in stripped:
|
|
112
|
+
header_idx = i
|
|
113
|
+
break
|
|
114
|
+
if header_idx < 0:
|
|
115
|
+
return []
|
|
116
|
+
header_line = lines[header_idx]
|
|
117
|
+
headers = [h.strip() for h in header_line.strip().strip("|").split("|")]
|
|
118
|
+
# Map header names to column indices (case-insensitive, English keys).
|
|
119
|
+
col_map: dict[str, int] = {}
|
|
120
|
+
for idx, name in enumerate(headers):
|
|
121
|
+
key = name.lower()
|
|
122
|
+
col_map[key] = idx
|
|
123
|
+
tasks: list[dict[str, str]] = []
|
|
124
|
+
for line in lines[header_idx + 2 :]: # skip header + separator
|
|
125
|
+
stripped = line.strip()
|
|
126
|
+
if not stripped.startswith("|"):
|
|
127
|
+
break # end of table
|
|
128
|
+
cells = [c.strip() for c in stripped.strip("|").split("|")]
|
|
129
|
+
if len(cells) < len(headers):
|
|
130
|
+
continue
|
|
131
|
+
task: dict[str, str] = {}
|
|
132
|
+
# Chinese headers
|
|
133
|
+
for cn, en in (("#", "num"), ("簇", "cluster"), ("任务", "task")):
|
|
134
|
+
if cn in col_map:
|
|
135
|
+
task[en] = cells[col_map[cn]]
|
|
136
|
+
# English optional columns
|
|
137
|
+
for en in ("ownerPhase", "implementationDoneWhen", "verificationPhase", "requiresExplicitAuthority"):
|
|
138
|
+
if en.lower() in col_map:
|
|
139
|
+
task[en] = cells[col_map[en.lower()]]
|
|
140
|
+
tasks.append(task)
|
|
141
|
+
return tasks
|
|
142
|
+
|
|
143
|
+
|
|
38
144
|
def _atomic_write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
39
145
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
40
146
|
fd, raw_tmp = tempfile.mkstemp(
|
|
@@ -146,6 +252,21 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
146
252
|
f"{rel.as_posix()}: missing reference {raw_link}",
|
|
147
253
|
)
|
|
148
254
|
|
|
255
|
+
# C8: validate ownerPhase values in plan.md task table.
|
|
256
|
+
plan_path = staging / "plans" / f"{change_name}-plan.md"
|
|
257
|
+
tasks = parse_plan_tasks(plan_path)
|
|
258
|
+
for task in tasks:
|
|
259
|
+
owner = task.get("ownerPhase")
|
|
260
|
+
if owner is not None and owner != "" and owner not in VALID_OWNER_PHASES:
|
|
261
|
+
return _result_error(
|
|
262
|
+
"PLAN_OWNER_PHASE_INVALID",
|
|
263
|
+
f"task {task.get('num', '?')}: ownerPhase '{owner}' not in {sorted(VALID_OWNER_PHASES)}",
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
# C9: parse test-scenarios.md for scenario manifest.
|
|
267
|
+
scenarios_path = staging / "plans" / f"{change_name}-test-scenarios.md"
|
|
268
|
+
scenarios = parse_test_scenarios(scenarios_path)
|
|
269
|
+
|
|
149
270
|
digest = hashlib.sha256()
|
|
150
271
|
artifact_names: list[str] = []
|
|
151
272
|
for path in files:
|
|
@@ -159,6 +280,8 @@ def validate_staging(staging: Path, change_name: str) -> dict[str, Any]:
|
|
|
159
280
|
"ok": True,
|
|
160
281
|
"files": artifact_names,
|
|
161
282
|
"artifactsHash": "sha256:" + digest.hexdigest(),
|
|
283
|
+
"tasks": tasks,
|
|
284
|
+
"scenarios": scenarios,
|
|
162
285
|
}
|
|
163
286
|
|
|
164
287
|
|
|
@@ -367,6 +490,30 @@ def finalize_plan(
|
|
|
367
490
|
finally:
|
|
368
491
|
tmp.unlink(missing_ok=True)
|
|
369
492
|
|
|
493
|
+
# C8: write implementation-checkpoints.json with parsed task ownerPhase.
|
|
494
|
+
tasks = validation.get("tasks") or []
|
|
495
|
+
if tasks:
|
|
496
|
+
checkpoints_path = change_dir / "meta" / "implementation-checkpoints.json"
|
|
497
|
+
checkpoints_payload = {
|
|
498
|
+
"schemaVersion": 1,
|
|
499
|
+
"changeName": change_name,
|
|
500
|
+
"tasks": tasks,
|
|
501
|
+
"foundationGate": "approved",
|
|
502
|
+
}
|
|
503
|
+
_atomic_write_json(checkpoints_path, checkpoints_payload)
|
|
504
|
+
created.append(checkpoints_path)
|
|
505
|
+
|
|
506
|
+
# C9: write scenario-manifest.json with parsed scenarios.
|
|
507
|
+
scenarios = validation.get("scenarios") or []
|
|
508
|
+
manifest_path = change_dir / "meta" / "scenario-manifest.json"
|
|
509
|
+
manifest_payload = {
|
|
510
|
+
"schemaVersion": 1,
|
|
511
|
+
"changeName": change_name,
|
|
512
|
+
"scenarios": scenarios,
|
|
513
|
+
}
|
|
514
|
+
_atomic_write_json(manifest_path, manifest_payload)
|
|
515
|
+
created.append(manifest_path)
|
|
516
|
+
|
|
370
517
|
pending_receipt = {
|
|
371
518
|
"schemaVersion": SCHEMA_VERSION,
|
|
372
519
|
"changeName": change_name,
|
|
@@ -27,6 +27,12 @@ import sys
|
|
|
27
27
|
from pathlib import Path
|
|
28
28
|
from typing import Any
|
|
29
29
|
|
|
30
|
+
SCRIPTS_DIR = Path(__file__).resolve().parent
|
|
31
|
+
if str(SCRIPTS_DIR) not in sys.path:
|
|
32
|
+
sys.path.insert(0, str(SCRIPTS_DIR))
|
|
33
|
+
|
|
34
|
+
import harness_paths # noqa: E402
|
|
35
|
+
|
|
30
36
|
|
|
31
37
|
if hasattr(sys.stdout, "reconfigure"):
|
|
32
38
|
sys.stdout.reconfigure(encoding="utf-8")
|
|
@@ -358,14 +364,53 @@ def empty_profile_skeleton(excluded: tuple[str, ...] | list[str]) -> dict[str, A
|
|
|
358
364
|
|
|
359
365
|
|
|
360
366
|
def load_profile(project: Path) -> dict[str, Any] | None:
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
+
"""C7: 分层读取 — 先读 common_root 的 build-profile.json,再叠加 execution root 的 override。
|
|
368
|
+
|
|
369
|
+
common_root 是 worktree 共享的主项目根(git common dir 的父目录);
|
|
370
|
+
execution root 是当前工作目录(可能是 linked worktree)。
|
|
371
|
+
override 合并策略:execution 的 buildCommands 覆盖 common 的同名 key;
|
|
372
|
+
common 独有的 key 保留。
|
|
373
|
+
"""
|
|
374
|
+
project = Path(project).resolve()
|
|
375
|
+
common = harness_paths.common_root(project)
|
|
376
|
+
common_path = common / PROFILE_REL
|
|
377
|
+
exec_path = project / PROFILE_REL
|
|
378
|
+
|
|
379
|
+
common_data: dict[str, Any] | None = None
|
|
380
|
+
if common_path.is_file():
|
|
381
|
+
try:
|
|
382
|
+
data = read_json(common_path)
|
|
383
|
+
if isinstance(data, dict):
|
|
384
|
+
common_data = data
|
|
385
|
+
except (OSError, json.JSONDecodeError):
|
|
386
|
+
pass
|
|
387
|
+
|
|
388
|
+
exec_data: dict[str, Any] | None = None
|
|
389
|
+
if exec_path.is_file():
|
|
390
|
+
try:
|
|
391
|
+
data = read_json(exec_path)
|
|
392
|
+
if isinstance(data, dict):
|
|
393
|
+
exec_data = data
|
|
394
|
+
except (OSError, json.JSONDecodeError):
|
|
395
|
+
pass
|
|
396
|
+
|
|
397
|
+
if common_data is None and exec_data is None:
|
|
367
398
|
return None
|
|
368
|
-
|
|
399
|
+
if common_data is None:
|
|
400
|
+
return exec_data
|
|
401
|
+
if exec_data is None:
|
|
402
|
+
return common_data
|
|
403
|
+
|
|
404
|
+
# Merge: start from common, overlay execution's buildCommands.
|
|
405
|
+
merged = dict(common_data)
|
|
406
|
+
common_cmds = common_data.get("buildCommands") or {}
|
|
407
|
+
exec_cmds = exec_data.get("buildCommands") or {}
|
|
408
|
+
if common_cmds or exec_cmds:
|
|
409
|
+
merged_cmds = dict(common_cmds) if isinstance(common_cmds, dict) else {}
|
|
410
|
+
if isinstance(exec_cmds, dict):
|
|
411
|
+
merged_cmds.update(exec_cmds)
|
|
412
|
+
merged["buildCommands"] = merged_cmds
|
|
413
|
+
return merged
|
|
369
414
|
|
|
370
415
|
|
|
371
416
|
def _merge_user_overrides(
|
|
@@ -616,6 +661,8 @@ def resolve_command(
|
|
|
616
661
|
*,
|
|
617
662
|
test_classes: list[str] | None = None,
|
|
618
663
|
modules: list[str] | None = None,
|
|
664
|
+
common_root: Path | None = None,
|
|
665
|
+
execution_root: Path | None = None,
|
|
619
666
|
) -> dict[str, Any]:
|
|
620
667
|
cmd = (profile.get("commands") or {}).get(key)
|
|
621
668
|
if not isinstance(cmd, dict):
|
|
@@ -625,6 +672,10 @@ def resolve_command(
|
|
|
625
672
|
replacements["{testClasses}"] = ",".join(test_classes)
|
|
626
673
|
if modules is not None:
|
|
627
674
|
replacements["{modules}"] = ",".join(modules)
|
|
675
|
+
if common_root is not None:
|
|
676
|
+
replacements["{commonRoot}"] = str(common_root)
|
|
677
|
+
if execution_root is not None:
|
|
678
|
+
replacements["{executionRoot}"] = str(execution_root)
|
|
628
679
|
|
|
629
680
|
command = cmd.get("command", "")
|
|
630
681
|
for placeholder, val in replacements.items():
|
|
@@ -14,6 +14,7 @@ Python 3.10+, stdlib only.
|
|
|
14
14
|
from __future__ import annotations
|
|
15
15
|
|
|
16
16
|
import argparse
|
|
17
|
+
import datetime as dt
|
|
17
18
|
import hashlib
|
|
18
19
|
import json
|
|
19
20
|
import os
|
|
@@ -270,6 +271,112 @@ def _emit(payload: Any, *, as_json: bool) -> int:
|
|
|
270
271
|
return 0 if payload.get("ok") else 1
|
|
271
272
|
|
|
272
273
|
|
|
274
|
+
def dispatch_review(
|
|
275
|
+
*,
|
|
276
|
+
change_dir: Path,
|
|
277
|
+
run_id: str,
|
|
278
|
+
budget_seconds: int = 300,
|
|
279
|
+
) -> dict[str, Any]:
|
|
280
|
+
"""C11: dispatch a bounded review task.
|
|
281
|
+
|
|
282
|
+
Returns reviewTaskId / deadline / heartbeatAt. The caller is expected to
|
|
283
|
+
poll the review subagent and collect partial findings on timeout.
|
|
284
|
+
"""
|
|
285
|
+
now = dt.datetime.now(dt.timezone.utc).astimezone()
|
|
286
|
+
task_id = f"review-{uuid.uuid4().hex}"
|
|
287
|
+
deadline = now + dt.timedelta(seconds=budget_seconds)
|
|
288
|
+
# heartbeat at midpoint of the budget
|
|
289
|
+
heartbeat = now + dt.timedelta(seconds=budget_seconds // 2)
|
|
290
|
+
return {
|
|
291
|
+
"ok": True,
|
|
292
|
+
"code": "DISPATCHED",
|
|
293
|
+
"reviewTaskId": task_id,
|
|
294
|
+
"runId": run_id,
|
|
295
|
+
"deadline": deadline.isoformat(timespec="milliseconds"),
|
|
296
|
+
"heartbeatAt": heartbeat.isoformat(timespec="milliseconds"),
|
|
297
|
+
"budgetSeconds": budget_seconds,
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def collect_partial_findings(
|
|
302
|
+
*,
|
|
303
|
+
change_dir: Path,
|
|
304
|
+
run_id: str,
|
|
305
|
+
completed_dimensions: list[str],
|
|
306
|
+
pending_dimensions: list[str],
|
|
307
|
+
) -> dict[str, Any]:
|
|
308
|
+
"""C11: collect partial findings after timeout — completed dimensions only.
|
|
309
|
+
|
|
310
|
+
Applies the degradation matrix to decide the fallback path.
|
|
311
|
+
"""
|
|
312
|
+
matrix = degradation_matrix(
|
|
313
|
+
subagent_timed_out=bool(pending_dimensions),
|
|
314
|
+
main_session_available=True,
|
|
315
|
+
)
|
|
316
|
+
return {
|
|
317
|
+
"ok": True,
|
|
318
|
+
"code": "PARTIAL_FINDINGS",
|
|
319
|
+
"runId": run_id,
|
|
320
|
+
"completedDimensions": list(completed_dimensions),
|
|
321
|
+
"pendingDimensions": list(pending_dimensions),
|
|
322
|
+
"degradationMatrix": matrix,
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
def degradation_matrix(
|
|
327
|
+
*,
|
|
328
|
+
subagent_timed_out: bool,
|
|
329
|
+
main_session_available: bool,
|
|
330
|
+
) -> dict[str, Any]:
|
|
331
|
+
"""C11: degradation matrix — subagent timeout → main session; main fail → ADVISORY."""
|
|
332
|
+
if not subagent_timed_out:
|
|
333
|
+
return {"fallback": "none", "status": "OK"}
|
|
334
|
+
if main_session_available:
|
|
335
|
+
return {"fallback": "main-session", "status": "DEGRADED"}
|
|
336
|
+
return {"fallback": "advisory", "status": "ADVISORY"}
|
|
337
|
+
|
|
338
|
+
|
|
339
|
+
def validate_codegraph_identity(
|
|
340
|
+
*,
|
|
341
|
+
response: dict[str, Any],
|
|
342
|
+
expected_repository_id: str,
|
|
343
|
+
expected_head: str,
|
|
344
|
+
) -> dict[str, Any]:
|
|
345
|
+
"""C12: validate CodeGraph identity — repositoryId/indexedHead must match.
|
|
346
|
+
|
|
347
|
+
On mismatch, signal CODEGRAPH_IDENTITY_MISMATCH with fallback to Grep/Glob + Read.
|
|
348
|
+
"""
|
|
349
|
+
repo_id = response.get("repositoryId")
|
|
350
|
+
indexed_head = response.get("indexedHead")
|
|
351
|
+
indexed_at = response.get("indexedAt")
|
|
352
|
+
if (
|
|
353
|
+
repo_id == expected_repository_id
|
|
354
|
+
and indexed_head == expected_head
|
|
355
|
+
and indexed_at
|
|
356
|
+
):
|
|
357
|
+
return {
|
|
358
|
+
"ok": True,
|
|
359
|
+
"code": "IDENTITY_OK",
|
|
360
|
+
"repositoryId": repo_id,
|
|
361
|
+
"indexedHead": indexed_head,
|
|
362
|
+
"indexedAt": indexed_at,
|
|
363
|
+
}
|
|
364
|
+
return {
|
|
365
|
+
"ok": False,
|
|
366
|
+
"code": "CODEGRAPH_IDENTITY_MISMATCH",
|
|
367
|
+
"expected": {
|
|
368
|
+
"repositoryId": expected_repository_id,
|
|
369
|
+
"indexedHead": expected_head,
|
|
370
|
+
},
|
|
371
|
+
"actual": {
|
|
372
|
+
"repositoryId": repo_id,
|
|
373
|
+
"indexedHead": indexed_head,
|
|
374
|
+
"indexedAt": indexed_at,
|
|
375
|
+
},
|
|
376
|
+
"fallback": "grep-glob-read",
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
|
|
273
380
|
def cmd_validate_findings(args: argparse.Namespace) -> int:
|
|
274
381
|
doc = _read_json(Path(args.input))
|
|
275
382
|
problems = validate_findings(doc)
|
|
@@ -2,12 +2,12 @@
|
|
|
2
2
|
"schema_version": 2,
|
|
3
3
|
"profile": "general",
|
|
4
4
|
"adapter": "claude-code",
|
|
5
|
-
"bundle_version": "0.2.
|
|
5
|
+
"bundle_version": "0.2.15",
|
|
6
6
|
"generator": "harness_deploy.py",
|
|
7
7
|
"files": [
|
|
8
8
|
{
|
|
9
9
|
"path": ".harness-build.json",
|
|
10
|
-
"sha256": "
|
|
10
|
+
"sha256": "d1e1ee4711727013db2693d31573711e91b87080f36fa1442089b1c911c87774"
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"path": "agents/harness-evaluator.md",
|
|
@@ -47,7 +47,7 @@
|
|
|
47
47
|
},
|
|
48
48
|
{
|
|
49
49
|
"path": "harness-archive/SKILL.md",
|
|
50
|
-
"sha256": "
|
|
50
|
+
"sha256": "9104b7b4bbc074301e29b8e270243770c96647222dd70fb60f6fd5def3bfaa3f"
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
"path": "harness-archive/templates/render-summary.mjs",
|
|
@@ -67,7 +67,7 @@
|
|
|
67
67
|
},
|
|
68
68
|
{
|
|
69
69
|
"path": "harness-codebase-map/SKILL.md",
|
|
70
|
-
"sha256": "
|
|
70
|
+
"sha256": "36517926819e5a197d8a02bab37ea0579c511faef6f70c74cb71e21a60689b3b"
|
|
71
71
|
},
|
|
72
72
|
{
|
|
73
73
|
"path": "harness-codebase-map/templates/ARCHITECTURE.md",
|
|
@@ -127,15 +127,15 @@
|
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"path": "harness-knowledge-ingest/scripts/harness_knowledge.py",
|
|
130
|
-
"sha256": "
|
|
130
|
+
"sha256": "062115a524d5bd4aec27e40e2b57cf4e7ed6e0509d4a8e6e78a79a62ee32e2c5"
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"path": "harness-knowledge-ingest/SKILL.md",
|
|
134
|
-
"sha256": "
|
|
134
|
+
"sha256": "088a51365d7bd7a918e415432cedc039c85a478f21a8894b5376dfe4b0c48f2e"
|
|
135
135
|
},
|
|
136
136
|
{
|
|
137
137
|
"path": "harness-knowledge-query/SKILL.md",
|
|
138
|
-
"sha256": "
|
|
138
|
+
"sha256": "18f05a403ae56a9c7ad08c58e3bd67243de67a684df895596312584650f8b5f0"
|
|
139
139
|
},
|
|
140
140
|
{
|
|
141
141
|
"path": "harness-plan/checklist.md",
|
|
@@ -151,7 +151,7 @@
|
|
|
151
151
|
},
|
|
152
152
|
{
|
|
153
153
|
"path": "harness-plan/SKILL.md",
|
|
154
|
-
"sha256": "
|
|
154
|
+
"sha256": "91b0e08f29cc598ac1e3ffea2465245354eea4ee5553afda58c34dae72afb58e"
|
|
155
155
|
},
|
|
156
156
|
{
|
|
157
157
|
"path": "harness-review/checklist.md",
|
|
@@ -163,11 +163,11 @@
|
|
|
163
163
|
},
|
|
164
164
|
{
|
|
165
165
|
"path": "harness-review/reference.md",
|
|
166
|
-
"sha256": "
|
|
166
|
+
"sha256": "5e43f753b2189bc8256c04905c079016a31694dad2aa1ad841aceae3670dd6c6"
|
|
167
167
|
},
|
|
168
168
|
{
|
|
169
169
|
"path": "harness-review/SKILL.md",
|
|
170
|
-
"sha256": "
|
|
170
|
+
"sha256": "bb4f36751348697398d6b9de1601e5c1a3c9d8895b2533b834f930761334c7f0"
|
|
171
171
|
},
|
|
172
172
|
{
|
|
173
173
|
"path": "harness-run/checklist.md",
|
|
@@ -183,7 +183,7 @@
|
|
|
183
183
|
},
|
|
184
184
|
{
|
|
185
185
|
"path": "harness-run/SKILL.md",
|
|
186
|
-
"sha256": "
|
|
186
|
+
"sha256": "35947fa05b0f366585e4e6eaa7d5421685cdbf07f0bbd248173e66d191e65e5b"
|
|
187
187
|
},
|
|
188
188
|
{
|
|
189
189
|
"path": "harness-submit/checklist.md",
|
|
@@ -195,7 +195,7 @@
|
|
|
195
195
|
},
|
|
196
196
|
{
|
|
197
197
|
"path": "harness-submit/SKILL.md",
|
|
198
|
-
"sha256": "
|
|
198
|
+
"sha256": "5fe0ea4267aabdf6527aea9589f1a0f2d7d13726af355c127e2b5cfb198b12ea"
|
|
199
199
|
},
|
|
200
200
|
{
|
|
201
201
|
"path": "harness-sync/reference.md",
|
|
@@ -203,7 +203,7 @@
|
|
|
203
203
|
},
|
|
204
204
|
{
|
|
205
205
|
"path": "harness-sync/SKILL.md",
|
|
206
|
-
"sha256": "
|
|
206
|
+
"sha256": "afd9ca9af1b69fcf4a32564a1d70e975e73bedcd61d0f662175a8f53887d4fc1"
|
|
207
207
|
},
|
|
208
208
|
{
|
|
209
209
|
"path": "harness-test/checklist.md",
|
|
@@ -227,7 +227,7 @@
|
|
|
227
227
|
},
|
|
228
228
|
{
|
|
229
229
|
"path": "harness-test/SKILL.md",
|
|
230
|
-
"sha256": "
|
|
230
|
+
"sha256": "fe3dae58e7f9537f71d793d5390d5f73f1d5066682b4f79b2a5b6cc58ca6e247"
|
|
231
231
|
},
|
|
232
232
|
{
|
|
233
233
|
"path": "protocols/archive-report-protocol.md",
|
|
@@ -291,19 +291,19 @@
|
|
|
291
291
|
},
|
|
292
292
|
{
|
|
293
293
|
"path": "scripts/harness_gate.py",
|
|
294
|
-
"sha256": "
|
|
294
|
+
"sha256": "c5762a90f0361496d1f6a64670ee4cc84dd5075924fec08c3846530393c0b2ff"
|
|
295
295
|
},
|
|
296
296
|
{
|
|
297
297
|
"path": "scripts/harness_integration.py",
|
|
298
|
-
"sha256": "
|
|
298
|
+
"sha256": "613440b92d14cef920647e61eb524eb8f539add5b7fd68e7a263fd541f99316a"
|
|
299
299
|
},
|
|
300
300
|
{
|
|
301
301
|
"path": "scripts/harness_ledger.py",
|
|
302
|
-
"sha256": "
|
|
302
|
+
"sha256": "60a6cdff5c3c81abf5606f7914c299828c4ed2b21d19e9acbb27b001a03fcaa5"
|
|
303
303
|
},
|
|
304
304
|
{
|
|
305
305
|
"path": "scripts/harness_paths.py",
|
|
306
|
-
"sha256": "
|
|
306
|
+
"sha256": "c60d272e61aa98fc84a89c24db81c1ff8cbf2dac16b88fddbcd18fcc5d6199a5"
|
|
307
307
|
},
|
|
308
308
|
{
|
|
309
309
|
"path": "scripts/harness_phase.py",
|
|
@@ -311,7 +311,7 @@
|
|
|
311
311
|
},
|
|
312
312
|
{
|
|
313
313
|
"path": "scripts/harness_plan_finalize.py",
|
|
314
|
-
"sha256": "
|
|
314
|
+
"sha256": "6fd97c6d91d180fa0ce2b73ac16c13ee741ef8a78f8a78b0af6e061d2b9eb7da"
|
|
315
315
|
},
|
|
316
316
|
{
|
|
317
317
|
"path": "scripts/harness_preflight.py",
|
|
@@ -319,11 +319,11 @@
|
|
|
319
319
|
},
|
|
320
320
|
{
|
|
321
321
|
"path": "scripts/harness_profile.py",
|
|
322
|
-
"sha256": "
|
|
322
|
+
"sha256": "e242f79d36d7a38bdf3927cd2f65d1c7bb687a4119b07e41a59fec6989d5e253"
|
|
323
323
|
},
|
|
324
324
|
{
|
|
325
325
|
"path": "scripts/harness_review.py",
|
|
326
|
-
"sha256": "
|
|
326
|
+
"sha256": "a741a63f62993d72889e78f540feb57a95e713b4d7a98fa61ff208b2ce914ca7"
|
|
327
327
|
},
|
|
328
328
|
{
|
|
329
329
|
"path": "scripts/harness_runtime.py",
|