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.
@@ -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 reads only and edits nothing, so it needs no destructive command. If a step seems to require one, report it as a blocker rather than running it.\n'
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 edits
76
- * nothing never runs rm and the shape rules add no value to its prompt. The
77
- * one-line destructive pointer keeps the escape-hatch guidance in view. The
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. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
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. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
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. Do NO edits of any kind — verification only; any edit invalidates the verdict you are about to emit.\n\n` +
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
- ` When verification fails, set all_pass to false and list the unresolved concerns in findings; still include the manifest_sha256. The verdict fence must be the last thing in your message.`,
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
- ` When verification fails, set all_pass to false and list the unresolved concerns in findings; still include the manifest_sha256. The verdict fence must be the last thing in your message.`
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. A missing fence, a parse
1351
- * failure, an empty findings list, or a findings list where no entry yields
1352
- * usable text falls back to a generic re-read instruction, so the re-fix step
1353
- * always receives actionable text.
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 |