@navels/neal 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +206 -209
- package/SECURITY.md +16 -19
- package/dist/neal/adjudicator/{blocked-adjudicator.js → consultant.js} +21 -21
- package/dist/neal/adjudicator/execute.js +33 -33
- package/dist/neal/adjudicator/final-completion.js +2 -31
- package/dist/neal/adjudicator/planning.js +230 -38
- package/dist/neal/agents/prompts.js +6 -17
- package/dist/neal/agents/rounds.js +43 -73
- package/dist/neal/agents/schemas.js +47 -13
- package/dist/neal/blocked-guidance.js +16 -9
- package/dist/neal/cli.js +2 -2
- package/dist/neal/commands/check.js +2 -2
- package/dist/neal/commands/compat.js +100 -36
- package/dist/neal/commands/interactive-activity.js +2 -2
- package/dist/neal/commands/recovery-guidance.js +20 -8
- package/dist/neal/commands/runtime.js +4 -2
- package/dist/neal/config.js +16 -7
- package/dist/neal/context/inline-review-context.js +13 -28
- package/dist/neal/context/reviewer-context.js +19 -14
- package/dist/neal/eval/reviewer-eval-manifest.js +86 -0
- package/dist/neal/eval/reviewer-eval.js +155 -0
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +49 -2
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +117 -80
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/transitions.js +7 -1
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/execute.js +7 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +7 -17
- package/dist/neal/prompts/specs.js +180 -32
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +18 -1
- package/dist/neal/providers/detection.js +3 -7
- package/dist/neal/providers/git-config-isolation.js +83 -0
- package/dist/neal/providers/openai-codex.js +25 -3
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1078 -430
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +1 -1
- package/dist/neal/providers/rate-card.js +6 -32
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/types.js +12 -0
- package/dist/neal/resume-decision.js +7 -6
- package/dist/neal/resume-planner.js +9 -0
- package/dist/neal/retrospective.js +3 -3
- package/dist/neal/review-debt.js +30 -0
- package/dist/neal/review-findings/provider.js +17 -29
- package/dist/neal/review.js +17 -1
- package/dist/neal/state-invariants.js +11 -3
- package/dist/neal/state-views.js +27 -5
- package/dist/neal/state.js +59 -15
- package/dist/neal/status.js +28 -1
- package/dist/neal/support.js +3 -3
- package/docs/README.md +22 -18
- package/docs/{ADJUDICATOR_INVENTORY.md → adjudicator-inventory.md} +58 -57
- package/docs/architecture.md +79 -83
- package/docs/automation.md +11 -11
- package/docs/compat.md +91 -61
- package/docs/compatible-models.md +145 -121
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +65 -34
- package/docs/plan-format.md +61 -33
- package/docs/prompt-evals.md +96 -0
- package/docs/{PROMPT_SPECS.md → prompt-specs.md} +52 -48
- package/docs/providers.md +279 -349
- package/docs/release.md +63 -81
- package/docs/review-convergence.md +246 -0
- package/docs/state-machine.md +61 -59
- package/docs/storage.md +40 -31
- package/docs/troubleshooting.md +32 -29
- package/examples/compat/add-edit-verify/PLAN.md +2 -0
- package/examples/compat/add-edit-verify/broken.diff +2 -2
- package/examples/compat/add-edit-verify/good.diff +2 -2
- package/examples/compat/add-edit-verify/src/add.js +1 -1
- package/examples/compat/manifest.json +25 -29
- package/examples/compat/sum-grep-edit/PLAN.md +33 -0
- package/examples/compat/sum-grep-edit/broken.diff +12 -0
- package/examples/compat/sum-grep-edit/good.diff +12 -0
- package/examples/compat/sum-grep-edit/package.json +5 -0
- package/examples/compat/sum-grep-edit/src/numbers.js +9 -0
- package/examples/compat/sum-grep-edit/test/numbers.test.js +15 -0
- package/examples/issue-triage-js/README.md +0 -15
- package/examples/reviewer-eval/clean-doc/base/src/clamp.ts +3 -0
- package/examples/reviewer-eval/clean-doc/change.diff +9 -0
- package/examples/reviewer-eval/clean-extract/base/src/greet.ts +3 -0
- package/examples/reviewer-eval/clean-extract/change.diff +13 -0
- package/examples/reviewer-eval/dropped-error/base/src/load.ts +4 -0
- package/examples/reviewer-eval/dropped-error/change.diff +15 -0
- package/examples/reviewer-eval/manifest.json +45 -0
- package/examples/reviewer-eval/off-by-one/base/src/paginate.ts +5 -0
- package/examples/reviewer-eval/off-by-one/change.diff +11 -0
- package/{config.yml → neal.yml} +1 -1
- package/package.json +15 -14
- package/dist/neal/providers/generic-agentic.js +0 -1256
- package/docs/comparison.md +0 -105
- package/docs/issue-pipeline.md +0 -124
- package/examples/compat/is-even-add-test/PLAN.md +0 -30
- package/examples/compat/is-even-add-test/broken.diff +0 -11
- package/examples/compat/is-even-add-test/good.diff +0 -11
- package/examples/compat/is-even-add-test/package.json +0 -5
- package/examples/compat/is-even-add-test/src/is-even.js +0 -3
- package/examples/compat/is-even-add-test/test/is-even.test.js +0 -9
- package/examples/compat/reverse-grep-edit/PLAN.md +0 -32
- package/examples/compat/reverse-grep-edit/broken.diff +0 -12
- package/examples/compat/reverse-grep-edit/good.diff +0 -12
- package/examples/compat/reverse-grep-edit/package.json +0 -5
- package/examples/compat/reverse-grep-edit/src/strings.js +0 -10
- 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('
|
|
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('
|
|
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,18 +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('
|
|
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."),
|
|
84
82
|
]);
|
|
85
|
-
const
|
|
83
|
+
const CONSULTANT_CONTEXT = context('ConsultantPromptContext', [
|
|
86
84
|
field('blockedReason', 'prompt_argument', true, 'Blocked reason reported by the stalled coder or reviewer turn.'),
|
|
87
|
-
field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context (plan content, open blocking findings, reviewer-round snapshots, or coder blocker plus changed files) the
|
|
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.'),
|
|
88
86
|
]);
|
|
89
87
|
export const PROMPT_SPECS = [
|
|
90
88
|
{
|
|
91
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
|
+
],
|
|
92
105
|
role: 'coder',
|
|
93
|
-
purpose: 'Author or revise Neal-executable plans
|
|
106
|
+
purpose: 'Author or revise concise, human-reviewable Neal-executable plans at moderate-to-high-level implementation detail.',
|
|
94
107
|
requiredContext: PLAN_AUTHOR_CONTEXT,
|
|
95
108
|
schemaTarget: {
|
|
96
109
|
kind: 'structured_json',
|
|
@@ -108,6 +121,8 @@ export const PROMPT_SPECS = [
|
|
|
108
121
|
evaluationNotes: [
|
|
109
122
|
'Render tests should assert the canonical Neal-executable execution-shape contract stays present.',
|
|
110
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.',
|
|
111
126
|
],
|
|
112
127
|
firstMigrationPriority: 1,
|
|
113
128
|
currentHome: 'src/neal/prompts',
|
|
@@ -160,8 +175,23 @@ export const PROMPT_SPECS = [
|
|
|
160
175
|
},
|
|
161
176
|
{
|
|
162
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
|
+
],
|
|
163
193
|
role: 'reviewer',
|
|
164
|
-
purpose: 'Review Neal-executable plans for
|
|
194
|
+
purpose: 'Review human-reviewable Neal-executable plans for material approach, scope, verification, and resume-safety defects.',
|
|
165
195
|
requiredContext: PLAN_REVIEWER_CONTEXT,
|
|
166
196
|
schemaTarget: {
|
|
167
197
|
kind: 'structured_json',
|
|
@@ -183,7 +213,8 @@ export const PROMPT_SPECS = [
|
|
|
183
213
|
providerVariants: SHARED_PROVIDER_VARIANTS,
|
|
184
214
|
evaluationNotes: [
|
|
185
215
|
'Render tests should assert the reviewer prompt requires executionShape confirmation.',
|
|
186
|
-
'Render tests should assert
|
|
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.',
|
|
187
218
|
'Fixture cases should cover ordinary plans and derived plans.',
|
|
188
219
|
],
|
|
189
220
|
firstMigrationPriority: 1,
|
|
@@ -206,8 +237,7 @@ export const PROMPT_SPECS = [
|
|
|
206
237
|
field('planDoc', 'prompt_argument', true, 'Path to the plan artifact under review.'),
|
|
207
238
|
field('round', 'orchestrator_state', true, 'Plan-review round number.'),
|
|
208
239
|
field('reviewMarkdownPath', 'run_artifact', true, 'Review history artifact path.'),
|
|
209
|
-
field('
|
|
210
|
-
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."),
|
|
211
241
|
]),
|
|
212
242
|
},
|
|
213
243
|
schemaTarget: {
|
|
@@ -230,8 +260,7 @@ export const PROMPT_SPECS = [
|
|
|
230
260
|
field('planDoc', 'prompt_argument', true, 'Path to the derived plan artifact under review.'),
|
|
231
261
|
field('parentPlanDoc', 'prompt_argument', false, 'Path to the parent plan artifact.'),
|
|
232
262
|
field('derivedFromScopeNumber', 'orchestrator_state', false, 'Parent scope number that the derived plan replaces.'),
|
|
233
|
-
field('
|
|
234
|
-
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."),
|
|
235
264
|
]),
|
|
236
265
|
},
|
|
237
266
|
schemaTarget: {
|
|
@@ -245,6 +274,13 @@ export const PROMPT_SPECS = [
|
|
|
245
274
|
},
|
|
246
275
|
{
|
|
247
276
|
id: 'scope_coder',
|
|
277
|
+
version: 1,
|
|
278
|
+
changelog: [
|
|
279
|
+
{
|
|
280
|
+
version: 1,
|
|
281
|
+
renderSha: 'c579d65db51be90fa81dbe3b7dd260083bc4960cc4cba32ccf2cd802d19703bc',
|
|
282
|
+
},
|
|
283
|
+
],
|
|
248
284
|
role: 'coder',
|
|
249
285
|
purpose: 'Execute exactly one bounded implementation scope and respond to in-scope review feedback without starting new scopes.',
|
|
250
286
|
requiredContext: SCOPE_CODER_CONTEXT,
|
|
@@ -347,6 +383,17 @@ export const PROMPT_SPECS = [
|
|
|
347
383
|
},
|
|
348
384
|
{
|
|
349
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
|
+
],
|
|
350
397
|
role: 'reviewer',
|
|
351
398
|
purpose: 'Review execute-scope results for correctness, verification coverage, and meaningful progress toward the active parent objective.',
|
|
352
399
|
requiredContext: SCOPE_REVIEWER_CONTEXT,
|
|
@@ -400,8 +447,7 @@ export const PROMPT_SPECS = [
|
|
|
400
447
|
field('reviewMarkdownPath', 'run_artifact', true, 'Review history artifact path.'),
|
|
401
448
|
field('parentScopeLabel', 'orchestrator_state', true, 'Active parent objective label.'),
|
|
402
449
|
field('scratchDir', 'run_artifact', true, 'Run-local scratch directory for reviewer verification artifacts.'),
|
|
403
|
-
field('
|
|
404
|
-
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."),
|
|
405
451
|
]),
|
|
406
452
|
},
|
|
407
453
|
schemaTarget: {
|
|
@@ -436,6 +482,13 @@ export const PROMPT_SPECS = [
|
|
|
436
482
|
},
|
|
437
483
|
{
|
|
438
484
|
id: 'completion_coder',
|
|
485
|
+
version: 1,
|
|
486
|
+
changelog: [
|
|
487
|
+
{
|
|
488
|
+
version: 1,
|
|
489
|
+
renderSha: 'f88b43d206de28212ad7f6f3ae84576718934b0a5e36953e35afee89c4d7ce5e',
|
|
490
|
+
},
|
|
491
|
+
],
|
|
439
492
|
role: 'coder',
|
|
440
493
|
purpose: 'Summarize whole-plan completion state in compact structured JSON.',
|
|
441
494
|
requiredContext: COMPLETION_CODER_CONTEXT,
|
|
@@ -489,6 +542,17 @@ export const PROMPT_SPECS = [
|
|
|
489
542
|
},
|
|
490
543
|
{
|
|
491
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
|
+
],
|
|
492
556
|
role: 'reviewer',
|
|
493
557
|
purpose: 'Judge whole-plan completion and decide whether Neal should accept completion, continue execution, or block for operator input.',
|
|
494
558
|
requiredContext: COMPLETION_REVIEWER_CONTEXT,
|
|
@@ -533,8 +597,7 @@ export const PROMPT_SPECS = [
|
|
|
533
597
|
field('packet', 'orchestrator_state', true, 'Whole-plan completion packet.'),
|
|
534
598
|
field('summary', 'review_history', true, 'Coder-authored completion summary.'),
|
|
535
599
|
field('scratchDir', 'run_artifact', true, 'Run-local scratch directory for final-completion reviewer artifacts.'),
|
|
536
|
-
field('
|
|
537
|
-
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."),
|
|
538
601
|
]),
|
|
539
602
|
},
|
|
540
603
|
schemaTarget: {
|
|
@@ -547,60 +610,144 @@ export const PROMPT_SPECS = [
|
|
|
547
610
|
],
|
|
548
611
|
},
|
|
549
612
|
{
|
|
550
|
-
id: '
|
|
613
|
+
id: 'consultant',
|
|
614
|
+
version: 1,
|
|
615
|
+
changelog: [
|
|
616
|
+
{
|
|
617
|
+
version: 1,
|
|
618
|
+
renderSha: '42e41552be1e8cceb0596a26f09892e13cc31adf07e58479d939d289c8b469e3',
|
|
619
|
+
},
|
|
620
|
+
],
|
|
551
621
|
role: 'reviewer',
|
|
552
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.',
|
|
553
|
-
requiredContext:
|
|
623
|
+
requiredContext: CONSULTANT_CONTEXT,
|
|
554
624
|
schemaTarget: {
|
|
555
625
|
kind: 'structured_json',
|
|
556
|
-
schemaBuilder: '
|
|
557
|
-
parser: '
|
|
626
|
+
schemaBuilder: 'buildConsultantSchema',
|
|
627
|
+
parser: 'validateConsultantVerdictPayload',
|
|
558
628
|
providerSurface: 'neal_json_block_protocol',
|
|
559
629
|
},
|
|
560
630
|
baseInstructions: {
|
|
561
631
|
kind: 'builder',
|
|
562
632
|
modulePath: 'src/neal/agents/prompts.ts',
|
|
563
|
-
exportName: '
|
|
564
|
-
inputShape: context('
|
|
633
|
+
exportName: 'buildConsultantPrompt',
|
|
634
|
+
inputShape: context('BuildConsultantPromptArgs', [
|
|
565
635
|
field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
|
|
566
636
|
field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
|
|
567
637
|
]),
|
|
568
638
|
},
|
|
569
639
|
providerVariants: SHARED_PROVIDER_VARIANTS,
|
|
570
640
|
evaluationNotes: [
|
|
571
|
-
'Render tests should assert the
|
|
641
|
+
'Render tests should assert the consultant judges entirely from inlined context and its static instructions carry no repository-access phrasing.',
|
|
572
642
|
'A golden render test pins the exact prompt bytes, including the ALL-CAPS emphasis lines.',
|
|
573
643
|
],
|
|
574
644
|
firstMigrationPriority: 3,
|
|
575
645
|
currentHome: 'mixed',
|
|
576
646
|
ownershipNotes: [
|
|
577
|
-
'Prompt spec owns the
|
|
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.',
|
|
578
648
|
],
|
|
579
649
|
variants: [
|
|
580
650
|
{
|
|
581
651
|
kind: 'primary',
|
|
582
652
|
status: 'adjacent',
|
|
583
653
|
description: 'Read-only blocked-run adjudication round.',
|
|
584
|
-
currentRoundEntrypoints: ['
|
|
654
|
+
currentRoundEntrypoints: ['runConsultantRound'],
|
|
585
655
|
baseInstructions: {
|
|
586
656
|
kind: 'builder',
|
|
587
657
|
modulePath: 'src/neal/agents/prompts.ts',
|
|
588
|
-
exportName: '
|
|
589
|
-
inputShape: context('
|
|
658
|
+
exportName: 'buildConsultantPrompt',
|
|
659
|
+
inputShape: context('BuildConsultantPromptArgs', [
|
|
590
660
|
field('blockedReason', 'prompt_argument', true, 'Blocked reason string.'),
|
|
591
661
|
field('inlineContext', 'repository_state', true, 'Neal-inlined adjudication context.'),
|
|
592
662
|
]),
|
|
593
663
|
},
|
|
594
664
|
schemaTarget: {
|
|
595
665
|
kind: 'structured_json',
|
|
596
|
-
schemaBuilder: '
|
|
597
|
-
parser: '
|
|
666
|
+
schemaBuilder: 'buildConsultantSchema',
|
|
667
|
+
parser: 'validateConsultantVerdictPayload',
|
|
598
668
|
providerSurface: 'neal_json_block_protocol',
|
|
599
669
|
},
|
|
600
670
|
},
|
|
601
671
|
],
|
|
602
672
|
},
|
|
603
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
|
+
}
|
|
604
751
|
function getContractFieldKeys(contract) {
|
|
605
752
|
return new Set(contract.fields.map((field) => field.key));
|
|
606
753
|
}
|
|
@@ -613,6 +760,7 @@ function validateBuilderInputShape(spec, builder, label) {
|
|
|
613
760
|
}
|
|
614
761
|
function validatePromptSpecContracts(specs) {
|
|
615
762
|
for (const spec of specs) {
|
|
763
|
+
validatePromptSpecVersioning(spec);
|
|
616
764
|
validateBuilderInputShape(spec, spec.baseInstructions, 'baseInstructions');
|
|
617
765
|
for (const variant of spec.variants) {
|
|
618
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
|
+
}
|
|
@@ -2,6 +2,8 @@ import { createRequire } from 'node:module';
|
|
|
2
2
|
import { resolve } from 'node:path';
|
|
3
3
|
import { query, } from '@anthropic-ai/claude-agent-sdk';
|
|
4
4
|
import { buildStructuredJsonPrompt, runStructuredJsonProtocol, runStructuredJsonRepairLoop, validateStructuredJsonPayload, } from '../agents/structured-json.js';
|
|
5
|
+
import { agentSettingsIsolated } from './agent-settings-isolation.js';
|
|
6
|
+
import { agentSubprocessEnv } from './git-config-isolation.js';
|
|
5
7
|
import { NealProviderError } from './types.js';
|
|
6
8
|
const ANTHROPIC_CLAUDE_PROVIDER_ID = 'anthropic-claude';
|
|
7
9
|
const requireFromProvider = createRequire(import.meta.url);
|
|
@@ -57,7 +59,7 @@ class ClaudeInactivityTimeoutError extends Error {
|
|
|
57
59
|
}
|
|
58
60
|
// In-round transient (`api_error`) retries back off exponentially: 500 ms
|
|
59
61
|
// base, doubling per retry, capped at 5 s — the same schedule as the
|
|
60
|
-
//
|
|
62
|
+
// openai-compatible adapter. `sleep` is injectable through the run functions so
|
|
61
63
|
// tests observe the delays without wall-clock waiting.
|
|
62
64
|
const API_RETRY_BASE_DELAY_MS = 500;
|
|
63
65
|
const API_RETRY_MAX_DELAY_MS = 5_000;
|
|
@@ -602,11 +604,26 @@ function deriveClaudeAbortController(signal) {
|
|
|
602
604
|
function buildClaudeCoreQueryOptions(spec) {
|
|
603
605
|
return {
|
|
604
606
|
cwd: spec.cwd,
|
|
607
|
+
// Full environment plus the git-config isolation overrides: Claude's Bash
|
|
608
|
+
// tool must not be able to write the operator's real global gitconfig.
|
|
609
|
+
// See git-config-isolation.ts.
|
|
610
|
+
env: agentSubprocessEnv(),
|
|
605
611
|
...(spec.abortController ? { abortController: spec.abortController } : {}),
|
|
606
612
|
...(spec.model ? { model: spec.model } : {}),
|
|
607
613
|
...(spec.effort ? { effort: spec.effort } : {}),
|
|
608
614
|
tools: spec.tools,
|
|
609
615
|
...(spec.hooks ? { hooks: spec.hooks } : {}),
|
|
616
|
+
// Under compat qualification only, run Claude in the SDK's isolation mode
|
|
617
|
+
// (load no filesystem settings). Left unset, the SDK loads ~/.claude and any
|
|
618
|
+
// repo .claude by default, pulling in the operator's interactive config —
|
|
619
|
+
// permissions, statusline, CLAUDE.md, and the Stop / Notification hooks that
|
|
620
|
+
// fire a desktop notifier on every turn. compat runs the whole role matrix,
|
|
621
|
+
// so those hooks would spam one notification per turn; isolating keeps the
|
|
622
|
+
// capability probe quiet and hermetic. Normal neal runs (pipeline, local
|
|
623
|
+
// `neal go`) honor the operator's config — it's their machine. neal's own
|
|
624
|
+
// hooks (the coder write-path guard passed programmatically above) are
|
|
625
|
+
// unaffected either way. See agent-settings-isolation.ts.
|
|
626
|
+
...(agentSettingsIsolated() ? { settingSources: [] } : {}),
|
|
610
627
|
permissionMode: 'bypassPermissions',
|
|
611
628
|
allowDangerouslySkipPermissions: true,
|
|
612
629
|
...(spec.resumeHandle ? { resume: spec.resumeHandle } : {}),
|
|
@@ -92,10 +92,9 @@ export function detectBuiltInProviders(options = {}) {
|
|
|
92
92
|
cwd: options.cwd ?? process.cwd(),
|
|
93
93
|
resolveOpenAICompatibleSettings: options.resolveOpenAICompatibleSettings ?? ((cwd) => getOpenAICompatibleSettings(cwd, env)),
|
|
94
94
|
};
|
|
95
|
-
//
|
|
96
|
-
// (providers.openai_compatible + env fallbacks)
|
|
97
|
-
//
|
|
98
|
-
// runs at most once per detection pass.
|
|
95
|
+
// openai-compatible resolves from the shared settings surface
|
|
96
|
+
// (providers.openai_compatible + env fallbacks). Memoized so the settings
|
|
97
|
+
// resolver still runs at most once per detection pass.
|
|
99
98
|
let sharedOpenAICompatibleDetection;
|
|
100
99
|
const detectFromSharedOpenAICompatibleSettings = () => {
|
|
101
100
|
sharedOpenAICompatibleDetection ??= detectOpenAICompatible(resolvedOptions);
|
|
@@ -119,9 +118,6 @@ export function detectBuiltInProviders(options = {}) {
|
|
|
119
118
|
case 'openai-compatible':
|
|
120
119
|
detection = detectFromSharedOpenAICompatibleSettings();
|
|
121
120
|
break;
|
|
122
|
-
case 'generic-agentic':
|
|
123
|
-
detection = detectFromSharedOpenAICompatibleSettings();
|
|
124
|
-
break;
|
|
125
121
|
default:
|
|
126
122
|
detection = {
|
|
127
123
|
runtimeAvailable: false,
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git-config isolation for agent subprocesses.
|
|
3
|
+
*
|
|
4
|
+
* Agents (the coder especially) run shell commands with the operator's real
|
|
5
|
+
* environment, so `git config --global ...` writes the operator's actual
|
|
6
|
+
* `~/.gitconfig`. That has happened twice: a coder model hit git identity
|
|
7
|
+
* friction inside a throwaway worktree and "fixed" it globally, clobbering the
|
|
8
|
+
* operator's `[user]` block and breaking commit signing on their machine
|
|
9
|
+
* (issue #54).
|
|
10
|
+
*
|
|
11
|
+
* The defense is environmental, not behavioral: every agent subprocess gets
|
|
12
|
+
* `GIT_CONFIG_GLOBAL` pointed at a neal-owned scratch copy of the operator's
|
|
13
|
+
* global config, and `GIT_CONFIG_SYSTEM` pointed at the null device. Reads
|
|
14
|
+
* behave identically (the copy has the operator's settings, and `includeIf`
|
|
15
|
+
* paths inside it still resolve), agent commits keep the operator's identity
|
|
16
|
+
* and signing config, and a `--global` write succeeds from the agent's point
|
|
17
|
+
* of view — it just lands in the scratch copy instead of the operator's file.
|
|
18
|
+
*
|
|
19
|
+
* One scratch copy per process, created lazily: adapters are constructed per
|
|
20
|
+
* round, and the copy must be stable across rounds so an agent's own global
|
|
21
|
+
* writes persist for the life of the run.
|
|
22
|
+
*/
|
|
23
|
+
import { copyFileSync, mkdtempSync, writeFileSync } from 'node:fs';
|
|
24
|
+
import { devNull, homedir, tmpdir } from 'node:os';
|
|
25
|
+
import { join } from 'node:path';
|
|
26
|
+
import process from 'node:process';
|
|
27
|
+
let cached = null;
|
|
28
|
+
/** Candidate sources for the operator's global config, in git's own precedence. */
|
|
29
|
+
function globalConfigSourcePaths() {
|
|
30
|
+
const fromEnv = process.env.GIT_CONFIG_GLOBAL;
|
|
31
|
+
if (fromEnv !== undefined && fromEnv !== '') {
|
|
32
|
+
return [fromEnv];
|
|
33
|
+
}
|
|
34
|
+
const xdgBase = process.env.XDG_CONFIG_HOME !== undefined && process.env.XDG_CONFIG_HOME !== ''
|
|
35
|
+
? process.env.XDG_CONFIG_HOME
|
|
36
|
+
: join(homedir(), '.config');
|
|
37
|
+
// ~/.gitconfig wins over the XDG file when both exist, matching git.
|
|
38
|
+
return [join(homedir(), '.gitconfig'), join(xdgBase, 'git', 'config')];
|
|
39
|
+
}
|
|
40
|
+
export function gitConfigIsolationEnv() {
|
|
41
|
+
if (cached !== null) {
|
|
42
|
+
return cached;
|
|
43
|
+
}
|
|
44
|
+
const dir = mkdtempSync(join(tmpdir(), 'neal-git-config-'));
|
|
45
|
+
const scratch = join(dir, 'gitconfig');
|
|
46
|
+
let copied = false;
|
|
47
|
+
for (const source of globalConfigSourcePaths()) {
|
|
48
|
+
try {
|
|
49
|
+
copyFileSync(source, scratch);
|
|
50
|
+
copied = true;
|
|
51
|
+
break;
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
// Missing source — try the next candidate.
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
if (!copied) {
|
|
58
|
+
// No global config on this machine; give agents an empty one to write to.
|
|
59
|
+
writeFileSync(scratch, '');
|
|
60
|
+
}
|
|
61
|
+
cached = { GIT_CONFIG_GLOBAL: scratch, GIT_CONFIG_SYSTEM: devNull };
|
|
62
|
+
return cached;
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* The full environment for an agent subprocess whose SDK replaces (rather
|
|
66
|
+
* than merges with) the parent environment when `env` is provided.
|
|
67
|
+
* `undefined` entries are dropped because those SDKs type env as
|
|
68
|
+
* `Record<string, string>`.
|
|
69
|
+
*/
|
|
70
|
+
export function agentSubprocessEnv() {
|
|
71
|
+
const env = {};
|
|
72
|
+
for (const [key, value] of Object.entries(process.env)) {
|
|
73
|
+
if (value !== undefined) {
|
|
74
|
+
env[key] = value;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
return { ...env, ...gitConfigIsolationEnv() };
|
|
78
|
+
}
|
|
79
|
+
// Test-only: drop the process-level cache so a test can point the source at a
|
|
80
|
+
// fixture file. Mirrors disableAgentSettingsIsolation's teardown role.
|
|
81
|
+
export function resetGitConfigIsolationForTests() {
|
|
82
|
+
cached = null;
|
|
83
|
+
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { Codex } from '@openai/codex-sdk';
|
|
2
2
|
import { runStructuredJsonProtocol } from '../agents/structured-json.js';
|
|
3
|
+
import { agentSettingsIsolated } from './agent-settings-isolation.js';
|
|
4
|
+
import { agentSubprocessEnv } from './git-config-isolation.js';
|
|
3
5
|
import { resolveRateCost } from './pricing.js';
|
|
4
|
-
import { NealProviderError } from './types.js';
|
|
6
|
+
import { isContentSafetyRefusalMessage, NealProviderError } from './types.js';
|
|
5
7
|
const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
6
8
|
class CodexInactivityTimeoutError extends Error {
|
|
7
9
|
constructor(timeoutMs) {
|
|
@@ -11,7 +13,7 @@ class CodexInactivityTimeoutError extends Error {
|
|
|
11
13
|
}
|
|
12
14
|
// In-round transient (`api_error`) retries back off exponentially: 500 ms
|
|
13
15
|
// base, doubling per retry, capped at 5 s — the same schedule as the
|
|
14
|
-
//
|
|
16
|
+
// openai-compatible adapter. `sleep` is injectable through the adapter options
|
|
15
17
|
// so tests observe the delays without wall-clock waiting.
|
|
16
18
|
const API_RETRY_BASE_DELAY_MS = 500;
|
|
17
19
|
const API_RETRY_MAX_DELAY_MS = 5_000;
|
|
@@ -68,6 +70,12 @@ function inferCodexErrorKind(error, message, fallback) {
|
|
|
68
70
|
if (error instanceof CodexInactivityTimeoutError || /\btimed out after\b/i.test(message)) {
|
|
69
71
|
return 'timeout';
|
|
70
72
|
}
|
|
73
|
+
// Placed before the permission check so a content-safety refusal is never
|
|
74
|
+
// misread as permission_denied when OpenAI phrases it with the word
|
|
75
|
+
// "authorized".
|
|
76
|
+
if (isContentSafetyRefusalMessage(message)) {
|
|
77
|
+
return 'content_refused';
|
|
78
|
+
}
|
|
71
79
|
if (text.includes('permission') || text.includes('denied') || text.includes('forbidden') || text.includes('not authorized')) {
|
|
72
80
|
return 'permission_denied';
|
|
73
81
|
}
|
|
@@ -229,7 +237,21 @@ function buildCodexThreadOptions(args) {
|
|
|
229
237
|
};
|
|
230
238
|
}
|
|
231
239
|
function createCodexThread(args) {
|
|
232
|
-
|
|
240
|
+
// Under compat qualification only, isolate from the user's interactive
|
|
241
|
+
// `notify` hook in ~/.codex/config.toml: compat runs the whole role matrix
|
|
242
|
+
// through Codex, and left alone the hook fires a desktop notification per
|
|
243
|
+
// planner/coder/reviewer turn. Override `notify` to empty (a `--config
|
|
244
|
+
// notify=[]` pass to the CLI) for that case. Normal neal runs (pipeline,
|
|
245
|
+
// local `neal go`) honor the user's config. See agent-settings-isolation.ts.
|
|
246
|
+
//
|
|
247
|
+
// `env` is always passed (the SDK stops inheriting process.env once env is
|
|
248
|
+
// provided, so it is the full environment plus the git-config isolation
|
|
249
|
+
// overrides): the Codex CLI and every shell it runs must not be able to
|
|
250
|
+
// write the operator's real global gitconfig. See git-config-isolation.ts.
|
|
251
|
+
const env = agentSubprocessEnv();
|
|
252
|
+
const codex = agentSettingsIsolated()
|
|
253
|
+
? new Codex({ config: { notify: [] }, env })
|
|
254
|
+
: new Codex({ env });
|
|
233
255
|
const threadOptions = buildCodexThreadOptions({
|
|
234
256
|
cwd: args.cwd,
|
|
235
257
|
model: args.model,
|