@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
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { anthropicClaudeProviderDefinition } from './anthropic-claude.js';
|
|
2
|
-
import { genericAgenticProviderDefinition } from './generic-agentic.js';
|
|
3
|
-
import { openAICodexProviderDefinition } from './openai-codex.js';
|
|
4
2
|
import { openAICompatibleProviderDefinition } from './openai-compatible.js';
|
|
3
|
+
import { openAICodexProviderDefinition } from './openai-codex.js';
|
|
5
4
|
// Computed lazily instead of as a top-level constant: openai-compatible.ts
|
|
6
5
|
// imports config.ts, which imports this module, so the definition bindings may
|
|
7
6
|
// still be in their temporal dead zone while this module body evaluates.
|
|
@@ -12,7 +11,6 @@ function getBuiltInProviderDefinitions() {
|
|
|
12
11
|
openAICodexProviderDefinition,
|
|
13
12
|
anthropicClaudeProviderDefinition,
|
|
14
13
|
openAICompatibleProviderDefinition,
|
|
15
|
-
genericAgenticProviderDefinition,
|
|
16
14
|
];
|
|
17
15
|
}
|
|
18
16
|
const providerCapabilityOverrides = new Map();
|
|
@@ -108,15 +106,6 @@ function assertRoleCapabilities(args) {
|
|
|
108
106
|
missingCapability: 'read_tool_access',
|
|
109
107
|
});
|
|
110
108
|
}
|
|
111
|
-
if (args.requirementContext.requireReadToolAccessOrInlineReviewerContext &&
|
|
112
|
-
!args.capabilities.toolAccess.read &&
|
|
113
|
-
!(args.capabilities.supported && args.capabilities.supportsStructuredOutput)) {
|
|
114
|
-
throwProviderCapabilityError({
|
|
115
|
-
requirementContext: args.requirementContext,
|
|
116
|
-
config: args.config,
|
|
117
|
-
missingCapability: 'read_tool_access_or_inline_reviewer_context',
|
|
118
|
-
});
|
|
119
|
-
}
|
|
120
109
|
if (args.requirementContext.requireWriteToolAccess && !args.capabilities.toolAccess.write) {
|
|
121
110
|
throwProviderCapabilityError({
|
|
122
111
|
requirementContext: args.requirementContext,
|
|
@@ -161,9 +150,9 @@ function assertRoleCapabilities(args) {
|
|
|
161
150
|
// Registry invariant: every *supported* structured-advisor (reviewer)
|
|
162
151
|
// capability must be read-only. Reviewers inspect work and return verdicts;
|
|
163
152
|
// they never mutate the checkout or run shell commands. Only `write`/`shell`
|
|
164
|
-
// are constrained here — `read` is
|
|
165
|
-
//
|
|
166
|
-
// capability is intentionally not checked: coders keep write/shell.
|
|
153
|
+
// are constrained here — `read` is not constrained by this assertion (the
|
|
154
|
+
// writer-run capability gates separately require reviewer read access). The
|
|
155
|
+
// `coder` capability is intentionally not checked: coders keep write/shell.
|
|
167
156
|
export function assertStructuredAdvisorReadOnly(definition) {
|
|
168
157
|
const capability = definition.capabilities['structured-advisor'];
|
|
169
158
|
if (!capability.supported) {
|
|
@@ -280,11 +269,11 @@ export function assertAgentConfigSupportsWriterRun(agentConfig, options = {}) {
|
|
|
280
269
|
role: 'reviewer',
|
|
281
270
|
context,
|
|
282
271
|
reason: 'Neal writer runs need a reviewer provider that can inspect work and return structured verdicts',
|
|
283
|
-
|
|
272
|
+
requireReadToolAccess: true,
|
|
284
273
|
requireStructuredOutput: true,
|
|
285
274
|
reasons: {
|
|
286
275
|
structured_advisor_adapter: 'reviewer, plan-review, support, and final-completion verdicts run through the structured-advisor adapter',
|
|
287
|
-
|
|
276
|
+
read_tool_access: 'reviewer rounds must inspect repository state, diffs, and run artifacts directly with read tools',
|
|
288
277
|
structured_output: 'reviewer rounds return schema-validated structured verdicts',
|
|
289
278
|
model_override: 'a non-null reviewer model override is configured for this writer run',
|
|
290
279
|
},
|
|
@@ -63,6 +63,8 @@ async function writeRunEvent(event, options) {
|
|
|
63
63
|
await logger.event('provider.turn_completed', {
|
|
64
64
|
...commonEventData(event),
|
|
65
65
|
...(event.usage !== undefined ? { usage: event.usage } : {}),
|
|
66
|
+
...(event.costUsd !== undefined ? { costUsd: event.costUsd } : {}),
|
|
67
|
+
...(event.costSource !== undefined ? { costSource: event.costSource } : {}),
|
|
66
68
|
});
|
|
67
69
|
break;
|
|
68
70
|
case 'tool_started':
|
|
@@ -121,6 +123,8 @@ async function writeRunEvent(event, options) {
|
|
|
121
123
|
await logger.event('provider.usage_reported', {
|
|
122
124
|
...commonEventData(event),
|
|
123
125
|
usage: event.usage,
|
|
126
|
+
...(event.costUsd !== undefined ? { costUsd: event.costUsd } : {}),
|
|
127
|
+
...(event.costSource !== undefined ? { costSource: event.costSource } : {}),
|
|
124
128
|
});
|
|
125
129
|
break;
|
|
126
130
|
case 'provider_error':
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
// Detects an OpenAI content-safety refusal in a provider-authored error or
|
|
2
|
+
// exception message. OpenAI flags security-adjacent requests with a stable
|
|
3
|
+
// notice ("This content was flagged for possible cybersecurity risk ... join
|
|
4
|
+
// the Trusted Access for Cyber ..."); either substring is specific enough that
|
|
5
|
+
// ordinary review findings mentioning "security" do not trip it. Callers must
|
|
6
|
+
// only pass error-channel text (an embedded gateway error message or an
|
|
7
|
+
// HTTP/transport exception message), never assistant review content.
|
|
8
|
+
export function isContentSafetyRefusalMessage(message) {
|
|
9
|
+
const text = message.toLowerCase();
|
|
10
|
+
return (text.includes('flagged for possible cybersecurity risk') ||
|
|
11
|
+
text.includes('trusted access for cyber'));
|
|
12
|
+
}
|
|
1
13
|
export class NealProviderError extends Error {
|
|
2
14
|
provider;
|
|
3
15
|
role;
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
3
|
import { planResumeActions } from './resume-planner.js';
|
|
4
|
-
import { getInteractiveRecoveryView } from './state-views.js';
|
|
4
|
+
import { getInteractiveRecoveryView, getPlanReviewGuidanceOriginPhase } from './state-views.js';
|
|
5
5
|
export function decideResumeAction(args) {
|
|
6
6
|
const resumeCommand = formatResumeCommand(args.selectedRunId);
|
|
7
7
|
const messageCommand = formatMessageCommand(args.selectedRunId);
|
|
@@ -86,11 +86,12 @@ export function decideResumeAction(args) {
|
|
|
86
86
|
};
|
|
87
87
|
}
|
|
88
88
|
function isWaitingForPlanReviewGuidance(state) {
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
89
|
+
// Shared discriminator: reviewer_plan plus coder-authored *response* blocks
|
|
90
|
+
// (which carry a durable blockerReason); dirty-worktree safety blocks at the
|
|
91
|
+
// same response phase have blockerReason null and are excluded. Evaluated before
|
|
92
|
+
// planResumeActions in decideResumeAction, so this needs_message decision takes
|
|
93
|
+
// precedence over a bare-resume restore of a genuine coder-authored block.
|
|
94
|
+
return getPlanReviewGuidanceOriginPhase(state) !== null;
|
|
94
95
|
}
|
|
95
96
|
function hasPendingPlanReviewGuidance(state) {
|
|
96
97
|
return state.topLevelMode === 'plan' && state.pendingPlanReviewGuidance !== null;
|
|
@@ -132,11 +132,13 @@ function applyPlannedStatusAction(state, action) {
|
|
|
132
132
|
...state,
|
|
133
133
|
phase: action.phase,
|
|
134
134
|
status: 'running',
|
|
135
|
+
blockerReason: null,
|
|
135
136
|
};
|
|
136
137
|
case 'normalize_stopped_status':
|
|
137
138
|
return {
|
|
138
139
|
...state,
|
|
139
140
|
status: 'running',
|
|
141
|
+
blockerReason: null,
|
|
140
142
|
};
|
|
141
143
|
default:
|
|
142
144
|
return state;
|
|
@@ -173,6 +175,7 @@ function applyPlannedDerivedPlanAction(state, action) {
|
|
|
173
175
|
phase: action.phase,
|
|
174
176
|
status: 'running',
|
|
175
177
|
blockedFromPhase: null,
|
|
178
|
+
blockerReason: null,
|
|
176
179
|
};
|
|
177
180
|
case 'block_rejected_abandoned_derived_plan':
|
|
178
181
|
return {
|
|
@@ -197,6 +200,10 @@ async function restoreResumableBlockedPhase(state, statePath, logger, action) {
|
|
|
197
200
|
...state,
|
|
198
201
|
phase: action.phase,
|
|
199
202
|
status: 'running',
|
|
203
|
+
// A coder-authored plan-stage response block persists a durable blockerReason;
|
|
204
|
+
// the resume planner is its first blocked->running writer, so clear it here so
|
|
205
|
+
// the reason never outlives its block (and the state satisfies the invariant).
|
|
206
|
+
blockerReason: null,
|
|
200
207
|
});
|
|
201
208
|
await logger?.event('run.resumed_from_blocked', {
|
|
202
209
|
statePath,
|
|
@@ -210,6 +217,7 @@ async function normalizeStoppedStatus(state, statePath, logger, action) {
|
|
|
210
217
|
const nextState = await saveState(statePath, {
|
|
211
218
|
...state,
|
|
212
219
|
status: 'running',
|
|
220
|
+
blockerReason: null,
|
|
213
221
|
});
|
|
214
222
|
await logger?.event('run.status_normalized_on_resume', {
|
|
215
223
|
statePath,
|
|
@@ -228,6 +236,7 @@ async function promoteUnexecutedDerivedPlan(state, statePath, logger, action) {
|
|
|
228
236
|
phase: action.phase,
|
|
229
237
|
status: 'running',
|
|
230
238
|
blockedFromPhase: null,
|
|
239
|
+
blockerReason: null,
|
|
231
240
|
});
|
|
232
241
|
await logger?.event(action.kind === 'promote_accepted_derived_plan'
|
|
233
242
|
? 'run.promoted_accepted_derived_plan_on_resume'
|
|
@@ -28,6 +28,26 @@ function getArchivedRetrospectivePath(state, kind) {
|
|
|
28
28
|
const suffix = state.finalCommit ? `-${state.finalCommit}` : '';
|
|
29
29
|
return join(state.runDir, `RETROSPECTIVE-final${suffix}.md`);
|
|
30
30
|
}
|
|
31
|
+
function getCurrentRunMetricsPath(runDir) {
|
|
32
|
+
return join(runDir, 'RUN_METRICS.json');
|
|
33
|
+
}
|
|
34
|
+
// Mirrors getArchivedRetrospectivePath's scope/kind/commit naming so the
|
|
35
|
+
// machine-readable metrics archive lines up with the retrospective archive.
|
|
36
|
+
function getArchivedRunMetricsPath(state, kind) {
|
|
37
|
+
const scopeLabel = getCurrentScopeLabel(state);
|
|
38
|
+
if (kind === 'scope_accepted') {
|
|
39
|
+
const suffix = state.finalCommit ? `-${state.finalCommit}` : '';
|
|
40
|
+
return join(state.runDir, `RUN_METRICS-scope-${scopeLabel}${suffix}.json`);
|
|
41
|
+
}
|
|
42
|
+
if (kind === 'blocked') {
|
|
43
|
+
return join(state.runDir, `RUN_METRICS-blocked-scope-${scopeLabel}.json`);
|
|
44
|
+
}
|
|
45
|
+
if (kind === 'failed') {
|
|
46
|
+
return join(state.runDir, `RUN_METRICS-failed-scope-${scopeLabel}.json`);
|
|
47
|
+
}
|
|
48
|
+
const suffix = state.finalCommit ? `-${state.finalCommit}` : '';
|
|
49
|
+
return join(state.runDir, `RUN_METRICS-final${suffix}.json`);
|
|
50
|
+
}
|
|
31
51
|
async function loadRunEvents(runDir) {
|
|
32
52
|
try {
|
|
33
53
|
const content = await readFile(getEventsPath(runDir), 'utf8');
|
|
@@ -228,7 +248,7 @@ function summarizeBlocker(state) {
|
|
|
228
248
|
const persistedBlocker = latestCompletedScope?.blocker?.trim() || null;
|
|
229
249
|
const recovery = state.interactiveBlockedRecovery;
|
|
230
250
|
const recoveryBlocker = recovery?.blockedReason?.trim() || null;
|
|
231
|
-
const advice = recovery?.
|
|
251
|
+
const advice = recovery?.consultantAdvice ?? null;
|
|
232
252
|
const lines = [];
|
|
233
253
|
if (persistedBlocker) {
|
|
234
254
|
lines.push(`- Final blocker: ${persistedBlocker}`);
|
|
@@ -237,9 +257,9 @@ function summarizeBlocker(state) {
|
|
|
237
257
|
lines.push(`- Recovery blocker: ${recoveryBlocker}`);
|
|
238
258
|
}
|
|
239
259
|
if (advice) {
|
|
240
|
-
lines.push(`-
|
|
260
|
+
lines.push(`- Consultant triage: ${advice.triageCategory} (recoverable: ${advice.recoverable ? 'yes' : 'no'})`);
|
|
241
261
|
if (advice.resolutionDirective.trim()) {
|
|
242
|
-
lines.push(`-
|
|
262
|
+
lines.push(`- Consultant suggested directive: ${advice.resolutionDirective.trim()}`);
|
|
243
263
|
}
|
|
244
264
|
}
|
|
245
265
|
if (lines.length === 0) {
|
|
@@ -306,7 +326,8 @@ async function renderRetrospective(state, kind) {
|
|
|
306
326
|
const changedFiles = await summarizeChangedFiles(state);
|
|
307
327
|
const verificationSummary = summarizeVerification(scopeEvents);
|
|
308
328
|
const metricsEvents = kind === 'done' ? events : scopeEvents;
|
|
309
|
-
const
|
|
329
|
+
const metrics = summarizeRunMetrics(metricsEvents);
|
|
330
|
+
const runMetricsSummary = renderRunMetricsMarkdown(metrics);
|
|
310
331
|
const assessment = buildAssessment(state, scopeEvents);
|
|
311
332
|
const narrativeRetrospective = buildNarrativeRetrospective({
|
|
312
333
|
state,
|
|
@@ -320,7 +341,7 @@ async function renderRetrospective(state, kind) {
|
|
|
320
341
|
const derivedPlan = getDerivedPlanView(state);
|
|
321
342
|
const showDerivedPlanContext = Boolean(derivedPlan);
|
|
322
343
|
const parentScopeLabel = derivedPlan?.executing ? getParentScopeLabel(state) : null;
|
|
323
|
-
|
|
344
|
+
const content = [
|
|
324
345
|
`# Neal Retrospective`,
|
|
325
346
|
'',
|
|
326
347
|
`## Outcome`,
|
|
@@ -377,15 +398,23 @@ async function renderRetrospective(state, kind) {
|
|
|
377
398
|
...renderInteractiveBlockedRecoveryHistoryLines(state.interactiveBlockedRecoveryHistory),
|
|
378
399
|
'',
|
|
379
400
|
].join('\n');
|
|
401
|
+
return { content, metrics };
|
|
380
402
|
}
|
|
381
403
|
async function writeRetrospectiveFile(path, content) {
|
|
382
404
|
await writeTextAtomic(path, content);
|
|
383
405
|
}
|
|
406
|
+
async function writeRunMetricsFile(path, metrics) {
|
|
407
|
+
await writeTextAtomic(path, `${JSON.stringify(metrics, null, 2)}\n`);
|
|
408
|
+
}
|
|
384
409
|
export async function writeCheckpointRetrospective(state, kind) {
|
|
385
|
-
const content = await renderRetrospective(state, kind);
|
|
410
|
+
const { content, metrics } = await renderRetrospective(state, kind);
|
|
386
411
|
const currentPath = getCurrentRetrospectivePath(state.runDir);
|
|
387
412
|
const archivedPath = getArchivedRetrospectivePath(state, kind);
|
|
388
413
|
await writeRetrospectiveFile(currentPath, content);
|
|
389
414
|
await writeRetrospectiveFile(archivedPath, content);
|
|
390
|
-
|
|
415
|
+
const currentMetricsPath = getCurrentRunMetricsPath(state.runDir);
|
|
416
|
+
const archivedMetricsPath = getArchivedRunMetricsPath(state, kind);
|
|
417
|
+
await writeRunMetricsFile(currentMetricsPath, metrics);
|
|
418
|
+
await writeRunMetricsFile(archivedMetricsPath, metrics);
|
|
419
|
+
return { currentPath, archivedPath, currentMetricsPath, archivedMetricsPath };
|
|
391
420
|
}
|
package/dist/neal/review-debt.js
CHANGED
|
@@ -16,3 +16,33 @@ export function toResidualReviewDebt(findings) {
|
|
|
16
16
|
coderCommit: finding.coderCommit,
|
|
17
17
|
}));
|
|
18
18
|
}
|
|
19
|
+
// Plan-review debt is a canonical-keyed *projection* of the current findings,
|
|
20
|
+
// never an accumulator: for each canonicalId whose latest-round finding is a
|
|
21
|
+
// deferred verification-hardening finding, emit exactly one debt item (latest
|
|
22
|
+
// round wins). A canonical that later reopens (its latest finding goes back to
|
|
23
|
+
// open) or is fixed/rejected simply drops out of the projection, so the debt
|
|
24
|
+
// lifecycle is automatic — no stale or duplicate entries and no removal rule.
|
|
25
|
+
export function toPlanReviewDebt(findings) {
|
|
26
|
+
const latestByCanonical = new Map();
|
|
27
|
+
for (const finding of findings) {
|
|
28
|
+
const existing = latestByCanonical.get(finding.canonicalId);
|
|
29
|
+
if (!existing || finding.round >= existing.round) {
|
|
30
|
+
latestByCanonical.set(finding.canonicalId, finding);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return [...latestByCanonical.values()]
|
|
34
|
+
.filter((finding) => finding.status === 'deferred' && finding.findingClass === 'verification_hardening')
|
|
35
|
+
.map((finding) => ({
|
|
36
|
+
id: finding.id,
|
|
37
|
+
canonicalId: finding.canonicalId,
|
|
38
|
+
status: 'deferred',
|
|
39
|
+
files: [...finding.files],
|
|
40
|
+
claim: finding.claim,
|
|
41
|
+
evidence: finding.evidence ?? null,
|
|
42
|
+
requiredAction: finding.requiredAction,
|
|
43
|
+
coderDisposition: finding.coderDisposition,
|
|
44
|
+
coderCommit: finding.coderCommit,
|
|
45
|
+
findingClass: finding.findingClass,
|
|
46
|
+
originRound: finding.round,
|
|
47
|
+
}));
|
|
48
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { getAgentTurnRetryLimit, getAgentTurnStartupTimeoutMs, getApiRetryLimit, getDefaultCoderModel, getDefaultCoderProvider, getDefaultReviewerModel, getDefaultReviewerProvider, getInactivityTimeoutMs, } from '../config.js';
|
|
2
2
|
import { runCoderStructuredPrompt } from '../agents/structured-coder.js';
|
|
3
|
-
import {
|
|
3
|
+
import { getReviewerDoctrineAccessMode, readOnlyReviewerNeedsInlinedDiff, } from '../context/inline-review-context.js';
|
|
4
4
|
import { runWithAgentTurnLiveness } from '../providers/liveness.js';
|
|
5
5
|
import { assertProviderSupportsCoder, assertProviderSupportsStructuredAdvisor, getStructuredAdvisorAdapter, } from '../providers/registry.js';
|
|
6
6
|
import { createProviderTelemetrySink } from '../providers/telemetry.js';
|
|
@@ -47,25 +47,21 @@ class AgentReviewFindingsProviderAdapter {
|
|
|
47
47
|
async reviewDraft(args) {
|
|
48
48
|
const reviewer = getStructuredAdvisorAdapter(this.args.agentConfig.reviewer);
|
|
49
49
|
const schema = buildReviewFindingsReviewSchema();
|
|
50
|
-
// The base adjudication prompt only previews the diff. A
|
|
51
|
-
//
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
// commit-range diff tool
|
|
55
|
-
//
|
|
56
|
-
//
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
// tool, nor rely on the truncated preview. Tool-access reviewers keep the
|
|
60
|
-
// built prompt byte-identical.
|
|
50
|
+
// The base adjudication prompt only previews the diff. A read-only
|
|
51
|
+
// reviewer (read tools, no shell) that exposes its own commit-range diff
|
|
52
|
+
// tool gets the read-only range-inspection section directing it to the
|
|
53
|
+
// git_diff tool with the exact resolved revisions; a read-only reviewer
|
|
54
|
+
// without a commit-range diff tool (native Claude/Codex) instead gets the
|
|
55
|
+
// full selected diff inlined and no git_diff-tool instruction, because it
|
|
56
|
+
// can neither run git commands, call a git_diff tool, nor rely on the
|
|
57
|
+
// truncated preview. Tool-access reviewers keep the built prompt
|
|
58
|
+
// byte-identical.
|
|
61
59
|
const accessMode = getReviewerDoctrineAccessMode(this.args.agentConfig.reviewer);
|
|
62
|
-
const prompt =
|
|
63
|
-
?
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
: `${args.prompt}\n\n${buildReviewFindingsReadOnlyInspectionSection(args.context)}`
|
|
68
|
-
: args.prompt;
|
|
60
|
+
const prompt = accessMode === 'read-only'
|
|
61
|
+
? readOnlyReviewerNeedsInlinedDiff(this.args.agentConfig.reviewer)
|
|
62
|
+
? `${args.prompt}\n\n${buildReviewFindingsInlinedDiffSection(args.context)}`
|
|
63
|
+
: `${args.prompt}\n\n${buildReviewFindingsReadOnlyInspectionSection(args.context)}`
|
|
64
|
+
: args.prompt;
|
|
69
65
|
const result = await runWithAgentTurnLiveness({
|
|
70
66
|
provider: this.args.agentConfig.reviewer.provider,
|
|
71
67
|
role: 'structured-advisor',
|
|
@@ -102,14 +98,6 @@ class AgentReviewFindingsProviderAdapter {
|
|
|
102
98
|
return validateReviewFindingsReview(result.structured);
|
|
103
99
|
}
|
|
104
100
|
}
|
|
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
101
|
function buildReviewFindingsProtocolSpec(args) {
|
|
114
102
|
return {
|
|
115
103
|
protocol: 'neal-json-block-v1',
|
|
@@ -160,11 +148,11 @@ function assertReviewAgentCapabilities(agentConfig) {
|
|
|
160
148
|
role: 'reviewer',
|
|
161
149
|
context: 'neal review',
|
|
162
150
|
reason: 'neal review uses the configured reviewer provider to adjudicate and accept or revise the findings artifact',
|
|
163
|
-
|
|
151
|
+
requireReadToolAccess: true,
|
|
164
152
|
requireStructuredOutput: true,
|
|
165
153
|
reasons: {
|
|
166
154
|
structured_advisor_adapter: 'review findings adjudication runs through the configured structured-advisor adapter',
|
|
167
|
-
|
|
155
|
+
read_tool_access: 'the reviewer must inspect the draft, selected diff, and repository context directly with read tools',
|
|
168
156
|
structured_output: 'the reviewer verdict must be schema-validated before Neal accepts the findings artifact',
|
|
169
157
|
model_override: 'a non-null reviewer model override is configured for review adjudication',
|
|
170
158
|
},
|
package/dist/neal/review.js
CHANGED
|
@@ -126,12 +126,28 @@ export function renderReviewMarkdown(state) {
|
|
|
126
126
|
for (const round of rounds) {
|
|
127
127
|
lines.push('', `## Round ${round} Findings`);
|
|
128
128
|
for (const finding of state.findings.filter((item) => item.round === round)) {
|
|
129
|
-
lines.push('', `### ${finding.id}`, `- Canonical ID: ${finding.canonicalId}`, `- Source: ${finding.source}`, `- Severity: ${finding.severity}`,
|
|
129
|
+
lines.push('', `### ${finding.id}`, `- Canonical ID: ${finding.canonicalId}`, `- Source: ${finding.source}`, `- Severity: ${finding.severity}`,
|
|
130
|
+
// 'n/a' keeps execute-review findings (which carry no class)
|
|
131
|
+
// semantically unchanged rather than mislabeling them plan_correctness.
|
|
132
|
+
`- Finding class: ${finding.findingClass ?? 'n/a'}`, `- Status: ${finding.status}`, `- Files: ${finding.files.join(', ') || 'n/a'}`, `- Claim: ${finding.claim}`, `- Evidence: ${finding.evidence?.trim() || 'n/a'}`, `- Required action: ${finding.requiredAction}`, `- Round summary: ${finding.roundSummary}`, `- Coder disposition: ${finding.coderDisposition ?? 'pending'}`, `- Coder commit: ${finding.coderCommit ?? 'pending'}`);
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
}
|
|
136
|
+
lines.push('', '## Plan Review Debt');
|
|
137
|
+
appendPlanReviewDebtGroup(lines, 'Inherited', state.inheritedPlanReviewDebt);
|
|
138
|
+
appendPlanReviewDebtGroup(lines, 'Current', state.planReviewDebt);
|
|
133
139
|
return `${lines.join('\n')}\n`;
|
|
134
140
|
}
|
|
141
|
+
function appendPlanReviewDebtGroup(lines, label, items) {
|
|
142
|
+
lines.push('', `### ${label}`);
|
|
143
|
+
if (items.length === 0) {
|
|
144
|
+
lines.push('- none');
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
for (const item of items) {
|
|
148
|
+
lines.push(`- ${item.canonicalId}: findingClass=${item.findingClass ?? 'n/a'}; originRound=${item.originRound ?? 'n/a'}; claim=${item.claim}; requiredAction=${item.requiredAction}; coderDisposition=${item.coderDisposition ?? 'none'}`);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
135
151
|
export async function writeReviewMarkdown(path, state) {
|
|
136
152
|
await writeTextAtomic(path, renderReviewMarkdown(state));
|
|
137
153
|
}
|
package/dist/neal/run-metrics.js
CHANGED
|
@@ -48,6 +48,21 @@ function addUsage(target, value) {
|
|
|
48
48
|
function hasUsage(usage) {
|
|
49
49
|
return Object.values(usage).some((value) => value > 0);
|
|
50
50
|
}
|
|
51
|
+
// Accumulate cost from one counted event into the provider bucket. Presence is
|
|
52
|
+
// tracked separately from value: costUsd stays null until an event carries a
|
|
53
|
+
// numeric costUsd, then it holds the running sum. A single provider/role bucket
|
|
54
|
+
// has one source; if both appear, 'provider' wins.
|
|
55
|
+
function accumulateCost(bucket, data) {
|
|
56
|
+
const raw = data.costUsd;
|
|
57
|
+
if (typeof raw !== 'number' || !Number.isFinite(raw)) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
bucket.costUsd = (bucket.costUsd ?? 0) + raw;
|
|
61
|
+
const source = data.costSource;
|
|
62
|
+
if (source === 'provider' || source === 'rate') {
|
|
63
|
+
bucket.costSource = bucket.costSource === 'provider' || source === 'provider' ? 'provider' : 'rate';
|
|
64
|
+
}
|
|
65
|
+
}
|
|
51
66
|
function cloneEmptyUsage() {
|
|
52
67
|
return { ...EMPTY_USAGE };
|
|
53
68
|
}
|
|
@@ -210,10 +225,13 @@ export function summarizeRunMetrics(events) {
|
|
|
210
225
|
label: identity.label,
|
|
211
226
|
turns: 0,
|
|
212
227
|
usage: cloneEmptyUsage(),
|
|
228
|
+
costUsd: null,
|
|
229
|
+
costSource: null,
|
|
213
230
|
};
|
|
214
231
|
existing.turns += 1;
|
|
215
232
|
if (!hasDedicatedUsageEvents) {
|
|
216
233
|
addUsage(existing.usage, data.usage);
|
|
234
|
+
accumulateCost(existing, data);
|
|
217
235
|
}
|
|
218
236
|
providers.set(identity.key, existing);
|
|
219
237
|
}
|
|
@@ -226,8 +244,11 @@ export function summarizeRunMetrics(events) {
|
|
|
226
244
|
label: identity.label,
|
|
227
245
|
turns: 0,
|
|
228
246
|
usage: cloneEmptyUsage(),
|
|
247
|
+
costUsd: null,
|
|
248
|
+
costSource: null,
|
|
229
249
|
};
|
|
230
250
|
addUsage(existing.usage, data.usage);
|
|
251
|
+
accumulateCost(existing, data);
|
|
231
252
|
providers.set(identity.key, existing);
|
|
232
253
|
}
|
|
233
254
|
else if (event.type === 'provider.command_completed') {
|
|
@@ -276,6 +297,25 @@ export function summarizeRunMetrics(events) {
|
|
|
276
297
|
phase.durationMs = start !== null && end !== null && end >= start ? end - start : null;
|
|
277
298
|
}
|
|
278
299
|
const publicPhases = phases.map(({ startIndex: _startIndex, endIndex: _endIndex, ...phase }) => phase);
|
|
300
|
+
const providerSummaries = [...providers.values()].sort((left, right) => {
|
|
301
|
+
const turns = right.turns - left.turns;
|
|
302
|
+
if (turns !== 0) {
|
|
303
|
+
return turns;
|
|
304
|
+
}
|
|
305
|
+
return usageSortValue(right) - usageSortValue(left);
|
|
306
|
+
});
|
|
307
|
+
// Coverage is computed only over usage-bearing buckets so a partial subtotal
|
|
308
|
+
// never masquerades as a complete run total.
|
|
309
|
+
const usageBearing = providerSummaries.filter((provider) => hasUsage(provider.usage));
|
|
310
|
+
const pricedUsageBearing = usageBearing.filter((provider) => provider.costUsd !== null);
|
|
311
|
+
const costCoverage = pricedUsageBearing.length === 0
|
|
312
|
+
? 'none'
|
|
313
|
+
: pricedUsageBearing.length === usageBearing.length
|
|
314
|
+
? 'complete'
|
|
315
|
+
: 'partial';
|
|
316
|
+
const totalCostUsd = costCoverage === 'none'
|
|
317
|
+
? null
|
|
318
|
+
: providerSummaries.reduce((sum, provider) => (provider.costUsd !== null ? sum + provider.costUsd : sum), 0);
|
|
279
319
|
return {
|
|
280
320
|
observedStartedAt: firstTimestamp,
|
|
281
321
|
observedCompletedAt: lastTimestamp,
|
|
@@ -288,13 +328,9 @@ export function summarizeRunMetrics(events) {
|
|
|
288
328
|
toolEventCount,
|
|
289
329
|
fileChangeEventCount,
|
|
290
330
|
phases: publicPhases,
|
|
291
|
-
providers:
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return turns;
|
|
295
|
-
}
|
|
296
|
-
return usageSortValue(right) - usageSortValue(left);
|
|
297
|
-
}),
|
|
331
|
+
providers: providerSummaries,
|
|
332
|
+
totalCostUsd,
|
|
333
|
+
costCoverage,
|
|
298
334
|
};
|
|
299
335
|
}
|
|
300
336
|
function formatDuration(value) {
|
|
@@ -327,6 +363,31 @@ function formatProviderName(summary) {
|
|
|
327
363
|
function usageCell(value) {
|
|
328
364
|
return value > 0 ? formatNumber(value) : '-';
|
|
329
365
|
}
|
|
366
|
+
function formatCostAmount(value) {
|
|
367
|
+
return `$${value.toFixed(4)}`;
|
|
368
|
+
}
|
|
369
|
+
// Cost cell for the Provider Usage table: '-' when the bucket reported no cost,
|
|
370
|
+
// otherwise the fixed-precision USD amount with a footnote marker on
|
|
371
|
+
// rate-computed cells.
|
|
372
|
+
function formatCost(value, source) {
|
|
373
|
+
if (value === null) {
|
|
374
|
+
return '-';
|
|
375
|
+
}
|
|
376
|
+
return source === 'rate' ? `${formatCostAmount(value)}*` : formatCostAmount(value);
|
|
377
|
+
}
|
|
378
|
+
// The top-summary cost line, labeled by coverage so a partial subtotal is never
|
|
379
|
+
// presented as a complete run total.
|
|
380
|
+
function formatEstimatedCostLine(metrics) {
|
|
381
|
+
if (metrics.costCoverage === 'none' || metrics.totalCostUsd === null) {
|
|
382
|
+
return '- Estimated cost: unknown';
|
|
383
|
+
}
|
|
384
|
+
if (metrics.costCoverage === 'partial') {
|
|
385
|
+
const usageBearing = metrics.providers.filter((provider) => hasUsage(provider.usage));
|
|
386
|
+
const priced = usageBearing.filter((provider) => provider.costUsd !== null);
|
|
387
|
+
return `- Estimated cost (partial — ${priced.length} of ${usageBearing.length} priced providers): ${formatCostAmount(metrics.totalCostUsd)}`;
|
|
388
|
+
}
|
|
389
|
+
return `- Estimated cost: ${formatCostAmount(metrics.totalCostUsd)}`;
|
|
390
|
+
}
|
|
330
391
|
export function renderRunMetricsMarkdown(metrics) {
|
|
331
392
|
const commandSummary = metrics.resolvedNonZeroCommandCount > 0
|
|
332
393
|
? `- Commands: ${metrics.commandCount} total, ${metrics.unresolvedNonZeroCommandCount} unresolved non-zero or failed, ${metrics.resolvedNonZeroCommandCount} resolved by later passing rerun`
|
|
@@ -337,16 +398,20 @@ export function renderRunMetricsMarkdown(metrics) {
|
|
|
337
398
|
commandSummary,
|
|
338
399
|
`- Tool events: ${metrics.toolEventCount}`,
|
|
339
400
|
`- File change events: ${metrics.fileChangeEventCount}`,
|
|
401
|
+
formatEstimatedCostLine(metrics),
|
|
340
402
|
];
|
|
341
403
|
if (metrics.phases.length > 0) {
|
|
342
404
|
lines.push('', '### Phase Timing', '', '| Phase | Duration | Provider turns | Commands | Non-zero commands | Tool events | File changes |', '| --- | ---: | ---: | ---: | ---: | ---: | ---: |', ...metrics.phases.map((phase) => `| ${formatPhaseName(phase.phase)} | ${formatPhaseDuration(phase)} | ${phase.providerTurns} | ${phase.commandCount} | ${phase.nonZeroCommandCount} | ${phase.toolEventCount} | ${phase.fileChangeEventCount} |`));
|
|
343
405
|
}
|
|
344
406
|
const providersWithUsage = metrics.providers.filter((provider) => provider.turns > 0 || hasUsage(provider.usage));
|
|
345
407
|
if (providersWithUsage.length > 0) {
|
|
346
|
-
lines.push('', '### Provider Usage', '', '| Provider / role | Turns | Input | Cached input | Cache created | Cache read | Output | Reasoning output | Total |', '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', ...providersWithUsage.map((provider) => {
|
|
408
|
+
lines.push('', '### Provider Usage', '', '| Provider / role | Turns | Input | Cached input | Cache created | Cache read | Output | Reasoning output | Total | Cost |', '| --- | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: | ---: |', ...providersWithUsage.map((provider) => {
|
|
347
409
|
const usage = provider.usage;
|
|
348
|
-
return `| ${formatProviderName(provider)} | ${provider.turns} | ${usageCell(usage.inputTokens)} | ${usageCell(usage.cachedInputTokens)} | ${usageCell(usage.cacheCreationInputTokens)} | ${usageCell(usage.cacheReadInputTokens)} | ${usageCell(usage.outputTokens)} | ${usageCell(usage.reasoningOutputTokens)} | ${usageCell(usage.totalTokens)} |`;
|
|
410
|
+
return `| ${formatProviderName(provider)} | ${provider.turns} | ${usageCell(usage.inputTokens)} | ${usageCell(usage.cachedInputTokens)} | ${usageCell(usage.cacheCreationInputTokens)} | ${usageCell(usage.cacheReadInputTokens)} | ${usageCell(usage.outputTokens)} | ${usageCell(usage.reasoningOutputTokens)} | ${usageCell(usage.totalTokens)} | ${formatCost(provider.costUsd, provider.costSource)} |`;
|
|
349
411
|
}));
|
|
412
|
+
if (providersWithUsage.some((provider) => provider.costUsd !== null && provider.costSource === 'rate')) {
|
|
413
|
+
lines.push('', '\\* Cost estimated from published or configured rates, not reported by the provider.');
|
|
414
|
+
}
|
|
350
415
|
}
|
|
351
416
|
else {
|
|
352
417
|
lines.push('', '### Provider Usage', '', '- No provider usage events recorded.');
|
|
@@ -307,8 +307,10 @@ function assertValidPendingPlanReviewGuidance(state, context) {
|
|
|
307
307
|
throwStateInvariant(context, 'pendingPlanReviewGuidance', `expected null for ${formatInvalidValue(state.topLevelMode)} mode`);
|
|
308
308
|
}
|
|
309
309
|
assertNonEmptyString(guidance.message, 'pendingPlanReviewGuidance.message', context);
|
|
310
|
-
if (guidance.sourcePhase !== 'reviewer_plan'
|
|
311
|
-
|
|
310
|
+
if (guidance.sourcePhase !== 'reviewer_plan' &&
|
|
311
|
+
guidance.sourcePhase !== 'coder_plan_response' &&
|
|
312
|
+
guidance.sourcePhase !== 'coder_plan_optional_response') {
|
|
313
|
+
throwStateInvariant(context, 'pendingPlanReviewGuidance.sourcePhase', `expected one of "reviewer_plan", "coder_plan_response", "coder_plan_optional_response", received ${formatInvalidValue(guidance.sourcePhase)}`);
|
|
312
314
|
}
|
|
313
315
|
if (typeof guidance.recordedAt !== 'string') {
|
|
314
316
|
throwStateInvariant(context, 'pendingPlanReviewGuidance.recordedAt', `expected string, received ${formatInvalidValue(guidance.recordedAt)}`);
|
|
@@ -466,10 +468,16 @@ export function assertValidOrchestrationState(state, context) {
|
|
|
466
468
|
if (activeStatusInTerminalPhase) {
|
|
467
469
|
throwStateInvariant(context, 'status', `cannot be ${JSON.stringify(state.status)} while phase is terminal ${JSON.stringify(state.phase)}`);
|
|
468
470
|
}
|
|
471
|
+
// The durable coder-authored plan-stage blocker reason must never outlive its
|
|
472
|
+
// block: any return to a non-blocked status (running/paused/done/failed) must
|
|
473
|
+
// have cleared it. This catches a missed clear at the next save/load.
|
|
474
|
+
if (state.status !== 'blocked' && state.blockerReason !== null) {
|
|
475
|
+
throwStateInvariant(context, 'blockerReason', `expected null when status is ${JSON.stringify(state.status)}, received ${formatInvalidValue(state.blockerReason)}`);
|
|
476
|
+
}
|
|
469
477
|
assertSafeIntegerAtLeast(state.currentScopeNumber, 'currentScopeNumber', 0, context);
|
|
470
478
|
assertSafeIntegerAtLeast(state.coderRetryCount, 'coderRetryCount', 0, context);
|
|
471
479
|
assertSafeIntegerAtLeast(state.unattendedAutoResumeCount, 'unattendedAutoResumeCount', 0, context);
|
|
472
|
-
assertSafeIntegerAtLeast(state.
|
|
480
|
+
assertSafeIntegerAtLeast(state.consultantAttemptCount, 'consultantAttemptCount', 0, context);
|
|
473
481
|
assertSafeIntegerAtLeast(state.finalCompletionContinueExecutionCount, 'finalCompletionContinueExecutionCount', 0, context);
|
|
474
482
|
assertSafeIntegerAtLeast(state.splitPlanCountForCurrentScope, 'splitPlanCountForCurrentScope', 0, context);
|
|
475
483
|
assertSafeIntegerAtLeast(state.derivedPlanDepth, 'derivedPlanDepth', 0, context);
|
package/dist/neal/state-views.js
CHANGED
|
@@ -117,13 +117,35 @@ export function getInteractiveRecoveryView(state) {
|
|
|
117
117
|
pendingOperatorGuidance,
|
|
118
118
|
};
|
|
119
119
|
}
|
|
120
|
+
// The plan-stage blocked origin that a top-level plan run can answer via
|
|
121
|
+
// `neal resume --message`, or null when the run is not in such a wait. The
|
|
122
|
+
// reviewer-plan message-resume path is always eligible. A coder-authored
|
|
123
|
+
// *response* block (coder_plan_response / coder_plan_optional_response) is
|
|
124
|
+
// eligible only when it carries the durable `blockerReason`: Scope 6 sets that
|
|
125
|
+
// reason exclusively for coder_authored landings and leaves it null for a
|
|
126
|
+
// dirty-worktree safety block, which lands at the same phase but must stay a
|
|
127
|
+
// normal blocked state (no --message route, no waiting-for-guidance status). The
|
|
128
|
+
// initial `coder_plan` authoring block is deliberately excluded.
|
|
129
|
+
export function getPlanReviewGuidanceOriginPhase(state) {
|
|
130
|
+
if (state.topLevelMode !== 'plan' ||
|
|
131
|
+
state.status !== 'blocked' ||
|
|
132
|
+
state.phase !== 'blocked' ||
|
|
133
|
+
state.pendingPlanReviewGuidance !== null) {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
if (state.blockedFromPhase === 'reviewer_plan') {
|
|
137
|
+
return 'reviewer_plan';
|
|
138
|
+
}
|
|
139
|
+
if ((state.blockedFromPhase === 'coder_plan_response' ||
|
|
140
|
+
state.blockedFromPhase === 'coder_plan_optional_response') &&
|
|
141
|
+
state.blockerReason !== null) {
|
|
142
|
+
return state.blockedFromPhase;
|
|
143
|
+
}
|
|
144
|
+
return null;
|
|
145
|
+
}
|
|
120
146
|
export function getPlanReviewGuidanceView(state) {
|
|
121
147
|
const pendingOperatorGuidance = state.topLevelMode === 'plan' && state.pendingPlanReviewGuidance !== null;
|
|
122
|
-
const waitingForOperatorGuidance = state
|
|
123
|
-
state.status === 'blocked' &&
|
|
124
|
-
state.phase === 'blocked' &&
|
|
125
|
-
state.blockedFromPhase === 'reviewer_plan' &&
|
|
126
|
-
state.pendingPlanReviewGuidance === null;
|
|
148
|
+
const waitingForOperatorGuidance = getPlanReviewGuidanceOriginPhase(state) !== null;
|
|
127
149
|
return {
|
|
128
150
|
kind: 'plan_review_guidance',
|
|
129
151
|
waitingForOperatorGuidance,
|