claude-dev-env 1.64.2 → 1.65.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.
package/CLAUDE.md CHANGED
@@ -105,9 +105,8 @@ Before any coding task, call the `initial_instructions` tool to load the Serena
105
105
 
106
106
  ### Tool hierarchy for code navigation
107
107
  1. **Serena** — symbol-level navigation (declarations, references, implementations, rename)
108
- 2. **Zoekt MCP** (`mcp__zoekt__*`) — content/text search within indexed repos
109
- 3. **Everything** (`everything_search`) file-system search by name/path/extension
110
- 4. **Grep/Glob** — fallback pattern matching
108
+ 2. **Everything** (`everything_search`) — file-system search by name/path/extension
109
+ 3. **Grep/Glob** — content and pattern matching
111
110
 
112
111
  ## Everything Search (MCP Tool)
113
112
 
@@ -115,13 +114,12 @@ This machine has **Everything (voidtools)** running with an HTTP server on port
115
114
  The `everything_search` MCP tool is available in every session.
116
115
 
117
116
  ### Use Everything for file-system searches
118
- Use `everything_search` for finding files by name, path, extension, size, or date. For content searches within Zoekt-indexed repos, prefer `mcp__zoekt__search` — Everything's `content:` search is the fallback when Zoekt is unavailable or returns nothing.
117
+ Use `everything_search` for finding files by name, path, extension, size, or date. For content searches, use Grep — Everything's `content:` search is a fallback when Grep returns nothing.
119
118
 
120
119
  ### Fallback order
121
- 1. **Zoekt MCP** (`mcp__zoekt__search`) — content search within indexed repos
122
- 2. **Everything** (`everything_search`) file-system search by name/path/extension/size/date, and content search outside indexed repos
123
- 3. **Grep** — complex regex content searches if Everything's `content:` returns nothing
124
- 4. **Glob** — precise relative-path pattern matching within the current project
120
+ 1. **Everything** (`everything_search`) — file-system search by name/path/extension/size/date, and content search
121
+ 2. **Grep** — complex regex content searches if Everything's `content:` returns nothing
122
+ 3. **Glob** — precise relative-path pattern matching within the current project
125
123
 
126
124
  ### Search syntax quick reference
127
125
  - `ext:py` — find by extension (multiple: `ext:ts;js`)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-dev-env",
3
- "version": "1.64.2",
3
+ "version": "1.65.0",
4
4
  "description": "Claude Code development standards — rules, hooks, agents, commands, and skills",
5
5
  "type": "module",
