@hunter-harness/workflow-harness 0.2.13 → 0.2.14

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 (186) 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-query/SKILL.md +1 -1
  6. package/harness/bundles/general/claude-code/harness-plan/SKILL.md +1 -1
  7. package/harness/bundles/general/claude-code/harness-review/SKILL.md +1 -1
  8. package/harness/bundles/general/claude-code/harness-run/SKILL.md +1 -1
  9. package/harness/bundles/general/claude-code/harness-submit/SKILL.md +2 -2
  10. package/harness/bundles/general/claude-code/harness-submit/checklist.md +1 -1
  11. package/harness/bundles/general/claude-code/harness-sync/SKILL.md +1 -1
  12. package/harness/bundles/general/claude-code/harness-test/SKILL.md +1 -1
  13. package/harness/bundles/general/claude-code/scripts/harness_archive.py +124 -0
  14. package/harness/bundles/general/claude-code/scripts/harness_change.py +54 -0
  15. package/harness/bundles/general/claude-code/scripts/harness_deploy.py +177 -0
  16. package/harness/bundles/general/claude-code/scripts/harness_events.py +99 -1
  17. package/harness/bundles/general/claude-code/scripts/harness_gate.py +57 -4
  18. package/harness/bundles/general/claude-code/scripts/harness_integration.py +3 -0
  19. package/harness/bundles/general/claude-code/scripts/harness_ledger.py +15 -3
  20. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +47 -0
  21. package/harness/bundles/general/claude-code/scripts/harness_test_guard.py +86 -13
  22. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  23. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -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-run/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +2 -2
  31. package/harness/bundles/general/codebuddy/harness-submit/checklist.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_archive.py +124 -0
  35. package/harness/bundles/general/codebuddy/scripts/harness_change.py +54 -0
  36. package/harness/bundles/general/codebuddy/scripts/harness_deploy.py +177 -0
  37. package/harness/bundles/general/codebuddy/scripts/harness_events.py +99 -1
  38. package/harness/bundles/general/codebuddy/scripts/harness_gate.py +57 -4
  39. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +3 -0
  40. package/harness/bundles/general/codebuddy/scripts/harness_ledger.py +15 -3
  41. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +47 -0
  42. package/harness/bundles/general/codebuddy/scripts/harness_test_guard.py +86 -13
  43. package/harness/bundles/general/codex/.harness-build.json +1 -1
  44. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  48. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/harness-submit/SKILL.md +2 -2
  52. package/harness/bundles/general/codex/harness-submit/checklist.md +1 -1
  53. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  54. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  55. package/harness/bundles/general/codex/scripts/harness_archive.py +124 -0
  56. package/harness/bundles/general/codex/scripts/harness_change.py +54 -0
  57. package/harness/bundles/general/codex/scripts/harness_deploy.py +177 -0
  58. package/harness/bundles/general/codex/scripts/harness_events.py +99 -1
  59. package/harness/bundles/general/codex/scripts/harness_gate.py +57 -4
  60. package/harness/bundles/general/codex/scripts/harness_integration.py +3 -0
  61. package/harness/bundles/general/codex/scripts/harness_ledger.py +15 -3
  62. package/harness/bundles/general/codex/scripts/harness_preflight.py +47 -0
  63. package/harness/bundles/general/codex/scripts/harness_test_guard.py +86 -13
  64. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  65. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  67. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  71. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  72. package/harness/bundles/general/cursor/harness-submit/SKILL.md +2 -2
  73. package/harness/bundles/general/cursor/harness-submit/checklist.md +1 -1
  74. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  75. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  76. package/harness/bundles/general/cursor/scripts/harness_archive.py +124 -0
  77. package/harness/bundles/general/cursor/scripts/harness_change.py +54 -0
  78. package/harness/bundles/general/cursor/scripts/harness_deploy.py +177 -0
  79. package/harness/bundles/general/cursor/scripts/harness_events.py +99 -1
  80. package/harness/bundles/general/cursor/scripts/harness_gate.py +57 -4
  81. package/harness/bundles/general/cursor/scripts/harness_integration.py +3 -0
  82. package/harness/bundles/general/cursor/scripts/harness_ledger.py +15 -3
  83. package/harness/bundles/general/cursor/scripts/harness_preflight.py +47 -0
  84. package/harness/bundles/general/cursor/scripts/harness_test_guard.py +86 -13
  85. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  86. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  87. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  88. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  89. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  90. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  91. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  92. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  93. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  94. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  95. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +2 -2
  96. package/harness/bundles/java/claude-code/harness-submit/checklist.md +1 -1
  97. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  98. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  99. package/harness/bundles/java/claude-code/scripts/harness_archive.py +124 -0
  100. package/harness/bundles/java/claude-code/scripts/harness_change.py +54 -0
  101. package/harness/bundles/java/claude-code/scripts/harness_deploy.py +177 -0
  102. package/harness/bundles/java/claude-code/scripts/harness_events.py +99 -1
  103. package/harness/bundles/java/claude-code/scripts/harness_gate.py +57 -4
  104. package/harness/bundles/java/claude-code/scripts/harness_integration.py +3 -0
  105. package/harness/bundles/java/claude-code/scripts/harness_ledger.py +15 -3
  106. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +47 -0
  107. package/harness/bundles/java/claude-code/scripts/harness_test_guard.py +86 -13
  108. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  109. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  110. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  111. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  112. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  113. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  114. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  115. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  116. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  117. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  118. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +2 -2
  119. package/harness/bundles/java/codebuddy/harness-submit/checklist.md +1 -1
  120. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  121. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  122. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +124 -0
  123. package/harness/bundles/java/codebuddy/scripts/harness_change.py +54 -0
  124. package/harness/bundles/java/codebuddy/scripts/harness_deploy.py +177 -0
  125. package/harness/bundles/java/codebuddy/scripts/harness_events.py +99 -1
  126. package/harness/bundles/java/codebuddy/scripts/harness_gate.py +57 -4
  127. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +3 -0
  128. package/harness/bundles/java/codebuddy/scripts/harness_ledger.py +15 -3
  129. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +47 -0
  130. package/harness/bundles/java/codebuddy/scripts/harness_test_guard.py +86 -13
  131. package/harness/bundles/java/codex/.harness-build.json +1 -1
  132. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  133. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  134. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  135. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  136. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  137. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  138. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  139. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  140. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  141. package/harness/bundles/java/codex/harness-submit/SKILL.md +2 -2
  142. package/harness/bundles/java/codex/harness-submit/checklist.md +1 -1
  143. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  144. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  145. package/harness/bundles/java/codex/scripts/harness_archive.py +124 -0
  146. package/harness/bundles/java/codex/scripts/harness_change.py +54 -0
  147. package/harness/bundles/java/codex/scripts/harness_deploy.py +177 -0
  148. package/harness/bundles/java/codex/scripts/harness_events.py +99 -1
  149. package/harness/bundles/java/codex/scripts/harness_gate.py +57 -4
  150. package/harness/bundles/java/codex/scripts/harness_integration.py +3 -0
  151. package/harness/bundles/java/codex/scripts/harness_ledger.py +15 -3
  152. package/harness/bundles/java/codex/scripts/harness_preflight.py +47 -0
  153. package/harness/bundles/java/codex/scripts/harness_test_guard.py +86 -13
  154. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  155. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  156. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  157. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  158. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  159. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  160. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  161. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  162. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  163. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  164. package/harness/bundles/java/cursor/harness-submit/SKILL.md +2 -2
  165. package/harness/bundles/java/cursor/harness-submit/checklist.md +1 -1
  166. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  167. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  168. package/harness/bundles/java/cursor/scripts/harness_archive.py +124 -0
  169. package/harness/bundles/java/cursor/scripts/harness_change.py +54 -0
  170. package/harness/bundles/java/cursor/scripts/harness_deploy.py +177 -0
  171. package/harness/bundles/java/cursor/scripts/harness_events.py +99 -1
  172. package/harness/bundles/java/cursor/scripts/harness_gate.py +57 -4
  173. package/harness/bundles/java/cursor/scripts/harness_integration.py +3 -0
  174. package/harness/bundles/java/cursor/scripts/harness_ledger.py +15 -3
  175. package/harness/bundles/java/cursor/scripts/harness_preflight.py +47 -0
  176. package/harness/bundles/java/cursor/scripts/harness_test_guard.py +86 -13
  177. package/harness/manifests/general/claude-code.json +22 -22
  178. package/harness/manifests/general/codebuddy.json +22 -22
  179. package/harness/manifests/general/codex.json +22 -22
  180. package/harness/manifests/general/cursor.json +22 -22
  181. package/harness/manifests/java/claude-code.json +24 -24
  182. package/harness/manifests/java/codebuddy.json +24 -24
  183. package/harness/manifests/java/codex.json +24 -24
  184. package/harness/manifests/java/cursor.json +24 -24
  185. package/hunter-workflow-family.json +2 -2
  186. package/package.json +15 -15
