@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.
Files changed (178) hide show
  1. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  2. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +1 -1
  3. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  4. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-review/reference.md +13 -0
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  14. package/harness/bundles/general/claude-code/scripts/harness_gate.py +75 -0
  15. package/harness/bundles/general/claude-code/scripts/harness_integration.py +38 -0
  16. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +70 -3
  17. package/harness/bundles/general/claude-code/scripts/harness_paths.py +17 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +147 -0
  19. package/harness/bundles/general/claude-code/scripts/harness_profile.py +58 -7
  20. package/harness/bundles/general/claude-code/scripts/harness_review.py +107 -0
  21. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  22. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  26. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  29. package/harness/bundles/general/codebuddy/harness-review/reference.md +13 -0
  30. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  33. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  34. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +75 -0
  35. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +38 -0
  36. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +70 -3
  37. package/harness/bundles/general/codebuddy/scripts/harness_paths.py +17 -0
  38. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +147 -0
  39. package/harness/bundles/general/codebuddy/scripts/harness_profile.py +58 -7
  40. package/harness/bundles/general/codebuddy/scripts/harness_review.py +107 -0
  41. package/harness/bundles/general/codex/.harness-build.json +1 -1
  42. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  43. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  46. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-review/reference.md +13 -0
  50. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  52. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  53. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/scripts/harness_gate.py +75 -0
  55. package/harness/bundles/general/codex/scripts/harness_integration.py +38 -0
  56. package/harness/bundles/general/codex/scripts/harness_ledger.py +70 -3
  57. package/harness/bundles/general/codex/scripts/harness_paths.py +17 -0
  58. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +147 -0
  59. package/harness/bundles/general/codex/scripts/harness_profile.py +58 -7
  60. package/harness/bundles/general/codex/scripts/harness_review.py +107 -0
  61. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  62. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  66. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-review/reference.md +13 -0
  70. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  73. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  74. package/harness/bundles/general/cursor/scripts/harness_gate.py +75 -0
  75. package/harness/bundles/general/cursor/scripts/harness_integration.py +38 -0
  76. package/harness/bundles/general/cursor/scripts/harness_ledger.py +70 -3
  77. package/harness/bundles/general/cursor/scripts/harness_paths.py +17 -0
  78. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +147 -0
  79. package/harness/bundles/general/cursor/scripts/harness_profile.py +58 -7
  80. package/harness/bundles/general/cursor/scripts/harness_review.py +107 -0
  81. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  82. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  84. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  85. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  86. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  87. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  88. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  89. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  90. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-review/reference.md +13 -0
  92. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  96. package/harness/bundles/java/claude-code/scripts/harness_gate.py +75 -0
  97. package/harness/bundles/java/claude-code/scripts/harness_integration.py +38 -0
  98. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +70 -3
  99. package/harness/bundles/java/claude-code/scripts/harness_paths.py +17 -0
  100. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +147 -0
  101. package/harness/bundles/java/claude-code/scripts/harness_profile.py +58 -7
  102. package/harness/bundles/java/claude-code/scripts/harness_review.py +107 -0
  103. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  104. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  105. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  106. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  107. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  108. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  109. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  110. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  111. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-review/reference.md +13 -0
  114. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +75 -0
  119. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +38 -0
  120. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +70 -3
  121. package/harness/bundles/java/codebuddy/scripts/harness_paths.py +17 -0
  122. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +147 -0
  123. package/harness/bundles/java/codebuddy/scripts/harness_profile.py +58 -7
  124. package/harness/bundles/java/codebuddy/scripts/harness_review.py +107 -0
  125. package/harness/bundles/java/codex/.harness-build.json +1 -1
  126. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  127. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  128. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  129. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  130. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  131. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  132. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  134. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-review/reference.md +13 -0
  136. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/scripts/harness_gate.py +75 -0
  141. package/harness/bundles/java/codex/scripts/harness_integration.py +38 -0
  142. package/harness/bundles/java/codex/scripts/harness_ledger.py +70 -3
  143. package/harness/bundles/java/codex/scripts/harness_paths.py +17 -0
  144. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +147 -0
  145. package/harness/bundles/java/codex/scripts/harness_profile.py +58 -7
  146. package/harness/bundles/java/codex/scripts/harness_review.py +107 -0
  147. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  148. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  149. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  150. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  151. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  152. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +16 -1
  153. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  154. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  155. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  156. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-review/reference.md +13 -0
  158. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/scripts/harness_gate.py +75 -0
  163. package/harness/bundles/java/cursor/scripts/harness_integration.py +38 -0
  164. package/harness/bundles/java/cursor/scripts/harness_ledger.py +70 -3
  165. package/harness/bundles/java/cursor/scripts/harness_paths.py +17 -0
  166. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +147 -0
  167. package/harness/bundles/java/cursor/scripts/harness_profile.py +58 -7
  168. package/harness/bundles/java/cursor/scripts/harness_review.py +107 -0
  169. package/harness/manifests/general/claude-code.json +21 -21
  170. package/harness/manifests/general/codebuddy.json +21 -21
  171. package/harness/manifests/general/codex.json +21 -21
  172. package/harness/manifests/general/cursor.json +21 -21
  173. package/harness/manifests/java/claude-code.json +23 -23
  174. package/harness/manifests/java/codebuddy.json +23 -23
  175. package/harness/manifests/java/codex.json +23 -23
  176. package/harness/manifests/java/cursor.json +23 -23
  177. package/hunter-workflow-family.json +2 -2
  178. 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
