claude-dev-env 2.7.0 → 2.7.1
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/worker-spawn.md +3 -1
- package/package.json +1 -1
- package/scripts/CLAUDE.md +3 -3
- package/scripts/dev_env_scripts_constants/CLAUDE.md +1 -1
- package/scripts/dev_env_scripts_constants/grok_worker_constants.py +79 -13
- package/scripts/grok_headless_runner.py +213 -16
- package/scripts/resolve_worker_spawn.py +56 -10
- package/scripts/spawn_grok_batch.py +43 -22
- package/scripts/test_grok_headless_runner.py +592 -10
- package/scripts/test_resolve_worker_spawn.py +179 -15
- package/scripts/test_spawn_grok_batch.py +225 -22
- package/skills/autoconverge/workflow/converge.contract.test.mjs +28 -6
- package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +73 -0
- package/skills/autoconverge/workflow/converge.mjs +63 -14
- package/skills/grok-spawn/SKILL.md +5 -3
- package/skills/grok-spawn/reference/flag-profiles.md +3 -1
|
@@ -258,6 +258,7 @@ const verifyObjectionModule = new Function(
|
|
|
258
258
|
`${functionSource('parseLastVerdictFence')}\n` +
|
|
259
259
|
`${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
|
|
260
260
|
`${functionSource('renderVerifyObjectionLine')}\n` +
|
|
261
|
+
`${functionSource('extractPreFenceProse')}\n` +
|
|
261
262
|
`${functionSource('extractVerifyObjection')}\n` +
|
|
262
263
|
'return { extractVerifyObjection, VERIFY_OBJECTION_FALLBACK };',
|
|
263
264
|
)();
|
|
@@ -338,6 +339,78 @@ test('extractVerifyObjection falls back when no finding yields usable text', ()
|
|
|
338
339
|
assert.equal(extractVerifyObjection(transcript), VERIFY_OBJECTION_FALLBACK);
|
|
339
340
|
});
|
|
340
341
|
|
|
342
|
+
test('the prose reader anchors on the last CLOSED fence, not a stray unterminated marker', () => {
|
|
343
|
+
const transcript =
|
|
344
|
+
'check X never showed red\n\n' +
|
|
345
|
+
'```verdict\n{"all_pass": false, "findings": []}\n```\n\n' +
|
|
346
|
+
'and here I started to restate it\n\n' +
|
|
347
|
+
'```verdict';
|
|
348
|
+
const objection = extractVerifyObjection(transcript);
|
|
349
|
+
assert.equal(objection, 'check X never showed red');
|
|
350
|
+
assert.doesNotMatch(objection, /all_pass/, 'expected the verdict body never to be read back as prose');
|
|
351
|
+
});
|
|
352
|
+
|
|
353
|
+
test('the prose reader skips a scaffolding-only paragraph above the fence', () => {
|
|
354
|
+
const transcript =
|
|
355
|
+
'check X never showed red\n\n' + '## Verdict\n\n' + '```verdict\n{"all_pass": false, "findings": []}\n```';
|
|
356
|
+
assert.equal(extractVerifyObjection(transcript), 'check X never showed red');
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
const verifyRecoveryPromptModule = new Function(
|
|
360
|
+
`${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
|
|
361
|
+
'const prCoordinates = "owner/repo#1";\n' +
|
|
362
|
+
'const PRE_COMMIT_GATE_STEP = "";\n' +
|
|
363
|
+
'const EDIT_SCHEMA = {};\n' +
|
|
364
|
+
'const TIERS = { sonnetMedium: {} };\n' +
|
|
365
|
+
'const convergeAgent = (spawnPrompt) => spawnPrompt;\n' +
|
|
366
|
+
`${functionSource('runCodeEditorTask')}\n` +
|
|
367
|
+
'return { runCodeEditorTask };',
|
|
368
|
+
)();
|
|
369
|
+
|
|
370
|
+
const { runCodeEditorTask: buildCodeEditorPrompt } = verifyRecoveryPromptModule;
|
|
371
|
+
|
|
372
|
+
function buildVerifyRecoveryPrompt(verifyTranscript) {
|
|
373
|
+
return buildCodeEditorPrompt('verify-recover', {
|
|
374
|
+
objection: extractVerifyObjection(verifyTranscript),
|
|
375
|
+
head: 'deadbeefcafe',
|
|
376
|
+
sourceLabel: 'round-1 lens findings',
|
|
377
|
+
attempt: 1,
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
const INCOMPLETE_CHECK_SENTENCE =
|
|
382
|
+
'This verdict is incomplete: the deliberate break for check_docstring_runon_sentence never showed red, so that gate is unproven rather than failed.';
|
|
383
|
+
|
|
384
|
+
test('an incomplete verdict carries its named check into the fixer prompt instead of the fallback', () => {
|
|
385
|
+
const transcript =
|
|
386
|
+
'I ran the named gates and read the diff against the task text.\n\n' +
|
|
387
|
+
`${INCOMPLETE_CHECK_SENTENCE}\n\n` +
|
|
388
|
+
'```verdict\n{"all_pass": false, "findings": [], "manifest_sha256": "0f1e2d"}\n```';
|
|
389
|
+
const fixerPrompt = buildVerifyRecoveryPrompt(transcript);
|
|
390
|
+
assert.match(fixerPrompt, /VERIFY-RECOVERY fixer/, 'expected the verify-recovery fixer prompt');
|
|
391
|
+
assert.ok(
|
|
392
|
+
fixerPrompt.includes(INCOMPLETE_CHECK_SENTENCE),
|
|
393
|
+
`expected the fixer prompt to carry the incomplete-check sentence, got:\n${fixerPrompt.slice(0, 600)}`,
|
|
394
|
+
);
|
|
395
|
+
assert.doesNotMatch(
|
|
396
|
+
fixerPrompt,
|
|
397
|
+
/without a parseable verdict/,
|
|
398
|
+
'expected the incomplete verdict never to reach the fixer as VERIFY_OBJECTION_FALLBACK',
|
|
399
|
+
);
|
|
400
|
+
});
|
|
401
|
+
|
|
402
|
+
test('a genuine code defect still reaches the fixer prompt from findings, unchanged by pre-fence prose', () => {
|
|
403
|
+
const transcript =
|
|
404
|
+
`${INCOMPLETE_CHECK_SENTENCE}\n\n` +
|
|
405
|
+
'```verdict\n{"all_pass": false, "findings": [{"check": "Finding 1", "detail": "boundary still over-blocks"}]}\n```';
|
|
406
|
+
const fixerPrompt = buildVerifyRecoveryPrompt(transcript);
|
|
407
|
+
assert.match(fixerPrompt, /1\. Finding 1 — boundary still over-blocks/);
|
|
408
|
+
assert.ok(
|
|
409
|
+
!fixerPrompt.includes(INCOMPLETE_CHECK_SENTENCE),
|
|
410
|
+
'expected findings to keep owning a verdict that names a code defect',
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
|
|
341
414
|
test('the verify-recover task in runCodeEditorTask is a clean-coder edit step bound to the verifier objection and leaves changes uncommitted', () => {
|
|
342
415
|
const recoverBody = functionSource('runCodeEditorTask');
|
|
343
416
|
assert.match(recoverBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder');
|
|
@@ -68,13 +68,15 @@ const HEADLESS_EDIT_PREAMBLE =
|
|
|
68
68
|
'- When your run was given a result schema, your final action is always the StructuredOutput call. If the poll budget is spent before the awaited signal arrives, call StructuredOutput with the whole time-out result the step documents — for the Copilot gate, the full down result {sha, clean:false, down:true, findings:[]}, never a bare down flag — rather than ending the turn without a result.\n\n'
|
|
69
69
|
|
|
70
70
|
const HEADLESS_READONLY_DESTRUCTIVE_POINTER =
|
|
71
|
-
'- Never run a destructive command (rm -rf, git reset --hard, dd, mkfs, chmod -R, a fork bomb) and never place its literal text in a Bash command: this step
|
|
71
|
+
'- Never run a destructive command (rm -rf, git reset --hard, dd, mkfs, chmod -R, a fork bomb) and never place its literal text in a Bash command: this step makes no edit to the tree it reads, so it needs no destructive command. If a step seems to require one, report it as a blocker rather than running it.\n'
|
|
72
72
|
|
|
73
73
|
/**
|
|
74
74
|
* The read-only preamble a review, verify, or utility agent receives: the full
|
|
75
|
-
* edit preamble with the rm-shape-rules bullet dropped, since an agent that
|
|
76
|
-
*
|
|
77
|
-
*
|
|
75
|
+
* edit preamble with the rm-shape-rules bullet dropped, since an agent that makes
|
|
76
|
+
* no edit in the tree it reads never runs rm against that tree, and the one file
|
|
77
|
+
* it may touch off that tree — a deliberate break at a break site outside it —
|
|
78
|
+
* is an edit rather than a delete, so the shape rules add no value to its
|
|
79
|
+
* prompt. The one-line destructive pointer keeps the escape-hatch guidance in view. The
|
|
78
80
|
* derivation reads the single rm-shape bullet out of the edit preamble and swaps
|
|
79
81
|
* the pointer in, so the two preambles share every other clause from one source.
|
|
80
82
|
*/
|
|
@@ -430,7 +432,7 @@ function runVerifierTask(task, context) {
|
|
|
430
432
|
if (task === 'fix-verify') {
|
|
431
433
|
const findingsBlock = renderFindingsBlock(context.findings)
|
|
432
434
|
return convergeReadOnlyAgent(
|
|
433
|
-
`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.
|
|
435
|
+
`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. Make NO edit to the tree under verification — verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
|
|
434
436
|
`Findings the working-tree fixes must address:\n${findingsBlock}\n\n` +
|
|
435
437
|
`Steps:\n` +
|
|
436
438
|
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
@@ -444,7 +446,7 @@ function runVerifierTask(task, context) {
|
|
|
444
446
|
? context.failures.map((each, position) => `${position + 1}. ${each}`).join('\n')
|
|
445
447
|
: 'none reported'
|
|
446
448
|
return convergeReadOnlyAgent(
|
|
447
|
-
`You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed.
|
|
449
|
+
`You are the VERIFY step for the convergence repair on ${prCoordinates}, HEAD ${context.head}. The edit step left its repair in the working tree (a bot-thread fix uncommitted, and/or a rebase onto origin/main), unpushed. Make NO edit to the tree under verification — verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
|
|
448
450
|
`Concerns the working-tree repair must resolve (the gates the convergence check flagged):\n${failureBlock}\n\n` +
|
|
449
451
|
`Steps:\n` +
|
|
450
452
|
`1. Resolve the worktree repo root for running tests: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
@@ -454,7 +456,7 @@ function runVerifierTask(task, context) {
|
|
|
454
456
|
)
|
|
455
457
|
}
|
|
456
458
|
return convergeReadOnlyAgent(
|
|
457
|
-
`You are the VERIFY step for an environment-hardening change (${context.sourceLabel}) staged in the working tree of ${context.hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there.
|
|
459
|
+
`You are the VERIFY step for an environment-hardening change (${context.sourceLabel}) staged in the working tree of ${context.hardeningRepoPath}. The edit step left the hooks/rules edits uncommitted there. Make NO edit to the tree under verification — verification only; any edit inside that tree invalidates the verdict you are about to emit.\n\n` +
|
|
458
460
|
`Concern the working-tree change must resolve: the edited hooks/rules block the code-standard violation classes from the deferred round at Write/Edit time, and a hook change carries a passing test per CODE_RULES.\n\n` +
|
|
459
461
|
`Steps:\n` +
|
|
460
462
|
`1. cd into ${context.hardeningRepoPath}, then resolve its repo root: REPO=$(git rev-parse --show-toplevel).\n` +
|
|
@@ -468,7 +470,7 @@ function runVerifierTask(task, context) {
|
|
|
468
470
|
" ```verdict\n" +
|
|
469
471
|
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
470
472
|
" ```\n" +
|
|
471
|
-
`
|
|
473
|
+
` Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.`,
|
|
472
474
|
{ label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
|
|
473
475
|
)
|
|
474
476
|
}
|
|
@@ -871,7 +873,7 @@ function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
|
|
|
871
873
|
" ```verdict\n" +
|
|
872
874
|
` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
|
|
873
875
|
" ```\n" +
|
|
874
|
-
`
|
|
876
|
+
` Set all_pass to false when verification fails, and list every code defect you found in findings. When the verdict is incomplete because a check it rests on never showed red, set all_pass to false and name that check in prose directly above the fence rather than in findings. Always include the manifest_sha256. The verdict fence must be the last thing in your message.`
|
|
875
877
|
)
|
|
876
878
|
}
|
|
877
879
|
|
|
@@ -1343,14 +1345,61 @@ function renderVerifyObjectionLine(eachFinding) {
|
|
|
1343
1345
|
return stringifiedFinding === '{}' ? null : stringifiedFinding
|
|
1344
1346
|
}
|
|
1345
1347
|
|
|
1348
|
+
/**
|
|
1349
|
+
* Read the prose paragraph the verifier wrote directly above its last verdict
|
|
1350
|
+
* fence. A verdict that is incomplete — a check it rests on never showed red —
|
|
1351
|
+
* is not a code defect, so the fence contract puts that reason here rather than
|
|
1352
|
+
* in findings; this reader is how the reason survives into the re-fix step.
|
|
1353
|
+
*
|
|
1354
|
+
* Anchors on the same fence parseLastVerdictFence reads — the last CLOSED
|
|
1355
|
+
* verdict fence, not the last bare marker — so a stray unterminated marker after
|
|
1356
|
+
* the real fence can never make this read the verdict body itself as prose. A
|
|
1357
|
+
* candidate paragraph carrying only markdown scaffolding (a heading, a fence
|
|
1358
|
+
* delimiter) is skipped, since handing the fixer a heading is worse than the
|
|
1359
|
+
* generic fallback the caller keeps.
|
|
1360
|
+
*
|
|
1361
|
+
* ::
|
|
1362
|
+
*
|
|
1363
|
+
* 'ran the gates\n\ncheck X never showed red\n\n```verdict\n{}\n```'
|
|
1364
|
+
* -> 'check X never showed red'
|
|
1365
|
+
* '```verdict\n{}\n```' -> null
|
|
1366
|
+
*
|
|
1367
|
+
* @param {string|null|undefined} verifyTranscript the verifier transcript text
|
|
1368
|
+
* @returns {string|null} the last prose paragraph above the final fence, or null when there is none
|
|
1369
|
+
*/
|
|
1370
|
+
function extractPreFenceProse(verifyTranscript) {
|
|
1371
|
+
if (typeof verifyTranscript !== 'string') return null
|
|
1372
|
+
const fenceMarker = '```verdict'
|
|
1373
|
+
let lastFenceStart = verifyTranscript.lastIndexOf(fenceMarker)
|
|
1374
|
+
while (lastFenceStart !== -1 && !verifyTranscript.slice(lastFenceStart + fenceMarker.length).includes('```')) {
|
|
1375
|
+
lastFenceStart = verifyTranscript.lastIndexOf(fenceMarker, lastFenceStart - 1)
|
|
1376
|
+
}
|
|
1377
|
+
if (lastFenceStart === -1) return null
|
|
1378
|
+
const proseParagraphs = verifyTranscript
|
|
1379
|
+
.slice(0, lastFenceStart)
|
|
1380
|
+
.split(/\n\s*\n/)
|
|
1381
|
+
.map((eachParagraph) => eachParagraph.trim())
|
|
1382
|
+
.filter((eachParagraph) =>
|
|
1383
|
+
eachParagraph
|
|
1384
|
+
.split('\n')
|
|
1385
|
+
.some((eachLine) => {
|
|
1386
|
+
const trimmedLine = eachLine.trim()
|
|
1387
|
+
return trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('```') && /[A-Za-z]/.test(trimmedLine)
|
|
1388
|
+
}),
|
|
1389
|
+
)
|
|
1390
|
+
return proseParagraphs.length === 0 ? null : proseParagraphs[proseParagraphs.length - 1]
|
|
1391
|
+
}
|
|
1392
|
+
|
|
1346
1393
|
/**
|
|
1347
1394
|
* Pull the verifier's stated objections out of a failed verify transcript so the
|
|
1348
1395
|
* re-fix step knows what the verdict rejected. Reads the last fenced verdict JSON
|
|
1349
1396
|
* (the same block verdictPassed reads) and renders each finding through
|
|
1350
|
-
* renderVerifyObjectionLine into a numbered list.
|
|
1351
|
-
*
|
|
1352
|
-
*
|
|
1353
|
-
*
|
|
1397
|
+
* renderVerifyObjectionLine into a numbered list. When findings yields no usable
|
|
1398
|
+
* line the verdict is an incomplete one rather than a code-defect one, so the
|
|
1399
|
+
* prose paragraph above the fence — where the fence contract puts the unshown-red
|
|
1400
|
+
* check — carries the objection instead. A missing fence, a parse failure, or an
|
|
1401
|
+
* empty findings list with no prose above the fence falls back to a generic
|
|
1402
|
+
* re-read instruction, so the re-fix step always receives actionable text.
|
|
1354
1403
|
* @param {string|null|undefined} verifyTranscript the failed verifier transcript text
|
|
1355
1404
|
* @returns {string} a human-readable block of the verifier's objections
|
|
1356
1405
|
*/
|
|
@@ -1361,7 +1410,7 @@ function extractVerifyObjection(verifyTranscript) {
|
|
|
1361
1410
|
const renderedObjections = allObjections
|
|
1362
1411
|
.map((eachFinding) => renderVerifyObjectionLine(eachFinding))
|
|
1363
1412
|
.filter((eachLine) => eachLine !== null)
|
|
1364
|
-
if (renderedObjections.length === 0) return VERIFY_OBJECTION_FALLBACK
|
|
1413
|
+
if (renderedObjections.length === 0) return extractPreFenceProse(verifyTranscript) || VERIFY_OBJECTION_FALLBACK
|
|
1365
1414
|
return renderedObjections.map((eachLine, position) => `${position + 1}. ${eachLine}`).join('\n')
|
|
1366
1415
|
}
|
|
1367
1416
|
|
|
@@ -125,7 +125,6 @@ Shape:
|
|
|
125
125
|
"tool_profile": "readonly",
|
|
126
126
|
"timeout_seconds": 600,
|
|
127
127
|
"is_repo_only": true,
|
|
128
|
-
"max_turns": 8,
|
|
129
128
|
"agent_name": null
|
|
130
129
|
}
|
|
131
130
|
]
|
|
@@ -141,11 +140,14 @@ Shape:
|
|
|
141
140
|
| `prompt_parts` | Ordered absolute paths to part files |
|
|
142
141
|
| `cwd` | Working directory for that worker |
|
|
143
142
|
| `tool_profile` | `readonly` or `build` |
|
|
144
|
-
| `timeout_seconds` | Per-worker timeout (default 600) |
|
|
143
|
+
| `timeout_seconds` | Per-worker timeout (default 600, ceiling 5400). A spec asking for more is refused |
|
|
145
144
|
| `is_repo_only` | Readonly only: when true, also pass `--disable-web-search` |
|
|
146
|
-
| `max_turns` | Turn cap (default 8) |
|
|
147
145
|
| `agent_name` | Optional `--agent` name, or `null` |
|
|
148
146
|
|
|
147
|
+
Workers run with no turn cap. The timeout is the only bound on a worker's
|
|
148
|
+
length, and a worker that hits it is killed with its whole process tree and
|
|
149
|
+
reported as `timeout`.
|
|
150
|
+
|
|
149
151
|
Put the spec file under the run state directory (or any path you pass to
|
|
150
152
|
`--spec`).
|
|
151
153
|
|
|
@@ -20,10 +20,12 @@ The headless runner always passes:
|
|
|
20
20
|
| `--cwd` | Worker `cwd` from the batch spec |
|
|
21
21
|
| `--output-format` | `json` |
|
|
22
22
|
| `--always-approve` | present (auto-approve tool runs) |
|
|
23
|
-
| `--max-turns` | Worker `max_turns` (default `8`) |
|
|
24
23
|
| `--leader-socket` | Unique per-worker socket path under the run state dir |
|
|
25
24
|
| `--debug-file` | Unique per-worker debug log (batch launcher adds this) |
|
|
26
25
|
|
|
26
|
+
The runner passes no `--max-turns`. Worker length is bounded by the per-worker
|
|
27
|
+
timeout alone. The preflight ping keeps its own single-turn cap.
|
|
28
|
+
|
|
27
29
|
Optional:
|
|
28
30
|
|
|
29
31
|
| Flag | When |
|