@henryqw/pi-subagent 18.0.1 → 22.0.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/CONTEXT.md +76 -24
- package/README.md +116 -142
- package/dist/composition.d.ts +11 -0
- package/dist/composition.js +107 -0
- package/dist/direct-herdr.d.ts +17 -0
- package/dist/direct-herdr.js +219 -0
- package/dist/ephemeral.d.ts +2 -2
- package/dist/ephemeral.js +7 -7
- package/dist/git-runtime.d.ts +110 -0
- package/dist/git-runtime.js +472 -0
- package/dist/herdr-runtime.d.ts +137 -0
- package/dist/herdr-runtime.js +1228 -0
- package/dist/index.d.ts +11 -5
- package/dist/index.js +20 -16
- package/dist/integration-git.d.ts +59 -0
- package/dist/integration-git.js +484 -0
- package/dist/launch-runtime.d.ts +54 -0
- package/dist/launch-runtime.js +195 -0
- package/dist/process.d.ts +9 -0
- package/dist/process.js +11 -0
- package/dist/runner.d.ts +356 -0
- package/dist/runner.js +2710 -0
- package/dist/schema.d.ts +2234 -0
- package/dist/schema.js +1236 -0
- package/dist/store.d.ts +50 -0
- package/dist/store.js +288 -0
- package/dist/working-evidence.d.ts +25 -0
- package/dist/working-evidence.js +261 -0
- package/docs/adr/001-composable-ephemeral-execution.md +3 -3
- package/docs/delegation-routing.html +89 -0
- package/docs/delegation-routing.svg +78 -0
- package/docs/orchestration-architecture.html +238 -0
- package/docs/orchestration-architecture.svg +189 -0
- package/docs/orchestration.md +122 -331
- package/example.png +0 -0
- package/examples/roles/implementer.md +2 -3
- package/extensions/admission.ts +66 -0
- package/extensions/config.ts +51 -37
- package/extensions/isolated.ts +638 -0
- package/extensions/result-transport.ts +13 -103
- package/extensions/role-tools.ts +11 -11
- package/extensions/subagent-command.ts +167 -0
- package/extensions/subagent.ts +264 -540
- package/extensions/task-name.ts +1 -2
- package/extensions/workflow.ts +60 -96
- package/package.json +14 -6
- package/skills/pi-subagent/SKILL.md +35 -0
package/dist/schema.js
ADDED
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
import { isAbsolute } from "node:path";
|
|
2
|
+
import { parseRoleName } from "./index.js";
|
|
3
|
+
import { Type } from "typebox";
|
|
4
|
+
import { Check, Errors } from "typebox/value";
|
|
5
|
+
export const RUN_STATE_VERSION = 5;
|
|
6
|
+
export const MAX_TASKS = 8;
|
|
7
|
+
export const MAX_EXECUTE_REQUEST_BYTES = 256 * 1024;
|
|
8
|
+
export const MAX_PERSISTED_RUNTIME_TEXT_BYTES = 8 * 1024;
|
|
9
|
+
export const MAX_POSSIBLE_RESOURCES = 32;
|
|
10
|
+
const ID_PATTERN = "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$";
|
|
11
|
+
const OID_PATTERN = "^(?:[0-9a-f]{40}|[0-9a-f]{64})$";
|
|
12
|
+
const TOKEN_PATTERN = "^[A-Za-z0-9_-]{16,128}$";
|
|
13
|
+
const IdSchema = Type.String({ minLength: 1, maxLength: 80, pattern: ID_PATTERN });
|
|
14
|
+
const TextSchema = Type.String({ minLength: 1, maxLength: 32_000 });
|
|
15
|
+
const RoleNameSchema = Type.String({ minLength: 1, maxLength: 32_000 });
|
|
16
|
+
const RuntimeTextSchema = Type.String({ minLength: 1, maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES });
|
|
17
|
+
const OptionalRuntimeTextSchema = Type.Optional(Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }));
|
|
18
|
+
const TimestampSchema = Type.Integer({ minimum: 0 });
|
|
19
|
+
const ModelClassSchema = Type.Union([
|
|
20
|
+
Type.Literal("fast"),
|
|
21
|
+
Type.Literal("balanced"),
|
|
22
|
+
Type.Literal("frontier"),
|
|
23
|
+
Type.Literal("fav"),
|
|
24
|
+
]);
|
|
25
|
+
export const CheckCommandSchema = Type.Object({
|
|
26
|
+
command: TextSchema,
|
|
27
|
+
args: Type.Array(Type.String({ maxLength: 16_000 }), { maxItems: 128 }),
|
|
28
|
+
}, { additionalProperties: false });
|
|
29
|
+
export const JudgmentSchema = Type.Object({
|
|
30
|
+
role: RoleNameSchema,
|
|
31
|
+
modelClass: ModelClassSchema,
|
|
32
|
+
criterion: TextSchema,
|
|
33
|
+
}, { additionalProperties: false });
|
|
34
|
+
const TaskRequestBaseFields = {
|
|
35
|
+
role: RoleNameSchema,
|
|
36
|
+
modelClass: ModelClassSchema,
|
|
37
|
+
requirements: TextSchema,
|
|
38
|
+
deliverable: TextSchema,
|
|
39
|
+
dependsOn: Type.Array(IdSchema, { maxItems: MAX_TASKS }),
|
|
40
|
+
contextFrom: Type.Array(IdSchema, { maxItems: MAX_TASKS }),
|
|
41
|
+
};
|
|
42
|
+
const TextTaskRequestSchema = Type.Object({
|
|
43
|
+
id: IdSchema,
|
|
44
|
+
kind: Type.Literal("text"),
|
|
45
|
+
...TaskRequestBaseFields,
|
|
46
|
+
}, { additionalProperties: false });
|
|
47
|
+
const ChangesetTaskRequestSchema = Type.Object({
|
|
48
|
+
id: IdSchema,
|
|
49
|
+
kind: Type.Literal("changeset"),
|
|
50
|
+
...TaskRequestBaseFields,
|
|
51
|
+
checks: Type.Array(CheckCommandSchema, { minItems: 1, maxItems: 32 }),
|
|
52
|
+
judgment: Type.Optional(JudgmentSchema),
|
|
53
|
+
}, { additionalProperties: false });
|
|
54
|
+
export const TaskRequestSchema = Type.Union([TextTaskRequestSchema, ChangesetTaskRequestSchema]);
|
|
55
|
+
export const ExecuteRequestSchema = Type.Object({
|
|
56
|
+
mode: Type.Literal("isolated"),
|
|
57
|
+
id: IdSchema,
|
|
58
|
+
goal: TextSchema,
|
|
59
|
+
tasks: Type.Array(TaskRequestSchema, { minItems: 1, maxItems: MAX_TASKS }),
|
|
60
|
+
finalChecks: Type.Optional(Type.Array(CheckCommandSchema, { maxItems: 32 })),
|
|
61
|
+
finalJudgment: Type.Optional(JudgmentSchema),
|
|
62
|
+
}, { additionalProperties: false });
|
|
63
|
+
export const IdOnlySchema = Type.Object({ id: IdSchema }, { additionalProperties: false });
|
|
64
|
+
export const ResumeRequestSchema = Type.Union([
|
|
65
|
+
Type.Object({ id: IdSchema, action: Type.Literal("retry"), taskId: IdSchema }, { additionalProperties: false }),
|
|
66
|
+
Type.Object({ id: IdSchema, action: Type.Literal("verify"), taskId: IdSchema }, { additionalProperties: false }),
|
|
67
|
+
Type.Object({ id: IdSchema, action: Type.Literal("finalize") }, { additionalProperties: false }),
|
|
68
|
+
]);
|
|
69
|
+
const AllocationLifecycleFields = {
|
|
70
|
+
generation: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
71
|
+
token: Type.String({ pattern: TOKEN_PATTERN }),
|
|
72
|
+
status: Type.Union([
|
|
73
|
+
Type.Literal("allocating"), Type.Literal("owned"), Type.Literal("absent"), Type.Literal("unknown"),
|
|
74
|
+
]),
|
|
75
|
+
possibleResources: Type.Optional(Type.Array(RuntimeTextSchema, { maxItems: MAX_POSSIBLE_RESOURCES })),
|
|
76
|
+
failure: OptionalRuntimeTextSchema,
|
|
77
|
+
};
|
|
78
|
+
const WorktreeRecordSchema = Type.Object({
|
|
79
|
+
path: RuntimeTextSchema,
|
|
80
|
+
cwd: RuntimeTextSchema,
|
|
81
|
+
branch: RuntimeTextSchema,
|
|
82
|
+
repoRoot: RuntimeTextSchema,
|
|
83
|
+
baseCommit: Type.String({ pattern: OID_PATTERN }),
|
|
84
|
+
}, { additionalProperties: false });
|
|
85
|
+
const WorktreeAllocationIntentSchema = Type.Object({
|
|
86
|
+
kind: Type.Literal("worktree"),
|
|
87
|
+
...AllocationLifecycleFields,
|
|
88
|
+
worktree: Type.Optional(WorktreeRecordSchema),
|
|
89
|
+
}, { additionalProperties: false });
|
|
90
|
+
const WorkspaceAllocationIntentSchema = Type.Object({
|
|
91
|
+
kind: Type.Literal("workspace"),
|
|
92
|
+
...AllocationLifecycleFields,
|
|
93
|
+
label: RuntimeTextSchema,
|
|
94
|
+
worktreeCwd: RuntimeTextSchema,
|
|
95
|
+
mainRoot: RuntimeTextSchema,
|
|
96
|
+
repoKey: RuntimeTextSchema,
|
|
97
|
+
herdrRepoRoot: RuntimeTextSchema,
|
|
98
|
+
workspaceId: Type.Optional(RuntimeTextSchema),
|
|
99
|
+
rootTabId: Type.Optional(RuntimeTextSchema),
|
|
100
|
+
rootPaneId: Type.Optional(RuntimeTextSchema),
|
|
101
|
+
}, { additionalProperties: false });
|
|
102
|
+
const WorkerTabAllocationIntentSchema = Type.Object({
|
|
103
|
+
kind: Type.Literal("worker_tab"),
|
|
104
|
+
...AllocationLifecycleFields,
|
|
105
|
+
label: RuntimeTextSchema,
|
|
106
|
+
workspaceId: RuntimeTextSchema,
|
|
107
|
+
workspaceRootTabId: RuntimeTextSchema,
|
|
108
|
+
workspaceRootPaneId: RuntimeTextSchema,
|
|
109
|
+
worktreeCwd: RuntimeTextSchema,
|
|
110
|
+
leasePath: RuntimeTextSchema,
|
|
111
|
+
tabId: Type.Optional(RuntimeTextSchema),
|
|
112
|
+
paneId: Type.Optional(RuntimeTextSchema),
|
|
113
|
+
}, { additionalProperties: false });
|
|
114
|
+
const AgentAllocationIntentSchema = Type.Object({
|
|
115
|
+
kind: Type.Literal("agent"),
|
|
116
|
+
...AllocationLifecycleFields,
|
|
117
|
+
agentName: RuntimeTextSchema,
|
|
118
|
+
workspaceId: RuntimeTextSchema,
|
|
119
|
+
tabId: RuntimeTextSchema,
|
|
120
|
+
paneId: RuntimeTextSchema,
|
|
121
|
+
worktreeCwd: RuntimeTextSchema,
|
|
122
|
+
leasePath: RuntimeTextSchema,
|
|
123
|
+
}, { additionalProperties: false });
|
|
124
|
+
export const AllocationIntentSchema = Type.Union([
|
|
125
|
+
WorktreeAllocationIntentSchema,
|
|
126
|
+
WorkspaceAllocationIntentSchema,
|
|
127
|
+
WorkerTabAllocationIntentSchema,
|
|
128
|
+
AgentAllocationIntentSchema,
|
|
129
|
+
]);
|
|
130
|
+
export const CLEANUP_KINDS = ["worker_tab", "workspace", "worktree", "branch"];
|
|
131
|
+
const WorkspaceSchema = Type.Object({
|
|
132
|
+
branch: RuntimeTextSchema,
|
|
133
|
+
head: Type.String({ pattern: OID_PATTERN }),
|
|
134
|
+
index: Type.String({ pattern: OID_PATTERN }),
|
|
135
|
+
tree: Type.String({ pattern: OID_PATTERN }),
|
|
136
|
+
}, { additionalProperties: false });
|
|
137
|
+
export const StageRequestSchema = Type.Object({
|
|
138
|
+
id: IdSchema,
|
|
139
|
+
action: Type.Union([Type.Literal("stage"), Type.Literal("resolve"), Type.Literal("reject"), Type.Literal("revise")]),
|
|
140
|
+
generation: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
141
|
+
taskId: IdSchema,
|
|
142
|
+
attempt: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
143
|
+
candidate: WorkspaceSchema,
|
|
144
|
+
expectedTip: WorkspaceSchema,
|
|
145
|
+
instruction: Type.Optional(TextSchema),
|
|
146
|
+
}, { additionalProperties: false });
|
|
147
|
+
export const IntegrationActionSchema = Type.Union([
|
|
148
|
+
Type.Object({
|
|
149
|
+
id: IdSchema,
|
|
150
|
+
generation: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
151
|
+
action: Type.Literal("refresh"),
|
|
152
|
+
expectedTip: WorkspaceSchema,
|
|
153
|
+
expectedMain: WorkspaceSchema,
|
|
154
|
+
newMain: WorkspaceSchema,
|
|
155
|
+
}, { additionalProperties: false }),
|
|
156
|
+
Type.Object({
|
|
157
|
+
id: IdSchema,
|
|
158
|
+
generation: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
159
|
+
action: Type.Union([Type.Literal("advance"), Type.Literal("validate"), Type.Literal("correct"), Type.Literal("promote"), Type.Literal("reconcile"), Type.Literal("cleanup")]),
|
|
160
|
+
expectedTip: WorkspaceSchema,
|
|
161
|
+
}, { additionalProperties: false }),
|
|
162
|
+
Type.Object({
|
|
163
|
+
id: IdSchema,
|
|
164
|
+
generation: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
165
|
+
action: Type.Literal("release"),
|
|
166
|
+
expectedTip: WorkspaceSchema,
|
|
167
|
+
taskId: Type.Optional(IdSchema),
|
|
168
|
+
attempt: Type.Optional(Type.Integer({ minimum: 1, maximum: 2 })),
|
|
169
|
+
}, { additionalProperties: false }),
|
|
170
|
+
]);
|
|
171
|
+
export function parseIntegrationAction(value) {
|
|
172
|
+
if (!Check(IntegrationActionSchema, value))
|
|
173
|
+
throw schemaValidationError("subagent_integrate requires an exact generation and tip", Errors(IntegrationActionSchema, value));
|
|
174
|
+
return value;
|
|
175
|
+
}
|
|
176
|
+
export function parseStageRequest(value) {
|
|
177
|
+
if (!Check(StageRequestSchema, value))
|
|
178
|
+
throw schemaValidationError("subagent_stage request must match exact candidate and generation", Errors(StageRequestSchema, value));
|
|
179
|
+
return value;
|
|
180
|
+
}
|
|
181
|
+
const PromptRecordSchema = Type.Object({
|
|
182
|
+
kind: Type.Union([Type.Literal("initial"), Type.Literal("correction"), Type.Literal("followup")]),
|
|
183
|
+
status: Type.Union([Type.Literal("submitting"), Type.Literal("not_sent"), Type.Literal("settled"), Type.Literal("ambiguous")]),
|
|
184
|
+
preCandidate: WorkspaceSchema,
|
|
185
|
+
candidate: Type.Optional(WorkspaceSchema),
|
|
186
|
+
instruction: Type.Optional(TextSchema),
|
|
187
|
+
failure: OptionalRuntimeTextSchema,
|
|
188
|
+
at: TimestampSchema,
|
|
189
|
+
}, { additionalProperties: false });
|
|
190
|
+
const CommandEvidenceSchema = Type.Object({
|
|
191
|
+
command: TextSchema,
|
|
192
|
+
args: Type.Array(Type.String({ maxLength: 16_000 }), { maxItems: 128 }),
|
|
193
|
+
code: Type.Integer(),
|
|
194
|
+
killed: Type.Boolean(),
|
|
195
|
+
stdout: Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }),
|
|
196
|
+
stderr: Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }),
|
|
197
|
+
}, { additionalProperties: false });
|
|
198
|
+
const CheckBatchEvidenceSchema = Type.Object({
|
|
199
|
+
phase: Type.Union([Type.Literal("preliminary"), Type.Literal("final")]),
|
|
200
|
+
candidate: WorkspaceSchema,
|
|
201
|
+
identityAfter: WorkspaceSchema,
|
|
202
|
+
results: Type.Array(CommandEvidenceSchema, { maxItems: 32 }),
|
|
203
|
+
passed: Type.Boolean(),
|
|
204
|
+
at: TimestampSchema,
|
|
205
|
+
}, { additionalProperties: false });
|
|
206
|
+
const ReviewEvidenceSchema = Type.Object({
|
|
207
|
+
phase: Type.Union([Type.Literal("preliminary"), Type.Literal("final")]),
|
|
208
|
+
criterion: TextSchema,
|
|
209
|
+
base: WorkspaceSchema,
|
|
210
|
+
tip: WorkspaceSchema,
|
|
211
|
+
identityAfter: WorkspaceSchema,
|
|
212
|
+
verdict: Type.String({ maxLength: MAX_PERSISTED_RUNTIME_TEXT_BYTES }),
|
|
213
|
+
passed: Type.Boolean(),
|
|
214
|
+
at: TimestampSchema,
|
|
215
|
+
}, { additionalProperties: false });
|
|
216
|
+
const WorkerTerminationSchema = Type.Object({
|
|
217
|
+
status: Type.Union([Type.Literal("terminating"), Type.Literal("terminated"), Type.Literal("unknown")]),
|
|
218
|
+
workerId: RuntimeTextSchema,
|
|
219
|
+
candidate: WorkspaceSchema,
|
|
220
|
+
at: Type.Optional(TimestampSchema),
|
|
221
|
+
failure: OptionalRuntimeTextSchema,
|
|
222
|
+
}, { additionalProperties: false });
|
|
223
|
+
const ReadinessSchema = Type.Object({
|
|
224
|
+
candidate: WorkspaceSchema,
|
|
225
|
+
base: WorkspaceSchema,
|
|
226
|
+
at: TimestampSchema,
|
|
227
|
+
}, { additionalProperties: false });
|
|
228
|
+
const CleanupStepSchema = Type.Object({
|
|
229
|
+
kind: Type.Union([
|
|
230
|
+
Type.Literal("worker_tab"), Type.Literal("workspace"), Type.Literal("worktree"), Type.Literal("branch"),
|
|
231
|
+
]),
|
|
232
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("running"), Type.Literal("completed")]),
|
|
233
|
+
failure: OptionalRuntimeTextSchema,
|
|
234
|
+
}, { additionalProperties: false });
|
|
235
|
+
const TaskAttemptSchema = Type.Object({
|
|
236
|
+
number: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
237
|
+
waveNumber: Type.Integer({ minimum: 1 }),
|
|
238
|
+
waveBase: WorkspaceSchema,
|
|
239
|
+
correlationToken: Type.String({ pattern: TOKEN_PATTERN }),
|
|
240
|
+
allocationGeneration: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
241
|
+
allocations: Type.Array(AllocationIntentSchema, { maxItems: 5 }),
|
|
242
|
+
prompts: Type.Array(PromptRecordSchema, { maxItems: 32 }),
|
|
243
|
+
candidate: Type.Optional(WorkspaceSchema),
|
|
244
|
+
candidateBase: Type.Optional(WorkspaceSchema),
|
|
245
|
+
preliminaryChecks: Type.Optional(CheckBatchEvidenceSchema),
|
|
246
|
+
readiness: Type.Optional(ReadinessSchema),
|
|
247
|
+
superseded: Type.Optional(Type.Literal(true)),
|
|
248
|
+
termination: Type.Optional(WorkerTerminationSchema),
|
|
249
|
+
preliminaryReview: Type.Optional(ReviewEvidenceSchema),
|
|
250
|
+
cleanup: Type.Array(CleanupStepSchema, { minItems: CLEANUP_KINDS.length, maxItems: CLEANUP_KINDS.length }),
|
|
251
|
+
}, { additionalProperties: false });
|
|
252
|
+
const ChangesetTaskStateSchema = Type.Object({
|
|
253
|
+
taskId: IdSchema,
|
|
254
|
+
kind: Type.Literal("changeset"),
|
|
255
|
+
status: Type.Union([
|
|
256
|
+
Type.Literal("pending"), Type.Literal("allocating"), Type.Literal("working"), Type.Literal("ready_to_integrate"),
|
|
257
|
+
Type.Literal("integrating"), Type.Literal("cleanup"), Type.Literal("completed"), Type.Literal("rejected"), Type.Literal("needs_attention"),
|
|
258
|
+
]),
|
|
259
|
+
attempts: Type.Array(TaskAttemptSchema, { maxItems: 2 }),
|
|
260
|
+
failure: OptionalRuntimeTextSchema,
|
|
261
|
+
}, { additionalProperties: false });
|
|
262
|
+
const TextTaskOutputSchema = Type.Object({
|
|
263
|
+
text: RuntimeTextSchema,
|
|
264
|
+
}, { additionalProperties: false });
|
|
265
|
+
const TextTaskAttemptSchema = Type.Object({
|
|
266
|
+
number: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
267
|
+
status: Type.Union([Type.Literal("running"), Type.Literal("completed"), Type.Literal("failed"), Type.Literal("superseded")]),
|
|
268
|
+
failure: OptionalRuntimeTextSchema,
|
|
269
|
+
output: Type.Optional(TextTaskOutputSchema),
|
|
270
|
+
}, { additionalProperties: false });
|
|
271
|
+
const TextTaskStateSchema = Type.Object({
|
|
272
|
+
taskId: IdSchema,
|
|
273
|
+
kind: Type.Literal("text"),
|
|
274
|
+
status: Type.Union([
|
|
275
|
+
Type.Literal("pending"), Type.Literal("running"), Type.Literal("completed"), Type.Literal("needs_attention"),
|
|
276
|
+
]),
|
|
277
|
+
attempts: Type.Array(TextTaskAttemptSchema, { maxItems: 2 }),
|
|
278
|
+
failure: OptionalRuntimeTextSchema,
|
|
279
|
+
}, { additionalProperties: false });
|
|
280
|
+
const TaskStateSchema = Type.Union([TextTaskStateSchema, ChangesetTaskStateSchema]);
|
|
281
|
+
const WaveStateSchema = Type.Object({
|
|
282
|
+
number: Type.Integer({ minimum: 1 }),
|
|
283
|
+
base: WorkspaceSchema,
|
|
284
|
+
taskIds: Type.Array(IdSchema, { minItems: 1, maxItems: MAX_TASKS }),
|
|
285
|
+
status: Type.Union([Type.Literal("dispatching"), Type.Literal("integrating"), Type.Literal("completed"), Type.Literal("needs_attention")]),
|
|
286
|
+
}, { additionalProperties: false });
|
|
287
|
+
const FinalGateSchema = Type.Object({
|
|
288
|
+
status: Type.Union([
|
|
289
|
+
Type.Literal("pending"), Type.Literal("running"), Type.Literal("interrupted"), Type.Literal("passed"),
|
|
290
|
+
Type.Literal("final_failed"), Type.Literal("superseded"),
|
|
291
|
+
]),
|
|
292
|
+
identity: Type.Optional(WorkspaceSchema),
|
|
293
|
+
checks: Type.Optional(CheckBatchEvidenceSchema),
|
|
294
|
+
review: Type.Optional(ReviewEvidenceSchema),
|
|
295
|
+
failure: OptionalRuntimeTextSchema,
|
|
296
|
+
}, { additionalProperties: false });
|
|
297
|
+
const ExecutionPolicySnapshotSchema = Type.Object({
|
|
298
|
+
maxSubagents: Type.Integer({ minimum: 1 }),
|
|
299
|
+
maxTurns: Type.Integer({ minimum: 1 }),
|
|
300
|
+
maxTokens: Type.Optional(Type.Integer({ minimum: 1 })),
|
|
301
|
+
childIdleMs: Type.Number({ exclusiveMinimum: 0, maximum: 2_147_483_647 }),
|
|
302
|
+
childMaxMs: Type.Number({ exclusiveMinimum: 0, maximum: 2_147_483_647 }),
|
|
303
|
+
maxCorrections: Type.Integer({ minimum: 0 }),
|
|
304
|
+
}, { additionalProperties: false });
|
|
305
|
+
// Main-owned integration state binds each ready candidate to its retained attempt.
|
|
306
|
+
const IntegrationCandidateSchema = Type.Object({
|
|
307
|
+
taskId: IdSchema,
|
|
308
|
+
attempt: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
309
|
+
base: WorkspaceSchema,
|
|
310
|
+
tip: WorkspaceSchema,
|
|
311
|
+
checks: CheckBatchEvidenceSchema,
|
|
312
|
+
review: Type.Optional(ReviewEvidenceSchema),
|
|
313
|
+
worker: Type.Union([Type.Literal("retained"), Type.Literal("release_pending"), Type.Literal("released")]),
|
|
314
|
+
decision: Type.Optional(Type.Literal("rejected")),
|
|
315
|
+
}, { additionalProperties: false });
|
|
316
|
+
const IntegrationStageSchema = Type.Object({
|
|
317
|
+
taskId: IdSchema,
|
|
318
|
+
attempt: Type.Integer({ minimum: 1, maximum: 2 }),
|
|
319
|
+
source: WorkspaceSchema,
|
|
320
|
+
onto: WorkspaceSchema,
|
|
321
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("staging"), Type.Literal("staged"), Type.Literal("conflict")]),
|
|
322
|
+
tip: Type.Optional(WorkspaceSchema),
|
|
323
|
+
failure: OptionalRuntimeTextSchema,
|
|
324
|
+
}, { additionalProperties: false });
|
|
325
|
+
const PromotionSchema = Type.Object({
|
|
326
|
+
status: Type.Union([Type.Literal("promoting"), Type.Literal("promoted"), Type.Literal("unknown"), Type.Literal("failed")]),
|
|
327
|
+
expectedMain: WorkspaceSchema,
|
|
328
|
+
tip: WorkspaceSchema,
|
|
329
|
+
mainAfter: Type.Optional(WorkspaceSchema),
|
|
330
|
+
outcome: Type.Optional(Type.Literal("drift")),
|
|
331
|
+
failure: OptionalRuntimeTextSchema,
|
|
332
|
+
}, { additionalProperties: false });
|
|
333
|
+
const IntegrationGenerationSchema = Type.Object({
|
|
334
|
+
number: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
335
|
+
status: Type.Union([
|
|
336
|
+
Type.Literal("staging"), Type.Literal("conflict"), Type.Literal("validating"), Type.Literal("validation_failed"),
|
|
337
|
+
Type.Literal("ready"), Type.Literal("promoting"), Type.Literal("promoted"),
|
|
338
|
+
Type.Literal("promotion_unknown"), Type.Literal("promotion_failed"), Type.Literal("superseded"),
|
|
339
|
+
]),
|
|
340
|
+
expectedMain: WorkspaceSchema,
|
|
341
|
+
integrationBase: WorkspaceSchema,
|
|
342
|
+
order: Type.Array(IdSchema, { maxItems: MAX_TASKS }),
|
|
343
|
+
stages: Type.Array(IntegrationStageSchema, { maxItems: MAX_TASKS }),
|
|
344
|
+
worktree: Type.Optional(WorktreeRecordSchema),
|
|
345
|
+
combinedTip: Type.Optional(WorkspaceSchema),
|
|
346
|
+
correction: Type.Optional(Type.Object({ from: WorkspaceSchema, to: WorkspaceSchema }, { additionalProperties: false })),
|
|
347
|
+
checks: Type.Optional(CheckBatchEvidenceSchema),
|
|
348
|
+
review: Type.Optional(ReviewEvidenceSchema),
|
|
349
|
+
promotion: Type.Optional(PromotionSchema),
|
|
350
|
+
supersededPromotion: Type.Optional(PromotionSchema),
|
|
351
|
+
cleanup: Type.Optional(Type.Array(CleanupStepSchema, { minItems: 2, maxItems: 2 })),
|
|
352
|
+
supersededFrom: Type.Optional(Type.Union([
|
|
353
|
+
Type.Literal("staging"), Type.Literal("conflict"), Type.Literal("validation_failed"),
|
|
354
|
+
Type.Literal("ready"), Type.Literal("promotion_failed"),
|
|
355
|
+
])),
|
|
356
|
+
failure: OptionalRuntimeTextSchema,
|
|
357
|
+
}, { additionalProperties: false });
|
|
358
|
+
export const IntegrationStateSchema = Type.Object({
|
|
359
|
+
candidates: Type.Array(IntegrationCandidateSchema, { maxItems: MAX_TASKS * 2 }),
|
|
360
|
+
generations: Type.Array(IntegrationGenerationSchema, { maxItems: 32 }),
|
|
361
|
+
refresh: Type.Optional(Type.Object({
|
|
362
|
+
from: WorkspaceSchema,
|
|
363
|
+
to: WorkspaceSchema,
|
|
364
|
+
generation: Type.Integer({ minimum: 1, maximum: 32 }),
|
|
365
|
+
status: Type.Union([Type.Literal("pending"), Type.Literal("ready"), Type.Literal("unknown")]),
|
|
366
|
+
failure: OptionalRuntimeTextSchema,
|
|
367
|
+
}, { additionalProperties: false })),
|
|
368
|
+
}, { additionalProperties: false });
|
|
369
|
+
const RunStateSchema = Type.Object({
|
|
370
|
+
version: Type.Literal(RUN_STATE_VERSION),
|
|
371
|
+
request: ExecuteRequestSchema,
|
|
372
|
+
policy: ExecutionPolicySnapshotSchema,
|
|
373
|
+
correctionCount: Type.Integer({ minimum: 0 }),
|
|
374
|
+
root: TextSchema,
|
|
375
|
+
requestStartMain: WorkspaceSchema,
|
|
376
|
+
main: WorkspaceSchema,
|
|
377
|
+
status: Type.Union([
|
|
378
|
+
Type.Literal("pending"), Type.Literal("running"), Type.Literal("needs_attention"), Type.Literal("completed"),
|
|
379
|
+
Type.Literal("final_failed"), Type.Literal("superseded"), Type.Literal("aborted"),
|
|
380
|
+
]),
|
|
381
|
+
tasks: Type.Array(TaskStateSchema, { minItems: 1, maxItems: MAX_TASKS }),
|
|
382
|
+
waves: Type.Array(WaveStateSchema, { maxItems: MAX_TASKS }),
|
|
383
|
+
integration: IntegrationStateSchema,
|
|
384
|
+
final: FinalGateSchema,
|
|
385
|
+
recovery: Type.Optional(Type.Object({
|
|
386
|
+
kind: Type.Literal("resume"),
|
|
387
|
+
action: Type.Union([Type.Literal("retry"), Type.Literal("verify"), Type.Literal("finalize")]),
|
|
388
|
+
taskId: Type.Optional(IdSchema),
|
|
389
|
+
}, { additionalProperties: false })),
|
|
390
|
+
accepted: Type.Boolean(),
|
|
391
|
+
acceptedAt: Type.Optional(TimestampSchema),
|
|
392
|
+
createdAt: TimestampSchema,
|
|
393
|
+
updatedAt: TimestampSchema,
|
|
394
|
+
}, { additionalProperties: false });
|
|
395
|
+
function normalizeCheck(check, field) {
|
|
396
|
+
return {
|
|
397
|
+
command: normalizeText(check.command, `${field}.command`),
|
|
398
|
+
args: check.args.map((arg, index) => {
|
|
399
|
+
if (arg.includes("\0"))
|
|
400
|
+
throw new Error(`${field}.args[${index}] must not contain NUL bytes.`);
|
|
401
|
+
return arg;
|
|
402
|
+
}),
|
|
403
|
+
};
|
|
404
|
+
}
|
|
405
|
+
function normalizeText(value, field) {
|
|
406
|
+
const normalized = value.trim();
|
|
407
|
+
if (!normalized || value.includes("\0"))
|
|
408
|
+
throw new Error(`${field} must be non-empty text without NUL bytes.`);
|
|
409
|
+
return normalized;
|
|
410
|
+
}
|
|
411
|
+
function normalizeRole(value, field) {
|
|
412
|
+
return parseRoleName(value, field);
|
|
413
|
+
}
|
|
414
|
+
function normalizeJudgment(judgment, field) {
|
|
415
|
+
if (!judgment)
|
|
416
|
+
return;
|
|
417
|
+
return {
|
|
418
|
+
...judgment,
|
|
419
|
+
role: normalizeRole(judgment.role, `${field}.role`),
|
|
420
|
+
criterion: normalizeText(judgment.criterion, `${field}.criterion`),
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
function normalizeTask(task, index) {
|
|
424
|
+
const fields = {
|
|
425
|
+
id: task.id,
|
|
426
|
+
role: normalizeRole(task.role, `tasks[${index}].role`),
|
|
427
|
+
modelClass: task.modelClass,
|
|
428
|
+
requirements: normalizeText(task.requirements, `tasks[${index}].requirements`),
|
|
429
|
+
deliverable: normalizeText(task.deliverable, `tasks[${index}].deliverable`),
|
|
430
|
+
dependsOn: [...task.dependsOn],
|
|
431
|
+
contextFrom: [...task.contextFrom],
|
|
432
|
+
};
|
|
433
|
+
if (task.kind === "text")
|
|
434
|
+
return { ...fields, kind: "text" };
|
|
435
|
+
return {
|
|
436
|
+
...fields,
|
|
437
|
+
kind: "changeset",
|
|
438
|
+
checks: task.checks.map((check, checkIndex) => normalizeCheck(check, `tasks[${index}].checks[${checkIndex}]`)),
|
|
439
|
+
...(task.judgment ? { judgment: normalizeJudgment(task.judgment, `tasks[${index}].judgment`) } : {}),
|
|
440
|
+
};
|
|
441
|
+
}
|
|
442
|
+
export function taskDependencies(task) {
|
|
443
|
+
return [...task.dependsOn, ...task.contextFrom];
|
|
444
|
+
}
|
|
445
|
+
export function validateGraph(tasks) {
|
|
446
|
+
const byId = new Map();
|
|
447
|
+
for (const task of tasks) {
|
|
448
|
+
if (byId.has(task.id))
|
|
449
|
+
throw new Error(`Duplicate task ID: ${task.id}.`);
|
|
450
|
+
byId.set(task.id, task);
|
|
451
|
+
const dependencies = new Set();
|
|
452
|
+
for (const dependency of task.dependsOn) {
|
|
453
|
+
if (dependency === task.id)
|
|
454
|
+
throw new Error(`Task ${task.id} cannot depend on itself.`);
|
|
455
|
+
if (dependencies.has(dependency))
|
|
456
|
+
throw new Error(`Task ${task.id} has duplicate dependency ${dependency}.`);
|
|
457
|
+
dependencies.add(dependency);
|
|
458
|
+
}
|
|
459
|
+
const context = new Set();
|
|
460
|
+
for (const source of task.contextFrom) {
|
|
461
|
+
if (source === task.id)
|
|
462
|
+
throw new Error(`Task ${task.id} cannot depend on itself.`);
|
|
463
|
+
if (context.has(source))
|
|
464
|
+
throw new Error(`Task ${task.id} has duplicate context source ${source}.`);
|
|
465
|
+
if (dependencies.has(source))
|
|
466
|
+
throw new Error(`Task ${task.id} has overlapping dependsOn and contextFrom edge ${source}.`);
|
|
467
|
+
context.add(source);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
for (const task of tasks) {
|
|
471
|
+
for (const dependency of task.dependsOn) {
|
|
472
|
+
if (!byId.has(dependency))
|
|
473
|
+
throw new Error(`Task ${task.id} has unknown dependency ${dependency}.`);
|
|
474
|
+
}
|
|
475
|
+
for (const source of task.contextFrom) {
|
|
476
|
+
const sourceTask = byId.get(source);
|
|
477
|
+
if (!sourceTask)
|
|
478
|
+
throw new Error(`Task ${task.id} has unknown context source ${source}.`);
|
|
479
|
+
if (sourceTask.kind !== "text")
|
|
480
|
+
throw new Error(`Task ${task.id} context source ${source} must be a text task.`);
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
const visiting = new Set();
|
|
484
|
+
const visited = new Set();
|
|
485
|
+
const visit = (id) => {
|
|
486
|
+
if (visiting.has(id))
|
|
487
|
+
throw new Error(`Task dependency cycle includes ${id}.`);
|
|
488
|
+
if (visited.has(id))
|
|
489
|
+
return;
|
|
490
|
+
visiting.add(id);
|
|
491
|
+
for (const dependency of taskDependencies(byId.get(id)))
|
|
492
|
+
visit(dependency);
|
|
493
|
+
visiting.delete(id);
|
|
494
|
+
visited.add(id);
|
|
495
|
+
};
|
|
496
|
+
for (const task of tasks)
|
|
497
|
+
visit(task.id);
|
|
498
|
+
}
|
|
499
|
+
function schemaIssuePath(issue) {
|
|
500
|
+
const propertyKey = issue.keyword === "additionalProperties" ? "additionalProperties"
|
|
501
|
+
: issue.keyword === "required" ? "requiredProperties"
|
|
502
|
+
: undefined;
|
|
503
|
+
const properties = propertyKey ? issue.params?.[propertyKey] : undefined;
|
|
504
|
+
if (!Array.isArray(properties) || properties.length !== 1 || typeof properties[0] !== "string") {
|
|
505
|
+
return issue.instancePath;
|
|
506
|
+
}
|
|
507
|
+
const property = properties[0].replaceAll("~", "~0").replaceAll("/", "~1");
|
|
508
|
+
return `${issue.instancePath}/${property}`;
|
|
509
|
+
}
|
|
510
|
+
function schemaValidationError(label, issues) {
|
|
511
|
+
let selected;
|
|
512
|
+
for (const issue of issues) {
|
|
513
|
+
const path = schemaIssuePath(issue);
|
|
514
|
+
if (!selected || path.length > selected.path.length)
|
|
515
|
+
selected = { issue, path };
|
|
516
|
+
}
|
|
517
|
+
if (!selected)
|
|
518
|
+
return new Error(`${label}.`);
|
|
519
|
+
const rawDetail = ` at ${selected.path || "/"}: ${selected.issue.message}`;
|
|
520
|
+
const detail = rawDetail.length <= 512 ? rawDetail : `${rawDetail.slice(0, 509)}...`;
|
|
521
|
+
return new Error(`${label}${detail}.`);
|
|
522
|
+
}
|
|
523
|
+
export function parseExecuteRequest(value) {
|
|
524
|
+
if (!Check(ExecuteRequestSchema, value)) {
|
|
525
|
+
throw schemaValidationError("isolated delegate_task request must match the strict task schema", Errors(ExecuteRequestSchema, value));
|
|
526
|
+
}
|
|
527
|
+
const input = value;
|
|
528
|
+
const tasks = input.tasks.map(normalizeTask);
|
|
529
|
+
const finalChecks = (input.finalChecks ?? []).map((check, index) => normalizeCheck(check, `finalChecks[${index}]`));
|
|
530
|
+
if (tasks.some((task) => task.kind === "changeset") && finalChecks.length === 0) {
|
|
531
|
+
throw new Error("isolated changeset graphs require at least one final check.");
|
|
532
|
+
}
|
|
533
|
+
const request = {
|
|
534
|
+
...input,
|
|
535
|
+
mode: "isolated",
|
|
536
|
+
goal: normalizeText(input.goal, "goal"),
|
|
537
|
+
tasks,
|
|
538
|
+
finalChecks,
|
|
539
|
+
...(input.finalJudgment ? { finalJudgment: normalizeJudgment(input.finalJudgment, "finalJudgment") } : {}),
|
|
540
|
+
};
|
|
541
|
+
if (Buffer.byteLength(JSON.stringify(request), "utf8") > MAX_EXECUTE_REQUEST_BYTES) {
|
|
542
|
+
throw new Error(`isolated delegate_task normalized request exceeds ${MAX_EXECUTE_REQUEST_BYTES} bytes.`);
|
|
543
|
+
}
|
|
544
|
+
validateGraph(request.tasks);
|
|
545
|
+
return request;
|
|
546
|
+
}
|
|
547
|
+
export function parseIdOnly(value) {
|
|
548
|
+
if (!Check(IdOnlySchema, value)) {
|
|
549
|
+
throw schemaValidationError("subagent request ID must match the strict schema", Errors(IdOnlySchema, value));
|
|
550
|
+
}
|
|
551
|
+
return value;
|
|
552
|
+
}
|
|
553
|
+
export function parseResumeRequest(value) {
|
|
554
|
+
if (!Check(ResumeRequestSchema, value)) {
|
|
555
|
+
throw schemaValidationError("subagent_resume request must match one strict action", Errors(ResumeRequestSchema, value));
|
|
556
|
+
}
|
|
557
|
+
return value;
|
|
558
|
+
}
|
|
559
|
+
export function sameIdentity(left, right) {
|
|
560
|
+
return left.branch === right.branch && left.head === right.head && left.index === right.index && left.tree === right.tree;
|
|
561
|
+
}
|
|
562
|
+
export function isCleanCommitted(identity) {
|
|
563
|
+
return identity.index === identity.tree;
|
|
564
|
+
}
|
|
565
|
+
function sameCheck(left, right) {
|
|
566
|
+
return left.command === right.command
|
|
567
|
+
&& left.args.length === right.args.length
|
|
568
|
+
&& left.args.every((arg, index) => arg === right.args[index]);
|
|
569
|
+
}
|
|
570
|
+
function hasWorktreePlan(allocation) {
|
|
571
|
+
return allocation.worktree !== undefined;
|
|
572
|
+
}
|
|
573
|
+
function requireExactAllocationText(value, field) {
|
|
574
|
+
if (!value.trim() || value.trim() !== value || value.includes("\0")) {
|
|
575
|
+
throw new Error(`${field} must be exact non-empty text.`);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
function requireAbsoluteAllocationPath(value, field) {
|
|
579
|
+
requireExactAllocationText(value, field);
|
|
580
|
+
if (!isAbsolute(value))
|
|
581
|
+
throw new Error(`${field} must be an absolute path.`);
|
|
582
|
+
}
|
|
583
|
+
function validateCheckBatchEvidence(evidence, checks, field) {
|
|
584
|
+
if (evidence.results.length !== checks.length
|
|
585
|
+
|| evidence.results.some((result, index) => !sameCheck(result, checks[index]))) {
|
|
586
|
+
throw new Error(`${field} does not retain every exact declared command and argv.`);
|
|
587
|
+
}
|
|
588
|
+
const commandsPassed = evidence.results.every((result) => result.code === 0 && !result.killed);
|
|
589
|
+
const identityMatches = sameIdentity(evidence.candidate, evidence.identityAfter);
|
|
590
|
+
if (evidence.passed !== (commandsPassed && identityMatches)) {
|
|
591
|
+
throw new Error(`${field} has an inconsistent pass result.`);
|
|
592
|
+
}
|
|
593
|
+
let diagnosticIndex = evidence.passed
|
|
594
|
+
? -1
|
|
595
|
+
: evidence.results.findIndex((result) => result.code !== 0 || result.killed);
|
|
596
|
+
if (diagnosticIndex < 0 && commandsPassed && !identityMatches)
|
|
597
|
+
diagnosticIndex = evidence.results.length - 1;
|
|
598
|
+
if (evidence.results.some((result, index) => index !== diagnosticIndex && (result.stdout !== "" || result.stderr !== ""))) {
|
|
599
|
+
throw new Error(`${field} retains non-diagnostic command output.`);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
export function checkBatchPasses(evidence, checks, candidate) {
|
|
603
|
+
return Boolean(evidence
|
|
604
|
+
&& evidence.passed
|
|
605
|
+
&& sameIdentity(evidence.candidate, candidate)
|
|
606
|
+
&& sameIdentity(evidence.identityAfter, candidate)
|
|
607
|
+
&& evidence.results.length === checks.length
|
|
608
|
+
&& evidence.results.every((result, index) => sameCheck(result, checks[index]) && result.code === 0 && !result.killed));
|
|
609
|
+
}
|
|
610
|
+
export function reviewEvidencePasses(evidence, phase, criterion, base, tip) {
|
|
611
|
+
return Boolean(evidence
|
|
612
|
+
&& evidence.phase === phase
|
|
613
|
+
&& evidence.passed
|
|
614
|
+
&& evidence.verdict === "PASS"
|
|
615
|
+
&& evidence.criterion === criterion
|
|
616
|
+
&& sameIdentity(evidence.base, base)
|
|
617
|
+
&& sameIdentity(evidence.tip, tip)
|
|
618
|
+
&& sameIdentity(evidence.identityAfter, tip));
|
|
619
|
+
}
|
|
620
|
+
function requireRuntimeTextByteLength(value, field) {
|
|
621
|
+
if (Buffer.byteLength(value, "utf8") > MAX_PERSISTED_RUNTIME_TEXT_BYTES) {
|
|
622
|
+
throw new Error(`${field} exceeds ${MAX_PERSISTED_RUNTIME_TEXT_BYTES} UTF-8 bytes.`);
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
function validateTextTaskState(taskState) {
|
|
626
|
+
const latest = taskState.attempts.at(-1);
|
|
627
|
+
for (const [attemptIndex, attempt] of taskState.attempts.entries()) {
|
|
628
|
+
if (attempt.number !== attemptIndex + 1)
|
|
629
|
+
throw new Error(`Malformed text attempt order for ${taskState.taskId}.`);
|
|
630
|
+
if (attemptIndex !== taskState.attempts.length - 1) {
|
|
631
|
+
if (!['failed', 'superseded'].includes(attempt.status) || attempt.output) {
|
|
632
|
+
throw new Error(`Non-latest text attempt ${attempt.number} for ${taskState.taskId} must be failed or superseded with no output.`);
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
else if (attempt.status === "completed") {
|
|
636
|
+
if (!attempt.output)
|
|
637
|
+
throw new Error(`Completed text attempt ${attempt.number} for ${taskState.taskId} lacks output.`);
|
|
638
|
+
}
|
|
639
|
+
else if (attempt.output) {
|
|
640
|
+
throw new Error(`Text attempt ${attempt.number} for ${taskState.taskId} has output without completion.`);
|
|
641
|
+
}
|
|
642
|
+
if (attempt.output) {
|
|
643
|
+
requireRuntimeTextByteLength(attempt.output.text, `Text attempt ${attempt.number} for ${taskState.taskId} output`);
|
|
644
|
+
}
|
|
645
|
+
if (attempt.failure !== undefined) {
|
|
646
|
+
if (attempt.status !== "failed") {
|
|
647
|
+
throw new Error(`Text attempt ${attempt.number} for ${taskState.taskId} has failure without failure status.`);
|
|
648
|
+
}
|
|
649
|
+
requireRuntimeTextByteLength(attempt.failure, `Text attempt ${attempt.number} for ${taskState.taskId} failure`);
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const expectedLatestStatus = taskState.status === "pending"
|
|
653
|
+
? taskState.attempts.length ? "superseded" : undefined
|
|
654
|
+
: taskState.status === "running"
|
|
655
|
+
? "running"
|
|
656
|
+
: taskState.status === "completed"
|
|
657
|
+
? "completed"
|
|
658
|
+
: "failed";
|
|
659
|
+
if (latest?.status !== expectedLatestStatus) {
|
|
660
|
+
throw new Error(`Text task ${taskState.taskId} status ${taskState.status} has an incompatible latest attempt.`);
|
|
661
|
+
}
|
|
662
|
+
if ((taskState.failure !== undefined) !== (taskState.status === "needs_attention")) {
|
|
663
|
+
throw new Error(`Text task ${taskState.taskId} must retain a failure exactly when it needs attention.`);
|
|
664
|
+
}
|
|
665
|
+
if (taskState.failure !== undefined) {
|
|
666
|
+
requireRuntimeTextByteLength(taskState.failure, `Text task ${taskState.taskId} failure`);
|
|
667
|
+
}
|
|
668
|
+
if (taskState.status === "needs_attention" && latest?.failure !== taskState.failure) {
|
|
669
|
+
throw new Error(`Text task ${taskState.taskId} needs-attention failure must exactly match its latest attempt failure.`);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
/** Check the persisted integration projection against the request before recovery or promotion. */
|
|
673
|
+
export function parseIntegrationState(value, request) {
|
|
674
|
+
if (!Check(IntegrationStateSchema, value)) {
|
|
675
|
+
throw schemaValidationError("Malformed integration state", Errors(IntegrationStateSchema, value));
|
|
676
|
+
}
|
|
677
|
+
const state = value;
|
|
678
|
+
const tasks = new Map(request.tasks.filter((task) => task.kind === "changeset")
|
|
679
|
+
.map((task) => [task.id, task]));
|
|
680
|
+
const candidates = new Map();
|
|
681
|
+
const latestCandidates = new Map();
|
|
682
|
+
for (const candidate of state.candidates) {
|
|
683
|
+
const task = tasks.get(candidate.taskId);
|
|
684
|
+
const previous = latestCandidates.get(candidate.taskId);
|
|
685
|
+
const key = `${candidate.taskId}\0${candidate.attempt}\0${candidate.tip.head}`;
|
|
686
|
+
if (!task || candidates.has(key) || (previous && (candidate.attempt < previous.attempt
|
|
687
|
+
|| candidate.attempt === previous.attempt && previous.decision !== "rejected")) || !isCleanCommitted(candidate.base)
|
|
688
|
+
|| !isCleanCommitted(candidate.tip) || candidate.base.head === candidate.tip.head
|
|
689
|
+
|| candidate.checks.phase !== "preliminary") {
|
|
690
|
+
throw new Error(`Invalid ready candidate for ${candidate.taskId}.`);
|
|
691
|
+
}
|
|
692
|
+
validateCheckBatchEvidence(candidate.checks, task.checks, `Ready checks for ${candidate.taskId}`);
|
|
693
|
+
if (!checkBatchPasses(candidate.checks, task.checks, candidate.tip)
|
|
694
|
+
|| (task.judgment && !reviewEvidencePasses(candidate.review, "preliminary", task.judgment.criterion, candidate.base, candidate.tip))
|
|
695
|
+
|| (!task.judgment && candidate.review !== undefined)) {
|
|
696
|
+
throw new Error(`Ready candidate ${candidate.taskId} lacks exact passing evidence.`);
|
|
697
|
+
}
|
|
698
|
+
candidates.set(key, candidate);
|
|
699
|
+
latestCandidates.set(candidate.taskId, candidate);
|
|
700
|
+
}
|
|
701
|
+
const refresh = state.refresh;
|
|
702
|
+
if (refresh && (!isCleanCommitted(refresh.from) || !isCleanCommitted(refresh.to)
|
|
703
|
+
|| refresh.from.branch !== refresh.to.branch || refresh.from.head === refresh.to.head
|
|
704
|
+
|| refresh.generation !== state.generations.length + (refresh.status === "pending" ? 1 : 0)
|
|
705
|
+
|| (refresh.status === "ready" ? refresh.failure !== undefined : refresh.failure !== undefined && !refresh.failure.trim())
|
|
706
|
+
|| (refresh.generation > 1 && (state.generations[refresh.generation - 2]?.status !== "superseded"
|
|
707
|
+
|| !sameIdentity(state.generations[refresh.generation - 2].expectedMain, refresh.from))))) {
|
|
708
|
+
throw new Error("Refresh intent has invalid Main lineage or status.");
|
|
709
|
+
}
|
|
710
|
+
for (const [index, generation] of state.generations.entries()) {
|
|
711
|
+
if (generation.number !== index + 1 || (index > 0 && state.generations[index - 1].status !== "superseded")) {
|
|
712
|
+
throw new Error("Integration generations must be ordered and superseded before replacement.");
|
|
713
|
+
}
|
|
714
|
+
if (!isCleanCommitted(generation.expectedMain) || !isCleanCommitted(generation.integrationBase)
|
|
715
|
+
|| generation.integrationBase.head !== generation.expectedMain.head
|
|
716
|
+
|| generation.integrationBase.index !== generation.expectedMain.index
|
|
717
|
+
|| generation.integrationBase.tree !== generation.expectedMain.tree) {
|
|
718
|
+
throw new Error(`Integration generation ${generation.number} has an invalid Main base.`);
|
|
719
|
+
}
|
|
720
|
+
if (generation.worktree) {
|
|
721
|
+
for (const path of [generation.worktree.path, generation.worktree.cwd, generation.worktree.repoRoot]) {
|
|
722
|
+
requireAbsoluteAllocationPath(path, `Generation ${generation.number} worktree path`);
|
|
723
|
+
}
|
|
724
|
+
if (generation.worktree.baseCommit !== generation.expectedMain.head
|
|
725
|
+
|| generation.worktree.path !== generation.worktree.cwd
|
|
726
|
+
|| generation.integrationBase.branch !== `refs/heads/${generation.worktree.branch}`) {
|
|
727
|
+
throw new Error(`Generation ${generation.number} has a mismatched integration worktree.`);
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
if (refresh?.generation === generation.number) {
|
|
731
|
+
if (!sameIdentity(generation.expectedMain, refresh.to)
|
|
732
|
+
|| (refresh.status === "unknown" && (generation.order.length || generation.stages.length))
|
|
733
|
+
|| refresh.status === "pending")
|
|
734
|
+
throw new Error("Refreshed generation does not match its durable intent.");
|
|
735
|
+
}
|
|
736
|
+
if (!generation.order.length && (!generation.worktree
|
|
737
|
+
|| (generation.status !== "superseded" && (index !== state.generations.length - 1 || !refresh
|
|
738
|
+
|| refresh.generation !== generation.number || generation.status !== "staging"))
|
|
739
|
+
|| generation.stages.length || generation.combinedTip || generation.checks || generation.review || generation.promotion)) {
|
|
740
|
+
throw new Error("Empty integration generation requires an exact refreshed worktree.");
|
|
741
|
+
}
|
|
742
|
+
const chosen = new Set(generation.order);
|
|
743
|
+
if (chosen.size !== generation.order.length || generation.order.some((id) => !latestCandidates.has(id))
|
|
744
|
+
|| generation.stages.length > generation.order.length) {
|
|
745
|
+
throw new Error(`Generation ${generation.number} must choose distinct ready candidates in explicit order.`);
|
|
746
|
+
}
|
|
747
|
+
let previous = generation.integrationBase;
|
|
748
|
+
let unfinished = false;
|
|
749
|
+
for (const [stageIndex, stage] of generation.stages.entries()) {
|
|
750
|
+
const candidate = candidates.get(`${stage.taskId}\0${stage.attempt}\0${stage.source.head}`);
|
|
751
|
+
if (!candidate || !sameIdentity(stage.source, candidate.tip)
|
|
752
|
+
|| generation.order[stageIndex] !== stage.taskId || !sameIdentity(stage.onto, previous) || unfinished) {
|
|
753
|
+
throw new Error(`Stage ${stageIndex + 1} breaks generation ${generation.number} lineage.`);
|
|
754
|
+
}
|
|
755
|
+
if (generation.status !== "superseded" && (candidate !== latestCandidates.get(stage.taskId) || candidate.decision)) {
|
|
756
|
+
throw new Error(`Stage ${stageIndex + 1} does not use the latest ready attempt.`);
|
|
757
|
+
}
|
|
758
|
+
const definition = tasks.get(stage.taskId);
|
|
759
|
+
const predecessorIds = definition.dependsOn.filter((id) => tasks.has(id));
|
|
760
|
+
if (predecessorIds.some((id) => !generation.stages.slice(0, stageIndex).some((prior) => prior.taskId === id && prior.status === "staged"))) {
|
|
761
|
+
throw new Error(`Stage ${stageIndex + 1} is missing its staged changeset predecessor.`);
|
|
762
|
+
}
|
|
763
|
+
if (stage.status === "staged") {
|
|
764
|
+
if (!stage.tip || stage.failure !== undefined || !isCleanCommitted(stage.tip)
|
|
765
|
+
|| stage.tip.branch !== generation.integrationBase.branch) {
|
|
766
|
+
throw new Error(`Stage ${stageIndex + 1} lacks a clean staged tip.`);
|
|
767
|
+
}
|
|
768
|
+
previous = stage.tip;
|
|
769
|
+
}
|
|
770
|
+
else {
|
|
771
|
+
if (stage.tip || (stage.status === "conflict" && !stage.failure?.trim())
|
|
772
|
+
|| (stage.status === "pending" && stage.failure !== undefined)) {
|
|
773
|
+
throw new Error(`Stage ${stageIndex + 1} has inconsistent pending outcome evidence.`);
|
|
774
|
+
}
|
|
775
|
+
unfinished = true;
|
|
776
|
+
if (stageIndex !== generation.stages.length - 1)
|
|
777
|
+
throw new Error("Unfinished stage must be last.");
|
|
778
|
+
}
|
|
779
|
+
}
|
|
780
|
+
const complete = !unfinished && generation.stages.length === generation.order.length;
|
|
781
|
+
if (generation.correction && (!complete || !isCleanCommitted(generation.correction.from)
|
|
782
|
+
|| !isCleanCommitted(generation.correction.to) || !sameIdentity(generation.correction.from, previous)
|
|
783
|
+
|| generation.correction.to.head === previous.head || generation.correction.to.branch !== previous.branch)) {
|
|
784
|
+
throw new Error(`Generation ${generation.number} has an invalid correction intent.`);
|
|
785
|
+
}
|
|
786
|
+
if (generation.combinedTip && (!complete || !sameIdentity(generation.combinedTip, generation.correction?.to ?? previous))) {
|
|
787
|
+
throw new Error(`Generation ${generation.number} combined tip breaks staged lineage.`);
|
|
788
|
+
}
|
|
789
|
+
if (generation.checks) {
|
|
790
|
+
if (generation.checks.phase !== "final")
|
|
791
|
+
throw new Error("Combined checks must be final-phase evidence.");
|
|
792
|
+
validateCheckBatchEvidence(generation.checks, request.finalChecks, "Combined checks");
|
|
793
|
+
}
|
|
794
|
+
if (generation.review && (generation.review.phase !== "final" || !request.finalJudgment
|
|
795
|
+
|| generation.review.criterion !== request.finalJudgment.criterion
|
|
796
|
+
|| !generation.combinedTip || !sameIdentity(generation.review.base, generation.expectedMain)
|
|
797
|
+
|| !sameIdentity(generation.review.tip, generation.combinedTip)
|
|
798
|
+
|| generation.review.passed !== (generation.review.verdict === "PASS"
|
|
799
|
+
&& sameIdentity(generation.review.identityAfter, generation.combinedTip)))) {
|
|
800
|
+
throw new Error(`Generation ${generation.number} has invalid combined review evidence.`);
|
|
801
|
+
}
|
|
802
|
+
if ((generation.checks || generation.review || generation.promotion) && !generation.combinedTip) {
|
|
803
|
+
throw new Error(`Generation ${generation.number} has evidence without a complete tip.`);
|
|
804
|
+
}
|
|
805
|
+
if (generation.checks && !sameIdentity(generation.checks.candidate, generation.combinedTip)) {
|
|
806
|
+
throw new Error(`Generation ${generation.number} checks target another tip.`);
|
|
807
|
+
}
|
|
808
|
+
const passes = integrationGenerationPasses(generation, request);
|
|
809
|
+
const promotion = generation.promotion;
|
|
810
|
+
if (promotion && (!passes || generation.failure !== undefined
|
|
811
|
+
|| !sameIdentity(promotion.expectedMain, generation.expectedMain)
|
|
812
|
+
|| !sameIdentity(promotion.tip, generation.combinedTip))) {
|
|
813
|
+
throw new Error(`Generation ${generation.number} promotion has stale evidence or lineage.`);
|
|
814
|
+
}
|
|
815
|
+
if (promotion && (promotion.outcome !== undefined && (promotion.status !== "failed" || promotion.outcome !== "drift")
|
|
816
|
+
|| (promotion.status === "promoting"
|
|
817
|
+
? promotion.mainAfter !== undefined || promotion.failure !== undefined
|
|
818
|
+
: promotion.status === "promoted"
|
|
819
|
+
? !promotion.mainAfter || promotion.failure !== undefined
|
|
820
|
+
|| promotion.mainAfter.branch !== promotion.expectedMain.branch
|
|
821
|
+
|| promotion.mainAfter.head !== promotion.tip.head || !isCleanCommitted(promotion.mainAfter)
|
|
822
|
+
: promotion.mainAfter !== undefined || !promotion.failure?.trim()))) {
|
|
823
|
+
throw new Error(`Generation ${generation.number} has inconsistent promotion outcome.`);
|
|
824
|
+
}
|
|
825
|
+
const expectedStatus = promotion?.status === "unknown" ? "promotion_unknown"
|
|
826
|
+
: promotion?.status === "failed" ? "promotion_failed" : promotion?.status;
|
|
827
|
+
if (generation.supersededPromotion && (generation.status !== "superseded" || generation.supersededFrom !== "promotion_failed"
|
|
828
|
+
|| generation.supersededPromotion.status !== "failed" || generation.supersededPromotion.outcome !== "drift"
|
|
829
|
+
|| generation.supersededPromotion.mainAfter || !generation.supersededPromotion.failure?.trim()
|
|
830
|
+
|| !sameIdentity(generation.supersededPromotion.expectedMain, generation.expectedMain)
|
|
831
|
+
|| !sameIdentity(generation.supersededPromotion.tip, generation.correction?.to ?? previous))) {
|
|
832
|
+
throw new Error("Superseded promotion lacks proven exact drift evidence.");
|
|
833
|
+
}
|
|
834
|
+
if (generation.status === "superseded") {
|
|
835
|
+
if (!generation.supersededFrom || !generation.failure?.trim() || generation.promotion
|
|
836
|
+
|| generation.checks || generation.review || generation.combinedTip
|
|
837
|
+
|| (generation.supersededFrom === "promotion_failed" ? !generation.supersededPromotion : generation.correction || generation.supersededPromotion)) {
|
|
838
|
+
throw new Error(`Superseded generation ${generation.number} retains usable evidence or lacks its reason.`);
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
else if (generation.supersededFrom !== undefined || (promotion ? generation.status !== expectedStatus
|
|
842
|
+
: generation.status === "ready" ? !passes || generation.failure !== undefined
|
|
843
|
+
: generation.status === "validating" ? !complete || !generation.combinedTip || generation.failure !== undefined
|
|
844
|
+
: generation.status === "validation_failed" ? !complete || !generation.combinedTip
|
|
845
|
+
|| passes || !generation.failure?.trim()
|
|
846
|
+
: generation.status === "conflict" ? generation.stages.at(-1)?.status !== "conflict" || !generation.failure?.trim()
|
|
847
|
+
: generation.status !== "staging" || generation.checks !== undefined
|
|
848
|
+
|| generation.review !== undefined || generation.failure !== undefined)) {
|
|
849
|
+
throw new Error(`Generation ${generation.number} has inconsistent status or invalidated evidence.`);
|
|
850
|
+
}
|
|
851
|
+
if (generation.cleanup && (!['promoted', 'superseded'].includes(generation.status)
|
|
852
|
+
|| generation.cleanup[0]?.kind !== "worktree" || generation.cleanup[1]?.kind !== "branch")) {
|
|
853
|
+
throw new Error("Integration checkout cleanup requires promotion or explicit supersession.");
|
|
854
|
+
}
|
|
855
|
+
if (index === state.generations.length - 1 && generation.status !== "promoted"
|
|
856
|
+
&& state.candidates.some((candidate) => candidate.worker !== "retained" && candidate.decision !== "rejected")) {
|
|
857
|
+
throw new Error(`Generation ${generation.number} released an unpromoted worker.`);
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
if (refresh?.status === "pending" && state.generations.at(-1)?.status !== "superseded" && state.generations.length) {
|
|
861
|
+
throw new Error("Pending refresh must freeze the old generation.");
|
|
862
|
+
}
|
|
863
|
+
const promoted = state.generations.at(-1)?.status === "promoted";
|
|
864
|
+
if (state.candidates.some((candidate) => candidate.worker !== "retained" && candidate.decision !== "rejected" && (!promoted
|
|
865
|
+
|| !state.generations.at(-1).stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt)))) {
|
|
866
|
+
throw new Error("Worker release requires exact promoted inclusion.");
|
|
867
|
+
}
|
|
868
|
+
return state;
|
|
869
|
+
}
|
|
870
|
+
/** Passing final evidence is bound to the exact assembled identity, not any worker tip. */
|
|
871
|
+
export function integrationGenerationPasses(generation, request) {
|
|
872
|
+
return Boolean(generation.combinedTip && isCleanCommitted(generation.combinedTip)
|
|
873
|
+
&& generation.stages.length === generation.order.length
|
|
874
|
+
&& generation.stages.every((stage, index) => stage.status === "staged" && stage.taskId === generation.order[index])
|
|
875
|
+
&& generation.checks?.phase === "final"
|
|
876
|
+
&& checkBatchPasses(generation.checks, request.finalChecks, generation.combinedTip)
|
|
877
|
+
&& (!request.finalJudgment || reviewEvidencePasses(generation.review, "final", request.finalJudgment.criterion, generation.expectedMain, generation.combinedTip)));
|
|
878
|
+
}
|
|
879
|
+
export function parseRunState(value) {
|
|
880
|
+
if (value && typeof value === "object" && !Array.isArray(value)
|
|
881
|
+
&& "version" in value && value.version !== RUN_STATE_VERSION) {
|
|
882
|
+
throw new Error(`Unsupported pi-subagent state version ${String(value.version)}; expected ${RUN_STATE_VERSION}.`);
|
|
883
|
+
}
|
|
884
|
+
if (!Check(RunStateSchema, value)) {
|
|
885
|
+
const first = Errors(RunStateSchema, value)[0];
|
|
886
|
+
const detail = first ? ` at ${first.instancePath || "/"}: ${first.message}` : "";
|
|
887
|
+
throw new Error(`Unsupported or malformed pi-subagent v${RUN_STATE_VERSION} state${detail}.`);
|
|
888
|
+
}
|
|
889
|
+
const state = value;
|
|
890
|
+
const request = parseExecuteRequest(state.request);
|
|
891
|
+
parseIntegrationState(state.integration, request);
|
|
892
|
+
const refresh = state.integration.refresh;
|
|
893
|
+
if (refresh && (refresh.generation === 1 && !sameIdentity(refresh.from, state.requestStartMain)
|
|
894
|
+
|| (state.integration.generations.at(-1)?.status !== "promoted" && !sameIdentity(refresh.to, state.main)))) {
|
|
895
|
+
throw new Error("Refreshed Main identity breaks recorded lineage.");
|
|
896
|
+
}
|
|
897
|
+
for (const wave of state.waves) {
|
|
898
|
+
for (const id of wave.taskIds) {
|
|
899
|
+
const definition = request.tasks.find((task) => task.id === id);
|
|
900
|
+
if (!definition)
|
|
901
|
+
throw new Error(`Wave has an unknown task ${id}.`);
|
|
902
|
+
const changesetParents = new Set();
|
|
903
|
+
const visit = (task) => {
|
|
904
|
+
for (const source of [...task.dependsOn, ...task.contextFrom]) {
|
|
905
|
+
const predecessor = request.tasks.find((item) => item.id === source);
|
|
906
|
+
if (predecessor.kind === "changeset")
|
|
907
|
+
changesetParents.add(source);
|
|
908
|
+
else
|
|
909
|
+
visit(predecessor);
|
|
910
|
+
}
|
|
911
|
+
};
|
|
912
|
+
visit(definition);
|
|
913
|
+
if (changesetParents.size && !state.integration.generations.some((generation) => generation.stages.some((stage) => stage.status === "staged" && stage.tip
|
|
914
|
+
&& sameIdentity(stage.tip, wave.base) && [...changesetParents].every((source) => generation.stages.some((prior) => prior.taskId === source && prior.status === "staged"))))) {
|
|
915
|
+
throw new Error(`Wave for ${id} lacks an exact staged dependency snapshot.`);
|
|
916
|
+
}
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
if (state.createdAt > state.updatedAt || state.correctionCount > state.policy.maxCorrections) {
|
|
920
|
+
throw new Error(`Malformed pi-subagent v${RUN_STATE_VERSION} timestamps or correction policy.`);
|
|
921
|
+
}
|
|
922
|
+
const recordedCorrections = state.tasks.reduce((count, task) => count + (task.kind === "changeset"
|
|
923
|
+
? task.attempts.reduce((sum, attempt) => sum + attempt.prompts.filter((prompt) => prompt.kind === "correction").length, 0)
|
|
924
|
+
: 0), 0);
|
|
925
|
+
if (recordedCorrections !== state.correctionCount)
|
|
926
|
+
throw new Error("Malformed pi-subagent correction count.");
|
|
927
|
+
if (state.tasks.length !== request.tasks.length)
|
|
928
|
+
throw new Error(`Malformed pi-subagent v${RUN_STATE_VERSION} task count.`);
|
|
929
|
+
for (let index = 0; index < request.tasks.length; index += 1) {
|
|
930
|
+
const definition = request.tasks[index];
|
|
931
|
+
const taskState = state.tasks[index];
|
|
932
|
+
if (taskState.taskId !== definition.id)
|
|
933
|
+
throw new Error(`Malformed pi-subagent v${RUN_STATE_VERSION} task order.`);
|
|
934
|
+
if (definition.kind === "text") {
|
|
935
|
+
if (taskState.kind !== "text")
|
|
936
|
+
throw new Error(`Malformed task kind for ${definition.id}.`);
|
|
937
|
+
validateTextTaskState(taskState);
|
|
938
|
+
continue;
|
|
939
|
+
}
|
|
940
|
+
if (taskState.kind !== "changeset")
|
|
941
|
+
throw new Error(`Malformed task kind for ${definition.id}.`);
|
|
942
|
+
if (taskState.status === "pending" && taskState.attempts.length && !taskState.attempts.at(-1)?.superseded) {
|
|
943
|
+
throw new Error(`Pending task ${definition.id} lacks superseded attempt evidence.`);
|
|
944
|
+
}
|
|
945
|
+
for (const [attemptIndex, attempt] of taskState.attempts.entries()) {
|
|
946
|
+
if (attempt.number !== attemptIndex + 1)
|
|
947
|
+
throw new Error(`Malformed attempt order for ${definition.id}.`);
|
|
948
|
+
if (attempt.preliminaryChecks) {
|
|
949
|
+
if (attempt.preliminaryChecks.phase !== "preliminary")
|
|
950
|
+
throw new Error(`Malformed preliminary check phase for ${definition.id}.`);
|
|
951
|
+
validateCheckBatchEvidence(attempt.preliminaryChecks, definition.checks, `Preliminary checks for ${definition.id}`);
|
|
952
|
+
}
|
|
953
|
+
if ((attempt.candidate === undefined) !== (attempt.candidateBase === undefined)) {
|
|
954
|
+
throw new Error(`Candidate and candidate base for ${definition.id} must be recorded together.`);
|
|
955
|
+
}
|
|
956
|
+
if (attempt.candidate && (!isCleanCommitted(attempt.candidate) || !isCleanCommitted(attempt.candidateBase))) {
|
|
957
|
+
throw new Error(`Candidate lineage for ${definition.id} must be clean and committed.`);
|
|
958
|
+
}
|
|
959
|
+
if (attempt.preliminaryReview && (!definition.judgment || !attempt.candidate || !attempt.candidateBase
|
|
960
|
+
|| attempt.preliminaryReview.phase !== "preliminary"
|
|
961
|
+
|| attempt.preliminaryReview.criterion !== definition.judgment.criterion
|
|
962
|
+
|| !sameIdentity(attempt.preliminaryReview.base, attempt.candidateBase)
|
|
963
|
+
|| !sameIdentity(attempt.preliminaryReview.tip, attempt.candidate))) {
|
|
964
|
+
throw new Error(`Preliminary review for ${definition.id} targets another candidate.`);
|
|
965
|
+
}
|
|
966
|
+
if (attempt.readiness) {
|
|
967
|
+
if (!attempt.candidate || !attempt.candidateBase
|
|
968
|
+
|| !sameIdentity(attempt.readiness.candidate, attempt.candidate)
|
|
969
|
+
|| !sameIdentity(attempt.readiness.base, attempt.candidateBase)
|
|
970
|
+
|| !checkBatchPasses(attempt.preliminaryChecks, definition.checks, attempt.candidate)) {
|
|
971
|
+
throw new Error(`Readiness for ${definition.id} does not match exact passing preliminary evidence.`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
if (attempt.termination) {
|
|
975
|
+
const termination = attempt.termination;
|
|
976
|
+
const ownedAgent = [...attempt.allocations].reverse().find((allocation) => allocation.kind === "agent");
|
|
977
|
+
if (!ownedAgent || ownedAgent.status !== "owned" || termination.workerId !== ownedAgent.agentName) {
|
|
978
|
+
throw new Error(`Worker termination for ${definition.id} does not match the exact owned agent.`);
|
|
979
|
+
}
|
|
980
|
+
if (termination.status === "terminated" ? termination.at === undefined || termination.failure !== undefined
|
|
981
|
+
: termination.status === "terminating" ? termination.at !== undefined || termination.failure !== undefined
|
|
982
|
+
: termination.at !== undefined || !termination.failure?.trim()) {
|
|
983
|
+
throw new Error(`Worker termination for ${definition.id} has inconsistent status evidence.`);
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
const initialPrompt = attempt.prompts[0];
|
|
987
|
+
if (attempt.prompts.filter((prompt) => prompt.kind === "correction").length > 1) {
|
|
988
|
+
throw new Error(`Malformed repeated correction history for ${definition.id}.`);
|
|
989
|
+
}
|
|
990
|
+
if (initialPrompt && initialPrompt.kind !== "initial") {
|
|
991
|
+
throw new Error(`Malformed initial prompt history for ${definition.id}.`);
|
|
992
|
+
}
|
|
993
|
+
if (attempt.prompts.some((prompt) => !isCleanCommitted(prompt.preCandidate))
|
|
994
|
+
|| (initialPrompt && initialPrompt.preCandidate.head !== attempt.waveBase.head)) {
|
|
995
|
+
throw new Error(`Prompt history for ${definition.id} lacks a clean exact pre-prompt candidate.`);
|
|
996
|
+
}
|
|
997
|
+
for (let promptIndex = 0; promptIndex < attempt.prompts.length; promptIndex += 1) {
|
|
998
|
+
const prompt = attempt.prompts[promptIndex];
|
|
999
|
+
if ((prompt.kind === "followup") !== (prompt.instruction !== undefined)) {
|
|
1000
|
+
throw new Error(`Follow-up prompt history for ${definition.id} has invalid instruction evidence.`);
|
|
1001
|
+
}
|
|
1002
|
+
if (prompt.instruction !== undefined) {
|
|
1003
|
+
requireExactAllocationText(prompt.instruction, `Follow-up prompt instruction for ${definition.id}`);
|
|
1004
|
+
}
|
|
1005
|
+
if (promptIndex === 0)
|
|
1006
|
+
continue;
|
|
1007
|
+
if (prompt.kind === "initial")
|
|
1008
|
+
throw new Error(`Malformed repeated initial prompt for ${definition.id}.`);
|
|
1009
|
+
const previous = attempt.prompts[promptIndex - 1];
|
|
1010
|
+
if (!sameIdentity(prompt.preCandidate, previous.candidate ?? previous.preCandidate)) {
|
|
1011
|
+
throw new Error(`Prompt ${promptIndex + 1} for ${definition.id} breaks exact candidate lineage.`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
if (attempt.cleanup.some((step, cleanupIndex) => step.kind !== CLEANUP_KINDS[cleanupIndex])) {
|
|
1015
|
+
throw new Error(`Malformed cleanup sequence for ${definition.id}.`);
|
|
1016
|
+
}
|
|
1017
|
+
if (attempt.allocations.filter((allocation) => allocation.status === "unknown").length > 1) {
|
|
1018
|
+
throw new Error(`Attempt for ${definition.id} has more than one ambiguous allocation result.`);
|
|
1019
|
+
}
|
|
1020
|
+
for (const allocation of attempt.allocations) {
|
|
1021
|
+
if (allocation.token !== attempt.correlationToken) {
|
|
1022
|
+
throw new Error(`${allocation.kind} allocation for ${definition.id} has the wrong correlation token.`);
|
|
1023
|
+
}
|
|
1024
|
+
if (allocation.status === "unknown") {
|
|
1025
|
+
if (!allocation.failure?.trim())
|
|
1026
|
+
throw new Error(`Unknown ${allocation.kind} allocation for ${definition.id} lacks a failure.`);
|
|
1027
|
+
for (const resource of allocation.possibleResources ?? []) {
|
|
1028
|
+
requireExactAllocationText(resource, `Unknown ${allocation.kind} allocation possible resource`);
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
else if (allocation.possibleResources) {
|
|
1032
|
+
throw new Error(`Only unknown allocations may record possible resources for ${definition.id}.`);
|
|
1033
|
+
}
|
|
1034
|
+
if (allocation.kind === "worktree") {
|
|
1035
|
+
if (allocation.status === "owned" && !hasWorktreePlan(allocation)) {
|
|
1036
|
+
throw new Error(`Owned worktree allocation for ${definition.id} lacks exact plan fields.`);
|
|
1037
|
+
}
|
|
1038
|
+
if (hasWorktreePlan(allocation)) {
|
|
1039
|
+
requireAbsoluteAllocationPath(allocation.worktree.path, `Worktree allocation path for ${definition.id}`);
|
|
1040
|
+
requireAbsoluteAllocationPath(allocation.worktree.cwd, `Worktree allocation cwd for ${definition.id}`);
|
|
1041
|
+
requireAbsoluteAllocationPath(allocation.worktree.repoRoot, `Worktree allocation repository root for ${definition.id}`);
|
|
1042
|
+
requireExactAllocationText(allocation.worktree.branch, `Worktree allocation branch for ${definition.id}`);
|
|
1043
|
+
if (allocation.worktree.path !== allocation.worktree.cwd)
|
|
1044
|
+
throw new Error(`Worktree path and cwd for ${definition.id} must match exactly.`);
|
|
1045
|
+
if (allocation.worktree.baseCommit !== attempt.waveBase.head) {
|
|
1046
|
+
throw new Error(`Worktree plan for ${definition.id} does not match its recorded wave base.`);
|
|
1047
|
+
}
|
|
1048
|
+
}
|
|
1049
|
+
continue;
|
|
1050
|
+
}
|
|
1051
|
+
const worktree = attempt.allocations.find((candidate) => candidate.kind === "worktree" && candidate.status === "owned" && hasWorktreePlan(candidate));
|
|
1052
|
+
if (!worktree)
|
|
1053
|
+
throw new Error(`${allocation.kind} allocation for ${definition.id} lacks its exact owned worktree parent.`);
|
|
1054
|
+
if (allocation.kind === "workspace") {
|
|
1055
|
+
for (const [field, value] of Object.entries({
|
|
1056
|
+
label: allocation.label, worktreeCwd: allocation.worktreeCwd, mainRoot: allocation.mainRoot,
|
|
1057
|
+
repoKey: allocation.repoKey, herdrRepoRoot: allocation.herdrRepoRoot,
|
|
1058
|
+
}))
|
|
1059
|
+
requireExactAllocationText(value, `Workspace allocation ${field} for ${definition.id}`);
|
|
1060
|
+
for (const [field, value] of Object.entries({
|
|
1061
|
+
worktreeCwd: allocation.worktreeCwd, mainRoot: allocation.mainRoot,
|
|
1062
|
+
repoKey: allocation.repoKey, herdrRepoRoot: allocation.herdrRepoRoot,
|
|
1063
|
+
}))
|
|
1064
|
+
requireAbsoluteAllocationPath(value, `Workspace allocation ${field} for ${definition.id}`);
|
|
1065
|
+
if (allocation.worktreeCwd !== worktree.worktree.cwd || allocation.mainRoot !== worktree.worktree.repoRoot) {
|
|
1066
|
+
throw new Error(`Workspace allocation for ${definition.id} drifted from its exact worktree parent.`);
|
|
1067
|
+
}
|
|
1068
|
+
const results = [allocation.workspaceId, allocation.rootTabId, allocation.rootPaneId];
|
|
1069
|
+
if (allocation.status === "owned" && results.some((value) => value === undefined)) {
|
|
1070
|
+
throw new Error(`Owned workspace allocation for ${definition.id} lacks exact result fields.`);
|
|
1071
|
+
}
|
|
1072
|
+
if (allocation.status !== "owned" && results.some((value) => value !== undefined)) {
|
|
1073
|
+
throw new Error(`Unowned workspace allocation for ${definition.id} must not claim result fields.`);
|
|
1074
|
+
}
|
|
1075
|
+
for (const value of results)
|
|
1076
|
+
if (value !== undefined)
|
|
1077
|
+
requireExactAllocationText(value, `Workspace allocation result for ${definition.id}`);
|
|
1078
|
+
continue;
|
|
1079
|
+
}
|
|
1080
|
+
const workspace = attempt.allocations.find((candidate) => candidate.kind === "workspace" && candidate.status === "owned");
|
|
1081
|
+
if (!workspace?.workspaceId || !workspace.rootTabId || !workspace.rootPaneId) {
|
|
1082
|
+
throw new Error(`${allocation.kind} allocation for ${definition.id} lacks its exact owned workspace parent.`);
|
|
1083
|
+
}
|
|
1084
|
+
if (allocation.kind === "worker_tab") {
|
|
1085
|
+
for (const [field, value] of Object.entries({
|
|
1086
|
+
label: allocation.label, workspaceId: allocation.workspaceId,
|
|
1087
|
+
workspaceRootTabId: allocation.workspaceRootTabId, workspaceRootPaneId: allocation.workspaceRootPaneId,
|
|
1088
|
+
worktreeCwd: allocation.worktreeCwd, leasePath: allocation.leasePath,
|
|
1089
|
+
}))
|
|
1090
|
+
requireExactAllocationText(value, `Worker-tab allocation ${field} for ${definition.id}`);
|
|
1091
|
+
if (allocation.workspaceId !== workspace.workspaceId
|
|
1092
|
+
|| allocation.workspaceRootTabId !== workspace.rootTabId
|
|
1093
|
+
|| allocation.workspaceRootPaneId !== workspace.rootPaneId
|
|
1094
|
+
|| allocation.worktreeCwd !== worktree.worktree.cwd) {
|
|
1095
|
+
throw new Error(`Worker-tab allocation for ${definition.id} drifted from its exact parents.`);
|
|
1096
|
+
}
|
|
1097
|
+
const results = [allocation.tabId, allocation.paneId];
|
|
1098
|
+
if (allocation.status === "owned" && results.some((value) => value === undefined)) {
|
|
1099
|
+
throw new Error(`Owned worker-tab allocation for ${definition.id} lacks exact result fields.`);
|
|
1100
|
+
}
|
|
1101
|
+
if (allocation.status !== "owned" && results.some((value) => value !== undefined)) {
|
|
1102
|
+
throw new Error(`Unowned worker-tab allocation for ${definition.id} must not claim result fields.`);
|
|
1103
|
+
}
|
|
1104
|
+
for (const value of results)
|
|
1105
|
+
if (value !== undefined)
|
|
1106
|
+
requireExactAllocationText(value, `Worker-tab allocation result for ${definition.id}`);
|
|
1107
|
+
continue;
|
|
1108
|
+
}
|
|
1109
|
+
const workerTab = attempt.allocations.find((candidate) => candidate.kind === "worker_tab" && candidate.status === "owned");
|
|
1110
|
+
if (!workerTab?.tabId || !workerTab.paneId)
|
|
1111
|
+
throw new Error(`Agent allocation for ${definition.id} lacks its exact owned worker-tab parent.`);
|
|
1112
|
+
for (const [field, value] of Object.entries({
|
|
1113
|
+
agentName: allocation.agentName, workspaceId: allocation.workspaceId, tabId: allocation.tabId,
|
|
1114
|
+
paneId: allocation.paneId, worktreeCwd: allocation.worktreeCwd, leasePath: allocation.leasePath,
|
|
1115
|
+
}))
|
|
1116
|
+
requireExactAllocationText(value, `Agent allocation ${field} for ${definition.id}`);
|
|
1117
|
+
if (allocation.workspaceId !== workspace.workspaceId
|
|
1118
|
+
|| allocation.tabId !== workerTab.tabId
|
|
1119
|
+
|| allocation.paneId !== workerTab.paneId
|
|
1120
|
+
|| allocation.worktreeCwd !== worktree.worktree.cwd
|
|
1121
|
+
|| allocation.leasePath !== workerTab.leasePath) {
|
|
1122
|
+
throw new Error(`Agent allocation for ${definition.id} drifted from its exact parents.`);
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
if (taskState.status === "rejected" && (!taskState.attempts.length
|
|
1127
|
+
|| !state.integration.candidates.some((candidate) => candidate.taskId === definition.id
|
|
1128
|
+
&& candidate.decision === "rejected" && candidate.attempt === taskState.attempts.length)
|
|
1129
|
+
|| taskState.attempts.at(-1)?.termination?.status !== "terminated")) {
|
|
1130
|
+
throw new Error(`Rejected task ${definition.id} lacks explicit decision and proven worker termination.`);
|
|
1131
|
+
}
|
|
1132
|
+
if (taskState.status === "completed") {
|
|
1133
|
+
const promoted = state.integration.generations.at(-1);
|
|
1134
|
+
const candidate = state.integration.candidates.find((item) => item.taskId === definition.id
|
|
1135
|
+
&& promoted?.stages.some((stage) => stage.taskId === definition.id && stage.attempt === item.attempt
|
|
1136
|
+
&& sameIdentity(stage.source, item.tip)));
|
|
1137
|
+
if (candidate && promoted?.status === "promoted") {
|
|
1138
|
+
const attempt = taskState.attempts.at(-1);
|
|
1139
|
+
if (candidate.worker === "released" && (attempt.termination?.status !== "terminated"
|
|
1140
|
+
|| attempt.cleanup.some((step) => step.status !== "completed"))) {
|
|
1141
|
+
throw new Error(`Promoted task ${definition.id} claims unproved worker cleanup.`);
|
|
1142
|
+
}
|
|
1143
|
+
}
|
|
1144
|
+
else
|
|
1145
|
+
throw new Error(`Completed task ${definition.id} lacks a promoted Main-owned integration generation.`);
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
if (state.final.checks) {
|
|
1149
|
+
if (state.final.checks.phase !== "final")
|
|
1150
|
+
throw new Error("Malformed final check phase.");
|
|
1151
|
+
validateCheckBatchEvidence(state.final.checks, request.finalChecks, "Final checks");
|
|
1152
|
+
}
|
|
1153
|
+
if (state.recovery) {
|
|
1154
|
+
if (state.recovery.action === "finalize" ? state.recovery.taskId !== undefined : !state.recovery.taskId
|
|
1155
|
+
|| !state.tasks.some((task) => task.taskId === state.recovery.taskId)) {
|
|
1156
|
+
throw new Error("Malformed resume recovery target.");
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
if (state.integration.candidates.length && state.integration.generations.at(-1)?.status !== "promoted"
|
|
1160
|
+
&& (state.accepted || state.status === "completed")) {
|
|
1161
|
+
throw new Error("Unpromoted integration cannot complete a request.");
|
|
1162
|
+
}
|
|
1163
|
+
for (const candidate of state.integration.candidates) {
|
|
1164
|
+
const task = state.tasks.find((item) => item.taskId === candidate.taskId);
|
|
1165
|
+
const attempt = task?.kind === "changeset" ? task.attempts[candidate.attempt - 1] : undefined;
|
|
1166
|
+
if (candidate.decision === "rejected") {
|
|
1167
|
+
const selected = state.integration.generations.at(-1);
|
|
1168
|
+
const releasedWithRevisedAttempt = selected?.status === "promoted" && selected.stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt && !sameIdentity(stage.source, candidate.tip))
|
|
1169
|
+
&& attempt?.cleanup.every((step) => step.status === "completed");
|
|
1170
|
+
const releasedAfterRejection = task?.status === "rejected" && attempt?.termination?.status === "terminated"
|
|
1171
|
+
&& (candidate.worker === "release_pending" || candidate.worker === "released"
|
|
1172
|
+
&& attempt.cleanup.every((step) => step.status === "completed"));
|
|
1173
|
+
if (!attempt || (candidate.worker !== "retained" && !releasedAfterRejection
|
|
1174
|
+
&& !(candidate.worker === "released" && releasedWithRevisedAttempt))) {
|
|
1175
|
+
throw new Error(`Rejected candidate ${candidate.taskId} lost its exact worker accounting.`);
|
|
1176
|
+
}
|
|
1177
|
+
continue;
|
|
1178
|
+
}
|
|
1179
|
+
if (!attempt?.readiness || !sameIdentity(attempt.readiness.base, candidate.base)
|
|
1180
|
+
|| !sameIdentity(attempt.readiness.candidate, candidate.tip)
|
|
1181
|
+
|| (candidate.review !== undefined && JSON.stringify(candidate.review) !== JSON.stringify(attempt.preliminaryReview))
|
|
1182
|
+
|| (state.integration.generations.at(-1)?.status !== "promoted"
|
|
1183
|
+
? attempt.termination || task?.status !== "ready_to_integrate" || candidate.worker !== "retained"
|
|
1184
|
+
: task?.status !== "ready_to_integrate" && task?.status !== "completed")) {
|
|
1185
|
+
throw new Error(`Ready candidate ${candidate.taskId} is not the retained exact worker attempt.`);
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
if (state.status === "completed" && !state.accepted) {
|
|
1189
|
+
throw new Error("A completed request must be accepted with exact final evidence.");
|
|
1190
|
+
}
|
|
1191
|
+
if (state.final.status === "passed" && !state.accepted) {
|
|
1192
|
+
throw new Error("A passed final gate must belong to an accepted request.");
|
|
1193
|
+
}
|
|
1194
|
+
if (state.accepted) {
|
|
1195
|
+
if (state.status !== "completed" || state.final.status !== "passed" || state.tasks.some((task) => task.status !== "completed" && task.status !== "rejected")) {
|
|
1196
|
+
throw new Error(`Malformed accepted pi-subagent v${RUN_STATE_VERSION} state.`);
|
|
1197
|
+
}
|
|
1198
|
+
if (!state.final.identity || !isCleanCommitted(state.final.identity)) {
|
|
1199
|
+
throw new Error("Accepted request lacks a clean final identity.");
|
|
1200
|
+
}
|
|
1201
|
+
const generation = state.integration.generations.at(-1);
|
|
1202
|
+
const acceptedChecks = generation?.status === "promoted"
|
|
1203
|
+
? Boolean(generation.combinedTip && generation.combinedTip.head === state.final.identity.head
|
|
1204
|
+
&& generation.combinedTip.tree === state.final.identity.tree
|
|
1205
|
+
&& generation.promotion?.mainAfter && sameIdentity(generation.promotion.mainAfter, state.final.identity)
|
|
1206
|
+
&& JSON.stringify(generation.checks) === JSON.stringify(state.final.checks)
|
|
1207
|
+
&& integrationGenerationPasses(generation, request))
|
|
1208
|
+
: checkBatchPasses(state.final.checks, request.finalChecks, state.final.identity);
|
|
1209
|
+
if (!acceptedChecks)
|
|
1210
|
+
throw new Error("Accepted request lacks passing final checks on its exact identity.");
|
|
1211
|
+
if (request.finalJudgment && !(generation?.status === "promoted"
|
|
1212
|
+
? JSON.stringify(generation.review) === JSON.stringify(state.final.review)
|
|
1213
|
+
: reviewEvidencePasses(state.final.review, "final", request.finalJudgment.criterion, state.requestStartMain, state.final.identity)))
|
|
1214
|
+
throw new Error("Accepted request lacks an exact passing final review.");
|
|
1215
|
+
if (state.integration.generations.length && state.integration.generations.at(-1)?.status !== "promoted") {
|
|
1216
|
+
throw new Error("Accepted integration requires proven promotion.");
|
|
1217
|
+
}
|
|
1218
|
+
if (generation && (generation.stages.some((stage) => state.tasks.find((task) => task.taskId === stage.taskId)?.status !== "completed")
|
|
1219
|
+
|| state.integration.candidates.some((candidate) => !candidate.decision
|
|
1220
|
+
&& !generation.stages.some((stage) => stage.taskId === candidate.taskId && stage.attempt === candidate.attempt
|
|
1221
|
+
&& sameIdentity(stage.source, candidate.tip))))) {
|
|
1222
|
+
throw new Error("Accepted integration has an unselected candidate or uncompleted selected task.");
|
|
1223
|
+
}
|
|
1224
|
+
if (!sameIdentity(state.main, state.final.identity) || state.acceptedAt === undefined) {
|
|
1225
|
+
throw new Error("Accepted request does not match its final-gate identity.");
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
else if (state.acceptedAt !== undefined) {
|
|
1229
|
+
throw new Error("Unaccepted request cannot have acceptedAt.");
|
|
1230
|
+
}
|
|
1231
|
+
if (state.status === "final_failed" && state.final.status !== "final_failed")
|
|
1232
|
+
throw new Error("Malformed final_failed state.");
|
|
1233
|
+
if (state.status === "superseded" && state.final.status !== "superseded")
|
|
1234
|
+
throw new Error("Malformed superseded state.");
|
|
1235
|
+
return { ...state, request };
|
|
1236
|
+
}
|