- path = project / PROFILE_REL
362
- if not path.is_file():
363
- return None
364
- try:
365
- data = read_json(path)
366
- except (OSError, json.JSONDecodeError):
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
- return data if isinstance(data, dict) else None
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.14",
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": "f7e5fcab41c868a1acf53d59f58cad0ca69f7da5dca394b3081de73b6e530074"
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": "55d393fc6b7accab0802115fc6628c68545e1069cbc1aa0e16c412d6a8edfa5a"
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": "8901d29948bd75933d22dea87d15b9b47eeab660063af3e4c13718e7e5fdc4a4"
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": "48620b3aa57da3cbc39c556b73006108e0ae4e40d7b662709869dfc1877158ee"
130
+ "sha256": "062115a524d5bd4aec27e40e2b57cf4e7ed6e0509d4a8e6e78a79a62ee32e2c5"
131
131
  },
132
132
  {
133
133
  "path": "harness-knowledge-ingest/SKILL.md",
134
- "sha256": "013252318d6a27d6c62bcf5247e0e3a2f78187fe7758950edf93e44340943fd9"
134
+ "sha256": "088a51365d7bd7a918e415432cedc039c85a478f21a8894b5376dfe4b0c48f2e"
135
135
  },
136
136
  {
137
137
  "path": "harness-knowledge-query/SKILL.md",
138
- "sha256": "5e0d111527aa23bc2ecd60e9808a8d65e23f21344920447c247aa8592ad176fd"
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": "c73337d7896b89c67ecdbc623145ee1ffad679302b72eeb054b7d1508dee4a86"
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": "0644a39417d51f1222905567dca80a433661a2411e2d4dd1b94644bc1cda596d"
166
+ "sha256": "5e43f753b2189bc8256c04905c079016a31694dad2aa1ad841aceae3670dd6c6"
167
167
  },
168
168
  {
169
169
  "path": "harness-review/SKILL.md",
170
- "sha256": "ec00e56acc221f34ab5b2717a560473670a897c8bf75039bae290a6d875c17df"
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": "9b3b608914a7bbf698c3a28a45e5d4cd6a8156bd49c9a780da5e1adbbde02408"
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": "279bb2546f41d617fe4402caa5cad7db788643407ba47b5cfe0dae00b1c18e72"
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": "6dc61858ea0293876a79be8130de7c3f7699f1f8510a50fac6a6da01d62b4673"
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": "a7c8d010c05c216c17ba353525ce06e3c8d39f5ec9182be29b4420f567fe36a7"
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": "f83e9d33816052553e2ae8e5514fae318c5b5bc48ba2aded67d88fc9f0b51a98"
294
+ "sha256": "c5762a90f0361496d1f6a64670ee4cc84dd5075924fec08c3846530393c0b2ff"
295
295
  },
296
296
  {
297
297
  "path": "scripts/harness_integration.py",
298
- "sha256": "6b078d7d1d053e4c46bc320496fab3060d883b256d517a56f0b078eec2fa0ce0"
298
+ "sha256": "613440b92d14cef920647e61eb524eb8f539add5b7fd68e7a263fd541f99316a"
299
299
  },
300
300
  {
301
301
  "path": "scripts/harness_ledger.py",
302
- "sha256": "5141168aa1c566327e421d6ab6de876dfe0cf6ae3e97d336fcce6dc7fc4bfb56"
302
+ "sha256": "60a6cdff5c3c81abf5606f7914c299828c4ed2b21d19e9acbb27b001a03fcaa5"
303
303
  },
304
304
  {
305
305
  "path": "scripts/harness_paths.py",
306
- "sha256": "75560c0141172b335811014320001a299846e4fba45dd0589cba86da058ab01a"
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": "5534eb277e29e3473149a0f113ea107dc17b6cade742d7de920befb87e46fdfa"
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": "7c02010be79352e552d028ac7a5884f273b832af304cb2336824d41a4371a800"
322
+ "sha256": "e242f79d36d7a38bdf3927cd2f65d1c7bb687a4119b07e41a59fec6989d5e253"
323
323
  },
324
324
  {
325
325
  "path": "scripts/harness_review.py",
326
- "sha256": "78009e09b79867a13c66a8c71114b6cd04fe75c72054f5ec35d63c7be990bb04"
326
+ "sha256": "a741a63f62993d72889e78f540feb57a95e713b4d7a98fa61ff208b2ce914ca7"
327
327
  },
328
328
  {
329
329
  "path": "scripts/harness_runtime.py",