claude-dev-env 1.74.0 → 1.75.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 (44) hide show
  1. package/hooks/blocking/CLAUDE.md +1 -0
  2. package/hooks/blocking/code_verifier_spawn_preflight_gate.py +2 -1
  3. package/hooks/blocking/duplicate_rmtree_helper_blocker.py +155 -0
  4. package/hooks/blocking/hedging_language_blocker.py +1 -13
  5. package/hooks/blocking/intent_only_ending_blocker.py +1 -15
  6. package/hooks/blocking/pre_tool_use_dispatcher.py +4 -5
  7. package/hooks/blocking/question_to_user_enforcer.py +1 -11
  8. package/hooks/blocking/session_handoff_blocker.py +1 -15
  9. package/hooks/blocking/test_code_verifier_spawn_preflight_gate.py +16 -0
  10. package/hooks/blocking/test_duplicate_rmtree_helper_blocker.py +328 -0
  11. package/hooks/blocking/test_hedging_language_blocker.py +6 -0
  12. package/hooks/blocking/test_intent_only_ending_blocker.py +5 -0
  13. package/hooks/blocking/test_pre_tool_use_dispatcher.py +52 -5
  14. package/hooks/blocking/test_question_to_user_enforcer.py +6 -0
  15. package/hooks/blocking/test_session_handoff_blocker.py +6 -0
  16. package/hooks/hooks_constants/CLAUDE.md +4 -1
  17. package/hooks/hooks_constants/code_verifier_spawn_preflight_gate_constants.py +2 -1
  18. package/hooks/hooks_constants/duplicate_rmtree_helper_blocker_constants.py +27 -0
  19. package/hooks/hooks_constants/post_tool_use_dispatcher_constants.py +2 -0
  20. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +8 -2
  21. package/hooks/hooks_constants/test_post_tool_use_dispatcher_constants.py +43 -0
  22. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +99 -0
  23. package/hooks/hooks_constants/test_text_stripping.py +39 -0
  24. package/hooks/hooks_constants/text_stripping.py +36 -0
  25. package/hooks/validation/CLAUDE.md +1 -0
  26. package/hooks/validation/post_tool_use_dispatcher.py +2 -2
  27. package/hooks/validation/test_mypy_validator.py +1 -1
  28. package/hooks/validation/test_post_tool_use_dispatcher.py +6 -0
  29. package/hooks/workflow/auto_formatter.py +8 -5
  30. package/hooks/workflow/test_auto_formatter.py +33 -0
  31. package/package.json +1 -1
  32. package/rules/windows-filesystem-safe.md +2 -0
  33. package/skills/autoconverge/SKILL.md +6 -3
  34. package/skills/autoconverge/reference/stop-conditions.md +7 -0
  35. package/skills/autoconverge/workflow/converge.clean-audit.test.mjs +5 -4
  36. package/skills/autoconverge/workflow/converge.contract.test.mjs +308 -132
  37. package/skills/autoconverge/workflow/converge.copilot-gate.test.mjs +16 -16
  38. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +36 -44
  39. package/skills/autoconverge/workflow/converge.merge-conflict.test.mjs +16 -24
  40. package/skills/autoconverge/workflow/converge.mjs +598 -606
  41. package/skills/autoconverge/workflow/convergence_summary.py +1 -1
  42. package/skills/autoconverge/workflow/render_report.py +2 -6
  43. package/skills/autoconverge/workflow/test_convergence_summary.py +17 -0
  44. package/skills/autoconverge/workflow/test_render_report.py +1 -0
@@ -129,12 +129,12 @@ test('a Copilot no-show after the poll cap returns a down result rather than a b
129
129
  );
130
130
  });
131
131
 
