@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,832 @@
1
+ import { repairReviewerSquashMessageDraft, validateReviewerSquashMessageDraft } from '../squash-message.js';
2
+ const REVIEWER_PAYLOAD_KEYS = [
3
+ 'summary',
4
+ 'findings',
5
+ 'meaningfulProgressAction',
6
+ 'meaningfulProgressRationale',
7
+ ];
8
+ const REVIEWER_FINDING_KEYS = ['severity', 'files', 'claim', 'evidence', 'requiredAction'];
9
+ const REVIEWER_FINDING_SEVERITIES = ['blocking', 'non_blocking'];
10
+ const REVIEWER_MEANINGFUL_PROGRESS_ACTIONS = [
11
+ 'accept',
12
+ 'block_for_operator',
13
+ 'replace_plan',
14
+ 'advance_parent',
15
+ ];
16
+ export const EXECUTE_SCOPE_PROGRESS_PAYLOAD_START = 'NEAL_PROGRESS_JUSTIFICATION_JSON_START';
17
+ export const EXECUTE_SCOPE_PROGRESS_PAYLOAD_END = 'NEAL_PROGRESS_JUSTIFICATION_JSON_END';
18
+ export function buildReviewerSchema() {
19
+ return {
20
+ type: 'object',
21
+ properties: {
22
+ summary: { type: 'string' },
23
+ findings: {
24
+ type: 'array',
25
+ items: {
26
+ type: 'object',
27
+ properties: {
28
+ severity: { type: 'string', enum: ['blocking', 'non_blocking'] },
29
+ files: { type: 'array', items: { type: 'string' } },
30
+ claim: { type: 'string' },
31
+ evidence: { type: 'string' },
32
+ requiredAction: { type: 'string' },
33
+ },
34
+ required: ['severity', 'files', 'claim', 'evidence', 'requiredAction'],
35
+ additionalProperties: false,
36
+ },
37
+ },
38
+ meaningfulProgressAction: { type: 'string', enum: ['accept', 'block_for_operator', 'replace_plan', 'advance_parent'] },
39
+ meaningfulProgressRationale: { type: 'string' },
40
+ },
41
+ required: ['summary', 'findings', 'meaningfulProgressAction', 'meaningfulProgressRationale'],
42
+ additionalProperties: false,
43
+ };
44
+ }
45
+ export function validateReviewerPayload(payload) {
46
+ const value = requireReviewerObject(payload, 'Reviewer payload');
47
+ requireReviewerProperties(value, REVIEWER_PAYLOAD_KEYS, 'Reviewer payload');
48
+ if (!Array.isArray(value.findings)) {
49
+ throw new Error('Reviewer payload.findings must be an array.');
50
+ }
51
+ return {
52
+ summary: requireReviewerString(value.summary, 'Reviewer payload.summary'),
53
+ findings: value.findings.map(validateReviewerFindingPayload),
54
+ meaningfulProgressAction: requireReviewerEnum(value.meaningfulProgressAction, 'Reviewer payload.meaningfulProgressAction', REVIEWER_MEANINGFUL_PROGRESS_ACTIONS),
55
+ meaningfulProgressRationale: requireReviewerString(value.meaningfulProgressRationale, 'Reviewer payload.meaningfulProgressRationale'),
56
+ };
57
+ }
58
+ function validateReviewerFindingPayload(finding, index) {
59
+ const label = `Reviewer payload.findings[${index}]`;
60
+ const value = requireReviewerObject(finding, label);
61
+ requireReviewerProperties(value, REVIEWER_FINDING_KEYS, label);
62
+ if (!Array.isArray(value.files)) {
63
+ throw new Error(`${label}.files must be an array.`);
64
+ }
65
+ return {
66
+ severity: requireReviewerEnum(value.severity, `${label}.severity`, REVIEWER_FINDING_SEVERITIES),
67
+ files: value.files.map((file, fileIndex) => requireReviewerString(file, `${label}.files[${fileIndex}]`)),
68
+ claim: requireReviewerString(value.claim, `${label}.claim`),
69
+ evidence: requireReviewerString(value.evidence, `${label}.evidence`),
70
+ requiredAction: requireReviewerString(value.requiredAction, `${label}.requiredAction`),
71
+ };
72
+ }
73
+ function requireReviewerObject(value, label) {
74
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
75
+ throw new Error(`${label} must be a non-null object.`);
76
+ }
77
+ return value;
78
+ }
79
+ function requireReviewerProperties(value, requiredKeys, label) {
80
+ for (const key of requiredKeys) {
81
+ if (!Object.prototype.hasOwnProperty.call(value, key)) {
82
+ throw new Error(`${label} is missing required property "${key}".`);
83
+ }
84
+ }
85
+ for (const key of Object.keys(value)) {
86
+ if (!requiredKeys.includes(key)) {
87
+ throw new Error(`${label} included unknown property "${key}".`);
88
+ }
89
+ }
90
+ }
91
+ function requireReviewerString(value, fieldPath) {
92
+ if (typeof value !== 'string') {
93
+ throw new Error(`${fieldPath} must be a string.`);
94
+ }
95
+ return value;
96
+ }
97
+ function requireReviewerEnum(value, fieldPath, allowedValues) {
98
+ if (typeof value !== 'string' || !allowedValues.includes(value)) {
99
+ throw new Error(`${fieldPath} must be exactly one of: ${allowedValues.join(', ')}.`);
100
+ }
101
+ return value;
102
+ }
103
+ export function buildPlanReviewerSchema() {
104
+ return {
105
+ type: 'object',
106
+ properties: {
107
+ summary: { type: 'string' },
108
+ executionShape: { type: 'string', enum: ['one_shot', 'multi_scope', 'multi_scope_unknown'] },
109
+ findings: {
110
+ type: 'array',
111
+ items: {
112
+ type: 'object',
113
+ properties: {
114
+ severity: { type: 'string', enum: ['blocking', 'non_blocking'] },
115
+ files: { type: 'array', items: { type: 'string' } },
116
+ claim: { type: 'string' },
117
+ requiredAction: { type: 'string' },
118
+ },
119
+ required: ['severity', 'files', 'claim', 'requiredAction'],
120
+ additionalProperties: false,
121
+ },
122
+ },
123
+ },
124
+ required: ['summary', 'executionShape', 'findings'],
125
+ additionalProperties: false,
126
+ };
127
+ }
128
+ export function buildBlockedAdjudicatorSchema() {
129
+ return {
130
+ type: 'object',
131
+ properties: {
132
+ recoverable: { type: 'boolean' },
133
+ triageCategory: {
134
+ type: 'string',
135
+ enum: ['misunderstanding', 'authorization', 'external_precondition', 'impossible_task'],
136
+ },
137
+ resolutionDirective: { type: 'string' },
138
+ targetCanonicalIds: { type: 'array', items: { type: 'string' } },
139
+ rationale: { type: 'string' },
140
+ },
141
+ required: ['recoverable', 'triageCategory', 'resolutionDirective', 'rationale'],
142
+ additionalProperties: false,
143
+ };
144
+ }
145
+ // Validates a read-only review_stuck arbiter verdict. Exactly one triage
146
+ // category is autonomously recoverable: `misunderstanding` requires
147
+ // recoverable=true plus a non-empty resolutionDirective; the three genuine-wall
148
+ // categories (`authorization`, `external_precondition`, `impossible_task`)
149
+ // require recoverable=false. Anything else (recoverable=true with another
150
+ // triage, or recoverable=false paired with `misunderstanding`) is rejected so a
151
+ // malformed verdict can never drive an autonomous recovery.
152
+ export function validateBlockedAdjudicatorVerdictPayload(rawPayload) {
153
+ const value = requirePayloadObject(rawPayload, 'Review-stuck arbiter payload');
154
+ const payload = {
155
+ recoverable: requirePayloadBoolean(value.recoverable, 'Review-stuck arbiter payload.recoverable'),
156
+ triageCategory: requirePayloadEnum(value.triageCategory, 'Review-stuck arbiter payload.triageCategory', [
157
+ 'misunderstanding',
158
+ 'authorization',
159
+ 'external_precondition',
160
+ 'impossible_task',
161
+ ]),
162
+ resolutionDirective: requirePayloadString(value.resolutionDirective, 'Review-stuck arbiter payload.resolutionDirective'),
163
+ // The verdict's targetCanonicalIds is optional: a coder/split-plan block has
164
+ // no reviewer findings to point at, so an absent value defaults to [].
165
+ targetCanonicalIds: value.targetCanonicalIds === undefined
166
+ ? []
167
+ : requirePayloadStringArray(value.targetCanonicalIds, 'Review-stuck arbiter payload.targetCanonicalIds'),
168
+ rationale: requirePayloadString(value.rationale, 'Review-stuck arbiter payload.rationale'),
169
+ };
170
+ if (!payload.rationale.trim()) {
171
+ throw new Error('Review-stuck arbiter returned an empty rationale.');
172
+ }
173
+ if (payload.recoverable) {
174
+ if (payload.triageCategory !== 'misunderstanding') {
175
+ throw new Error('Review-stuck arbiter returned recoverable=true with a triageCategory other than misunderstanding.');
176
+ }
177
+ if (!payload.resolutionDirective.trim()) {
178
+ throw new Error('Review-stuck arbiter returned recoverable=true without a non-empty resolutionDirective.');
179
+ }
180
+ }
181
+ else if (payload.triageCategory === 'misunderstanding') {
182
+ throw new Error('Review-stuck arbiter returned recoverable=false paired with triageCategory=misunderstanding.');
183
+ }
184
+ return payload;
185
+ }
186
+ export function validatePlanReviewerPayload(payload) {
187
+ const value = requirePayloadObject(payload, 'Plan reviewer payload');
188
+ return {
189
+ summary: requirePayloadString(value.summary, 'Plan reviewer payload.summary'),
190
+ executionShape: requirePayloadEnum(value.executionShape, 'Plan reviewer payload.executionShape', [
191
+ 'one_shot',
192
+ 'multi_scope',
193
+ 'multi_scope_unknown',
194
+ ]),
195
+ findings: requirePayloadObjectArray(value.findings, 'Plan reviewer payload.findings').map((finding, index) => ({
196
+ severity: requirePayloadEnum(finding.severity, `Plan reviewer payload.findings[${index}].severity`, [
197
+ 'blocking',
198
+ 'non_blocking',
199
+ ]),
200
+ files: requirePayloadStringArray(finding.files, `Plan reviewer payload.findings[${index}].files`),
201
+ claim: requirePayloadString(finding.claim, `Plan reviewer payload.findings[${index}].claim`),
202
+ evidence: '',
203
+ requiredAction: requirePayloadString(finding.requiredAction, `Plan reviewer payload.findings[${index}].requiredAction`),
204
+ })),
205
+ };
206
+ }
207
+ export function buildCoderResponseSchema() {
208
+ return {
209
+ type: 'object',
210
+ properties: {
211
+ outcome: { type: 'string', enum: ['responded', 'blocked', 'split_plan'] },
212
+ summary: { type: 'string' },
213
+ blocker: { type: 'string' },
214
+ derivedPlan: { type: 'string' },
215
+ responses: {
216
+ type: 'array',
217
+ items: {
218
+ type: 'object',
219
+ properties: {
220
+ id: { type: 'string' },
221
+ decision: { type: 'string', enum: ['fixed', 'rejected', 'deferred'] },
222
+ summary: { type: 'string' },
223
+ },
224
+ required: ['id', 'decision', 'summary'],
225
+ additionalProperties: false,
226
+ },
227
+ },
228
+ },
229
+ required: ['outcome', 'summary', 'blocker', 'derivedPlan', 'responses'],
230
+ additionalProperties: false,
231
+ };
232
+ }
233
+ export function buildCoderBlockedRecoveryDispositionSchema() {
234
+ return {
235
+ type: 'object',
236
+ properties: {
237
+ action: {
238
+ type: 'string',
239
+ enum: ['resume_current_scope', 'replace_current_scope', 'stay_blocked', 'terminal_block'],
240
+ },
241
+ summary: { type: 'string' },
242
+ rationale: { type: 'string' },
243
+ blocker: { type: 'string' },
244
+ replacementPlan: { type: 'string' },
245
+ },
246
+ required: ['action', 'summary', 'rationale', 'blocker', 'replacementPlan'],
247
+ additionalProperties: false,
248
+ };
249
+ }
250
+ export function buildCoderPlanResponseSchema() {
251
+ return {
252
+ type: 'object',
253
+ properties: {
254
+ outcome: { type: 'string', enum: ['responded', 'blocked'] },
255
+ summary: { type: 'string' },
256
+ blocker: { type: 'string' },
257
+ responses: {
258
+ type: 'array',
259
+ items: {
260
+ type: 'object',
261
+ properties: {
262
+ id: { type: 'string' },
263
+ decision: { type: 'string', enum: ['fixed', 'rejected', 'deferred'] },
264
+ summary: { type: 'string' },
265
+ },
266
+ required: ['id', 'decision', 'summary'],
267
+ additionalProperties: false,
268
+ },
269
+ },
270
+ },
271
+ required: ['outcome', 'summary', 'blocker', 'responses'],
272
+ additionalProperties: false,
273
+ };
274
+ }
275
+ export function buildCoderPlanSchema() {
276
+ return {
277
+ type: 'object',
278
+ properties: {
279
+ action: { type: 'string', enum: ['ready_for_review', 'blocked'] },
280
+ message: { type: 'string' },
281
+ executionShape: { type: 'string', enum: ['one_shot', 'multi_scope', 'multi_scope_unknown'] },
282
+ planBody: { type: 'string' },
283
+ blockedReason: { type: 'string' },
284
+ },
285
+ required: ['action', 'message', 'executionShape', 'planBody', 'blockedReason'],
286
+ additionalProperties: false,
287
+ };
288
+ }
289
+ export function buildCoderScopeSchema() {
290
+ const manualGateResumeCheckSchema = {
291
+ type: 'object',
292
+ properties: {
293
+ type: { type: 'string', enum: ['command'] },
294
+ name: { type: 'string' },
295
+ command: {
296
+ type: 'array',
297
+ items: { type: 'string' },
298
+ },
299
+ cwd: { type: ['string', 'null'], enum: ['repo', 'run_dir', null] },
300
+ timeoutMs: { type: ['number', 'null'] },
301
+ },
302
+ required: ['type', 'name', 'command', 'cwd', 'timeoutMs'],
303
+ additionalProperties: false,
304
+ };
305
+ return {
306
+ type: 'object',
307
+ properties: {
308
+ action: { type: 'string', enum: ['continue', 'scope_done', 'done', 'blocked', 'split_plan', 'manual_gate'] },
309
+ message: { type: 'string' },
310
+ progress: buildExecuteScopeProgressSchema(),
311
+ manualGate: {
312
+ type: ['object', 'null'],
313
+ properties: {
314
+ id: { type: 'string' },
315
+ title: { type: 'string' },
316
+ reason: { type: 'string' },
317
+ instructionsMarkdown: { type: 'string' },
318
+ resumeChecks: {
319
+ type: 'array',
320
+ items: manualGateResumeCheckSchema,
321
+ },
322
+ },
323
+ required: ['id', 'title', 'reason', 'instructionsMarkdown', 'resumeChecks'],
324
+ additionalProperties: false,
325
+ },
326
+ derivedPlan: { type: 'string' },
327
+ blockedReason: { type: 'string' },
328
+ },
329
+ required: ['action', 'message', 'progress', 'manualGate', 'derivedPlan', 'blockedReason'],
330
+ additionalProperties: false,
331
+ };
332
+ }
333
+ export function buildExecuteScopeProgressSchema() {
334
+ return {
335
+ type: 'object',
336
+ properties: {
337
+ milestoneTargeted: { type: 'string' },
338
+ newEvidence: { type: 'string' },
339
+ whyNotRedundant: { type: 'string' },
340
+ nextStepUnlocked: { type: 'string' },
341
+ },
342
+ required: ['milestoneTargeted', 'newEvidence', 'whyNotRedundant', 'nextStepUnlocked'],
343
+ additionalProperties: false,
344
+ };
345
+ }
346
+ export function buildFinalCompletionSummarySchema() {
347
+ return {
348
+ type: 'object',
349
+ properties: {
350
+ planGoalSatisfied: { type: 'boolean' },
351
+ whatChangedOverall: { type: 'string' },
352
+ verificationSummary: { type: 'string' },
353
+ remainingKnownGaps: {
354
+ type: 'array',
355
+ items: { type: 'string' },
356
+ },
357
+ },
358
+ required: ['planGoalSatisfied', 'whatChangedOverall', 'verificationSummary', 'remainingKnownGaps'],
359
+ additionalProperties: false,
360
+ };
361
+ }
362
+ export function buildFinalCompletionReviewerSchema() {
363
+ return {
364
+ type: 'object',
365
+ properties: {
366
+ action: { type: 'string', enum: ['accept_complete', 'continue_execution', 'block_for_operator'] },
367
+ summary: { type: 'string' },
368
+ rationale: { type: 'string' },
369
+ missingWork: {
370
+ type: ['object', 'null'],
371
+ properties: {
372
+ summary: { type: 'string' },
373
+ requiredOutcome: { type: 'string' },
374
+ verification: { type: 'string' },
375
+ },
376
+ required: ['summary', 'requiredOutcome', 'verification'],
377
+ additionalProperties: false,
378
+ },
379
+ squashCommitMessage: {
380
+ description: 'Project-facing Git history metadata used only when action is accept_complete; set null for continue_execution or block_for_operator.',
381
+ type: ['object', 'null'],
382
+ properties: {
383
+ subject: {
384
+ description: 'Concise project-facing commit subject summarizing code or product behavior, not plan documents, paths, scopes, Neal mechanics, provider process, reviewer process, or final cleanup.',
385
+ type: 'string',
386
+ },
387
+ bullets: {
388
+ description: 'Two to five project-facing Git history bullets summarizing behavior changes; avoid plan paths, markdown plan filenames, temporary run paths, scope wording, Neal mechanics, provider process, or reviewer process.',
389
+ type: 'array',
390
+ minItems: 2,
391
+ maxItems: 5,
392
+ items: { type: 'string' },
393
+ },
394
+ },
395
+ required: ['subject', 'bullets'],
396
+ additionalProperties: false,
397
+ },
398
+ },
399
+ required: ['action', 'summary', 'rationale', 'missingWork', 'squashCommitMessage'],
400
+ additionalProperties: false,
401
+ };
402
+ }
403
+ function parseJsonPayload(raw, label) {
404
+ try {
405
+ return JSON.parse(raw);
406
+ }
407
+ catch (error) {
408
+ const message = error instanceof Error ? error.message : String(error);
409
+ throw new Error(`${label} returned invalid JSON: ${message}\nRaw response:\n${raw}`);
410
+ }
411
+ }
412
+ function extractDelimitedPayload(raw, label) {
413
+ const startIndex = raw.indexOf(EXECUTE_SCOPE_PROGRESS_PAYLOAD_START);
414
+ if (startIndex === -1) {
415
+ throw new Error(`${label} did not include the required progress-justification payload start marker.`);
416
+ }
417
+ if (raw.indexOf(EXECUTE_SCOPE_PROGRESS_PAYLOAD_START, startIndex + EXECUTE_SCOPE_PROGRESS_PAYLOAD_START.length) !== -1) {
418
+ throw new Error(`${label} included multiple progress-justification payload start markers.`);
419
+ }
420
+ const endIndex = raw.indexOf(EXECUTE_SCOPE_PROGRESS_PAYLOAD_END, startIndex + EXECUTE_SCOPE_PROGRESS_PAYLOAD_START.length);
421
+ if (endIndex === -1) {
422
+ throw new Error(`${label} did not include the required progress-justification payload end marker.`);
423
+ }
424
+ if (raw.indexOf(EXECUTE_SCOPE_PROGRESS_PAYLOAD_END, endIndex + EXECUTE_SCOPE_PROGRESS_PAYLOAD_END.length) !== -1) {
425
+ throw new Error(`${label} included multiple progress-justification payload end markers.`);
426
+ }
427
+ const payloadText = raw.slice(startIndex + EXECUTE_SCOPE_PROGRESS_PAYLOAD_START.length, endIndex).trim();
428
+ if (!payloadText) {
429
+ throw new Error(`${label} returned an empty progress-justification payload.`);
430
+ }
431
+ return {
432
+ payloadText,
433
+ startIndex,
434
+ endIndex,
435
+ };
436
+ }
437
+ function requireNonEmptyString(value, field, label) {
438
+ if (typeof value !== 'string' || value.trim().length === 0) {
439
+ throw new Error(`${label} returned an empty or missing ${field} field in the progress-justification payload.`);
440
+ }
441
+ return value.trim();
442
+ }
443
+ function requirePayloadObject(value, label) {
444
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
445
+ throw new Error(`${label} must be a non-null object.`);
446
+ }
447
+ return value;
448
+ }
449
+ function requirePayloadString(value, fieldPath) {
450
+ if (typeof value !== 'string') {
451
+ throw new Error(`${fieldPath} must be a string.`);
452
+ }
453
+ return value;
454
+ }
455
+ function requirePayloadBoolean(value, fieldPath) {
456
+ if (typeof value !== 'boolean') {
457
+ throw new Error(`${fieldPath} must be a boolean.`);
458
+ }
459
+ return value;
460
+ }
461
+ function requirePayloadStringArray(value, fieldPath) {
462
+ if (!Array.isArray(value)) {
463
+ throw new Error(`${fieldPath} must be an array.`);
464
+ }
465
+ return value.map((item, index) => requirePayloadString(item, `${fieldPath}[${index}]`));
466
+ }
467
+ function requirePayloadObjectArray(value, fieldPath) {
468
+ if (!Array.isArray(value)) {
469
+ throw new Error(`${fieldPath} must be an array.`);
470
+ }
471
+ return value.map((item, index) => requirePayloadObject(item, `${fieldPath}[${index}]`));
472
+ }
473
+ function requirePayloadEnum(value, fieldPath, allowedValues) {
474
+ if (typeof value !== 'string' || !allowedValues.includes(value)) {
475
+ throw new Error(`${fieldPath} must be exactly one of: ${allowedValues.join(', ')}.`);
476
+ }
477
+ return value;
478
+ }
479
+ function validateCoderFindingResponse(value, fieldPath) {
480
+ const response = requirePayloadObject(value, fieldPath);
481
+ return {
482
+ id: requirePayloadString(response.id, `${fieldPath}.id`),
483
+ decision: requirePayloadEnum(response.decision, `${fieldPath}.decision`, ['fixed', 'rejected', 'deferred']),
484
+ summary: requirePayloadString(response.summary, `${fieldPath}.summary`),
485
+ };
486
+ }
487
+ // Legacy marker-protocol compatibility for active legacy_marker_v1 execution
488
+ // sessions. New primary execution sessions use CoderScopePayload instead.
489
+ export function parseExecuteScopeProgressPayload(raw) {
490
+ const label = 'Coder scope round';
491
+ const { payloadText } = extractDelimitedPayload(raw, label);
492
+ const payload = parseJsonPayload(payloadText, `${label} progress-justification payload`);
493
+ return {
494
+ milestoneTargeted: requireNonEmptyString(payload.milestoneTargeted, 'milestoneTargeted', label),
495
+ newEvidence: requireNonEmptyString(payload.newEvidence, 'newEvidence', label),
496
+ whyNotRedundant: requireNonEmptyString(payload.whyNotRedundant, 'whyNotRedundant', label),
497
+ nextStepUnlocked: requireNonEmptyString(payload.nextStepUnlocked, 'nextStepUnlocked', label),
498
+ };
499
+ }
500
+ export function stripExecuteScopeProgressPayload(raw) {
501
+ const { startIndex, endIndex } = extractDelimitedPayload(raw, 'Coder scope round');
502
+ const before = raw.slice(0, startIndex).trimEnd();
503
+ const after = raw.slice(endIndex + EXECUTE_SCOPE_PROGRESS_PAYLOAD_END.length).trimStart();
504
+ if (before && after) {
505
+ return `${before}\n\n${after}`;
506
+ }
507
+ return before || after;
508
+ }
509
+ export function validateCoderResponsePayload(rawPayload) {
510
+ const value = requirePayloadObject(rawPayload, 'Coder response round payload');
511
+ const payload = {
512
+ outcome: requirePayloadEnum(value.outcome, 'Coder response round payload.outcome', [
513
+ 'responded',
514
+ 'blocked',
515
+ 'split_plan',
516
+ ]),
517
+ summary: requirePayloadString(value.summary, 'Coder response round payload.summary'),
518
+ blocker: requirePayloadString(value.blocker, 'Coder response round payload.blocker'),
519
+ derivedPlan: requirePayloadString(value.derivedPlan, 'Coder response round payload.derivedPlan'),
520
+ responses: requirePayloadObjectArray(value.responses, 'Coder response round payload.responses').map((response, index) => validateCoderFindingResponse(response, `Coder response round payload.responses[${index}]`)),
521
+ };
522
+ const derivedPlan = payload.derivedPlan?.trim() ?? '';
523
+ if (payload.outcome === 'split_plan' && !derivedPlan) {
524
+ throw new Error('Coder response round returned outcome=split_plan without a derivedPlan payload.');
525
+ }
526
+ if (payload.outcome !== 'split_plan' && derivedPlan) {
527
+ throw new Error('Coder response round returned a derivedPlan payload without outcome=split_plan.');
528
+ }
529
+ return payload;
530
+ }
531
+ export function validateCoderPlanResponsePayload(payload) {
532
+ const value = requirePayloadObject(payload, 'Planner plan-response round payload');
533
+ return {
534
+ outcome: requirePayloadEnum(value.outcome, 'Planner plan-response round payload.outcome', [
535
+ 'responded',
536
+ 'blocked',
537
+ ]),
538
+ summary: requirePayloadString(value.summary, 'Planner plan-response round payload.summary'),
539
+ blocker: requirePayloadString(value.blocker, 'Planner plan-response round payload.blocker'),
540
+ responses: requirePayloadObjectArray(value.responses, 'Planner plan-response round payload.responses').map((response, index) => validateCoderFindingResponse(response, `Planner plan-response round payload.responses[${index}]`)),
541
+ };
542
+ }
543
+ export function validateCoderPlanPayload(rawPayload) {
544
+ const value = requirePayloadObject(rawPayload, 'Planner plan round payload');
545
+ const payload = {
546
+ action: requirePayloadEnum(value.action, 'Planner plan round payload.action', [
547
+ 'ready_for_review',
548
+ 'blocked',
549
+ ]),
550
+ message: requirePayloadString(value.message, 'Planner plan round payload.message'),
551
+ executionShape: requirePayloadEnum(value.executionShape, 'Planner plan round payload.executionShape', [
552
+ 'one_shot',
553
+ 'multi_scope',
554
+ 'multi_scope_unknown',
555
+ ]),
556
+ planBody: requirePayloadString(value.planBody, 'Planner plan round payload.planBody'),
557
+ blockedReason: requirePayloadString(value.blockedReason, 'Planner plan round payload.blockedReason'),
558
+ };
559
+ const planBody = payload.planBody.trim();
560
+ const blockedReason = payload.blockedReason.trim();
561
+ if (payload.action === 'ready_for_review' && !planBody) {
562
+ throw new Error('Planner plan round returned action=ready_for_review without a planBody payload.');
563
+ }
564
+ if (payload.action === 'ready_for_review' && blockedReason) {
565
+ throw new Error('Planner plan round returned a blockedReason payload with action=ready_for_review.');
566
+ }
567
+ if (payload.action === 'blocked' && !blockedReason) {
568
+ throw new Error('Planner plan round returned action=blocked without a blockedReason payload.');
569
+ }
570
+ return {
571
+ ...payload,
572
+ message: payload.message.trim(),
573
+ };
574
+ }
575
+ export function validateCoderScopePayload(rawPayload) {
576
+ const value = requirePayloadObject(rawPayload, 'Coder scope round payload');
577
+ const payload = {
578
+ action: requirePayloadEnum(value.action, 'Coder scope round payload.action', [
579
+ 'continue',
580
+ 'scope_done',
581
+ 'done',
582
+ 'blocked',
583
+ 'split_plan',
584
+ 'manual_gate',
585
+ ]),
586
+ message: requirePayloadString(value.message, 'Coder scope round payload.message'),
587
+ progress: requirePayloadObject(value.progress, 'Coder scope round payload.progress'),
588
+ manualGate: value.manualGate === null
589
+ ? null
590
+ : requirePayloadObject(value.manualGate, 'Coder scope round payload.manualGate'),
591
+ derivedPlan: requirePayloadString(value.derivedPlan, 'Coder scope round payload.derivedPlan'),
592
+ blockedReason: requirePayloadString(value.blockedReason, 'Coder scope round payload.blockedReason'),
593
+ };
594
+ const progress = {
595
+ milestoneTargeted: requireNonEmptyString(payload.progress?.milestoneTargeted, 'milestoneTargeted', 'Coder scope round'),
596
+ newEvidence: requireNonEmptyString(payload.progress?.newEvidence, 'newEvidence', 'Coder scope round'),
597
+ whyNotRedundant: requireNonEmptyString(payload.progress?.whyNotRedundant, 'whyNotRedundant', 'Coder scope round'),
598
+ nextStepUnlocked: requireNonEmptyString(payload.progress?.nextStepUnlocked, 'nextStepUnlocked', 'Coder scope round'),
599
+ };
600
+ const derivedPlan = payload.derivedPlan.trim();
601
+ const blockedReason = payload.blockedReason.trim();
602
+ const manualGate = payload.manualGate;
603
+ if (payload.action === 'split_plan' && !derivedPlan) {
604
+ throw new Error('Coder scope round returned action=split_plan without a derivedPlan payload.');
605
+ }
606
+ if (payload.action !== 'split_plan' && derivedPlan) {
607
+ throw new Error('Coder scope round returned a derivedPlan payload without action=split_plan.');
608
+ }
609
+ if (payload.action === 'blocked' && !blockedReason) {
610
+ throw new Error('Coder scope round returned action=blocked without a blockedReason payload.');
611
+ }
612
+ if (payload.action !== 'blocked' && blockedReason) {
613
+ throw new Error('Coder scope round returned a blockedReason payload without action=blocked.');
614
+ }
615
+ if (payload.action === 'manual_gate') {
616
+ if (manualGate === null || typeof manualGate !== 'object') {
617
+ throw new Error('Coder scope round returned action=manual_gate without a manualGate payload.');
618
+ }
619
+ if (derivedPlan) {
620
+ throw new Error('Coder scope round returned a derivedPlan payload with action=manual_gate.');
621
+ }
622
+ if (blockedReason) {
623
+ throw new Error('Coder scope round returned a blockedReason payload with action=manual_gate.');
624
+ }
625
+ const id = requireNonEmptyManualGateString(manualGate.id, 'manualGate.id');
626
+ const title = requireNonEmptyManualGateString(manualGate.title, 'manualGate.title');
627
+ const reason = requireNonEmptyManualGateString(manualGate.reason, 'manualGate.reason');
628
+ const instructionsMarkdown = requireNonEmptyManualGateString(manualGate.instructionsMarkdown, 'manualGate.instructionsMarkdown');
629
+ const resumeChecks = validateManualGateResumeChecks(manualGate.resumeChecks);
630
+ return {
631
+ ...payload,
632
+ message: payload.message.trim(),
633
+ progress,
634
+ manualGate: {
635
+ id,
636
+ title,
637
+ reason,
638
+ instructionsMarkdown,
639
+ resumeChecks,
640
+ },
641
+ };
642
+ }
643
+ if (manualGate !== null) {
644
+ throw new Error('Coder scope round returned a manualGate payload without action=manual_gate.');
645
+ }
646
+ return {
647
+ ...payload,
648
+ message: payload.message.trim(),
649
+ progress,
650
+ };
651
+ }
652
+ function requireNonEmptyManualGateString(value, fieldPath) {
653
+ if (typeof value !== 'string' || value.trim().length === 0) {
654
+ throw new Error(`Coder scope round returned an empty or missing ${fieldPath} field.`);
655
+ }
656
+ return value.trim();
657
+ }
658
+ function validateManualGateResumeChecks(value) {
659
+ if (!Array.isArray(value) || value.length === 0) {
660
+ throw new Error('Coder scope round returned manualGate.resumeChecks without at least one command check.');
661
+ }
662
+ return value.map((check, index) => {
663
+ const fieldPath = `manualGate.resumeChecks[${index}]`;
664
+ if (check === null || typeof check !== 'object') {
665
+ throw new Error(`Coder scope round returned invalid ${fieldPath}.`);
666
+ }
667
+ const candidate = check;
668
+ if (candidate.type !== 'command') {
669
+ throw new Error(`Coder scope round returned ${fieldPath}.type that is not "command".`);
670
+ }
671
+ const name = requireNonEmptyManualGateString(candidate.name, `${fieldPath}.name`);
672
+ if (!Array.isArray(candidate.command) || candidate.command.length === 0) {
673
+ throw new Error(`Coder scope round returned ${fieldPath}.command without a non-empty string array.`);
674
+ }
675
+ const command = candidate.command.map((part, commandIndex) => requireNonEmptyManualGateString(part, `${fieldPath}.command[${commandIndex}]`));
676
+ if (candidate.cwd !== undefined &&
677
+ candidate.cwd !== null &&
678
+ candidate.cwd !== 'repo' &&
679
+ candidate.cwd !== 'run_dir') {
680
+ throw new Error(`Coder scope round returned ${fieldPath}.cwd that is not "repo" or "run_dir".`);
681
+ }
682
+ if (candidate.timeoutMs !== undefined &&
683
+ candidate.timeoutMs !== null &&
684
+ (typeof candidate.timeoutMs !== 'number' || !Number.isSafeInteger(candidate.timeoutMs) || candidate.timeoutMs < 1)) {
685
+ throw new Error(`Coder scope round returned ${fieldPath}.timeoutMs that is not a positive safe integer.`);
686
+ }
687
+ return {
688
+ type: 'command',
689
+ name,
690
+ command,
691
+ ...(candidate.cwd === undefined || candidate.cwd === null ? {} : { cwd: candidate.cwd }),
692
+ ...(candidate.timeoutMs === undefined || candidate.timeoutMs === null ? {} : { timeoutMs: candidate.timeoutMs }),
693
+ };
694
+ });
695
+ }
696
+ function nonEmptyTrimmedItems(items) {
697
+ return items.map((item) => item.trim()).filter((item) => item.length > 0);
698
+ }
699
+ export function validateCoderBlockedRecoveryDispositionPayload(rawPayload) {
700
+ const value = requirePayloadObject(rawPayload, 'Coder blocked-recovery payload');
701
+ const payload = {
702
+ action: requirePayloadEnum(value.action, 'Coder blocked-recovery payload.action', [
703
+ 'resume_current_scope',
704
+ 'replace_current_scope',
705
+ 'stay_blocked',
706
+ 'terminal_block',
707
+ ]),
708
+ summary: requirePayloadString(value.summary, 'Coder blocked-recovery payload.summary'),
709
+ rationale: requirePayloadString(value.rationale, 'Coder blocked-recovery payload.rationale'),
710
+ blocker: requirePayloadString(value.blocker, 'Coder blocked-recovery payload.blocker'),
711
+ replacementPlan: requirePayloadString(value.replacementPlan, 'Coder blocked-recovery payload.replacementPlan'),
712
+ };
713
+ const blocker = payload.blocker.trim();
714
+ const replacementPlan = payload.replacementPlan.trim();
715
+ if (payload.action === 'replace_current_scope' && !replacementPlan) {
716
+ throw new Error('Coder blocked-recovery round returned action=replace_current_scope without a replacementPlan payload.');
717
+ }
718
+ if (payload.action !== 'replace_current_scope' && replacementPlan) {
719
+ throw new Error('Coder blocked-recovery round returned a replacementPlan payload without action=replace_current_scope.');
720
+ }
721
+ if ((payload.action === 'stay_blocked' || payload.action === 'terminal_block') && !blocker) {
722
+ throw new Error(`Coder blocked-recovery round returned action=${payload.action} without a blocker payload.`);
723
+ }
724
+ return payload;
725
+ }
726
+ export function parseFinalCompletionSummaryPayload(rawPayload) {
727
+ const value = requirePayloadObject(rawPayload, 'Final completion summary payload');
728
+ const payload = {
729
+ planGoalSatisfied: requirePayloadBoolean(value.planGoalSatisfied, 'Final completion summary payload.planGoalSatisfied'),
730
+ whatChangedOverall: requirePayloadString(value.whatChangedOverall, 'Final completion summary payload.whatChangedOverall'),
731
+ verificationSummary: requirePayloadString(value.verificationSummary, 'Final completion summary payload.verificationSummary'),
732
+ remainingKnownGaps: requirePayloadStringArray(value.remainingKnownGaps, 'Final completion summary payload.remainingKnownGaps'),
733
+ };
734
+ const whatChangedOverall = payload.whatChangedOverall.trim();
735
+ const verificationSummary = payload.verificationSummary.trim();
736
+ const remainingKnownGaps = payload.remainingKnownGaps
737
+ .map((gap) => gap.trim())
738
+ .filter((gap) => gap.length > 0);
739
+ if (!whatChangedOverall) {
740
+ throw new Error('Final completion summary returned an empty whatChangedOverall field.');
741
+ }
742
+ if (!verificationSummary) {
743
+ throw new Error('Final completion summary returned an empty verificationSummary field.');
744
+ }
745
+ if (payload.planGoalSatisfied && remainingKnownGaps.length > 0) {
746
+ throw new Error('Final completion summary cannot set planGoalSatisfied=true while remainingKnownGaps is non-empty.');
747
+ }
748
+ if (!payload.planGoalSatisfied && remainingKnownGaps.length === 0) {
749
+ throw new Error('Final completion summary cannot set planGoalSatisfied=false with an empty remainingKnownGaps array.');
750
+ }
751
+ return {
752
+ planGoalSatisfied: payload.planGoalSatisfied,
753
+ whatChangedOverall,
754
+ verificationSummary,
755
+ remainingKnownGaps,
756
+ };
757
+ }
758
+ export function parseFinalCompletionReviewerPayload(rawPayload) {
759
+ const value = requirePayloadObject(rawPayload, 'Final completion reviewer verdict payload');
760
+ const action = requirePayloadEnum(value.action, 'Final completion reviewer verdict payload.action', [
761
+ 'accept_complete',
762
+ 'continue_execution',
763
+ 'block_for_operator',
764
+ ]);
765
+ const missingWorkValue = value.missingWork;
766
+ const missingWorkShape = missingWorkValue === null
767
+ ? null
768
+ : requirePayloadObject(missingWorkValue, 'Final completion reviewer verdict payload.missingWork');
769
+ const payload = {
770
+ action,
771
+ summary: requirePayloadString(value.summary, 'Final completion reviewer verdict payload.summary'),
772
+ rationale: requirePayloadString(value.rationale, 'Final completion reviewer verdict payload.rationale'),
773
+ missingWork: missingWorkShape === null
774
+ ? null
775
+ : {
776
+ summary: requirePayloadString(missingWorkShape.summary, 'Final completion reviewer verdict payload.missingWork.summary'),
777
+ requiredOutcome: requirePayloadString(missingWorkShape.requiredOutcome, 'Final completion reviewer verdict payload.missingWork.requiredOutcome'),
778
+ verification: requirePayloadString(missingWorkShape.verification, 'Final completion reviewer verdict payload.missingWork.verification'),
779
+ },
780
+ squashCommitMessage: value.squashCommitMessage === null || value.squashCommitMessage === undefined
781
+ ? null
782
+ : requirePayloadObject(value.squashCommitMessage, 'Final completion reviewer verdict payload.squashCommitMessage'),
783
+ };
784
+ const summary = payload.summary.trim();
785
+ const rationale = payload.rationale.trim();
786
+ const squashCommitMessageValue = payload.squashCommitMessage ?? null;
787
+ if (!['accept_complete', 'continue_execution', 'block_for_operator'].includes(payload.action)) {
788
+ throw new Error(`Final completion reviewer verdict returned unsupported action ${JSON.stringify(payload.action)}.`);
789
+ }
790
+ if (!summary) {
791
+ throw new Error('Final completion reviewer verdict returned an empty summary field.');
792
+ }
793
+ if (!rationale) {
794
+ throw new Error('Final completion reviewer verdict returned an empty rationale field.');
795
+ }
796
+ const missingWork = payload.missingWork === null
797
+ ? null
798
+ : {
799
+ summary: payload.missingWork.summary.trim(),
800
+ requiredOutcome: payload.missingWork.requiredOutcome.trim(),
801
+ verification: payload.missingWork.verification.trim(),
802
+ };
803
+ if (payload.action === 'continue_execution') {
804
+ if (!missingWork ||
805
+ !missingWork.summary ||
806
+ !missingWork.requiredOutcome ||
807
+ !missingWork.verification) {
808
+ throw new Error('Final completion reviewer verdict must include a non-empty missingWork payload when action=continue_execution.');
809
+ }
810
+ }
811
+ if (payload.action !== 'continue_execution' && missingWork) {
812
+ throw new Error(`Final completion reviewer verdict cannot include missingWork when action=${payload.action}.`);
813
+ }
814
+ let squashCommitMessage = null;
815
+ if (payload.action === 'accept_complete' && squashCommitMessageValue !== null) {
816
+ try {
817
+ squashCommitMessage = validateReviewerSquashMessageDraft(squashCommitMessageValue, {
818
+ label: 'Final completion reviewer squashCommitMessage',
819
+ });
820
+ }
821
+ catch (error) {
822
+ squashCommitMessage = repairReviewerSquashMessageDraft(squashCommitMessageValue);
823
+ }
824
+ }
825
+ return {
826
+ action: payload.action,
827
+ summary,
828
+ rationale,
829
+ missingWork,
830
+ squashCommitMessage,
831
+ };
832
+ }