claude-dev-env 2.7.1 → 2.9.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 (127) hide show
  1. package/CLAUDE.md +7 -1
  2. package/_shared/CLAUDE.md +1 -0
  3. package/_shared/advisor/advisor-protocol.md +19 -9
  4. package/_shared/pr-loop/audit-contract.md +4 -4
  5. package/_shared/pr-loop/precatch-rubric.md +2 -2
  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/clean-coder.md +9 -19
  13. package/agents/code-quality-agent.md +6 -5
  14. package/agents/deep-research.md +7 -24
  15. package/agents/docs-agent.md +1 -27
  16. package/agents/issue-tracker.md +1 -7
  17. package/agents/skill-writer-agent.md +1 -2
  18. package/agents/test_agent_frontmatter.py +335 -12
  19. package/docs/CODE_RULES.md +4 -2
  20. package/docs/references/CLAUDE.md +2 -2
  21. package/docs/references/advisor-tool.md +44 -6
  22. package/docs/references/team-advisor-skill.md +14 -8
  23. package/hooks/blocking/CLAUDE.md +2 -0
  24. package/hooks/blocking/fable_spawn_gate.py +187 -0
  25. package/hooks/blocking/piped_pytest_blocker.py +1223 -0
  26. package/hooks/blocking/plain_language_blocker.py +287 -15
  27. package/hooks/blocking/test_fable_spawn_gate.py +374 -0
  28. package/hooks/blocking/test_piped_pytest_blocker.py +587 -0
  29. package/hooks/blocking/test_plain_language_blocker.py +277 -2
  30. package/hooks/blocking/test_pre_tool_use_dispatcher.py +34 -2
  31. package/hooks/git-hooks/CLAUDE.md +2 -2
  32. package/hooks/git-hooks/git_hooks_constants/__init__.py +28 -0
  33. package/hooks/git-hooks/pre_push.py +343 -54
  34. package/hooks/git-hooks/test_pre_push.py +852 -6
  35. package/hooks/hooks.json +9 -19
  36. package/hooks/hooks_constants/CLAUDE.md +2 -0
  37. package/hooks/hooks_constants/bash_pre_tool_use_dispatcher_constants.py +1 -0
  38. package/hooks/hooks_constants/code_rules_path_utils_constants.py +1 -0
  39. package/hooks/hooks_constants/fable_spawn_gate_constants.py +62 -0
  40. package/hooks/hooks_constants/piped_pytest_blocker_constants.py +360 -0
  41. package/hooks/hooks_constants/plain_language_blocker_constants.py +64 -1
  42. package/hooks/hooks_constants/pre_tool_use_dispatcher_constants.py +10 -0
  43. package/hooks/hooks_constants/shell_command_segments.py +1 -1
  44. package/hooks/hooks_constants/test_bash_pre_tool_use_dispatcher_constants.py +1 -0
  45. package/hooks/hooks_constants/test_pre_tool_use_dispatcher_constants.py +27 -0
  46. package/hooks/hooks_constants/test_prose_metrics_parity.py +124 -0
  47. package/output-styles/CLAUDE.md +17 -0
  48. package/output-styles/caveman-agent.md +37 -0
  49. package/package.json +2 -1
  50. package/rules/CLAUDE.md +1 -0
  51. package/rules/ask-user-question-required.md +26 -0
  52. package/rules/claims-as-quotes.md +65 -0
  53. package/rules/code-standards.md +33 -7
  54. package/rules/eli11-replies.md +1 -1
  55. package/scripts/CLAUDE.md +3 -3
  56. package/scripts/_code_review_test_support.py +6 -0
  57. package/scripts/check.ps1 +18 -5
  58. package/scripts/claude_chain_runner.py +203 -31
  59. package/scripts/codec_forwarding_test_support.py +2 -0
  60. package/scripts/dev_env_scripts_constants/CLAUDE.md +4 -4
  61. package/scripts/dev_env_scripts_constants/claude_chain_constants.py +38 -0
  62. package/scripts/dev_env_scripts_constants/code_review_constants.py +403 -2
  63. package/scripts/dev_env_scripts_constants/grok_run_ledger_constants.py +50 -0
  64. package/scripts/dev_env_scripts_constants/grok_worker_constants.py +132 -24
  65. package/scripts/grok_headless_runner.py +16 -83
  66. package/scripts/grok_patch_artifacts.py +123 -0
  67. package/scripts/grok_run_ledger.py +318 -0
  68. package/scripts/spawn_grok_batch.py +591 -10
  69. package/scripts/test_claude_chain_runner.py +358 -0
  70. package/scripts/test_grok_headless_runner.py +18 -63
  71. package/scripts/test_grok_patch_artifacts.py +82 -0
  72. package/scripts/test_grok_run_ledger.py +116 -0
  73. package/scripts/test_invoke_code_review.py +298 -0
  74. package/scripts/test_resolve_worker_spawn.py +6 -0
  75. package/scripts/test_spawn_grok_batch.py +396 -0
  76. package/scripts/tests/CLAUDE.md +1 -0
  77. package/scripts/tests/test_grok_worker_constants.py +59 -0
  78. package/skills/CLAUDE.md +4 -2
  79. package/skills/_shared/CLAUDE.md +37 -4
  80. package/skills/_shared/advisor/CLAUDE.md +9 -0
  81. package/skills/_shared/advisor/advisor-protocol.md +5 -0
  82. package/skills/_shared/advisor/scripts/README.md +9 -0
  83. package/skills/_shared/end-of-run-gotcha-recommendations.md +156 -0
  84. package/skills/_shared/pr-loop/CLAUDE.md +18 -1
  85. package/skills/_shared/pr-loop/audit-contract.md +5 -0
  86. package/skills/_shared/pr-loop/audit-reply-template.md +5 -0
  87. package/skills/_shared/pr-loop/code-rules-gate.md +5 -0
  88. package/skills/_shared/pr-loop/fix-protocol.md +5 -0
  89. package/skills/_shared/pr-loop/gh-payloads.md +5 -0
  90. package/skills/_shared/pr-loop/post-audit-thread-contract.md +5 -0
  91. package/skills/_shared/pr-loop/precatch-rubric.md +5 -0
  92. package/skills/_shared/pr-loop/scripts/CLAUDE.md +8 -1
  93. package/skills/_shared/pr-loop/scripts/RUNTIME_SCRIPTS.md +29 -0
  94. package/skills/_shared/pr-loop/scripts/test_build_audit_prompt.py +46 -0
  95. package/skills/_shared/pr-loop/state-schema.md +5 -0
  96. package/skills/_shared/pr-loop/worker-spawn.md +5 -0
  97. package/skills/autoconverge/workflow/converge.contract.test.mjs +105 -2
  98. package/skills/autoconverge/workflow/converge.fix-recovery.test.mjs +34 -1
  99. package/skills/autoconverge/workflow/converge.mjs +66 -33
  100. package/skills/codex-review/scripts/codex_review_scripts_constants/codex_usage_probe_constants.py +0 -4
  101. package/skills/codex-review/scripts/codex_usage_probe.py +20 -33
  102. package/skills/codex-review/scripts/run_codex_review.py +16 -64
  103. package/skills/codex-review/scripts/test_codex_usage_probe.py +46 -41
  104. package/skills/codex-review/scripts/test_run_codex_review.py +1 -33
  105. package/skills/e-code-review/SKILL.md +15 -9
  106. package/skills/e-code-review/reference/fix.md +29 -7
  107. package/skills/e-code-review/reference/loop.md +230 -14
  108. package/skills/e-code-review/reference/low.md +33 -15
  109. package/skills/e-code-review/reference/medium.md +55 -21
  110. package/skills/e-code-review/reference/runner-selection.md +40 -0
  111. package/skills/e-code-review/reference/xhigh.md +30 -12
  112. package/skills/e-code-review/scripts/e_code_review_scripts_constants/__init__.py +1 -0
  113. package/skills/e-code-review/scripts/e_code_review_scripts_constants/grok_code_review_constants.py +55 -0
  114. package/skills/e-code-review/scripts/grok_code_review.py +221 -0
  115. package/skills/e-code-review/scripts/test_grok_code_review.py +212 -0
  116. package/skills/fresh-branch/CLAUDE.md +5 -5
  117. package/skills/fresh-branch/SKILL.md +14 -6
  118. package/skills/fresh-branch/scripts/create_fresh_branch.py +122 -39
  119. package/skills/fresh-branch/scripts/fresh_branch_scripts_constants/fresh_branch_cli_constants.py +10 -3
  120. package/skills/fresh-branch/scripts/test_create_fresh_branch.py +251 -0
  121. package/skills/grok-spawn/SKILL.md +10 -0
  122. package/skills/orchestrator/SKILL.md +9 -1
  123. package/skills/orchestrator-refresh/SKILL.md +5 -1
  124. package/skills/task-build/reference/tool-routing.md +3 -0
  125. package/skills/team-advisor/SKILL.md +23 -41
  126. package/system-prompts/software-engineer.xml +6 -3
  127. package/skills/test_markdown_link_integrity.py +0 -107
