@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,172 @@
|
|
|
1
|
+
const DIFF_PREVIEW_LIMIT = 12000;
|
|
2
|
+
export const REVIEW_FINDINGS_READ_ONLY_RULES = [
|
|
3
|
+
'Do not mutate the repository.',
|
|
4
|
+
'Do not make commits, amend commits, rebase, reset, squash, or rewrite history.',
|
|
5
|
+
'Do not edit source files, Neal writer-run state, queue files, current-run pointers, or run-local RUN_STATE.json files.',
|
|
6
|
+
'Do not run shell write commands or claim that fixes were applied.',
|
|
7
|
+
'Review only the frozen selected commit range and the supplied instruction.',
|
|
8
|
+
'The checkout may advance while this review is running; do not treat current HEAD or dirty worktree files as part of the review target unless they are in the supplied resolved range.',
|
|
9
|
+
'Produce findings only; do not perform repair work.',
|
|
10
|
+
];
|
|
11
|
+
/**
|
|
12
|
+
* Read-only range-inspection section appended to the adjudication prompt for
|
|
13
|
+
* reviewers with read tool access but no shell (the 'read-only' doctrine
|
|
14
|
+
* access mode). The base adjudication prompt previews at most
|
|
15
|
+
* DIFF_PREVIEW_LIMIT characters of the resolved-range diff; a read-only
|
|
16
|
+
* reviewer cannot run git commands, so the read-only git_diff tool is its
|
|
17
|
+
* only source of truth for what the range actually changed (deletions and
|
|
18
|
+
* renames included). The section names the exact resolved revisions so the
|
|
19
|
+
* reviewer does not have to parse them back out of the context summary, and
|
|
20
|
+
* only claims the preview is truncated when it actually is.
|
|
21
|
+
*/
|
|
22
|
+
export function buildReviewFindingsReadOnlyInspectionSection(context) {
|
|
23
|
+
const previewTruncated = context.diff.trim() !== '' && context.diff.length > DIFF_PREVIEW_LIMIT;
|
|
24
|
+
const sourceOfTruthLine = previewTruncated
|
|
25
|
+
? 'You have read-only repository tools: read_file, list_dir, grep, and git_diff. The diff preview above is truncated; the git_diff tool is the source of truth for what the resolved range actually changed, including deletions and renames that head-state file reads cannot reveal.'
|
|
26
|
+
: 'You have read-only repository tools: read_file, list_dir, grep, and git_diff. The git_diff tool is the source of truth for what the resolved range actually changed, including deletions and renames that head-state file reads cannot reveal.';
|
|
27
|
+
return [
|
|
28
|
+
'## Read-Only Range Inspection',
|
|
29
|
+
'',
|
|
30
|
+
sourceOfTruthLine,
|
|
31
|
+
`Before returning a verdict, inspect the resolved range with git_diff: first call git_diff with base ${context.externalBaseCommit}, head ${context.externalHeadCommit}, and stat:true for the changed-file overview, then request per-path diffs for the files the draft findings cite.`,
|
|
32
|
+
'Verify draft claims against the surrounding code with read_file and grep; absence from a diff hunk is not evidence of absence from the repository.',
|
|
33
|
+
].join('\n');
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Inlined-diff section appended to the adjudication prompt for a read-only
|
|
37
|
+
* reviewer that has read tools but no commit-range diff tool (native
|
|
38
|
+
* Claude/Codex). The base adjudication prompt only previews at most
|
|
39
|
+
* DIFF_PREVIEW_LIMIT characters of the resolved-range diff and such a reviewer
|
|
40
|
+
* can neither run git commands nor call a git_diff tool, so Neal inlines the
|
|
41
|
+
* full selected-range diff here as the source of truth and instructs only the
|
|
42
|
+
* read tools the provider actually has. Reviewers that expose a commit-range
|
|
43
|
+
* diff tool get buildReviewFindingsReadOnlyInspectionSection instead.
|
|
44
|
+
*/
|
|
45
|
+
export function buildReviewFindingsInlinedDiffSection(context) {
|
|
46
|
+
return [
|
|
47
|
+
'## Inlined Selected-Range Diff',
|
|
48
|
+
'',
|
|
49
|
+
'You have read-only repository tools (read and search, no shell) but no commit-range diff tool, so the full selected-range diff is inlined below.',
|
|
50
|
+
'This inlined diff is the source of truth for what the resolved range actually changed, including deletions and renames that head-state file reads cannot reveal.',
|
|
51
|
+
`Resolved range: ${context.externalBaseCommit}..${context.externalHeadCommit}.`,
|
|
52
|
+
'Verify draft claims against the surrounding code with your read tools; absence from a diff hunk is not evidence of absence from the repository.',
|
|
53
|
+
'',
|
|
54
|
+
context.diff.trim() === '' ? '(empty diff)' : context.diff,
|
|
55
|
+
].join('\n');
|
|
56
|
+
}
|
|
57
|
+
export function buildReviewFindingsDraftPrompt(context, draftContext = {}) {
|
|
58
|
+
const round = draftContext.round ?? 1;
|
|
59
|
+
const reviewFindings = draftContext.reviewFindings ?? [];
|
|
60
|
+
return [
|
|
61
|
+
'# Neal Review Findings Draft',
|
|
62
|
+
'',
|
|
63
|
+
`You are drafting round ${round}${draftContext.maxRounds ? `/${draftContext.maxRounds}` : ''} of a read-only \`neal review\` findings artifact.`,
|
|
64
|
+
'',
|
|
65
|
+
'## Read-Only Rules',
|
|
66
|
+
'',
|
|
67
|
+
...REVIEW_FINDINGS_READ_ONLY_RULES.map((rule) => `- ${rule}`),
|
|
68
|
+
...(reviewFindings.length > 0
|
|
69
|
+
? [
|
|
70
|
+
'',
|
|
71
|
+
'## Required Revision Findings',
|
|
72
|
+
'',
|
|
73
|
+
...reviewFindings.map((finding) => `- ${finding}`),
|
|
74
|
+
'',
|
|
75
|
+
'## Previous Draft',
|
|
76
|
+
'',
|
|
77
|
+
renderDraftSummary(draftContext.previousDraft),
|
|
78
|
+
]
|
|
79
|
+
: []),
|
|
80
|
+
'',
|
|
81
|
+
'## Review Instruction',
|
|
82
|
+
'',
|
|
83
|
+
context.instruction,
|
|
84
|
+
'',
|
|
85
|
+
'## Review Target',
|
|
86
|
+
'',
|
|
87
|
+
renderContextSummary(context),
|
|
88
|
+
'',
|
|
89
|
+
'Return a summary, concrete findings, and warnings only. Each finding needs severity, files, claim, evidence, and requiredAction. Do not suggest that Neal applied fixes.',
|
|
90
|
+
].join('\n');
|
|
91
|
+
}
|
|
92
|
+
export function buildReviewFindingsReviewPrompt(context, draft, round = 1) {
|
|
93
|
+
return [
|
|
94
|
+
'# Neal Review Findings Review',
|
|
95
|
+
'',
|
|
96
|
+
`Review findings draft round ${round} for missing important findings, weak evidence, false positives, wrong severity, unclear required actions, and insufficient test or integration analysis.`,
|
|
97
|
+
'',
|
|
98
|
+
'## Read-Only Rules',
|
|
99
|
+
'',
|
|
100
|
+
...REVIEW_FINDINGS_READ_ONLY_RULES.map((rule) => `- ${rule}`),
|
|
101
|
+
'',
|
|
102
|
+
'## Review Instruction',
|
|
103
|
+
'',
|
|
104
|
+
context.instruction,
|
|
105
|
+
'',
|
|
106
|
+
'## Review Target',
|
|
107
|
+
'',
|
|
108
|
+
renderContextSummary(context),
|
|
109
|
+
'',
|
|
110
|
+
'## Draft Findings',
|
|
111
|
+
'',
|
|
112
|
+
renderDraftSummary(draft),
|
|
113
|
+
'',
|
|
114
|
+
'Return verdict=`accepted` only when the final findings artifact is ready. Return verdict=`revise` with concrete findings when another draft is required. Return verdict=`blocked` only when a safe read-only review cannot be produced. Keep accepted finalMarkdown read-only and artifact-ready. Return empty strings for finalMarkdown or blockedReason when they do not apply.',
|
|
115
|
+
].join('\n');
|
|
116
|
+
}
|
|
117
|
+
function renderContextSummary(context) {
|
|
118
|
+
return [
|
|
119
|
+
`Selector: ${formatSelector(context.selector)}`,
|
|
120
|
+
`Resolved range: ${context.externalBaseCommit}..${context.externalHeadCommit}`,
|
|
121
|
+
'Range stability: frozen at review startup; current HEAD/worktree may move independently.',
|
|
122
|
+
`Commit count: ${context.externalCommits.length}`,
|
|
123
|
+
'',
|
|
124
|
+
'Commits:',
|
|
125
|
+
...renderList(context.externalCommitSubjects),
|
|
126
|
+
'',
|
|
127
|
+
'Changed files:',
|
|
128
|
+
...renderList(context.externalChangedFiles),
|
|
129
|
+
'',
|
|
130
|
+
'Diff stat:',
|
|
131
|
+
context.diffStat.trim() || '(none)',
|
|
132
|
+
'',
|
|
133
|
+
'Diff preview:',
|
|
134
|
+
renderDiffPreview(context.diff),
|
|
135
|
+
].join('\n');
|
|
136
|
+
}
|
|
137
|
+
function formatSelector(selector) {
|
|
138
|
+
switch (selector.kind) {
|
|
139
|
+
case 'last':
|
|
140
|
+
return `--last ${selector.count}`;
|
|
141
|
+
case 'since':
|
|
142
|
+
return `--since ${selector.baseRef}`;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function renderDraftSummary(draft) {
|
|
146
|
+
if (!draft) {
|
|
147
|
+
return '(none)';
|
|
148
|
+
}
|
|
149
|
+
return [
|
|
150
|
+
`Summary: ${draft.summary}`,
|
|
151
|
+
'',
|
|
152
|
+
'Findings:',
|
|
153
|
+
...(draft.findings.length === 0
|
|
154
|
+
? ['- (none)']
|
|
155
|
+
: draft.findings.map((finding, index) => `- F${index + 1} [${finding.severity}] ${finding.claim} Files: ${finding.files.join(', ') || '(none)'} Evidence: ${finding.evidence} Required action: ${finding.requiredAction}`)),
|
|
156
|
+
'',
|
|
157
|
+
'Warnings:',
|
|
158
|
+
...renderList(draft.warnings ?? []),
|
|
159
|
+
].join('\n');
|
|
160
|
+
}
|
|
161
|
+
function renderList(items) {
|
|
162
|
+
return items.length > 0 ? items.map((item) => `- ${item}`) : ['- (none)'];
|
|
163
|
+
}
|
|
164
|
+
function renderDiffPreview(diff) {
|
|
165
|
+
if (!diff.trim()) {
|
|
166
|
+
return '(none)';
|
|
167
|
+
}
|
|
168
|
+
if (diff.length <= DIFF_PREVIEW_LIMIT) {
|
|
169
|
+
return diff;
|
|
170
|
+
}
|
|
171
|
+
return `${diff.slice(0, DIFF_PREVIEW_LIMIT).trimEnd()}\n\n[diff truncated to ${DIFF_PREVIEW_LIMIT} characters for review prompt]`;
|
|
172
|
+
}
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getDefaultCoderModel, getDefaultCoderProvider, getDefaultReviewerModel, getDefaultReviewerProvider, getInactivityTimeoutMs, } from '../config.js';
|
|
2
|
+
import { runCoderStructuredPrompt } from '../agents/structured-coder.js';
|
|
3
|
+
import { createInlineSection, getReviewerDoctrineAccessMode, readOnlyReviewerNeedsInlinedDiff, renderInlineReviewerContext, reviewerNeedsInlineContext, } from '../context/inline-review-context.js';
|
|
4
|
+
import { runWithAgentTurnLiveness } from '../providers/liveness.js';
|
|
5
|
+
import { assertProviderSupportsCoder, assertProviderSupportsStructuredAdvisor, getStructuredAdvisorAdapter, } from '../providers/registry.js';
|
|
6
|
+
import { createProviderTelemetrySink } from '../providers/telemetry.js';
|
|
7
|
+
import { buildReviewFindingsInlinedDiffSection, buildReviewFindingsReadOnlyInspectionSection, } from './prompts.js';
|
|
8
|
+
// An accepted (read-only) review must not claim the reviewer mutated code, nor
|
|
9
|
+
// instruct mutating commands. Pattern #1 requires a first-person agency subject
|
|
10
|
+
// ("I/we … applied/made/committed … a fix/change"), so it flags self-attribution
|
|
11
|
+
// without rejecting benign passive descriptions of the coder's change under review
|
|
12
|
+
// (e.g. "the fix was applied and tests pass" — a thing a read-only reviewer may say).
|
|
13
|
+
// The earlier `(fix) (was|has|applied|…)` form false-positived on that passive
|
|
14
|
+
// phrasing, failing otherwise-capable review models. #2-#5 catch explicit commit
|
|
15
|
+
// claims and mutating git/neal command instructions regardless of subject.
|
|
16
|
+
const FORBIDDEN_ACCEPTED_TEXT = [
|
|
17
|
+
/\b(?:i|we)\s+(?:\w+\s+){0,2}(?:applied|made|wrote|added|introduced|implemented|created|committed)\s+(?:\w+\s+){0,2}(?:fix|fixes|repair|repairs|change|changes|edit|edits|patch|patches|commit|correction)\b/i,
|
|
18
|
+
/\bcreated\s+(an?\s+)?(additive\s+)?fix\s+commit\b/i,
|
|
19
|
+
/\bcommitted\s+(the\s+)?fix\b/i,
|
|
20
|
+
/\b(run|execute|start|invoke|call|use)\s+`?git\s+(commit|reset|rebase|squash|checkout|switch|merge|push)\b/i,
|
|
21
|
+
/\b(run|execute|start|invoke|call|use)\s+`?neal\s+(execute|run|resume|squash)\b/i,
|
|
22
|
+
];
|
|
23
|
+
class AgentReviewFindingsProviderAdapter {
|
|
24
|
+
args;
|
|
25
|
+
constructor(args) {
|
|
26
|
+
this.args = args;
|
|
27
|
+
assertReviewAgentCapabilities(args.agentConfig);
|
|
28
|
+
}
|
|
29
|
+
async draftFindings(args) {
|
|
30
|
+
const schema = buildReviewFindingsDraftSchema();
|
|
31
|
+
const result = await runCoderStructuredPrompt({
|
|
32
|
+
coder: this.args.agentConfig.coder,
|
|
33
|
+
cwd: this.args.cwd,
|
|
34
|
+
prompt: args.prompt,
|
|
35
|
+
schema,
|
|
36
|
+
label: 'Review findings draft',
|
|
37
|
+
structuredJsonProtocol: buildReviewFindingsProtocolSpec({
|
|
38
|
+
schemaLabel: 'review_findings_draft_payload',
|
|
39
|
+
schema,
|
|
40
|
+
validator: validateReviewFindingsDraft,
|
|
41
|
+
}),
|
|
42
|
+
logger: this.args.logger,
|
|
43
|
+
});
|
|
44
|
+
args.onSessionHandle?.(result.sessionHandle ?? null);
|
|
45
|
+
return validateReviewFindingsDraft(result.structured);
|
|
46
|
+
}
|
|
47
|
+
async reviewDraft(args) {
|
|
48
|
+
const reviewer = getStructuredAdvisorAdapter(this.args.agentConfig.reviewer);
|
|
49
|
+
const schema = buildReviewFindingsReviewSchema();
|
|
50
|
+
// The base adjudication prompt only previews the diff. A reviewer without
|
|
51
|
+
// repository read access gets the full selected diff and the draft
|
|
52
|
+
// findings artifact inlined so it can adjudicate entirely from the
|
|
53
|
+
// prompt. A read-only reviewer (read tools, no shell) that exposes its own
|
|
54
|
+
// commit-range diff tool gets the read-only range-inspection section
|
|
55
|
+
// directing it to the git_diff tool with the exact resolved revisions; a
|
|
56
|
+
// read-only reviewer without a commit-range diff tool (native Claude/Codex)
|
|
57
|
+
// instead gets the full selected diff inlined and no git_diff-tool
|
|
58
|
+
// instruction, because it can neither run git commands, call a git_diff
|
|
59
|
+
// tool, nor rely on the truncated preview. Tool-access reviewers keep the
|
|
60
|
+
// built prompt byte-identical.
|
|
61
|
+
const accessMode = getReviewerDoctrineAccessMode(this.args.agentConfig.reviewer);
|
|
62
|
+
const prompt = reviewerNeedsInlineContext(this.args.agentConfig.reviewer)
|
|
63
|
+
? `${args.prompt}\n\n${renderInlineReviewerContext(buildReviewFindingsReviewInlineContext(args))}`
|
|
64
|
+
: accessMode === 'read-only'
|
|
65
|
+
? readOnlyReviewerNeedsInlinedDiff(this.args.agentConfig.reviewer)
|
|
66
|
+
? `${args.prompt}\n\n${buildReviewFindingsInlinedDiffSection(args.context)}`
|
|
67
|
+
: `${args.prompt}\n\n${buildReviewFindingsReadOnlyInspectionSection(args.context)}`
|
|
68
|
+
: args.prompt;
|
|
69
|
+
const result = await runWithAgentTurnLiveness({
|
|
70
|
+
provider: this.args.agentConfig.reviewer.provider,
|
|
71
|
+
role: 'structured-advisor',
|
|
72
|
+
label: 'review-findings',
|
|
73
|
+
startupTimeoutMs: Math.min(getAgentTurnStartupTimeoutMs(this.args.cwd), getInactivityTimeoutMs(this.args.cwd)),
|
|
74
|
+
// Structured-advisor turns retry locally: they are non-writing by policy
|
|
75
|
+
// and do not resume sessions at this call site.
|
|
76
|
+
retryLimit: getAgentTurnRetryLimit(this.args.cwd),
|
|
77
|
+
logger: this.args.logger,
|
|
78
|
+
baseSink: createProviderTelemetrySink({
|
|
79
|
+
logger: this.args.logger,
|
|
80
|
+
provider: this.args.agentConfig.reviewer.provider,
|
|
81
|
+
role: 'structured-advisor',
|
|
82
|
+
label: 'review-findings',
|
|
83
|
+
cwd: this.args.cwd,
|
|
84
|
+
}),
|
|
85
|
+
run: (events, attempt) => reviewer.runStructuredRound({
|
|
86
|
+
label: 'review-findings',
|
|
87
|
+
cwd: this.args.cwd,
|
|
88
|
+
prompt,
|
|
89
|
+
schema,
|
|
90
|
+
structuredJsonProtocol: buildReviewFindingsProtocolSpec({
|
|
91
|
+
schemaLabel: 'review_findings_review_payload',
|
|
92
|
+
schema,
|
|
93
|
+
validator: validateReviewFindingsReview,
|
|
94
|
+
}),
|
|
95
|
+
inactivityTimeoutMs: getInactivityTimeoutMs(this.args.cwd),
|
|
96
|
+
apiRetryLimit: getApiRetryLimit(this.args.cwd),
|
|
97
|
+
signal: attempt.signal,
|
|
98
|
+
events,
|
|
99
|
+
}),
|
|
100
|
+
});
|
|
101
|
+
args.onSessionHandle?.(result.sessionHandle ?? null);
|
|
102
|
+
return validateReviewFindingsReview(result.structured);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
function buildReviewFindingsReviewInlineContext(args) {
|
|
106
|
+
return {
|
|
107
|
+
sections: [
|
|
108
|
+
createInlineSection(`Full selected diff for range ${args.context.externalBaseCommit}..${args.context.externalHeadCommit}`, args.context.diff || '(empty diff)'),
|
|
109
|
+
createInlineSection('Draft findings artifact under adjudication', JSON.stringify(args.draft, null, 2)),
|
|
110
|
+
],
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
function buildReviewFindingsProtocolSpec(args) {
|
|
114
|
+
return {
|
|
115
|
+
protocol: 'neal-json-block-v1',
|
|
116
|
+
schemaLabel: args.schemaLabel,
|
|
117
|
+
schema: args.schema,
|
|
118
|
+
validator: args.validator,
|
|
119
|
+
repairAttemptLimit: 2,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
122
|
+
export function getDefaultReviewAgentConfig(cwd) {
|
|
123
|
+
return {
|
|
124
|
+
planner: {
|
|
125
|
+
provider: getDefaultCoderProvider(cwd),
|
|
126
|
+
model: getDefaultCoderModel(cwd),
|
|
127
|
+
},
|
|
128
|
+
coder: {
|
|
129
|
+
provider: getDefaultCoderProvider(cwd),
|
|
130
|
+
model: getDefaultCoderModel(cwd),
|
|
131
|
+
},
|
|
132
|
+
reviewer: {
|
|
133
|
+
provider: getDefaultReviewerProvider(cwd),
|
|
134
|
+
model: getDefaultReviewerModel(cwd),
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export function createAgentReviewFindingsProviderAdapter(args) {
|
|
139
|
+
return new AgentReviewFindingsProviderAdapter({
|
|
140
|
+
cwd: args.cwd,
|
|
141
|
+
agentConfig: args.agentConfig ?? getDefaultReviewAgentConfig(args.cwd),
|
|
142
|
+
logger: args.logger,
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
function assertReviewAgentCapabilities(agentConfig) {
|
|
146
|
+
assertProviderSupportsCoder(agentConfig.coder, {
|
|
147
|
+
role: 'coder',
|
|
148
|
+
context: 'neal review',
|
|
149
|
+
reason: 'neal review uses the configured coder provider to inspect the selected committed range and draft structured findings',
|
|
150
|
+
requireReadToolAccess: true,
|
|
151
|
+
requireStructuredOutput: true,
|
|
152
|
+
reasons: {
|
|
153
|
+
coder_adapter: 'the review findings draft starts through the configured coder adapter',
|
|
154
|
+
read_tool_access: 'the coder must inspect the selected diff and repository context without editing it',
|
|
155
|
+
structured_output: 'the review findings draft must be schema-validated before reviewer adjudication',
|
|
156
|
+
model_override: 'a non-null coder model override is configured for review drafting',
|
|
157
|
+
},
|
|
158
|
+
});
|
|
159
|
+
assertProviderSupportsStructuredAdvisor(agentConfig.reviewer, {
|
|
160
|
+
role: 'reviewer',
|
|
161
|
+
context: 'neal review',
|
|
162
|
+
reason: 'neal review uses the configured reviewer provider to adjudicate and accept or revise the findings artifact',
|
|
163
|
+
requireReadToolAccessOrInlineReviewerContext: true,
|
|
164
|
+
requireStructuredOutput: true,
|
|
165
|
+
reasons: {
|
|
166
|
+
structured_advisor_adapter: 'review findings adjudication runs through the configured structured-advisor adapter',
|
|
167
|
+
read_tool_access_or_inline_reviewer_context: 'the reviewer must either inspect the draft, selected diff, and repository context directly with read tools, or support structured output so Neal can inline the draft findings artifact and the full selected diff into the adjudication prompt',
|
|
168
|
+
structured_output: 'the reviewer verdict must be schema-validated before Neal accepts the findings artifact',
|
|
169
|
+
model_override: 'a non-null reviewer model override is configured for review adjudication',
|
|
170
|
+
},
|
|
171
|
+
});
|
|
172
|
+
}
|
|
173
|
+
function buildReviewFindingSchema() {
|
|
174
|
+
return {
|
|
175
|
+
type: 'object',
|
|
176
|
+
properties: {
|
|
177
|
+
severity: { type: 'string', enum: ['blocking', 'non_blocking'] },
|
|
178
|
+
files: { type: 'array', items: { type: 'string' } },
|
|
179
|
+
claim: { type: 'string' },
|
|
180
|
+
evidence: { type: 'string' },
|
|
181
|
+
requiredAction: { type: 'string' },
|
|
182
|
+
},
|
|
183
|
+
required: ['severity', 'files', 'claim', 'evidence', 'requiredAction'],
|
|
184
|
+
additionalProperties: false,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
function buildReviewFindingsDraftSchema() {
|
|
188
|
+
return {
|
|
189
|
+
type: 'object',
|
|
190
|
+
properties: {
|
|
191
|
+
summary: { type: 'string' },
|
|
192
|
+
findings: {
|
|
193
|
+
type: 'array',
|
|
194
|
+
items: buildReviewFindingSchema(),
|
|
195
|
+
},
|
|
196
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
197
|
+
},
|
|
198
|
+
required: ['summary', 'findings', 'warnings'],
|
|
199
|
+
additionalProperties: false,
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
function buildReviewFindingsReviewSchema() {
|
|
203
|
+
return {
|
|
204
|
+
type: 'object',
|
|
205
|
+
properties: {
|
|
206
|
+
verdict: { type: 'string', enum: ['accepted', 'revise', 'blocked'] },
|
|
207
|
+
findings: { type: 'array', items: { type: 'string' } },
|
|
208
|
+
finalMarkdown: { type: 'string' },
|
|
209
|
+
blockedReason: { type: 'string' },
|
|
210
|
+
warnings: { type: 'array', items: { type: 'string' } },
|
|
211
|
+
},
|
|
212
|
+
required: ['verdict', 'findings', 'finalMarkdown', 'blockedReason', 'warnings'],
|
|
213
|
+
additionalProperties: false,
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
export function validateReviewFindingsDraft(draft) {
|
|
217
|
+
if (!draft || typeof draft !== 'object') {
|
|
218
|
+
throw new Error('Review provider returned an invalid draft object');
|
|
219
|
+
}
|
|
220
|
+
const raw = draft;
|
|
221
|
+
if (typeof raw.summary !== 'string' || raw.summary.trim() === '') {
|
|
222
|
+
throw new Error('Review provider returned an empty draft summary');
|
|
223
|
+
}
|
|
224
|
+
return {
|
|
225
|
+
summary: raw.summary.trim(),
|
|
226
|
+
findings: validateFindingItems(raw.findings),
|
|
227
|
+
warnings: validateStringArray(raw.warnings),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
export function validateReviewFindingsReview(review) {
|
|
231
|
+
if (!review || typeof review !== 'object') {
|
|
232
|
+
throw new Error('Review provider returned an invalid review object');
|
|
233
|
+
}
|
|
234
|
+
const raw = review;
|
|
235
|
+
if (raw.verdict !== 'accepted' && raw.verdict !== 'revise' && raw.verdict !== 'blocked') {
|
|
236
|
+
throw new Error('Review provider returned an invalid review verdict');
|
|
237
|
+
}
|
|
238
|
+
const findings = validateStringArray(raw.findings);
|
|
239
|
+
if (raw.verdict === 'revise' && findings.length === 0) {
|
|
240
|
+
throw new Error('Review provider requested review revision without findings');
|
|
241
|
+
}
|
|
242
|
+
if (raw.verdict === 'blocked' &&
|
|
243
|
+
findings.length === 0 &&
|
|
244
|
+
(typeof raw.blockedReason !== 'string' || raw.blockedReason.trim() === '')) {
|
|
245
|
+
throw new Error('Review provider blocked without findings or a blocked reason');
|
|
246
|
+
}
|
|
247
|
+
const finalMarkdown = typeof raw.finalMarkdown === 'string' && raw.finalMarkdown.trim() !== ''
|
|
248
|
+
? raw.finalMarkdown.trim()
|
|
249
|
+
: undefined;
|
|
250
|
+
if (finalMarkdown && FORBIDDEN_ACCEPTED_TEXT.some((pattern) => pattern.test(finalMarkdown))) {
|
|
251
|
+
throw new Error('Review provider returned accepted final markdown with mutation or repair-commit wording');
|
|
252
|
+
}
|
|
253
|
+
return {
|
|
254
|
+
verdict: raw.verdict,
|
|
255
|
+
findings,
|
|
256
|
+
finalMarkdown,
|
|
257
|
+
blockedReason: typeof raw.blockedReason === 'string' && raw.blockedReason.trim() !== ''
|
|
258
|
+
? raw.blockedReason.trim()
|
|
259
|
+
: undefined,
|
|
260
|
+
warnings: validateStringArray(raw.warnings),
|
|
261
|
+
};
|
|
262
|
+
}
|
|
263
|
+
export function renderReviewFindingsFinalMarkdown(context, draft, review) {
|
|
264
|
+
return [
|
|
265
|
+
'# Review Findings',
|
|
266
|
+
'',
|
|
267
|
+
'## Summary',
|
|
268
|
+
'',
|
|
269
|
+
draft.summary,
|
|
270
|
+
'',
|
|
271
|
+
'## Range',
|
|
272
|
+
'',
|
|
273
|
+
`- Base: ${context.externalBaseCommit}`,
|
|
274
|
+
`- Head: ${context.externalHeadCommit}`,
|
|
275
|
+
`- Commits: ${context.externalCommits.length}`,
|
|
276
|
+
'',
|
|
277
|
+
'## Findings',
|
|
278
|
+
'',
|
|
279
|
+
...formatFindings(draft.findings),
|
|
280
|
+
'',
|
|
281
|
+
'## Review Acceptance',
|
|
282
|
+
'',
|
|
283
|
+
`- Verdict: ${review.verdict}`,
|
|
284
|
+
`- Revision findings: ${review.findings.length === 0 ? 'none' : review.findings.join('; ')}`,
|
|
285
|
+
`- Warnings: ${(review.warnings ?? []).length === 0 ? 'none' : (review.warnings ?? []).join('; ')}`,
|
|
286
|
+
].join('\n') + '\n';
|
|
287
|
+
}
|
|
288
|
+
function validateFindingItems(value) {
|
|
289
|
+
if (!Array.isArray(value)) {
|
|
290
|
+
return [];
|
|
291
|
+
}
|
|
292
|
+
const findings = [];
|
|
293
|
+
for (const [index, item] of value.entries()) {
|
|
294
|
+
if (!item || typeof item !== 'object') {
|
|
295
|
+
throw new Error(`Review provider returned invalid finding at index ${index}`);
|
|
296
|
+
}
|
|
297
|
+
const raw = item;
|
|
298
|
+
if (raw.severity !== 'blocking' && raw.severity !== 'non_blocking') {
|
|
299
|
+
throw new Error(`Review provider returned invalid finding severity at index ${index}`);
|
|
300
|
+
}
|
|
301
|
+
findings.push({
|
|
302
|
+
severity: raw.severity,
|
|
303
|
+
files: validateStringArray(raw.files),
|
|
304
|
+
claim: typeof raw.claim === 'string' ? raw.claim.trim() : '',
|
|
305
|
+
evidence: typeof raw.evidence === 'string' ? raw.evidence.trim() : '',
|
|
306
|
+
requiredAction: typeof raw.requiredAction === 'string' ? raw.requiredAction.trim() : '',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
return findings;
|
|
310
|
+
}
|
|
311
|
+
function validateStringArray(value) {
|
|
312
|
+
if (!Array.isArray(value)) {
|
|
313
|
+
return [];
|
|
314
|
+
}
|
|
315
|
+
return value.filter((item) => typeof item === 'string' && item.trim() !== '').map((item) => item.trim());
|
|
316
|
+
}
|
|
317
|
+
function formatFindings(findings) {
|
|
318
|
+
if (findings.length === 0) {
|
|
319
|
+
return ['No findings were identified in the selected range.'];
|
|
320
|
+
}
|
|
321
|
+
return findings.flatMap((finding, index) => [
|
|
322
|
+
`### F${index + 1}: ${finding.claim}`,
|
|
323
|
+
'',
|
|
324
|
+
`- Severity: ${finding.severity}`,
|
|
325
|
+
`- Files: ${finding.files.length === 0 ? '(none)' : finding.files.join(', ')}`,
|
|
326
|
+
`- Evidence: ${finding.evidence}`,
|
|
327
|
+
`- Required action: ${finding.requiredAction}`,
|
|
328
|
+
'',
|
|
329
|
+
]);
|
|
330
|
+
}
|