claude-dev-env 1.75.0 → 1.76.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/_shared/pr-loop/scripts/code_rules_gate.py +60 -5
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/CLAUDE.md +1 -0
- package/_shared/pr-loop/scripts/pr_loop_shared_constants/inline_duplicate_body_span_constants.py +22 -0
- package/_shared/pr-loop/scripts/tests/test_code_rules_gate.py +147 -3
- package/docs/CODE_RULES.md +1 -1
- package/hooks/blocking/CLAUDE.md +2 -2
- package/hooks/blocking/claude_md_orphan_file_blocker.py +170 -20
- package/hooks/blocking/code_rules_duplicate_body.py +378 -26
- package/hooks/blocking/code_rules_enforcer.py +36 -5
- package/hooks/blocking/code_rules_imports_logging.py +679 -1
- package/hooks/blocking/code_rules_shared.py +8 -5
- package/hooks/blocking/code_rules_test_assertions.py +6 -7
- package/hooks/blocking/test_claude_md_orphan_file_blocker.py +484 -0
- package/hooks/blocking/test_code_rules_enforcer_cap_meta.py +1 -0
- package/hooks/blocking/test_code_rules_enforcer_import_block_sort.py +157 -0
- package/hooks/blocking/test_code_rules_enforcer_same_file_inline_duplicate.py +466 -0
- package/hooks/blocking/test_code_rules_enforcer_split_test_assertions.py +11 -9
- package/hooks/blocking/test_code_rules_js_resume_task_enumeration.py +758 -0
- package/hooks/blocking/test_code_rules_logging_printf_tokens.py +134 -0
- package/hooks/blocking/test_verification_verdict_store.py +66 -1
- package/hooks/blocking/test_verifier_verdict_minter.py +64 -5
- package/hooks/blocking/verification_verdict_store.py +19 -5
- package/hooks/blocking/verifier_verdict_minter.py +18 -15
- package/hooks/hooks_constants/blocking_check_limits.py +30 -1
- package/hooks/hooks_constants/claude_md_orphan_file_blocker_constants.py +52 -24
- package/hooks/hooks_constants/code_rules_enforcer_constants.py +41 -1
- package/hooks/hooks_constants/duplicate_function_body_constants.py +21 -5
- package/package.json +1 -1
- package/rules/claude-md-orphan-file.md +7 -8
- package/rules/docstring-prose-matches-implementation.md +1 -0
- package/rules/package-inventory-stale-entry.md +8 -0
- package/skills/anthropic-plan/CLAUDE.md +1 -1
- package/skills/anthropic-plan/SKILL.md +15 -2
- package/skills/autoconverge/workflow/converge.contract.test.mjs +12 -19
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +71 -0
- package/skills/autoconverge/workflow/converge.mjs +86 -110
- package/skills/bugteam/scripts/bugteam_code_rules_gate.py +58 -4
- package/skills/bugteam/scripts/bugteam_scripts_constants/bugteam_code_rules_gate_constants.py +9 -0
- package/skills/bugteam/scripts/test_bugteam_code_rules_gate.py +42 -0
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
"""Constants for the duplicate-function-body
|
|
1
|
+
"""Constants for the duplicate-function-body scans in ``code_rules_enforcer``.
|
|
2
2
|
|
|
3
|
-
The blocking scan flags a top-level function whose body is
|
|
4
|
-
to a top-level function already defined in a sibling
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
The cross-file blocking scan flags a top-level function whose body is
|
|
4
|
+
structurally identical to a top-level function already defined in a sibling
|
|
5
|
+
``.py`` module in the same directory. The same-file blocking scan flags a
|
|
6
|
+
top-level function whose body appears verbatim as a contiguous statement block
|
|
7
|
+
inside another function in the same module. Both catch the Reuse-before-create /
|
|
8
|
+
DRY violation where a block of logic is copied instead of called from one shared
|
|
9
|
+
home, so a fix that lands in one copy leaves the other carrying the bug.
|
|
7
10
|
|
|
8
11
|
The ``CROSS_SKILL_*`` and ``SKILL*`` constants feed the non-blocking companion
|
|
9
12
|
advisory: a helper copied between two skills' ``scripts`` directories, where a
|
|
@@ -12,6 +15,7 @@ skill on stderr rather than denying the write.
|
|
|
12
15
|
"""
|
|
13
16
|
|
|
14
17
|
MINIMUM_DUPLICATE_BODY_STATEMENTS: int = 3
|
|
18
|
+
MINIMUM_INLINE_DUPLICATE_BODY_STATEMENTS: int = 1
|
|
15
19
|
MAX_DUPLICATE_BODY_ISSUES: int = 25
|
|
16
20
|
DUNDER_INIT_FILENAME: str = "__init__.py"
|
|
17
21
|
PYTHON_SOURCE_SUFFIX: str = ".py"
|
|
@@ -20,6 +24,18 @@ DUPLICATE_BODY_GUIDANCE: str = (
|
|
|
20
24
|
"extract a single shared helper (for example in hooks_constants/) and "
|
|
21
25
|
"import it from both modules instead of copying it (Reuse before create / DRY)"
|
|
22
26
|
)
|
|
27
|
+
SAME_FILE_INLINE_DUPLICATE_GUIDANCE: str = (
|
|
28
|
+
"this function body is also present inline as a contiguous statement block "
|
|
29
|
+
"inside another function in the same module; call this helper from that "
|
|
30
|
+
"function instead of repeating the block, so a single helper backs both call "
|
|
31
|
+
"sites and a fix cannot land in one copy while the other keeps the bug "
|
|
32
|
+
"(Reuse before create / DRY)"
|
|
33
|
+
)
|
|
34
|
+
SAME_FILE_INLINE_DUPLICATE_SPAN_SUFFIX_TEMPLATE: str = (
|
|
35
|
+
"(inline duplicate body spans: helper at line {helper_start} spanning "
|
|
36
|
+
"{helper_length} lines, enclosing at line {enclosing_start} spanning "
|
|
37
|
+
"{enclosing_length} lines)"
|
|
38
|
+
)
|
|
23
39
|
|
|
24
40
|
SKILLS_DIRECTORY_NAME: str = "skills"
|
|
25
41
|
SKILL_SCRIPTS_DIRECTORY_NAME: str = "scripts"
|
package/package.json
CHANGED
|
@@ -1,24 +1,23 @@
|
|
|
1
1
|
# Orphan File Reference in a Per-Directory CLAUDE.md
|
|
2
2
|
|
|
3
|
-
**When this applies:** Any Write, Edit, or MultiEdit to a file named `CLAUDE.md` that lists files in a markdown table whose first column names each file in backticks.
|
|
3
|
+
**When this applies:** Any Write, Edit, or MultiEdit to a file named `CLAUDE.md` that lists files in a markdown table whose first column names each file in backticks, or that shows run commands invoking those files inside fenced code blocks.
|
|
4
4
|
|
|
5
5
|
## Rule
|
|
6
6
|
|
|
7
|
-
Every bare filename a per-directory `CLAUDE.md`
|
|
7
|
+
Every bare filename a per-directory `CLAUDE.md` names points at a file that exists in the directory subtree the `CLAUDE.md` describes — both the filenames its table cells list and the scripts its fenced run commands invoke (`python script.py`). A table cell or a run command naming a file that exists nowhere in that subtree points a reader at something that is not there: the doc claims a file the directory does not hold.
|
|
8
8
|
|
|
9
|
-
When you add a table row, the file it names already exists in this directory or a subdirectory of it. When you remove a file, drop the row that named it.
|
|
9
|
+
When you add a table row or a run command, the file it names already exists in this directory or a subdirectory of it. When you remove a file, drop the row and the run command that named it.
|
|
10
10
|
|
|
11
11
|
## What the gate checks
|
|
12
12
|
|
|
13
13
|
The `claude_md_orphan_file_blocker.py` hook runs on every Write, Edit, and MultiEdit whose target basename is `CLAUDE.md`. It:
|
|
14
14
|
|
|
15
15
|
1. Reads the content the tool would leave on disk. For a Write that is the full `content`. For an Edit or MultiEdit it reconstructs the post-edit file — the existing on-disk file with the replacements applied — and also notes which orphans the file already held before the edit, so a pre-existing orphan on an untouched line is excluded and only an orphan the edit introduces is reported; when the existing file cannot be read, it scans the raw `new_string` fragment(s) instead.
|
|
16
|
-
2.
|
|
17
|
-
3.
|
|
18
|
-
4. Blocks the write when a named file exists nowhere under the scan root — the `CLAUDE.md` directory's parent, which covers the directory, its subdirectories, and its siblings. A filesystem error that halts the whole subtree walk fails open (the write proceeds), so an unreadable tree never blocks a write.
|
|
16
|
+
2. Collects two kinds of referenced filename. Table cells: the first column of each markdown table row **outside** a fenced code block, keeping cells that name a bare filename wrapped in backticks, no path separator, not a slash-command, ending in a known file extension (`.py`, `.md`, `.json`, `.mjs`, `.js`, `.ts`, `.ps1`, `.cmd`, `.ahk`, `.yml`, `.yaml`, `.sh`, `.txt`, `.cfg`, `.toml`, `.ini`). Run commands: each line **inside** a fenced code block (between a ``` or `~~~` fence pair) that invokes an interpreter (`python`, `python.exe`, `python3`, `node`, `pwsh`, `powershell`, `bash`, `sh`, `ruby`, `perl`) on a script, taking that script's basename when it ends in `.py`, `.mjs`, `.js`, `.ts`, `.ps1`, `.sh`, `.rb`, or `.pl`. A fenced *table row* is an example, not a live listing, so it contributes no table-cell filename; a fenced *run command* is the contract a reader runs, so its script filename is checked.
|
|
17
|
+
3. Blocks the write when a referenced filename — from a table cell or a fenced run command — exists nowhere under the scan root — the `CLAUDE.md` directory's parent, which covers the directory, its subdirectories, and its siblings. A filesystem error that halts the whole subtree walk fails open (the write proceeds), so an unreadable tree never blocks a write.
|
|
19
18
|
|
|
20
|
-
The check stays quiet for a target that is not a `CLAUDE.md`, for a table cell that holds a path, a subdirectory ending in `/`, or a slash-command, for a table row inside a fenced code block, and for a table whose content names an explicit relative-path source (a `../` token), since that table documents files that sit outside the subtree by design.
|
|
19
|
+
The check stays quiet for a target that is not a `CLAUDE.md`, for a table cell that holds a path, a subdirectory ending in `/`, or a slash-command, for a table row inside a fenced code block, for an inline `python x.py` mention outside a fence (prose, not a runnable contract), and for a table whose content names an explicit relative-path source (a `../` token), since that table documents files that sit outside the subtree by design.
|
|
21
20
|
|
|
22
21
|
## Why this is a hook, not a lint pass
|
|
23
22
|
|
|
24
|
-
A table row that names an absent file reads as a contract: a reader trusts the listing to map the directory. A wrong row sends the reader looking for a file that is not there
|
|
23
|
+
A table row or a run command that names an absent file reads as a contract: a reader trusts the listing to map the directory and trusts the shown command to run. A wrong row sends the reader looking for a file that is not there; a stale run command fails the moment the reader runs it. Both erode trust in every other entry. Catching them as each line is written keeps the doc and the directory in step.
|
|
@@ -16,6 +16,7 @@ Read the body and the docstring side by side:
|
|
|
16
16
|
- **Suppressor / skip lists.** A body with several early returns that suppress the check names each suppressor in the prose.
|
|
17
17
|
- **Shared fallback routes.** A summary that scopes a fallback call to one condition names every condition that reaches that call. When the body routes to the same fallback from two or more early-return guards (`if a is None: fallback(); return` and `if random() < p: fallback(); return`), the prose enumerates both guards. The `check_docstring_fallback_branch_coverage` gate blocks the single-condition form of this drift at Write/Edit time.
|
|
18
18
|
- **Step order.** A docstring that says `A then B then C` matches the call order in the body. A step enumeration that names the body's linear steps also names every corrective step the body guards inside an `if`/`elif` branch (`if not await cancel_and_reinitiate_update(...): return`). The `check_docstring_step_enumeration_dispatch_coverage` gate blocks the branch-guarded-dispatch form of this drift — a step-enumeration docstring that omits a two-or-more-token dispatch step the body guards inside a branch — at Write/Edit time.
|
|
19
|
+
- **JS/`.mjs` resume-task enumerations.** A `spawn<Role>Agent` JSDoc that enumerates its sibling `resume<Role>Agent`'s resume tasks in a parenthetical `resume (repair-verify, hardening-verify)` list names every `task === '<name>'` branch the resume body dispatches on. The `check_js_resume_task_enumeration_coverage` gate blocks the JavaScript form of this drift — a spawn JSDoc whose resume enumeration omits a dispatched task — at Write/Edit time. This is the `.mjs` slice of the same Category O6 standard the Python gates carry; the Python AST docstring gates never inspect JavaScript source.
|
|
19
20
|
- **Returns-clause cardinality.** A `Returns:` clause that names a dict-key prefix family with a plural noun (`the sheen stops`) matches the count of keys in that family in the returned dict literal. When the dict holds one key in the family (`sheen_mid`), the noun is singular (`the sheen stop`); a plural noun there claims two or more entries the dict does not hold. The `check_docstring_returns_plural_cardinality` gate blocks the single-key-with-plural-noun form of this drift at Write/Edit time.
|
|
20
21
|
- **Predicate breadth.** A boolean helper whose prose promises a narrow check accepts only the inputs the prose names — no broader input class the name and prose do not mention.
|
|
21
22
|
- **Exclusion-clause distinguisher.** A docstring sentence that says a named category of input "are not" / "is not" the thing the function flags (`plain logging, screenshot, or method-on-local calls inside a branch are not dispatch steps`) keys the exclusion to the same axis the body's classification keys on. When the body decides on one axis (a call sits in an `If.test` guard versus a plain statement) but the prose excludes on a different axis (the call's receiver shape — a method on a local), the exclusion clause names a category the body still flags: a guarded method-on-local call is flagged even though the prose lists method-on-local calls as excluded. Read the body's actual branch condition, then state the exclusion on that same axis (`plain (unguarded) calls inside a branch body are not dispatch steps`), so every member the prose excludes is a member the body also excludes.
|
|
@@ -8,6 +8,14 @@ A package directory that documents its own files in a `README.md` Layout table o
|
|
|
8
8
|
|
|
9
9
|
When you create a new production file in such a directory, add an entry naming it — a row in the `README.md` table, a bullet in the `CLAUDE.md` list — in the same change. The entry names the file in backticks and says what it does.
|
|
10
10
|
|
|
11
|
+
## Companion: keep the Purpose/scope sentence in step with the new responsibility
|
|
12
|
+
|
|
13
|
+
The file-list entry is the deterministic slice the gate enforces. A package inventory also carries a free-prose scope sentence — a `## Purpose` paragraph in a `CLAUDE.md`, a one-line summary the parent directory's inventory gives each subdirectory — that names the responsibilities the package's modules cover. When the new module adds a responsibility the scope sentence omits, the same change broadens that sentence to name it, and updates the parent inventory's one-line summary of this subdirectory to match.
|
|
14
|
+
|
|
15
|
+
Take a `files/` package whose `Purpose` reads "Holds helpers for downloading files over HTTP and extracting zip archives" and whose parent summary reads "file download, extraction, and path config helpers". Once a `force_remove.py` module that removes a directory tree sits beside the download helpers, both sentences name a narrower responsibility set than the directory holds. The required file-list bullet alone leaves that gap open. Broaden the `Purpose` sentence to name directory removal, and broaden the parent summary to match, in the same change that adds the module and its bullet.
|
|
16
|
+
|
|
17
|
+
This scope-sentence slice is free prose: a hook cannot derive a module's responsibility from its filename, so the gate leaves it to judgment. It is the judgment companion to the file-list entry the gate enforces, and it belongs in the same change. This is the `category-o-docstring-vs-impl-drift` (O8) orphaned-doc-claim shape applied to a package inventory: a behavior change orphans a scope claim the prose still makes.
|
|
18
|
+
|
|
11
19
|
## What the gate checks
|
|
12
20
|
|
|
13
21
|
The `package_inventory_stale_blocker.py` hook runs on every Write whose target is a new file (a path not yet on disk). It:
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
**Trigger:** `/anthropic-plan`, `/plan`, "plan this first", "think before coding", "make a plan", "scope this out", "don't code yet", and non-trivial requests that need source-grounded design before build work.
|
|
4
4
|
|
|
5
|
-
Creates a repo-local plan packet under `docs/plans/<slug>/` by running the `plan-packet.mjs` workflow. The packet holds context, spec, implementation steps, validation, and a handoff prompt for the build agent. The skill stops before any production code changes.
|
|
5
|
+
Creates a repo-local plan packet under `docs/plans/<slug>/` by running the `plan-packet.mjs` workflow. The skill first drafts a short starting plan and gets the user's approval in plan mode (`EnterPlanMode` / `ExitPlanMode`); on approval it runs the workflow. The packet holds context, spec, implementation steps, validation, and a handoff prompt for the build agent. The skill stops before any production code changes.
|
|
6
6
|
|
|
7
7
|
## Subdirectories
|
|
8
8
|
|
|
@@ -5,9 +5,20 @@ description: Workflow-backed implementation planning that creates a deep repo-lo
|
|
|
5
5
|
|
|
6
6
|
# Anthropic Plan
|
|
7
7
|
|
|
8
|
-
Create a source-grounded plan packet through the Claude Code Workflow runtime.
|
|
8
|
+
Create a source-grounded plan packet through the Claude Code Workflow runtime. First draft a short starting plan and get it approved in plan mode; then the workflow builds the full repo-local `docs/plans/<slug>/` packet — context, spec, implementation, validation, and handoff docs. Stop before implementation.
|
|
9
9
|
|
|
10
|
-
##
|
|
10
|
+
## Plan first (plan mode)
|
|
11
|
+
|
|
12
|
+
Before any worktree or workflow, build a short starting plan in plan mode and get the user's approval. This is a cheap gate: it confirms the direction before the workflow spends tokens building the full packet.
|
|
13
|
+
|
|
14
|
+
1. Enter plan mode with `EnterPlanMode` if the session is not already in it.
|
|
15
|
+
2. Read the handful of files closest to the request — the entry points, the modules the change touches, the tests that cover them — enough to ground the approach in real source.
|
|
16
|
+
3. Write a short plan: the goal in a sentence or two, the approach, the files the build will touch, and the main risks or open choices. Use `AskUserQuestion` for any product choice that local context cannot settle.
|
|
17
|
+
4. Call `ExitPlanMode` to hand the plan to the user for approval.
|
|
18
|
+
|
|
19
|
+
On approval, fold the approved scope and direction into the `task` payload at launch, then continue with the steps below. If the user revises the plan, fold the changes in and present it again. Do not isolate a worktree or launch the workflow until the starting plan is approved.
|
|
20
|
+
|
|
21
|
+
## Isolate the session
|
|
11
22
|
|
|
12
23
|
The workflow's background subagents write the packet into the working tree. A background session that has not isolated into a worktree cannot write a shared checkout — the background-isolation guard rejects the write. So put the session in a worktree before launching the workflow:
|
|
13
24
|
|
|
@@ -27,6 +38,8 @@ Workflow({
|
|
|
27
38
|
})
|
|
28
39
|
```
|
|
29
40
|
|
|
41
|
+
The `task` string carries the user request together with the approved starting plan, so the packet reflects the direction the user signed off on.
|
|
42
|
+
|
|
30
43
|
If the Workflow tool is unavailable, say `anthropic-plan requires the Workflow tool; aborting` and stop.
|
|
31
44
|
|
|
32
45
|
## Self-healing writes
|
|
@@ -294,11 +294,11 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
|
|
|
294
294
|
}
|
|
295
295
|
});
|
|
296
296
|
|
|
297
|
-
test('resumeFixerAgent
|
|
297
|
+
test('resumeFixerAgent never verifies its own session — verification belongs to the separate verifier', () => {
|
|
298
298
|
const fixerBody = lensPromptBody('resumeFixerAgent');
|
|
299
|
-
assert.
|
|
300
|
-
assert.
|
|
301
|
-
assert.match(fixerBody, /
|
|
299
|
+
assert.doesNotMatch(fixerBody, /buildVerdictFenceSteps\(/, 'expected the fixer to not emit a verdict fence — the separate verifier does');
|
|
300
|
+
assert.doesNotMatch(fixerBody, /agentType:\s*'code-verifier'/, 'expected the fixer session to be clean-coder only');
|
|
301
|
+
assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
|
|
302
302
|
});
|
|
303
303
|
|
|
304
304
|
test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
|
|
@@ -315,15 +315,14 @@ test('resumeVerifierAgent uses --manifest-hash-for-branch with the hardening bra
|
|
|
315
315
|
);
|
|
316
316
|
});
|
|
317
317
|
|
|
318
|
-
test('resumeVerifierAgent
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
}
|
|
318
|
+
test('resumeVerifierAgent passes PR coordinates to buildVerdictFenceSteps for the fix-verify and repair-verify tasks', () => {
|
|
319
|
+
const verifyBody = lensPromptBody('resumeVerifierAgent');
|
|
320
|
+
assert.match(verifyBody, /task === 'fix-verify'/, 'expected resumeVerifierAgent to carry the fix-path verify task');
|
|
321
|
+
assert.match(
|
|
322
|
+
verifyBody,
|
|
323
|
+
/buildVerdictFenceSteps\(input\.owner, input\.repo, input\.prNumber\)/,
|
|
324
|
+
'expected resumeVerifierAgent to pass PR coordinates to buildVerdictFenceSteps',
|
|
325
|
+
);
|
|
327
326
|
});
|
|
328
327
|
|
|
329
328
|
test('the commit path in resumeFixerAgent forbids further edits and uses clean-coder', () => {
|
|
@@ -663,7 +662,6 @@ const newSpawnResumeHelpers = [
|
|
|
663
662
|
{ name: 'resumeGeneralUtilityAgent', isAsync: false },
|
|
664
663
|
{ name: 'spawnConvergenceCheckAgent', isAsync: true },
|
|
665
664
|
{ name: 'resumeConvergenceCheckAgent', isAsync: false },
|
|
666
|
-
{ name: 'extractVerdict', isAsync: false },
|
|
667
665
|
];
|
|
668
666
|
|
|
669
667
|
for (const { name, isAsync } of newSpawnResumeHelpers) {
|
|
@@ -824,8 +822,3 @@ test('extractVerifyObjection calls parseLastVerdictFence', () => {
|
|
|
824
822
|
const objectionBody = lensPromptBody('extractVerifyObjection');
|
|
825
823
|
assert.match(objectionBody, /parseLastVerdictFence\(/, 'expected extractVerifyObjection to call the shared parser');
|
|
826
824
|
});
|
|
827
|
-
|
|
828
|
-
test('extractVerdict calls parseLastVerdictFence', () => {
|
|
829
|
-
const verdictBody = lensPromptBody('extractVerdict');
|
|
830
|
-
assert.match(verdictBody, /parseLastVerdictFence\(/, 'expected extractVerdict to call the shared parser');
|
|
831
|
-
});
|
|
@@ -167,6 +167,77 @@ test('applyFixes routes through spawnFixerAgent and fixerWithRecovery', () => {
|
|
|
167
167
|
assert.match(applyFixesBody, /fixerWithRecovery\(/, 'expected applyFixes to call fixerWithRecovery');
|
|
168
168
|
});
|
|
169
169
|
|
|
170
|
+
test('applyFixes spawns a separate verifier and passes both ids into fixerWithRecovery', () => {
|
|
171
|
+
const applyFixesBody = functionSource('applyFixes');
|
|
172
|
+
assert.match(
|
|
173
|
+
applyFixesBody,
|
|
174
|
+
/spawnVerifierAgent\(/,
|
|
175
|
+
'expected applyFixes to spawn a separate verifier agent so the fix-path verdict is independent of the fixer',
|
|
176
|
+
);
|
|
177
|
+
const fixerSpawnIndex = applyFixesBody.search(/spawnFixerAgent\(/);
|
|
178
|
+
const verifierSpawnIndex = applyFixesBody.search(/spawnVerifierAgent\(/);
|
|
179
|
+
assert.notEqual(fixerSpawnIndex, -1, 'expected applyFixes to spawn the fixer');
|
|
180
|
+
assert.notEqual(verifierSpawnIndex, -1, 'expected applyFixes to spawn the verifier');
|
|
181
|
+
assert.match(
|
|
182
|
+
applyFixesBody,
|
|
183
|
+
/fixerWithRecovery\(\s*fixerAgentId,\s*verifierId,/,
|
|
184
|
+
'expected applyFixes to pass both the fixer and verifier ids into fixerWithRecovery',
|
|
185
|
+
);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
test('fixerWithRecovery runs verify on the verifier id and edits/commits on the fixer id', () => {
|
|
189
|
+
const recoveryBody = functionSource('fixerWithRecovery');
|
|
190
|
+
assert.match(
|
|
191
|
+
recoveryBody,
|
|
192
|
+
/resumeVerifierAgent\(\s*verifierId,/,
|
|
193
|
+
'expected fixerWithRecovery to resume the separate verifier for the verify step',
|
|
194
|
+
);
|
|
195
|
+
assert.match(
|
|
196
|
+
recoveryBody,
|
|
197
|
+
/resumeFixerAgent\(\s*fixerAgentId,\s*'commit'/,
|
|
198
|
+
'expected fixerWithRecovery to resume the fixer for the commit step',
|
|
199
|
+
);
|
|
200
|
+
assert.doesNotMatch(
|
|
201
|
+
recoveryBody,
|
|
202
|
+
/resumeFixerAgent\(\s*fixerAgentId,\s*'(?:verify-commit|fix-verify)'/,
|
|
203
|
+
'expected the verify step never to resume the fixer session — the verifier must grade a different session than the one that edits',
|
|
204
|
+
);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
test('the fix-path verify resumes a different agent id than the fix-path edits, mirroring the repair path', () => {
|
|
208
|
+
const recoveryBody = functionSource('fixerWithRecovery');
|
|
209
|
+
const verifyResumeMatch = /resumeVerifierAgent\(\s*(\w+),/.exec(recoveryBody);
|
|
210
|
+
const editResumeMatch = /resumeFixerAgent\(\s*(\w+),/.exec(recoveryBody);
|
|
211
|
+
assert.ok(verifyResumeMatch, 'expected a verify resume call naming its agent id');
|
|
212
|
+
assert.ok(editResumeMatch, 'expected an edit/commit resume call naming its agent id');
|
|
213
|
+
assert.notEqual(
|
|
214
|
+
verifyResumeMatch[1],
|
|
215
|
+
editResumeMatch[1],
|
|
216
|
+
'expected the verify step to resume a different agent id than the edit/commit step',
|
|
217
|
+
);
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('the verifier carries a fix-verify task so the fix path verdict comes from the verifier group', () => {
|
|
221
|
+
const verifierBody = functionSource('resumeVerifierAgent');
|
|
222
|
+
assert.match(verifierBody, /task === 'fix-verify'/, 'expected resumeVerifierAgent to handle the fix-verify task');
|
|
223
|
+
assert.match(verifierBody, /buildVerdictFenceSteps\(/, 'expected the fix-verify task to emit a verdict fence');
|
|
224
|
+
assert.match(verifierBody, /agentType:\s*'code-verifier'/, 'expected the fix-verify task to run as code-verifier');
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
test('resumeFixerAgent no longer verifies its own edits — no code-verifier verify-commit task remains', () => {
|
|
228
|
+
const fixerBody = functionSource('resumeFixerAgent');
|
|
229
|
+
assert.doesNotMatch(
|
|
230
|
+
fixerBody,
|
|
231
|
+
/task === 'verify-commit'/,
|
|
232
|
+
'expected the fixer to no longer carry the verify-commit task that graded its own session',
|
|
233
|
+
);
|
|
234
|
+
assert.doesNotMatch(
|
|
235
|
+
fixerBody,
|
|
236
|
+
/agentType:\s*'code-verifier'/,
|
|
237
|
+
'expected the fixer session to be clean-coder only — a separate verifier grades the working tree',
|
|
238
|
+
);
|
|
239
|
+
});
|
|
240
|
+
|
|
170
241
|
test('repairConvergence routes its commit through commitWithRecovery wired to the repair-path steps', () => {
|
|
171
242
|
const repairBody = functionSource('repairConvergence');
|
|
172
243
|
assert.match(repairBody, /commitWithRecovery\(/, 'expected repairConvergence to call commitWithRecovery');
|
|
@@ -128,27 +128,32 @@ function resumeGitAgent(agentId, task, head) {
|
|
|
128
128
|
|
|
129
129
|
/**
|
|
130
130
|
* Spawn the fixer clean-coder agent for one fix batch, establishing its role and
|
|
131
|
-
* the PR coordinates so each later resume (
|
|
132
|
-
*
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
*
|
|
131
|
+
* the PR coordinates so each later resume (commit and recovery edits) continues
|
|
132
|
+
* the same session. The fixer never verifies — a separate verifier agent grades
|
|
133
|
+
* the working tree, so the verdict comes from a different session than the one
|
|
134
|
+
* that commits and recovers, mirroring the repair and conflict paths. The spawn
|
|
135
|
+
* makes no edits — the first recovery resume is the earliest step that touches
|
|
136
|
+
* the working tree. Returns the runtime agent id so the resume calls target the
|
|
137
|
+
* live session; a runtime without resume support returns no agent id, and each
|
|
138
|
+
* resume falls back to a fresh spawn.
|
|
136
139
|
* @param {string} head PR HEAD SHA
|
|
137
140
|
* @param {Array<object>} findings the findings to fix
|
|
138
141
|
* @param {string} sourceLabel short description of where the findings came from
|
|
139
|
-
* @param {string} task initial task name
|
|
140
142
|
* @returns {Promise<string|undefined>} the runtime agent id, or undefined when the runtime returns none
|
|
141
143
|
*/
|
|
142
|
-
async function spawnFixerAgent(head, findings, sourceLabel
|
|
144
|
+
async function spawnFixerAgent(head, findings, sourceLabel) {
|
|
143
145
|
const result = await convergeAgent(
|
|
144
|
-
`You are the fixer agent for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Across this session you run a sequence of steps
|
|
146
|
+
`You are the fixer agent for ${findings.length} finding(s) (${sourceLabel}) on ${prCoordinates}, HEAD ${head}. The edit step left fixes in the working tree, uncommitted. Across this session you run a sequence of steps: commit and push the working-tree fixes once a separate verifier passes them, and recover when a verify objection or a commit-gate block needs another edit. A separate verifier agent grades the working tree, so you never verify your own edits. Make NO edits in this first turn — confirm only that the working tree is on the PR branch at HEAD ${head} with uncommitted fixes present, then wait for the next step's instructions. Reply READY.`,
|
|
145
147
|
{ label: `fixer:${sourceLabel}`, phase: 'Converge', agentType: 'clean-coder' },
|
|
146
148
|
)
|
|
147
149
|
return result?.agentId
|
|
148
150
|
}
|
|
149
151
|
|
|
150
152
|
/**
|
|
151
|
-
* Resume the fixer agent for
|
|
153
|
+
* Resume the fixer agent for commit or recovery edits. The fixer never verifies;
|
|
154
|
+
* a separate verifier agent emits the verdict, so commit, verify-recover, and
|
|
155
|
+
* commit-recover all run on the editing session while the verdict that gates the
|
|
156
|
+
* commit comes from a different session.
|
|
152
157
|
* @param {string} agentId the agent id from spawnFixerAgent
|
|
153
158
|
* @param {string} task the short task name
|
|
154
159
|
* @param {object} context task-specific context
|
|
@@ -156,18 +161,6 @@ async function spawnFixerAgent(head, findings, sourceLabel, task) {
|
|
|
156
161
|
*/
|
|
157
162
|
function resumeFixerAgent(agentId, task, context) {
|
|
158
163
|
const label = `fixer:${context.sourceLabel}`
|
|
159
|
-
if (task === 'verify-commit') {
|
|
160
|
-
const findingsBlock = renderFindingsBlock(context.findings)
|
|
161
|
-
return convergeAgent(
|
|
162
|
-
`You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
163
|
-
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
164
|
-
`Steps:\n` +
|
|
165
|
-
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
166
|
-
`2. Verify the uncommitted working-tree changes resolve every finding above: run the relevant tests and the named gates against the working tree. Read the diff (git diff) and confirm each finding is fixed test-first per CODE_RULES.\n` +
|
|
167
|
-
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
168
|
-
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
169
|
-
)
|
|
170
|
-
}
|
|
171
164
|
if (task === 'commit') {
|
|
172
165
|
return convergeAgent(
|
|
173
166
|
`You are the COMMIT step for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree and the verify step passed, so a verifier verdict already binds to this exact working tree.\n\n` +
|
|
@@ -181,6 +174,20 @@ function resumeFixerAgent(agentId, task, context) {
|
|
|
181
174
|
{ label, phase: 'Converge', schema: FIX_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
182
175
|
)
|
|
183
176
|
}
|
|
177
|
+
if (task === 'commit-recover') {
|
|
178
|
+
const attempt = context.attempt || 1
|
|
179
|
+
return convergeAgent(
|
|
180
|
+
`You are the COMMIT-RECOVERY fixer (attempt ${attempt}) for fixes (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.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` +
|
|
181
|
+
`The blocking hook or gate said:\n${context.blockerDetail}\n\n` +
|
|
182
|
+
`Rules:\n` +
|
|
183
|
+
`- Confirm the working tree is on the PR branch at HEAD ${context.head} with the prior fixes still present.\n` +
|
|
184
|
+
`- 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` +
|
|
185
|
+
`- 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` +
|
|
186
|
+
`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.` +
|
|
187
|
+
PRE_COMMIT_GATE_STEP,
|
|
188
|
+
{ label, phase: 'Converge', schema: EDIT_SCHEMA, agentType: 'clean-coder', resume: agentId },
|
|
189
|
+
)
|
|
190
|
+
}
|
|
184
191
|
const objection = context.objection || VERIFY_OBJECTION_FALLBACK
|
|
185
192
|
const attempt = context.attempt || 1
|
|
186
193
|
return convergeAgent(
|
|
@@ -197,77 +204,41 @@ function resumeFixerAgent(agentId, task, context) {
|
|
|
197
204
|
}
|
|
198
205
|
|
|
199
206
|
/**
|
|
200
|
-
* Joined fixer recovery loop:
|
|
201
|
-
*
|
|
207
|
+
* Joined fixer recovery loop: a separate verifier agent grades the working-tree
|
|
208
|
+
* fixes while the fixer session only recovers and commits, so the verdict that
|
|
209
|
+
* gates the commit comes from a different session than the one that edits and
|
|
210
|
+
* pushes — the same editor/verifier separation the repair and conflict paths
|
|
211
|
+
* use. The verify step routes through verifyWithRecovery (verify on the verifier,
|
|
212
|
+
* recover on the fixer); the commit step routes through commitWithRecovery
|
|
213
|
+
* (commit and commit-recover on the fixer, re-verify on the verifier). A failed
|
|
214
|
+
* verdict returns the unchanged HEAD so the round reads as not-progressed.
|
|
202
215
|
* @param {string} fixerAgentId the fixer agent id from spawnFixerAgent
|
|
216
|
+
* @param {string} verifierId the verifier agent id from spawnVerifierAgent
|
|
203
217
|
* @param {string} head PR HEAD SHA
|
|
204
218
|
* @param {Array<object>} findings the findings to fix
|
|
205
219
|
* @param {string} sourceLabel short description of where the findings came from
|
|
206
220
|
* @returns {Promise<object>} FIX_SCHEMA result
|
|
207
221
|
*/
|
|
208
|
-
async function fixerWithRecovery(fixerAgentId, head, findings, sourceLabel) {
|
|
209
|
-
const verifyTranscript = await
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (recoverEdit?.edited !== true) break
|
|
222
|
-
const reVerify = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
223
|
-
const reVerdict = extractVerdict(reVerify)
|
|
224
|
-
if (!reVerdict || reVerdict.all_pass !== true) break
|
|
225
|
-
recoveryResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
226
|
-
}
|
|
227
|
-
return recoveryResult
|
|
228
|
-
}
|
|
229
|
-
return commitResult
|
|
230
|
-
}
|
|
231
|
-
let attempt = 0
|
|
232
|
-
let lastTranscript = verifyTranscript
|
|
233
|
-
while ((!verdict || verdict.all_pass !== true) && attempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
234
|
-
attempt += 1
|
|
235
|
-
const objection = extractVerifyObjection(lastTranscript)
|
|
236
|
-
const recoverEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
237
|
-
head, findings, sourceLabel, objection, attempt,
|
|
238
|
-
})
|
|
239
|
-
if (recoverEdit?.edited !== true) break
|
|
240
|
-
lastTranscript = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
241
|
-
const freshVerdict = extractVerdict(lastTranscript)
|
|
242
|
-
if (freshVerdict && freshVerdict.all_pass === true) {
|
|
243
|
-
const commitResult = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
244
|
-
if (commitNeedsCodeRecovery(commitResult)) {
|
|
245
|
-
let commitAttempt = 0
|
|
246
|
-
let commitRecovery = commitResult
|
|
247
|
-
while (commitNeedsCodeRecovery(commitRecovery) && commitAttempt < FIX_RECOVERY_MAX_ATTEMPTS) {
|
|
248
|
-
commitAttempt += 1
|
|
249
|
-
const commitEdit = await resumeFixerAgent(fixerAgentId, 'verify-recover', {
|
|
250
|
-
head, findings, sourceLabel, objection: commitRecovery.blockerDetail, attempt: commitAttempt,
|
|
251
|
-
})
|
|
252
|
-
if (commitEdit?.edited !== true) break
|
|
253
|
-
const reVerify2 = await resumeFixerAgent(fixerAgentId, 'verify-commit', { head, findings, sourceLabel })
|
|
254
|
-
const reVerdict2 = extractVerdict(reVerify2)
|
|
255
|
-
if (!reVerdict2 || reVerdict2.all_pass !== true) break
|
|
256
|
-
commitRecovery = await resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel })
|
|
257
|
-
}
|
|
258
|
-
return commitRecovery
|
|
259
|
-
}
|
|
260
|
-
return commitResult
|
|
222
|
+
async function fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel) {
|
|
223
|
+
const verifyTranscript = await verifyWithRecovery({
|
|
224
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
|
|
225
|
+
runRecoverEdit: (objection, attempt) => resumeFixerAgent(fixerAgentId, 'verify-recover', { head, findings, sourceLabel, objection, attempt }),
|
|
226
|
+
})
|
|
227
|
+
if (!verdictPassed(verifyTranscript)) {
|
|
228
|
+
return {
|
|
229
|
+
newSha: head,
|
|
230
|
+
pushed: false,
|
|
231
|
+
resolvedWithoutCommit: false,
|
|
232
|
+
summary: `verify step did not pass the working-tree fixes for ${findings.length} finding(s) — not committing`,
|
|
233
|
+
blockedNeedingEdit: false,
|
|
234
|
+
blockerDetail: '',
|
|
261
235
|
}
|
|
262
236
|
}
|
|
263
|
-
return {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
blockedNeedingEdit: false,
|
|
269
|
-
blockerDetail: '',
|
|
270
|
-
}
|
|
237
|
+
return commitWithRecovery({
|
|
238
|
+
runCommit: () => resumeFixerAgent(fixerAgentId, 'commit', { head, findings, sourceLabel }),
|
|
239
|
+
runVerify: () => resumeVerifierAgent(verifierId, 'fix-verify', { head, findings, sourceLabel }),
|
|
240
|
+
runRecoverEdit: (detail, attempt) => resumeFixerAgent(fixerAgentId, 'commit-recover', { head, findings, sourceLabel, blockerDetail: detail, attempt }),
|
|
241
|
+
})
|
|
271
242
|
}
|
|
272
243
|
|
|
273
244
|
/**
|
|
@@ -424,7 +395,7 @@ function resumeCodeEditorAgent(agentId, task, context) {
|
|
|
424
395
|
|
|
425
396
|
/**
|
|
426
397
|
* Spawn the verifier code-verifier agent once per converge round, establishing
|
|
427
|
-
* its role so each later resume (repair-verify, hardening-verify) continues the
|
|
398
|
+
* its role so each later resume (fix-verify, repair-verify, hardening-verify) continues the
|
|
428
399
|
* same session. The spawn makes no edits — verification only. Returns the runtime
|
|
429
400
|
* agent id so the resume calls target the live session; a runtime without resume
|
|
430
401
|
* support returns no agent id, and each resume falls back to a fresh spawn.
|
|
@@ -447,6 +418,18 @@ async function spawnVerifierAgent() {
|
|
|
447
418
|
*/
|
|
448
419
|
function resumeVerifierAgent(agentId, task, context) {
|
|
449
420
|
const label = `verifier:${task}`
|
|
421
|
+
if (task === 'fix-verify') {
|
|
422
|
+
const findingsBlock = renderFindingsBlock(context.findings)
|
|
423
|
+
return convergeAgent(
|
|
424
|
+
`You are the VERIFY step for ${context.findings.length} finding(s) (${context.sourceLabel}) on ${prCoordinates}, HEAD ${context.head}. The edit step left fixes in the working tree, uncommitted. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
|
|
425
|
+
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
426
|
+
`Steps:\n` +
|
|
427
|
+
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
428
|
+
`2. Verify the uncommitted working-tree changes resolve every finding above: run the relevant tests and the named gates against the working tree. Read the diff (git diff) and confirm each finding is fixed test-first per CODE_RULES.\n` +
|
|
429
|
+
`3. ${buildVerdictFenceSteps(input.owner, input.repo, input.prNumber)}`,
|
|
430
|
+
{ label, phase: 'Converge', agentType: 'code-verifier', resume: agentId },
|
|
431
|
+
)
|
|
432
|
+
}
|
|
450
433
|
if (task === 'repair-verify') {
|
|
451
434
|
const failureBlock = context.failures.length
|
|
452
435
|
? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
@@ -929,15 +912,6 @@ function parseLastVerdictFence(transcript) {
|
|
|
929
912
|
}
|
|
930
913
|
}
|
|
931
914
|
|
|
932
|
-
/**
|
|
933
|
-
* Extract the full verdict object from a transcript carrying a verdict fence.
|
|
934
|
-
* @param {string|null|undefined} transcript the agent transcript text
|
|
935
|
-
* @returns {object|null} the parsed verdict with all_pass, findings, and manifest_sha256, or null
|
|
936
|
-
*/
|
|
937
|
-
function extractVerdict(transcript) {
|
|
938
|
-
return parseLastVerdictFence(transcript)
|
|
939
|
-
}
|
|
940
|
-
|
|
941
915
|
/**
|
|
942
916
|
* Decide whether a workflow code-verifier transcript ended in a passing
|
|
943
917
|
* verdict. Reads the LAST ```verdict ...``` fenced JSON block via the shared
|
|
@@ -1055,7 +1029,7 @@ function commitNeedsCodeRecovery(commitResult) {
|
|
|
1055
1029
|
* resolve-head agent or a malformed result yields a falsy SHA; spawning lenses
|
|
1056
1030
|
* against it interpolates the literal string 'HEAD undefined' into their prompts
|
|
1057
1031
|
* and produces a spurious clean verdict on a non-existent commit.
|
|
1058
|
-
* @param {string|null|undefined} resolvedHead the SHA from
|
|
1032
|
+
* @param {string|null|undefined} resolvedHead the SHA from the git-utility agent resume for 'resolve-head'
|
|
1059
1033
|
* @returns {boolean} true only when the SHA is a non-empty string
|
|
1060
1034
|
*/
|
|
1061
1035
|
function isResolvedHeadUsable(resolvedHead) {
|
|
@@ -1068,7 +1042,7 @@ function isResolvedHeadUsable(resolvedHead) {
|
|
|
1068
1042
|
* not-conflicting so the run proceeds straight to the bug checks rather than
|
|
1069
1043
|
* force-pushing a rebase on a verdict that does not exist — a transient check
|
|
1070
1044
|
* failure must never trigger a destructive rebase.
|
|
1071
|
-
* @param {object|null|undefined} mergeState the
|
|
1045
|
+
* @param {object|null|undefined} mergeState the git-utility agent resume result for 'check-merge-conflicts'
|
|
1072
1046
|
* @returns {boolean} true only when the check reported conflicting:true
|
|
1073
1047
|
*/
|
|
1074
1048
|
function isMergeConflicting(mergeState) {
|
|
@@ -1357,14 +1331,16 @@ async function verifyWithRecovery({ runVerify, runRecoverEdit }) {
|
|
|
1357
1331
|
}
|
|
1358
1332
|
|
|
1359
1333
|
/**
|
|
1360
|
-
* Fix lens: edit (clean-coder, no commit) -> verify (code-verifier
|
|
1361
|
-
* verdict fence binding the working tree) -> commit (clean-coder, one
|
|
1362
|
-
* push, no edits).
|
|
1363
|
-
*
|
|
1364
|
-
*
|
|
1365
|
-
*
|
|
1366
|
-
*
|
|
1367
|
-
*
|
|
1334
|
+
* Fix lens: edit (clean-coder, no commit) -> verify (a separate code-verifier
|
|
1335
|
+
* emits a verdict fence binding the working tree) -> commit (clean-coder, one
|
|
1336
|
+
* commit + push, no edits). The verifier is a distinct persistent group from the
|
|
1337
|
+
* fixer, so the verdict that gates the commit comes from a different session than
|
|
1338
|
+
* the one that edits and pushes — the same editor/verifier separation the repair
|
|
1339
|
+
* and conflict paths use, and the separation a workflow code-verifier needs to
|
|
1340
|
+
* produce the verdict the verified-commit gate requires, which the SubagentStop
|
|
1341
|
+
* minter cannot mint for workflow-spawned agents. When verification fails (or the
|
|
1342
|
+
* edit step stalled with no thread to resolve), the commit step is skipped and the
|
|
1343
|
+
* unchanged HEAD is returned so the round reads as not-progressed.
|
|
1368
1344
|
* @param {string} head PR HEAD SHA the findings were raised against
|
|
1369
1345
|
* @param {Array<object>} findings deduped findings across all lenses
|
|
1370
1346
|
* @param {string} sourceLabel short description of where the findings came from
|
|
@@ -1383,8 +1359,9 @@ async function applyFixes(head, findings, sourceLabel) {
|
|
|
1383
1359
|
blockerDetail: '',
|
|
1384
1360
|
}
|
|
1385
1361
|
}
|
|
1386
|
-
const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel
|
|
1387
|
-
|
|
1362
|
+
const fixerAgentId = await spawnFixerAgent(head, findings, sourceLabel)
|
|
1363
|
+
const verifierId = await spawnVerifierAgent()
|
|
1364
|
+
return fixerWithRecovery(fixerAgentId, verifierId, head, findings, sourceLabel)
|
|
1388
1365
|
}
|
|
1389
1366
|
|
|
1390
1367
|
/**
|
|
@@ -1561,7 +1538,7 @@ function standardsDeferralNote(findingsCount, hardeningPrOpened) {
|
|
|
1561
1538
|
* @param {string} sourceLabel short description of where the findings came from
|
|
1562
1539
|
* @returns {Promise<object>} `{ hardeningPrOpened }` — true only when the hardening PR was opened this round
|
|
1563
1540
|
*/
|
|
1564
|
-
async function spawnStandardsFollowUp(head, findings, sourceLabel
|
|
1541
|
+
async function spawnStandardsFollowUp(head, findings, sourceLabel) {
|
|
1565
1542
|
const codeEditorId = await spawnCodeEditorAgent()
|
|
1566
1543
|
const editResult = await resumeCodeEditorAgent(codeEditorId, 'standards-edit', { head, findings, sourceLabel })
|
|
1567
1544
|
if (editResult?.hardeningEdited !== true || !editResult?.hardeningRepoPath) {
|
|
@@ -1647,7 +1624,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1647
1624
|
if (isStandardsOnlyRound(findings)) {
|
|
1648
1625
|
log(`Round ${rounds}: ${findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the round as passed`)
|
|
1649
1626
|
const generalId = await spawnGeneralUtilityAgent()
|
|
1650
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round'
|
|
1627
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, findings, 'converge-round')
|
|
1651
1628
|
standardsNote = standardsDeferralNote(findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1652
1629
|
const auditResult = await resumeGeneralUtilityAgent(generalId, 'post-clean-audit', { head })
|
|
1653
1630
|
if (!auditResult?.posted) {
|
|
@@ -1704,8 +1681,7 @@ while (iterations < CONFIG.maxIterations) {
|
|
|
1704
1681
|
if (copilotOutcome.kind === 'fix') {
|
|
1705
1682
|
if (isStandardsOnlyRound(copilotOutcome.findings)) {
|
|
1706
1683
|
log(`Copilot raised ${copilotOutcome.findings.length} code-standard-only finding(s) — deferring to follow-up PRs and treating the gate as passed`)
|
|
1707
|
-
const
|
|
1708
|
-
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot', copilotGeneralId)
|
|
1684
|
+
const standardsOutcome = await spawnStandardsFollowUp(head, copilotOutcome.findings, 'copilot')
|
|
1709
1685
|
standardsNote = standardsDeferralNote(copilotOutcome.findings.length, standardsOutcome?.hardeningPrOpened === true)
|
|
1710
1686
|
copilotDown = false
|
|
1711
1687
|
copilotNote = null
|