agent-skillboard 0.2.18 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +129 -258
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +35 -109
- package/docs/capabilities.md +17 -104
- package/docs/install.md +36 -485
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +115 -363
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +60 -284
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +31 -264
- package/examples/v2-multi-source.config.yaml +35 -0
- package/examples/v2-policy-error.config.yaml +6 -0
- package/package.json +1 -1
- package/src/advisor/actions.mjs +102 -6
- package/src/advisor/application-commands.mjs +10 -9
- package/src/advisor/apply-action.mjs +74 -1
- package/src/advisor/guidance.mjs +24 -16
- package/src/advisor/schema.mjs +17 -6
- package/src/advisor/skills.mjs +18 -5
- package/src/advisor.mjs +27 -9
- package/src/agent-integration-cli.mjs +13 -4
- package/src/agent-integration-content.mjs +21 -11
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-inventory-platforms.mjs +10 -0
- package/src/agent-inventory.mjs +23 -1
- package/src/agent-skill-import.mjs +2 -2
- package/src/audit-paths.mjs +42 -0
- package/src/brief-cli.mjs +3 -2
- package/src/brief-renderer.mjs +1 -0
- package/src/cli.mjs +498 -232
- package/src/compatibility.mjs +24 -0
- package/src/control/can-use-guard.mjs +21 -1
- package/src/control/config-write.mjs +32 -2
- package/src/control/skill-crud.mjs +5 -0
- package/src/control/v2-guard.mjs +175 -0
- package/src/control/v2-skill-crud.mjs +32 -0
- package/src/control/v2-skill-forget.mjs +38 -0
- package/src/control/variant-status.mjs +47 -1
- package/src/control.mjs +55 -0
- package/src/doctor.mjs +63 -4
- package/src/domain/v2-policy.mjs +111 -0
- package/src/hook-plan.mjs +33 -3
- package/src/impact.mjs +52 -29
- package/src/index.mjs +25 -1
- package/src/init.mjs +50 -34
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +163 -18
- package/src/lifecycle-cli.mjs +40 -12
- package/src/lifecycle-content.mjs +52 -67
- package/src/migration/v1-to-v2.mjs +212 -0
- package/src/migration/v2-files.mjs +211 -0
- package/src/migration/v2-journal.mjs +169 -0
- package/src/migration/v2-projection.mjs +108 -0
- package/src/migration/v2-transaction.mjs +205 -0
- package/src/policy.mjs +3 -0
- package/src/reconcile.mjs +139 -111
- package/src/report.mjs +168 -148
- package/src/review.mjs +2 -0
- package/src/route-advisory.mjs +47 -2
- package/src/route-selection.mjs +38 -2
- package/src/route.mjs +62 -2
- package/src/shared-skill.mjs +301 -0
- package/src/source-digest.mjs +42 -0
- package/src/source-profiles.mjs +171 -144
- package/src/source-verification.mjs +32 -48
- package/src/uninstall.mjs +22 -0
- package/src/user-state-paths.mjs +19 -0
- package/src/user-uninstall.mjs +146 -0
- package/src/workspace.mjs +119 -79
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { lstat, readdir, readFile, rm } from "node:fs/promises";
|
|
2
|
+
import { basename, join, resolve } from "node:path";
|
|
3
|
+
import { uninstallAgentIntegration } from "./agent-integration-files.mjs";
|
|
4
|
+
import { resolveSetupHome } from "./agent-integration-home.mjs";
|
|
5
|
+
import {
|
|
6
|
+
agentSkillRootCandidates,
|
|
7
|
+
setupAgentSkillTargets,
|
|
8
|
+
supportedAgentNames
|
|
9
|
+
} from "./agent-skill-roots.mjs";
|
|
10
|
+
|
|
11
|
+
const SHARE_MARKER = ".skillboard-share.json";
|
|
12
|
+
|
|
13
|
+
export async function uninstallUser(options = {}) {
|
|
14
|
+
const env = options.env ?? process.env;
|
|
15
|
+
const home = await resolveSetupHome(env, options.runtime ?? {});
|
|
16
|
+
const planned = await planUserUninstall(home, env);
|
|
17
|
+
if (options.dryRun) {
|
|
18
|
+
const guidance = await uninstallAgentIntegration(planned.guidanceTargets, true);
|
|
19
|
+
return result(home, true, planned.managedCopies, guidance, planned.statePaths, planned.preserved);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const removedCopies = [];
|
|
23
|
+
const preserved = [...planned.preserved];
|
|
24
|
+
for (const copy of planned.managedCopies) {
|
|
25
|
+
if (await removeManagedCopy(copy)) {
|
|
26
|
+
removedCopies.push(copy);
|
|
27
|
+
} else {
|
|
28
|
+
preserved.push(copy.path);
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const guidance = await uninstallAgentIntegration(planned.guidanceTargets, false);
|
|
32
|
+
const removedState = [];
|
|
33
|
+
for (const state of planned.statePaths) {
|
|
34
|
+
if (await removeStatePath(state)) removedState.push(state);
|
|
35
|
+
else preserved.push(state);
|
|
36
|
+
}
|
|
37
|
+
return result(home, false, removedCopies, guidance, removedState, preserved);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async function planUserUninstall(home, env) {
|
|
41
|
+
const preserved = [];
|
|
42
|
+
const roots = [{ root: join(home, ".agents", "shared-skills"), agent: null }];
|
|
43
|
+
for (const agent of supportedAgentNames()) {
|
|
44
|
+
for (const candidate of await agentSkillRootCandidates(agent, home, env)) {
|
|
45
|
+
roots.push({ root: candidate.skillRoot, agent });
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const managedCopies = [];
|
|
49
|
+
const seenRoots = new Set();
|
|
50
|
+
for (const entry of roots) {
|
|
51
|
+
const root = resolve(entry.root);
|
|
52
|
+
if (seenRoots.has(root)) continue;
|
|
53
|
+
seenRoots.add(root);
|
|
54
|
+
managedCopies.push(...await managedCopiesInRoot(root, entry.agent, preserved));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const guidanceTargets = [];
|
|
58
|
+
for (const agent of supportedAgentNames()) {
|
|
59
|
+
guidanceTargets.push(...await setupAgentSkillTargets(agent, home, env));
|
|
60
|
+
}
|
|
61
|
+
return {
|
|
62
|
+
managedCopies: managedCopies.sort((left, right) => left.path.localeCompare(right.path)),
|
|
63
|
+
guidanceTargets,
|
|
64
|
+
statePaths: await existingStatePaths(home),
|
|
65
|
+
preserved: [...new Set(preserved)].sort()
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async function managedCopiesInRoot(root, agent, preserved) {
|
|
70
|
+
const stats = await pathStats(root);
|
|
71
|
+
if (stats === null) return [];
|
|
72
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) {
|
|
73
|
+
preserved.push(root);
|
|
74
|
+
return [];
|
|
75
|
+
}
|
|
76
|
+
const copies = [];
|
|
77
|
+
const entries = await readdir(root, { withFileTypes: true });
|
|
78
|
+
for (const entry of entries) {
|
|
79
|
+
if (!entry.isDirectory() || entry.isSymbolicLink()) continue;
|
|
80
|
+
const path = join(root, entry.name);
|
|
81
|
+
const marker = await readManagedMarker(path, entry.name, agent);
|
|
82
|
+
if (marker !== null) copies.push({ path, skill: entry.name, agent, mode: marker.mode });
|
|
83
|
+
}
|
|
84
|
+
return copies;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
async function readManagedMarker(path, skill, agent) {
|
|
88
|
+
const markerPath = join(path, SHARE_MARKER);
|
|
89
|
+
const stats = await pathStats(markerPath);
|
|
90
|
+
if (stats === null || stats.isSymbolicLink() || !stats.isFile()) return null;
|
|
91
|
+
const value = await readFile(markerPath, "utf8").then(JSON.parse, () => null);
|
|
92
|
+
if (value?.version !== 1 || value.managed_by !== "skillboard" || value.skill !== skill) return null;
|
|
93
|
+
if (agent === null && value.mode === "shared-source") return value;
|
|
94
|
+
if (agent !== null && value.mode === "agent-copy" && value.target_agent === agent) return value;
|
|
95
|
+
return null;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function removeManagedCopy(copy) {
|
|
99
|
+
const stats = await pathStats(copy.path);
|
|
100
|
+
if (stats === null) return true;
|
|
101
|
+
if (stats.isSymbolicLink() || !stats.isDirectory()) return false;
|
|
102
|
+
const marker = await readManagedMarker(copy.path, copy.skill, copy.agent);
|
|
103
|
+
if (marker === null || marker.mode !== copy.mode) return false;
|
|
104
|
+
await rm(copy.path, { recursive: true, force: true });
|
|
105
|
+
return true;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
async function existingStatePaths(home) {
|
|
109
|
+
const result = [];
|
|
110
|
+
for (const path of [join(home, "skillboard.config.yaml"), join(home, ".skillboard")]) {
|
|
111
|
+
if (await pathStats(path) !== null) result.push(path);
|
|
112
|
+
}
|
|
113
|
+
return result;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async function removeStatePath(path) {
|
|
117
|
+
const stats = await pathStats(path);
|
|
118
|
+
if (stats === null) return true;
|
|
119
|
+
const name = basename(path);
|
|
120
|
+
const supported = name === "skillboard.config.yaml"
|
|
121
|
+
? stats.isFile() || stats.isSymbolicLink()
|
|
122
|
+
: name === ".skillboard" && (stats.isDirectory() || stats.isSymbolicLink());
|
|
123
|
+
if (!supported) return false;
|
|
124
|
+
await rm(path, { recursive: stats.isDirectory(), force: true });
|
|
125
|
+
return true;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function result(home, dryRun, managedCopies, guidance, statePaths, preserved) {
|
|
129
|
+
return {
|
|
130
|
+
ok: true,
|
|
131
|
+
mode: "user",
|
|
132
|
+
dry_run: dryRun,
|
|
133
|
+
home,
|
|
134
|
+
managed_copies: managedCopies,
|
|
135
|
+
guidance,
|
|
136
|
+
state_paths: statePaths,
|
|
137
|
+
preserved: [...new Set([...preserved, ...guidance.preserved])].sort()
|
|
138
|
+
};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
async function pathStats(path) {
|
|
142
|
+
return await lstat(path).catch((error) => {
|
|
143
|
+
if (error?.code === "ENOENT") return null;
|
|
144
|
+
throw error;
|
|
145
|
+
});
|
|
146
|
+
}
|
package/src/workspace.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { readdir, readFile, realpath, stat } from "node:fs/promises";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { dirname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
|
-
import YAML from "yaml";
|
|
4
|
+
import YAML from "yaml";
|
|
5
5
|
import {
|
|
6
6
|
readBoolean,
|
|
7
7
|
readOptionalRecord,
|
|
@@ -19,16 +19,26 @@ import {
|
|
|
19
19
|
} from "./domain/constants.mjs";
|
|
20
20
|
import { parseInstallUnits } from "./install-units.mjs";
|
|
21
21
|
import { normalizeSkillPath } from "./skill-paths.mjs";
|
|
22
|
+
import { parseV2Policy, serializeV2Policy } from "./domain/v2-policy.mjs";
|
|
23
|
+
import { loadV2InventoryIndex } from "./control/v2-guard.mjs";
|
|
24
|
+
import { compatibilityForVersion } from "./compatibility.mjs";
|
|
22
25
|
|
|
26
|
+
export { serializeV2Policy };
|
|
27
|
+
|
|
28
|
+
/** @returns {Promise<any>} */
|
|
23
29
|
export async function loadWorkspace(options) {
|
|
24
|
-
const configText = await readFile(options.configPath, "utf8");
|
|
25
|
-
const parsed = YAML.parse(configText);
|
|
30
|
+
const configText = await readFile(options.configPath, "utf8");
|
|
31
|
+
const parsed = YAML.parse(configText);
|
|
26
32
|
const config = requireRecord(parsed, "config root");
|
|
27
33
|
const version = parseVersion(config.version);
|
|
34
|
+
if (version === 2) {
|
|
35
|
+
return loadV2Workspace(config, options);
|
|
36
|
+
}
|
|
28
37
|
const skills = parseSkills(config.skills);
|
|
29
38
|
const installUnits = parseInstallUnits(config.install_units);
|
|
30
39
|
return {
|
|
31
40
|
version,
|
|
41
|
+
compatibility: compatibilityForVersion(version),
|
|
32
42
|
defaults: parseDefaults(config.defaults),
|
|
33
43
|
installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
|
|
34
44
|
configPath: options.configPath,
|
|
@@ -44,6 +54,29 @@ export async function loadWorkspace(options) {
|
|
|
44
54
|
};
|
|
45
55
|
}
|
|
46
56
|
|
|
57
|
+
async function loadV2Workspace(config, options) {
|
|
58
|
+
const { workflows, skills } = parseV2Policy(config);
|
|
59
|
+
const inventoryPath = options.inventoryPath ?? join(dirname(options.configPath), ".skillboard", "inventory.json");
|
|
60
|
+
const inventory = await loadV2InventoryIndex(inventoryPath);
|
|
61
|
+
return {
|
|
62
|
+
version: 2,
|
|
63
|
+
compatibility: null,
|
|
64
|
+
defaults: {},
|
|
65
|
+
installedSkills: await discoverInstalledSkills(options.skillsRoot, skills, {
|
|
66
|
+
configPath: options.configPath,
|
|
67
|
+
env: options.env ?? process.env,
|
|
68
|
+
home: options.home,
|
|
69
|
+
installUnits: []
|
|
70
|
+
}),
|
|
71
|
+
skills,
|
|
72
|
+
capabilities: [],
|
|
73
|
+
harnesses: [],
|
|
74
|
+
installUnits: inventory.installUnits ?? [],
|
|
75
|
+
workflows,
|
|
76
|
+
inventory
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
47
80
|
async function discoverInstalledSkills(skillsRoot, declaredSkills, options = {}) {
|
|
48
81
|
const installed = [];
|
|
49
82
|
const installedKeys = new Set();
|
|
@@ -123,7 +156,7 @@ function resolveStoredPath(value, options) {
|
|
|
123
156
|
}
|
|
124
157
|
return resolve(dirname(options.configPath ?? "."), value);
|
|
125
158
|
}
|
|
126
|
-
|
|
159
|
+
|
|
127
160
|
async function findSkillFiles(root, seen = new Set()) {
|
|
128
161
|
const files = [];
|
|
129
162
|
const resolvedRoot = await realpath(root).catch(() => root);
|
|
@@ -148,8 +181,8 @@ async function findSkillFiles(root, seen = new Set()) {
|
|
|
148
181
|
}
|
|
149
182
|
}
|
|
150
183
|
return files;
|
|
151
|
-
}
|
|
152
|
-
|
|
184
|
+
}
|
|
185
|
+
|
|
153
186
|
function parseSkillFrontmatter(text) {
|
|
154
187
|
const match = /^---[ \t]*\r?\n(?<body>[\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/.exec(text);
|
|
155
188
|
if (match?.groups === undefined) {
|
|
@@ -170,49 +203,56 @@ function parseSkillFrontmatter(text) {
|
|
|
170
203
|
description: readString(raw, "description", "")
|
|
171
204
|
};
|
|
172
205
|
}
|
|
173
|
-
|
|
174
|
-
function parseVersion(value) {
|
|
175
|
-
if (value === undefined) {
|
|
176
|
-
return 1;
|
|
177
|
-
}
|
|
178
|
-
if (value !== 1) {
|
|
179
|
-
throw new Error(`Unsupported config version: ${value}`);
|
|
180
|
-
}
|
|
181
|
-
return value;
|
|
182
|
-
}
|
|
183
|
-
|
|
184
|
-
function parseDefaults(value) {
|
|
185
|
-
const raw = requireRecord(value ?? {}, "defaults");
|
|
186
|
-
return {
|
|
187
|
-
invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
|
|
188
|
-
allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
|
|
189
|
-
requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
|
|
190
|
-
};
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
function parseSkills(value) {
|
|
194
|
-
const raw = requireRecord(value ?? {}, "skills");
|
|
195
|
-
return Object.entries(raw).map(([id, entry]) => {
|
|
196
|
-
const skill = requireRecord(entry, `skills.${id}`);
|
|
197
|
-
const
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
206
|
+
|
|
207
|
+
function parseVersion(value) {
|
|
208
|
+
if (value === undefined) {
|
|
209
|
+
return 1;
|
|
210
|
+
}
|
|
211
|
+
if (value !== 1 && value !== 2) {
|
|
212
|
+
throw new Error(`Unsupported config version: ${value}`);
|
|
213
|
+
}
|
|
214
|
+
return value;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function parseDefaults(value) {
|
|
218
|
+
const raw = requireRecord(value ?? {}, "defaults");
|
|
219
|
+
return {
|
|
220
|
+
invocationPolicy: readString(raw, "invocation_policy", "deny-by-default"),
|
|
221
|
+
allowModelInvocation: readBoolean(raw, "allow_model_invocation", false),
|
|
222
|
+
requireExplicitWorkflow: readBoolean(raw, "require_explicit_workflow", true)
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseSkills(value) {
|
|
227
|
+
const raw = requireRecord(value ?? {}, "skills");
|
|
228
|
+
return Object.entries(raw).map(([id, entry]) => {
|
|
229
|
+
const skill = requireRecord(entry, `skills.${id}`);
|
|
230
|
+
const v2Keys = ["enabled", "shared", "preference"].filter((key) => Object.prototype.hasOwnProperty.call(skill, key));
|
|
231
|
+
if (v2Keys.length > 0) {
|
|
232
|
+
throw new Error(
|
|
233
|
+
`skills.${id} uses version 2 key ${v2Keys.join(", ")} in a version 1 config. ` +
|
|
234
|
+
"Run `skillboard migrate v2` to convert the complete policy."
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
const status = readString(skill, "status", "vendor");
|
|
238
|
+
const invocation = readString(skill, "invocation", "manual-only");
|
|
239
|
+
const exposure = readString(skill, "exposure", "exported");
|
|
240
|
+
if (!STATUS_VALUES.has(status)) {
|
|
241
|
+
throw new Error(`Unsupported status for ${id}: ${status}`);
|
|
242
|
+
}
|
|
243
|
+
if (!INVOCATION_VALUES.has(invocation)) {
|
|
244
|
+
throw new Error(`Unsupported invocation for ${id}: ${invocation}`);
|
|
245
|
+
}
|
|
246
|
+
if (!EXPOSURE_VALUES.has(exposure)) {
|
|
247
|
+
throw new Error(`Unsupported exposure for ${id}: ${exposure}`);
|
|
248
|
+
}
|
|
209
249
|
return {
|
|
210
250
|
id,
|
|
211
251
|
path: normalizeSkillPath(readString(skill, "path", id), `skills.${id}.path`),
|
|
212
|
-
status,
|
|
213
|
-
invocation,
|
|
214
|
-
exposure,
|
|
215
|
-
category: readString(skill, "category", "uncategorized"),
|
|
252
|
+
status,
|
|
253
|
+
invocation,
|
|
254
|
+
exposure,
|
|
255
|
+
category: readString(skill, "category", "uncategorized"),
|
|
216
256
|
canonicalFor: readStringList(skill, "canonical_for"),
|
|
217
257
|
conflictsWith: readStringList(skill, "conflicts_with"),
|
|
218
258
|
replacedBy: readOptionalString(skill, "replaced_by"),
|
|
@@ -266,39 +306,39 @@ function parseCapabilities(value) {
|
|
|
266
306
|
};
|
|
267
307
|
});
|
|
268
308
|
}
|
|
269
|
-
|
|
270
|
-
function parseHarnesses(value) {
|
|
271
|
-
const raw = requireRecord(value ?? {}, "harnesses");
|
|
272
|
-
return Object.entries(raw).map(([name, entry]) => {
|
|
273
|
-
const harness = requireRecord(entry, `harnesses.${name}`);
|
|
274
|
-
const status = readString(harness, "status", "available");
|
|
275
|
-
if (!HARNESS_STATUS_VALUES.has(status)) {
|
|
276
|
-
throw new Error(`Unsupported harness status for ${name}: ${status}`);
|
|
277
|
-
}
|
|
278
|
-
return {
|
|
279
|
-
name,
|
|
280
|
-
status,
|
|
281
|
-
workflows: readStringList(harness, "workflows"),
|
|
282
|
-
commands: readStringList(harness, "commands")
|
|
283
|
-
};
|
|
284
|
-
});
|
|
285
|
-
}
|
|
286
|
-
|
|
287
|
-
function parseWorkflows(value) {
|
|
288
|
-
const raw = requireRecord(value ?? {}, "workflows");
|
|
289
|
-
return Object.entries(raw).map(([name, entry]) => {
|
|
290
|
-
const workflow = requireRecord(entry, `workflows.${name}`);
|
|
291
|
-
return {
|
|
292
|
-
name,
|
|
293
|
-
harness: readString(workflow, "harness", "unspecified"),
|
|
294
|
-
activeSkills: readStringList(workflow, "active_skills"),
|
|
295
|
-
blockedSkills: readStringList(workflow, "blocked_skills"),
|
|
296
|
-
requiredOutputs: readStringList(workflow, "required_outputs"),
|
|
297
|
-
requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
|
|
298
|
-
};
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
309
|
+
|
|
310
|
+
function parseHarnesses(value) {
|
|
311
|
+
const raw = requireRecord(value ?? {}, "harnesses");
|
|
312
|
+
return Object.entries(raw).map(([name, entry]) => {
|
|
313
|
+
const harness = requireRecord(entry, `harnesses.${name}`);
|
|
314
|
+
const status = readString(harness, "status", "available");
|
|
315
|
+
if (!HARNESS_STATUS_VALUES.has(status)) {
|
|
316
|
+
throw new Error(`Unsupported harness status for ${name}: ${status}`);
|
|
317
|
+
}
|
|
318
|
+
return {
|
|
319
|
+
name,
|
|
320
|
+
status,
|
|
321
|
+
workflows: readStringList(harness, "workflows"),
|
|
322
|
+
commands: readStringList(harness, "commands")
|
|
323
|
+
};
|
|
324
|
+
});
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
function parseWorkflows(value) {
|
|
328
|
+
const raw = requireRecord(value ?? {}, "workflows");
|
|
329
|
+
return Object.entries(raw).map(([name, entry]) => {
|
|
330
|
+
const workflow = requireRecord(entry, `workflows.${name}`);
|
|
331
|
+
return {
|
|
332
|
+
name,
|
|
333
|
+
harness: readString(workflow, "harness", "unspecified"),
|
|
334
|
+
activeSkills: readStringList(workflow, "active_skills"),
|
|
335
|
+
blockedSkills: readStringList(workflow, "blocked_skills"),
|
|
336
|
+
requiredOutputs: readStringList(workflow, "required_outputs"),
|
|
337
|
+
requiredCapabilities: parseRequiredCapabilities(workflow.required_capabilities, `workflows.${name}.required_capabilities`)
|
|
338
|
+
};
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
302
342
|
function parseRequiredCapabilities(value, label) {
|
|
303
343
|
const raw = requireRecord(value ?? {}, label);
|
|
304
344
|
return Object.entries(raw).map(([name, entry]) => {
|