@hunter-harness/workflow-harness 0.2.14 → 0.2.15

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 (166) 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-run/reference.md +3 -1
  10. package/harness/bundles/general/claude-code/harness-submit/SKILL.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 +80 -0
  14. package/harness/bundles/general/claude-code/scripts/harness_change.py +256 -16
  15. package/harness/bundles/general/claude-code/scripts/harness_events.py +14 -0
  16. package/harness/bundles/general/claude-code/scripts/harness_integration.py +64 -7
  17. package/harness/bundles/general/claude-code/scripts/harness_plan_finalize.py +85 -0
  18. package/harness/bundles/general/claude-code/scripts/harness_preflight.py +146 -77
  19. package/harness/bundles/general/claude-code/scripts/harness_state.py +12 -4
  20. package/harness/bundles/general/codebuddy/.harness-build.json +1 -1
  21. package/harness/bundles/general/codebuddy/harness-archive/SKILL.md +1 -1
  22. package/harness/bundles/general/codebuddy/harness-codebase-map/SKILL.md +1 -1
  23. package/harness/bundles/general/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  24. package/harness/bundles/general/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  25. package/harness/bundles/general/codebuddy/harness-plan/SKILL.md +1 -1
  26. package/harness/bundles/general/codebuddy/harness-review/SKILL.md +1 -1
  27. package/harness/bundles/general/codebuddy/harness-run/SKILL.md +1 -1
  28. package/harness/bundles/general/codebuddy/harness-run/reference.md +3 -1
  29. package/harness/bundles/general/codebuddy/harness-submit/SKILL.md +1 -1
  30. package/harness/bundles/general/codebuddy/harness-sync/SKILL.md +1 -1
  31. package/harness/bundles/general/codebuddy/harness-test/SKILL.md +1 -1
  32. package/harness/bundles/general/codebuddy/scripts/harness_archive.py +80 -0
  33. package/harness/bundles/general/codebuddy/scripts/harness_change.py +256 -16
  34. package/harness/bundles/general/codebuddy/scripts/harness_events.py +14 -0
  35. package/harness/bundles/general/codebuddy/scripts/harness_integration.py +64 -7
  36. package/harness/bundles/general/codebuddy/scripts/harness_plan_finalize.py +85 -0
  37. package/harness/bundles/general/codebuddy/scripts/harness_preflight.py +146 -77
  38. package/harness/bundles/general/codebuddy/scripts/harness_state.py +12 -4
  39. package/harness/bundles/general/codex/.harness-build.json +1 -1
  40. package/harness/bundles/general/codex/harness-archive/SKILL.md +1 -1
  41. package/harness/bundles/general/codex/harness-codebase-map/SKILL.md +1 -1
  42. package/harness/bundles/general/codex/harness-knowledge-ingest/SKILL.md +1 -1
  43. package/harness/bundles/general/codex/harness-knowledge-query/SKILL.md +1 -1
  44. package/harness/bundles/general/codex/harness-plan/SKILL.md +1 -1
  45. package/harness/bundles/general/codex/harness-review/SKILL.md +1 -1
  46. package/harness/bundles/general/codex/harness-run/SKILL.md +1 -1
  47. package/harness/bundles/general/codex/harness-run/reference.md +3 -1
  48. package/harness/bundles/general/codex/harness-submit/SKILL.md +1 -1
  49. package/harness/bundles/general/codex/harness-sync/SKILL.md +1 -1
  50. package/harness/bundles/general/codex/harness-test/SKILL.md +1 -1
  51. package/harness/bundles/general/codex/scripts/harness_archive.py +80 -0
  52. package/harness/bundles/general/codex/scripts/harness_change.py +256 -16
  53. package/harness/bundles/general/codex/scripts/harness_events.py +14 -0
  54. package/harness/bundles/general/codex/scripts/harness_integration.py +64 -7
  55. package/harness/bundles/general/codex/scripts/harness_plan_finalize.py +85 -0
  56. package/harness/bundles/general/codex/scripts/harness_preflight.py +146 -77
  57. package/harness/bundles/general/codex/scripts/harness_state.py +12 -4
  58. package/harness/bundles/general/cursor/.harness-build.json +1 -1
  59. package/harness/bundles/general/cursor/harness-archive/SKILL.md +1 -1
  60. package/harness/bundles/general/cursor/harness-codebase-map/SKILL.md +1 -1
  61. package/harness/bundles/general/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  62. package/harness/bundles/general/cursor/harness-knowledge-query/SKILL.md +1 -1
  63. package/harness/bundles/general/cursor/harness-plan/SKILL.md +1 -1
  64. package/harness/bundles/general/cursor/harness-review/SKILL.md +1 -1
  65. package/harness/bundles/general/cursor/harness-run/SKILL.md +1 -1
  66. package/harness/bundles/general/cursor/harness-run/reference.md +3 -1
  67. package/harness/bundles/general/cursor/harness-submit/SKILL.md +1 -1
  68. package/harness/bundles/general/cursor/harness-sync/SKILL.md +1 -1
  69. package/harness/bundles/general/cursor/harness-test/SKILL.md +1 -1
  70. package/harness/bundles/general/cursor/scripts/harness_archive.py +80 -0
  71. package/harness/bundles/general/cursor/scripts/harness_change.py +256 -16
  72. package/harness/bundles/general/cursor/scripts/harness_events.py +14 -0
  73. package/harness/bundles/general/cursor/scripts/harness_integration.py +64 -7
  74. package/harness/bundles/general/cursor/scripts/harness_plan_finalize.py +85 -0
  75. package/harness/bundles/general/cursor/scripts/harness_preflight.py +146 -77
  76. package/harness/bundles/general/cursor/scripts/harness_state.py +12 -4
  77. package/harness/bundles/java/claude-code/.harness-build.json +1 -1
  78. package/harness/bundles/java/claude-code/harness-apidoc/SKILL.md +1 -1
  79. package/harness/bundles/java/claude-code/harness-archive/SKILL.md +1 -1
  80. package/harness/bundles/java/claude-code/harness-codebase-map/SKILL.md +1 -1
  81. package/harness/bundles/java/claude-code/harness-knowledge-ingest/SKILL.md +1 -1
  82. package/harness/bundles/java/claude-code/harness-knowledge-query/SKILL.md +1 -1
  83. package/harness/bundles/java/claude-code/harness-package/SKILL.md +1 -1
  84. package/harness/bundles/java/claude-code/harness-plan/SKILL.md +1 -1
  85. package/harness/bundles/java/claude-code/harness-review/SKILL.md +1 -1
  86. package/harness/bundles/java/claude-code/harness-run/SKILL.md +1 -1
  87. package/harness/bundles/java/claude-code/harness-submit/SKILL.md +1 -1
  88. package/harness/bundles/java/claude-code/harness-sync/SKILL.md +1 -1
  89. package/harness/bundles/java/claude-code/harness-test/SKILL.md +1 -1
  90. package/harness/bundles/java/claude-code/scripts/harness_archive.py +80 -0
  91. package/harness/bundles/java/claude-code/scripts/harness_change.py +256 -16
  92. package/harness/bundles/java/claude-code/scripts/harness_events.py +14 -0
  93. package/harness/bundles/java/claude-code/scripts/harness_integration.py +64 -7
  94. package/harness/bundles/java/claude-code/scripts/harness_plan_finalize.py +85 -0
  95. package/harness/bundles/java/claude-code/scripts/harness_preflight.py +146 -77
  96. package/harness/bundles/java/claude-code/scripts/harness_state.py +12 -4
  97. package/harness/bundles/java/codebuddy/.harness-build.json +1 -1
  98. package/harness/bundles/java/codebuddy/harness-apidoc/SKILL.md +1 -1
  99. package/harness/bundles/java/codebuddy/harness-archive/SKILL.md +1 -1
  100. package/harness/bundles/java/codebuddy/harness-codebase-map/SKILL.md +1 -1
  101. package/harness/bundles/java/codebuddy/harness-knowledge-ingest/SKILL.md +1 -1
  102. package/harness/bundles/java/codebuddy/harness-knowledge-query/SKILL.md +1 -1
  103. package/harness/bundles/java/codebuddy/harness-package/SKILL.md +1 -1
  104. package/harness/bundles/java/codebuddy/harness-plan/SKILL.md +1 -1
  105. package/harness/bundles/java/codebuddy/harness-review/SKILL.md +1 -1
  106. package/harness/bundles/java/codebuddy/harness-run/SKILL.md +1 -1
  107. package/harness/bundles/java/codebuddy/harness-submit/SKILL.md +1 -1
  108. package/harness/bundles/java/codebuddy/harness-sync/SKILL.md +1 -1
  109. package/harness/bundles/java/codebuddy/harness-test/SKILL.md +1 -1
  110. package/harness/bundles/java/codebuddy/scripts/harness_archive.py +80 -0
  111. package/harness/bundles/java/codebuddy/scripts/harness_change.py +256 -16
  112. package/harness/bundles/java/codebuddy/scripts/harness_events.py +14 -0
  113. package/harness/bundles/java/codebuddy/scripts/harness_integration.py +64 -7
  114. package/harness/bundles/java/codebuddy/scripts/harness_plan_finalize.py +85 -0
  115. package/harness/bundles/java/codebuddy/scripts/harness_preflight.py +146 -77
  116. package/harness/bundles/java/codebuddy/scripts/harness_state.py +12 -4
  117. package/harness/bundles/java/codex/.harness-build.json +1 -1
  118. package/harness/bundles/java/codex/harness-apidoc/SKILL.md +1 -1
  119. package/harness/bundles/java/codex/harness-archive/SKILL.md +1 -1
  120. package/harness/bundles/java/codex/harness-codebase-map/SKILL.md +1 -1
  121. package/harness/bundles/java/codex/harness-knowledge-ingest/SKILL.md +1 -1
  122. package/harness/bundles/java/codex/harness-knowledge-query/SKILL.md +1 -1
  123. package/harness/bundles/java/codex/harness-package/SKILL.md +1 -1
  124. package/harness/bundles/java/codex/harness-plan/SKILL.md +1 -1
  125. package/harness/bundles/java/codex/harness-review/SKILL.md +1 -1
  126. package/harness/bundles/java/codex/harness-run/SKILL.md +1 -1
  127. package/harness/bundles/java/codex/harness-submit/SKILL.md +1 -1
  128. package/harness/bundles/java/codex/harness-sync/SKILL.md +1 -1
  129. package/harness/bundles/java/codex/harness-test/SKILL.md +1 -1
  130. package/harness/bundles/java/codex/scripts/harness_archive.py +80 -0
  131. package/harness/bundles/java/codex/scripts/harness_change.py +256 -16
  132. package/harness/bundles/java/codex/scripts/harness_events.py +14 -0
  133. package/harness/bundles/java/codex/scripts/harness_integration.py +64 -7
  134. package/harness/bundles/java/codex/scripts/harness_plan_finalize.py +85 -0
  135. package/harness/bundles/java/codex/scripts/harness_preflight.py +146 -77
  136. package/harness/bundles/java/codex/scripts/harness_state.py +12 -4
  137. package/harness/bundles/java/cursor/.harness-build.json +1 -1
  138. package/harness/bundles/java/cursor/harness-apidoc/SKILL.md +1 -1
  139. package/harness/bundles/java/cursor/harness-archive/SKILL.md +1 -1
  140. package/harness/bundles/java/cursor/harness-codebase-map/SKILL.md +1 -1
  141. package/harness/bundles/java/cursor/harness-knowledge-ingest/SKILL.md +1 -1
  142. package/harness/bundles/java/cursor/harness-knowledge-query/SKILL.md +1 -1
  143. package/harness/bundles/java/cursor/harness-package/SKILL.md +1 -1
  144. package/harness/bundles/java/cursor/harness-plan/SKILL.md +1 -1
  145. package/harness/bundles/java/cursor/harness-review/SKILL.md +1 -1
  146. package/harness/bundles/java/cursor/harness-run/SKILL.md +1 -1
  147. package/harness/bundles/java/cursor/harness-submit/SKILL.md +1 -1
  148. package/harness/bundles/java/cursor/harness-sync/SKILL.md +1 -1
  149. package/harness/bundles/java/cursor/harness-test/SKILL.md +1 -1
  150. package/harness/bundles/java/cursor/scripts/harness_archive.py +80 -0
  151. package/harness/bundles/java/cursor/scripts/harness_change.py +256 -16
  152. package/harness/bundles/java/cursor/scripts/harness_events.py +14 -0
  153. package/harness/bundles/java/cursor/scripts/harness_integration.py +64 -7
  154. package/harness/bundles/java/cursor/scripts/harness_plan_finalize.py +85 -0
  155. package/harness/bundles/java/cursor/scripts/harness_preflight.py +146 -77
  156. package/harness/bundles/java/cursor/scripts/harness_state.py +12 -4
  157. package/harness/manifests/general/claude-code.json +19 -19
  158. package/harness/manifests/general/codebuddy.json +19 -19
  159. package/harness/manifests/general/codex.json +19 -19
  160. package/harness/manifests/general/cursor.json +19 -19
  161. package/harness/manifests/java/claude-code.json +20 -20
  162. package/harness/manifests/java/codebuddy.json +20 -20
  163. package/harness/manifests/java/codex.json +20 -20
  164. package/harness/manifests/java/cursor.json +20 -20
  165. package/hunter-workflow-family.json +1 -1
  166. package/package.json +1 -1
