claude-dev-env 1.71.0 → 1.72.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.
@@ -0,0 +1,106 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Opens files on screen, sizing each image window to the image's own dimensions.
4
+
5
+ .DESCRIPTION
6
+ For every path given, an image opens in a window whose client area matches the
7
+ image's pixel size, scaled down to fit the primary screen's working area when the
8
+ image is larger than the screen. A small image gets a usable minimum window with
9
+ the picture centered at native size. Non-image files open in their registered
10
+ default application, and any file that cannot be loaded as an image falls back to
11
+ that default application too. Escape or the close button dismisses a window; the
12
+ process exits once every window is closed.
13
+
14
+ .PARAMETER Paths
15
+ One or more file paths to open.
16
+ #>
17
+ param(
18
+ [Parameter(ValueFromRemainingArguments = $true)]
19
+ [string[]]$Paths
20
+ )
21
+
22
+ Add-Type -AssemblyName System.Windows.Forms
23
+ Add-Type -AssemblyName System.Drawing
24
+
25
+ try {
26
+ [System.Windows.Forms.Application]::SetHighDpiMode([System.Windows.Forms.HighDpiMode]::PerMonitorV2) | Out-Null
27
+ }
28
+ catch {
29
+ $null = $_
30
+ }
31
+ [System.Windows.Forms.Application]::EnableVisualStyles()
32
+
33
+ $imageExtensions = @('.png', '.jpg', '.jpeg', '.gif', '.bmp', '.webp', '.tif', '.tiff', '.ico')
34
+ $screenMargin = 80
35
+ $minimumClientWidth = 220
36
+ $minimumClientHeight = 160
37
+ $openWindowCount = 0
38
+
39
+ foreach ($path in $Paths) {
40
+ if (-not (Test-Path -LiteralPath $path)) { continue }
41
+ $fullPath = (Resolve-Path -LiteralPath $path).Path
42
+ $extension = [System.IO.Path]::GetExtension($fullPath).ToLowerInvariant()
43
+
44
+ if ($imageExtensions -notcontains $extension) {
45
+ Invoke-Item -LiteralPath $fullPath
46
+ continue
47
+ }
48
+
49
+ try {
50
+ $imageBytes = [System.IO.File]::ReadAllBytes($fullPath)
51
+ $imageStream = New-Object System.IO.MemoryStream(, $imageBytes)
52
+ $loadedImage = [System.Drawing.Image]::FromStream($imageStream)
53
+ $image = New-Object System.Drawing.Bitmap($loadedImage)
54
+ $loadedImage.Dispose()
55
+ $imageStream.Dispose()
56
+ }
57
+ catch {
58
+ Invoke-Item -LiteralPath $fullPath
59
+ continue
60
+ }
61
+
62
+ $workingArea = [System.Windows.Forms.Screen]::PrimaryScreen.WorkingArea
63
+ $maximumWidth = $workingArea.Width - $screenMargin
64
+ $maximumHeight = $workingArea.Height - $screenMargin
65
+ $scale = [Math]::Min(1.0, [Math]::Min($maximumWidth / $image.Width, $maximumHeight / $image.Height))
66
+
67
+ $pictureBox = New-Object System.Windows.Forms.PictureBox
68
+ $pictureBox.Dock = [System.Windows.Forms.DockStyle]::Fill
69
+ $pictureBox.Image = $image
70
+
71
+ if ($scale -lt 1.0) {
72
+ $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::Zoom
73
+ $clientWidth = [int][Math]::Round($image.Width * $scale)
74
+ $clientHeight = [int][Math]::Round($image.Height * $scale)
75
+ }
76
+ else {
77
+ $pictureBox.SizeMode = [System.Windows.Forms.PictureBoxSizeMode]::CenterImage
78
+ $clientWidth = [Math]::Max($minimumClientWidth, $image.Width)
79
+ $clientHeight = [Math]::Max($minimumClientHeight, $image.Height)
80
+ }
81
+
82
+ $form = New-Object System.Windows.Forms.Form
83
+ $form.Text = [System.IO.Path]::GetFileName($fullPath)
84
+ $form.AutoScaleMode = [System.Windows.Forms.AutoScaleMode]::None
85
+ $form.StartPosition = [System.Windows.Forms.FormStartPosition]::CenterScreen
86
+ $form.ClientSize = New-Object System.Drawing.Size($clientWidth, $clientHeight)
87
+ $form.KeyPreview = $true
88
+ $form.BackColor = [System.Drawing.Color]::FromArgb(24, 24, 24)
89
+ $form.Controls.Add($pictureBox)
90
+
91
+ $form.Add_KeyDown({
92
+ param($sender, $eventArguments)
93
+ if ($eventArguments.KeyCode -eq [System.Windows.Forms.Keys]::Escape) { $sender.Close() }
94
+ })
95
+ $form.Add_FormClosed({
96
+ $script:openWindowCount--
97
+ if ($script:openWindowCount -le 0) { [System.Windows.Forms.Application]::Exit() }
98
+ })
99
+
100
+ $openWindowCount++
101
+ $form.Show()
102
+ }
103
+
104
+ if ($openWindowCount -gt 0) {
105
+ [System.Windows.Forms.Application]::Run()
106
+ }
@@ -101,7 +101,7 @@ own. The workflow runs in the background and notifies this session on
101
101
  completion. Watch live progress with `/workflows`.