@@ -0,0 +1,156 @@
1
+ # End-of-run gotcha recommendations
2
+
3
+ **Shared close-out** for every skill.
4
+
5
+ After the deliverable, recommend pasteable gotchas from issues **this run** hit.
6
+
7
+ ## When
8
+
9
+ - **With issues:** end of work, after the deliverable.
10
+ - **Clean run:** end with the deliverable only.
11
+
12
+ ## What counts
13
+
14
+ Keep only items that **bit this run**:
15
+
16
+ - Wrong **path, file, or config**
17
+ - **Command / CLI** failed or needed different flags
18
+ - **Auth, permission, or environment** blocked a step
19
+ - **Data shape** mismatch (empty set, wrong schema, bad field)
20
+ - **Tool, UI, or selector** missed the target
21
+ - **Timing** needed a retry or longer wait
22
+ - **Dependency or install** gap or wrong version
23
+ - **Output format** wrong for the next step
24
+ - A **workaround** required to finish
25
+
26
+ ## What to produce
27
+
28
+ **Order (hard):**
29
+
30
+ 1. Show findings in chat; categorize as P0-P3.
31
+ 2. Then `AskUserQuestion`.
32
+ 3. On confirm only, file via `issue-tracker`.
33
+
34
+ ### Cold-reader frame
35
+
36
+ Assume zero context.
37
+
38
+ **Exactly two setup sentences**, each on its **own rendered line**:
39
+
40
+ 1. What we were doing.
41
+ 2. Why these notes matter.
42
+
43
+ ### 1. Chat findings (always first)
44
+
45
+ #### Sentence law (HARD)
46
+
47
+ | Rule | Required |
48
+ |---|---|
49
+ | **One sentence per rendered line** | Exactly one terminal `.` `?` or `!` on that line |
50
+ | **No collapsed pairs** | A single markdown newline is **not** a line break — it joins sentences |
51
+ | **Force the break** | Blank line between every prose sentence, **or** one list item per sentence |
52
+ | **No clause glue** | No em-dash or semicolon joining two clauses |
53
+ | **One idea** | If you can split it, split it |
54
+ | **Heavy detail** | Prefer an **ASCII infographic** over a stack of sentences |
55
+
56
+ **Self-check before send:** paste the chat into a viewer that collapses soft breaks.
57
+
58
+ If two periods appear on one visual line, **split or diagram**.
59
+
60
+ #### Per gotcha (HARD shape)
61
+
62
+ | Part | Form |
63
+ |---|---|
64
+ | **Title** | Heading fragment only |
65
+ | **Broke** | One sentence alone on its rendered line |
66
+ | **Fix** | One sentence alone on its rendered line |
67
+ | **ASCII** | Required for path, flag, or before/after |
68
+
69
+ **Cap: two prose sentences per gotcha.**
70
+
71
+ ```markdown
72
+ ## Session close-out
73
+
74
+ We built phone-theme prompts from a project style catalog.
75
+
76
+ Keep these traps for the next run.
77
+
78
+ ## Gotcha recommendations
79
+
80
+ ### Wrong catalog folder
81
+
82
+ The style picker failed outside the project.
83
+
84
+ Run from the project root with data/midjourney_sref_catalog.json.
85
+
86
+ ```text
87
+ wrong folder --> fail
88
+ project root --> data/midjourney_sref_catalog.json
89
+ ```
90
+ ```
91
+
92
+ **Wrong (soft break joins sentences into one line):**
93
+
94
+ ```markdown
95
+ The style picker failed outside the project.
96
+ Run from the project root with data/midjourney_sref_catalog.json.
97
+ ```
98
+
99
+ Renders as one line with **two** sentences.
100
+
101
+ **Right (blank line forces two lines):**
102
+
103
+ ```markdown
104
+ The style picker failed outside the project.
105
+
106
+ Run from the project root with data/midjourney_sref_catalog.json.
107
+ ```
108
+
109
+ **Right (list items):**
110
+
111
+ ```markdown
112
+ - The style picker failed outside the project.
113
+ - Run from the project root with data/midjourney_sref_catalog.json.
114
+ ```
115
+
116
+ ### 2. Issue offer (`AskUserQuestion`)
117
+
118
+ Only after the chat block.
119
+
120
+ | Field | Limit |
121
+ |---|---|
122
+ | **Question** | One short sentence |
123
+ | **Header** | `File issues` |
124
+ | **label** | Plain name |
125
+ | **description** | One short sentence |
126
+
127
+ Detail and ASCII stay in chat.
128
+
129
+ **On confirm:** file via `issue-tracker`.
130
+
131
+ Cold-reader issue body.
132
+
133
+ Clean run: skip.
134
+
135
+ ## Built-in skill gotcha
136
+
137
+ **Two sentences on one rendered line.**
138
+
139
+ Markdown soft-breaks join prose.
140
+
141
+ Review the close-out before send.
142
+
143
+ Split with a blank line or a list item.
144
+
145
+ Or move the detail into an ASCII infographic.
146
+
147
+ ## Rules
148
+
149
+ - Cold-reader first.
150
+ - Chat first, then ask.
151
+ - One sentence per **rendered** line.
152
+ - Blank line or list item between prose sentences.
153
+ - Two prose sentences per gotcha max.
154
+ - ASCII for path, flag, or before/after.
155
+ - File only on confirm.
156
+ - Lived issues only.
@@ -12,7 +12,24 @@ those skills invoke during each loop tick.
12
12
  | `prompts/` | XML agent prompt templates. |
