@hunter-harness/workflow-harness 0.2.46 → 0.2.47

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 (155) hide show
  1. package/README.md +1 -1
  2. package/harness/bundles/general/claude-code/.harness-build.json +1 -1
  3. package/harness/bundles/general/claude-code/harness-archive/SKILL.md +9 -10
  4. package/harness/bundles/general/claude-code/harness-codebase-map/SKILL.md +1 -1
  5. package/harness/bundles/general/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  7. package/harness/bundles/general/claude-code/harness-knowledge-query/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +6 -3
  9. package/harness/bundles/general/claude-code/harness-review/SKILL.md +6 -7
  10. package/harness/bundles/general/claude-code/harness-run/SKILL.md +6 -3
  11. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +5 -7
  12. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/harness-test/SKILL.md +6 -3
  14. package/harness/bundles/general/claude-code/scripts/harness_archive.py +164 -17
  15. package/harness/bundles/general/claude-code/scripts/harness_events.py +30 -1
  16. package/harness/bundles/general/claude-code/scripts/harness_events_sync.py +269 -0
  17. package/harness/bundles/general/claude-code/scripts/harness_gate.py +216 -64
  18. package/harness/bundles/general/claude-code/scripts/harness_headless.py +37 -0
  19. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  20. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +9 -11
  21. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  24. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +6 -3
  26. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +6 -7
  27. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +6 -3
  28. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +5 -8
  29. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +6 -3
  31. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +164 -17
  32. package/harness/bundles/general/codebuddy/scripts/harness_events.py +30 -1
  33. package/harness/bundles/general/codebuddy/scripts/harness_events_sync.py +269 -0
  34. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +216 -64
  35. package/harness/bundles/general/codebuddy/scripts/harness_headless.py +37 -0
  36. package/harness/bundles/general/codex/.harness-build.json +1 -1
  37. package/harness/bundles/general/codex/harness-archive/SKILL.md +9 -11
  38. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  39. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  40. package/harness/bundles/general/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  41. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-plan/SKILL.md +6 -3
  43. package/harness/bundles/general/codex/harness-review/SKILL.md +6 -7
  44. package/harness/bundles/general/codex/harness-run/SKILL.md +6 -3
  45. package/harness/bundles/general/codex/harness-submit/SKILL.md +5 -8
  46. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-test/SKILL.md +6 -3
  48. package/harness/bundles/general/codex/scripts/harness_archive.py +164 -17
  49. package/harness/bundles/general/codex/scripts/harness_events.py +30 -1
  50. package/harness/bundles/general/codex/scripts/harness_events_sync.py +269 -0
  51. package/harness/bundles/general/codex/scripts/harness_gate.py +216 -64
  52. package/harness/bundles/general/codex/scripts/harness_headless.py +37 -0
  53. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  54. package/harness/bundles/general/cursor/harness-archive/SKILL.md +9 -11
  55. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  56. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  57. package/harness/bundles/general/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  58. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  59. package/harness/bundles/general/cursor/harness-plan/SKILL.md +6 -3
  60. package/harness/bundles/general/cursor/harness-review/SKILL.md +6 -7
  61. package/harness/bundles/general/cursor/harness-run/SKILL.md +6 -3
  62. package/harness/bundles/general/cursor/harness-submit/SKILL.md +5 -8
  63. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-test/SKILL.md +6 -3
  65. package/harness/bundles/general/cursor/scripts/harness_archive.py +164 -17
  66. package/harness/bundles/general/cursor/scripts/harness_events.py +30 -1
  67. package/harness/bundles/general/cursor/scripts/harness_events_sync.py +269 -0
  68. package/harness/bundles/general/cursor/scripts/harness_gate.py +216 -64
  69. package/harness/bundles/general/cursor/scripts/harness_headless.py +37 -0
  70. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  71. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +3 -2
  72. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +9 -10
  73. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  74. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  75. package/harness/bundles/java/claude-code/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  76. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  77. package/harness/bundles/java/claude-code/harness-package/SKILL.md +3 -2
  78. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +6 -3
  79. package/harness/bundles/java/claude-code/harness-review/SKILL.md +6 -7
  80. package/harness/bundles/java/claude-code/harness-run/SKILL.md +6 -3
  81. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +5 -7
  82. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-test/SKILL.md +6 -3
  84. package/harness/bundles/java/claude-code/scripts/harness_archive.py +164 -17
  85. package/harness/bundles/java/claude-code/scripts/harness_events.py +30 -1
  86. package/harness/bundles/java/claude-code/scripts/harness_events_sync.py +269 -0
  87. package/harness/bundles/java/claude-code/scripts/harness_gate.py +216 -64
  88. package/harness/bundles/java/claude-code/scripts/harness_headless.py +37 -0
  89. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  90. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +3 -2
  91. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +9 -11
  92. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  93. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  94. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  95. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  96. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +2 -2
  97. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +6 -3
  98. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +6 -7
  99. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +6 -3
  100. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +5 -8
  101. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  102. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +6 -3
  103. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +164 -17
  104. package/harness/bundles/java/codebuddy/scripts/harness_events.py +30 -1
  105. package/harness/bundles/java/codebuddy/scripts/harness_events_sync.py +269 -0
  106. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +216 -64
  107. package/harness/bundles/java/codebuddy/scripts/harness_headless.py +37 -0
  108. package/harness/bundles/java/codex/.harness-build.json +1 -1
  109. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +3 -2
  110. package/harness/bundles/java/codex/harness-archive/SKILL.md +9 -11
  111. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  112. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  113. package/harness/bundles/java/codex/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  114. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  115. package/harness/bundles/java/codex/harness-package/SKILL.md +2 -2
  116. package/harness/bundles/java/codex/harness-plan/SKILL.md +6 -3
  117. package/harness/bundles/java/codex/harness-review/SKILL.md +6 -7
  118. package/harness/bundles/java/codex/harness-run/SKILL.md +6 -3
  119. package/harness/bundles/java/codex/harness-submit/SKILL.md +5 -8
  120. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/harness-test/SKILL.md +6 -3
  122. package/harness/bundles/java/codex/scripts/harness_archive.py +164 -17
  123. package/harness/bundles/java/codex/scripts/harness_events.py +30 -1
  124. package/harness/bundles/java/codex/scripts/harness_events_sync.py +269 -0
  125. package/harness/bundles/java/codex/scripts/harness_gate.py +216 -64
  126. package/harness/bundles/java/codex/scripts/harness_headless.py +37 -0
  127. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  128. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +3 -2
  129. package/harness/bundles/java/cursor/harness-archive/SKILL.md +9 -11
  130. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  131. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  132. package/harness/bundles/java/cursor/harness-knowledge-ingest/scripts/harness_knowledge.py +347 -1
  133. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  134. package/harness/bundles/java/cursor/harness-package/SKILL.md +2 -2
  135. package/harness/bundles/java/cursor/harness-plan/SKILL.md +6 -3
  136. package/harness/bundles/java/cursor/harness-review/SKILL.md +6 -7
  137. package/harness/bundles/java/cursor/harness-run/SKILL.md +6 -3
  138. package/harness/bundles/java/cursor/harness-submit/SKILL.md +5 -8
  139. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  140. package/harness/bundles/java/cursor/harness-test/SKILL.md +6 -3
  141. package/harness/bundles/java/cursor/scripts/harness_archive.py +164 -17
  142. package/harness/bundles/java/cursor/scripts/harness_events.py +30 -1
  143. package/harness/bundles/java/cursor/scripts/harness_events_sync.py +269 -0
  144. package/harness/bundles/java/cursor/scripts/harness_gate.py +216 -64
  145. package/harness/bundles/java/cursor/scripts/harness_headless.py +37 -0
  146. package/harness/manifests/general/claude-code.json +25 -17
  147. package/harness/manifests/general/codebuddy.json +25 -17
  148. package/harness/manifests/general/codex.json +25 -17
  149. package/harness/manifests/general/cursor.json +25 -17
  150. package/harness/manifests/java/claude-code.json +27 -19
  151. package/harness/manifests/java/codebuddy.json +27 -19
  152. package/harness/manifests/java/codex.json +27 -19
  153. package/harness/manifests/java/cursor.json +27 -19
  154. package/hunter-workflow-family.json +4 -4
  155. package/package.json +1 -1
