@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +37 -1
  2. package/README.md +46 -23
  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 +282 -0
  21. package/dist/harness/boot.js +209 -9
  22. package/dist/harness/builtins.js +12 -9
  23. package/dist/harness/event-bus.js +64 -0
  24. package/dist/harness/loader.js +114 -1
  25. package/dist/harness/naming.js +6 -0
  26. package/dist/harness/packages.js +14 -9
  27. package/dist/harness/reserved-sections.js +2 -0
  28. package/dist/harness/seam-host.js +139 -26
  29. package/dist/harness/settings.js +2 -2
  30. package/dist/harness/tools.js +70 -0
  31. package/dist/jobs/fire.js +33 -11
  32. package/dist/jobs/skills-refs.js +40 -0
  33. package/dist/sandbox/policy.js +14 -0
  34. package/dist/server/blocks/answers.js +9 -3
  35. package/dist/server/conversation-ops.js +21 -3
  36. package/dist/server/helpers.js +1 -1
  37. package/dist/server/host-bridge.js +6 -1
  38. package/dist/server/notifications.js +6 -0
  39. package/dist/server/routes/meta.js +8 -3
  40. package/dist/server/routes/submissions.js +5 -0
  41. package/dist/server/routes/surfaces.js +3 -1
  42. package/dist/server/routes/users.js +7 -0
  43. package/dist/server/schema-fixture.js +168 -0
  44. package/dist/server/session-meta.js +12 -10
  45. package/dist/settings/core-sections.js +9 -0
  46. package/dist/settings/registry.js +6 -0
  47. package/dist/skills/index.js +88 -19
  48. package/node_modules/@sigrun/shared/CHANGELOG.md +3 -1
  49. package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
  50. package/node_modules/@sigrun/shared/dist/access.js +8 -0
  51. package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
  52. package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
  53. package/node_modules/@sigrun/shared/package.json +1 -1
  54. package/package.json +9 -7
  55. package/webui/dist/assets/{index-DWmsAQl2.js → index-BtczbUXo.js} +1 -1
  56. package/webui/dist/index.html +1 -1
@@ -3,16 +3,20 @@
3
3
  * package.
4
4
  *
5
5
  * v1 discovered ONE root: `skills/<dir>/SKILL.md` in the conversation
6
- * workspace. W5 Task 4 (spec §9.4) grows discovery to THREE roots, with
7
- * first-wins precedence in this order:
6
+ * workspace. W5 Task 4 (spec §9.4) grew discovery to THREE roots, and
7
+ * curator A3 (P2) adds a fourth — the conversation OWNER's learned
8
+ * skills. First-wins precedence in this order:
8
9
  *
9
10
  * 1. the WORKSPACE root — `skills/` under the conversation workspace, read
10
11
  * through the conversation `ExecutionEnv` (the section via
11
12
  * `PromptInput.env`, the tool via `ToolExecutionApi.env`), so discovery
12
13
  * follows the resolved workspace;
13
- * 2. the USER root — `<configDir>/skills/` (a named config-root entry),
14
+ * 2. the LEARNED root — the owner's `<dataDir>/users/<owner>/skills/`,
15
+ * read from the host filesystem; only for OWNED conversations (an
16
+ * ownerless conversation has no learned root);
17
+ * 3. the USER root — `<configDir>/skills/` (a named config-root entry),
14
18
  * read from the host filesystem;
15
- * 3. PACKAGE roots — skill directories shipped by extension packages
19
+ * 4. PACKAGE roots — skill directories shipped by extension packages
16
20
  * (manifest `sigrun.skills`/`pi.skills` dirs and factory-result `skills`
17
21
  * dirs, collected by the loader in install order), read from the host
18
22
  * filesystem.
@@ -60,6 +64,21 @@ import { Type } from "@earendil-works/pi-ai";
60
64
  import { defineExtension, defineTool, section, } from "@earendil-works/pi-durable";
61
65
  import { readdirSync, readFileSync } from "node:fs";
62
66
  import { join } from "node:path";
