@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.
Files changed (170) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +527 -0
  3. package/SECURITY.md +91 -0
  4. package/config.yml +104 -0
  5. package/dist/neal/activity-footer.js +177 -0
  6. package/dist/neal/activity-reporting.js +1 -0
  7. package/dist/neal/adjudicator/artifacts.js +58 -0
  8. package/dist/neal/adjudicator/blocked-adjudicator.js +223 -0
  9. package/dist/neal/adjudicator/contracts.js +139 -0
  10. package/dist/neal/adjudicator/execute.js +611 -0
  11. package/dist/neal/adjudicator/final-completion.js +104 -0
  12. package/dist/neal/adjudicator/planning.js +145 -0
  13. package/dist/neal/adjudicator/specs.js +453 -0
  14. package/dist/neal/agents/prompts.js +120 -0
  15. package/dist/neal/agents/rounds.js +706 -0
  16. package/dist/neal/agents/schemas.js +832 -0
  17. package/dist/neal/agents/structured-coder.js +82 -0
  18. package/dist/neal/agents/structured-json.js +528 -0
  19. package/dist/neal/agents.js +4 -0
  20. package/dist/neal/atomic-write.js +18 -0
  21. package/dist/neal/blocked-guidance.js +406 -0
  22. package/dist/neal/cli.js +471 -0
  23. package/dist/neal/commands/check.js +401 -0
  24. package/dist/neal/commands/compat.js +807 -0
  25. package/dist/neal/commands/interactive-activity.js +57 -0
  26. package/dist/neal/commands/new-run.js +79 -0
  27. package/dist/neal/commands/plan-and-execute.js +44 -0
  28. package/dist/neal/commands/recovery-guidance.js +217 -0
  29. package/dist/neal/commands/resume-run.js +395 -0
  30. package/dist/neal/commands/review.js +21 -0
  31. package/dist/neal/commands/runtime.js +557 -0
  32. package/dist/neal/commands/setup.js +596 -0
  33. package/dist/neal/commands/squash.js +113 -0
  34. package/dist/neal/commands/status.js +33 -0
  35. package/dist/neal/commands/writer-exit-codes.js +42 -0
  36. package/dist/neal/commit-message.js +17 -0
  37. package/dist/neal/config.js +432 -0
  38. package/dist/neal/context/artifacts.js +140 -0
  39. package/dist/neal/context/context.js +324 -0
  40. package/dist/neal/context/inline-review-context.js +131 -0
  41. package/dist/neal/context/reviewer-context.js +166 -0
  42. package/dist/neal/context/shared.js +117 -0
  43. package/dist/neal/context/types.js +1 -0
  44. package/dist/neal/diagnostic.js +208 -0
  45. package/dist/neal/execute-finalization.js +5 -0
  46. package/dist/neal/final-completion-review.js +188 -0
  47. package/dist/neal/final-completion.js +229 -0
  48. package/dist/neal/git.js +339 -0
  49. package/dist/neal/index.js +135 -0
  50. package/dist/neal/interactive-controls.js +85 -0
  51. package/dist/neal/logger.js +102 -0
  52. package/dist/neal/manual-gates.js +121 -0
  53. package/dist/neal/orchestrator/artifacts.js +70 -0
  54. package/dist/neal/orchestrator/completion.js +531 -0
  55. package/dist/neal/orchestrator/failures.js +31 -0
  56. package/dist/neal/orchestrator/notifications.js +175 -0
  57. package/dist/neal/orchestrator/phases/coder.js +516 -0
  58. package/dist/neal/orchestrator/phases/planning.js +540 -0
  59. package/dist/neal/orchestrator/phases/recovery.js +798 -0
  60. package/dist/neal/orchestrator/phases/review.js +136 -0
  61. package/dist/neal/orchestrator/phases/shared.js +279 -0
  62. package/dist/neal/orchestrator/run-loop.js +113 -0
  63. package/dist/neal/orchestrator/split-plan.js +235 -0
  64. package/dist/neal/orchestrator/transitions.js +309 -0
  65. package/dist/neal/orchestrator.js +215 -0
  66. package/dist/neal/phase-display.js +27 -0
  67. package/dist/neal/plan-doc.js +154 -0
  68. package/dist/neal/plan-queue.js +1092 -0
  69. package/dist/neal/plan-refinement.js +39 -0
  70. package/dist/neal/plan-validation.js +525 -0
  71. package/dist/neal/progress.js +237 -0
  72. package/dist/neal/prompts/assert-builder.js +13 -0
  73. package/dist/neal/prompts/execute.js +290 -0
  74. package/dist/neal/prompts/guidance.js +70 -0
  75. package/dist/neal/prompts/planning.js +313 -0
  76. package/dist/neal/prompts/review-doctrine.js +142 -0
  77. package/dist/neal/prompts/shared.js +101 -0
  78. package/dist/neal/prompts/specialized.js +212 -0
  79. package/dist/neal/prompts/specs.js +572 -0
  80. package/dist/neal/providers/anthropic-claude.js +1599 -0
  81. package/dist/neal/providers/detection.js +139 -0
  82. package/dist/neal/providers/generic-agentic-tools.js +586 -0
  83. package/dist/neal/providers/generic-agentic.js +1238 -0
  84. package/dist/neal/providers/liveness.js +151 -0
  85. package/dist/neal/providers/openai-codex.js +1014 -0
  86. package/dist/neal/providers/openai-compatible.js +654 -0
  87. package/dist/neal/providers/registry.js +389 -0
  88. package/dist/neal/providers/telemetry.js +208 -0
  89. package/dist/neal/providers/types.js +21 -0
  90. package/dist/neal/recovery-artifacts.js +50 -0
  91. package/dist/neal/resume-decision.js +220 -0
  92. package/dist/neal/resume-planner.js +265 -0
  93. package/dist/neal/retrospective.js +391 -0
  94. package/dist/neal/review-debt.js +18 -0
  95. package/dist/neal/review-findings/artifacts.js +173 -0
  96. package/dist/neal/review-findings/prompts.js +172 -0
  97. package/dist/neal/review-findings/provider.js +330 -0
  98. package/dist/neal/review-findings/run.js +373 -0
  99. package/dist/neal/review-findings/types.js +1 -0
  100. package/dist/neal/review-mode.js +67 -0
  101. package/dist/neal/review.js +137 -0
  102. package/dist/neal/run-lock.js +334 -0
  103. package/dist/neal/run-metrics.js +355 -0
  104. package/dist/neal/run-narrative-types.js +1 -0
  105. package/dist/neal/run-narrative.js +1374 -0
  106. package/dist/neal/run-registry.js +218 -0
  107. package/dist/neal/run-status.js +25 -0
  108. package/dist/neal/scopes.js +451 -0
  109. package/dist/neal/sensitive-text.js +8 -0
  110. package/dist/neal/squash-message.js +379 -0
  111. package/dist/neal/squash.js +591 -0
  112. package/dist/neal/state-invariants.js +496 -0
  113. package/dist/neal/state-views.js +344 -0
  114. package/dist/neal/state.js +887 -0
  115. package/dist/neal/status-footer.js +258 -0
  116. package/dist/neal/status.js +1260 -0
  117. package/dist/neal/storage-paths.js +57 -0
  118. package/dist/neal/support.js +58 -0
  119. package/dist/neal/terminal-narrator.js +435 -0
  120. package/dist/neal/types.js +1 -0
  121. package/dist/neal/verification-events.js +81 -0
  122. package/dist/neal/version.js +37 -0
  123. package/dist/neal/worktree-status.js +137 -0
  124. package/dist/notifier.js +44 -0
  125. package/docs/ADJUDICATOR_INVENTORY.md +310 -0
  126. package/docs/PROMPT_SPECS.md +266 -0
  127. package/docs/README.md +22 -0
  128. package/docs/architecture.md +113 -0
  129. package/docs/assets/neal-execution-flow.png +0 -0
  130. package/docs/automation.md +65 -0
  131. package/docs/comparison.md +105 -0
  132. package/docs/compat.md +269 -0
  133. package/docs/compatible-models.md +135 -0
  134. package/docs/demo.md +55 -0
  135. package/docs/maintenance.md +64 -0
  136. package/docs/plan-format.md +213 -0
  137. package/docs/providers.md +751 -0
  138. package/docs/release.md +147 -0
  139. package/docs/state-machine.md +266 -0
  140. package/docs/storage.md +207 -0
  141. package/docs/troubleshooting.md +152 -0
  142. package/examples/compat/add-edit-verify/PLAN.md +29 -0
  143. package/examples/compat/add-edit-verify/broken.diff +8 -0
  144. package/examples/compat/add-edit-verify/good.diff +8 -0
  145. package/examples/compat/add-edit-verify/package.json +5 -0
  146. package/examples/compat/add-edit-verify/src/add.js +2 -0
  147. package/examples/compat/add-edit-verify/test/add.test.js +9 -0
  148. package/examples/compat/is-even-add-test/PLAN.md +30 -0
  149. package/examples/compat/is-even-add-test/broken.diff +11 -0
  150. package/examples/compat/is-even-add-test/good.diff +11 -0
  151. package/examples/compat/is-even-add-test/package.json +5 -0
  152. package/examples/compat/is-even-add-test/src/is-even.js +3 -0
  153. package/examples/compat/is-even-add-test/test/is-even.test.js +9 -0
  154. package/examples/compat/manifest.json +60 -0
  155. package/examples/compat/plan-greeting/ISSUE.md +25 -0
  156. package/examples/compat/plan-greeting/package.json +5 -0
  157. package/examples/compat/plan-greeting/src/greet.js +2 -0
  158. package/examples/compat/plan-greeting/test/greet.test.js +8 -0
  159. package/examples/compat/reverse-grep-edit/PLAN.md +32 -0
  160. package/examples/compat/reverse-grep-edit/broken.diff +12 -0
  161. package/examples/compat/reverse-grep-edit/good.diff +12 -0
  162. package/examples/compat/reverse-grep-edit/package.json +5 -0
  163. package/examples/compat/reverse-grep-edit/src/strings.js +10 -0
  164. package/examples/compat/reverse-grep-edit/test/strings.test.js +15 -0
  165. package/examples/issue-triage-js/PLAN.md +83 -0
  166. package/examples/issue-triage-js/README.md +76 -0
  167. package/examples/issue-triage-js/package.json +9 -0
  168. package/examples/issue-triage-js/src/issue-triage.js +87 -0
  169. package/examples/issue-triage-js/test/issue-triage.test.js +107 -0
  170. package/package.json +70 -0