102
102
 
103
103
  The workflow returns
104
- `{ converged, rounds, finalSha, blocker, standardsNote, copilotNote }`.
104
+ `{ converged, rounds, finalSha, blocker, standardsNote, copilotNote, reuseNote }`.
105
105
 
106
106
  ## Budget-aware round boundaries
107
107
 
@@ -207,8 +207,31 @@ round records nothing resumable and replays dirty.
207
207
  Blocker: <blocker> # only when blocked
208
208
  Standards: <standardsNote> # only when a round deferred code-standard findings
209
209
  Copilot: <copilotNote> # only when Copilot was down or out of quota
210
+ Reuse: <reuseNote> # only when the reuse pass identified an improvement
210
211
  ```
211
212
 
213
+ ## Reuse pass (before convergence)
214
+
215
+ Before the first round, one reuse lens (`code-quality-agent`) scans the full
216
+ `origin/main...HEAD` diff for places the PR re-implements behavior the codebase
217
+ already provides. It reports a reuse improvement only when all three criteria
218
+ hold, and drops any case where even one is in doubt:
219
+
220
+ - **Certain** — an existing symbol or module unquestionably covers the new
221
+ code's behavior, cited at `file:line`.
222
+ - **Behaviorally identical** — swapping the new code for the existing one
223
+ changes no observable behavior: same inputs, outputs, side effects, and error
224
+ handling.
225
+ - **Autonomously implementable** — the replacement is a mechanical edit (import
226
+ and call the existing symbol, delete the duplicate) needing no product
227
+ decision and no human judgment.
228
+
229
+ The reuse lens reports without editing. Qualifying improvements then run through
230
+ the same edit → verify → commit fix flow the rounds use, so they land in one
231
+ verified commit before convergence starts. The pass is best-effort: when no case
232
+ clears all three criteria, the run proceeds straight to convergence, and
233
+ `reuseNote` records what landed.
234
+
212
235
  ## What the workflow does each round
213
236
 
214
237
  See [`reference/convergence.md`](reference/convergence.md) for the full round
@@ -227,8 +250,12 @@ suite (`python -m pytest`) and keep scratch work in ephemeral temp dirs.
227
250
  - **Converge:** `parallel([Bugbot lens, code-review lens, bug-audit lens])` on
228
251
  the current HEAD, full `origin/main...HEAD` diff. Dedup findings; one
229
252
  `clean-coder` applies all fixes in a single commit, pushes, replies to and
230
- resolves any bot threads; re-verify next round on the new HEAD. When all
231
- three are clean on a stable HEAD, post the CLEAN bugteam audit artifact.
253
+ resolves any bot threads; re-verify next round on the new HEAD. Every edit
254
+ step ends with a pre-commit gate check: before its turn ends, the fixer
255
+ dry-runs the CODE_RULES commit gate (`code_rules_gate.py --staged`) and keeps
256
+ fixing until that gate would accept the commit — it makes no commit itself.
257
+ When all three are clean on a stable HEAD, post the CLEAN bugteam audit
258
+ artifact.
232
259
  A round whose findings are ALL code-standard violations (pure CODE_RULES/style,
233
260
  no behavioral impact) passes for convergence purposes: the workflow files a
234
261
  follow-up issue listing the findings, opens a draft environment-hardening PR
@@ -1,5 +1,42 @@
1
1
  # Convergence — round shape and the ready definition
2
2
 
3
+ ## Pre-flight: clear merge conflicts
4
+
5
+ Before the first round, the workflow checks once whether the PR branch conflicts
6
+ with `origin/main`. When GitHub reports a conflict (`mergeable` false or
7
+ `mergeable_state` dirty), one `clean-coder` rebases the branch onto `origin/main`
8
+ and resolves every conflict — gated the same way as every other code change: the
9
+ edit leaves the rebase in the working tree, a `code-verifier` binds a verdict to
10
+ it, and the commit step force-pushes with lease. The bug checks then run on a
11
+ conflict-free diff.
12
+
13
+ A PR that merges cleanly skips the rebase. A conflict that surfaces mid-run, when
14
+ `origin/main` advances during a later round, is caught by the convergence repair
15
+ at the end of the loop, which also rebases.
16
+
17
+ ## Reuse pass (runs after the conflict pre-flight, before convergence)
18
+
19
+ One reuse lens (`code-quality-agent`) reviews the full `origin/main...HEAD` diff
20
+ for code that re-implements behavior the repository already provides. It reports a
21
+ reuse improvement only when all three criteria hold together, and omits any case
22
+ where even one is in doubt:
23
+
24
+ 1. **Certain** — an existing symbol or module unquestionably covers the new
25
+ code's behavior, cited at `file:line`.
26
+ 2. **Behaviorally the same** — swapping the new code for the existing one
27
+ changes no observable behavior: same inputs, outputs, side effects, and
28
+ error handling.
29
+ 3. **Autonomously implementable** — the replacement is a mechanical edit (import
30
+ and call the existing symbol, drop the duplicate) needing no product
31
+ decision and no human judgment.
32
+
33
+ The lens reports without editing. Each qualifying improvement runs through the
34
+ same edit → verify → commit fix flow the rounds use, landing in one verified
35
+ commit before convergence begins. The pass is best-effort: when no case clears
36
+ all three criteria the run proceeds straight to convergence. Whatever the reuse
37
+ pass surfaces also joins the round findings, so the code-review lens re-checks
38
+ any improvement that did not land.
39
+
3
40
  ## The round loop
4
41
 
5
42
  The workflow holds three states and moves between them until the PR is ready or
@@ -26,7 +63,10 @@ tracks CONVERGE passes only and is never the cap.
26
63
  colliding threads.
27
64
  4. **Any findings** → one `clean-coder` applies every fix in a single test-first
28
65
  commit, pushes, then replies to and resolves each finding that carries a
29
- GitHub review thread. A round progresses when the fix lens lands a push that
66
+ GitHub review thread. Before its turn ends, the edit step dry-runs the
67
+ CODE_RULES commit gate (`code_rules_gate.py --staged`) over its staged
68
+ changes and keeps fixing until that gate would accept the commit, so the
69
+ later commit step never hits a gate rejection. A round progresses when the fix lens lands a push that
30
70
  moves HEAD, or when every finding was already addressed so no code change is
31
71
  needed yet each finding thread is still resolved (the fix lens reports
32
72
  `resolvedWithoutCommit` and the run re-converges on the unchanged HEAD). A
@@ -457,3 +457,93 @@ test('both standards-deferral call sites build standardsNote from the spawnStand
457
457
  'expected no unconditional hardening-PR claim in standardsNote',
458
458
  );
459
459
  });
460
+
461
+ test('a reuse-audit lens builder exists', () => {
462
+ assert.match(convergeSource, /function runReuseAuditPass\(/);
463
+ });
464
+
465
+ test('the reuse pass runs once before the convergence loop', () => {
466
+ const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
467
+ const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
468
+ assert.notEqual(reuseCallIndex, -1, 'expected the reuse pass to be invoked');
469
+ assert.notEqual(loopIndex, -1, 'expected the convergence loop to exist');
470
+ assert.ok(
471
+ reuseCallIndex < loopIndex,
472
+ 'expected the reuse pass to run before the convergence loop starts',
473
+ );
474
+ });
475
+
476
+ test('the reuse lens prompt enumerates all three qualifying criteria and an omit rule', () => {
477
+ const reusePrompt = lensPromptBody('runReuseAuditPass');
478
+ assert.match(reusePrompt, /CERTAIN/);
479
+ assert.match(reusePrompt, /BEHAVIORALLY IDENTICAL/);
480
+ assert.match(reusePrompt, /AUTONOMOUSLY IMPLEMENTABLE/);
481
+ assert.match(
482
+ reusePrompt,
483
+ /when any one is in doubt, omit the finding/i,
484
+ 'expected the reuse lens to drop any finding that fails a criterion',
485
+ );
486
+ });
487
+
488
+ test('the reuse lens reviews the full diff and does not edit', () => {
489
+ const reusePrompt = lensPromptBody('runReuseAuditPass');
490
+ assert.match(reusePrompt, /origin\/main\.\.\.HEAD/);
491
+ assert.match(
492
+ reusePrompt,
493
+ /Do NOT edit, commit, or push/,
494
+ 'expected the reuse lens to report findings without editing',
495
+ );
496
+ });
497
+
498
+ test('the reuse pass applies its findings through applyFixes, not the standards-deferral path', () => {
499
+ const reuseCallIndex = convergeSource.indexOf('await runReuseAuditPass(');
500
+ const loopIndex = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
501
+ const reuseBlock = convergeSource.slice(reuseCallIndex, loopIndex);
502
+ assert.match(
503
+ reuseBlock,
504
+ /applyFixes\(reuseHead, reuseFindings, 'reuse-pass'\)/,
505
+ 'expected the reuse pass to apply its findings via applyFixes',
506
+ );
507
+ assert.doesNotMatch(
508
+ reuseBlock,
509
+ /spawnStandardsFollowUp/,
510
+ 'expected the reuse pass to apply improvements, not defer them',
511
+ );
512
+ });
513
+
514
+ test('the reuse lens runs under the Reuse phase', () => {
515
+ const reusePrompt = lensPromptBody('runReuseAuditPass');
516
+ assert.match(reusePrompt, /phase: 'Reuse'/);
517
+ });
518
+
519
+ test('the pre-commit gate step is a shared constant that dry-runs the CODE_RULES commit gate', () => {
520
+ assert.match(convergeSource, /const PRE_COMMIT_GATE_STEP =/);
521
+ const stepStart = convergeSource.indexOf('const PRE_COMMIT_GATE_STEP =');
522
+ const stepEnd = convergeSource.indexOf('\n\n', stepStart);
523
+ const stepBody = convergeSource.slice(stepStart, stepEnd);
524
+ assert.match(stepBody, /code_rules_gate\.py/);
525
+ assert.match(stepBody, /--staged/);
526
+ assert.match(
527
+ stepBody,
528
+ /do NOT commit/i,
529
+ 'expected the gate step to forbid committing — it is a dry committability check',
530
+ );
531
+ });
532
+
533
+ const editStepBuilders = [
534
+ 'applyFixesEdit',
535
+ 'recoverCommitBlockEdit',
536
+ 'recoverVerifyFailEdit',
537
+ 'repairConvergenceEdit',
538
+ 'standardsFollowUpEdit',
539
+ ];
540
+
541
+ for (const builderName of editStepBuilders) {
542
+ test(`${builderName} appends the pre-commit gate step to its edit prompt`, () => {
543
+ assert.match(
544
+ lensPromptBody(builderName),
545
+ /\+\s*PRE_COMMIT_GATE_STEP/,
546
+ `expected ${builderName} to append PRE_COMMIT_GATE_STEP`,
547
+ );
548
+ });
549
+ }
@@ -0,0 +1,98 @@
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 functionBody(functionName) {
11
+ const functionStart = convergeSource.indexOf(`function ${functionName}(`);
12
+ assert.notEqual(functionStart, -1, `expected ${functionName} to exist`);
13
+ const nextFunctionMatch = /\n(?:async )?function /.exec(convergeSource.slice(functionStart + 1));
14
+ const functionEnd =
15
+ nextFunctionMatch === null ? convergeSource.length : functionStart + 1 + nextFunctionMatch.index;
16
+ return convergeSource.slice(functionStart, functionEnd);
17
+ }
18
+
19
+ const helperModule = new Function(
20
+ `${functionBody('isMergeConflicting')}\nreturn { isMergeConflicting };`,
21
+ )();
22
+ const { isMergeConflicting } = helperModule;
23
+
24
+ test('isMergeConflicting treats a dead check agent (null/undefined) as not conflicting', () => {
25
+ assert.equal(isMergeConflicting(null), false);
26
+ assert.equal(isMergeConflicting(undefined), false);
27
+ });
28
+
29
+ test('isMergeConflicting reports a conflict only when the check returned conflicting:true', () => {
30
+ assert.equal(isMergeConflicting({ conflicting: true }), true);
31
+ assert.equal(isMergeConflicting({ conflicting: false }), false);
32
+ });
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');
37
+ assert.match(
38
+ body,
39
+ /do not edit, commit, push, or rebase|read only/i,
40
+ 'expected the probe to be read-only',
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');
46
+ });
47
+
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');
51
+ assert.match(
52
+ body,
53
+ /do not push|no push|not push/i,
54
+ 'expected the edit step to leave the push to the commit step',
55
+ );
56
+ assert.match(body, /agentType:\s*'clean-coder'/, 'expected the edit step to use clean-coder');
57
+ });
58
+
59
+ test('resolveMergeConflicts runs check -> edit -> verify -> commit and gates the push on the verdict', () => {
60
+ 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(');
65
+ assert.notEqual(checkIndex, -1, 'expected the conflict check to run');
66
+ assert.notEqual(editIndex, -1, 'expected the rebase edit step to run');
67
+ assert.notEqual(verifyIndex, -1, 'expected the verify step to run');
68
+ assert.notEqual(commitIndex, -1, 'expected the commit step to run');
69
+ assert.ok(
70
+ checkIndex < editIndex && editIndex < verifyIndex && verifyIndex < commitIndex,
71
+ 'expected the order check -> edit -> verify -> commit',
72
+ );
73
+ 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
+ });
80
+
81
+ test('resolveMergeConflicts rebases only when the check reports a conflict', () => {
82
+ const body = functionBody('resolveMergeConflicts');
83
+ assert.match(body, /isMergeConflicting\(/, 'expected the orchestrator to branch on the conflict decision');
84
+ assert.match(
85
+ body,
86
+ /if \(!isMergeConflicting\([^)]*\)\) return head/,
87
+ 'expected a clean PR to return the unchanged HEAD without rebasing',
88
+ );
89
+ });
90
+
91
+ test('the merge-conflict pre-flight runs once before the round loop, ahead of the parallel bug-check lenses', () => {
92
+ const preflightCall = convergeSource.indexOf('await resolveMergeConflicts(');
93
+ const whileLoop = convergeSource.indexOf('while (iterations < CONFIG.maxIterations)');
94
+ const firstLens = convergeSource.indexOf('const lenses = await parallel(');
95
+ assert.notEqual(preflightCall, -1, 'expected the pre-flight resolveMergeConflicts call site');
96
+ assert.ok(preflightCall < whileLoop, 'expected the pre-flight to run before the round loop');
97
+ assert.ok(preflightCall < firstLens, 'expected the pre-flight to run before the first bug-check lenses');
98
+ });