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.
- package/README.md +52 -28
- package/dist/brainclaw-vscode.vsix +0 -0
- package/dist/cli.js +159 -12
- package/dist/commands/assignment-resource.js +182 -0
- package/dist/commands/bootstrap-loop.js +206 -0
- package/dist/commands/init.js +158 -22
- package/dist/commands/loop.js +156 -0
- package/dist/commands/loops-handlers.js +110 -55
- package/dist/commands/mcp-read-handlers.js +45 -4
- package/dist/commands/mcp.js +628 -205
- package/dist/commands/questions.js +180 -0
- package/dist/commands/reply.js +190 -0
- package/dist/commands/session-end.js +105 -3
- package/dist/commands/session-start.js +32 -53
- package/dist/commands/setup.js +87 -48
- package/dist/commands/switch.js +21 -1
- package/dist/core/agentrun-reconciler.js +65 -0
- package/dist/core/agentruns.js +10 -0
- package/dist/core/assignments.js +29 -10
- package/dist/core/claims.js +29 -0
- package/dist/core/context.js +1 -1
- package/dist/core/coordination.js +1 -1
- package/dist/core/dispatch-status.js +219 -0
- package/dist/core/entity-operations.js +166 -10
- package/dist/core/entity-registry.js +11 -10
- package/dist/core/execution-adapters.js +38 -2
- package/dist/core/facade-schema.js +55 -0
- package/dist/core/federation-cloud.js +27 -12
- package/dist/core/federation-materialize.js +57 -0
- package/dist/core/instruction-templates.js +2 -0
- package/dist/core/loops/bootstrap-acquire.js +195 -0
- package/dist/core/loops/facade-schema.js +68 -1
- package/dist/core/loops/hooks/bootstrap-write.js +144 -0
- package/dist/core/loops/hooks/notify-operator.js +148 -0
- package/dist/core/loops/hooks/survey-source-reader.js +256 -0
- package/dist/core/loops/index.js +8 -2
- package/dist/core/loops/next-expected.js +63 -0
- package/dist/core/loops/presets/bootstrap.js +75 -0
- package/dist/core/loops/presets/index.js +16 -0
- package/dist/core/loops/store.js +224 -4
- package/dist/core/loops/types.js +346 -1
- package/dist/core/loops/verbs.js +739 -6
- package/dist/core/schema.js +31 -2
- package/dist/core/state.js +62 -0
- package/dist/core/store-resolution.js +26 -16
- package/dist/facts.js +7 -5
- package/dist/facts.json +6 -4
- package/docs/cli.md +115 -30
- package/docs/concepts/dispatch-lifecycle.md +228 -0
- package/docs/concepts/loop-engine.md +55 -0
- package/docs/concepts/multi-agent-workflows.md +167 -166
- package/docs/concepts/troubleshooting.md +10 -2
- package/docs/integrations/agents.md +14 -14
- package/docs/integrations/codex.md +15 -12
- package/docs/integrations/mcp.md +10 -4
- package/docs/integrations/overview.md +11 -0
- package/docs/playbooks/productivity/index.md +3 -3
- package/docs/quickstart-existing-project.md +48 -28
- package/docs/quickstart.md +42 -28
- package/package.json +1 -1
package/dist/core/loops/types.js
CHANGED
|
@@ -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),
|