brainclaw 1.5.4 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/README.md +52 -28
  2. package/dist/brainclaw-vscode.vsix +0 -0
  3. package/dist/cli.js +159 -12
  4. package/dist/commands/assignment-resource.js +182 -0
  5. package/dist/commands/bootstrap-loop.js +206 -0
  6. package/dist/commands/init.js +158 -22
  7. package/dist/commands/loop.js +156 -0
  8. package/dist/commands/loops-handlers.js +110 -55
  9. package/dist/commands/mcp-read-handlers.js +45 -4
  10. package/dist/commands/mcp.js +628 -205
  11. package/dist/commands/questions.js +180 -0
  12. package/dist/commands/reply.js +190 -0
  13. package/dist/commands/session-end.js +105 -3
  14. package/dist/commands/session-start.js +32 -53
  15. package/dist/commands/setup.js +87 -48
  16. package/dist/commands/switch.js +21 -1
  17. package/dist/core/agentrun-reconciler.js +65 -0
  18. package/dist/core/agentruns.js +10 -0
  19. package/dist/core/assignments.js +29 -10
  20. package/dist/core/claims.js +29 -0
  21. package/dist/core/context.js +1 -1
  22. package/dist/core/coordination.js +1 -1
  23. package/dist/core/dispatch-status.js +219 -0
  24. package/dist/core/entity-operations.js +166 -10
  25. package/dist/core/entity-registry.js +11 -10
  26. package/dist/core/execution-adapters.js +38 -2
  27. package/dist/core/facade-schema.js +55 -0
  28. package/dist/core/federation-cloud.js +27 -12
  29. package/dist/core/federation-materialize.js +57 -0
  30. package/dist/core/instruction-templates.js +2 -0
  31. package/dist/core/loops/bootstrap-acquire.js +195 -0
  32. package/dist/core/loops/facade-schema.js +68 -1
  33. package/dist/core/loops/hooks/bootstrap-write.js +144 -0
  34. package/dist/core/loops/hooks/notify-operator.js +148 -0
  35. package/dist/core/loops/hooks/survey-source-reader.js +256 -0
  36. package/dist/core/loops/index.js +8 -2
  37. package/dist/core/loops/next-expected.js +63 -0
  38. package/dist/core/loops/presets/bootstrap.js +75 -0
  39. package/dist/core/loops/presets/index.js +16 -0
  40. package/dist/core/loops/store.js +224 -4
  41. package/dist/core/loops/types.js +346 -1
  42. package/dist/core/loops/verbs.js +739 -6
  43. package/dist/core/schema.js +31 -2
  44. package/dist/core/state.js +62 -0
  45. package/dist/core/store-resolution.js +26 -16
  46. package/dist/facts.js +7 -5
  47. package/dist/facts.json +6 -4
  48. package/docs/cli.md +115 -30
  49. package/docs/concepts/dispatch-lifecycle.md +228 -0
  50. package/docs/concepts/loop-engine.md +55 -0
  51. package/docs/concepts/multi-agent-workflows.md +167 -166
  52. package/docs/concepts/troubleshooting.md +10 -2
  53. package/docs/integrations/agents.md +14 -14
  54. package/docs/integrations/codex.md +15 -12
  55. package/docs/integrations/mcp.md +10 -4
  56. package/docs/integrations/overview.md +11 -0
  57. package/docs/playbooks/productivity/index.md +3 -3
  58. package/docs/quickstart-existing-project.md +48 -28
  59. package/docs/quickstart.md +42 -28
  60. package/package.json +1 -1
@@ -7,8 +7,13 @@ export const REVIEW_MODES = ['asymmetric', 'symmetric'];
7
7
  * Slot lifecycle states. `done` / `failed` / `cancelled` are terminal and
8
8
  * mirror the `complete_turn` outcome so a caller reading the thread can
9
9
  * observe the per-slot outcome without replaying the event journal.