@@ -124,6 +124,87 @@ def _read_json(path: Path) -> Any:
124
124
  return json.loads(path.read_text(encoding="utf-8-sig"))
125
125
 
126
126
 
127
+ # ---------------------------------------------------------------------------
128
+ # Gate severity tiering (P1 controllability)
129
+ #
130
+ # Hard invariants stay fail-closed in every mode:
131
+ # 1. submit/merge identity & final-hash consistency (ledger/integration);
132
+ # 2. archive integrity (manifest/minimal set, enforced in harness_archive);
133
+ # 3. destructive-action protection (cleanup topology / protected paths).
134
+ # The sites listed in SOFT_GATE_SITES guard regenerable bookkeeping
135
+ # (plan-handoff projections, phase capsules, scenario coverage receipts,
136
+ # test-guard tracking). In "lenient" mode a failure at one of those sites is
137
+ # recorded as a WARN receipt (evidence/gate-warnings.ndjson) instead of
138
+ # blocking the phase. Release phases (submit/merge/archive/release/deploy)
139
+ # never downgrade.
140
+ # ---------------------------------------------------------------------------
141
+
142
+ GATE_RELEASE_PHASES = frozenset({"submit", "merge", "archive", "release", "deploy"})
143
+ SOFT_GATE_SITES = frozenset({
144
+ "plan-handoff",
145
+ "capsule",
146
+ "scenario-coverage",
147
+ "test-guard",
148
+ })
149
+ GATE_WARNINGS_REL = Path("evidence") / "gate-warnings.ndjson"
150
+
151
+
152
+ def gate_severity_mode(project: Path, change_dir: Path | None = None) -> str:
153
+ """Resolve gate severity mode: env > change gate-policy > project config."""
154
+ env_mode = str(os.environ.get("HUNTER_HARNESS_GATE_MODE") or "").strip().lower()
155
+ if env_mode in {"strict", "lenient"}:
156
+ return env_mode
157
+ candidates: list[Path] = []
158
+ if change_dir is not None:
159
+ candidates.append(change_dir / "meta" / "gate-policy.json")
160
+ candidates.append(project / ".harness" / "config" / "gate-policy.json")
161
+ for path in candidates:
162
+ if not path.is_file():
163
+ continue
164
+ try:
165
+ document = _read_json(path)
166
+ except (OSError, json.JSONDecodeError):
167
+ continue
168
+ if not isinstance(document, dict):
169
+ continue
170
+ value = str(document.get("severityMode") or "").strip().lower()
171
+ if value in {"strict", "lenient"}:
172
+ return value
173
+ return "strict"
174
+
175
+
176
+ def gate_soft_allowed(mode: str, phase: str, site: str) -> bool:
177
+ return (
178
+ mode == "lenient"
179
+ and phase not in GATE_RELEASE_PHASES
180
+ and site in SOFT_GATE_SITES
181
+ )
182
+
183
+
184
+ def record_gate_warning(
185
+ change_dir: Path,
186
+ *,
187
+ phase: str,
188
+ site: str,
189
+ code: str,
190
+ message: str,
191
+ ) -> dict[str, Any]:
192
+ """Append a downgraded-gate receipt; the phase continues with WARN."""
193
+ entry = {
194
+ "ts": he.now_iso(),
195
+ "phase": phase,
196
+ "site": site,
197
+ "code": code,
198
+ "message": message,
199
+ "mode": "lenient",
200
+ }
201
+ path = change_dir / GATE_WARNINGS_REL
202
+ path.parent.mkdir(parents=True, exist_ok=True)
203
+ with path.open("a", encoding="utf-8", newline="\n") as handle:
204
+ handle.write(json.dumps(entry, ensure_ascii=False, separators=(",", ":")) + "\n")
205
+ return entry
206
+
207
+
127
208
  def _write_json(path: Path, data: Any) -> None:
128
209
  path.parent.mkdir(parents=True, exist_ok=True)
129
210
  text = json.dumps(data, ensure_ascii=False, indent=2) + "\n"
@@ -1561,7 +1642,8 @@ def append_phase_event(
1561
1642
  he.atomic_append_line(events_file, line)
1562
1643
  rendered = False
1563
1644
  log_path = None
1564
- if type_ == "phase.end" or auto_sealed:
1645
+ if (type_ == "phase.end" or auto_sealed) and \
1646
+ he.execution_log_render_enabled(change_dir):
1565
1647
  events = he.load_events(events_file)
1566
1648
  content = he.render_execution_log(events)
1567
1649
  log_path = he.write_execution_log(change_dir, content)
@@ -1882,6 +1964,8 @@ def cmd_begin(args: argparse.Namespace) -> int:
1882
1964
  extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
1883
1965
  )
1884
1966
  change_dir = Path(resolved["changeDir"])
1967
+ severity_mode = gate_severity_mode(project, change_dir)
1968
+ gate_warnings: list[dict[str, Any]] = []
1885
1969
  plan_verification: dict[str, Any] | None = None
1886
1970
  if args.phase == "run":
1887
1971
  plan_verification = validate_plan_handoff(change_dir)
@@ -1894,16 +1978,25 @@ def cmd_begin(args: argparse.Namespace) -> int:
1894
1978
  or plan_verification.get("message")
