claude-dev-env 2.7.0 → 2.8.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.
Files changed (88) hide show
  1. package/_shared/CLAUDE.md +1 -0
  2. package/_shared/advisor/advisor-protocol.md +19 -9
  3. package/_shared/pr-loop/audit-contract.md +4 -4
  4. package/_shared/pr-loop/precatch-rubric.md +2 -2
  5. package/_shared/pr-loop/worker-spawn.md +3 -1
  6. package/_shared/process-tree/CLAUDE.md +41 -0
  7. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/__init__.py +1 -0
  8. package/_shared/process-tree/scripts/config/process_tree_scripts_constants/process_tree_kill_constants.py +27 -0
  9. package/_shared/process-tree/scripts/process_tree_kill.py +141 -0
  10. package/_shared/process-tree/scripts/pyproject.toml +16 -0
  11. package/_shared/process-tree/scripts/test_process_tree_kill.py +278 -0
  12. package/agents/code-quality-agent.md +6 -5
  13. package/agents/deep-research.md +7 -24
  14. package/agents/docs-agent.md +1 -27
  15. package/agents/issue-tracker.md +1 -7
  16. package/agents/skill-writer-agent.md +1 -2
  17. package/agents/test_agent_frontmatter.py +309 -12
  18. package/hooks/blocking/CLAUDE.md +2 -0
  19. package/hooks/blocking/fable_spawn_gate.py +187 -0
  20. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  21. package/hooks/blocking/plain_language_blocker.py +287 -15
  22. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  23. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  24. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  25. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  26. package/hooks/git-hooks/CLAUDE.md +2 -2
  27. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  28. package/hooks/git-hooks/pre_push.py +343 -54
  29. package/hooks/git-hooks/test_pre_push.py +852 -6
  30. package/hooks/hooks.json +9 -19
  31. package/hooks/hooks_constants/CLAUDE.md +2 -0
  32. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  33. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  34. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  35. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  36. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  37. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  38. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  39. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  40. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  41. package/package.json +1 -1
  42. package/rules/CLAUDE.md +1 -0
  43. package/rules/ask-user-question-required.md +26 -0
  44. package/rules/claims-as-quotes.md +65 -0
  45. package/scripts/CLAUDE.md +4 -4
  46. package/scripts/_code_review_test_support.py +6 -0
  47. package/scripts/check.ps1 +18 -5
  48. package/scripts/claude_chain_runner.py +203 -31
  49. package/scripts/codec_forwarding_test_support.py +2 -0
  50. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  51. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  52. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  53. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +83 -13
  54. package/scripts/grok_headless_runner.py +148 -18
  55. package/scripts/resolve_worker_spawn.py +56 -10
  56. package/scripts/spawn_grok_batch.py +81 -23
  57. package/scripts/test_claude_chain_runner.py +358 -0
  58. package/scripts/test_grok_headless_runner.py +547 -10
  59. package/scripts/test_invoke_code_review.py +298 -0
  60. package/scripts/test_resolve_worker_spawn.py +185 -15
  61. package/scripts/test_spawn_grok_batch.py +326 -22
  62. package/scripts/tests/CLAUDE.md +1 -0
  63. package/scripts/tests/test_grok_worker_constants.py +59 -0
  64. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  65. package/skills/autoconverge/workflow/converge.contract.test.mjs +133 -8
  66. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +107 -1
  67. package/skills/autoconverge/workflow/converge.mjs +113 -31
  68. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  69. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  70. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  71. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  72. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  73. package/skills/e-code-review/SKILL.md +9 -8
  74. package/skills/e-code-review/reference/fix.md +29 -7
  75. package/skills/e-code-review/reference/loop.md +230 -14
  76. package/skills/e-code-review/reference/low.md +33 -15
  77. package/skills/e-code-review/reference/medium.md +55 -21
  78. package/skills/e-code-review/reference/xhigh.md +30 -12
  79. package/skills/fresh-branch/CLAUDE.md +5 -5
  80. package/skills/fresh-branch/SKILL.md +14 -6
  81. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  82. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  83. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  84. package/skills/grok-spawn/SKILL.md +10 -3
  85. package/skills/grok-spawn/reference/flag-profiles.md +3 -1
  86. package/skills/orchestrator/SKILL.md +4 -1
  87. package/skills/orchestrator-refresh/SKILL.md +5 -1
  88. package/skills/team-advisor/SKILL.md +4 -1
