@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.
- 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 +264 -24
- package/dist/neal/agents/prompts.js +7 -15
- package/dist/neal/agents/rounds.js +51 -73
- package/dist/neal/agents/schemas.js +615 -502
- package/dist/neal/agents/structured-json.js +36 -0
- 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 +40 -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/git.js +9 -3
- package/dist/neal/interactive-controls.js +5 -0
- package/dist/neal/orchestrator/completion.js +215 -113
- package/dist/neal/orchestrator/failures.js +7 -0
- package/dist/neal/orchestrator/notifications.js +9 -9
- package/dist/neal/orchestrator/phases/planning.js +124 -112
- package/dist/neal/orchestrator/phases/recovery.js +151 -115
- package/dist/neal/orchestrator/phases/review.js +28 -1
- package/dist/neal/orchestrator/split-plan.js +12 -11
- package/dist/neal/orchestrator/transitions.js +36 -71
- package/dist/neal/orchestrator.js +3 -0
- package/dist/neal/plan-doc.js +24 -1
- package/dist/neal/plan-queue.js +29 -1
- package/dist/neal/prompts/assert-builder.js +8 -1
- package/dist/neal/prompts/execute.js +11 -19
- package/dist/neal/prompts/planning.js +64 -55
- package/dist/neal/prompts/review-doctrine.js +13 -50
- package/dist/neal/prompts/specialized.js +28 -22
- package/dist/neal/prompts/specs.js +223 -17
- package/dist/neal/providers/agent-settings-isolation.js +35 -0
- package/dist/neal/providers/anthropic-claude.js +309 -248
- 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 +102 -204
- package/dist/neal/providers/{generic-agentic-tools.js → openai-compatible-tools.js} +11 -5
- package/dist/neal/providers/openai-compatible.js +1079 -403
- package/dist/neal/providers/openrouter-routing.js +68 -0
- package/dist/neal/providers/pricing.js +124 -0
- package/dist/neal/providers/rate-card.js +2275 -0
- package/dist/neal/providers/registry.js +6 -17
- package/dist/neal/providers/telemetry.js +4 -0
- 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 +36 -7
- 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/run-metrics.js +74 -9
- 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 -110
- package/docs/demo.md +15 -5
- package/docs/maintenance.md +74 -32
- 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 +330 -283
- package/docs/release.md +43 -40
- 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 +20 -15
- package/dist/neal/providers/generic-agentic.js +0 -1238
- package/docs/comparison.md +0 -105
- 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
|
@@ -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,6 +1,9 @@
|
|
|
1
1
|
import { Codex } from '@openai/codex-sdk';
|
|
2
2
|
import { runStructuredJsonProtocol } from '../agents/structured-json.js';
|
|
3
|
-
import {
|
|
3
|
+
import { agentSettingsIsolated } from './agent-settings-isolation.js';
|
|
4
|
+
import { agentSubprocessEnv } from './git-config-isolation.js';
|
|
5
|
+
import { resolveRateCost } from './pricing.js';
|
|
6
|
+
import { isContentSafetyRefusalMessage, NealProviderError } from './types.js';
|
|
4
7
|
const OPENAI_CODEX_PROVIDER_ID = 'openai-codex';
|
|
5
8
|
class CodexInactivityTimeoutError extends Error {
|
|
6
9
|
constructor(timeoutMs) {
|
|
@@ -10,7 +13,7 @@ class CodexInactivityTimeoutError extends Error {
|
|
|
10
13
|
}
|
|
11
14
|
// In-round transient (`api_error`) retries back off exponentially: 500 ms
|
|
12
15
|
// base, doubling per retry, capped at 5 s — the same schedule as the
|
|
13
|
-
//
|
|
16
|
+
// openai-compatible adapter. `sleep` is injectable through the adapter options
|
|
14
17
|
// so tests observe the delays without wall-clock waiting.
|
|
15
18
|
const API_RETRY_BASE_DELAY_MS = 500;
|
|
16
19
|
const API_RETRY_MAX_DELAY_MS = 5_000;
|
|
@@ -67,6 +70,12 @@ function inferCodexErrorKind(error, message, fallback) {
|
|
|
67
70
|
if (error instanceof CodexInactivityTimeoutError || /\btimed out after\b/i.test(message)) {
|
|
68
71
|
return 'timeout';
|
|
69
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
|
+
}
|
|
70
79
|
if (text.includes('permission') || text.includes('denied') || text.includes('forbidden') || text.includes('not authorized')) {
|
|
71
80
|
return 'permission_denied';
|
|
72
81
|
}
|
|
@@ -228,7 +237,21 @@ function buildCodexThreadOptions(args) {
|
|
|
228
237
|
};
|
|
229
238
|
}
|
|
230
239
|
function createCodexThread(args) {
|
|
231
|
-
|
|
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 });
|
|
232
255
|
const threadOptions = buildCodexThreadOptions({
|
|
233
256
|
cwd: args.cwd,
|
|
234
257
|
model: args.model,
|
|
@@ -269,7 +292,17 @@ async function emitCodexStreamError(args) {
|
|
|
269
292
|
});
|
|
270
293
|
return true;
|
|
271
294
|
}
|
|
272
|
-
|
|
295
|
+
// Shared turn consumer behind `consumeCodexTurn` (coder) and
|
|
296
|
+
// `consumeCodexAdvisorTurn` (structured advisor). The two paths differ only
|
|
297
|
+
// in the role stamped on emitted events and normalized errors, the
|
|
298
|
+
// advisor-only `label` stamp, and the coder-only `onSessionStarted`
|
|
299
|
+
// callback; everything else is identical.
|
|
300
|
+
async function consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, options) {
|
|
301
|
+
const { role, label, events, onSessionStarted, abortController, model } = options;
|
|
302
|
+
// The advisor path stamps `label` on every emitted event while the coder
|
|
303
|
+
// path emits events without a `label` property at all — the conditional
|
|
304
|
+
// spread preserves that own-property distinction exactly.
|
|
305
|
+
const labelFields = label === undefined ? {} : { label };
|
|
273
306
|
let finalResponse = '';
|
|
274
307
|
let observedFailure = null;
|
|
275
308
|
let sessionHandle = null;
|
|
@@ -282,13 +315,13 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
282
315
|
catch (error) {
|
|
283
316
|
if (observedFailure) {
|
|
284
317
|
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
285
|
-
role
|
|
318
|
+
role,
|
|
286
319
|
sessionHandle,
|
|
287
320
|
cause: error,
|
|
288
321
|
});
|
|
289
322
|
}
|
|
290
323
|
throw normalizeCodexProviderError(error, {
|
|
291
|
-
role
|
|
324
|
+
role,
|
|
292
325
|
sessionHandle,
|
|
293
326
|
});
|
|
294
327
|
}
|
|
@@ -302,7 +335,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
302
335
|
await emitProviderEvent(events, {
|
|
303
336
|
type: 'session_started',
|
|
304
337
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
305
|
-
role
|
|
338
|
+
role,
|
|
339
|
+
...labelFields,
|
|
306
340
|
sessionHandle: event.thread_id,
|
|
307
341
|
providerData: { sdkEventType: event.type },
|
|
308
342
|
});
|
|
@@ -312,7 +346,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
312
346
|
await emitProviderEvent(events, {
|
|
313
347
|
type: 'turn_started',
|
|
314
348
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
315
|
-
role
|
|
349
|
+
role,
|
|
350
|
+
...labelFields,
|
|
316
351
|
sessionHandle,
|
|
317
352
|
providerData: { sdkEventType: event.type },
|
|
318
353
|
});
|
|
@@ -321,7 +356,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
321
356
|
await emitProviderEvent(events, {
|
|
322
357
|
type: 'tool_started',
|
|
323
358
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
324
|
-
role
|
|
359
|
+
role,
|
|
360
|
+
...labelFields,
|
|
325
361
|
sessionHandle,
|
|
326
362
|
itemId: summarizeItem(event.item).itemId,
|
|
327
363
|
toolName: event.item.type,
|
|
@@ -332,7 +368,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
332
368
|
await emitProviderEvent(events, {
|
|
333
369
|
type: 'tool_progress',
|
|
334
370
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
335
|
-
role
|
|
371
|
+
role,
|
|
372
|
+
...labelFields,
|
|
336
373
|
sessionHandle,
|
|
337
374
|
itemId: summarizeItem(event.item).itemId,
|
|
338
375
|
toolName: event.item.type,
|
|
@@ -344,7 +381,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
344
381
|
await emitProviderEvent(events, {
|
|
345
382
|
type: 'command_completed',
|
|
346
383
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
347
|
-
role
|
|
384
|
+
role,
|
|
385
|
+
...labelFields,
|
|
348
386
|
sessionHandle,
|
|
349
387
|
itemId: event.item.id,
|
|
350
388
|
command: event.item.command,
|
|
@@ -361,7 +399,8 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
361
399
|
await emitProviderEvent(events, {
|
|
362
400
|
type: 'file_changed',
|
|
363
401
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
364
|
-
role
|
|
402
|
+
role,
|
|
403
|
+
...labelFields,
|
|
365
404
|
sessionHandle,
|
|
366
405
|
files,
|
|
367
406
|
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
@@ -371,206 +410,41 @@ async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessio
|
|
|
371
410
|
finalResponse = event.item.text;
|
|
372
411
|
}
|
|
373
412
|
break;
|
|
374
|
-
case 'turn.completed':
|
|
413
|
+
case 'turn.completed': {
|
|
414
|
+
// Card-derived cost only when the turn actually carried usage. Codex
|
|
415
|
+
// advertises opportunistic usage reporting, and `computeRateCostUsd`
|
|
416
|
+
// returns 0 for absent usage, so gating on `event.usage !== undefined`
|
|
417
|
+
// (matching the usage_reported emit) keeps a usage-less turn's
|
|
418
|
+
// tokens-only shape byte-stable rather than attaching costUsd: 0. Codex
|
|
419
|
+
// has no config pricing, so cost comes from the card via the configured
|
|
420
|
+
// model only.
|
|
421
|
+
const cost = event.usage !== undefined
|
|
422
|
+
? (resolveRateCost({ usage: event.usage, model: model ?? null, configPricing: null }) ?? {})
|
|
423
|
+
: {};
|
|
375
424
|
await emitProviderEvent(events, {
|
|
376
425
|
type: 'turn_completed',
|
|
377
426
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
378
|
-
role
|
|
427
|
+
role,
|
|
428
|
+
...labelFields,
|
|
379
429
|
sessionHandle,
|
|
380
430
|
usage: event.usage,
|
|
431
|
+
...cost,
|
|
381
432
|
providerData: { sdkEventType: event.type },
|
|
382
433
|
});
|
|
383
434
|
if (event.usage !== undefined) {
|
|
384
435
|
await emitProviderEvent(events, {
|
|
385
436
|
type: 'usage_reported',
|
|
386
437
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
387
|
-
role
|
|
438
|
+
role,
|
|
439
|
+
...labelFields,
|
|
388
440
|
sessionHandle,
|
|
389
441
|
usage: event.usage,
|
|
442
|
+
...cost,
|
|
390
443
|
providerData: { sdkEventType: event.type },
|
|
391
444
|
});
|
|
392
445
|
}
|
|
393
446
|
break;
|
|
394
|
-
case 'turn.failed':
|
|
395
|
-
{
|
|
396
|
-
const nextFailure = observeCodexFailure({
|
|
397
|
-
message: event.error.message,
|
|
398
|
-
source: event.type,
|
|
399
|
-
cause: event.error,
|
|
400
|
-
});
|
|
401
|
-
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
402
|
-
observedFailure = nextFailure;
|
|
403
|
-
await emitProviderEvent(events, {
|
|
404
|
-
type: 'provider_error',
|
|
405
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
406
|
-
role: 'coder',
|
|
407
|
-
sessionHandle,
|
|
408
|
-
message: event.error.message,
|
|
409
|
-
providerData: { sdkEventType: event.type },
|
|
410
|
-
});
|
|
411
|
-
}
|
|
412
|
-
}
|
|
413
|
-
break;
|
|
414
|
-
case 'error':
|
|
415
|
-
{
|
|
416
|
-
const nextFailure = observeCodexFailure({
|
|
417
|
-
message: event.message,
|
|
418
|
-
source: event.type,
|
|
419
|
-
cause: event,
|
|
420
|
-
});
|
|
421
|
-
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
422
|
-
if (await emitCodexStreamError({
|
|
423
|
-
events,
|
|
424
|
-
role: 'coder',
|
|
425
|
-
sessionHandle,
|
|
426
|
-
message: event.message,
|
|
427
|
-
})) {
|
|
428
|
-
observedFailure = nextFailure;
|
|
429
|
-
}
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
break;
|
|
433
|
-
default:
|
|
434
|
-
break;
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
if (observedFailure) {
|
|
438
|
-
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
439
|
-
role: 'coder',
|
|
440
|
-
sessionHandle,
|
|
441
|
-
});
|
|
442
|
-
}
|
|
443
|
-
return { finalResponse, sessionHandle };
|
|
444
|
-
}
|
|
445
|
-
async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, events, abortController) {
|
|
446
|
-
let finalResponse = '';
|
|
447
|
-
let observedFailure = null;
|
|
448
|
-
let sessionHandle = null;
|
|
449
|
-
const iterator = turn.events[Symbol.asyncIterator]();
|
|
450
|
-
while (true) {
|
|
451
|
-
let next;
|
|
452
|
-
try {
|
|
453
|
-
next = await nextWithTimeout(iterator.next(), inactivityTimeoutMs, () => abortController?.abort());
|
|
454
|
-
}
|
|
455
|
-
catch (error) {
|
|
456
|
-
if (observedFailure) {
|
|
457
|
-
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
458
|
-
role: 'structured-advisor',
|
|
459
|
-
sessionHandle,
|
|
460
|
-
cause: error,
|
|
461
|
-
});
|
|
462
447
|
}
|
|
463
|
-
throw normalizeCodexProviderError(error, {
|
|
464
|
-
role: 'structured-advisor',
|
|
465
|
-
sessionHandle,
|
|
466
|
-
});
|
|
467
|
-
}
|
|
468
|
-
if (next.done) {
|
|
469
|
-
break;
|
|
470
|
-
}
|
|
471
|
-
const event = next.value;
|
|
472
|
-
switch (event.type) {
|
|
473
|
-
case 'thread.started':
|
|
474
|
-
sessionHandle = event.thread_id;
|
|
475
|
-
await emitProviderEvent(events, {
|
|
476
|
-
type: 'session_started',
|
|
477
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
478
|
-
role: 'structured-advisor',
|
|
479
|
-
label,
|
|
480
|
-
sessionHandle: event.thread_id,
|
|
481
|
-
providerData: { sdkEventType: event.type },
|
|
482
|
-
});
|
|
483
|
-
break;
|
|
484
|
-
case 'turn.started':
|
|
485
|
-
await emitProviderEvent(events, {
|
|
486
|
-
type: 'turn_started',
|
|
487
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
488
|
-
role: 'structured-advisor',
|
|
489
|
-
label,
|
|
490
|
-
sessionHandle,
|
|
491
|
-
providerData: { sdkEventType: event.type },
|
|
492
|
-
});
|
|
493
|
-
break;
|
|
494
|
-
case 'item.started':
|
|
495
|
-
await emitProviderEvent(events, {
|
|
496
|
-
type: 'tool_started',
|
|
497
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
498
|
-
role: 'structured-advisor',
|
|
499
|
-
label,
|
|
500
|
-
sessionHandle,
|
|
501
|
-
itemId: summarizeItem(event.item).itemId,
|
|
502
|
-
toolName: event.item.type,
|
|
503
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
504
|
-
});
|
|
505
|
-
break;
|
|
506
|
-
case 'item.updated':
|
|
507
|
-
await emitProviderEvent(events, {
|
|
508
|
-
type: 'tool_progress',
|
|
509
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
510
|
-
role: 'structured-advisor',
|
|
511
|
-
label,
|
|
512
|
-
sessionHandle,
|
|
513
|
-
itemId: summarizeItem(event.item).itemId,
|
|
514
|
-
toolName: event.item.type,
|
|
515
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
516
|
-
});
|
|
517
|
-
break;
|
|
518
|
-
case 'item.completed':
|
|
519
|
-
if (event.item.type === 'command_execution') {
|
|
520
|
-
await emitProviderEvent(events, {
|
|
521
|
-
type: 'command_completed',
|
|
522
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
523
|
-
role: 'structured-advisor',
|
|
524
|
-
label,
|
|
525
|
-
sessionHandle,
|
|
526
|
-
itemId: event.item.id,
|
|
527
|
-
command: event.item.command,
|
|
528
|
-
status: event.item.status,
|
|
529
|
-
exitCode: event.item.exit_code ?? null,
|
|
530
|
-
output: event.item.aggregated_output,
|
|
531
|
-
outputLength: event.item.aggregated_output.length,
|
|
532
|
-
cwd,
|
|
533
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
534
|
-
});
|
|
535
|
-
}
|
|
536
|
-
else if (event.item.type === 'file_change' && event.item.changes.length > 0) {
|
|
537
|
-
const files = event.item.changes.map((change) => change.path);
|
|
538
|
-
await emitProviderEvent(events, {
|
|
539
|
-
type: 'file_changed',
|
|
540
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
541
|
-
role: 'structured-advisor',
|
|
542
|
-
label,
|
|
543
|
-
sessionHandle,
|
|
544
|
-
files,
|
|
545
|
-
providerData: { sdkEventType: event.type, item: summarizeItem(event.item) },
|
|
546
|
-
});
|
|
547
|
-
}
|
|
548
|
-
else if (event.item.type === 'agent_message') {
|
|
549
|
-
finalResponse = event.item.text;
|
|
550
|
-
}
|
|
551
|
-
break;
|
|
552
|
-
case 'turn.completed':
|
|
553
|
-
await emitProviderEvent(events, {
|
|
554
|
-
type: 'turn_completed',
|
|
555
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
556
|
-
role: 'structured-advisor',
|
|
557
|
-
label,
|
|
558
|
-
sessionHandle,
|
|
559
|
-
usage: event.usage,
|
|
560
|
-
providerData: { sdkEventType: event.type },
|
|
561
|
-
});
|
|
562
|
-
if (event.usage !== undefined) {
|
|
563
|
-
await emitProviderEvent(events, {
|
|
564
|
-
type: 'usage_reported',
|
|
565
|
-
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
566
|
-
role: 'structured-advisor',
|
|
567
|
-
label,
|
|
568
|
-
sessionHandle,
|
|
569
|
-
usage: event.usage,
|
|
570
|
-
providerData: { sdkEventType: event.type },
|
|
571
|
-
});
|
|
572
|
-
}
|
|
573
|
-
break;
|
|
574
448
|
case 'turn.failed':
|
|
575
449
|
{
|
|
576
450
|
const nextFailure = observeCodexFailure({
|
|
@@ -583,8 +457,8 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
583
457
|
await emitProviderEvent(events, {
|
|
584
458
|
type: 'provider_error',
|
|
585
459
|
provider: OPENAI_CODEX_PROVIDER_ID,
|
|
586
|
-
role
|
|
587
|
-
|
|
460
|
+
role,
|
|
461
|
+
...labelFields,
|
|
588
462
|
sessionHandle,
|
|
589
463
|
message: event.error.message,
|
|
590
464
|
providerData: { sdkEventType: event.type },
|
|
@@ -602,7 +476,7 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
602
476
|
if (shouldReplaceCodexFailure(observedFailure, nextFailure)) {
|
|
603
477
|
if (await emitCodexStreamError({
|
|
604
478
|
events,
|
|
605
|
-
role
|
|
479
|
+
role,
|
|
606
480
|
label,
|
|
607
481
|
sessionHandle,
|
|
608
482
|
message: event.message,
|
|
@@ -618,12 +492,36 @@ async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, ev
|
|
|
618
492
|
}
|
|
619
493
|
if (observedFailure) {
|
|
620
494
|
throw createCodexProviderErrorFromObservedFailure(observedFailure, {
|
|
621
|
-
role
|
|
495
|
+
role,
|
|
622
496
|
sessionHandle,
|
|
623
497
|
});
|
|
624
498
|
}
|
|
625
499
|
return { finalResponse, sessionHandle };
|
|
626
500
|
}
|
|
501
|
+
async function consumeCodexTurn(turn, cwd, inactivityTimeoutMs, events, onSessionStarted, abortController,
|
|
502
|
+
// Appended last so existing positional callers (including test hooks) stay
|
|
503
|
+
// valid; forwards the configured slug for card-derived cost.
|
|
504
|
+
model) {
|
|
505
|
+
return consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, {
|
|
506
|
+
role: 'coder',
|
|
507
|
+
events,
|
|
508
|
+
onSessionStarted,
|
|
509
|
+
abortController,
|
|
510
|
+
model,
|
|
511
|
+
});
|
|
512
|
+
}
|
|
513
|
+
async function consumeCodexAdvisorTurn(turn, cwd, label, inactivityTimeoutMs, events, abortController,
|
|
514
|
+
// Appended last so existing positional callers (including test hooks) stay
|
|
515
|
+
// valid; forwards the configured slug for card-derived cost.
|
|
516
|
+
model) {
|
|
517
|
+
return consumeCodexTurnWithOptions(turn, cwd, inactivityTimeoutMs, {
|
|
518
|
+
role: 'structured-advisor',
|
|
519
|
+
label,
|
|
520
|
+
events,
|
|
521
|
+
abortController,
|
|
522
|
+
model,
|
|
523
|
+
});
|
|
524
|
+
}
|
|
627
525
|
function parseCodexStructuredOutput(finalResponse, label, sessionHandle) {
|
|
628
526
|
try {
|
|
629
527
|
return JSON.parse(finalResponse);
|
|
@@ -685,7 +583,7 @@ class OpenAICodexCoderAdapter {
|
|
|
685
583
|
...(args.outputSchema ? { outputSchema: args.outputSchema } : {}),
|
|
686
584
|
signal: abortController.signal,
|
|
687
585
|
});
|
|
688
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
586
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController, this.options.model ?? null);
|
|
689
587
|
if (args.outputSchema) {
|
|
690
588
|
await emitProviderEvent(args.events, {
|
|
691
589
|
type: 'structured_output_received',
|
|
@@ -745,7 +643,7 @@ class OpenAICodexCoderAdapter {
|
|
|
745
643
|
const streamedTurn = await thread.runStreamed(prompt, {
|
|
746
644
|
signal: abortController.signal,
|
|
747
645
|
});
|
|
748
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController);
|
|
646
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, args.onSessionStarted, abortController, this.options.model ?? null);
|
|
749
647
|
return {
|
|
750
648
|
assistantText: result.finalResponse,
|
|
751
649
|
sessionHandle: thread.id,
|
|
@@ -767,7 +665,7 @@ class OpenAICodexCoderAdapter {
|
|
|
767
665
|
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
768
666
|
signal: repairAbortController.signal,
|
|
769
667
|
});
|
|
770
|
-
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, undefined, repairAbortController);
|
|
668
|
+
const result = await consumeCodexTurn(streamedTurn, args.cwd, args.inactivityTimeoutMs, args.events, undefined, repairAbortController, this.options.model ?? null);
|
|
771
669
|
return {
|
|
772
670
|
assistantText: result.finalResponse,
|
|
773
671
|
sessionHandle: repairThread.id,
|
|
@@ -865,7 +763,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
865
763
|
const streamedTurn = await thread.runStreamed(prompt, {
|
|
866
764
|
signal: abortController.signal,
|
|
867
765
|
});
|
|
868
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
766
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController, this.options.model ?? null);
|
|
869
767
|
return {
|
|
870
768
|
assistantText: result.finalResponse,
|
|
871
769
|
sessionHandle: thread.id ?? result.sessionHandle ?? null,
|
|
@@ -883,7 +781,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
883
781
|
const streamedTurn = await repairThread.runStreamed(prompt, {
|
|
884
782
|
signal: repairAbortController.signal,
|
|
885
783
|
});
|
|
886
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, repairAbortController);
|
|
784
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, repairAbortController, this.options.model ?? null);
|
|
887
785
|
return {
|
|
888
786
|
assistantText: result.finalResponse,
|
|
889
787
|
sessionHandle: repairThread.id ?? result.sessionHandle ?? null,
|
|
@@ -900,7 +798,7 @@ class OpenAICodexStructuredAdvisorAdapter {
|
|
|
900
798
|
});
|
|
901
799
|
}
|
|
902
800
|
const streamedTurn = await thread.runStreamed(args.prompt, buildCodexStructuredAdvisorRunOptions(args, abortController.signal));
|
|
903
|
-
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController);
|
|
801
|
+
const result = await consumeCodexAdvisorTurn(streamedTurn, args.cwd, args.label, args.inactivityTimeoutMs, args.events, abortController, this.options.model ?? null);
|
|
904
802
|
const structured = parseCodexStructuredOutput(result.finalResponse, args.label, thread.id);
|
|
905
803
|
await emitProviderEvent(args.events, {
|
|
906
804
|
type: 'structured_output_received',
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
* root directory.
|
|
4
|
-
* never imported — the spike stays a frozen historical reference) with the
|
|
5
|
-
* Phase 1 hardening from the integration plan:
|
|
2
|
+
* openai-compatible provider tool layer: the six neal-defined AI SDK tools,
|
|
3
|
+
* jailed to a root directory. Key mechanics:
|
|
6
4
|
*
|
|
7
5
|
* - an event-emission hook so tool execution surfaces the existing provider
|
|
8
6
|
* event grammar (`tool_started`, `tool_progress`, `command_completed`,
|
|
@@ -42,6 +40,7 @@ import { promises as fs } from 'node:fs';
|
|
|
42
40
|
import path from 'node:path';
|
|
43
41
|
import { tool } from 'ai';
|
|
44
42
|
import { z } from 'zod';
|
|
43
|
+
import { gitConfigIsolationEnv } from './git-config-isolation.js';
|
|
45
44
|
export const RESULT_BYTE_LIMIT = 16 * 1024;
|
|
46
45
|
export const READ_FILE_BYTE_LIMIT = 256 * 1024;
|
|
47
46
|
export const GREP_MAX_MATCHING_LINES = 200;
|
|
@@ -386,7 +385,14 @@ function gitDiffBody(rootDir, base, head, relativePath, stat) {
|
|
|
386
385
|
}
|
|
387
386
|
function runBody(rootDir, command, timeoutMs) {
|
|
388
387
|
return new Promise((resolve) => {
|
|
389
|
-
exec(command, {
|
|
388
|
+
exec(command, {
|
|
389
|
+
cwd: path.resolve(rootDir),
|
|
390
|
+
// Agent-issued shell commands must not be able to write the operator's
|
|
391
|
+
// real global gitconfig. See git-config-isolation.ts.
|
|
392
|
+
env: { ...process.env, ...gitConfigIsolationEnv() },
|
|
393
|
+
timeout: timeoutMs,
|
|
394
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
395
|
+
}, (error, stdout, stderr) => {
|
|
390
396
|
const timedOut = Boolean(error && error.killed);
|
|
391
397
|
let exitCode;
|
|
392
398
|
if (error) {
|