1895
1979
  or "finalized plan verification failed"
1896
1980
  )
1897
- return emit_error(
1898
- code,
1899
- message,
1900
- as_json=as_json,
1901
- extra={
1902
- key: value
1903
- for key, value in plan_verification.items()
1904
- if key not in {"ok", "code", "error", "message"}
1905
- },
1906
- )
1981
+ if gate_soft_allowed(severity_mode, args.phase, "plan-handoff"):
1982
+ gate_warnings.append(record_gate_warning(
1983
+ change_dir,
1984
+ phase=args.phase,
1985
+ site="plan-handoff",
1986
+ code=code,
1987
+ message=message,
1988
+ ))
1989
+ else:
1990
+ return emit_error(
1991
+ code,
1992
+ message,
1993
+ as_json=as_json,
1994
+ extra={
1995
+ key: value
1996
+ for key, value in plan_verification.items()
1997
+ if key not in {"ok", "code", "error", "message"}
1998
+ },
1999
+ )
1907
2000
  concurrency_block = hc.check_concurrency_block(project, resolved["changeId"])
1908
2001
  if concurrency_block is not None:
1909
2002
  record_blocked_attempt(
@@ -1998,7 +2091,18 @@ def cmd_begin(args: argparse.Namespace) -> int:
1998
2091
  skills_root=Path(args.skills_root).expanduser(),
1999
2092
  )
2000
2093
  except ValueError as exc:
2001
- return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
2094
+ if gate_soft_allowed(severity_mode, args.phase, "capsule"):
2095
+ gate_warnings.append(record_gate_warning(
2096
+ change_dir,
2097
+ phase=args.phase,
2098
+ site="capsule",
2099
+ code="PHASE_CAPSULE_MISMATCH",
2100
+ message=str(exc),
2101
+ ))
2102
+ # Stale capsule is regenerable state: rebuild it below.
2103
+ capsule = None
2104
+ else:
2105
+ return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
2002
2106
  claim = hc.claim_lease(
2003
2107
  project,
2004
2108
  change_id=resolved["changeId"],
@@ -2022,18 +2126,27 @@ def cmd_begin(args: argparse.Namespace) -> int:
2022
2126
  if args.phase in {"run", "test"}:
2023
2127
  guard_result = htg.begin(execution_root, change_dir)
2024
2128
  if not guard_result.get("ok"):
2025
- hc.release_lease(
2026
- project,
2027
- change_id=resolved["changeId"],
2028
- phase=args.phase,
2029
- run_id=run_id,
2030
- )
2031
- return emit_error(
2032
- str(guard_result.get("code", "TEST_GUARD_BEGIN_FAILED")),
2033
- "test guard begin failed",
2034
- as_json=as_json,
2035
- extra=guard_result,
2036
- )
2129
+ if gate_soft_allowed(severity_mode, args.phase, "test-guard"):
2130
+ gate_warnings.append(record_gate_warning(
2131
+ change_dir,
2132
+ phase=args.phase,
2133
+ site="test-guard",
2134
+ code=str(guard_result.get("code", "TEST_GUARD_BEGIN_FAILED")),
2135
+ message="test guard begin failed (downgraded to WARN)",
2136
+ ))
2137
+ else:
2138
+ hc.release_lease(
2139
+ project,
2140
+ change_id=resolved["changeId"],
2141
+ phase=args.phase,
2142
+ run_id=run_id,
2143
+ )
2144
+ return emit_error(
2145
+ str(guard_result.get("code", "TEST_GUARD_BEGIN_FAILED")),
2146
+ "test guard begin failed",
2147
+ as_json=as_json,
2148
+ extra=guard_result,
2149
+ )
2037
2150
  state_root = Path(hp.resolve_state_dir_for_contract(change_dir)).resolve()
2038
2151
  current_head = _git_text(execution_root, "rev-parse", "--verify", "HEAD")
2039
2152
  ledger, _ = hl.load_ledger(change_dir)
@@ -2091,6 +2204,8 @@ def cmd_begin(args: argparse.Namespace) -> int:
2091
2204
  "planVerification": plan_verification,
2092
2205
  "projection": projection,
2093
2206
  "executionRootSource": execution_hint["source"],
2207
+ "gateSeverityMode": severity_mode,
2208
+ "gateWarnings": gate_warnings,
2094
2209
  }
2095
2210
  emit(payload, as_json=as_json)
2096
2211
  return 0
@@ -2108,6 +2223,8 @@ def cmd_close(args: argparse.Namespace) -> int:
2108
2223
  extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
2109
2224
  )
2110
2225
  change_dir = Path(resolved["changeDir"])
2226
+ severity_mode = gate_severity_mode(project, change_dir)
2227
+ gate_warnings: list[dict[str, Any]] = []
2111
2228
  blocked = foundation_gate_blocks(getattr(args, "task", None), change_dir)
2112
2229
  if blocked:
2113
2230
  return emit_error(
@@ -2204,18 +2321,27 @@ def cmd_close(args: argparse.Namespace) -> int:
2204
2321
  allow_head_advance=args.phase in {"run", "submit", "merge"},
2205
2322
  )
2206
2323
  except ValueError as exc:
2207
- persist_close_failure(
2208
- change_dir,
2209
- args.phase,
2210
- run_id,
2211
- capsule,
2212
- status="CAPSULE_VALIDATION_FAILED",
2213
- error={
2214
- "code": "PHASE_CAPSULE_MISMATCH",
2215
- "message": str(exc),
2216
- },
2217
- )
2218
- return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
2324
+ if gate_soft_allowed(severity_mode, args.phase, "capsule"):
2325
+ gate_warnings.append(record_gate_warning(
2326
+ change_dir,
2327
+ phase=args.phase,
2328
+ site="capsule",
2329
+ code="PHASE_CAPSULE_MISMATCH",
2330
+ message=str(exc),
2331
+ ))
2332
+ else:
2333
+ persist_close_failure(
2334
+ change_dir,
2335
+ args.phase,
2336
+ run_id,
2337
+ capsule,
2338
+ status="CAPSULE_VALIDATION_FAILED",
2339
+ error={
2340
+ "code": "PHASE_CAPSULE_MISMATCH",
2341
+ "message": str(exc),
2342
+ },
2343
+ )
2344
+ return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
2219
2345
 
2220
2346
  projection = evaluate_projection_gate(project, args.phase)
2221
2347
  if not projection.get("ok"):
@@ -2294,20 +2420,29 @@ def cmd_close(args: argparse.Namespace) -> int:
2294
2420
  if args.phase in {"run", "test"}:
2295
2421
  coverage = _validate_scenario_coverage(change_dir)
2296
2422
  if not coverage.get("ok"):
