@navels/neal 0.1.0 → 0.3.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 (120) hide show
  1. package/README.md +206 -209
  2. package/SECURITY.md +16 -19
  3. package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
  4. package/dist/neal/adjudicator/execute.js +33 -33
  5. package/dist/neal/adjudicator/final-completion.js +2 -31
  6. package/dist/neal/adjudicator/planning.js +264 -24
  7. package/dist/neal/agents/prompts.js +7 -15
  8. package/dist/neal/agents/rounds.js +51 -73
  9. package/dist/neal/agents/schemas.js +615 -502
  10. package/dist/neal/agents/structured-json.js +36 -0
  11. package/dist/neal/blocked-guidance.js +16 -9
  12. package/dist/neal/cli.js +2 -2
  13. package/dist/neal/commands/check.js +2 -2
  14. package/dist/neal/commands/compat.js +100 -36
  15. package/dist/neal/commands/interactive-activity.js +2 -2
  16. package/dist/neal/commands/recovery-guidance.js +20 -8
  17. package/dist/neal/commands/runtime.js +4 -2
  18. package/dist/neal/config.js +40 -7
  19. package/dist/neal/context/inline-review-context.js +13 -28
  20. package/dist/neal/context/reviewer-context.js +19 -14
  21. package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
  22. package/dist/neal/eval/reviewer-eval.js +155 -0
  23. package/dist/neal/git.js +9 -3
  24. package/dist/neal/interactive-controls.js +5 -0
  25. package/dist/neal/orchestrator/completion.js +215 -113
  26. package/dist/neal/orchestrator/failures.js +7 -0
  27. package/dist/neal/orchestrator/notifications.js +9 -9
  28. package/dist/neal/orchestrator/phases/planning.js +124 -112
  29. package/dist/neal/orchestrator/phases/recovery.js +151 -115
  30. package/dist/neal/orchestrator/phases/review.js +28 -1
  31. package/dist/neal/orchestrator/split-plan.js +12 -11
  32. package/dist/neal/orchestrator/transitions.js +36 -71
  33. package/dist/neal/orchestrator.js +3 -0
  34. package/dist/neal/plan-doc.js +24 -1
  35. package/dist/neal/plan-queue.js +29 -1
  36. package/dist/neal/prompts/assert-builder.js +8 -1
  37. package/dist/neal/prompts/execute.js +11 -19
  38. package/dist/neal/prompts/planning.js +64 -55
  39. package/dist/neal/prompts/review-doctrine.js +13 -50
  40. package/dist/neal/prompts/specialized.js +28 -22
  41. package/dist/neal/prompts/specs.js +223 -17
  42. package/dist/neal/providers/agent-settings-isolation.js +35 -0
  43. package/dist/neal/providers/anthropic-claude.js +309 -248
  44. package/dist/neal/providers/detection.js +3 -7
  45. package/dist/neal/providers/git-config-isolation.js +83 -0
  46. package/dist/neal/providers/openai-codex.js +102 -204
  47. package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
  48. package/dist/neal/providers/openai-compatible.js +1079 -403
  49. package/dist/neal/providers/openrouter-routing.js +68 -0
  50. package/dist/neal/providers/pricing.js +124 -0
  51. package/dist/neal/providers/rate-card.js +2275 -0
  52. package/dist/neal/providers/registry.js +6 -17
  53. package/dist/neal/providers/telemetry.js +4 -0
  54. package/dist/neal/providers/types.js +12 -0
  55. package/dist/neal/resume-decision.js +7 -6
  56. package/dist/neal/resume-planner.js +9 -0
  57. package/dist/neal/retrospective.js +36 -7
  58. package/dist/neal/review-debt.js +30 -0
  59. package/dist/neal/review-findings/provider.js +17 -29
  60. package/dist/neal/review.js +17 -1
  61. package/dist/neal/run-metrics.js +74 -9
  62. package/dist/neal/state-invariants.js +11 -3
  63. package/dist/neal/state-views.js +27 -5
  64. package/dist/neal/state.js +59 -15
  65. package/dist/neal/status.js +28 -1
  66. package/dist/neal/support.js +3 -3
  67. package/docs/README.md +22 -18
  68. package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
  69. package/docs/architecture.md +79 -83
  70. package/docs/automation.md +11 -11
  71. package/docs/compat.md +91 -61
  72. package/docs/compatible-models.md +145 -110
  73. package/docs/demo.md +15 -5
  74. package/docs/maintenance.md +74 -32
  75. package/docs/plan-format.md +61 -33
  76. package/docs/prompt-evals.md +96 -0
  77. package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
  78. package/docs/providers.md +330 -283
  79. package/docs/release.md +43 -40
  80. package/docs/review-convergence.md +246 -0
  81. package/docs/state-machine.md +61 -59
  82. package/docs/storage.md +40 -31
  83. package/docs/troubleshooting.md +32 -29
  84. package/examples/compat/add-edit-verify/PLAN.md +2 -0
  85. package/examples/compat/add-edit-verify/broken.diff +2 -2
  86. package/examples/compat/add-edit-verify/good.diff +2 -2
  87. package/examples/compat/add-edit-verify/src/add.js +1 -1
  88. package/examples/compat/manifest.json +25 -29
  89. package/examples/compat/sum-grep-edit/PLAN.md +33 -0
  90. package/examples/compat/sum-grep-edit/broken.diff +12 -0
  91. package/examples/compat/sum-grep-edit/good.diff +12 -0
  92. package/examples/compat/sum-grep-edit/package.json +5 -0
  93. package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
  94. package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
  95. package/examples/issue-triage-js/README.md +0 -15
  96. package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
  97. package/examples/reviewer-eval/clean-doc/change.diff +9 -0
  98. package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
  99. package/examples/reviewer-eval/clean-extract/change.diff +13 -0
  100. package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
  101. package/examples/reviewer-eval/dropped-error/change.diff +15 -0
  102. package/examples/reviewer-eval/manifest.json +45 -0
  103. package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
  104. package/examples/reviewer-eval/off-by-one/change.diff +11 -0
  105. package/{config.yml → neal.yml} +1 -1
  106. package/package.json +20 -15
  107. package/dist/neal/providers/generic-agentic.js +0 -1238
  108. package/docs/comparison.md +0 -105
  109. package/examples/compat/is-even-add-test/PLAN.md +0 -30
  110. package/examples/compat/is-even-add-test/broken.diff +0 -11
  111. package/examples/compat/is-even-add-test/good.diff +0 -11
  112. package/examples/compat/is-even-add-test/package.json +0 -5
  113. package/examples/compat/is-even-add-test/src/is-even.js +0 -3
  114. package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
  115. package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
  116. package/examples/compat/reverse-grep-edit/broken.diff +0 -12
  117. package/examples/compat/reverse-grep-edit/good.diff +0 -12
  118. package/examples/compat/reverse-grep-edit/package.json +0 -5
  119. package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
  120. package/examples/compat/reverse-grep-edit/test/strings.test.js +0 -15
