@sigrun/sigrun 0.4.2 → 0.5.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/CHANGELOG.md +37 -1
- package/README.md +46 -23
- package/dist/commands/expand.js +37 -5
- package/dist/curation/attribution.js +135 -0
- package/dist/curation/deletion.js +32 -0
- package/dist/curation/gate.js +50 -0
- package/dist/curation/host.js +342 -0
- package/dist/curation/internal.js +54 -0
- package/dist/curation/names.js +5 -0
- package/dist/curation/overlays.js +325 -0
- package/dist/curation/paths.js +17 -0
- package/dist/curation/run-doc.js +64 -0
- package/dist/curation/runs.js +579 -0
- package/dist/curation/scan.js +30 -0
- package/dist/curation/skill-tool.js +308 -0
- package/dist/curation/store.js +444 -0
- package/dist/curation/targets.js +77 -0
- package/dist/curation/usage.js +124 -0
- package/dist/curation/wrap.js +212 -0
- package/dist/extension.d.ts +282 -0
- package/dist/harness/boot.js +209 -9
- package/dist/harness/builtins.js +12 -9
- package/dist/harness/event-bus.js +64 -0
- package/dist/harness/loader.js +114 -1
- package/dist/harness/naming.js +6 -0
- package/dist/harness/packages.js +14 -9
- package/dist/harness/reserved-sections.js +2 -0
- package/dist/harness/seam-host.js +139 -26
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/jobs/fire.js +33 -11
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/sandbox/policy.js +14 -0
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +21 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +6 -1
- package/dist/server/notifications.js +6 -0
- package/dist/server/routes/meta.js +8 -3
- package/dist/server/routes/submissions.js +5 -0
- package/dist/server/routes/surfaces.js +3 -1
- package/dist/server/routes/users.js +7 -0
- package/dist/server/schema-fixture.js +168 -0
- package/dist/server/session-meta.js +12 -10
- package/dist/settings/core-sections.js +9 -0
- package/dist/settings/registry.js +6 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +3 -1
- package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
- package/node_modules/@sigrun/shared/dist/access.js +8 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +9 -7
- package/webui/dist/assets/{index-DWmsAQl2.js → index-BtczbUXo.js} +1 -1
- package/webui/dist/index.html +1 -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
|
+
}
|