@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,496 @@
|
|
|
1
|
+
import { EXECUTE_FINALIZATION_PHASE } from './execute-finalization.js';
|
|
2
|
+
// state.ts requires the current v1 record shape; this layer validates semantic relationships after parsing.
|
|
3
|
+
export const ORCHESTRATION_PHASES = [
|
|
4
|
+
'coder_plan',
|
|
5
|
+
'reviewer_plan',
|
|
6
|
+
'coder_plan_response',
|
|
7
|
+
'coder_plan_optional_response',
|
|
8
|
+
'awaiting_derived_plan_execution',
|
|
9
|
+
'coder_scope',
|
|
10
|
+
'manual_gate',
|
|
11
|
+
'reviewer_scope',
|
|
12
|
+
'coder_response',
|
|
13
|
+
'coder_optional_response',
|
|
14
|
+
'interactive_blocked_recovery',
|
|
15
|
+
EXECUTE_FINALIZATION_PHASE,
|
|
16
|
+
'final_completion_review',
|
|
17
|
+
'done',
|
|
18
|
+
'blocked',
|
|
19
|
+
];
|
|
20
|
+
export const ORCHESTRATION_STATUSES = [
|
|
21
|
+
'running',
|
|
22
|
+
'paused',
|
|
23
|
+
'done',
|
|
24
|
+
'blocked',
|
|
25
|
+
'failed',
|
|
26
|
+
];
|
|
27
|
+
export const INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES = [
|
|
28
|
+
'coder_plan',
|
|
29
|
+
'reviewer_plan',
|
|
30
|
+
'coder_plan_response',
|
|
31
|
+
'coder_plan_optional_response',
|
|
32
|
+
'awaiting_derived_plan_execution',
|
|
33
|
+
'coder_scope',
|
|
34
|
+
'reviewer_scope',
|
|
35
|
+
'coder_response',
|
|
36
|
+
'coder_optional_response',
|
|
37
|
+
EXECUTE_FINALIZATION_PHASE,
|
|
38
|
+
'final_completion_review',
|
|
39
|
+
];
|
|
40
|
+
export const RESUMABLE_BLOCKED_PHASES = [
|
|
41
|
+
'coder_scope',
|
|
42
|
+
'coder_response',
|
|
43
|
+
'coder_optional_response',
|
|
44
|
+
'coder_plan',
|
|
45
|
+
'coder_plan_response',
|
|
46
|
+
'coder_plan_optional_response',
|
|
47
|
+
];
|
|
48
|
+
const ORCHESTRATION_PHASE_SET = new Set(ORCHESTRATION_PHASES);
|
|
49
|
+
const ORCHESTRATION_STATUS_SET = new Set(ORCHESTRATION_STATUSES);
|
|
50
|
+
const INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASE_SET = new Set(INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES);
|
|
51
|
+
const SCOPE_MARKERS = new Set([
|
|
52
|
+
'AUTONOMY_SCOPE_DONE',
|
|
53
|
+
'AUTONOMY_CHUNK_DONE',
|
|
54
|
+
'AUTONOMY_DONE',
|
|
55
|
+
'AUTONOMY_BLOCKED',
|
|
56
|
+
'AUTONOMY_SPLIT_PLAN',
|
|
57
|
+
]);
|
|
58
|
+
const FINAL_COMPLETION_ACTIONS = new Set([
|
|
59
|
+
'accept_complete',
|
|
60
|
+
'continue_execution',
|
|
61
|
+
'block_for_operator',
|
|
62
|
+
]);
|
|
63
|
+
const CODER_SESSION_PROTOCOLS = new Set([
|
|
64
|
+
'legacy_marker_v1',
|
|
65
|
+
'structured_json_v1',
|
|
66
|
+
]);
|
|
67
|
+
function formatInvalidValue(value) {
|
|
68
|
+
return typeof value === 'string' ? JSON.stringify(value) : String(value);
|
|
69
|
+
}
|
|
70
|
+
function formatValidationContext(context) {
|
|
71
|
+
return context.path ? ` during ${context.source} at ${context.path}` : ` during ${context.source}`;
|
|
72
|
+
}
|
|
73
|
+
function throwStateInvariant(context, fieldPath, reason) {
|
|
74
|
+
throw new Error(`Invalid run state${formatValidationContext(context)}: invalid ${fieldPath}: ${reason}`);
|
|
75
|
+
}
|
|
76
|
+
export function isOrchestrationPhase(value) {
|
|
77
|
+
return typeof value === 'string' && ORCHESTRATION_PHASE_SET.has(value);
|
|
78
|
+
}
|
|
79
|
+
export function assertOrchestrationPhase(value, fieldPath = 'phase') {
|
|
80
|
+
if (!isOrchestrationPhase(value)) {
|
|
81
|
+
throw new Error(`Invalid run state: invalid ${fieldPath}: expected orchestration phase, received ${formatInvalidValue(value)}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
function assertKnownPhase(value, fieldPath, context) {
|
|
85
|
+
if (!isOrchestrationPhase(value)) {
|
|
86
|
+
throwStateInvariant(context, fieldPath, `expected orchestration phase, received ${formatInvalidValue(value)}`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
function assertKnownStatus(value, fieldPath, context) {
|
|
90
|
+
if (!isOrchestrationStatus(value)) {
|
|
91
|
+
throwStateInvariant(context, fieldPath, `expected orchestration status, received ${formatInvalidValue(value)}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
function assertSafeIntegerAtLeast(value, fieldPath, minimum, context) {
|
|
95
|
+
if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < minimum) {
|
|
96
|
+
throwStateInvariant(context, fieldPath, `expected safe integer greater than or equal to ${minimum}, received ${formatInvalidValue(value)}`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
function assertNonEmptyString(value, fieldPath, context) {
|
|
100
|
+
if (typeof value !== 'string' || value.trim() === '') {
|
|
101
|
+
throwStateInvariant(context, fieldPath, `expected non-empty string, received ${formatInvalidValue(value)}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function assertNullableSafeIntegerAtLeast(value, fieldPath, minimum, context) {
|
|
105
|
+
if (value !== null) {
|
|
106
|
+
assertSafeIntegerAtLeast(value, fieldPath, minimum, context);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
function assertOptionalSafeIntegerAtLeast(value, fieldPath, minimum, context) {
|
|
110
|
+
if (value !== undefined) {
|
|
111
|
+
assertSafeIntegerAtLeast(value, fieldPath, minimum, context);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
function assertUniqueCompletedScopeNumbers(completedScopes, context) {
|
|
115
|
+
const seen = new Set();
|
|
116
|
+
for (let index = 0; index < completedScopes.length; index += 1) {
|
|
117
|
+
const number = completedScopes[index].number;
|
|
118
|
+
if (seen.has(number)) {
|
|
119
|
+
throwStateInvariant(context, `completedScopes[${index}].number`, `duplicate completed scope label ${JSON.stringify(number)}`);
|
|
120
|
+
}
|
|
121
|
+
seen.add(number);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
function getInteractiveBlockedRecoveryResumePhase(sourcePhase) {
|
|
125
|
+
switch (sourcePhase) {
|
|
126
|
+
case 'reviewer_scope':
|
|
127
|
+
return 'coder_response';
|
|
128
|
+
case 'reviewer_plan':
|
|
129
|
+
return 'coder_plan_response';
|
|
130
|
+
case 'awaiting_derived_plan_execution':
|
|
131
|
+
return 'coder_scope';
|
|
132
|
+
default:
|
|
133
|
+
return sourcePhase;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
function isAllowedInteractiveBlockedRecoveryDispositionResult(args) {
|
|
137
|
+
switch (args.action) {
|
|
138
|
+
case 'resume_current_scope':
|
|
139
|
+
return args.resultPhase === getInteractiveBlockedRecoveryResumePhase(args.sourcePhase);
|
|
140
|
+
case 'replace_current_scope':
|
|
141
|
+
return args.resultPhase === 'reviewer_plan' || args.resultPhase === 'blocked';
|
|
142
|
+
case 'stay_blocked':
|
|
143
|
+
return args.resultPhase === 'interactive_blocked_recovery';
|
|
144
|
+
case 'terminal_block':
|
|
145
|
+
return args.resultPhase === 'blocked';
|
|
146
|
+
default:
|
|
147
|
+
return false;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function assertInteractiveBlockedRecoveryDispositionResult(args) {
|
|
151
|
+
if (!isAllowedInteractiveBlockedRecoveryDispositionResult({
|
|
152
|
+
action: args.action,
|
|
153
|
+
resultPhase: args.resultPhase,
|
|
154
|
+
sourcePhase: args.sourcePhase,
|
|
155
|
+
})) {
|
|
156
|
+
throwStateInvariant(args.context, args.fieldPath, `action ${JSON.stringify(args.action)} cannot result in phase ${JSON.stringify(args.resultPhase)} from source phase ${JSON.stringify(args.sourcePhase)}`);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
function assertValidInteractiveBlockedRecoveryState(args) {
|
|
160
|
+
const { recovery, fieldPath, state, context } = args;
|
|
161
|
+
if (state.topLevelMode !== 'execute') {
|
|
162
|
+
throwStateInvariant(context, 'topLevelMode', `interactive blocked recovery is unsupported for ${formatInvalidValue(state.topLevelMode)}`);
|
|
163
|
+
}
|
|
164
|
+
if (!INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASE_SET.has(recovery.sourcePhase)) {
|
|
165
|
+
throwStateInvariant(context, `${fieldPath}.sourcePhase`, `unsupported interactive blocked recovery source phase ${JSON.stringify(recovery.sourcePhase)}`);
|
|
166
|
+
}
|
|
167
|
+
assertSafeIntegerAtLeast(recovery.maxTurns, `${fieldPath}.maxTurns`, 1, context);
|
|
168
|
+
assertSafeIntegerAtLeast(recovery.lastHandledTurn, `${fieldPath}.lastHandledTurn`, 0, context);
|
|
169
|
+
if (recovery.lastHandledTurn > recovery.turns.length) {
|
|
170
|
+
throwStateInvariant(context, `${fieldPath}.lastHandledTurn`, `must not exceed recorded turn count ${recovery.turns.length}`);
|
|
171
|
+
}
|
|
172
|
+
if (recovery.turns.length > recovery.maxTurns) {
|
|
173
|
+
throwStateInvariant(context, `${fieldPath}.turns`, `recorded turn count ${recovery.turns.length} exceeds maxTurns ${recovery.maxTurns}`);
|
|
174
|
+
}
|
|
175
|
+
if (recovery.pendingDirective) {
|
|
176
|
+
assertNonEmptyString(recovery.pendingDirective.recordedAt, `${fieldPath}.pendingDirective.recordedAt`, context);
|
|
177
|
+
assertNonEmptyString(recovery.pendingDirective.operatorGuidance, `${fieldPath}.pendingDirective.operatorGuidance`, context);
|
|
178
|
+
}
|
|
179
|
+
for (let index = 0; index < recovery.turns.length; index += 1) {
|
|
180
|
+
const turn = recovery.turns[index];
|
|
181
|
+
const expectedTurnNumber = index + 1;
|
|
182
|
+
if (turn.number !== expectedTurnNumber) {
|
|
183
|
+
throwStateInvariant(context, `${fieldPath}.turns[${index}].number`, `expected contiguous turn number ${expectedTurnNumber}, received ${formatInvalidValue(turn.number)}`);
|
|
184
|
+
}
|
|
185
|
+
if (turn.disposition) {
|
|
186
|
+
assertKnownPhase(turn.disposition.resultingPhase, `${fieldPath}.turns[${index}].disposition.resultingPhase`, context);
|
|
187
|
+
assertInteractiveBlockedRecoveryDispositionResult({
|
|
188
|
+
action: turn.disposition.action,
|
|
189
|
+
resultPhase: turn.disposition.resultingPhase,
|
|
190
|
+
sourcePhase: recovery.sourcePhase,
|
|
191
|
+
fieldPath: `${fieldPath}.turns[${index}].disposition.resultingPhase`,
|
|
192
|
+
context,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
function assertValidInteractiveBlockedRecoveryHistory(state, context) {
|
|
198
|
+
for (let index = 0; index < state.interactiveBlockedRecoveryHistory.length; index += 1) {
|
|
199
|
+
const record = state.interactiveBlockedRecoveryHistory[index];
|
|
200
|
+
const fieldPath = `interactiveBlockedRecoveryHistory[${index}]`;
|
|
201
|
+
assertValidInteractiveBlockedRecoveryState({
|
|
202
|
+
recovery: record,
|
|
203
|
+
fieldPath,
|
|
204
|
+
state,
|
|
205
|
+
context,
|
|
206
|
+
});
|
|
207
|
+
assertKnownPhase(record.resultPhase, `${fieldPath}.resultPhase`, context);
|
|
208
|
+
assertInteractiveBlockedRecoveryDispositionResult({
|
|
209
|
+
action: record.resolvedByAction,
|
|
210
|
+
resultPhase: record.resultPhase,
|
|
211
|
+
sourcePhase: record.sourcePhase,
|
|
212
|
+
fieldPath: `${fieldPath}.resultPhase`,
|
|
213
|
+
context,
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
function assertValidInteractiveBlockedRecovery(state, context) {
|
|
218
|
+
if (state.phase === 'interactive_blocked_recovery' && !state.interactiveBlockedRecovery) {
|
|
219
|
+
throwStateInvariant(context, 'interactiveBlockedRecovery', 'expected active recovery state when phase is "interactive_blocked_recovery"');
|
|
220
|
+
}
|
|
221
|
+
if (state.interactiveBlockedRecovery) {
|
|
222
|
+
if (state.phase !== 'interactive_blocked_recovery') {
|
|
223
|
+
throwStateInvariant(context, 'phase', `expected "interactive_blocked_recovery" while interactiveBlockedRecovery is active, received ${JSON.stringify(state.phase)}`);
|
|
224
|
+
}
|
|
225
|
+
assertValidInteractiveBlockedRecoveryState({
|
|
226
|
+
recovery: state.interactiveBlockedRecovery,
|
|
227
|
+
fieldPath: 'interactiveBlockedRecovery',
|
|
228
|
+
state,
|
|
229
|
+
context,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
assertValidInteractiveBlockedRecoveryHistory(state, context);
|
|
233
|
+
}
|
|
234
|
+
function assertValidManualGate(state, context) {
|
|
235
|
+
const manualGate = state.manualGate;
|
|
236
|
+
if (state.phase === 'manual_gate' && manualGate === null) {
|
|
237
|
+
throwStateInvariant(context, 'manualGate', 'expected active manual gate when phase is "manual_gate"');
|
|
238
|
+
}
|
|
239
|
+
if (state.phase !== 'manual_gate' && manualGate !== null) {
|
|
240
|
+
throwStateInvariant(context, 'phase', `expected "manual_gate" while manualGate is active, received ${JSON.stringify(state.phase)}`);
|
|
241
|
+
}
|
|
242
|
+
if (manualGate === null) {
|
|
243
|
+
return;
|
|
244
|
+
}
|
|
245
|
+
if (state.topLevelMode !== 'execute') {
|
|
246
|
+
throwStateInvariant(context, 'topLevelMode', 'manual gates are supported only for execute-mode runs');
|
|
247
|
+
}
|
|
248
|
+
if (state.status !== 'running') {
|
|
249
|
+
throwStateInvariant(context, 'status', `expected "running" while manualGate is active, received ${JSON.stringify(state.status)}`);
|
|
250
|
+
}
|
|
251
|
+
if (state.blockedFromPhase !== null) {
|
|
252
|
+
throwStateInvariant(context, 'blockedFromPhase', 'expected null while manualGate is active');
|
|
253
|
+
}
|
|
254
|
+
assertNonEmptyString(manualGate.id, 'manualGate.id', context);
|
|
255
|
+
assertNonEmptyString(manualGate.title, 'manualGate.title', context);
|
|
256
|
+
assertNonEmptyString(manualGate.reason, 'manualGate.reason', context);
|
|
257
|
+
assertNonEmptyString(manualGate.instructionsPath, 'manualGate.instructionsPath', context);
|
|
258
|
+
assertNonEmptyString(manualGate.createdAt, 'manualGate.createdAt', context);
|
|
259
|
+
assertNonEmptyString(manualGate.updatedAt, 'manualGate.updatedAt', context);
|
|
260
|
+
if (manualGate.lastCheckedAt !== null) {
|
|
261
|
+
assertNonEmptyString(manualGate.lastCheckedAt, 'manualGate.lastCheckedAt', context);
|
|
262
|
+
}
|
|
263
|
+
if (manualGate.resumePhase !== 'coder_scope') {
|
|
264
|
+
throwStateInvariant(context, 'manualGate.resumePhase', `expected "coder_scope", received ${formatInvalidValue(manualGate.resumePhase)}`);
|
|
265
|
+
}
|
|
266
|
+
if (manualGate.resumeChecks.length === 0) {
|
|
267
|
+
throwStateInvariant(context, 'manualGate.resumeChecks', 'expected at least one resume check');
|
|
268
|
+
}
|
|
269
|
+
for (let index = 0; index < manualGate.resumeChecks.length; index += 1) {
|
|
270
|
+
const check = manualGate.resumeChecks[index];
|
|
271
|
+
const fieldPath = `manualGate.resumeChecks[${index}]`;
|
|
272
|
+
if (check.type !== 'command') {
|
|
273
|
+
throwStateInvariant(context, `${fieldPath}.type`, `expected "command", received ${formatInvalidValue(check.type)}`);
|
|
274
|
+
}
|
|
275
|
+
assertNonEmptyString(check.name, `${fieldPath}.name`, context);
|
|
276
|
+
if (!Array.isArray(check.command) || check.command.length === 0) {
|
|
277
|
+
throwStateInvariant(context, `${fieldPath}.command`, 'expected non-empty string array');
|
|
278
|
+
}
|
|
279
|
+
for (let commandIndex = 0; commandIndex < check.command.length; commandIndex += 1) {
|
|
280
|
+
assertNonEmptyString(check.command[commandIndex], `${fieldPath}.command[${commandIndex}]`, context);
|
|
281
|
+
}
|
|
282
|
+
if (check.cwd !== undefined && check.cwd !== 'repo' && check.cwd !== 'run_dir') {
|
|
283
|
+
throwStateInvariant(context, `${fieldPath}.cwd`, `expected "repo" or "run_dir", received ${formatInvalidValue(check.cwd)}`);
|
|
284
|
+
}
|
|
285
|
+
assertOptionalSafeIntegerAtLeast(check.timeoutMs, `${fieldPath}.timeoutMs`, 1, context);
|
|
286
|
+
}
|
|
287
|
+
if (manualGate.lastFailure !== null) {
|
|
288
|
+
assertNonEmptyString(manualGate.lastFailure.checkName, 'manualGate.lastFailure.checkName', context);
|
|
289
|
+
assertNullableSafeIntegerAtLeast(manualGate.lastFailure.exitCode, 'manualGate.lastFailure.exitCode', 0, context);
|
|
290
|
+
if (manualGate.lastFailure.signal !== null) {
|
|
291
|
+
assertNonEmptyString(manualGate.lastFailure.signal, 'manualGate.lastFailure.signal', context);
|
|
292
|
+
}
|
|
293
|
+
if (typeof manualGate.lastFailure.stdoutTail !== 'string') {
|
|
294
|
+
throwStateInvariant(context, 'manualGate.lastFailure.stdoutTail', 'expected string');
|
|
295
|
+
}
|
|
296
|
+
if (typeof manualGate.lastFailure.stderrTail !== 'string') {
|
|
297
|
+
throwStateInvariant(context, 'manualGate.lastFailure.stderrTail', 'expected string');
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
function assertValidPendingPlanReviewGuidance(state, context) {
|
|
302
|
+
const guidance = state.pendingPlanReviewGuidance;
|
|
303
|
+
if (guidance === null) {
|
|
304
|
+
return;
|
|
305
|
+
}
|
|
306
|
+
if (state.topLevelMode !== 'plan') {
|
|
307
|
+
throwStateInvariant(context, 'pendingPlanReviewGuidance', `expected null for ${formatInvalidValue(state.topLevelMode)} mode`);
|
|
308
|
+
}
|
|
309
|
+
assertNonEmptyString(guidance.message, 'pendingPlanReviewGuidance.message', context);
|
|
310
|
+
if (guidance.sourcePhase !== 'reviewer_plan') {
|
|
311
|
+
throwStateInvariant(context, 'pendingPlanReviewGuidance.sourcePhase', `expected "reviewer_plan", received ${formatInvalidValue(guidance.sourcePhase)}`);
|
|
312
|
+
}
|
|
313
|
+
if (typeof guidance.recordedAt !== 'string') {
|
|
314
|
+
throwStateInvariant(context, 'pendingPlanReviewGuidance.recordedAt', `expected string, received ${formatInvalidValue(guidance.recordedAt)}`);
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function assertValidDerivedPlanState(state, context) {
|
|
318
|
+
assertNullableSafeIntegerAtLeast(state.derivedFromScopeNumber, 'derivedFromScopeNumber', 0, context);
|
|
319
|
+
assertNullableSafeIntegerAtLeast(state.derivedScopeIndex, 'derivedScopeIndex', 1, context);
|
|
320
|
+
if (state.derivedPlanStatus !== null && state.derivedPlanPath === null) {
|
|
321
|
+
throwStateInvariant(context, 'derivedPlanPath', `expected non-null path when derivedPlanStatus is ${JSON.stringify(state.derivedPlanStatus)}`);
|
|
322
|
+
}
|
|
323
|
+
if (state.derivedPlanPath !== null) {
|
|
324
|
+
assertNonEmptyString(state.derivedPlanPath, 'derivedPlanPath', context);
|
|
325
|
+
}
|
|
326
|
+
if (state.derivedScopeIndex !== null) {
|
|
327
|
+
if (state.topLevelMode !== 'execute') {
|
|
328
|
+
throwStateInvariant(context, 'topLevelMode', 'derived scope execution is supported only for execute-mode runs');
|
|
329
|
+
}
|
|
330
|
+
if (state.derivedPlanStatus !== 'accepted') {
|
|
331
|
+
throwStateInvariant(context, 'derivedPlanStatus', `expected "accepted" when derivedScopeIndex is set, received ${formatInvalidValue(state.derivedPlanStatus)}`);
|
|
332
|
+
}
|
|
333
|
+
if (state.derivedPlanPath === null) {
|
|
334
|
+
throwStateInvariant(context, 'derivedPlanPath', 'expected non-null path when derivedScopeIndex is set');
|
|
335
|
+
}
|
|
336
|
+
if (state.derivedFromScopeNumber === null) {
|
|
337
|
+
throwStateInvariant(context, 'derivedFromScopeNumber', 'expected non-null parent scope when derivedScopeIndex is set');
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
if ((state.derivedPlanStatus === 'pending_review' || state.derivedPlanStatus === 'rejected') &&
|
|
341
|
+
state.derivedScopeIndex !== null) {
|
|
342
|
+
throwStateInvariant(context, 'derivedScopeIndex', `expected null when derivedPlanStatus is ${JSON.stringify(state.derivedPlanStatus)}`);
|
|
343
|
+
}
|
|
344
|
+
if (state.phase === 'awaiting_derived_plan_execution') {
|
|
345
|
+
if (state.topLevelMode !== 'execute' ||
|
|
346
|
+
state.derivedPlanStatus !== 'accepted' ||
|
|
347
|
+
state.derivedPlanPath === null ||
|
|
348
|
+
state.derivedFromScopeNumber === null) {
|
|
349
|
+
throwStateInvariant(context, 'derivedPlanStatus', 'awaiting derived plan execution requires an accepted execute-mode derived plan');
|
|
350
|
+
}
|
|
351
|
+
if (state.derivedScopeIndex !== null) {
|
|
352
|
+
throwStateInvariant(context, 'derivedScopeIndex', 'expected null before accepted derived plan execution starts');
|
|
353
|
+
}
|
|
354
|
+
if (state.createdCommits.length > 0) {
|
|
355
|
+
throwStateInvariant(context, 'createdCommits', 'awaiting derived plan execution must not carry active created commits');
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
if (context.source !== 'load' &&
|
|
359
|
+
state.phase === 'done' &&
|
|
360
|
+
state.derivedScopeIndex === null &&
|
|
361
|
+
(state.derivedPlanStatus === 'accepted' || state.derivedPlanStatus === 'pending_review')) {
|
|
362
|
+
throwStateInvariant(context, 'phase', `cannot be "done" while an unexecuted ${state.derivedPlanStatus === 'accepted' ? 'accepted' : 'pending'} derived plan is still active`);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
function getExpectedFinalCompletionResolvedAction(state) {
|
|
366
|
+
const verdict = state.finalCompletionReviewVerdict;
|
|
367
|
+
if (!verdict) {
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
if (verdict.action === 'continue_execution' && state.finalCompletionContinueExecutionCapReached) {
|
|
371
|
+
return 'block_for_operator';
|
|
372
|
+
}
|
|
373
|
+
return verdict.action;
|
|
374
|
+
}
|
|
375
|
+
function assertValidFinalCompletionState(state, context) {
|
|
376
|
+
if (state.phase === 'final_completion_review') {
|
|
377
|
+
if (state.topLevelMode !== 'execute') {
|
|
378
|
+
throwStateInvariant(context, 'topLevelMode', 'final completion review is supported only for execute-mode runs');
|
|
379
|
+
}
|
|
380
|
+
if (!state.finalCompletionSummary) {
|
|
381
|
+
throwStateInvariant(context, 'finalCompletionSummary', 'expected final completion summary when phase is "final_completion_review"');
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (state.finalCompletionResolvedAction !== null && !FINAL_COMPLETION_ACTIONS.has(state.finalCompletionResolvedAction)) {
|
|
385
|
+
throwStateInvariant(context, 'finalCompletionResolvedAction', `unsupported final completion action ${JSON.stringify(state.finalCompletionResolvedAction)}`);
|
|
386
|
+
}
|
|
387
|
+
if (state.finalCompletionContinueExecutionCapReached) {
|
|
388
|
+
if (state.finalCompletionReviewVerdict?.action !== 'continue_execution') {
|
|
389
|
+
throwStateInvariant(context, 'finalCompletionReviewVerdict.action', 'expected "continue_execution" when final completion continue-execution cap is reached');
|
|
390
|
+
}
|
|
391
|
+
if (state.finalCompletionResolvedAction !== 'block_for_operator') {
|
|
392
|
+
throwStateInvariant(context, 'finalCompletionResolvedAction', 'expected "block_for_operator" when final completion continue-execution cap is reached');
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (state.finalCompletionReviewVerdict && state.finalCompletionResolvedAction === null) {
|
|
396
|
+
throwStateInvariant(context, 'finalCompletionResolvedAction', 'expected resolved action when finalCompletionReviewVerdict is present');
|
|
397
|
+
}
|
|
398
|
+
if (!state.finalCompletionReviewVerdict && state.finalCompletionResolvedAction !== null) {
|
|
399
|
+
throwStateInvariant(context, 'finalCompletionReviewVerdict', 'expected verdict when finalCompletionResolvedAction is present');
|
|
400
|
+
}
|
|
401
|
+
if (state.finalCompletionReviewVerdict &&
|
|
402
|
+
state.finalCompletionReviewVerdict.action !== 'accept_complete' &&
|
|
403
|
+
state.finalCompletionReviewVerdict.squashCommitMessage !== null) {
|
|
404
|
+
throwStateInvariant(context, 'finalCompletionReviewVerdict.squashCommitMessage', `expected null when finalCompletionReviewVerdict.action is ${JSON.stringify(state.finalCompletionReviewVerdict.action)}`);
|
|
405
|
+
}
|
|
406
|
+
const expectedResolvedAction = getExpectedFinalCompletionResolvedAction(state);
|
|
407
|
+
if (expectedResolvedAction && state.finalCompletionResolvedAction !== expectedResolvedAction) {
|
|
408
|
+
throwStateInvariant(context, 'finalCompletionResolvedAction', `expected ${JSON.stringify(expectedResolvedAction)} for finalCompletionReviewVerdict.action ${JSON.stringify(state.finalCompletionReviewVerdict?.action)}, received ${formatInvalidValue(state.finalCompletionResolvedAction)}`);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
function assertValidCompletedScopeAccounting(state, context) {
|
|
412
|
+
for (let index = 0; index < state.completedScopes.length; index += 1) {
|
|
413
|
+
const scope = state.completedScopes[index];
|
|
414
|
+
const fieldPath = `completedScopes[${index}]`;
|
|
415
|
+
assertNonEmptyString(scope.number, `${fieldPath}.number`, context);
|
|
416
|
+
if (!SCOPE_MARKERS.has(scope.marker)) {
|
|
417
|
+
throwStateInvariant(context, `${fieldPath}.marker`, `unsupported scope marker ${formatInvalidValue(scope.marker)}`);
|
|
418
|
+
}
|
|
419
|
+
if (scope.result !== 'accepted' && scope.result !== 'blocked') {
|
|
420
|
+
throwStateInvariant(context, `${fieldPath}.result`, `unsupported scope result ${formatInvalidValue(scope.result)}`);
|
|
421
|
+
}
|
|
422
|
+
assertSafeIntegerAtLeast(scope.reviewRounds, `${fieldPath}.reviewRounds`, 0, context);
|
|
423
|
+
assertSafeIntegerAtLeast(scope.findings, `${fieldPath}.findings`, 0, context);
|
|
424
|
+
if (scope.result === 'accepted' && state.topLevelMode === 'execute') {
|
|
425
|
+
assertNonEmptyString(scope.finalCommit, `${fieldPath}.finalCommit`, context);
|
|
426
|
+
assertNonEmptyString(scope.commitSubject, `${fieldPath}.commitSubject`, context);
|
|
427
|
+
}
|
|
428
|
+
if (scope.result === 'blocked') {
|
|
429
|
+
assertNonEmptyString(scope.blocker, `${fieldPath}.blocker`, context);
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
export function assertValidOrchestrationState(state, context) {
|
|
434
|
+
// This layer validates the parsed ledger without IO.
|
|
435
|
+
assertKnownPhase(state.phase, 'phase', context);
|
|
436
|
+
assertKnownStatus(state.status, 'status', context);
|
|
437
|
+
if (state.blockedFromPhase !== null) {
|
|
438
|
+
assertKnownPhase(state.blockedFromPhase, 'blockedFromPhase', context);
|
|
439
|
+
}
|
|
440
|
+
if (state.plannerSessionProtocol !== null && !CODER_SESSION_PROTOCOLS.has(state.plannerSessionProtocol)) {
|
|
441
|
+
throwStateInvariant(context, 'plannerSessionProtocol', `unsupported planner session protocol ${formatInvalidValue(state.plannerSessionProtocol)}`);
|
|
442
|
+
}
|
|
443
|
+
if (state.plannerSessionHandle === null && state.plannerSessionProtocol !== null) {
|
|
444
|
+
throwStateInvariant(context, 'plannerSessionProtocol', 'must be null when plannerSessionHandle is null');
|
|
445
|
+
}
|
|
446
|
+
if (state.plannerSessionHandle !== null && state.plannerSessionProtocol === null) {
|
|
447
|
+
throwStateInvariant(context, 'plannerSessionProtocol', 'must not be null when plannerSessionHandle is present');
|
|
448
|
+
}
|
|
449
|
+
if (state.coderSessionProtocol !== null && !CODER_SESSION_PROTOCOLS.has(state.coderSessionProtocol)) {
|
|
450
|
+
throwStateInvariant(context, 'coderSessionProtocol', `unsupported coder session protocol ${formatInvalidValue(state.coderSessionProtocol)}`);
|
|
451
|
+
}
|
|
452
|
+
if (state.coderSessionHandle === null && state.coderSessionProtocol !== null) {
|
|
453
|
+
throwStateInvariant(context, 'coderSessionProtocol', 'must be null when coderSessionHandle is null');
|
|
454
|
+
}
|
|
455
|
+
if (state.phase === 'done' && state.status !== 'done') {
|
|
456
|
+
throwStateInvariant(context, 'status', `expected "done" when phase is "done", received ${JSON.stringify(state.status)}`);
|
|
457
|
+
}
|
|
458
|
+
if (state.status === 'done' && state.phase !== 'done') {
|
|
459
|
+
throwStateInvariant(context, 'phase', `expected "done" when status is "done", received ${JSON.stringify(state.phase)}`);
|
|
460
|
+
}
|
|
461
|
+
if (state.phase === 'blocked' && state.status !== 'blocked' && state.status !== 'failed') {
|
|
462
|
+
throwStateInvariant(context, 'status', `expected "blocked" or "failed" when phase is "blocked", received ${JSON.stringify(state.status)}`);
|
|
463
|
+
}
|
|
464
|
+
const activeStatusInTerminalPhase = (state.status === 'running' || state.status === 'paused') &&
|
|
465
|
+
(state.phase === 'done' || state.phase === 'blocked');
|
|
466
|
+
if (activeStatusInTerminalPhase) {
|
|
467
|
+
throwStateInvariant(context, 'status', `cannot be ${JSON.stringify(state.status)} while phase is terminal ${JSON.stringify(state.phase)}`);
|
|
468
|
+
}
|
|
469
|
+
assertSafeIntegerAtLeast(state.currentScopeNumber, 'currentScopeNumber', 0, context);
|
|
470
|
+
assertSafeIntegerAtLeast(state.coderRetryCount, 'coderRetryCount', 0, context);
|
|
471
|
+
assertSafeIntegerAtLeast(state.unattendedAutoResumeCount, 'unattendedAutoResumeCount', 0, context);
|
|
472
|
+
assertSafeIntegerAtLeast(state.reviewStuckArbiterCount, 'reviewStuckArbiterCount', 0, context);
|
|
473
|
+
assertSafeIntegerAtLeast(state.finalCompletionContinueExecutionCount, 'finalCompletionContinueExecutionCount', 0, context);
|
|
474
|
+
assertSafeIntegerAtLeast(state.splitPlanCountForCurrentScope, 'splitPlanCountForCurrentScope', 0, context);
|
|
475
|
+
assertSafeIntegerAtLeast(state.derivedPlanDepth, 'derivedPlanDepth', 0, context);
|
|
476
|
+
assertSafeIntegerAtLeast(state.maxDerivedPlanReviewRounds, 'maxDerivedPlanReviewRounds', 1, context);
|
|
477
|
+
assertSafeIntegerAtLeast(state.maxRounds, 'maxRounds', 1, context);
|
|
478
|
+
assertUniqueCompletedScopeNumbers(state.completedScopes, context);
|
|
479
|
+
assertValidCompletedScopeAccounting(state, context);
|
|
480
|
+
if (state.blockedFromPhase === 'done' || state.blockedFromPhase === 'blocked') {
|
|
481
|
+
throwStateInvariant(context, 'blockedFromPhase', `must not reference terminal phase ${JSON.stringify(state.blockedFromPhase)}`);
|
|
482
|
+
}
|
|
483
|
+
assertValidInteractiveBlockedRecovery(state, context);
|
|
484
|
+
assertValidManualGate(state, context);
|
|
485
|
+
assertValidPendingPlanReviewGuidance(state, context);
|
|
486
|
+
assertValidDerivedPlanState(state, context);
|
|
487
|
+
assertValidFinalCompletionState(state, context);
|
|
488
|
+
}
|
|
489
|
+
export function isOrchestrationStatus(value) {
|
|
490
|
+
return typeof value === 'string' && ORCHESTRATION_STATUS_SET.has(value);
|
|
491
|
+
}
|
|
492
|
+
export function assertOrchestrationStatus(value, fieldPath = 'status') {
|
|
493
|
+
if (!isOrchestrationStatus(value)) {
|
|
494
|
+
throw new Error(`Invalid run state: invalid ${fieldPath}: expected orchestration status, received ${formatInvalidValue(value)}`);
|
|
495
|
+
}
|
|
496
|
+
}
|