132
- test('checkConvergence wires the --copilot-down flag from a copilotDown argument', () => {
133
- const checkConvergenceBody = functionBody('checkConvergence');
132
+ test('resumeConvergenceCheckAgent wires the --copilot-down flag from the copilotDown context', () => {
133
+ const checkConvergenceBody = functionBody('resumeConvergenceCheckAgent');
134
134
  assert.match(
135
135
  checkConvergenceBody,
136
- /copilotDown \? ' --copilot-down' : ''/,
137
- 'expected checkConvergence to append --copilot-down when copilotDown is set',
136
+ /context\.copilotDown \? ' --copilot-down' : ''/,
137
+ 'expected resumeConvergenceCheckAgent to append --copilot-down when copilotDown is set',
138
138
  );
139
139
  assert.match(
140
140
  checkConvergenceBody,
@@ -195,7 +195,7 @@ test('the standards-only Copilot sub-path resets copilotDown before FINALIZE', (
195
195
  -1,
196
196
  'expected the COPILOT phase to handle a standards-only Copilot fix outcome',
197
197
  );
198
- const standardsBranch = convergeSource.slice(standardsBranchStart, standardsBranchStart + 600);
198
+ const standardsBranch = convergeSource.slice(standardsBranchStart, standardsBranchStart + 800);
199
199
  const resetIndex = standardsBranch.indexOf('copilotDown = false');
200
200
  const finalizeIndex = standardsBranch.indexOf("phase = 'FINALIZE'");
201
201
  assert.notEqual(
@@ -234,32 +234,32 @@ test('the COPILOT phase recomputes copilotDown from each gate outcome via resolv
234
234
  test('markReady receives copilotDown so it can opt the unflagged hook out of the Copilot gate', () => {
235
235
  const finalizeStart = convergeSource.indexOf("if (phase === 'FINALIZE') {");
236
236
  assert.notEqual(finalizeStart, -1, 'expected a FINALIZE phase block');
237
- const markReadyCall = convergeSource.indexOf('await markReady(', finalizeStart);
238
- assert.notEqual(markReadyCall, -1, 'expected the FINALIZE phase to call markReady');
239
- const callSlice = convergeSource.slice(markReadyCall, markReadyCall + 40);
237
+ const markReadyCall = convergeSource.indexOf("'mark-ready'", finalizeStart);
238
+ assert.notEqual(markReadyCall, -1, 'expected the FINALIZE phase to route mark-ready through the general-utility agent');
239
+ const callSlice = convergeSource.slice(markReadyCall - 20, markReadyCall + 60);
240
240
  assert.match(
241
241
  callSlice,
242
- /markReady\(head,\s*copilotDown\)/,
243
- 'expected markReady to receive copilotDown so the mark-ready agent can opt the unflagged hook out of the Copilot gate',
242
+ /copilotDown/,
243
+ 'expected mark-ready context to include copilotDown so the agent can opt the unflagged hook out of the Copilot gate',
244
244
  );
245
245
  });
246
246
 
247
- test('the markReady prompt opts the unflagged convergence hook out of Copilot when copilotDown', () => {
248
- const markReadyBody = functionBody('markReady');
247
+ test('the mark-ready task in resumeGeneralUtilityAgent opts the unflagged convergence hook out of Copilot when copilotDown', () => {
248
+ const markReadyBody = functionBody('resumeGeneralUtilityAgent');
249
249
  assert.match(
250
250
  markReadyBody,
251
- /copilotDown/,
252
- 'expected markReady to branch on copilotDown',
251
+ /context\.copilotDown/,
252
+ 'expected the mark-ready task to branch on copilotDown',
253
253
  );
254
254
  assert.match(
255
255
  markReadyBody,
256
256
  /CLAUDE_REVIEWS_DISABLED/,
257
- 'expected the markReady prompt to set CLAUDE_REVIEWS_DISABLED so the unflagged hook re-derives the Copilot bypass',
257
+ 'expected the mark-ready prompt to set CLAUDE_REVIEWS_DISABLED so the unflagged hook re-derives the Copilot bypass',
258
258
  );
259
259
  assert.match(
260
260
  markReadyBody,
261
261
  /copilot/,
262
- 'expected the markReady opt-out to name the copilot token',
262
+ 'expected the mark-ready opt-out to name the copilot token',
263
263
  );
264
264
  });
265
265
 
@@ -99,30 +99,28 @@ test('FIX_RECOVERY_MAX_ATTEMPTS is declared and bounds the recovery loop at 2',
99
99
  assert.match(constantLine('FIX_RECOVERY_MAX_ATTEMPTS'), /=\s*2\s*$/);
100
100
  });
101
101
 
102
- for (const commitFunctionName of ['commitVerifiedFixes', 'commitRepairFixes']) {
103
- test(`${commitFunctionName} prompt separates an edit-requiring block from a transient failure`, () => {
104
- const commitBody = functionSource(commitFunctionName);
105
- assert.match(commitBody, /blockedNeedingEdit/, 'expected the edit-block flag to be set in the prompt');
106
- assert.match(commitBody, /blockerDetail/, 'expected the verbatim blocker detail to be requested');
107
- assert.match(
108
- commitBody,
109
- /code_rules_gate|CODE_RULES/,
110
- 'expected the commit prompt to name the CODE_RULES commit gate as an edit-requiring block',
111
- );
112
- assert.match(
113
- commitBody,
114
- /transient/i,
115
- 'expected the commit prompt to name the transient (non-code) failure case',
116
- );
117
- });
118
- }
102
+ test('the commit path in resumeCodeEditorAgent separates an edit-requiring block from a transient failure', () => {
103
+ const commitBody = functionSource('resumeCodeEditorAgent');
104
+ assert.match(commitBody, /blockedNeedingEdit/, 'expected the edit-block flag to be set in the prompt');
105
+ assert.match(commitBody, /blockerDetail/, 'expected the verbatim blocker detail to be requested');
106
+ assert.match(
107
+ commitBody,
108
+ /code_rules_gate|CODE_RULES/,
109
+ 'expected the commit prompt to name the CODE_RULES commit gate as an edit-requiring block',
110
+ );
111
+ assert.match(
112
+ commitBody,
113
+ /transient/i,
114
+ 'expected the commit prompt to name the transient (non-code) failure case',
115
+ );
116
+ });
119
117
 
120
- test('recoverCommitBlockEdit is a clean-coder edit step bound to the blocker detail and leaves changes uncommitted', () => {
121
- const recoverBody = functionSource('recoverCommitBlockEdit');
118
+ test('the commit-recover task in resumeCodeEditorAgent is a clean-coder edit step bound to the blocker detail and leaves changes uncommitted', () => {
119
+ const recoverBody = functionSource('resumeCodeEditorAgent');
122
120
  assert.match(recoverBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder');
123
121
  assert.match(recoverBody, /schema:\s*EDIT_SCHEMA/, 'expected the fixer to reuse EDIT_SCHEMA');
124
- assert.match(recoverBody, /label:\s*`fix-recover:/, 'expected the fix-recover label');
125
- assert.match(recoverBody, /blockerDetail/, 'expected the fixer prompt to consume the blocker detail');
122
+ assert.match(recoverBody, /task === 'commit-recover'/, 'expected the commit-recover task branch');
123
+ assert.match(recoverBody, /context\.blockerDetail/, 'expected the fixer prompt to consume the blocker detail');
126
124
  assert.match(
127
125
  recoverBody,
128
126
  /only the (?:violation|finding|block)/i,
@@ -163,20 +161,17 @@ test('commitWithRecovery bounds the loop, re-verifies, and retries the commit on
163
161
  );
164
162
  });
165
163
 
166
- test('applyFixes routes its commit through commitWithRecovery wired to the fix-path steps', () => {
164
+ test('applyFixes routes through spawnFixerAgent and fixerWithRecovery', () => {
167
165
  const applyFixesBody = functionSource('applyFixes');
168
- assert.match(applyFixesBody, /commitWithRecovery\(/, 'expected applyFixes to call commitWithRecovery');
169
- assert.match(applyFixesBody, /runCommit:\s*\(\)\s*=>\s*commitVerifiedFixes\(/);
170
- assert.match(applyFixesBody, /runVerify:\s*\(\)\s*=>\s*verifyFixesInWorkingTree\(/);
171
- assert.match(applyFixesBody, /runRecoverEdit:[\s\S]*?recoverCommitBlockEdit\(/);
166
+ assert.match(applyFixesBody, /spawnFixerAgent\(/, 'expected applyFixes to call spawnFixerAgent');
167
+ assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
172
168
  });
173
169
 
174
170
  test('repairConvergence routes its commit through commitWithRecovery wired to the repair-path steps', () => {
175
171
  const repairBody = functionSource('repairConvergence');
176
172
  assert.match(repairBody, /commitWithRecovery\(/, 'expected repairConvergence to call commitWithRecovery');
177
- assert.match(repairBody, /runCommit:\s*\(\)\s*=>\s*commitRepairFixes\(/);
178
- assert.match(repairBody, /runVerify:\s*\(\)\s*=>\s*verifyRepairChanges\(/);
179
- assert.match(repairBody, /runRecoverEdit:[\s\S]*?recoverCommitBlockEdit\(/);
173
+ assert.match(repairBody, /resumeCodeEditorAgent\(/, 'expected repairConvergence to use resumeCodeEditorAgent');
174
+ assert.match(repairBody, /resumeVerifierAgent\(/, 'expected repairConvergence to use resumeVerifierAgent');
180
175
  });
181
176
 
182
177
  test('the round-loop fix-stalled blockers survive the recovery wiring', () => {
@@ -185,7 +180,8 @@ test('the round-loop fix-stalled blockers survive the recovery wiring', () => {
185
180
  });
186
181
 
187
182
  const verifyObjectionModule = new Function(
188
- `${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
183
+ `${functionSource('parseLastVerdictFence')}\n` +
184
+ `${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
189
185
  `${functionSource('renderVerifyObjectionLine')}\n` +
190
186
  `${functionSource('extractVerifyObjection')}\n` +
191
187
  'return { extractVerifyObjection, VERIFY_OBJECTION_FALLBACK };',
@@ -267,12 +263,12 @@ test('extractVerifyObjection falls back when no finding yields usable text', ()
267
263
  assert.equal(extractVerifyObjection(transcript), VERIFY_OBJECTION_FALLBACK);
268
264
  });
269
265
 
270
- test('recoverVerifyFailEdit is a clean-coder edit step bound to the verifier objection and leaves changes uncommitted', () => {
271
- const recoverBody = functionSource('recoverVerifyFailEdit');
266
+ test('the verify-recover task in resumeCodeEditorAgent is a clean-coder edit step bound to the verifier objection and leaves changes uncommitted', () => {
267
+ const recoverBody = functionSource('resumeCodeEditorAgent');
272
268
  assert.match(recoverBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder');
273
269
  assert.match(recoverBody, /schema:\s*EDIT_SCHEMA/, 'expected the fixer to reuse EDIT_SCHEMA');
274
- assert.match(recoverBody, /label:\s*`fix-verify-recover:/, 'expected the fix-verify-recover label');
275
- assert.match(recoverBody, /objection/, 'expected the fixer prompt to consume the verifier objection');
270
+ assert.match(recoverBody, /VERIFY-RECOVERY fixer/, 'expected the verify-recovery prompt body');
271
+ assert.match(recoverBody, /context\.objection/, 'expected the fixer prompt to consume the verifier objection');
276
272
  assert.match(
277
273
  recoverBody,
278
274
  /do not commit and do not push|Do NOT commit|leave .*uncommitted|uncommitted/i,
@@ -302,19 +298,15 @@ test('verifyWithRecovery bounds the loop, re-fixes on a failed verdict, and re-v
302
298
  assert.ok(recoverEditIndex < reverifyIndex, 'expected order recover-edit -> re-verify, so a swap fails');
303
299
  });
304
300
 
305
- test('applyFixes routes its verify through verifyWithRecovery before commitWithRecovery', () => {
301
+ test('applyFixes routes through fixerWithRecovery which handles verify and commit', () => {
306
302
  const applyFixesBody = functionSource('applyFixes');
307
- assert.match(applyFixesBody, /verifyWithRecovery\(/, 'expected applyFixes to call verifyWithRecovery');
308
- assert.match(applyFixesBody, /runVerify:\s*\(\)\s*=>\s*verifyFixesInWorkingTree\(/);
309
- assert.match(applyFixesBody, /runRecoverEdit:[\s\S]*?recoverVerifyFailEdit\(/);
310
- const verifyIndex = applyFixesBody.search(/verifyWithRecovery\(/);
311
- const commitIndex = applyFixesBody.search(/commitWithRecovery\(/);
312
- assert.ok(verifyIndex < commitIndex, 'expected verify-recovery to precede commit-recovery');
303
+ assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
304
+ assert.match(applyFixesBody, /spawnFixerAgent\(/, 'expected applyFixes to call spawnFixerAgent');
313
305
  });
314
306
 
315
- test('repairConvergence routes its verify through verifyWithRecovery wired to the repair verify step', () => {
307
+ test('repairConvergence routes its verify through verifyWithRecovery wired to resume helpers', () => {
316
308
  const repairBody = functionSource('repairConvergence');
317
309
  assert.match(repairBody, /verifyWithRecovery\(/, 'expected repairConvergence to call verifyWithRecovery');
318
- assert.match(repairBody, /runVerify:\s*\(\)\s*=>\s*verifyRepairChanges\(/);
319
- assert.match(repairBody, /runRecoverEdit:[\s\S]*?recoverVerifyFailEdit\(/);
310
+ assert.match(repairBody, /resumeVerifierAgent\(/, 'expected repairConvergence to use resumeVerifierAgent for verify');
311
+ assert.match(repairBody, /resumeCodeEditorAgent\(/, 'expected repairConvergence to use resumeCodeEditorAgent for recover');
320
312
  });
@@ -31,39 +31,36 @@ test('isMergeConflicting reports a conflict only when the check returned conflic
31
31
  assert.equal(isMergeConflicting({ conflicting: false }), false);
32
32
  });
33
33
 
34
- test('checkMergeConflicts is a read-only mergeability probe that polls until GitHub computes it', () => {
35
- const body = functionBody('checkMergeConflicts');
36
- assert.match(body, /mergeable/, 'expected the probe to read the PR mergeable field');
34
+ test('the git agent handles merge-conflict checks with shell-agnostic polling', () => {
35
+ const gitBody = functionBody('resumeGitAgent');
36
+ assert.match(gitBody, /mergeable/, 'expected the git agent to read the PR mergeable field');
37
37
  assert.match(
38
- body,
38
+ gitBody,
39
39
  /do not edit, commit, push, or rebase|read only/i,
40
- 'expected the probe to be read-only',
40
+ 'expected the git agent merge check to be read-only',
41
41
  );
42
- assert.match(body, /agentType:\s*'Explore'/, 'expected the probe to use the read-only Explore agent');
43
- assert.match(body, /schema:\s*MERGE_CONFLICT_SCHEMA/, 'expected the probe to return MERGE_CONFLICT_SCHEMA');
44
- assert.match(body, /null/, 'expected the probe to handle GitHub returning mergeable:null while it computes');
45
- assert.match(body, /sleep 5|Start-Sleep/, 'expected a shell-agnostic poll delay');
42
+ assert.match(gitBody, /MERGE_CONFLICT_SCHEMA/, 'expected the git agent to return MERGE_CONFLICT_SCHEMA');
43
+ assert.match(gitBody, /sleep 5|Start-Sleep/, 'expected a shell-agnostic poll delay');
46
44
  });
47
45
 
48
- test('resolveConflictsEdit rebases onto origin/main and makes no push', () => {
49
- const body = functionBody('resolveConflictsEdit');
50
- assert.match(body, /git rebase origin\/main/, 'expected the edit step to rebase onto origin/main');
46
+ test('resumeCodeEditorAgent conflict-edit path rebases onto origin/main and makes no push', () => {
47
+ const body = functionBody('resumeCodeEditorAgent');
48
+ assert.match(body, /git rebase origin\/main/, 'expected the edit path to rebase onto origin/main');
51
49
  assert.match(
52
50
  body,
53
51
  /do not push|no push|not push/i,
54
- 'expected the edit step to leave the push to the commit step',
52
+ 'expected the edit path to leave the push to the commit step',
55
53
  );
56
- assert.match(body, /agentType:\s*'clean-coder'/, 'expected the edit step to use clean-coder');
57
54
  });
58
55
 
59
56
  test('resolveMergeConflicts runs check -> edit -> verify -> commit and gates the push on the verdict', () => {
60
57
  const body = functionBody('resolveMergeConflicts');
61
- const checkIndex = body.indexOf('checkMergeConflicts(');
62
- const editIndex = body.indexOf('resolveConflictsEdit(');
63
- const verifyIndex = body.indexOf('verifyRepairChanges(');
64
- const commitIndex = body.indexOf('commitRepairFixes(');
58
+ const checkIndex = body.indexOf("resumeGitAgent(gitAgentId, 'check-merge-conflicts'");
59
+ const editIndex = body.indexOf('spawnCodeEditorAgent(');
60
+ const verifyIndex = body.indexOf('spawnVerifierAgent(');
61
+ const commitIndex = body.indexOf('commitWithRecovery(');
65
62
  assert.notEqual(checkIndex, -1, 'expected the conflict check to run');
66
- assert.notEqual(editIndex, -1, 'expected the rebase edit step to run');
63
+ assert.notEqual(editIndex, -1, 'expected the edit step to run');
67
64
  assert.notEqual(verifyIndex, -1, 'expected the verify step to run');
68
65
  assert.notEqual(commitIndex, -1, 'expected the commit step to run');
69
66
  assert.ok(
@@ -71,11 +68,6 @@ test('resolveMergeConflicts runs check -> edit -> verify -> commit and gates the
71
68
  'expected the order check -> edit -> verify -> commit',
72
69
  );
73
70
  assert.match(body, /verdictPassed\(/, 'expected the verifier verdict to gate the force-push');
74
- assert.match(
75
- body,
76
- /commitRepairFixes\(head,\s*true\)/,
77
- 'expected the commit to force-with-lease (wasRebased=true) after a rebase',
78
- );
79
71
  });
80
72
 
81
73
  test('resolveMergeConflicts rebases only when the check reports a conflict', () => {