@navels/neal 0.4.2 → 0.5.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 +18 -13
- package/dist/neal/adjudicator/execute.js +0 -1
- package/dist/neal/adjudicator/final-completion.js +0 -1
- package/dist/neal/adjudicator/planning.js +0 -1
- package/dist/neal/agents/rounds.js +1 -2
- package/dist/neal/blocked-guidance.js +1 -7
- package/dist/neal/cli.js +3 -19
- package/dist/neal/commands/compat.js +36 -13
- package/dist/neal/commands/new-run.js +1 -6
- package/dist/neal/commands/plan-and-execute.js +1 -5
- package/dist/neal/commands/resume-run.js +5 -7
- package/dist/neal/config.js +3 -11
- package/dist/neal/orchestrator/completion.js +7 -23
- package/dist/neal/orchestrator/notifications.js +1 -1
- package/dist/neal/orchestrator/phases/coder.js +0 -1
- package/dist/neal/orchestrator/phases/planning.js +7 -16
- package/dist/neal/orchestrator/phases/recovery.js +42 -285
- package/dist/neal/orchestrator/phases/shared.js +0 -26
- package/dist/neal/orchestrator/run-loop.js +1 -5
- package/dist/neal/orchestrator.js +0 -1
- package/dist/neal/plan-queue.js +5 -15
- package/dist/neal/prompts/execute.js +2 -4
- package/dist/neal/prompts/planning.js +1 -3
- package/dist/neal/prompts/shared.js +0 -7
- package/dist/neal/prompts/specialized.js +0 -2
- package/dist/neal/prompts/specs.js +25 -5
- package/dist/neal/state-invariants.js +0 -1
- package/dist/neal/state.js +0 -4
- package/docs/adjudicator-inventory.md +2 -3
- package/docs/architecture.md +1 -2
- package/docs/compat.md +17 -7
- package/docs/maintenance.md +7 -6
- package/docs/release.md +9 -0
- package/docs/review-convergence.md +7 -8
- package/docs/state-machine.md +53 -63
- package/docs/troubleshooting.md +19 -6
- package/neal.yml +0 -9
- package/package.json +8 -8
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { CoderRoundError, runBlockedRecoveryCoderRound, } from '../../agents.js';
|
|
2
2
|
import { CONSULTANT_ELIGIBLE_SOURCE_PHASES, buildRecentBlockCandidate, isReviewerConsultantPhase, runConsultant, upsertRecentBlock, } from '../../adjudicator/consultant.js';
|
|
3
|
-
import { UNATTENDED_AUTO_RESUME_GUIDANCE } from '../../blocked-guidance.js';
|
|
4
3
|
import { getInteractiveBlockedRecoveryMaxTurns, getConsultantMaxAttempts } from '../../config.js';
|
|
5
4
|
import { EXECUTE_FINALIZATION_PHASE } from '../../execute-finalization.js';
|
|
6
5
|
import { hasPendingOperatorGuidance } from '../../run-status.js';
|
|
@@ -11,14 +10,7 @@ import { writeExecutionArtifacts } from '../artifacts.js';
|
|
|
11
10
|
import { isCoderTimeoutError, shouldNotifyFailure } from '../failures.js';
|
|
12
11
|
import { flushDerivedPlanNotifications, notifyBlocked } from '../notifications.js';
|
|
13
12
|
import { persistSplitPlanRecovery } from '../split-plan.js';
|
|
14
|
-
import { bestEffortCleanupTimedOutCoder, persistBlockedScope, persistCoderFailureState,
|
|
15
|
-
// Bounded number of synthesized conservative auto-resumes the execute-mode
|
|
16
|
-
// interactive-recovery chokepoint performs under `unattended` before it fails
|
|
17
|
-
// cleanly and terminally. Kept a module constant (not a config knob) and held
|
|
18
|
-
// at or below `interactive_blocked_recovery_max_turns` (default 3) so an
|
|
19
|
-
// auto-resume turn never pushes past the recovery turn cap. Revisit here if the
|
|
20
|
-
// unattended push proves too short or too long for headless runs.
|
|
21
|
-
export const UNATTENDED_MAX_AUTO_RESUMES = 2;
|
|
13
|
+
import { bestEffortCleanupTimedOutCoder, persistBlockedScope, persistCoderFailureState, scheduleCoderFreshSessionRetry, shouldRetryCoderWithFreshSession, } from './shared.js';
|
|
22
14
|
export class InteractiveBlockedRecoveryPendingTurnError extends Error {
|
|
23
15
|
pendingTurn;
|
|
24
16
|
constructor(pendingTurn) {
|
|
@@ -78,12 +70,10 @@ function isConsultantEligibleBlock(reason, sourcePhase) {
|
|
|
78
70
|
// budget-exhausted cases emit NO `consultant.*` events so they preserve
|
|
79
71
|
// the generic recovery path byte-for-byte: a disabled or exhausted consultant
|
|
80
72
|
// must be indistinguishable from the consultant never having existed.
|
|
81
|
-
//
|
|
82
|
-
//
|
|
83
|
-
//
|
|
84
|
-
//
|
|
85
|
-
// scope-advance transitions and the split-plan persist) so one scope's
|
|
86
|
-
// adjudication never exhausts a later scope.
|
|
73
|
+
// An invocation consumes one unit whether it auto-applies a recoverable verdict
|
|
74
|
+
// or produces advice and yields for the operator, and the budget is reset to 0
|
|
75
|
+
// at every scope boundary (see the scope-advance transitions and the split-plan
|
|
76
|
+
// persist) so one scope's adjudication never exhausts a later scope.
|
|
87
77
|
function isConsultantBudgetAvailable(state) {
|
|
88
78
|
const maxAttempts = getConsultantMaxAttempts(state.cwd);
|
|
89
79
|
if (maxAttempts <= 0) {
|
|
@@ -91,121 +81,21 @@ function isConsultantBudgetAvailable(state) {
|
|
|
91
81
|
}
|
|
92
82
|
return state.consultantAttemptCount < maxAttempts;
|
|
93
83
|
}
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
//
|
|
97
|
-
//
|
|
98
|
-
//
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
//
|
|
102
|
-
// ineligible source phase, the disabled/exhausted budget, the turn cap, or an
|
|
103
|
-
// consultant error — returns null so the caller falls through to the existing
|
|
104
|
-
// generic auto-resume / terminal-fail path with `recentBlocks` left unchanged.
|
|
105
|
-
// The consultant itself makes zero commits and zero file edits; this function is
|
|
106
|
-
// the sole writer of `recentBlocks`, and only on the branches where the
|
|
107
|
-
// consultant actually ran.
|
|
108
|
-
async function maybeResolveBlockedUnattended(state, statePath, reason, sourcePhase, nextRecovery, logger) {
|
|
109
|
-
if (!isConsultantEligibleBlock(reason, sourcePhase)) {
|
|
110
|
-
return null;
|
|
111
|
-
}
|
|
112
|
-
if (!isConsultantBudgetAvailable(state)) {
|
|
113
|
-
return null;
|
|
114
|
-
}
|
|
115
|
-
// Never push past the recovery turn cap; if there is no room for a recovery
|
|
116
|
-
// turn, fall through to the generic bound check unchanged.
|
|
117
|
-
if (nextRecovery.turns.length >= nextRecovery.maxTurns) {
|
|
118
|
-
return null;
|
|
119
|
-
}
|
|
120
|
-
await logger?.event('consultant.start', {
|
|
121
|
-
scopeNumber: state.currentScopeNumber,
|
|
122
|
-
sourcePhase,
|
|
123
|
-
blockedReason: reason,
|
|
124
|
-
});
|
|
125
|
-
let verdict;
|
|
126
|
-
try {
|
|
127
|
-
verdict = await runConsultant(state, reason, sourcePhase, logger);
|
|
128
|
-
}
|
|
129
|
-
catch (error) {
|
|
130
|
-
// An consultant failure must never crash the run or weaken existing recovery;
|
|
131
|
-
// record the decline and fall through to the generic path with `recentBlocks`
|
|
132
|
-
// unchanged (the consultant did not complete for this block).
|
|
133
|
-
await logger?.event('consultant.declined', {
|
|
134
|
-
scopeNumber: state.currentScopeNumber,
|
|
135
|
-
sourcePhase,
|
|
136
|
-
blockedReason: reason,
|
|
137
|
-
error: error instanceof Error ? error.message : String(error),
|
|
138
|
-
});
|
|
139
|
-
return null;
|
|
140
|
-
}
|
|
141
|
-
// The consultant ran (possibly short-circuiting internally on a thrash repeat),
|
|
142
|
-
// so this block is recorded in the anti-thrash window regardless of the verdict.
|
|
143
|
-
// The candidate is built from the PRE-update array and written in this same
|
|
144
|
-
// transition, so a block can never match itself. It MUST be built from the same
|
|
145
|
-
// `state` snapshot `runConsultant` checked: the candidate's evidence
|
|
146
|
-
// fingerprint is derived from `state.createdCommits`, and a divergent snapshot
|
|
147
|
-
// would record a fingerprint the guard never compared against.
|
|
148
|
-
const candidate = buildRecentBlockCandidate(state, reason, sourcePhase);
|
|
149
|
-
const recentBlocks = upsertRecentBlock(state.recentBlocks, candidate);
|
|
150
|
-
const resolutionDirective = verdict.resolutionDirective.trim();
|
|
151
|
-
if (!verdict.recoverable || !resolutionDirective) {
|
|
152
|
-
// Genuine wall (recoverable:false) or a thrash repeat: an unattended run has
|
|
153
|
-
// no operator to escalate to, so finalize TERMINALLY instead of synthesizing a
|
|
154
|
-
// generic auto-resume. The consultant actually ran, so this branch consumes
|
|
155
|
-
// one unit of the shared per-scope budget (`consultantAttemptCount`) exactly
|
|
156
|
-
// like the recoverable branch, and persists the anti-thrash record via the
|
|
157
|
-
// threaded `recentBlocks`. Only the fallback paths where `runConsultant`
|
|
158
|
-
// was never invoked leave the budget untouched.
|
|
159
|
-
await logger?.event('consultant.declined', {
|
|
160
|
-
scopeNumber: state.currentScopeNumber,
|
|
161
|
-
sourcePhase,
|
|
162
|
-
blockedReason: reason,
|
|
163
|
-
recoverable: verdict.recoverable,
|
|
164
|
-
triageCategory: verdict.triageCategory,
|
|
165
|
-
consultantAttemptCount: state.consultantAttemptCount + 1,
|
|
166
|
-
});
|
|
167
|
-
return failUnattendedRecoveryTerminally({ ...state, recentBlocks, consultantAttemptCount: state.consultantAttemptCount + 1 }, statePath, 'terminal_block', state.coderSessionHandle, logger);
|
|
168
|
-
}
|
|
169
|
-
await logger?.event('consultant.verdict', {
|
|
170
|
-
scopeNumber: state.currentScopeNumber,
|
|
171
|
-
sourcePhase,
|
|
172
|
-
blockedReason: reason,
|
|
173
|
-
recoverable: verdict.recoverable,
|
|
174
|
-
triageCategory: verdict.triageCategory,
|
|
175
|
-
targetCanonicalIds: verdict.targetCanonicalIds,
|
|
176
|
-
// Report the post-increment count this verdict is about to consume so the
|
|
177
|
-
// verdict and the later `resolved` event agree on the budget figure.
|
|
178
|
-
consultantAttemptCount: state.consultantAttemptCount + 1,
|
|
179
|
-
});
|
|
180
|
-
// Recoverable verdict with a concrete directive: auto-apply it (shared with the
|
|
181
|
-
// attended path) so the coder consumes the directive and the run continues.
|
|
182
|
-
return applyRecoverableConsultantDirective({
|
|
183
|
-
state,
|
|
184
|
-
statePath,
|
|
185
|
-
reason,
|
|
186
|
-
sourcePhase,
|
|
187
|
-
nextRecovery,
|
|
188
|
-
recentBlocks,
|
|
189
|
-
resolutionDirective,
|
|
190
|
-
verdict,
|
|
191
|
-
logger,
|
|
192
|
-
});
|
|
193
|
-
}
|
|
194
|
-
// Attended interception for every eligible block class. The consultant NEVER
|
|
195
|
-
// auto-applies its verdict in attended mode; instead it triages read-only and the
|
|
196
|
-
// verdict is returned as advice (plus the updated anti-thrash window) for the
|
|
197
|
-
// caller to persist alongside the operator yield. Gated by the SAME eligibility +
|
|
198
|
-
// disable knob + per-scope budget as the unattended path; when any gate blocks the
|
|
199
|
-
// consultant (ineligible phase, knob 0, exhausted budget, or an consultant
|
|
200
|
-
// error) this returns null and the caller yields exactly as today with no advice,
|
|
201
|
-
// no budget consumption, and `recentBlocks` unchanged.
|
|
84
|
+
// Consultant interception for every eligible block class. The consultant triages
|
|
85
|
+
// read-only and the verdict is returned as advice (plus the updated anti-thrash
|
|
86
|
+
// window) for the caller to either auto-apply (recoverable verdict with a
|
|
87
|
+
// concrete directive) or persist alongside the operator yield. Gated by the
|
|
88
|
+
// eligibility check, the disable knob, and the per-scope budget; when any gate
|
|
89
|
+
// blocks the consultant (ineligible phase, knob 0, exhausted budget, or a
|
|
90
|
+
// consultant error) this returns null and the caller yields for the operator
|
|
91
|
+
// with no advice, no budget consumption, and `recentBlocks` unchanged.
|
|
202
92
|
//
|
|
203
|
-
// The consultant runs BEFORE any `consultant.*` event is emitted, so
|
|
204
|
-
// consultant error degrades to
|
|
93
|
+
// The consultant runs BEFORE any `consultant.*` event is emitted, so a
|
|
94
|
+
// consultant error degrades to a plain operator yield with a byte-for-byte
|
|
205
95
|
// generic observable surface: zero consultant events, no advice, and no
|
|
206
96
|
// counter/`recentBlocks` mutation. The `start`/`verdict` audit pair is emitted only
|
|
207
97
|
// once the consultant has actually produced a verdict for this block.
|
|
208
|
-
async function
|
|
98
|
+
async function buildConsultantAdvice(state, reason, sourcePhase, logger) {
|
|
209
99
|
if (!isConsultantEligibleBlock(reason, sourcePhase)) {
|
|
210
100
|
return null;
|
|
211
101
|
}
|
|
@@ -217,7 +107,7 @@ async function buildAttendedConsultantAdvice(state, reason, sourcePhase, logger)
|
|
|
217
107
|
verdict = await runConsultant(state, reason, sourcePhase, logger);
|
|
218
108
|
}
|
|
219
109
|
catch {
|
|
220
|
-
// Degrade to
|
|
110
|
+
// Degrade to a plain operator yield: never crash the run, and emit NO
|
|
221
111
|
// `consultant.*` events so the fallback is indistinguishable from the
|
|
222
112
|
// disabled/exhausted/ineligible generic yield.
|
|
223
113
|
return null;
|
|
@@ -236,9 +126,11 @@ async function buildAttendedConsultantAdvice(state, reason, sourcePhase, logger)
|
|
|
236
126
|
targetCanonicalIds: verdict.targetCanonicalIds,
|
|
237
127
|
consultantAttemptCount: state.consultantAttemptCount + 1,
|
|
238
128
|
});
|
|
239
|
-
// Same-snapshot rule
|
|
240
|
-
//
|
|
241
|
-
//
|
|
129
|
+
// Same-snapshot rule: the recorded candidate's commit-trail evidence
|
|
130
|
+
// fingerprint must come from the `state` the consultant just checked, never a
|
|
131
|
+
// fresher snapshot — the candidate's evidence fingerprint is derived from
|
|
132
|
+
// `state.createdCommits`, and a divergent snapshot would record a fingerprint
|
|
133
|
+
// the anti-thrash guard never compared against.
|
|
242
134
|
const candidate = buildRecentBlockCandidate(state, reason, sourcePhase);
|
|
243
135
|
const recentBlocks = upsertRecentBlock(state.recentBlocks, candidate);
|
|
244
136
|
const advice = {
|
|
@@ -250,13 +142,13 @@ async function buildAttendedConsultantAdvice(state, reason, sourcePhase, logger)
|
|
|
250
142
|
};
|
|
251
143
|
return { advice, recentBlocks, verdict };
|
|
252
144
|
}
|
|
253
|
-
// Applies a recoverable consultant verdict
|
|
254
|
-
//
|
|
255
|
-
//
|
|
256
|
-
//
|
|
257
|
-
//
|
|
258
|
-
//
|
|
259
|
-
//
|
|
145
|
+
// Applies a recoverable consultant verdict. Enters interactive recovery and
|
|
146
|
+
// injects the consultant's in-scope directive as the pending turn, exactly like
|
|
147
|
+
// a human-supplied `neal resume --message`, so the coder consumes it and the
|
|
148
|
+
// run continues. Consumes one unit of the per-scope consultant budget
|
|
149
|
+
// (`consultantAttemptCount`) and persists the anti-thrash `recentBlocks`. The
|
|
150
|
+
// caller has already emitted the `consultant.verdict` audit event for this
|
|
151
|
+
// verdict.
|
|
260
152
|
async function applyRecoverableConsultantDirective(args) {
|
|
261
153
|
const { state, statePath, reason, sourcePhase, nextRecovery, recentBlocks, resolutionDirective, verdict, logger } = args;
|
|
262
154
|
const enteredState = await saveState(statePath, {
|
|
@@ -305,67 +197,13 @@ export async function enterInteractiveBlockedRecovery(state, statePath, reason,
|
|
|
305
197
|
pendingDirective: null,
|
|
306
198
|
turns: [],
|
|
307
199
|
};
|
|
308
|
-
//
|
|
309
|
-
//
|
|
310
|
-
//
|
|
311
|
-
// the persisted counter and the recovery turn cap, never on guidance text.
|
|
312
|
-
if (state.unattended) {
|
|
313
|
-
// Before the generic auto-resume/terminal-fail decision, give the bounded
|
|
314
|
-
// read-only consultant a chance to triage the block: autonomously resolve it
|
|
315
|
-
// with an in-scope directive (recoverable) or finalize terminally (a genuine
|
|
316
|
-
// wall or thrash repeat). Any ineligible source phase, disabled/exhausted
|
|
317
|
-
// budget, turn cap, or consultant error falls through to the existing generic
|
|
318
|
-
// behavior unchanged.
|
|
319
|
-
const consultantResolved = await maybeResolveBlockedUnattended(state, statePath, reason, sourcePhase, nextRecovery, logger);
|
|
320
|
-
if (consultantResolved) {
|
|
321
|
-
return consultantResolved;
|
|
322
|
-
}
|
|
323
|
-
const canAutoResume = state.unattendedAutoResumeCount < UNATTENDED_MAX_AUTO_RESUMES &&
|
|
324
|
-
nextRecovery.turns.length < nextRecovery.maxTurns;
|
|
325
|
-
if (!canAutoResume) {
|
|
326
|
-
// Past the bound, finalize any active recovery record into history and land
|
|
327
|
-
// on a terminal failed shape (status:'failed', phase:'blocked',
|
|
328
|
-
// interactiveBlockedRecovery:null) so the run is never persisted as an
|
|
329
|
-
// active/waiting recovery. Reuses the same finalizer as the disposition
|
|
330
|
-
// terminal-fail paths.
|
|
331
|
-
return failUnattendedRecoveryTerminally(state, statePath, 'terminal_block', state.coderSessionHandle, logger);
|
|
332
|
-
}
|
|
333
|
-
const enteredState = await saveState(statePath, {
|
|
334
|
-
...state,
|
|
335
|
-
phase: 'interactive_blocked_recovery',
|
|
336
|
-
status: 'running',
|
|
337
|
-
blockedFromPhase: state.blockedFromPhase ?? state.phase,
|
|
338
|
-
unattendedAutoResumeCount: state.unattendedAutoResumeCount + 1,
|
|
339
|
-
interactiveBlockedRecovery: {
|
|
340
|
-
...nextRecovery,
|
|
341
|
-
sourcePhase,
|
|
342
|
-
blockedReason: reason,
|
|
343
|
-
},
|
|
344
|
-
});
|
|
345
|
-
await writeExecutionArtifacts(enteredState);
|
|
346
|
-
await logger?.event('interactive_blocked_recovery.entered', {
|
|
347
|
-
scopeNumber: enteredState.currentScopeNumber,
|
|
348
|
-
sourcePhase: enteredState.interactiveBlockedRecovery?.sourcePhase,
|
|
349
|
-
blockedReason: reason,
|
|
350
|
-
});
|
|
351
|
-
await logger?.event('interactive_blocked_recovery.unattended_auto_resume', {
|
|
352
|
-
scopeNumber: enteredState.currentScopeNumber,
|
|
353
|
-
sourcePhase,
|
|
354
|
-
autoResumeCount: enteredState.unattendedAutoResumeCount,
|
|
355
|
-
maxAutoResumes: UNATTENDED_MAX_AUTO_RESUMES,
|
|
356
|
-
});
|
|
357
|
-
// Reuse the turn-recording helper so the synthesized guidance turn satisfies
|
|
358
|
-
// the same invariants a human-supplied `neal resume --message` would; the run
|
|
359
|
-
// loop then proceeds into runInteractiveBlockedRecoveryPhase to consume it.
|
|
360
|
-
return recordInteractiveBlockedRecoveryGuidance(statePath, UNATTENDED_AUTO_RESUME_GUIDANCE, logger);
|
|
361
|
-
}
|
|
362
|
-
// Attended runs run the same bounded read-only consultant. On a recoverable
|
|
363
|
-
// verdict with a concrete directive, they auto-apply it exactly as unattended
|
|
364
|
-
// runs do — the consultant's advice is acted on in both modes. On a genuine wall
|
|
200
|
+
// Give the bounded read-only consultant a chance to triage the block. On a
|
|
201
|
+
// recoverable verdict with a concrete directive, auto-apply it so the coder
|
|
202
|
+
// consumes the directive and the run continues. On a genuine wall
|
|
365
203
|
// (recoverable:false), or when the disable knob / budget / eligibility gate the
|
|
366
|
-
// consultant off, the
|
|
367
|
-
//
|
|
368
|
-
const advisory = await
|
|
204
|
+
// consultant off, the run yields for the operator, carrying the verdict as
|
|
205
|
+
// advice when there is one so the operator sees why it stopped.
|
|
206
|
+
const advisory = await buildConsultantAdvice(state, reason, sourcePhase, logger);
|
|
369
207
|
if (advisory) {
|
|
370
208
|
const resolutionDirective = advisory.advice.resolutionDirective.trim();
|
|
371
209
|
if (advisory.advice.recoverable && resolutionDirective) {
|
|
@@ -405,16 +243,14 @@ export async function enterInteractiveBlockedRecovery(state, statePath, reason,
|
|
|
405
243
|
});
|
|
406
244
|
return nextState;
|
|
407
245
|
}
|
|
408
|
-
// Whether a caller of `enterInteractiveBlockedRecovery` should emit
|
|
409
|
-
//
|
|
410
|
-
//
|
|
411
|
-
//
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
// or terminally fail), so `!state.unattended` already excludes them. Gate
|
|
415
|
-
// structurally on the derived recovery view, never on text.
|
|
246
|
+
// Whether a caller of `enterInteractiveBlockedRecovery` should emit a blocked /
|
|
247
|
+
// interactive-recovery notification for the returned state. Notify only when the
|
|
248
|
+
// run is actually WAITING for the operator. A run whose block the consultant
|
|
249
|
+
// auto-fixed leaves a pending directive to consume (status 'running', a recorded
|
|
250
|
+
// recovery turn) — that is `waitingForOperatorGuidance: false`, so it must not
|
|
251
|
+
// notify. Gate structurally on the derived recovery view, never on text.
|
|
416
252
|
export function shouldNotifyInteractiveBlockedRecoveryEntry(state) {
|
|
417
|
-
return
|
|
253
|
+
return getInteractiveRecoveryView(state)?.waitingForOperatorGuidance ?? false;
|
|
418
254
|
}
|
|
419
255
|
export async function recordInteractiveBlockedRecoveryGuidance(statePath, operatorGuidance, logger) {
|
|
420
256
|
const trimmedGuidance = operatorGuidance.trim();
|
|
@@ -586,65 +422,6 @@ function getInteractiveBlockedRecoveryResumePhase(sourcePhase) {
|
|
|
586
422
|
return sourcePhase;
|
|
587
423
|
}
|
|
588
424
|
}
|
|
589
|
-
// Shared unattended terminal-fail for a recovery disposition. The active
|
|
590
|
-
// recovery record is finalized into history (so `interactiveBlockedRecovery`
|
|
591
|
-
// becomes null and the lifecycle view is no longer "active"/waiting — required
|
|
592
|
-
// because the state invariant ties a non-null record to the recovery phase),
|
|
593
|
-
// the run lands on the recovery's source phase for diagnostics, and the shared
|
|
594
|
-
// classified terminal-fail action runs (status:'failed', no `notifyBlocked`).
|
|
595
|
-
// `state` must carry the disposition already recorded on its latest turn.
|
|
596
|
-
async function failUnattendedRecoveryTerminally(state, statePath, action, sessionHandle, logger) {
|
|
597
|
-
const recovery = state.interactiveBlockedRecovery;
|
|
598
|
-
const sourcePhase = recovery?.sourcePhase ?? state.blockedFromPhase ?? state.phase;
|
|
599
|
-
// The history record's resultPhase must satisfy the disposition-result invariant
|
|
600
|
-
// (stay_blocked -> recovery, terminal_block/replace -> blocked); the run itself
|
|
601
|
-
// lands on the terminal `blocked` phase with status:'failed', which keeps the
|
|
602
|
-
// lifecycle view out of any waiting/active recovery state.
|
|
603
|
-
const historyResultPhase = action === 'stay_blocked' ? 'interactive_blocked_recovery' : 'blocked';
|
|
604
|
-
await logger?.event('interactive_blocked_recovery.unattended_terminal_fail', {
|
|
605
|
-
scopeNumber: state.currentScopeNumber,
|
|
606
|
-
sourcePhase,
|
|
607
|
-
autoResumeCount: state.unattendedAutoResumeCount,
|
|
608
|
-
maxAutoResumes: UNATTENDED_MAX_AUTO_RESUMES,
|
|
609
|
-
});
|
|
610
|
-
const finalized = recovery ? finalizeInteractiveBlockedRecovery(state, action, historyResultPhase) : state;
|
|
611
|
-
return persistUnattendedBlockUnresolvedFailure({
|
|
612
|
-
...finalized,
|
|
613
|
-
phase: 'blocked',
|
|
614
|
-
blockedFromPhase: sourcePhase,
|
|
615
|
-
coderSessionHandle: sessionHandle,
|
|
616
|
-
coderSessionProtocol: sessionHandle ? state.coderSessionProtocol : null,
|
|
617
|
-
coderRetryCount: 0,
|
|
618
|
-
}, statePath, 'interactive_blocked_recovery', logger);
|
|
619
|
-
}
|
|
620
|
-
// Under `unattended`, a recovery disposition that would otherwise leave the run
|
|
621
|
-
// waiting for an operator (`stay_blocked`) is resolved structurally: synthesize
|
|
622
|
-
// another conservative auto-resume turn while still under the persisted
|
|
623
|
-
// auto-resume cap and the recovery turn cap, otherwise run the shared
|
|
624
|
-
// terminal-fail action. `state` must already be persisted in
|
|
625
|
-
// `interactive_blocked_recovery` with its recovery record holding the handled
|
|
626
|
-
// turns. Gates only on the persisted counter and turn cap, never on text.
|
|
627
|
-
async function continueOrTerminateUnattendedRecovery(state, statePath, sessionHandle, logger) {
|
|
628
|
-
const recovery = state.interactiveBlockedRecovery;
|
|
629
|
-
const canAutoResume = !!recovery &&
|
|
630
|
-
state.unattendedAutoResumeCount < UNATTENDED_MAX_AUTO_RESUMES &&
|
|
631
|
-
recovery.turns.length < recovery.maxTurns;
|
|
632
|
-
if (!canAutoResume) {
|
|
633
|
-
return failUnattendedRecoveryTerminally(state, statePath, 'stay_blocked', sessionHandle, logger);
|
|
634
|
-
}
|
|
635
|
-
const incremented = await saveState(statePath, {
|
|
636
|
-
...state,
|
|
637
|
-
unattendedAutoResumeCount: state.unattendedAutoResumeCount + 1,
|
|
638
|
-
});
|
|
639
|
-
await writeExecutionArtifacts(incremented);
|
|
640
|
-
await logger?.event('interactive_blocked_recovery.unattended_auto_resume', {
|
|
641
|
-
scopeNumber: incremented.currentScopeNumber,
|
|
642
|
-
sourcePhase: recovery.sourcePhase,
|
|
643
|
-
autoResumeCount: incremented.unattendedAutoResumeCount,
|
|
644
|
-
maxAutoResumes: UNATTENDED_MAX_AUTO_RESUMES,
|
|
645
|
-
});
|
|
646
|
-
return recordInteractiveBlockedRecoveryGuidance(statePath, UNATTENDED_AUTO_RESUME_GUIDANCE, logger);
|
|
647
|
-
}
|
|
648
425
|
export async function applyInteractiveBlockedRecoveryDisposition(state, statePath, disposition, sessionHandle, logger) {
|
|
649
426
|
if (state.phase !== 'interactive_blocked_recovery' || !state.interactiveBlockedRecovery) {
|
|
650
427
|
throw new Error(`Run is not in interactive blocked recovery: ${statePath}`);
|
|
@@ -688,14 +465,6 @@ export async function applyInteractiveBlockedRecoveryDisposition(state, statePat
|
|
|
688
465
|
writeExecutionArtifacts,
|
|
689
466
|
});
|
|
690
467
|
const resultPhase = persistedState.phase === 'blocked' ? 'blocked' : 'reviewer_plan';
|
|
691
|
-
if (state.unattended && resultPhase === 'blocked') {
|
|
692
|
-
// An unattended replacement that could not produce a runnable plan must not
|
|
693
|
-
// leave the run resumable-blocked; finalize recovery and fail cleanly.
|
|
694
|
-
return failUnattendedRecoveryTerminally({
|
|
695
|
-
...persistedState,
|
|
696
|
-
interactiveBlockedRecovery: withRecordedInteractiveBlockedRecoveryDisposition({ ...persistedState, interactiveBlockedRecovery: state.interactiveBlockedRecovery }, disposition, sessionHandle, 'blocked').interactiveBlockedRecovery,
|
|
697
|
-
}, statePath, 'replace_current_scope', sessionHandle, logger);
|
|
698
|
-
}
|
|
699
468
|
return persistFinalizedInteractiveBlockedRecovery({
|
|
700
469
|
...persistedState,
|
|
701
470
|
interactiveBlockedRecovery: state.interactiveBlockedRecovery,
|
|
@@ -737,11 +506,6 @@ export async function applyInteractiveBlockedRecoveryDisposition(state, statePat
|
|
|
737
506
|
coderRetryCount: 0,
|
|
738
507
|
});
|
|
739
508
|
await writeExecutionArtifacts(nextState);
|
|
740
|
-
if (nextState.unattended) {
|
|
741
|
-
// No operator will answer a stay_blocked: synthesize another conservative
|
|
742
|
-
// auto-resume turn under the bounds, or run the shared terminal-fail action.
|
|
743
|
-
return continueOrTerminateUnattendedRecovery(nextState, statePath, sessionHandle, logger);
|
|
744
|
-
}
|
|
745
509
|
return nextState;
|
|
746
510
|
}
|
|
747
511
|
const terminalBlockedState = isActivePendingDerivedPlanReview(state)
|
|
@@ -751,13 +515,6 @@ export async function applyInteractiveBlockedRecoveryDisposition(state, statePat
|
|
|
751
515
|
derivedScopeIndex: null,
|
|
752
516
|
}
|
|
753
517
|
: state;
|
|
754
|
-
if (state.unattended) {
|
|
755
|
-
// Unattended terminal_block must not take the attended blocked + notifyBlocked
|
|
756
|
-
// path; record the coder's terminal decision for diagnostics, then run the
|
|
757
|
-
// shared classified terminal-fail action (status:'failed', no notifyBlocked).
|
|
758
|
-
const recordedTerminalState = withRecordedInteractiveBlockedRecoveryDisposition(terminalBlockedState, disposition, sessionHandle, 'blocked');
|
|
759
|
-
return failUnattendedRecoveryTerminally(recordedTerminalState, statePath, 'terminal_block', sessionHandle, logger);
|
|
760
|
-
}
|
|
761
518
|
const finalizedBlockedState = await persistFinalizedInteractiveBlockedRecovery(terminalBlockedState, statePath, disposition, sessionHandle, 'blocked');
|
|
762
519
|
const blockedState = await saveState(statePath, {
|
|
763
520
|
...finalizedBlockedState,
|
|
@@ -86,32 +86,6 @@ export async function persistCoderFailureState(state, statePath, phase, error, l
|
|
|
86
86
|
});
|
|
87
87
|
return failedState;
|
|
88
88
|
}
|
|
89
|
-
// Shared terminal-fail action for the three operator-block sites under
|
|
90
|
-
// `unattended`. Mirrors `persistCoderFailureState`: save `status:'failed'`,
|
|
91
|
-
// re-render the execution artifacts, write a `failed` checkpoint retrospective,
|
|
92
|
-
// and emit a classified log event — deliberately WITHOUT `notifyBlocked` (that
|
|
93
|
-
// is the attended wait notification). Any produced diff/plan is left in the
|
|
94
|
-
// worktree/run dir as an artifact and is not submitted, exactly as today's
|
|
95
|
-
// failed runs leave it. `phase`/`blockedFromPhase` are preserved for
|
|
96
|
-
// diagnostics (defaulting `blockedFromPhase` to the current phase).
|
|
97
|
-
export async function persistUnattendedBlockUnresolvedFailure(state, statePath, site, logger) {
|
|
98
|
-
const blockedFromPhase = state.blockedFromPhase ?? state.phase;
|
|
99
|
-
const failedState = await saveState(statePath, {
|
|
100
|
-
...state,
|
|
101
|
-
status: 'failed',
|
|
102
|
-
blockedFromPhase,
|
|
103
|
-
});
|
|
104
|
-
await writeExecutionArtifacts(failedState);
|
|
105
|
-
await writeCheckpointRetrospective(failedState, 'failed');
|
|
106
|
-
await logger?.event('unattended.block_unresolved', {
|
|
107
|
-
reason: 'unattended_block_unresolved',
|
|
108
|
-
site,
|
|
109
|
-
blockedFromPhase,
|
|
110
|
-
phase: failedState.phase,
|
|
111
|
-
scopeNumber: failedState.currentScopeNumber,
|
|
112
|
-
});
|
|
113
|
-
return failedState;
|
|
114
|
-
}
|
|
115
89
|
export async function persistBlockedScope(state, statePath, reason) {
|
|
116
90
|
const scopeLabel = getCurrentScopeLabel(state);
|
|
117
91
|
if (state.completedScopes.some((scope) => scope.number === scopeLabel)) {
|
|
@@ -103,11 +103,7 @@ export async function runOnePass(args) {
|
|
|
103
103
|
archivedReviewPath: currentState.archivedReviewPath,
|
|
104
104
|
});
|
|
105
105
|
if (currentState.phase === 'blocked' || currentState.phase === 'done') {
|
|
106
|
-
|
|
107
|
-
// phase:'blocked' with status:'failed') must keep its failed retrospective
|
|
108
|
-
// rather than be overwritten as a blocked one.
|
|
109
|
-
const retrospectiveReason = currentState.status === 'failed' ? 'failed' : currentState.phase === 'blocked' ? 'blocked' : 'done';
|
|
110
|
-
await runtime.writeCheckpointRetrospective(currentState, retrospectiveReason);
|
|
106
|
+
await runtime.writeCheckpointRetrospective(currentState, currentState.phase === 'blocked' ? 'blocked' : 'done');
|
|
111
107
|
}
|
|
112
108
|
return currentState;
|
|
113
109
|
}
|
|
@@ -74,7 +74,6 @@ export async function initializeOrchestration(planDoc, cwd, agentConfig, topLeve
|
|
|
74
74
|
topLevelMode,
|
|
75
75
|
allowedDirtyPaths: options?.allowedDirtyPaths ?? [],
|
|
76
76
|
agentConfig,
|
|
77
|
-
unattended: options?.unattended ?? false,
|
|
78
77
|
autoSquashOnCompletion: options?.autoSquashOnCompletion ?? true,
|
|
79
78
|
progressJsonPath: join(logger.runDir, 'plan-progress.json'),
|
|
80
79
|
progressMarkdownPath: join(logger.runDir, 'PLAN_PROGRESS.md'),
|
package/dist/neal/plan-queue.js
CHANGED
|
@@ -148,7 +148,6 @@ export async function runPlanAndExecuteQueue(args) {
|
|
|
148
148
|
state: queue,
|
|
149
149
|
agentConfig: args.agentConfig,
|
|
150
150
|
squashOnCompletion: args.squashOnCompletion ?? true,
|
|
151
|
-
unattended: args.unattended ?? false,
|
|
152
151
|
deps: args.deps,
|
|
153
152
|
});
|
|
154
153
|
}
|
|
@@ -162,7 +161,7 @@ export async function continuePlanAndExecuteQueue(args) {
|
|
|
162
161
|
break;
|
|
163
162
|
}
|
|
164
163
|
if (item.status === 'pending') {
|
|
165
|
-
state = await runQueueChildStage(state, item.index, 'planning', args.agentConfig, args.squashOnCompletion ?? true,
|
|
164
|
+
state = await runQueueChildStage(state, item.index, 'planning', args.agentConfig, args.squashOnCompletion ?? true, deps);
|
|
166
165
|
if (state.status !== 'running') {
|
|
167
166
|
return state;
|
|
168
167
|
}
|
|
@@ -172,7 +171,7 @@ export async function continuePlanAndExecuteQueue(args) {
|
|
|
172
171
|
break;
|
|
173
172
|
}
|
|
174
173
|
if (plannedItem.status === 'planned') {
|
|
175
|
-
state = await runQueueChildStage(state, plannedItem.index, 'execution', args.agentConfig, args.squashOnCompletion ?? true,
|
|
174
|
+
state = await runQueueChildStage(state, plannedItem.index, 'execution', args.agentConfig, args.squashOnCompletion ?? true, deps);
|
|
176
175
|
if (state.status !== 'running') {
|
|
177
176
|
return state;
|
|
178
177
|
}
|
|
@@ -220,15 +219,9 @@ export async function continuePlanAndExecuteQueueFromChildRun(args) {
|
|
|
220
219
|
state: advancedState,
|
|
221
220
|
agentConfig: args.agentConfig,
|
|
222
221
|
// Carry the squash preference forward across the cross-process
|
|
223
|
-
// queue-resume handoff the
|
|
224
|
-
//
|
|
222
|
+
// queue-resume handoff: the resumed child's persisted run state wins when
|
|
223
|
+
// the caller supplies nothing.
|
|
225
224
|
squashOnCompletion: args.squashOnCompletion ?? args.childResult.finalState.autoSquashOnCompletion,
|
|
226
|
-
// Carry unattended forward across the cross-process queue-resume handoff.
|
|
227
|
-
// The resumed child's persisted finalState is the authoritative source (it
|
|
228
|
-
// was initialized with the value the original `neal run` resolved), so
|
|
229
|
-
// subsequent queue children do not silently revert to attended mode. An
|
|
230
|
-
// explicit caller-supplied value still wins.
|
|
231
|
-
unattended: args.unattended ?? args.childResult.finalState.unattended,
|
|
232
225
|
deps: args.deps,
|
|
233
226
|
});
|
|
234
227
|
}
|
|
@@ -352,7 +345,7 @@ function resolveQueueRunnerDeps(deps) {
|
|
|
352
345
|
runFreshChild: deps?.runFreshChild ?? runFreshPlanAndExecuteChild,
|
|
353
346
|
};
|
|
354
347
|
}
|
|
355
|
-
async function runQueueChildStage(state, itemIndex, stage, agentConfig, squashOnCompletion,
|
|
348
|
+
async function runQueueChildStage(state, itemIndex, stage, agentConfig, squashOnCompletion, deps) {
|
|
356
349
|
try {
|
|
357
350
|
await assertQueueWorktreeReady(state, deps);
|
|
358
351
|
}
|
|
@@ -374,7 +367,6 @@ async function runQueueChildStage(state, itemIndex, stage, agentConfig, squashOn
|
|
|
374
367
|
planDoc,
|
|
375
368
|
agentConfig,
|
|
376
369
|
squashOnCompletion,
|
|
377
|
-
unattended,
|
|
378
370
|
}, async (child) => {
|
|
379
371
|
initialized = true;
|
|
380
372
|
activeState = await recordQueueChildInitialized(activeState, itemIndex, stage, child);
|
|
@@ -490,7 +482,6 @@ async function runFreshPlanAndExecuteChild(args, onInitialized) {
|
|
|
490
482
|
const loaded = await loadOrInitialize(planDoc, cwd, args.agentConfig, undefined, topLevelMode, {
|
|
491
483
|
allowedDirtyPaths: args.stage === 'execution' ? [planDoc] : [],
|
|
492
484
|
runDir: prepared.runDir,
|
|
493
|
-
unattended: args.unattended,
|
|
494
485
|
// Seed the queue's resolved squash preference onto the child run
|
|
495
486
|
// state (unstaged: planning children persist it too so a resumed
|
|
496
487
|
// planning child carries it back into queue continuation). Once
|
|
@@ -513,7 +504,6 @@ async function runFreshPlanAndExecuteChild(args, onInitialized) {
|
|
|
513
504
|
});
|
|
514
505
|
return executeRun(loaded.state, loaded.statePath, loaded.logger, {
|
|
515
506
|
autoSquashOnCompletion: args.stage === 'execution' && args.squashOnCompletion,
|
|
516
|
-
unattended: args.unattended,
|
|
517
507
|
});
|
|
518
508
|
});
|
|
519
509
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { renderInlinedRangeDiffSection, truncateInlineSectionBody } from '../context/inline-review-context.js';
|
|
2
|
-
import { AUTONOMY_BLOCKED, AUTONOMY_DONE, AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN, buildProgressSection, getCanonicalPlanContractLines, getDerivedPlanSectionContractLines, getExecuteScopeProgressPayloadContractLines, getProtocolMarkerArtifactProhibitionLines, getStandalonePlanPayloadSourceOfTruthLines, getTerminalMarkerArtifactBoundaryLines,
|
|
2
|
+
import { AUTONOMY_BLOCKED, AUTONOMY_DONE, AUTONOMY_SCOPE_DONE, AUTONOMY_SPLIT_PLAN, buildProgressSection, getCanonicalPlanContractLines, getDerivedPlanSectionContractLines, getExecuteScopeProgressPayloadContractLines, getProtocolMarkerArtifactProhibitionLines, getStandalonePlanPayloadSourceOfTruthLines, getTerminalMarkerArtifactBoundaryLines, } from './shared.js';
|
|
3
3
|
import { assertPromptBuilder } from './assert-builder.js';
|
|
4
4
|
import { getUserGuidanceLines } from './guidance.js';
|
|
5
5
|
import { getAdversarialReviewDoctrineLines, getCodeReviewFalsificationLines, getFindingQualityLines, getPreexistingFailureContractLines, getRegressionPreservationLines, getVerificationSkepticismLines, } from './review-doctrine.js';
|
|
@@ -11,7 +11,7 @@ export const CODER_PREEXISTING_FAILURE_LINES = [
|
|
|
11
11
|
"When it is inside the surface and the fix is bounded, fix it and add a focused regression check when practical. When it is inside the surface but the fix is not bounded, do not silently expand the scope; surface it as a blocking concern through this prompt's blocked escape naming the failure as the blocker, or through this prompt's split-plan escape with a derived plan that sequences the fix, so it becomes an explicit decision.",
|
|
12
12
|
'When it is outside the surface, record what you observed and why it cannot affect the required behavior in your progress justification, then continue without fixing it. Fixing out-of-surface pre-existing issues is scope drift.',
|
|
13
13
|
];
|
|
14
|
-
export function buildScopePrompt(planDoc, progressText
|
|
14
|
+
export function buildScopePrompt(planDoc, progressText) {
|
|
15
15
|
const spec = assertPromptBuilder('scope_coder', 'buildScopePrompt', PROMPT_MODULE_PATH);
|
|
16
16
|
const primaryVariant = spec.variants.find((variant) => variant.kind === 'primary');
|
|
17
17
|
if (!primaryVariant) {
|
|
@@ -25,7 +25,6 @@ export function buildScopePrompt(planDoc, progressText, options) {
|
|
|
25
25
|
'2. Read any companion docs or required-context files explicitly referenced by that plan before starting work.',
|
|
26
26
|
'3. Reset your instructions for this turn from the current contents of the plan, the inlined progress state below, and required context.',
|
|
27
27
|
'',
|
|
28
|
-
...getUnattendedAutonomyLines(options?.unattended),
|
|
29
28
|
'Then execute exactly one implementation scope.',
|
|
30
29
|
'Do not start a second scope in this turn.',
|
|
31
30
|
'Return only the structured execution envelope requested by the provider schema.',
|
|
@@ -198,7 +197,6 @@ export function buildReviewerPrompt(args) {
|
|
|
198
197
|
'Do not use `block_for_operator` solely to ask Neal to retire an already-complete parent objective inside that derived-plan case; use `advance_parent` only for that narrow empty-derived-scope case.',
|
|
199
198
|
'Keep `block_for_operator` for ambiguous already-satisfied claims, missing evidence, missing operator decisions, or unsafe parent-completion claims.',
|
|
200
199
|
'A non-empty or uncertain current diff is the normal case, not a reason to escalate: handle it by accepting, requesting a bounded revision, or splitting the plan rather than choosing `block_for_operator`.',
|
|
201
|
-
...getUnattendedAutonomyLines(args.unattended),
|
|
202
200
|
'Use `meaningfulProgressRationale` to explain the convergence judgment against the parent objective and recent accepted-scope history. Do not use it to restate correctness findings.',
|
|
203
201
|
'',
|
|
204
202
|
'Coder progress justification for this scope:',
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { AUTONOMY_BLOCKED, AUTONOMY_DONE, getCanonicalPlanContractLines, getDerivedPlanSectionContractLines, getProtocolMarkerArtifactProhibitionLines, getTerminalMarkerArtifactBoundaryLines,
|
|
1
|
+
import { AUTONOMY_BLOCKED, AUTONOMY_DONE, getCanonicalPlanContractLines, getDerivedPlanSectionContractLines, getProtocolMarkerArtifactProhibitionLines, getTerminalMarkerArtifactBoundaryLines, } from './shared.js';
|
|
2
2
|
import { assertPromptBuilder } from './assert-builder.js';
|
|
3
3
|
import { getUserGuidanceLines } from './guidance.js';
|
|
4
4
|
const PROMPT_MODULE_PATH = 'src/neal/prompts/planning.ts';
|
|
@@ -142,7 +142,6 @@ export function buildPlanningPrompt(planDoc, planDocument, options) {
|
|
|
142
142
|
...getPlanningPromptBaseLines(planDoc),
|
|
143
143
|
...getInlineCurrentPlanLines(planDocument),
|
|
144
144
|
...getAuthoredOneShotPlanningLines(options?.authoredOneShot),
|
|
145
|
-
...getUnattendedAutonomyLines(options?.unattended),
|
|
146
145
|
...getProtocolMarkerArtifactProhibitionLines(),
|
|
147
146
|
...getCanonicalPlanContractLines(),
|
|
148
147
|
'If critical information is missing, do not invent it. Surface the concrete missing questions in your final response.',
|
|
@@ -221,7 +220,6 @@ export function buildPlanReviewerPrompt(args) {
|
|
|
221
220
|
reviewHistoryLine,
|
|
222
221
|
...getAuthoredOneShotReviewerLines(args.authoredOneShot),
|
|
223
222
|
...getReviewerContextLines(args.reviewerContext),
|
|
224
|
-
...getUnattendedAutonomyLines(args.unattended),
|
|
225
223
|
...getUserGuidanceLines('reviewer'),
|
|
226
224
|
'',
|
|
227
225
|
planInspectionLine,
|
|
@@ -4,13 +4,6 @@ export const AUTONOMY_CHUNK_DONE = 'AUTONOMY_CHUNK_DONE';
|
|
|
4
4
|
export const AUTONOMY_DONE = 'AUTONOMY_DONE';
|
|
5
5
|
export const AUTONOMY_BLOCKED = 'AUTONOMY_BLOCKED';
|
|
6
6
|
export const AUTONOMY_SPLIT_PLAN = 'AUTONOMY_SPLIT_PLAN';
|
|
7
|
-
// Single autonomy line rendered into agent prompts when the run is unattended.
|
|
8
|
-
// It tells the agent no operator can answer while keeping every verification
|
|
9
|
-
// requirement intact (unattended means "no human to ask", not "skip checks").
|
|
10
|
-
export const UNATTENDED_AUTONOMY_PROMPT_LINE = 'No operator is available to answer. Resolve this autonomously: do not escalate for operator guidance; make your best judgment and keep all verification requirements intact.';
|
|
11
|
-
export function getUnattendedAutonomyLines(unattended) {
|
|
12
|
-
return unattended ? [UNATTENDED_AUTONOMY_PROMPT_LINE] : [];
|
|
13
|
-
}
|
|
14
7
|
export function getCanonicalPlanContractLines() {
|
|
15
8
|
return [
|
|
16
9
|
'Choose exactly one execution shape: `one_shot`, `multi_scope`, or `multi_scope_unknown`.',
|