@@ -411,8 +411,13 @@ test('the shared verdict-fence builder names the binding-hash command and the ve
411
411
  /verification_verdict_store\.py/,
412
412
  'expected the verdict-store script that computes the binding hash to be named',
413
413
  );
414
- assert.match(fenceBuilder, /```verdict/, 'expected the verdict fence to be specified');
415
- assert.match(fenceBuilder, /manifest_sha256/, 'expected the verdict fence to carry manifest_sha256');
414
+ assert.match(fenceBuilder, /buildVerdictFenceTail\(/, 'expected the fence builder to close with the shared fence tail');
415
+ const renderedFenceTail = loadVerdictFenceTailBuilder()('');
416
+ assert.match(renderedFenceTail, /```verdict/, 'expected the verdict fence to be specified');
417
+ assert.ok(
418
+ renderedFenceTail.includes(VERDICT_FENCE_JSON_LINE),
419
+ 'expected the verdict fence to carry manifest_sha256',
420
+ );
416
421
  assert.match(
417
422
  fenceBuilder,
418
423
  /gh pr view/,
@@ -425,6 +430,100 @@ test('the shared verdict-fence builder names the binding-hash command and the ve
425
430
  );
426
431
  });
427
432
 
433
+ test('the incomplete-verdict contract sentence is written once and used by both verify paths', () => {
434
+ const sentenceOccurrences = convergeSource.split(
435
+ 'name that check in prose directly above the fence rather than in findings',
436
+ ).length - 1;
437
+ assert.equal(
438
+ sentenceOccurrences,
439
+ 1,
440
+ 'expected the incomplete-verdict contract sentence to be written exactly once so the two verify paths cannot drift',
441
+ );
442
+ assert.match(
443
+ lensPromptBody('buildVerdictFenceTail'),
444
+ /VERDICT_FENCE_CONTRACT_SENTENCE/,
445
+ 'expected the shared fence tail to use the one contract sentence',
446
+ );
447
+ assert.match(
448
+ lensPromptBody('buildVerdictFenceSteps'),
449
+ /buildVerdictFenceTail\(/,
450
+ 'expected the gh-lookup fence builder to reach the contract sentence through the shared fence tail',
451
+ );
452
+ assert.match(
453
+ lensPromptBody('runVerifierTask'),
454
+ /buildVerdictFenceTail\(/,
455
+ 'expected the hardening-verify prompt to reach the contract sentence through the shared fence tail',
456
+ );
457
+ });
458
+
459
+ const VERDICT_FENCE_JSON_LINE = '{"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}';
460
+ const VERDICT_FENCE_LEAD_IN = 'END your message with a fenced verdict block exactly in this shape';
461
+
462
+ function countOccurrences(haystack, needle) {
463
+ return haystack.split(needle).length - 1;
464
+ }
465
+
466
+ function loadVerdictFenceTailBuilder() {
467
+ const sentenceMatch = /const VERDICT_FENCE_CONTRACT_SENTENCE =\s*\n?\s*'[^']*'/.exec(convergeSource);
468
+ assert.notEqual(sentenceMatch, null, 'expected VERDICT_FENCE_CONTRACT_SENTENCE to be declared as one string literal');
469
+ const builderSource = lensPromptBody('buildVerdictFenceTail');
470
+ return new Function(
471
+ `${sentenceMatch[0]}\n${builderSource}\nreturn buildVerdictFenceTail;`,
472
+ )();
473
+ }
474
+
475
+ test('the verdict fence recipe is written once — no verify path re-inlines its own copy', () => {
476
+ assert.equal(
477
+ countOccurrences(convergeSource, VERDICT_FENCE_JSON_LINE),
478
+ 1,
479
+ 'expected the verdict fence JSON skeleton to be written exactly once so no verify path can drift from the shared recipe',
480
+ );
481
+ assert.equal(
482
+ countOccurrences(convergeSource, VERDICT_FENCE_LEAD_IN),
483
+ 1,
484
+ 'expected the fence lead-in sentence to be written exactly once so no verify path can drift from the shared recipe',
485
+ );
486
+ assert.match(
487
+ lensPromptBody('buildVerdictFenceSteps'),
488
+ /buildVerdictFenceTail\(/,
489
+ 'expected the gh-lookup binding path to render its fence through the shared tail builder',
490
+ );
491
+ assert.match(
492
+ lensPromptBody('runVerifierTask'),
493
+ /buildVerdictFenceTail\(/,
494
+ 'expected the hardening-verify path to render its fence through the shared tail builder',
495
+ );
496
+ });
497
+
498
+ test('the shared fence tail renders the same fence JSON and contract sentence at every indent', () => {
499
+ const buildVerdictFenceTail = loadVerdictFenceTailBuilder();
500
+ const topLevelTail = buildVerdictFenceTail('');
501
+ const nestedTail = buildVerdictFenceTail(' ');
502
+ for (const [siteName, renderedTail] of [['top-level', topLevelTail], ['nested', nestedTail]]) {
503
+ assert.ok(
504
+ renderedTail.includes(VERDICT_FENCE_JSON_LINE),
505
+ `expected the ${siteName} fence tail to carry the verdict JSON skeleton`,
506
+ );
507
+ assert.ok(
508
+ renderedTail.includes(VERDICT_FENCE_LEAD_IN),
509
+ `expected the ${siteName} fence tail to carry the fence lead-in sentence`,
510
+ );
511
+ assert.ok(
512
+ renderedTail.includes('```verdict'),
513
+ `expected the ${siteName} fence tail to open a verdict fence`,
514
+ );
515
+ assert.ok(
516
+ renderedTail.includes('name that check in prose directly above the fence rather than in findings'),
517
+ `expected the ${siteName} fence tail to carry the one incomplete-verdict contract sentence`,
518
+ );
519
+ }
520
+ assert.equal(
521
+ nestedTail,
522
+ topLevelTail.split('\n').map((eachLine) => ` ${eachLine}`).join('\n'),
523
+ 'expected indent to be the only difference between the two rendered fence tails',
524
+ );
525
+ });
526
+
428
527
  test('the verdict-fence binding does not self-resolve a cwd via git rev-parse for the manifest hash', () => {
429
528
  const fenceBuilder = lensPromptBody('buildVerdictFenceSteps');
430
529
  assert.doesNotMatch(
@@ -434,7 +533,7 @@ test('the verdict-fence binding does not self-resolve a cwd via git rev-parse fo
434
533
  );
435
534
  });
436
535
 
437
- test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids edits', () => {
536
+ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and forbids editing the tree under verification', () => {
438
537
  for (const verifyFunctionName of ['runVerifierTask']) {
439
538
  const verifyBody = lensPromptBody(verifyFunctionName);
440
539
  assert.match(
@@ -454,8 +553,8 @@ test('every verify step calls buildVerdictFenceSteps, uses code-verifier, and fo
454
553
  );
455
554
  assert.match(
456
555
  verifyBody,
457
- /do no edits|make no edits|not edit|no file edits/i,
458
- `expected ${verifyFunctionName} to be told to make no edits`,
556
+ /(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
557
+ `expected ${verifyFunctionName} to forbid editing the tree under verification (a deliberate break off that tree stays allowed)`,
459
558
  );
460
559
  }
461
560
  });
@@ -467,7 +566,7 @@ test('runFixerTask never verifies — verification belongs to the separate verif
467
566
  assert.match(fixerBody, /agentType:\s*'clean-coder'/, 'expected the fixer to use clean-coder for its commit and recovery edits');
468
567
  });
469
568
 
470
- test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids edits', () => {
569
+ test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch and forbids editing the tree under verification', () => {
471
570
  const verifyBody = lensPromptBody('runVerifierTask');
472
571
  assert.match(
473
572
  verifyBody,
@@ -476,8 +575,8 @@ test('runVerifierTask uses --manifest-hash-for-branch with the hardening branch
476
575
  );
477
576
  assert.match(
478
577
  verifyBody,
479
- /do no edits|make no edits|not edit|no file edits/i,
480
- 'expected the verifier to be told to make no edits',
578
+ /(?:no|not|never)[^.\n]{0,30}edit[^.\n]{0,40}tree[^.\n]{0,30}(?:under|being)\s+verif/i,
579
+ 'expected the verifier to forbid editing the tree under verification (a deliberate break off that tree stays allowed)',
481
580
  );
482
581
  });
483
582
 
@@ -901,6 +1000,28 @@ test('convergeReadOnlyAgent prepends HEADLESS_READONLY_PREAMBLE and the worktree
901
1000
  );
902
1001
  });