10
+ *
11
+ * pln#508 step 1 — `waiting_input` added to support the bootstrap loop's
12
+ * operator-question primitive. A slot in `waiting_input` is non-terminal:
13
+ * the engine resumes it back to `working` once its open_question is
14
+ * answered (see request_input/provide_input intents, pln#508 step 2).
10
15
  */
11
- export const SLOT_STATUSES = ['open', 'assigned', 'working', 'done', 'failed', 'cancelled'];
16
+ export const SLOT_STATUSES = ['open', 'assigned', 'working', 'waiting_input', 'done', 'failed', 'cancelled'];
12
17
  export const TERMINAL_SLOT_STATUSES = ['done', 'failed', 'cancelled'];
13
18
  export const LOOP_REF_KINDS = ['plan', 'sequence', 'claim', 'handoff', 'candidate', 'message'];
14
19
  export const LoopRefSchema = z.object({
@@ -103,6 +108,27 @@ export const LoopIterationSchema = z.object({
103
108
  export const LoopProtocolConfigSchema = z.object({
104
109
  review_mode: z.enum(REVIEW_MODES).optional(),
105
110
  iteration: LoopIterationSchema.optional(),
111
+ /**
112
+ * pln#508 step 1 — protocol preset selector. When set (e.g. `'bootstrap'`),
113
+ * the coordinate facade routes preset-specific behaviors (close hook,
114
+ * phase config, dispatch eligibility) keyed off this value. Loops without
115
+ * a preset fall back to kind-default behavior.
116
+ */
117
+ preset: z.string().min(1).optional(),
118
+ /**
119
+ * pln#508 step 1 — cap on operator_question artifacts per loop. Enforced
120
+ * at request_input intent time (pln#508 step 2). Bootstrap preset sets
121
+ * this to 3 to prevent the "agent defers everything to the human" failure
122
+ * mode documented in feedback_agent_autonomy_gap.md.
123
+ */
124
+ max_operator_questions: z.number().int().positive().optional(),
125
+ /**
126
+ * pln#508 step 1 — ISO-8601 duration string (e.g. 'P7D') capping how long
127
+ * a loop may stay in status='paused' before the timeout machinery fires.
128
+ * Used by request_input artifacts with on_timeout policy. Bootstrap
129
+ * preset defaults to 'P7D'.
130
+ */
131
+ max_pause_duration: z.string().min(1).optional(),
106
132
  });
107
133
  export const LoopSlotSchema = z.object({
108
134
  slot_id: z.string().regex(/^lsl_[0-9a-z]+$/),
@@ -114,6 +140,189 @@ export const LoopSlotSchema = z.object({
114
140
  phase: z.string().optional(),
115
141
  status: z.enum(SLOT_STATUSES),
116
142
  });
143
+ // ───────────────────────────────────────────────────────────────────────
144
+ // pln#508 step 1 — bootstrap loop foundation: operator-interaction schemas
145
+ // ───────────────────────────────────────────────────────────────────────
146
+ //
147
+ // These body schemas validate the JSON payload encoded in LoopArtifact.body
148
+ // for the artifact types introduced by the bootstrap loop preset and
149
+ // generally reusable for any human-in-the-loop primitive. Step 2's
150
+ // request_input / provide_input handlers parse and validate via
151
+ // `KNOWN_ARTIFACT_BODY_SCHEMAS[type]`.
152
+ /** Where the operator pause applies: just the asking slot, or the whole loop. */
153
+ export const PAUSE_SCOPES = ['slot', 'loop'];
154
+ /** What the engine should do when an operator_question times out (Phase 0 spec §6). */
155
+ export const ON_TIMEOUT_POLICIES = ['use_default', 'cancel_loop', 'continue_incomplete'];
156
+ /** How an operator_answer arrived (Phase 0 spec §2). */
157
+ export const RESOLVED_VIA = ['answer', 'choose', 'skip', 'timeout_default'];
158
+ /** Reasons a loop may be paused. Maintained alongside LoopThread.pause_reason. */
159
+ export const PAUSE_REASONS = ['awaiting_operator', 'awaiting_file_apply'];
160
+ export const OperatorQuestionOptionSchema = z.object({
161
+ id: z.string().min(1),
162
+ label: z.string().min(1),
163
+ tradeoff: z.string().optional(),
164
+ });
165
+ /**
166
+ * Operator question artifact body. The Champion records the question the
167
+ * operator must answer, with `evidence` (anti-autonomy-gap: the slot must
168
+ * show it tried), an optional `suggested_default` (used by skip / timeout
169
+ * resolution), and an optional `options` set (2..4) that enables structured
170
+ * `--choose` replies via the CLI.
171
+ */
172
+ export const OperatorQuestionBodySchema = z
173
+ .object({
174
+ question_id: z.string().regex(/^qst_[0-9a-z]+$/),
175
+ question_text: z.string().min(1).max(500),
176
+ evidence: z.array(z.string().min(1)).min(1),
177
+ suggested_default: z.string().optional(),
178
+ options: z.array(OperatorQuestionOptionSchema).min(2).max(4).optional(),
179
+ pause_scope: z.enum(PAUSE_SCOPES),
180
+ on_timeout: z.enum(ON_TIMEOUT_POLICIES),
181
+ timeout_at: z.string().datetime().optional(),
182
+ /**
183
+ * pln#508 step 2 — slot that asked the question. Set by the
184
+ * `request_input` handler from its `slot_id` parameter so the
185
+ * `provide_input` handler can find the right slot to resume when
186
+ * `pause_scope='slot'`. Optional for forward compatibility with
187
+ * questions created from non-slot contexts (e.g. timeout-synthesized
188
+ * answers don't need it; pause_scope='loop' doesn't need it either).
189
+ */
190
+ by_slot_id: z.string().min(1).optional(),
191
+ })
192
+ .superRefine((q, ctx) => {
193
+ if (q.options && q.suggested_default !== undefined) {
194
+ const optionIds = new Set(q.options.map((o) => o.id));
195
+ if (!optionIds.has(q.suggested_default)) {
196
+ ctx.addIssue({
197
+ code: z.ZodIssueCode.custom,
198
+ message: `OperatorQuestion.suggested_default "${q.suggested_default}" must match an options[].id when options is present`,
199
+ path: ['suggested_default'],
200
+ });
201
+ }
202
+ }
203
+ if (q.on_timeout === 'use_default' && q.suggested_default === undefined) {
204
+ ctx.addIssue({
205
+ code: z.ZodIssueCode.custom,
206
+ message: 'OperatorQuestion.on_timeout=use_default requires suggested_default to be set',
207
+ path: ['on_timeout'],
208
+ });
209
+ }
210
+ });
211
+ /**
212
+ * Operator answer artifact body. `replies_to` correlates to a question_id
213
+ * tracked in `LoopThread.open_questions`. `by` distinguishes human-provided
214
+ * answers from system-synthesized ones (timeout default fallback); synthetic
215
+ * answers MUST be flagged so audit can identify them.
216
+ */
217
+ export const OperatorAnswerBodySchema = z
218
+ .object({
219
+ replies_to: z.string().regex(/^qst_[0-9a-z]+$/),
220
+ resolved_via: z.enum(RESOLVED_VIA),
221
+ answer_text: z.string().optional(),
222
+ chosen_option_id: z.string().optional(),
223
+ by: z.enum(['operator', 'system']),
224
+ synthetic: z.boolean().optional(),
225
+ })
226
+ .superRefine((a, ctx) => {
227
+ if (a.by === 'system' && a.resolved_via !== 'timeout_default') {
228
+ ctx.addIssue({
229
+ code: z.ZodIssueCode.custom,
230
+ message: 'OperatorAnswer.by="system" requires resolved_via="timeout_default" (synthetic answers can only be timeout-induced)',
231
+ path: ['resolved_via'],
232
+ });
233
+ }
234
+ if (a.resolved_via === 'timeout_default' && a.by !== 'system') {
235
+ ctx.addIssue({
236
+ code: z.ZodIssueCode.custom,
237
+ message: 'OperatorAnswer.resolved_via="timeout_default" requires by="system"',
238
+ path: ['by'],
239
+ });
240
+ }
241
+ if (a.by === 'system' && a.synthetic !== true) {
242
+ ctx.addIssue({
243
+ code: z.ZodIssueCode.custom,
244
+ message: 'OperatorAnswer.by="system" must have synthetic=true for audit clarity',
245
+ path: ['synthetic'],
246
+ });
247
+ }
248
+ const hasText = a.answer_text !== undefined;
249
+ const hasChosen = a.chosen_option_id !== undefined;
250
+ if (hasText && hasChosen) {
251
+ ctx.addIssue({
252
+ code: z.ZodIssueCode.custom,
253
+ message: 'OperatorAnswer must have exactly one of {answer_text, chosen_option_id}, not both',
254
+ path: ['answer_text'],
255
+ });
256
+ }
257
+ if (!hasText && !hasChosen) {
258
+ ctx.addIssue({
259
+ code: z.ZodIssueCode.custom,
260
+ message: 'OperatorAnswer must have exactly one of {answer_text, chosen_option_id}',
261
+ path: ['answer_text'],
262
+ });
263
+ }
264
+ });
265
+ /**
266
+ * Ref-based artifact body — used by project_md_draft, project_md_final,
267
+ * signals_report, file_diff. The artifact's actual content lives at
268
+ * `.brainclaw/coordination/loops/<loop_id>/artifacts/<ref>`. The body
269
+ * JSON itself carries only metadata: path, size, content hash. This sidesteps
270
+ * the 4 KiB LOOP_ARTIFACT_BODY_MAX_BYTES limit empirically hit during the
271
+ * pln#508 design session (PROJECT.md + AGENTS.md = 6,714 bytes; unified
272
+ * diffs are bigger).
273
+ */
274
+ export const RefBasedArtifactBodySchema = z.object({
275
+ ref: z.string().min(1),
276
+ byte_count: z.number().int().nonnegative(),
277
+ sha256: z.string().regex(/^[0-9a-f]{64}$/),
278
+ });
279
+ /**
280
+ * Set of artifact `type` strings whose `body` MUST be a JSON-encoded
281
+ * RefBasedArtifactBody. Validation lives in LoopArtifactSchema's
282
+ * superRefine below — older artifact types (proposal, critique, plan_draft,
283
+ * etc.) keep freeform-string body semantics for backward compatibility.
284
+ */
285
+ export const REF_BASED_ARTIFACT_TYPES = new Set([
286
+ 'project_md_draft',
287
+ 'project_md_final',
288
+ 'signals_report',
289
+ 'file_diff',
290
+ ]);
291
+ /**
292
+ * Lookup table mapping known artifact types to their body Zod schemas.
293
+ * Step 2 handlers (request_input/provide_input) parse `artifact.body` as
294
+ * JSON and call `safeParse` on `KNOWN_ARTIFACT_BODY_SCHEMAS[type]`.
295
+ *
296
+ * Body shapes fall into two categories:
297
+ * - RefBasedArtifactBodySchema: `body` is JSON metadata for a file written
298
+ * under `.brainclaw/loops/threads/<loop_id>/artifacts/<ref>`.
299
+ * - Inline body schemas: `body` is the complete small JSON payload.
300
+ *
301
+ * Schema definitions live above in this file. Keep this table explicit so
302
+ * attach-call errors can name the expected shape for each known type.
303
+ *
304
+ * Types not listed keep the legacy freeform-body behavior — no body schema
305
+ * is enforced. This preserves backward compatibility with proposal / critique
306
+ * / revision / plan_draft / change_summary artifacts produced before pln#508.
307
+ */
308
+ export const KNOWN_ARTIFACT_BODY_SCHEMAS = {
309
+ // inline JSON body: body = JSON.stringify({ ...fields per OperatorQuestionBodySchema })
310
+ operator_question: OperatorQuestionBodySchema,
311
+ // inline JSON body: body = JSON.stringify({ ...fields per OperatorAnswerBodySchema })
312
+ operator_answer: OperatorAnswerBodySchema,
313
+ // ref-based: body = JSON.stringify({ ref, byte_count, sha256 })
314
+ // Ref file lives at .brainclaw/loops/threads/<loop_id>/artifacts/<ref>
315
+ project_md_draft: RefBasedArtifactBodySchema,
316
+ // ref-based: body = JSON.stringify({ ref, byte_count, sha256 })
317
+ // Ref file lives at .brainclaw/loops/threads/<loop_id>/artifacts/<ref>
318
+ project_md_final: RefBasedArtifactBodySchema,
319
+ // ref-based: body = JSON.stringify({ ref, byte_count, sha256 })
320
+ // Ref file lives at .brainclaw/loops/threads/<loop_id>/artifacts/<ref>
321
+ signals_report: RefBasedArtifactBodySchema,
322
+ // ref-based: body = JSON.stringify({ ref, byte_count, sha256 })
323
+ // Ref file lives at .brainclaw/loops/threads/<loop_id>/artifacts/<ref>
324
+ file_diff: RefBasedArtifactBodySchema,
325
+ };
117
326
  export const LoopArtifactSchema = z
118
327
  .object({
119
328
  artifact_id: z.string().min(1),
@@ -160,11 +369,48 @@ export const LoopArtifactSchema = z
160
369
  });
161
370
  }
162
371
  }
372
+ // pln#508 step 1 — validate the body of known-typed artifacts against
373
+ // their schema. For ref-based types (project_md_*, signals_report,
374
+ // file_diff) this enforces metadata-only bodies (ref + sha256 + byte_count)
375
+ // and rejects raw markdown/diff content inline. For operator_question /
376
+ // operator_answer it validates the structured fields used by the
377
+ // request_input/provide_input intents (step 2). Older artifact types
378
+ // (proposal, critique, revision, change_summary, plan_draft, ...) are
379
+ // not in KNOWN_ARTIFACT_BODY_SCHEMAS and keep freeform body semantics.
380
+ if (artifact.body !== undefined && artifact.type in KNOWN_ARTIFACT_BODY_SCHEMAS) {
381
+ const schema = KNOWN_ARTIFACT_BODY_SCHEMAS[artifact.type];
382
+ let parsed;
383
+ try {
384
+ parsed = JSON.parse(artifact.body);
385
+ }
386
+ catch {
387
+ ctx.addIssue({
388
+ code: z.ZodIssueCode.custom,
389
+ message: `LoopArtifact type="${artifact.type}" requires body to be a JSON-encoded payload; got non-JSON content`,
390
+ path: ['body'],
391
+ });
392
+ return;
393
+ }
394
+ const result = schema.safeParse(parsed);
395
+ if (!result.success) {
396
+ ctx.addIssue({
397
+ code: z.ZodIssueCode.custom,
398
+ message: `LoopArtifact type="${artifact.type}" body failed schema validation: ` +
399
+ result.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; '),
400
+ path: ['body'],
401
+ });
402
+ }
403
+ }
163
404
  });
164
405
  export const AtomicStopConditionSchema = z.discriminatedUnion('kind', [
165
406
  z.object({ kind: z.literal('phase_reached'), phase: z.string().min(1) }),
166
407
  z.object({ kind: z.literal('reviewer_green') }),
167
408
  z.object({ kind: z.literal('max_iterations'), n: z.number().int().positive() }),
409
+ // pln#516 step 2 — minimum-iteration floor. Used by the bootstrap preset's
410
+ // clarify gate (composed inside `all`) to refuse exiting the phase before
411
+ // the champion has had at least one iteration tick to call requestInput.
412
+ // Symmetric to `max_iterations`: matches when `iteration_count >= n`.
413
+ z.object({ kind: z.literal('min_iterations'), n: z.number().int().positive() }),
168
414
  z.object({
169
415
  kind: z.literal('artifact_produced'),
170
416
  phase: z.string().min(1),
@@ -181,6 +427,10 @@ export const AtomicStopConditionSchema = z.discriminatedUnion('kind', [
181
427
  n: z.number().int().positive(),
182
428
  scope: z.enum(['phase', 'loop']),
183
429
  }),
430
+ // pln#511 step 1 — bootstrap preset's clarify phase advances when the
431
+ // operator has no pending questions. Composed with `max_iterations=1`
432
+ // under an `any` gate so the loop never blocks the operator forever.
433
+ z.object({ kind: z.literal('no_open_questions') }),
184
434
  z.object({ kind: z.literal('manual') }),
185
435
  ]);
186
436
  export const StopConditionSchema = z.lazy(() => z.union([
@@ -206,6 +456,34 @@ export const LoopThreadSchema = z
206
456
  artifacts: z.array(LoopArtifactSchema),
207
457
  linked: LoopLinksSchema.optional(),
208
458
  stop_condition: StopConditionSchema.optional(),
459
+ /**
460
+ * pln#508 step 1 — set of unresolved operator_question artifact ids.
461
+ * The engine maintains this on every request_input / provide_input
462
+ * intent (step 2). Default `[]` for backward compatibility with loops
463
+ * created before this schema field landed.
464
+ */
465
+ open_questions: z.array(z.string().regex(/^qst_[0-9a-z]+$/)).default([]),
466
+ /**
467
+ * pln#508 step 1 — why the loop is paused, when status='paused'. The
468
+ * two valid reasons cover the bootstrap loop's operator-question and
469
+ * file-overwrite-approval primitives. Old paused loops without this
470
+ * field continue to load; only newly-paused loops are required to set it.
471
+ */
472
+ pause_reason: z.enum(PAUSE_REASONS).optional(),
473
+ /**
474
+ * pln#508 step 1 — set when `pause_reason='awaiting_file_apply'`.
475
+ * Carries the source artifact (project_md_final), target file path,
476
+ * and the diff artifact the operator is approving. The file is only
477
+ * written once an operator_answer with resolved_via in
478
+ * {answer, choose} lands AND the answer indicates approval.
479
+ */
480
+ pending_file_apply: z
481
+ .object({
482
+ artifact_id: z.string().min(1),
483
+ target_path: z.string().min(1),
484
+ diff_artifact_id: z.string().min(1),
485
+ })
486
+ .optional(),
209
487
  created_at: z.string().datetime(),
210
488
  updated_at: z.string().datetime(),
211
489
  closed_at: z.string().datetime().optional(),
@@ -228,6 +506,27 @@ export const LoopThreadSchema = z
228
506
  path: ['current_phase'],
229
507
  });
230
508
  }
509
+ // pln#508 step 1 — pause_reason / pending_file_apply invariants.
510
+ // We intentionally do NOT enforce "status=paused requires pause_reason"
511
+ // because pre-existing paused loops on disk may lack it; that bidirectional
512
+ // invariant is enforced by the request_input handler in step 2 at write
513
+ // time, not by the load-time schema.
514
+ if (thread.pause_reason !== undefined && thread.status !== 'paused') {
515
+ ctx.addIssue({
516
+ code: z.ZodIssueCode.custom,
517
+ message: `LoopThread.pause_reason is set ("${thread.pause_reason}") but status is "${thread.status}" — pause_reason requires status="paused"`,
518
+ path: ['pause_reason'],
519
+ });
520
+ }
521
+ if (thread.pending_file_apply !== undefined && thread.pause_reason !== 'awaiting_file_apply') {
522
+ ctx.addIssue({
523
+ code: z.ZodIssueCode.custom,
524
+ message: `LoopThread.pending_file_apply is set but pause_reason is ` +
525
+ `${thread.pause_reason === undefined ? 'undefined' : `"${thread.pause_reason}"`}` +
526
+ ' — pending_file_apply requires pause_reason="awaiting_file_apply"',
527
+ path: ['pending_file_apply'],
528
+ });
529
+ }
231
530
  });
232
531
  const LoopEventBaseShape = {
233
532
  event_id: z.string().min(1),
@@ -314,6 +613,52 @@ export const LoopEventSchema = z.discriminatedUnion('kind', [
314
613
  iteration: z.number().int().nonnegative(),
315
614
  max_iterations: z.number().int().positive(),
316
615
  }),
616
+ // pln#508 step 2 — bootstrap loop operator-interaction events.
617
+ z.object({
618
+ ...LoopEventBaseShape,
619
+ kind: z.literal('input_requested'),
620
+ question_id: z.string().regex(/^qst_[0-9a-z]+$/),
621
+ pause_scope: z.enum(PAUSE_SCOPES),
622
+ by_slot_id: z.string().min(1),
623
+ }),
624
+ z.object({
625
+ ...LoopEventBaseShape,
626
+ kind: z.literal('input_provided'),
627
+ question_id: z.string().regex(/^qst_[0-9a-z]+$/),
628
+ resolved_via: z.enum(RESOLVED_VIA),
629
+ /**
630
+ * Whose answer this is — `'operator'` for human-provided, `'system'`
631
+ * for engine-synthesized (timeout default). Named `answered_by` to
632
+ * avoid clashing with `LoopEventBaseShape.by` (event actor / source).
633
+ */
634
+ answered_by: z.enum(['operator', 'system']),
635
+ synthetic: z.boolean(),
636
+ }),
637
+ z.object({
638
+ ...LoopEventBaseShape,
639
+ kind: z.literal('slot_status_changed'),
640
+ slot_id: z.string().min(1),
641
+ from_status: z.enum(SLOT_STATUSES),
642
+ to_status: z.enum(SLOT_STATUSES),
643
+ }),
644
+ z.object({
645
+ ...LoopEventBaseShape,
646
+ kind: z.literal('pause_timeout'),
647
+ question_id: z.string().regex(/^qst_[0-9a-z]+$/),
648
+ action_taken: z.enum(ON_TIMEOUT_POLICIES),
649
+ }),
650
+ z.object({
651
+ ...LoopEventBaseShape,
652
+ kind: z.literal('file_apply_requested'),
653
+ artifact_id: z.string().min(1),
654
+ target_path: z.string().min(1),
655
+ }),
656
+ z.object({
657
+ ...LoopEventBaseShape,
658
+ kind: z.literal('file_apply_resolved'),
659
+ artifact_id: z.string().min(1),
660
+ approved: z.boolean(),
661
+ }),
317
662
  ]);
318
663
  export const LoopConflictRecordSchema = z.object({
319
664
  conflict_id: z.string().min(1),