@plurnk/plurnk-service 1.24.0 → 1.26.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 (226) hide show
  1. package/.env.defaults +17 -15
  2. package/INSTALL.md +96 -20
  3. package/README.md +2 -2
  4. package/SPEC.md +517 -193
  5. package/dist/Paths.d.ts +1 -0
  6. package/dist/Paths.d.ts.map +1 -1
  7. package/dist/Paths.js +1 -0
  8. package/dist/Paths.js.map +1 -1
  9. package/dist/build-info.json +1 -1
  10. package/dist/content/read-projector.d.ts.map +1 -1
  11. package/dist/content/read-projector.js +19 -0
  12. package/dist/content/read-projector.js.map +1 -1
  13. package/dist/core/AdministrativeLoop.d.ts +1 -1
  14. package/dist/core/AdministrativeLoop.d.ts.map +1 -1
  15. package/dist/core/AdministrativeLoop.js +5 -5
  16. package/dist/core/AdministrativeLoop.js.map +1 -1
  17. package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
  18. package/dist/core/AdmittedTurnExecutor.js +1 -0
  19. package/dist/core/AdmittedTurnExecutor.js.map +1 -1
  20. package/dist/core/CapabilityResolver.js +1 -1
  21. package/dist/core/CapabilityResolver.js.map +1 -1
  22. package/dist/core/DataStatementRunner.d.ts.map +1 -1
  23. package/dist/core/DataStatementRunner.js +1 -3
  24. package/dist/core/DataStatementRunner.js.map +1 -1
  25. package/dist/core/Dispatcher.d.ts +1 -1
  26. package/dist/core/Dispatcher.d.ts.map +1 -1
  27. package/dist/core/Dispatcher.js +76 -21
  28. package/dist/core/Dispatcher.js.map +1 -1
  29. package/dist/core/Dispatcher.sql +0 -7
  30. package/dist/core/Engine.d.ts +2 -2
  31. package/dist/core/Engine.d.ts.map +1 -1
  32. package/dist/core/ExecutorRegistry.d.ts +16 -4
  33. package/dist/core/ExecutorRegistry.d.ts.map +1 -1
  34. package/dist/core/ExecutorRegistry.js +31 -31
  35. package/dist/core/ExecutorRegistry.js.map +1 -1
  36. package/dist/core/HostPaths.d.ts +7 -1
  37. package/dist/core/HostPaths.d.ts.map +1 -1
  38. package/dist/core/HostPaths.js +25 -12
  39. package/dist/core/HostPaths.js.map +1 -1
  40. package/dist/core/LoopDriver.d.ts.map +1 -1
  41. package/dist/core/LoopDriver.js +11 -2
  42. package/dist/core/LoopDriver.js.map +1 -1
  43. package/dist/core/LoopLifecycle.d.ts +1 -1
  44. package/dist/core/LoopLifecycle.js +1 -1
  45. package/dist/core/LoopLifecycle.sql +2 -2
  46. package/dist/core/LoopPolicies.d.ts.map +1 -1
  47. package/dist/core/LoopPolicies.js +12 -7
  48. package/dist/core/LoopPolicies.js.map +1 -1
  49. package/dist/core/MembershipMaterialization.js +1 -1
  50. package/dist/core/MembershipMaterialization.js.map +1 -1
  51. package/dist/core/OperatorConfig.d.ts.map +1 -1
  52. package/dist/core/OperatorConfig.js +97 -51
  53. package/dist/core/OperatorConfig.js.map +1 -1
  54. package/dist/core/PacketBuilder.d.ts +1 -0
  55. package/dist/core/PacketBuilder.d.ts.map +1 -1
  56. package/dist/core/PacketBuilder.js +27 -25
  57. package/dist/core/PacketBuilder.js.map +1 -1
  58. package/dist/core/ReasoningView.d.ts +1 -1
  59. package/dist/core/ReasoningView.d.ts.map +1 -1
  60. package/dist/core/ReasoningView.js +4 -5
  61. package/dist/core/ReasoningView.js.map +1 -1
  62. package/dist/core/SchemeRegistry.d.ts.map +1 -1
  63. package/dist/core/SchemeRegistry.js +3 -2
  64. package/dist/core/SchemeRegistry.js.map +1 -1
  65. package/dist/core/Turn.d.ts +1 -1
  66. package/dist/core/Turn.d.ts.map +1 -1
  67. package/dist/core/Turn.js +2 -2
  68. package/dist/core/Turn.js.map +1 -1
  69. package/dist/core/Turn.sql +1 -1
  70. package/dist/core/TurnDispositionHandler.d.ts +1 -1
  71. package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
  72. package/dist/core/TurnDispositionHandler.js +6 -15
  73. package/dist/core/TurnDispositionHandler.js.map +1 -1
  74. package/dist/core/TurnMaterialization.js +1 -1
  75. package/dist/core/TurnMaterialization.js.map +1 -1
  76. package/dist/core/TurnOps.d.ts +1 -0
  77. package/dist/core/TurnOps.d.ts.map +1 -1
  78. package/dist/core/TurnOps.js +18 -0
  79. package/dist/core/TurnOps.js.map +1 -1
  80. package/dist/core/TurnRunner.d.ts.map +1 -1
  81. package/dist/core/TurnRunner.js +53 -32
  82. package/dist/core/TurnRunner.js.map +1 -1
  83. package/dist/core/env-defaults.d.ts +12 -1
  84. package/dist/core/env-defaults.d.ts.map +1 -1
  85. package/dist/core/env-defaults.js +50 -7
  86. package/dist/core/env-defaults.js.map +1 -1
  87. package/dist/core/file-creation-policy.d.ts.map +1 -1
  88. package/dist/core/file-creation-policy.js +2 -1
  89. package/dist/core/file-creation-policy.js.map +1 -1
  90. package/dist/core/git-membership.d.ts.map +1 -1
  91. package/dist/core/git-membership.js +0 -3
  92. package/dist/core/git-membership.js.map +1 -1
  93. package/dist/core/namespace.d.ts +1 -0
  94. package/dist/core/namespace.d.ts.map +1 -1
  95. package/dist/core/namespace.js +21 -75
  96. package/dist/core/namespace.js.map +1 -1
  97. package/dist/core/notifications.d.ts +2 -0
  98. package/dist/core/notifications.d.ts.map +1 -1
  99. package/dist/core/packet-wire.d.ts +1 -0
  100. package/dist/core/packet-wire.d.ts.map +1 -1
  101. package/dist/core/packet-wire.js +28 -7
  102. package/dist/core/packet-wire.js.map +1 -1
  103. package/dist/core/results.d.ts +2 -0
  104. package/dist/core/results.d.ts.map +1 -1
  105. package/dist/core/results.js +7 -0
  106. package/dist/core/results.js.map +1 -1
  107. package/dist/digest/Digest.d.ts.map +1 -1
  108. package/dist/digest/Digest.js +1 -2
  109. package/dist/digest/Digest.js.map +1 -1
  110. package/dist/digest/DigestEvidence.d.ts +1 -0
  111. package/dist/digest/DigestEvidence.d.ts.map +1 -1
  112. package/dist/digest/DigestEvidence.js +8 -0
  113. package/dist/digest/DigestEvidence.js.map +1 -1
  114. package/dist/digest/DigestRender.d.ts.map +1 -1
  115. package/dist/digest/DigestRender.js +23 -21
  116. package/dist/digest/DigestRender.js.map +1 -1
  117. package/dist/digest/digest-rows.d.ts +2 -1
  118. package/dist/digest/digest-rows.d.ts.map +1 -1
  119. package/dist/digest/digest.sql +4 -0
  120. package/dist/observe/init.d.ts +1 -0
  121. package/dist/observe/init.d.ts.map +1 -1
  122. package/dist/observe/init.js +5 -1
  123. package/dist/observe/init.js.map +1 -1
  124. package/dist/schemes/EffectPolicy.d.ts.map +1 -1
  125. package/dist/schemes/EffectPolicy.js +4 -4
  126. package/dist/schemes/EffectPolicy.js.map +1 -1
  127. package/dist/schemes/Exec.d.ts +1 -0
  128. package/dist/schemes/Exec.d.ts.map +1 -1
  129. package/dist/schemes/Exec.js +30 -14
  130. package/dist/schemes/Exec.js.map +1 -1
  131. package/dist/schemes/ExecScheduler.d.ts.map +1 -1
  132. package/dist/schemes/ExecScheduler.js +4 -3
  133. package/dist/schemes/ExecScheduler.js.map +1 -1
  134. package/dist/schemes/ExecScratch.d.ts.map +1 -1
  135. package/dist/schemes/ExecScratch.js +2 -1
  136. package/dist/schemes/ExecScratch.js.map +1 -1
  137. package/dist/schemes/File.d.ts.map +1 -1
  138. package/dist/schemes/File.js +21 -22
  139. package/dist/schemes/File.js.map +1 -1
  140. package/dist/schemes/_entry-find.d.ts +1 -0
  141. package/dist/schemes/_entry-find.d.ts.map +1 -1
  142. package/dist/schemes/_entry-find.js +4 -3
  143. package/dist/schemes/_entry-find.js.map +1 -1
  144. package/dist/schemes/_path-scope.d.ts +2 -2
  145. package/dist/schemes/_path-scope.d.ts.map +1 -1
  146. package/dist/schemes/_path-scope.js +23 -2
  147. package/dist/schemes/_path-scope.js.map +1 -1
  148. package/dist/server/AgentRoots.d.ts +6 -0
  149. package/dist/server/AgentRoots.d.ts.map +1 -0
  150. package/dist/server/AgentRoots.js +25 -0
  151. package/dist/server/AgentRoots.js.map +1 -0
  152. package/dist/server/ConfigurationDiagnostics.d.ts +12 -0
  153. package/dist/server/ConfigurationDiagnostics.d.ts.map +1 -0
  154. package/dist/server/ConfigurationDiagnostics.js +40 -0
  155. package/dist/server/ConfigurationDiagnostics.js.map +1 -0
  156. package/dist/server/Daemon.d.ts +11 -5
  157. package/dist/server/Daemon.d.ts.map +1 -1
  158. package/dist/server/Daemon.js +125 -48
  159. package/dist/server/Daemon.js.map +1 -1
  160. package/dist/server/DaemonModule.d.ts +11 -4
  161. package/dist/server/DaemonModule.d.ts.map +1 -1
  162. package/dist/server/EnvFunctionality.d.ts.map +1 -1
  163. package/dist/server/EnvFunctionality.js +5 -2
  164. package/dist/server/EnvFunctionality.js.map +1 -1
  165. package/dist/server/Functionality.d.ts +6 -0
  166. package/dist/server/Functionality.d.ts.map +1 -1
  167. package/dist/server/Functionality.js +173 -37
  168. package/dist/server/Functionality.js.map +1 -1
  169. package/dist/server/FunctionalityManager.js +6 -6
  170. package/dist/server/FunctionalityManager.js.map +1 -1
  171. package/dist/server/MembersFunctionality.d.ts.map +1 -1
  172. package/dist/server/MembersFunctionality.js +13 -41
  173. package/dist/server/MembersFunctionality.js.map +1 -1
  174. package/dist/server/PluginSources.d.ts +20 -0
  175. package/dist/server/PluginSources.d.ts.map +1 -0
  176. package/dist/server/PluginSources.js +52 -0
  177. package/dist/server/PluginSources.js.map +1 -0
  178. package/dist/server/PlurnkSkill.js +1 -1
  179. package/dist/server/PlurnkSkill.js.map +1 -1
  180. package/dist/server/Retention.d.ts.map +1 -1
  181. package/dist/server/Retention.js +11 -30
  182. package/dist/server/Retention.js.map +1 -1
  183. package/dist/server/ServiceModules.d.ts +1 -0
  184. package/dist/server/ServiceModules.d.ts.map +1 -1
  185. package/dist/server/ServiceModules.js +9 -3
  186. package/dist/server/ServiceModules.js.map +1 -1
  187. package/dist/server/SkillSource.d.ts +37 -0
  188. package/dist/server/SkillSource.d.ts.map +1 -0
  189. package/dist/server/SkillSource.js +267 -0
  190. package/dist/server/SkillSource.js.map +1 -0
  191. package/dist/server/SkillsFunctionality.d.ts +8 -27
  192. package/dist/server/SkillsFunctionality.d.ts.map +1 -1
  193. package/dist/server/SkillsFunctionality.js +218 -270
  194. package/dist/server/SkillsFunctionality.js.map +1 -1
  195. package/dist/server/WorkerModelResolver.d.ts +1 -2
  196. package/dist/server/WorkerModelResolver.d.ts.map +1 -1
  197. package/dist/server/WorkerModelResolver.js +18 -19
  198. package/dist/server/WorkerModelResolver.js.map +1 -1
  199. package/dist/server/WorkspacePlugins.d.ts +25 -0
  200. package/dist/server/WorkspacePlugins.d.ts.map +1 -0
  201. package/dist/server/WorkspacePlugins.js +43 -0
  202. package/dist/server/WorkspacePlugins.js.map +1 -0
  203. package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
  204. package/dist/server/dispatch-as-plurnk.js +2 -1
  205. package/dist/server/dispatch-as-plurnk.js.map +1 -1
  206. package/dist/server/envelope.js +1 -1
  207. package/dist/server/envelope.js.map +1 -1
  208. package/dist/server/loop-model.d.ts.map +1 -1
  209. package/dist/server/loop-model.js +1 -2
  210. package/dist/server/loop-model.js.map +1 -1
  211. package/dist/server/module-discovery.d.ts +6 -0
  212. package/dist/server/module-discovery.d.ts.map +1 -1
  213. package/dist/server/module-discovery.js +46 -24
  214. package/dist/server/module-discovery.js.map +1 -1
  215. package/dist/server/skills-problems.d.ts +8 -0
  216. package/dist/server/skills-problems.d.ts.map +1 -0
  217. package/dist/server/skills-problems.js +18 -0
  218. package/dist/server/skills-problems.js.map +1 -0
  219. package/dist/service.d.ts.map +1 -1
  220. package/dist/service.js +55 -45
  221. package/dist/service.js.map +1 -1
  222. package/docs/env.md +7 -8
  223. package/docs/members.md +4 -3
  224. package/docs/skills.md +45 -22
  225. package/package.json +37 -35
  226. package/plurnk.service +29 -0
