@avocadostudio-ai/orchestrator-core 0.22.0 → 0.24.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/dist/agent/agent-loop-openai.js +3 -1
  2. package/dist/agent/agent-provider.js +2 -2
  3. package/dist/agents/backtest.d.ts +35 -0
  4. package/dist/agents/backtest.js +92 -0
  5. package/dist/agents/builtins.d.ts +21 -0
  6. package/dist/agents/builtins.js +102 -0
  7. package/dist/agents/draft-spec.d.ts +44 -0
  8. package/dist/agents/draft-spec.js +195 -0
  9. package/dist/agents/edit-safety-resolve.d.ts +70 -0
  10. package/dist/agents/edit-safety-resolve.js +195 -0
  11. package/dist/agents/edit-safety-runner.d.ts +68 -0
  12. package/dist/agents/edit-safety-runner.js +157 -0
  13. package/dist/agents/edit-safety.d.ts +82 -0
  14. package/dist/agents/edit-safety.js +425 -0
  15. package/dist/agents/enabled.d.ts +18 -0
  16. package/dist/agents/enabled.js +21 -0
  17. package/dist/agents/fix-writer.d.ts +38 -0
  18. package/dist/agents/fix-writer.js +197 -0
  19. package/dist/agents/inbox.d.ts +141 -0
  20. package/dist/agents/inbox.js +364 -0
  21. package/dist/agents/learning.d.ts +32 -0
  22. package/dist/agents/learning.js +74 -0
  23. package/dist/agents/mention.d.ts +76 -0
  24. package/dist/agents/mention.js +165 -0
  25. package/dist/agents/run-spec.d.ts +27 -0
  26. package/dist/agents/run-spec.js +82 -0
  27. package/dist/agents/schedule.d.ts +22 -0
  28. package/dist/agents/schedule.js +53 -0
  29. package/dist/agents/settings.d.ts +30 -0
  30. package/dist/agents/settings.js +64 -0
  31. package/dist/agents/spec.d.ts +84 -0
  32. package/dist/agents/spec.js +70 -0
  33. package/dist/agents/stats.d.ts +15 -0
  34. package/dist/agents/stats.js +48 -0
  35. package/dist/agents/templates.d.ts +34 -0
  36. package/dist/agents/templates.js +268 -0
  37. package/dist/agents/tick.d.ts +59 -0
  38. package/dist/agents/tick.js +164 -0
  39. package/dist/chat/anthropic-model-caps.d.ts +19 -0
  40. package/dist/chat/anthropic-model-caps.js +56 -0
  41. package/dist/chat/anthropic-planner.d.ts +1 -0
  42. package/dist/chat/anthropic-planner.js +72 -15
  43. package/dist/chat/chat-pipeline.js +54 -1
  44. package/dist/chat/model-defaults.js +6 -3
  45. package/dist/checks/rules-draft.d.ts +6 -1
  46. package/dist/checks/rules-draft.js +10 -7
  47. package/dist/checks/rules-i18n.d.ts +25 -0
  48. package/dist/checks/rules-i18n.js +193 -0
  49. package/dist/checks/run-checks.d.ts +16 -1
  50. package/dist/checks/run-checks.js +80 -63
  51. package/dist/checks/session-runner.js +7 -0
  52. package/dist/checks/types.d.ts +6 -0
  53. package/dist/durable/in-memory-durable-store.d.ts +34 -1
  54. package/dist/durable/in-memory-durable-store.js +122 -1
  55. package/dist/durable/index.d.ts +1 -1
  56. package/dist/durable/sqlite-durable-store.d.ts +19 -1
  57. package/dist/durable/sqlite-durable-store.js +220 -1
  58. package/dist/durable/types.d.ts +146 -0
  59. package/dist/handler/create-orchestrator.js +195 -2
  60. package/dist/http/history-actions.d.ts +22 -0
  61. package/dist/http/history-actions.js +33 -5
  62. package/dist/http/inbox-actions.d.ts +156 -0
  63. package/dist/http/inbox-actions.js +498 -0
  64. package/dist/http/ops-actions.d.ts +17 -1
  65. package/dist/http/ops-actions.js +36 -0
  66. package/dist/http/variation-preview-actions.d.ts +30 -0
  67. package/dist/http/variation-preview-actions.js +75 -0
  68. package/dist/index.d.ts +10 -1
  69. package/dist/index.js +9 -0
  70. package/dist/nlp/deterministic-planner.d.ts +1 -1
  71. package/dist/nlp/intent-detection.d.ts +15 -0
  72. package/dist/nlp/intent-detection.js +5 -0
  73. package/dist/state/session-state.d.ts +11 -3
  74. package/dist/state/session-state.js +8 -0
  75. package/dist/state/sqlite-store.d.ts +12 -0
  76. package/dist/telemetry/usage.js +4 -0
  77. package/package.json +3 -3
