@sigrun/sigrun 0.8.0 → 0.10.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 (49) hide show
  1. package/CHANGELOG.md +55 -87
  2. package/dist/config.js +47 -0
  3. package/dist/curation/host.js +125 -21
  4. package/dist/curation/runs.js +15 -4
  5. package/dist/extension.d.ts +268 -3
  6. package/dist/harness/boot.js +113 -3
  7. package/dist/harness/complete.js +127 -0
  8. package/dist/harness/decision-tool.js +93 -0
  9. package/dist/harness/decisions.js +477 -0
  10. package/dist/harness/event-bus.js +12 -0
  11. package/dist/harness/interactions.js +81 -0
  12. package/dist/harness/interactive-tools.js +97 -0
  13. package/dist/harness/loader.js +243 -1
  14. package/dist/harness/packages.js +7 -0
  15. package/dist/harness/published-hooks.js +208 -0
  16. package/dist/harness/seam-host.js +136 -1
  17. package/dist/harness/settings.js +41 -0
  18. package/dist/history/git.js +6 -2
  19. package/dist/jobs/fire.js +10 -0
  20. package/dist/jobs/index.js +3 -0
  21. package/dist/jobs/observe.js +27 -2
  22. package/dist/jobs/spec.js +15 -1
  23. package/dist/server/conversation-ops.js +31 -0
  24. package/dist/server/core-views/jobs-form.js +335 -0
  25. package/dist/server/core-views/jobs.js +300 -15
  26. package/dist/server/core-views/skills-tree.js +3 -12
  27. package/dist/server/core-views/skills.js +11 -23
  28. package/dist/server/host-bridge.js +53 -1
  29. package/dist/server/index.js +2 -1
  30. package/dist/server/routes/extensions.js +5 -0
  31. package/dist/server/routes/interactions.js +48 -1
  32. package/dist/server/routes/meta.js +17 -0
  33. package/dist/server/routes/providers.js +65 -8
  34. package/dist/server/routes/submissions.js +3 -0
  35. package/dist/settings/registry.js +18 -0
  36. package/node_modules/@sigrun/shared/CHANGELOG.md +12 -0
  37. package/node_modules/@sigrun/shared/dist/decisions.d.ts +95 -0
  38. package/node_modules/@sigrun/shared/dist/decisions.js +16 -0
  39. package/node_modules/@sigrun/shared/dist/interactions.d.ts +14 -0
  40. package/node_modules/@sigrun/shared/dist/jobs.d.ts +4 -0
  41. package/node_modules/@sigrun/shared/dist/settings.d.ts +32 -0
  42. package/node_modules/@sigrun/shared/dist/views.d.ts +58 -0
  43. package/node_modules/@sigrun/shared/package.json +6 -2
  44. package/package.json +8 -8
  45. package/webui/dist/assets/index-By63QKOA.js +55 -0
  46. package/webui/dist/assets/index-D5KowS7k.css +1 -0
  47. package/webui/dist/index.html +2 -2
  48. package/webui/dist/assets/index-B3uMNF59.css +0 -1
  49. package/webui/dist/assets/index-BXsCyod4.js +0 -55
