@hcflabs/pi-subagent 0.0.0-stage → 27.10.8

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