@@ -1,159 +1,101 @@
1
1
  // {§skills-functionality} — standard Agent Skills as one workspace Functionality
2
- // family. Universal roots and host-provided trees own resources;
3
- // workspace state owns enablement; the standard
4
- // `skills` CLI is the deterministic installer beneath `add`/`remove` and the
5
- // registry behind `discover`, and neither is the model's or a client's contract.
6
- import { execFile } from "node:child_process";
7
- import ExecEnv from "../schemes/exec-env.js";
2
+ // family. Configuration roots are inputs; live references and fetched sources
3
+ // belong to workspace definitions, never to an installation scope.
8
4
  import { createHash } from "node:crypto";
9
5
  import { readdir, readFile, realpath, stat } from "node:fs/promises";
10
6
  import { join } from "node:path";
11
- import { promisify, stripVTControlCharacters } from "node:util";
12
7
  import { SkillDirectory } from "@plurnk/plurnk-agent-skills";
13
- import { Problems, SKILL_NAME, Validator, } from "@plurnk/plurnk-contracts";
8
+ import { SKILL_NAME, Validator, } from "@plurnk/plurnk-contracts";
14
9
  import HostPaths from "../core/HostPaths.js";
15
- import { Knob } from "@plurnk/plurnk-meta";
10
+ import { ConfigurationError, Knob, ResourceEnvironment } from "@plurnk/plurnk-meta";
16
11
  import Paths from "../Paths.js";