@@ -0,0 +1,887 @@
1
+ import { readFile, realpath } from 'node:fs/promises';
2
+ import { basename, dirname, resolve } from 'node:path';
3
+ import { writeJsonAtomic } from './atomic-write.js';
4
+ import { validatePlanDocument } from './plan-validation.js';
5
+ import { getRunStatePath as getStorageRunStatePath } from './storage-paths.js';
6
+ import { getDefaultCoderEffort, getDefaultCoderModel, getDefaultCoderProvider, getDefaultPlannerEffort, getDefaultPlannerModel, getDefaultPlannerProvider, getDefaultReviewerEffort, getDefaultReviewerModel, getDefaultReviewerProvider, } from './config.js';
7
+ import { EXECUTE_FINALIZATION_PHASE } from './execute-finalization.js';
8
+ import { formatRegisteredProviderIds, isRegisteredProviderId } from './providers/registry.js';
9
+ import { updateRunPointersAfterStateSave } from './run-registry.js';
10
+ import { refreshActiveRunLock } from './run-lock.js';
11
+ import { validateReviewerSquashMessageDraft } from './squash-message.js';
12
+ import { assertOrchestrationPhase, assertOrchestrationStatus, assertValidOrchestrationState, } from './state-invariants.js';
13
+ const TOP_LEVEL_MODES = new Set(['plan', 'execute']);
14
+ const INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES = new Set([
15
+ 'coder_plan',
16
+ 'reviewer_plan',
17
+ 'coder_plan_response',
18
+ 'coder_plan_optional_response',
19
+ 'awaiting_derived_plan_execution',
20
+ 'coder_scope',
21
+ 'reviewer_scope',
22
+ 'coder_response',
23
+ 'coder_optional_response',
24
+ EXECUTE_FINALIZATION_PHASE,
25
+ 'final_completion_review',
26
+ ]);
27
+ export function getDefaultAgentConfig(cwd = process.cwd()) {
28
+ return {
29
+ planner: {
30
+ provider: getDefaultPlannerProvider(cwd),
31
+ model: getDefaultPlannerModel(cwd),
32
+ effort: getDefaultPlannerEffort(cwd),
33
+ },
34
+ coder: {
35
+ provider: getDefaultCoderProvider(cwd),
36
+ model: getDefaultCoderModel(cwd),
37
+ effort: getDefaultCoderEffort(cwd),
38
+ },
39
+ reviewer: {
40
+ provider: getDefaultReviewerProvider(cwd),
41
+ model: getDefaultReviewerModel(cwd),
42
+ effort: getDefaultReviewerEffort(cwd),
43
+ },
44
+ };
45
+ }
46
+ export function getRunStatePath(runDir) {
47
+ return getStorageRunStatePath(runDir);
48
+ }
49
+ // Read the execution shape declared in the seed plan document, regardless of top-level
50
+ // mode. Returns null when the plan document is absent (ENOENT). This is the authored
51
+ // shape captured write-once at run creation for both `plan` and `execute` modes.
52
+ async function readSeedExecutionShape(init) {
53
+ try {
54
+ return validatePlanDocument(await readFile(init.planDoc, 'utf8')).executionShape;
55
+ }
56
+ catch (error) {
57
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT') {
58
+ return null;
59
+ }
60
+ throw error;
61
+ }
62
+ }
63
+ async function readInitialExecutionShape(init) {
64
+ // Execute mode adopts the seed shape immediately; plan mode starts with no adopted
65
+ // shape (it is established/refined through plan review). Unchanged behavior.
66
+ if (init.topLevelMode !== 'execute') {
67
+ return null;
68
+ }
69
+ return readSeedExecutionShape(init);
70
+ }
71
+ export async function createInitialState(init, baseCommit) {
72
+ const now = new Date().toISOString();
73
+ const executionShape = await readInitialExecutionShape(init);
74
+ // Capture the author-declared shape from the seed plan for all top-level modes, so plan
75
+ // refinement can honor an author-declared `one_shot`. Write-once: never reassigned.
76
+ const authoredExecutionShape = await readSeedExecutionShape(init);
77
+ return {
78
+ version: 1,
79
+ planDoc: init.planDoc,
80
+ planDocBackupPath: init.planDocBackupPath ?? null,
81
+ cwd: init.cwd,
82
+ runDir: init.runDir,
83
+ topLevelMode: init.topLevelMode,
84
+ allowedDirtyPaths: [...init.allowedDirtyPaths],
85
+ agentConfig: init.agentConfig,
86
+ unattended: init.unattended ?? false,
87
+ unattendedAutoResumeCount: 0,
88
+ reviewStuckArbiterCount: 0,
89
+ autoSquashOnCompletion: init.autoSquashOnCompletion ?? true,
90
+ progressJsonPath: init.progressJsonPath,
91
+ progressMarkdownPath: init.progressMarkdownPath,
92
+ recoveryMarkdownPath: init.recoveryMarkdownPath,
93
+ phase: init.topLevelMode === 'plan' ? 'coder_plan' : 'coder_scope',
94
+ createdAt: now,
95
+ updatedAt: now,
96
+ reviewMarkdownPath: init.reviewMarkdownPath,
97
+ archivedReviewPath: null,
98
+ initialBaseCommit: baseCommit,
99
+ baseCommit,
100
+ finalCommit: null,
101
+ plannerSessionHandle: null,
102
+ plannerSessionProtocol: null,
103
+ coderSessionHandle: null,
104
+ coderSessionProtocol: null,
105
+ reviewerSessionHandle: null,
106
+ executionShape,
107
+ authoredExecutionShape,
108
+ currentScopeNumber: 1,
109
+ coderRetryCount: 0,
110
+ lastScopeMarker: null,
111
+ currentScopeProgressJustification: null,
112
+ currentScopeMeaningfulProgressVerdict: null,
113
+ manualGate: null,
114
+ finalCompletionSummary: null,
115
+ finalCompletionReviewVerdict: null,
116
+ finalCompletionResolvedAction: null,
117
+ finalCompletionContinueExecutionCount: 0,
118
+ finalCompletionContinueExecutionCapReached: false,
119
+ derivedPlanPath: null,
120
+ derivedFromScopeNumber: null,
121
+ derivedPlanStatus: null,
122
+ derivedScopeIndex: null,
123
+ splitPlanStartedNotified: false,
124
+ derivedPlanAcceptedNotified: false,
125
+ splitPlanBlockedNotified: false,
126
+ splitPlanCountForCurrentScope: 0,
127
+ derivedPlanDepth: 0,
128
+ maxDerivedPlanReviewRounds: 5,
129
+ rounds: [],
130
+ recentBlocks: [],
131
+ findings: [],
132
+ createdCommits: [],
133
+ completedScopes: [],
134
+ maxRounds: init.maxRounds,
135
+ blockedFromPhase: null,
136
+ interactiveBlockedRecovery: null,
137
+ interactiveBlockedRecoveryHistory: [],
138
+ pendingPlanReviewGuidance: null,
139
+ status: 'running',
140
+ };
141
+ }
142
+ export async function saveState(path, state) {
143
+ const nextState = {
144
+ ...state,
145
+ updatedAt: new Date().toISOString(),
146
+ };
147
+ const runStatePath = getRunStatePath(nextState.runDir);
148
+ if (!(await samePath(path, runStatePath))) {
149
+ throw new Error(`saveState requires the run-local state path: ${runStatePath}; received ${path}`);
150
+ }
151
+ assertValidOrchestrationState(nextState, { source: 'save', path: runStatePath });
152
+ await writeJsonAtomic(runStatePath, nextState);
153
+ await updateRunPointersAfterStateSave(runStatePath, nextState);
154
+ await refreshActiveRunLock({
155
+ cwd: nextState.cwd,
156
+ runId: basename(nextState.runDir),
157
+ runStatePath,
158
+ planDoc: nextState.planDoc,
159
+ topLevelMode: nextState.topLevelMode,
160
+ });
161
+ return nextState;
162
+ }
163
+ async function samePath(left, right) {
164
+ if (resolve(left) === resolve(right)) {
165
+ return true;
166
+ }
167
+ try {
168
+ if (basename(left) !== basename(right)) {
169
+ return false;
170
+ }
171
+ const leftDir = await realpath(dirname(left));
172
+ const rightDir = await realpath(dirname(right));
173
+ return leftDir === rightDir;
174
+ }
175
+ catch {
176
+ return false;
177
+ }
178
+ }
179
+ const EXECUTION_SHAPES = ['one_shot', 'multi_scope', 'multi_scope_unknown'];
180
+ const SCOPE_MARKERS = [
181
+ 'AUTONOMY_SCOPE_DONE',
182
+ 'AUTONOMY_CHUNK_DONE',
183
+ 'AUTONOMY_DONE',
184
+ 'AUTONOMY_BLOCKED',
185
+ 'AUTONOMY_SPLIT_PLAN',
186
+ ];
187
+ const FINAL_COMPLETION_ACTIONS = ['accept_complete', 'continue_execution', 'block_for_operator'];
188
+ const REVIEWER_MEANINGFUL_PROGRESS_ACTIONS = ['accept', 'block_for_operator', 'replace_plan', 'advance_parent'];
189
+ const REVIEW_FINDING_SOURCES = ['reviewer', 'plan_structure'];
190
+ const REVIEW_FINDING_SEVERITIES = ['blocking', 'non_blocking'];
191
+ const REVIEW_FINDING_STATUSES = ['open', 'fixed', 'rejected', 'deferred'];
192
+ const MANUAL_GATE_CHECK_TYPES = ['command'];
193
+ const MANUAL_GATE_CHECK_CWDS = ['repo', 'run_dir'];
194
+ const MANUAL_GATE_RESUME_PHASES = ['coder_scope'];
195
+ const INTERACTIVE_BLOCKED_RECOVERY_ACTIONS = [
196
+ 'resume_current_scope',
197
+ 'replace_current_scope',
198
+ 'stay_blocked',
199
+ 'terminal_block',
200
+ ];
201
+ const RESIDUAL_REVIEW_DEBT_STATUSES = ['open', 'deferred'];
202
+ const CODER_SESSION_PROTOCOLS = ['legacy_marker_v1', 'structured_json_v1'];
203
+ const PLANNER_SESSION_PHASES = new Set([
204
+ 'coder_plan',
205
+ 'reviewer_plan',
206
+ 'coder_plan_response',
207
+ 'coder_plan_optional_response',
208
+ ]);
209
+ function isStringArray(value) {
210
+ return Array.isArray(value) && value.every((item) => typeof item === 'string');
211
+ }
212
+ function isTopLevelMode(value) {
213
+ return typeof value === 'string' && TOP_LEVEL_MODES.has(value);
214
+ }
215
+ function formatStateValue(value) {
216
+ return typeof value === 'string' ? JSON.stringify(value) : String(value);
217
+ }
218
+ function throwInvalidState(fieldPath, reason) {
219
+ throw new Error(`Invalid run state: invalid ${fieldPath}: ${reason}`);
220
+ }
221
+ function isStateRecord(value) {
222
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
223
+ }
224
+ function requireRecord(value, fieldPath) {
225
+ if (!isStateRecord(value)) {
226
+ throwInvalidState(fieldPath, `expected object, received ${formatStateValue(value)}`);
227
+ }
228
+ return value;
229
+ }
230
+ function hasOwn(record, key) {
231
+ return Object.prototype.hasOwnProperty.call(record, key);
232
+ }
233
+ function readRequired(record, key, fieldPath = key) {
234
+ if (!hasOwn(record, key)) {
235
+ throwInvalidState(fieldPath, 'missing required field');
236
+ }
237
+ return record[key];
238
+ }
239
+ function readString(record, key, fieldPath = key) {
240
+ const value = readRequired(record, key, fieldPath);
241
+ if (typeof value !== 'string') {
242
+ throwInvalidState(fieldPath, `expected string, received ${formatStateValue(value)}`);
243
+ }
244
+ return value;
245
+ }
246
+ function readNullableString(record, key, fieldPath = key) {
247
+ const value = readRequired(record, key, fieldPath);
248
+ if (value === null || typeof value === 'string') {
249
+ return value;
250
+ }
251
+ throwInvalidState(fieldPath, `expected string or null, received ${formatStateValue(value)}`);
252
+ }
253
+ function readOptionalBoolean(record, key, fieldPath = key) {
254
+ if (!hasOwn(record, key)) {
255
+ return undefined;
256
+ }
257
+ const value = record[key];
258
+ if (typeof value === 'boolean') {
259
+ return value;
260
+ }
261
+ throwInvalidState(fieldPath, `expected boolean, received ${formatStateValue(value)}`);
262
+ }
263
+ function readOptionalNullableString(record, key, fieldPath = key) {
264
+ if (!hasOwn(record, key)) {
265
+ return undefined;
266
+ }
267
+ const value = record[key];
268
+ if (value === null || typeof value === 'string') {
269
+ return value;
270
+ }
271
+ throwInvalidState(fieldPath, `expected string or null, received ${formatStateValue(value)}`);
272
+ }
273
+ function hydrateCoderSessionProtocol(state, coderSessionHandle) {
274
+ const value = readOptionalNullableString(state, 'coderSessionProtocol');
275
+ if (value === undefined) {
276
+ return coderSessionHandle ? 'legacy_marker_v1' : null;
277
+ }
278
+ if (value === null) {
279
+ return null;
280
+ }
281
+ if (CODER_SESSION_PROTOCOLS.includes(value)) {
282
+ return value;
283
+ }
284
+ throwInvalidState('coderSessionProtocol', `expected one of "legacy_marker_v1", "structured_json_v1", or null, received ${formatStateValue(value)}`);
285
+ }
286
+ function hydrateOptionalCoderSessionProtocol(state, key, fieldPath = key) {
287
+ const value = readOptionalNullableString(state, key, fieldPath);
288
+ if (value === undefined || value === null) {
289
+ return null;
290
+ }
291
+ if (CODER_SESSION_PROTOCOLS.includes(value)) {
292
+ return value;
293
+ }
294
+ throwInvalidState(fieldPath, `expected one of "legacy_marker_v1", "structured_json_v1", or null, received ${formatStateValue(value)}`);
295
+ }
296
+ function readBoolean(record, key, fieldPath = key) {
297
+ const value = readRequired(record, key, fieldPath);
298
+ if (typeof value !== 'boolean') {
299
+ throwInvalidState(fieldPath, `expected boolean, received ${formatStateValue(value)}`);
300
+ }
301
+ return value;
302
+ }
303
+ function readSafeInteger(record, key, fieldPath = key) {
304
+ const value = readRequired(record, key, fieldPath);
305
+ if (typeof value !== 'number' || !Number.isSafeInteger(value)) {
306
+ throwInvalidState(fieldPath, `expected safe integer, received ${formatStateValue(value)}`);
307
+ }
308
+ return value;
309
+ }
310
+ function readNullableSafeInteger(record, key, fieldPath = key) {
311
+ const value = readRequired(record, key, fieldPath);
312
+ if (value === null) {
313
+ return null;
314
+ }
315
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
316
+ return value;
317
+ }
318
+ throwInvalidState(fieldPath, `expected safe integer or null, received ${formatStateValue(value)}`);
319
+ }
320
+ function readOptionalNullableSafeInteger(record, key, fieldPath = key) {
321
+ if (!hasOwn(record, key)) {
322
+ return null;
323
+ }
324
+ const value = record[key];
325
+ if (value === null) {
326
+ return null;
327
+ }
328
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
329
+ return value;
330
+ }
331
+ throwInvalidState(fieldPath, `expected safe integer or null, received ${formatStateValue(value)}`);
332
+ }
333
+ function readStringArray(record, key, fieldPath = key) {
334
+ const value = readRequired(record, key, fieldPath);
335
+ if (!isStringArray(value)) {
336
+ throwInvalidState(fieldPath, `expected string array, received ${formatStateValue(value)}`);
337
+ }
338
+ return value;
339
+ }
340
+ function readOptionalStringArray(record, key, fieldPath = key) {
341
+ if (!hasOwn(record, key)) {
342
+ return undefined;
343
+ }
344
+ const value = record[key];
345
+ if (!isStringArray(value)) {
346
+ throwInvalidState(fieldPath, `expected string array, received ${formatStateValue(value)}`);
347
+ }
348
+ return value;
349
+ }
350
+ function readArray(record, key, fieldPath = key) {
351
+ const value = readRequired(record, key, fieldPath);
352
+ if (!Array.isArray(value)) {
353
+ throwInvalidState(fieldPath, `expected array, received ${formatStateValue(value)}`);
354
+ }
355
+ return value;
356
+ }
357
+ function readRecord(record, key, fieldPath = key) {
358
+ return requireRecord(readRequired(record, key, fieldPath), fieldPath);
359
+ }
360
+ function readOptionalNullableRecord(record, key, fieldPath = key) {
361
+ if (!hasOwn(record, key)) {
362
+ return null;
363
+ }
364
+ const value = record[key];
365
+ if (value === null) {
366
+ return null;
367
+ }
368
+ return requireRecord(value, fieldPath);
369
+ }
370
+ function readEnum(record, key, values, fieldPath = key) {
371
+ const value = readRequired(record, key, fieldPath);
372
+ if (typeof value === 'string' && values.includes(value)) {
373
+ return value;
374
+ }
375
+ throwInvalidState(fieldPath, `expected one of ${values.map((item) => JSON.stringify(item)).join(', ')}, received ${formatStateValue(value)}`);
376
+ }
377
+ function readNullableEnum(record, key, values, fieldPath = key) {
378
+ const value = readRequired(record, key, fieldPath);
379
+ if (value === null) {
380
+ return null;
381
+ }
382
+ if (typeof value === 'string' && values.includes(value)) {
383
+ return value;
384
+ }
385
+ throwInvalidState(fieldPath, `expected one of ${values.map((item) => JSON.stringify(item)).join(', ')} or null, received ${formatStateValue(value)}`);
386
+ }
387
+ function readOptionalEnum(record, key, values, fieldPath = key) {
388
+ if (!hasOwn(record, key)) {
389
+ return undefined;
390
+ }
391
+ const value = record[key];
392
+ if (typeof value === 'string' && values.includes(value)) {
393
+ return value;
394
+ }
395
+ throwInvalidState(fieldPath, `expected one of ${values.map((item) => JSON.stringify(item)).join(', ')}, received ${formatStateValue(value)}`);
396
+ }
397
+ function readOptionalNullableEnum(record, key, values, fieldPath = key) {
398
+ if (!hasOwn(record, key)) {
399
+ return null;
400
+ }
401
+ const value = record[key];
402
+ if (value === null) {
403
+ return null;
404
+ }
405
+ if (typeof value === 'string' && values.includes(value)) {
406
+ return value;
407
+ }
408
+ throwInvalidState(fieldPath, `expected one of ${values.map((item) => JSON.stringify(item)).join(', ')} or null, received ${formatStateValue(value)}`);
409
+ }
410
+ function readOptionalSafeInteger(record, key, fieldPath = key) {
411
+ if (!hasOwn(record, key)) {
412
+ return undefined;
413
+ }
414
+ const value = record[key];
415
+ if (typeof value === 'number' && Number.isSafeInteger(value)) {
416
+ return value;
417
+ }
418
+ throwInvalidState(fieldPath, `expected safe integer, received ${formatStateValue(value)}`);
419
+ }
420
+ function hydrateOrchestrationPhase(value, fieldPath) {
421
+ assertOrchestrationPhase(value, fieldPath);
422
+ return value;
423
+ }
424
+ function readOrchestrationPhase(record, key, fieldPath = key) {
425
+ return hydrateOrchestrationPhase(readRequired(record, key, fieldPath), fieldPath);
426
+ }
427
+ function readNullableOrchestrationPhase(record, key, fieldPath = key) {
428
+ const value = readRequired(record, key, fieldPath);
429
+ if (value === null) {
430
+ return null;
431
+ }
432
+ return hydrateOrchestrationPhase(value, fieldPath);
433
+ }
434
+ function hydrateAgentConfig(value, fieldPath = 'agentConfig') {
435
+ const config = requireRecord(value, fieldPath);
436
+ function hydrateRoleConfig(key, fallback) {
437
+ const rolePath = `${fieldPath}.${key}`;
438
+ const role = fallback && !(key in config)
439
+ ? fallback
440
+ : readRecord(config, key, rolePath);
441
+ const provider = readString(role, 'provider', `${rolePath}.provider`);
442
+ if (!isRegisteredProviderId(provider)) {
443
+ throwInvalidState(`${rolePath}.provider`, `unsupported provider ${JSON.stringify(provider)}; registered providers: ${formatRegisteredProviderIds()}`);
444
+ }
445
+ return {
446
+ provider,
447
+ model: readNullableString(role, 'model', `${rolePath}.model`),
448
+ effort: readOptionalNullableString(role, 'effort', `${rolePath}.effort`) ?? null,
449
+ };
450
+ }
451
+ const coder = hydrateRoleConfig('coder');
452
+ return {
453
+ planner: hydrateRoleConfig('planner', coder),
454
+ coder,
455
+ reviewer: hydrateRoleConfig('reviewer'),
456
+ };
457
+ }
458
+ function hydrateExecuteScopeProgressJustification(value, fieldPath = 'currentScopeProgressJustification') {
459
+ if (value === null) {
460
+ return null;
461
+ }
462
+ const justification = requireRecord(value, fieldPath);
463
+ return {
464
+ milestoneTargeted: readString(justification, 'milestoneTargeted', `${fieldPath}.milestoneTargeted`),
465
+ newEvidence: readString(justification, 'newEvidence', `${fieldPath}.newEvidence`),
466
+ whyNotRedundant: readString(justification, 'whyNotRedundant', `${fieldPath}.whyNotRedundant`),
467
+ nextStepUnlocked: readString(justification, 'nextStepUnlocked', `${fieldPath}.nextStepUnlocked`),
468
+ };
469
+ }
470
+ function hydrateReviewerMeaningfulProgressVerdict(value, fieldPath = 'currentScopeMeaningfulProgressVerdict') {
471
+ if (value === null) {
472
+ return null;
473
+ }
474
+ const verdict = requireRecord(value, fieldPath);
475
+ return {
476
+ action: readEnum(verdict, 'action', REVIEWER_MEANINGFUL_PROGRESS_ACTIONS, `${fieldPath}.action`),
477
+ rationale: readString(verdict, 'rationale', `${fieldPath}.rationale`),
478
+ };
479
+ }
480
+ function hydrateManualGateResumeCheck(value, fieldPath) {
481
+ const check = requireRecord(value, fieldPath);
482
+ return {
483
+ type: readEnum(check, 'type', MANUAL_GATE_CHECK_TYPES, `${fieldPath}.type`),
484
+ name: readString(check, 'name', `${fieldPath}.name`),
485
+ command: readStringArray(check, 'command', `${fieldPath}.command`),
486
+ cwd: readOptionalEnum(check, 'cwd', MANUAL_GATE_CHECK_CWDS, `${fieldPath}.cwd`),
487
+ timeoutMs: readOptionalSafeInteger(check, 'timeoutMs', `${fieldPath}.timeoutMs`),
488
+ };
489
+ }
490
+ function hydrateManualGateLastFailure(value, fieldPath) {
491
+ if (value === null) {
492
+ return null;
493
+ }
494
+ const failure = requireRecord(value, fieldPath);
495
+ return {
496
+ checkName: readString(failure, 'checkName', `${fieldPath}.checkName`),
497
+ exitCode: readNullableSafeInteger(failure, 'exitCode', `${fieldPath}.exitCode`),
498
+ signal: readNullableString(failure, 'signal', `${fieldPath}.signal`),
499
+ stdoutTail: readString(failure, 'stdoutTail', `${fieldPath}.stdoutTail`),
500
+ stderrTail: readString(failure, 'stderrTail', `${fieldPath}.stderrTail`),
501
+ };
502
+ }
503
+ function hydrateManualGate(record, fieldPath = 'manualGate') {
504
+ if (!hasOwn(record, fieldPath)) {
505
+ return null;
506
+ }
507
+ const value = record[fieldPath];
508
+ if (value === null) {
509
+ return null;
510
+ }
511
+ const gate = requireRecord(value, fieldPath);
512
+ return {
513
+ id: readString(gate, 'id', `${fieldPath}.id`),
514
+ title: readString(gate, 'title', `${fieldPath}.title`),
515
+ reason: readString(gate, 'reason', `${fieldPath}.reason`),
516
+ instructionsPath: readString(gate, 'instructionsPath', `${fieldPath}.instructionsPath`),
517
+ resumeChecks: readArray(gate, 'resumeChecks', `${fieldPath}.resumeChecks`).map((check, index) => hydrateManualGateResumeCheck(check, `${fieldPath}.resumeChecks[${index}]`)),
518
+ resumePhase: readEnum(gate, 'resumePhase', MANUAL_GATE_RESUME_PHASES, `${fieldPath}.resumePhase`),
519
+ createdAt: readString(gate, 'createdAt', `${fieldPath}.createdAt`),
520
+ updatedAt: readString(gate, 'updatedAt', `${fieldPath}.updatedAt`),
521
+ lastCheckedAt: readNullableString(gate, 'lastCheckedAt', `${fieldPath}.lastCheckedAt`),
522
+ lastFailure: hydrateManualGateLastFailure(readRequired(gate, 'lastFailure', `${fieldPath}.lastFailure`), `${fieldPath}.lastFailure`),
523
+ };
524
+ }
525
+ function hydrateFinalCompletionSummary(value, fieldPath = 'finalCompletionSummary') {
526
+ if (value === null) {
527
+ return null;
528
+ }
529
+ const summary = requireRecord(value, fieldPath);
530
+ return {
531
+ planGoalSatisfied: readBoolean(summary, 'planGoalSatisfied', `${fieldPath}.planGoalSatisfied`),
532
+ whatChangedOverall: readString(summary, 'whatChangedOverall', `${fieldPath}.whatChangedOverall`),
533
+ verificationSummary: readString(summary, 'verificationSummary', `${fieldPath}.verificationSummary`),
534
+ remainingKnownGaps: readStringArray(summary, 'remainingKnownGaps', `${fieldPath}.remainingKnownGaps`),
535
+ };
536
+ }
537
+ function hydrateFinalCompletionReviewerVerdict(value, fieldPath = 'finalCompletionReviewVerdict') {
538
+ if (value === null) {
539
+ return null;
540
+ }
541
+ const verdict = requireRecord(value, fieldPath);
542
+ const action = readEnum(verdict, 'action', FINAL_COMPLETION_ACTIONS, `${fieldPath}.action`);
543
+ const squashCommitMessageValue = hasOwn(verdict, 'squashCommitMessage')
544
+ ? readRequired(verdict, 'squashCommitMessage', `${fieldPath}.squashCommitMessage`)
545
+ : null;
546
+ return {
547
+ action,
548
+ summary: readString(verdict, 'summary', `${fieldPath}.summary`),
549
+ rationale: readString(verdict, 'rationale', `${fieldPath}.rationale`),
550
+ missingWork: hydrateFinalCompletionMissingWork(readRequired(verdict, 'missingWork', `${fieldPath}.missingWork`), `${fieldPath}.missingWork`),
551
+ squashCommitMessage: action === 'accept_complete'
552
+ ? hydrateSquashCommitMessageDraft(squashCommitMessageValue, `${fieldPath}.squashCommitMessage`)
553
+ : null,
554
+ };
555
+ }
556
+ function hydrateSquashCommitMessageDraft(value, fieldPath) {
557
+ if (value === null) {
558
+ return null;
559
+ }
560
+ try {
561
+ return validateReviewerSquashMessageDraft(value, { label: fieldPath });
562
+ }
563
+ catch (error) {
564
+ const message = error instanceof Error ? error.message : String(error);
565
+ throwInvalidState(fieldPath, message);
566
+ }
567
+ }
568
+ function hydrateFinalCompletionMissingWork(value, fieldPath) {
569
+ if (value === null) {
570
+ return null;
571
+ }
572
+ const missingWork = requireRecord(value, fieldPath);
573
+ return {
574
+ summary: readString(missingWork, 'summary', `${fieldPath}.summary`),
575
+ requiredOutcome: readString(missingWork, 'requiredOutcome', `${fieldPath}.requiredOutcome`),
576
+ verification: readString(missingWork, 'verification', `${fieldPath}.verification`),
577
+ };
578
+ }
579
+ function hydrateRecentBlock(value, fieldPath = 'recentBlocks[]') {
580
+ const record = requireRecord(value, fieldPath);
581
+ const sourcePhase = readOrchestrationPhase(record, 'sourcePhase', `${fieldPath}.sourcePhase`);
582
+ if (!INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES.has(sourcePhase)) {
583
+ throwInvalidState(`${fieldPath}.sourcePhase`, `unsupported recent-block source phase ${JSON.stringify(sourcePhase)}`);
584
+ }
585
+ return {
586
+ scopeNumber: readSafeInteger(record, 'scopeNumber', `${fieldPath}.scopeNumber`),
587
+ derivedScopeIndex: readNullableSafeInteger(record, 'derivedScopeIndex', `${fieldPath}.derivedScopeIndex`),
588
+ sourcePhase: sourcePhase,
589
+ normalizedKey: readString(record, 'normalizedKey', `${fieldPath}.normalizedKey`),
590
+ evidenceFingerprint: readString(record, 'evidenceFingerprint', `${fieldPath}.evidenceFingerprint`),
591
+ count: readSafeInteger(record, 'count', `${fieldPath}.count`),
592
+ recordedAt: readString(record, 'recordedAt', `${fieldPath}.recordedAt`),
593
+ };
594
+ }
595
+ function hydrateFinding(value, fieldPath = 'findings[]') {
596
+ const finding = requireRecord(value, fieldPath);
597
+ const evidence = readOptionalNullableString(finding, 'evidence', `${fieldPath}.evidence`);
598
+ return {
599
+ id: readString(finding, 'id', `${fieldPath}.id`),
600
+ canonicalId: readString(finding, 'canonicalId', `${fieldPath}.canonicalId`),
601
+ round: readSafeInteger(finding, 'round', `${fieldPath}.round`),
602
+ source: readEnum(finding, 'source', REVIEW_FINDING_SOURCES, `${fieldPath}.source`),
603
+ severity: readEnum(finding, 'severity', REVIEW_FINDING_SEVERITIES, `${fieldPath}.severity`),
604
+ files: readStringArray(finding, 'files', `${fieldPath}.files`),
605
+ claim: readString(finding, 'claim', `${fieldPath}.claim`),
606
+ ...(evidence !== undefined ? { evidence } : {}),
607
+ requiredAction: readString(finding, 'requiredAction', `${fieldPath}.requiredAction`),
608
+ status: readEnum(finding, 'status', REVIEW_FINDING_STATUSES, `${fieldPath}.status`),
609
+ roundSummary: readString(finding, 'roundSummary', `${fieldPath}.roundSummary`),
610
+ coderDisposition: readNullableString(finding, 'coderDisposition', `${fieldPath}.coderDisposition`),
611
+ coderCommit: readNullableString(finding, 'coderCommit', `${fieldPath}.coderCommit`),
612
+ };
613
+ }
614
+ function hydrateInteractiveBlockedRecoveryTurnDisposition(value, fieldPath) {
615
+ if (value === null) {
616
+ return null;
617
+ }
618
+ const disposition = requireRecord(value, fieldPath);
619
+ return {
620
+ recordedAt: readString(disposition, 'recordedAt', `${fieldPath}.recordedAt`),
621
+ sessionHandle: readNullableString(disposition, 'sessionHandle', `${fieldPath}.sessionHandle`),
622
+ action: readEnum(disposition, 'action', INTERACTIVE_BLOCKED_RECOVERY_ACTIONS, `${fieldPath}.action`),
623
+ summary: readString(disposition, 'summary', `${fieldPath}.summary`),
624
+ rationale: readString(disposition, 'rationale', `${fieldPath}.rationale`),
625
+ blocker: readString(disposition, 'blocker', `${fieldPath}.blocker`),
626
+ replacementPlan: readString(disposition, 'replacementPlan', `${fieldPath}.replacementPlan`),
627
+ resultingPhase: readOrchestrationPhase(disposition, 'resultingPhase', `${fieldPath}.resultingPhase`),
628
+ };
629
+ }
630
+ function hydrateInteractiveBlockedRecoveryTurn(value, fieldPath) {
631
+ const turn = requireRecord(value, fieldPath);
632
+ return {
633
+ number: readSafeInteger(turn, 'number', `${fieldPath}.number`),
634
+ recordedAt: readString(turn, 'recordedAt', `${fieldPath}.recordedAt`),
635
+ operatorGuidance: readString(turn, 'operatorGuidance', `${fieldPath}.operatorGuidance`),
636
+ disposition: hydrateInteractiveBlockedRecoveryTurnDisposition(readRequired(turn, 'disposition', `${fieldPath}.disposition`), `${fieldPath}.disposition`),
637
+ };
638
+ }
639
+ function hydrateInteractiveBlockedRecoveryDirective(value, fieldPath) {
640
+ if (value === null) {
641
+ return null;
642
+ }
643
+ const directive = requireRecord(value, fieldPath);
644
+ return {
645
+ recordedAt: readString(directive, 'recordedAt', `${fieldPath}.recordedAt`),
646
+ operatorGuidance: readString(directive, 'operatorGuidance', `${fieldPath}.operatorGuidance`),
647
+ terminalOnly: readBoolean(directive, 'terminalOnly', `${fieldPath}.terminalOnly`),
648
+ };
649
+ }
650
+ const BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES = [
651
+ 'misunderstanding',
652
+ 'authorization',
653
+ 'external_precondition',
654
+ 'impossible_task',
655
+ ];
656
+ function hydrateInteractiveBlockedRecoveryAdjudicatorAdvice(record, fieldPath) {
657
+ const advice = readOptionalNullableRecord(record, 'adjudicatorAdvice', `${fieldPath}.adjudicatorAdvice`);
658
+ if (!advice) {
659
+ return undefined;
660
+ }
661
+ return {
662
+ recordedAt: readString(advice, 'recordedAt', `${fieldPath}.adjudicatorAdvice.recordedAt`),
663
+ recoverable: readBoolean(advice, 'recoverable', `${fieldPath}.adjudicatorAdvice.recoverable`),
664
+ triageCategory: readEnum(advice, 'triageCategory', BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES, `${fieldPath}.adjudicatorAdvice.triageCategory`),
665
+ resolutionDirective: readString(advice, 'resolutionDirective', `${fieldPath}.adjudicatorAdvice.resolutionDirective`),
666
+ rationale: readString(advice, 'rationale', `${fieldPath}.adjudicatorAdvice.rationale`),
667
+ };
668
+ }
669
+ function hydrateInteractiveBlockedRecoveryFields(record, fieldPath) {
670
+ const sourcePhase = readOrchestrationPhase(record, 'sourcePhase', `${fieldPath}.sourcePhase`);
671
+ if (!INTERACTIVE_BLOCKED_RECOVERY_SOURCE_PHASES.has(sourcePhase)) {
672
+ throwInvalidState(`${fieldPath}.sourcePhase`, `unsupported interactive blocked recovery source phase ${JSON.stringify(sourcePhase)}`);
673
+ }
674
+ const advice = hydrateInteractiveBlockedRecoveryAdjudicatorAdvice(record, fieldPath);
675
+ return {
676
+ enteredAt: readString(record, 'enteredAt', `${fieldPath}.enteredAt`),
677
+ sourcePhase: sourcePhase,
678
+ blockedReason: readString(record, 'blockedReason', `${fieldPath}.blockedReason`),
679
+ maxTurns: readSafeInteger(record, 'maxTurns', `${fieldPath}.maxTurns`),
680
+ lastHandledTurn: readSafeInteger(record, 'lastHandledTurn', `${fieldPath}.lastHandledTurn`),
681
+ pendingDirective: hydrateInteractiveBlockedRecoveryDirective(readRequired(record, 'pendingDirective', `${fieldPath}.pendingDirective`), `${fieldPath}.pendingDirective`),
682
+ turns: readArray(record, 'turns', `${fieldPath}.turns`).map((turn, index) => hydrateInteractiveBlockedRecoveryTurn(turn, `${fieldPath}.turns[${index}]`)),
683
+ // Only attach the key when persisted advice exists, so a recovery record with
684
+ // no adjudicator advice round-trips byte-for-byte (no `adjudicatorAdvice:
685
+ // undefined` residue).
686
+ ...(advice ? { adjudicatorAdvice: advice } : {}),
687
+ };
688
+ }
689
+ function hydrateInteractiveBlockedRecovery(value, fieldPath = 'interactiveBlockedRecovery') {
690
+ if (value === null) {
691
+ return null;
692
+ }
693
+ return hydrateInteractiveBlockedRecoveryFields(requireRecord(value, fieldPath), fieldPath);
694
+ }
695
+ function hydrateInteractiveBlockedRecoveryRecord(value, fieldPath = 'interactiveBlockedRecoveryHistory[]') {
696
+ const record = requireRecord(value, fieldPath);
697
+ return {
698
+ ...hydrateInteractiveBlockedRecoveryFields(record, fieldPath),
699
+ resolvedAt: readString(record, 'resolvedAt', `${fieldPath}.resolvedAt`),
700
+ resolvedByAction: readEnum(record, 'resolvedByAction', INTERACTIVE_BLOCKED_RECOVERY_ACTIONS, `${fieldPath}.resolvedByAction`),
701
+ resultPhase: readOrchestrationPhase(record, 'resultPhase', `${fieldPath}.resultPhase`),
702
+ };
703
+ }
704
+ function hydratePendingPlanReviewGuidance(record, fieldPath = 'pendingPlanReviewGuidance') {
705
+ if (!hasOwn(record, fieldPath)) {
706
+ return null;
707
+ }
708
+ const value = record[fieldPath];
709
+ if (value === null) {
710
+ return null;
711
+ }
712
+ const guidance = requireRecord(value, fieldPath);
713
+ return {
714
+ message: readString(guidance, 'message', `${fieldPath}.message`),
715
+ sourcePhase: readEnum(guidance, 'sourcePhase', ['reviewer_plan'], `${fieldPath}.sourcePhase`),
716
+ recordedAt: readString(guidance, 'recordedAt', `${fieldPath}.recordedAt`),
717
+ };
718
+ }
719
+ function hydrateRound(value, fieldPath = 'rounds[]') {
720
+ const round = requireRecord(value, fieldPath);
721
+ const commitRange = readRecord(round, 'commitRange', `${fieldPath}.commitRange`);
722
+ const openBlockingCanonicalIds = readOptionalStringArray(round, 'openBlockingCanonicalIds', `${fieldPath}.openBlockingCanonicalIds`);
723
+ return {
724
+ round: readSafeInteger(round, 'round', `${fieldPath}.round`),
725
+ reviewerSessionHandle: readNullableString(round, 'reviewerSessionHandle', `${fieldPath}.reviewerSessionHandle`),
726
+ reviewedPlanPath: readNullableString(round, 'reviewedPlanPath', `${fieldPath}.reviewedPlanPath`),
727
+ normalizationApplied: readBoolean(round, 'normalizationApplied', `${fieldPath}.normalizationApplied`),
728
+ normalizationOperations: readStringArray(round, 'normalizationOperations', `${fieldPath}.normalizationOperations`),
729
+ normalizationScopeLabelMappings: readArray(round, 'normalizationScopeLabelMappings', `${fieldPath}.normalizationScopeLabelMappings`).map((mapping, index) => {
730
+ const mappingRecord = requireRecord(mapping, `${fieldPath}.normalizationScopeLabelMappings[${index}]`);
731
+ return {
732
+ normalizedScopeNumber: readSafeInteger(mappingRecord, 'normalizedScopeNumber', `${fieldPath}.normalizationScopeLabelMappings[${index}].normalizedScopeNumber`),
733
+ originalScopeLabel: readString(mappingRecord, 'originalScopeLabel', `${fieldPath}.normalizationScopeLabelMappings[${index}].originalScopeLabel`),
734
+ };
735
+ }),
736
+ commitRange: {
737
+ base: readString(commitRange, 'base', `${fieldPath}.commitRange.base`),
738
+ head: readString(commitRange, 'head', `${fieldPath}.commitRange.head`),
739
+ },
740
+ openBlockingCanonicalCount: readSafeInteger(round, 'openBlockingCanonicalCount', `${fieldPath}.openBlockingCanonicalCount`),
741
+ ...(openBlockingCanonicalIds === undefined ? {} : { openBlockingCanonicalIds }),
742
+ findings: readStringArray(round, 'findings', `${fieldPath}.findings`),
743
+ };
744
+ }
745
+ function hydrateResidualReviewDebtItem(value, fieldPath) {
746
+ const item = requireRecord(value, fieldPath);
747
+ return {
748
+ id: readString(item, 'id', `${fieldPath}.id`),
749
+ canonicalId: readString(item, 'canonicalId', `${fieldPath}.canonicalId`),
750
+ status: readEnum(item, 'status', RESIDUAL_REVIEW_DEBT_STATUSES, `${fieldPath}.status`),
751
+ files: readStringArray(item, 'files', `${fieldPath}.files`),
752
+ claim: readString(item, 'claim', `${fieldPath}.claim`),
753
+ evidence: readNullableString(item, 'evidence', `${fieldPath}.evidence`),
754
+ requiredAction: readString(item, 'requiredAction', `${fieldPath}.requiredAction`),
755
+ coderDisposition: readNullableString(item, 'coderDisposition', `${fieldPath}.coderDisposition`),
756
+ coderCommit: readNullableString(item, 'coderCommit', `${fieldPath}.coderCommit`),
757
+ };
758
+ }
759
+ function hydrateCompletedScope(value, fieldPath = 'completedScopes[]') {
760
+ const scope = requireRecord(value, fieldPath);
761
+ return {
762
+ number: readString(scope, 'number', `${fieldPath}.number`),
763
+ marker: readEnum(scope, 'marker', SCOPE_MARKERS, `${fieldPath}.marker`),
764
+ result: readEnum(scope, 'result', ['accepted', 'blocked'], `${fieldPath}.result`),
765
+ baseCommit: readNullableString(scope, 'baseCommit', `${fieldPath}.baseCommit`),
766
+ finalCommit: readNullableString(scope, 'finalCommit', `${fieldPath}.finalCommit`),
767
+ summary: readNullableString(scope, 'summary', `${fieldPath}.summary`),
768
+ commitSubject: readNullableString(scope, 'commitSubject', `${fieldPath}.commitSubject`),
769
+ changedFiles: readStringArray(scope, 'changedFiles', `${fieldPath}.changedFiles`),
770
+ reviewRounds: readSafeInteger(scope, 'reviewRounds', `${fieldPath}.reviewRounds`),
771
+ findings: readSafeInteger(scope, 'findings', `${fieldPath}.findings`),
772
+ residualReviewDebt: readArray(scope, 'residualReviewDebt', `${fieldPath}.residualReviewDebt`).map((item, index) => hydrateResidualReviewDebtItem(item, `${fieldPath}.residualReviewDebt[${index}]`)),
773
+ archivedReviewPath: readNullableString(scope, 'archivedReviewPath', `${fieldPath}.archivedReviewPath`),
774
+ blocker: readNullableString(scope, 'blocker', `${fieldPath}.blocker`),
775
+ derivedFromParentScope: readNullableString(scope, 'derivedFromParentScope', `${fieldPath}.derivedFromParentScope`),
776
+ replacedByDerivedPlanPath: readNullableString(scope, 'replacedByDerivedPlanPath', `${fieldPath}.replacedByDerivedPlanPath`),
777
+ };
778
+ }
779
+ function isLegacyPlannerSessionState(args) {
780
+ if (args.topLevelMode !== 'plan') {
781
+ return false;
782
+ }
783
+ return (PLANNER_SESSION_PHASES.has(args.phase) ||
784
+ (args.blockedFromPhase !== null && PLANNER_SESSION_PHASES.has(args.blockedFromPhase)));
785
+ }
786
+ function normalizeStateV1(parsed) {
787
+ const state = requireRecord(parsed, 'state');
788
+ const version = readRequired(state, 'version');
789
+ if (version !== 1) {
790
+ throw new Error(`Invalid run state version: ${String(version)}`);
791
+ }
792
+ const topLevelMode = readRequired(state, 'topLevelMode');
793
+ if (!isTopLevelMode(topLevelMode)) {
794
+ throwInvalidState('topLevelMode', `expected one of "plan", "execute", received ${formatStateValue(topLevelMode)}`);
795
+ }
796
+ assertOrchestrationStatus(readRequired(state, 'status'), 'status');
797
+ const phase = readOrchestrationPhase(state, 'phase');
798
+ const blockedFromPhase = readNullableOrchestrationPhase(state, 'blockedFromPhase');
799
+ const hasPersistedPlannerSessionHandle = hasOwn(state, 'plannerSessionHandle');
800
+ const hasPersistedPlannerSessionProtocol = hasOwn(state, 'plannerSessionProtocol');
801
+ const persistedPlannerSessionHandle = readOptionalNullableString(state, 'plannerSessionHandle') ?? null;
802
+ const persistedPlannerSessionProtocol = hydrateOptionalCoderSessionProtocol(state, 'plannerSessionProtocol');
803
+ const coderSessionHandle = readNullableString(state, 'coderSessionHandle');
804
+ const coderSessionProtocol = hydrateCoderSessionProtocol(state, coderSessionHandle);
805
+ const migrateLegacyPlannerSession = !hasPersistedPlannerSessionHandle &&
806
+ !hasPersistedPlannerSessionProtocol &&
807
+ coderSessionHandle !== null &&
808
+ isLegacyPlannerSessionState({ phase, blockedFromPhase, topLevelMode });
809
+ const plannerSessionHandle = migrateLegacyPlannerSession ? coderSessionHandle : persistedPlannerSessionHandle;
810
+ const plannerSessionProtocol = migrateLegacyPlannerSession ? coderSessionProtocol : persistedPlannerSessionProtocol;
811
+ return {
812
+ version: 1,
813
+ planDoc: readString(state, 'planDoc'),
814
+ planDocBackupPath: readNullableString(state, 'planDocBackupPath'),
815
+ cwd: readString(state, 'cwd'),
816
+ runDir: readString(state, 'runDir'),
817
+ topLevelMode,
818
+ allowedDirtyPaths: readStringArray(state, 'allowedDirtyPaths'),
819
+ agentConfig: hydrateAgentConfig(readRequired(state, 'agentConfig'), 'agentConfig'),
820
+ unattended: readOptionalBoolean(state, 'unattended') ?? false,
821
+ unattendedAutoResumeCount: readOptionalSafeInteger(state, 'unattendedAutoResumeCount') ?? 0,
822
+ reviewStuckArbiterCount: readOptionalSafeInteger(state, 'reviewStuckArbiterCount') ?? 0,
823
+ // Legacy-tolerant: states persisted before this field existed hydrate to
824
+ // true, preserving the historical always-squash behavior on resume.
825
+ autoSquashOnCompletion: readOptionalBoolean(state, 'autoSquashOnCompletion') ?? true,
826
+ progressJsonPath: readString(state, 'progressJsonPath'),
827
+ progressMarkdownPath: readString(state, 'progressMarkdownPath'),
828
+ recoveryMarkdownPath: readString(state, 'recoveryMarkdownPath'),
829
+ phase,
830
+ createdAt: readString(state, 'createdAt'),
831
+ updatedAt: readString(state, 'updatedAt'),
832
+ reviewMarkdownPath: readString(state, 'reviewMarkdownPath'),
833
+ archivedReviewPath: readNullableString(state, 'archivedReviewPath'),
834
+ initialBaseCommit: readNullableString(state, 'initialBaseCommit'),
835
+ baseCommit: readNullableString(state, 'baseCommit'),
836
+ finalCommit: readNullableString(state, 'finalCommit'),
837
+ plannerSessionHandle,
838
+ plannerSessionProtocol,
839
+ coderSessionHandle: migrateLegacyPlannerSession ? null : coderSessionHandle,
840
+ coderSessionProtocol: migrateLegacyPlannerSession ? null : coderSessionProtocol,
841
+ reviewerSessionHandle: readNullableString(state, 'reviewerSessionHandle'),
842
+ executionShape: readNullableEnum(state, 'executionShape', EXECUTION_SHAPES),
843
+ // Legacy-tolerant: states persisted before this field existed default to null.
844
+ authoredExecutionShape: readOptionalNullableEnum(state, 'authoredExecutionShape', EXECUTION_SHAPES),
845
+ currentScopeNumber: readSafeInteger(state, 'currentScopeNumber'),
846
+ coderRetryCount: readSafeInteger(state, 'coderRetryCount'),
847
+ lastScopeMarker: readNullableEnum(state, 'lastScopeMarker', SCOPE_MARKERS),
848
+ currentScopeProgressJustification: hydrateExecuteScopeProgressJustification(readRequired(state, 'currentScopeProgressJustification')),
849
+ currentScopeMeaningfulProgressVerdict: hydrateReviewerMeaningfulProgressVerdict(readRequired(state, 'currentScopeMeaningfulProgressVerdict')),
850
+ manualGate: hydrateManualGate(state),
851
+ finalCompletionSummary: hydrateFinalCompletionSummary(readRequired(state, 'finalCompletionSummary')),
852
+ finalCompletionReviewVerdict: hydrateFinalCompletionReviewerVerdict(readRequired(state, 'finalCompletionReviewVerdict')),
853
+ finalCompletionResolvedAction: readNullableEnum(state, 'finalCompletionResolvedAction', FINAL_COMPLETION_ACTIONS),
854
+ finalCompletionContinueExecutionCount: readSafeInteger(state, 'finalCompletionContinueExecutionCount'),
855
+ finalCompletionContinueExecutionCapReached: readBoolean(state, 'finalCompletionContinueExecutionCapReached'),
856
+ derivedPlanPath: readNullableString(state, 'derivedPlanPath'),
857
+ derivedFromScopeNumber: readNullableSafeInteger(state, 'derivedFromScopeNumber'),
858
+ derivedPlanStatus: readNullableEnum(state, 'derivedPlanStatus', ['pending_review', 'accepted', 'rejected']),
859
+ derivedScopeIndex: readNullableSafeInteger(state, 'derivedScopeIndex'),
860
+ splitPlanStartedNotified: readBoolean(state, 'splitPlanStartedNotified'),
861
+ derivedPlanAcceptedNotified: readBoolean(state, 'derivedPlanAcceptedNotified'),
862
+ splitPlanBlockedNotified: readBoolean(state, 'splitPlanBlockedNotified'),
863
+ splitPlanCountForCurrentScope: readSafeInteger(state, 'splitPlanCountForCurrentScope'),
864
+ derivedPlanDepth: readSafeInteger(state, 'derivedPlanDepth'),
865
+ maxDerivedPlanReviewRounds: readSafeInteger(state, 'maxDerivedPlanReviewRounds'),
866
+ rounds: readArray(state, 'rounds').map((round, index) => hydrateRound(round, `rounds[${index}]`)),
867
+ recentBlocks: hasOwn(state, 'recentBlocks')
868
+ ? readArray(state, 'recentBlocks').map((record, index) => hydrateRecentBlock(record, `recentBlocks[${index}]`))
869
+ : [],
870
+ findings: readArray(state, 'findings').map((finding, index) => hydrateFinding(finding, `findings[${index}]`)),
871
+ createdCommits: readStringArray(state, 'createdCommits'),
872
+ completedScopes: readArray(state, 'completedScopes').map((scope, index) => hydrateCompletedScope(scope, `completedScopes[${index}]`)),
873
+ maxRounds: readSafeInteger(state, 'maxRounds'),
874
+ blockedFromPhase,
875
+ interactiveBlockedRecovery: hydrateInteractiveBlockedRecovery(readRequired(state, 'interactiveBlockedRecovery')),
876
+ interactiveBlockedRecoveryHistory: readArray(state, 'interactiveBlockedRecoveryHistory').map((record, index) => hydrateInteractiveBlockedRecoveryRecord(record, `interactiveBlockedRecoveryHistory[${index}]`)),
877
+ pendingPlanReviewGuidance: hydratePendingPlanReviewGuidance(state),
878
+ status: readEnum(state, 'status', ['running', 'paused', 'done', 'blocked', 'failed']),
879
+ };
880
+ }
881
+ export async function loadState(path) {
882
+ const content = await readFile(path, 'utf8');
883
+ const parsed = JSON.parse(content);
884
+ const state = normalizeStateV1(parsed);
885
+ assertValidOrchestrationState(state, { source: 'load', path });
886
+ return state;
887
+ }