2297
- persist_close_failure(
2298
- change_dir,
2299
- args.phase,
2300
- run_id,
2301
- capsule,
2302
- status="SCENARIO_VALIDATION_FAILED",
2303
- error=coverage,
2304
- )
2305
- return emit_error(
2306
- str(coverage.get("code", "SCENARIO_COVERAGE_FAILED")),
2307
- str(coverage.get("message", "scenario coverage validation failed")),
2308
- as_json=as_json,
2309
- extra={k: v for k, v in coverage.items() if k not in {"ok", "message", "code"}},
2310
- )
2423
+ if gate_soft_allowed(severity_mode, args.phase, "scenario-coverage"):
2424
+ gate_warnings.append(record_gate_warning(
2425
+ change_dir,
2426
+ phase=args.phase,
2427
+ site="scenario-coverage",
2428
+ code=str(coverage.get("code", "SCENARIO_COVERAGE_FAILED")),
2429
+ message=str(coverage.get("message", "scenario coverage validation failed")),
2430
+ ))
2431
+ else:
2432
+ persist_close_failure(
2433
+ change_dir,
2434
+ args.phase,
2435
+ run_id,
2436
+ capsule,
2437
+ status="SCENARIO_VALIDATION_FAILED",
2438
+ error=coverage,
2439
+ )
2440
+ return emit_error(
2441
+ str(coverage.get("code", "SCENARIO_COVERAGE_FAILED")),
2442
+ str(coverage.get("message", "scenario coverage validation failed")),
2443
+ as_json=as_json,
2444
+ extra={k: v for k, v in coverage.items() if k not in {"ok", "message", "code"}},
2445
+ )
2311
2446
 
2312
2447
  close_status = args.status
2313
2448
  close_code = "PHASE_CLOSED"
@@ -2316,6 +2451,12 @@ def cmd_close(args: argparse.Namespace) -> int:
2316
2451
  # Degraded close: phase.end status must not exceed WARN (OK → WARN).
2317
2452
  if close_status == "OK":
2318
2453
  close_status = "WARN"
2454
+ if gate_warnings:
2455
+ # Lenient-mode downgrades: phase closes, but never better than WARN.
2456
+ if close_status == "OK":
2457
+ close_status = "WARN"
2458
+ if close_code == "PHASE_CLOSED":
2459
+ close_code = "CLOSED_WITH_WARNINGS"
2319
2460
 
2320
2461
  close_transaction: dict[str, Any] = {}
2321
2462
  if capsule is not None:
@@ -2340,20 +2481,29 @@ def cmd_close(args: argparse.Namespace) -> int:
2340
2481
  else:
2341
2482
  guard_result = htg.close(execution_root, change_dir)
2342
2483
  if not guard_result.get("ok"):
2343
- if capsule is not None:
2344
- close_transaction.update({
2345
- "status": "GUARD_CLOSE_FAILED",
2346
- "lastError": guard_result,
2347
- "updatedAt": he.now_iso(),
2348
- })
2349
- write_phase_capsule(change_dir, args.phase, run_id, capsule)
2350
- return emit_error(
2351
- str(guard_result.get("code", "TEST_GUARD_CLOSE_FAILED")),
2352
- "test guard close failed",
2353
- as_json=as_json,
2354
- extra=guard_result,
2355
- )
2356
- if capsule is not None:
2484
+ if gate_soft_allowed(severity_mode, args.phase, "test-guard"):
2485
+ gate_warnings.append(record_gate_warning(
2486
+ change_dir,
2487
+ phase=args.phase,
2488
+ site="test-guard",
2489
+ code=str(guard_result.get("code", "TEST_GUARD_CLOSE_FAILED")),
2490
+ message="test guard close failed (downgraded to WARN)",
2491
+ ))
2492
+ else:
2493
+ if capsule is not None:
2494
+ close_transaction.update({
2495
+ "status": "GUARD_CLOSE_FAILED",
2496
+ "lastError": guard_result,
2497
+ "updatedAt": he.now_iso(),
2498
+ })
2499
+ write_phase_capsule(change_dir, args.phase, run_id, capsule)
2500
+ return emit_error(
2501
+ str(guard_result.get("code", "TEST_GUARD_CLOSE_FAILED")),
2502
+ "test guard close failed",
2503
+ as_json=as_json,
2504
+ extra=guard_result,
2505
+ )
2506
+ if guard_result.get("ok") and capsule is not None:
2357
2507
  close_transaction["guardClosed"] = True