17
- const execFileP = promisify(execFile);
12
+ import SkillSource from "./SkillSource.js";
13
+ import { agentRootScopes } from "./AgentRoots.js";
14
+ import { SkillsActionError, actionError, messageOf } from "./skills-problems.js";
15
+ import WorkspacePlugins from "./WorkspacePlugins.js";
18
16
  const SKILLS_FAMILY = "skills";
19
17
  const SKILLS_OWNER = "@plurnk/plurnk-core/skills";
20
18
  const DEFINITION = { $ref: "https://schemas.plurnk.xyz/v0/SkillDefinition.json" };
21
- // The ceiling of one skills CLI run's captured output: a bound on a child process, not a choice.
22
- const CLI_OUTPUT_BYTES = 8 * 1024 * 1024;
23
- class SkillsActionError extends Error {
24
- problem;
25
- constructor(problem, cause) {
26
- super(problem.detail, cause === undefined ? undefined : { cause });
27
- this.name = "SkillsActionError";
28
- this.problem = problem;
29
- }
30
- }
31
- const problem = (code, status, detail, extensions = {}) => Problems.create("skills:functionality", code, status, detail, {
32
- stage: "skills-functionality",
33
- retryable: status === 409 || status >= 500,
34
- ...extensions,
19
+ // {§skills-sources} — the vendor installer's knobs; each names what replaced it.
20
+ const RETIRED_KNOBS = Object.freeze({
21
+ PLURNK_SERVICE_SKILLS_CLI: "add fetches git, folder and file sources itself",
22
+ PLURNK_SERVICE_SKILLS_CLI_TIMEOUT_MS: "PLURNK_SERVICE_SKILLS_FETCH_TIMEOUT_MS bounds each fetch",
23
+ PLURNK_SERVICE_SKILLS_REGISTRY_URL: "discover takes a source; Agent Skills have no standard registry",
24
+ PLURNK_SERVICE_SKILLS_REGISTRY_LIMIT: "discover takes a source; Agent Skills have no standard registry",
25
+ PLURNK_SERVICE_SKILLS_REGISTRY_TIMEOUT_MS: "discover takes a source; Agent Skills have no standard registry",
35
26
  });
36
- const actionError = (code, status, detail, extensions = {}, cause) => new SkillsActionError(problem(code, status, detail, extensions), cause);
27
+ // Installed roots in precedence order: a nearer root shadows a farther one by name.
28
+ const ROOTS = ["project", "plurnk", "global"];
37
29
  const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
38
- const messageOf = (cause) => cause instanceof Error ? cause.message : String(cause);
39
30
  const missing = (cause) => {
40
31
  if (cause?.code === "ENOENT")
41
32
  return false;
42
33
  throw cause;
43
34
  };
44
35
  const isFile = (path) => stat(path).then((info) => info.isFile(), missing);
45
- const exists = (path) => stat(path).then(() => true, missing);
46
- // `skills add <source> --list` prints, after an "Available Skills" heading,
47
- // each skill name at one indentation and its description at a deeper one,
48
- // behind the CLI's box-drawing gutter. Only that structure is read.
49
- export const parseListing = (output) => {
50
- const lines = stripVTControlCharacters(output).split(/\r?\n/u);
51
- const start = lines.findIndex((line) => /Available Skills\s*$/u.test(line));
52
- if (start === -1)
53
- return [];
54
- const skills = [];
55
- for (const line of lines.slice(start + 1)) {
56
- const match = /^[^\s\p{L}\p{N}]*( +)(\S.*)$/u.exec(line);
57
- if (match === null)
58
- continue;
59
- const indent = match[1].length;
60
- const text = match[2].trim();
61
- if (indent === 4) {
62
- if (SKILL_NAME.test(text))
63
- skills.push({ name: text, description: "" });
64
- continue;
36
+ const environment = (env = process.env) => new ResourceEnvironment("PLURNK_SKILLS_", { controls: [], settings: [], aliasPattern: SKILL_NAME }, env);
37
+ export const serviceSkills = (env = process.env) => {
38
+ const definitions = new Map();
39
+ for (const [alias, { key, value }] of environment(env).definitions) {
40
+ let definition;
41
+ try {
42
+ definition = Validator.assertSkillDefinition(JSON.parse(value));
65
43
  }
66
- const last = skills.at(-1);
67
- if (last === undefined || indent < 6)
68
- continue;
69
- skills[skills.length - 1] = { name: last.name, description: last.description.length === 0 ? text : `${last.description} ${text}` };
70
- }
71
- return skills;
72
- };
73
- export class StandardSkillsToolchain {
74
- #command;
75
- #registry;
76
- constructor(env = process.env) {
77
- // {§operator-config-only-home} — the panel states both values; an unset key is a broken floor.
78
- const cli = env.PLURNK_SERVICE_SKILLS_CLI;
79
- if (cli === undefined)
80
- throw new Error("PLURNK_SERVICE_SKILLS_CLI is missing from the assembled environment floor.");
81
- if (cli.trim().length === 0)
82
- throw new Error("PLURNK_SERVICE_SKILLS_CLI must name the Agent Skills installer command.");
83
- this.#command = cli.trim().split(/\s+/u);
84
- const registry = env.PLURNK_SERVICE_SKILLS_REGISTRY_URL;
85
- if (registry === undefined)
86
- throw new Error("PLURNK_SERVICE_SKILLS_REGISTRY_URL is missing from the assembled environment floor.");
87
- this.#registry = registry.trim().length === 0 ? null : registry.trim().replace(/\/+$/u, "");
88
- }
89
- get registry() {
90
- return this.#registry;
91
- }
92
- async run(args, cwd, home) {
93
- const [command, ...prefix] = this.#command;
94
- const { stdout, stderr } = await execFileP(command, [...prefix, ...args], {
95
- cwd,
96
- // {§exec-env-scoped} — the registry CLI is plurnk's own tooling, so it keeps the
97
- // operator's environment (npm config, proxies) but never plurnk's secrets. NO_COLOR
98
- // and CI arrive as declared floors from @plurnk/plurnk-execs rather than inline.
99
- env: { ...ExecEnv.withoutOwnSecrets(), HOME: home },
100
- timeout: Knob.integer("PLURNK_SERVICE_SKILLS_CLI_TIMEOUT_MS", 1),
101
- maxBuffer: CLI_OUTPUT_BYTES,
102
- });
103
- return `${stdout}\n${stderr}`;
104
- }
105
- async search(query) {
106
- if (this.#registry === null) {
107
- throw actionError("registry-not-configured", 501, "Skills registry search is disabled; PLURNK_SERVICE_SKILLS_REGISTRY_URL is empty.", { query, retryable: false });
44
+ catch (cause) {
45
+ throw new ConfigurationError(key, `${key} must contain a complete SkillDefinition.`, { cause });
108
46
  }
109
- const url = `${this.#registry}/api/search?${new URLSearchParams({ q: query, limit: String(Knob.integer("PLURNK_SERVICE_SKILLS_REGISTRY_LIMIT", 1)) })}`;
110
- let response;
47
+ if (definition.name !== alias)
48
+ throw new ConfigurationError(key, `${key}: the definition name must equal '${alias}'.`);
49
+ if (definition.source === undefined)
50
+ throw new ConfigurationError(key, `${key}: a configured skill requires a source.`);
51
+ if (definition.commit !== undefined)
52
+ throw new ConfigurationError(key, `${key}: commit is service-recorded; configure a Git ref instead.`);
111
53
  try {
112
- response = await fetch(url, { signal: AbortSignal.timeout(Knob.integer("PLURNK_SERVICE_SKILLS_REGISTRY_TIMEOUT_MS", 1)) });
54
+ const remote = SkillSource.remote(definition.source);
55
+ if (definition.ref !== undefined && remote === null)
56
+ throw new Error("A ref names a branch or tag of a git source.");
113
57
  }
114
58
  catch (cause) {
115
- throw actionError("registry-unreachable", 502, `Skills registry ${this.#registry} could not be reached.`, { query, registry: this.#registry, retryable: true }, cause);
59
+ throw new ConfigurationError(key, `${key}: invalid skill source or ref.`, { cause });
116
60
  }
117
- if (!response.ok)
118
- throw actionError("registry-rejected", 502, `Skills registry ${this.#registry} answered ${response.status}.`, { query, registry: this.#registry, status: response.status, retryable: true });
119
- const body = await response.json();
120
- if (!Array.isArray(body.skills))
121
- throw actionError("registry-invalid", 502, `Skills registry ${this.#registry} returned no skills array.`, { query, registry: this.#registry, retryable: true });
122
- return body.skills.flatMap((skill) => {
123
- if (!isRecord(skill) || typeof skill.name !== "string" || typeof skill.id !== "string")
124
- return [];
125
- return [{
126
- name: skill.name,
127
- id: skill.id,
128
- source: typeof skill.source === "string" && skill.source.length > 0 ? skill.source : skill.id.split("/").slice(0, 2).join("/"),
129
- installs: typeof skill.installs === "number" ? skill.installs : null,
130
- reference: `${this.#registry}/${skill.id}`,
131
- }];
132
- });
61
+ definitions.set(alias, definition);
133
62
  }
134
- }
63
+ return definitions;
64
+ };
135
65
  export default class SkillsFunctionality {
136
66
  family = SKILLS_FAMILY;
137
67
  aliasPattern = SKILL_NAME;
138
68
  namespaceOwner = SKILLS_OWNER;
139
69
  summary = "Manage Agent Skills";
140
70
  definitionSchema = DEFINITION;
141
- example = { alias: "sql-formatter", definition: { name: "sql-formatter", scope: "project", source: "example/skills" } };
71
+ example = { alias: "sql-formatter", definition: { name: "sql-formatter", source: "https://git.example/acme/skills.git" } };
142
72
  docsDir = Paths.packageRoot;
143
73
  discovery = {
144
- details: "`query` searches the standard skills registry; `source` inspects one installer package reference (`owner/repo`, a git URL, or a local path). A candidate carries the exact definition to add.",
74
+ details: "`source` lists the Agent Skills one source carries: a git remote as a full https or ssh URL, a folder, a lone SKILL.md, or a zip or tar archive. A candidate carries the exact definition to add.",
145
75
  };
146
76
  #db;
147
77
  #hostPaths;
148
- #toolchain;
78
+ #storage;
149
79
  #provided;
80
+ #plugins;
150
81
  #snapshots = new Map();
151
82
  #handle = null;
152
- constructor({ db, hostPaths = new HostPaths(), toolchain = new StandardSkillsToolchain(), provided = async () => new Map() }) {
83
+ constructor({ db, storage, hostPaths = new HostPaths(), provided = async () => new Map(), plugins }) {
153
84
  this.#db = db;
85
+ this.#storage = storage;
154
86
  this.#hostPaths = hostPaths;
155
- this.#toolchain = toolchain;
156
87
  this.#provided = provided;
88
+ this.#plugins = plugins ?? new WorkspacePlugins({ db, hostPaths });
89
+ }
90
+ // Refuses the vendor installer's retired knobs, naming what replaced each, and reads the fetch deadline.
91
+ static validateConfiguration() {
92
+ for (const [knob, successor] of Object.entries(RETIRED_KNOBS)) {
93
+ const stale = process.env[knob];
94
+ if (stale !== undefined && stale.length > 0)
95
+ throw new ConfigurationError(knob, `${knob} is retired: ${successor}.`);
96
+ }
97
+ Knob.integer("PLURNK_SERVICE_SKILLS_FETCH_TIMEOUT_MS", 1);
98
+ serviceSkills();
157
99
  }
158
100
  attach(handle) {
159
101
  this.#handle = handle;
@@ -166,15 +108,14 @@ export default class SkillsFunctionality {
166
108
  return workspace?.project_root ?? null;
167
109
  }
168
110
  #rootFor(scope, projectRoot) {
169
- if (scope === "service")
170
- throw new TypeError("A service-provided skill has no installer root.");
111
+ if (!agentRootScopes().has(scope))
112
+ return null;
171
113
  if (scope === "global")
172
114
  return this.#hostPaths.globalSkillsDir;
115
+ if (scope === "plurnk")
116
+ return this.#hostPaths.plurnkSkillsDir;
173
117
  return projectRoot === null ? null : this.#hostPaths.projectSkillsDir(projectRoot);
174
118
  }
175
- #cwdFor(scope, projectRoot) {
176
- return scope === "global" || projectRoot === null ? this.#hostPaths.home : projectRoot;
177
- }
178
119
  async #installedIn(scope, dir) {
179
120
  let entries;
180
121
  try {
@@ -189,57 +130,38 @@ export default class SkillsFunctionality {
189
130
  for (const entry of entries.filter((candidate) => candidate.isDirectory() || candidate.isSymbolicLink())) {
190
131
  const file = join(dir, entry.name, "SKILL.md");
191
132
  if (await isFile(file))
192
- installed.push({ name: entry.name, scope, dir: join(dir, entry.name), file });
133
+ installed.push({ name: entry.name, dir: join(dir, entry.name), file });
193
134
  }
194
135
  return installed.toSorted((left, right) => left.name.localeCompare(right.name));
195
136
  }
196
- // The effective installed union: project shadows global by name.
197
- async #scan(projectRoot) {
137
+ // The effective installed union: project shadows plurnk shadows global, by name.
138
+ async #scan(workspaceId, projectRoot) {
198
139
  const union = new Map();
199
- for (const scope of ["project", "global"]) {
200
- const dir = this.#rootFor(scope, projectRoot);
201
- if (dir === null)
202
- continue;
203
- for (const installed of await this.#installedIn(scope, dir)) {
140
+ const { plugins } = await this.#plugins.read(workspaceId);
141
+ for (const scope of [...ROOTS, "npm"]) {
142
+ const dir = scope === "npm" ? null : this.#rootFor(scope, projectRoot);
143
+ const standalone = scope === "npm" || dir === null ? [] : await this.#installedIn(scope, dir);
144
+ for (const installed of standalone) {
204
145
  if (!union.has(installed.name))
205
146
  union.set(installed.name, installed);
206
147
  }
207
- }
208
- return union;
209
- }
210
- // The standard installer's lock files record each installed skill's source.
211
- async #lockSources(projectRoot) {
212
- const sources = new Map();
213
- for (const [scope, file] of [
214
- ["project", projectRoot === null ? null : join(projectRoot, "skills-lock.json")],
215
- ["global", this.#hostPaths.globalSkillsLockFile],
216
- ]) {
217
- if (file === null)
218
- continue;
219
- let lock;
220
- try {
221
- lock = JSON.parse(await readFile(file, "utf8"));
222
- }
223
- catch (cause) {
224
- if (cause?.code === "ENOENT")
225
- continue;
226
- throw new Error(`Cannot read Agent Skills lock ${file}.`, { cause });
227
- }
228
- if (!isRecord(lock) || !isRecord(lock.skills))
229
- throw new TypeError(`Invalid Agent Skills lock ${file}: expected a skills object.`);
230
- for (const [name, entry] of Object.entries(lock.skills)) {
231
- if (isRecord(entry) && typeof entry.source === "string")
232
- sources.set(`${scope}:${name}`, entry.source);
148
+ for (const plugin of plugins.filter((candidate) => candidate.scope === scope)) {
149
+ for (const skill of plugin.skills) {
150
+ const name = skill.document.name;
151
+ if (!union.has(name))
152
+ union.set(name, { name, kind: "plugin", dir: skill.directory, file: join(skill.directory, "SKILL.md") });
153
+ }
233
154
  }
234
155
  }
235
- return sources;
156
+ return union;
236
157
  }
237
- async #signature(projectRoot) {
158
+ async #signature(workspaceId, projectRoot, definitions) {
238
159
  const hash = createHash("sha256");
160
+ hash.update((await this.#plugins.read(workspaceId)).signature);
239
161
  for (const [name, tree] of await this.#provided()) {
240
162
  hash.update(JSON.stringify([name, "service", tree.document.source]));
241
163
  }
242
- for (const scope of ["project", "global"]) {
164
+ for (const scope of ROOTS) {
243
165
  const root = this.#rootFor(scope, projectRoot);
244
166
  if (root === null)
245
167
  continue;
@@ -249,70 +171,92 @@ export default class SkillsFunctionality {
249
171
  hash.update(JSON.stringify([installed.name, scope, ...identity]));
250
172
  }
251
173
  }
252
- hash.update(JSON.stringify([...(await this.#lockSources(projectRoot))]));
174
+ const settings = environment();
175
+ hash.update(JSON.stringify([...serviceSkills()]));
176
+ for (const [alias, raw] of definitions) {
177
+ const definition = raw;
178
+ hash.update(JSON.stringify([alias, definition, settings.enabled(alias)]));
179
+ if (definition.source === undefined)
180
+ continue;
181
+ try {
182
+ const located = await SkillSource.locate(definition.source, { projectRoot, home: this.#hostPaths.home });
183
+ if (located.kind !== "folder" && located.kind !== "skill-file")
184
+ continue;
185
+ const opened = await SkillSource.open(located);
186
+ try {
187
+ for (const found of opened.skills.filter(({ name }) => name === alias)) {
188
+ hash.update(JSON.stringify([await realpath(found.dir), await readFile(join(found.dir, "SKILL.md"), "utf8")]));
189
+ }
190
+ hash.update(JSON.stringify(opened.invalid));
191
+ }
192
+ finally {
193
+ await opened.close();
194
+ }
195
+ }
196
+ catch (cause) {
197
+ // The preparation reports the source failure; its fingerprint lets restoration trigger a retry.
198
+ hash.update(messageOf(cause));
199
+ }
200
+ }
253
201
  return hash.digest("hex");
254
202
  }
255
- // {§skills-hotload} — filesystem installers operate out of band; before a
256
- // turn assembles its packet the family republishes when the roots changed.
203
+ // {§skills-hotload} — skills placed, edited or deleted out of band are admitted before a turn
204
+ // assembles its packet. Changed content republishes; otherwise the coordinator republishes only
205
+ // when the skills it would publish differ from the published ones.
257
206
  async refreshIfChanged(identity) {
258
- const published = this.#snapshots.get(identity.workspaceId)?.signature;
207
+ const published = this.#snapshots.get(identity.workspaceId);
259
208
  if (published === undefined)
260
209
  return;
261
- const current = await this.#signature(await this.#projectRoot(identity.workspaceId));
262
- if (current === published)
263
- return;
210
+ const current = await this.#signature(identity.workspaceId, await this.#projectRoot(identity.workspaceId), published.definitions);
264
211
  if (this.#handle === null)
265
212
  throw new Error("Skills Functionality is not attached to its coordinator handle.");
266
- await this.#handle.refresh(identity, { gate: "none" });
213
+ await this.#handle.refresh(identity, { gate: "none", ifChanged: current === published.signature });
267
214
  }
268
215
  async available(identity) {
269
- const projectRoot = await this.#projectRoot(identity.workspaceId);
270
- const sources = await this.#lockSources(projectRoot);
271
- const installedSkills = await this.#scan(projectRoot);
272
- const native = [...installedSkills.values()].map((installed) => {
273
- const source = sources.get(`${installed.scope}:${installed.name}`);
274
- const definition = { name: installed.name, scope: installed.scope, ...(source === undefined ? {} : { source }) };
275
- return { alias: installed.name, definition, enabled: true };
276
- });
277
- const provided = [...(await this.#provided()).keys()]
278
- .filter((name) => !installedSkills.has(name))
279
- .map((name) => ({ alias: name, definition: { name, scope: "service" }, enabled: true }));
280
- return [...native, ...provided];
216
+ SkillsFunctionality.validateConfiguration();
217
+ const installedSkills = await this.#scan(identity.workspaceId, await this.#projectRoot(identity.workspaceId));
218
+ const settings = environment();
219
+ const definitions = new Map();
220
+ for (const name of (await this.#provided()).keys()) {
221
+ definitions.set(name, { alias: name, definition: { name }, enabled: settings.enabled(name) });
222
+ }
223
+ for (const { name, dir, file, kind } of installedSkills.values()) {
224
+ definitions.set(name, {
225
+ alias: name, definition: { name, source: dir }, enabled: settings.enabled(name),
226
+ provenance: { kind: kind ?? "file", source: file },
227
+ });
228
+ }
229
+ for (const [name, definition] of serviceSkills()) {
230
+ definitions.set(name, {
231
+ alias: name, definition, enabled: settings.enabled(name),
232
+ provenance: { kind: "environment", source: settings.definitions.get(name).key },
233
+ });
234
+ }
235
+ return [...definitions.values()];
281
236
  }
282
237
  async discover(query, identity) {
283
238
  if (query.configuration !== undefined) {
284
- throw actionError("configuration-unsupported", 400, "Agent Skills discovery takes a registry query or an explicit source; client configuration contributes nothing.", { retryable: false });
239
+ throw actionError("configuration-unsupported", 400, "Agent Skills discovery takes a source; client configuration contributes nothing.", { retryable: false });
285
240
  }
286
- if (query.source !== undefined) {
287
- const source = query.source;
288
- const cwd = this.#cwdFor("project", await this.#projectRoot(identity.workspaceId));
289
- let output;
290
- try {
291
- output = await this.#toolchain.run(["add", source, "--list"], cwd, this.#hostPaths.home);
292
- }
293
- catch (cause) {
294
- throw actionError("discover-failed", 502, `Agent Skills source '${source}' could not be listed: ${messageOf(cause)}`, { source, retryable: true }, cause);
295
- }
296
- return parseListing(output).map((skill) => ({
241
+ if (query.query !== undefined) {
242
+ throw actionError("query-unsupported", 400, "Agent Skills have no standard registry to search; discover takes a source: a git remote as a full https or ssh URL, a folder, a lone SKILL.md, or a zip or tar archive.", { query: query.query, retryable: false });
243
+ }
244
+ if (query.source === undefined)
245
+ return [];
246
+ const source = query.source;
247
+ const located = await SkillSource.locate(source, { projectRoot: await this.#projectRoot(identity.workspaceId), home: this.#hostPaths.home });
248
+ const opened = await SkillSource.open(located);
249
+ try {
250
+ return opened.skills.map((skill) => ({
297
251
  alias: skill.name,
298
- ...(skill.description.length === 0 ? {} : { summary: skill.description }),
299
- definition: { name: skill.name, scope: "project", source },
252
+ summary: skill.description,
253
+ definition: { name: skill.name, source },
300
254
  provenance: { kind: "source", source },
301
255
  }));
302
256
  }
303
- if (query.query === undefined)
304
- return [];
305
- const registry = await this.#toolchain.search(query.query);
306
- return registry.flatMap((skill) => {
307
- if (!SKILL_NAME.test(skill.name))
308
- return [];
309
- return [{
310
- alias: skill.name,
311
- ...(skill.installs === null ? {} : { summary: `${skill.installs} installs` }),
312
- definition: { name: skill.name, scope: "project", source: skill.source },
313
- provenance: { kind: "registry", source: skill.source, reference: skill.reference },
314
- }];
315
- });
257
+ finally {
258
+ await opened.close();
259
+ }
316
260
  }
317
261
  async admit(input, identity) {
318
262
  const params = isRecord(input) ? input : {};
@@ -327,99 +271,103 @@ export default class SkillsFunctionality {
327
271
  if (alias !== definition.name) {
328
272
  throw actionError("alias-mismatch", 400, `Alias '${alias}' must equal the skill name '${definition.name}'.`, { alias, name: definition.name, retryable: false });
329
273
  }
330
- if (definition.scope === "service") {
331
- throw actionError("scope-not-installable", 400, "Service-provided skills can be enabled or disabled; adding a skill requires project or global scope.", { alias, retryable: false });
274
+ if (definition.commit !== undefined) {
275
+ throw actionError("definition-invalid", 400, "The service records the commit a skill was added at; name a ref instead.", { alias, retryable: false });
332
276
  }
333
277
  if (definition.source === undefined) {
334
- throw actionError("source-required", 400, `Adding '${alias}' requires the standard installer source that provides it.`, { alias, retryable: false });
278
+ throw actionError("source-required", 400, `Adding '${alias}' requires the source that provides it.`, { alias, retryable: false });
335
279
  }
336
- if (definition.scope === "project" && await this.#projectRoot(identity.workspaceId) === null) {
337
- throw actionError("project-root-required", 400, `'${alias}' targets the project scope, but this workspace has no project root.`, { alias, recovery: "Add it with scope \"global\" or open a workspace rooted in a project.", retryable: false });
338
- }
339
- return { alias, definition };
340
- }
341
- async #install(definition, root, cwd) {
342
- const args = ["add", definition.source, "--agent", "universal", "--skill", definition.name, "--yes", ...(definition.scope === "global" ? ["--global"] : [])];
343
- let output;
344
- try {
345
- output = await this.#toolchain.run(args, cwd, this.#hostPaths.home);
346
- }
347
- catch (cause) {
348
- throw actionError("install-failed", 502, `Agent Skill '${definition.name}' could not be installed from '${definition.source}': ${messageOf(cause)}`, { name: definition.name, source: definition.source, scope: definition.scope, retryable: true }, cause);
349
- }
350
- const file = join(root, definition.name, "SKILL.md");
351
- if (!(await isFile(file))) {
352
- throw actionError("install-failed", 502, `The installer reported '${definition.name}' from '${definition.source}' but ${file} does not exist.`, { name: definition.name, source: definition.source, scope: definition.scope, output: stripVTControlCharacters(output).trim().slice(-2000), retryable: true });
280
+ const projectRoot = await this.#projectRoot(identity.workspaceId);
281
+ const located = await SkillSource.locate(definition.source, { projectRoot, home: this.#hostPaths.home });
282
+ if (located.kind !== "git") {
283
+ if (definition.ref !== undefined)
284
+ throw actionError("definition-invalid", 400, "A ref names a branch or tag of a git source.", { alias, retryable: false });
285
+ return { alias, definition: { ...definition, source: located.location } };
353
286
  }
354
- return { name: definition.name, scope: definition.scope, dir: join(root, definition.name), file };
287
+ return { alias, definition: { ...definition, commit: await SkillSource.resolveCommit(located.location, definition.ref) } };
355
288
  }
356
- // {§skills-remove} — the coordinator forgets the workspace definition;
357
- // the adapter uninstalls what that definition installed at its scope.
358
- async forget(source, identity) {
359
- const definition = source.definition;
360
- const projectRoot = await this.#projectRoot(identity.workspaceId);
361
- const root = this.#rootFor(definition.scope, projectRoot);
362
- if (root === null)
363
- return;
364
- const dir = join(root, definition.name);
365
- if (!(await exists(dir)))
366
- return;
289
+ // {§skills-sources} — complete source identity owns a reusable materialization within one workspace.
290
+ async #loadSource(definition, workspaceId, projectRoot) {
291
+ const key = createHash("sha256").update(JSON.stringify([
292
+ definition.name, definition.source, definition.ref ?? null, definition.commit ?? null,
293
+ ])).digest("hex");
294
+ const root = join(await this.#storage.directory(workspaceId, SKILLS_OWNER), key);
295
+ const cached = join(root, definition.name);
296
+ if (await isFile(join(cached, "SKILL.md")))
297
+ return this.#loadDirectory(definition.name, cached);
298
+ const located = await SkillSource.locate(definition.source, { projectRoot, home: this.#hostPaths.home });
299
+ const opened = await SkillSource.open(located, {
300
+ ...(definition.ref === undefined ? {} : { ref: definition.ref }),
301
+ ...(definition.commit === undefined ? {} : { commit: definition.commit }),
302
+ });
367
303
  try {
368
- await this.#toolchain.run(["remove", definition.name, "--yes", ...(definition.scope === "global" ? ["--global"] : [])], this.#cwdFor(definition.scope, projectRoot), this.#hostPaths.home);
304
+ const matches = opened.skills.filter(({ name }) => name === definition.name);
305
+ if (matches.length > 1) {
306
+ throw actionError("skill-ambiguous", 409, `'${definition.source}' carries ${matches.length} skills named '${definition.name}'.`, {
307
+ name: definition.name, source: definition.source, retryable: false,
308
+ });
309
+ }
310
+ const match = matches[0];
311
+ if (match === undefined) {
312
+ const broken = opened.invalid.find(({ dir }) => dir.endsWith(`/${definition.name}`));
313
+ if (broken !== undefined) {
314
+ throw actionError("skill-invalid", 422, `Agent Skill '${definition.name}' in '${definition.source}' is not a valid standard skill: ${broken.reason}`, {
315
+ name: definition.name, source: definition.source, retryable: false,
316
+ });
317
+ }
318
+ throw actionError("skill-not-found", 404, `'${definition.source}' carries no Agent Skill named '${definition.name}'.`, {
319
+ name: definition.name, source: definition.source, retryable: false,
320
+ });
321
+ }
322
+ if (located.kind === "folder" || located.kind === "skill-file") {
323
+ await SkillSource.assertInward(match.dir);
324
+ return await this.#loadDirectory(definition.name, match.dir);
325
+ }
326
+ let dir;
327
+ try {
328
+ dir = await SkillSource.install(match, root);
329
+ }
330
+ catch (cause) {
331
+ if (cause instanceof SkillsActionError)
332
+ throw cause;
333
+ throw actionError("install-failed", 500, `Agent Skill '${definition.name}' could not be placed under ${root}: ${messageOf(cause)}`, {
334
+ name: definition.name, retryable: false,
335
+ }, cause);
336
+ }
337
+ return await this.#loadDirectory(definition.name, dir);
369
338
  }
370
- catch (cause) {
371
- throw actionError("uninstall-failed", 502, `Agent Skill '${definition.name}' could not be removed from its ${definition.scope} root: ${messageOf(cause)}`, { name: definition.name, scope: definition.scope, retryable: true }, cause);
339
+ finally {
340
+ await opened.close();
372
341
  }
373
- if (await exists(dir))
374
- throw actionError("uninstall-failed", 502, `The installer reported removal of '${definition.name}' but ${dir} still exists.`, { name: definition.name, scope: definition.scope, retryable: true });
375
342
  }
376
- async #locate(alias, definition, installed, projectRoot) {
377
- const shadowing = installed.get(alias);
378
- if (shadowing === undefined || shadowing.scope === definition.scope)
379
- return shadowing;
380
- // The workspace definition names a root below the one currently
381
- // shadowing that name; the definition's scope is truth.
382
- const root = this.#rootFor(definition.scope, projectRoot);
383
- if (root === null)
384
- return undefined;
385
- const file = join(root, alias, "SKILL.md");
386
- return (await isFile(file)) ? { name: alias, scope: definition.scope, dir: join(root, alias), file } : undefined;
387
- }
388
- async #loadInstalled(alias, definition, installed, projectRoot) {
389
- let located = await this.#locate(alias, definition, installed, projectRoot);
390
- if (located === undefined) {
391
- const root = this.#rootFor(definition.scope, projectRoot);
392
- if (root === null)
393
- throw actionError("project-root-required", 409, `'${alias}' targets the project scope, but this workspace has no project root.`, { name: alias, retryable: false });
394
- if (definition.source === undefined)
395
- throw actionError("skill-missing", 404, `Agent Skill '${alias}' is not installed under its ${definition.scope} root.`, { name: alias, scope: definition.scope, root, retryable: false });
396
- located = await this.#install(definition, root, this.#cwdFor(definition.scope, projectRoot));
397
- }
343
+ async #loadDirectory(alias, dir) {
398
344
  try {
399
- return await SkillDirectory.load(located.dir);
345
+ return await SkillDirectory.load(dir);
400
346
  }
401
347
  catch (cause) {
402
- throw actionError("skill-invalid", 422, `Agent Skill '${alias}' is not a valid standard skill: ${messageOf(cause)}`, { name: alias, scope: located.scope, path: located.file, retryable: false }, cause);
348
+ throw actionError("skill-invalid", 422, `Agent Skill '${alias}' is not a valid standard skill: ${messageOf(cause)}`, { name: alias, path: join(dir, "SKILL.md"), retryable: false }, cause);
403
349
  }
404
350
  }
405
351
  async prepare(preparation) {
406
352
  const projectRoot = await this.#projectRoot(preparation.workspaceId);
407
- const installed = await this.#scan(projectRoot);
353
+ const definitions = new Map([...preparation.enabled].map(([alias, { definition }]) => [alias, definition]));
354
+ // Read before the skills it describes, so content changed while they load is seen as changed.
355
+ const signature = await this.#signature(preparation.workspaceId, projectRoot, definitions);
408
356
  const provided = await this.#provided();
409
357
  const previous = preparation.previous;
410
358
  const carried = new Set(previous?.unavailable ?? []);
411
359
  const outcomes = new Map();
412
360
  const trees = new Map();
413
- for (const [alias, raw] of preparation.enabled) {
361
+ for (const [alias, raw] of definitions) {
362
+ preparation.progress(alias);
414
363
  const definition = raw;
415
364
  try {
416
- const tree = definition.scope === "service" ? provided.get(alias)
417
- : await this.#loadInstalled(alias, definition, installed, projectRoot);
365
+ const tree = definition.source === undefined ? provided.get(alias)
366
+ : await this.#loadSource(definition, preparation.workspaceId, projectRoot);
418
367
  if (tree === undefined)
419
368
  throw actionError("skill-missing", 404, `Agent Skill '${alias}' is not provided by this service.`, { name: alias, retryable: false });
420
369
  trees.set(new URL(`skill://${alias}/`).hostname, tree);
421
370
  outcomes.set(alias, { state: "active", detail: {
422
- scope: definition.scope,
423
371
  ...(tree instanceof SkillDirectory ? { path: tree.directory } : {}),
424
372
  description: tree.document.description,
425
373
  } });
@@ -435,10 +383,10 @@ export default class SkillsFunctionality {
435
383
  outcomes.set(alias, { state: "unavailable", problem: structuredClone(cause.problem) });
436
384
  }
437
385
  }
438
- const signature = await this.#signature(projectRoot);
439
386
  const snapshot = {
440
387
  signature,
441
388
  trees,
389
+ definitions,
442
390
  unavailable: [...outcomes].filter(([, outcome]) => outcome.state === "unavailable").map(([alias]) => alias),
443
391
  };
444
392
  const { workspaceId } = preparation;