13
13
  | `scripts/` | Python scripts for loop state management, prompt building, outcome recording, path resolution, pacer selection, and preflight checks. |
14
14
 
15
- ## Key files
15
+ ## Canonical-path stubs (`~/.claude/_shared/pr-loop/`)
16
+
17
+ Open a stub, then load the `@` target:
18
+
19
+ | Stub here | Load |
20
+ |---|---|
21
+ | `audit-contract.md` | `@~/.claude/_shared/pr-loop/audit-contract.md` |
22
+ | `audit-reply-template.md` | `@~/.claude/_shared/pr-loop/audit-reply-template.md` |
23
+ | `code-rules-gate.md` | `@~/.claude/_shared/pr-loop/code-rules-gate.md` |
24
+ | `fix-protocol.md` | `@~/.claude/_shared/pr-loop/fix-protocol.md` |
25
+ | `gh-payloads.md` | `@~/.claude/_shared/pr-loop/gh-payloads.md` |
26
+ | `post-audit-thread-contract.md` | `@~/.claude/_shared/pr-loop/post-audit-thread-contract.md` |
27
+ | `precatch-rubric.md` | `@~/.claude/_shared/pr-loop/precatch-rubric.md` |
28
+ | `state-schema.md` | `@~/.claude/_shared/pr-loop/state-schema.md` |
29
+ | `worker-spawn.md` | `@~/.claude/_shared/pr-loop/worker-spawn.md` |
30
+ | `scripts/RUNTIME_SCRIPTS.md` | `@~/.claude/_shared/pr-loop/scripts/` |
31
+
32
+ ## Key files (live in this tree)
16
33
 