903
1002
 
1003
+ test('the read-only destructive pointer scopes its no-edit clause to the tree it reads', () => {
1004
+ const destructivePointer = convergeSource
1005
+ .split('\n')
1006
+ .find((eachLine) => eachLine.includes('Never run a destructive command'));
1007
+ assert.ok(destructivePointer, 'expected the read-only destructive pointer to be declared');
1008
+ assert.match(
1009
+ destructivePointer,
1010
+ /Never run a destructive command/,
1011
+ 'expected the destructive-command prohibition to stay absolute',
1012
+ );
1013
+ assert.match(
1014
+ destructivePointer,
1015
+ /no edit to the tree it reads/,
1016
+ 'expected the no-edit clause to be scoped to the tree under verification',
1017
+ );
1018
+ assert.doesNotMatch(
1019
+ destructivePointer,
1020
+ /edits nothing/,
1021
+ 'expected no blanket edits-nothing wording, which forbids the deliberate break off that tree',
1022
+ );
1023
+ });
1024
+
904
1025
  const taskDispatchers = [
905
1026
  { name: 'runGitTask', isAsync: false },
906
1027
  { name: 'runFixerTask', isAsync: false },
@@ -984,6 +1105,7 @@ test('the whole priming spawn-agent family is removed — every dispatcher spawn
984
1105
 
985
1106
  test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
986
1107
  const parseModule = new Function(
1108
+ `${functionSource('findLastVerdictFence')}\n` +
987
1109
  `${functionSource('parseLastVerdictFence')}\n` +
988
1110
  'return { parseLastVerdictFence };',
989
1111
  )();
@@ -994,6 +1116,7 @@ test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON
994
1116
 
995
1117
  test('parseLastVerdictFence returns null for non-string input', () => {
996
1118
  const parseModule = new Function(
1119
+ `${functionSource('findLastVerdictFence')}\n` +
997
1120
  `${functionSource('parseLastVerdictFence')}\n` +
998
1121
  'return { parseLastVerdictFence };',
999
1122
  )();
@@ -1003,6 +1126,7 @@ test('parseLastVerdictFence returns null for non-string input', () => {
1003
1126
 
1004
1127
  test('parseLastVerdictFence returns null when no verdict fence is present', () => {
1005
1128
  const parseModule = new Function(
1129
+ `${functionSource('findLastVerdictFence')}\n` +
1006
1130
  `${functionSource('parseLastVerdictFence')}\n` +
1007
1131
  'return { parseLastVerdictFence };',
1008
1132
  )();
@@ -1011,6 +1135,7 @@ test('parseLastVerdictFence returns null when no verdict fence is present', () =
1011
1135
 
1012
1136
  test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
1013
1137
  const parseModule = new Function(
1138
+ `${functionSource('findLastVerdictFence')}\n` +
1014
1139
  `${functionSource('parseLastVerdictFence')}\n` +
1015
1140
  'return { parseLastVerdictFence };',
1016
1141
  )();
@@ -255,9 +255,11 @@ test('the round-loop fix-stalled blockers survive the recovery wiring', () => {
255
255
  });
256
256
 
257
257
  const verifyObjectionModule = new Function(
258
- `${functionSource('parseLastVerdictFence')}\n` +
258
+ `${functionSource('findLastVerdictFence')}\n` +
259
+ `${functionSource('parseLastVerdictFence')}\n` +
259
260
  `${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
260
261
  `${functionSource('renderVerifyObjectionLine')}\n` +
262
+ `${functionSource('extractPreFenceProse')}\n` +
261
263
  `${functionSource('extractVerifyObjection')}\n` +
262
264
  'return { extractVerifyObjection, VERIFY_OBJECTION_FALLBACK };',
263
265
  )();
@@ -338,6 +340,110 @@ test('extractVerifyObjection falls back when no finding yields usable text', ()
338
340
  assert.equal(extractVerifyObjection(transcript), VERIFY_OBJECTION_FALLBACK);
339
341
  });
340
342
 
343
+ test('the prose reader anchors on the last CLOSED fence, not a stray unterminated marker', () => {
344
+ const transcript =
345
+ 'check X never showed red\n\n' +
346
+ '```verdict\n{"all_pass": false, "findings": []}\n```\n\n' +
347
+ 'and here I started to restate it\n\n' +
348
+ '```verdict';
349
+ const objection = extractVerifyObjection(transcript);
350
+ assert.equal(objection, 'check X never showed red');
351
+ assert.doesNotMatch(objection, /all_pass/, 'expected the verdict body never to be read back as prose');
352
+ });
353
+
354
+ test('both fence readers locate the fence through the one shared locator', () => {
355
+ assert.match(
356
+ functionSource('parseLastVerdictFence'),
357
+ /findLastVerdictFence\(/,
358
+ 'expected the verdict parser to locate its fence through findLastVerdictFence',
359
+ );
360
+ assert.match(
361
+ functionSource('extractPreFenceProse'),
362
+ /findLastVerdictFence\(/,
363
+ 'expected the prose reader to locate its fence through findLastVerdictFence, not a second search',
364
+ );
365
+ assert.doesNotMatch(
366
+ functionSource('extractPreFenceProse'),
367
+ /lastIndexOf\(/,
368
+ 'expected no second fence-search procedure in the prose reader to drift from the parser',
369
+ );
370
+ });
371
+
372
+ test('the prose reader and the verdict parser land on the same fence when a later marker carries no newline', () => {
373
+ const transcript =
374
+ 'check X never showed red\n\n' +
375
+ '```verdict\n{"all_pass": false, "findings": []}\n```\n\n' +
376
+ '```verdict {"all_pass": true, "findings": []}```';
377
+ const objection = extractVerifyObjection(transcript);
378
+ assert.doesNotMatch(
379
+ objection,
380
+ /all_pass/,
381
+ 'expected the prose reader to anchor on the fence the parser read, never to slice a verdict body in as prose',
382
+ );
383
+ assert.equal(objection, 'check X never showed red');
384
+ });
385
+
386
+ test('the prose reader skips a scaffolding-only paragraph above the fence', () => {
387
+ const transcript =
388
+ 'check X never showed red\n\n' + '## Verdict\n\n' + '```verdict\n{"all_pass": false, "findings": []}\n```';
389
+ assert.equal(extractVerifyObjection(transcript), 'check X never showed red');
390
+ });
391
+
392
+ const verifyRecoveryPromptModule = new Function(
393
+ `${constantLine('VERIFY_OBJECTION_FALLBACK')}\n` +
394
+ 'const prCoordinates = "owner/repo#1";\n' +
395
+ 'const PRE_COMMIT_GATE_STEP = "";\n' +
396
+ 'const EDIT_SCHEMA = {};\n' +
397
+ 'const TIERS = { sonnetMedium: {} };\n' +
398
+ 'const convergeAgent = (spawnPrompt) => spawnPrompt;\n' +
399
+ `${functionSource('runCodeEditorTask')}\n` +
400
+ 'return { runCodeEditorTask };',
401
+ )();
402
+
403
+ const { runCodeEditorTask: buildCodeEditorPrompt } = verifyRecoveryPromptModule;
404
+
405
+ function buildVerifyRecoveryPrompt(verifyTranscript) {
406
+ return buildCodeEditorPrompt('verify-recover', {
407
+ objection: extractVerifyObjection(verifyTranscript),
408
+ head: 'deadbeefcafe',
409
+ sourceLabel: 'round-1 lens findings',
410
+ attempt: 1,
411
+ });
412
+ }
413
+
414
+ const INCOMPLETE_CHECK_SENTENCE =
415
+ 'This verdict is incomplete: the deliberate break for check_docstring_runon_sentence never showed red, so that gate is unproven rather than failed.';
416
+
417
+ test('an incomplete verdict carries its named check into the fixer prompt instead of the fallback', () => {
418
+ const transcript =
419
+ 'I ran the named gates and read the diff against the task text.\n\n' +
420
+ `${INCOMPLETE_CHECK_SENTENCE}\n\n` +
421
+ '```verdict\n{"all_pass": false, "findings": [], "manifest_sha256": "0f1e2d"}\n```';
422
+ const fixerPrompt = buildVerifyRecoveryPrompt(transcript);
423
+ assert.match(fixerPrompt, /VERIFY-RECOVERY fixer/, 'expected the verify-recovery fixer prompt');
424
+ assert.ok(
425
+ fixerPrompt.includes(INCOMPLETE_CHECK_SENTENCE),
426
+ `expected the fixer prompt to carry the incomplete-check sentence, got:\n${fixerPrompt.slice(0, 600)}`,
427
+ );
428
+ assert.doesNotMatch(
429
+ fixerPrompt,
430
+ /without a parseable verdict/,
431
+ 'expected the incomplete verdict never to reach the fixer as VERIFY_OBJECTION_FALLBACK',
432
+ );
433
+ });
434
+
435
+ test('a genuine code defect still reaches the fixer prompt from findings, unchanged by pre-fence prose', () => {
436
+ const transcript =
437
+ `${INCOMPLETE_CHECK_SENTENCE}\n\n` +
438
+ '```verdict\n{"all_pass": false, "findings": [{"check": "Finding 1", "detail": "boundary still over-blocks"}]}\n```';
439
+ const fixerPrompt = buildVerifyRecoveryPrompt(transcript);
440
+ assert.match(fixerPrompt, /1\. Finding 1 — boundary still over-blocks/);
441
+ assert.ok(
442
+ !fixerPrompt.includes(INCOMPLETE_CHECK_SENTENCE),
443
+ 'expected findings to keep owning a verdict that names a code defect',
444
+ );
445
+ });
446
+
341
447
  test('the verify-recover task in runCodeEditorTask is a clean-coder edit step bound to the verifier objection and leaves changes uncommitted', () => {
342
448
  const recoverBody = functionSource('runCodeEditorTask');
343
449
  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` +
@@ -464,11 +466,7 @@ function runVerifierTask(task, context) {
464
466
  ` Run exactly:\n` +
465
467
  ` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "${context.hardeningBranch}"\n` +
466
468
  ` (substitute the REPO path you resolved for the script path). That prints a single 64-char hex hash on stdout — capture it.\n` +
467
- ` Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
468
- " ```verdict\n" +
469
- ` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
470
- " ```\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.`,
469
+ buildVerdictFenceTail(' '),
472
470
  { label, phase: 'Converge', agentType: 'code-verifier', ...TIERS.sonnetMedium },
473
471
  )
474
472
  }
@@ -847,6 +845,29 @@ const HARDENING_COMMIT_SCHEMA = {
847
845
  required: ['hardeningPrUrl', 'summary'],
848
846
  }
849
847
 
848
+ const VERDICT_FENCE_CONTRACT_SENTENCE =
849
+ '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.'
850
+
851
+ /**
852
+ * Build the closing fence recipe every verify prompt ends with: the lead-in
853
+ * sentence, the ```verdict block shape carrying the binding hash, and the
854
+ * contract sentence. Each verify path resolves the binding hash its own way
855
+ * and sits at its own nesting depth, so the caller passes the indent its
856
+ * surrounding prose uses; the fence body itself sits one level deeper.
857
+ * @param {string} indent leading whitespace the caller's prose sits at
858
+ * @returns {string} the fence recipe tail for a verify prompt
859
+ */
860
+ function buildVerdictFenceTail(indent) {
861
+ const fenceIndent = `${indent} `
862
+ return (
863
+ `${indent}Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
864
+ `${fenceIndent}` + "```verdict\n" +
865
+ `${fenceIndent}{"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
866
+ `${fenceIndent}` + "```\n" +
867
+ `${fenceIndent}${VERDICT_FENCE_CONTRACT_SENTENCE}`
868
+ )
869
+ }
870
+
850
871
  /**
851
872
  * Build the verdict-fence step instructions for a verify agent, binding the
852
873
  * surface hash by branch name rather than by a self-resolved cwd. Resolving
@@ -867,11 +888,7 @@ function buildVerdictFenceSteps(prOwner, prRepo, prNumber) {
867
888
  ` b. Run exactly:\n` +
868
889
  ` "C:\\Python313\\python.exe" "<REPO>/packages/claude-dev-env/hooks/blocking/verification_verdict_store.py" --manifest-hash-for-branch "<that branch>"\n` +
869
890
  ` (substitute the REPO path you resolved for the script path, and the branch name for <that branch>). That prints a single 64-char hex hash on stdout — capture it.\n` +
870
- `Then END your message with a fenced verdict block exactly in this shape, on its own, carrying that hash:\n` +
871
- " ```verdict\n" +
872
- ` {"all_pass": true, "findings": [], "manifest_sha256": "<that hash>"}\n` +
873
- " ```\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.`
891
+ buildVerdictFenceTail('')
875
892
  )
876
893
  }
877
894
 
@@ -1280,23 +1297,45 @@ function normalizeShaForComparison(sha) {
1280
1297
  }
1281
1298
 
1282
1299
  /**
1283
- * Parse the LAST ```verdict ...``` fenced JSON block from a transcript.
1284
- * Guards against non-string input, iterates all fence matches for the last one,
1285
- * parses the JSON, and returns the object or null on any failure.
1300
+ * Locate the LAST ```verdict ...``` fence in a transcript: the marker, optional
1301
+ * whitespace, a required newline, then a body closed by ```. This is the ONE
1302
+ * definition of "the verdict fence" the verdict parser and the pre-fence prose
1303
+ * reader both anchor here, so they cannot land on different fences.
1304
+ *
1305
+ * ::
1306
+ *
1307
+ * ok: 'note\n\n```verdict\n{}\n```' -> body '{}\n' starting at index 6
1308
+ * flag: 'note\n\n```verdict {}```' -> null (no newline after the marker)
1309
+ *
1310
+ * A marker with no newline after it is not a fence, so a later one cannot pull
1311
+ * either reader off the fence the other reads.
1312
+ *
1286
1313
  * @param {string|null|undefined} transcript the agent transcript text
1287
- * @returns {object|null} the parsed verdict object, or null when absent or malformed
1314
+ * @returns {{bodyText: string, startIndex: number}|null} the last fence, or null when there is none
1288
1315
  */
1289
- function parseLastVerdictFence(transcript) {
1316
+ function findLastVerdictFence(transcript) {
1290
1317
  if (typeof transcript !== 'string') return null
1291
1318
  const fencePattern = /```verdict\s*\n([\s\S]*?)```/g
1292
- let lastFenceBody = null
1319
+ let lastFence = null
1293
1320
  let eachMatch
1294
1321
  while ((eachMatch = fencePattern.exec(transcript)) !== null) {
1295
- lastFenceBody = eachMatch[1]
1322
+ lastFence = { bodyText: eachMatch[1], startIndex: eachMatch.index }
1296
1323
  }
1297
- if (lastFenceBody === null) return null
1324
+ return lastFence
1325
+ }
1326
+
1327
+ /**
1328
+ * Parse the LAST ```verdict ...``` fenced JSON block from a transcript.
1329
+ * Guards against non-string input, takes the fence findLastVerdictFence locates,
1330
+ * parses the JSON, and returns the object or null on any failure.
1331
+ * @param {string|null|undefined} transcript the agent transcript text
1332
+ * @returns {object|null} the parsed verdict object, or null when absent or malformed
1333
+ */
1334
+ function parseLastVerdictFence(transcript) {
1335
+ const lastFence = findLastVerdictFence(transcript)
1336
+ if (lastFence === null) return null
1298
1337
  try {
1299
- return JSON.parse(lastFenceBody)
1338
+ return JSON.parse(lastFence.bodyText)
1300
1339
  } catch {
1301
1340
  return null
1302
1341
  }
@@ -1343,14 +1382,57 @@ function renderVerifyObjectionLine(eachFinding) {
1343
1382
  return stringifiedFinding === '{}' ? null : stringifiedFinding
1344
1383
  }
1345
1384
 
1385
+ /**
1386
+ * Read the prose paragraph the verifier wrote directly above its last verdict
1387
+ * fence. A verdict that is incomplete — a check it rests on never showed red —
1388
+ * is not a code defect, so the fence contract puts that reason here rather than
1389
+ * in findings; this reader is how the reason survives into the re-fix step.
1390
+ *
1391
+ * Anchors through findLastVerdictFence, the single locator parseLastVerdictFence
1392
+ * reads its JSON from, so both readers see the same fence by construction rather
1393
+ * than by two searches that happen to agree: a stray marker after the real fence
1394
+ * — unterminated, or closed but carrying no newline after the marker — moves
1395
+ * neither reader. A candidate paragraph carrying only markdown scaffolding (a
1396
+ * heading, a fence delimiter) is skipped, since handing the fixer a heading is
1397
+ * worse than the generic fallback the caller keeps.
1398
+ *
1399
+ * ::
1400
+ *
1401
+ * 'ran the gates\n\ncheck X never showed red\n\n```verdict\n{}\n```'
1402
+ * -> 'check X never showed red'
1403
+ * '```verdict\n{}\n```' -> null
1404
+ *
1405
+ * @param {string|null|undefined} verifyTranscript the verifier transcript text
1406
+ * @returns {string|null} the last prose paragraph above the final fence, or null when there is none
1407
+ */
1408
+ function extractPreFenceProse(verifyTranscript) {
1409
+ const lastFence = findLastVerdictFence(verifyTranscript)
1410
+ if (lastFence === null) return null
1411
+ const proseParagraphs = verifyTranscript
1412
+ .slice(0, lastFence.startIndex)
1413
+ .split(/\n\s*\n/)
1414
+ .map((eachParagraph) => eachParagraph.trim())
1415
+ .filter((eachParagraph) =>
1416
+ eachParagraph
1417
+ .split('\n')
1418
+ .some((eachLine) => {
1419
+ const trimmedLine = eachLine.trim()
1420
+ return trimmedLine.length > 0 && !trimmedLine.startsWith('#') && !trimmedLine.startsWith('```') && /[A-Za-z]/.test(trimmedLine)
1421
+ }),
1422
+ )
1423
+ return proseParagraphs.length === 0 ? null : proseParagraphs[proseParagraphs.length - 1]
1424
+ }
1425
+
1346
1426
  /**
1347
1427
  * Pull the verifier's stated objections out of a failed verify transcript so the
1348
1428
  * re-fix step knows what the verdict rejected. Reads the last fenced verdict JSON
1349
1429
  * (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.
1430
+ * renderVerifyObjectionLine into a numbered list. When findings yields no usable
1431
+ * line the verdict is an incomplete one rather than a code-defect one, so the
1432
+ * prose paragraph above the fence where the fence contract puts the unshown-red
1433
+ * check carries the objection instead. A missing fence, a parse failure, or an
1434
+ * empty findings list with no prose above the fence falls back to a generic
1435
+ * re-read instruction, so the re-fix step always receives actionable text.
1354
1436
  * @param {string|null|undefined} verifyTranscript the failed verifier transcript text
1355
1437
  * @returns {string} a human-readable block of the verifier's objections
1356
1438
  */
@@ -1361,7 +1443,7 @@ function extractVerifyObjection(verifyTranscript) {
1361
1443
  const renderedObjections = allObjections
1362
1444
  .map((eachFinding) => renderVerifyObjectionLine(eachFinding))
1363
1445
  .filter((eachLine) => eachLine !== null)
1364
- if (renderedObjections.length === 0) return VERIFY_OBJECTION_FALLBACK
1446
+ if (renderedObjections.length === 0) return extractPreFenceProse(verifyTranscript) || VERIFY_OBJECTION_FALLBACK
1365
1447
  return renderedObjections.map((eachLine, position) => `${position + 1}. ${eachLine}`).join('\n')
1366
1448
  }
1367
1449
 
@@ -22,10 +22,6 @@ WINDOWS_OS_NAME = "nt"
22
22
  WINDOWS_COMMAND_SHELL = "cmd"
23
23
  WINDOWS_COMMAND_SHELL_RUN_FLAG = "/c"
24
24
  ALL_WINDOWS_SCRIPT_SUFFIXES = (".cmd", ".bat")
25
- WINDOWS_TASKKILL_COMMAND = "taskkill"
26
- WINDOWS_TASKKILL_FORCE_FLAG = "/F"
27
- WINDOWS_TASKKILL_TREE_FLAG = "/T"
28
- WINDOWS_TASKKILL_PID_FLAG = "/PID"
29
25
 
30
26
  JSONRPC_VERSION = "2.0"
31
27
  JSONRPC_KEY_ID = "id"