@@ -1,3 +1,4 @@
1
+ import { createHash } from 'node:crypto';
1
2
  function field(key, source, required, description) {
2
3
  return { key, source, required, description };
3
4
  }
@@ -36,8 +37,7 @@ const PLAN_REVIEWER_CONTEXT = context('PlanReviewerPromptContext', [
36
37
  field('parentPlanDoc', 'prompt_argument', false, 'Path to the parent plan when reviewing a derived plan.'),
37
38
  field('derivedFromScopeNumber', 'orchestrator_state', false, 'Parent scope number when reviewing a derived plan.'),
38
39
  field('repositoryState', 'repository_state', true, 'Current repository context and directly referenced companion docs.'),
39
- field('inlineContext', 'repository_state', false, "Neal-inlined plan document, parent plan, and review-history sections for reviewers without repository read access; only valid with the 'no-read' access mode."),
40
- field('accessMode', 'orchestrator_state', false, "Three-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute), 'read-only' (read tools only; no command execution, test runs, or scratch work), or 'no-read' (judge entirely from Neal-inlined context). When absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
40
+ field('accessMode', 'orchestrator_state', false, "Two-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute) or 'read-only' (read tools only; no command execution, test runs, or scratch work). Defaults to 'tool-access' when absent."),
41
41
  ]);
42
42
  const SCOPE_CODER_CONTEXT = context('ScopeCoderPromptContext', [
43
43
  field('planDoc', 'prompt_argument', true, 'Path to the active execute-mode plan.'),
@@ -65,8 +65,7 @@ const SCOPE_REVIEWER_CONTEXT = context('ScopeReviewerPromptContext', [
65
65
  field('recentHistorySummary', 'review_history', true, 'Accepted-scope history for the active parent objective.'),
66
66
  field('reviewMarkdownPath', 'run_artifact', true, 'Review artifact that carries prior findings and coder responses.'),
67
67
  field('scratchDir', 'run_artifact', true, 'Run-local reviewer scratch directory for temporary verification artifacts.'),
68
- field('inlineContext', 'repository_state', false, "Neal-inlined full diff, plan document, and review-history sections for reviewers without repository read access; only valid with the 'no-read' access mode."),
69
- field('accessMode', 'orchestrator_state', false, "Three-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute), 'read-only' (read tools only; no command execution, test runs, or scratch work), or 'no-read' (judge entirely from Neal-inlined context). When absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
68
+ field('accessMode', 'orchestrator_state', false, "Two-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute) or 'read-only' (read tools only; no command execution, test runs, or scratch work). Defaults to 'tool-access' when absent."),
70
69
  ]);
71
70
  const COMPLETION_CODER_CONTEXT = context('CompletionCoderPromptContext', [
72
71
  field('planDoc', 'prompt_argument', true, 'Path to the execute-mode plan being evaluated for final completion.'),
@@ -79,14 +78,32 @@ const COMPLETION_REVIEWER_CONTEXT = context('CompletionReviewerPromptContext', [
79
78
  field('summary', 'review_history', true, 'Coder-authored whole-plan completion summary under review.'),
80
79
  field('scratchDir', 'run_artifact', true, 'Run-local final-completion reviewer scratch directory for temporary verification artifacts.'),
81
80
  field('repositoryState', 'repository_state', true, 'Current repository state used to judge whole-plan completion.'),
82
- field('inlineContext', 'repository_state', false, "Neal-inlined aggregate diff (or evidence-gap statement) and plan-document sections for reviewers without repository read access; only valid with the 'no-read' access mode."),
83
- field('accessMode', 'orchestrator_state', false, "Three-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute), 'read-only' (read tools only; no command execution, test runs, or scratch work), or 'no-read' (judge entirely from Neal-inlined context). When absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
81
+ field('accessMode', 'orchestrator_state', false, "Two-way reviewer doctrine access mode derived from the reviewer provider's structured-advisor tool access: 'tool-access' (inspect and execute) or 'read-only' (read tools only; no command execution, test runs, or scratch work). Defaults to 'tool-access' when absent."),
82
+ ]);
83
+ const CONSULTANT_CONTEXT = context('ConsultantPromptContext', [
84
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason reported by the stalled coder or reviewer turn.'),
85
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context (plan content, open blocking findings, reviewer-round snapshots, or coder blocker plus changed files) the consultant judges entirely from.'),
84
86
  ]);
85
87
  export const PROMPT_SPECS = [
86
88
  {
87
89
  id: 'plan_author',
90
+ version: 3,
91
+ changelog: [
92
+ {
93
+ version: 1,
94
+ renderSha: '2e5600ef64885419b574cc0a04b684dc93f3efcc9ede65e4cacc8951a220d5cd',
95
+ },
96
+ {
97
+ version: 2,
98
+ renderSha: 'a9ebcafa81fa0f60812adcb7c7b68f999a6c45c1c7fa7b7a20911031d6b0fd9e',
99
+ },
100
+ {
101
+ version: 3,
102
+ renderSha: '51943007b2a334ee0c92d8314616fb882e9cea7b410b05c5aeda668c7c9e04dc',
103
+ },
104
+ ],
88
105
  role: 'coder',
89
- purpose: 'Author or revise Neal-executable plans without leaking planning-task scaffolding into final artifacts.',
106
+ purpose: 'Author or revise concise, human-reviewable Neal-executable plans at moderate-to-high-level implementation detail.',
90
107
  requiredContext: PLAN_AUTHOR_CONTEXT,
91
108
  schemaTarget: {
92
109
  kind: 'structured_json',
@@ -104,6 +121,8 @@ export const PROMPT_SPECS = [
104
121
  evaluationNotes: [
105
122
  'Render tests should assert the canonical Neal-executable execution-shape contract stays present.',
106
123
  'Fixture cases should cover single-scope vs multi-scope decisions and cleanup of planning-only scaffolding.',
124
+ 'Prompt tests should preserve repository grounding while rejecting exhaustive implementation inventories.',
125
+ 'Planner and response prompts should narrow self-imposed global verification guarantees unless they are necessary for the requested change to be correct.',
107
126
  ],
108
127
  firstMigrationPriority: 1,
109
128
  currentHome: 'src/neal/prompts',
@@ -156,8 +175,23 @@ export const PROMPT_SPECS = [
156
175
  },
157
176
  {
158
177
  id: 'plan_reviewer',
178
+ version: 3,
179
+ changelog: [
180
+ {
181
+ version: 1,
182
+ renderSha: 'c84e630d1463590eda463726a0529dab6277f1980a2c9c586a3fe3673e44d2ee',
183
+ },
184
+ {
185
+ version: 2,
186
+ renderSha: '43a6f77d767bb5a010203f37b2c1ac980f0d7c6e1eb104f038c33afcea97c658',
187
+ },
188
+ {
189
+ version: 3,
190
+ renderSha: 'fa0309ed0e394c690dfc3135da3c98a74dd5413ccb754fefa100171bf35b72b6',
191
+ },
192
+ ],
159
193
  role: 'reviewer',
160
- purpose: 'Review Neal-executable plans for execution-shape correctness, verification concreteness, and resume safety.',
194
+ purpose: 'Review human-reviewable Neal-executable plans for material approach, scope, verification, and resume-safety defects.',
161
195
  requiredContext: PLAN_REVIEWER_CONTEXT,
162
196
  schemaTarget: {
163
197
  kind: 'structured_json',
@@ -179,7 +213,8 @@ export const PROMPT_SPECS = [
179
213
  providerVariants: SHARED_PROVIDER_VARIANTS,
180
214
  evaluationNotes: [
181
215
  'Render tests should assert the reviewer prompt requires executionShape confirmation.',
182
- 'Render tests should assert shared adversarial plan-readiness posture and verification skepticism without adding implementation diff-review instructions.',
216
+ 'Render tests should assert independent material plan review without adding implementation diff-review instructions or exhaustive detail requirements.',
217
+ 'Reviewer tests should distinguish requested-change correctness from incomplete enforcement of broader guarantees introduced by the plan.',
183
218
  'Fixture cases should cover ordinary plans and derived plans.',
184
219
  ],
185
220
  firstMigrationPriority: 1,
@@ -202,8 +237,7 @@ export const PROMPT_SPECS = [
202
237
  field('planDoc', 'prompt_argument', true, 'Path to the plan artifact under review.'),
203
238
  field('round', 'orchestrator_state', true, 'Plan-review round number.'),
204
239
  field('reviewMarkdownPath', 'run_artifact', true, 'Review history artifact path.'),
205
- field('inlineContext', 'repository_state', false, "Optional Neal-inlined plan/review-history context; only valid with the 'no-read' access mode, whose prompt contains no repository-access instructions."),
206
- field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access', 'read-only', or 'no-read'); when absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
240
+ field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access' or 'read-only'); defaults to 'tool-access' when absent."),
207
241
  ]),
208
242
  },
209
243
  schemaTarget: {
@@ -226,8 +260,7 @@ export const PROMPT_SPECS = [
226
260
  field('planDoc', 'prompt_argument', true, 'Path to the derived plan artifact under review.'),
227
261
  field('parentPlanDoc', 'prompt_argument', false, 'Path to the parent plan artifact.'),
228
262
  field('derivedFromScopeNumber', 'orchestrator_state', false, 'Parent scope number that the derived plan replaces.'),
229
- field('inlineContext', 'repository_state', false, "Optional Neal-inlined derived-plan/parent-plan/review-history context; only valid with the 'no-read' access mode, whose prompt contains no repository-access instructions."),
230
- field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access', 'read-only', or 'no-read'); when absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
263
+ field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access' or 'read-only'); defaults to 'tool-access' when absent."),
231
264
  ]),
232
265
  },
233
266
  schemaTarget: {
@@ -241,6 +274,13 @@ export const PROMPT_SPECS = [
241
274
  },
242
275
  {
243
276
  id: 'scope_coder',
277
+ version: 1,
278
+ changelog: [
279
+ {
280
+ version: 1,
281
+ renderSha: 'c579d65db51be90fa81dbe3b7dd260083bc4960cc4cba32ccf2cd802d19703bc',
282
+ },
283
+ ],
244
284
  role: 'coder',
245
285
  purpose: 'Execute exactly one bounded implementation scope and respond to in-scope review feedback without starting new scopes.',
246
286
  requiredContext: SCOPE_CODER_CONTEXT,
@@ -343,6 +383,17 @@ export const PROMPT_SPECS = [
343
383
  },
344
384
  {
345
385
  id: 'scope_reviewer',
386
+ version: 2,
387
+ changelog: [
388
+ {
389
+ version: 1,
390
+ renderSha: '38a19da2b70dc10f6f4d8c4b955ece084bbe6e1291151de9c557dca194411386',
391
+ },
392
+ {
393
+ version: 2,
394
+ renderSha: '431a75ad341a531535606af607187239a31d12cf921575eb17774b317ad639a0',
395
+ },
396
+ ],
346
397
  role: 'reviewer',
347
398
  purpose: 'Review execute-scope results for correctness, verification coverage, and meaningful progress toward the active parent objective.',
348
399
  requiredContext: SCOPE_REVIEWER_CONTEXT,
@@ -396,8 +447,7 @@ export const PROMPT_SPECS = [
396
447
  field('reviewMarkdownPath', 'run_artifact', true, 'Review history artifact path.'),
397
448
  field('parentScopeLabel', 'orchestrator_state', true, 'Active parent objective label.'),
398
449
  field('scratchDir', 'run_artifact', true, 'Run-local scratch directory for reviewer verification artifacts.'),
399
- field('inlineContext', 'repository_state', false, "Optional Neal-inlined diff/plan/review-history context; only valid with the 'no-read' access mode, whose prompt contains no repository-access instructions."),
400
- field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access', 'read-only', or 'no-read'); when absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
450
+ field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access' or 'read-only'); defaults to 'tool-access' when absent."),
401
451
  ]),
402
452
  },
403
453
  schemaTarget: {
@@ -432,6 +482,13 @@ export const PROMPT_SPECS = [
432
482
  },
433
483
  {
434
484
  id: 'completion_coder',
485
+ version: 1,
486
+ changelog: [
487
+ {
488
+ version: 1,
489
+ renderSha: 'f88b43d206de28212ad7f6f3ae84576718934b0a5e36953e35afee89c4d7ce5e',
490
+ },
491
+ ],
435
492
  role: 'coder',
436
493
  purpose: 'Summarize whole-plan completion state in compact structured JSON.',
437
494
  requiredContext: COMPLETION_CODER_CONTEXT,
@@ -485,6 +542,17 @@ export const PROMPT_SPECS = [
485
542
  },
486
543
  {
487
544
  id: 'completion_reviewer',
545
+ version: 2,
546
+ changelog: [
547
+ {
548
+ version: 1,
549
+ renderSha: 'e5d104d46899f01cf69db9d2e6d109aa32d0faf645d6d908aae443558900a131',
550
+ },
551
+ {
552
+ version: 2,
553
+ renderSha: 'cc06cea4a8de6366f07a6943718fc1ae219e4c9092a5cf52542fa9de0caa02da',
554
+ },
555
+ ],
488
556
  role: 'reviewer',
489
557
  purpose: 'Judge whole-plan completion and decide whether Neal should accept completion, continue execution, or block for operator input.',
490
558
  requiredContext: COMPLETION_REVIEWER_CONTEXT,
@@ -529,8 +597,7 @@ export const PROMPT_SPECS = [
529
597
  field('packet', 'orchestrator_state', true, 'Whole-plan completion packet.'),
530
598
  field('summary', 'review_history', true, 'Coder-authored completion summary.'),
531
599
  field('scratchDir', 'run_artifact', true, 'Run-local scratch directory for final-completion reviewer artifacts.'),
532
- field('inlineContext', 'repository_state', false, "Optional Neal-inlined aggregate-diff/plan context; only valid with the 'no-read' access mode, whose prompt contains no repository-access instructions."),
533
- field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access', 'read-only', or 'no-read'); when absent the builder derives 'no-read' from inline-context presence, else 'tool-access'."),
600
+ field('accessMode', 'orchestrator_state', false, "Optional explicit doctrine access mode ('tool-access' or 'read-only'); defaults to 'tool-access' when absent."),
534
601
  ]),
535
602
  },
536
603
  schemaTarget: {
@@ -542,7 +609,145 @@ export const PROMPT_SPECS = [
542
609
  },
543
610
  ],
544
611
  },
612
+ {
613
+ id: 'consultant',
614
+ version: 1,
615
+ changelog: [
616
+ {
617
+ version: 1,
618
+ renderSha: '42e41552be1e8cceb0596a26f09892e13cc31adf07e58479d939d289c8b469e3',
619
+ },
620
+ ],
621
+ role: 'reviewer',
622
+ purpose: 'Triage a blocked Neal run entirely from Neal-inlined context and decide whether the block is an in-scope recoverable misunderstanding or a genuine wall that must escalate to a human.',
623
+ requiredContext: CONSULTANT_CONTEXT,
624
+ schemaTarget: {
625
+ kind: 'structured_json',
626
+ schemaBuilder: 'buildConsultantSchema',
627
+ parser: 'validateConsultantVerdictPayload',
628
+ providerSurface: 'neal_json_block_protocol',
629
+ },
630
+ baseInstructions: {
631
+ kind: 'builder',
632
+ modulePath: 'src/neal/agents/prompts.ts',
633
+ exportName: 'buildConsultantPrompt',
634
+ inputShape: context('BuildConsultantPromptArgs', [
635
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
636
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
637
+ ]),
638
+ },
639
+ providerVariants: SHARED_PROVIDER_VARIANTS,
640
+ evaluationNotes: [
641
+ 'Render tests should assert the consultant judges entirely from inlined context and its static instructions carry no repository-access phrasing.',
642
+ 'A golden render test pins the exact prompt bytes, including the ALL-CAPS emphasis lines.',
643
+ ],
644
+ firstMigrationPriority: 3,
645
+ currentHome: 'mixed',
646
+ ownershipNotes: [
647
+ 'Prompt spec owns the consultant instructions and required context only; anti-thrash guarding, recovery routing, and verdict persistence stay in src/neal/adjudicator/ outside the prompt-spec library.',
648
+ ],
649
+ variants: [
650
+ {
651
+ kind: 'primary',
652
+ status: 'adjacent',
653
+ description: 'Read-only blocked-run adjudication round.',
654
+ currentRoundEntrypoints: ['runConsultantRound'],
655
+ baseInstructions: {
656
+ kind: 'builder',
657
+ modulePath: 'src/neal/agents/prompts.ts',
658
+ exportName: 'buildConsultantPrompt',
659
+ inputShape: context('BuildConsultantPromptArgs', [
660
+ field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
661
+ field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
662
+ ]),
663
+ },
664
+ schemaTarget: {
665
+ kind: 'structured_json',
666
+ schemaBuilder: 'buildConsultantSchema',
667
+ parser: 'validateConsultantVerdictPayload',
668
+ providerSurface: 'neal_json_block_protocol',
669
+ },
670
+ },
671
+ ],
672
+ },
545
673
  ];
674
+ // SHA-256 of `text` as lowercase hex. Used to pin the render matrix (via the
675
+ // changelog `renderSha`) and, in the render-integrity test, the builder-module
676
+ // source tripwire.
677
+ export function sha256Hex(text) {
678
+ return createHash('sha256').update(text, 'utf8').digest('hex');
679
+ }
680
+ // Canonical, order-independent serialization of a render matrix. Each cell is a
681
+ // `{ key, render }` pair; cells are sorted by `key` and joined as
682
+ // `=== <key> ===\n<render>\n` blocks so the output is deterministic regardless
683
+ // of generation order and stays human-diffable in the versioned goldens.
684
+ export function serializeRenderMatrix(cells) {
685
+ return [...cells]
686
+ .sort((a, b) => (a.key < b.key ? -1 : a.key > b.key ? 1 : 0))
687
+ .map((cell) => `=== ${cell.key} ===\n${cell.render}\n`)
688
+ .join('');
689
+ }
690
+ // Structural validation of a spec's integer version and sha-anchored changelog.
691
+ // This is format/shape enforcement only; it does not compare the recorded
692
+ // `renderSha` against any live render (that is verifyRenderVersionContract's
693
+ // job) so it can run cheaply at module load for every spec.
694
+ export function validatePromptSpecVersioning(spec) {
695
+ const { id, version, changelog } = spec;
696
+ if (!Number.isSafeInteger(version) || version < 1) {
697
+ throw new Error(`Prompt spec ${id} version must be a safe integer >= 1, received ${String(version)}`);
698
+ }
699
+ if (changelog.length === 0) {
700
+ throw new Error(`Prompt spec ${id} changelog must not be empty`);
701
+ }
702
+ let previousVersion = 0;
703
+ for (const entry of changelog) {
704
+ if (!Number.isSafeInteger(entry.version) || entry.version < 1) {
705
+ throw new Error(`Prompt spec ${id} changelog entry version must be a safe integer >= 1, received ${String(entry.version)}`);
706
+ }
707
+ if (entry.version <= previousVersion) {
708
+ throw new Error(`Prompt spec ${id} changelog entry versions must be strictly increasing, received ${entry.version} after ${previousVersion}`);
709
+ }
710
+ previousVersion = entry.version;
711
+ if (!/^[0-9a-f]{64}$/.test(entry.renderSha)) {
712
+ throw new Error(`Prompt spec ${id} changelog entry renderSha must be 64-char lowercase hex, received ${entry.renderSha}`);
713
+ }
714
+ }
715
+ const last = changelog[changelog.length - 1];
716
+ if (last.version !== version) {
717
+ throw new Error(`Prompt spec ${id} last changelog entry version ${last.version} must equal spec version ${version}`);
718
+ }
719
+ }
720
+ // Internal-consistency and change-visibility contract over a spec's live render
721
+ // matrix and its versioned goldens. Throws when the last changelog entry does
722
+ // not match the spec version, when the live matrix sha does not match the last
723
+ // recorded `renderSha`, or when any changelog entry's golden is missing or does
724
+ // not hash to its recorded `renderSha`. This is not tamper evidence: an author
725
+ // who edits the live render, the golden, and the recorded sha together produces
726
+ // an internally consistent state that passes here; catching that coordinated
727
+ // same-version repin is the documented review/CI append-only rule, not this
728
+ // unit contract.
729
+ export function verifyRenderVersionContract(args) {
730
+ const { specId, version, changelog, liveMatrix, readGolden } = args;
731
+ const last = changelog[changelog.length - 1];
732
+ if (!last || last.version !== version) {
733
+ throw new Error(`Prompt spec ${specId} last changelog entry version ${String(last?.version)} must equal spec version ${version}`);
734
+ }
735
+ const liveSha = sha256Hex(liveMatrix);
736
+ if (liveSha !== last.renderSha) {
737
+ throw new Error(`Prompt spec ${specId} live render matrix sha ${liveSha} does not match recorded renderSha ${last.renderSha} for version ${version}. ` +
738
+ 'The render changed: re-audit the builder axes, bump the version, add a new versioned golden, and re-pin the recorded renderSha.');
739
+ }
740
+ for (const entry of changelog) {
741
+ const golden = readGolden(entry.version);
742
+ if (golden === undefined) {
743
+ throw new Error(`Prompt spec ${specId} is missing the versioned golden for version ${entry.version}`);
744
+ }
745
+ const goldenSha = sha256Hex(golden);
746
+ if (goldenSha !== entry.renderSha) {
747
+ throw new Error(`Prompt spec ${specId} golden for version ${entry.version} sha ${goldenSha} does not match recorded renderSha ${entry.renderSha}`);
748
+ }
749
+ }
750
+ }
546
751
  function getContractFieldKeys(contract) {
547
752
  return new Set(contract.fields.map((field) => field.key));
548
753
  }
@@ -555,6 +760,7 @@ function validateBuilderInputShape(spec, builder, label) {
555
760
  }
556
761
  function validatePromptSpecContracts(specs) {
557
762
  for (const spec of specs) {
763
+ validatePromptSpecVersioning(spec);
558
764
  validateBuilderInputShape(spec, spec.baseInstructions, 'baseInstructions');
559
765
  for (const variant of spec.variants) {
560
766
  validateBuilderInputShape(spec, variant.baseInstructions, `variant ${variant.kind}/${variant.baseInstructions.exportName}`);
@@ -0,0 +1,35 @@
1
+ /**
2
+ * Whether neal-driven agent runs should be isolated from the operator's
3
+ * interactive agent config.
4
+ *
5
+ * The native adapters drive the Codex and Claude SDKs, which by default load
6
+ * the operator's own config — the Codex `notify` hook, the Claude
7
+ * `Stop`/`Notification` hooks, `CLAUDE.md`, permissions. In normal use
8
+ * (pipeline, local `neal go`) that is correct: it is the operator's machine
9
+ * and their configured agent behavior, so neal honors it.
10
+ *
11
+ * The one exception is the compat qualification harness (`neal compat`, which
12
+ * `scripts/qualify-sdk.sh` drives). It is a hermetic, repeatable capability
13
+ * probe that runs the whole planner/coder/reviewer matrix — many turns — and
14
+ * must not fire the operator's per-turn notifier hooks or let ambient config
15
+ * skew the verdict. So `runCompat` turns isolation ON for its process, and the
16
+ * native adapters read it here.
17
+ *
18
+ * A process-level env flag (matching NEAL_STOP_AFTER_CURRENT_SCOPE_FILE /
19
+ * NEAL_GUIDANCE_DIR) rather than a threaded option, because compat drives the
20
+ * adapters through the full orchestrator, and the flag would otherwise have to
21
+ * cross every layer between the command and adapter construction.
22
+ */
23
+ const ISOLATION_ENV_FLAG = 'NEAL_ISOLATE_AGENT_SETTINGS';
24
+ export function enableAgentSettingsIsolation() {
25
+ process.env[ISOLATION_ENV_FLAG] = '1';
26
+ }
27
+ // Symmetric off switch. Production only ever enables (compat, once), but tests
28
+ // that exercise the isolated path need a clean teardown so the process-level
29
+ // flag does not leak into sibling tests.
30
+ export function disableAgentSettingsIsolation() {
31
+ delete process.env[ISOLATION_ENV_FLAG];
32
+ }
33
+ export function agentSettingsIsolated() {
34
+ return process.env[ISOLATION_ENV_FLAG] === '1';
35
+ }