17
34
  | File | Role |
18
35
  |---|---|
@@ -0,0 +1,5 @@
1
+ # Audit contract
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/audit-contract.md
@@ -0,0 +1,5 @@
1
+ # Audit reply template
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/audit-reply-template.md
@@ -0,0 +1,5 @@
1
+ # Code rules gate
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/code-rules-gate.md
@@ -0,0 +1,5 @@
1
+ # Fix protocol
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/fix-protocol.md
@@ -0,0 +1,5 @@
1
+ # Gh payloads
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/gh-payloads.md
@@ -0,0 +1,5 @@
1
+ # Post-audit thread contract
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/post-audit-thread-contract.md
@@ -0,0 +1,5 @@
1
+ # Precatch rubric
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/precatch-rubric.md
@@ -2,7 +2,14 @@
2
2
 
3
3
  Python scripts that run the PR audit-fix loop at runtime. Both `bugteam` and `pr-converge` invoke these scripts during each loop tick.
4
4
 
5
- ## Key files
5
+ ## Two script homes
6
+
7
+ | Home | Path |
8
+ |---|---|
9
+ | **Skill-local** (this folder) | Converge helpers: `build_*_prompt.py`, `init_loop_state.py`, `portable_converge_driver.py`, … |
10
+ | **Runtime** (top-level shared) | Gate / preflight / review helpers — see `RUNTIME_SCRIPTS.md` → `@~/.claude/_shared/pr-loop/scripts/` |
11
+
12
+ ## Key files (skill-local)
6
13
 