@@ -506,6 +506,81 @@ def new_event_id(existing: list[dict[str, Any]] | None = None) -> str:
506
506
  return f"evt-{uuid.uuid4().hex}"
507
507
 
508
508
 
509
+ def append_event(
510
+ change_dir: Path,
511
+ *,
512
+ phase: str,
513
+ type_: str,
514
+ note: str = "",
515
+ kind: str | None = None,
516
+ path: str | None = None,
517
+ run_id: str | None = None,
518
+ executor_tool: str | None = None,
519
+ ) -> dict[str, Any]:
520
+ """Programmatic append API (retro §5.31 C5/T16).
521
+
522
+ Validates and appends an event without going through argparse. Returns
523
+ a payload dict with ``ok``/``code``/``event``.
524
+ """
525
+ args = argparse.Namespace(
526
+ change_dir=str(change_dir),
527
+ phase=phase,
528
+ type=type_,
529
+ note=note,
530
+ kind=kind,
531
+ path=path,
532
+ run_id=run_id,
533
+ executor_tool=executor_tool,
534
+ command=None,
535
+ exit_code=None,
536
+ duration_ms=None,
537
+ status=None,
538
+ name=None,
539
+ code=None,
540
+ severity=None,
541
+ message=None,
542
+ decision=None,
543
+ reason=None,
544
+ issue_id=None,
545
+ scope=None,
546
+ target_event_id=None,
547
+ target_field=None,
548
+ old_value_hash=None,
549
+ new_value_json=None,
550
+ attempt=None,
551
+ executor_agent=None,
552
+ executor_model=None,
553
+ handoff_from_tool=None,
554
+ handoff_reason=None,
555
+ trace_id=None,
556
+ span_id=None,
557
+ parent_span_id=None,
558
+ runner_ms=None,
559
+ orchestration_active_ms=None,
560
+ wall_clock_ms=None,
561
+ user_wait_ms=None,
562
+ legacy_lenient=False,
563
+ json=True,
564
+ )
565
+ as_json = True
566
+ if type_ not in EVENT_TYPES:
567
+ return {"ok": False, "code": "EVENT_TYPE_INVALID", "message": f"unsupported type: {type_}"}
568
+ validation = validate_append_event(args)
569
+ if validation:
570
+ error_code, message = validation
571
+ return {"ok": False, "code": error_code, "message": message}
572
+ archived = archived_change_dir(change_dir)
573
+ if archived is not None:
574
+ return {"ok": False, "code": "ARCHIVED_CHANGE_IMMUTABLE", "message": str(archived)}
575
+ events_path_obj = events_path(change_dir)
576
+ lock_path = events_path_obj.with_name(events_path_obj.name + ".lock")
577
+ existing = []
578
+ event = build_event(args, existing)
579
+ with event_file_lock(lock_path):
580
+ atomic_append_line(events_path_obj, json.dumps(event, ensure_ascii=False))
581
+ return {"ok": True, "event": event, "events_path": str(events_path_obj), "rendered": False}
582
+
583
+
509
584
  def build_event(args: argparse.Namespace, existing: list[dict[str, Any]]) -> dict[str, Any]:
510
585
  event: dict[str, Any] = {
511
586
  "schema_version": SCHEMA_VERSION,
@@ -588,8 +663,31 @@ def validate_append_event(args: argparse.Namespace) -> tuple[str, str] | None:
588
663
  except json.JSONDecodeError as exc:
589
664
  return (
590
665
  "CORRECTION_VALUE_INVALID_JSON",
591
- f"CORRECTION_VALUE_INVALID_JSON: {exc.msg}",
666
+ f"CORRECTION_VALUE_INVALID_JSON: {exc}",
667
+ )
668
+ # Retro §5.31: artifact events must distinguish file-backed from
669
+ # informational. file-backed requires a change-relative path; informational
670
+ # (preview/summary) must not masquerade as a file artifact. Legacy kind
671
+ # values (ledger, report, etc.) are treated as file-backed aliases.
672
+ if event_type == "artifact":
673
+ kind = str(getattr(args, "kind", "") or "").strip()
674
+ path = str(getattr(args, "path", "") or "").strip()
675
+ is_informational = kind == "informational"
676
+ if kind == "file-backed" and not path:
677
+ return (
678
+ "ARTIFACT_PATH_REQUIRED",
679
+ "ARTIFACT_PATH_REQUIRED: file-backed artifact requires --path",
680
+ )
681
+ # If kind is explicitly informational, path is optional. If kind is
682
+ # absent and path is absent, reject (can't infer file-backed without path).
683
+ if not kind and not path:
684
+ return (
685
+ "ARTIFACT_PATH_REQUIRED",
686
+ "ARTIFACT_PATH_REQUIRED: artifact without --kind or --path is ambiguous; "
687
+ "use --kind informational for previews or --path for file artifacts",
592
688
  )
689
+ # If no kind is provided, infer from path presence: a path implies
690
+ # file-backed, absence implies informational (legacy compatibility).
593
691
  return None
594
692
 
595
693
 
@@ -329,6 +329,12 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> tuple[
329
329
 
330
330
  Returns ``(missing_fields, degraded_ok)`` where ``degraded_ok`` is True only when
331
331
  the entry is a valid DEGRADED NOT_RUN record with no other missing fields.
332
+
333
+ Retro §5.14: ``status=OK``/``status=NOT_RUN``/``status=FAIL`` in the
334
+ missing list is a *status value* hint, not a field-completeness defect.
335
+ Callers consuming this under ``phase_status=FAIL`` must filter out
336
+ ``status=*`` entries from the missing list before treating them as
337
+ blocking problems.
332
338
  """
333
339
  missing: list[str] = []
334
340
  degraded = is_degraded_ledger_entry(entry)
@@ -341,7 +347,7 @@ def validate_ledger_entry_v2(entry: dict[str, Any], verification: str) -> tuple[
341
347
  missing.append("inputsFiles(non-empty)")
342
348
  elif field == "status":
343
349
  if value != "OK" and not degraded:
344
- missing.append("status=OK")
350
+ missing.append(f"status={value}")
345
351
  elif not (isinstance(value, str) and value.strip()):
346
352
  missing.append(field)
347
353
  elif field == "coverage" and str(value).strip() not in hl.COVERAGE_RANK:
@@ -357,8 +363,16 @@ def validate_ledger_for_phase_close(
357
363
  policy: dict[str, Any],
358
364
  *,
359
365
  execution_root: Path | None = None,
366
+ phase_status: str = "OK",
360
367
  ) -> dict[str, Any]:
361
- """Validate ledger v2 fields required for phase close (UT-026)."""
368
+ """Validate ledger v2 fields required for phase close (UT-026).
369
+
370
+ Retro §5.14: ``phase_status`` separates phase outcome from promotion gate.
371
+ ``phase_status=OK`` (default) requires all required validations to be OK;
372
+ ``phase_status=FAIL`` validates field completeness and identity but allows
373
+ individual validation status to be FAIL/NOT_RUN, so a failing phase can
374
+ close honestly without polluting evidence or blocking lease release.
375
+ """
362
376
  required = policy.get("requiredValidations", {}).get(phase, [])
363
377
  if not required:
364
378
  return {"ok": True, "code": "LEDGER_NOT_REQUIRED", "phase": phase}
@@ -443,6 +457,7 @@ def validate_ledger_for_phase_close(
443
457
 
444
458
  problems: list[dict[str, Any]] = []
445
459
  degraded: list[str] = []
460
+ fail_status: list[str] = []
446
461
  for verification in required:
447
462
  entry = validations.get(verification)
448
463
  if not isinstance(entry, dict):
@@ -455,6 +470,12 @@ def validate_ledger_for_phase_close(
455
470
  )
456
471
  continue
457
472
  missing, is_degraded = validate_ledger_entry_v2(entry, verification)
473
+ # Retro §5.14: under phase_status=FAIL/WARN, status value hints
474
+ # (status=FAIL, status=NOT_RUN) are acceptable; only real field-
475
+ # completeness defects block close. Under phase_status=OK, status
476
+ # value hints are blocking.
477
+ if phase_status.upper() in {"FAIL", "WARN"}:
478
+ missing = [m for m in missing if not m.startswith("status=")]
458
479
  if missing:
459
480
  code = (
460
481
  "MISSING_V2_FIELDS"
@@ -476,8 +497,21 @@ def validate_ledger_for_phase_close(
476
497
  "code": code,
477
498
  }
478
499
  )
479
- elif is_degraded:
500
+ continue
501
+ if is_degraded:
480
502
  degraded.append(verification)
503
+ continue
504
+ # Under phase_status=FAIL/WARN, a FAIL/NOT_RUN status is acceptable.
505
+ entry_status = str(entry.get("status") or "").upper()
506
+ if entry_status in {"FAIL", "NOT_RUN"}:
507
+ if phase_status.upper() in {"FAIL", "WARN"}:
508
+ fail_status.append(verification)
509
+ else:
510
+ problems.append({
511
+ "verification": verification,
512
+ "missing": [f"status={entry_status}"],
513
+ "code": "VALIDATION_NOT_OK",
514
+ })
481
515
 
482
516
  if problems:
483
517
  return {
@@ -489,6 +523,16 @@ def validate_ledger_for_phase_close(
489
523
  "ledgerPath": str(ledger_path) if ledger_path else None,
490
524
  "detail": "natural-language override is not permitted",
491
525
  }
526
+ if fail_status:
527
+ return {
528
+ "ok": True,
529
+ "code": "LEDGER_OK_FAIL",
530
+ "phase": phase,
531
+ "validated": required,
532
+ "failed": fail_status,
533
+ "degraded": degraded,
534
+ "ledgerPath": str(ledger_path) if ledger_path else None,
535
+ }
492
536
  if degraded:
493
537
  return {
494
538
  "ok": True,
@@ -1169,6 +1213,14 @@ def cmd_begin(args: argparse.Namespace) -> int:
1169
1213
  extra={k: v for k, v in resolved.items() if k not in {"ok", "message"}},
1170
1214
  )
1171
1215
  change_dir = Path(resolved["changeDir"])
1216
+ concurrency_block = hc.check_concurrency_block(project, resolved["changeId"])
1217
+ if concurrency_block is not None:
1218
+ return emit_error(
1219
+ str(concurrency_block.get("code", "CONCURRENCY_BLOCKED")),
1220
+ str(concurrency_block.get("message", "concurrency mode blocked begin")),
1221
+ as_json=as_json,
1222
+ extra={k: v for k, v in concurrency_block.items() if k not in {"ok", "message"}},
1223
+ )
1172
1224
  blocked = foundation_gate_blocks(getattr(args, "task", None), change_dir)
1173
1225
  if blocked:
1174
1226
  return emit_error(
@@ -1407,7 +1459,8 @@ def cmd_close(args: argparse.Namespace) -> int:
1407
1459
  return emit_error("PHASE_CAPSULE_MISMATCH", str(exc), as_json=as_json)
1408
1460
 
1409
1461
  ledger_result = validate_ledger_for_phase_close(
1410
- change_dir, args.phase, policy, execution_root=execution_root
1462
+ change_dir, args.phase, policy, execution_root=execution_root,
1463
+ phase_status=args.status,
1411
1464
  )
1412
1465
  if not ledger_result.get("ok") and args.phase in {"run", "test", "package"}:
1413
1466
  return emit_error(
@@ -422,6 +422,9 @@ class IntegrationTransaction:
422
422
  else:
423
423
  result = hl.record_integration_hashes(
424
424
  ledger_path,
425
+ change_dir=(
426
+ self.project_root / ".harness" / "changes" / self.change_id
427
+ ),
425
428
  repository_id=str(journal["repositoryId"]),
426
429
  merge_final_hash=merge_commit,
427
430
  ci_expected_head=merge_commit,
@@ -621,12 +621,13 @@ def validate_ledger_identity(ledger: dict[str, Any]) -> list[str]:
621
621
  def record_integration_hashes(
622
622
  ledger_path: Path,
623
623
  *,
624
+ change_dir: Path | None = None,
624
625
  repository_id: str,
625
626
  merge_final_hash: str,
626
627
  ci_expected_head: str,
627
628
  remote_head: str,
628
629
  ) -> dict[str, Any]:
629
- """Atomically attach post-push identity to an existing ledger v3."""
630
+ """Atomically attach post-push hashes using the change contract's ledger rules."""
630
631
  path = Path(ledger_path).resolve()
631
632
  if not path.is_file():
632
633
  return {"ok": False, "code": "LEDGER_MISSING", "ledgerPath": str(path)}
@@ -637,15 +638,26 @@ def record_integration_hashes(
637
638
  "ok": False, "code": "LEDGER_INVALID", "ledgerPath": str(path),
638
639
  "message": str(exc),
639
640
  }
641
+ legacy_contract = False
642
+ if change_dir is not None:
643
+ resolved_change_dir = Path(change_dir).resolve()
644
+ if ledger.get("changeName") != resolved_change_dir.name:
645
+ return {
646
+ "ok": False,
647
+ "code": "LEDGER_CHANGE_MISMATCH",
648
+ "ledgerPath": str(path),
649
+ }
650
+ legacy_contract = not _contract_is_v2(resolved_change_dir)
651
+
640
652
  missing = validate_ledger_identity(ledger)
641
- if missing:
653
+ if missing and not legacy_contract:
642
654
  return {
643
655
  "ok": False,
644
656
  "code": "LEDGER_IDENTITY_INVALID",
645
657
  "ledgerPath": str(path),
646
658
  "missing": missing,
647
659
  }
648
- if ledger.get("repositoryId") != repository_id:
660
+ if not missing and ledger.get("repositoryId") != repository_id:
649
661
  return {
650
662
  "ok": False,
651
663
  "code": "LEDGER_REPOSITORY_MISMATCH",
@@ -29,6 +29,7 @@ if hasattr(sys.stderr, "reconfigure"):
29
29
  sys.stderr.reconfigure(encoding="utf-8")
30
30
 
31
31
 
32
+ import harness_change as hc # noqa: E402
32
33
  import harness_profile as hprof # noqa: E402
33
34
 
34
35
 
@@ -69,6 +70,52 @@ def cmd_check(project: Path) -> dict[str, Any]:
69
70
  return result
70
71
 
71
72
 
73
+ def check_concurrency(project: Path) -> dict[str, Any]:
74
+ """Report the configured concurrency mode and active Change landscape.
75
+
76
+ Retro §5.2: preflight must surface concurrencyMode, activeChanges, and
77
+ allowedParallelLevels so agents don't have to guess whether parallel
78
+ Changes are supported.
79
+ """
80
+ mode = hc.read_concurrency_mode(project)
81
+ active = hc.list_active_changes(project)
82
+ shared_conflicts: list[dict[str, Any]] = []
83
+ # In isolated-multi-active, surface any shared-state conflicts (e.g. same
84
+ # worktree path) so callers know isolation is incomplete.
85
+ if mode == "isolated-multi-active" and len(active) > 1:
86
+ seen_worktrees: dict[str, str] = {}
87
+ for entry in active:
88
+ change_dir = Path(entry["path"])
89
+ worktree_meta = change_dir / "meta" / "worktree.json"
90
+ if not worktree_meta.is_file():
91
+ continue
92
+ try:
93
+ meta = json.loads(worktree_meta.read_text(encoding="utf-8"))
94
+ except (OSError, json.JSONDecodeError):
95
+ continue
96
+ wt = meta.get("path") if isinstance(meta, dict) else None
97
+ if isinstance(wt, str) and wt:
98
+ if wt in seen_worktrees:
99
+ shared_conflicts.append({
100
+ "kind": "worktree",
101
+ "path": wt,
102
+ "changes": [seen_worktrees[wt], entry["changeId"]],
103
+ })
104
+ else:
105
+ seen_worktrees[wt] = entry["changeId"]
106
+ levels = ["change-internal"] if mode == "single-active" else [
107
+ "change-internal",
108
+ "multi-change",
109
+ ]
110
+ return {
111
+ "ok": True,
112
+ "concurrencyMode": mode,
113
+ "activeChanges": active,
114
+ "sharedStateConflicts": shared_conflicts,
115
+ "allowedParallelLevels": levels,
116
+ }
117
+
118
+
72
119
  # ---------------------------------------------------------------------------
73
120
  # record-quirk — 适配 v2 commands(source=user override)
74
121
  # ---------------------------------------------------------------------------
@@ -448,9 +448,18 @@ def _validate_existing_manifest(
448
448
  tracked_now = _git(
449
449
  project, "ls-files", "--error-unmatch", "--", rel
450
450
  ).returncode == 0
451
- if item["ignored"] != ignored_now or item["trackedBefore"] != tracked_now:
451
+ checkpointed = not item["trackedBefore"] and tracked_now
452
+ if (
453
+ item["trackedBefore"] != tracked_now and not checkpointed
454
+ ) or (
455
+ item["ignored"] != ignored_now and not checkpointed
456
+ ):
452
457
  return "MANIFEST_INVALID", {}
453
- validated[rel] = dict(item)
458
+ normalized_item = dict(item)
459
+ if checkpointed:
460
+ normalized_item["trackedBefore"] = True
461
+ normalized_item["ignored"] = ignored_now
462
+ validated[rel] = normalized_item
454
463
  return None, validated
455
464
 
456
465
 
@@ -734,11 +743,26 @@ def _stage_locked(
734
743
  error, entries = validator(project_root, manifest, require_files=True)
735
744
  if error:
736
745
  return _result(False, action, error, [])
737
- rels = [
738
- rel
739
- for rel, entry in entries.items()
740
- if not is_v2 or entry.get("commitScope") == "current-change"
741
- ]
746
+ rels: list[str] = []
747
+ for rel, entry in entries.items():
748
+ if is_v2:
749
+ if entry.get("commitScope") == "current-change":
750
+ rels.append(rel)
751
+ continue
752
+ if not entry["trackedBefore"]:
753
+ rels.append(rel)
754
+ continue
755
+ changed = _git(project_root, "diff", "--quiet", "HEAD", "--", rel)
756
+ if changed.returncode == 1:
757
+ rels.append(rel)
758
+ elif changed.returncode != 0:
759
+ return _result(
760
+ False,
761
+ action,
762
+ "GIT_DIFF_FAILED",
763
+ [rel],
764
+ error=changed.stderr.strip(),
765
+ )
742
766
  if not rels:
743
767
  return _result(True, action, "STAGED", [])
744
768
 
@@ -925,9 +949,22 @@ def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
925
949
  if (
926
950
  not isinstance(snapshot, dict)
927
951
  or snapshot.get("schemaVersion") != SCHEMA_VERSION
928
- or snapshot.get("projectRoot") != str(project_root)
929
952
  ):
930
953
  return _result(False, action, "SNAPSHOT_INVALID", [])
954
+ # Execution-root contract (retro §5.10): a snapshot captured against a
955
+ # different project root must fail with EXECUTION_ROOT_MISMATCH before
956
+ # the generic SNAPSHOT_INVALID, so callers can distinguish "wrong root"
957
+ # from "corrupt snapshot".
958
+ snapshot_root = snapshot.get("projectRoot")
959
+ if snapshot_root is not None and snapshot_root != str(project_root):
960
+ return _result(
961
+ False,
962
+ action,
963
+ "EXECUTION_ROOT_MISMATCH",
964
+ [],
965
+ expectedRoot=snapshot_root,
966
+ actualRoot=str(project_root),
967
+ )
931
968
 
932
969
  before_entries = snapshot.get("files")
933
970
  if not isinstance(before_entries, list):
@@ -963,15 +1000,51 @@ def close(project: Path | str, change_dir: Path | str) -> dict[str, Any]:
963
1000
  if before[rel]["sha256"] != digest:
964
1001
  touched.append((rel, "test-updated"))
965
1002
 
966
- recorded: list[str] = []
967
- for rel, reason in touched:
968
- result = record(project_root, change_root, [str(project_root / rel)], reason)
1003
+ for reason in ("tdd-created", "test-updated"):
1004
+ rels = [rel for rel, item_reason in touched if item_reason == reason]
1005
+ if not rels:
1006
+ continue
1007
+ result = record(
1008
+ project_root,
1009
+ change_root,
1010
+ [str(project_root / rel) for rel in rels],
1011
+ reason,
1012
+ )
969
1013
  if not result.get("ok"):
970
1014
  code = result.get("code", "RECORD_FAILED")
971
1015
  if code == "TEST_PATH_NOT_ALLOWED":
972
- return _result(False, action, "UNCLASSIFIABLE_TEST", [rel])
1016
+ return _result(False, action, "UNCLASSIFIABLE_TEST", rels)
973
1017
  return result
974
- recorded.append(rel)
1018
+
1019
+ recorded = [rel for rel, _ in touched]
1020
+
1021
+ # Cross-check (retro §5.10): if the manifest has active entries for this
1022
+ # change but close computed recordedCount=0, the snapshot/manifest/diff
1023
+ # are inconsistent. Fail closed instead of silently returning success.
1024
+ manifest_path = _manifest_path(change_root)
1025
+ if manifest_path is not None and manifest_path.is_file():
1026
+ try:
1027
+ manifest = _read_json(manifest_path)
1028
+ except (OSError, json.JSONDecodeError):
1029
+ manifest = None
1030
+ if isinstance(manifest, dict):
1031
+ manifest_files = manifest.get("files")
1032
+ if isinstance(manifest_files, list):
1033
+ active_entries = [
1034
+ f for f in manifest_files
1035
+ if isinstance(f, dict)
1036
+ and f.get("reason") in ("tdd-created", "test-updated", "stale-test-repair")
1037
+ ]
1038
+ if active_entries and not recorded:
1039
+ return _result(
1040
+ False,
1041
+ action,
1042
+ "MANIFEST_DIFF_HASH_MISMATCH",
1043
+ [],
1044
+ manifestEntries=len(active_entries),
1045
+ recordedCount=0,
1046
+ detail="manifest has active entries but close computed 0 recorded tests",
1047
+ )
975
1048
 
976
1049
  return _result(
977
1050
  True,
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "cursor",
5
- "coreHash": "2c6789d0b3bf31ca"
5
+ "coreHash": "db36b7a4a36352f5"
6
6
  }
@@ -3,7 +3,7 @@ name: harness-archive
3
3
  description: 归档所有变更产出(计划/测试报告/审查/SQL/API文档)到
4
4
  .harness/archive/,含归档元数据和可视化最终总结。使用场景:归档、完成归档、收尾、打包产出物
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
7
7
  # harness-archive — 变更归档
8
8
 
9
9
  ## Purpose
@@ -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=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; 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=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
7
7
  # harness-knowledge-ingest
8
8
 
9
9
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -3,7 +3,7 @@ name: harness-knowledge-query
3
3
  description: 根据新需求、设计任务、代码修改请求或问题排查,在规划和编码前查询 .harness/knowledge
4
4
  历史上下文。适用场景:query knowledge、查找历史需求、根据归档理解需求、继续之前类似开发、生成需求上下文包。
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
7
7
  # harness-knowledge-query
8
8
 
9
9
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -2,7 +2,7 @@
2
2
  name: harness-plan
3
3
  description: 将需求转化为设计文档+实施计划+完整测试场景表,必须在编码前完成。使用场景:需求分析、feature plan、技术方案设计、实现方案规划
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
6
6
  # harness-plan — 需求规划
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-review
3
3
  description: 6维度代码审查(架构/安全/规范/兼容/测试/性能),对照项目规则(见
4
4
  .harness/context-index.json)和测试场景表,在隔离上下文运行。使用场景:代码审查、提交前检查、合并评审
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
7
7
  # harness-review — 代码审查
8
8
 
9
9
  ## Purpose
@@ -2,7 +2,7 @@
2
2
  name: harness-run
3
3
  description: 按变更簇执行 TDD 编码循环(RED→GREEN→REFACTOR→编译验证),逐变更簇实现计划中的任务。使用场景:开始编码、实现功能、写代码、TDD 编码
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
6
6
  # harness-run — 需求编码
7
7
 
8
8
  ## Purpose
@@ -3,7 +3,7 @@ name: harness-submit
3
3
  description: 最终提交封装:验证→中文 commit→提交/推送;worktree 模式含 --no-ff
4
4
  合并回主分支。使用场景:提交代码、commit、push、合并分支、merge to main、完成开发
5
5
  ---
6
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
6
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
7
7
  # harness-submit — 最终提交(含 worktree 合并)
8
8
 
9
9
  ## Purpose
@@ -106,7 +106,7 @@ commit+push / 仅本地 commit / 取消;**不调用 Superpowers 呈现 PR/丢
106
106
 
107
107
  见 `checklist.md` 与 `../protocols/ledger-protocol.md`、`../protocols/submit-protocol.md`。
108
108
 
109
- test-tracking manifest 是 ignored test 的唯一强制暂存授权:只允许 `harness_test_guard.py stage` 暂存 manifest 中已校验路径,禁止 `git add -f .`、目录级 `git add -f` 或全局修改 `.gitignore`。worktree commit 前须确认 manifest 路径全部进入 cached diff;合并回主分支后、删除 worktree 前须确认这些路径已由目标 commit 跟踪,否则停止清理。
109
+ test-tracking manifest 是 ignored test 的唯一强制暂存授权:只允许 `harness_test_guard.py stage` 暂存 manifest 中已校验且相对 `HEAD` 仍有差异的路径,禁止 `git add -f .`、目录级 `git add -f` 或全局修改 `.gitignore`。worktree commit 前须确认 guard 返回的 `files` 全部进入 cached diff;manifest 中已由 `HEAD` 跟踪且未变化的历史条目允许保留而不进入本次 cached diff。合并回主分支后、删除 worktree 前仍须确认 manifest 路径已由目标 commit 跟踪,否则停止清理。
110
110
 
111
111
  ### 四、worktree 合并硬规则
112
112
 
@@ -119,7 +119,7 @@ python <skills-root>/scripts/harness_ledger.py can-reuse `
119
119
 
120
120
  - [ ] 检查 `.harness/changes/<change-name>/evidence/test-tracking.json` 是否存在
121
121
  - [ ] 存在时先执行 `python <skills-root>/scripts/harness_test_guard.py stage --project . --change-dir ".harness/changes/<change-name>" --json`;任一 manifest/path/hash/index 校验失败立即停止
122
- - [ ] 用 `git diff --cached --name-only` 验证 manifest 中每个精确路径均已暂存,且没有 guard 引入的额外路径
122
+ - [ ] 用 `git diff --cached --name-only` 验证 guard 响应 `files` 中的每个精确路径均已暂存,且没有 guard 引入的额外路径;manifest 中已由 `HEAD` 跟踪且未变化的历史条目无需进入本次 cached diff
123
123
  - [ ] 无 manifest 时禁止使用 `git add -f`;有 manifest 时也只允许 guard 内部的 exact force-add,**禁止全局 force-add**、目录级 force-add 和全局修改 `.gitignore`
124
124
 
125
125
  提交前**强制检查 `.gitignore`** 是否包含 `.harness/`:
@@ -2,7 +2,7 @@
2
2
  name: harness-sync
3
3
  description: 检查并更新项目AI元数据(CodeGraph索引、harness-codebase-map分析、CLAUDE.md、AGENTS.md、.harness/完整性),确保Claude对代码库的理解与最新代码一致。当用户说'同步/更新索引/刷新元数据/检查一致性'时使用
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
6
6
  # harness-sync — 元数据同步
7
7
 
8
8
  ## Purpose
@@ -2,7 +2,7 @@
2
2
  name: harness-test
3
3
  description: 测试执行:读取场景表,执行单元测试+API接口测试+数据兼容验证,输出测试报告。当用户说'跑测试/验证/跑用例/接口测试/单元测试'时使用
4
4
  ---
5
- <!-- generated by harness_deploy.py; core=2c6789d0b3bf31ca; overlay=none; agent=cursor; do not edit -->
5
+ <!-- generated by harness_deploy.py; core=db36b7a4a36352f5; overlay=none; agent=cursor; do not edit -->
6
6
  # harness-test — 测试执行
7
7
 
8
8
  ## Purpose