@sigrun/sigrun 0.4.2 → 0.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (68) hide show
  1. package/CHANGELOG.md +92 -1
  2. package/README.md +63 -33
  3. package/dist/commands/expand.js +37 -5
  4. package/dist/curation/attribution.js +135 -0
  5. package/dist/curation/deletion.js +32 -0
  6. package/dist/curation/gate.js +50 -0
  7. package/dist/curation/host.js +342 -0
  8. package/dist/curation/internal.js +54 -0
  9. package/dist/curation/names.js +5 -0
  10. package/dist/curation/overlays.js +325 -0
  11. package/dist/curation/paths.js +17 -0
  12. package/dist/curation/run-doc.js +64 -0
  13. package/dist/curation/runs.js +579 -0
  14. package/dist/curation/scan.js +30 -0
  15. package/dist/curation/skill-tool.js +308 -0
  16. package/dist/curation/store.js +444 -0
  17. package/dist/curation/targets.js +77 -0
  18. package/dist/curation/usage.js +124 -0
  19. package/dist/curation/wrap.js +212 -0
  20. package/dist/extension.d.ts +336 -1
  21. package/dist/harness/boot.js +236 -13
  22. package/dist/harness/builtins.js +19 -12
  23. package/dist/harness/event-bus.js +64 -0
  24. package/dist/harness/identity/primary-admin.js +19 -0
  25. package/dist/harness/loader.js +114 -1
  26. package/dist/harness/naming.js +6 -0
  27. package/dist/harness/packages.js +14 -9
  28. package/dist/harness/reserved-sections.js +4 -0
  29. package/dist/harness/seam-host.js +154 -26
  30. package/dist/harness/sections/context-files.js +6 -3
  31. package/dist/harness/sections/job-state.js +24 -0
  32. package/dist/harness/settings.js +2 -2
  33. package/dist/harness/tools.js +70 -0
  34. package/dist/harness/workspace.js +30 -3
  35. package/dist/jobs/fire.js +37 -14
  36. package/dist/jobs/index.js +34 -7
  37. package/dist/jobs/reconcile.js +9 -0
  38. package/dist/jobs/skills-refs.js +40 -0
  39. package/dist/jobs/state-dir.js +16 -0
  40. package/dist/jobs/transfer.js +14 -0
  41. package/dist/sandbox/policy.js +27 -4
  42. package/dist/serve.js +5 -3
  43. package/dist/server/blocks/answers.js +9 -3
  44. package/dist/server/conversation-ops.js +34 -3
  45. package/dist/server/helpers.js +1 -1
  46. package/dist/server/host-bridge.js +31 -1
  47. package/dist/server/notifications.js +108 -16
  48. package/dist/server/routes/jobs.js +31 -0
  49. package/dist/server/routes/meta.js +8 -3
  50. package/dist/server/routes/submissions.js +5 -0
  51. package/dist/server/routes/surfaces.js +3 -1
  52. package/dist/server/routes/users.js +7 -0
  53. package/dist/server/schema-fixture.js +168 -0
  54. package/dist/server/session-meta.js +12 -10
  55. package/dist/settings/core-sections.js +18 -0
  56. package/dist/settings/registry.js +12 -0
  57. package/dist/skills/index.js +88 -19
  58. package/node_modules/@sigrun/shared/CHANGELOG.md +6 -1
  59. package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
  60. package/node_modules/@sigrun/shared/dist/access.js +8 -0
  61. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
  62. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
  63. package/node_modules/@sigrun/shared/package.json +1 -1
  64. package/package.json +9 -7
  65. package/webui/dist/assets/index-BR0di0OG.css +1 -0
  66. package/webui/dist/assets/{index-DWmsAQl2.js → index-BzrA1T4g.js} +5 -5
  67. package/webui/dist/index.html +2 -2
  68. package/webui/dist/assets/index-h7zDY9vD.css +0 -1
