claude-dev-env 1.92.1 → 1.93.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 (214) hide show
  1. package/CLAUDE.md +6 -2
  2. package/_shared/CLAUDE.md +2 -1
  3. package/_shared/advisor/CLAUDE.md +21 -0
  4. package/_shared/advisor/advisor-protocol.md +142 -0
  5. package/_shared/advisor/scripts/config/advisor_scripts_constants/__init__.py +1 -0
  6. package/_shared/advisor/scripts/config/advisor_scripts_constants/model_tier_run_validator_constants.py +86 -0
  7. package/_shared/advisor/scripts/model_tier_run_validator.py +246 -0
  8. package/_shared/advisor/scripts/pyproject.toml +3 -0
  9. package/_shared/advisor/scripts/tests/test_model_tier_run_validator.py +302 -0
  10. package/_shared/advisor/scripts/tests/test_tier_model_ids.py +180 -0
  11. package/_shared/advisor/scripts/tier_model_ids.py +164 -0
  12. package/_shared/pr-loop/CLAUDE.md +1 -0
  13. package/_shared/pr-loop/precatch-rubric.md +65 -0
  14. package/_shared/pr-loop/scripts/CLAUDE.md +1 -1
  15. package/_shared/pr-loop/scripts/code_rules_gate.py +137 -5
  16. package/_shared/pr-loop/scripts/post_audit_thread.py +1 -1
  17. package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -1
  18. package/_shared/pr-loop/scripts/pr_loop_shared_constants/code_rules_gate_constants.py +16 -0
  19. package/_shared/pr-loop/scripts/pr_loop_shared_constants/reviews_disabled_constants.py +2 -1
  20. package/_shared/pr-loop/scripts/reviewer_availability.py +42 -12
  21. package/_shared/pr-loop/scripts/reviews_disabled.py +65 -21
  22. package/_shared/pr-loop/scripts/terminology_sweep.py +69 -21
  23. package/_shared/pr-loop/scripts/tests/CLAUDE.md +1 -1
  24. package/_shared/pr-loop/scripts/tests/fixtures/{copilot_internal_user_jonecho.json → copilot_internal_user_example.json} +1 -1
  25. package/_shared/pr-loop/scripts/tests/test_agent_config_carveout.py +1 -1
  26. package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +146 -2
  27. package/_shared/pr-loop/scripts/tests/test_copilot_quota.py +8 -8
  28. package/_shared/pr-loop/scripts/tests/test_reviewer_availability.py +18 -5
  29. package/_shared/pr-loop/scripts/tests/test_reviews_disabled.py +56 -5
  30. package/_shared/pr-loop/scripts/tests/test_terminology_sweep.py +104 -17
  31. package/agents/CLAUDE.md +3 -2
  32. package/agents/clasp-deployment-orchestrator.md +2 -2
  33. package/agents/code-advisor.md +7 -5
  34. package/agents/code-verifier.md +6 -2
  35. package/agents/session-advisor.md +27 -0
  36. package/audit-rubrics/category_rubrics/category-j-code-rules-compliance.md +1 -1
  37. package/audit-rubrics/category_rubrics/category-k-codebase-conflicts.md +1 -1
  38. package/audit-rubrics/prompts/category-a-api-contracts.md +2 -2
  39. package/audit-rubrics/prompts/category-b-selector-engine-compat.md +2 -2
  40. package/audit-rubrics/prompts/category-c-resource-cleanup.md +2 -2
  41. package/audit-rubrics/prompts/category-d-scoping-and-ordering.md +2 -2
  42. package/audit-rubrics/prompts/category-e-dead-code.md +2 -2
  43. package/audit-rubrics/prompts/category-f-silent-failures.md +2 -2
  44. package/audit-rubrics/prompts/category-g-bounds-and-overflow.md +2 -2
  45. package/audit-rubrics/prompts/category-h-security-boundaries.md +2 -2
  46. package/audit-rubrics/prompts/category-i-concurrency.md +2 -2
  47. package/audit-rubrics/prompts/category-j-code-rules-compliance.md +2 -2
  48. package/audit-rubrics/prompts/category-k-codebase-conflicts.md +2 -2
  49. package/audit-rubrics/prompts/category-l-behavior-equivalence.md +2 -2
  50. package/audit-rubrics/prompts/category-m-producer-consumer-cardinality.md +2 -2
  51. package/audit-rubrics/prompts/category-n-test-name-scenario-verifier.md +2 -2
  52. package/audit-rubrics/prompts/category-o-docstring-vs-impl-drift.md +2 -2
  53. package/audit-rubrics/prompts/category-p-name-vs-behavior-contract.md +2 -2
  54. package/bin/CLAUDE.md +1 -0
  55. package/bin/expand_home_directory_tokens.mjs +75 -0
  56. package/bin/install.mjs +21 -7
  57. package/bin/install.test.mjs +220 -10
  58. package/hooks/CLAUDE.md +1 -1
  59. package/hooks/blocking/CLAUDE.md +4 -1
  60. package/hooks/blocking/_path_setup.py +13 -0
  61. package/hooks/blocking/bash_pre_tool_use_dispatcher.py +281 -0
  62. package/hooks/blocking/code_rules_dead_module_constant.py +88 -16
  63. package/hooks/blocking/code_rules_enforcer.py +6 -0
  64. package/hooks/blocking/code_rules_imports_logging.py +34 -1
  65. package/hooks/blocking/code_rules_paths_syspath.py +1 -1
  66. package/hooks/blocking/code_rules_test_layout.py +272 -0
  67. package/hooks/blocking/nas_ssh_binary_enforcer.py +22 -12
  68. package/hooks/blocking/pii_prevention_blocker.py +781 -0
  69. package/hooks/blocking/pii_scanner.py +340 -0
  70. package/hooks/blocking/pre_tool_use_dispatcher.py +3 -3
  71. package/hooks/blocking/stop_dispatcher.py +84 -0
  72. package/hooks/blocking/test__path_setup.py +39 -0
  73. package/hooks/blocking/test_bash_dispatcher_interpreter_starts.py +61 -0
  74. package/hooks/blocking/test_bash_pre_tool_use_dispatcher.py +294 -0
  75. package/hooks/blocking/test_code_rules_enforcer_cross_skill_duplicate.py +1 -1
  76. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant.py +51 -0
  77. package/hooks/blocking/test_code_rules_enforcer_dead_module_constant_read_cap.py +103 -0
  78. package/hooks/blocking/test_code_rules_enforcer_dot_test_pattern.py +1 -1
  79. package/hooks/blocking/test_code_rules_enforcer_file_global_constants.py +1 -1
  80. package/hooks/blocking/test_code_rules_enforcer_hardcoded_user_path.py +11 -11
  81. package/hooks/blocking/test_code_rules_enforcer_naive_datetime.py +1 -1
  82. package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +1 -1
  83. package/hooks/blocking/test_code_rules_enforcer_test_layout.py +111 -0
  84. package/hooks/blocking/test_code_rules_enforcer_type_checking_scope.py +76 -0
  85. package/hooks/blocking/test_destructive_command_blocker.py +29 -9
  86. package/hooks/blocking/test_gh_body_arg_blocker.py +3 -3
  87. package/hooks/blocking/test_nas_ssh_binary_enforcer.py +96 -39
  88. package/hooks/blocking/test_pii_prevention_blocker.py +905 -0
  89. package/hooks/blocking/test_pii_prevention_windows_git_path.py +41 -0
  90. package/hooks/blocking/test_pii_scanner.py +165 -0
  91. package/hooks/blocking/test_pr_description_enforcer_pr_number.py +1 -1
  92. package/hooks/blocking/test_pre_tool_use_dispatcher.py +14 -13
  93. package/hooks/blocking/test_stop_dispatcher.py +187 -0
  94. package/hooks/blocking/test_verdict_directory_write_blocker.py +1 -1
  95. package/hooks/blocking/test_volatile_path_in_post_blocker.py +3 -3
  96. package/hooks/blocking/volatile_path_in_post_blocker.py +54 -5
  97. package/hooks/diagnostic/migrations/README.md +25 -26
  98. package/hooks/diagnostic/test_hook_log_extractor.py +10 -10
  99. package/hooks/hooks.json +10 -120
  100. package/hooks/hooks_constants/CLAUDE.md +12 -3
  101. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +78 -0
  102. package/hooks/hooks_constants/dead_module_constant_constants.py +1 -0
  103. package/hooks/hooks_constants/hosted_hook_runner.py +73 -0
  104. package/hooks/hooks_constants/local_identity.py +182 -0
  105. package/hooks/hooks_constants/nas_ssh_binary_enforcer_constants.py +4 -37
  106. package/hooks/hooks_constants/pii_prevention_constants.py +299 -0
  107. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +4 -0
  108. package/hooks/hooks_constants/python_style_checks_constants.py +15 -0
  109. package/hooks/hooks_constants/stop_dispatcher_constants.py +26 -0
  110. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +87 -0
  111. package/hooks/hooks_constants/test_hardcoded_user_path_constants.py +2 -2
  112. package/hooks/hooks_constants/test_hosted_hook_runner.py +101 -0
  113. package/hooks/hooks_constants/test_layout_constants.py +28 -0
  114. package/hooks/hooks_constants/test_local_identity.py +88 -0
  115. package/hooks/hooks_constants/test_session_env_cleanup_constants.py +1 -1
  116. package/hooks/hooks_constants/test_stop_dispatcher_constants.py +32 -0
  117. package/hooks/validators/README.md +5 -6
  118. package/hooks/validators/__init__.py +2 -2
  119. package/hooks/validators/python_style_checks.py +298 -243
  120. package/hooks/validators/run_all_validators.py +6 -0
  121. package/hooks/validators/test_python_style_checks.py +278 -163
  122. package/package.json +2 -2
  123. package/rules/CLAUDE.md +2 -1
  124. package/rules/bdd.md +1 -1
  125. package/rules/nas-ssh-invocation.md +6 -4
  126. package/rules/no-justification-noise.md +61 -0
  127. package/scripts/test_setup_project_paths.py +1 -1
  128. package/skills/CLAUDE.md +6 -3
  129. package/skills/_shared/pr-loop/scripts/_path_resolver.py +2 -2
  130. package/skills/_shared/pr-loop/scripts/build_audit_prompt.py +1 -1
  131. package/skills/_shared/pr-loop/scripts/build_fix_prompt.py +1 -1
  132. package/skills/_shared/pr-loop/scripts/test__path_resolver.py +42 -3
  133. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +11 -11
  134. package/skills/_shared/pr-loop/scripts/test_build_fix_prompt.py +26 -3
  135. package/skills/_shared/pr-loop/scripts/test_preflight_worktree.py +49 -25
  136. package/skills/autoconverge/CLAUDE.md +3 -3
  137. package/skills/autoconverge/SKILL.md +113 -35
  138. package/skills/autoconverge/reference/CLAUDE.md +2 -2
  139. package/skills/autoconverge/reference/convergence.md +44 -22
  140. package/skills/autoconverge/reference/gotchas.md +11 -0
  141. package/skills/autoconverge/reference/stop-conditions.md +23 -13
  142. package/skills/autoconverge/workflow/CLAUDE.md +2 -1
  143. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +15 -19
  144. package/skills/autoconverge/workflow/converge.contract.test.mjs +43 -42
  145. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +141 -16
  146. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +2 -2
  147. package/skills/autoconverge/workflow/converge.mjs +329 -68
  148. package/skills/autoconverge/workflow/converge.path-aware.test.mjs +1 -1
  149. package/skills/autoconverge/workflow/converge.precatch.test.mjs +152 -0
  150. package/skills/autoconverge/workflow/converge.run-input.test.mjs +3 -3
  151. package/skills/autoconverge/workflow/converge_multi.run-input.test.mjs +15 -15
  152. package/skills/autoconverge/workflow/test_convergence_summary.py +6 -6
  153. package/skills/bdd-protocol/SKILL.md +4 -5
  154. package/skills/bdd-protocol/references/anti-patterns.md +1 -1
  155. package/skills/bugteam/SKILL.md +8 -0
  156. package/skills/bugteam/reference/copilot-gap-analysis.md +20 -20
  157. package/skills/bugteam/reference/obstacles/audit-walk-categories.md +1 -1
  158. package/skills/bugteam/reference/team-setup.md +7 -5
  159. package/skills/bugteam/scripts/reflow_skill_md.py +1 -1
  160. package/skills/copilot-finding-triage/SKILL.md +124 -0
  161. package/skills/copilot-finding-triage/reference/tier-rubric.md +49 -0
  162. package/skills/copilot-finding-triage/scripts/conftest.py +8 -0
  163. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/__init__.py +0 -0
  164. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/__init__.py +0 -0
  165. package/skills/copilot-finding-triage/scripts/copilot_finding_triage_constants/config/notify_ntfy_constants.py +32 -0
  166. package/skills/copilot-finding-triage/scripts/notify_ntfy.py +192 -0
  167. package/skills/copilot-finding-triage/scripts/test_notify_ntfy.py +141 -0
  168. package/skills/copilot-finding-triage/templates/notification.md +34 -0
  169. package/skills/copilot-review/SKILL.md +8 -6
  170. package/skills/findbugs/SKILL.md +4 -0
  171. package/skills/fixbugs/SKILL.md +8 -7
  172. package/skills/gotcha/CLAUDE.md +2 -2
  173. package/skills/gotcha/SKILL.md +4 -4
  174. package/skills/log-audit/SKILL.md +4 -6
  175. package/skills/monitor-open-prs/CLAUDE.md +1 -1
  176. package/skills/monitor-open-prs/SKILL.md +6 -1
  177. package/skills/orchestrator/SKILL.md +74 -78
  178. package/skills/orchestrator-refresh/SKILL.md +24 -12
  179. package/skills/post-audit-findings/SKILL.md +5 -9
  180. package/skills/pr-consistency-audit/SKILL.md +5 -1
  181. package/skills/pr-converge/CLAUDE.md +1 -1
  182. package/skills/pr-converge/SKILL.md +86 -47
  183. package/skills/pr-converge/reference/CLAUDE.md +1 -1
  184. package/skills/pr-converge/reference/convergence-gates.md +20 -17
  185. package/skills/pr-converge/reference/examples.md +63 -47
  186. package/skills/pr-converge/reference/fix-protocol.md +15 -15
  187. package/skills/pr-converge/reference/ground-rules.md +11 -7
  188. package/skills/pr-converge/reference/multi-pr-orchestration.md +6 -5
  189. package/skills/pr-converge/reference/per-tick.md +129 -107
  190. package/skills/pr-converge/reference/state-schema.md +15 -10
  191. package/skills/pr-converge/scripts/check_convergence.py +5 -4
  192. package/skills/pr-converge/scripts/test_check_convergence.py +6 -4
  193. package/skills/pr-fix-protocol/SKILL.md +3 -8
  194. package/skills/pr-loop-cloud-transport/SKILL.md +102 -0
  195. package/skills/pr-loop-cloud-transport/reference/identity-and-hooks.md +40 -0
  196. package/skills/pr-loop-cloud-transport/reference/substitution-matrix.md +48 -0
  197. package/skills/pr-loop-lifecycle/SKILL.md +5 -9
  198. package/skills/pr-scope-resolve/SKILL.md +3 -6
  199. package/skills/privacy-hygiene/SKILL.md +114 -0
  200. package/skills/qbug/SKILL.md +8 -8
  201. package/skills/rebase/SKILL.md +5 -1
  202. package/skills/refine/SKILL.md +4 -5
  203. package/skills/reviewer-gates/SKILL.md +7 -11
  204. package/skills/session-log/SKILL.md +4 -1
  205. package/skills/skill-builder/SKILL.md +3 -6
  206. package/skills/structure-prompt/SKILL.md +4 -5
  207. package/skills/team-advisor/SKILL.md +56 -0
  208. package/skills/test_markdown_link_integrity.py +10 -6
  209. package/skills/update/SKILL.md +5 -1
  210. package/skills/usage-pause/SKILL.md +14 -5
  211. package/skills/usage-pause/scripts/resolve_usage_window.py +83 -5
  212. package/skills/usage-pause/scripts/test_resolve_usage_window.py +185 -17
  213. package/skills/usage-pause/scripts/usage_pause_constants/resolve_usage_window_constants.py +4 -3
  214. package/skills/verified-build/SKILL.md +4 -9
