claude-dev-env 2.12.0 → 2.13.0

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 (193) hide show
  1. package/_shared/pr-loop/scripts/code_rules_gate.py +2 -3
  2. package/_shared/pr-loop/scripts/test_code_rules_gate.py +0 -26
  3. package/agents/CLAUDE.md +1 -2
  4. package/agents/pr-description-writer.md +11 -81
  5. package/bin/install.mjs +48 -9
  6. package/bin/install.prune.test.mjs +26 -0
  7. package/bin/install.settings-defaults.test.mjs +60 -33
  8. package/bin/install.test.mjs +2 -4
  9. package/commands/sr-loop.md +9 -5
  10. package/docs/CLAUDE.md +0 -1
  11. package/docs/references/CLAUDE.md +0 -1
  12. package/hooks/CLAUDE.md +1 -1
  13. package/hooks/blocking/CLAUDE.md +2 -27
  14. package/hooks/blocking/config/CLAUDE.md +1 -13
  15. package/hooks/blocking/conventional_pr_title_gate.py +1 -2
  16. package/hooks/blocking/send_user_file_open_locally_blocker.py +1 -1
  17. package/hooks/blocking/stop_dispatcher.py +4 -4
  18. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +0 -3
  19. package/hooks/blocking/test_pre_tool_use_dispatcher.py +6 -7
  20. package/hooks/blocking/test_send_user_file_open_locally_blocker.py +4 -3
  21. package/hooks/blocking/test_shared_stdin_adoption.py +0 -2
  22. package/hooks/diagnostic/CLAUDE.md +3 -3
  23. package/hooks/diagnostic/hook_log_extractor.py +2 -36
  24. package/hooks/diagnostic/hook_log_stop_wrapper.py +6 -155
  25. package/hooks/diagnostic/test_hook_log_extractor.py +8 -21
  26. package/hooks/diagnostic/test_hook_log_stop_wrapper.py +3 -331
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +0 -6
  28. package/hooks/git-hooks/pre_push.py +3 -85
  29. package/hooks/git-hooks/test_pre_push.py +0 -130
  30. package/hooks/hooks.json +0 -35
  31. package/hooks/hooks_constants/CLAUDE.md +0 -9
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +0 -11
  33. package/hooks/hooks_constants/convergence_branch_constants.py +0 -1
  34. package/hooks/hooks_constants/fable_spawn_gate_constants.py +3 -4
  35. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +0 -8
  36. package/hooks/hooks_constants/send_user_file_open_locally_blocker_constants.py +1 -6
  37. package/hooks/hooks_constants/stop_dispatcher_constants.py +0 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -26
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +0 -5
  40. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +0 -1
  41. package/hooks/lifecycle/CLAUDE.md +1 -3
  42. package/package.json +1 -1
  43. package/rules/CLAUDE.md +0 -2
  44. package/rules/gh-cli-conventions.md +0 -1
  45. package/rules/git-workflow.md +1 -3
  46. package/scripts/CLAUDE.md +0 -1
  47. package/scripts/_code_review_test_support.py +0 -95
  48. package/scripts/dev_env_scripts_constants/code_review_constants.py +9 -98
  49. package/scripts/dev_env_scripts_constants/test_code_review_constants.py +1 -44
  50. package/scripts/invoke_code_review.py +8 -451
  51. package/scripts/test_invoke_code_review.py +5 -61
  52. package/scripts/test_invoke_code_review_cli.py +1 -45
  53. package/scripts/test_invoke_code_review_contract.py +1 -118
  54. package/settings.json +1 -10
  55. package/skills/CLAUDE.md +8 -0
  56. package/skills/autoconverge/reference/CLAUDE.md +1 -1
  57. package/skills/autoconverge/reference/convergence.md +7 -7
  58. package/skills/autoconverge/reference/gotchas.md +3 -10
  59. package/skills/autoconverge/workflow/converge.contract.test.mjs +53 -1388
  60. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +4 -3
  61. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +33 -493
  62. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +15 -97
  63. package/skills/autoconverge/workflow/converge.mjs +201 -238
  64. package/skills/autoconverge/workflow/converge.precatch.test.mjs +1 -1
  65. package/skills/bugteam/CONSTRAINTS.md +1 -1
  66. package/skills/bugteam/SKILL.md +9 -0
  67. package/skills/bugteam/reference/audit-and-teammates.md +10 -6
  68. package/skills/bugteam/test_skill_additions.py +9 -0
  69. package/skills/comments/SKILL.md +65 -0
  70. package/skills/descriptions/SKILL.md +51 -0
  71. package/skills/emergencies/SKILL.md +42 -0
  72. package/skills/imagegen/SKILL.md +21 -0
  73. package/skills/imagegen/scripts/config/__init__.py +1 -0
  74. package/skills/imagegen/scripts/config/constants.py +28 -0
  75. package/skills/imagegen/scripts/imagegen.py +45 -0
  76. package/skills/imagegen/scripts/imagegen_core.py +528 -0
  77. package/skills/imagegen/scripts/test_imagegen.py +204 -0
  78. package/skills/orchestrator/SKILL.md +2 -4
  79. package/skills/plan-to-pr/SKILL.md +6 -7
  80. package/skills/plan-to-pr/reference/final-validation-tasks.md +1 -1
  81. package/skills/plan-to-pr/reference/process-inventory.md +2 -2
  82. package/skills/plan-to-pr/reference/review-loop.md +2 -2
  83. package/skills/plan-to-pr/reference/run-record.schema.json +1 -1
  84. package/skills/plan-to-pr/reference/task-seeds.md +1 -1
  85. package/skills/plan-to-pr/reference/task-ticket.md +3 -4
  86. package/skills/plan-to-pr/scripts/config/constants.py +1 -2
  87. package/skills/plan-to-pr/scripts/test_validate_protocol.py +1 -1
  88. package/skills/plan-to-pr/test_skill_contract.py +2 -2
  89. package/skills/pr-converge/SKILL.md +13 -16
  90. package/skills/pr-converge/reference/progress-checklist.md +6 -5
  91. package/skills/pr-converge/reference/state-schema.md +1 -27
  92. package/skills/pr-loop-cloud-transport/SKILL.md +1 -1
  93. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +3 -3
  94. package/skills/pr-loop-lifecycle/SKILL.md +1 -1
  95. package/skills/pr-loop-lifecycle/reference/teardown-publish-permissions.md +1 -1
  96. package/skills/prototype/SKILL.md +4 -4
  97. package/skills/prototype/reference/honest-limitations.md +4 -4
  98. package/skills/prototype/reference/promotion-tasks.md +2 -2
  99. package/skills/prototype/workflows/promotion.md +4 -6
  100. package/skills/reviews/SKILL.md +85 -0
  101. package/skills/small-cl/SKILL.md +52 -0
  102. package/agents/code-verifier.md +0 -74
  103. package/docs/PR_DESCRIPTION_GUIDE.md +0 -157
  104. package/docs/references/code-review-enforcement.md +0 -132
  105. package/hooks/blocking/code_review_enforcement_config_bootstrap.py +0 -53
  106. package/hooks/blocking/code_review_gate_deny.py +0 -74
  107. package/hooks/blocking/code_review_pr_create_gate.py +0 -198
  108. package/hooks/blocking/code_review_push_gate.py +0 -145
  109. package/hooks/blocking/code_review_stamp_directory_write_blocker.py +0 -348
  110. package/hooks/blocking/code_review_stamp_store.py +0 -233
  111. package/hooks/blocking/code_review_stamp_write_blocker_parts/__init__.py +0 -7
  112. package/hooks/blocking/code_review_stamp_write_blocker_parts/conftest.py +0 -15
  113. package/hooks/blocking/code_review_stamp_write_blocker_parts/obfuscated_stamp_path_reference.py +0 -212
  114. package/hooks/blocking/code_review_stamp_write_blocker_parts/split_directory_change_into_stamp.py +0 -138
  115. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_obfuscated_stamp_path_reference.py +0 -49
  116. package/hooks/blocking/code_review_stamp_write_blocker_parts/test_split_directory_change_into_stamp.py +0 -38
  117. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +0 -483
  118. package/hooks/blocking/config/code_review_enforcement_constants.py +0 -143
  119. package/hooks/blocking/config/test_code_review_enforcement_constants.py +0 -166
  120. package/hooks/blocking/config/verified_commit_constants.py +0 -160
  121. package/hooks/blocking/config/verified_commit_context_constants.py +0 -21
  122. package/hooks/blocking/config/verified_commit_gate_output_constants.py +0 -14
  123. package/hooks/blocking/conftest.py +0 -123
  124. package/hooks/blocking/convergence_gate_blocker.py +0 -310
  125. package/hooks/blocking/pr_converge_bugteam_enforcer.py +0 -170
  126. package/hooks/blocking/pr_description_body_audit.py +0 -148
  127. package/hooks/blocking/pr_description_command_parser.py +0 -237
  128. package/hooks/blocking/pr_description_enforcer.py +0 -227
  129. package/hooks/blocking/pr_description_pr_number.py +0 -155
  130. package/hooks/blocking/pr_description_proof_of_work.py +0 -385
  131. package/hooks/blocking/pr_description_readability.py +0 -364
  132. package/hooks/blocking/reviewer_spawn_gate.py +0 -182
  133. package/hooks/blocking/test_code_review_enforcement_config_bootstrap.py +0 -62
  134. package/hooks/blocking/test_code_review_gate_deny.py +0 -54
  135. package/hooks/blocking/test_code_review_pr_create_gate.py +0 -199
  136. package/hooks/blocking/test_code_review_push_gate.py +0 -205
  137. package/hooks/blocking/test_code_review_stamp_directory_write_blocker.py +0 -199
  138. package/hooks/blocking/test_code_review_stamp_store.py +0 -205
  139. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +0 -710
  140. package/hooks/blocking/test_code_verifier_tools_contract.py +0 -28
  141. package/hooks/blocking/test_convergence_gate_blocker.py +0 -428
  142. package/hooks/blocking/test_pr_converge_bugteam_enforcer.py +0 -311
  143. package/hooks/blocking/test_pr_converge_bugteam_enforcer_state_tolerance.py +0 -184
  144. package/hooks/blocking/test_pr_description_enforcer.py +0 -221
  145. package/hooks/blocking/test_pr_description_enforcer_body_audit.py +0 -247
  146. package/hooks/blocking/test_pr_description_enforcer_body_rules.py +0 -493
  147. package/hooks/blocking/test_pr_description_enforcer_command_parser.py +0 -366
  148. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +0 -159
  149. package/hooks/blocking/test_pr_description_enforcer_proof_gate.py +0 -175
  150. package/hooks/blocking/test_pr_description_enforcer_readability.py +0 -443
  151. package/hooks/blocking/test_pr_description_proof_of_work.py +0 -313
  152. package/hooks/blocking/test_reviewer_spawn_gate.py +0 -230
  153. package/hooks/blocking/test_verdict_directory_write_blocker.py +0 -804
  154. package/hooks/blocking/test_verification_verdict_store.py +0 -974
  155. package/hooks/blocking/test_verified_commit_config_bootstrap.py +0 -67
  156. package/hooks/blocking/test_verified_commit_docs_delta.py +0 -176
  157. package/hooks/blocking/test_verified_commit_gate.py +0 -581
  158. package/hooks/blocking/test_verified_commit_gate_additional_context.py +0 -134
  159. package/hooks/blocking/test_verified_commit_message_accuracy_blocker.py +0 -131
  160. package/hooks/blocking/test_verifier_verdict_minter.py +0 -299
  161. package/hooks/blocking/tests/test_verified_commit_gate.py +0 -41
  162. package/hooks/blocking/verdict_directory_write_blocker.py +0 -687
  163. package/hooks/blocking/verification_verdict_store.py +0 -1039
  164. package/hooks/blocking/verified_commit_config_bootstrap.py +0 -63
  165. package/hooks/blocking/verified_commit_gate.py +0 -113
  166. package/hooks/blocking/verified_commit_gate_parts/CLAUDE.md +0 -28
  167. package/hooks/blocking/verified_commit_gate_parts/__init__.py +0 -1
  168. package/hooks/blocking/verified_commit_gate_parts/command_tokenization.py +0 -174
  169. package/hooks/blocking/verified_commit_gate_parts/deny_payload.py +0 -53
  170. package/hooks/blocking/verified_commit_gate_parts/deny_reason.py +0 -80
  171. package/hooks/blocking/verified_commit_gate_parts/directory_resolution.py +0 -170
  172. package/hooks/blocking/verified_commit_gate_parts/gated_invocations.py +0 -217
  173. package/hooks/blocking/verified_commit_gate_parts/tests/conftest.py +0 -10
  174. package/hooks/blocking/verified_commit_gate_parts/tests/test_command_tokenization.py +0 -94
  175. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_payload.py +0 -17
  176. package/hooks/blocking/verified_commit_gate_parts/tests/test_deny_reason.py +0 -38
  177. package/hooks/blocking/verified_commit_gate_parts/tests/test_directory_resolution.py +0 -71
  178. package/hooks/blocking/verified_commit_gate_parts/tests/test_gated_invocations.py +0 -96
  179. package/hooks/blocking/verified_commit_message_accuracy_blocker.py +0 -167
  180. package/hooks/blocking/verifier_verdict_minter.py +0 -280
  181. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +0 -64
  182. package/hooks/hooks_constants/convergence_gate_blocker_constants.py +0 -53
  183. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_constants.py +0 -55
  184. package/hooks/hooks_constants/pr_converge_bugteam_enforcer_state.py +0 -67
  185. package/hooks/hooks_constants/pr_description_enforcer_constants.py +0 -167
  186. package/hooks/hooks_constants/pr_description_proof_of_work_constants.py +0 -107
  187. package/hooks/hooks_constants/reviewer_spawn_gate_constants.py +0 -41
  188. package/hooks/hooks_constants/test_pr_description_enforcer_constants.py +0 -292
  189. package/hooks/lifecycle/pr_converge_bugteam_skill_tracker.py +0 -198
  190. package/hooks/lifecycle/test_pr_converge_bugteam_skill_tracker.py +0 -283
  191. package/rules/proof-of-work-pr-comments.md +0 -22
  192. package/rules/verified-commit-gate-skip.md +0 -4
  193. package/scripts/Show-Asset.ps1 +0 -106
