@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.
- package/.env.defaults +17 -15
- package/INSTALL.md +96 -20
- package/README.md +2 -2
- package/SPEC.md +517 -193
- package/dist/Paths.d.ts +1 -0
- package/dist/Paths.d.ts.map +1 -1
- package/dist/Paths.js +1 -0
- package/dist/Paths.js.map +1 -1
- package/dist/build-info.json +1 -1
- package/dist/content/read-projector.d.ts.map +1 -1
- package/dist/content/read-projector.js +19 -0
- package/dist/content/read-projector.js.map +1 -1
- package/dist/core/AdministrativeLoop.d.ts +1 -1
- package/dist/core/AdministrativeLoop.d.ts.map +1 -1
- package/dist/core/AdministrativeLoop.js +5 -5
- package/dist/core/AdministrativeLoop.js.map +1 -1
- package/dist/core/AdmittedTurnExecutor.d.ts.map +1 -1
- package/dist/core/AdmittedTurnExecutor.js +1 -0
- package/dist/core/AdmittedTurnExecutor.js.map +1 -1
- package/dist/core/CapabilityResolver.js +1 -1
- package/dist/core/CapabilityResolver.js.map +1 -1
- package/dist/core/DataStatementRunner.d.ts.map +1 -1
- package/dist/core/DataStatementRunner.js +1 -3
- package/dist/core/DataStatementRunner.js.map +1 -1
- package/dist/core/Dispatcher.d.ts +1 -1
- package/dist/core/Dispatcher.d.ts.map +1 -1
- package/dist/core/Dispatcher.js +76 -21
- package/dist/core/Dispatcher.js.map +1 -1
- package/dist/core/Dispatcher.sql +0 -7
- package/dist/core/Engine.d.ts +2 -2
- package/dist/core/Engine.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.d.ts +16 -4
- package/dist/core/ExecutorRegistry.d.ts.map +1 -1
- package/dist/core/ExecutorRegistry.js +31 -31
- package/dist/core/ExecutorRegistry.js.map +1 -1
- package/dist/core/HostPaths.d.ts +7 -1
- package/dist/core/HostPaths.d.ts.map +1 -1
- package/dist/core/HostPaths.js +25 -12
- package/dist/core/HostPaths.js.map +1 -1
- package/dist/core/LoopDriver.d.ts.map +1 -1
- package/dist/core/LoopDriver.js +11 -2
- package/dist/core/LoopDriver.js.map +1 -1
- package/dist/core/LoopLifecycle.d.ts +1 -1
- package/dist/core/LoopLifecycle.js +1 -1
- package/dist/core/LoopLifecycle.sql +2 -2
- package/dist/core/LoopPolicies.d.ts.map +1 -1
- package/dist/core/LoopPolicies.js +12 -7
- package/dist/core/LoopPolicies.js.map +1 -1
- package/dist/core/MembershipMaterialization.js +1 -1
- package/dist/core/MembershipMaterialization.js.map +1 -1
- package/dist/core/OperatorConfig.d.ts.map +1 -1
- package/dist/core/OperatorConfig.js +97 -51
- package/dist/core/OperatorConfig.js.map +1 -1
- package/dist/core/PacketBuilder.d.ts +1 -0
- package/dist/core/PacketBuilder.d.ts.map +1 -1
- package/dist/core/PacketBuilder.js +27 -25
- package/dist/core/PacketBuilder.js.map +1 -1
- package/dist/core/ReasoningView.d.ts +1 -1
- package/dist/core/ReasoningView.d.ts.map +1 -1
- package/dist/core/ReasoningView.js +4 -5
- package/dist/core/ReasoningView.js.map +1 -1
- package/dist/core/SchemeRegistry.d.ts.map +1 -1
- package/dist/core/SchemeRegistry.js +3 -2
- package/dist/core/SchemeRegistry.js.map +1 -1
- package/dist/core/Turn.d.ts +1 -1
- package/dist/core/Turn.d.ts.map +1 -1
- package/dist/core/Turn.js +2 -2
- package/dist/core/Turn.js.map +1 -1
- package/dist/core/Turn.sql +1 -1
- package/dist/core/TurnDispositionHandler.d.ts +1 -1
- package/dist/core/TurnDispositionHandler.d.ts.map +1 -1
- package/dist/core/TurnDispositionHandler.js +6 -15
- package/dist/core/TurnDispositionHandler.js.map +1 -1
- package/dist/core/TurnMaterialization.js +1 -1
- package/dist/core/TurnMaterialization.js.map +1 -1
- package/dist/core/TurnOps.d.ts +1 -0
- package/dist/core/TurnOps.d.ts.map +1 -1
- package/dist/core/TurnOps.js +18 -0
- package/dist/core/TurnOps.js.map +1 -1
- package/dist/core/TurnRunner.d.ts.map +1 -1
- package/dist/core/TurnRunner.js +53 -32
- package/dist/core/TurnRunner.js.map +1 -1
- package/dist/core/env-defaults.d.ts +12 -1
- package/dist/core/env-defaults.d.ts.map +1 -1
- package/dist/core/env-defaults.js +50 -7
- package/dist/core/env-defaults.js.map +1 -1
- package/dist/core/file-creation-policy.d.ts.map +1 -1
- package/dist/core/file-creation-policy.js +2 -1
- package/dist/core/file-creation-policy.js.map +1 -1
- package/dist/core/git-membership.d.ts.map +1 -1
- package/dist/core/git-membership.js +0 -3
- package/dist/core/git-membership.js.map +1 -1
- package/dist/core/namespace.d.ts +1 -0
- package/dist/core/namespace.d.ts.map +1 -1
- package/dist/core/namespace.js +21 -75
- package/dist/core/namespace.js.map +1 -1
- package/dist/core/notifications.d.ts +2 -0
- package/dist/core/notifications.d.ts.map +1 -1
- package/dist/core/packet-wire.d.ts +1 -0
- package/dist/core/packet-wire.d.ts.map +1 -1
- package/dist/core/packet-wire.js +28 -7
- package/dist/core/packet-wire.js.map +1 -1
- package/dist/core/results.d.ts +2 -0
- package/dist/core/results.d.ts.map +1 -1
- package/dist/core/results.js +7 -0
- package/dist/core/results.js.map +1 -1
- package/dist/digest/Digest.d.ts.map +1 -1
- package/dist/digest/Digest.js +1 -2
- package/dist/digest/Digest.js.map +1 -1
- package/dist/digest/DigestEvidence.d.ts +1 -0
- package/dist/digest/DigestEvidence.d.ts.map +1 -1
- package/dist/digest/DigestEvidence.js +8 -0
- package/dist/digest/DigestEvidence.js.map +1 -1
- package/dist/digest/DigestRender.d.ts.map +1 -1
- package/dist/digest/DigestRender.js +23 -21
- package/dist/digest/DigestRender.js.map +1 -1
- package/dist/digest/digest-rows.d.ts +2 -1
- package/dist/digest/digest-rows.d.ts.map +1 -1
- package/dist/digest/digest.sql +4 -0
- package/dist/observe/init.d.ts +1 -0
- package/dist/observe/init.d.ts.map +1 -1
- package/dist/observe/init.js +5 -1
- package/dist/observe/init.js.map +1 -1
- package/dist/schemes/EffectPolicy.d.ts.map +1 -1
- package/dist/schemes/EffectPolicy.js +4 -4
- package/dist/schemes/EffectPolicy.js.map +1 -1
- package/dist/schemes/Exec.d.ts +1 -0
- package/dist/schemes/Exec.d.ts.map +1 -1
- package/dist/schemes/Exec.js +30 -14
- package/dist/schemes/Exec.js.map +1 -1
- package/dist/schemes/ExecScheduler.d.ts.map +1 -1
- package/dist/schemes/ExecScheduler.js +4 -3
- package/dist/schemes/ExecScheduler.js.map +1 -1
- package/dist/schemes/ExecScratch.d.ts.map +1 -1
- package/dist/schemes/ExecScratch.js +2 -1
- package/dist/schemes/ExecScratch.js.map +1 -1
- package/dist/schemes/File.d.ts.map +1 -1
- package/dist/schemes/File.js +21 -22
- package/dist/schemes/File.js.map +1 -1
- package/dist/schemes/_entry-find.d.ts +1 -0
- package/dist/schemes/_entry-find.d.ts.map +1 -1
- package/dist/schemes/_entry-find.js +4 -3
- package/dist/schemes/_entry-find.js.map +1 -1
- package/dist/schemes/_path-scope.d.ts +2 -2
- package/dist/schemes/_path-scope.d.ts.map +1 -1
- package/dist/schemes/_path-scope.js +23 -2
- package/dist/schemes/_path-scope.js.map +1 -1
- package/dist/server/AgentRoots.d.ts +6 -0
- package/dist/server/AgentRoots.d.ts.map +1 -0
- package/dist/server/AgentRoots.js +25 -0
- package/dist/server/AgentRoots.js.map +1 -0
- package/dist/server/ConfigurationDiagnostics.d.ts +12 -0
- package/dist/server/ConfigurationDiagnostics.d.ts.map +1 -0
- package/dist/server/ConfigurationDiagnostics.js +40 -0
- package/dist/server/ConfigurationDiagnostics.js.map +1 -0
- package/dist/server/Daemon.d.ts +11 -5
- package/dist/server/Daemon.d.ts.map +1 -1
- package/dist/server/Daemon.js +125 -48
- package/dist/server/Daemon.js.map +1 -1
- package/dist/server/DaemonModule.d.ts +11 -4
- package/dist/server/DaemonModule.d.ts.map +1 -1
- package/dist/server/EnvFunctionality.d.ts.map +1 -1
- package/dist/server/EnvFunctionality.js +5 -2
- package/dist/server/EnvFunctionality.js.map +1 -1
- package/dist/server/Functionality.d.ts +6 -0
- package/dist/server/Functionality.d.ts.map +1 -1
- package/dist/server/Functionality.js +173 -37
- package/dist/server/Functionality.js.map +1 -1
- package/dist/server/FunctionalityManager.js +6 -6
- package/dist/server/FunctionalityManager.js.map +1 -1
- package/dist/server/MembersFunctionality.d.ts.map +1 -1
- package/dist/server/MembersFunctionality.js +13 -41
- package/dist/server/MembersFunctionality.js.map +1 -1
- package/dist/server/PluginSources.d.ts +20 -0
- package/dist/server/PluginSources.d.ts.map +1 -0
- package/dist/server/PluginSources.js +52 -0
- package/dist/server/PluginSources.js.map +1 -0
- package/dist/server/PlurnkSkill.js +1 -1
- package/dist/server/PlurnkSkill.js.map +1 -1
- package/dist/server/Retention.d.ts.map +1 -1
- package/dist/server/Retention.js +11 -30
- package/dist/server/Retention.js.map +1 -1
- package/dist/server/ServiceModules.d.ts +1 -0
- package/dist/server/ServiceModules.d.ts.map +1 -1
- package/dist/server/ServiceModules.js +9 -3
- package/dist/server/ServiceModules.js.map +1 -1
- package/dist/server/SkillSource.d.ts +37 -0
- package/dist/server/SkillSource.d.ts.map +1 -0
- package/dist/server/SkillSource.js +267 -0
- package/dist/server/SkillSource.js.map +1 -0
- package/dist/server/SkillsFunctionality.d.ts +8 -27
- package/dist/server/SkillsFunctionality.d.ts.map +1 -1
- package/dist/server/SkillsFunctionality.js +218 -270
- package/dist/server/SkillsFunctionality.js.map +1 -1
- package/dist/server/WorkerModelResolver.d.ts +1 -2
- package/dist/server/WorkerModelResolver.d.ts.map +1 -1
- package/dist/server/WorkerModelResolver.js +18 -19
- package/dist/server/WorkerModelResolver.js.map +1 -1
- package/dist/server/WorkspacePlugins.d.ts +25 -0
- package/dist/server/WorkspacePlugins.d.ts.map +1 -0
- package/dist/server/WorkspacePlugins.js +43 -0
- package/dist/server/WorkspacePlugins.js.map +1 -0
- package/dist/server/dispatch-as-plurnk.d.ts.map +1 -1
- package/dist/server/dispatch-as-plurnk.js +2 -1
- package/dist/server/dispatch-as-plurnk.js.map +1 -1
- package/dist/server/envelope.js +1 -1
- package/dist/server/envelope.js.map +1 -1
- package/dist/server/loop-model.d.ts.map +1 -1
- package/dist/server/loop-model.js +1 -2
- package/dist/server/loop-model.js.map +1 -1
- package/dist/server/module-discovery.d.ts +6 -0
- package/dist/server/module-discovery.d.ts.map +1 -1
- package/dist/server/module-discovery.js +46 -24
- package/dist/server/module-discovery.js.map +1 -1
- package/dist/server/skills-problems.d.ts +8 -0
- package/dist/server/skills-problems.d.ts.map +1 -0
- package/dist/server/skills-problems.js +18 -0
- package/dist/server/skills-problems.js.map +1 -0
- package/dist/service.d.ts.map +1 -1
- package/dist/service.js +55 -45
- package/dist/service.js.map +1 -1
- package/docs/env.md +7 -8
- package/docs/members.md +4 -3
- package/docs/skills.md +45 -22
- package/package.json +37 -35
- package/plurnk.service +29 -0
|
@@ -1,159 +1,101 @@
|
|
|
1
1
|
// {§skills-functionality} — standard Agent Skills as one workspace Functionality
|
|
2
|
-
// family.
|
|
3
|
-
// workspace
|
|
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 {
|
|
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
|
-
|
|
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
|
-
//
|
|
22
|
-
const
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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
|
-
|
|
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
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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
|
-
|
|
67
|
-
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
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
|
|
59
|
+
throw new ConfigurationError(key, `${key}: invalid skill source or ref.`, { cause });
|
|
116
60
|
}
|
|
117
|
-
|
|
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",
|
|
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: "`
|
|
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
|
-
#
|
|
78
|
+
#storage;
|
|
149
79
|
#provided;
|
|
80
|
+
#plugins;
|
|
150
81
|
#snapshots = new Map();
|
|
151
82
|
#handle = null;
|
|
152
|
-
constructor({ db, hostPaths = new HostPaths(),
|
|
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
|
|
170
|
-
|
|
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,
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
for (const installed of
|
|
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
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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} —
|
|
256
|
-
//
|
|
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)
|
|
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
|
-
|
|
270
|
-
const
|
|
271
|
-
const
|
|
272
|
-
const
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
})
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
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
|
|
239
|
+
throw actionError("configuration-unsupported", 400, "Agent Skills discovery takes a source; client configuration contributes nothing.", { retryable: false });
|
|
285
240
|
}
|
|
286
|
-
if (query.
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
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
|
-
|
|
299
|
-
definition: { name: skill.name,
|
|
252
|
+
summary: skill.description,
|
|
253
|
+
definition: { name: skill.name, source },
|
|
300
254
|
provenance: { kind: "source", source },
|
|
301
255
|
}));
|
|
302
256
|
}
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
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.
|
|
331
|
-
throw actionError("
|
|
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
|
|
278
|
+
throw actionError("source-required", 400, `Adding '${alias}' requires the source that provides it.`, { alias, retryable: false });
|
|
335
279
|
}
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
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 {
|
|
287
|
+
return { alias, definition: { ...definition, commit: await SkillSource.resolveCommit(located.location, definition.ref) } };
|
|
355
288
|
}
|
|
356
|
-
// {§skills-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const root = this.#
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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
|
-
|
|
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
|
-
|
|
371
|
-
|
|
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 #
|
|
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(
|
|
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,
|
|
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
|
|
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
|
|
361
|
+
for (const [alias, raw] of definitions) {
|
|
362
|
+
preparation.progress(alias);
|
|
414
363
|
const definition = raw;
|
|
415
364
|
try {
|
|
416
|
-
const tree = definition.
|
|
417
|
-
: await this.#
|
|
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;
|