@@ -0,0 +1,335 @@
1
+ /**
2
+ * Jobs-editor Task 3: the pure form logic behind the jobs view's
3
+ * "New job" form — the values shape, the field list, the submit
4
+ * envelope read, and the spec assembly. No I/O here: `jobs.ts` owns
5
+ * the surface (the `{edit: true}` params branch, the `createJob`
6
+ * action) and this module owns the form's content.
7
+ *
8
+ * - `JobFormData.model`/`thinking` carry `"default"` (NOT `""`) — it
9
+ * is the choice field's option id AND the omission sentinel
10
+ * (`createSpecOf` drops the spec keys for it).
11
+ * - The schedule is wholly form-owned: `scheduleType` picks the shape
12
+ * (`{cron, timezone?}` / `{every}` / `{at}`) and `schedule` carries
13
+ * the single text value; `timezone` rides cron only.
14
+ * - New jobs are prompt-only with `conversation: "new"` (the binding
15
+ * ruling) — the kind param exists for the edit shape (Task 4), whose
16
+ * field list differs only in the action column.
17
+ */
18
+ import { NAME_PATTERN, validateJobSpec } from "../../jobs/spec.js";
19
+ /** The thinking choice's options ("default" labels as Default). */
20
+ export const THINKING_OPTIONS = [
21
+ "default",
22
+ "off",
23
+ "minimal",
24
+ "low",
25
+ "medium",
26
+ "high",
27
+ "xhigh",
28
+ "max",
29
+ ];
30
+ /** A new job's seeded values (bound at `/form`). */
31
+ export function emptyForm() {
32
+ return {
33
+ name: "",
34
+ description: "",
35
+ scheduleType: "cron",
36
+ schedule: "",
37
+ timezone: "",
38
+ prompt: "",
39
+ model: "default",
40
+ thinking: "default",
41
+ enabled: true,
42
+ };
43
+ }
44
+ /**
45
+ * An existing job's form values (the Task-4 edit seed; Task 3 creates
46
+ * only, but the shape ships here). Name stays editable on the field
47
+ * list; model/thinking absent ⇒ `"default"`.
48
+ */
49
+ export function formFromJob(job) {
50
+ const schedule = job.schedule;
51
+ const action = job.action;
52
+ return {
53
+ name: job.name,
54
+ description: job.description ?? "",
55
+ scheduleType: "cron" in schedule ? "cron" : "every" in schedule ? "every" : "at",
56
+ schedule: "cron" in schedule
57
+ ? schedule.cron
58
+ : "every" in schedule
59
+ ? schedule.every
60
+ : schedule.at,
61
+ timezone: "cron" in schedule ? (schedule.timezone ?? "") : "",
62
+ prompt: action.kind === "prompt" ? action.prompt : "",
63
+ model: action.kind === "prompt" && action.model !== undefined
64
+ ? action.model
65
+ : "default",
66
+ thinking: action.kind === "prompt" && action.thinking !== undefined
67
+ ? action.thinking
68
+ : "default",
69
+ enabled: job.enabled,
70
+ };
71
+ }
72
+ /** Whether `value` is a plain object. */
73
+ function isObject(value) {
74
+ return typeof value === "object" && value !== null && !Array.isArray(value);
75
+ }
76
+ function stringOf(value) {
77
+ return typeof value === "string" ? value : "";
78
+ }
79
+ /**
80
+ * The submitted form values: form dispatches wrap the fields as
81
+ * `{values: {...}}` (the webui FormBlock contract — the `fieldOf`
82
+ * convention in skills.ts); a plain object resolves too. An ABSENT
83
+ * `enabled` reads `true` (the toggle defaults on; a disabled/absent
84
+ * field never rides the envelope).
85
+ */
86
+ export function formValuesOf(payload) {
87
+ const source = isObject(payload) && isObject(payload.values) ? payload.values : payload;
88
+ const record = isObject(source) ? source : {};
89
+ const scheduleType = record.scheduleType;
90
+ return {
91
+ name: stringOf(record.name),
92
+ description: stringOf(record.description),
93
+ scheduleType: scheduleType === "every" || scheduleType === "at" ? scheduleType : "cron",
94
+ schedule: stringOf(record.schedule),
95
+ timezone: stringOf(record.timezone),
96
+ prompt: stringOf(record.prompt),
97
+ model: stringOf(record.model) || "default",
98
+ thinking: stringOf(record.thinking) || "default",
99
+ enabled: typeof record.enabled === "boolean" ? record.enabled : true,
100
+ };
101
+ }
102
+ /**
103
+ * Map a `validateJobSpec` field path onto the form's field (the
104
+ * field-scoped action error). Unknown paths are FORM-LEVEL (no field).
105
+ */
106
+ export function formFieldOf(path) {
107
+ switch (path) {
108
+ case "name":
109
+ return "name";
110
+ case "description":
111
+ return "description";
112
+ case "schedule":
113
+ case "schedule.cron":
114
+ case "schedule.every":
115
+ case "schedule.at":
116
+ return "schedule";
117
+ case "schedule.timezone":
118
+ return "timezone";
119
+ case "action.prompt":
120
+ return "prompt";
121
+ case "action.model":
122
+ return "model";
123
+ case "action.thinking":
124
+ return "thinking";
125
+ default:
126
+ return undefined;
127
+ }
128
+ }
129
+ /**
130
+ * The schedule assembly shared by create and edit-save (review fix:
131
+ * extracted so the rule can never drift between the two) — wholly
132
+ * form-owned: `scheduleType` picks the shape (`{cron, timezone?}` /
133
+ * `{every}` / `{at}`) and `schedule` carries the single text value;
134
+ * `timezone` rides cron only.
135
+ */
136
+ function scheduleOf(values) {
137
+ return values.scheduleType === "every"
138
+ ? { every: values.schedule.trim() }
139
+ : values.scheduleType === "at"
140
+ ? { at: values.schedule.trim() }
141
+ : values.timezone.trim() === ""
142
+ ? { cron: values.schedule.trim() }
143
+ : { cron: values.schedule.trim(), timezone: values.timezone.trim() };
144
+ }
145
+ /**
146
+ * Assemble the `JobSpec` for `createApi` from the form values, or
147
+ * reject with the field-scoped failure. New jobs are prompt-only with
148
+ * `conversation: "new"`; the `"default"` model/thinking omit the spec
149
+ * keys (the server default applies).
150
+ */
151
+ export function createSpecOf(values) {
152
+ const schedule = scheduleOf(values);
153
+ const spec = {
154
+ name: values.name,
155
+ ...(values.description.trim() === ""
156
+ ? {}
157
+ : { description: values.description.trim() }),
158
+ schedule,
159
+ action: {
160
+ kind: "prompt",
161
+ prompt: values.prompt,
162
+ ...(values.model === "default" ? {} : { model: values.model }),
163
+ ...(values.thinking === "default" ? {} : { thinking: values.thinking }),
164
+ conversation: "new",
165
+ },
166
+ enabled: values.enabled,
167
+ };
168
+ const validated = validateJobSpec(spec);
169
+ if (!validated.ok) {
170
+ return {
171
+ ok: false,
172
+ field: formFieldOf(validated.field) ?? validated.field,
173
+ message: validated.message,
174
+ };
175
+ }
176
+ return { ok: true, spec: validated.spec };
177
+ }
178
+ /**
179
+ * The edit-save patch (jobs-editor Task 4, spec §4 save rules).
180
+ *
181
+ * `updateApi` merges ONE level deep (`{...prior.spec, ...patch}`), so
182
+ * every key the patch carries must be the WHOLE desired value:
183
+ *
184
+ * - the schedule is wholly form-owned (assembled from `scheduleType`
185
+ * + `schedule` + `timezone`; switching type leaves no second
186
+ * discriminant — cron→every drops the timezone);
187
+ * - a PROMPT job's action is `{...prior.action, prompt}` with
188
+ * `model`/`thinking` set from the form or DELETED for `"default"`
189
+ * (never a fresh literal — `conversation`, `runSharing`, `deliver`
190
+ * carry over untouched);
191
+ * - a TASK job's patch carries NO `action` key at all (the editor
192
+ * hides prompt/model/thinking; task + input carry over);
193
+ * - an emptied description becomes an OWN KEY `description: undefined`
194
+ * after validation, so the spread overrides the prior value and the
195
+ * stored record + hash drop it.
196
+ *
197
+ * For field-scoped errors the patch is spread over the prior spec
198
+ * (rebuilt from the flattened wire row — owner is not part of a spec)
199
+ * and the MERGED object is validated with `validateJobSpec`.
200
+ */
201
+ export function savePatchOf(values, prior) {
202
+ const schedule = scheduleOf(values);
203
+ const patch = {
204
+ name: values.name,
205
+ schedule,
206
+ enabled: values.enabled,
207
+ };
208
+ if (prior.action.kind === "prompt") {
209
+ const action = { ...prior.action, prompt: values.prompt };
210
+ if (values.model === "default") {
211
+ delete action.model;
212
+ }
213
+ else {
214
+ action.model = values.model;
215
+ }
216
+ if (values.thinking === "default") {
217
+ delete action.thinking;
218
+ }
219
+ else {
220
+ action.thinking = values.thinking;
221
+ }
222
+ patch.action = action;
223
+ }
224
+ // Field-scoped validation on the MERGED spec (the wire is
225
+ // flattened; owner lives on the record, not the spec).
226
+ const priorSpec = {
227
+ name: prior.name,
228
+ ...(prior.description === undefined
229
+ ? {}
230
+ : { description: prior.description }),
231
+ schedule: prior.schedule,
232
+ action: prior.action,
233
+ ...(prior.sharing === undefined ? {} : { sharing: prior.sharing }),
234
+ enabled: prior.enabled,
235
+ };
236
+ const merged = {
237
+ ...priorSpec,
238
+ ...patch,
239
+ ...(values.description.trim() === ""
240
+ ? {}
241
+ : { description: values.description.trim() }),
242
+ };
243
+ const validated = validateJobSpec(merged);
244
+ if (!validated.ok) {
245
+ return {
246
+ ok: false,
247
+ field: formFieldOf(validated.field),
248
+ message: validated.message,
249
+ };
250
+ }
251
+ // Only AFTER validation does the cleared description land as the
252
+ // own key whose undefined value the spread + JSON round-trip drop.
253
+ if (values.description.trim() === "") {
254
+ patch.description = undefined;
255
+ }
256
+ else {
257
+ patch.description = values.description.trim();
258
+ }
259
+ return { ok: true, patch };
260
+ }
261
+ /**
262
+ * The form's field list (the same for new and edit; Name stays
263
+ * editable). For `kind: "task"` the prompt/model/thinking fields are
264
+ * omitted (a task job names a task id instead — Task 4's edit shape).
265
+ */
266
+ export function jobFormFields(kind) {
267
+ const fields = [
268
+ {
269
+ type: "text",
270
+ name: "name",
271
+ label: "Name",
272
+ required: true,
273
+ pattern: NAME_PATTERN.source,
274
+ help: "lowercase letters, digits, dot, dash, underscore",
275
+ },
276
+ {
277
+ type: "text",
278
+ name: "description",
279
+ label: "Description",
280
+ },
281
+ {
282
+ type: "choice",
283
+ name: "scheduleType",
284
+ label: "Schedule type",
285
+ options: [
286
+ { id: "cron", label: "Cron" },
287
+ { id: "every", label: "Every" },
288
+ { id: "at", label: "Once" },
289
+ ],
290
+ },
291
+ {
292
+ type: "text",
293
+ name: "schedule",
294
+ label: "Schedule",
295
+ required: true,
296
+ help: "Cron: 0 9 * * 1-5 · Every: 15m or 2h (60s minimum) · Once: an ISO date-time",
297
+ },
298
+ {
299
+ type: "text",
300
+ name: "timezone",
301
+ label: "Timezone",
302
+ help: "Cron only, e.g. America/Toronto; empty = server timezone",
303
+ },
304
+ ];
305
+ if (kind === "prompt") {
306
+ fields.push({
307
+ type: "longtext",
308
+ name: "prompt",
309
+ label: "Prompt",
310
+ required: true,
311
+ syntax: "markdown",
312
+ rows: 10,
313
+ }, {
314
+ type: "choice",
315
+ name: "model",
316
+ label: "Model",
317
+ options: { $: "/modelOptions" },
318
+ }, {
319
+ type: "choice",
320
+ name: "thinking",
321
+ label: "Thinking",
322
+ options: THINKING_OPTIONS.map((level) => ({
323
+ id: level,
324
+ label: level === "default" ? "Default" : level,
325
+ })),
326
+ });
327
+ }
328
+ fields.push({
329
+ type: "toggle",
330
+ name: "enabled",
331
+ label: "Enabled",
332
+ default: true,
333
+ });
334
+ return fields;
335
+ }