2358
2508
  close_transaction["updatedAt"] = he.now_iso()
2359
2509
  write_phase_capsule(change_dir, args.phase, run_id, capsule)
@@ -2437,6 +2587,8 @@ def cmd_close(args: argparse.Namespace) -> int:
2437
2587
  "testGuard": guard_result,
2438
2588
  "event": event_result,
2439
2589
  "lease": release,
2590
+ "gateSeverityMode": severity_mode,
2591
+ "gateWarnings": gate_warnings,
2440
2592
  }
2441
2593
  emit(payload, as_json=as_json)
2442
2594
  return 0
@@ -0,0 +1,37 @@
1
+ #!/usr/bin/env python3
2
+ """Shared headless JSON envelope helpers for stage scripts (P5 contract)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import json
7
+ import sys
8
+ from typing import Any
9
+
10
+
11
+ def headless_envelope(
12
+ *,
13
+ command: str,
14
+ ok: bool,
15
+ exit_code: int,
16
+ change: str | None = None,
17
+ phase: str | None = None,
18
+ warnings: list[Any] | None = None,
19
+ errors: list[Any] | None = None,
20
+ result: dict[str, Any] | None = None,
21
+ ) -> dict[str, Any]:
22
+ return {
23
+ "schema_version": 1,
24
+ "ok": ok,
25
+ "command": command,
26
+ "change": change,
27
+ "phase": phase,
28
+ "exit_code": exit_code,
29
+ "warnings": warnings or [],
30
+ "errors": errors or [],
31
+ "result": result or {},
32
+ }
33
+
34
+
35
+ def emit_headless(payload: dict[str, Any], *, stream=None) -> None:
36
+ out = stream or sys.stdout
37
+ print(json.dumps(payload, ensure_ascii=False, indent=2), file=out)
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "codebuddy",
5
- "coreHash": "519b79b5e5c36857"
5
+ "coreHash": "d5cc283de8c743a2"
6
6
  }
@@ -1,9 +1,9 @@
1
1
  ---
2
2
  name: harness-archive
3
- description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
- .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
3
+ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到 .harness/archive/,含归档元数据。仅当用户显式调用
4
+ /harness-archive 时使用;不得被其他阶段 skill 自动接续触发。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=519b79b5e5c36857; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=d5cc283de8c743a2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -12,11 +12,9 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
12
12
 
13
13
  ## When to Use
14
14
 
15
- 使用场景:
15
+ 仅当用户显式调用 `/harness-archive` 时执行。submit/merge 完成后**不自动**进入本阶段;用户口头说"归档/收尾"而未调用本 skill 时,先确认。
16
16
 
17
- - 用户说"归档 / 完成归档 / 收尾 / 打包产出物"。
18
- - submit 已推送、test/review 报告已就绪(或已明确标记跳过/未运行),需要封存变更产出。
19
- - 变更闭环最后一站,归档后从工作区移除 `.harness/changes/<change-name>/`。
17
+ 前置:submit 已推送、test/review 报告已就绪(或已明确标记跳过/未运行)。变更闭环最后一站,归档后从工作区移除 `.harness/changes/<change-name>/`。
20
18
 
21
19
  跳过场景:
22
20
 
@@ -25,8 +23,8 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
25
23
 
26
24
  自动调用边界:
27
25
 
28
- - 归档涉及移动/删除原变更目录,支持被其他 skill 调用,但调用前必须确保前置条件已满足;也支持用户显式 `/harness-archive` 调用。
29
- - 默认归档前确认是阻断检查点。若 `harness_archive.py auto-gate` 返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足),后续 Archive 可无 AskQuestion 自动运行;其他情况仍须确认。
26
+ - 归档涉及移动/删除原变更目录,**禁止被其他 skill 自动调用**;只接受用户显式 `/harness-archive` 调用。
27
+ - `harness_archive.py auto-gate` 只影响**本次显式调用内**是否需要归档确认对话:返回 `ARCHIVE_AUTO_GATE_SATISFIED`(Submit 或 Merge 已终态,archive-boundary snapshot 存在且全部归档门禁满足)时可跳过 AskQuestion 直接归档;其他情况仍须确认。auto-gate 不构成跨阶段自动触发的授权。
30
28
 
31
29
  ## 前置条件
32
30
 
@@ -160,7 +158,7 @@ description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档
160
158
 
161
159
  无测试报告 → 显示"未运行测试 / 静态验证",不得 100% 通过率;无 review → "📝ADVISORY:未运行 review"。状态用 ✅OK / 🟡WARN / 🔁REUSED / 🔁RETESTED / 📝ADVISORY / 🧹NON_BEHAVIORAL_CLEANUP,复用前一阶段结果显示 🔁REUSED,**不得伪装成重新执行,不得无脑全绿**。
162
160
 
163
- **final-summary 必须产出**:Node 渲染器不可用/超时/exit 非 0/未产出文件时,`harness_archive.py` 自动用内置 Python fallback 渲染(含 changeName/finalStatus/commands/verification/changedFiles/archiveManifest/knownRisks/manualActions/maintenanceNotes,USER_SKIPPED/BLOCKED_BY_DBA/失败状态可见,全部 HTML 转义)。Node Python fallback **都失败**时,finalize 立即恢复原 change 目录并 exit 非 0,**绝不归档一个没有 final-summary.html 的变更**。
161
+ **final-summary 是可再生展示投影,非硬产物**:`summary-data.json` 是最终报告的唯一权威数据源,必须产出并通过 validate/adequacy。HTML 渲染默认执行(Node 渲染器失败时自动 Python fallback),但渲染失败只记 warning,**不再回滚归档**;`finalize --no-html` 可完全跳过本地 HTML(配置远端平台时由平台按 summary-data.json 渲染)。
164
162
 
165
163
  ### 九、CONDITIONAL_OK 最终状态
166
164
 
@@ -191,7 +189,7 @@ git 命令通过 `powershell.exe -Command "..."` 执行;archive-meta.md 和 fi
191
189
  - `.harness/archive/YYYY-MM-DD-<change-name>/meta/archive-meta.md` — 归档元数据
192
190
  - `.harness/archive/YYYY-MM-DD-<change-name>/events.ndjson` — 结构化事件层(新流程推荐;旧 archive 可缺失)
193
191
  - `.harness/archive/YYYY-MM-DD-<change-name>/reports/final/summary-data.json` — 最终报告数据源
194
- - `.harness/archive/YYYY-MM-DD-<change-name>/reports/final/final-summary.html` — `render-summary.mjs` 渲染(Node 不可用或失败时由 `harness_archive.py` 内置 Python fallback 渲染);**始终必须存在**,否则 finalize 恢复原 change 目录并 exit 非 0
192
+ - `.harness/archive/YYYY-MM-DD-<change-name>/reports/final/final-summary.html` — 可选展示投影,由 `render-summary.mjs` 渲染(Node 失败时 Python fallback);渲染失败降级 warning,`--no-html` 可跳过
195
193
  - `.harness/archive/YYYY-MM-DD-<change-name>/evidence/archive-manifest-before.json` / `archive-manifest-after.json` — 归档前后 manifest/checksum
196
194
 
197
195
  ## 渐进披露
@@ -3,7 +3,7 @@ name: harness-codebase-map
3
3
  description: 分析现有代码库并生成 .harness/codebase/map/
4
4
  下的结构化代码库地图。用于陌生项目理解、重大改造前梳理、harness-sync 提示 map 过期后的人工刷新。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=519b79b5e5c36857; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=d5cc283de8c743a2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-codebase-map — 代码库地图
8
8
 
9
9
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-knowledge-ingest
3
3
  description: 从 .harness/archive 归档整理、同步和维护项目知识索引。适用场景:ingest knowledge、sync
4
4
  knowledge、rebuild knowledge index、promote knowledge、确认知识条目、检查知识库是否过期。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=519b79b5e5c36857; overlay=none; agent=codebuddy; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=d5cc283de8c743a2; overlay=none; agent=codebuddy; do not edit -->
7
7
  # harness-knowledge-ingest
8
8
 
9
9
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。