@@ -48,6 +48,11 @@ const CONFIG = {
48
48
  }
49
49
 
50
50
  const REVIEWER_GATE_SENTINEL = 'CLAUDE_REVIEWER_GATE=autoconverge '
51
+ const REVIEW_GUIDE_REFERENCE = 'skills/reviews/SKILL.md#review-workflow'
52
+ const REVIEW_RESULT_PASS_MARKER = 'REVIEW_RESULT: PASS'
53
+ const REVIEW_RESULT_FINDINGS_MARKER = 'REVIEW_RESULT: FINDINGS'
54
+ const REVIEW_RESULT_INSTRUCTION =
55
+ `3. State every required finding before the result line. End with exactly one standalone terminal ${REVIEW_RESULT_PASS_MARKER} when no required finding remains, or ${REVIEW_RESULT_FINDINGS_MARKER} when findings remain. Include no other REVIEW_RESULT: text.`
51
56
 
52
57
  const TIERS = {
53
58
  opusMedium: { model: 'opus', effort: 'medium' },
@@ -179,10 +184,9 @@ function runGitTask(task) {
179
184
  }
180
185
 
181
186
  /**
182
- * Spawn a fresh fixer clean-coder agent for a commit or recovery edit. The fixer
183
- * never verifies; a separate verifier agent emits the verdict, so the verdict
184
- * that gates the commit comes from a different agent than the one that commits
185
- * and recovers.
187
+ * Spawn a fresh fixer clean-coder agent for a commit or recovery edit. The
188
+ * review step completes before the commit step, and the fixer preserves that
189
+ * reviewed working tree while it commits or recovers.
186
190
  * @param {string} task the short task name
187
191
  * @param {object} context task-specific context
188
192
  * @returns {Promise<object>} the structured output
@@ -191,9 +195,9 @@ function runFixerTask(task, context) {
191
195
  const label = `fixer:${context.sourceLabel}`
192
196
  if (task === 'commit') {
193
197
  return convergeAgent(
194
- `You are the COMMIT step for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
198
+ `You are the COMMIT step for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree and the review step completed under ${REVIEW_GUIDE_REFERENCE}.\n\n` +
195
199
  `Rules:\n` +
196
- `- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the commit would be blocked. Do not run a formatter, do not touch a test, do not re-fix anything — only commit and push what is already there.\n` +
200
+ `- Make no further file edits. Preserve the reviewed working tree; only commit and push what is already there.\n` +
197
201
  `- Make ONE commit for all the working-tree fixes, then push to the PR branch.\n\n` +
198
202
  `Return values:\n` +
199
203
  `- On a successful push: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a one-line summary.\n` +
@@ -210,7 +214,7 @@ function runFixerTask(task, context) {
210
214
  `Rules:\n` +
211
215
  `- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
212
216
  `- Fix ONLY the violation named above, test-first (failing test, then minimum code to pass) per CODE_RULES. Do not re-open the original findings, and do not touch GitHub review threads — the edit step already handled those.\n` +
213
- `- Leave the corrected fixes in the working tree. Do NOT commit and do NOT push the verify step re-binds a verdict and the commit step pushes after you.\n\n` +
217
+ `- Leave the corrected fixes in the working tree. Do not commit or push; the review and commit steps run after you.\n\n` +
214
218
  `Return values: edited=true with a one-line summary when you changed code to clear the block; edited=false, resolvedWithoutCommit=false when the block cannot be cleared with a code change.` +
215
219
  PRE_COMMIT_GATE_STEP,
216
220
  { label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', ...TIERS.sonnetMedium },
@@ -232,14 +236,10 @@ function runFixerTask(task, context) {
232
236
  }
233
237
 
234
238
  /**
235
- * Joined fixer recovery loop: the verify step spawns a code-verifier agent to
236
- * grade the working-tree fixes while the commit and recovery steps spawn
237
- * clean-coder fixer agents, so the verdict that gates the commit comes from a
238
- * different agent than the one that edits and pushes the same editor/verifier
239
- * separation the repair and conflict paths use. The verify step routes through
240
- * verifyWithRecovery (verify on the verifier, recover on the fixer); the commit
241
- * step routes through commitWithRecovery (commit and commit-recover on the fixer,
242
- * re-verify on the verifier). A failed verdict returns the unchanged HEAD so the
239
+ * Joined fixer recovery loop: the verify step applies the canonical review guide
240
+ * to the working-tree fixes while the commit and recovery steps spawn clean-coder
241
+ * fixer agents. The verify step routes through verifyWithRecovery; the commit
242
+ * step routes through commitWithRecovery. A failed review returns the unchanged HEAD so the
243
243
  * round reads as not-progressed.
244
244
  * @param {string} head PR HEAD SHA
245
245
  * @param {Array<object>} findings the findings to fix
@@ -248,10 +248,10 @@ function runFixerTask(task, context) {
248
248
  */
249
249
  async function fixerWithRecovery(head, findings, sourceLabel) {
250
250
  const verifyTranscript = await verifyWithRecovery({
251
- runVerify: () => runVerifierTask('fix-verify', { head, findings, sourceLabel }),
251
+ runVerify: () => runReviewTask('fix-verify', { head, findings, sourceLabel }),
252
252
  runRecoverEdit: (objection, attempt) => runFixerTask('verify-recover', { head, findings, sourceLabel, objection, attempt }),
253
253
  })
254
- if (!verdictPassed(verifyTranscript)) {
254
+ if (!reviewPassed(verifyTranscript)) {
255
255
  return {
256
256
  newSha: head,
257
257
  pushed: false,
@@ -261,9 +261,15 @@ async function fixerWithRecovery(head, findings, sourceLabel) {
261
261
  blockerDetail: '',
262
262
  }
263
263
  }
264
+ const captureReviewedSurface = () => captureReviewedSurfaceHash({ sourceLabel, phase: 'Converge' })
265
+ const reviewedSurfaceHash = await captureReviewedSurface()
266
+ if (reviewedSurfaceHash === null) return reviewedSurfaceUnavailableResult(head)
264
267
  return commitWithRecovery({
268
+ head,
269
+ reviewedSurfaceHash,
270
+ captureReviewedSurface,
265
271
  runCommit: () => runFixerTask('commit', { head, findings, sourceLabel }),
266
- runVerify: () => runVerifierTask('fix-verify', { head, findings, sourceLabel }),
272
+ runVerify: () => runReviewTask('fix-verify', { head, findings, sourceLabel }),
267
273
  runRecoverEdit: (detail, attempt) => runFixerTask('commit-recover', { head, findings, sourceLabel, blockerDetail: detail, attempt }),
268
274
  })
269
275
  }
@@ -337,9 +343,9 @@ function runCodeEditorTask(task, context) {
337
343
  ? 'The edit step rebased the branch, so push with git push --force-with-lease.'
338
344
  : 'Push to the PR branch with a plain git push.'
339
345
  return convergeAgent(
340
- `You are the COMMIT step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
346
+ `You are the COMMIT step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree and the review step completed under ${REVIEW_GUIDE_REFERENCE}.\n\n` +
341
347
  `Rules:\n` +
342
- `- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Do not run a formatter, do not re-fix anything — only commit and push what is already there.\n` +
348
+ `- Make no further file edits. Preserve the reviewed working tree; only commit and push what is already there.\n` +
343
349
  `- Commit any uncommitted bot-thread fix in ONE commit (skip the commit when the working tree carries only already-committed rebase results). ${pushInstruction}\n\n` +
344
350
  `Return values:\n` +
345
351
  `- On a successful push: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a one-line summary.\n` +
@@ -357,7 +363,7 @@ function runCodeEditorTask(task, context) {
357
363
  `You are the EDIT step deferring a code-standard-only round on ${prCoordinates}, HEAD ${context.head} (${context.sourceLabel}). The round surfaced ONLY code-standard violations (CODE_RULES/style, no behavioral impact); the run treats it as passed and defers the fixes to follow-up work, which you now stage. A separate verify step then a separate commit step open the hardening PR after you. Do NOT commit or push to the PR's own branch.\n\n` +
358
364
  `Findings:\n${findingsBlock}\n\n` +
359
365
  `1. Follow-up fix issue: file a GitHub issue on ${input.owner}/${input.repo} (gh issue create --body-file with a temp file) titled "Deferred code-standard fixes from PR #${input.prNumber}". The body references the PR and lists each finding with its file:line, severity, and detail. The issue carries the fix work; do not open a fix PR. Capture the issue URL.\n` +
360
- `2. Stage the environment-hardening change: in the Claude environment config repo (the repo owning ~/.claude hooks and rules — JonEcho/llm-settings for hooks, jl-cmd/claude-dev-env for rules/skills; pick whichever owns the surface that would block these violation classes), find or clone a local checkout, fetch origin, and create a branch off origin/main. Edit the hooks/rules in that checkout's WORKING TREE so each violation class found here is blocked at Write/Edit time, before code is written. Do NOT commit and do NOT push the commit step does that after the verify step binds a verdict to the working tree. Return the checkout's absolute path in hardeningRepoPath, the branch name in hardeningBranch, and set hardeningEdited=true. When no hardening is feasible for these classes, leave hardeningRepoPath and hardeningBranch empty and hardeningEdited=false; the follow-up issue still stands.\n` +
366
+ `2. Stage the environment-hardening change: in the Claude environment config repo (the repo owning ~/.claude hooks and rules — JonEcho/llm-settings for hooks, jl-cmd/claude-dev-env for rules/skills; pick whichever owns the surface that would block these violation classes), find or clone a local checkout, fetch origin, and create a branch off origin/main. Edit the hooks/rules in that checkout's WORKING TREE so each violation class found here is blocked at Write/Edit time, before code is written. Do not commit or push; review and commit run after this step. Return the checkout's absolute path in hardeningRepoPath, the branch name in hardeningBranch, and set hardeningEdited=true. When no hardening is feasible for these classes, leave hardeningRepoPath and hardeningBranch empty and hardeningEdited=false; the follow-up issue still stands.\n` +
361
367
  `3. For each finding that carries a GitHub review comment id (${threadIds.length ? threadIds.join(', ') : 'none this batch'}): post an inline reply via python "${CONFIG.sharedScripts}/post_fix_reply.py" --owner ${input.owner} --repo ${input.repo} --pr-number ${input.prNumber} --in-reply-to <id> --body "Code-standard-only finding — deferred to follow-up issue <url>." Then resolve the thread by its PRRT_ node id (GraphQL lookup on comment databaseId, then resolveReviewThread or the github MCP pull_request_review_write method=resolve_thread — not the numeric comment id).\n\n` +
362
368
  `Return the issue URL in issueUrl (empty string when it could not be filed), the hardening checkout path and branch, hardeningEdited, and a one-line summary.` +
363
369
  PRE_COMMIT_GATE_STEP,
@@ -380,9 +386,9 @@ function runCodeEditorTask(task, context) {
380
386
  }
381
387
  if (task === 'hardening-commit') {
382
388
  return convergeAgent(
383
- `You are the COMMIT step opening the environment-hardening PR (${context.sourceLabel}) for the change staged in ${context.hardeningRepoPath} on branch ${context.hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree. Do NOT touch the PR's own branch.\n\n` +
389
+ `You are the COMMIT step opening the environment-hardening PR (${context.sourceLabel}) for the change staged in ${context.hardeningRepoPath} on branch ${context.hardeningBranch}. The edit step left the hooks/rules edits in the working tree and the review step completed under ${REVIEW_GUIDE_REFERENCE}. Do not touch the PR's own branch.\n\n` +
384
390
  `Rules:\n` +
385
- `- Make NO further file edits of any kind. Any edit changes the surface and invalidates the verdict that unlocks the commit gate, so the push would be blocked. Only commit and push what is already there.\n` +
391
+ `- Make no further file edits. Preserve the reviewed working tree; only commit and push what is already there.\n` +
386
392
  `- In ${context.hardeningRepoPath}: make ONE commit of the staged hooks/rules change on branch ${context.hardeningBranch}, push it, then open a DRAFT PR. The PR body references the follow-up issue ${context.issueUrl || '(none)'} and states the PR hardens the environment so the deferred violation classes are blocked at Write/Edit time. Honor the gh-cli-conventions rule: write a BOM-free temp file and pass --body-file.\n` +
387
393
  `- Title the PR as a Conventional Commit — a type prefix (feat, fix, chore, docs, refactor, perf, ci, style, test, build, revert), an optional scope in parentheses, then a colon and a short summary, e.g. "feat(hooks): block the deferred violation class". The target repo's CI validates the PR title as a semantic commit and rejects a non-conforming title.\n\n` +
388
394
  `Return the full https URL of the DRAFT hardening PR in hardeningPrUrl (empty string when no PR was opened) and a one-line summary.`,
@@ -420,25 +426,24 @@ function runCodeEditorTask(task, context) {
420
426
  }
421
427
 
422
428
  /**
423
- * Spawn a fresh verifier code-verifier agent for a specific verify task
424
- * (fix-verify, repair-verify, hardening-verify). The agent makes no edits
425
- * verification only — and ends its message with a fenced verdict block.
429
+ * Spawn a fresh review agent for a specific verify task (fix-verify,
430
+ * repair-verify, hardening-verify). The agent makes no edits.
426
431
  * @param {string} task the short task name
427
432
  * @param {object} context task-specific context
428
- * @returns {Promise<string>} the verifier transcript carrying the verdict fence
433
+ * @returns {Promise<string>} the review transcript
429
434
  */
430
- function runVerifierTask(task, context) {
431
- const label = `verifier:${task}`
435
+ function runReviewTask(task, context) {
436
+ const label = `review:${task}`
432
437
  if (task === 'fix-verify') {
433
438
  const findingsBlock = renderFindingsBlock(context.findings)
434
439
  return convergeReadOnlyAgent(
435
- `You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Make NO edit to the tree under verification verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
440
+ `You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Make no edit to the tree under verification. Apply the review guide at ${REVIEW_GUIDE_REFERENCE}.\n\n` +
436
441
  `Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
437
442
  `Steps:\n` +
438
443
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
439
444
  `2. Verify the uncommitted working-tree changes resolve every finding above: run the relevant tests and the named gates against the working tree. Read the diff (git diff) and confirm each finding is fixed test-first per CODE_RULES.\n` +
440
- `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
441
- { label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
445
+ REVIEW_RESULT_INSTRUCTION,
446
+ { label, phase: 'Converge', agentType: 'code-quality-agent', ...TIERS.sonnetMedium },
442
447
  )
443
448
  }
444
449
  if (task === 'repair-verify') {
@@ -446,28 +451,34 @@ function runVerifierTask(task, context) {
446
451
  ? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
447
452
  : 'none reported'
448
453
  return convergeReadOnlyAgent(
449
- `You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Make NO edit to the tree under verification verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
454
+ `You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Make no edit to the tree under verification. Apply the review guide at ${REVIEW_GUIDE_REFERENCE}.\n\n` +
450
455
  `Concerns the working-tree repair must resolve (the gates the convergence check flagged):\n${failureBlock}\n\n` +
451
456
  `Steps:\n` +
452
457
  `1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
453
458
  `2. Verify the working tree against origin/main: any bot-thread code fix is correct test-first per CODE_RULES, and a rebase (if any) left a clean, conflict-free tree. Read the diff (git diff origin/main) and run the relevant tests and named gates.\n` +
454
- `3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
455
- { label, phase: 'Finalize', agentType: 'code-verifier', ...TIERS.sonnetMedium },
459
+ REVIEW_RESULT_INSTRUCTION,
460
+ { label, phase: 'Finalize', agentType: 'code-quality-agent', ...TIERS.sonnetMedium },
456
461
  )
457
462
  }
458
463
  return convergeReadOnlyAgent(
459
- `You are the VERIFY step for an environment-hardening change (${context.sourceLabel}) staged in the working tree of ${context.hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Make NO edit to the tree under verification verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
464
+ `You are the VERIFY step for an environment-hardening change (${context.sourceLabel}) staged in the working tree of ${context.hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Make no edit to the tree under verification. Apply the review guide at ${REVIEW_GUIDE_REFERENCE}.\n\n` +
460
465
  `Concern the working-tree change must resolve: the edited hooks/rules block the code-standard violation classes from the deferred round at Write/Edit time, and a hook change carries a passing test per CODE_RULES.\n\n` +
461
466
  `Steps:\n` +
462
467
  `1. cd into ${context.hardeningRepoPath}, then resolve its repo root: REPO=$(git rev-parse --show-toplevel).\n` +
463
468
  `2. Verify the uncommitted working-tree change in REPO: read the diff (git diff) and run the hook/rule tests in that repo, confirming each violation class is now blocked.\n` +
464
- `3. Compute the binding hash for the live surface:\n` +
465
- ` The hardening branch is: ${context.hardeningBranch}\n` +
466
- ` Run exactly:\n` +
467
- ` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "${context.hardeningBranch}"\n` +
468
- ` (substitute the REPO path you resolved for the script path). That prints a single 64-char hex hash on stdout — capture it.\n` +
469
- buildVerdictFenceTail(' '),
470
- { label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
469
+ REVIEW_RESULT_INSTRUCTION,
470
+ { label, phase: 'Converge', agentType: 'code-quality-agent', ...TIERS.sonnetMedium },
471
+ )
472
+ }
473
+
474
+ function runWorkingTreeSurfaceTask(context) {
475
+ const repositoryInstruction = context.repositoryPath
476
+ ? `cd into ${context.repositoryPath}, then resolve its repo root with git rev-parse --show-toplevel.`
477
+ : 'Resolve the current worktree repo root with git rev-parse --show-toplevel.'
478
+ return convergeReadOnlyAgent(
479
+ `Capture the reviewed working-tree surface for ${context.sourceLabel}. Do not edit, stage, commit, push, rebase, or modify any file. ${repositoryInstruction}\n\n` +
480
+ `Use a temporary Python script outside the repository to print one lowercase SHA-256 hash. Hash this exact length-prefixed byte stream: first the literal bytes "tracked", then the exact stdout bytes from git diff --no-ext-diff --binary HEAD; then, for every path from git ls-files --others --exclude-standard -z sorted as raw bytes, the literal bytes "untracked", the raw path bytes, the lstat file-type bits as ASCII, and the payload bytes. The payload is file bytes for a regular file and the raw link target bytes for a symlink. Prefix every element with its eight-byte big-endian length. Return the hash in surfaceHash.`,
481
+ { label: `working-tree-surface:${context.sourceLabel}`, phase: context.phase, schema: WORKING_TREE_SURFACE_SCHEMA, agentType: 'general-purpose', ...TIERS.haikuLow },
471
482
  )
472
483
  }
473
484
 
@@ -607,7 +618,7 @@ function runConvergenceCheck(context) {
607
618
  const PRE_COMMIT_GATE_STEP =
608
619
  `\n\nFINAL STEP — pre-commit gate check (do NOT commit): before your turn ends, prove your working-tree changes CAN be committed by dry-running the CODE_RULES commit gate that gates git commit (precommit_code_rules_gate). From inside the checkout that holds your changes, resolve its root with git rev-parse --show-toplevel, stage your changes with git add -A, then run exactly:\n` +
609
620
  ` python "${CONFIG.prLoopScripts}/code_rules_gate.py" --repo-root "<that root>" --staged\n` +
610
- `Exit 0 means the commit gate would accept the commit. On any non-zero exit, read every violation it prints, fix each one test-first per CODE_RULES, and re-run the gate until it exits 0. Then unstage with git restore --staged . so the verify step reads the working-tree diff. Make NO git commit and NO git push here — this is a dry committability check; the separate verify and commit steps run after you, and the verified-commit gate is their job, not yours. Your turn does not end while the commit gate would reject the commit.`
621
+ `Exit 0 means the commit gate would accept the commit. On any non-zero exit, read every violation it prints, fix each one test-first per CODE_RULES, and re-run the gate until it exits 0. Then unstage with git restore --staged . so the review step reads the working-tree diff. Make no git commit or git push here — this is a dry committability check; the separate review and commit steps run after you. Your turn does not end while the commit gate would reject the commit.`
611
622
 
612
623
  const LENS_SCHEMA = {
613
624
  type: 'object',
@@ -789,6 +800,15 @@ const FIX_SCHEMA = {
789
800
  required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary', 'blockedNeedingEdit', 'blockerDetail'],
790
801
  }
791
802
 
803
+ const WORKING_TREE_SURFACE_SCHEMA = {
804
+ type: 'object',
805
+ additionalProperties: false,
806
+ properties: {
807
+ surfaceHash: { type: 'string', description: 'the lowercase SHA-256 fingerprint of the complete tracked and untracked working-tree surface' },
808
+ },
809
+ required: ['surfaceHash'],
810
+ }
811
+
792
812
  const EDIT_SCHEMA = {
793
813
  type: 'object',
794
814
  additionalProperties: false,
@@ -845,53 +865,6 @@ const HARDENING_COMMIT_SCHEMA = {
845
865
  required: ['hardeningPrUrl', 'summary'],
846
866
  }
847
867
 
848
- const VERDICT_FENCE_CONTRACT_SENTENCE =
849
- 'Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.'
850
-
851
- /**
852
- * Build the closing fence recipe every verify prompt ends with: the lead-in
853
- * sentence, the ```verdict block shape carrying the binding hash, and the
854
- * contract sentence. Each verify path resolves the binding hash its own way
855
- * and sits at its own nesting depth, so the caller passes the indent its
856
- * surrounding prose uses; the fence body itself sits one level deeper.
857
- * @param {string} indent leading whitespace the caller's prose sits at
858
- * @returns {string} the fence recipe tail for a verify prompt
859
- */
860
- function buildVerdictFenceTail(indent) {
861
- const fenceIndent = `${indent} `
862
- return (
863
- `${indent}Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
864
- `${fenceIndent}` + "```verdict\n" +
865
- `${fenceIndent}{"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
866
- `${fenceIndent}` + "```\n" +
867
- `${fenceIndent}${VERDICT_FENCE_CONTRACT_SENTENCE}`
868
- )
869
- }
870
-
871
- /**
872
- * Build the verdict-fence step instructions for a verify agent, binding the
873
- * surface hash by branch name rather than by a self-resolved cwd. Resolving
874
- * the branch via `gh pr view` is cwd-immune: it does not matter which worktree
875
- * the verify agent runs in, so a launcher session whose cwd is a different
876
- * worktree cannot poison the binding hash.
877
- * @param {string} prOwner GitHub owner of the repo that holds the branch
878
- * @param {string} prRepo GitHub repo name
879
- * @param {number|string} prNumber PR number used to resolve the head branch
880
- * @returns {string} binding-hash and verdict-fence instructions for a verify prompt
881
- */
882
- function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
883
- return (
884
- `Compute the binding hash for the live surface:\n` +
885
- ` a. Resolve the PR head branch (cwd-immune): run exactly\n` +
886
- ` gh pr view ${prNumber} --repo ${prOwner}/${prRepo} --json headRefName -q .headRefName\n` +
887
- ` Capture the branch name printed on stdout.\n` +
888
- ` b. Run exactly:\n` +
889
- ` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "<that branch>"\n` +
890
- ` (substitute the REPO path you resolved for the script path, and the branch name for <that branch>). That prints a single 64-char hex hash on stdout — capture it.\n` +
891
- buildVerdictFenceTail('')
892
- )
893
- }
894
-
895
868
  const FINALIZE_SCHEMA = {
896
869
  type: 'object',
897
870
  additionalProperties: false,
@@ -1297,63 +1270,32 @@ function normalizeShaForComparison(sha) {
1297
1270
  }
1298
1271
 
1299
1272
  /**
1300
- * Locate the LAST ```verdict ...``` fence in a transcript: the marker, optional
1301
- * whitespace, a required newline, then a body closed by ```. This is the ONE
1302
- * definition of "the verdict fence" — the verdict parser and the pre-fence prose
1303
- * reader both anchor here, so they cannot land on different fences.
1304
- *
1305
- * ::
1306
- *
1307
- * ok: 'note\n\n```verdict\n{}\n```' -> body '{}\n' starting at index 6
1308
- * flag: 'note\n\n```verdict {}```' -> null (no newline after the marker)
1309
- *
1310
- * A marker with no newline after it is not a fence, so a later one cannot pull
1311
- * either reader off the fence the other reads.
1312
- *
1313
1273
  * @param {string|null|undefined} transcript the agent transcript text
1314
- * @returns {{bodyText: string, startIndex: number}|null} the last fence, or null when there is none
1274
+ * @returns {'PASS'|'FINDINGS'|null} the final review result, or null when absent
1315
1275
  */
1316
- function findLastVerdictFence(transcript) {
1276
+ function parseReviewResult(transcript) {
1317
1277
  if (typeof transcript !== 'string') return null
1318
- const fencePattern = /```verdict\s*\n([\s\S]*?)```/g
1319
- let lastFence = null
1320
- let eachMatch
1321
- while ((eachMatch = fencePattern.exec(transcript)) !== null) {
1322
- lastFence = { bodyText: eachMatch[1], startIndex: eachMatch.index }
1323
- }
1324
- return lastFence
1325
- }
1326
-
1327
- /**
1328
- * Parse the LAST ```verdict ...``` fenced JSON block from a transcript.
1329
- * Guards against non-string input, takes the fence findLastVerdictFence locates,
1330
- * parses the JSON, and returns the object or null on any failure.
1331
- * @param {string|null|undefined} transcript the agent transcript text
1332
- * @returns {object|null} the parsed verdict object, or null when absent or malformed
1333
- */
1334
- function parseLastVerdictFence(transcript) {
1335
- const lastFence = findLastVerdictFence(transcript)
1336
- if (lastFence === null) return null
1337
- try {
1338
- return JSON.parse(lastFence.bodyText)
1339
- } catch {
1340
- return null
1341
- }
1278
+ const allTranscriptLines = transcript.trimEnd().split(/\r?\n/)
1279
+ const allResultLikeLines = allTranscriptLines.filter((eachTranscriptLine) =>
1280
+ eachTranscriptLine.includes('REVIEW_RESULT:'),
1281
+ )
1282
+ if (allResultLikeLines.length !== 1) return null
1283
+ const terminalReviewLine = allTranscriptLines.at(-1)
1284
+ if (terminalReviewLine === REVIEW_RESULT_PASS_MARKER) return 'PASS'
1285
+ if (terminalReviewLine === REVIEW_RESULT_FINDINGS_MARKER) return 'FINDINGS'
1286
+ return null
1342
1287
  }
1343
1288
 
1344
1289
  /**
1345
- * Decide whether a workflow code-verifier transcript ended in a passing
1346
- * verdict. Reads the LAST ```verdict ...``` fenced JSON block via the shared
1347
- * parser and returns true only when it parses to an object with all_pass true.
1348
- * @param {string|null|undefined} verifyTranscript the verifier's transcript text
1349
- * @returns {boolean} true only when the last verdict fence reports all_pass true
1290
+ * Decide whether a review transcript reports a passed review.
1291
+ * @param {string|null|undefined} verifyTranscript the review transcript text
1292
+ * @returns {boolean} true only when the final review result is PASS
1350
1293
  */
1351
- function verdictPassed(verifyTranscript) {
1352
- const verdictRecord = parseLastVerdictFence(verifyTranscript)
1353
- return verdictRecord != null && verdictRecord.all_pass === true
1294
+ function reviewPassed(verifyTranscript) {
1295
+ return parseReviewResult(verifyTranscript) === 'PASS'
1354
1296
  }
1355
1297
 
1356
- const VERIFY_OBJECTION_FALLBACK = 'The verify step rejected the working-tree fixes without a parseable verdict; re-read the fix-verify transcript above and address every concern it raised.'
1298
+ const VERIFY_OBJECTION_FALLBACK = 'The review step rejected the working-tree fixes without an actionable result; re-read the fix-verify transcript and address every concern it raises.'
1357
1299
 
1358
1300
  /**
1359
1301
  * Render one verdict finding as a single objection line, tolerant of the shapes a
@@ -1383,68 +1325,16 @@ function renderVerifyObjectionLine(eachFinding) {
1383
1325
  }
1384
1326
 
1385
1327
  /**
1386
- * Read the prose paragraph the verifier wrote directly above its last verdict
1387
- * fence. A verdict that is incomplete — a check it rests on never showed red —
1388
- * is not a code defect, so the fence contract puts that reason here rather than
1389
- * in findings; this reader is how the reason survives into the re-fix step.
1390
- *
1391
- * Anchors through findLastVerdictFence, the single locator parseLastVerdictFence
1392
- * reads its JSON from, so both readers see the same fence by construction rather
1393
- * than by two searches that happen to agree: a stray marker after the real fence
1394
- * — unterminated, or closed but carrying no newline after the marker — moves
1395
- * neither reader. A candidate paragraph carrying only markdown scaffolding (a
1396
- * heading, a fence delimiter) is skipped, since handing the fixer a heading is
1397
- * worse than the generic fallback the caller keeps.
1398
- *
1399
- * ::
1400
- *
1401
- * 'ran the gates\n\ncheck X never showed red\n\n```verdict\n{}\n```'
1402
- * -> 'check X never showed red'
1403
- * '```verdict\n{}\n```' -> null
1404
- *
1405
- * @param {string|null|undefined} verifyTranscript the verifier transcript text
1406
- * @returns {string|null} the last prose paragraph above the final fence, or null when there is none
1407
- */
1408
- function extractPreFenceProse(verifyTranscript) {
1409
- const lastFence = findLastVerdictFence(verifyTranscript)
1410
- if (lastFence === null) return null
1411
- const proseParagraphs = verifyTranscript
1412
- .slice(0, lastFence.startIndex)
1413
- .split(/\n\s*\n/)
1414
- .map((eachParagraph) => eachParagraph.trim())
1415
- .filter((eachParagraph) =>
1416
- eachParagraph
1417
- .split('\n')
1418
- .some((eachLine) => {
1419
- const trimmedLine = eachLine.trim()
1420
- return trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('```') && /[A-Za-z]/.test(trimmedLine)
1421
- }),
1422
- )
1423
- return proseParagraphs.length === 0 ? null : proseParagraphs[proseParagraphs.length - 1]
1424
- }
1425
-
1426
- /**
1427
- * Pull the verifier's stated objections out of a failed verify transcript so the
1428
- * re-fix step knows what the verdict rejected. Reads the last fenced verdict JSON
1429
- * (the same block verdictPassed reads) and renders each finding through
1430
- * renderVerifyObjectionLine into a numbered list. When findings yields no usable
1431
- * line the verdict is an incomplete one rather than a code-defect one, so the
1432
- * prose paragraph above the fence — where the fence contract puts the unshown-red
1433
- * check — carries the objection instead. A missing fence, a parse failure, or an
1434
- * empty findings list with no prose above the fence falls back to a generic
1435
- * re-read instruction, so the re-fix step always receives actionable text.
1436
- * @param {string|null|undefined} verifyTranscript the failed verifier transcript text
1437
- * @returns {string} a human-readable block of the verifier's objections
1328
+ * Pull required findings from a failed review transcript so the repair step
1329
+ * receives actionable text.
1330
+ * @param {string|null|undefined} verifyTranscript the failed review transcript
1331
+ * @returns {string} the review findings or a fallback instruction
1438
1332
  */
1439
1333
  function extractVerifyObjection(verifyTranscript) {
1440
- const verdictRecord = parseLastVerdictFence(verifyTranscript)
1441
- if (verdictRecord == null) return VERIFY_OBJECTION_FALLBACK
1442
- const allObjections = Array.isArray(verdictRecord?.findings) ? verdictRecord.findings : []
1443
- const renderedObjections = allObjections
1444
- .map((eachFinding) => renderVerifyObjectionLine(eachFinding))
1445
- .filter((eachLine) => eachLine !== null)
1446
- if (renderedObjections.length === 0) return extractPreFenceProse(verifyTranscript) || VERIFY_OBJECTION_FALLBACK
1447
- return renderedObjections.map((eachLine, position) => `${position + 1}. ${eachLine}`).join('\n')
1334
+ if (parseReviewResult(verifyTranscript) !== 'FINDINGS') return VERIFY_OBJECTION_FALLBACK
1335
+ const allTranscriptLines = verifyTranscript.trimEnd().split(/\r?\n/)
1336
+ const findingText = allTranscriptLines.slice(0, -1).join('\n').trim()
1337
+ return findingText.length > 0 ? findingText : VERIFY_OBJECTION_FALLBACK
1448
1338
  }
1449
1339
 
1450
1340
  /**
@@ -1974,6 +1864,45 @@ const FIX_RECOVERY_MAX_ATTEMPTS = 2
1974
1864
  */
1975
1865
  const COMMIT_RECOVERY_MAX_ATTEMPTS = 1
1976
1866
 
1867
+ function readReviewedSurfaceHash(reportedSurfaceHash) {
1868
+ if (typeof reportedSurfaceHash !== 'string') return null
1869
+ return /^[0-9a-f]{64}$/.test(reportedSurfaceHash) ? reportedSurfaceHash : null
1870
+ }
1871
+
1872
+ async function captureReviewedSurfaceHash(context) {
1873
+ const surfaceRecord = await runWorkingTreeSurfaceTask(context)
1874
+ return readReviewedSurfaceHash(surfaceRecord?.surfaceHash)
1875
+ }
1876
+
1877
+ function reviewedSurfaceUnavailableResult(head) {
1878
+ return {
1879
+ newSha: head,
1880
+ pushed: false,
1881
+ resolvedWithoutCommit: false,
1882
+ summary: 'the reviewed working-tree surface changed or could not be fingerprinted before commit — not committing',
1883
+ blockedNeedingEdit: false,
1884
+ blockerDetail: '',
1885
+ }
1886
+ }
1887
+
1888
+ async function rebindReviewedSurface({ reviewedSurfaceHash, captureReviewedSurface, runVerify, runRecoverEdit }) {
1889
+ let boundSurfaceHash = reviewedSurfaceHash
1890
+ let driftAttempt = 0
1891
+ while (driftAttempt <= COMMIT_RECOVERY_MAX_ATTEMPTS) {
1892
+ const currentSurfaceHash = await captureReviewedSurface()
1893
+ if (boundSurfaceHash !== null && currentSurfaceHash === boundSurfaceHash) return boundSurfaceHash
1894
+ if (driftAttempt === COMMIT_RECOVERY_MAX_ATTEMPTS) return null
1895
+ driftAttempt += 1
1896
+ const verifyTranscript =
1897
+ typeof runRecoverEdit === 'function'
1898
+ ? await verifyWithRecovery({ runVerify, runRecoverEdit })
1899
+ : await runVerify()
1900
+ if (!reviewPassed(verifyTranscript)) return null
1901
+ boundSurfaceHash = await captureReviewedSurface()
1902
+ }
1903
+ return null
1904
+ }
1905
+
1977
1906
  /**
1978
1907
  * Run a commit step and, when it is blocked by a commit-time hook or gate that
1979
1908
  * requires a code change, route back to a fixer: fix the blocking violation,
@@ -1986,18 +1915,29 @@ const COMMIT_RECOVERY_MAX_ATTEMPTS = 1
1986
1915
  * @param {{runCommit: function, runVerify: function, runRecoverEdit: function}} steps the commit, re-verify, and recover-edit thunks
1987
1916
  * @returns {Promise<object>} the final FIX_SCHEMA result
1988
1917
  */
1989
- async function commitWithRecovery({ runCommit, runVerify, runRecoverEdit }) {
1990
- let commitResult = await runCommit()
1918
+ async function commitWithRecovery({ head, reviewedSurfaceHash, captureReviewedSurface, runCommit, runVerify, runRecoverEdit }) {
1919
+ let boundSurfaceHash = reviewedSurfaceHash
1920
+ let commitResult = null
1991
1921
  let attempt = 0
1992
- while (commitNeedsCodeRecovery(commitResult) && attempt < COMMIT_RECOVERY_MAX_ATTEMPTS) {
1922
+ while (attempt <= COMMIT_RECOVERY_MAX_ATTEMPTS) {
1923
+ boundSurfaceHash = await rebindReviewedSurface({
1924
+ reviewedSurfaceHash: boundSurfaceHash,
1925
+ captureReviewedSurface,
1926
+ runVerify,
1927
+ runRecoverEdit,
1928
+ })
1929
+ if (boundSurfaceHash === null) return reviewedSurfaceUnavailableResult(head)
1930
+ commitResult = await runCommit()
1931
+ if (!commitNeedsCodeRecovery(commitResult) || attempt === COMMIT_RECOVERY_MAX_ATTEMPTS) return commitResult
1993
1932
  attempt += 1
1994
1933
  const recoverEdit = await runRecoverEdit(commitResult.blockerDetail, attempt)
1995
- if (recoverEdit?.edited !== true) break
1934
+ if (recoverEdit?.edited !== true) return commitResult
1996
1935
  const verifyTranscript = await runVerify()
1997
- if (!verdictPassed(verifyTranscript)) break
1998
- commitResult = await runCommit()
1936
+ if (!reviewPassed(verifyTranscript)) return commitResult
1937
+ boundSurfaceHash = await captureReviewedSurface()
1938
+ if (boundSurfaceHash === null) return reviewedSurfaceUnavailableResult(head)
1999
1939
  }
2000
- return commitResult
1940
+ return reviewedSurfaceUnavailableResult(head)
2001
1941
  }
2002
1942
 
2003
1943
  /**
@@ -2013,7 +1953,7 @@ async function commitWithRecovery({ runCommit, runVerify, runRecoverEdit }) {
2013
1953
  async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
2014
1954
  let verifyTranscript = await runVerify()
2015
1955
  let attempt = 0
2016
- while (!verdictPassed(verifyTranscript) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
1956
+ while (!reviewPassed(verifyTranscript) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
2017
1957
  attempt += 1
2018
1958
  const objection = extractVerifyObjection(verifyTranscript)
2019
1959
  const recoverEdit = await runRecoverEdit(objection, attempt)
@@ -2024,14 +1964,9 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
2024
1964
  }
2025
1965
 
2026
1966
  /**
2027
- * Fix lens: edit (clean-coder, no commit) -> verify (a separate code-verifier
2028
- * emits a verdict fence binding the working tree) -> commit (clean-coder, one
2029
- * commit + push, no edits). The verifier is a distinct agent from the fixer, so
2030
- * the verdict that gates the commit comes from a different agent than the one
2031
- * that edits and pushes — the same editor/verifier separation the repair and
2032
- * conflict paths use, and the separation a workflow code-verifier needs to
2033
- * produce the verdict the verified-commit gate requires, which the SubagentStop
2034
- * minter cannot mint for workflow-spawned agents. When verification fails (or the
1967
+ * Fix lens: edit (clean-coder, no commit) -> review (code-quality-agent under
1968
+ * the canonical review guide) -> commit (clean-coder, one commit + push, no
1969
+ * edits). When review fails (or the
2035
1970
  * edit step stalled with no thread to resolve), the commit step is skipped and the
2036
1971
  * unchanged HEAD is returned so the round reads as not-progressed.
2037
1972
  * @param {string} head PR HEAD SHA the findings were raised against
@@ -2264,11 +2199,8 @@ function runRefutedThreadResolution(head, refutedFindings) {
2264
2199
  /**
2265
2200
  * Address the gates a convergence check reported as failing, then hand control
2266
2201
  * back to the converge phase: edit (clean-coder resolves bot threads, applies
2267
- * any fix and rebase in the working tree, no push) -> verify (code-verifier
2268
- * emits a verdict fence binding the working tree) -> commit (clean-coder, one
2269
- * commit + push, no edits). Splitting the edit from the push lets a workflow
2270
- * code-verifier produce the verdict the verified-commit gate requires for the
2271
- * bot-thread fix commit and the post-rebase force-push. When the edit resolved
2202
+ * any fix and rebase in the working tree, no push) -> review -> commit
2203
+ * (clean-coder, one commit + push, no edits). When the edit resolved
2272
2204
  * the gates with neither a code change nor a rebase, or the verify step fails,
2273
2205
  * the commit step is skipped and the unchanged HEAD is returned.
2274
2206
  * @param {string} head current PR HEAD SHA
@@ -2289,10 +2221,10 @@ async function repairConvergence(head, failures) {
2289
2221
  }
2290
2222
  }
2291
2223
  const verifyTranscript = await verifyWithRecovery({
2292
- runVerify: () => runVerifierTask('repair-verify', { head, failures }),
2224
+ runVerify: () => runReviewTask('repair-verify', { head, failures }),
2293
2225
  runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'repair', objection, attempt }),
2294
2226
  })
2295
- if (!verdictPassed(verifyTranscript)) {
2227
+ if (!reviewPassed(verifyTranscript)) {
2296
2228
  return {
2297
2229
  newSha: head,
2298
2230
  pushed: false,
@@ -2303,9 +2235,15 @@ async function repairConvergence(head, failures) {
2303
2235
  }
2304
2236
  }
2305
2237
  const wasRebased = editResult?.rebased === true
2238
+ const captureReviewedSurface = () => captureReviewedSurfaceHash({ sourceLabel: 'repair', phase: 'Finalize' })
2239
+ const reviewedSurfaceHash = await captureReviewedSurface()
2240
+ if (reviewedSurfaceHash === null) return reviewedSurfaceUnavailableResult(head)
2306
2241
  return commitWithRecovery({
2242
+ head,
2243
+ reviewedSurfaceHash,
2244
+ captureReviewedSurface,
2307
2245
  runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased }),
2308
- runVerify: () => runVerifierTask('repair-verify', { head, failures }),
2246
+ runVerify: () => runReviewTask('repair-verify', { head, failures }),
2309
2247
  runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'repair', blockerDetail: detail, attempt }),
2310
2248
  })
2311
2249
  }
@@ -2314,12 +2252,12 @@ async function repairConvergence(head, failures) {
2314
2252
  /**
2315
2253
  * Pre-flight conflict resolution: when the PR branch conflicts with its base,
2316
2254
  * rebase it clean before the bug checks run — edit (clean-coder rebases and
2317
- * resolves, no push) -> verify (code-verifier binds a verdict to the rebased
2318
- * tree) -> commit (clean-coder force-with-lease pushes). The conflict decision
2255
+ * resolves, no push) -> review -> commit (clean-coder force-with-lease pushes).
2256
+ * The conflict decision
2319
2257
  * comes from the merged 'preflight-git' probe the caller already ran, so this
2320
2258
  * function spawns no mergeability agent of its own. Returns the post-rebase
2321
2259
  * HEAD so the first converge round runs its lenses on the conflict-free diff.
2322
- * A non-conflicting PR, a rebase the edit step declined, or a failed verdict
2260
+ * A non-conflicting PR, a rebase the edit step declined, or a failed review
2323
2261
  * returns the unchanged HEAD so the run proceeds to the bug checks unchanged.
2324
2262
  * A mid-run conflict (origin/main advancing later) is still caught by the
2325
2263
  * FINALIZE convergence repair, which also rebases.
@@ -2334,13 +2272,19 @@ async function resolveMergeConflicts(head, isConflicting) {
2334
2272
  if (editResult?.rebased !== true) return head
2335
2273
  const failures = ['PR branch had merge conflicts with origin/main; the rebase must leave a clean, conflict-free tree']
2336
2274
  const verifyTranscript = await verifyWithRecovery({
2337
- runVerify: () => runVerifierTask('repair-verify', { head, failures }),
2275
+ runVerify: () => runReviewTask('repair-verify', { head, failures }),
2338
2276
  runRecoverEdit: (objection, attempt) => runCodeEditorTask('verify-recover', { head, sourceLabel: 'conflict-rebase', objection, attempt }),
2339
2277
  })
2340
- if (!verdictPassed(verifyTranscript)) return head
2278
+ if (!reviewPassed(verifyTranscript)) return head
2279
+ const captureReviewedSurface = () => captureReviewedSurfaceHash({ sourceLabel: 'conflict-rebase', phase: 'Converge' })
2280
+ const reviewedSurfaceHash = await captureReviewedSurface()
2281
+ if (reviewedSurfaceHash === null) return head
2341
2282
  const commitResult = await commitWithRecovery({
2283
+ head,
2284
+ reviewedSurfaceHash,
2285
+ captureReviewedSurface,
2342
2286
  runCommit: () => runCodeEditorTask('repair-commit', { head, wasRebased: true }),
2343
- runVerify: () => runVerifierTask('repair-verify', { head, failures }),
2287
+ runVerify: () => runReviewTask('repair-verify', { head, failures }),
2344
2288
  runRecoverEdit: (detail, attempt) => runCodeEditorTask('commit-recover', { head, sourceLabel: 'conflict-rebase', blockerDetail: detail, attempt }),
2345
2289
  })
2346
2290
  return commitResult?.newSha || head
@@ -2426,9 +2370,9 @@ async function applyP2OnlyFix(head, findings, sourceLabel, stallSubject, noPushS
2426
2370
  }
2427
2371
 
2428
2372
  /**
2429
- * Route after a successful P2-only fix. HEAD-bound CLEAN/review stamps are
2430
- * SHA-specific: a push that moves HEAD must re-enter CONVERGE so those stamps
2431
- * rebuild. A no-push progress (resolvedWithoutCommit) advances to the next
2373
+ * Route after a successful P2-only fix. A push that moves HEAD re-enters
2374
+ * CONVERGE so the current review state rebuilds. A no-push progress
2375
+ * (resolvedWithoutCommit) advances to the next
2432
2376
  * phase on the same HEAD.
2433
2377
  *
2434
2378
  * ::
@@ -2604,10 +2548,8 @@ function parseDeferredPr(prUrl) {
2604
2548
  * Defer a standards-only round: edit (clean-coder files the follow-up fix issue,
2605
2549
  * stages an environment-hardening hooks/rules change in the config repo's
2606
2550
  * working tree without committing, and resolves the PR's code-standard threads)
2607
- * -> verify (code-verifier binds a verdict to the hardening working tree) ->
2608
- * commit (clean-coder makes one commit, pushes, and opens the DRAFT hardening
2609
- * PR). Splitting the edit from the push lets a workflow code-verifier produce the
2610
- * verdict the verified-commit gate requires for the cross-repo hardening commit.
2551
+ * -> review -> commit (clean-coder makes one commit, pushes, and opens the DRAFT
2552
+ * hardening PR).
2611
2553
  * This PR's own branch is never touched. When a hardening PR already opened for
2612
2554
  * this run, the edit staged no hardening, or the verify step fails, the follow-up
2613
2555
  * issue still stands and the commit step is skipped.
@@ -2628,10 +2570,31 @@ async function spawnStandardsFollowUp(head, findings, sourceLabel, hasHardeningP
2628
2570
  if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
2629
2571
  return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
2630
2572
  }
2631
- const verifyTranscript = await runVerifierTask('hardening-verify', {
2573
+ const verifyTranscript = await runReviewTask('hardening-verify', {
2632
2574
  head, sourceLabel, hardeningRepoPath: editResult.hardeningRepoPath, hardeningBranch: editResult.hardeningBranch,
2633
2575
  })
2634
- if (!verdictPassed(verifyTranscript)) {
2576
+ if (!reviewPassed(verifyTranscript)) {
2577
+ return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
2578
+ }
2579
+ const hardeningReviewContext = {
2580
+ head,
2581
+ sourceLabel,
2582
+ hardeningRepoPath: editResult.hardeningRepoPath,
2583
+ hardeningBranch: editResult.hardeningBranch,
2584
+ }
2585
+ const captureReviewedSurface = () =>
2586
+ captureReviewedSurfaceHash({
2587
+ sourceLabel: `hardening:${sourceLabel}`,
2588
+ phase: 'Converge',
2589
+ repositoryPath: editResult.hardeningRepoPath,
2590
+ })
2591
+ const reviewedSurfaceHash = await captureReviewedSurface()
2592
+ const stableReviewedSurfaceHash = await rebindReviewedSurface({
2593
+ reviewedSurfaceHash,
2594
+ captureReviewedSurface,
2595
+ runVerify: () => runReviewTask('hardening-verify', hardeningReviewContext),
2596
+ })
2597
+ if (stableReviewedSurfaceHash === null) {
2635
2598
  return { followUpIssueFiled, issueUrl: followUpIssueUrl, hardeningPrOpened: false, deferredPr: null }
2636
2599
  }
2637
2600
  const commitResult = await runCodeEditorTask('hardening-commit', {