@@ -0,0 +1,425 @@
1
+ import { fieldText } from "../checks/field-walk.js";
2
+ function isRecord(value) {
3
+ return typeof value === "object" && value !== null && !Array.isArray(value);
4
+ }
5
+ /**
6
+ * Whether a value holds something a reader would miss.
7
+ *
8
+ * Booleans never count: `false` is a setting, not an absence. Objects count
9
+ * when any string or number inside them does, so an image `{ src: "" }` is
10
+ * empty and a rich-text document with one word is not.
11
+ */
12
+ export function hasContent(value) {
13
+ if (value == null)
14
+ return false;
15
+ if (typeof value === "string")
16
+ return value.trim().length > 0;
17
+ if (typeof value === "number")
18
+ return Number.isFinite(value);
19
+ if (typeof value === "boolean")
20
+ return false;
21
+ if (Array.isArray(value))
22
+ return value.some((entry) => hasContent(entry) || isRecord(entry));
23
+ if (isRecord(value)) {
24
+ if (fieldText(value).trim().length > 0)
25
+ return true;
26
+ return Object.entries(value).some(([key, entry]) => key !== "id" && key !== "type" && hasContent(entry));
27
+ }
28
+ return false;
29
+ }
30
+ function shapeOf(value) {
31
+ if (typeof value === "string")
32
+ return "text";
33
+ if (typeof value === "number")
34
+ return "number";
35
+ if (Array.isArray(value))
36
+ return "list";
37
+ if (isRecord(value))
38
+ return "object";
39
+ return "other";
40
+ }
41
+ function definitionFor(manifest, type) {
42
+ return manifest.blocks.find((b) => b.type === type);
43
+ }
44
+ const BLOCK_LABEL_MAX = 48;
45
+ /** What a reader calls a block: its first non-empty text field. Mirrors field-walk. */
46
+ function labelFor(block, definition) {
47
+ const props = isRecord(block.props) ? block.props : {};
48
+ const keys = definition
49
+ ? Object.entries(definition.fields ?? {})
50
+ .filter(([, meta]) => meta.kind === "text")
51
+ .map(([key]) => key)
52
+ : Object.keys(props);
53
+ for (const key of keys) {
54
+ const value = props[key];
55
+ if (typeof value !== "string")
56
+ continue;
57
+ const trimmed = value.trim();
58
+ if (!trimmed || /^https?:\/\//.test(trimmed))
59
+ continue;
60
+ return trimmed.length > BLOCK_LABEL_MAX ? `${trimmed.slice(0, BLOCK_LABEL_MAX - 1)}…` : trimmed;
61
+ }
62
+ return undefined;
63
+ }
64
+ function itemFieldsFor(list, item) {
65
+ const merged = (list.itemFields ?? {});
66
+ if (!list.discriminator || !list.itemFieldsByType || !isRecord(item))
67
+ return merged;
68
+ const branch = item[list.discriminator];
69
+ if (typeof branch !== "string")
70
+ return merged;
71
+ return list.itemFieldsByType[branch] ?? merged;
72
+ }
73
+ /**
74
+ * Pair a list's items before and after the edit.
75
+ *
76
+ * By `id` when every item has one — the only pairing that survives an insert
77
+ * or a reorder. By position only when the list kept its length; otherwise a
78
+ * removed item would pair every later item with its neighbour and report
79
+ * fields as emptied that simply moved up.
80
+ */
81
+ function pairItems(before, after) {
82
+ const idOf = (item) => (isRecord(item) && typeof item.id === "string" && item.id ? item.id : undefined);
83
+ const allHaveIds = before.every((item) => idOf(item)) && after.every((item) => idOf(item));
84
+ if (allHaveIds) {
85
+ const afterIndexById = new Map(after.map((item, index) => [idOf(item), index]));
86
+ const out = [];
87
+ for (const item of before) {
88
+ const id = idOf(item);
89
+ const index = afterIndexById.get(id);
90
+ if (index === undefined)
91
+ continue;
92
+ out.push({ before: item, after: after[index], afterIndex: index, itemId: id });
93
+ }
94
+ return out;
95
+ }
96
+ if (before.length !== after.length)
97
+ return [];
98
+ return before.map((item, index) => ({ before: item, after: after[index], afterIndex: index }));
99
+ }
100
+ function fieldChange(before, after, kind) {
101
+ if (!hasContent(before))
102
+ return null;
103
+ if (!hasContent(after))
104
+ return "emptied";
105
+ if (kind === "richtext")
106
+ return null;
107
+ const from = shapeOf(before);
108
+ const to = shapeOf(after);
109
+ if (from === to)
110
+ return null;
111
+ // Only a change a reader would notice: a list or an object collapsing into a
112
+ // scalar, or the reverse. A number stored as text is the same value.
113
+ const structural = (shape) => shape === "list" || shape === "object";
114
+ return structural(from) !== structural(to) ? "type_changed" : null;
115
+ }
116
+ function blockIssues(slug, before, after, manifest, ops) {
117
+ const definition = definitionFor(manifest, after.type) ?? definitionFor(manifest, before.type);
118
+ const beforeProps = isRecord(before.props) ? before.props : {};
119
+ const afterProps = isRecord(after.props) ? after.props : {};
120
+ const blockLabel = labelFor(before, definition);
121
+ const located = {
122
+ slug,
123
+ blockId: after.id,
124
+ blockType: after.type,
125
+ ...(blockLabel ? { blockLabel } : {})
126
+ };
127
+ const out = [];
128
+ const topLevel = definition
129
+ ? Object.entries(definition.fields ?? {}).map(([key, meta]) => [key, meta])
130
+ : Object.keys(beforeProps)
131
+ .filter((key) => key !== "id")
132
+ .map((key) => [key, undefined]);
133
+ const listKeys = new Set(Object.keys(definition?.listFields ?? {}));
134
+ for (const [key, meta] of topLevel) {
135
+ if (listKeys.has(key))
136
+ continue;
137
+ const change = fieldChange(beforeProps[key], afterProps[key], meta?.kind);
138
+ if (!change)
139
+ continue;
140
+ out.push({
141
+ kind: change,
142
+ ...located,
143
+ path: key,
144
+ ...(meta?.label ? { label: meta.label } : {}),
145
+ before: beforeProps[key],
146
+ after: afterProps[key] ?? null,
147
+ revert: { op: "update_props", pageSlug: slug, blockId: after.id, patch: { [key]: beforeProps[key] } }
148
+ });
149
+ }
150
+ for (const [listKey, list] of Object.entries(definition?.listFields ?? {})) {
151
+ const beforeItems = beforeProps[listKey];
152
+ const afterItems = afterProps[listKey];
153
+ if (!Array.isArray(beforeItems) || beforeItems.length === 0)
154
+ continue;
155
+ if (!Array.isArray(afterItems) || afterItems.length === 0) {
156
+ // The whole list went. One finding for it, not one per field inside it.
157
+ out.push({
158
+ kind: Array.isArray(afterItems) || afterItems == null ? "emptied" : "type_changed",
159
+ ...located,
160
+ path: listKey,
161
+ ...(list.label ? { label: list.label } : {}),
162
+ before: beforeItems,
163
+ after: afterItems ?? null,
164
+ revert: { op: "update_props", pageSlug: slug, blockId: after.id, patch: { [listKey]: beforeItems } }
165
+ });
166
+ continue;
167
+ }
168
+ out.push(...droppedItems(slug, located, listKey, list.label, beforeItems, afterItems, ops));
169
+ for (const pair of pairItems(beforeItems, afterItems)) {
170
+ if (!isRecord(pair.before))
171
+ continue;
172
+ const afterItem = isRecord(pair.after) ? pair.after : {};
173
+ for (const [itemKey, meta] of Object.entries(itemFieldsFor(list, pair.before))) {
174
+ const change = fieldChange(pair.before[itemKey], afterItem[itemKey], meta.kind);
175
+ if (!change)
176
+ continue;
177
+ out.push({
178
+ kind: change,
179
+ ...located,
180
+ path: `${listKey}[${pair.afterIndex}].${itemKey}`,
181
+ ...(meta.label ? { label: meta.label } : {}),
182
+ before: pair.before[itemKey],
183
+ after: afterItem[itemKey] ?? null,
184
+ revert: {
185
+ op: "update_item",
186
+ pageSlug: slug,
187
+ blockId: after.id,
188
+ listKey,
189
+ ...(pair.itemId ? { itemId: pair.itemId } : { index: pair.afterIndex }),
190
+ patch: { [itemKey]: pair.before[itemKey] }
191
+ }
192
+ });
193
+ }
194
+ }
195
+ }
196
+ return out;
197
+ }
198
+ function itemIdOf(item) {
199
+ return isRecord(item) && typeof item.id === "string" && item.id ? item.id : undefined;
200
+ }
201
+ /**
202
+ * Entries that left a list without anyone removing them. Only reported when
203
+ * every entry carries an id — without ids a removal and a reorder cannot be
204
+ * told apart — and never for a list the batch explicitly removed from.
205
+ */
206
+ function droppedItems(slug, located, listKey, listLabel, beforeItems, afterItems, ops) {
207
+ const removedOnPurpose = ops.some((op) => op.op === "remove_item" && op.pageSlug === slug && op.blockId === located.blockId && op.listKey === listKey);
208
+ if (removedOnPurpose)
209
+ return [];
210
+ if (!beforeItems.every(itemIdOf) || !afterItems.every(itemIdOf))
211
+ return [];
212
+ const surviving = new Set(afterItems.map((item) => itemIdOf(item)));
213
+ const out = [];
214
+ beforeItems.forEach((item, index) => {
215
+ const itemId = itemIdOf(item);
216
+ if (surviving.has(itemId) || !hasContent(item))
217
+ return;
218
+ const anchors = beforeItems
219
+ .slice(0, index)
220
+ .reverse()
221
+ .map((earlier) => itemIdOf(earlier));
222
+ const anchor = anchors.find((id) => surviving.has(id));
223
+ out.push({
224
+ kind: "item_dropped",
225
+ ...located,
226
+ path: `${listKey}[${index}]`,
227
+ ...(listLabel ? { label: listLabel } : {}),
228
+ listKey,
229
+ itemId,
230
+ before: item,
231
+ after: null,
232
+ anchors,
233
+ revert: {
234
+ op: "add_item",
235
+ pageSlug: located.slug,
236
+ blockId: located.blockId,
237
+ listKey,
238
+ item: structuredClone(item),
239
+ ...(anchor ? { afterItemId: anchor } : {})
240
+ }
241
+ });
242
+ });
243
+ return out;
244
+ }
245
+ const PAGE_META_FIELDS = ["title", "description", "ogImage"];
246
+ /**
247
+ * Everything an edit did to one page that nobody asked for.
248
+ *
249
+ * `ops` is the batch that produced `after`: a block removed by a
250
+ * `remove_block` naming it was asked for and is not reported. A page created
251
+ * or deleted by the batch is not this agent's business — `before` or `after`
252
+ * is null and the answer is empty; deleting a page already goes through the
253
+ * destructive-action gate.
254
+ */
255
+ export function detectEditDamage(args) {
256
+ const { slug, before, after, ops, manifest } = args;
257
+ if (!before || !after)
258
+ return [];
259
+ const out = [];
260
+ const beforeMeta = isRecord(before.meta) ? before.meta : {};
261
+ const afterMeta = isRecord(after.meta) ? after.meta : {};
262
+ for (const key of PAGE_META_FIELDS) {
263
+ if (!hasContent(beforeMeta[key]) || hasContent(afterMeta[key]))
264
+ continue;
265
+ out.push({
266
+ kind: "emptied",
267
+ slug,
268
+ path: `meta.${key}`,
269
+ before: beforeMeta[key],
270
+ after: afterMeta[key] ?? null,
271
+ revert: { op: "update_page_meta", pageSlug: after.slug, patch: { [key]: beforeMeta[key] } }
272
+ });
273
+ }
274
+ const removedOnPurpose = new Set(ops.flatMap((op) => (op.op === "remove_block" && op.pageSlug === slug ? [op.blockId] : [])));
275
+ const afterById = new Map(after.blocks.map((block) => [block.id, block]));
276
+ before.blocks.forEach((block, index) => {
277
+ const survivor = afterById.get(block.id);
278
+ if (survivor) {
279
+ out.push(...blockIssues(after.slug, block, survivor, manifest, ops));
280
+ return;
281
+ }
282
+ if (removedOnPurpose.has(block.id))
283
+ return;
284
+ // Put it back after the nearest earlier block that is still on the page,
285
+ // so it returns to where it was rather than to the end.
286
+ const anchors = before.blocks
287
+ .slice(0, index)
288
+ .reverse()
289
+ .map((candidate) => candidate.id);
290
+ const anchor = anchors.find((id) => afterById.has(id));
291
+ const blockLabel = labelFor(block, definitionFor(manifest, block.type));
292
+ out.push({
293
+ kind: "block_dropped",
294
+ slug,
295
+ blockId: block.id,
296
+ blockType: block.type,
297
+ ...(blockLabel ? { blockLabel } : {}),
298
+ before: block,
299
+ after: null,
300
+ anchors,
301
+ revert: {
302
+ op: "add_block",
303
+ pageSlug: after.slug,
304
+ ...(anchor ? { afterBlockId: anchor } : {}),
305
+ block: structuredClone(block)
306
+ }
307
+ });
308
+ });
309
+ return out;
310
+ }
311
+ // ---------------------------------------------------------------------------
312
+ // Reading a value back — for the stale check before a revert
313
+ // ---------------------------------------------------------------------------
314
+ const PATH_SEGMENT = /([^.[\]]+)|\[(\d+)\]/g;
315
+ /** The value at an editable-target path inside `root`, or undefined. */
316
+ export function valueAtPath(root, path) {
317
+ let current = root;
318
+ for (const match of path.matchAll(PATH_SEGMENT)) {
319
+ if (current == null)
320
+ return undefined;
321
+ if (match[2] !== undefined) {
322
+ if (!Array.isArray(current))
323
+ return undefined;
324
+ current = current[Number(match[2])];
325
+ }
326
+ else {
327
+ if (!isRecord(current))
328
+ return undefined;
329
+ current = current[match[1]];
330
+ }
331
+ }
332
+ return current;
333
+ }
334
+ /**
335
+ * What the issue's location holds on `page` now, in the same form `after` was
336
+ * recorded in. For a dropped block, the block if it is back; otherwise null.
337
+ */
338
+ export function currentValueFor(issue, page) {
339
+ if (!page)
340
+ return undefined;
341
+ if (issue.kind === "block_dropped")
342
+ return page.blocks.find((b) => b.id === issue.blockId) ?? null;
343
+ if (!issue.path)
344
+ return undefined;
345
+ if (!issue.blockId)
346
+ return valueAtPath(page, issue.path) ?? null;
347
+ const block = page.blocks.find((b) => b.id === issue.blockId);
348
+ if (!block)
349
+ return undefined;
350
+ if (issue.kind === "item_dropped") {
351
+ const list = isRecord(block.props) ? block.props[issue.listKey ?? ""] : undefined;
352
+ if (!Array.isArray(list))
353
+ return undefined;
354
+ return list.find((item) => itemIdOf(item) === issue.itemId) ?? null;
355
+ }
356
+ return valueAtPath(block.props, issue.path) ?? null;
357
+ }
358
+ function stable(value) {
359
+ return JSON.stringify(value ?? null);
360
+ }
361
+ /**
362
+ * True while the location still holds exactly what the edit left there — the
363
+ * only state in which putting the old value back restores the edit's damage
364
+ * and nothing else.
365
+ */
366
+ export function isStillAsEditLeftIt(issue, page) {
367
+ const current = currentValueFor(issue, page);
368
+ if (current === undefined)
369
+ return false;
370
+ if (issue.kind === "block_dropped" || issue.kind === "item_dropped")
371
+ return current === null;
372
+ return stable(current) === stable(issue.after);
373
+ }
374
+ export function countIssues(issues) {
375
+ return {
376
+ emptied: issues.filter((i) => i.kind === "emptied").length,
377
+ blocksDropped: issues.filter((i) => i.kind === "block_dropped").length,
378
+ itemsDropped: issues.filter((i) => i.kind === "item_dropped").length,
379
+ typeChanged: issues.filter((i) => i.kind === "type_changed").length
380
+ };
381
+ }
382
+ /** One English line for logs, the Inbox row and anything outside the editor. */
383
+ export function describeIssues(issues) {
384
+ const counts = countIssues(issues);
385
+ const parts = [];
386
+ if (counts.emptied)
387
+ parts.push(`emptied ${counts.emptied} ${counts.emptied === 1 ? "field" : "fields"}`);
388
+ if (counts.blocksDropped) {
389
+ parts.push(`dropped ${counts.blocksDropped} ${counts.blocksDropped === 1 ? "block" : "blocks"}`);
390
+ }
391
+ if (counts.itemsDropped) {
392
+ parts.push(`dropped ${counts.itemsDropped} list ${counts.itemsDropped === 1 ? "entry" : "entries"}`);
393
+ }
394
+ if (counts.typeChanged) {
395
+ parts.push(`changed the shape of ${counts.typeChanged} ${counts.typeChanged === 1 ? "field" : "fields"}`);
396
+ }
397
+ if (parts.length <= 1)
398
+ return parts[0] ?? "";
399
+ return `${parts.slice(0, -1).join(", ")} and ${parts[parts.length - 1]}`;
400
+ }
401
+ /**
402
+ * The revert op for `issue`, re-anchored against `page` as it is now. For a
403
+ * dropped block or entry, the anchor is the nearest earlier neighbour that is
404
+ * on the page at this moment; every other revert is returned unchanged.
405
+ */
406
+ export function revertFor(issue, page) {
407
+ const op = issue.revert;
408
+ if (!op || !issue.anchors || !page)
409
+ return op;
410
+ if (op.op === "add_block") {
411
+ const present = new Set(page.blocks.map((block) => block.id));
412
+ const anchor = issue.anchors.find((id) => present.has(id));
413
+ const { afterBlockId: _previous, ...rest } = op;
414
+ return anchor ? { ...rest, afterBlockId: anchor } : rest;
415
+ }
416
+ if (op.op === "add_item") {
417
+ const block = page.blocks.find((b) => b.id === op.blockId);
418
+ const list = block && isRecord(block.props) ? block.props[op.listKey] : undefined;
419
+ const present = new Set(Array.isArray(list) ? list.map(itemIdOf).filter(Boolean) : []);
420
+ const anchor = issue.anchors.find((id) => present.has(id));
421
+ const { afterItemId: _previous, ...rest } = op;
422
+ return anchor ? { ...rest, afterItemId: anchor } : rest;
423
+ }
424
+ return op;
425
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Site ops agents — Edit safety, the Inbox, scheduled and custom agents, site
3
+ * facts, @mentions — are pre-alpha, and off unless `SITE_OPS_AGENTS=1`.
4
+ *
5
+ * Off has to mean off on the server, not just hidden tabs: Edit safety alerts
6
+ * hold publishing (409) until someone resolves them in the Inbox, so an editor
7
+ * that hides the Inbox while the server still raises alerts leaves publish
8
+ * stuck with no screen that can unstick it. Each entry point checks this:
9
+ * Edit safety raises nothing, nothing holds publish, the planner is given no
10
+ * site facts, a tick runs nothing, and an @handle goes to the planner. The
11
+ * editor reads it from `/status/planner` (`features.siteOpsAgents`) and hides
12
+ * Inbox, Agents and @-agent routing.
13
+ *
14
+ * Read per call, like every flag here, so a test or a library-mode host can set
15
+ * it after import. The hermetic test runner switches it on; the tests that pin
16
+ * the default delete it.
17
+ */
18
+ export declare function siteOpsAgentsEnabled(): boolean;
@@ -0,0 +1,21 @@
1
+ import { envFlag } from "../env-flags.js";
2
+ /**
3
+ * Site ops agents — Edit safety, the Inbox, scheduled and custom agents, site
4
+ * facts, @mentions — are pre-alpha, and off unless `SITE_OPS_AGENTS=1`.
5
+ *
6
+ * Off has to mean off on the server, not just hidden tabs: Edit safety alerts
7
+ * hold publishing (409) until someone resolves them in the Inbox, so an editor
8
+ * that hides the Inbox while the server still raises alerts leaves publish
9
+ * stuck with no screen that can unstick it. Each entry point checks this:
10
+ * Edit safety raises nothing, nothing holds publish, the planner is given no
11
+ * site facts, a tick runs nothing, and an @handle goes to the planner. The
12
+ * editor reads it from `/status/planner` (`features.siteOpsAgents`) and hides
13
+ * Inbox, Agents and @-agent routing.
14
+ *
15
+ * Read per call, like every flag here, so a test or a library-mode host can set
16
+ * it after import. The hermetic test runner switches it on; the tests that pin
17
+ * the default delete it.
18
+ */
19
+ export function siteOpsAgentsEnabled() {
20
+ return envFlag("SITE_OPS_AGENTS", false);
21
+ }
@@ -0,0 +1,38 @@
1
+ import { type Operation, type PageDoc } from "@avocadostudio-ai/shared";
2
+ import type { FindingFix, FindingRecord } from "../durable/types.ts";
3
+ import type { DraftClient } from "./draft-spec.ts";
4
+ export type FixField = FindingFix["field"];
5
+ /** The rules a fix can be written for, and which field each one fixes. */
6
+ export declare const FIXABLE_RULES: Record<string, FixField>;
7
+ export declare function fixFieldFor(ruleId: string): FixField | undefined;
8
+ export type FixDeps = {
9
+ /** Text model for titles and descriptions. */
10
+ client?: DraftClient;
11
+ model?: string;
12
+ /** Vision alt text; defaults to the editor's own generator. */
13
+ altText?: (imageUrl: string) => Promise<string | null>;
14
+ /** Site facts, one per line: words to avoid, product names, tone. */
15
+ facts?: string[];
16
+ };
17
+ export type WriteFixResult = {
18
+ ok: true;
19
+ fix: FindingFix;
20
+ } | {
21
+ ok: false;
22
+ error: string;
23
+ };
24
+ /** The value a fix for this finding replaces, read off the page as it is now. */
25
+ export declare function currentValue(finding: Pick<FindingRecord, "ruleId" | "evidence">, page: PageDoc): string | undefined;
26
+ /** The ops that put `text` in place — the same ops whether the text was written or edited. */
27
+ export declare function fixOps(finding: Pick<FindingRecord, "ruleId" | "slug" | "evidence">, text: string): Operation[];
28
+ /** What the page says, compactly: headings and the first sentences of its text, for the model to summarise. */
29
+ export declare function pageDigest(page: PageDoc, maxChars?: number): string;
30
+ /**
31
+ * Write a fix for one finding against the page as it is. A title or
32
+ * description outside its length is retried once with the count; a second
33
+ * miss is refused rather than stored, because the fix would reopen the very
34
+ * finding it was written for.
35
+ */
36
+ export declare function writeFix(finding: FindingRecord, page: PageDoc, deps?: FixDeps): Promise<WriteFixResult>;
37
+ /** "German" for "de": the model is told a language, not a code. */
38
+ export declare function languageName(code: string): string;