@bendyline/gezel 0.1.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.
@@ -0,0 +1,4022 @@
1
+ // src/schemas/craftbook.ts
2
+ import { z as z5 } from "zod";
3
+
4
+ // src/roles/tier.ts
5
+ var MODEL_TIER_ORDER = [
6
+ "tiny",
7
+ "small",
8
+ "medium",
9
+ "large",
10
+ "cloud"
11
+ ];
12
+
13
+ // src/schemas/assignee.ts
14
+ import { z } from "zod";
15
+ var TaskAssigneeSchema = z.discriminatedUnion("kind", [
16
+ z.object({ kind: z.literal("gezel"), gezelId: z.string() }),
17
+ z.object({ kind: z.literal("user") })
18
+ ]);
19
+
20
+ // src/schemas/gate.ts
21
+ import { z as z3 } from "zod";
22
+
23
+ // src/schemas/script.ts
24
+ import { z as z2 } from "zod";
25
+ var NamedScriptCapabilitySchema = z2.enum([
26
+ "llm",
27
+ "network",
28
+ "workspace.read",
29
+ "workspace.write",
30
+ "artifacts.read",
31
+ "artifacts.write",
32
+ "documents.read",
33
+ "documents.write",
34
+ "tasks.read",
35
+ "tasks.write",
36
+ "memory.read",
37
+ "memory.write"
38
+ ]);
39
+ var CredentialCapabilitySchema = z2.string().regex(
40
+ /^credential:[a-zA-Z][\w.:-]*$/,
41
+ 'credential capability must be of the form "credential:<name>" where <name> starts with a letter and contains letters, digits, underscore, hyphen, dot, or colon'
42
+ );
43
+ var ScriptCapabilitySchema = z2.union([
44
+ NamedScriptCapabilitySchema,
45
+ CredentialCapabilitySchema
46
+ ]);
47
+ var ScriptStringInputSchema = z2.object({
48
+ type: z2.literal("string"),
49
+ description: z2.string(),
50
+ required: z2.boolean().optional(),
51
+ default: z2.string().optional(),
52
+ pattern: z2.string().optional(),
53
+ multiline: z2.boolean().optional()
54
+ });
55
+ var ScriptNumberInputSchema = z2.object({
56
+ type: z2.literal("number"),
57
+ description: z2.string(),
58
+ required: z2.boolean().optional(),
59
+ default: z2.number().optional(),
60
+ min: z2.number().optional(),
61
+ max: z2.number().optional(),
62
+ integer: z2.boolean().optional()
63
+ });
64
+ var ScriptBooleanInputSchema = z2.object({
65
+ type: z2.literal("boolean"),
66
+ description: z2.string(),
67
+ required: z2.boolean().optional(),
68
+ default: z2.boolean().optional()
69
+ });
70
+ var ScriptChoiceInputSchema = z2.object({
71
+ type: z2.literal("choice"),
72
+ description: z2.string(),
73
+ required: z2.boolean().optional(),
74
+ default: z2.string().optional(),
75
+ options: z2.array(
76
+ z2.object({
77
+ value: z2.string(),
78
+ label: z2.string().optional()
79
+ })
80
+ ).min(1)
81
+ });
82
+ var ScriptRefInputSchema = z2.object({
83
+ type: z2.literal("ref"),
84
+ description: z2.string(),
85
+ required: z2.boolean().optional(),
86
+ kind: z2.enum(["gezel", "task", "artifact", "document"])
87
+ });
88
+ var ScriptJsonInputSchema = z2.object({
89
+ type: z2.literal("json"),
90
+ description: z2.string(),
91
+ required: z2.boolean().optional(),
92
+ default: z2.unknown().optional(),
93
+ schema: z2.unknown().optional()
94
+ });
95
+ var ScriptInputFieldSchema = z2.discriminatedUnion("type", [
96
+ ScriptStringInputSchema,
97
+ ScriptNumberInputSchema,
98
+ ScriptBooleanInputSchema,
99
+ ScriptChoiceInputSchema,
100
+ ScriptRefInputSchema,
101
+ ScriptJsonInputSchema
102
+ ]);
103
+ var ScriptInputsSchema = z2.record(z2.string(), ScriptInputFieldSchema);
104
+ var ScriptStringOutputSchema = z2.object({
105
+ type: z2.literal("string"),
106
+ description: z2.string(),
107
+ nullable: z2.boolean().optional()
108
+ });
109
+ var ScriptNumberOutputSchema = z2.object({
110
+ type: z2.literal("number"),
111
+ description: z2.string(),
112
+ nullable: z2.boolean().optional()
113
+ });
114
+ var ScriptBooleanOutputSchema = z2.object({
115
+ type: z2.literal("boolean"),
116
+ description: z2.string(),
117
+ nullable: z2.boolean().optional()
118
+ });
119
+ var ScriptArrayOutputSchema = z2.object({
120
+ type: z2.literal("array"),
121
+ description: z2.string(),
122
+ itemType: z2.enum(["string", "number", "boolean", "object"])
123
+ });
124
+ var ScriptObjectOutputSchema = z2.object({
125
+ type: z2.literal("object"),
126
+ description: z2.string(),
127
+ schema: z2.unknown().optional()
128
+ });
129
+ var ScriptJsonOutputSchema = z2.object({
130
+ type: z2.literal("json"),
131
+ description: z2.string(),
132
+ schema: z2.unknown().optional()
133
+ });
134
+ var ScriptOutputFieldSchema = z2.discriminatedUnion("type", [
135
+ ScriptStringOutputSchema,
136
+ ScriptNumberOutputSchema,
137
+ ScriptBooleanOutputSchema,
138
+ ScriptArrayOutputSchema,
139
+ ScriptObjectOutputSchema,
140
+ ScriptJsonOutputSchema
141
+ ]);
142
+ var ScriptOutputsSchema = z2.record(z2.string(), ScriptOutputFieldSchema);
143
+ var ScriptMetaSchema = z2.object({
144
+ name: z2.string().regex(
145
+ /^[a-zA-Z][\w-]*$/,
146
+ "name must start with a letter and contain only letters, digits, underscore, or hyphen"
147
+ ),
148
+ description: z2.string().min(10),
149
+ /**
150
+ * What the script is for. `gate` = returns a structured GateResult
151
+ * (`{ decision, message, ... }`) and is meant to be attached to a
152
+ * step's gate. Advisory — pickers filter on it; the runtime validates
153
+ * gate outputs against the result schema regardless. Absent = 'action'.
154
+ */
155
+ kind: z2.enum(["action", "gate"]).optional(),
156
+ inputs: ScriptInputsSchema.optional(),
157
+ outputs: ScriptOutputsSchema.optional(),
158
+ requires: z2.array(ScriptCapabilitySchema).optional()
159
+ });
160
+ var ScriptScopeSchema = z2.enum(["project", "craftbook", "user", "standard"]);
161
+ var ScriptOutputPredicateSchema = z2.discriminatedUnion("op", [
162
+ z2.object({ op: z2.literal("always") }),
163
+ z2.object({ op: z2.literal("never") }),
164
+ z2.object({ op: z2.literal("ok") }),
165
+ z2.object({
166
+ op: z2.literal("equals"),
167
+ field: z2.string().min(1),
168
+ value: z2.union([z2.string(), z2.number(), z2.boolean(), z2.null()])
169
+ }),
170
+ z2.object({
171
+ op: z2.literal("exists"),
172
+ field: z2.string().min(1),
173
+ negate: z2.boolean().optional()
174
+ }),
175
+ z2.object({
176
+ op: z2.literal("gt"),
177
+ field: z2.string().min(1),
178
+ value: z2.number()
179
+ })
180
+ ]);
181
+ var ScriptRefSchema = z2.object({
182
+ name: z2.string().min(1),
183
+ /** Resolution scope; absent = 'project'. See {@link ScriptScopeSchema}. */
184
+ scope: ScriptScopeSchema.optional(),
185
+ inputs: z2.record(z2.string(), z2.unknown()).optional(),
186
+ autoAdvanceOnSuccess: z2.boolean().optional(),
187
+ autoAdvanceWhen: ScriptOutputPredicateSchema.optional()
188
+ });
189
+ var ScriptRefListSchema = z2.union([ScriptRefSchema, z2.array(ScriptRefSchema)]);
190
+ function normalizeScriptRefs(value) {
191
+ if (value === void 0) return [];
192
+ return Array.isArray(value) ? value : [value];
193
+ }
194
+ var ScriptRunStatusSchema = z2.enum(["running", "ok", "error"]);
195
+ var ScriptRunCallSchema = z2.object({
196
+ at: z2.string(),
197
+ kind: z2.string(),
198
+ argsSummary: z2.string(),
199
+ outputSummary: z2.string().optional(),
200
+ durationMs: z2.number().nonnegative(),
201
+ error: z2.string().optional()
202
+ });
203
+ var ScriptRunTriggerSchema = z2.discriminatedUnion("kind", [
204
+ z2.object({
205
+ kind: z2.literal("step"),
206
+ taskRef: z2.string(),
207
+ stepId: z2.string(),
208
+ moment: z2.enum(["enter", "exit", "gate"])
209
+ }),
210
+ z2.object({
211
+ kind: z2.literal("chat"),
212
+ sessionId: z2.string(),
213
+ gezelId: z2.string()
214
+ }),
215
+ z2.object({
216
+ kind: z2.literal("manual"),
217
+ userInitiated: z2.literal(true)
218
+ }),
219
+ z2.object({
220
+ kind: z2.literal("nested"),
221
+ parentRunId: z2.string()
222
+ }),
223
+ z2.object({
224
+ kind: z2.literal("connector"),
225
+ typeId: z2.string(),
226
+ bindingId: z2.string()
227
+ }),
228
+ /**
229
+ * A project-type page invoked one of its declared `pages.tools` through
230
+ * the first-party page-invoke route. User-click-shaped (like `manual`)
231
+ * and therefore ungated by `allowScriptExecution`; the distinct kind
232
+ * exists for auditability and future policy.
233
+ */
234
+ z2.object({
235
+ kind: z2.literal("page"),
236
+ /** The declared tool name the page invoked (not the script name). */
237
+ tool: z2.string()
238
+ })
239
+ ]);
240
+ var ScriptRunSchema = z2.object({
241
+ id: z2.string(),
242
+ projectId: z2.string(),
243
+ scriptName: z2.string(),
244
+ startedAt: z2.string(),
245
+ finishedAt: z2.string().optional(),
246
+ status: ScriptRunStatusSchema,
247
+ trigger: ScriptRunTriggerSchema,
248
+ inputs: z2.record(z2.string(), z2.unknown()),
249
+ output: z2.unknown().optional(),
250
+ calls: z2.array(ScriptRunCallSchema),
251
+ logs: z2.string(),
252
+ error: z2.string().optional()
253
+ });
254
+ var RunScriptRequestSchema = z2.object({
255
+ name: z2.string().min(1),
256
+ /** Resolution scope (default 'project') — lets the editor test-run standard/user scripts. */
257
+ scope: ScriptScopeSchema.optional(),
258
+ input: z2.record(z2.string(), z2.unknown()).optional()
259
+ });
260
+ var RunScriptResponseSchema = z2.object({
261
+ runId: z2.string(),
262
+ status: ScriptRunStatusSchema,
263
+ output: z2.unknown().optional(),
264
+ callsSummary: z2.array(
265
+ z2.object({
266
+ kind: z2.string(),
267
+ durationMs: z2.number().nonnegative(),
268
+ error: z2.string().optional()
269
+ })
270
+ ),
271
+ error: z2.string().optional()
272
+ });
273
+ var InvokePageToolRequestSchema = z2.object({
274
+ tool: z2.string().regex(/^[a-z][a-z0-9_]*$/),
275
+ input: z2.record(z2.string(), z2.unknown()).optional()
276
+ });
277
+ var InvokePageToolResponseSchema = RunScriptResponseSchema.extend({
278
+ /** Present when the invoked tool declares a reaction. */
279
+ reaction: z2.object({
280
+ delivered: z2.boolean(),
281
+ gezelId: z2.string().optional(),
282
+ /** 'engagement-off' | 'project-inactive' | 'no-target' | 'send-failed' */
283
+ reason: z2.string().optional()
284
+ }).optional()
285
+ });
286
+ var ListScriptsResponseSchema = z2.object({
287
+ scripts: z2.array(
288
+ z2.object({
289
+ name: z2.string(),
290
+ meta: ScriptMetaSchema,
291
+ path: z2.string()
292
+ })
293
+ )
294
+ });
295
+ var ScriptNameSchema = z2.string().regex(
296
+ /^[a-zA-Z][\w-]*$/,
297
+ "script name must start with a letter and contain only letters, digits, underscore, or hyphen"
298
+ );
299
+ var ScriptDiagnosticSchema = z2.object({
300
+ severity: z2.enum(["error", "warning", "info"]),
301
+ source: z2.enum(["meta", "typescript", "runtime-compat"]),
302
+ message: z2.string(),
303
+ /** 1-based, when the diagnostic is anchored to a location. */
304
+ line: z2.number().optional(),
305
+ column: z2.number().optional()
306
+ });
307
+ var ScriptProvenanceSchema = z2.object({
308
+ kind: z2.enum(["craftbook", "import", "user", "standard"]),
309
+ /** e.g. "pu/pull-request-review@1.0.0", "skill-x#abc123", "stdlib@1.2.0". */
310
+ ref: z2.string()
311
+ });
312
+ var GetScriptSourceResponseSchema = z2.object({
313
+ name: ScriptNameSchema,
314
+ source: z2.string(),
315
+ /** sha256 hex of the file bytes; opaque token for conflict detection. */
316
+ hash: z2.string(),
317
+ mtimeMs: z2.number(),
318
+ meta: ScriptMetaSchema.optional(),
319
+ metaError: z2.string().optional(),
320
+ provenance: ScriptProvenanceSchema.optional()
321
+ });
322
+ var SaveScriptSourceRequestSchema = z2.object({
323
+ name: ScriptNameSchema,
324
+ source: z2.string(),
325
+ /**
326
+ * Hash the editor loaded/saved last. When set and it no longer matches
327
+ * the on-disk file, the save is rejected with `status: 'conflict'`
328
+ * (someone or something else wrote the file). Omit to overwrite
329
+ * unconditionally.
330
+ */
331
+ baseHash: z2.string().optional()
332
+ });
333
+ var SaveScriptSourceResponseSchema = z2.discriminatedUnion("status", [
334
+ z2.object({
335
+ status: z2.literal("saved"),
336
+ hash: z2.string(),
337
+ metaOk: z2.boolean(),
338
+ meta: ScriptMetaSchema.optional(),
339
+ diagnostics: z2.array(ScriptDiagnosticSchema)
340
+ }),
341
+ z2.object({
342
+ status: z2.literal("conflict"),
343
+ currentHash: z2.string(),
344
+ currentSource: z2.string()
345
+ })
346
+ ]);
347
+ var ScriptTemplateIdSchema = z2.enum([
348
+ "blank",
349
+ "post-message",
350
+ "fetch-and-summarize",
351
+ "check-files",
352
+ "call-tool",
353
+ "ask-ai"
354
+ ]);
355
+ var CreateScriptRequestSchema = z2.object({
356
+ name: ScriptNameSchema,
357
+ description: z2.string().optional(),
358
+ template: ScriptTemplateIdSchema.optional(),
359
+ /**
360
+ * Full source to write instead of a template scaffold. Used by the
361
+ * copy-on-write flow (duplicate a craftbook script minus its
362
+ * provenance marker) and by AI drafting.
363
+ */
364
+ source: z2.string().optional()
365
+ });
366
+ var CreateScriptResponseSchema = z2.object({
367
+ name: ScriptNameSchema,
368
+ source: z2.string(),
369
+ hash: z2.string()
370
+ });
371
+ var SdkTypesResponseSchema = z2.object({
372
+ version: z2.string(),
373
+ files: z2.array(z2.object({ name: z2.string(), content: z2.string() }))
374
+ });
375
+ var DraftScriptRequestSchema = z2.object({
376
+ name: ScriptNameSchema,
377
+ description: z2.string().min(1)
378
+ });
379
+ var DraftScriptResponseSchema = z2.object({
380
+ source: z2.string()
381
+ });
382
+
383
+ // src/schemas/gate.ts
384
+ var StepSniffSchema = z3.enum([
385
+ "html-complete",
386
+ "html-game",
387
+ "nonempty",
388
+ "json-valid",
389
+ "data-table"
390
+ ]);
391
+ var GateJsonScalarSchema = z3.union([z3.string(), z3.number(), z3.boolean(), z3.null()]);
392
+ var GateCheckSchema = z3.discriminatedUnion("kind", [
393
+ /** A single file is at least `bytes` long (workspace, or the artifacts drawer when `artifact`). */
394
+ z3.object({
395
+ kind: z3.literal("minBytes"),
396
+ file: z3.string().min(1),
397
+ bytes: z3.number().int().positive(),
398
+ artifact: z3.boolean().optional()
399
+ }),
400
+ /** The summed size of several files clears `bytes` (e.g. index.html + game.js ≥ 5 KB). */
401
+ z3.object({
402
+ kind: z3.literal("totalMinBytes"),
403
+ files: z3.array(z3.string().min(1)).min(1),
404
+ bytes: z3.number().int().positive()
405
+ }),
406
+ /**
407
+ * At least `min` workspace files with one of `ext` exist (e.g. ≥3 images).
408
+ * `verifyImageBytes` additionally requires each raster candidate
409
+ * (png/jpg/jpeg/gif/webp) to carry real image bytes — magic signature
410
+ * plus a 1 KiB floor — so a text placeholder named `panel-1.png` stops
411
+ * counting. Set it on any gate whose deliverable is an actual rendered
412
+ * image; leave it off for listings that legitimately include vector or
413
+ * stub assets. The check fails loudly rather than degrading when the
414
+ * evaluating surface cannot read bytes.
415
+ */
416
+ z3.object({
417
+ kind: z3.literal("fileCount"),
418
+ ext: z3.array(z3.string().min(1)).min(1),
419
+ min: z3.number().int().positive(),
420
+ dir: z3.string().optional(),
421
+ verifyImageBytes: z3.boolean().optional()
422
+ }),
423
+ /** Inline `<style>` + linked `.css` in `file` (default index.html) clears `bytes`. */
424
+ z3.object({
425
+ kind: z3.literal("cssMinBytes"),
426
+ bytes: z3.number().int().positive(),
427
+ file: z3.string().optional()
428
+ }),
429
+ /** A named content sniff passes on `file` (workspace, or the artifacts drawer when `artifact`). */
430
+ z3.object({
431
+ kind: z3.literal("sniff"),
432
+ file: z3.string().min(1),
433
+ sniff: StepSniffSchema,
434
+ artifact: z3.boolean().optional()
435
+ }),
436
+ /** A JSON scalar at `path` equals the expected value. Path supports dot keys and numeric array indexes. */
437
+ z3.object({
438
+ kind: z3.literal("jsonPathEquals"),
439
+ file: z3.string().min(1),
440
+ path: z3.string().min(1),
441
+ value: GateJsonScalarSchema,
442
+ label: z3.string().min(1).optional()
443
+ }),
444
+ /** A CSV file has the expected header/row shape and optional picklist values. */
445
+ z3.object({
446
+ kind: z3.literal("csvShape"),
447
+ file: z3.string().min(1),
448
+ requiredColumns: z3.array(z3.string().min(1)).min(1).optional(),
449
+ exactColumns: z3.array(z3.string().min(1)).min(1).optional(),
450
+ minRows: z3.number().int().nonnegative().optional(),
451
+ consistentColumns: z3.boolean().optional(),
452
+ allowedValues: z3.record(z3.string(), z3.array(z3.string().min(1)).min(1)).optional()
453
+ }),
454
+ /** `file` matches `pattern` (regex), in the workspace or the artifacts drawer when `artifact`. */
455
+ z3.object({
456
+ kind: z3.literal("contains"),
457
+ file: z3.string().min(1),
458
+ pattern: z3.string().min(1),
459
+ flags: z3.string().optional(),
460
+ label: z3.string().min(1).optional(),
461
+ artifact: z3.boolean().optional()
462
+ }),
463
+ /** `file` must NOT match `pattern` (regex). E.g. exclude internal-only release noise. */
464
+ z3.object({
465
+ kind: z3.literal("notContains"),
466
+ file: z3.string().min(1),
467
+ pattern: z3.string().min(1),
468
+ flags: z3.string().optional(),
469
+ label: z3.string().min(1).optional()
470
+ }),
471
+ /**
472
+ * `file` may use high-risk claim wording only when the exact matched
473
+ * phrase appears in `sourceFiles`. This catches overclaim/tone drift in
474
+ * factual prose without requiring an LLM judge.
475
+ */
476
+ z3.object({
477
+ kind: z3.literal("unsupportedClaims"),
478
+ file: z3.string().min(1),
479
+ sourceFiles: z3.array(z3.string().min(1)).min(1),
480
+ patterns: z3.array(
481
+ z3.object({
482
+ pattern: z3.string().min(1),
483
+ label: z3.string().min(1).optional()
484
+ })
485
+ ).min(1),
486
+ flags: z3.string().optional()
487
+ }),
488
+ /**
489
+ * Every inline `<script>` body in `file` (default index.html) parses as
490
+ * JavaScript. The "ships broken JS" floor for the single-file-web class:
491
+ * a page whose inline script has a syntax error never runs in a browser,
492
+ * so the gate loops the builder back with the parse error. Files with no
493
+ * inline JS pass (nothing to judge); `type="module"` scripts are size-only
494
+ * (top-level import can't be function-parsed) and pass here.
495
+ */
496
+ z3.object({
497
+ kind: z3.literal("jsParses"),
498
+ file: z3.string().optional()
499
+ }),
500
+ /**
501
+ * An in-process, zero-spawn static gate for an HTML deliverable: the document
502
+ * shell closes, static DOM ids are unique, every inline classic/module
503
+ * script parses, and top-level function declarations do not silently
504
+ * replace one another. Unlike a live browser check, this runs in-process
505
+ * in locked-down modes and is safe to attach to every HTML deliverable.
506
+ */
507
+ z3.object({
508
+ kind: z3.literal("htmlLint"),
509
+ file: z3.string().min(1)
510
+ }),
511
+ /**
512
+ * Named `node:` imports in `file` resolve to real builtin exports, and a
513
+ * `.mjs` file doesn't call `require()`. The "ships broken module imports"
514
+ * floor for the code class: a name imported from the wrong builtin (e.g.
515
+ * `dirname` from `node:url`) or a `require()` in ESM throws at LOAD and
516
+ * nothing runs — and `jsParses` can't catch it (it skips `type="module"`
517
+ * and never sees a standalone `.mjs`/`.ts`). Files with no `node:` named
518
+ * imports pass.
519
+ */
520
+ z3.object({
521
+ kind: z3.literal("esmImports"),
522
+ file: z3.string().min(1)
523
+ }),
524
+ /**
525
+ * `file` parses as source code. For `.ts/.tsx/.js/.mjs/.cjs/.jsx` the
526
+ * service runs a syntax-only TypeScript transpile (milliseconds, zero
527
+ * spawns) and the failure message carries the first diagnostic with
528
+ * line:column; `.html` delegates to the inline-`jsParses` path. The
529
+ * code-class floor that catches truncated/unbalanced files a
530
+ * `minBytes`-only gate happily waves through.
531
+ */
532
+ z3.object({
533
+ kind: z3.literal("sourceParses"),
534
+ file: z3.string().min(1)
535
+ }),
536
+ /**
537
+ * The first Markdown table in `file` has the required header set and
538
+ * row floor. (CSV/JSON record data uses `csvShape`/`recordSchema`.)
539
+ */
540
+ z3.object({
541
+ kind: z3.literal("tableShape"),
542
+ file: z3.string().min(1),
543
+ requiredColumns: z3.array(z3.string().min(1)).min(1).optional(),
544
+ minRows: z3.number().int().nonnegative().optional(),
545
+ artifact: z3.boolean().optional()
546
+ }),
547
+ /**
548
+ * A JSON array (or CSV) of records in `file` conforms to a declared
549
+ * field schema — the ETL schema-conformance floor. Field `type` is a
550
+ * built-in cell type (`string`/`nonempty`/`number`/`integer`/`boolean`/
551
+ * `date`/`iso-date`/`email`) or a regex source.
552
+ */
553
+ z3.object({
554
+ kind: z3.literal("recordSchema"),
555
+ file: z3.string().min(1),
556
+ fields: z3.array(
557
+ z3.object({
558
+ name: z3.string().min(1),
559
+ type: z3.string().optional(),
560
+ required: z3.boolean().optional()
561
+ })
562
+ ).min(1),
563
+ minRows: z3.number().int().nonnegative().optional(),
564
+ uniqueBy: z3.string().optional(),
565
+ format: z3.enum(["json", "csv", "auto"]).optional(),
566
+ artifact: z3.boolean().optional()
567
+ }),
568
+ /**
569
+ * Execute `file` in the sandbox and require exit code 0. The ONE
570
+ * spawning check — evaluated service-side through an injected executor
571
+ * (absent executor = fail-closed reject) and gated behind the same
572
+ * `allowScriptExecution` security policy as user scripts. Only
573
+ * dependency-free files can run (node built-ins ok; the sandbox has no
574
+ * npm tree): a `node:test`/`assert` file exits nonzero on failure,
575
+ * which is exactly the contract.
576
+ */
577
+ z3.object({
578
+ kind: z3.literal("nodeRuns"),
579
+ file: z3.string().min(1),
580
+ /** Wall-clock budget. Default 20s, cap 60s. */
581
+ timeoutMs: z3.number().int().positive().max(6e4).optional()
582
+ }),
583
+ /**
584
+ * Every source `file` cites must exist — the anti-fabrication floor for
585
+ * research/review deliverables (a cited path that resolves is the
586
+ * difference between grounded work and an invented bibliography).
587
+ * File-path citations resolve against the workspace listing; URLs pass
588
+ * unless `corpus` is given, in which case cited paths AND URLs must be
589
+ * corpus members. Optional `minCitations` floors the citation count.
590
+ */
591
+ z3.object({
592
+ kind: z3.literal("citationsResolve"),
593
+ file: z3.string().min(1),
594
+ pattern: z3.string().min(1).optional(),
595
+ flags: z3.string().optional(),
596
+ minCitations: z3.number().int().nonnegative().optional(),
597
+ corpus: z3.array(z3.string().min(1)).min(1).optional(),
598
+ artifact: z3.boolean().optional()
599
+ }),
600
+ /**
601
+ * Require observable source acquisition during this activation. When
602
+ * `sourcePath` is non-empty, an exact successful `read_file` of that path
603
+ * qualifies; otherwise one of `tools` must have completed against an
604
+ * external source. The runtime supplies the live tool-call evidence.
605
+ */
606
+ z3.object({
607
+ kind: z3.literal("researchEvidence"),
608
+ sourcePath: z3.string().optional(),
609
+ tools: z3.array(z3.string().min(1)).min(1),
610
+ minSuccessful: z3.number().int().positive().optional()
611
+ }),
612
+ /**
613
+ * The H1 slide titles in `file` must match the numbered slide headings in
614
+ * `outlineFile`, one-for-one and in order. This makes a locked Markdown
615
+ * outline mechanically binding instead of relying on a reviewer to notice
616
+ * that slides were merged, dropped, or reordered.
617
+ */
618
+ z3.object({
619
+ kind: z3.literal("markdownHeadingsMatch"),
620
+ file: z3.string().min(1),
621
+ outlineFile: z3.string().min(1)
622
+ }),
623
+ /**
624
+ * Named facts in `file` must come from authorized sources: each fact
625
+ * requires one of its `required` patterns to appear, and its `forbidden`
626
+ * (decoy) patterns must not appear at all. The distractor-grounding
627
+ * floor for research/analysis deliverables, productized from the
628
+ * decoy-research grader.
629
+ */
630
+ z3.object({
631
+ kind: z3.literal("valueGrounding"),
632
+ file: z3.string().min(1),
633
+ facts: z3.array(
634
+ z3.object({
635
+ id: z3.string().min(1),
636
+ label: z3.string().min(1).optional(),
637
+ required: z3.array(z3.string().min(1)).min(1),
638
+ forbidden: z3.array(z3.string().min(1)).optional()
639
+ })
640
+ ).min(1),
641
+ /** Digit-group normalization ("1,234" ≈ "1234"); defaults on. */
642
+ normalizeDigits: z3.boolean().optional(),
643
+ artifact: z3.boolean().optional()
644
+ }),
645
+ /**
646
+ * Value-conservation floor for transform/ETL deliverables: every value
647
+ * in `file` matching `pattern` (one capture group) must appear verbatim
648
+ * in at least one `sourceFiles` member (entries may use `*`/`**` globs;
649
+ * the output file itself is always excluded from the sources). Catches
650
+ * renumbered/invented identifiers that pass every shape check — the
651
+ * failure mode that took out 7 of 8 local models on precision ETL in
652
+ * the core sweep.
653
+ */
654
+ z3.object({
655
+ kind: z3.literal("valuesSubsetOf"),
656
+ file: z3.string().min(1),
657
+ sourceFiles: z3.array(z3.string().min(1)).min(1),
658
+ pattern: z3.string().min(1),
659
+ flags: z3.string().optional(),
660
+ minMatches: z3.number().int().nonnegative().optional(),
661
+ artifact: z3.boolean().optional()
662
+ }),
663
+ /**
664
+ * LLM-judge check for unverifiable qualities (tone, faithfulness).
665
+ * FAIL-OPEN by design: when no judge is available (keurmeester
666
+ * unconfigured, timeout, unparseable verdict) the check approves
667
+ * with a "(fail-open)" note — the opposite polarity of `nodeRuns`.
668
+ * ADVISORY by default in v1: a would-reject never holds the step;
669
+ * the opinion rides the approve verdict + gate telemetry until
670
+ * false-reject data justifies flipping `advisory: false`.
671
+ */
672
+ z3.object({
673
+ kind: z3.literal("judge"),
674
+ file: z3.string().min(1),
675
+ /** Imperative rubric: the quality to judge, in one or two sentences. */
676
+ rubric: z3.string().min(1),
677
+ /** Context files quoted to the judge alongside the artifact (e.g. a voice guide). */
678
+ sourceFiles: z3.array(z3.string().min(1)).optional(),
679
+ /** A fail verdict must quote ≥1 verbatim excerpt from the artifact; default true. */
680
+ requireEvidence: z3.boolean().optional(),
681
+ /** Default true (v1). False = enforcing: a valid fail verdict rejects the step. */
682
+ advisory: z3.boolean().optional(),
683
+ /** Judge one-shot budget. Default 60s, capped at 120s. */
684
+ timeoutMs: z3.number().int().positive().max(12e4).optional(),
685
+ label: z3.string().min(1).optional(),
686
+ artifact: z3.boolean().optional()
687
+ }),
688
+ /**
689
+ * Structural plan validation on the first Markdown table in `file`:
690
+ * owners on-roster, dependencies resolve/acyclic/earlier-only,
691
+ * checkable done-states. The Planner role's mechanical floor.
692
+ */
693
+ z3.object({
694
+ kind: z3.literal("planStructure"),
695
+ file: z3.string().min(1),
696
+ minRows: z3.number().int().positive().optional(),
697
+ /** When given, every Owner cell must be one of these names. */
698
+ ownerRoster: z3.array(z3.string().min(1)).optional(),
699
+ /** Rows may only depend on EARLIER rows (default true). */
700
+ requireEarlierOnly: z3.boolean().optional(),
701
+ /** Minimum length of each Done-when cell (default 12 chars). */
702
+ doneWhenMinChars: z3.number().int().positive().optional(),
703
+ artifact: z3.boolean().optional()
704
+ })
705
+ ]);
706
+ var JudgeVerdictSchema = z3.object({
707
+ verdict: z3.enum(["pass", "fail"]),
708
+ reasons: z3.array(z3.string().min(1)).max(5),
709
+ /** Verbatim quotes from the artifact backing the verdict. */
710
+ evidence: z3.array(z3.string()).default([]),
711
+ confidence: z3.enum(["low", "medium", "high"]).optional()
712
+ });
713
+ var GateSpecSchema = z3.object({
714
+ checks: z3.array(GateCheckSchema).min(1),
715
+ /** Step to activate when a check fails. Defaults to the entry/build step (loop back). */
716
+ onFail: z3.string().optional(),
717
+ /** Step to activate when all checks pass. Defaults to `next`. */
718
+ onPass: z3.string().optional(),
719
+ /** After this many failed loops, route to a terminal step instead of looping. Default 4. */
720
+ maxAttempts: z3.number().int().positive().optional(),
721
+ /** Role to hand to AFTER static checks pass — the dynamic (Playwright) reviewer layer. */
722
+ reviewer: z3.string().optional()
723
+ });
724
+ var GateScriptRefSchema = z3.object({
725
+ name: z3.string().min(1),
726
+ /** Where the script resolves from. Absent = 'project'. */
727
+ scope: ScriptScopeSchema.optional(),
728
+ inputs: z3.record(z3.string(), z3.unknown()).optional()
729
+ });
730
+ var GateScriptResultSchema = z3.object({
731
+ decision: z3.enum(["approve", "reject"]),
732
+ message: z3.string().optional(),
733
+ goto: z3.string().optional(),
734
+ handoff: z3.object({
735
+ message: z3.string(),
736
+ params: z3.record(z3.string(), z3.unknown()).optional()
737
+ }).optional()
738
+ }).superRefine((v, ctx) => {
739
+ if (v.decision === "reject" && !v.message?.trim()) {
740
+ ctx.addIssue({
741
+ code: z3.ZodIssueCode.custom,
742
+ message: "a reject result must carry a message \u2014 it is the prescriptive guidance the working session uses to fix the step",
743
+ path: ["message"]
744
+ });
745
+ }
746
+ });
747
+ var StepGateSchema = z3.object({
748
+ at: z3.enum(["completion", "activation"]),
749
+ /** Cheap in-process floor; evaluated before any script spawns. */
750
+ checks: z3.array(GateCheckSchema).optional(),
751
+ /** Ordered gate scripts; first reject short-circuits. */
752
+ scripts: z3.array(GateScriptRefSchema).optional(),
753
+ /**
754
+ * Step to activate on reject. Absent = stay on this step (the
755
+ * rejection message re-prompts the working session). Set to the
756
+ * step's own id to force a fresh re-activation/handoff per reject —
757
+ * the loop shape that carries small models.
758
+ */
759
+ onReject: z3.string().optional(),
760
+ /** Step to activate on approve. Overrides `step.next`; a gate script's `goto` outranks it. */
761
+ onApprove: z3.string().optional(),
762
+ /** Rejections before the task pauses for help. Default 4. */
763
+ maxAttempts: z3.number().int().positive().optional(),
764
+ /** Legacy Layer-2 reviewer handoff (activation gates only). */
765
+ reviewer: z3.string().optional()
766
+ }).superRefine((v, ctx) => {
767
+ if (!v.checks?.length && !v.scripts?.length) {
768
+ ctx.addIssue({
769
+ code: z3.ZodIssueCode.custom,
770
+ message: "a gate needs at least one check or one script"
771
+ });
772
+ }
773
+ });
774
+ var StepGateUnionSchema = z3.union([StepGateSchema, GateSpecSchema]);
775
+ var GATE_DEFAULT_MAX_ATTEMPTS = 4;
776
+ function isLegacyGateSpec(gate) {
777
+ return !("at" in gate);
778
+ }
779
+ function normalizeStepGate(gate) {
780
+ if (isLegacyGateSpec(gate)) {
781
+ return {
782
+ at: "activation",
783
+ checks: gate.checks,
784
+ scripts: [],
785
+ ...gate.onFail !== void 0 ? { onReject: gate.onFail } : {},
786
+ ...gate.onPass !== void 0 ? { onApprove: gate.onPass } : {},
787
+ maxAttempts: gate.maxAttempts ?? GATE_DEFAULT_MAX_ATTEMPTS,
788
+ ...gate.reviewer !== void 0 ? { reviewer: gate.reviewer } : {},
789
+ legacy: true
790
+ };
791
+ }
792
+ return {
793
+ at: gate.at,
794
+ checks: gate.checks ?? [],
795
+ scripts: gate.scripts ?? [],
796
+ ...gate.onReject !== void 0 ? { onReject: gate.onReject } : {},
797
+ ...gate.onApprove !== void 0 ? { onApprove: gate.onApprove } : {},
798
+ maxAttempts: gate.maxAttempts ?? GATE_DEFAULT_MAX_ATTEMPTS,
799
+ ...gate.reviewer !== void 0 ? { reviewer: gate.reviewer } : {},
800
+ legacy: false
801
+ };
802
+ }
803
+ function gateEdgeTargets(gate) {
804
+ const n = normalizeStepGate(gate);
805
+ return [n.onReject, n.onApprove].filter((t) => t !== void 0);
806
+ }
807
+
808
+ // src/schemas/hook.ts
809
+ import { z as z4 } from "zod";
810
+ var HookPhaseSchema = z4.enum(["PreToolUse", "PostToolUse"]);
811
+ var HookDecisionSchema = z4.enum(["allow", "deny", "ask"]);
812
+ var HookSpecSchema = z4.object({
813
+ phase: HookPhaseSchema,
814
+ matcher: z4.string().default(".*"),
815
+ script: ScriptRefSchema.optional(),
816
+ /** Static verdict — mutually exclusive with `script`. */
817
+ decision: HookDecisionSchema.optional(),
818
+ /**
819
+ * Optional human-readable label shown in the UI when this hook
820
+ * blocks/asks. Useful when one craftbook installs multiple hooks
821
+ * against the same matcher.
822
+ */
823
+ label: z4.string().optional()
824
+ }).superRefine((spec, ctx) => {
825
+ const hasScript = spec.script !== void 0;
826
+ const hasDecision = spec.decision !== void 0;
827
+ if (hasScript === hasDecision) {
828
+ ctx.addIssue({
829
+ code: z4.ZodIssueCode.custom,
830
+ message: "a hook must set exactly one of `script` or `decision`",
831
+ path: [hasScript ? "decision" : "script"]
832
+ });
833
+ }
834
+ });
835
+ var HookResultSchema = z4.object({
836
+ decision: HookDecisionSchema.default("allow"),
837
+ message: z4.string().optional()
838
+ });
839
+
840
+ // src/schemas/craftbook.ts
841
+ var CraftbookBranchSchema = z5.object({
842
+ when: ScriptOutputPredicateSchema,
843
+ goto: z5.string()
844
+ });
845
+ var AdvanceWhenSchema = z5.object({
846
+ /** Workspace-relative deliverable whose presence signals "this step is done". */
847
+ file: z5.string().min(1),
848
+ /** Liveness floor in bytes (guards against an empty/stub file). Default 1. */
849
+ minBytes: z5.number().int().positive().optional(),
850
+ /**
851
+ * Named content check the runtime runs before advancing. `html-complete`
852
+ * = a non-truncated HTML doc (balanced `<script>` tags + a closing
853
+ * `</body>`/`</html>`); `html-game` adds a render surface + substantial
854
+ * JS; `nonempty`/`json-valid` are generic. Absent = existence + minBytes
855
+ * only.
856
+ */
857
+ sniff: StepSniffSchema.optional(),
858
+ /**
859
+ * Edit-gate. When true, presence is NOT enough — the assignee must have
860
+ * *written to* `file` during the turn that triggers the advance. This is
861
+ * what makes `advanceWhen` usable on a step whose deliverable is an EDIT
862
+ * to a pre-existing source file (fix-a-bug, refactor): without it the
863
+ * gate would fire on the very first turn because the file already exists
864
+ * and clears `minBytes`, advancing past the step before any fix lands.
865
+ * With it, the step holds until the model actually edits the file (a
866
+ * successful `write_file`/`replace_in_file`/`append_to_file`/`apply_patch`/
867
+ * `insert_at_marker` targeting `file` this turn). The `sniff`/`minBytes`
868
+ * floor still applies on top. Absent → legacy "exists is enough".
869
+ */
870
+ requireChange: z5.boolean().optional(),
871
+ /**
872
+ * Resolve `file` against the project's ARTIFACTS drawer
873
+ * (`read_artifact` / `write_artifact`) instead of the shipped workspace.
874
+ * For deliverables that are review/analysis output — a threat model, an
875
+ * audit report — not product source the user ships. The runtime's
876
+ * observable-progress + gate readers honor this flag so an artifact
877
+ * deliverable is gated for size/shape exactly like a workspace one.
878
+ * Absent → workspace (the default).
879
+ */
880
+ artifact: z5.boolean().optional(),
881
+ /** Step to activate on the signal. Defaults to `next`; must resolve like `next`. */
882
+ goto: z5.string().optional()
883
+ });
884
+ var ModelTierSchema = z5.enum(MODEL_TIER_ORDER);
885
+ var CraftbookStepSchema = z5.object({
886
+ id: z5.string().min(1),
887
+ name: z5.string().min(1),
888
+ description: z5.string().optional(),
889
+ /**
890
+ * Per-step prompt body. In bundled/local catalog form, this is loaded
891
+ * from the per-step `prompt.md` file (when the version manifest carries
892
+ * a separate file) and inlined at resolution time.
893
+ */
894
+ prompt: z5.string().optional(),
895
+ suggestedGezelId: z5.string().optional(),
896
+ /**
897
+ * Free-form role hint ("reviewer", "developer", "designer"). When set
898
+ * and `suggestedGezelId` / `assignee` are both absent at step
899
+ * activation, the TaskManager runs the role through `ensureGezel`
900
+ * (fuzzy roster match → gilde template → bespoke fallback) and
901
+ * persists the result onto the step's `suggestedGezelId`. Lets a
902
+ * craftbook step say "do this with a Reviewer" without baking in a
903
+ * specific gezel id. Override by setting `assignee` or
904
+ * `suggestedGezelId` at task create time.
905
+ */
906
+ suggestedRole: z5.string().optional(),
907
+ /**
908
+ * Minimum model tier to run this step unsupervised. Overrides the
909
+ * `suggestedRole`'s registry floor (roles/registry.ts) when set;
910
+ * absent → role floor → no routing. Consumed by per-step model
911
+ * routing at handoff dispatch: the cheapest installed local model
912
+ * that clears the floor runs the step.
913
+ */
914
+ capabilityFloor: ModelTierSchema.optional(),
915
+ assignee: TaskAssigneeSchema.optional(),
916
+ /** Setup scripts, run in order when the step activates. Single ref = legacy shape. */
917
+ onEnter: ScriptRefListSchema.optional(),
918
+ /**
919
+ * Cleanup scripts — the `finally` of the step. Run in order AFTER the
920
+ * gate (if any) approves; never on a gate reject. Branch predicates
921
+ * read the LAST ref's output (legacy routing; prefer gate routing).
922
+ */
923
+ onExit: ScriptRefListSchema.optional(),
924
+ /** See {@link AdvanceWhenSchema}. */
925
+ advanceWhen: AdvanceWhenSchema.optional(),
926
+ /** The end-of-step decision. See {@link StepGateSchema} (current) / {@link GateSpecSchema} (legacy). */
927
+ gate: StepGateUnionSchema.optional(),
928
+ next: z5.string().optional(),
929
+ branches: z5.array(CraftbookBranchSchema).optional(),
930
+ terminal: z5.boolean().optional(),
931
+ /**
932
+ * Marks the parent step that triggers a declarative per-item fanout
933
+ * (see {@link CraftbookSpawnSchema}). When this step activates on a
934
+ * spawn-host task, the runtime reads the craftbook's `spawn.overFile`
935
+ * workspace JSON array and spawns one child task per item — no model
936
+ * tool call. Inert unless the craftbook also declares `spawn`.
937
+ */
938
+ spawnFanout: z5.boolean().optional()
939
+ });
940
+ var CraftbookSpawnSchema = z5.object({
941
+ /** Workspace-relative JSON file the parent produces; its array drives the fanout. */
942
+ overFile: z5.string().min(1),
943
+ /** Dotted path to the array inside `overFile`. Absent → the file itself is the array. */
944
+ itemsPath: z5.string().optional(),
945
+ /** Entry step id of the child template. Defaults to the first `steps` entry. */
946
+ entryStepId: z5.string().optional(),
947
+ /** The per-child step template — same shape as a craftbook's own steps. */
948
+ steps: z5.array(CraftbookStepSchema).min(1)
949
+ });
950
+ function validateCraftbookGraph(cb) {
951
+ const problems = [];
952
+ const ids = /* @__PURE__ */ new Set();
953
+ for (const s of cb.steps) {
954
+ if (ids.has(s.id)) problems.push(`duplicate step id "${s.id}"`);
955
+ ids.add(s.id);
956
+ if (s.terminal && (s.next || s.branches && s.branches.length > 0)) {
957
+ problems.push(`step "${s.id}" is terminal but also has next/branches`);
958
+ }
959
+ }
960
+ if (!ids.has(cb.entryStepId)) {
961
+ problems.push(`entryStepId "${cb.entryStepId}" not in steps`);
962
+ }
963
+ for (const s of cb.steps) {
964
+ if (s.next && !ids.has(s.next)) {
965
+ problems.push(`step "${s.id}".next "${s.next}" missing from steps`);
966
+ }
967
+ for (const b of s.branches ?? []) {
968
+ if (!ids.has(b.goto)) {
969
+ problems.push(`step "${s.id}" branch goto "${b.goto}" missing from steps`);
970
+ }
971
+ }
972
+ if (s.advanceWhen) {
973
+ if (s.terminal) problems.push(`step "${s.id}" is terminal but also has advanceWhen`);
974
+ if (s.advanceWhen.goto && !ids.has(s.advanceWhen.goto)) {
975
+ problems.push(`step "${s.id}" advanceWhen.goto "${s.advanceWhen.goto}" missing from steps`);
976
+ }
977
+ }
978
+ if (s.gate) {
979
+ const gate = normalizeStepGate(s.gate);
980
+ if (s.terminal && gate.at === "activation") {
981
+ problems.push(`step "${s.id}" is terminal but also has an activation gate`);
982
+ }
983
+ for (const edge of gateEdgeTargets(s.gate)) {
984
+ if (!ids.has(edge)) {
985
+ problems.push(`step "${s.id}" gate route "${edge}" missing from steps`);
986
+ }
987
+ }
988
+ }
989
+ }
990
+ return problems;
991
+ }
992
+ function refineCraftbook(cb, ctx) {
993
+ for (const message of validateCraftbookGraph(cb)) {
994
+ ctx.addIssue({
995
+ code: z5.ZodIssueCode.custom,
996
+ message,
997
+ path: message.startsWith("entryStepId") ? ["entryStepId"] : ["steps"]
998
+ });
999
+ }
1000
+ for (const message of validateCraftbookScriptRefs(cb)) {
1001
+ ctx.addIssue({ code: z5.ZodIssueCode.custom, message, path: ["steps"] });
1002
+ }
1003
+ }
1004
+ var CraftbookRequirementSchema = z5.discriminatedUnion("kind", [
1005
+ /** The project is connected to a GitHub repository. */
1006
+ z5.object({ kind: z5.literal("github") }),
1007
+ /** The project's current git branch is not the main line (main/master). */
1008
+ z5.object({ kind: z5.literal("non-main-branch") })
1009
+ ]);
1010
+ var CraftbookRunModesSchema = z5.object({
1011
+ scheduled: z5.enum(["supported", "recommended"]).optional(),
1012
+ nightShift: z5.enum(["supported", "recommended"]).optional()
1013
+ });
1014
+ var CraftbookToolsetNeedSchema = z5.object({
1015
+ /** Catalog toolset id, e.g. `github`, `usb-camera`. */
1016
+ toolsetId: z5.string().min(1),
1017
+ /** Catalog source provenance (bundled/community), when pinned. */
1018
+ sourceId: z5.string().optional(),
1019
+ /** Optional semver floor; presence is recorded but not yet enforced. */
1020
+ minVersion: z5.string().optional(),
1021
+ /**
1022
+ * When true, the need is a suggestion only — surfaced as a hint, never
1023
+ * blocks invocation. Default (absent/false) = required: the launcher
1024
+ * offers install before the craftbook can run.
1025
+ */
1026
+ optional: z5.boolean().optional(),
1027
+ /**
1028
+ * When true, pre-authorize every tool this toolset exposes for the
1029
+ * duration the craftbook is active — both via a synthesized PreToolUse
1030
+ * allow-hook (in-process providers) and the Claude-CLI permission
1031
+ * broker. Off by default; opt in for unattended/scheduled craftbooks.
1032
+ */
1033
+ autoAllow: z5.boolean().optional(),
1034
+ /** Human-readable rationale shown in the launcher ("pull camera frames"). */
1035
+ reason: z5.string().optional()
1036
+ });
1037
+ var CraftbookConnectorNeedSchema = z5.object({
1038
+ /** Catalog connector-type id, e.g. `github-pulls`, `mail-gmail`. */
1039
+ typeId: z5.string().min(1),
1040
+ /** Catalog source provenance (bundled/community), when pinned. */
1041
+ sourceId: z5.string().optional(),
1042
+ /**
1043
+ * When true, the craftbook still runs without the connector bound —
1044
+ * the corpus is a bonus, not the substrate. Default (absent/false) =
1045
+ * required: the launcher offers to bind it before the craftbook runs.
1046
+ */
1047
+ optional: z5.boolean().optional(),
1048
+ /** Human-readable rationale shown in the launcher ("pull the PR diff"). */
1049
+ reason: z5.string().optional()
1050
+ });
1051
+ var CRAFTBOOK_SCRIPT_MAX_BYTES = 64 * 1024;
1052
+ var CRAFTBOOK_SCRIPTS_TOTAL_MAX_BYTES = 256 * 1024;
1053
+ var CRAFTBOOK_SCRIPTS_MAX_COUNT = 24;
1054
+ var CraftbookScriptsSchema = z5.record(ScriptNameSchema, z5.string().min(1).max(CRAFTBOOK_SCRIPT_MAX_BYTES)).superRefine((scripts, ctx) => {
1055
+ const names = Object.keys(scripts);
1056
+ if (names.length > CRAFTBOOK_SCRIPTS_MAX_COUNT) {
1057
+ ctx.addIssue({
1058
+ code: z5.ZodIssueCode.custom,
1059
+ message: `a craftbook carries at most ${CRAFTBOOK_SCRIPTS_MAX_COUNT} scripts (got ${names.length})`
1060
+ });
1061
+ }
1062
+ let total = 0;
1063
+ for (const name of names) total += scripts[name].length;
1064
+ if (total > CRAFTBOOK_SCRIPTS_TOTAL_MAX_BYTES) {
1065
+ ctx.addIssue({
1066
+ code: z5.ZodIssueCode.custom,
1067
+ message: `craftbook scripts total ${total} bytes \u2014 the ceiling is ${CRAFTBOOK_SCRIPTS_TOTAL_MAX_BYTES}`
1068
+ });
1069
+ }
1070
+ });
1071
+ var CraftbookBasedOnSchema = z5.object({
1072
+ name: z5.string().min(1),
1073
+ url: z5.string().url().regex(/^https?:\/\//i, "basedOn.url must use http or https")
1074
+ });
1075
+ function validateCraftbookScriptRefs(cb) {
1076
+ if (!cb.scripts) return [];
1077
+ const problems = [];
1078
+ const names = new Set(Object.keys(cb.scripts));
1079
+ const check = (stepId, where, refs) => {
1080
+ for (const ref of refs) {
1081
+ if ((ref.scope ?? "project") === "craftbook" && !names.has(ref.name)) {
1082
+ problems.push(
1083
+ `step "${stepId}" ${where} references craftbook script "${ref.name}" which is not in the scripts map${names.size > 0 ? ` (available: ${[...names].join(", ")})` : ""}`
1084
+ );
1085
+ }
1086
+ }
1087
+ };
1088
+ for (const s of cb.steps) {
1089
+ check(s.id, "onEnter", normalizeScriptRefs(s.onEnter));
1090
+ check(s.id, "onExit", normalizeScriptRefs(s.onExit));
1091
+ if (s.gate) check(s.id, "gate", normalizeStepGate(s.gate).scripts);
1092
+ }
1093
+ return problems;
1094
+ }
1095
+ var CraftbookSchema = z5.object({
1096
+ /** Matches catalog folder name. For ad-hoc embedded craftbooks (inline-steps tasks), a generated id. */
1097
+ id: z5.string().min(1),
1098
+ name: z5.string().min(1),
1099
+ description: z5.string().optional(),
1100
+ /** Semver from the catalog version this craftbook was resolved from. Unset for ad-hoc embedded books. */
1101
+ version: z5.string().optional(),
1102
+ /** Optional credit + link to the upstream work this craftbook adapts. */
1103
+ basedOn: CraftbookBasedOnSchema.optional(),
1104
+ plan: z5.string().optional(),
1105
+ defaultAssignee: TaskAssigneeSchema.optional(),
1106
+ steps: z5.array(CraftbookStepSchema).min(1),
1107
+ entryStepId: z5.string().min(1),
1108
+ /**
1109
+ * User-speech phrases that route to this craftbook. Matched
1110
+ * case-insensitively as substrings against the user's chat input
1111
+ * (or against a slash-command argument). Optional; absent =
1112
+ * invocable only via explicit `invoke_craftbook` / Commands panel.
1113
+ */
1114
+ triggers: z5.array(z5.string()).optional(),
1115
+ /**
1116
+ * Pre/PostToolUse hooks installed for the duration the craftbook
1117
+ * is active. The MCP bridge consults this list before forwarding
1118
+ * a `tools/call`. See `hook.ts` for the decision contract.
1119
+ */
1120
+ hooks: z5.array(HookSpecSchema).optional(),
1121
+ /**
1122
+ * Optional squisq/JSON-Schema object describing the parameters this
1123
+ * craftbook collects before it runs. Its top-level `properties` are
1124
+ * the (scalar) params, in declaration order: the command launcher
1125
+ * renders them into positional CLI tokens (`code-review <focus>
1126
+ * <intensity>`) and squisq's `JsonEditor` renders them as a form.
1127
+ * Stored permissively (an arbitrary squisq schema) and read
1128
+ * structurally; typed as squisq's `SquisqAnnotatedSchema` at the UI
1129
+ * boundary. Absent = parameterless (the command is injected directly).
1130
+ */
1131
+ paramSchema: z5.record(z5.string(), z5.unknown()).optional(),
1132
+ /**
1133
+ * CLI token the launcher stages into the terminal and that the
1134
+ * terminal recognizes. Defaults to `id` when absent — e.g. the
1135
+ * "Code Review" craftbook (id `review`) sets `command: "code-review"`.
1136
+ */
1137
+ command: z5.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
1138
+ /**
1139
+ * Prerequisites a project must satisfy for this craftbook to be
1140
+ * applicable. When unmet, the craftbook is hidden from the launcher
1141
+ * and not recognized as a terminal command. Absent = always offered.
1142
+ */
1143
+ requirements: z5.array(CraftbookRequirementSchema).optional(),
1144
+ /** Unattended launch modes this recipe is suitable for. */
1145
+ runModes: CraftbookRunModesSchema.optional(),
1146
+ /**
1147
+ * Toolsets (MCP servers / CLIs / APIs) this craftbook depends on. Drives
1148
+ * the launcher's install/config affordance and — for entries with
1149
+ * `autoAllow` — pre-authorizes those toolsets' tools while the craftbook
1150
+ * is active. See {@link CraftbookToolsetNeedSchema}. Unlike
1151
+ * `requirements`, this is carried into the runtime craftbook and the task
1152
+ * snapshot. Absent = no declared toolset dependencies.
1153
+ */
1154
+ toolsets: z5.array(CraftbookToolsetNeedSchema).optional(),
1155
+ /**
1156
+ * Connectors whose mirrored `artifacts/data/` corpus this craftbook reads. The
1157
+ * launcher binds and syncs them before the first step runs. See
1158
+ * {@link CraftbookConnectorNeedSchema}. Carried into the runtime
1159
+ * craftbook and the task snapshot so a running task records what it
1160
+ * was launched against. Absent = no connector dependencies.
1161
+ */
1162
+ connectors: z5.array(CraftbookConnectorNeedSchema).optional(),
1163
+ /**
1164
+ * Embedded script sources (name → TypeScript). See
1165
+ * {@link CraftbookScriptsSchema}. Hydrated at resolution time for
1166
+ * bundled/local/project books (their sources stay `scripts/*.ts`
1167
+ * files on disk) and carried verbatim into the task snapshot, so a
1168
+ * running task executes its gates from its own copy.
1169
+ */
1170
+ scripts: CraftbookScriptsSchema.optional(),
1171
+ /**
1172
+ * Declarative per-item fanout config. When present, the craftbook is a
1173
+ * spawn host: its `spawnFanout` step fans out one child per item in
1174
+ * `spawn.overFile`. See {@link CraftbookSpawnSchema}. Carried into the
1175
+ * task snapshot so the runtime reads it at fanout time.
1176
+ */
1177
+ spawn: CraftbookSpawnSchema.optional(),
1178
+ createdAt: z5.string(),
1179
+ updatedAt: z5.string()
1180
+ }).superRefine(refineCraftbook);
1181
+ var DeliverableKindSchema = z5.enum([
1182
+ "html-game",
1183
+ "html-multiscreen-game",
1184
+ "html-page",
1185
+ "html-marketing-site",
1186
+ "markdown-doc",
1187
+ "markdown-report",
1188
+ "markdown-notes",
1189
+ "json",
1190
+ "yaml-spec",
1191
+ "code-module",
1192
+ "code-with-tests",
1193
+ "security-report",
1194
+ "image-set",
1195
+ "audio-file",
1196
+ "slide-deck",
1197
+ "data-file",
1198
+ "generic-file"
1199
+ ]);
1200
+ var StepDeliverableSchema = z5.object({
1201
+ /** Workspace-relative file the step must produce. The only required field. */
1202
+ path: z5.string().min(1),
1203
+ /** Artifact class; inferred from the file extension when absent. */
1204
+ kind: DeliverableKindSchema.optional(),
1205
+ /** Override the class-default byte floor. */
1206
+ minBytes: z5.number().int().positive().optional(),
1207
+ /** Gate rejections before the task pauses for help. */
1208
+ maxAttempts: z5.number().int().positive().optional(),
1209
+ /** Gate the artifacts drawer instead of the workspace. */
1210
+ artifact: z5.boolean().optional(),
1211
+ /**
1212
+ * The deliverable is an EDIT to a pre-existing file (fix/refactor):
1213
+ * presence alone never advances the step — the assignee must have
1214
+ * written to the file this turn. See {@link AdvanceWhenSchema}.
1215
+ */
1216
+ requireChange: z5.boolean().optional(),
1217
+ /**
1218
+ * For code deliverables: additionally execute the file in the sandbox
1219
+ * and require exit 0 (a `node:test`/`assert` file exits nonzero on
1220
+ * failure — exactly the contract). Opt-in.
1221
+ */
1222
+ execute: z5.boolean().optional(),
1223
+ /** For data deliverables: required column names (adds a tableShape check). */
1224
+ columns: z5.array(z5.string().min(1)).min(1).optional(),
1225
+ /** For data deliverables: minimum row count (adds a tableShape check). */
1226
+ minRows: z5.number().int().positive().optional()
1227
+ });
1228
+ var NewCraftbookStepSchema = z5.object({
1229
+ id: z5.string().optional(),
1230
+ name: z5.string().min(1),
1231
+ description: z5.string().optional(),
1232
+ prompt: z5.string().optional(),
1233
+ suggestedGezelId: z5.string().optional(),
1234
+ /** See {@link CraftbookStepSchema.shape.suggestedRole}. */
1235
+ suggestedRole: z5.string().optional(),
1236
+ /** See {@link CraftbookStepSchema.shape.capabilityFloor}. */
1237
+ capabilityFloor: ModelTierSchema.optional(),
1238
+ assignee: TaskAssigneeSchema.optional(),
1239
+ onEnter: ScriptRefListSchema.optional(),
1240
+ onExit: ScriptRefListSchema.optional(),
1241
+ advanceWhen: AdvanceWhenSchema.optional(),
1242
+ gate: StepGateUnionSchema.optional(),
1243
+ /** See {@link StepDeliverableSchema} — one field attaches the enforced gate. */
1244
+ deliverable: StepDeliverableSchema.optional(),
1245
+ next: z5.string().optional(),
1246
+ branches: z5.array(CraftbookBranchSchema).optional(),
1247
+ terminal: z5.boolean().optional(),
1248
+ /** See {@link CraftbookStepSchema.shape.spawnFanout}. */
1249
+ spawnFanout: z5.boolean().optional()
1250
+ });
1251
+ var CreateCraftbookRequestSchema = z5.object({
1252
+ id: z5.string().optional(),
1253
+ name: z5.string().min(1),
1254
+ description: z5.string().optional(),
1255
+ basedOn: CraftbookBasedOnSchema.optional(),
1256
+ plan: z5.string().optional(),
1257
+ defaultAssignee: TaskAssigneeSchema.optional(),
1258
+ steps: z5.array(NewCraftbookStepSchema).min(1),
1259
+ entryStepId: z5.string().optional(),
1260
+ paramSchema: z5.record(z5.string(), z5.unknown()).optional(),
1261
+ command: z5.string().regex(/^[a-z][a-z0-9-]*$/).optional(),
1262
+ requirements: z5.array(CraftbookRequirementSchema).optional(),
1263
+ runModes: CraftbookRunModesSchema.optional(),
1264
+ toolsets: z5.array(CraftbookToolsetNeedSchema).optional(),
1265
+ scripts: CraftbookScriptsSchema.optional()
1266
+ });
1267
+ var UpdateCraftbookRequestSchema = z5.object({
1268
+ name: z5.string().min(1).optional(),
1269
+ description: z5.string().nullable().optional(),
1270
+ basedOn: CraftbookBasedOnSchema.nullable().optional(),
1271
+ plan: z5.string().nullable().optional(),
1272
+ defaultAssignee: TaskAssigneeSchema.nullable().optional(),
1273
+ steps: z5.array(NewCraftbookStepSchema).min(1).optional(),
1274
+ entryStepId: z5.string().optional(),
1275
+ paramSchema: z5.record(z5.string(), z5.unknown()).nullable().optional(),
1276
+ command: z5.string().regex(/^[a-z][a-z0-9-]*$/).nullable().optional(),
1277
+ requirements: z5.array(CraftbookRequirementSchema).nullable().optional(),
1278
+ runModes: CraftbookRunModesSchema.nullable().optional(),
1279
+ toolsets: z5.array(CraftbookToolsetNeedSchema).nullable().optional(),
1280
+ /** Full-replace semantics: the map is the truth. `null` clears all scripts. */
1281
+ scripts: CraftbookScriptsSchema.nullable().optional()
1282
+ });
1283
+ var CraftbookSummarySchema = z5.object({
1284
+ id: z5.string(),
1285
+ name: z5.string(),
1286
+ description: z5.string().optional(),
1287
+ version: z5.string().optional(),
1288
+ basedOn: CraftbookBasedOnSchema.optional(),
1289
+ /**
1290
+ * Where the craftbook came from. `bundled` — shipped catalog;
1291
+ * `local` — user-authored under `~/.gezel/craftbook-templates/`;
1292
+ * `project` — project-local, defined in a workspace `.gezel/craftbooks/`
1293
+ * folder (often auto-imported from a `.claude/skills` / `agents/skills`
1294
+ * SKILL.md). Project craftbooks only surface inside their own project.
1295
+ */
1296
+ source: z5.enum(["bundled", "local", "project"]),
1297
+ stepCount: z5.number().int().nonnegative()
1298
+ });
1299
+ var ListCraftbooksResponseSchema = z5.object({
1300
+ craftbooks: z5.array(CraftbookSummarySchema)
1301
+ });
1302
+ var ProjectCraftbookProvenanceSchema = z5.object({
1303
+ installedBy: z5.literal("project-type"),
1304
+ typeId: z5.string(),
1305
+ typeVersion: z5.string(),
1306
+ bookVersion: z5.string(),
1307
+ /** sha256 over the installed book's canonical recipe JSON. */
1308
+ contentHash: z5.string(),
1309
+ installedAt: z5.string()
1310
+ });
1311
+ var CraftbookSuggestionSchema = z5.object({
1312
+ id: z5.string(),
1313
+ name: z5.string(),
1314
+ description: z5.string().optional(),
1315
+ source: z5.enum(["bundled", "local", "project"]),
1316
+ version: z5.string().optional(),
1317
+ basedOn: CraftbookBasedOnSchema.optional(),
1318
+ stepCount: z5.number().int().nonnegative(),
1319
+ tags: z5.array(z5.string()).optional(),
1320
+ triggers: z5.array(z5.string()).optional(),
1321
+ score: z5.number(),
1322
+ semantic: z5.number().optional(),
1323
+ lexical: z5.number()
1324
+ });
1325
+ var SuggestCraftbooksResponseSchema = z5.object({
1326
+ suggestions: z5.array(CraftbookSuggestionSchema)
1327
+ });
1328
+ var CraftbookResponseSchema = z5.object({
1329
+ craftbook: CraftbookSchema
1330
+ });
1331
+ function slugifyStepId(input) {
1332
+ return input.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64) || "step";
1333
+ }
1334
+ var StepPositionSchema = z5.object({
1335
+ /** Insert immediately after the step with this id. */
1336
+ after: z5.string().optional(),
1337
+ /** Insert immediately before the step with this id. */
1338
+ before: z5.string().optional(),
1339
+ /** Absolute insertion index (clamped to [0, length]). */
1340
+ index: z5.number().int().nonnegative().optional()
1341
+ });
1342
+
1343
+ // src/markdown/frontmatter.ts
1344
+ import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
1345
+ function parseYamlFrontmatter(source) {
1346
+ const opening = /^(?:\uFEFF)?---[ \t]*(?:\r?\n|$)/.exec(source);
1347
+ if (!opening) return { data: {}, content: source };
1348
+ const closing = /^(?:---|\.\.\.)[ \t]*(?:\r?\n|$)/gm;
1349
+ closing.lastIndex = opening[0].length;
1350
+ const end = closing.exec(source);
1351
+ if (!end) throw new Error("YAML frontmatter is missing its closing delimiter");
1352
+ const yamlText = source.slice(opening[0].length, end.index);
1353
+ const value = parseYaml(yamlText, {
1354
+ prettyErrors: false,
1355
+ uniqueKeys: true
1356
+ });
1357
+ if (value === null || value === void 0) {
1358
+ return { data: {}, content: source.slice(closing.lastIndex) };
1359
+ }
1360
+ if (typeof value !== "object" || Array.isArray(value)) {
1361
+ throw new Error("YAML frontmatter must contain a mapping");
1362
+ }
1363
+ return {
1364
+ data: value,
1365
+ content: source.slice(closing.lastIndex)
1366
+ };
1367
+ }
1368
+ function stringifyYamlFrontmatter(content, data) {
1369
+ const yaml = stringifyYaml(data, {
1370
+ lineWidth: 0,
1371
+ minContentWidth: 0
1372
+ });
1373
+ return `---
1374
+ ${yaml}---
1375
+ ${content || "\n"}`;
1376
+ }
1377
+ function parseYamlMapping(source) {
1378
+ if (source.trim().length === 0) return {};
1379
+ const value = parseYaml(source, {
1380
+ prettyErrors: false,
1381
+ uniqueKeys: true
1382
+ });
1383
+ if (value === null || value === void 0) return {};
1384
+ if (typeof value !== "object" || Array.isArray(value)) {
1385
+ throw new Error("YAML block must contain a mapping");
1386
+ }
1387
+ return value;
1388
+ }
1389
+ function stringifyYamlMapping(data) {
1390
+ return stringifyYaml(data, {
1391
+ lineWidth: 0,
1392
+ minContentWidth: 0
1393
+ });
1394
+ }
1395
+
1396
+ // src/markdown/md-structure.ts
1397
+ var FENCE = /^(```+|~~~+)(.*)$/;
1398
+ function splitSections(lines) {
1399
+ const sections = [];
1400
+ const preamble = [];
1401
+ let current = null;
1402
+ let openFence = null;
1403
+ for (const line of lines) {
1404
+ const fence = FENCE.exec(line.trim());
1405
+ if (openFence !== null) {
1406
+ if (fence && fence[1][0] === openFence[0] && fence[1].length >= openFence.length && fence[2].trim() === "") {
1407
+ openFence = null;
1408
+ }
1409
+ } else if (fence) {
1410
+ openFence = fence[1];
1411
+ } else if (/^##\s+/.test(line)) {
1412
+ if (current) sections.push({ heading: current.heading, body: current.body.join("\n") });
1413
+ current = { heading: line.trim(), body: [] };
1414
+ continue;
1415
+ }
1416
+ if (current) current.body.push(line);
1417
+ else preamble.push(line);
1418
+ }
1419
+ if (current) sections.push({ heading: current.heading, body: current.body.join("\n") });
1420
+ return { preamble: preamble.join("\n"), sections };
1421
+ }
1422
+ function findFirstH1(lines) {
1423
+ let openFence = null;
1424
+ for (let i = 0; i < lines.length; i++) {
1425
+ const line = lines[i];
1426
+ const fence = FENCE.exec(line.trim());
1427
+ if (openFence !== null) {
1428
+ if (fence && fence[1][0] === openFence[0] && fence[1].length >= openFence.length && fence[2].trim() === "") {
1429
+ openFence = null;
1430
+ }
1431
+ continue;
1432
+ }
1433
+ if (fence) {
1434
+ openFence = fence[1];
1435
+ continue;
1436
+ }
1437
+ const h1 = /^#\s+(.+?)\s*$/.exec(line);
1438
+ if (h1) return { index: i, title: h1[1] };
1439
+ }
1440
+ return null;
1441
+ }
1442
+ function extractFirstFence(body) {
1443
+ const lines = body.split("\n");
1444
+ let inFence = false;
1445
+ let fenceMarker = "";
1446
+ let out = null;
1447
+ for (const line of lines) {
1448
+ const fence = FENCE.exec(line);
1449
+ if (fence && !inFence) {
1450
+ inFence = true;
1451
+ fenceMarker = fence[1];
1452
+ out = [];
1453
+ continue;
1454
+ }
1455
+ if (inFence && isFenceClose(line, fenceMarker)) {
1456
+ return out.join("\n");
1457
+ }
1458
+ if (inFence) out.push(line);
1459
+ }
1460
+ return void 0;
1461
+ }
1462
+ function extractAllFences(body) {
1463
+ const lines = body.split("\n");
1464
+ const fences = [];
1465
+ let inFence = false;
1466
+ let fenceMarker = "";
1467
+ let lang = "";
1468
+ let buf = [];
1469
+ for (const line of lines) {
1470
+ const fence = FENCE.exec(line);
1471
+ if (fence && !inFence) {
1472
+ inFence = true;
1473
+ fenceMarker = fence[1];
1474
+ lang = fence[2].trim().toLowerCase();
1475
+ buf = [];
1476
+ continue;
1477
+ }
1478
+ if (inFence && isFenceClose(line, fenceMarker)) {
1479
+ inFence = false;
1480
+ fences.push({ lang, code: buf.join("\n") });
1481
+ continue;
1482
+ }
1483
+ if (inFence) buf.push(line);
1484
+ }
1485
+ return fences;
1486
+ }
1487
+ function isFenceClose(line, marker) {
1488
+ const m = FENCE.exec(line.trim());
1489
+ return m !== null && m[1][0] === marker[0] && m[1].length >= marker.length && m[2].trim() === "";
1490
+ }
1491
+ function parseYamlBlock(yamlText) {
1492
+ return parseYamlMapping(yamlText);
1493
+ }
1494
+ function stringifyYamlBlock(fields) {
1495
+ return stringifyYamlMapping(fields);
1496
+ }
1497
+ function pickFence(source) {
1498
+ let longest = 2;
1499
+ for (const m of source.matchAll(/`{3,}/g)) longest = Math.max(longest, m[0].length);
1500
+ return "`".repeat(longest + 1);
1501
+ }
1502
+
1503
+ // src/markdown/craftbook-md.ts
1504
+ var STEP_HEADING = /^##\s+step\s*:\s*(.+?)\s*$/i;
1505
+ var SCRIPT_HEADING = /^##\s+script\s*:\s*(.+?)\s*$/i;
1506
+ var FRONTMATTER_KEYS = [
1507
+ "id",
1508
+ "name",
1509
+ "entry",
1510
+ "basedOn",
1511
+ "triggers",
1512
+ "command",
1513
+ "plan",
1514
+ "defaultAssignee",
1515
+ "requirements",
1516
+ "runModes",
1517
+ "toolsets",
1518
+ "connectors",
1519
+ "paramSchema",
1520
+ "hooks",
1521
+ "version",
1522
+ "releasedAt"
1523
+ ];
1524
+ var STEP_FENCE_KEYS = [
1525
+ "id",
1526
+ "description",
1527
+ "suggestedGezelId",
1528
+ "suggestedRole",
1529
+ "capabilityFloor",
1530
+ "assignee",
1531
+ "deliverable",
1532
+ "onEnter",
1533
+ "onExit",
1534
+ "advanceWhen",
1535
+ "gate",
1536
+ "next",
1537
+ "branches",
1538
+ "terminal"
1539
+ ];
1540
+ function parseCraftbookMarkdown(text) {
1541
+ const errors = [];
1542
+ let fm;
1543
+ let content;
1544
+ try {
1545
+ const parsed = parseYamlFrontmatter(text);
1546
+ fm = parsed.data;
1547
+ content = parsed.content;
1548
+ } catch (err) {
1549
+ return {
1550
+ ok: false,
1551
+ errors: [
1552
+ {
1553
+ where: "frontmatter",
1554
+ message: `the YAML frontmatter does not parse: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
1555
+ fix: "check indentation and quoting between the opening and closing --- lines"
1556
+ }
1557
+ ]
1558
+ };
1559
+ }
1560
+ const lines = content.split(/\r?\n/);
1561
+ const sections = splitSections(lines);
1562
+ const doc = {};
1563
+ for (const key of FRONTMATTER_KEYS) {
1564
+ if (fm[key] !== void 0 && fm[key] !== null) {
1565
+ doc[key === "entry" ? "entryStepId" : key] = fm[key];
1566
+ }
1567
+ }
1568
+ for (const key of Object.keys(fm)) {
1569
+ if (!FRONTMATTER_KEYS.includes(key)) {
1570
+ errors.push({
1571
+ where: "frontmatter",
1572
+ message: `unknown key "${key}".`,
1573
+ fix: `legal keys: ${FRONTMATTER_KEYS.join(", ")} \u2014 step fields go in the step's yaml block`
1574
+ });
1575
+ }
1576
+ }
1577
+ if (sections.preamble.trim().length > 0) doc.description = sections.preamble.trim();
1578
+ const steps = [];
1579
+ const scripts = {};
1580
+ for (const section of sections.sections) {
1581
+ const stepMatch = STEP_HEADING.exec(section.heading);
1582
+ const scriptMatch = SCRIPT_HEADING.exec(section.heading);
1583
+ if (stepMatch) {
1584
+ const name = stepMatch[1];
1585
+ const { fenced, prose, fenceError } = extractStepSection(section.body);
1586
+ let fields = {};
1587
+ if (fenceError) {
1588
+ errors.push({
1589
+ where: `section "## Step: ${name}"`,
1590
+ message: fenceError,
1591
+ fix: "put the step's structured fields in one ```yaml fence directly under the heading"
1592
+ });
1593
+ } else if (fenced !== void 0) {
1594
+ try {
1595
+ fields = parseYamlBlock(fenced);
1596
+ } catch (err) {
1597
+ errors.push({
1598
+ where: `section "## Step: ${name}"`,
1599
+ message: `the yaml step block does not parse: ${err instanceof Error ? err.message.split("\n")[0] : String(err)}`,
1600
+ fix: "check indentation/quoting inside the ```yaml fence \u2014 flow-style JSON values are also legal"
1601
+ });
1602
+ }
1603
+ }
1604
+ const step = { ...fields, name };
1605
+ if (prose.trim().length > 0) step.prompt = prose.trim();
1606
+ steps.push(step);
1607
+ } else if (scriptMatch) {
1608
+ const name = scriptMatch[1];
1609
+ const source = extractFirstFence(section.body);
1610
+ if (source === void 0) {
1611
+ errors.push({
1612
+ where: `section "## Script: ${name}"`,
1613
+ message: "no fenced code block found under this heading.",
1614
+ fix: "put the script source in one ```ts fence directly under the heading"
1615
+ });
1616
+ } else {
1617
+ scripts[name] = source;
1618
+ }
1619
+ } else {
1620
+ errors.push({
1621
+ where: `section "${section.heading}"`,
1622
+ message: "unrecognized section heading.",
1623
+ fix: "every `## ` heading must be `## Step: <name>` or `## Script: <name>`"
1624
+ });
1625
+ }
1626
+ }
1627
+ if (steps.length > 0) doc.steps = steps;
1628
+ if (Object.keys(scripts).length > 0) doc.scripts = scripts;
1629
+ if (steps.length === 0) {
1630
+ errors.push({
1631
+ where: "document",
1632
+ message: "no steps found.",
1633
+ fix: "add at least one `## Step: <name>` section"
1634
+ });
1635
+ }
1636
+ return errors.length > 0 ? { ok: false, errors } : { ok: true, doc, errors: [] };
1637
+ }
1638
+ function serializeCraftbookMarkdown(doc) {
1639
+ const fm = {};
1640
+ for (const key of FRONTMATTER_KEYS) {
1641
+ const docKey = key === "entry" ? "entryStepId" : key;
1642
+ const value = doc[docKey];
1643
+ if (value !== void 0) fm[key] = value;
1644
+ }
1645
+ const parts = [];
1646
+ if (doc.description) parts.push(doc.description.trim());
1647
+ for (const step of doc.steps) {
1648
+ const fields = {};
1649
+ for (const key of STEP_FENCE_KEYS) {
1650
+ const value = step[key];
1651
+ if (value !== void 0) fields[key] = value;
1652
+ }
1653
+ let section = `## Step: ${step.name}`;
1654
+ if (Object.keys(fields).length > 0) {
1655
+ section += `
1656
+
1657
+ \`\`\`yaml
1658
+ ${stringifyYamlBlock(fields)}\`\`\``;
1659
+ }
1660
+ if (step.prompt) section += `
1661
+
1662
+ ${step.prompt.trim()}`;
1663
+ parts.push(section);
1664
+ }
1665
+ for (const [name, source] of Object.entries(doc.scripts ?? {})) {
1666
+ const fence = pickFence(source);
1667
+ parts.push(`## Script: ${name}
1668
+
1669
+ ${fence}ts
1670
+ ${source.trimEnd()}
1671
+ ${fence}`);
1672
+ }
1673
+ const body = `${parts.join("\n\n")}
1674
+ `;
1675
+ return Object.keys(fm).length > 0 ? stringifyYamlFrontmatter(body, fm) : body;
1676
+ }
1677
+ function extractStepSection(body) {
1678
+ const lines = body.split("\n");
1679
+ const proseLines = [];
1680
+ let fenced = null;
1681
+ let inFence = false;
1682
+ let fenceTag = "";
1683
+ let taken = false;
1684
+ let fenceMarker = "";
1685
+ for (const line of lines) {
1686
+ const fence = FENCE.exec(line);
1687
+ if (fence && !inFence) {
1688
+ inFence = true;
1689
+ fenceMarker = fence[1];
1690
+ fenceTag = fence[2].trim().toLowerCase();
1691
+ if (!taken && (fenceTag === "yaml" || fenceTag === "yml" || fenceTag === "")) {
1692
+ fenced = [];
1693
+ } else {
1694
+ proseLines.push(line);
1695
+ }
1696
+ continue;
1697
+ }
1698
+ if (inFence && isFenceClose(line, fenceMarker)) {
1699
+ inFence = false;
1700
+ if (fenced !== null && !taken) {
1701
+ taken = true;
1702
+ } else {
1703
+ proseLines.push(line);
1704
+ }
1705
+ continue;
1706
+ }
1707
+ if (inFence && fenced !== null && !taken) {
1708
+ fenced.push(line);
1709
+ continue;
1710
+ }
1711
+ proseLines.push(line);
1712
+ }
1713
+ if (inFence) {
1714
+ return { prose: proseLines.join("\n"), fenceError: "a code fence is never closed." };
1715
+ }
1716
+ return {
1717
+ ...taken && fenced !== null ? { fenced: fenced.join("\n") } : {},
1718
+ prose: proseLines.join("\n")
1719
+ };
1720
+ }
1721
+ function defaultStepIdForName(name) {
1722
+ return slugifyStepId(name);
1723
+ }
1724
+
1725
+ // src/schemas/gezel.ts
1726
+ import { z as z14 } from "zod";
1727
+
1728
+ // src/poppetje/schema.ts
1729
+ import { z as z6 } from "zod";
1730
+
1731
+ // src/poppetje/catalogs.ts
1732
+ var BODY_ARCHETYPES = {
1733
+ broad: {
1734
+ baseW: 46,
1735
+ hipW: 52,
1736
+ waistW: 52,
1737
+ chestW: 56,
1738
+ shoulderW: 54,
1739
+ label: "broad",
1740
+ note: "Barrel torso, broad through the shoulders. Reads adult, sturdy."
1741
+ },
1742
+ tapered: {
1743
+ baseW: 58,
1744
+ hipW: 52,
1745
+ waistW: 46,
1746
+ chestW: 38,
1747
+ shoulderW: 32,
1748
+ label: "tapered",
1749
+ note: "Classic peg-doll cone. Wide base, smooth taper to a narrow top."
1750
+ },
1751
+ curvy: {
1752
+ baseW: 48,
1753
+ hipW: 60,
1754
+ waistW: 34,
1755
+ chestW: 46,
1756
+ shoulderW: 36,
1757
+ label: "curvy",
1758
+ note: "Hourglass \u2014 hip flare, pinched waist, narrower shoulders."
1759
+ },
1760
+ athletic: {
1761
+ baseW: 38,
1762
+ hipW: 40,
1763
+ waistW: 42,
1764
+ chestW: 54,
1765
+ shoulderW: 56,
1766
+ label: "athletic",
1767
+ note: "V-shape. Wider chest and shoulders, narrower waist and hips."
1768
+ },
1769
+ slender: {
1770
+ baseW: 46,
1771
+ hipW: 42,
1772
+ waistW: 34,
1773
+ chestW: 36,
1774
+ shoulderW: 32,
1775
+ label: "slender",
1776
+ note: "Narrow throughout. Reads tall and lean."
1777
+ },
1778
+ stout: {
1779
+ baseW: 62,
1780
+ hipW: 64,
1781
+ waistW: 62,
1782
+ chestW: 58,
1783
+ shoulderW: 50,
1784
+ label: "stout",
1785
+ note: "Wide throughout. Reads grounded, generous."
1786
+ }
1787
+ };
1788
+ var BODY_SHAPE_KEYS = Object.keys(BODY_ARCHETYPES);
1789
+ var FIGURE_SCALES = {
1790
+ adult: { bodyScale: 1, headScale: 1, label: "adult", note: "Standard crew member." },
1791
+ shorter: {
1792
+ bodyScale: 0.88,
1793
+ headScale: 0.98,
1794
+ label: "shorter",
1795
+ note: "Older or simply shorter adult."
1796
+ },
1797
+ child: {
1798
+ bodyScale: 0.78,
1799
+ headScale: 0.96,
1800
+ label: "child",
1801
+ note: "Short body, head reads proportionally larger."
1802
+ },
1803
+ toddler: { bodyScale: 0.76, headScale: 0.95, label: "toddler", note: "Smallest. Mostly head." }
1804
+ };
1805
+ var FIGURE_SCALE_KEYS = Object.keys(FIGURE_SCALES);
1806
+ var HAT_OPTIONS = ["cap", "beanie", "kerchief", "straw", "newsboy", "hood"];
1807
+ var DRESS_OPTIONS = ["scarf", "apron", "collar", "turtleneck"];
1808
+ var HAIR_SHAPES = ["halo", "short", "long", "bun", "braids", "shaved", "bald"];
1809
+ var ACCESSORY_OPTIONS = [
1810
+ "glasses",
1811
+ "sunglasses",
1812
+ "cateye",
1813
+ "readers",
1814
+ "monocle",
1815
+ "eyepatch",
1816
+ "earrings",
1817
+ "earring-left",
1818
+ "earring-right",
1819
+ "flower",
1820
+ "hairclip",
1821
+ "headband",
1822
+ "bowtie",
1823
+ "necklace",
1824
+ "brooch",
1825
+ "facemask",
1826
+ "goggles",
1827
+ "safety-glasses",
1828
+ "pince-nez",
1829
+ "headphones",
1830
+ "hearing-aid",
1831
+ "nose-ring",
1832
+ "hoop-earrings",
1833
+ "drop-earrings",
1834
+ "pearl-earrings",
1835
+ "bandage",
1836
+ "feather",
1837
+ "pencil",
1838
+ "ribbon",
1839
+ "necktie",
1840
+ "cravat",
1841
+ "bolo-tie",
1842
+ "lanyard",
1843
+ "medal",
1844
+ "pocket-square",
1845
+ "tool-pendant"
1846
+ ];
1847
+ var FACIAL_HAIR_OPTIONS = ["beard", "mustache"];
1848
+ var MARK_OPTIONS = ["freckles", "mole"];
1849
+ var SHIRT_PATTERN_OPTIONS = [
1850
+ "plain",
1851
+ "buttons",
1852
+ "stripes",
1853
+ "sash",
1854
+ "yoke",
1855
+ "twotone"
1856
+ ];
1857
+ var EXPRESSION_OPTIONS = ["smile", "wider", "neutral", "wink", "sleepy"];
1858
+
1859
+ // src/poppetje/schema.ts
1860
+ function migrateLegacyAccessory(raw) {
1861
+ if (!raw || typeof raw !== "object") return raw;
1862
+ const r = { ...raw };
1863
+ const acc = r.accessory;
1864
+ if (acc === "beard" || acc === "mustache") {
1865
+ if (r.facialHair == null) r.facialHair = acc;
1866
+ r.accessory = null;
1867
+ } else if (acc === "freckles" || acc === "mole") {
1868
+ if (r.mark == null) r.mark = acc;
1869
+ r.accessory = null;
1870
+ } else if (acc === "earring") {
1871
+ r.accessory = "earrings";
1872
+ }
1873
+ return r;
1874
+ }
1875
+ var PoppetjeSchema = z6.preprocess(
1876
+ migrateLegacyAccessory,
1877
+ z6.object({
1878
+ /** Stable id; locks the wood-grain noise seed. Pin to the gezel id at creation. */
1879
+ key: z6.string().min(1),
1880
+ name: z6.string(),
1881
+ bodyShape: z6.enum(BODY_SHAPE_KEYS),
1882
+ figureScale: z6.enum(FIGURE_SCALE_KEYS),
1883
+ /** Mid-tone of the head. Paired with skin2 for the lateral gradient. */
1884
+ skin: z6.string(),
1885
+ /** Darker edge tone. */
1886
+ skin2: z6.string(),
1887
+ hair: z6.string(),
1888
+ hairShape: z6.enum(HAIR_SHAPES),
1889
+ /** Replaces hair when set; 'hood' renders at body level. */
1890
+ hat: z6.enum(HAT_OPTIONS).nullable().optional().default(null),
1891
+ /** Body overlay; null for an unadorned shirt. */
1892
+ dress: z6.enum(DRESS_OPTIONS).nullable().optional().default(null),
1893
+ /** Wearable face accessory (glasses, earring, monocle). User-toggleable. */
1894
+ accessory: z6.enum(ACCESSORY_OPTIONS).nullable().optional().default(null),
1895
+ /** Facial hair (beard, mustache). Physical — reroll-only. */
1896
+ facialHair: z6.enum(FACIAL_HAIR_OPTIONS).nullable().optional().default(null),
1897
+ /** Small facial detail (freckles, mole). Physical — reroll-only. */
1898
+ mark: z6.enum(MARK_OPTIONS).nullable().optional().default(null),
1899
+ expression: z6.enum(EXPRESSION_OPTIONS).optional().default("smile"),
1900
+ shirt: z6.string(),
1901
+ shirtAccent: z6.string(),
1902
+ /**
1903
+ * Painted garment pattern over the shirt (buttons, stripes, sash, …).
1904
+ * Files written before patterns existed parse as `plain` — the bare
1905
+ * shirt those characters always had.
1906
+ */
1907
+ shirtPattern: z6.enum(SHIRT_PATTERN_OPTIONS).optional().default("plain")
1908
+ })
1909
+ );
1910
+
1911
+ // src/schemas/codex.ts
1912
+ import { z as z7 } from "zod";
1913
+ var CodexPermissionModeSchema = z7.enum(["plan", "edit", "reviewed", "full"]);
1914
+ var LegacyCodexPermissionModeSchema = z7.enum([
1915
+ "default",
1916
+ "acceptEdits",
1917
+ "bypassPermissions"
1918
+ ]);
1919
+ var CodexPermissionModeCompatSchema = z7.union([
1920
+ CodexPermissionModeSchema,
1921
+ LegacyCodexPermissionModeSchema
1922
+ ]);
1923
+
1924
+ // src/schemas/growth.ts
1925
+ import { z as z9 } from "zod";
1926
+
1927
+ // src/schemas/tuning-profile-registry.ts
1928
+ import { z as z8 } from "zod";
1929
+ var TuningProfileIdSchema = z8.string().min(1).describe("Canonical tuning profile id.");
1930
+
1931
+ // src/schemas/growth.ts
1932
+ var GrowthEvidenceSchema = z9.object({
1933
+ /** Memory-file day (YYYY-MM-DD) — rewritten server-side from the matched entry. */
1934
+ day: z9.string().regex(/^\d{4}-\d{2}-\d{2}$/),
1935
+ kind: z9.enum(["fact", "decision", "pref", "status"]),
1936
+ excerpt: z9.string().min(1).max(400)
1937
+ });
1938
+ var TraitProposalSchema = z9.object({
1939
+ id: z9.string(),
1940
+ kind: z9.literal("trait"),
1941
+ title: z9.string().min(1).max(80),
1942
+ /** One imperative second-person sentence destined for the prompt. */
1943
+ traitText: z9.string().min(1).max(200),
1944
+ evidence: z9.array(GrowthEvidenceSchema).min(1).max(3)
1945
+ });
1946
+ var TuningActionSchema = z9.discriminatedUnion("type", [
1947
+ z9.object({ type: z9.literal("profile"), profile: TuningProfileIdSchema }),
1948
+ /** Resolved + clamped at accept time against the then-current frontmatter. */
1949
+ z9.object({ type: z9.literal("temperature"), delta: z9.union([z9.literal(0.1), z9.literal(-0.1)]) })
1950
+ ]);
1951
+ var TuningProposalSchema = z9.object({
1952
+ id: z9.string(),
1953
+ kind: z9.literal("tuning"),
1954
+ title: z9.string().max(80),
1955
+ description: z9.string().max(300),
1956
+ action: TuningActionSchema
1957
+ });
1958
+ var CosmeticProposalSchema = z9.object({
1959
+ id: z9.string(),
1960
+ kind: z9.literal("cosmetic"),
1961
+ title: z9.string().max(80),
1962
+ /** Key into GROWTH_COSMETICS, or a generic `level-N` milestone marker. */
1963
+ cosmeticId: z9.string()
1964
+ });
1965
+ var GrowthProposalSchema = z9.discriminatedUnion("kind", [
1966
+ TraitProposalSchema,
1967
+ TuningProposalSchema,
1968
+ CosmeticProposalSchema
1969
+ ]);
1970
+ var PendingLevelUpSchema = z9.object({
1971
+ toLevel: z9.number().int().min(2),
1972
+ proposals: z9.array(GrowthProposalSchema).min(1).max(5),
1973
+ createdAt: z9.string()
1974
+ });
1975
+ var GrowthSignalsSchema = z9.object({
1976
+ memoryXp: z9.number().int().nonnegative().default(0),
1977
+ lessonsXp: z9.number().int().nonnegative().default(0),
1978
+ taskXp: z9.number().int().nonnegative().default(0),
1979
+ consultXp: z9.number().int().nonnegative().default(0)
1980
+ });
1981
+ var AdoptedTraitRecordSchema = z9.object({
1982
+ traitId: z9.string(),
1983
+ text: z9.string(),
1984
+ level: z9.number().int(),
1985
+ adoptedAt: z9.string(),
1986
+ evidence: z9.array(GrowthEvidenceSchema),
1987
+ /** Set when the user later retires the trait — kept for the character sheet. */
1988
+ removedAt: z9.string().optional()
1989
+ });
1990
+ var DeclinedProposalRecordSchema = z9.object({
1991
+ kind: z9.enum(["trait", "tuning", "cosmetic"]),
1992
+ title: z9.string(),
1993
+ /** Used for never-re-offer matching on trait proposals. */
1994
+ traitText: z9.string().optional(),
1995
+ level: z9.number().int(),
1996
+ declinedAt: z9.string()
1997
+ });
1998
+ var GezelGrowthStateSchema = z9.object({
1999
+ version: z9.literal(1).default(1),
2000
+ level: z9.number().int().min(1).default(1),
2001
+ xp: z9.number().int().nonnegative().default(0),
2002
+ signals: GrowthSignalsSchema.prefault({}),
2003
+ lastComputedAt: z9.string().optional(),
2004
+ pendingLevelUp: PendingLevelUpSchema.optional(),
2005
+ adoptedTraits: z9.array(AdoptedTraitRecordSchema).default([]),
2006
+ declinedProposals: z9.array(DeclinedProposalRecordSchema).default([]),
2007
+ unlockedCosmetics: z9.array(z9.object({ id: z9.string(), at: z9.string() })).default([])
2008
+ });
2009
+ var GezelGrowthSummarySchema = z9.object({
2010
+ level: z9.number().int().min(1),
2011
+ /** True when a level-up is waiting for the user's choice. */
2012
+ pending: z9.boolean().optional()
2013
+ });
2014
+
2015
+ // src/schemas/model-tuning.ts
2016
+ import { z as z10 } from "zod";
2017
+ var DrySamplerSchema = z10.object({
2018
+ multiplier: z10.number().min(0).max(5).describe("DRY penalty strength. 0 disables. 0.8 is a reasonable default."),
2019
+ base: z10.number().min(0).max(5).optional().describe("Base of the exponential penalty for repeated tokens. Default 1.75."),
2020
+ allowedLength: z10.number().int().min(0).max(64).optional().describe("Minimum n-gram length before DRY kicks in. Default 2.")
2021
+ }).describe("DRY anti-repetition sampler (llama.cpp only).");
2022
+ var XtcSamplerSchema = z10.object({
2023
+ probability: z10.number().min(0).max(1).describe("Probability of triggering XTC on any given step."),
2024
+ threshold: z10.number().min(0).max(1).describe("Minimum top-token probability for XTC to fire.")
2025
+ }).describe("XTC sampler (llama.cpp only).");
2026
+ var SamplingBlockSchema = z10.object({
2027
+ temperature: z10.number().min(0).max(2).optional().describe("Sampling temperature. 0 = greedy. Most reasoning models want 0.6\u20131.0."),
2028
+ topP: z10.number().min(0).max(1).optional().describe("Nucleus sampling. 0.95 is the common default for Qwen, Gemma, Nemotron."),
2029
+ topK: z10.number().int().min(0).max(1e3).optional().describe("Top-k cutoff. 20 for Qwen think, 64 for Gemma, 1 for greedy-on-instruct."),
2030
+ minP: z10.number().min(0).max(1).optional().describe("Min-p cutoff (llama.cpp / MLX / Ollama). Qwen recommends 0."),
2031
+ maxTokens: z10.number().int().positive().optional().describe("Per-turn output token cap. Maps to num_predict / max_tokens / n_predict."),
2032
+ seed: z10.number().int().optional().describe("RNG seed. Unset / negative = random. Best-effort determinism on cloud."),
2033
+ repetitionPenalty: z10.number().min(0).max(3).optional().describe("Local engines (Ollama / llama.cpp / MLX): repeat_penalty. 1.0 = off, 1.1 = mild."),
2034
+ repetitionContext: z10.number().int().positive().optional().describe("Local engines: window size for repetition penalty (`repeat_last_n`)."),
2035
+ frequencyPenalty: z10.number().min(-2).max(2).optional().describe("Cloud (OpenAI) and Ollama / llama.cpp: penalize repeated tokens by frequency."),
2036
+ presencePenalty: z10.number().min(-2).max(2).optional().describe("Cloud (OpenAI) and Ollama / llama.cpp: penalize any reused token."),
2037
+ dry: DrySamplerSchema.optional(),
2038
+ xtc: XtcSamplerSchema.optional()
2039
+ }).describe("Sampling parameters applied per-request.");
2040
+ var ReasoningBlockSchema = z10.object({
2041
+ effort: z10.enum(["low", "medium", "high"]).optional().describe(
2042
+ "Cloud reasoning effort. Maps to OpenAI `reasoning.effort` and Anthropic `thinking.budget_tokens` tiers."
2043
+ ),
2044
+ thinkingBudget: z10.number().int().positive().optional().describe(
2045
+ "Explicit thinking-token budget (Anthropic `thinking.budget_tokens`, Nemotron `reasoning_budget`). Wins over `effort` when both set."
2046
+ ),
2047
+ enableThinking: z10.boolean().optional().describe(
2048
+ "Chat-template toggle for dual-mode models (Qwen3+, Nemotron Nano/Super). Implicit on cloud thinking models."
2049
+ ),
2050
+ templateKwargs: z10.record(z10.string(), z10.union([z10.string(), z10.number(), z10.boolean()])).optional().describe(
2051
+ "Chat-template variables that drive this model's reasoning depth, forwarded verbatim as `chat_template_kwargs` on local engines. The names are the model's own \u2014 GPT-OSS reads `reasoning_effort`, Muse Glimmer reads `reasoning_strength` (low|medium|high|xhigh) \u2014 so the manifest declares them rather than the runtime guessing. Lives under `reasoning` (not `engine`) because depth is a per-request choice a tuning profile overrides: `thinking-coding` can ask for xhigh while `instruct` asks for low. Cloud providers ignore it; use `reasoning.effort` there."
2052
+ )
2053
+ }).describe("Reasoning controls.");
2054
+ var StructuredOutputSchema = z10.object({
2055
+ responseFormat: z10.enum(["text", "json_object"]).optional().describe("Output mode. `text` = freeform (default). `json_object` = enforce JSON."),
2056
+ jsonSchema: z10.unknown().optional().describe("Pin output to a JSON Schema (OpenAI strict mode, llama.cpp --json-schema)."),
2057
+ grammar: z10.string().optional().describe("llama.cpp GBNF grammar. Last-resort structured-output knob.")
2058
+ }).describe("Structured-output controls.");
2059
+ var PromptTagsSchema = z10.object({
2060
+ enableThinkingTag: z10.string().optional().describe("User-prompt tag that enables thinking for this turn (e.g. `/think`)."),
2061
+ disableThinkingTag: z10.string().optional().describe("User-prompt tag that disables thinking for this turn (e.g. `/no_think`).")
2062
+ }).describe("Per-turn reasoning toggle tags.");
2063
+ var LlamaCppEngineConfigSchema = z10.object({
2064
+ nGpuLayers: z10.number().int().min(-1).optional().describe("`--n-gpu-layers` override. -1 = all. Unset = b9843 `auto`/`--fit`."),
2065
+ cpuMoe: z10.boolean().optional().describe("`--cpu-moe`: keep ALL MoE experts in system RAM (attention/dense on GPU)."),
2066
+ nCpuMoe: z10.number().int().min(0).optional().describe("`--n-cpu-moe N`: keep the first N layers\u2019 MoE experts in RAM. Partial split."),
2067
+ cacheReuse: z10.number().int().min(0).optional().describe(
2068
+ "`--cache-reuse N` prefix-KV reuse chunk. 0 = disable. Unset inherits the global default."
2069
+ ),
2070
+ swaFull: z10.boolean().optional().describe("`--swa-full`: full-size SWA cache (Gemma family)."),
2071
+ flashAttn: z10.enum(["on", "off", "auto"]).optional().describe("`--flash-attn` mode override for this model."),
2072
+ ubatchSize: z10.number().int().positive().optional().describe("`--ubatch-size` (inner microbatch) override for this model."),
2073
+ contextSize: z10.number().int().positive().optional().describe(
2074
+ "Per-turn context ceiling (tokens) this model launches with. Capped by GGUF train ctx."
2075
+ ),
2076
+ chatTemplate: z10.string().min(1).optional().describe(
2077
+ "`--chat-template` override for GGUFs whose embedded Jinja template is incompatible with llama.cpp tool parsing."
2078
+ ),
2079
+ threads: z10.number().int().positive().optional().describe("`--threads` override."),
2080
+ batchSize: z10.number().int().positive().optional().describe("`--batch-size` override."),
2081
+ spec: z10.object({
2082
+ type: z10.enum([
2083
+ "none",
2084
+ "draft-mtp",
2085
+ "draft-eagle3",
2086
+ "draft-dflash",
2087
+ "draft-simple",
2088
+ "ngram-mod",
2089
+ "ngram-simple",
2090
+ "ngram-map-k",
2091
+ "ngram-map-k4v",
2092
+ "ngram-cache"
2093
+ ]).optional().describe("`--spec-type` speculative-decoding mode for this model."),
2094
+ mtp: z10.boolean().optional().describe(
2095
+ "VERIFIED capability metadata: this model\u2019s target or companion GGUF carries MTP tensors. Does not enable `draft-mtp` by itself."
2096
+ ),
2097
+ draftModelId: z10.string().optional().describe("Catalog id / path of the draft model for `draft-simple`."),
2098
+ nMax: z10.number().int().positive().optional().describe("`--spec-draft-n-max`: tokens drafted per step.")
2099
+ }).optional().describe("Speculative-decoding config for this model.")
2100
+ }).describe("Per-model llama.cpp launch-flag defaults (engine-level, applied at model load).");
2101
+ var EngineConfigSchema = z10.object({
2102
+ llamaCpp: LlamaCppEngineConfigSchema.optional()
2103
+ }).describe("Per-model engine launch-flag defaults, keyed by engine.");
2104
+ var ChatModelTuningBaseSchema = z10.object({
2105
+ sampling: SamplingBlockSchema.optional(),
2106
+ samplingWhenThinking: SamplingBlockSchema.optional().describe(
2107
+ "Sparse override of `sampling` applied when the runtime determines reasoning is engaged. Used by Qwen3+ (different sampling for /think mode) and Nemotron Nano (different sampling for thinking vs instruct)."
2108
+ ),
2109
+ reasoning: ReasoningBlockSchema.optional(),
2110
+ output: StructuredOutputSchema.optional(),
2111
+ toolChoice: z10.enum(["auto", "required", "none"]).optional().describe(
2112
+ "Tool selection mode (cloud + llama.cpp + MLX). `auto` lets the model decide; `required` forces a tool call this turn; `none` disables tools."
2113
+ ),
2114
+ promptTags: PromptTagsSchema.optional()
2115
+ }).describe("Per-model sampling, reasoning, and output defaults (without profiles).");
2116
+ var ChatModelTuningSchema = ChatModelTuningBaseSchema.extend({
2117
+ engine: EngineConfigSchema.optional().describe(
2118
+ "Per-model ENGINE launch-flag defaults (llama.cpp `--n-gpu-layers`, `--cpu-moe`, `--spec-type`, \u2026). Applied once at model load, not per request \u2014 so it lives on the top-level tuning object, not inside per-request `profiles`."
2119
+ ),
2120
+ profiles: z10.record(TuningProfileIdSchema, ChatModelTuningBaseSchema.partial()).optional().describe(
2121
+ "Named tuning presets this model implements. Gezel frontmatter `tuningProfile` selects one; the resolver applies the profile as a layer between installDefault and catalog base. Missing requested profiles walk the canonical fallback chain."
2122
+ )
2123
+ }).describe("Per-model sampling, reasoning, and output defaults.");
2124
+
2125
+ // src/schemas/question.ts
2126
+ import { z as z11 } from "zod";
2127
+ var NpmInstallApprovalDecisionSchema = z11.object({
2128
+ package: z11.string(),
2129
+ version: z11.string(),
2130
+ decision: z11.enum(["install", "always", "decline"])
2131
+ });
2132
+ var QuestionAnswerSchema = z11.object({
2133
+ /** Indices into `choices` the user picked. Empty when only write-in. */
2134
+ selectedChoices: z11.array(z11.number().int().min(0)).optional(),
2135
+ /** Free-text the user typed. Empty when they only clicked choices. */
2136
+ writeIn: z11.string().optional(),
2137
+ /**
2138
+ * Set when the user explicitly dismissed BUT wants the gezel to
2139
+ * proceed anyway with sensible defaults. Triggers a synthetic
2140
+ * follow-up turn with a `[The user wants you to proceed…]` seed
2141
+ * so the gezel knows to make decisions on the user's behalf.
2142
+ * UI label: "Just do whatever".
2143
+ */
2144
+ declined: z11.boolean().optional(),
2145
+ /**
2146
+ * Set when the user just wants the question to go away — no
2147
+ * follow-up turn, no work done, nothing for the gezel to act on.
2148
+ * The card collapses and the gezel's session is left as-is (its
2149
+ * turn already ended when it called `ask_user_question`). Distinct
2150
+ * from `declined` so the model never sees a "user wants defaults"
2151
+ * signal that the user didn't intend. UI label: "Skip".
2152
+ */
2153
+ silentSkip: z11.boolean().optional(),
2154
+ /**
2155
+ * Per-package decisions for `npm-install-approval` questions. When
2156
+ * set, the answer handler installs / always-allows / declines each
2157
+ * package and emits a single follow-up summary into the session.
2158
+ */
2159
+ npmInstallDecisions: z11.array(NpmInstallApprovalDecisionSchema).optional(),
2160
+ at: z11.string()
2161
+ });
2162
+ var NpmInstallApprovalPackageSchema = z11.object({
2163
+ package: z11.string(),
2164
+ version: z11.string()
2165
+ });
2166
+ var CommandApprovalScopeSchema = z11.enum(["script", "npx"]);
2167
+ var CommandApprovalIntentSchema = z11.object({
2168
+ kind: z11.literal("command-approval"),
2169
+ scope: CommandApprovalScopeSchema,
2170
+ name: z11.string().min(1),
2171
+ body: z11.string().optional(),
2172
+ args: z11.array(z11.string()).optional()
2173
+ });
2174
+ var ToolPermissionIntentSchema = z11.object({
2175
+ kind: z11.literal("tool-permission"),
2176
+ toolName: z11.string(),
2177
+ toolInput: z11.record(z11.string(), z11.unknown())
2178
+ });
2179
+ var ToolsetInstallApprovalIntentSchema = z11.object({
2180
+ kind: z11.literal("toolset-install-approval"),
2181
+ toolsetId: z11.string(),
2182
+ sourceId: z11.string(),
2183
+ version: z11.string(),
2184
+ targetProjectId: z11.string(),
2185
+ craftbookId: z11.string()
2186
+ });
2187
+ var ImageGenerationApprovalIntentSchema = z11.object({
2188
+ kind: z11.literal("image-generation-approval"),
2189
+ provider: z11.string(),
2190
+ model: z11.string(),
2191
+ /** Truncated prompt the model is about to send. Surfaced verbatim. */
2192
+ promptPreview: z11.string(),
2193
+ /** Resolved generation size, e.g. '2K 16:9' or '1024x1024'. Optional. */
2194
+ estimatedSize: z11.string().optional()
2195
+ });
2196
+ var VideoGenerationApprovalIntentSchema = z11.object({
2197
+ kind: z11.literal("video-generation-approval"),
2198
+ provider: z11.string(),
2199
+ model: z11.string(),
2200
+ promptPreview: z11.string(),
2201
+ /** Resolved clip shape, e.g. '704×480 · 97f · 24fps'. Optional. */
2202
+ estimatedSize: z11.string().optional()
2203
+ });
2204
+ var ScheduleApprovalIntentSchema = z11.object({
2205
+ kind: z11.literal("schedule-approval"),
2206
+ typeId: z11.string(),
2207
+ craftbookId: z11.string(),
2208
+ /**
2209
+ * Recurrence flavor. Absent → 'scheduled' (every question written
2210
+ * before this field existed is a cron schedule). 'night-shift' hosts
2211
+ * run inside the Night Shift window instead of on a user-visible cron;
2212
+ * the card copy switches accordingly.
2213
+ */
2214
+ runMode: z11.enum(["scheduled", "night-shift"]).optional(),
2215
+ /**
2216
+ * 5-field cron expression (UTC). Surfaced verbatim on the card for
2217
+ * 'scheduled' hosts; for 'night-shift' hosts it is the internal
2218
+ * heartbeat and the card shows the window instead.
2219
+ */
2220
+ cron: z11.string(),
2221
+ overlap: z11.enum(["skip", "queue", "concurrent"]).optional()
2222
+ });
2223
+ var NightShiftReviewIntentSchema = z11.object({
2224
+ kind: z11.literal("night-shift-review"),
2225
+ /** The window's day key (see `nightShiftWindowKey`). */
2226
+ windowKey: z11.string(),
2227
+ tasksCompleted: z11.number(),
2228
+ reports: z11.array(
2229
+ z11.object({
2230
+ projectId: z11.string(),
2231
+ path: z11.string(),
2232
+ title: z11.string().optional(),
2233
+ actionCount: z11.number()
2234
+ })
2235
+ )
2236
+ });
2237
+ var TaskPausedReasonSchema = z11.enum([
2238
+ "gate_exhausted",
2239
+ "gate_plateau",
2240
+ "gate_unsatisfiable",
2241
+ "gate_infrastructure",
2242
+ "step_stalled",
2243
+ "budget_exhausted"
2244
+ ]);
2245
+ var TaskPausedIntentSchema = z11.object({
2246
+ kind: z11.literal("task-paused"),
2247
+ /** `projectId/num` of the paused task — the dedup key. */
2248
+ taskRef: z11.string(),
2249
+ stepId: z11.string().optional(),
2250
+ reason: TaskPausedReasonSchema
2251
+ });
2252
+ var QuestionIntentSchema = z11.discriminatedUnion("kind", [
2253
+ z11.object({
2254
+ kind: z11.literal("npm-install-approval"),
2255
+ /**
2256
+ * Packages that need approval. Always at least one; multiple when
2257
+ * the gezel batched an install call (encouraged) or when we merged
2258
+ * a later request into the same pending question for dedup.
2259
+ */
2260
+ packages: z11.array(NpmInstallApprovalPackageSchema).min(1)
2261
+ }),
2262
+ CommandApprovalIntentSchema,
2263
+ ToolPermissionIntentSchema,
2264
+ ToolsetInstallApprovalIntentSchema,
2265
+ ImageGenerationApprovalIntentSchema,
2266
+ VideoGenerationApprovalIntentSchema,
2267
+ ScheduleApprovalIntentSchema,
2268
+ NightShiftReviewIntentSchema,
2269
+ TaskPausedIntentSchema
2270
+ ]);
2271
+ var QuestionSchema = z11.object({
2272
+ id: z11.string(),
2273
+ projectId: z11.string(),
2274
+ gezelId: z11.string(),
2275
+ sessionId: z11.string(),
2276
+ /** Body of the question — supports markdown. */
2277
+ prompt: z11.string().min(1),
2278
+ /** Optional preset choices. Empty / omitted => write-in only. */
2279
+ choices: z11.array(z11.string()).max(20).optional(),
2280
+ /** Whether the user can also type a write-in alongside choices. Default true. */
2281
+ allowWriteIn: z11.boolean().optional(),
2282
+ /** Whether multiple choices may be selected. Default false. */
2283
+ multiSelect: z11.boolean().optional(),
2284
+ /**
2285
+ * Approval-flow attachment: a task this question is *about*. Stored in
2286
+ * `projectId/num` form so existing parsing helpers work. The UI shows
2287
+ * the task title + status above the prompt and offers an "Open task"
2288
+ * link.
2289
+ */
2290
+ taskRef: z11.string().optional(),
2291
+ /**
2292
+ * Approval-flow attachment: a document this question is *about*.
2293
+ * Project-relative path when `projectId` is set, otherwise into the
2294
+ * global `~/.gezel/documents/` library. The UI renders a collapsed
2295
+ * preview + "Open document" link.
2296
+ */
2297
+ documentPath: z11.string().optional(),
2298
+ /**
2299
+ * Service-created specialized-question marker (see `QuestionIntent`
2300
+ * for context). Absent for plain user-facing questions asked via
2301
+ * the `ask_user_question` MCP tool.
2302
+ */
2303
+ intent: QuestionIntentSchema.optional(),
2304
+ createdAt: z11.string(),
2305
+ /** Set once the user has answered (or declined). */
2306
+ answer: QuestionAnswerSchema.optional()
2307
+ });
2308
+
2309
+ // src/schemas/recognition.ts
2310
+ import { z as z12 } from "zod";
2311
+ var ImageExifSchema = z12.object({
2312
+ make: z12.string().optional(),
2313
+ model: z12.string().optional(),
2314
+ lensModel: z12.string().optional(),
2315
+ dateTimeOriginal: z12.string().optional(),
2316
+ orientation: z12.number().int().min(1).max(8).optional(),
2317
+ software: z12.string().optional(),
2318
+ imageDescription: z12.string().optional()
2319
+ });
2320
+ var ImageStaticMetaSchema = z12.object({
2321
+ format: z12.enum(["png", "jpeg", "gif", "webp", "svg", "unknown"]),
2322
+ width: z12.number().int().positive().optional(),
2323
+ height: z12.number().int().positive().optional(),
2324
+ byteLength: z12.number().int().nonnegative(),
2325
+ sha256: z12.string().regex(/^[a-f0-9]{64}$/),
2326
+ /**
2327
+ * PNG `tEXt`/`iTXt`/`zTXt` keyword→value pairs. Generation provenance lives
2328
+ * here (A1111 writes `parameters`, ComfyUI writes `prompt`/`workflow`) and
2329
+ * screenshot tools stamp `Software`.
2330
+ *
2331
+ * Attacker-controlled: anyone can author a PNG whose `Description` chunk
2332
+ * reads "Ignore previous instructions". Renderers MUST fence and cap this.
2333
+ */
2334
+ pngText: z12.record(z12.string(), z12.string()).optional(),
2335
+ exif: ImageExifSchema.optional(),
2336
+ /** Withheld from every prompt. See the schema doc above. */
2337
+ gps: z12.object({ lat: z12.number(), lon: z12.number() }).optional(),
2338
+ /** True when the file carried location data we deliberately dropped. */
2339
+ gpsRedacted: z12.boolean().optional(),
2340
+ /**
2341
+ * Heuristic from dimensions, format, and metadata — drives `auto` mode
2342
+ * selection without paying a classifier call.
2343
+ */
2344
+ likelyScreenshot: z12.boolean().optional()
2345
+ });
2346
+ var RecognitionModeSchema = z12.enum(["describe", "ocr", "ui", "extract"]);
2347
+ var RecognitionModeRequestSchema = z12.enum(["auto", "describe", "ocr", "ui", "extract"]);
2348
+ var ImageRecognitionSchema = z12.object({
2349
+ schemaVersion: z12.literal(1),
2350
+ sha256: z12.string().regex(/^[a-f0-9]{64}$/),
2351
+ meta: ImageStaticMetaSchema,
2352
+ modes: z12.array(RecognitionModeSchema).min(1),
2353
+ description: z12.string().optional(),
2354
+ ocrText: z12.string().optional(),
2355
+ structured: z12.object({
2356
+ templateId: z12.string().optional(),
2357
+ data: z12.unknown()
2358
+ }).optional(),
2359
+ engine: z12.enum(["llama-cpp", "mlx", "mock", "none"]),
2360
+ modelId: z12.string(),
2361
+ status: z12.enum(["ok", "partial", "failed", "static-only"]),
2362
+ failureReason: z12.string().optional(),
2363
+ durationMs: z12.number().int().nonnegative(),
2364
+ at: z12.string()
2365
+ });
2366
+ var MessageImageDigestSchema = z12.object({
2367
+ /** The markdown ref exactly as it appears in the message body. */
2368
+ ref: z12.string(),
2369
+ sha256: z12.string().regex(/^[a-f0-9]{64}$/),
2370
+ /** Pre-rendered, already capped and fenced-safe. */
2371
+ digest: z12.string(),
2372
+ modelId: z12.string(),
2373
+ modes: z12.array(RecognitionModeSchema),
2374
+ status: z12.enum(["ok", "partial", "failed", "static-only"]),
2375
+ at: z12.string()
2376
+ });
2377
+ var RecognitionRequestSchema = z12.object({
2378
+ /** Project-relative artifact path, e.g. `attachments/<uuid>.png`. */
2379
+ artifactPath: z12.string().min(1).optional(),
2380
+ data: z12.string().min(1).optional(),
2381
+ mimeType: z12.string().optional(),
2382
+ mode: RecognitionModeRequestSchema.default("auto"),
2383
+ /** JSON Schema for `extract` mode — fed to llama-server `response_format`. */
2384
+ schema: z12.unknown().optional(),
2385
+ /** Overrides the configured recognition model for this call. */
2386
+ model: z12.string().optional()
2387
+ });
2388
+ var RecognitionHealthSchema = z12.object({
2389
+ state: z12.enum(["ok", "no-model", "not-configured", "error"]),
2390
+ modelId: z12.string().optional(),
2391
+ detail: z12.string().optional()
2392
+ });
2393
+ var RecognitionPullEventSchema = z12.union([
2394
+ z12.object({
2395
+ type: z12.literal("progress"),
2396
+ bytesWritten: z12.number().int().nonnegative(),
2397
+ totalBytes: z12.number().int().nonnegative().optional()
2398
+ }),
2399
+ z12.object({ type: z12.literal("error"), error: z12.string() }),
2400
+ z12.object({ type: z12.literal("done"), id: z12.string() })
2401
+ ]);
2402
+ var RecognitionCatalogEntrySchema = z12.object({
2403
+ id: z12.string(),
2404
+ name: z12.string(),
2405
+ description: z12.string(),
2406
+ license: z12.string(),
2407
+ approxSizeBytes: z12.number().int().nonnegative(),
2408
+ recoScore: z12.number()
2409
+ });
2410
+ var ListRecognitionCatalogResponseSchema = z12.object({
2411
+ models: z12.array(RecognitionCatalogEntrySchema)
2412
+ });
2413
+ var InstalledRecognitionModelSchema = z12.object({
2414
+ id: z12.string(),
2415
+ name: z12.string(),
2416
+ approxSizeBytes: z12.number().int().nonnegative(),
2417
+ installedAt: z12.string(),
2418
+ weightsPath: z12.string().optional(),
2419
+ mmprojPath: z12.string().optional()
2420
+ });
2421
+ var ListInstalledRecognitionModelsResponseSchema = z12.object({
2422
+ models: z12.array(InstalledRecognitionModelSchema)
2423
+ });
2424
+
2425
+ // src/schemas/session-telemetry.ts
2426
+ import { z as z13 } from "zod";
2427
+ var SessionGpuTaskSchema = z13.enum([
2428
+ "image_generation",
2429
+ "video_generation",
2430
+ "image_recognition"
2431
+ ]);
2432
+ var SessionTurnTelemetrySchema = z13.object({
2433
+ /** Epoch ms when the in-flight turn started. */
2434
+ startedAt: z13.number(),
2435
+ streamedContentChars: z13.number().int().nonnegative(),
2436
+ toolCalls: z13.number().int().nonnegative(),
2437
+ fileMutations: z13.number().int().nonnegative()
2438
+ });
2439
+ var SessionTelemetrySchema = z13.object({
2440
+ sessionId: z13.string(),
2441
+ gezelId: z13.string(),
2442
+ projectId: z13.string(),
2443
+ /** True while a send is currently running for this session. */
2444
+ inflight: z13.boolean(),
2445
+ turnsStarted: z13.number().int().nonnegative(),
2446
+ deltaChunks: z13.number().int().nonnegative(),
2447
+ streamedContentChars: z13.number().int().nonnegative(),
2448
+ wirePulses: z13.number().int().nonnegative(),
2449
+ heartbeats: z13.number().int().nonnegative(),
2450
+ enginePhaseEvents: z13.number().int().nonnegative(),
2451
+ /**
2452
+ * `engine_phase === 'generating'` transitions — roughly one per completion
2453
+ * request the engine served (the slot-launch granularity stall logic and
2454
+ * the eval chatter threshold were calibrated against).
2455
+ */
2456
+ generationSpurts: z13.number().int().nonnegative(),
2457
+ toolCalls: z13.number().int().nonnegative(),
2458
+ toolArgChars: z13.number().int().nonnegative(),
2459
+ fileMutations: z13.number().int().nonnegative(),
2460
+ gpuEvents: z13.number().int().nonnegative(),
2461
+ gpuTaskActive: SessionGpuTaskSchema.nullable(),
2462
+ /** Epoch ms of the last streamed signal (delta / pulse / heartbeat / phase). */
2463
+ lastStreamActivityAt: z13.number().nullable(),
2464
+ lastToolActivityAt: z13.number().nullable(),
2465
+ lastMutationAt: z13.number().nullable(),
2466
+ lastGpuActivityAt: z13.number().nullable(),
2467
+ /** Max of all activity timestamps — "when did ANY progress signal last fire". */
2468
+ lastProgressAt: z13.number().nullable(),
2469
+ /** Counters scoped to the currently-running turn; null between turns. */
2470
+ currentTurn: SessionTurnTelemetrySchema.nullable()
2471
+ });
2472
+ var SessionTelemetryListResponseSchema = z13.object({
2473
+ /** Bumped when counter semantics change; consumers gate on it. */
2474
+ version: z13.literal(1),
2475
+ capturedAt: z13.number(),
2476
+ sessions: z13.array(SessionTelemetrySchema)
2477
+ });
2478
+
2479
+ // src/schemas/gezel.ts
2480
+ var ProviderNameSchema = z14.enum([
2481
+ "copilot",
2482
+ "openai",
2483
+ "anthropic",
2484
+ "anthropic-cli",
2485
+ "codex-cli",
2486
+ "ollama",
2487
+ "llama-cpp",
2488
+ "mlx",
2489
+ // DwarfStar/ds4 — antirez's DeepSeek-V4-specific engine. Like llama-cpp/mlx
2490
+ // it serves an OpenAI-compatible HTTP API from a supervised native binary,
2491
+ // but it only loads antirez's DeepSeek-V4 GGUFs and streams MoE experts from
2492
+ // SSD so a 284B model fits a 64GB Mac. GPU-only (Metal/CUDA); see the ds4
2493
+ // provider for the availability gating.
2494
+ "ds4",
2495
+ // Inference hosted on another paired gezel daemon ("remote models"). A
2496
+ // single enum arm fronts a family of paired servers; the specific server is
2497
+ // selected by the namespaced model id `remote:<remoteId>/<bLocalId>`. Like
2498
+ // the cloud providers, the turn loop + tools run locally — only the model
2499
+ // forward-pass is remoted — so `remote` is NOT a local provider.
2500
+ "remote"
2501
+ ]);
2502
+ var GezelGenderSchema = z14.enum(["male", "female", "non-binary"]);
2503
+ var FixedFunctionConfigSchema = z14.object({
2504
+ /** MCP tool name to forward to (e.g. `'generate_image'`). */
2505
+ tool: z14.string().min(1),
2506
+ /** Argument key on the tool that receives the user's message text. */
2507
+ promptKey: z14.string().min(1).default("prompt"),
2508
+ /** Defaults merged into every call; user text on `promptKey` always wins. */
2509
+ defaults: z14.record(z14.string(), z14.unknown()).optional()
2510
+ });
2511
+ var GezelTraitSchema = z14.object({
2512
+ id: z14.string(),
2513
+ /** One imperative second-person sentence, rendered as a prompt bullet. */
2514
+ text: z14.string().min(1).max(200),
2515
+ adoptedAt: z14.string(),
2516
+ source: z14.enum(["levelup", "manual"]).optional()
2517
+ });
2518
+ var GezelFrontmatterSchema = z14.object({
2519
+ id: z14.string().optional(),
2520
+ name: z14.string(),
2521
+ description: z14.string().optional(),
2522
+ role: z14.string().optional(),
2523
+ /**
2524
+ * Kebab-case slug derived from `role` (or `gezel-N` when role is absent),
2525
+ * globally unique across the install. Collisions get `-2`, `-3`, …
2526
+ * suffixes. Backfilled at startup for legacy gezels. Used as a secondary
2527
+ * identifier for @-mentions and as the sole rendered identifier when
2528
+ * `config.roleBasedNameOnlyMode` is enabled.
2529
+ */
2530
+ roleBasedName: z14.string().optional(),
2531
+ /**
2532
+ * One of `male` / `female` / `non-binary`. Assigned at creation time
2533
+ * from the matching gendered first-name pool (with a small chance of
2534
+ * non-binary regardless of name). Drives pronouns in UI copy and in prompt
2535
+ * references made by other gezels. It is not rendered into this gezel's
2536
+ * own prompt. Absent on legacy gezels, where references omit pronouns.
2537
+ */
2538
+ gender: GezelGenderSchema.optional(),
2539
+ model: z14.string().optional(),
2540
+ provider: ProviderNameSchema.optional(),
2541
+ reasoningEffort: z14.string().optional(),
2542
+ /**
2543
+ * Per-gezel sampling / reasoning / structured-output / tool-call overrides.
2544
+ * Sparse — only set fields override the catalog's recommended defaults.
2545
+ * Resolution order is gezel `tuning` > installDefault > selected
2546
+ * `tuningProfile` > catalog identity `tuning` > provider fallback. See
2547
+ * {@link ChatModelTuningSchema} for the full field list and dual-mode
2548
+ * (`samplingWhenThinking`) semantics.
2549
+ */
2550
+ tuning: ChatModelTuningSchema.optional(),
2551
+ /**
2552
+ * Named tuning preset this gezel uses against the active model. Models
2553
+ * declare which presets they implement under `tuning.profiles` in the
2554
+ * catalog manifest (e.g. `thinking-coding`, `thinking-general`, `instruct`,
2555
+ * `creative`). At request time the resolver applies the named profile as
2556
+ * a layer between `installDefault` and the model's base tuning; the
2557
+ * gezel's explicit `tuning` overrides still win per-leaf. Missing profiles
2558
+ * walk a canonical fallback chain (`thinking-coding → thinking-general →
2559
+ * instruct`). See `tuning-profile-registry.ts` for the canonical set.
2560
+ */
2561
+ tuningProfile: TuningProfileIdSchema.optional(),
2562
+ /**
2563
+ * Role/template-**suggested** tuning profile. Unlike `tuningProfile`
2564
+ * (an explicit per-gezel user pick that overrides the install preset),
2565
+ * this is a soft default a gilde template declares for its role. In the
2566
+ * resolution stack it sits BELOW both the per-gezel pick and the
2567
+ * install-wide preset, and ABOVE the app-wide fallback
2568
+ * (`thinking-general`): a gezel uses its role's suggestion unless the
2569
+ * user has explicitly chosen a profile (per-gezel or install preset).
2570
+ * Lets coordinator roles (meester / voorman / planner) default to a
2571
+ * low-temperature `thinking-precise` without locking the user out.
2572
+ */
2573
+ suggestedTuningProfile: TuningProfileIdSchema.optional(),
2574
+ tools: z14.array(z14.string()).optional(),
2575
+ tags: z14.array(z14.string()).optional(),
2576
+ /**
2577
+ * When set, switches this gezel into "fixed-function" mode: chat
2578
+ * messages bypass the LLM and forward to the named MCP tool. See
2579
+ * {@link FixedFunctionConfigSchema}. The LLM-config fields above
2580
+ * (`model`, `provider`, etc.) are ignored for fixed-function gezels
2581
+ * and `about.md` is not used / not written.
2582
+ */
2583
+ fixedFunction: FixedFunctionConfigSchema.optional(),
2584
+ /** Ollama-only: override the context window (tokens) for this gezel. */
2585
+ numCtx: z14.number().int().positive().optional(),
2586
+ /** When false, suppresses auto-recall on session start for this gezel. */
2587
+ autoRecall: z14.boolean().optional(),
2588
+ /**
2589
+ * Chat bubble font id (one of `GEZEL_CHAT_FONTS[*].id`). When unset or
2590
+ * unrecognized, chat bubbles inherit the app default (Hanken Grotesk).
2591
+ */
2592
+ font: z14.string().optional(),
2593
+ /**
2594
+ * Kokoro TTS voice id (one of `KOKORO_VOICES[*].id`, e.g. `af_heart`,
2595
+ * `bm_george`). Drives spoken audio rendering — `synthesize_speech`
2596
+ * defaults to this voice for the gezel. Assigned at creation time
2597
+ * from the gender-matched pool; users can override in the edit
2598
+ * dialog. Absent on legacy gezels — synthesize falls back to the
2599
+ * default voice (`af_heart`) when missing.
2600
+ */
2601
+ voice: z14.string().optional(),
2602
+ /**
2603
+ * Provenance: the id of the gilde catalog template this gezel was
2604
+ * created from, if any. Written by exact-template or about-omitted
2605
+ * `create_gezel`, `ensure_gezel`'s template path, and the catalog route.
2606
+ * Absent on bespoke-generated or hand-authored gezels. The UI uses
2607
+ * this to offer "reset to original template" on the about editor.
2608
+ */
2609
+ templateId: z14.string().optional(),
2610
+ /**
2611
+ * Provenance: the semver of the template version installed at create
2612
+ * time. Paired with `templateId`. Absent on gezels created before this
2613
+ * field existed — the "refresh template" UI treats absent as "unknown
2614
+ * source version" and offers a refresh to current latest without
2615
+ * comparing.
2616
+ */
2617
+ templateVersion: z14.string().optional(),
2618
+ /**
2619
+ * Copilot-only: when true, deny the Copilot CLI's built-in tools
2620
+ * (bash, web_fetch, view, str_replace_editor, read_file, write_file,
2621
+ * grep, etc.) and force the model to work through our MCP tools. When
2622
+ * Unset inherits the install-level `GezelConfig.sandboxCopilot`, which
2623
+ * itself defaults to the sandboxed MCP surface.
2624
+ * Provider other than copilot: ignored.
2625
+ */
2626
+ sandboxCopilot: z14.boolean().optional(),
2627
+ /**
2628
+ * `anthropic-cli`-only: per-gezel override for the Claude CLI permission
2629
+ * mode. Forwarded as `--permission-mode <value>` to each `claude` invocation.
2630
+ * - `default`: prompt-aware mode the CLI uses outside scripted contexts.
2631
+ * - `acceptEdits`: auto-approve file edits; Bash + other side-effecting
2632
+ * tools still gate. Sensible default for a chat-driven gezel.
2633
+ * - `plan`: read-only — useful for review-style gezels.
2634
+ * - `bypassPermissions`: yolo — every tool call auto-approved including
2635
+ * Bash. Reserve for builder gezels you trust to run shell commands.
2636
+ * When unset, inherits `config.anthropicCli.defaultPermissionMode` (which
2637
+ * itself defaults to `acceptEdits`). Other providers: ignored.
2638
+ */
2639
+ claudePermissionMode: z14.enum(["default", "acceptEdits", "plan", "bypassPermissions"]).optional(),
2640
+ /**
2641
+ * `codex-cli`-only execution posture. New writes use Plan / Edit /
2642
+ * Reviewed / Full; legacy Codex values remain readable for compatibility.
2643
+ * When unset, the project override and then install default apply.
2644
+ */
2645
+ codexPermissionMode: CodexPermissionModeCompatSchema.optional(),
2646
+ /**
2647
+ * When true and the gezel has a custom `icon.svg`, the UI renders that
2648
+ * abstract sigil instead of the parametric poppetje. Default (absent /
2649
+ * false) renders the poppetje everywhere. Toggled from Gezel Detail.
2650
+ */
2651
+ iconOverride: z14.boolean().optional(),
2652
+ /**
2653
+ * Standing behavior traits, adopted through the growth system (or
2654
+ * hand-authored). Rendered as their own `### Traits` block in the
2655
+ * stable system-prompt prefix right after the about body. Capped at 8
2656
+ * visible slots; fully revertible (the user can retire any trait).
2657
+ * The frontmatter list is AUTHORITATIVE for what's active — growth.json
2658
+ * keeps the evidence-bearing adoption log.
2659
+ */
2660
+ traits: z14.array(GezelTraitSchema).max(8).optional(),
2661
+ /**
2662
+ * Overrides `config.recognition.mode` for this gezel. A gezel whose job is
2663
+ * reading screenshots sets `always`; everyone else inherits.
2664
+ *
2665
+ * Deliberately a single enum rather than a mirror of the config object —
2666
+ * frontmatter is user-edited YAML, and a nested policy struct there is a
2667
+ * support burden. Native vision is a property of the model *install*, not of
2668
+ * the gezel, so it has no frontmatter counterpart.
2669
+ */
2670
+ recognition: z14.enum(["auto", "always", "off"]).optional()
2671
+ });
2672
+ var GezelSectionSchema = z14.object({
2673
+ heading: z14.string(),
2674
+ template: z14.string().optional(),
2675
+ params: z14.record(z14.string(), z14.string()).optional(),
2676
+ body: z14.string()
2677
+ });
2678
+ var ParsedGezelSchema = z14.object({
2679
+ frontmatter: GezelFrontmatterSchema,
2680
+ sections: z14.array(GezelSectionSchema),
2681
+ source: z14.string()
2682
+ });
2683
+ var GezelSummarySchema = z14.object({
2684
+ id: z14.string(),
2685
+ name: z14.string(),
2686
+ description: z14.string().optional(),
2687
+ role: z14.string().optional(),
2688
+ /** Mirrors `GezelFrontmatter.roleBasedName`. */
2689
+ roleBasedName: z14.string().optional(),
2690
+ /** Mirrors `GezelFrontmatter.gender`. */
2691
+ gender: GezelGenderSchema.optional(),
2692
+ model: z14.string().optional(),
2693
+ provider: ProviderNameSchema.optional(),
2694
+ reasoningEffort: z14.string().optional(),
2695
+ /** Mirrors `GezelFrontmatter.tuningProfile`. */
2696
+ tuningProfile: TuningProfileIdSchema.optional(),
2697
+ /** Mirrors `GezelFrontmatter.suggestedTuningProfile`. */
2698
+ suggestedTuningProfile: TuningProfileIdSchema.optional(),
2699
+ numCtx: z14.number().int().positive().optional(),
2700
+ autoRecall: z14.boolean().optional(),
2701
+ font: z14.string().optional(),
2702
+ /** Mirrors `GezelFrontmatter.voice` — Kokoro TTS voice id. */
2703
+ voice: z14.string().optional(),
2704
+ /** Mirrors `GezelFrontmatter.templateId` when the gezel came from a gilde template. */
2705
+ templateId: z14.string().optional(),
2706
+ /** Mirrors `GezelFrontmatter.templateVersion`. */
2707
+ templateVersion: z14.string().optional(),
2708
+ /** Mirrors `GezelFrontmatter.sandboxCopilot`. */
2709
+ sandboxCopilot: z14.boolean().optional(),
2710
+ /** Mirrors `GezelFrontmatter.claudePermissionMode`. */
2711
+ claudePermissionMode: z14.enum(["default", "acceptEdits", "plan", "bypassPermissions"]).optional(),
2712
+ /** Mirrors `GezelFrontmatter.codexPermissionMode`. */
2713
+ codexPermissionMode: CodexPermissionModeCompatSchema.optional(),
2714
+ /**
2715
+ * Mirrors `GezelFrontmatter.fixedFunction`. Present means this gezel
2716
+ * skips the LLM and forwards messages to an MCP tool. The UI uses
2717
+ * its presence to render a sidebar badge, hide LLM controls in the
2718
+ * edit dialog, and replace the about pane with a "Forwards to: …"
2719
+ * affordance.
2720
+ */
2721
+ fixedFunction: FixedFunctionConfigSchema.optional(),
2722
+ icon: z14.string().optional(),
2723
+ /**
2724
+ * The resolved poppetje character data for this gezel. Inlined on
2725
+ * every list/detail response so the UI can render the parametric SVG
2726
+ * figure without a follow-up round-trip. Generated from the gezel id
2727
+ * on first read; persisted thereafter (so re-rolling the catalog
2728
+ * later doesn't drift existing characters).
2729
+ */
2730
+ poppetje: PoppetjeSchema.optional(),
2731
+ /** Mirrors `GezelFrontmatter.iconOverride`. */
2732
+ iconOverride: z14.boolean().optional(),
2733
+ /** Mirrors `GezelFrontmatter.recognition`. */
2734
+ recognition: z14.enum(["auto", "always", "off"]).optional(),
2735
+ /**
2736
+ * Where this gezel lives. `global` (the default when absent — back-compat
2737
+ * with every gezel on disk before this field existed) is the install-wide
2738
+ * roster under `~/.gezel/gezels/`. `project` is a project-local gezel
2739
+ * defined in a workspace `.gezel/` folder — its id is the encoded
2740
+ * `proj__<projectId>__<localId>` form (see `encodeProjectGezelId`). The UI
2741
+ * badges project-scoped gezels and the roster only surfaces them inside
2742
+ * their own project.
2743
+ */
2744
+ scope: z14.enum(["global", "project"]).optional(),
2745
+ /**
2746
+ * Filesystem ownership boundary, distinct from `scope` above. Shared gezel
2747
+ * identity lives in the installer-managed machine root; chats, memories,
2748
+ * growth, credentials, and installed toolsets remain in the user home.
2749
+ */
2750
+ storageScope: z14.enum(["user", "machine-shared"]).optional(),
2751
+ /** Mirrors `GezelFrontmatter.traits`. */
2752
+ traits: z14.array(GezelTraitSchema).optional(),
2753
+ /**
2754
+ * Lightweight growth summary (level + pending level-up flag),
2755
+ * hydrated from growth.json and inlined on list/detail responses —
2756
+ * like `poppetje` — so the roster badge and Growth-tab dot render
2757
+ * without N+1 follow-up requests.
2758
+ */
2759
+ growth: GezelGrowthSummarySchema.optional(),
2760
+ updatedAt: z14.string()
2761
+ });
2762
+ var GezelDetailSchema = GezelSummarySchema.extend({
2763
+ parsed: ParsedGezelSchema,
2764
+ about: z14.string(),
2765
+ /**
2766
+ * Optional contents of the per-gezel `tools.md` file. When present
2767
+ * (non-null), fully replaces the auto-injected `## Tools available
2768
+ * this turn` block in the system prompt. Power-user opt-in: the
2769
+ * gezel's owner accepts responsibility for keeping the listing
2770
+ * accurate as the install's registered tools evolve. `null` (the
2771
+ * default) means no override file exists and the runtime renders
2772
+ * the auto-block from the live MCP bridge.
2773
+ */
2774
+ toolsMd: z14.string().nullable().default(null)
2775
+ });
2776
+ var ToolCallImageSchema = z14.object({
2777
+ /** Path relative to the project's artifacts/ root (e.g. `sessions/2026-04-19_143015_snake-test/tool-3-img-0.png`). */
2778
+ path: z14.string(),
2779
+ /** MIME type of the image, used by the UI to set the right `<img>` src URL. */
2780
+ mimeType: z14.string()
2781
+ });
2782
+ var ToolCallAudioSchema = z14.object({
2783
+ /** Path relative to the project's artifacts/ root. */
2784
+ path: z14.string(),
2785
+ /** MIME type, e.g. `audio/wav`. */
2786
+ mimeType: z14.string(),
2787
+ /** Duration in seconds when known — used by the UI to show length without preloading the blob. */
2788
+ durationSeconds: z14.number().optional(),
2789
+ /** Voice id used to produce this audio (TTS only). */
2790
+ voice: z14.string().optional()
2791
+ });
2792
+ var ToolCallVideoSchema = z14.object({
2793
+ /** Path relative to the project's artifacts/ root (e.g. `generated/video-123.mp4`). */
2794
+ path: z14.string(),
2795
+ /** MIME type, e.g. `video/mp4`. */
2796
+ mimeType: z14.string(),
2797
+ /** Optional poster-frame artifact path for the `<video poster>` attribute. */
2798
+ posterPath: z14.string().optional()
2799
+ });
2800
+ var ChatMessageToolCallSchema = z14.object({
2801
+ name: z14.string(),
2802
+ durationMs: z14.number(),
2803
+ success: z14.boolean(),
2804
+ errorMessage: z14.string().optional(),
2805
+ /** File path the tool touched, for the References pane. */
2806
+ path: z14.string().optional(),
2807
+ /** Ordered file paths touched by a batched filesystem tool. */
2808
+ paths: z14.array(z14.string()).optional(),
2809
+ /** Compact, human-readable one-liner ("→ Freja: update the game loop · file: workspace/index.html"). */
2810
+ argsSummary: z14.string().optional(),
2811
+ /**
2812
+ * The tool call's FULL arguments, rendered as readable text (field by
2813
+ * field, bulky values shown in full — not truncated like
2814
+ * `argsSummary`). Surfaced in the UI behind an expand + copy so a user
2815
+ * can verify exactly what a handoff/edit actually sent without digging
2816
+ * through the debug log. Capped (~100 KB) server-side; longer blobs get
2817
+ * a truncation marker. Same exposure stance as `argsSummary` — MCP tool
2818
+ * arguments are not a secret vector in this codebase (secrets live in
2819
+ * the toolset-config path), so this is not separately redacted.
2820
+ */
2821
+ argsFull: z14.string().optional(),
2822
+ /** Short full response, or a bounded beginning/end summary for a long response. */
2823
+ resultText: z14.string().optional(),
2824
+ /** True when `resultText` is a bounded summary rather than the complete response. */
2825
+ resultTruncated: z14.boolean().optional(),
2826
+ /** Image artifacts the tool returned (e.g. browser_snapshot screenshots). */
2827
+ images: z14.array(ToolCallImageSchema).optional(),
2828
+ /** Audio artifacts the tool returned (e.g. synthesize_speech WAV). */
2829
+ audios: z14.array(ToolCallAudioSchema).optional(),
2830
+ /** Video artifacts the tool returned (e.g. generate_video mp4). */
2831
+ videos: z14.array(ToolCallVideoSchema).optional(),
2832
+ /**
2833
+ * Unified diff describing the change a surgical-edit tool made
2834
+ * (`replace_in_file`, `apply_patch`, `insert_at_marker`). Used by the UI
2835
+ * to render an inline diff under the tool-call row. Capped at ~100KB
2836
+ * server-side; larger diffs are truncated with a marker line.
2837
+ */
2838
+ diff: z14.string().optional(),
2839
+ addedLines: z14.number().int().nonnegative().optional(),
2840
+ removedLines: z14.number().int().nonnegative().optional()
2841
+ });
2842
+ var ChatMessageSchema = z14.object({
2843
+ role: z14.enum(["user", "assistant"]),
2844
+ content: z14.string(),
2845
+ at: z14.string(),
2846
+ from: z14.object({
2847
+ gezelId: z14.string(),
2848
+ gezelName: z14.string()
2849
+ }).optional(),
2850
+ /**
2851
+ * Artifact paths (relative to the project's `artifacts/` directory)
2852
+ * the assistant reply referenced in its body text. Populated on save
2853
+ * by the server-side reference parser, and used by the chat UI to
2854
+ * render a chip row under the bubble and to linkify inline code spans
2855
+ * that match a real artifact. Back-stops Copilot's tool-call
2856
+ * blindspot and any "AI wrote a file outside the MCP tools" paths.
2857
+ */
2858
+ referencedArtifacts: z14.array(z14.string()).optional(),
2859
+ /**
2860
+ * Task refs (`<projectId>/<num>`) the assistant reply mentioned. Same
2861
+ * shape as `referencedArtifacts` — populated on save, gated on the
2862
+ * ref actually existing in the task store, surfaced in the chat
2863
+ * bubble as click-through chips. Catches the common "I created task
2864
+ * gezel-ux-roadmap/2" mention so the user can jump to it without
2865
+ * scrolling the body for the ref.
2866
+ */
2867
+ referencedTasks: z14.array(z14.string()).optional(),
2868
+ /**
2869
+ * Tool calls the assistant fired during this turn. Populated on the
2870
+ * final assistant message; the UI renders them as a collapsible
2871
+ * "thinking" expando above the reply body so the user can still see
2872
+ * what ran even after the live stream has closed.
2873
+ */
2874
+ toolCalls: z14.array(ChatMessageToolCallSchema).optional(),
2875
+ /**
2876
+ * Phase announcements the model emitted via Copilot's `report_intent`
2877
+ * tool during this turn. Each entry carries the intent label and an
2878
+ * `afterChars` offset into `content` marking where (in the final
2879
+ * reply) the intent fired. The UI splices them back in as horizontal
2880
+ * dividers at those offsets so long multi-phase turns are visually
2881
+ * segmented. `content` itself stays free of intent text — model
2882
+ * replay and history export are unaffected. Copilot-only; other
2883
+ * providers don't emit these.
2884
+ */
2885
+ intents: z14.array(
2886
+ z14.object({
2887
+ label: z14.string(),
2888
+ afterChars: z14.number().int().min(0)
2889
+ })
2890
+ ).optional(),
2891
+ /**
2892
+ * Set when the assistant turn invoked `ask_user_question`. Lets the
2893
+ * chat UI correlate the question card with the exact bubble that
2894
+ * asked it, mirroring how `toolCalls` is captured. The Question
2895
+ * itself lives in the per-project questions file; this id is just
2896
+ * the foreign key.
2897
+ */
2898
+ pendingQuestionId: z14.string().optional(),
2899
+ /**
2900
+ * Marks the message as a system-generated synthesis rather than a real
2901
+ * model turn.
2902
+ *
2903
+ * - `'compaction-summary'` — emitted by the in-flight compaction path
2904
+ * that replaces older messages with a single synthesized "[Earlier
2905
+ * in this conversation: …]" assistant message to keep the prompt
2906
+ * under Ollama's context window.
2907
+ * - `'context-loop-halt'` — runtime ended a runaway tool-loop turn
2908
+ * gracefully and recorded a placeholder so the loop wouldn't keep
2909
+ * re-entering.
2910
+ * - `'turn-aborted'` — provider threw mid-turn (repeat-tracker,
2911
+ * failure-tracker, or any other unhandled error). Without this
2912
+ * record the drained tool calls and the abort warning would only
2913
+ * live in the client-side streaming slot — refreshing or copying a
2914
+ * debug bundle would lose them. Content is whatever streamed
2915
+ * (often empty); the abort message lives in `warnings`.
2916
+ * - `'growth-announcement'` — deterministic level-up announcement the
2917
+ * growth engine appends to the gezel's most recent session ("I just
2918
+ * reached level N…"). First-person and factually true, but not a
2919
+ * real model turn.
2920
+ * - `'keurmeester-notice'` — the Keurmeester stepped in on a stalled
2921
+ * turn: one-line diagnosis + what was done, dropped into the thread
2922
+ * before the granted recovery continuation runs.
2923
+ *
2924
+ * UI renders these as muted bubbles; the model sees them as normal
2925
+ * assistant turns (the role label is what matters to the API).
2926
+ */
2927
+ synthetic: z14.enum([
2928
+ "compaction-summary",
2929
+ "context-loop-halt",
2930
+ "turn-aborted",
2931
+ "growth-announcement",
2932
+ "keurmeester-notice"
2933
+ ]).optional(),
2934
+ /**
2935
+ * Display flag: the model sees this message as normal history, but the
2936
+ * chat transcript UI never renders a bubble for it. Set on
2937
+ * machine-authored facilitation seeds that would only be noise to the
2938
+ * reader — e.g. a project-type page's `reaction` seed ("[Checkers page]:
2939
+ * Your opponent played c3-d4. Board now: …") when the reaction opts in
2940
+ * via `hideSeed`. Unlike `synthetic` (a muted-but-visible bubble), a
2941
+ * `hidden` message is dropped entirely: `Store.listTimeline` filters it
2942
+ * from loaded transcripts and the live-timeline handler skips its bubble
2943
+ * while still opening the assistant's streaming slot.
2944
+ */
2945
+ hidden: z14.boolean().optional(),
2946
+ /**
2947
+ * This user message was delivered from the session's mid-turn queue
2948
+ * as a nudge — typed while the previous turn was still streaming and
2949
+ * held until it finished (contiguous nudges merge into one message).
2950
+ * Display-only marker: the model sees a normal user turn; the UI
2951
+ * renders a small "nudged" chip on the bubble.
2952
+ */
2953
+ nudge: z14.boolean().optional(),
2954
+ /**
2955
+ * Persistent warnings attached to this assistant turn — fabricated
2956
+ * tool-use detection, degraded provider state, etc. The streaming
2957
+ * `warning` events show during the live render but vanish when the
2958
+ * slot is replaced by the persisted message; this field carries the
2959
+ * same text into history so the bubble keeps showing the banner on
2960
+ * reload. Populated by the chat manager just before `events.publish`
2961
+ * fires the `complete` event.
2962
+ */
2963
+ warnings: z14.array(z14.string()).optional(),
2964
+ /**
2965
+ * Chain-of-thought captured during this turn. Local providers
2966
+ * (ollama, llama-cpp, mlx, ds4) extract `<think>…</think>` /
2967
+ * `<reasoning>…</reasoning>` tagged blocks during commit so they
2968
+ * don't pollute the visible reply; Copilot captures its
2969
+ * `assistant.reasoning_delta` stream; Anthropic captures
2970
+ * `thinking_delta` events. All stash the text here so the chat
2971
+ * bubble can render it behind a collapsed "Thinking" expander
2972
+ * instead of dropping it when the live stream closes. OpenAI
2973
+ * Responses hides reasoning server-side and leaves this unset.
2974
+ * Empty / whitespace-only captures are dropped.
2975
+ */
2976
+ reasoning: z14.string().optional(),
2977
+ /**
2978
+ * Observed wall-clock span of the streamed private-reasoning trace,
2979
+ * measured from the first `reasoning_delta` to the last. Optional
2980
+ * because older messages and providers that only expose reasoning at
2981
+ * commit time have no trustworthy phase timing.
2982
+ */
2983
+ reasoningDurationMs: z14.number().int().nonnegative().optional(),
2984
+ /**
2985
+ * Tool-call bodies the model emitted that the salvage layer
2986
+ * couldn't parse — the literal text from `<|tool_call|>` markers
2987
+ * (or prose-shaped `name(args)` attempts) that failed both the
2988
+ * structured-call channel AND the repair pipeline. Surfaced here so
2989
+ * a debug bundle for "model attempted a tool call but couldn't form
2990
+ * it correctly" turns can show what the model actually produced.
2991
+ * Without this the diagnostic disappears into provider logs.
2992
+ *
2993
+ * Populated by the MLX provider when retry budget exhausts. Other
2994
+ * providers may add their own equivalents later. Truncated to a few
2995
+ * hundred chars per body in the populator so a long fabricated body
2996
+ * doesn't blow up the session file.
2997
+ */
2998
+ attemptedToolCalls: z14.array(
2999
+ z14.object({
3000
+ body: z14.string(),
3001
+ reason: z14.string().optional()
3002
+ })
3003
+ ).optional(),
3004
+ /**
3005
+ * Text descriptions of images this message embedded, for models that can't
3006
+ * see. `content` keeps the user's literal markdown (so the thumbnail still
3007
+ * renders and the composer can round-trip it); the digest rides alongside and
3008
+ * is spliced into the model-visible text at send time and again on every
3009
+ * history replay.
3010
+ *
3011
+ * This has to be persisted rather than injected per-turn: `priorMessages` is
3012
+ * rebuilt from the session record for every stateless provider, so an
3013
+ * ephemeral digest would make a turn-1 screenshot vanish by turn 5 — after a
3014
+ * daemon restart, a provider reset, or a context-pressure rebuild — leaving
3015
+ * the model replaying a bare `![](attachments/9f3.png)`.
3016
+ */
3017
+ recognizedImages: z14.array(MessageImageDigestSchema).optional()
3018
+ });
3019
+ var ChatTurnErrorDetailSchema = z14.object({
3020
+ code: z14.string().max(64).optional(),
3021
+ /** Component that failed — a provider name (`llama-cpp`) or a subsystem. */
3022
+ engine: z14.string().max(64).optional(),
3023
+ /** Correlation key, also written into the engine's own incident log. */
3024
+ incidentId: z14.string().max(64).optional(),
3025
+ /** Native crash class from the exit snapshot, e.g. `cuda-out-of-memory`. */
3026
+ panicKind: z14.string().max(64).optional(),
3027
+ exitCode: z14.number().int().nullable().optional(),
3028
+ signal: z14.string().max(32).nullable().optional(),
3029
+ /**
3030
+ * Request-independent launch facts copied from the crash snapshot, which
3031
+ * is contractually free of prompts, tool arguments, and secrets. Bounded
3032
+ * by the extractor. A machine profile cannot reconstruct which model at
3033
+ * which context size with which KV type crashed; this can.
3034
+ */
3035
+ diagnostics: z14.record(z14.string(), z14.union([z14.string(), z14.number(), z14.boolean()])).optional()
3036
+ });
3037
+ var ChatEventSchema = z14.discriminatedUnion("type", [
3038
+ z14.object({ type: z14.literal("delta"), content: z14.string() }),
3039
+ /**
3040
+ * Live private-reasoning tokens (ds4's think phase), streamed on their
3041
+ * own channel so they never mix into the visible `delta` stream, the
3042
+ * committed reply body, or the external API-compat forwarders. The UI
3043
+ * renders them as a distinct "thinking" block that collapses into the
3044
+ * committed message's reasoning expander once `complete` lands.
3045
+ */
3046
+ z14.object({ type: z14.literal("reasoning_delta"), content: z14.string() }),
3047
+ z14.object({ type: z14.literal("complete"), message: ChatMessageSchema }),
3048
+ /**
3049
+ * Emitted right after the user's message is appended to the session
3050
+ * record. The legacy session-scoped UI inserted user messages locally
3051
+ * before POSTing, so it didn't need this — but the project + global
3052
+ * envelope streams need it so the interleaved timeline can render the
3053
+ * user's bubble immediately, before any assistant deltas.
3054
+ */
3055
+ z14.object({ type: z14.literal("user_message"), message: ChatMessageSchema }),
3056
+ /**
3057
+ * Emitted when the assistant invokes an MCP tool (OpenAI + Mock paths
3058
+ * only — Copilot runs tools inside its subprocess, invisible to us).
3059
+ * The UI surfaces these as "thinking" breadcrumbs.
3060
+ */
3061
+ z14.object({
3062
+ type: z14.literal("tool"),
3063
+ name: z14.string(),
3064
+ durationMs: z14.number(),
3065
+ success: z14.boolean(),
3066
+ errorMessage: z14.string().optional(),
3067
+ /**
3068
+ * File path the tool touched (if any). Set for tools that take a `path`
3069
+ * argument: readFile, writeFile, read_artifact, write_artifact,
3070
+ * read_document, write_document. Lets the UI build a References panel
3071
+ * without guessing.
3072
+ */
3073
+ path: z14.string().optional(),
3074
+ /** Ordered file paths touched by a batched filesystem tool. */
3075
+ paths: z14.array(z14.string()).optional(),
3076
+ /**
3077
+ * Compact human-readable preview of the non-bulky arguments. Example:
3078
+ * `gezel: "Maya", message: "what's the status of..."`. Values are
3079
+ * truncated and bulky fields like `content` are omitted. Used by the
3080
+ * UI to render a useful in-progress tool line instead of just the
3081
+ * tool name.
3082
+ */
3083
+ argsSummary: z14.string().optional(),
3084
+ /** Full, readable args for the expand + copy affordance. See the persisted `ChatMessageToolCall.argsFull`. */
3085
+ argsFull: z14.string().optional(),
3086
+ /** Short full response, or a bounded beginning/end summary. */
3087
+ resultText: z14.string().optional(),
3088
+ /** True when `resultText` is a bounded summary rather than the complete response. */
3089
+ resultTruncated: z14.boolean().optional(),
3090
+ /**
3091
+ * Image artifacts the tool returned (most commonly browser screenshots).
3092
+ * Paths are relative to the project's artifacts/ root and resolved
3093
+ * to URLs by the UI via the artifact-read endpoint.
3094
+ */
3095
+ images: z14.array(ToolCallImageSchema).optional(),
3096
+ /**
3097
+ * Audio artifacts the tool returned (synthesize_speech narrations,
3098
+ * voice memos transcribed via transcribe_audio). Same artifact-path
3099
+ * resolution as `images`.
3100
+ */
3101
+ audios: z14.array(ToolCallAudioSchema).optional(),
3102
+ /**
3103
+ * Video artifacts the tool returned (`generate_video`). Same
3104
+ * artifact-path resolution as `images`; rendered as a `<video>`
3105
+ * player in the chat row.
3106
+ */
3107
+ videos: z14.array(ToolCallVideoSchema).optional(),
3108
+ /**
3109
+ * Unified diff describing the change a surgical-edit tool made
3110
+ * (`replace_in_file`, `apply_patch`, `insert_at_marker`). Streams to the
3111
+ * UI mid-turn so the chat bubble can render an inline diff under
3112
+ * the tool-call row even before the assistant message is finalized.
3113
+ */
3114
+ diff: z14.string().optional(),
3115
+ addedLines: z14.number().int().nonnegative().optional(),
3116
+ removedLines: z14.number().int().nonnegative().optional()
3117
+ }),
3118
+ z14.object({
3119
+ type: z14.literal("error"),
3120
+ error: z14.string(),
3121
+ // Not `detail` — three sibling variants in this union already use that
3122
+ // name for free-form progress prose, and one union with two meanings for
3123
+ // one key is a trap.
3124
+ errorDetail: ChatTurnErrorDetailSchema.optional()
3125
+ }),
3126
+ /**
3127
+ * The running turn was intentionally stopped through the cancellation
3128
+ * surface. This is terminal for the live UI, but is deliberately not an
3129
+ * error: it must not poison the session or render failure recovery UI.
3130
+ */
3131
+ z14.object({ type: z14.literal("cancelled") }),
3132
+ z14.object({ type: z14.literal("done") }),
3133
+ /**
3134
+ * Emitted when a turn ends up waiting in the provider queue for more
3135
+ * than a brief grace period (~200ms). `aheadOf` is an approximate
3136
+ * count of turns that will dispatch before this one. The UI replaces
3137
+ * its "thinking" indicator with a numbered place-in-line diagram
3138
+ * until the first `delta` or the wait clears. Sub-threshold waits don't
3139
+ * emit this event — avoids flashing the indicator on the happy path
3140
+ * where the queue is empty.
3141
+ */
3142
+ z14.object({ type: z14.literal("queued"), aheadOf: z14.number().int().min(0) }),
3143
+ /**
3144
+ * Ollama-only: emitted for each bare framing chunk that arrives
3145
+ * on the wire without visible content / tool_calls — the
3146
+ * housekeeping pings some Ollama versions stream between actual
3147
+ * tokens. Lets the UI accumulate "···" dots in the streaming
3148
+ * bubble so the user can see "Ollama is alive on the wire" even
3149
+ * when the model isn't producing visible output. Reset on the
3150
+ * next real `delta`, `tool`, or `complete`.
3151
+ */
3152
+ z14.object({ type: z14.literal("wire_pulse") }),
3153
+ /**
3154
+ * Live tool-argument stream. Fired while the model is generating a
3155
+ * structured tool call — most visibly a multi-minute `write_file`
3156
+ * whose argument tokens never appear as visible `delta`s, so without
3157
+ * this channel the only signal is the accumulating wire-pulse count.
3158
+ * `name` is the tool being called ('' until the name chunk arrives);
3159
+ * `content` is the raw argument-text chunk (JSON fragments — file
3160
+ * content mid-write). The UI accumulates these into a dimmed live
3161
+ * "working" block (same pattern as `reasoning_delta`) and drops the
3162
+ * block when the corresponding `tool` event lands.
3163
+ */
3164
+ z14.object({ type: z14.literal("tool_args_delta"), name: z14.string(), content: z14.string() }),
3165
+ /**
3166
+ * Emitted when a provider tells us it's still doing work — even though
3167
+ * no visible text/tool event has arrived. Today this is wired from
3168
+ * Copilot's `session.thinking_start` / `session.thinking_stop` events,
3169
+ * which fire during server-side reasoning that otherwise looks like
3170
+ * silence. The UI treats heartbeats as activity so the "silent for Xs"
3171
+ * banner doesn't climb while the model is legitimately thinking.
3172
+ * Optional `label` carries a short phase hint ('thinking', 'tool',
3173
+ * etc.) that the streaming bubble can surface as a status line.
3174
+ */
3175
+ z14.object({ type: z14.literal("heartbeat"), label: z14.string().optional() }),
3176
+ /**
3177
+ * Provider-side warning surfaced mid-turn (e.g. Copilot rate-limit,
3178
+ * context pressure, degraded mode). The UI renders these inline on
3179
+ * the streaming bubble so the user sees them immediately instead of
3180
+ * only finding out when the turn completes or times out.
3181
+ */
3182
+ z14.object({
3183
+ type: z14.literal("warning"),
3184
+ message: z14.string(),
3185
+ /**
3186
+ * Optional in-app destination for a warning's inline action. Kept
3187
+ * deliberately narrow: warnings are still readable prose when an older
3188
+ * client ignores this additive field.
3189
+ */
3190
+ action: z14.object({
3191
+ kind: z14.literal("settings"),
3192
+ section: z14.enum(["llamaCpp", "mlx", "ds4"])
3193
+ }).optional()
3194
+ }),
3195
+ /**
3196
+ * Copilot-only: the model announced a phase transition via its
3197
+ * `report_intent` built-in tool (e.g. "Building cart checkout
3198
+ * flow"). UI inserts a horizontal-rule divider with the label into
3199
+ * the streaming bubble at the arrival position, then persists the
3200
+ * offset on the final assistant message so completed bubbles render
3201
+ * the same segmentation on reload.
3202
+ */
3203
+ z14.object({ type: z14.literal("intent"), label: z14.string() }),
3204
+ /**
3205
+ * Emitted when a new message is enqueued on the per-session queue
3206
+ * because the session already has a turn in flight. The timeline
3207
+ * renders a "ghost bubble" under the session's streaming bubble
3208
+ * showing the queued text preview. Cleared via `queue_removed`.
3209
+ */
3210
+ z14.object({
3211
+ type: z14.literal("queue_enqueued"),
3212
+ queueId: z14.string(),
3213
+ preview: z14.string(),
3214
+ enqueuedAt: z14.string(),
3215
+ /**
3216
+ * The entry was queued as a mid-turn nudge — the ghost bubble labels
3217
+ * it "nudge" and contiguous nudges merge into one turn on drain.
3218
+ * Full text is deliberately NOT on the event (it re-publishes on
3219
+ * every coalesce/edit); the edit affordance fetches it lazily via
3220
+ * `GET /api/sessions/:id/queue`.
3221
+ */
3222
+ nudge: z14.boolean().optional()
3223
+ }),
3224
+ /**
3225
+ * Emitted when a queued entry leaves the queue — either because
3226
+ * it started running (`reason: 'started'`, the next `user_message`
3227
+ * event follows for the same queueId / session) or because it was
3228
+ * dropped without running (`reason: 'canceled'` via user action,
3229
+ * `reason: 'rejected'` via session archive / delete / shutdown).
3230
+ */
3231
+ z14.object({
3232
+ type: z14.literal("queue_removed"),
3233
+ queueId: z14.string(),
3234
+ reason: z14.enum(["started", "canceled", "rejected"])
3235
+ }),
3236
+ /**
3237
+ * Local-provider context policy: emitted when accumulated conversation
3238
+ * exceeds a safety fraction of the session's effective model context.
3239
+ * The UI may suggest starting fresh because this event is never emitted
3240
+ * for a first-turn standing system/tool prefix.
3241
+ */
3242
+ z14.object({
3243
+ type: z14.literal("context_warning"),
3244
+ estimatedTokens: z14.number(),
3245
+ numCtx: z14.number(),
3246
+ model: z14.string()
3247
+ }),
3248
+ /**
3249
+ * Local-provider context policy: emitted right after in-flight compaction collapses
3250
+ * older messages into a single synthesized "[Earlier in this
3251
+ * conversation: …]" assistant bubble. The UI swaps the warning banner
3252
+ * for a "compacted" variant and refreshes the visible timeline (older
3253
+ * bubbles are now gone from disk).
3254
+ */
3255
+ z14.object({
3256
+ type: z14.literal("context_compacted"),
3257
+ removedCount: z14.number().int().nonnegative(),
3258
+ model: z14.string()
3259
+ }),
3260
+ /**
3261
+ * Emitted when the chat manager detects a self-chat / compaction loop —
3262
+ * the same user-initiated send has already triggered N compactions, which
3263
+ * means the model is regenerating the same prompt → context-pressure →
3264
+ * compaction cycle without making progress. The pipeline halts the turn
3265
+ * so the user can intervene; the UI surfaces a "looks stuck" banner.
3266
+ */
3267
+ z14.object({
3268
+ type: z14.literal("context_loop"),
3269
+ compactionsThisSend: z14.number().int().positive(),
3270
+ reason: z14.string()
3271
+ }),
3272
+ /**
3273
+ * Emitted when the Keurmeester (frontier quality inspector) steps in
3274
+ * on a struggling session — diagnosis delivered, action applied. The
3275
+ * UI renders a "stepped in" notice on the thread; the full case
3276
+ * record lives under `~/.gezel/keurmeester/cases/` keyed by caseId.
3277
+ */
3278
+ z14.object({
3279
+ type: z14.literal("keurmeester_intervention"),
3280
+ caseId: z14.string(),
3281
+ gezelId: z14.string(),
3282
+ gezelName: z14.string(),
3283
+ action: z14.string(),
3284
+ summary: z14.string()
3285
+ }),
3286
+ /**
3287
+ * Emitted once per session the first time auto-recall runs, so the UI
3288
+ * can render a "pulled N memories from prior work" chip above the first
3289
+ * assistant reply.
3290
+ */
3291
+ z14.object({
3292
+ type: z14.literal("recall_applied"),
3293
+ hitCount: z14.number(),
3294
+ query: z14.string()
3295
+ }),
3296
+ /**
3297
+ * Emitted when a gezel posts a structured question via the
3298
+ * `ask_user_question` MCP tool. The UI re-fetches its pending
3299
+ * questions on this event so the in-chat card, Home pane, and Home
3300
+ * tab badge all light up together.
3301
+ */
3302
+ z14.object({ type: z14.literal("question_asked"), question: QuestionSchema }),
3303
+ /**
3304
+ * Emitted when the user submits (or declines) an answer. The UI
3305
+ * uses the same fan-out as `question_asked` to refresh every
3306
+ * surface; the chat bubble's pending card collapses to its
3307
+ * answered state.
3308
+ */
3309
+ z14.object({ type: z14.literal("question_answered"), question: QuestionSchema }),
3310
+ /**
3311
+ * A durable task audit event, fanned onto the project's live stream after
3312
+ * it has been appended to History. This keeps lightweight clients (most
3313
+ * notably the CLI) current without polling task files or inventing a
3314
+ * second task lifecycle bus. `task.tick` heartbeats are intentionally not
3315
+ * published; this channel is for user-meaningful changes.
3316
+ */
3317
+ z14.object({
3318
+ type: z14.literal("task_event"),
3319
+ eventId: z14.string(),
3320
+ kind: z14.string(),
3321
+ summary: z14.string(),
3322
+ at: z14.string(),
3323
+ taskRef: z14.string().optional()
3324
+ }),
3325
+ /**
3326
+ * Emitted when a gezel crosses a growth level threshold and a pending
3327
+ * level-up is created. The UI refreshes growth badges/dots and raises
3328
+ * a single calm OS notification when the window is hidden.
3329
+ */
3330
+ z14.object({
3331
+ type: z14.literal("growth_level_up"),
3332
+ gezelId: z14.string(),
3333
+ gezelName: z14.string(),
3334
+ toLevel: z14.number().int()
3335
+ }),
3336
+ /**
3337
+ * llama-cpp-only: lifecycle phase of the supervised on-device engine
3338
+ * for this turn. Fills the gap between "user sent a message" and
3339
+ * "first token arrives" — long enough (up to 60-180s on a cold start)
3340
+ * that a bare "thinking" spinner reads as hung.
3341
+ *
3342
+ * Phases:
3343
+ * - `starting`: supervisor spawned llama-server; waiting for `/health`.
3344
+ * - `loading_model`: llama-server is mapping GGUF weights into
3345
+ * memory / compiling Metal shaders. `progress` (0-1) is set when
3346
+ * the stdout parser can extract a percentage.
3347
+ * - `prefill`: request dispatched, waiting for first token.
3348
+ * - `generating`: first token arrived; streaming in progress.
3349
+ * - `ready`: engine is up and idle between turns. Used primarily
3350
+ * to clear stale status labels; not expected to render prominently.
3351
+ *
3352
+ * `detail` is free-form nerdy text (e.g. "loading layer 28/40",
3353
+ * "Metal shader compile") — the UI can surface it verbatim under
3354
+ * the phase label so users who want to know what's happening can
3355
+ * see, without cluttering the happy-path status line.
3356
+ */
3357
+ z14.object({
3358
+ type: z14.literal("engine_phase"),
3359
+ provider: z14.enum(["llama-cpp", "mlx", "ds4"]),
3360
+ phase: z14.enum(["starting", "loading_model", "prefill", "generating", "ready"]),
3361
+ detail: z14.string().optional(),
3362
+ progress: z14.number().min(0).max(1).optional(),
3363
+ ttftMs: z14.number().int().nonnegative().optional()
3364
+ }),
3365
+ /**
3366
+ * Per-turn telemetry for locally-hosted providers (llama-cpp +
3367
+ * Ollama). Emitted once at turn end with the concrete token counts
3368
+ * the UI needs for a speed readout — input tokens, output tokens,
3369
+ * wall-clock duration, and tokens-per-second on the generation
3370
+ * phase. UI accumulates these in a rolling window to show an
3371
+ * average tok/s over the last N turns.
3372
+ *
3373
+ * Cloud providers (Copilot / OpenAI) don't emit this — their usage
3374
+ * is tracked differently (`UsageSummary` via `/api/usage`); the
3375
+ * local speed metric isn't meaningful when the latency is
3376
+ * dominated by network round-trips.
3377
+ */
3378
+ z14.object({
3379
+ type: z14.literal("turn_stats"),
3380
+ provider: z14.enum(["llama-cpp", "ollama", "mlx", "ds4"]),
3381
+ promptTokens: z14.number().int().nonnegative(),
3382
+ completionTokens: z14.number().int().nonnegative(),
3383
+ durationMs: z14.number().int().nonnegative(),
3384
+ /** Generation speed in tokens/sec — completionTokens / generationSeconds. */
3385
+ tokensPerSec: z14.number().nonnegative().optional()
3386
+ }),
3387
+ /**
3388
+ * Static engine-level metrics that don't change during a session —
3389
+ * total memory allocated for model weights + KV cache, reported
3390
+ * after the supervised engine finishes loading. llama-cpp only
3391
+ * today; Ollama doesn't expose this via its HTTP surface.
3392
+ *
3393
+ * Emitted once per engine lifecycle, fan-out identical to
3394
+ * `engine_phase` (every session waiting on the same supervisor
3395
+ * startup gets a copy).
3396
+ */
3397
+ z14.object({
3398
+ type: z14.literal("engine_stats"),
3399
+ provider: z14.enum(["llama-cpp", "mlx", "ds4"]),
3400
+ /** Total bytes allocated across all GGUF buffers + KV cache. */
3401
+ ramAllocBytes: z14.number().nonnegative()
3402
+ }),
3403
+ /**
3404
+ * VRAM tenancy change — a non-LLM workload has taken (or released)
3405
+ * the GPU. Fires for the local image generator (sd-cpp), the video
3406
+ * generator, and the local image-recognition engine; in the future
3407
+ * the same event will surface STT / TTS engines that share the pool.
3408
+ *
3409
+ * The user-visible point of these events: when the chat session
3410
+ * has issued a `generate_image` tool call, the LLM is actually
3411
+ * paused (and in `swap` policy, evicted from VRAM) while sd-server
3412
+ * runs. Without this signal the bubble would just say "thinking"
3413
+ * for 30+ seconds — actively wrong, and the silence-watchdog would
3414
+ * fire a "still working — silent for X seconds" reassurance that
3415
+ * doesn't apply.
3416
+ *
3417
+ * Lifecycle: a `started` event when the workload begins, optional
3418
+ * `progress` events while the engine reports per-step advancement
3419
+ * (sd-server emits `|==> | 3/20 - 18.20s/it` per sampling step), and
3420
+ * a paired `ended` event when it finishes (or errors). The UI treats
3421
+ * all three as activity signals, so the silence timer resets across
3422
+ * the swap.
3423
+ *
3424
+ * `prompt` carries the user-facing narrative for the workload — for
3425
+ * image_generation, the prompt the model passed to `generate_image`.
3426
+ * It's set on `started` so the bubble can show "Designing: <prompt>"
3427
+ * instead of just dots while sd-server runs. `progress` (0-1) and
3428
+ * `step`/`totalSteps` drive a real progress bar; `secondsPerStep`
3429
+ * lets the UI show an ETA.
3430
+ */
3431
+ z14.object({
3432
+ type: z14.literal("gpu_swap"),
3433
+ state: z14.enum(["started", "progress", "ended"]),
3434
+ /**
3435
+ * Shared with session telemetry so a new workload can't light up the
3436
+ * bubble while staying invisible to stall detection.
3437
+ *
3438
+ * `image_recognition` carries no `progress` — llama-server emits no
3439
+ * per-step ticks for a single vision decode, and a fabricated bar reads
3440
+ * worse than a spinner. It still has to ride this event rather than a log
3441
+ * line, because the silence watchdog treats `gpu_swap` as an activity
3442
+ * signal; without it a 6s vision pass trips "still working — silent for X
3443
+ * seconds", which reads as a bug.
3444
+ */
3445
+ task: SessionGpuTaskSchema,
3446
+ detail: z14.string().optional(),
3447
+ prompt: z14.string().optional(),
3448
+ progress: z14.number().min(0).max(1).optional(),
3449
+ step: z14.number().int().nonnegative().optional(),
3450
+ totalSteps: z14.number().int().positive().optional(),
3451
+ secondsPerStep: z14.number().nonnegative().optional()
3452
+ }),
3453
+ /**
3454
+ * The turn is parked inside a synchronous `ask_gezel` /
3455
+ * `ask_specialist` consultation — this gezel's model is idle, blocked
3456
+ * on a reply from another gezel. Without this signal the asker's
3457
+ * bubble keeps showing the last "Thinking it through" label and looks
3458
+ * indistinguishable from the specialist that's actually doing the
3459
+ * work, so the user can't tell where the ball is.
3460
+ *
3461
+ * Lifecycle mirrors `gpu_swap`: a `started` event when the ask is
3462
+ * registered (before the question is even delivered), a paired
3463
+ * `ended` when the reply arrives, times out, or errors. The UI treats
3464
+ * both as activity (the silence watchdog shouldn't fire — the wait is
3465
+ * expected, not a stall) and, while `started` is unpaired, dims the
3466
+ * bubble and swaps the active "thinking" status for a passive
3467
+ * "Waiting on <name>".
3468
+ *
3469
+ * `targetGezelName` is the already-display-formatted name of the
3470
+ * gezel being consulted (role-based-name mode is resolved service-
3471
+ * side, same as the `ask_gezel` tool result), so the UI can render
3472
+ * it verbatim.
3473
+ */
3474
+ z14.object({
3475
+ type: z14.literal("awaiting_gezel"),
3476
+ state: z14.enum(["started", "ended"]),
3477
+ targetGezelName: z14.string()
3478
+ }),
3479
+ /**
3480
+ * A new project was created (via the New Project dialog, the
3481
+ * `start_project` macro, or any other path through
3482
+ * `POST /api/projects`). Emitted on the project + global streams so
3483
+ * always-mounted surfaces — the left sidebar PROJECTS list in
3484
+ * particular — can fold the project in immediately instead of waiting
3485
+ * for the next manual refresh / tab-focus poll. Not a renderable
3486
+ * timeline event (like `growth_level_up`); the chat surfaces ignore it.
3487
+ */
3488
+ z14.object({
3489
+ type: z14.literal("project_created"),
3490
+ projectId: z14.string(),
3491
+ name: z14.string()
3492
+ }),
3493
+ /**
3494
+ * A project was deleted (via the Project Actions menu, or an equivalent
3495
+ * path through `DELETE /api/projects/:id`). Emitted on the project +
3496
+ * global streams so always-mounted surfaces — the left sidebar PROJECTS
3497
+ * list and the Projects view rail — drop the row immediately instead of
3498
+ * waiting for the next manual refresh / tab-focus poll. Not a renderable
3499
+ * timeline event; the chat surfaces ignore it.
3500
+ */
3501
+ z14.object({
3502
+ type: z14.literal("project_deleted"),
3503
+ projectId: z14.string(),
3504
+ name: z14.string()
3505
+ }),
3506
+ /**
3507
+ * A new shared gezel joined the global roster. Emitted on the global
3508
+ * stream for every Store-backed creation path (including ensure_gezel),
3509
+ * so always-mounted roster surfaces can refresh immediately. Project-local
3510
+ * gezels deliberately do not emit this event because they do not belong in
3511
+ * the global Gezellen list.
3512
+ */
3513
+ z14.object({
3514
+ type: z14.literal("gezel_created"),
3515
+ gezelId: z14.string(),
3516
+ name: z14.string()
3517
+ }),
3518
+ /**
3519
+ * Global (project-less) signal that Night Shift mode flipped ON/OFF.
3520
+ * Emitted by `NightShiftManager` on every state transition so the UI
3521
+ * menu pill reflects the live state. `source` is the active driver
3522
+ * (scheduled window vs. a manual shift), null when inactive.
3523
+ */
3524
+ z14.object({
3525
+ type: z14.literal("night_shift"),
3526
+ active: z14.boolean(),
3527
+ source: z14.enum(["scheduled", "manual"]).nullable()
3528
+ }),
3529
+ /**
3530
+ * Global signal from the meester status generator. `started` fires
3531
+ * when a run begins (manual runs show "Meester is writing…"),
3532
+ * `ended` when a fresh report landed on disk, `failed` when the run
3533
+ * produced nothing usable — the Home greeting refetches on the
3534
+ * terminal states instead of polling.
3535
+ */
3536
+ z14.object({
3537
+ type: z14.literal("meester_status"),
3538
+ state: z14.enum(["started", "ended", "failed"]),
3539
+ generatedAt: z14.string().optional()
3540
+ }),
3541
+ /**
3542
+ * Global (history-free) heartbeat from the boekwachter indexing loops —
3543
+ * workspace scans, AI enrichment batches, weekly digests. Drives the live
3544
+ * indicator pill; complements (doesn't replace) the polled per-project
3545
+ * index status. `pending` = files still awaiting AI enrichment when known.
3546
+ */
3547
+ z14.object({
3548
+ type: z14.literal("index_progress"),
3549
+ phase: z14.enum(["scan", "enrich", "review", "digest"]),
3550
+ state: z14.enum(["started", "progress", "ended"]),
3551
+ projectId: z14.string().optional(),
3552
+ detail: z14.string().optional(),
3553
+ pending: z14.number().int().nonnegative().optional(),
3554
+ /** The concrete autonomous gezel doing this work, when the project has one. */
3555
+ gezelId: z14.string().optional(),
3556
+ /** Snapshot of their display name so transient progress remains human-readable. */
3557
+ gezelName: z14.string().optional()
3558
+ })
3559
+ ]);
3560
+ var ChatEventEnvelopeSchema = z14.object({
3561
+ sessionId: z14.string(),
3562
+ gezelId: z14.string(),
3563
+ projectId: z14.string(),
3564
+ event: ChatEventSchema
3565
+ });
3566
+
3567
+ // src/markdown/gezel-md.ts
3568
+ var HEADING_RE = /^(#{1,6})\s+(.+?)\s*$/;
3569
+ var TEMPLATE_RE = /\{\[\s*([a-zA-Z_][\w-]*)\s*([^\]]*)\]\}\s*$/;
3570
+ function parseGezelMarkdown(source) {
3571
+ const parsed = parseYamlFrontmatter(source);
3572
+ const frontmatter = GezelFrontmatterSchema.parse({
3573
+ name: "Untitled agent",
3574
+ ...parsed.data
3575
+ });
3576
+ const lines = parsed.content.split(/\r?\n/);
3577
+ const headings = collectHeadings(lines);
3578
+ if (headings.length === 0) {
3579
+ return {
3580
+ frontmatter,
3581
+ sections: [
3582
+ {
3583
+ heading: "",
3584
+ body: parsed.content.trim()
3585
+ }
3586
+ ],
3587
+ source
3588
+ };
3589
+ }
3590
+ const topLevel = Math.min(...headings.map((h) => h.level));
3591
+ const sectionStarts = headings.filter((h) => h.level === topLevel);
3592
+ const sections = [];
3593
+ for (let i = 0; i < sectionStarts.length; i++) {
3594
+ const start = sectionStarts[i];
3595
+ const next = sectionStarts[i + 1];
3596
+ const bodyLines = lines.slice(start.lineIndex + 1, next ? next.lineIndex : lines.length);
3597
+ const body = bodyLines.join("\n").trim();
3598
+ const { heading, template, params } = parseHeadingAnnotation(start.text);
3599
+ sections.push({
3600
+ heading,
3601
+ body,
3602
+ ...template ? { template } : {},
3603
+ ...params && Object.keys(params).length > 0 ? { params } : {}
3604
+ });
3605
+ }
3606
+ return { frontmatter, sections, source };
3607
+ }
3608
+ function collectHeadings(lines) {
3609
+ const out = [];
3610
+ for (let i = 0; i < lines.length; i++) {
3611
+ const line = lines[i];
3612
+ const m = HEADING_RE.exec(line);
3613
+ if (m) out.push({ lineIndex: i, level: m[1].length, text: m[2] });
3614
+ }
3615
+ return out;
3616
+ }
3617
+ function parseHeadingAnnotation(raw) {
3618
+ const m = TEMPLATE_RE.exec(raw);
3619
+ if (!m) return { heading: raw.trim() };
3620
+ const heading = raw.slice(0, m.index).trim();
3621
+ const template = m[1];
3622
+ const params = parseParams(m[2] ?? "");
3623
+ return { heading, template, params };
3624
+ }
3625
+ function parseParams(src) {
3626
+ const out = {};
3627
+ const re = /([a-zA-Z_][\w-]*)=(?:"([^"]*)"|'([^']*)'|([^\s]+))/g;
3628
+ let m;
3629
+ while ((m = re.exec(src)) !== null) {
3630
+ out[m[1]] = m[2] ?? m[3] ?? m[4] ?? "";
3631
+ }
3632
+ return out;
3633
+ }
3634
+ function serializeGezelMarkdown(parsed) {
3635
+ const body = parsed.sections.map((section) => {
3636
+ const annotation = section.template ? ` {[${section.template}${formatParams(section.params)}]}` : "";
3637
+ const heading = section.heading ? `## ${section.heading}${annotation}
3638
+
3639
+ ` : "";
3640
+ return `${heading}${section.body}`.trimEnd();
3641
+ }).join("\n\n");
3642
+ return stringifyYamlFrontmatter(`${body}
3643
+ `, parsed.frontmatter);
3644
+ }
3645
+ function formatParams(params) {
3646
+ if (!params) return "";
3647
+ const keys = Object.keys(params);
3648
+ if (keys.length === 0) return "";
3649
+ return ` ${keys.map((k) => `${k}=${quoteIfNeeded(params[k])}`).join(" ")}`;
3650
+ }
3651
+ function quoteIfNeeded(value) {
3652
+ return /\s/.test(value) ? `"${value}"` : value;
3653
+ }
3654
+
3655
+ // src/markdown/promote-channel-names.ts
3656
+ var KNOWN_CHANNEL_NAMES = ["thought", "analysis", "commentary", "final"];
3657
+ var MODE_INDICATORS = {
3658
+ thought: "_Thinking\u2026_",
3659
+ analysis: "_Analyzing\u2026_",
3660
+ commentary: "_Reflecting\u2026_",
3661
+ final: null
3662
+ };
3663
+ function splitInlineChannelLeaks(text) {
3664
+ return text.replace(/^(thought|analysis|commentary|final)(?=[A-Z])/gm, "$1\n");
3665
+ }
3666
+ function promoteBareChannelNames(text) {
3667
+ if (!text) return text;
3668
+ const split = splitInlineChannelLeaks(text);
3669
+ const lines = split.split("\n");
3670
+ const out = [];
3671
+ let lastIndicator = null;
3672
+ for (const raw of lines) {
3673
+ const trimmed = raw.trim().toLowerCase();
3674
+ const match = KNOWN_CHANNEL_NAMES.includes(trimmed) ? trimmed : null;
3675
+ if (match) {
3676
+ const indicator = MODE_INDICATORS[match];
3677
+ if (indicator === null) {
3678
+ lastIndicator = null;
3679
+ continue;
3680
+ }
3681
+ if (indicator !== lastIndicator) {
3682
+ out.push(indicator);
3683
+ lastIndicator = indicator;
3684
+ }
3685
+ continue;
3686
+ }
3687
+ if (raw.trim().length > 0) lastIndicator = null;
3688
+ out.push(raw);
3689
+ }
3690
+ return out.join("\n");
3691
+ }
3692
+
3693
+ // src/schemas/report-action.ts
3694
+ import { z as z15 } from "zod";
3695
+ var ReportActionKindSchema = z15.enum(["fire-craftbook", "create-task", "apply-edits"]);
3696
+ var commonFields = {
3697
+ /**
3698
+ * Author-supplied stable slug — keeps lifecycle state attached across
3699
+ * nightly report regenerations. Optional: the parser falls back to a
3700
+ * content hash (`a-<hash>`), which is stable only while the block's
3701
+ * body is byte-identical.
3702
+ */
3703
+ id: z15.string().regex(/^[a-z0-9][a-z0-9_-]*$/i).optional(),
3704
+ /** Short human label for the card ("Fix the unchecked null in parser.ts"). */
3705
+ title: z15.string().min(1),
3706
+ /** One-or-two-sentence rationale shown under the title. */
3707
+ reason: z15.string().optional(),
3708
+ /**
3709
+ * Target project. Defaults to the report's own project. Cross-project
3710
+ * targets are a primary case — the bundled oversight report lives in
3711
+ * `default` but recommends work in specific projects.
3712
+ */
3713
+ projectId: z15.string().optional()
3714
+ };
3715
+ var FireCraftbookActionSchema = z15.object({
3716
+ kind: z15.literal("fire-craftbook"),
3717
+ ...commonFields,
3718
+ craftbookId: z15.string().min(1),
3719
+ /** Invocation params for the craftbook's paramSchema (stringified values). */
3720
+ params: z15.record(z15.string(), z15.string()).optional()
3721
+ });
3722
+ var CreateTaskActionSchema = z15.object({
3723
+ kind: z15.literal("create-task"),
3724
+ ...commonFields,
3725
+ /** Full work instruction for the bespoke task's single step. */
3726
+ prompt: z15.string().min(1),
3727
+ /** Role to recruit for the work (ensure_gezel jobTitle). Default: software developer. */
3728
+ role: z15.string().optional()
3729
+ });
3730
+ var ApplyEditsActionSchema = z15.object({
3731
+ kind: z15.literal("apply-edits"),
3732
+ ...commonFields,
3733
+ edits: z15.array(
3734
+ z15.object({
3735
+ /** Workspace-relative target file in the TARGET project. */
3736
+ path: z15.string().min(1),
3737
+ /** Artifacts-relative sidecar `.diff` path in the REPORT's project. */
3738
+ diffArtifact: z15.string().min(1)
3739
+ })
3740
+ ).min(1)
3741
+ });
3742
+ var ReportActionSchema = z15.discriminatedUnion("kind", [
3743
+ FireCraftbookActionSchema,
3744
+ CreateTaskActionSchema,
3745
+ ApplyEditsActionSchema
3746
+ ]);
3747
+ var ReportActionStateSchema = z15.enum([
3748
+ "suggested",
3749
+ "fired",
3750
+ "applied",
3751
+ "failed",
3752
+ "dismissed"
3753
+ ]);
3754
+ var ReportActionRecordSchema = z15.object({
3755
+ actionId: z15.string(),
3756
+ /** Artifacts-relative path of the report the action came from. */
3757
+ reportPath: z15.string(),
3758
+ kind: ReportActionKindSchema,
3759
+ contentHash: z15.string(),
3760
+ firstSeenAt: z15.string(),
3761
+ state: ReportActionStateSchema,
3762
+ /** Task materialized by fire-craftbook / create-task. */
3763
+ taskRef: z15.string().optional(),
3764
+ firedAt: z15.string().optional(),
3765
+ /** Stamped when the fired task settles. */
3766
+ settledAt: z15.string().optional(),
3767
+ outcome: z15.enum(["complete", "canceled"]).optional(),
3768
+ /** apply-edits per-file results. */
3769
+ results: z15.array(
3770
+ z15.object({
3771
+ path: z15.string(),
3772
+ ok: z15.boolean(),
3773
+ error: z15.string().optional()
3774
+ })
3775
+ ).optional()
3776
+ });
3777
+ var ReportActionParseIssueSchema = z15.object({
3778
+ /** Zero-based index among the report's gezel-action fences. */
3779
+ index: z15.number().int(),
3780
+ message: z15.string(),
3781
+ /** Raw fence body, for the "unreadable action block" card. */
3782
+ raw: z15.string()
3783
+ });
3784
+ var ReportActionViewSchema = z15.object({
3785
+ action: ReportActionSchema,
3786
+ id: z15.string(),
3787
+ contentHash: z15.string(),
3788
+ state: ReportActionStateSchema,
3789
+ taskRef: z15.string().optional(),
3790
+ firedAt: z15.string().optional(),
3791
+ settledAt: z15.string().optional(),
3792
+ outcome: z15.enum(["complete", "canceled"]).optional(),
3793
+ results: z15.array(z15.object({ path: z15.string(), ok: z15.boolean(), error: z15.string().optional() })).optional(),
3794
+ /** The stored record predates a changed block body (report regenerated). */
3795
+ contentChanged: z15.boolean().optional()
3796
+ });
3797
+ var ReportActionsResponseSchema = z15.object({
3798
+ actions: z15.array(ReportActionViewSchema),
3799
+ issues: z15.array(ReportActionParseIssueSchema),
3800
+ /** Records whose action vanished from the regenerated report. */
3801
+ stale: z15.array(ReportActionRecordSchema)
3802
+ });
3803
+ var FireReportActionRequestSchema = z15.object({
3804
+ /** Artifacts-relative report path. */
3805
+ path: z15.string().min(1),
3806
+ actionId: z15.string().min(1),
3807
+ /** apply-edits: none. fire-craftbook: overrides the block's params. */
3808
+ params: z15.record(z15.string(), z15.string()).optional()
3809
+ });
3810
+ var FireReportActionResponseSchema = z15.object({
3811
+ record: ReportActionRecordSchema,
3812
+ /** Set for fire-craftbook / create-task. */
3813
+ taskRef: z15.string().optional()
3814
+ });
3815
+ var DismissReportActionRequestSchema = z15.object({
3816
+ path: z15.string().min(1),
3817
+ actionId: z15.string().min(1)
3818
+ });
3819
+
3820
+ // src/markdown/report-actions.ts
3821
+ var REPORT_ACTION_FENCE_LANG = "gezel-action";
3822
+ function reportActionContentHash(body) {
3823
+ let hash = 2166136261;
3824
+ const text = body.trim();
3825
+ for (let i = 0; i < text.length; i++) {
3826
+ hash ^= text.charCodeAt(i);
3827
+ hash = Math.imul(hash, 16777619) >>> 0;
3828
+ }
3829
+ return hash.toString(16).padStart(8, "0");
3830
+ }
3831
+ var KIND_ALIASES = {
3832
+ craftbook: "fire-craftbook",
3833
+ "run-craftbook": "fire-craftbook",
3834
+ task: "create-task",
3835
+ "new-task": "create-task",
3836
+ edits: "apply-edits",
3837
+ "apply-diffs": "apply-edits",
3838
+ diff: "apply-edits",
3839
+ "diff-pack": "apply-edits"
3840
+ };
3841
+ var KNOWN_KEYS_BY_KIND = {
3842
+ "fire-craftbook": ["kind", "id", "title", "reason", "projectId", "craftbookId", "params"],
3843
+ "create-task": ["kind", "id", "title", "reason", "projectId", "prompt", "role"],
3844
+ "apply-edits": ["kind", "id", "title", "reason", "projectId", "edits"]
3845
+ };
3846
+ var KEY_ALIASES = {
3847
+ name: "title",
3848
+ project: "projectId",
3849
+ craftbook: "craftbookId",
3850
+ why: "reason"
3851
+ };
3852
+ function normalizeRaw(raw) {
3853
+ const out = {};
3854
+ for (const [key, value] of Object.entries(raw)) {
3855
+ out[KEY_ALIASES[key] ?? key] = value;
3856
+ }
3857
+ const kindRaw = typeof out.kind === "string" ? out.kind.trim().toLowerCase() : "";
3858
+ const kind = KIND_ALIASES[kindRaw] ?? kindRaw;
3859
+ out.kind = kind;
3860
+ const known = KNOWN_KEYS_BY_KIND[kind];
3861
+ const stripped = {};
3862
+ for (const [key, value] of Object.entries(out)) {
3863
+ if (!known || known.includes(key)) stripped[key] = value;
3864
+ }
3865
+ if (stripped.params && typeof stripped.params === "object" && !Array.isArray(stripped.params)) {
3866
+ const params = {};
3867
+ for (const [key, value] of Object.entries(stripped.params)) {
3868
+ if (value === null || value === void 0) continue;
3869
+ params[key] = typeof value === "string" ? value : String(value);
3870
+ }
3871
+ stripped.params = params;
3872
+ }
3873
+ if (Array.isArray(stripped.edits)) {
3874
+ stripped.edits = stripped.edits.map((entry) => {
3875
+ if (!entry || typeof entry !== "object") return entry;
3876
+ const record = entry;
3877
+ return {
3878
+ ...record.path !== void 0 ? { path: String(record.path) } : {},
3879
+ ...record.diffArtifact !== void 0 ? { diffArtifact: String(record.diffArtifact) } : record.diff !== void 0 ? { diffArtifact: String(record.diff) } : {}
3880
+ };
3881
+ }).filter((entry) => entry && typeof entry === "object");
3882
+ }
3883
+ return stripped;
3884
+ }
3885
+ function parseReportActionBlock(body, index) {
3886
+ const contentHash = reportActionContentHash(body);
3887
+ let raw;
3888
+ try {
3889
+ raw = parseYamlBlock(body);
3890
+ } catch (err) {
3891
+ return {
3892
+ ok: false,
3893
+ issue: {
3894
+ index,
3895
+ message: `not a YAML mapping: ${err instanceof Error ? err.message : String(err)}`,
3896
+ raw: body
3897
+ }
3898
+ };
3899
+ }
3900
+ const normalized = normalizeRaw(raw);
3901
+ const parsed = ReportActionSchema.safeParse(normalized);
3902
+ if (!parsed.success) {
3903
+ const first = parsed.error.issues[0];
3904
+ return {
3905
+ ok: false,
3906
+ issue: {
3907
+ index,
3908
+ message: first ? `${first.path.join(".") || "block"}: ${first.message}` : "invalid action block",
3909
+ raw: body
3910
+ }
3911
+ };
3912
+ }
3913
+ const id = parsed.data.id ?? `a-${contentHash}`;
3914
+ return { ok: true, action: { ...parsed.data, id, contentHash } };
3915
+ }
3916
+ function parseReportActions(markdown) {
3917
+ const actions = [];
3918
+ const issues = [];
3919
+ const seen = /* @__PURE__ */ new Map();
3920
+ let index = 0;
3921
+ for (const fence of extractAllFences(markdown)) {
3922
+ const lang = fence.lang.split(/\s+/, 1)[0] ?? "";
3923
+ if (lang !== REPORT_ACTION_FENCE_LANG) continue;
3924
+ const fenceIndex = index++;
3925
+ const result = parseReportActionBlock(fence.code, fenceIndex);
3926
+ if (!result.ok) {
3927
+ issues.push(result.issue);
3928
+ continue;
3929
+ }
3930
+ const action = result.action;
3931
+ const count = (seen.get(action.id) ?? 0) + 1;
3932
+ seen.set(action.id, count);
3933
+ if (count > 1) {
3934
+ const suffixed = `${action.id}-${count}`;
3935
+ issues.push({
3936
+ index: fenceIndex,
3937
+ message: `duplicate action id "${action.id}" \u2014 renamed to "${suffixed}"; give each block a unique id`,
3938
+ raw: fence.code
3939
+ });
3940
+ actions.push({ ...action, id: suffixed });
3941
+ } else {
3942
+ actions.push(action);
3943
+ }
3944
+ }
3945
+ return { actions, issues };
3946
+ }
3947
+ function hasReportActionFence(markdown) {
3948
+ const lines = markdown.split("\n");
3949
+ let openFence = null;
3950
+ for (const line of lines) {
3951
+ const fence = FENCE.exec(line.trim());
3952
+ if (openFence !== null) {
3953
+ if (fence && isFenceClose(line, openFence)) openFence = null;
3954
+ continue;
3955
+ }
3956
+ if (fence) {
3957
+ openFence = fence[1];
3958
+ const lang = fence[2].trim().toLowerCase().split(/\s+/, 1)[0];
3959
+ if (lang === REPORT_ACTION_FENCE_LANG) return true;
3960
+ }
3961
+ }
3962
+ return false;
3963
+ }
3964
+ var REPORT_ACTION_AUTHORING_GUIDE = `When a recommendation is directly actionable, follow it with a \`\`\`gezel-action fenced YAML block so the user can fire it with one click in the morning. Three kinds, flat keys only, one block per action, each with a unique stable \`id\` slug:
3965
+
3966
+ Run an existing craftbook:
3967
+ \`\`\`gezel-action
3968
+ kind: fire-craftbook
3969
+ id: nightly-a11y-sweep
3970
+ title: Run an accessibility audit
3971
+ reason: Three templates changed without alt text review.
3972
+ craftbookId: a11y-audit
3973
+ projectId: webshop
3974
+ \`\`\`
3975
+
3976
+ Delegate a bespoke task:
3977
+ \`\`\`gezel-action
3978
+ kind: create-task
3979
+ id: fix-null-parse
3980
+ title: Fix the unchecked null in parser.ts
3981
+ reason: parseHeader returns null on empty input and callers dereference it.
3982
+ prompt: In src/parser.ts, parseHeader can return null (line ~88) and both callers dereference the result. Add the guard, mirror the fix in parseFooter, and extend parser.test.ts with the empty-input case.
3983
+ role: software developer
3984
+ projectId: webshop
3985
+ \`\`\`
3986
+
3987
+ Propose file edits (diffs go in SIDECAR artifact files \u2014 never inline):
3988
+ \`\`\`gezel-action
3989
+ kind: apply-edits
3990
+ id: harden-csp-headers
3991
+ title: Add missing security headers
3992
+ reason: Responses lack X-Content-Type-Options and a CSP.
3993
+ projectId: webshop
3994
+ edits:
3995
+ - path: src/server/headers.ts
3996
+ diffArtifact: night-shift-report/edits/harden-csp-headers.diff
3997
+ \`\`\`
3998
+
3999
+ For apply-edits: write each proposed change as ONE unified diff per target file with write_artifact (a single-file diff against the file's current content), and reference it via diffArtifact. Keep titles short, reasons to a sentence or two, and never invent craftbook ids \u2014 only reference books you confirmed exist.`;
4000
+ export {
4001
+ FENCE,
4002
+ REPORT_ACTION_AUTHORING_GUIDE,
4003
+ REPORT_ACTION_FENCE_LANG,
4004
+ defaultStepIdForName,
4005
+ extractAllFences,
4006
+ extractFirstFence,
4007
+ findFirstH1,
4008
+ hasReportActionFence,
4009
+ isFenceClose,
4010
+ parseCraftbookMarkdown,
4011
+ parseGezelMarkdown,
4012
+ parseReportActionBlock,
4013
+ parseReportActions,
4014
+ parseYamlBlock,
4015
+ pickFence,
4016
+ promoteBareChannelNames,
4017
+ reportActionContentHash,
4018
+ serializeCraftbookMarkdown,
4019
+ serializeGezelMarkdown,
4020
+ splitSections,
4021
+ stringifyYamlBlock
4022
+ };