67
+ import { overlayForSkill, resolveUpstream, withOverlay } from "../curation/overlays.js";
68
+ import { buildSkillManageTool } from "../curation/skill-tool.js";
69
+ import { SessionMetaDoc } from "../server/session-meta.js";
70
+ /**
71
+ * Resolve a conversation's owner through any `DocumentReader` (curator
72
+ * A3): the section's `PromptInput.read` and the tool's `ToolExecutionApi`
73
+ * both carry one. `undefined` for an ownerless conversation — and for a
74
+ * bare test double without the reader surface (no learned root then).
75
+ */
76
+ async function conversationOwner(read, conversationId, context) {
77
+ if (typeof read?.snapshot !== "function")
78
+ return undefined;
79
+ const meta = await read.snapshot(SessionMetaDoc, conversationId, context);
80
+ return meta?.owner;
81
+ }
63
82
  /** The section key the skills list renders under. */
64
83
  export const SKILLS_SECTION_KEY = "skills";
65
84
  /** Total rendered-section budget (spec §3.2: 4KB, notice included). */
@@ -68,8 +87,8 @@ const SKILLS_DIRECTORY = "skills";
68
87
  const SKILL_FILE = "SKILL.md";
69
88
  const FRONTMATTER_DELIMITER = "---";
70
89
  const SKILLS_SECTION_HEADER = "Workspace skills. Call the load_skill tool with a skill's name to read its full instructions; each entry shows the skill's path, so you can also read its SKILL.md directly.";
71
- /** The three discovery roots, in precedence order (§9.4). */
72
- const ROOT_PRECEDENCE = ["workspace", "user dir", "package"];
90
+ /** The discovery roots, in precedence order (§9.4 + curator P2). */
91
+ const ROOT_PRECEDENCE = ["workspace", "learned", "user dir", "package"];
73
92
  const encoder = new TextEncoder();
74
93
  /** UTF-8 byte length — the unit the section cap is measured in. */
