@navels/neal 0.1.0 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/neal/adjudicator/planning.js +48 -0
- package/dist/neal/agents/prompts.js +3 -0
- package/dist/neal/agents/rounds.js +8 -0
- package/dist/neal/agents/schemas.js +575 -496
- package/dist/neal/agents/structured-json.js +36 -0
- package/dist/neal/config.js +24 -0
- package/dist/neal/git.js +9 -3
- package/dist/neal/orchestrator/completion.js +167 -112
- package/dist/neal/orchestrator/phases/planning.js +14 -39
- package/dist/neal/orchestrator/split-plan.js +12 -11
- package/dist/neal/orchestrator/transitions.js +30 -71
- package/dist/neal/plan-doc.js +24 -1
- package/dist/neal/prompts/assert-builder.js +8 -1
- package/dist/neal/prompts/execute.js +4 -0
- package/dist/neal/prompts/specialized.js +22 -6
- package/dist/neal/prompts/specs.js +58 -0
- package/dist/neal/providers/anthropic-claude.js +291 -247
- package/dist/neal/providers/generic-agentic.js +18 -0
- package/dist/neal/providers/openai-codex.js +77 -201
- package/dist/neal/providers/openai-compatible.js +33 -5
- package/dist/neal/providers/pricing.js +124 -0
- package/dist/neal/providers/rate-card.js +2301 -0
- package/dist/neal/providers/telemetry.js +4 -0
- package/dist/neal/retrospective.js +33 -4
- package/dist/neal/run-metrics.js +74 -9
- package/docs/compatible-models.md +11 -0
- package/docs/issue-pipeline.md +124 -0
- package/docs/maintenance.md +30 -19
- package/docs/providers.md +117 -0
- package/docs/release.md +29 -25
- package/package.json +7 -3
|
@@ -1,11 +1,8 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
1
2
|
import { repairReviewerSquashMessageDraft, validateReviewerSquashMessageDraft } from '../squash-message.js';
|
|
2
|
-
const
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
'meaningfulProgressAction',
|
|
6
|
-
'meaningfulProgressRationale',
|
|
7
|
-
];
|
|
8
|
-
const REVIEWER_FINDING_KEYS = ['severity', 'files', 'claim', 'evidence', 'requiredAction'];
|
|
3
|
+
export const EXECUTE_SCOPE_PROGRESS_PAYLOAD_START = 'NEAL_PROGRESS_JUSTIFICATION_JSON_START';
|
|
4
|
+
export const EXECUTE_SCOPE_PROGRESS_PAYLOAD_END = 'NEAL_PROGRESS_JUSTIFICATION_JSON_END';
|
|
5
|
+
// --- zod payload definitions (single source of truth) -----------------------
|
|
9
6
|
const REVIEWER_FINDING_SEVERITIES = ['blocking', 'non_blocking'];
|
|
10
7
|
const REVIEWER_MEANINGFUL_PROGRESS_ACTIONS = [
|
|
11
8
|
'accept',
|
|
@@ -13,135 +10,491 @@ const REVIEWER_MEANINGFUL_PROGRESS_ACTIONS = [
|
|
|
13
10
|
'replace_plan',
|
|
14
11
|
'advance_parent',
|
|
15
12
|
];
|
|
16
|
-
|
|
17
|
-
|
|
13
|
+
const EXECUTION_SHAPES = ['one_shot', 'multi_scope', 'multi_scope_unknown'];
|
|
14
|
+
const BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES = [
|
|
15
|
+
'misunderstanding',
|
|
16
|
+
'authorization',
|
|
17
|
+
'external_precondition',
|
|
18
|
+
'impossible_task',
|
|
19
|
+
];
|
|
20
|
+
const CODER_FINDING_RESPONSE_DECISIONS = ['fixed', 'rejected', 'deferred'];
|
|
21
|
+
const CODER_RESPONSE_OUTCOMES = ['responded', 'blocked', 'split_plan'];
|
|
22
|
+
const CODER_BLOCKED_RECOVERY_ACTIONS = [
|
|
23
|
+
'resume_current_scope',
|
|
24
|
+
'replace_current_scope',
|
|
25
|
+
'stay_blocked',
|
|
26
|
+
'terminal_block',
|
|
27
|
+
];
|
|
28
|
+
const CODER_PLAN_RESPONSE_OUTCOMES = ['responded', 'blocked'];
|
|
29
|
+
const CODER_PLAN_ACTIONS = ['ready_for_review', 'blocked'];
|
|
30
|
+
const CODER_SCOPE_ACTIONS = ['continue', 'scope_done', 'done', 'blocked', 'split_plan', 'manual_gate'];
|
|
31
|
+
const MANUAL_GATE_RESUME_CHECK_TYPES = ['command'];
|
|
32
|
+
const MANUAL_GATE_RESUME_CHECK_CWDS = ['repo', 'run_dir'];
|
|
33
|
+
const FINAL_COMPLETION_REVIEWER_ACTIONS = ['accept_complete', 'continue_execution', 'block_for_operator'];
|
|
34
|
+
const SQUASH_COMMIT_MESSAGE_DESCRIPTION = 'Project-facing Git history metadata used only when action is accept_complete; set null for continue_execution or block_for_operator.';
|
|
35
|
+
const SQUASH_COMMIT_SUBJECT_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.';
|
|
36
|
+
const SQUASH_COMMIT_BULLETS_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.';
|
|
37
|
+
// The reviewer payload is the strict unknown-key family: unknown properties
|
|
38
|
+
// are rejected at both the top level and inside findings (z.strictObject).
|
|
39
|
+
// Every other payload uses zod's default strip-mode z.object — unknown
|
|
40
|
+
// properties are accepted and omitted from the normalized output.
|
|
41
|
+
const reviewerFindingSchema = z.strictObject({
|
|
42
|
+
severity: z.enum(REVIEWER_FINDING_SEVERITIES),
|
|
43
|
+
files: z.array(z.string()),
|
|
44
|
+
claim: z.string(),
|
|
45
|
+
evidence: z.string(),
|
|
46
|
+
requiredAction: z.string(),
|
|
47
|
+
});
|
|
48
|
+
const reviewerPayloadSchema = z.strictObject({
|
|
49
|
+
summary: z.string(),
|
|
50
|
+
findings: z.array(reviewerFindingSchema),
|
|
51
|
+
meaningfulProgressAction: z.enum(REVIEWER_MEANINGFUL_PROGRESS_ACTIONS),
|
|
52
|
+
meaningfulProgressRationale: z.string(),
|
|
53
|
+
});
|
|
54
|
+
const planReviewerFindingSchema = z.object({
|
|
55
|
+
severity: z.enum(REVIEWER_FINDING_SEVERITIES),
|
|
56
|
+
files: z.array(z.string()),
|
|
57
|
+
claim: z.string(),
|
|
58
|
+
requiredAction: z.string(),
|
|
59
|
+
});
|
|
60
|
+
const planReviewerPayloadSchema = z.object({
|
|
61
|
+
summary: z.string(),
|
|
62
|
+
executionShape: z.enum(EXECUTION_SHAPES),
|
|
63
|
+
findings: z.array(planReviewerFindingSchema),
|
|
64
|
+
});
|
|
65
|
+
const blockedAdjudicatorPayloadSchema = z.object({
|
|
66
|
+
recoverable: z.boolean(),
|
|
67
|
+
triageCategory: z.enum(BLOCKED_ADJUDICATOR_TRIAGE_CATEGORIES),
|
|
68
|
+
resolutionDirective: z.string(),
|
|
69
|
+
targetCanonicalIds: z.array(z.string()).optional(),
|
|
70
|
+
rationale: z.string(),
|
|
71
|
+
});
|
|
72
|
+
const coderFindingResponseSchema = z.object({
|
|
73
|
+
id: z.string(),
|
|
74
|
+
decision: z.enum(CODER_FINDING_RESPONSE_DECISIONS),
|
|
75
|
+
summary: z.string(),
|
|
76
|
+
});
|
|
77
|
+
const coderResponsePayloadSchema = z.object({
|
|
78
|
+
outcome: z.enum(CODER_RESPONSE_OUTCOMES),
|
|
79
|
+
summary: z.string(),
|
|
80
|
+
blocker: z.string(),
|
|
81
|
+
derivedPlan: z.string(),
|
|
82
|
+
responses: z.array(coderFindingResponseSchema),
|
|
83
|
+
});
|
|
84
|
+
const coderBlockedRecoveryDispositionPayloadSchema = z.object({
|
|
85
|
+
action: z.enum(CODER_BLOCKED_RECOVERY_ACTIONS),
|
|
86
|
+
summary: z.string(),
|
|
87
|
+
rationale: z.string(),
|
|
88
|
+
blocker: z.string(),
|
|
89
|
+
replacementPlan: z.string(),
|
|
90
|
+
});
|
|
91
|
+
const coderPlanResponsePayloadSchema = z.object({
|
|
92
|
+
outcome: z.enum(CODER_PLAN_RESPONSE_OUTCOMES),
|
|
93
|
+
summary: z.string(),
|
|
94
|
+
blocker: z.string(),
|
|
95
|
+
responses: z.array(coderFindingResponseSchema),
|
|
96
|
+
});
|
|
97
|
+
const coderPlanPayloadSchema = z.object({
|
|
98
|
+
action: z.enum(CODER_PLAN_ACTIONS),
|
|
99
|
+
message: z.string(),
|
|
100
|
+
executionShape: z.enum(EXECUTION_SHAPES),
|
|
101
|
+
planBody: z.string(),
|
|
102
|
+
blockedReason: z.string(),
|
|
103
|
+
});
|
|
104
|
+
const executeScopeProgressSchema = z.object({
|
|
105
|
+
milestoneTargeted: z.string(),
|
|
106
|
+
newEvidence: z.string(),
|
|
107
|
+
whyNotRedundant: z.string(),
|
|
108
|
+
nextStepUnlocked: z.string(),
|
|
109
|
+
});
|
|
110
|
+
const manualGateResumeCheckSchema = z.object({
|
|
111
|
+
type: z.enum(MANUAL_GATE_RESUME_CHECK_TYPES),
|
|
112
|
+
name: z.string(),
|
|
113
|
+
command: z.array(z.string()),
|
|
114
|
+
cwd: z.enum(MANUAL_GATE_RESUME_CHECK_CWDS).nullable(),
|
|
115
|
+
timeoutMs: z.number().nullable(),
|
|
116
|
+
});
|
|
117
|
+
const manualGateSchema = z.object({
|
|
118
|
+
id: z.string(),
|
|
119
|
+
title: z.string(),
|
|
120
|
+
reason: z.string(),
|
|
121
|
+
instructionsMarkdown: z.string(),
|
|
122
|
+
resumeChecks: z.array(manualGateResumeCheckSchema),
|
|
123
|
+
});
|
|
124
|
+
const coderScopePayloadSchema = z.object({
|
|
125
|
+
action: z.enum(CODER_SCOPE_ACTIONS),
|
|
126
|
+
message: z.string(),
|
|
127
|
+
progress: executeScopeProgressSchema,
|
|
128
|
+
manualGate: manualGateSchema.nullable(),
|
|
129
|
+
derivedPlan: z.string(),
|
|
130
|
+
blockedReason: z.string(),
|
|
131
|
+
});
|
|
132
|
+
// Parse-stage view of the coder scope payload. The full definition above is
|
|
133
|
+
// what the emitted JSON Schema promises callers; validation historically only
|
|
134
|
+
// checks object-ness of `progress` and `manualGate` up front, then applies
|
|
135
|
+
// the delimiter-protocol progress messages and the manual-gate deep
|
|
136
|
+
// validation (which trims strings and accepts absent cwd/timeoutMs) as
|
|
137
|
+
// explicit post-parse steps with their own pinned message grammar.
|
|
138
|
+
const coderScopeParseSchema = coderScopePayloadSchema.extend({
|
|
139
|
+
progress: z.looseObject({}),
|
|
140
|
+
manualGate: z.looseObject({}).nullable(),
|
|
141
|
+
});
|
|
142
|
+
const finalCompletionSummaryPayloadSchema = z.object({
|
|
143
|
+
planGoalSatisfied: z.boolean(),
|
|
144
|
+
whatChangedOverall: z.string(),
|
|
145
|
+
verificationSummary: z.string(),
|
|
146
|
+
remainingKnownGaps: z.array(z.string()),
|
|
147
|
+
});
|
|
148
|
+
const squashCommitMessageDraftSchema = z.object({
|
|
149
|
+
subject: z.string().describe(SQUASH_COMMIT_SUBJECT_DESCRIPTION),
|
|
150
|
+
bullets: z.array(z.string()).min(2).max(5).describe(SQUASH_COMMIT_BULLETS_DESCRIPTION),
|
|
151
|
+
});
|
|
152
|
+
const finalCompletionReviewerPayloadSchema = z.object({
|
|
153
|
+
action: z.enum(FINAL_COMPLETION_REVIEWER_ACTIONS),
|
|
154
|
+
summary: z.string(),
|
|
155
|
+
rationale: z.string(),
|
|
156
|
+
missingWork: z
|
|
157
|
+
.object({
|
|
158
|
+
summary: z.string(),
|
|
159
|
+
requiredOutcome: z.string(),
|
|
160
|
+
verification: z.string(),
|
|
161
|
+
})
|
|
162
|
+
.nullable(),
|
|
163
|
+
squashCommitMessage: squashCommitMessageDraftSchema.nullable().describe(SQUASH_COMMIT_MESSAGE_DESCRIPTION),
|
|
164
|
+
});
|
|
165
|
+
// Parse-stage view of the final-completion reviewer verdict. The squash draft
|
|
166
|
+
// is advisory: a malformed or absent draft must never reject the verdict, so
|
|
167
|
+
// validation only checks object-ness here and routes the draft through
|
|
168
|
+
// validate/repair in parseFinalCompletionReviewerPayload.
|
|
169
|
+
const finalCompletionReviewerParseSchema = finalCompletionReviewerPayloadSchema.extend({
|
|
170
|
+
squashCommitMessage: z.looseObject({}).nullable().optional(),
|
|
171
|
+
});
|
|
172
|
+
// Canonical key order of the previous hand-written builders. Emitted schemas
|
|
173
|
+
// are serialized into prompt text with JSON.stringify(schema, null, 2), so
|
|
174
|
+
// this ordering changes prompt bytes and is pinned byte-for-byte by
|
|
175
|
+
// test/agent-payload-schemas.test.ts.
|
|
176
|
+
const EMITTED_SCHEMA_KEY_ORDER = [
|
|
177
|
+
'description',
|
|
178
|
+
'type',
|
|
179
|
+
'enum',
|
|
180
|
+
'minItems',
|
|
181
|
+
'maxItems',
|
|
182
|
+
'items',
|
|
183
|
+
'properties',
|
|
184
|
+
'required',
|
|
185
|
+
'additionalProperties',
|
|
186
|
+
];
|
|
187
|
+
function isJsonSchemaRecord(value) {
|
|
188
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value);
|
|
189
|
+
}
|
|
190
|
+
// Collapse zod's `anyOf: [X, { type: 'null' }]` encoding of .nullable() into
|
|
191
|
+
// the hand-written builders' merged form: X with `type: [<X type>, 'null']`,
|
|
192
|
+
// appending null to X's enum when one is present (e.g. the resume-check cwd).
|
|
193
|
+
function collapseNullableAnyOf(node) {
|
|
194
|
+
const { anyOf, ...rest } = node;
|
|
195
|
+
if (!Array.isArray(anyOf) || anyOf.length !== 2) {
|
|
196
|
+
return node;
|
|
197
|
+
}
|
|
198
|
+
const [base, nullBranch] = anyOf;
|
|
199
|
+
if (!isJsonSchemaRecord(base) || !isJsonSchemaRecord(nullBranch)) {
|
|
200
|
+
return node;
|
|
201
|
+
}
|
|
202
|
+
if (nullBranch.type !== 'null' || Object.keys(nullBranch).length !== 1) {
|
|
203
|
+
return node;
|
|
204
|
+
}
|
|
205
|
+
const merged = { ...base, ...rest };
|
|
206
|
+
merged.type = Array.isArray(base.type) ? [...base.type, 'null'] : [base.type, 'null'];
|
|
207
|
+
if (Array.isArray(base.enum)) {
|
|
208
|
+
merged.enum = [...base.enum, null];
|
|
209
|
+
}
|
|
210
|
+
return merged;
|
|
211
|
+
}
|
|
212
|
+
function normalizeEmittedSchemaNode(node) {
|
|
213
|
+
const record = collapseNullableAnyOf(node);
|
|
214
|
+
const keys = Object.keys(record);
|
|
215
|
+
const orderedKeys = [
|
|
216
|
+
...EMITTED_SCHEMA_KEY_ORDER.filter((key) => keys.includes(key)),
|
|
217
|
+
...keys.filter((key) => !EMITTED_SCHEMA_KEY_ORDER.includes(key)),
|
|
218
|
+
];
|
|
219
|
+
const normalized = {};
|
|
220
|
+
for (const key of orderedKeys) {
|
|
221
|
+
const value = record[key];
|
|
222
|
+
if (key === 'items' && isJsonSchemaRecord(value)) {
|
|
223
|
+
normalized[key] = normalizeEmittedSchemaNode(value);
|
|
224
|
+
}
|
|
225
|
+
else if (key === 'properties' && isJsonSchemaRecord(value)) {
|
|
226
|
+
const properties = {};
|
|
227
|
+
for (const [propertyName, propertySchema] of Object.entries(value)) {
|
|
228
|
+
properties[propertyName] = isJsonSchemaRecord(propertySchema)
|
|
229
|
+
? normalizeEmittedSchemaNode(propertySchema)
|
|
230
|
+
: propertySchema;
|
|
231
|
+
}
|
|
232
|
+
normalized[key] = properties;
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
normalized[key] = value;
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
const type = normalized.type;
|
|
239
|
+
const isObjectNode = type === 'object' || (Array.isArray(type) && type.includes('object'));
|
|
240
|
+
if (isObjectNode && normalized.properties !== undefined) {
|
|
241
|
+
normalized.additionalProperties = false;
|
|
242
|
+
}
|
|
243
|
+
return normalized;
|
|
244
|
+
}
|
|
245
|
+
// The declared literal return shape is asserted here rather than proven
|
|
246
|
+
// structurally: the byte-level serialization pins in
|
|
247
|
+
// test/agent-payload-schemas.test.ts fail whenever the zod-derived emission
|
|
248
|
+
// diverges from the declared shapes.
|
|
249
|
+
function emitJsonSchema(schema) {
|
|
250
|
+
const emitted = z.toJSONSchema(schema);
|
|
251
|
+
delete emitted.$schema;
|
|
252
|
+
return normalizeEmittedSchemaNode(emitted);
|
|
253
|
+
}
|
|
18
254
|
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
|
-
};
|
|
255
|
+
return emitJsonSchema(reviewerPayloadSchema);
|
|
44
256
|
}
|
|
45
|
-
export function
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
257
|
+
export function buildPlanReviewerSchema() {
|
|
258
|
+
return emitJsonSchema(planReviewerPayloadSchema);
|
|
259
|
+
}
|
|
260
|
+
export function buildBlockedAdjudicatorSchema() {
|
|
261
|
+
return emitJsonSchema(blockedAdjudicatorPayloadSchema);
|
|
262
|
+
}
|
|
263
|
+
export function buildCoderResponseSchema() {
|
|
264
|
+
return emitJsonSchema(coderResponsePayloadSchema);
|
|
265
|
+
}
|
|
266
|
+
export function buildCoderBlockedRecoveryDispositionSchema() {
|
|
267
|
+
return emitJsonSchema(coderBlockedRecoveryDispositionPayloadSchema);
|
|
268
|
+
}
|
|
269
|
+
export function buildCoderPlanResponseSchema() {
|
|
270
|
+
return emitJsonSchema(coderPlanResponsePayloadSchema);
|
|
271
|
+
}
|
|
272
|
+
export function buildCoderPlanSchema() {
|
|
273
|
+
return emitJsonSchema(coderPlanPayloadSchema);
|
|
274
|
+
}
|
|
275
|
+
export function buildCoderScopeSchema() {
|
|
276
|
+
return emitJsonSchema(coderScopePayloadSchema);
|
|
277
|
+
}
|
|
278
|
+
export function buildExecuteScopeProgressSchema() {
|
|
279
|
+
return emitJsonSchema(executeScopeProgressSchema);
|
|
280
|
+
}
|
|
281
|
+
export function buildFinalCompletionSummarySchema() {
|
|
282
|
+
return emitJsonSchema(finalCompletionSummaryPayloadSchema);
|
|
283
|
+
}
|
|
284
|
+
export function buildFinalCompletionReviewerSchema() {
|
|
285
|
+
return emitJsonSchema(finalCompletionReviewerPayloadSchema);
|
|
286
|
+
}
|
|
287
|
+
// --- sequential zod-backed validation ------------------------------------------
|
|
288
|
+
//
|
|
289
|
+
// The historical validators were strictly sequential: each field was read and
|
|
290
|
+
// checked one at a time in a fixed order, throwing at the FIRST failure, so a
|
|
291
|
+
// property later in the validation order was never read once an earlier check
|
|
292
|
+
// failed — a later throwing accessor could not mask an earlier error. A
|
|
293
|
+
// whole-payload safeParse instead reads every field eagerly to collect all
|
|
294
|
+
// issues. The traversal below therefore keeps the historical sequential read
|
|
295
|
+
// order and short-circuit semantics while zod performs the actual
|
|
296
|
+
// validation: each field or array element is parsed by its own zod
|
|
297
|
+
// sub-schema at the point the traversal reaches it, and the first zod issue
|
|
298
|
+
// is translated through ONE shared formatter into the historical grammar:
|
|
299
|
+
// `<Label>[.<path>] must be a non-null object.` / `... must be a string.` /
|
|
300
|
+
// `... must be a boolean.` / `... must be an array.` /
|
|
301
|
+
// `... must be exactly one of: a, b.`
|
|
302
|
+
function describeSchemaIssue(fieldPath, issue) {
|
|
303
|
+
if (issue.code === 'invalid_type') {
|
|
304
|
+
switch (issue.expected) {
|
|
305
|
+
case 'object':
|
|
306
|
+
return `${fieldPath} must be a non-null object.`;
|
|
307
|
+
case 'string':
|
|
308
|
+
return `${fieldPath} must be a string.`;
|
|
309
|
+
case 'boolean':
|
|
310
|
+
return `${fieldPath} must be a boolean.`;
|
|
311
|
+
case 'array':
|
|
312
|
+
return `${fieldPath} must be an array.`;
|
|
313
|
+
default:
|
|
314
|
+
break;
|
|
315
|
+
}
|
|
50
316
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
};
|
|
317
|
+
if (issue.code === 'invalid_value') {
|
|
318
|
+
return `${fieldPath} must be exactly one of: ${issue.values.join(', ')}.`;
|
|
319
|
+
}
|
|
320
|
+
// Any other zod constraint surfaces with zod's own issue description.
|
|
321
|
+
return `${fieldPath} failed validation: ${issue.message}`;
|
|
57
322
|
}
|
|
58
|
-
function
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
throw new Error(`${label}.files must be an array.`);
|
|
323
|
+
function firstSchemaIssueMessage(schema, value, fieldPath) {
|
|
324
|
+
const result = schema.safeParse(value);
|
|
325
|
+
if (result.success) {
|
|
326
|
+
// Unreachable: callers invoke this only for values the schema rejects.
|
|
327
|
+
return `${fieldPath} failed validation.`;
|
|
64
328
|
}
|
|
65
|
-
return
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
329
|
+
return describeSchemaIssue(fieldPath, result.error.issues[0]);
|
|
330
|
+
}
|
|
331
|
+
// Container gates: return the value when it has the container type the
|
|
332
|
+
// schema demands; otherwise derive the failure message from the schema's own
|
|
333
|
+
// zod issue. zod fails fast on container-type mismatches without reading any
|
|
334
|
+
// property or element, so no accessor is invoked for a rejected value, and
|
|
335
|
+
// the gate lets the traversal defer member reads to their historical
|
|
336
|
+
// positions instead of letting a whole-container parse read them eagerly.
|
|
337
|
+
function requireRecordSequential(schema, value, fieldPath) {
|
|
338
|
+
if (value !== null && typeof value === 'object' && !Array.isArray(value)) {
|
|
339
|
+
return value;
|
|
340
|
+
}
|
|
341
|
+
throw new Error(firstSchemaIssueMessage(schema, value, fieldPath));
|
|
72
342
|
}
|
|
73
|
-
function
|
|
74
|
-
if (
|
|
75
|
-
|
|
343
|
+
function requireArraySequential(schema, value, fieldPath) {
|
|
344
|
+
if (Array.isArray(value)) {
|
|
345
|
+
return value;
|
|
76
346
|
}
|
|
77
|
-
|
|
347
|
+
throw new Error(firstSchemaIssueMessage(schema, value, fieldPath));
|
|
78
348
|
}
|
|
79
|
-
function
|
|
80
|
-
|
|
81
|
-
if (
|
|
349
|
+
function validateFieldSequential(fieldSchema, value, fieldPath) {
|
|
350
|
+
if (fieldSchema instanceof z.ZodOptional) {
|
|
351
|
+
if (value === undefined) {
|
|
352
|
+
return undefined;
|
|
353
|
+
}
|
|
354
|
+
return validateFieldSequential(fieldSchema.unwrap(), value, fieldPath);
|
|
355
|
+
}
|
|
356
|
+
if (fieldSchema instanceof z.ZodNullable) {
|
|
357
|
+
if (value === null) {
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
return validateFieldSequential(fieldSchema.unwrap(), value, fieldPath);
|
|
361
|
+
}
|
|
362
|
+
if (fieldSchema instanceof z.ZodArray) {
|
|
363
|
+
const items = requireArraySequential(fieldSchema, value, fieldPath);
|
|
364
|
+
const element = fieldSchema.element;
|
|
365
|
+
if (element instanceof z.ZodObject) {
|
|
366
|
+
// Historical object-array semantics: every item's object-ness is
|
|
367
|
+
// checked before any item's fields are read.
|
|
368
|
+
const records = items.map((item, index) => requireRecordSequential(element, item, `${fieldPath}[${index}]`));
|
|
369
|
+
return records.map((item, index) => walkObjectSequential(element, item, `${fieldPath}[${index}]`));
|
|
370
|
+
}
|
|
371
|
+
return items.map((item, index) => validateFieldSequential(element, item, `${fieldPath}[${index}]`));
|
|
372
|
+
}
|
|
373
|
+
if (fieldSchema instanceof z.ZodObject) {
|
|
374
|
+
const record = requireRecordSequential(fieldSchema, value, fieldPath);
|
|
375
|
+
// Empty-shape loose objects are the historical object-ness-only
|
|
376
|
+
// passthroughs (coder-scope progress/manualGate, the squash draft): the
|
|
377
|
+
// ORIGINAL reference is returned so the explicit post-parse steps read
|
|
378
|
+
// fields at their historical positions.
|
|
379
|
+
if (Object.keys(fieldSchema.shape).length === 0) {
|
|
380
|
+
return record;
|
|
381
|
+
}
|
|
382
|
+
return walkObjectSequential(fieldSchema, record, fieldPath);
|
|
383
|
+
}
|
|
384
|
+
// Leaf schema: zod itself enforces the constraint at the point the
|
|
385
|
+
// traversal reaches this field; its first issue is translated through the
|
|
386
|
+
// shared formatter.
|
|
387
|
+
const result = fieldSchema.safeParse(value);
|
|
388
|
+
if (!result.success) {
|
|
389
|
+
throw new Error(describeSchemaIssue(fieldPath, result.error.issues[0]));
|
|
390
|
+
}
|
|
391
|
+
return result.data;
|
|
392
|
+
}
|
|
393
|
+
// Walks a strip-mode object schema field-by-field in shape declaration order,
|
|
394
|
+
// reading each property only when its turn comes and throwing at the first
|
|
395
|
+
// failure. Unknown keys are omitted from the rebuilt output (historical
|
|
396
|
+
// permissive behavior).
|
|
397
|
+
function walkObjectSequential(schema, record, basePath) {
|
|
398
|
+
const output = {};
|
|
399
|
+
for (const [key, fieldSchema] of Object.entries(schema.shape)) {
|
|
400
|
+
const fieldPath = `${basePath}.${key}`;
|
|
401
|
+
// Historical optional/nullable field patterns performed one property
|
|
402
|
+
// read for the undefined/null test and a FRESH read for validation when
|
|
403
|
+
// the field was present, so stateful accessors observe both reads (the
|
|
404
|
+
// blocked-adjudicator targetCanonicalIds ternary and the coder-scope
|
|
405
|
+
// manualGate ternary behaved this way).
|
|
406
|
+
if (fieldSchema instanceof z.ZodOptional) {
|
|
407
|
+
if (record[key] === undefined) {
|
|
408
|
+
continue;
|
|
409
|
+
}
|
|
410
|
+
output[key] = validateFieldSequential(fieldSchema.unwrap(), record[key], fieldPath);
|
|
411
|
+
continue;
|
|
412
|
+
}
|
|
413
|
+
if (fieldSchema instanceof z.ZodNullable) {
|
|
414
|
+
if (record[key] === null) {
|
|
415
|
+
output[key] = null;
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
output[key] = validateFieldSequential(fieldSchema.unwrap(), record[key], fieldPath);
|
|
419
|
+
continue;
|
|
420
|
+
}
|
|
421
|
+
output[key] = validateFieldSequential(fieldSchema, record[key], fieldPath);
|
|
422
|
+
}
|
|
423
|
+
return output;
|
|
424
|
+
}
|
|
425
|
+
// Permissive-family entry point: object-ness first (`<Label> must be a
|
|
426
|
+
// non-null object.`), then the sequential field walk in shape declaration
|
|
427
|
+
// order, which matches the historical field-by-field check order of these
|
|
428
|
+
// validators.
|
|
429
|
+
function parsePayload(schema, payload, label) {
|
|
430
|
+
if (!(schema instanceof z.ZodObject)) {
|
|
431
|
+
throw new Error(`${label} failed validation: sequential parsing requires an object schema.`);
|
|
432
|
+
}
|
|
433
|
+
const record = requireRecordSequential(schema, payload, label);
|
|
434
|
+
return walkObjectSequential(schema, record, label);
|
|
435
|
+
}
|
|
436
|
+
// --- reviewer payload (strict unknown-key family) ----------------------------
|
|
437
|
+
// The strict reviewer family additionally enforced, at each object level and
|
|
438
|
+
// BEFORE reading any declared field: required own properties (in key order,
|
|
439
|
+
// via Object.prototype.hasOwnProperty — prototype-inherited properties are
|
|
440
|
+
// rejected and their accessors never invoked) and unknown-key rejection (in
|
|
441
|
+
// input order, via Object.keys). The historical sequence, reproduced below:
|
|
442
|
+
// object-ness, missing required own properties, unknown properties, findings
|
|
443
|
+
// array-ness, summary, each finding in order (object-ness, missing, unknown,
|
|
444
|
+
// files array-ness, severity, files items, claim, evidence, requiredAction),
|
|
445
|
+
// then meaningfulProgressAction and meaningfulProgressRationale.
|
|
446
|
+
function assertReviewerOwnProperties(record, shape, label) {
|
|
447
|
+
for (const key of Object.keys(shape)) {
|
|
448
|
+
if (!Object.prototype.hasOwnProperty.call(record, key)) {
|
|
82
449
|
throw new Error(`${label} is missing required property "${key}".`);
|
|
83
450
|
}
|
|
84
451
|
}
|
|
85
|
-
for (const key of Object.keys(
|
|
86
|
-
if (!
|
|
452
|
+
for (const key of Object.keys(record)) {
|
|
453
|
+
if (!Object.prototype.hasOwnProperty.call(shape, key)) {
|
|
87
454
|
throw new Error(`${label} included unknown property "${key}".`);
|
|
88
455
|
}
|
|
89
456
|
}
|
|
90
457
|
}
|
|
91
|
-
function
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
458
|
+
function validateReviewerFindingSequential(finding, index) {
|
|
459
|
+
const label = `Reviewer payload.findings[${index}]`;
|
|
460
|
+
const record = requireRecordSequential(reviewerFindingSchema, finding, label);
|
|
461
|
+
const shape = reviewerFindingSchema.shape;
|
|
462
|
+
assertReviewerOwnProperties(record, shape, label);
|
|
463
|
+
// Historical order and access pattern: the files array-ness check read
|
|
464
|
+
// `files` before severity (or any other declared field) was read, and the
|
|
465
|
+
// items mapping below RE-READS the property after severity, so a stateful
|
|
466
|
+
// accessor's second read supplies the validated items.
|
|
467
|
+
const filesProbe = record.files;
|
|
468
|
+
if (!Array.isArray(filesProbe)) {
|
|
469
|
+
throw new Error(firstSchemaIssueMessage(shape.files, filesProbe, `${label}.files`));
|
|
100
470
|
}
|
|
101
|
-
return value;
|
|
102
|
-
}
|
|
103
|
-
export function buildPlanReviewerSchema() {
|
|
104
471
|
return {
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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,
|
|
472
|
+
severity: validateFieldSequential(shape.severity, record.severity, `${label}.severity`),
|
|
473
|
+
files: record.files.map((file, fileIndex) => validateFieldSequential(shape.files.element, file, `${label}.files[${fileIndex}]`)),
|
|
474
|
+
claim: validateFieldSequential(shape.claim, record.claim, `${label}.claim`),
|
|
475
|
+
evidence: validateFieldSequential(shape.evidence, record.evidence, `${label}.evidence`),
|
|
476
|
+
requiredAction: validateFieldSequential(shape.requiredAction, record.requiredAction, `${label}.requiredAction`),
|
|
126
477
|
};
|
|
127
478
|
}
|
|
128
|
-
export function
|
|
479
|
+
export function validateReviewerPayload(payload) {
|
|
480
|
+
const record = requireRecordSequential(reviewerPayloadSchema, payload, 'Reviewer payload');
|
|
481
|
+
const shape = reviewerPayloadSchema.shape;
|
|
482
|
+
assertReviewerOwnProperties(record, shape, 'Reviewer payload');
|
|
483
|
+
// Historical access pattern: the array-ness check performs its own read,
|
|
484
|
+
// and the mapping below re-reads the property (after summary), so a
|
|
485
|
+
// stateful accessor's second read is the one validated and normalized.
|
|
486
|
+
const findingsProbe = record.findings;
|
|
487
|
+
if (!Array.isArray(findingsProbe)) {
|
|
488
|
+
throw new Error(firstSchemaIssueMessage(shape.findings, findingsProbe, 'Reviewer payload.findings'));
|
|
489
|
+
}
|
|
129
490
|
return {
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
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,
|
|
491
|
+
summary: validateFieldSequential(shape.summary, record.summary, 'Reviewer payload.summary'),
|
|
492
|
+
findings: record.findings.map((finding, index) => validateReviewerFindingSequential(finding, index)),
|
|
493
|
+
meaningfulProgressAction: validateFieldSequential(shape.meaningfulProgressAction, record.meaningfulProgressAction, 'Reviewer payload.meaningfulProgressAction'),
|
|
494
|
+
meaningfulProgressRationale: validateFieldSequential(shape.meaningfulProgressRationale, record.meaningfulProgressRationale, 'Reviewer payload.meaningfulProgressRationale'),
|
|
143
495
|
};
|
|
144
496
|
}
|
|
497
|
+
// --- permissive validators ----------------------------------------------------
|
|
145
498
|
// Validates a read-only review_stuck arbiter verdict. Exactly one triage
|
|
146
499
|
// category is autonomously recoverable: `misunderstanding` requires
|
|
147
500
|
// recoverable=true plus a non-empty resolutionDirective; the three genuine-wall
|
|
@@ -150,22 +503,15 @@ export function buildBlockedAdjudicatorSchema() {
|
|
|
150
503
|
// triage, or recoverable=false paired with `misunderstanding`) is rejected so a
|
|
151
504
|
// malformed verdict can never drive an autonomous recovery.
|
|
152
505
|
export function validateBlockedAdjudicatorVerdictPayload(rawPayload) {
|
|
153
|
-
const
|
|
506
|
+
const parsed = parsePayload(blockedAdjudicatorPayloadSchema, rawPayload, 'Review-stuck arbiter payload');
|
|
154
507
|
const payload = {
|
|
155
|
-
recoverable:
|
|
156
|
-
triageCategory:
|
|
157
|
-
|
|
158
|
-
'authorization',
|
|
159
|
-
'external_precondition',
|
|
160
|
-
'impossible_task',
|
|
161
|
-
]),
|
|
162
|
-
resolutionDirective: requirePayloadString(value.resolutionDirective, 'Review-stuck arbiter payload.resolutionDirective'),
|
|
508
|
+
recoverable: parsed.recoverable,
|
|
509
|
+
triageCategory: parsed.triageCategory,
|
|
510
|
+
resolutionDirective: parsed.resolutionDirective,
|
|
163
511
|
// The verdict's targetCanonicalIds is optional: a coder/split-plan block has
|
|
164
512
|
// no reviewer findings to point at, so an absent value defaults to [].
|
|
165
|
-
targetCanonicalIds:
|
|
166
|
-
|
|
167
|
-
: requirePayloadStringArray(value.targetCanonicalIds, 'Review-stuck arbiter payload.targetCanonicalIds'),
|
|
168
|
-
rationale: requirePayloadString(value.rationale, 'Review-stuck arbiter payload.rationale'),
|
|
513
|
+
targetCanonicalIds: parsed.targetCanonicalIds ?? [],
|
|
514
|
+
rationale: parsed.rationale,
|
|
169
515
|
};
|
|
170
516
|
if (!payload.rationale.trim()) {
|
|
171
517
|
throw new Error('Review-stuck arbiter returned an empty rationale.');
|
|
@@ -184,222 +530,21 @@ export function validateBlockedAdjudicatorVerdictPayload(rawPayload) {
|
|
|
184
530
|
return payload;
|
|
185
531
|
}
|
|
186
532
|
export function validatePlanReviewerPayload(payload) {
|
|
187
|
-
const
|
|
533
|
+
const parsed = parsePayload(planReviewerPayloadSchema, payload, 'Plan reviewer payload');
|
|
188
534
|
return {
|
|
189
|
-
summary:
|
|
190
|
-
executionShape:
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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`),
|
|
535
|
+
summary: parsed.summary,
|
|
536
|
+
executionShape: parsed.executionShape,
|
|
537
|
+
findings: parsed.findings.map((finding) => ({
|
|
538
|
+
severity: finding.severity,
|
|
539
|
+
files: finding.files,
|
|
540
|
+
claim: finding.claim,
|
|
541
|
+
// Plan-review findings carry no evidence field; the normalized finding
|
|
542
|
+
// always forces an empty string.
|
|
202
543
|
evidence: '',
|
|
203
|
-
requiredAction:
|
|
544
|
+
requiredAction: finding.requiredAction,
|
|
204
545
|
})),
|
|
205
546
|
};
|
|
206
547
|
}
|
|
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
548
|
function parseJsonPayload(raw, label) {
|
|
404
549
|
try {
|
|
405
550
|
return JSON.parse(raw);
|
|
@@ -435,54 +580,15 @@ function extractDelimitedPayload(raw, label) {
|
|
|
435
580
|
};
|
|
436
581
|
}
|
|
437
582
|
function requireNonEmptyString(value, field, label) {
|
|
438
|
-
|
|
583
|
+
// Base string-ness is enforced by the field's zod node from
|
|
584
|
+
// executeScopeProgressSchema; the non-empty rule, trimming, and the pinned
|
|
585
|
+
// delimiter-protocol message (shared by missing, wrong-typed, and blank
|
|
586
|
+
// values) are runtime semantics layered on top.
|
|
587
|
+
const parsed = executeScopeProgressSchema.shape[field].safeParse(value);
|
|
588
|
+
if (!parsed.success || parsed.data.trim().length === 0) {
|
|
439
589
|
throw new Error(`${label} returned an empty or missing ${field} field in the progress-justification payload.`);
|
|
440
590
|
}
|
|
441
|
-
return
|
|
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
|
-
};
|
|
591
|
+
return parsed.data.trim();
|
|
486
592
|
}
|
|
487
593
|
// Legacy marker-protocol compatibility for active legacy_marker_v1 execution
|
|
488
594
|
// sessions. New primary execution sessions use CoderScopePayload instead.
|
|
@@ -507,18 +613,7 @@ export function stripExecuteScopeProgressPayload(raw) {
|
|
|
507
613
|
return before || after;
|
|
508
614
|
}
|
|
509
615
|
export function validateCoderResponsePayload(rawPayload) {
|
|
510
|
-
const
|
|
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
|
-
};
|
|
616
|
+
const payload = parsePayload(coderResponsePayloadSchema, rawPayload, 'Coder response round payload');
|
|
522
617
|
const derivedPlan = payload.derivedPlan?.trim() ?? '';
|
|
523
618
|
if (payload.outcome === 'split_plan' && !derivedPlan) {
|
|
524
619
|
throw new Error('Coder response round returned outcome=split_plan without a derivedPlan payload.');
|
|
@@ -529,33 +624,10 @@ export function validateCoderResponsePayload(rawPayload) {
|
|
|
529
624
|
return payload;
|
|
530
625
|
}
|
|
531
626
|
export function validateCoderPlanResponsePayload(payload) {
|
|
532
|
-
|
|
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
|
-
};
|
|
627
|
+
return parsePayload(coderPlanResponsePayloadSchema, payload, 'Planner plan-response round payload');
|
|
542
628
|
}
|
|
543
629
|
export function validateCoderPlanPayload(rawPayload) {
|
|
544
|
-
const
|
|
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
|
-
};
|
|
630
|
+
const payload = parsePayload(coderPlanPayloadSchema, rawPayload, 'Planner plan round payload');
|
|
559
631
|
const planBody = payload.planBody.trim();
|
|
560
632
|
const blockedReason = payload.blockedReason.trim();
|
|
561
633
|
if (payload.action === 'ready_for_review' && !planBody) {
|
|
@@ -573,23 +645,14 @@ export function validateCoderPlanPayload(rawPayload) {
|
|
|
573
645
|
};
|
|
574
646
|
}
|
|
575
647
|
export function validateCoderScopePayload(rawPayload) {
|
|
576
|
-
const
|
|
648
|
+
const parsed = parsePayload(coderScopeParseSchema, rawPayload, 'Coder scope round payload');
|
|
577
649
|
const payload = {
|
|
578
|
-
action:
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
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'),
|
|
650
|
+
action: parsed.action,
|
|
651
|
+
message: parsed.message,
|
|
652
|
+
progress: parsed.progress,
|
|
653
|
+
manualGate: parsed.manualGate,
|
|
654
|
+
derivedPlan: parsed.derivedPlan,
|
|
655
|
+
blockedReason: parsed.blockedReason,
|
|
593
656
|
};
|
|
594
657
|
const progress = {
|
|
595
658
|
milestoneTargeted: requireNonEmptyString(payload.progress?.milestoneTargeted, 'milestoneTargeted', 'Coder scope round'),
|
|
@@ -622,10 +685,10 @@ export function validateCoderScopePayload(rawPayload) {
|
|
|
622
685
|
if (blockedReason) {
|
|
623
686
|
throw new Error('Coder scope round returned a blockedReason payload with action=manual_gate.');
|
|
624
687
|
}
|
|
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');
|
|
688
|
+
const id = requireNonEmptyManualGateString(manualGateSchema.shape.id, manualGate.id, 'manualGate.id');
|
|
689
|
+
const title = requireNonEmptyManualGateString(manualGateSchema.shape.title, manualGate.title, 'manualGate.title');
|
|
690
|
+
const reason = requireNonEmptyManualGateString(manualGateSchema.shape.reason, manualGate.reason, 'manualGate.reason');
|
|
691
|
+
const instructionsMarkdown = requireNonEmptyManualGateString(manualGateSchema.shape.instructionsMarkdown, manualGate.instructionsMarkdown, 'manualGate.instructionsMarkdown');
|
|
629
692
|
const resumeChecks = validateManualGateResumeChecks(manualGate.resumeChecks);
|
|
630
693
|
return {
|
|
631
694
|
...payload,
|
|
@@ -649,39 +712,72 @@ export function validateCoderScopePayload(rawPayload) {
|
|
|
649
712
|
progress,
|
|
650
713
|
};
|
|
651
714
|
}
|
|
652
|
-
|
|
653
|
-
|
|
715
|
+
// Base-constraint nodes for the resume-check cwd/timeoutMs fields, unwrapped
|
|
716
|
+
// from the emitted schema's nullable wrappers. Runtime optionality
|
|
717
|
+
// intentionally differs from the emitted schema's requiredness (the runtime
|
|
718
|
+
// validator accepts absence where the emitted schema requires the key), and
|
|
719
|
+
// the historical validator performed ONE property read per comparison and
|
|
720
|
+
// re-read the property for the normalized output — stateful accessors
|
|
721
|
+
// legitimately observe multiple short-circuited reads. The membership
|
|
722
|
+
// literals are therefore destructured from the zod enum node (one comparison
|
|
723
|
+
// per option, per read, as historically), and number-ness parses through the
|
|
724
|
+
// zod number node at its historical read position.
|
|
725
|
+
const resumeCheckCwdOptions = manualGateResumeCheckSchema.shape.cwd.unwrap().options;
|
|
726
|
+
const resumeCheckTimeoutMsNumberSchema = manualGateResumeCheckSchema.shape.timeoutMs.unwrap();
|
|
727
|
+
function requireNonEmptyManualGateString(fieldSchema, value, fieldPath) {
|
|
728
|
+
// Base string-ness is enforced by the field's zod node; the non-empty rule,
|
|
729
|
+
// trimming, and the pinned message (shared by missing, wrong-typed, and
|
|
730
|
+
// blank values) are runtime semantics layered on top.
|
|
731
|
+
const parsed = fieldSchema.safeParse(value);
|
|
732
|
+
if (!parsed.success || typeof parsed.data !== 'string' || parsed.data.trim().length === 0) {
|
|
654
733
|
throw new Error(`Coder scope round returned an empty or missing ${fieldPath} field.`);
|
|
655
734
|
}
|
|
656
|
-
return
|
|
735
|
+
return parsed.data.trim();
|
|
657
736
|
}
|
|
658
737
|
function validateManualGateResumeChecks(value) {
|
|
738
|
+
// The at-least-one-command rule is runtime-only semantics (the emitted
|
|
739
|
+
// schema does not constrain resumeChecks length), and its pinned message
|
|
740
|
+
// covers the array-ness base constraint too.
|
|
659
741
|
if (!Array.isArray(value) || value.length === 0) {
|
|
660
742
|
throw new Error('Coder scope round returned manualGate.resumeChecks without at least one command check.');
|
|
661
743
|
}
|
|
744
|
+
const checkShape = manualGateResumeCheckSchema.shape;
|
|
662
745
|
return value.map((check, index) => {
|
|
663
746
|
const fieldPath = `manualGate.resumeChecks[${index}]`;
|
|
747
|
+
// Historical container gate with its pinned message; array items
|
|
748
|
+
// intentionally fall through to the type check below, exactly as the
|
|
749
|
+
// old typeof-based gate behaved.
|
|
664
750
|
if (check === null || typeof check !== 'object') {
|
|
665
751
|
throw new Error(`Coder scope round returned invalid ${fieldPath}.`);
|
|
666
752
|
}
|
|
667
753
|
const candidate = check;
|
|
668
|
-
if (candidate.type
|
|
754
|
+
if (!checkShape.type.safeParse(candidate.type).success) {
|
|
669
755
|
throw new Error(`Coder scope round returned ${fieldPath}.type that is not "command".`);
|
|
670
756
|
}
|
|
671
|
-
const name = requireNonEmptyManualGateString(candidate.name, `${fieldPath}.name`);
|
|
757
|
+
const name = requireNonEmptyManualGateString(checkShape.name, candidate.name, `${fieldPath}.name`);
|
|
758
|
+
// Emptiness is runtime-only semantics with a pinned message that also
|
|
759
|
+
// covers array-ness; each part's base string-ness routes through the
|
|
760
|
+
// command element's zod node below.
|
|
672
761
|
if (!Array.isArray(candidate.command) || candidate.command.length === 0) {
|
|
673
762
|
throw new Error(`Coder scope round returned ${fieldPath}.command without a non-empty string array.`);
|
|
674
763
|
}
|
|
675
|
-
const command = candidate.command.map((part, commandIndex) => requireNonEmptyManualGateString(part, `${fieldPath}.command[${commandIndex}]`));
|
|
764
|
+
const command = candidate.command.map((part, commandIndex) => requireNonEmptyManualGateString(checkShape.command.element, part, `${fieldPath}.command[${commandIndex}]`));
|
|
765
|
+
// Historical access semantics preserved exactly: one property read per
|
|
766
|
+
// comparison with short-circuiting, and fresh reads for the normalized
|
|
767
|
+
// output below. The undefined/null comparisons carry the runtime
|
|
768
|
+
// optionality; membership tests one zod enum option per read; the
|
|
769
|
+
// positive-safe-integer rule stays explicit runtime semantics.
|
|
676
770
|
if (candidate.cwd !== undefined &&
|
|
677
771
|
candidate.cwd !== null &&
|
|
678
|
-
candidate.cwd !==
|
|
679
|
-
candidate.cwd !==
|
|
772
|
+
candidate.cwd !== resumeCheckCwdOptions[0] &&
|
|
773
|
+
candidate.cwd !== resumeCheckCwdOptions[1]) {
|
|
680
774
|
throw new Error(`Coder scope round returned ${fieldPath}.cwd that is not "repo" or "run_dir".`);
|
|
681
775
|
}
|
|
682
776
|
if (candidate.timeoutMs !== undefined &&
|
|
683
777
|
candidate.timeoutMs !== null &&
|
|
684
|
-
(
|
|
778
|
+
(!resumeCheckTimeoutMsNumberSchema.safeParse(candidate.timeoutMs).success ||
|
|
779
|
+
!Number.isSafeInteger(candidate.timeoutMs) ||
|
|
780
|
+
candidate.timeoutMs < 1)) {
|
|
685
781
|
throw new Error(`Coder scope round returned ${fieldPath}.timeoutMs that is not a positive safe integer.`);
|
|
686
782
|
}
|
|
687
783
|
return {
|
|
@@ -693,23 +789,8 @@ function validateManualGateResumeChecks(value) {
|
|
|
693
789
|
};
|
|
694
790
|
});
|
|
695
791
|
}
|
|
696
|
-
function nonEmptyTrimmedItems(items) {
|
|
697
|
-
return items.map((item) => item.trim()).filter((item) => item.length > 0);
|
|
698
|
-
}
|
|
699
792
|
export function validateCoderBlockedRecoveryDispositionPayload(rawPayload) {
|
|
700
|
-
const
|
|
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
|
-
};
|
|
793
|
+
const payload = parsePayload(coderBlockedRecoveryDispositionPayloadSchema, rawPayload, 'Coder blocked-recovery payload');
|
|
713
794
|
const blocker = payload.blocker.trim();
|
|
714
795
|
const replacementPlan = payload.replacementPlan.trim();
|
|
715
796
|
if (payload.action === 'replace_current_scope' && !replacementPlan) {
|
|
@@ -724,13 +805,7 @@ export function validateCoderBlockedRecoveryDispositionPayload(rawPayload) {
|
|
|
724
805
|
return payload;
|
|
725
806
|
}
|
|
726
807
|
export function parseFinalCompletionSummaryPayload(rawPayload) {
|
|
727
|
-
const
|
|
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
|
-
};
|
|
808
|
+
const payload = parsePayload(finalCompletionSummaryPayloadSchema, rawPayload, 'Final completion summary payload');
|
|
734
809
|
const whatChangedOverall = payload.whatChangedOverall.trim();
|
|
735
810
|
const verificationSummary = payload.verificationSummary.trim();
|
|
736
811
|
const remainingKnownGaps = payload.remainingKnownGaps
|
|
@@ -756,37 +831,41 @@ export function parseFinalCompletionSummaryPayload(rawPayload) {
|
|
|
756
831
|
};
|
|
757
832
|
}
|
|
758
833
|
export function parseFinalCompletionReviewerPayload(rawPayload) {
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
const
|
|
766
|
-
const
|
|
834
|
+
// Historical sequential order (pinned by test/agent-payload-schemas.test.ts,
|
|
835
|
+
// a deliberate deviation from shape order): action, missingWork
|
|
836
|
+
// object-ness, summary, rationale, missingWork nested fields, then
|
|
837
|
+
// squashCommitMessage object-ness. Each step short-circuits before any
|
|
838
|
+
// later property is read.
|
|
839
|
+
const label = 'Final completion reviewer verdict payload';
|
|
840
|
+
const record = requireRecordSequential(finalCompletionReviewerParseSchema, rawPayload, label);
|
|
841
|
+
const shape = finalCompletionReviewerParseSchema.shape;
|
|
842
|
+
const missingWorkSchema = finalCompletionReviewerPayloadSchema.shape.missingWork.unwrap();
|
|
843
|
+
const action = validateFieldSequential(shape.action, record.action, `${label}.action`);
|
|
844
|
+
const missingWorkValue = record.missingWork;
|
|
845
|
+
const missingWorkRecord = missingWorkValue === null
|
|
767
846
|
? null
|
|
768
|
-
:
|
|
847
|
+
: requireRecordSequential(missingWorkSchema, missingWorkValue, `${label}.missingWork`);
|
|
848
|
+
const summaryValue = validateFieldSequential(shape.summary, record.summary, `${label}.summary`);
|
|
849
|
+
const rationaleValue = validateFieldSequential(shape.rationale, record.rationale, `${label}.rationale`);
|
|
769
850
|
const payload = {
|
|
770
851
|
action,
|
|
771
|
-
summary:
|
|
772
|
-
rationale:
|
|
773
|
-
missingWork:
|
|
852
|
+
summary: summaryValue,
|
|
853
|
+
rationale: rationaleValue,
|
|
854
|
+
missingWork: missingWorkRecord === null
|
|
774
855
|
? null
|
|
775
|
-
: {
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
856
|
+
: walkObjectSequential(missingWorkSchema, missingWorkRecord, `${label}.missingWork`),
|
|
857
|
+
// Historical access pattern: the null test, the undefined test, and the
|
|
858
|
+
// object-ness validation each read the property (three reads for a
|
|
859
|
+
// present draft, short-circuiting for null). Only object-ness is checked
|
|
860
|
+
// here — via the parse-variant zod node — because the advisory draft
|
|
861
|
+
// routes through the validate/repair post-parse step below.
|
|
862
|
+
squashCommitMessage: (record.squashCommitMessage === null || record.squashCommitMessage === undefined
|
|
781
863
|
? null
|
|
782
|
-
:
|
|
864
|
+
: requireRecordSequential(shape.squashCommitMessage, record.squashCommitMessage, `${label}.squashCommitMessage`)),
|
|
783
865
|
};
|
|
784
866
|
const summary = payload.summary.trim();
|
|
785
867
|
const rationale = payload.rationale.trim();
|
|
786
868
|
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
869
|
if (!summary) {
|
|
791
870
|
throw new Error('Final completion reviewer verdict returned an empty summary field.');
|
|
792
871
|
}
|
|
@@ -818,7 +897,7 @@ export function parseFinalCompletionReviewerPayload(rawPayload) {
|
|
|
818
897
|
label: 'Final completion reviewer squashCommitMessage',
|
|
819
898
|
});
|
|
820
899
|
}
|
|
821
|
-
catch
|
|
900
|
+
catch {
|
|
822
901
|
squashCommitMessage = repairReviewerSquashMessageDraft(squashCommitMessageValue);
|
|
823
902
|
}
|
|
824
903
|
}
|