@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.
- package/LICENSE +21 -0
- package/README.md +39 -0
- package/dist/checks/index.d.ts +693 -0
- package/dist/checks/index.js +1848 -0
- package/dist/device-safety-DezzpNyR.d.ts +10 -0
- package/dist/index-D_dch9Qh.d.ts +59398 -0
- package/dist/index.d.ts +3217 -0
- package/dist/index.js +25602 -0
- package/dist/markdown/index.d.ts +174 -0
- package/dist/markdown/index.js +4022 -0
- package/dist/native/index.d.ts +650 -0
- package/dist/native/index.js +1121 -0
- package/dist/paths.d.ts +578 -0
- package/dist/paths.js +573 -0
- package/dist/report-action-DzdQHzGG.d.ts +4270 -0
- package/dist/schemas/index.d.ts +3 -0
- package/dist/schemas/index.js +15382 -0
- package/package.json +85 -0
|
@@ -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 ``.
|
|
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
|
+
};
|