75
94
  function byteLength(text) {
@@ -129,7 +148,7 @@ function parseSkillFile(text) {
129
148
  * rules with a log. `displayDir` is the path named in the logs and the
130
149
  * section entry; `fileLabel` names the file in skip logs.
131
150
  */
132
- function skillFromFile(text, displayDir, fileLabel) {
151
+ function skillFromFile(text, displayDir, fileLabel, root) {
133
152
  const parsed = parseSkillFile(text);
134
153
  if (parsed === undefined) {
135
154
  console.warn(`skills: skipping ${fileLabel} (the frontmatter block is required)`);
@@ -149,6 +168,7 @@ function skillFromFile(text, displayDir, fileLabel) {
149
168
  body: parsed.body,
150
169
  dir: displayDir,
151
170
  disableModelInvocation: parsed.disableModelInvocation,
171
+ root,
152
172
  };
153
173
  }
154
174
  /** Directory-name comparison (plain code-point order, locale-independent). */
@@ -179,7 +199,7 @@ async function readSkill(env, dir, context) {
179
199
  cause: read.error,
180
200
  });
181
201
  }
182
- return skillFromFile(read.value, `${SKILLS_DIRECTORY}/${dir.name}`, relative);
202
+ return skillFromFile(read.value, `${SKILLS_DIRECTORY}/${dir.name}`, relative, "workspace");
183
203
  }
184
204
  /**
185
205
  * Scan the workspace's skills through the conversation `ExecutionEnv`.
@@ -236,7 +256,7 @@ export async function scanSkills(env, context, opts) {
236
256
  * `disable-model-invocation: true` skills (the `listSkills` path); the
237
257
  * default hides them with a log, exactly like the workspace root.
238
258
  */
239
- function scanFsRoot(root, opts) {
259
+ export function scanFsRoot(root, rootLabel, opts) {
240
260
  let entries;
241
261
  try {
242
262
  entries = readdirSync(root, { withFileTypes: true });
@@ -263,7 +283,7 @@ function scanFsRoot(root, opts) {
263
283
  }
264
284
  throw new Error(`Failed to read ${file}: ${cause instanceof Error ? cause.message : String(cause)}`, { cause });
265
285
  }
266
- const skill = skillFromFile(text, join(root, entry.name), file);
286
+ const skill = skillFromFile(text, join(root, entry.name), file, rootLabel);
267
287
  if (skill === undefined)
268
288
  continue;
269
289
  if (skill.disableModelInvocation && opts?.includeHidden !== true) {
@@ -320,9 +340,15 @@ export async function discoverSkills(env, context, opts, options) {
320
340
  if (env !== undefined) {
321
341
  merge(await scanSkills(env, context, options), "workspace");
322
342
  }
323
- merge(scanFsRoot(opts.userDir, options), "user dir");
343
+ // The learned root (curator P2): the owner's own skills, between the
344
+ // workspace and the deployment's user dir. Ownerless → none.
345
+ const learnedRoot = opts.learnedRoot?.(options?.owner);
346
+ if (learnedRoot !== undefined) {
347
+ merge(scanFsRoot(learnedRoot, "learned", options), "learned");
348
+ }
349
+ merge(scanFsRoot(opts.userDir, "user dir", options), "user dir");
324
350
  for (const root of opts.packageRoots()) {
325
- merge(scanFsRoot(root.dir, options), "package", root.extension);
351
+ merge(scanFsRoot(root.dir, "package", options), "package", root.extension);
326
352
  }
327
353
  return [...byName.values()].map((entry) => entry.skill).sort(bySkillName);
328
354
  }
@@ -333,8 +359,11 @@ export async function discoverSkills(env, context, opts, options) {
333
359
  * them while the section and `load_skill` keep them hidden. Read LIVE
334
360
  * per call (the same posture as the section's per-render scan).
335
361
  */
336
- export async function listSkills(env, context, opts) {
337
- return discoverSkills(env, context, opts, { includeHidden: true });
362
+ export async function listSkills(env, context, opts, options) {
363
+ return discoverSkills(env, context, opts, {
364
+ includeHidden: true,
365
+ ...(options?.owner === undefined ? {} : { owner: options.owner }),
366
+ });
338
367
  }
339
368
  /**
340
369
  * Assemble the section text: the header plus one `- name: description
@@ -373,9 +402,10 @@ export function renderSkillList(skills, limitBytes) {
373
402
  // rendering — it says skills exist and how many were cut).
374
403
  return `(skill list truncated: ${lines.length} more not shown — call load_skill by name)`;
375
404
  }
376
- /** Section render: discover all three roots, list the winning skills. */
405
+ /** Section render: discover all roots, list the winning skills. */
377
406
  async function renderSkills(input, context, opts) {
378
- const skills = await discoverSkills(input.env, context, opts);
407
+ const owner = await conversationOwner(input.read, input.conversationId, context);
408
+ const skills = await discoverSkills(input.env, context, opts, { owner });
379
409
  return renderSkillList(skills, opts.sectionMaxBytes());
380
410
  }
381
411
  /** An explicit tool error: text content the model can read and recover from. */
@@ -393,7 +423,8 @@ function buildLoadSkillTool(opts) {
393
423
  }),
394
424
  }),
395
425
  async execute(args, api, context) {
396
- const skills = await discoverSkills(api.env, context, opts);
426
+ const owner = await conversationOwner(api, api.conversationId, context);
427
+ const skills = await discoverSkills(api.env, context, opts, { owner });
397
428
  const skill = skills.find((candidate) => candidate.name === args.name);
398
429
  if (skill === undefined) {
399
430
  if (skills.length === 0) {
@@ -402,7 +433,19 @@ function buildLoadSkillTool(opts) {
402
433
  const available = skills.map((candidate) => candidate.name).join(", ");
403
434
  return skillError(`load_skill: unknown skill "${args.name}"; available skills: ${available}`);
404
435
  }
405
- api.output(skill.body);
436
+ // Curator A5 (P2): an overlay for this owner rides on the upstream
437
+ // body — upstream, then the heading, then the overlay text. The
438
+ // section is unchanged (the skill lists once, plain).
439
+ const overlay = await overlayForSkill(opts, owner, skill, api.env);
440
+ api.output(overlay === undefined ? skill.body : withOverlay(skill.body, overlay));
441
+ // Curator A6 (P4): a learned-root skill load counts as usage.
442
+ if (skill.root === "learned" && owner !== undefined) {
443
+ const usage = opts.usage?.(owner);
444
+ if (usage !== undefined) {
445
+ usage.bump(skill.name, "load");
446
+ void usage.flush();
447
+ }
448
+ }
406
449
  return {};
407
450
  },
408
451
  });
@@ -419,6 +462,32 @@ export function createSkillsExtension(opts) {
419
462
  sections: [
420
463
  section(SKILLS_SECTION_KEY, (input, context) => renderSkills(input, context, opts)),
421
464
  ],
422
- tools: [buildLoadSkillTool(opts)],
465
+ tools: [
466
+ buildLoadSkillTool(opts),
467
+ // Curator A6 (P3): the skill_manage tool installs once the options
468
+ // carry the curation wiring (a skill-store seam) — the read actions
469
+ // for every owned conversation, the mutating actions for curator
470
+ // runs. The overlay actions resolve the CURRENT upstream through
471
+ // A5's machinery over the same discovery roots (the final review
472
+ // fix wave: hashing the learned root stored `""` for every overlay).
473
+ ...(opts.skillStore === undefined
474
+ ? []
475
+ : [
476
+ buildSkillManageTool({
477
+ storesOf: (user) => {
478
+ const store = opts.skillStore?.(user);
479
+ const overlays = opts.overlays?.(user);
480
+ return store === undefined || overlays === undefined
481
+ ? undefined
482
+ : { store, overlays };
483
+ },
484
+ upstreamOf: (key, env, ctx) => resolveUpstream(key, env, ctx, {
485
+ userDir: opts.userDir,
486
+ packageRoots: opts.packageRoots,
487
+ }),
488
+ workspaceOf: (env) => opts.workspaceRoot?.() ?? env?.cwd,
489
+ }),
490
+ ]),
491
+ ],
423
492
  });
424
493
  }
@@ -5,8 +5,9 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
 
8
- ## [Unreleased]
8
+ ## [0.5.0] - 2026-11-02
9
9
 
10
+ Version-companion release for sigrun 0.5.0 (no shared changes).
10
11
  ## [0.4.2] - 2026-10-31
11
12
 
12
13
  ### Fixed
@@ -60,3 +61,4 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
60
61
  conversation/turn shaping, the channel wire types, session-meta,
61
62
  commands, interactions, ambient records, access, settings, views,
62
63
  view-delta, and jobs — the surfaces the webui, TUI, and server share.
64
+
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * The rules, in order (first match wins):
11
11
  *
12
+ * 0. internal resource → none (curator P5: a conversation created for
13
+ * curator work is invisible to EVERY caller — the superuser included);
12
14
  * 1. superuser → write;
13
15
  * 2. ownerless resource → household write (everyone);
14
16
  * 3. user === owner → write;
@@ -48,6 +50,7 @@ export interface AccessPrincipal {
48
50
  export declare function accessOf(p: AccessPrincipal, r: {
49
51
  readonly owner?: string;
50
52
  readonly sharing?: Sharing;
53
+ readonly internal?: true;
51
54
  }): Access;
52
55
  /**
53
56
  * Type guard for the wire value: every scope shape checked strictly
@@ -9,6 +9,8 @@
9
9
  *
10
10
  * The rules, in order (first match wins):
11
11
  *
12
+ * 0. internal resource → none (curator P5: a conversation created for
13
+ * curator work is invisible to EVERY caller — the superuser included);
12
14
  * 1. superuser → write;
13
15
  * 2. ownerless resource → household write (everyone);
14
16
  * 3. user === owner → write;
@@ -26,6 +28,12 @@
26
28
  * module doc; the first matching rule wins.
27
29
  */
28
30
  export function accessOf(p, r) {
31
+ // 0. Internal (curator P5): invisible to everyone, the superuser
32
+ // included — BEFORE any other rule can widen access. The marker is
33
+ // server-owned (written once in the creating commit), so no caller,
34
+ // share, or role can ever open an internal resource.
35
+ if (r.internal === true)
36
+ return "none";
29
37
  // 1. The superuser always writes.
30
38
  if (p.superuser === true)
31
39
  return "write";
@@ -25,10 +25,10 @@ export type { Sharing } from "./access.js";
25
25
  * `invalid_field`.
26
26
  *
27
27
  * Server-owned fields (`autoName`, `autoNameAttempts`,
28
- * `lastActivityAt`, `jobId`, `deliver`) are written by host code only
29
- * (activity.ts, the A2 auto-namer, fire.ts's job stamp); they are NOT
30
- * patch surface — the strict validator keeps rejecting them as
31
- * `invalid_field`.
28
+ * `lastActivityAt`, `jobId`, `deliver`, `internal`) are written by host
29
+ * code only (activity.ts, the A2 auto-namer, fire.ts's job stamp, the
30
+ * curator's creating commit); they are NOT patch surface — the strict
31
+ * validator keeps rejecting them as `invalid_field`.
32
32
  */
33
33
  /** The per-conversation session metadata (spec §5). */
34
34
  export type SessionMeta = {
@@ -85,6 +85,17 @@ export type SessionMeta = {
85
85
  * strict validator rejects the key as `invalid_field`.
86
86
  */
87
87
  deliver?: string[];
88
+ /**
89
+ * Server-owned internal-run marker (curator P5, spec §6.1): true on a
90
+ * conversation created for curator work — hidden from every list and
91
+ * view, 404 on every `:id` route for every caller (superuser
92
+ * included), never named, never notified, never idle-clocked, and
93
+ * absent from every event surface. Written ONCE by the curator's
94
+ * creating commit (`markInternal`); the flag never changes after
95
+ * creation. Never patchable — the strict validator rejects the key
96
+ * as `invalid_field`.
97
+ */
98
+ internal?: true;
88
99
  /**
89
100
  * The §5.9 per-resource sharing state; absent = private (owner and
90
101
  * superuser only). Patchable by the OWNER or a superuser (403
@@ -303,11 +303,11 @@ export interface NotificationsSettings {
303
303
  readonly runSettled?: "unwatched" | "cron" | "always" | "never";
304
304
  }
305
305
  /**
306
- * The built-in extensions toggles: codemode, MCP, tool search and OAuth
307
- * ship WITH core. Each key defaults to `true`; `false`
308
- * removes the built-in entirely (absent from `/v1/extensions`, its config
309
- * ignored with one log line). RESTART-REQUIRED: the built-in set resolves
310
- * at boot, like `extensions`.
306
+ * The built-in extensions toggles: codemode, MCP, tool search, the
307
+ * gateway, OAuth and the curator ship WITH core. Each key defaults to
308
+ * `true`; `false` removes the built-in entirely (absent from
309
+ * `/v1/extensions`, its config ignored with one log line).
310
+ * RESTART-REQUIRED: the built-in set resolves at boot, like `extensions`.
311
311
  */
312
312
  export interface BuiltinExtensionsSettings {
313
313
  readonly codemode?: boolean;
@@ -317,6 +317,8 @@ export interface BuiltinExtensionsSettings {
317
317
  readonly gateway?: boolean;
318
318
  /** OAuth login providers (multi-user M3b), default true. */
319
319
  readonly oauth?: boolean;
320
+ /** Curator (Task B1): the per-user curation loops, default true. */
321
+ readonly curator?: boolean;
320
322
  }
321
323
  /**
322
324
  * The persisted shape: HarnessSettings pass-throughs + sigrun fields.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sigrun/shared",
3
- "version": "0.4.2",
3
+ "version": "0.5.0",
4
4
  "description": "sigrun shared leaf package \u2014 theme tokens, turn/conversation shaping, and the channel wire types (pure leaf, zero dependencies)",
5
5
  "type": "module",
6
6
  "exports": {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@sigrun/sigrun",
3
- "version": "0.4.2",
4
- "description": "Durable personal assistant on pi-durable",
3
+ "version": "0.5.0",
4
+ "description": "Self-hosted, multi-user personal-assistant server",
5
5
  "type": "module",
6
6
  "exports": {
7
7
  ".": "./dist/index.js",
@@ -45,11 +45,12 @@
45
45
  "@earendil-works/pi-tui": "^1.3.0",
46
46
  "@fastify/static": "^10.1.7",
47
47
  "@sigrun/ext-codemode": "0.1.2",
48
- "@sigrun/ext-gateway": "0.1.7",
48
+ "@sigrun/ext-curator": "0.1.0",
49
+ "@sigrun/ext-gateway": "0.1.9",
49
50
  "@sigrun/ext-mcp": "0.2.2",
50
51
  "@sigrun/ext-tool-search": "0.1.2",
51
52
  "@sigrun/oauth": "0.1.1",
52
- "@sigrun/shared": "0.4.2",
53
+ "@sigrun/shared": "0.5.0",
53
54
  "croner": "^10.0.1",
54
55
  "fastify": "^5.12.7",
55
56
  "semver": "^7.8.5",
@@ -60,11 +61,12 @@
60
61
  "@sigrun/shared"
61
62
  ],
62
63
  "optionalDependencies": {
63
- "@sigrun/sandbox-linux-arm64": "0.4.2",
64
- "@sigrun/sandbox-linux-x64": "0.4.2",
65
- "@sigrun/tui": "0.4.2"
64
+ "@sigrun/sandbox-linux-arm64": "0.5.0",
65
+ "@sigrun/sandbox-linux-x64": "0.5.0",
66
+ "@sigrun/tui": "0.5.0"
66
67
  },
67
68
  "devDependencies": {
69
+ "@sigrun/memory-markdown": "file:../sigrun-memory-markdown",
68
70
  "@types/node": "^26.6.4",
69
71
  "@types/semver": "^7.8.0",
70
72
  "fast-check": "^4.10.2",