7
14
  | File | Role |
8
15
  |---|---|
@@ -0,0 +1,29 @@
1
+ # PR-loop runtime scripts
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/scripts/code_rules_gate.py
6
+
7
+ @~/.claude/_shared/pr-loop/scripts/preflight.py
8
+
9
+ @~/.claude/_shared/pr-loop/scripts/post_audit_thread.py
10
+
11
+ @~/.claude/_shared/pr-loop/scripts/post_audit_review.py
12
+
13
+ @~/.claude/_shared/pr-loop/scripts/gh_util.py
14
+
15
+ @~/.claude/_shared/pr-loop/scripts/reviews_disabled.py
16
+
17
+ @~/.claude/_shared/pr-loop/scripts/reviewer_availability.py
18
+
19
+ @~/.claude/_shared/pr-loop/scripts/copilot_quota.py
20
+
21
+ @~/.claude/_shared/pr-loop/scripts/grant_project_claude_permissions.py
22
+
23
+ @~/.claude/_shared/pr-loop/scripts/revoke_project_claude_permissions.py
24
+
25
+ @~/.claude/_shared/pr-loop/scripts/verify_review.py
26
+
27
+ @~/.claude/_shared/pr-loop/scripts/README.md
28
+
29
+ Skill-local converge helpers (`build_*_prompt.py`, `init_loop_state.py`, and siblings) live in this `scripts/` folder. Runtime gate, preflight, and review helpers live under `~/.claude/_shared/pr-loop/scripts/`.
@@ -378,3 +378,49 @@ def test_main_headless_flavor_emits_outcome_path_on_stdout(
378
378
  assert "post_audit_thread.py" in captured.out
379
379
  assert "Do not post reviews" in captured.out
380
380
  assert "add_comment_to_pending_review" not in captured.out
381
+
382
+
383
+ _PACKAGE_ROOT = _SCRIPTS_DIR.parents[3]
384
+ _CODE_QUALITY_AGENT_PATH = _PACKAGE_ROOT / "agents" / "code-quality-agent.md"
385
+ _AUDIT_CONTRACT_PATH = _PACKAGE_ROOT / "_shared" / "pr-loop" / "audit-contract.md"
386
+ _PRECATCH_RUBRIC_PATH = _PACKAGE_ROOT / "_shared" / "pr-loop" / "precatch-rubric.md"
387
+ _CATEGORY_Q_LABEL = (
388
+ "Cross-surface claim consistency "
389
+ "(terminology, PR-description claims, message-vs-guard)"
390
+ )
391
+ _CATEGORY_Q_RUBRIC_REFERENCE = (
392
+ "../audit-rubrics/category_rubrics/category-q-cross-surface-claims.md"
393
+ )
394
+ _A_THROUGH_Q_PATTERN = re.compile(r"A[\u2013-]Q")
395
+ _SEVENTEEN_CATEGORIES_PATTERN = re.compile(r"seventeen categor", re.IGNORECASE)
396
+ _STALE_SIXTEEN_PATTERN = re.compile(r"sixteen categor", re.IGNORECASE)
397
+ _STALE_A_THROUGH_P_PATTERN = re.compile(r"A[\u2013-]P")
398
+
399
+
400
+ def test_code_quality_agent_default_scope_is_a_through_q() -> None:
401
+ agent_text = _CODE_QUALITY_AGENT_PATH.read_text(encoding="utf-8")
402
+ assert _A_THROUGH_Q_PATTERN.search(agent_text) is not None
403
+ assert _SEVENTEEN_CATEGORIES_PATTERN.search(agent_text) is not None
404
+ assert _STALE_SIXTEEN_PATTERN.search(agent_text) is None
405
+ assert "A through Q" in agent_text
406
+ assert "A through P" not in agent_text
407
+ assert _STALE_A_THROUGH_P_PATTERN.search(agent_text) is None
408
+ assert "## Bug Categories A–Q" in agent_text
409
+ assert _CATEGORY_Q_LABEL in agent_text
410
+ assert _CATEGORY_Q_RUBRIC_REFERENCE in agent_text
411
+ assert "| Q |" in agent_text
412
+
413
+
414
+ def test_audit_contract_category_schema_includes_q() -> None:
415
+ contract_text = _AUDIT_CONTRACT_PATH.read_text(encoding="utf-8")
416
+ assert '"category": "A | B | C | D | E | F | G | H | I | J | K | L | M | N | O | P | Q"' in (
417
+ contract_text
418
+ )
419
+ assert _A_THROUGH_Q_PATTERN.search(contract_text) is not None
420
+ assert _STALE_A_THROUGH_P_PATTERN.search(contract_text) is None
421
+
422
+
423
+ def test_precatch_rubric_adversarial_lane_uses_a_through_q() -> None:
424
+ rubric_text = _PRECATCH_RUBRIC_PATH.read_text(encoding="utf-8")
425
+ assert _A_THROUGH_Q_PATTERN.search(rubric_text) is not None
426
+ assert _STALE_A_THROUGH_P_PATTERN.search(rubric_text) is None
@@ -0,0 +1,5 @@
1
+ # State schema
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/state-schema.md
@@ -0,0 +1,5 @@
1
+ # Worker spawn
2
+
3
+ **Canonical home:**
4
+
5
+ @~/.claude/_shared/pr-loop/worker-spawn.md
@@ -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(
@@ -1006,6 +1105,7 @@ test('the whole priming spawn-agent family is removed — every dispatcher spawn
1006
1105
 
1007
1106
  test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON', () => {
1008
1107
  const parseModule = new Function(
1108
+ `${functionSource('findLastVerdictFence')}\n` +
1009
1109
  `${functionSource('parseLastVerdictFence')}\n` +
1010
1110
  'return { parseLastVerdictFence };',
1011
1111
  )();
@@ -1016,6 +1116,7 @@ test('parseLastVerdictFence returns non-null for a verdict fence with valid JSON
1016
1116
 
1017
1117
  test('parseLastVerdictFence returns null for non-string input', () => {
1018
1118
  const parseModule = new Function(
1119
+ `${functionSource('findLastVerdictFence')}\n` +
1019
1120
  `${functionSource('parseLastVerdictFence')}\n` +
1020
1121
  'return { parseLastVerdictFence };',
1021
1122
  )();
@@ -1025,6 +1126,7 @@ test('parseLastVerdictFence returns null for non-string input', () => {
1025
1126
 
1026
1127
  test('parseLastVerdictFence returns null when no verdict fence is present', () => {
1027
1128
  const parseModule = new Function(
1129
+ `${functionSource('findLastVerdictFence')}\n` +
1028
1130
  `${functionSource('parseLastVerdictFence')}\n` +
1029
1131
  'return { parseLastVerdictFence };',
1030
1132
  )();
@@ -1033,6 +1135,7 @@ test('parseLastVerdictFence returns null when no verdict fence is present', () =
1033
1135
 
1034
1136
  test('parseLastVerdictFence returns null for malformed JSON in the fence', () => {
1035
1137
  const parseModule = new Function(
1138
+ `${functionSource('findLastVerdictFence')}\n` +
1036
1139
  `${functionSource('parseLastVerdictFence')}\n` +
1037
1140
  'return { parseLastVerdictFence };',
1038
1141
  )();
@@ -255,7 +255,8 @@ 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` +
261
262
  `${functionSource('extractPreFenceProse')}\n` +
@@ -350,6 +351,38 @@ test('the prose reader anchors on the last CLOSED fence, not a stray unterminate
350
351
  assert.doesNotMatch(objection, /all_pass/, 'expected the verdict body never to be read back as prose');
351
352
  });
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
+
353
386
  test('the prose reader skips a scaffolding-only paragraph above the fence', () => {
354
387
  const transcript =
355
388
  'check X never showed red\n\n' + '## Verdict\n\n' + '```verdict\n{"all_pass": false, "findings": []}\n```';