@@ -2,5 +2,5 @@
2
2
  "schemaVersion": 1,
3
3
  "overlay": "none",
4
4
  "agent": "claude-code",
5
- "coreHash": "50a141f3d56ac500"
5
+ "coreHash": "6232f66fbf691292"
6
6
  }
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-archive — 变更归档
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-codebase-map — 代码库地图
25
25
 
26
26
  ## Purpose
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-ingest
25
25
 
26
26
  从 `.harness/archive/**/reports/final/summary-data.json` 抽取项目知识,生成并维护本地 `.harness/knowledge/` 索引。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-knowledge-query
25
25
 
26
26
  并行 change 中查询必须传 `--change <change-id>`;成功后以该 change 的 `meta/knowledge-context.json` 为稳定指针。全局 `context-packs/latest.json` 仅向后兼容,不得用于判断多个 active change 的上下文归属。
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-plan — 需求规划
25
25
 
26
26
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(curl *)
22
22
  - Bash(codegraph *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-review — 代码审查
26
26
 
27
27
  ## Purpose
@@ -21,7 +21,7 @@ disallowed-tools:
21
21
  - Bash(curl *)
22
22
  - Bash(codegraph *)
23
23
  ---
24
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
24
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
25
25
  # harness-run — 需求编码
26
26
 
27
27
  ## Purpose
@@ -52,7 +52,7 @@ requested=true + path missing
52
52
  先执行 runtime doctor,并从 `meta/runtime.json` 读取 adapter 的 `path`、`branch` 以及 PowerShell `argvPrefix`;不要依赖裸 `python`/`powershell.exe`,也不要硬编码 Claude 路径。
53
53
 
54
54
  ```powershell
55
- & <powershell-argv-prefix> -Command "git worktree add -- '<adapter-worktree-path>' -b '<adapter-branch>'"
55
+ & <powershell-argv-prefix> -Command "git worktree add -b '<adapter-branch>' -- '<adapter-worktree-path>'"
56
56
  ```
57
57
 
58
58
  如果分支已存在:
@@ -61,6 +61,8 @@ requested=true + path missing
61
61
  & <powershell-argv-prefix> -Command "git worktree add -- '<adapter-worktree-path>' '<adapter-branch>'"
62
62
  ```
63
63
 
64
+ > ⚠️ **argv 顺序**(复盘 §5.11):`-b <branch>` 必须在 `--` 之前;`git worktree add -- <path> -b <branch>` 中 `--` 后的 `-b` 会被 Git 视为位置参数,命令直接失败。等价 argv 数组:`["git","worktree","add","-b",branch,"--",path]`。
65
+
64
66
  验证:
65
67
 
66
68
  ```powershell
@@ -20,7 +20,7 @@ disallowed-tools:
20
20
  - Bash(awk *)
21
21
  - Bash(curl *)
22
22
  ---
23
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
23
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
24
24
  # harness-submit — 最终提交(含 worktree 合并)
25
25
 
26
26
  ## Purpose
@@ -19,7 +19,7 @@ disallowed-tools:
19
19
  - Bash(awk *)
20
20
  - Bash(curl *)
21
21
  ---
22
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
22
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
23
23
  # harness-sync — 元数据同步
24
24
 
25
25
  ## Purpose
@@ -22,7 +22,7 @@ disallowed-tools:
22
22
  - Bash(node *)
23
23
  - Bash(codegraph *)
24
24
  ---
25
- <!-- generated by harness_deploy.py; core=50a141f3d56ac500; overlay=none; agent=claude-code; do not edit -->
25
+ <!-- generated by harness_deploy.py; core=8fbcf97a5a4408df; overlay=none; agent=claude-code; do not edit -->
26
26
  # harness-test — 测试执行
27
27
 
28
28
  ## Purpose
@@ -683,6 +683,34 @@ def check_status(
683
683
  else:
684
684
  blockers.append(tier_issue)
685
685
 
686
+ # --- artifact preflight (retro §5.31) ---
687
+ # Classify artifact events before destructive finalize: informational (OK),
688
+ # canonicalizable (same-change repo-relative path, warning), or blocking
689
+ # (cross-change/absolute/escaping path, fail closed).
690
+ try:
691
+ preflight = artifact_preflight(change_dir)
692
+ except Exception as exc: # noqa: BLE001 — preflight must not crash status
693
+ preflight = {"ok": False, "items": [], "blocking": [], "error": str(exc)}
694
+ checks["artifact_preflight"] = preflight
695
+ for item in preflight.get("blocking") or []:
696
+ blockers.append({
697
+ "code": "artifact-path-blocking",
698
+ "message": (
699
+ f"artifact event {item.get('eventId', '')} path "
700
+ f"{item.get('path', '')}: {item.get('reason', 'blocking')}"
701
+ ),
702
+ })
703
+ for item in preflight.get("items") or []:
704
+ if item.get("category") == "canonicalizable":
705
+ warnings.append({
706
+ "code": "artifact-path-canonicalizable",
707
+ "message": (
708
+ f"artifact event {item.get('eventId', '')} path "
709
+ f"{item.get('path', '')} is repo-relative; "
710
+ f"canonical={item.get('canonicalPath', '')}"
711
+ ),
712
+ })
713
+
686
714
  archivable = len(blockers) == 0
687
715
  return {
688
716
  "ok": True,
@@ -3231,6 +3259,36 @@ def cmd_finalize(
3231
3259
  note="review missing on full tier (allowed by user)",
3232
3260
  )
3233
3261
 
3262
+ # --- 2c. artifact preflight (retro §5.31) ---
3263
+ # Classify artifact events before freeze: blocking paths must fail closed
3264
+ # before the staged operation commits a phase.end.
3265
+ try:
3266
+ preflight = artifact_preflight(work_dir)
3267
+ except Exception as exc: # noqa: BLE001 — preflight must not crash finalize
3268
+ preflight = {"ok": False, "items": [], "blocking": [], "error": str(exc)}
3269
+ payload["steps"]["artifact_preflight"] = {
3270
+ "ok": bool(preflight.get("ok")),
3271
+ "blockingCount": len(preflight.get("blocking") or []),
3272
+ "itemsCount": len(preflight.get("items") or []),
3273
+ }
3274
+ if preflight.get("blocking"):
3275
+ payload["error"] = (
3276
+ f"artifact preflight blocking: "
3277
+ f"{len(preflight['blocking'])} path(s) cannot be archived"
3278
+ )
3279
+ payload["issues"] = [
3280
+ {
3281
+ "code": "ARTIFACT_PATH_BLOCKING",
3282
+ "severity": "error",
3283
+ "message": f"{item.get('path', '')}: {item.get('reason', 'blocking')}",
3284
+ }
3285
+ for item in preflight["blocking"]
3286
+ ]
3287
+ _restore_finalize_failure()
3288
+ payload["warnings"] = warnings
3289
+ payload["ok"] = False
3290
+ return 1, payload
3291
+
3234
3292
  # --- 3. candidate phase.end + freeze cutoff (RET-19 freeze-first) ---
3235
3293
  # This terminal exists only inside the isolated operation staging tree. It
3236
3294
  # becomes authoritative atomically at publish; validation failure discards
@@ -3293,6 +3351,28 @@ def cmd_finalize(
3293
3351
  payload["ok"] = False
3294
3352
  return 1, payload
3295
3353
 
3354
+ # --- 5b. report adequacy (retro §5.32) ---
3355
+ # Validate that the summary is factually complete: no diff=0 with non-empty
3356
+ # commit, no missing typed metrics despite test reports, no stageStatus
3357
+ # contradictions. Fail closed rather than archiving an incomplete summary.
3358
+ adequacy_result = validate_report_adequacy(summary)
3359
+ payload["steps"]["report_adequacy"] = adequacy_result
3360
+ summary.setdefault("reportPipeline", {})["reportAdequacy"] = {
3361
+ "ok": bool(adequacy_result.get("ok")),
3362
+ "issues": adequacy_result.get("issues") or [],
3363
+ }
3364
+ try:
3365
+ write_json(summary_path, summary)
3366
+ except OSError as exc:
3367
+ warnings.append(f"could not write reportAdequacy: {exc}")
3368
+ if not adequacy_result.get("ok"):
3369
+ payload["issues"] = adequacy_result.get("issues") or []
3370
+ payload["error"] = "report adequacy failed; staged operation discarded"
3371
+ _restore_finalize_failure()
3372
+ payload["warnings"] = warnings
3373
+ payload["ok"] = False
3374
+ return 1, payload
3375
+
3296
3376
  # --- 6. render (Node, else Python fallback) ---
3297
3377
  render_result = render_final_summary(work_dir, summary_path)
3298
3378
  payload["steps"]["render"] = render_result
@@ -590,7 +590,9 @@ def lease_port(
590
590
  }
591
591
  for port in range(start, end + 1):
592
592
  if port not in used:
593
+ lease_id = str(uuid.uuid4())
593
594
  entry = {
595
+ "leaseId": lease_id,
594
596
  "changeId": change_id,
595
597
  "runId": run_id,
596
598
  "pid": os.getpid(),
@@ -603,7 +605,7 @@ def lease_port(
603
605
  leases.append(entry)
604
606
  registry["leases"] = leases
605
607
  _write_json(registry_path, registry)
606
- return {"ok": True, "code": "PORT_LEASED", "port": port, "lease": entry}
608
+ return {"ok": True, "code": "PORT_LEASED", "port": port, "leaseId": lease_id, "lease": entry}
607
609
  return {
608
610
  "ok": False,
609
611
  "code": "PORT_RANGE_EXHAUSTED",
@@ -616,7 +618,16 @@ def release_port(
616
618
  *,
617
619
  change_id: str,
618
620
  run_id: str,
621
+ port: int | None = None,
622
+ lease_id: str | None = None,
619
623
  ) -> dict[str, Any]:
624
+ """Release port leases by subset (retro §5.16).
625
+
626
+ - `--lease-id`: release only the matching leaseId.
627
+ - `--port`: release only the matching port (must match changeId+runId).
628
+ - Neither: release all leases matching (changeId, runId) — does NOT require
629
+ all leases under changeId to have the same runId.
630
+ """
620
631
  registry_path = project_root / PORTS_REL / "registry.json"
621
632
  if not registry_path.is_file():
622
633
  return {"ok": True, "code": "PORT_LEASE_ABSENT", "changeId": change_id}
@@ -628,27 +639,84 @@ def release_port(
628
639
  leases = registry.get("leases") if isinstance(registry, dict) else None
629
640
  if not isinstance(leases, list):
630
641
  return {"ok": False, "code": "PORT_REGISTRY_INVALID", "message": "leases is not a list"}
631
- owned = [
632
- item for item in leases
633
- if isinstance(item, dict) and str(item.get("changeId")) == change_id
634
- ]
635
- if not owned:
642
+
643
+ # Select the subset to release based on lease_id, port, or (changeId, runId)
644
+ if lease_id is not None:
645
+ to_release = [
646
+ item for item in leases
647
+ if isinstance(item, dict) and str(item.get("leaseId")) == lease_id
648
+ ]
649
+ # Validate that the lease belongs to this change
650
+ for item in to_release:
651
+ if str(item.get("changeId")) != change_id:
652
+ return {
653
+ "ok": False,
654
+ "code": "PORT_LEASE_OWNER_MISMATCH",
655
+ "message": "leaseId does not belong to this change",
656
+ "holder": item,
657
+ }
658
+ elif port is not None:
659
+ to_release = [
660
+ item for item in leases
661
+ if isinstance(item, dict)
662
+ and item.get("port") == port
663
+ and str(item.get("changeId")) == change_id
664
+ ]
665
+ # Validate runId matches
666
+ for item in to_release:
667
+ if str(item.get("runId")) != run_id:
668
+ return {
669
+ "ok": False,
670
+ "code": "PORT_LEASE_OWNER_MISMATCH",
671
+ "message": f"port {port} owned by different runId",
672
+ "holder": item,
673
+ "conflictingOwners": [
674
+ {"runId": str(i.get("runId")), "port": i.get("port")}
675
+ for i in leases
676
+ if isinstance(i, dict)
677
+ and i.get("port") == port
678
+ and str(i.get("changeId")) == change_id
679
+ ],
680
+ }
681
+ else:
682
+ # Release all matching (changeId, runId) — subset release
683
+ to_release = [
684
+ item for item in leases
685
+ if isinstance(item, dict)
686
+ and str(item.get("changeId")) == change_id
687
+ and str(item.get("runId")) == run_id
688
+ ]
689
+
690
+ if not to_release:
691
+ # Check if there are other leases under this changeId (different runId)
692
+ other_owned = [
693
+ item for item in leases
694
+ if isinstance(item, dict)
695
+ and str(item.get("changeId")) == change_id
696
+ and str(item.get("runId")) != run_id
697
+ ]
698
+ if other_owned:
699
+ return {
700
+ "ok": False,
701
+ "code": "PORT_LEASE_OWNER_MISMATCH",
702
+ "message": "no matching leases for this runId; other runIds exist under this changeId",
703
+ "conflictingOwners": [
704
+ {"runId": str(i.get("runId")), "port": i.get("port"), "leaseId": i.get("leaseId")}
705
+ for i in other_owned
706
+ ],
707
+ }
636
708
  return {"ok": True, "code": "PORT_LEASE_ABSENT", "changeId": change_id}
637
- if any(str(item.get("runId")) != run_id for item in owned):
638
- return {
639
- "ok": False,
640
- "code": "PORT_LEASE_OWNER_MISMATCH",
641
- "message": "run id does not match port lease owner",
642
- "holder": owned[0],
643
- }
644
- released_ports = [item.get("port") for item in owned]
645
- registry["leases"] = [item for item in leases if item not in owned]
709
+
710
+ released_ports = [item.get("port") for item in to_release]
711
+ released_ids = [item.get("leaseId") for item in to_release]
712
+ registry["leases"] = [item for item in leases if item not in to_release]
646
713
  _write_json(registry_path, registry)
647
714
  return {
648
715
  "ok": True,
649
716
  "code": "PORT_LEASE_RELEASED",
650
717
  "changeId": change_id,
651
718
  "ports": released_ports,
719
+ "leaseIds": released_ids,
652
720
  }
653
721
 
654
722
 
@@ -848,7 +916,13 @@ def cmd_lease_port(args: argparse.Namespace) -> int:
848
916
 
849
917
  def cmd_release_port(args: argparse.Namespace) -> int:
850
918
  project = resolve_main_project_root()
851
- payload = release_port(project, change_id=args.change, run_id=args.run_id)
919
+ payload = release_port(
920
+ project,
921
+ change_id=args.change,
922
+ run_id=args.run_id,
923
+ port=getattr(args, "port", None),
924
+ lease_id=getattr(args, "lease_id", None),
925
+ )
852
926
  if payload.get("ok"):
853
927
  emit(payload, as_json=bool(args.json))
854
928
  return 0
@@ -881,6 +955,161 @@ def cmd_integration_lock(args: argparse.Namespace) -> int:
881
955
  )
882
956
 
883
957
 
958
+ def _ensure_change_identity(change_dir: Path) -> dict[str, str]:
959
+ """Ensure meta/change-identity.json exists with a stable UUID (retro §5.5).
960
+
961
+ Returns the identity dict with changeUuid and changeName.
962
+ """
963
+ identity_path = change_dir / "meta" / "change-identity.json"
964
+ if identity_path.is_file():
965
+ try:
966
+ data = json.loads(identity_path.read_text(encoding="utf-8-sig"))
967
+ if isinstance(data, dict) and data.get("changeUuid"):
968
+ return data
969
+ except (OSError, json.JSONDecodeError):
970
+ pass
971
+ # Generate new identity
972
+ identity = {
973
+ "schemaVersion": 1,
974
+ "changeUuid": str(uuid.uuid4()),
975
+ "changeName": change_dir.name,
976
+ "createdAt": now_iso(),
977
+ }
978
+ identity_path.parent.mkdir(parents=True, exist_ok=True)
979
+ identity_path.write_text(
980
+ json.dumps(identity, ensure_ascii=False, indent=2) + "\n",
981
+ encoding="utf-8",
982
+ newline="\n",
983
+ )
984
+ return identity
985
+
986
+
987
+ def cmd_rename(args: argparse.Namespace) -> int:
988
+ """Rename a change atomically: directory, pointers, worktree, identity (retro §5.5).
989
+
990
+ Appends a change.rename event; does not rewrite history.
991
+ """
992
+ project = resolve_main_project_root()
993
+ changes_root = project / ".harness" / "changes"
994
+ old_dir = changes_root / args.change
995
+ new_dir = changes_root / args.to
996
+
997
+ if not old_dir.is_dir():
998
+ return emit_error(
999
+ "CHANGE_NOT_FOUND",
1000
+ f"change directory not found: {old_dir}",
1001
+ as_json=bool(args.json),
1002
+ )
1003
+ if new_dir.exists():
1004
+ return emit_error(
1005
+ "CHANGE_ALREADY_EXISTS",
1006
+ f"target change directory already exists: {new_dir}",
1007
+ as_json=bool(args.json),
1008
+ )
1009
+
1010
+ # Ensure identity exists before rename
1011
+ identity = _ensure_change_identity(old_dir)
1012
+ old_uuid = identity.get("changeUuid", "")
1013
+
1014
+ # Rename directory
1015
+ import shutil
1016
+ shutil.move(str(old_dir), str(new_dir))
1017
+
1018
+ # Update knowledge-context.json.changeId if present
1019
+ kc_path = new_dir / "meta" / "knowledge-context.json"
1020
+ if kc_path.is_file():
1021
+ try:
1022
+ kc = json.loads(kc_path.read_text(encoding="utf-8-sig"))
1023
+ if isinstance(kc, dict):
1024
+ kc["changeId"] = args.to
1025
+ kc["changeUuid"] = old_uuid
1026
+ kc_path.write_text(
1027
+ json.dumps(kc, ensure_ascii=False, indent=2) + "\n",
1028
+ encoding="utf-8",
1029
+ newline="\n",
1030
+ )
1031
+ except (OSError, json.JSONDecodeError):
1032
+ pass
1033
+
1034
+ # Update worktree.json path/branch if present
1035
+ wt_path = new_dir / "meta" / "worktree.json"
1036
+ if wt_path.is_file():
1037
+ try:
1038
+ wt = json.loads(wt_path.read_text(encoding="utf-8-sig"))
1039
+ if isinstance(wt, dict):
1040
+ old_name = args.change
1041
+ new_name = args.to
1042
+ # Update path and branch if they contain old name
1043
+ if isinstance(wt.get("path"), str) and old_name in wt["path"]:
1044
+ wt["path"] = wt["path"].replace(old_name, new_name)
1045
+ if isinstance(wt.get("branch"), str) and old_name in wt["branch"]:
1046
+ wt["branch"] = wt["branch"].replace(old_name, new_name)
1047
+ wt_path.write_text(
1048
+ json.dumps(wt, ensure_ascii=False, indent=2) + "\n",
1049
+ encoding="utf-8",
1050
+ newline="\n",
1051
+ )
1052
+ except (OSError, json.JSONDecodeError):
1053
+ pass
1054
+
1055
+ # Update change-identity.json.changeName
1056
+ identity_path = new_dir / "meta" / "change-identity.json"
1057
+ if identity_path.is_file():
1058
+ try:
1059
+ ident = json.loads(identity_path.read_text(encoding="utf-8-sig"))
1060
+ if isinstance(ident, dict):
1061
+ ident["changeName"] = args.to
1062
+ ident["renamedFrom"] = args.change
1063
+ identity_path.write_text(
1064
+ json.dumps(ident, ensure_ascii=False, indent=2) + "\n",
1065
+ encoding="utf-8",
1066
+ newline="\n",
1067
+ )
1068
+ except (OSError, json.JSONDecodeError):
1069
+ pass
1070
+
1071
+ # Append change.rename event (does not rewrite history)
1072
+ try:
1073
+ import harness_events
1074
+ harness_events.append_event(
1075
+ new_dir,
1076
+ phase="plan",
1077
+ type_="change.rename",
1078
+ renamed_from=args.change,
1079
+ renamed_to=args.to,
1080
+ change_uuid=old_uuid,
1081
+ note=f"change renamed from {args.change} to {args.to}",
1082
+ )
1083
+ except Exception:
1084
+ pass # event append failure should not block rename
1085
+
1086
+ payload = {
1087
+ "ok": True,
1088
+ "code": "RENAMED",
1089
+ "changeUuid": old_uuid,
1090
+ "renamedFrom": args.change,
1091
+ "renamedTo": args.to,
1092
+ "changeDir": str(new_dir),
1093
+ }
1094
+ emit(payload, as_json=bool(args.json))
1095
+ return 0
1096
+
1097
+
1098
+ def cmd_ensure_identity(args: argparse.Namespace) -> int:
1099
+ """Ensure meta/change-identity.json exists with a stable UUID (retro §5.5)."""
1100
+ project = resolve_main_project_root()
1101
+ change_dir = project / ".harness" / "changes" / args.change
1102
+ if not change_dir.is_dir():
1103
+ return emit_error(
1104
+ "CHANGE_NOT_FOUND",
1105
+ f"change directory not found: {change_dir}",
1106
+ as_json=bool(args.json),
1107
+ )
1108
+ identity = _ensure_change_identity(change_dir)
1109
+ emit(identity, as_json=bool(args.json))
1110
+ return 0
1111
+
1112
+
884
1113
  def build_parser() -> argparse.ArgumentParser:
885
1114
  parser = argparse.ArgumentParser(prog="harness_change.py")
886
1115
  parser.add_argument("--json", action="store_true")
@@ -923,6 +1152,8 @@ def build_parser() -> argparse.ArgumentParser:
923
1152
  p_port_release = sub.add_parser("release-port", parents=[shared])
924
1153
  p_port_release.add_argument("--change", required=True)
925
1154
  p_port_release.add_argument("--run-id", required=True)
1155
+ p_port_release.add_argument("--port", type=int, default=None)
1156
+ p_port_release.add_argument("--lease-id", default=None)
926
1157
  p_port_release.set_defaults(func=cmd_release_port)
927
1158
 
928
1159
  p_lock = sub.add_parser("integration-lock", parents=[shared])
@@ -931,6 +1162,15 @@ def build_parser() -> argparse.ArgumentParser:
931
1162
  p_lock.add_argument("--ttl-seconds", type=int, default=3600)
932
1163
  p_lock.set_defaults(func=cmd_integration_lock)
933
1164
 
1165
+ p_rename = sub.add_parser("rename", parents=[shared])
1166
+ p_rename.add_argument("--change", required=True)
1167
+ p_rename.add_argument("--to", required=True)
1168
+ p_rename.set_defaults(func=cmd_rename)
1169
+
1170
+ p_identity = sub.add_parser("ensure-identity", parents=[shared])
1171
+ p_identity.add_argument("--change", required=True)
1172
+ p_identity.set_defaults(func=cmd_ensure_identity)
1173
+
934
1174
  return parser
935
1175
 
936
1176
 
@@ -50,6 +50,7 @@ EVENT_TYPES = frozenset(
50
50
  "issue.resolve",
51
51
  "decision",
52
52
  "correction",
53
+ "change.rename",
53
54
  }
54
55
  )
55
56
 
@@ -94,6 +95,9 @@ OPTIONAL_FIELDS = (
94
95
  "orchestration_active_ms",
95
96
  "wall_clock_ms",
96
97
  "user_wait_ms",
98
+ "renamed_from",
99
+ "renamed_to",
100
+ "change_uuid",
97
101
  )
98
102
 
99
103
  _PROVENANCE_FIELDS = frozenset(
@@ -144,6 +148,10 @@ _EVENT_ALLOWED_FIELDS = {
144
148
  }
145
149
  )
146
150
  | _PROVENANCE_FIELDS,
151
+ "change.rename": frozenset(
152
+ {"renamed_from", "renamed_to", "change_uuid", "note"}
153
+ )
154
+ | _PROVENANCE_FIELDS,
147
155
  }
148
156
  _EVENT_REQUIRED_FIELDS = {
149
157
  "issue": ("severity",),
@@ -516,6 +524,9 @@ def append_event(
516
524
  path: str | None = None,
517
525
  run_id: str | None = None,
518
526
  executor_tool: str | None = None,
527
+ renamed_from: str | None = None,
528
+ renamed_to: str | None = None,
529
+ change_uuid: str | None = None,
519
530
  ) -> dict[str, Any]:
520
531
  """Programmatic append API (retro §5.31 C5/T16).
521
532
 
@@ -547,6 +558,9 @@ def append_event(
547
558
  target_field=None,
548
559
  old_value_hash=None,
549
560
  new_value_json=None,
561
+ renamed_from=renamed_from,
562
+ renamed_to=renamed_to,
563
+ change_uuid=change_uuid,
550
564
  attempt=None,
551
565
  executor_agent=None,
552
566
  executor_model=None,