@@ -119,37 +119,21 @@ test('the reviewer-availability probe rides the merged preflight-git spawn, not
119
119
  );
120
120
  });
121
121
 
122
- test('the Bugbot lens is not spawned pre-spawn when the shared gate reports Bugbot down', () => {
122
+ test('the round parallel array holds exactly the three internal lenses in order and no Bugbot slot', () => {
123
123
  const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
124
124
  assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
125
- const isBugbotDownPreSpawnIndex = convergeSource.indexOf('const isBugbotDownPreSpawn = resolveReviewerDown(');
126
- assert.notEqual(isBugbotDownPreSpawnIndex, -1, 'expected a pre-spawn Bugbot-down decision computed before the lens array');
127
- assert.ok(
128
- isBugbotDownPreSpawnIndex < parallelLensIndex,
129
- 'expected the pre-spawn Bugbot-down decision to be computed before the parallel lens array',
130
- );
131
125
  const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
132
126
  const lensArray = convergeSource.slice(parallelLensIndex, lensArrayEnd);
133
- assert.match(
134
- lensArray,
135
- /isBugbotDownPreSpawn \? Promise\.resolve\(\{ sha: head, clean: true, down: true, notSpawned: true, findings: \[\] \}\) : runBugbotLens\(head, reviewerAvailability\)/,
136
- 'expected the Bugbot lens slot to substitute a resolved down placeholder instead of spawning runBugbotLens when isBugbotDownPreSpawn is true',
137
- );
138
- });
139
-
140
- test('the post-round bugbotDown verdict treats a dead Bugbot lens as down, not fail-open', () => {
141
- const parallelLensIndex = convergeSource.indexOf('const lenses = await parallel(');
142
- assert.notEqual(parallelLensIndex, -1, 'expected the parallel lens block to exist');
143
- const lensArrayEnd = convergeSource.indexOf('])', parallelLensIndex);
144
- const bugbotDownAssignIndex = convergeSource.indexOf('bugbotDown = ', lensArrayEnd);
145
- assert.notEqual(bugbotDownAssignIndex, -1, 'expected the post-round bugbotDown assignment after the lens array');
146
- const bugbotDownAssignLineEnd = convergeSource.indexOf('\n', bugbotDownAssignIndex);
147
- const bugbotDownAssignLine = convergeSource.slice(bugbotDownAssignIndex, bugbotDownAssignLineEnd);
148
- assert.match(
149
- bugbotDownAssignLine,
150
- /bugbotDown = lenses\[0\] == null \? true : resolveReviewerDown\(lenses\[0\], input\.bugbotDisabled \|\| false\)/,
151
- 'expected a dead (null) Bugbot lens result to report bugbotDown:true — resolveReviewerDown alone is fail-open on null, which would wrongly report bugbotDown:false for a lens agent that actually spawned and died, leaving check_convergence.py to look for a bugbot review that was never posted',
127
+ const codeReviewIndex = lensArray.indexOf('runCodeReviewLens(head, reviewerAvailability)');
128
+ const auditIndex = lensArray.indexOf('runAuditLens(head, reviewerAvailability)');
129
+ const selfReviewIndex = lensArray.indexOf('runSelfReviewLens(head, reviewerAvailability)');
130
+ assert.ok(codeReviewIndex !== -1 && auditIndex !== -1 && selfReviewIndex !== -1, 'expected the three internal lenses in the parallel array');
131
+ assert.ok(
132
+ codeReviewIndex < auditIndex && auditIndex < selfReviewIndex,
133
+ 'expected the code-review, bug-audit, self-review order to match LENS_NAMES',
152
134
  );
135
+ assert.doesNotMatch(lensArray, /runBugbotLens/, 'expected Bugbot to leave the per-round parallel and become a terminal gate');
136
+ assert.doesNotMatch(convergeSource, /isBugbotDownPreSpawn/, 'expected the pre-spawn Bugbot-down decision to be gone from the round');
153
137
  });
154
138
 
155
139
  test('both reviewer trigger commands carry the CLAUDE_REVIEWER_GATE=autoconverge sentinel', () => {
@@ -324,8 +308,8 @@ test('each fix push, each lens-retry, and the convergence repair invalidate the
324
308
  const invalidationMatches = convergeSource.match(/^ +head = null$/gm) || [];
325
309
  assert.equal(
326
310
  invalidationMatches.length,
327
- 6,
328
- 'expected head invalidation after the CONVERGE fix push, the COPILOT fix push, the convergence repair, the all-lenses-dead retry, the not-clean-no-findings retry, and the all-clean no-lens-ran clean-audit refusal',
311
+ 8,
312
+ 'expected head invalidation after the static-sweep fix push, the CONVERGE fix push, the terminal-Bugbot fix push, the COPILOT fix push, the convergence repair, the all-lenses-dead retry, the not-clean-no-findings retry, and the all-clean no-lens-ran clean-audit refusal',
329
313
  );
330
314
  });
331
315
 
@@ -625,12 +609,12 @@ test('the standards-deferral surfaces disclose the hardening-PR state unconditio
625
609
  );
626
610
  });
627
611
 
628
- test('both standards-deferral call sites build standardsNote from the spawnStandardsFollowUp outcome', () => {
612
+ test('every standards-deferral call site builds standardsNote from the spawnStandardsFollowUp outcome', () => {
629
613
  const callSiteUses = convergeSource.match(/standardsNote = standardsDeferralNote\(/g) || [];
630
614
  assert.equal(
631
615
  callSiteUses.length,
632
- 2,
633
- 'expected both standards-deferral call sites to build standardsNote via standardsDeferralNote(...)',
616
+ 3,
617
+ 'expected the converge-round, terminal-Bugbot, and Copilot standards call sites to each build standardsNote via standardsDeferralNote(...)',
634
618
  );
635
619
  assert.doesNotMatch(
636
620
  convergeSource,
@@ -741,8 +725,8 @@ for (const [helperName, taskName] of editStepTasks) {
741
725
  }
742
726
 
743
727
  function preambleText() {
744
- const preambleStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
745
- assert.notEqual(preambleStart, -1, 'expected HEADLESS_SAFETY_PREAMBLE to exist');
728
+ const preambleStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
729
+ assert.notEqual(preambleStart, -1, 'expected HEADLESS_EDIT_PREAMBLE to exist');
746
730
  const preambleEnd = convergeSource.indexOf('\n\nlet ', preambleStart);
747
731
  return convergeSource.slice(preambleStart, preambleEnd === -1 ? undefined : preambleEnd);
748
732
  }
@@ -850,15 +834,24 @@ test('SKILL.md attributes the known-temp-var resolution to the cwd-scoped auto-a
850
834
  );
851
835
  });
852
836
 
853
- test('convergeAgent prepends HEADLESS_SAFETY_PREAMBLE and the worktree directive to every prompt', () => {
837
+ test('convergeAgent prepends HEADLESS_EDIT_PREAMBLE and the worktree directive to every prompt', () => {
854
838
  const convergeAgentBody = lensPromptBody('convergeAgent');
855
839
  assert.match(
856
840
  convergeAgentBody,
857
- /HEADLESS_SAFETY_PREAMBLE.*worktreeDirective/,
841
+ /HEADLESS_EDIT_PREAMBLE.*worktreeDirective/,
858
842
  'expected convergeAgent to prepend both preamble and worktree directive',
859
843
  );
860
844
  });
861
845
 
846
+ test('convergeReadOnlyAgent prepends HEADLESS_READONLY_PREAMBLE and the worktree directive to every read-only prompt', () => {
847
+ const readOnlyAgentBody = lensPromptBody('convergeReadOnlyAgent');
848
+ assert.match(
849
+ readOnlyAgentBody,
850
+ /HEADLESS_READONLY_PREAMBLE.*worktreeDirective/,
851
+ 'expected convergeReadOnlyAgent to prepend the trimmed read-only preamble and the worktree directive',
852
+ );
853
+ });
854
+
862
855
  const taskDispatchers = [
863
856
  { name: 'runGitTask', isAsync: false },
864
857
  { name: 'runFixerTask', isAsync: false },
@@ -986,8 +979,8 @@ test('extractVerifyObjection calls parseLastVerdictFence', () => {
986
979
  });
987
980
 
988
981
  test('the headless preamble routes waits through the Monitor tool and forbids ending a turn to await work', () => {
989
- const preambleStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
990
- assert.notEqual(preambleStart, -1, 'expected a HEADLESS_SAFETY_PREAMBLE definition');
982
+ const preambleStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
983
+ assert.notEqual(preambleStart, -1, 'expected a HEADLESS_EDIT_PREAMBLE definition');
991
984
  const preambleEnd = convergeSource.indexOf('\n\nlet activeRepoPath', preambleStart);
992
985
  assert.notEqual(preambleEnd, -1, 'expected the preamble to end before activeRepoPath');
993
986
  const preamble = convergeSource.slice(preambleStart, preambleEnd);
@@ -998,7 +991,7 @@ test('the headless preamble routes waits through the Monitor tool and forbids en
998
991
  });
999
992
 
1000
993
  test('the preamble describes the Monitor wait by its real contract: a bounded until-loop consuming notifications as they arrive, rather than an in-turn synchronous return', () => {
1001
- const blockStart = convergeSource.indexOf('const HEADLESS_SAFETY_PREAMBLE =');
994
+ const blockStart = convergeSource.indexOf('const HEADLESS_EDIT_PREAMBLE =');
1002
995
  assert.ok(blockStart !== -1, 'expected a wait-safety preamble to exist');
1003
996
  const blockEnd = convergeSource.indexOf('\n\nlet activeRepoPath', blockStart);
1004
997
  assert.ok(blockEnd !== -1, 'expected the preamble to end before activeRepoPath');
@@ -1129,8 +1122,8 @@ test('every convergeAgent spawn options object carries a model and effort tier',
1129
1122
  }
1130
1123
  });
1131
1124
 
1132
- test('the four review lenses run on the opusMedium tier', () => {
1133
- for (const label of ['lens:bugbot', 'lens:code-review', 'lens:bug-audit', 'lens:reuse']) {
1125
+ test('the semantic review lenses and the terminal Bugbot gate run on the opusMedium tier', () => {
1126
+ for (const label of ['lens:bugbot', 'lens:code-review', 'lens:bug-audit', 'lens:self-review', 'lens:reuse']) {
1134
1127
  assert.match(
1135
1128
  optionsLineForLabel(label),
1136
1129
  /\.\.\.TIERS\.opusMedium/,
@@ -1139,6 +1132,14 @@ test('the four review lenses run on the opusMedium tier', () => {
1139
1132
  }
1140
1133
  });
1141
1134
 
1135
+ test('the deterministic static sweep runs on the cheaper sonnetMedium tier', () => {
1136
+ assert.match(
1137
+ optionsLineForLabel('lens:static-sweep'),
1138
+ /\.\.\.TIERS\.sonnetMedium/,
1139
+ 'expected the deterministic static sweep to run on the sonnet tier, not opus',
1140
+ );
1141
+ });
1142
+
1142
1143
  test('the copilot gate runs on the haikuLow tier', () => {
1143
1144
  assert.match(optionsLineForLabel('copilot-gate'), /\.\.\.TIERS\.haikuLow/);
1144
1145
  });
@@ -1161,8 +1162,8 @@ test('the preflight-git task returns the changed-file list and diffstat for the
1161
1162
  assert.match(schema, /required:[^\n]*'changedFiles'[^\n]*'diffstat'/);
1162
1163
  });
1163
1164
 
1164
- test('each per-round lens and the reuse lens inject the preflight changed-file context', () => {
1165
- for (const builder of ['runBugbotLens', 'runCodeReviewLens', 'runAuditLens', 'runReuseAuditPass']) {
1165
+ test('each per-round lens, the static sweep, the reuse lens, and the terminal Bugbot gate inject the preflight changed-file context', () => {
1166
+ for (const builder of ['runStaticSweep', 'runCodeReviewLens', 'runAuditLens', 'runSelfReviewLens', 'runReuseAuditPass', 'runBugbotLens']) {
1166
1167
  assert.match(
1167
1168
  lensPromptBody(builder),
1168
1169
  /renderLensDiffContext\(preflightResult\)/,
@@ -16,12 +16,36 @@ function functionBody(functionName) {
16
16
  }
17
17
 
18
18
  const productionModule = new Function(
19
- `${functionBody('classifyCopilotOutcome')}\n` +
19
+ `${functionBody('classifyReviewerGateOutcome')}\n` +
20
+ `${functionBody('classifyCopilotOutcome')}\n` +
21
+ `${functionBody('hasCodeConcernFinding')}\n` +
22
+ `${functionBody('buildUserReview')}\n` +
20
23
  `${functionBody('resolveCopilotDown')}\n` +
21
24
  `${functionBody('resolveReviewerDown')}\n` +
22
- 'return { classifyCopilotOutcome, resolveCopilotDown, resolveReviewerDown };',
25
+ 'return { classifyReviewerGateOutcome, classifyCopilotOutcome, hasCodeConcernFinding, buildUserReview, resolveCopilotDown, resolveReviewerDown };',
23
26
  )();
24
- const { classifyCopilotOutcome, resolveCopilotDown, resolveReviewerDown } = productionModule;
27
+ const {
28
+ classifyReviewerGateOutcome,
29
+ classifyCopilotOutcome,
30
+ hasCodeConcernFinding,
31
+ buildUserReview,
32
+ resolveCopilotDown,
33
+ resolveReviewerDown,
34
+ } = productionModule;
35
+
36
+ function copilotFinding(overrides) {
37
+ return {
38
+ file: 'a.py',
39
+ line: 1,
40
+ severity: 'P1',
41
+ category: 'bug',
42
+ tier: 'self-healing',
43
+ title: 't',
44
+ detail: 'd',
45
+ replyToCommentId: null,
46
+ ...overrides,
47
+ };
48
+ }
25
49
 
26
50
  function copilotResult(overrides) {
27
51
  return {
@@ -34,26 +58,26 @@ function copilotResult(overrides) {
34
58
  }
35
59
 
36
60
  test('an out-of-usage Copilot result (down) routes to the down kind', () => {
37
- const outcome = classifyCopilotOutcome(copilotResult({ clean: true, down: true }));
61
+ const outcome = classifyReviewerGateOutcome(copilotResult({ clean: true, down: true }));
38
62
  assert.equal(outcome.kind, 'down');
39
63
  });
40
64
 
41
65
  test('a down Copilot result routes to down even when clean is false', () => {
42
- const outcome = classifyCopilotOutcome(copilotResult({ clean: false, down: true }));
66
+ const outcome = classifyReviewerGateOutcome(copilotResult({ clean: false, down: true }));
43
67
  assert.equal(outcome.kind, 'down');
44
68
  });
45
69
 
46
70
  test('a dead Copilot gate agent retries rather than passing', () => {
47
- assert.equal(classifyCopilotOutcome(null).kind, 'retry');
71
+ assert.equal(classifyReviewerGateOutcome(null).kind, 'retry');
48
72
  });
49
73
 
50
74
  test('a reachable Copilot gate with no findings and no clean verdict retries', () => {
51
- const outcome = classifyCopilotOutcome(copilotResult({ clean: false, down: false }));
75
+ const outcome = classifyReviewerGateOutcome(copilotResult({ clean: false, down: false }));
52
76
  assert.equal(outcome.kind, 'retry');
53
77
  });
54
78
 
55
79
  test('Copilot findings route to a fix when Copilot is reachable and not down', () => {
56
- const outcome = classifyCopilotOutcome(
80
+ const outcome = classifyReviewerGateOutcome(
57
81
  copilotResult({
58
82
  findings: [
59
83
  {
@@ -71,6 +95,61 @@ test('Copilot findings route to a fix when Copilot is reachable and not down', (
71
95
  assert.equal(outcome.kind, 'fix');
72
96
  });
73
97
 
98
+ test('self-healing-only Copilot findings route to a fix when Copilot is reachable and not down', () => {
99
+ const outcome = classifyCopilotOutcome(
100
+ copilotResult({
101
+ findings: [copilotFinding({ tier: 'self-healing' })],
102
+ }),
103
+ );
104
+ assert.equal(outcome.kind, 'fix');
105
+ });
106
+
107
+ test('a code-concern Copilot finding routes to user-review rather than an auto-fix', () => {
108
+ const outcome = classifyCopilotOutcome(
109
+ copilotResult({
110
+ reviewUrl: 'https://github.com/o/r/pull/1#pullrequestreview-9',
111
+ findings: [copilotFinding({ tier: 'code-concern', severity: 'P0' })],
112
+ }),
113
+ );
114
+ assert.equal(outcome.kind, 'user-review');
115
+ assert.equal(outcome.findings.length, 1);
116
+ });
117
+
118
+ test('a mixed round with one code-concern finding routes the whole round to user-review', () => {
119
+ const outcome = classifyCopilotOutcome(
120
+ copilotResult({
121
+ findings: [
122
+ copilotFinding({ tier: 'self-healing' }),
123
+ copilotFinding({ tier: 'code-concern', file: 'b.py', line: 2 }),
124
+ ],
125
+ }),
126
+ );
127
+ assert.equal(outcome.kind, 'user-review');
128
+ });
129
+
130
+ test('hasCodeConcernFinding treats a missing or unexpected tier as a code concern', () => {
131
+ assert.equal(hasCodeConcernFinding([copilotFinding({ tier: 'self-healing' })]), false);
132
+ assert.equal(hasCodeConcernFinding([copilotFinding({ tier: 'code-concern' })]), true);
133
+ assert.equal(hasCodeConcernFinding([{ file: 'a.py', line: 1 }]), true);
134
+ });
135
+
136
+ test('buildUserReview carries the review URL and pares findings to the triage fields', () => {
137
+ const reviewUrl = 'https://github.com/o/r/pull/1#pullrequestreview-9';
138
+ const userReview = buildUserReview(
139
+ copilotResult({ reviewUrl }),
140
+ [copilotFinding({ tier: 'code-concern', severity: 'P0', title: 'unsafe eval', detail: 'ignored' })],
141
+ );
142
+ assert.equal(userReview.reviewUrl, reviewUrl);
143
+ assert.deepEqual(userReview.findings, [
144
+ { file: 'a.py', line: 1, severity: 'P0', tier: 'code-concern', title: 'unsafe eval' },
145
+ ]);
146
+ });
147
+
148
+ test('buildUserReview defaults a missing review URL to an empty string', () => {
149
+ const userReview = buildUserReview(copilotResult({}), [copilotFinding({ tier: 'code-concern' })]);
150
+ assert.equal(userReview.reviewUrl, '');
151
+ });
152
+
74
153
  test('COPILOT_SCHEMA carries a required down field', () => {
75
154
  const schemaStart = convergeSource.indexOf('const COPILOT_SCHEMA =');
76
155
  const schemaEnd = convergeSource.indexOf('const REVIEWER_AVAILABILITY_SCHEMA =');
@@ -85,6 +164,52 @@ test('COPILOT_SCHEMA carries a required down field', () => {
85
164
  );
86
165
  });
87
166
 
167
+ test('the tier enum constant names both routing tiers', () => {
168
+ const tiersMatch = convergeSource.match(/const COPILOT_FINDING_TIERS = \[([^\]]*)\]/);
169
+ assert.notEqual(tiersMatch, null, 'expected a COPILOT_FINDING_TIERS constant');
170
+ assert.match(tiersMatch[1], /'self-healing'/, 'expected the tier enum to name self-healing');
171
+ assert.match(tiersMatch[1], /'code-concern'/, 'expected the tier enum to name code-concern');
172
+ });
173
+
174
+ test('COPILOT_SCHEMA findings carry a required tier enum drawn from the tier constant', () => {
175
+ const schemaStart = convergeSource.indexOf('const COPILOT_FINDINGS_SCHEMA =');
176
+ const schemaEnd = convergeSource.indexOf('const COPILOT_SCHEMA =');
177
+ assert.notEqual(schemaStart, -1, 'expected COPILOT_FINDINGS_SCHEMA to exist');
178
+ const schemaSource = convergeSource.slice(schemaStart, schemaEnd);
179
+ assert.match(schemaSource, /tier:\s*\{/, 'expected a tier property on the Copilot finding schema');
180
+ assert.match(schemaSource, /enum:\s*COPILOT_FINDING_TIERS/, 'expected the tier enum to draw from the tier constant');
181
+ assert.match(schemaSource, /required:\s*\[[^\]]*'tier'[^\]]*\]/, 'expected tier to be a required finding field');
182
+ });
183
+
184
+ test('COPILOT_SCHEMA carries a reviewUrl the user-review payload links to', () => {
185
+ const schemaStart = convergeSource.indexOf('const COPILOT_SCHEMA =');
186
+ const schemaEnd = convergeSource.indexOf('const REVIEWER_AVAILABILITY_SCHEMA =');
187
+ const schemaSource = convergeSource.slice(schemaStart, schemaEnd);
188
+ assert.match(schemaSource, /reviewUrl:\s*\{\s*type:\s*'string'/, 'expected a reviewUrl string field on COPILOT_SCHEMA');
189
+ });
190
+
191
+ test('the Copilot gate prompt tiers each finding and returns the review URL', () => {
192
+ const copilotPrompt = functionBody('runCopilotGate');
193
+ assert.match(copilotPrompt, /tier 'self-healing'/, 'expected the prompt to define the self-healing tier');
194
+ assert.match(copilotPrompt, /tier 'code-concern'/, 'expected the prompt to define the code-concern tier');
195
+ assert.match(copilotPrompt, /in doubt/i, 'expected the prompt to default to code-concern in doubt');
196
+ assert.match(copilotPrompt, /reviewUrl set to the Copilot review html_url/, 'expected the findings branch to return the review URL');
197
+ });
198
+
199
+ test('the COPILOT phase returns blocker user-review with the findings carried through on a code-concern outcome', () => {
200
+ const copilotPhaseStart = convergeSource.indexOf("if (phase === 'COPILOT') {");
201
+ const finalizePhaseStart = convergeSource.indexOf("if (phase === 'FINALIZE') {", copilotPhaseStart);
202
+ const copilotPhase = convergeSource.slice(copilotPhaseStart, finalizePhaseStart);
203
+ const userReviewBranchStart = copilotPhase.indexOf("copilotOutcome.kind === 'user-review'");
204
+ assert.notEqual(userReviewBranchStart, -1, 'expected the COPILOT phase to handle a user-review outcome');
205
+ const userReviewBranch = copilotPhase.slice(userReviewBranchStart, userReviewBranchStart + 500);
206
+ assert.match(userReviewBranch, /blocker:\s*'user-review'/, 'expected the branch to return blocker user-review');
207
+ assert.match(userReviewBranch, /converged:\s*false/, 'expected the branch to return converged false');
208
+ assert.match(userReviewBranch, /userReview:\s*buildUserReview\(copilot, copilotOutcome\.findings\)/, 'expected the branch to carry the user-review payload');
209
+ const autoFixIndex = userReviewBranch.indexOf('applyFixes');
210
+ assert.equal(autoFixIndex, -1, 'expected the user-review branch to not auto-fix the code-concern findings');
211
+ });
212
+
88
213
  test('the Copilot gate prompt detects an out-of-usage notice and returns a down result', () => {
89
214
  const copilotPrompt = functionBody('runCopilotGate');
90
215
  assert.match(
@@ -412,12 +537,12 @@ test('openStandardsFollowUpOnce gates spawnStandardsFollowUp behind the run-once
412
537
  );
413
538
  });
414
539
 
415
- test('both standards-deferral call sites route the create through openStandardsFollowUpOnce', () => {
540
+ test('every standards-deferral call site routes the create through openStandardsFollowUpOnce', () => {
416
541
  const onceCalls = convergeSource.match(/await openStandardsFollowUpOnce\(/g) || [];
417
542
  assert.equal(
418
543
  onceCalls.length,
419
- 2,
420
- 'expected the converge-round and copilot standards call sites to both defer to openStandardsFollowUpOnce',
544
+ 3,
545
+ 'expected the converge-round, terminal-Bugbot, and Copilot standards call sites to all defer to openStandardsFollowUpOnce',
421
546
  );
422
547
  const directCreates = convergeSource.match(/await spawnStandardsFollowUp\(/g) || [];
423
548
  assert.equal(
@@ -497,8 +622,8 @@ function loadParseDeferredPr() {
497
622
  test('parseDeferredPr parses a full canonical hardening PR URL into its coordinates', () => {
498
623
  const parseDeferredPr = loadParseDeferredPr();
499
624
  assert.deepEqual(
500
- parseDeferredPr('https://github.com/jl-cmd/claude-code-config/pull/824'),
501
- { owner: 'jl-cmd', repo: 'claude-code-config', prNumber: 824 },
625
+ parseDeferredPr('https://github.com/jl-cmd/claude-dev-env/pull/824'),
626
+ { owner: 'jl-cmd', repo: 'claude-dev-env', prNumber: 824 },
502
627
  );
503
628
  });
504
629
 
@@ -679,7 +804,7 @@ test('a hardening-commit that opens no PR (empty hardeningPrUrl) leaves the run-
679
804
  test('a later standards-only round resolves its own review threads after the follow-up issue was already filed', async () => {
680
805
  const recordedCalls = [];
681
806
  const issueFiledNoHardening = {
682
- issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/900',
807
+ issueUrl: 'https://github.com/jl-cmd/claude-dev-env/issues/900',
683
808
  hardeningEdited: false,
684
809
  hardeningRepoPath: '',
685
810
  hardeningBranch: '',
@@ -701,7 +826,7 @@ test('a later standards-only round resolves its own review threads after the fol
701
826
  );
702
827
  assert.equal(
703
828
  resolveCalls[0].context.issueUrl,
704
- 'https://github.com/jl-cmd/claude-code-config/issues/900',
829
+ 'https://github.com/jl-cmd/claude-dev-env/issues/900',
705
830
  'expected the resolve step to reference the already-filed follow-up issue in its inline reply',
706
831
  );
707
832
  });
@@ -709,7 +834,7 @@ test('a later standards-only round resolves its own review threads after the fol
709
834
  test('a reuse-path standards round carrying no review threads spawns no thread-resolution agent', async () => {
710
835
  const recordedCalls = [];
711
836
  const issueFiledNoHardening = {
712
- issueUrl: 'https://github.com/jl-cmd/claude-code-config/issues/901',
837
+ issueUrl: 'https://github.com/jl-cmd/claude-dev-env/issues/901',
713
838
  hardeningEdited: false,
714
839
  hardeningRepoPath: '',
715
840
  hardeningBranch: '',
@@ -138,8 +138,8 @@ test('commitWithRecovery bounds the loop, re-verifies, and retries the commit on
138
138
  assert.match(recoveryBody, /commitNeedsCodeRecovery\(/, 'expected the loop guard to call commitNeedsCodeRecovery');
139
139
  assert.match(
140
140
  recoveryBody,
141
- /attempt\s*<\s*FIX_RECOVERY_MAX_ATTEMPTS/,
142
- 'expected the loop to be bounded by FIX_RECOVERY_MAX_ATTEMPTS',
141
+ /attempt\s*<\s*COMMIT_RECOVERY_MAX_ATTEMPTS/,
142
+ 'expected the commit-recovery loop to be bounded by the dedicated COMMIT_RECOVERY_MAX_ATTEMPTS cap of one',
143
143
  );
144
144
  assert.match(recoveryBody, /runRecoverEdit\(/, 'expected the loop to spawn the recover-edit fixer');
145
145
  assert.match(recoveryBody, /runVerify\(/, 'expected the loop to re-verify after the fixer edit');