6
6
  "bin": {
@@ -0,0 +1,178 @@
1
+ import { test } from 'node:test';
2
+ import { strict as assert } from 'node:assert';
3
+ import { readFileSync } from 'node:fs';
4
+ import { fileURLToPath } from 'node:url';
5
+ import { dirname, join } from 'node:path';
6
+
7
+ const workflowDirectory = dirname(fileURLToPath(import.meta.url));
8
+ const convergeSource = readFileSync(join(workflowDirectory, 'converge.mjs'), 'utf8');
9
+
10
+ function functionSource(functionName) {
11
+ const functionStart = convergeSource.indexOf(`function ${functionName}(`);
12
+ assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
13
+ const nextMatch = /\n(?:async )?function /.exec(convergeSource.slice(functionStart + 1));
14
+ const functionEnd =
15
+ nextMatch === null ? convergeSource.length : functionStart + 1 + nextMatch.index;
16
+ return convergeSource.slice(functionStart, functionEnd);
17
+ }
18
+
19
+ function constantLine(constantName) {
20
+ const matchedLine = convergeSource
21
+ .split('\n')
22
+ .find((eachLine) => eachLine.trimStart().startsWith(`const ${constantName} =`));
23
+ assert.ok(matchedLine, `expected ${constantName} to be declared`);
24
+ return matchedLine;
25
+ }
26
+
27
+ function schemaSource(schemaName, nextDeclaration) {
28
+ const schemaStart = convergeSource.indexOf(`const ${schemaName} = {`);
29
+ assert.notEqual(schemaStart, -1, `expected ${schemaName} to exist`);
30
+ const schemaEnd = convergeSource.indexOf(`const ${nextDeclaration}`, schemaStart);
31
+ assert.notEqual(schemaEnd, -1, `expected ${nextDeclaration} to follow ${schemaName}`);
32
+ return convergeSource.slice(schemaStart, schemaEnd);
33
+ }
34
+
35
+ const pureModule = new Function(
36
+ `${functionSource('commitNeedsCodeRecovery')}\n` + 'return { commitNeedsCodeRecovery };',
37
+ )();
38
+
39
+ const { commitNeedsCodeRecovery } = pureModule;
40
+
41
+ test('a dead commit agent (null result) does not need code recovery', () => {
42
+ assert.equal(commitNeedsCodeRecovery(null), false);
43
+ });
44
+
45
+ test('a pushed commit does not need code recovery even with the flag and detail set', () => {
46
+ assert.equal(
47
+ commitNeedsCodeRecovery({ pushed: true, blockedNeedingEdit: true, blockerDetail: 'CODE_RULES' }),
48
+ false,
49
+ );
50
+ });
51
+
52
+ test('a transient failure (flag false, empty detail) does not need code recovery', () => {
53
+ assert.equal(
54
+ commitNeedsCodeRecovery({ pushed: false, blockedNeedingEdit: false, blockerDetail: '' }),
55
+ false,
56
+ );
57
+ });
58
+
59
+ test('a code-edit block (flag true, concrete detail) needs code recovery', () => {
60
+ assert.equal(
61
+ commitNeedsCodeRecovery({
62
+ pushed: false,
63
+ blockedNeedingEdit: true,
64
+ blockerDetail: 'BLOCKED [code-rules]: collection param needs all_ prefix',
65
+ }),
66
+ true,
67
+ );
68
+ });
69
+
70
+ test('a flagged block with an empty detail does not need code recovery', () => {
71
+ assert.equal(
72
+ commitNeedsCodeRecovery({ pushed: false, blockedNeedingEdit: true, blockerDetail: '' }),
73
+ false,
74
+ );
75
+ });
76
+
77
+ test('a detail without the flag does not need code recovery', () => {
78
+ assert.equal(
79
+ commitNeedsCodeRecovery({ pushed: false, blockedNeedingEdit: false, blockerDetail: 'some text' }),
80
+ false,
81
+ );
82
+ });
83
+
84
+ test('FIX_SCHEMA declares blockedNeedingEdit and blockerDetail as properties', () => {
85
+ const fixSchema = schemaSource('FIX_SCHEMA', 'EDIT_SCHEMA');
86
+ assert.match(fixSchema, /blockedNeedingEdit:\s*\{[\s\S]*?type:\s*'boolean'/);
87
+ assert.match(fixSchema, /blockerDetail:\s*\{[\s\S]*?type:\s*'string'/);
88
+ });
89
+
90
+ test('FIX_SCHEMA requires blockedNeedingEdit and blockerDetail', () => {
91
+ const fixSchema = schemaSource('FIX_SCHEMA', 'EDIT_SCHEMA');
92
+ const requiredMatch = /required:\s*\[([^\]]*)\]/.exec(fixSchema);
93
+ assert.ok(requiredMatch, 'expected FIX_SCHEMA to carry a required array');
94
+ assert.match(requiredMatch[1], /blockedNeedingEdit/);
95
+ assert.match(requiredMatch[1], /blockerDetail/);
96
+ });
97
+
98
+ test('FIX_RECOVERY_MAX_ATTEMPTS is declared and bounds the recovery loop at 2', () => {
99
+ assert.match(constantLine('FIX_RECOVERY_MAX_ATTEMPTS'), /=\s*2/);
100
+ });
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
+ }
119
+
120
+ test('recoverCommitBlockEdit is a clean-coder edit step bound to the blocker detail and leaves changes uncommitted', () => {
121
+ const recoverBody = functionSource('recoverCommitBlockEdit');
122
+ assert.match(recoverBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder');
123
+ 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');
126
+ assert.match(
127
+ recoverBody,
128
+ /only the (?:violation|finding|block)/i,
129
+ 'expected the fixer to be scoped to only the blocking violation',
130
+ );
131
+ assert.match(
132
+ recoverBody,
133
+ /do not commit and do not push|NO commit and NO push|Do NOT commit|leave .*uncommitted|uncommitted/i,
134
+ 'expected the fixer to leave its fix uncommitted for the re-verify and retry commit',
135
+ );
136
+ });
137
+
138
+ test('commitWithRecovery bounds the loop, re-verifies, and retries the commit on a code block', () => {
139
+ const recoveryBody = functionSource('commitWithRecovery');
140
+ assert.match(recoveryBody, /commitNeedsCodeRecovery\(/, 'expected the loop guard to call commitNeedsCodeRecovery');
141
+ assert.match(
142
+ recoveryBody,
143
+ /attempt\s*<\s*FIX_RECOVERY_MAX_ATTEMPTS/,
144
+ 'expected the loop to be bounded by FIX_RECOVERY_MAX_ATTEMPTS',
145
+ );
146
+ assert.match(recoveryBody, /runRecoverEdit\(/, 'expected the loop to spawn the recover-edit fixer');
147
+ assert.match(recoveryBody, /runVerify\(/, 'expected the loop to re-verify after the fixer edit');
148
+ assert.match(recoveryBody, /verdictPassed\(/, 'expected a fresh verdict to gate the retry commit');
149
+ assert.match(recoveryBody, /runCommit\(/, 'expected the loop to retry the commit');
150
+ const editGuardIndex = recoveryBody.search(/edited\s*!==\s*true/);
151
+ const verifyGateIndex = recoveryBody.search(/verdictPassed\(/);
152
+ assert.notEqual(editGuardIndex, -1, 'expected an early break when the fixer made no edit');
153
+ assert.ok(
154
+ editGuardIndex < verifyGateIndex,
155
+ 'expected the no-edit break to precede the re-verify gate',
156
+ );
157
+ });
158
+
159
+ test('applyFixes routes its commit through commitWithRecovery wired to the fix-path steps', () => {
160
+ const applyFixesBody = functionSource('applyFixes');
161
+ assert.match(applyFixesBody, /commitWithRecovery\(/, 'expected applyFixes to call commitWithRecovery');
162
+ assert.match(applyFixesBody, /runCommit:\s*\(\)\s*=>\s*commitVerifiedFixes\(/);
163
+ assert.match(applyFixesBody, /runVerify:\s*\(\)\s*=>\s*verifyFixesInWorkingTree\(/);
164
+ assert.match(applyFixesBody, /runRecoverEdit:[\s\S]*?recoverCommitBlockEdit\(/);
165
+ });
166
+
167
+ test('repairConvergence routes its commit through commitWithRecovery wired to the repair-path steps', () => {
168
+ const repairBody = functionSource('repairConvergence');
169
+ assert.match(repairBody, /commitWithRecovery\(/, 'expected repairConvergence to call commitWithRecovery');
170
+ assert.match(repairBody, /runCommit:\s*\(\)\s*=>\s*commitRepairFixes\(/);
171
+ assert.match(repairBody, /runVerify:\s*\(\)\s*=>\s*verifyRepairChanges\(/);
172
+ assert.match(repairBody, /runRecoverEdit:[\s\S]*?recoverCommitBlockEdit\(/);
173
+ });
174
+
175
+ test('the round-loop fix-stalled blockers survive the recovery wiring', () => {
176
+ assert.match(convergeSource, /fix lens landed no push for/);
177
+ assert.match(convergeSource, /copilot fix lens landed no push for/);
178
+ });
@@ -101,8 +101,10 @@ const FIX_SCHEMA = {
101
101
  pushed: { type: 'boolean' },
102
102
  resolvedWithoutCommit: { type: 'boolean', description: 'true when every finding was already addressed so no code change was made, yet each finding thread was still resolved — the round advances rather than stalling' },
103
103
  summary: { type: 'string' },
104
+ blockedNeedingEdit: { type: 'boolean', description: 'true only when the commit or push was rejected by a commit-time hook or gate whose message requires a code change (for example a CODE_RULES violation the fix introduced), not a transient or auth failure' },
105
+ blockerDetail: { type: 'string', description: 'verbatim hook or gate rejection text naming the file and rule that must change, or an empty string when no edit-requiring block occurred' },
104
106
  },
105
- required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary'],
107
+ required: ['newSha', 'pushed', 'resolvedWithoutCommit', 'summary', 'blockedNeedingEdit', 'blockerDetail'],
106
108
  }
107
109
 
108
110
  const EDIT_SCHEMA = {
@@ -434,6 +436,25 @@ function detectFixProgress(fixResult, priorHead, hadThreadBearingFinding) {
434
436
  return { progressed, newSha }
435
437
  }
436
438
 
439
+ /**
440
+ * Decide whether a commit step was blocked by a commit-time hook or gate that
441
+ * requires a code change, so the recovery loop should route back to a fixer. A
442
+ * null result, a successful push, a transient failure (blockedNeedingEdit
443
+ * false), or a flagged block carrying no detail all read as not-needing-recovery,
444
+ * so only a flagged block with a concrete message routes to the fixer.
445
+ * @param {object|null} commitResult the FIX_SCHEMA result, or null on agent failure
446
+ * @returns {boolean} true only when the commit needs a code-edit recovery pass
447
+ */
448
+ function commitNeedsCodeRecovery(commitResult) {
449
+ if (commitResult == null) return false
450
+ if (commitResult.pushed === true) return false
451
+ return (
452
+ commitResult.blockedNeedingEdit === true &&
453
+ typeof commitResult.blockerDetail === 'string' &&
454
+ commitResult.blockerDetail.length > 0
455
+ )
456
+ }
457
+
437
458
  /**
438
459
  * Decide whether a resolved HEAD SHA is safe to spawn lenses against. A dead
439
460
  * resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
@@ -753,11 +774,68 @@ function commitVerifiedFixes(head, sourceLabel) {
753
774
  `Rules:\n` +
754
775
  `- 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` +
755
776
  `- Make ONE commit for all the working-tree fixes, then push to the PR branch.\n\n` +
756
- `Return values: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, and a one-line summary. If the commit or push is blocked or fails, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, and a summary naming the failure.`,
777
+ `Return values:\n` +
778
+ `- 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` +
779
+ `- When a commit-time hook or gate (for example code_rules_gate, the CODE_RULES commit gate) rejects the commit because the fix needs a code change: keep the no-edit rule, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=true, blockerDetail=<the verbatim hook message naming the file and rule>, and a summary. A recovery fixer runs after you to clear it.\n` +
780
+ `- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
757
781
  { label: `fix-commit:${sourceLabel}`, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder' },
758
782
  )
759
783
  }
760
784
 
785
+ /**
786
+ * Commit-recovery fixer: when a commit step is blocked by a commit-time hook or
787
+ * gate that requires a code change, one clean-coder fixes only that blocking
788
+ * violation test-first in the working tree and leaves it uncommitted, so the
789
+ * re-verify step can bind a fresh verdict and the retry commit can push. It does
790
+ * not re-open the original findings or touch GitHub threads — the edit step
791
+ * already handled those.
792
+ * @param {string} head PR HEAD SHA the fixes were raised against
793
+ * @param {string} blockerDetail verbatim hook/gate message naming the file and rule to change
794
+ * @param {string} sourceLabel short description of where the findings came from
795
+ * @param {number} attempt the 1-based recovery attempt number
796
+ * @returns {Promise<object>} EDIT_SCHEMA result
797
+ */
798
+ function recoverCommitBlockEdit(head, blockerDetail, sourceLabel, attempt) {
799
+ return convergeAgent(
800
+ `You are the COMMIT-RECOVERY fixer (attempt ${attempt}) for fixes (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. A prior commit step was blocked by a commit-time hook or gate that requires a code change. A separate verify step then a separate commit step run after you.\n\n` +
801
+ `The blocking hook or gate said:\n${blockerDetail}\n\n` +
802
+ `Rules:\n` +
803
+ `- Confirm the working tree is on the PR branch at HEAD ${head} with the prior fixes still present.\n` +
804
+ `- 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` +
805
+ `- 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` +
806
+ `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.`,
807
+ { label: `fix-recover:${sourceLabel}`, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder' },
808
+ )
809
+ }
810
+
811
+ const FIX_RECOVERY_MAX_ATTEMPTS = 4
812
+
813
+ /**
814
+ * Run a commit step and, when it is blocked by a commit-time hook or gate that
815
+ * requires a code change, route back to a fixer: fix the blocking violation,
816
+ * re-verify so a fresh verdict binds the corrected surface, then retry the
817
+ * commit — bounded by FIX_RECOVERY_MAX_ATTEMPTS. The loop breaks early when the
818
+ * fixer makes no edit or the re-verify does not pass, returning the last commit
819
+ * result so the caller's existing no-push handling still applies. A transient
820
+ * failure never enters the loop (commitNeedsCodeRecovery is false), so an auth or
821
+ * network failure keeps the existing blocker path.
822
+ * @param {{runCommit: function, runVerify: function, runRecoverEdit: function}} steps the commit, re-verify, and recover-edit thunks
823
+ * @returns {Promise<object>} the final FIX_SCHEMA result
824
+ */
825
+ async function commitWithRecovery({ runCommit, runVerify, runRecoverEdit }) {
826
+ let commitResult = await runCommit()
827
+ let attempt = 0
828
+ while (commitNeedsCodeRecovery(commitResult) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
829
+ attempt += 1
830
+ const recoverEdit = await runRecoverEdit(commitResult.blockerDetail, attempt)
831
+ if (recoverEdit?.edited !== true) break
832
+ const verifyTranscript = await runVerify()
833
+ if (!verdictPassed(verifyTranscript)) break
834
+ commitResult = await runCommit()
835
+ }
836
+ return commitResult
837
+ }
838
+
761
839
  /**
762
840
  * Fix lens: edit (clean-coder, no commit) -> verify (code-verifier emits a
763
841
  * verdict fence binding the working tree) -> commit (clean-coder, one commit +
@@ -791,7 +869,11 @@ async function applyFixes(head, findings, sourceLabel) {
791
869
  summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
792
870
  }
793
871
  }
794
- return commitVerifiedFixes(head, sourceLabel)
872
+ return commitWithRecovery({
873
+ runCommit: () => commitVerifiedFixes(head, sourceLabel),
874
+ runVerify: () => verifyFixesInWorkingTree(head, findings, sourceLabel),
875
+ runRecoverEdit: (detail, attempt) => recoverCommitBlockEdit(head, detail, sourceLabel, attempt),
876
+ })
795
877
  }
796
878
 
797
879
  /**
@@ -978,7 +1060,10 @@ function commitRepairFixes(head, wasRebased) {
978
1060
  `Rules:\n` +
979
1061
  `- 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` +
980
1062
  `- 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` +
981
- `Return values: newSha=the new HEAD SHA after your push, pushed=true, resolvedWithoutCommit=false, and a one-line summary. If the commit or push is blocked or fails, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, and a summary naming the failure.`,
1063
+ `Return values:\n` +
1064
+ `- 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` +
1065
+ `- When a commit-time hook or gate (for example code_rules_gate, the CODE_RULES commit gate) rejects the commit because the fix needs a code change: keep the no-edit rule, return newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=true, blockerDetail=<the verbatim hook message naming the file and rule>, and a summary. A recovery fixer runs after you to clear it.\n` +
1066
+ `- On a transient or non-code failure (auth, network, a non-fast-forward, a lock): newSha=${head}, pushed=false, resolvedWithoutCommit=false, blockedNeedingEdit=false, blockerDetail="", and a summary naming the failure.`,
982
1067
  { label: 'repair-commit', phase: 'Finalize', schema: FIX_SCHEMA, agentType: 'clean-coder' },
983
1068
  )
984
1069
  }
@@ -1017,7 +1102,12 @@ async function repairConvergence(head, failures) {
1017
1102
  summary: `repair verify step did not pass the working-tree repair on HEAD ${head} — not pushing`,
1018
1103
  }
1019
1104
  }
1020
- return commitRepairFixes(head, editResult?.rebased === true)
1105
+ const wasRebased = editResult?.rebased === true
1106
+ return commitWithRecovery({
1107
+ runCommit: () => commitRepairFixes(head, wasRebased),
1108
+ runVerify: () => verifyRepairChanges(head, failures),
1109
+ runRecoverEdit: (detail, attempt) => recoverCommitBlockEdit(head, detail, 'repair', attempt),
1110
+ })
1021
1111
  }
1022
1112
 
1023
1113
  /**