@navels/neal 0.1.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/LICENSE +21 -0
- package/README.md +527 -0
- package/SECURITY.md +91 -0
- package/config.yml +104 -0
- package/dist/neal/activity-footer.js +177 -0
- package/dist/neal/activity-reporting.js +1 -0
- package/dist/neal/adjudicator/artifacts.js +58 -0
- package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
- package/dist/neal/adjudicator/contracts.js +139 -0
- package/dist/neal/adjudicator/execute.js +611 -0
- package/dist/neal/adjudicator/final-completion.js +104 -0
- package/dist/neal/adjudicator/planning.js +145 -0
- package/dist/neal/adjudicator/specs.js +453 -0
- package/dist/neal/agents/prompts.js +120 -0
- package/dist/neal/agents/rounds.js +706 -0
- package/dist/neal/agents/schemas.js +832 -0
- package/dist/neal/agents/structured-coder.js +82 -0
- package/dist/neal/agents/structured-json.js +528 -0
- package/dist/neal/agents.js +4 -0
- package/dist/neal/atomic-write.js +18 -0
- package/dist/neal/blocked-guidance.js +406 -0
- package/dist/neal/cli.js +471 -0
- package/dist/neal/commands/check.js +401 -0
- package/dist/neal/commands/compat.js +807 -0
- package/dist/neal/commands/interactive-activity.js +57 -0
- package/dist/neal/commands/new-run.js +79 -0
- package/dist/neal/commands/plan-and-execute.js +44 -0
- package/dist/neal/commands/recovery-guidance.js +217 -0
- package/dist/neal/commands/resume-run.js +395 -0
- package/dist/neal/commands/review.js +21 -0
- package/dist/neal/commands/runtime.js +557 -0
- package/dist/neal/commands/setup.js +596 -0
- package/dist/neal/commands/squash.js +113 -0
- package/dist/neal/commands/status.js +33 -0
- package/dist/neal/commands/writer-exit-codes.js +42 -0
- package/dist/neal/commit-message.js +17 -0
- package/dist/neal/config.js +432 -0
- package/dist/neal/context/artifacts.js +140 -0
- package/dist/neal/context/context.js +324 -0
- package/dist/neal/context/inline-review-context.js +131 -0
- package/dist/neal/context/reviewer-context.js +166 -0
- package/dist/neal/context/shared.js +117 -0
- package/dist/neal/context/types.js +1 -0
- package/dist/neal/diagnostic.js +208 -0
- package/dist/neal/execute-finalization.js +5 -0
- package/dist/neal/final-completion-review.js +188 -0
- package/dist/neal/final-completion.js +229 -0
- package/dist/neal/git.js +339 -0
- package/dist/neal/index.js +135 -0
- package/dist/neal/interactive-controls.js +85 -0
- package/dist/neal/logger.js +102 -0
- package/dist/neal/manual-gates.js +121 -0
- package/dist/neal/orchestrator/artifacts.js +70 -0
- package/dist/neal/orchestrator/completion.js +531 -0
- package/dist/neal/orchestrator/failures.js +31 -0
- package/dist/neal/orchestrator/notifications.js +175 -0
- package/dist/neal/orchestrator/phases/coder.js +516 -0
- package/dist/neal/orchestrator/phases/planning.js +540 -0
- package/dist/neal/orchestrator/phases/recovery.js +798 -0
- package/dist/neal/orchestrator/phases/review.js +136 -0
- package/dist/neal/orchestrator/phases/shared.js +279 -0
- package/dist/neal/orchestrator/run-loop.js +113 -0
- package/dist/neal/orchestrator/split-plan.js +235 -0
- package/dist/neal/orchestrator/transitions.js +309 -0
- package/dist/neal/orchestrator.js +215 -0
- package/dist/neal/phase-display.js +27 -0
- package/dist/neal/plan-doc.js +154 -0
- package/dist/neal/plan-queue.js +1092 -0
- package/dist/neal/plan-refinement.js +39 -0
- package/dist/neal/plan-validation.js +525 -0
- package/dist/neal/progress.js +237 -0
- package/dist/neal/prompts/assert-builder.js +13 -0
- package/dist/neal/prompts/execute.js +290 -0
- package/dist/neal/prompts/guidance.js +70 -0
- package/dist/neal/prompts/planning.js +313 -0
- package/dist/neal/prompts/review-doctrine.js +142 -0
- package/dist/neal/prompts/shared.js +101 -0
- package/dist/neal/prompts/specialized.js +212 -0
- package/dist/neal/prompts/specs.js +572 -0
- package/dist/neal/providers/anthropic-claude.js +1599 -0
- package/dist/neal/providers/detection.js +139 -0
- package/dist/neal/providers/generic-agentic-tools.js +586 -0
- package/dist/neal/providers/generic-agentic.js +1238 -0
- package/dist/neal/providers/liveness.js +151 -0
- package/dist/neal/providers/openai-codex.js +1014 -0
- package/dist/neal/providers/openai-compatible.js +654 -0
- package/dist/neal/providers/registry.js +389 -0
- package/dist/neal/providers/telemetry.js +208 -0
- package/dist/neal/providers/types.js +21 -0
- package/dist/neal/recovery-artifacts.js +50 -0
- package/dist/neal/resume-decision.js +220 -0
- package/dist/neal/resume-planner.js +265 -0
- package/dist/neal/retrospective.js +391 -0
- package/dist/neal/review-debt.js +18 -0
- package/dist/neal/review-findings/artifacts.js +173 -0
- package/dist/neal/review-findings/prompts.js +172 -0
- package/dist/neal/review-findings/provider.js +330 -0
- package/dist/neal/review-findings/run.js +373 -0
- package/dist/neal/review-findings/types.js +1 -0
- package/dist/neal/review-mode.js +67 -0
- package/dist/neal/review.js +137 -0
- package/dist/neal/run-lock.js +334 -0
- package/dist/neal/run-metrics.js +355 -0
- package/dist/neal/run-narrative-types.js +1 -0
- package/dist/neal/run-narrative.js +1374 -0
- package/dist/neal/run-registry.js +218 -0
- package/dist/neal/run-status.js +25 -0
- package/dist/neal/scopes.js +451 -0
- package/dist/neal/sensitive-text.js +8 -0
- package/dist/neal/squash-message.js +379 -0
- package/dist/neal/squash.js +591 -0
- package/dist/neal/state-invariants.js +496 -0
- package/dist/neal/state-views.js +344 -0
- package/dist/neal/state.js +887 -0
- package/dist/neal/status-footer.js +258 -0
- package/dist/neal/status.js +1260 -0
- package/dist/neal/storage-paths.js +57 -0
- package/dist/neal/support.js +58 -0
- package/dist/neal/terminal-narrator.js +435 -0
- package/dist/neal/types.js +1 -0
- package/dist/neal/verification-events.js +81 -0
- package/dist/neal/version.js +37 -0
- package/dist/neal/worktree-status.js +137 -0
- package/dist/notifier.js +44 -0
- package/docs/ADJUDICATOR_INVENTORY.md +310 -0
- package/docs/PROMPT_SPECS.md +266 -0
- package/docs/README.md +22 -0
- package/docs/architecture.md +113 -0
- package/docs/assets/neal-execution-flow.png +0 -0
- package/docs/automation.md +65 -0
- package/docs/comparison.md +105 -0
- package/docs/compat.md +269 -0
- package/docs/compatible-models.md +135 -0
- package/docs/demo.md +55 -0
- package/docs/maintenance.md +64 -0
- package/docs/plan-format.md +213 -0
- package/docs/providers.md +751 -0
- package/docs/release.md +147 -0
- package/docs/state-machine.md +266 -0
- package/docs/storage.md +207 -0
- package/docs/troubleshooting.md +152 -0
- package/examples/compat/add-edit-verify/PLAN.md +29 -0
- package/examples/compat/add-edit-verify/broken.diff +8 -0
- package/examples/compat/add-edit-verify/good.diff +8 -0
- package/examples/compat/add-edit-verify/package.json +5 -0
- package/examples/compat/add-edit-verify/src/add.js +2 -0
- package/examples/compat/add-edit-verify/test/add.test.js +9 -0
- package/examples/compat/is-even-add-test/PLAN.md +30 -0
- package/examples/compat/is-even-add-test/broken.diff +11 -0
- package/examples/compat/is-even-add-test/good.diff +11 -0
- package/examples/compat/is-even-add-test/package.json +5 -0
- package/examples/compat/is-even-add-test/src/is-even.js +3 -0
- package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
- package/examples/compat/manifest.json +60 -0
- package/examples/compat/plan-greeting/ISSUE.md +25 -0
- package/examples/compat/plan-greeting/package.json +5 -0
- package/examples/compat/plan-greeting/src/greet.js +2 -0
- package/examples/compat/plan-greeting/test/greet.test.js +8 -0
- package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
- package/examples/compat/reverse-grep-edit/broken.diff +12 -0
- package/examples/compat/reverse-grep-edit/good.diff +12 -0
- package/examples/compat/reverse-grep-edit/package.json +5 -0
- package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
- package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
- package/examples/issue-triage-js/PLAN.md +83 -0
- package/examples/issue-triage-js/README.md +76 -0
- package/examples/issue-triage-js/package.json +9 -0
- package/examples/issue-triage-js/src/issue-triage.js +87 -0
- package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
- package/package.json +70 -0
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import readline from 'node:readline';
|
|
3
|
+
import { parseSquashArgs } from '../cli.js';
|
|
4
|
+
import { assertGitRepositoryWithCommit } from '../git.js';
|
|
5
|
+
import { buildSquashCommitMessage, buildSquashResultMetadata, executeSquashForRun, selectLatestSquashRun, selectSquashRunForPlan, validateSelectedRunForSquash, } from '../squash.js';
|
|
6
|
+
export async function runSquashCommand(args) {
|
|
7
|
+
const parsed = parseSquashArgs(args);
|
|
8
|
+
await assertGitRepositoryWithCommit(process.cwd(), 'neal squash');
|
|
9
|
+
const selection = parsed.planDoc
|
|
10
|
+
? await selectSquashRunForPlan({
|
|
11
|
+
cwd: process.cwd(),
|
|
12
|
+
planDocArg: parsed.planDoc,
|
|
13
|
+
})
|
|
14
|
+
: await selectLatestSquashRun({
|
|
15
|
+
cwd: process.cwd(),
|
|
16
|
+
});
|
|
17
|
+
const validation = await validateSelectedRunForSquash({
|
|
18
|
+
cwd: process.cwd(),
|
|
19
|
+
selected: selection.selected,
|
|
20
|
+
});
|
|
21
|
+
const commitMessage = await buildSquashCommitMessage({
|
|
22
|
+
cwd: process.cwd(),
|
|
23
|
+
selected: selection.selected,
|
|
24
|
+
});
|
|
25
|
+
const metadata = await buildSquashResultMetadata({
|
|
26
|
+
cwd: process.cwd(),
|
|
27
|
+
selected: selection.selected,
|
|
28
|
+
validation,
|
|
29
|
+
commitMessage,
|
|
30
|
+
});
|
|
31
|
+
writeSquashPreview({
|
|
32
|
+
selection,
|
|
33
|
+
validation,
|
|
34
|
+
commitMessage,
|
|
35
|
+
metadata,
|
|
36
|
+
});
|
|
37
|
+
await confirmSquashRewrite();
|
|
38
|
+
const execution = await executeSquashForRun({
|
|
39
|
+
cwd: process.cwd(),
|
|
40
|
+
selected: selection.selected,
|
|
41
|
+
validation,
|
|
42
|
+
commitMessage,
|
|
43
|
+
});
|
|
44
|
+
process.stdout.write(JSON.stringify({
|
|
45
|
+
ok: true,
|
|
46
|
+
mode: 'squash_result',
|
|
47
|
+
rewriteReady: true,
|
|
48
|
+
planDoc: selection.normalizedPlanDoc,
|
|
49
|
+
selectedRunDir: selection.selected.runDir,
|
|
50
|
+
selectedRunId: selection.selected.runId,
|
|
51
|
+
completedMatchCount: selection.completedMatchCount,
|
|
52
|
+
selectionWarning: selection.selectionWarning,
|
|
53
|
+
baseCommit: validation.baseCommit,
|
|
54
|
+
finalCommit: validation.finalCommit,
|
|
55
|
+
createdCommits: validation.createdCommits,
|
|
56
|
+
postPlanCommits: validation.postPlanCommits,
|
|
57
|
+
commitMessageSource: commitMessage.source,
|
|
58
|
+
commitMessageSubject: commitMessage.subject,
|
|
59
|
+
commitMessage: commitMessage.message,
|
|
60
|
+
planDocDisposition: execution.artifact.metadata.planDocDisposition,
|
|
61
|
+
planDocIncludedInReplacementCommit: execution.artifact.metadata.planDocIncludedInReplacementCommit,
|
|
62
|
+
metadata: execution.artifact.metadata,
|
|
63
|
+
replacementCommit: execution.replacementCommit,
|
|
64
|
+
finalHeadCommit: execution.finalHeadCommit,
|
|
65
|
+
squashArtifactPath: execution.artifactPath,
|
|
66
|
+
}, null, 2) + '\n');
|
|
67
|
+
}
|
|
68
|
+
function writeSquashPreview({ selection, validation, commitMessage, metadata }) {
|
|
69
|
+
const lines = [
|
|
70
|
+
`[neal] selected squash run: ${selection.selected.runId}`,
|
|
71
|
+
`[neal] run dir: ${selection.selected.runDir}`,
|
|
72
|
+
`[neal] plan doc: ${selection.normalizedPlanDoc}`,
|
|
73
|
+
`[neal] plan doc disposition: ${metadata.planDocDisposition}`,
|
|
74
|
+
`[neal] replacement tree includes plan doc: ${metadata.planDocIncludedInReplacementCommit ? 'yes' : 'no'}`,
|
|
75
|
+
`[neal] base commit: ${validation.baseCommit}`,
|
|
76
|
+
`[neal] final commit: ${validation.finalCommit}`,
|
|
77
|
+
'[neal] commits to replace:',
|
|
78
|
+
...validation.createdCommits.map((commit) => ` - ${commit}`),
|
|
79
|
+
validation.postPlanCommits.length > 0 ? '[neal] later commits to preserve:' : '[neal] later commits to preserve: none',
|
|
80
|
+
...validation.postPlanCommits.map((commit) => ` - ${commit}`),
|
|
81
|
+
`[neal] commit message source: ${metadata.commitMessageSource}`,
|
|
82
|
+
`[neal] commit message subject: ${metadata.commitMessageSubject}`,
|
|
83
|
+
'[neal] generated commit message:',
|
|
84
|
+
commitMessage.message,
|
|
85
|
+
];
|
|
86
|
+
if (selection.selectionWarning) {
|
|
87
|
+
lines.splice(2, 0, `[neal] ${selection.selectionWarning}`);
|
|
88
|
+
}
|
|
89
|
+
process.stderr.write(lines.join('\n') + '\n');
|
|
90
|
+
}
|
|
91
|
+
async function confirmSquashRewrite() {
|
|
92
|
+
if (!process.stdin.isTTY || !process.stderr.isTTY) {
|
|
93
|
+
process.stderr.write('[neal] Interactive TTY confirmation is required before rewriting history; automated squash confirmation is not part of the public CLI.\n');
|
|
94
|
+
throw new Error('neal squash requires interactive TTY confirmation; no history was rewritten');
|
|
95
|
+
}
|
|
96
|
+
const rl = readline.createInterface({
|
|
97
|
+
input: process.stdin,
|
|
98
|
+
output: process.stderr,
|
|
99
|
+
terminal: true,
|
|
100
|
+
});
|
|
101
|
+
try {
|
|
102
|
+
const answer = await new Promise((resolve) => {
|
|
103
|
+
rl.question('[neal] Proceed with squash rewrite? [y/N] ', resolve);
|
|
104
|
+
});
|
|
105
|
+
const normalized = answer.trim().toLowerCase();
|
|
106
|
+
if (normalized !== 'y' && normalized !== 'yes') {
|
|
107
|
+
throw new Error('neal squash aborted; no history was rewritten');
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
finally {
|
|
111
|
+
rl.close();
|
|
112
|
+
}
|
|
113
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { parseStatusArgs } from '../cli.js';
|
|
3
|
+
import { listRuns } from '../run-registry.js';
|
|
4
|
+
import { buildStatusListSnapshot, buildStatusSnapshot, renderHumanStatusListSnapshot, renderHumanStatusSnapshot, } from '../status.js';
|
|
5
|
+
import { resolveCliRunStatePath } from './runtime.js';
|
|
6
|
+
export async function runStatusCommand(args) {
|
|
7
|
+
const parsed = parseStatusArgs(args);
|
|
8
|
+
if (parsed.all) {
|
|
9
|
+
const snapshot = await buildStatusListSnapshot({ cwd: process.cwd() });
|
|
10
|
+
if (parsed.json) {
|
|
11
|
+
process.stdout.write(JSON.stringify(snapshot, null, 2) + '\n');
|
|
12
|
+
return;
|
|
13
|
+
}
|
|
14
|
+
process.stdout.write(renderHumanStatusListSnapshot(snapshot).trimEnd() + '\n');
|
|
15
|
+
return;
|
|
16
|
+
}
|
|
17
|
+
if (!parsed.json && parsed.runId === null && (await listRuns(process.cwd())).length === 0) {
|
|
18
|
+
process.stdout.write('No Neal runs found in this repository yet. Start one with: neal run <plan.md>\n');
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
const resolution = await resolveCliRunStatePath({
|
|
22
|
+
runId: parsed.runId,
|
|
23
|
+
});
|
|
24
|
+
const snapshot = await buildStatusSnapshot({
|
|
25
|
+
cwd: process.cwd(),
|
|
26
|
+
statePath: resolution.statePath,
|
|
27
|
+
});
|
|
28
|
+
if (parsed.json) {
|
|
29
|
+
process.stdout.write(JSON.stringify(snapshot, null, 2) + '\n');
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
process.stdout.write(renderHumanStatusSnapshot(snapshot).trimEnd() + '\n');
|
|
33
|
+
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import process from 'node:process';
|
|
2
|
+
import { determinePlanRefinementConvergence, isPlanRefinementState, planRefinementExitCode, } from '../plan-refinement.js';
|
|
3
|
+
import { getRunDisplayStatus } from '../run-status.js';
|
|
4
|
+
export function getExecuteRunResultExitCode(result) {
|
|
5
|
+
if (isPlanRefinementState(result.finalState)) {
|
|
6
|
+
const convergenceReason = determinePlanRefinementConvergence(result.finalState);
|
|
7
|
+
if (convergenceReason !== null) {
|
|
8
|
+
return planRefinementExitCode(convergenceReason);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const displayStatus = getRunDisplayStatus(result.finalState);
|
|
12
|
+
if (displayStatus.effectiveStatus === 'waiting_for_operator' ||
|
|
13
|
+
displayStatus.effectiveStatus === 'waiting_for_manual_gate' ||
|
|
14
|
+
displayStatus.pendingOperatorGuidance) {
|
|
15
|
+
return 2;
|
|
16
|
+
}
|
|
17
|
+
switch (result.finalState.status) {
|
|
18
|
+
case 'done':
|
|
19
|
+
return 0;
|
|
20
|
+
case 'failed':
|
|
21
|
+
return 3;
|
|
22
|
+
case 'blocked':
|
|
23
|
+
case 'paused':
|
|
24
|
+
case 'running':
|
|
25
|
+
return 2;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
export function getPlanAndExecuteQueueExitCode(state) {
|
|
29
|
+
switch (state.status) {
|
|
30
|
+
case 'completed':
|
|
31
|
+
return 0;
|
|
32
|
+
case 'failed':
|
|
33
|
+
return 3;
|
|
34
|
+
case 'blocked':
|
|
35
|
+
case 'paused':
|
|
36
|
+
case 'running':
|
|
37
|
+
return 2;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function setWriterCommandExitCode(code) {
|
|
41
|
+
process.exitCode = code;
|
|
42
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
const SCOPE_PREFIX_PATTERN = /^Scope(?:\s+|:\s*)\d+(?:\.\d+)*(?:[A-Za-z])?(?:\s*(?::|-)\s*|$)/;
|
|
2
|
+
export function stripScopePrefixFromSubject(subject) {
|
|
3
|
+
return subject.trim().replace(SCOPE_PREFIX_PATTERN, '').trim();
|
|
4
|
+
}
|
|
5
|
+
export function stripScopePrefixFromCommitMessage(message) {
|
|
6
|
+
const normalized = message.replace(/\r\n/g, '\n');
|
|
7
|
+
const lines = normalized.split('\n');
|
|
8
|
+
const strippedSubject = stripScopePrefixFromSubject(lines[0] ?? '');
|
|
9
|
+
if (strippedSubject) {
|
|
10
|
+
return [strippedSubject, ...lines.slice(1)].join('\n');
|
|
11
|
+
}
|
|
12
|
+
const firstBodyLineIndex = lines.findIndex((line, index) => index > 0 && line.trim());
|
|
13
|
+
if (firstBodyLineIndex === -1) {
|
|
14
|
+
return '';
|
|
15
|
+
}
|
|
16
|
+
return lines.slice(firstBodyLineIndex).join('\n');
|
|
17
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
|
+
import { homedir } from 'node:os';
|
|
3
|
+
import { join, resolve } from 'node:path';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import { assertAgentConfigSupportsWriterRun, parseProviderId, } from './providers/registry.js';
|
|
6
|
+
const OPENAI_COMPATIBLE_DEFAULT_API_KEY_ENV = 'OPENAI_COMPATIBLE_API_KEY';
|
|
7
|
+
const WRITER_PROVIDER_CONFIG_KEYS = {
|
|
8
|
+
coder: 'agent.coder.provider',
|
|
9
|
+
reviewer: 'agent.reviewer.provider',
|
|
10
|
+
};
|
|
11
|
+
const DEFAULT_CONFIG = {
|
|
12
|
+
neal: {
|
|
13
|
+
phase_heartbeat_ms: 60_000,
|
|
14
|
+
max_review_rounds: 20,
|
|
15
|
+
review_stuck_window: 5,
|
|
16
|
+
inactivity_timeout_ms: 600_000,
|
|
17
|
+
api_retry_limit: 10,
|
|
18
|
+
agent_turn_startup_timeout_ms: 300_000,
|
|
19
|
+
agent_turn_retry_limit: 1,
|
|
20
|
+
interactive_blocked_recovery_max_turns: 3,
|
|
21
|
+
final_completion_continue_execution_max: 2,
|
|
22
|
+
review_stuck_arbiter_max_attempts: 1,
|
|
23
|
+
notify_bin: null,
|
|
24
|
+
},
|
|
25
|
+
agent: {
|
|
26
|
+
planner: {
|
|
27
|
+
provider: 'openai-codex',
|
|
28
|
+
model: null,
|
|
29
|
+
},
|
|
30
|
+
coder: {
|
|
31
|
+
provider: 'openai-codex',
|
|
32
|
+
model: null,
|
|
33
|
+
},
|
|
34
|
+
reviewer: {
|
|
35
|
+
provider: 'anthropic-claude',
|
|
36
|
+
model: null,
|
|
37
|
+
},
|
|
38
|
+
unattended: false,
|
|
39
|
+
},
|
|
40
|
+
};
|
|
41
|
+
const cachedConfig = new Map();
|
|
42
|
+
function buildWriterProviderSetupLines(guidance, missingProviderKeys) {
|
|
43
|
+
const missingKeys = missingProviderKeys.length > 0
|
|
44
|
+
? missingProviderKeys
|
|
45
|
+
: Object.values(WRITER_PROVIDER_CONFIG_KEYS);
|
|
46
|
+
if (guidance === 'check') {
|
|
47
|
+
return [
|
|
48
|
+
'Neal is not set up yet.',
|
|
49
|
+
'Run `neal setup` to choose providers, then run `neal check` again.',
|
|
50
|
+
];
|
|
51
|
+
}
|
|
52
|
+
return [
|
|
53
|
+
'Neal is not set up yet.',
|
|
54
|
+
`Run \`neal setup\` to choose providers, or set ${formatInlineList(missingKeys)} in config.yml.`,
|
|
55
|
+
];
|
|
56
|
+
}
|
|
57
|
+
function formatInlineList(values) {
|
|
58
|
+
if (values.length <= 1) {
|
|
59
|
+
return values[0] ?? '';
|
|
60
|
+
}
|
|
61
|
+
return `${values.slice(0, -1).join(', ')} and ${values[values.length - 1]}`;
|
|
62
|
+
}
|
|
63
|
+
export class WriterProvidersNotConfiguredError extends Error {
|
|
64
|
+
guidance;
|
|
65
|
+
lines;
|
|
66
|
+
missingProviderKeys;
|
|
67
|
+
constructor(guidance = 'writer-run', missingProviderKeys = []) {
|
|
68
|
+
const lines = buildWriterProviderSetupLines(guidance, missingProviderKeys);
|
|
69
|
+
super(lines.join('\n'));
|
|
70
|
+
this.name = 'WriterProvidersNotConfiguredError';
|
|
71
|
+
this.guidance = guidance;
|
|
72
|
+
this.lines = lines;
|
|
73
|
+
this.missingProviderKeys = [...missingProviderKeys];
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export function isWriterProvidersNotConfiguredError(error) {
|
|
77
|
+
return error instanceof WriterProvidersNotConfiguredError;
|
|
78
|
+
}
|
|
79
|
+
export function formatWriterProviderSetupRequiredLines(error, prefix = '') {
|
|
80
|
+
return prefix ? error.lines.map((line) => `${prefix} ${line}`) : [...error.lines];
|
|
81
|
+
}
|
|
82
|
+
export function clearConfigCache(cwd) {
|
|
83
|
+
if (cwd) {
|
|
84
|
+
cachedConfig.delete(resolve(cwd));
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
cachedConfig.clear();
|
|
88
|
+
}
|
|
89
|
+
function parseNumberValue(value) {
|
|
90
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
91
|
+
return value;
|
|
92
|
+
}
|
|
93
|
+
if (typeof value === 'string' && value.trim()) {
|
|
94
|
+
const parsed = Number(value.trim());
|
|
95
|
+
if (Number.isFinite(parsed)) {
|
|
96
|
+
return parsed;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return undefined;
|
|
100
|
+
}
|
|
101
|
+
function parseStringValue(value) {
|
|
102
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
103
|
+
}
|
|
104
|
+
function parseConfigProviderValue(value, fieldPath) {
|
|
105
|
+
if (value === undefined || value === null) {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
if (typeof value === 'string' && !value.trim()) {
|
|
109
|
+
return undefined;
|
|
110
|
+
}
|
|
111
|
+
return parseProviderId(value, fieldPath);
|
|
112
|
+
}
|
|
113
|
+
function parseExplicitProviderValue(value, fieldPath) {
|
|
114
|
+
if (value === undefined || value === null) {
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
if (typeof value === 'string' && !value.trim()) {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
return parseProviderId(value, fieldPath);
|
|
121
|
+
}
|
|
122
|
+
function parseExplicitModelValue(value) {
|
|
123
|
+
return parseStringValue(value) ?? null;
|
|
124
|
+
}
|
|
125
|
+
function parseExplicitEffortValue(value) {
|
|
126
|
+
return parseStringValue(value) ?? null;
|
|
127
|
+
}
|
|
128
|
+
function hasOwnConfigKey(value, key) {
|
|
129
|
+
return value !== null && value !== undefined && Object.prototype.hasOwnProperty.call(value, key);
|
|
130
|
+
}
|
|
131
|
+
function readYamlFileIfPresent(path) {
|
|
132
|
+
if (!existsSync(path)) {
|
|
133
|
+
return null;
|
|
134
|
+
}
|
|
135
|
+
const parsed = YAML.parse(readFileSync(path, 'utf8'));
|
|
136
|
+
if (!parsed || typeof parsed !== 'object') {
|
|
137
|
+
return null;
|
|
138
|
+
}
|
|
139
|
+
return parsed;
|
|
140
|
+
}
|
|
141
|
+
function mergeConfig(base, override) {
|
|
142
|
+
if (!override) {
|
|
143
|
+
return base;
|
|
144
|
+
}
|
|
145
|
+
return {
|
|
146
|
+
neal: {
|
|
147
|
+
...base.neal,
|
|
148
|
+
...override.neal,
|
|
149
|
+
},
|
|
150
|
+
agent: {
|
|
151
|
+
planner: {
|
|
152
|
+
...base.agent?.planner,
|
|
153
|
+
...override.agent?.planner,
|
|
154
|
+
},
|
|
155
|
+
coder: {
|
|
156
|
+
...base.agent?.coder,
|
|
157
|
+
...override.agent?.coder,
|
|
158
|
+
},
|
|
159
|
+
reviewer: {
|
|
160
|
+
...base.agent?.reviewer,
|
|
161
|
+
...override.agent?.reviewer,
|
|
162
|
+
},
|
|
163
|
+
// Scalar agent toggle: merged explicitly because the deep-merge above only
|
|
164
|
+
// covers the three provider sub-objects. Override wins when present.
|
|
165
|
+
unattended: override.agent?.unattended ?? base.agent?.unattended,
|
|
166
|
+
},
|
|
167
|
+
providers: {
|
|
168
|
+
openai_compatible: {
|
|
169
|
+
...base.providers?.openai_compatible,
|
|
170
|
+
...override.providers?.openai_compatible,
|
|
171
|
+
},
|
|
172
|
+
},
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
export function getConfigSourceInfo(cwd = process.cwd()) {
|
|
176
|
+
const repoPath = resolve(cwd, 'config.yml');
|
|
177
|
+
const userPath = join(homedir(), '.neal', 'config.yml');
|
|
178
|
+
return {
|
|
179
|
+
repo: {
|
|
180
|
+
path: repoPath,
|
|
181
|
+
exists: existsSync(repoPath),
|
|
182
|
+
},
|
|
183
|
+
user: {
|
|
184
|
+
path: userPath,
|
|
185
|
+
exists: existsSync(userPath),
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
function loadConfigFile(cwd = process.cwd()) {
|
|
190
|
+
const cacheKey = resolve(cwd);
|
|
191
|
+
const cached = cachedConfig.get(cacheKey);
|
|
192
|
+
if (cached) {
|
|
193
|
+
return cached;
|
|
194
|
+
}
|
|
195
|
+
const sources = getConfigSourceInfo(cacheKey);
|
|
196
|
+
const resolved = mergeConfig(mergeConfig({}, readYamlFileIfPresent(sources.user.path)), readYamlFileIfPresent(sources.repo.path));
|
|
197
|
+
cachedConfig.set(cacheKey, resolved);
|
|
198
|
+
return resolved;
|
|
199
|
+
}
|
|
200
|
+
export function getRawMergedConfig(cwd = process.cwd()) {
|
|
201
|
+
return loadConfigFile(cwd);
|
|
202
|
+
}
|
|
203
|
+
function readExplicitAgentConfig(cwd = process.cwd()) {
|
|
204
|
+
const config = getRawMergedConfig(cwd);
|
|
205
|
+
const coderProvider = parseExplicitProviderValue(config.agent?.coder?.provider, WRITER_PROVIDER_CONFIG_KEYS.coder);
|
|
206
|
+
const reviewerProvider = parseExplicitProviderValue(config.agent?.reviewer?.provider, WRITER_PROVIDER_CONFIG_KEYS.reviewer);
|
|
207
|
+
const missingProviderKeys = [];
|
|
208
|
+
if (!coderProvider) {
|
|
209
|
+
missingProviderKeys.push(WRITER_PROVIDER_CONFIG_KEYS.coder);
|
|
210
|
+
}
|
|
211
|
+
if (!reviewerProvider) {
|
|
212
|
+
missingProviderKeys.push(WRITER_PROVIDER_CONFIG_KEYS.reviewer);
|
|
213
|
+
}
|
|
214
|
+
if (!coderProvider || !reviewerProvider) {
|
|
215
|
+
return {
|
|
216
|
+
agentConfig: null,
|
|
217
|
+
missingProviderKeys,
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
const plannerProvider = parseConfigProviderValue(config.agent?.planner?.provider, 'agent.planner.provider');
|
|
221
|
+
return {
|
|
222
|
+
agentConfig: {
|
|
223
|
+
planner: {
|
|
224
|
+
provider: plannerProvider ?? coderProvider,
|
|
225
|
+
model: hasOwnConfigKey(config.agent?.planner, 'model')
|
|
226
|
+
? parseExplicitModelValue(config.agent.planner.model)
|
|
227
|
+
: plannerProvider
|
|
228
|
+
? null
|
|
229
|
+
: parseExplicitModelValue(config.agent?.coder?.model),
|
|
230
|
+
effort: hasOwnConfigKey(config.agent?.planner, 'effort')
|
|
231
|
+
? parseExplicitEffortValue(config.agent.planner.effort)
|
|
232
|
+
: plannerProvider
|
|
233
|
+
? null
|
|
234
|
+
: parseExplicitEffortValue(config.agent?.coder?.effort),
|
|
235
|
+
},
|
|
236
|
+
coder: {
|
|
237
|
+
provider: coderProvider,
|
|
238
|
+
model: parseExplicitModelValue(config.agent?.coder?.model),
|
|
239
|
+
effort: parseExplicitEffortValue(config.agent?.coder?.effort),
|
|
240
|
+
},
|
|
241
|
+
reviewer: {
|
|
242
|
+
provider: reviewerProvider,
|
|
243
|
+
model: parseExplicitModelValue(config.agent?.reviewer?.model),
|
|
244
|
+
effort: parseExplicitEffortValue(config.agent?.reviewer?.effort),
|
|
245
|
+
},
|
|
246
|
+
},
|
|
247
|
+
missingProviderKeys: [],
|
|
248
|
+
};
|
|
249
|
+
}
|
|
250
|
+
export function getExplicitAgentConfig(cwd = process.cwd()) {
|
|
251
|
+
return readExplicitAgentConfig(cwd).agentConfig;
|
|
252
|
+
}
|
|
253
|
+
export function assertWriterProvidersConfigured(cwd = process.cwd(), options = {}) {
|
|
254
|
+
const { agentConfig, missingProviderKeys } = readExplicitAgentConfig(cwd);
|
|
255
|
+
if (!agentConfig) {
|
|
256
|
+
throw new WriterProvidersNotConfiguredError(options.guidance ?? 'writer-run', missingProviderKeys);
|
|
257
|
+
}
|
|
258
|
+
assertAgentConfigSupportsWriterRun(agentConfig, { context: options.context });
|
|
259
|
+
return agentConfig;
|
|
260
|
+
}
|
|
261
|
+
export function getInactivityTimeoutMs(cwd = process.cwd()) {
|
|
262
|
+
const config = loadConfigFile(cwd);
|
|
263
|
+
return (parseNumberValue(config.neal?.inactivity_timeout_ms) ??
|
|
264
|
+
DEFAULT_CONFIG.neal.inactivity_timeout_ms);
|
|
265
|
+
}
|
|
266
|
+
export function getApiRetryLimit(cwd = process.cwd()) {
|
|
267
|
+
const config = loadConfigFile(cwd);
|
|
268
|
+
return (parseNumberValue(config.neal?.api_retry_limit) ??
|
|
269
|
+
DEFAULT_CONFIG.neal.api_retry_limit);
|
|
270
|
+
}
|
|
271
|
+
export function getAgentTurnStartupTimeoutMs(cwd = process.cwd()) {
|
|
272
|
+
const config = loadConfigFile(cwd);
|
|
273
|
+
return (parseNumberValue(config.neal?.agent_turn_startup_timeout_ms) ??
|
|
274
|
+
DEFAULT_CONFIG.neal.agent_turn_startup_timeout_ms);
|
|
275
|
+
}
|
|
276
|
+
export function getAgentTurnRetryLimit(cwd = process.cwd()) {
|
|
277
|
+
const config = loadConfigFile(cwd);
|
|
278
|
+
return (parseNumberValue(config.neal?.agent_turn_retry_limit) ??
|
|
279
|
+
DEFAULT_CONFIG.neal.agent_turn_retry_limit);
|
|
280
|
+
}
|
|
281
|
+
export function getPhaseHeartbeatMs(cwd = process.cwd()) {
|
|
282
|
+
const config = loadConfigFile(cwd);
|
|
283
|
+
return (parseNumberValue(config.neal?.phase_heartbeat_ms) ??
|
|
284
|
+
DEFAULT_CONFIG.neal.phase_heartbeat_ms);
|
|
285
|
+
}
|
|
286
|
+
export function getMaxReviewRounds(cwd = process.cwd()) {
|
|
287
|
+
const config = loadConfigFile(cwd);
|
|
288
|
+
return (parseNumberValue(config.neal?.max_review_rounds) ??
|
|
289
|
+
DEFAULT_CONFIG.neal.max_review_rounds);
|
|
290
|
+
}
|
|
291
|
+
export function getReviewStuckWindow(cwd = process.cwd()) {
|
|
292
|
+
const config = loadConfigFile(cwd);
|
|
293
|
+
return (parseNumberValue(config.neal?.review_stuck_window) ??
|
|
294
|
+
DEFAULT_CONFIG.neal.review_stuck_window);
|
|
295
|
+
}
|
|
296
|
+
export function getReviewStuckArbiterMaxAttempts(cwd = process.cwd()) {
|
|
297
|
+
const config = loadConfigFile(cwd);
|
|
298
|
+
const raw = parseNumberValue(config.neal?.review_stuck_arbiter_max_attempts) ??
|
|
299
|
+
DEFAULT_CONFIG.neal.review_stuck_arbiter_max_attempts;
|
|
300
|
+
// Clamp to a non-negative integer (0 disables the arbiter entirely).
|
|
301
|
+
if (!Number.isFinite(raw) || raw <= 0) {
|
|
302
|
+
return 0;
|
|
303
|
+
}
|
|
304
|
+
return Math.floor(raw);
|
|
305
|
+
}
|
|
306
|
+
export function getInteractiveBlockedRecoveryMaxTurns(cwd = process.cwd()) {
|
|
307
|
+
const config = loadConfigFile(cwd);
|
|
308
|
+
return (parseNumberValue(config.neal?.interactive_blocked_recovery_max_turns) ??
|
|
309
|
+
DEFAULT_CONFIG.neal.interactive_blocked_recovery_max_turns);
|
|
310
|
+
}
|
|
311
|
+
export function getFinalCompletionContinueExecutionMax(cwd = process.cwd()) {
|
|
312
|
+
const config = loadConfigFile(cwd);
|
|
313
|
+
return (parseNumberValue(config.neal?.final_completion_continue_execution_max) ??
|
|
314
|
+
DEFAULT_CONFIG.neal.final_completion_continue_execution_max);
|
|
315
|
+
}
|
|
316
|
+
export function getNotifyBin(cwd = process.cwd(), env = process.env) {
|
|
317
|
+
// NEAL_NOTIFY_BIN overrides config entirely: a non-empty value selects that
|
|
318
|
+
// helper, and a defined-but-empty value disables notifications. The test
|
|
319
|
+
// suite sets the empty form so test fixtures that resolve effective config
|
|
320
|
+
// (which falls through to ~/.neal/config.yml) never invoke the operator's
|
|
321
|
+
// real notification helper.
|
|
322
|
+
const envOverride = env.NEAL_NOTIFY_BIN;
|
|
323
|
+
if (envOverride !== undefined) {
|
|
324
|
+
return envOverride.trim() === '' ? null : envOverride;
|
|
325
|
+
}
|
|
326
|
+
const config = loadConfigFile(cwd);
|
|
327
|
+
return (parseStringValue(config.neal?.notify_bin) ??
|
|
328
|
+
DEFAULT_CONFIG.neal.notify_bin);
|
|
329
|
+
}
|
|
330
|
+
function parseOpenAICompatibleHeaders(value) {
|
|
331
|
+
if (value === undefined || value === null) {
|
|
332
|
+
return {};
|
|
333
|
+
}
|
|
334
|
+
if (typeof value !== 'object' || Array.isArray(value)) {
|
|
335
|
+
throw new Error('Invalid providers.openai_compatible.headers: expected a map of string header names to string values.');
|
|
336
|
+
}
|
|
337
|
+
const headers = {};
|
|
338
|
+
for (const [key, headerValue] of Object.entries(value)) {
|
|
339
|
+
if (!key.trim()) {
|
|
340
|
+
throw new Error('Invalid providers.openai_compatible.headers: header names must be non-empty strings.');
|
|
341
|
+
}
|
|
342
|
+
if (typeof headerValue !== 'string') {
|
|
343
|
+
throw new Error(`Invalid providers.openai_compatible.headers: value for "${key}" must be a string.`);
|
|
344
|
+
}
|
|
345
|
+
headers[key] = headerValue;
|
|
346
|
+
}
|
|
347
|
+
return headers;
|
|
348
|
+
}
|
|
349
|
+
export function getOpenAICompatibleSettings(cwd = process.cwd(), env = process.env) {
|
|
350
|
+
const config = loadConfigFile(cwd).providers?.openai_compatible;
|
|
351
|
+
const baseUrl = parseStringValue(config?.base_url) ??
|
|
352
|
+
parseStringValue(env.OPENAI_COMPATIBLE_BASE_URL) ??
|
|
353
|
+
null;
|
|
354
|
+
const apiKeyEnv = parseStringValue(config?.api_key_env) ?? OPENAI_COMPATIBLE_DEFAULT_API_KEY_ENV;
|
|
355
|
+
const apiKey = parseStringValue(env[apiKeyEnv]) ?? null;
|
|
356
|
+
const defaultModel = parseStringValue(config?.default_model) ??
|
|
357
|
+
parseStringValue(env.OPENAI_COMPATIBLE_MODEL) ??
|
|
358
|
+
null;
|
|
359
|
+
const headers = parseOpenAICompatibleHeaders(config?.headers);
|
|
360
|
+
return {
|
|
361
|
+
baseUrl,
|
|
362
|
+
apiKeyEnv,
|
|
363
|
+
apiKey,
|
|
364
|
+
defaultModel,
|
|
365
|
+
headers,
|
|
366
|
+
};
|
|
367
|
+
}
|
|
368
|
+
export function getDefaultCoderProvider(cwd = process.cwd()) {
|
|
369
|
+
const config = loadConfigFile(cwd);
|
|
370
|
+
return (parseConfigProviderValue(config.agent?.coder?.provider, 'agent.coder.provider') ??
|
|
371
|
+
DEFAULT_CONFIG.agent.coder.provider);
|
|
372
|
+
}
|
|
373
|
+
export function getDefaultCoderModel(cwd = process.cwd()) {
|
|
374
|
+
const config = loadConfigFile(cwd);
|
|
375
|
+
return config.agent?.coder?.model === null
|
|
376
|
+
? null
|
|
377
|
+
: (parseStringValue(config.agent?.coder?.model) ?? DEFAULT_CONFIG.agent.coder.model);
|
|
378
|
+
}
|
|
379
|
+
export function getConfiguredUnattended(cwd = process.cwd()) {
|
|
380
|
+
const config = loadConfigFile(cwd);
|
|
381
|
+
return typeof config.agent?.unattended === 'boolean'
|
|
382
|
+
? config.agent.unattended
|
|
383
|
+
: DEFAULT_CONFIG.agent.unattended;
|
|
384
|
+
}
|
|
385
|
+
export function getDefaultPlannerProvider(cwd = process.cwd()) {
|
|
386
|
+
const config = loadConfigFile(cwd);
|
|
387
|
+
return (parseConfigProviderValue(config.agent?.planner?.provider, 'agent.planner.provider') ??
|
|
388
|
+
getDefaultCoderProvider(cwd));
|
|
389
|
+
}
|
|
390
|
+
export function getDefaultPlannerModel(cwd = process.cwd()) {
|
|
391
|
+
const config = loadConfigFile(cwd);
|
|
392
|
+
const plannerProvider = parseConfigProviderValue(config.agent?.planner?.provider, 'agent.planner.provider');
|
|
393
|
+
if (hasOwnConfigKey(config.agent?.planner, 'model')) {
|
|
394
|
+
return config.agent.planner.model === null
|
|
395
|
+
? null
|
|
396
|
+
: (parseStringValue(config.agent.planner.model) ?? null);
|
|
397
|
+
}
|
|
398
|
+
if (plannerProvider) {
|
|
399
|
+
return DEFAULT_CONFIG.agent.planner.model;
|
|
400
|
+
}
|
|
401
|
+
return getDefaultCoderModel(cwd);
|
|
402
|
+
}
|
|
403
|
+
export function getDefaultReviewerProvider(cwd = process.cwd()) {
|
|
404
|
+
const config = loadConfigFile(cwd);
|
|
405
|
+
return (parseConfigProviderValue(config.agent?.reviewer?.provider, 'agent.reviewer.provider') ??
|
|
406
|
+
DEFAULT_CONFIG.agent.reviewer.provider);
|
|
407
|
+
}
|
|
408
|
+
export function getDefaultReviewerModel(cwd = process.cwd()) {
|
|
409
|
+
const config = loadConfigFile(cwd);
|
|
410
|
+
return config.agent?.reviewer?.model === null
|
|
411
|
+
? null
|
|
412
|
+
: (parseStringValue(config.agent?.reviewer?.model) ?? DEFAULT_CONFIG.agent.reviewer.model);
|
|
413
|
+
}
|
|
414
|
+
export function getDefaultCoderEffort(cwd = process.cwd()) {
|
|
415
|
+
const config = loadConfigFile(cwd);
|
|
416
|
+
return parseStringValue(config.agent?.coder?.effort) ?? null;
|
|
417
|
+
}
|
|
418
|
+
export function getDefaultReviewerEffort(cwd = process.cwd()) {
|
|
419
|
+
const config = loadConfigFile(cwd);
|
|
420
|
+
return parseStringValue(config.agent?.reviewer?.effort) ?? null;
|
|
421
|
+
}
|
|
422
|
+
export function getDefaultPlannerEffort(cwd = process.cwd()) {
|
|
423
|
+
const config = loadConfigFile(cwd);
|
|
424
|
+
const plannerProvider = parseConfigProviderValue(config.agent?.planner?.provider, 'agent.planner.provider');
|
|
425
|
+
if (hasOwnConfigKey(config.agent?.planner, 'effort')) {
|
|
426
|
+
return parseStringValue(config.agent.planner.effort) ?? null;
|
|
427
|
+
}
|
|
428
|
+
if (plannerProvider) {
|
|
429
|
+
return null;
|
|
430
|
+
}
|
|
431
|
+
return getDefaultCoderEffort(cwd);
|
|
432
|
+
}
|