@@ -0,0 +1,308 @@
1
+ /**
2
+ * The `skill_manage` tool (curator P3, spec §6.3): the model-facing
3
+ * surface over the per-user {@link SkillStore} and {@link OverlayStore}.
4
+ *
5
+ * Read actions — `list`/`view` — work for the conversation owner
6
+ * ANYWHERE (an ordinary owned conversation can inspect its own user's
7
+ * learned skills; an ownerless one is answered with "there is no
8
+ * owner").
9
+ *
10
+ * Mutating actions — `create`/`patch`/`write_file`/`remove_file`/
11
+ * `archive`/`restore`/`overlay` — execute ONLY inside a curator run
12
+ * (`CurationRunDoc` present): the store user is the run doc's `user`,
13
+ * NEVER the calling principal (a run reviewing a bob-shared parent on
14
+ * alice's behalf writes alice's store). Outside a run the call is
15
+ * refused here (A8's wrapper will intercept first later — this is the
16
+ * backstop the brief pins).
17
+ *
18
+ * Read-before-write: `patch`/`write_file`/`remove_file`/`archive`/
19
+ * `overlay` on an EXISTING item require its name in the run doc's
20
+ * `viewed` ledger (fed by `view` through `markViewed`) — the curator
21
+ * never edits a skill it has not seen. A `create` on a name that does
22
+ * not exist yet needs no prior view.
23
+ *
24
+ * Every mutating result carries `details.curation = {changeId, summary}`
25
+ * — the change id for the P8 undo surface and a one-line summary for
26
+ * the UI.
27
+ */
28
+ import { Type } from "@earendil-works/pi-ai";
29
+ import { defineTool } from "@earendil-works/pi-durable";
30
+ import { OVERLAY_HEADING, withOverlay, } from "./overlays.js";
31
+ import { curationRun, markViewed } from "./run-doc.js";
32
+ import { SessionMetaDoc } from "../server/session-meta.js";
33
+ /** The tool's parameter union (the brief's exact shape). */
34
+ const skillManageParameters = Type.Object({
35
+ action: Type.Union([
36
+ "list",
37
+ "view",
38
+ "create",
39
+ "patch",
40
+ "write_file",
41
+ "remove_file",
42
+ "archive",
43
+ "restore",
44
+ "overlay",
45
+ ].map((action) => Type.Literal(action)), { description: "The action to perform." }),
46
+ name: Type.Optional(Type.String({ description: "The skill name." })),
47
+ file_path: Type.Optional(Type.String({ description: "A support file path (patch/write_file/remove_file)." })),
48
+ content: Type.Optional(Type.String({ description: "The full content (create) or file body (write_file)." })),
49
+ old_text: Type.Optional(Type.String({ description: "The unique span to replace (patch)." })),
50
+ new_text: Type.Optional(Type.String({ description: "The replacement span (patch)." })),
51
+ absorbed_into: Type.Optional(Type.String({ description: "The skill this one merges into (archive)." })),
52
+ reason: Type.Optional(Type.String({ description: "Why the change is made (the feed entry's reason)." })),
53
+ overlay_root: Type.Optional(Type.Union(["workspace", "user dir", "package"].map((root) => Type.Literal(root)), { description: "The upstream root an overlay attaches to." })),
54
+ remove: Type.Optional(Type.Boolean({ description: "overlay: remove the stored overlay instead of writing it." })),
55
+ });
56
+ /** The mutating actions (the read actions are `list` and `view`). */
57
+ const MUTATING = new Set([
58
+ "create",
59
+ "patch",
60
+ "write_file",
61
+ "remove_file",
62
+ "archive",
63
+ "restore",
64
+ "overlay",
65
+ ]);
66
+ /** The read-before-write actions: an EXISTING item must have been viewed. */
67
+ const READ_BEFORE_WRITE = new Set([
68
+ "patch",
69
+ "write_file",
70
+ "remove_file",
71
+ "archive",
72
+ "overlay",
73
+ ]);
74
+ /** An explicit tool error: text content the model can read and recover from. */
75
+ function error(message) {
76
+ return { output: [{ type: "text", text: message }], isError: true };
77
+ }
78
+ /** The resolved conversation owner through the api's document reader. */
79
+ async function ownerOf(api, ctx) {
80
+ try {
81
+ const meta = await api.snapshot(SessionMetaDoc, api.conversationId, ctx);
82
+ return meta?.owner;
83
+ }
84
+ catch {
85
+ return undefined;
86
+ }
87
+ }
88
+ /**
89
+ * The `view` fallback for upstream-only names (the overlay lifecycle):
90
+ * a name with no learned copy can still carry a stored overlay and/or a
91
+ * live upstream — the exact shape an overlay update/remove decision
92
+ * needs. Serves the upstream body with the overlay under
93
+ * {@link OVERLAY_HEADING} (the served shape `load_skill` uses), or the
94
+ * overlay alone when the upstream is currently absent (an orphan).
95
+ * Returns an error message naming the skill when neither exists.
96
+ */
97
+ async function viewUpstreamOnly(deps, stores, args, env, ctx) {
98
+ // A stored overlay for this skill name: the exact root when the call
99
+ // names one, else whichever root holds one (that key decides).
100
+ const stored = args.overlay_root !== undefined
101
+ ? await stores.overlays
102
+ .get({ root: args.overlay_root, skill: args.name })
103
+ .catch(() => undefined)
104
+ : (await stores.overlays.list().catch(() => [])).find((overlay) => overlay.key.skill === args.name);
105
+ const key = stored?.key ?? {
106
+ root: args.overlay_root ?? "user dir",
107
+ skill: args.name,
108
+ ...(args.overlay_root === "workspace"
109
+ ? { workspace: deps.workspaceOf(env) ?? "" }
110
+ : {}),
111
+ };
112
+ const upstream = await deps.upstreamOf(key, env, ctx).catch(() => undefined);
113
+ if (stored === undefined && upstream === undefined) {
114
+ return `unknown skill "${args.name}" — no learned copy, no stored overlay, and no upstream resolved in the ${key.root} root.`;
115
+ }
116
+ const notes = [
117
+ `(no learned copy of "${args.name}"; showing the ${key.root} upstream${stored === undefined ? "" : " with the stored overlay"})`,
118
+ ];
119
+ if (stored?.orphanedSince !== undefined) {
120
+ notes.push("(the upstream is currently ABSENT — the overlay is orphaned)");
121
+ }
122
+ let body;
123
+ if (upstream === undefined) {
124
+ body = `${OVERLAY_HEADING}\n\n${stored?.text ?? ""}`;
125
+ }
126
+ else if (stored === undefined) {
127
+ body = upstream.body;
128
+ }
129
+ else {
130
+ body = withOverlay(upstream.body, stored.text);
131
+ }
132
+ return `${notes.join(" ")}\n\n${body}`;
133
+ }
134
+ /** Render the store's `list` result as the model-facing text. */
135
+ function renderList(skills) {
136
+ if (skills.length === 0)
137
+ return "No learned skills.";
138
+ return skills
139
+ .map((skill) => `- ${skill.name}: ${skill.description}${skill.archived ? " (archived)" : ""}${skill.pinned ? " (pinned)" : ""}`)
140
+ .join("\n");
141
+ }
142
+ /**
143
+ * Build the `skill_manage` tool. Installed by the skills extension
144
+ * (`src/skills/index.ts`) once its options carry the curation wiring —
145
+ * ordinary conversations get the read actions, curator runs get
146
+ * everything.
147
+ */
148
+ export function buildSkillManageTool(deps) {
149
+ return defineTool({
150
+ name: "skill_manage",
151
+ description: "Inspect and manage the user's learned skills. list/view work for the conversation owner anywhere; every other action runs only inside a curator run.",
152
+ parameters: skillManageParameters,
153
+ async execute(args, api, context) {
154
+ const owner = await ownerOf(api, context);
155
+ // Ownerless conversations: nothing to read (and no user to write
156
+ // for — the mutating refusal below covers those too, but the read
157
+ // answer is the one the brief pins).
158
+ if (args.action === "list" || args.action === "view") {
159
+ if (owner === undefined) {
160
+ return error("This conversation has no owner, so there are no learned skills to inspect.");
161
+ }
162
+ const stores = deps.storesOf(owner);
163
+ if (stores === undefined) {
164
+ return error("No skill store is configured for this owner.");
165
+ }
166
+ if (args.action === "list") {
167
+ api.output(renderList(await stores.store.list()));
168
+ return {};
169
+ }
170
+ if (args.name === undefined)
171
+ return error("view requires a skill name.");
172
+ const learned = await stores.store
173
+ .view(args.name, args.file_path ?? "SKILL.md")
174
+ .then((body) => body, () => undefined);
175
+ if (learned === undefined) {
176
+ // Upstream-only names (the overlay lifecycle): serve the stored
177
+ // overlay and/or the live upstream — without this fallback the
178
+ // learned-root ENOENT traps an overlay update/remove in an
179
+ // unrecoverable "view X first" → "unknown skill" loop.
180
+ const served = await viewUpstreamOnly(deps, stores, { name: args.name, overlay_root: args.overlay_root }, api.env, context);
181
+ if (served.startsWith("unknown skill"))
182
+ return error(served);
183
+ api.output(served);
184
+ }
185
+ else {
186
+ api.output(learned);
187
+ }
188
+ // A view inside a curator run feeds the read-before-write ledger.
189
+ const run = await curationRun(api, api.conversationId, context);
190
+ if (run !== undefined)
191
+ await markViewed(api, args.name, context);
192
+ return {};
193
+ }
194
+ if (!MUTATING.has(args.action)) {
195
+ return error(`Unknown action: ${args.action}`);
196
+ }
197
+ // Mutating actions run only inside a curator run; the store user
198
+ // is the run doc's user, never the calling principal.
199
+ const run = await curationRun(api, api.conversationId, context);
200
+ if (run === undefined || run.user === "") {
201
+ return error("Mutating skill changes run only inside a curator run; this conversation carries no run.");
202
+ }
203
+ const stores = deps.storesOf(run.user);
204
+ if (stores === undefined) {
205
+ return error(`No skill store is configured for ${run.user}.`);
206
+ }
207
+ if (args.name === undefined)
208
+ return error(`${args.action} requires a skill name.`);
209
+ // The overlay key: the workspace root path (A9) for a workspace
210
+ // overlay, exactly as serving keys it.
211
+ const overlayKey = args.action === "overlay"
212
+ ? {
213
+ root: args.overlay_root ?? "user dir",
214
+ skill: args.name,
215
+ ...(args.overlay_root === "workspace"
216
+ ? { workspace: deps.workspaceOf(api.env) ?? "" }
217
+ : {}),
218
+ }
219
+ : undefined;
220
+ const meta = {
221
+ actor: "curator",
222
+ ...(run.reviewId === undefined ? {} : { reviewId: run.reviewId }),
223
+ conversationId: api.conversationId,
224
+ ...(args.reason === undefined ? {} : { reason: args.reason }),
225
+ };
226
+ // Read-before-write: an existing item must carry its name in the
227
+ // run doc's viewed ledger. A skill that does not exist yet (a
228
+ // create, or a first overlay) needs no prior view.
229
+ const exists = args.action === "overlay"
230
+ ? await stores.overlays
231
+ .get(overlayKey)
232
+ .then((overlay) => overlay !== undefined)
233
+ .catch(() => false)
234
+ : (await stores.store.list().catch(() => []))
235
+ .some((skill) => skill.name === args.name);
236
+ if (exists && READ_BEFORE_WRITE.has(args.action) && !run.viewed.includes(args.name)) {
237
+ return error(`Read-before-write: view "${args.name}" before changing it in this run.`);
238
+ }
239
+ try {
240
+ let entry;
241
+ switch (args.action) {
242
+ case "create":
243
+ entry = await stores.store.apply(args.name, { kind: "create", content: args.content ?? "" }, meta);
244
+ break;
245
+ case "patch":
246
+ entry = await stores.store.apply(args.name, {
247
+ kind: "patch",
248
+ old: args.old_text ?? "",
249
+ new: args.new_text ?? "",
250
+ ...(args.file_path === undefined ? {} : { file: args.file_path }),
251
+ }, meta);
252
+ break;
253
+ case "write_file":
254
+ entry = await stores.store.apply(args.name, { kind: "write_file", file: args.file_path ?? "", content: args.content ?? "" }, meta);
255
+ break;
256
+ case "remove_file":
257
+ entry = await stores.store.apply(args.name, { kind: "remove_file", file: args.file_path ?? "" }, meta);
258
+ break;
259
+ case "archive":
260
+ entry = await stores.store.apply(args.name, {
261
+ kind: "archive",
262
+ ...(args.absorbed_into === undefined ? {} : { absorbedInto: args.absorbed_into }),
263
+ }, meta);
264
+ break;
265
+ case "restore":
266
+ entry = await stores.store.apply(args.name, { kind: "restore" }, meta);
267
+ break;
268
+ case "overlay": {
269
+ const key = overlayKey;
270
+ if (args.remove === true) {
271
+ entry = await stores.overlays.remove(key, meta);
272
+ break;
273
+ }
274
+ // The upstream hash pins what the overlay was written
275
+ // against: the CURRENT upstream body resolved through the
276
+ // A5 machinery (the workspace/user-dir/package roots). The
277
+ // learned root is by construction ABSENT for an overlay
278
+ // target — hashing it stored `""` and the first drift
279
+ // pass flagged every tool-written overlay.
280
+ const upstream = await deps.upstreamOf(key, api.env, context);
281
+ if (upstream === undefined) {
282
+ // A vanished upstream is a user-visible problem, not a ""
283
+ // hash: refuse the create, naming the missing skill.
284
+ return error(`no upstream skill "${args.name}" resolved in the ${key.root} root — an overlay attaches to a skill the user does not own. Restore the upstream (or pass its real overlay_root) before writing an overlay for it.`);
285
+ }
286
+ entry = await stores.overlays.put(key, args.content ?? "", upstream.hash, meta);
287
+ break;
288
+ }
289
+ }
290
+ if (entry === undefined) {
291
+ return error(`Unknown action: ${args.action}`);
292
+ }
293
+ api.output(`${args.action} ${args.name}: recorded as ${entry.id}`);
294
+ return {
295
+ details: {
296
+ curation: {
297
+ changeId: entry.id,
298
+ summary: `${args.action} ${args.name}${args.reason === undefined ? "" : ` — ${args.reason}`}`,
299
+ },
300
+ },
301
+ };
302
+ }
303
+ catch (cause) {
304
+ return error(cause instanceof Error ? cause.message : String(cause));
305
+ }
306
+ },
307
+ });
308
+ }