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,35 @@
|
|
|
1
|
+
version: 2
|
|
2
|
+
|
|
3
|
+
skills:
|
|
4
|
+
anthropic.docx:
|
|
5
|
+
enabled: true
|
|
6
|
+
shared: true
|
|
7
|
+
|
|
8
|
+
omo.review-work:
|
|
9
|
+
enabled: true
|
|
10
|
+
shared: false
|
|
11
|
+
preference:
|
|
12
|
+
intents:
|
|
13
|
+
- review
|
|
14
|
+
priority: 90
|
|
15
|
+
|
|
16
|
+
private.tdd-work-continuity:
|
|
17
|
+
enabled: true
|
|
18
|
+
shared: false
|
|
19
|
+
preference:
|
|
20
|
+
intents:
|
|
21
|
+
- implementation
|
|
22
|
+
- testing
|
|
23
|
+
priority: 100
|
|
24
|
+
|
|
25
|
+
private.workflow-router:
|
|
26
|
+
enabled: true
|
|
27
|
+
shared: false
|
|
28
|
+
preference:
|
|
29
|
+
intents:
|
|
30
|
+
- routing
|
|
31
|
+
priority: 100
|
|
32
|
+
|
|
33
|
+
unused.example:
|
|
34
|
+
enabled: false
|
|
35
|
+
shared: false
|
package/package.json
CHANGED
package/src/advisor/actions.mjs
CHANGED
|
@@ -17,17 +17,17 @@ const NON_ACTIVATABLE_STATUSES = new Set(["blocked", "deprecated", "archived", "
|
|
|
17
17
|
|
|
18
18
|
export function buildInitActions(paths) {
|
|
19
19
|
return [makeAction({
|
|
20
|
-
kind: "
|
|
20
|
+
kind: "setup-user",
|
|
21
21
|
targetId: paths.root,
|
|
22
|
-
label: "
|
|
23
|
-
reason: "SkillBoard
|
|
22
|
+
label: "Set up the SkillBoard user control plane",
|
|
23
|
+
reason: "The user-level SkillBoard policy and generated inventory do not exist yet.",
|
|
24
24
|
risk: "low",
|
|
25
25
|
requiresUserConfirmation: true,
|
|
26
26
|
dryRun: null,
|
|
27
27
|
apply: null,
|
|
28
|
-
appliesTo: { kind: "
|
|
29
|
-
blockedReason: "skillboard
|
|
30
|
-
advanced: { root: paths.root }
|
|
28
|
+
appliesTo: { kind: "user_state", id: paths.root },
|
|
29
|
+
blockedReason: "Run `skillboard setup --yes` after user confirmation.",
|
|
30
|
+
advanced: { root: paths.root, command: "skillboard setup --yes" }
|
|
31
31
|
})];
|
|
32
32
|
}
|
|
33
33
|
|
|
@@ -47,6 +47,102 @@ export function buildActionCards(context) {
|
|
|
47
47
|
};
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export function buildV2ActionCards({ options, paths, workflow, workspace }) {
|
|
51
|
+
if (workflow.unknown || workflow.needs_selection) {
|
|
52
|
+
return [];
|
|
53
|
+
}
|
|
54
|
+
const actions = [];
|
|
55
|
+
const observedSkills = new Set(workspace.inventory.skillIds);
|
|
56
|
+
for (const skill of workspace.skills) {
|
|
57
|
+
if (!observedSkills.has(skill.id)) {
|
|
58
|
+
actions.push(skill.shared
|
|
59
|
+
? v2SharingAction(skill, paths, "unshare")
|
|
60
|
+
: v2ForgetAction(skill, paths));
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
const kind = skill.enabled ? "v2:disable-skill" : "v2:enable-skill";
|
|
64
|
+
const operation = skill.enabled ? "disable" : "enable";
|
|
65
|
+
const policyArgs = ["skillboard", "skill", operation, skill.id, "--config", paths.configPath,
|
|
66
|
+
...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
|
|
67
|
+
const dryRun = command([...policyArgs, "--dry-run", "--json"]);
|
|
68
|
+
actions.push(makeAction({
|
|
69
|
+
kind,
|
|
70
|
+
targetId: skill.id,
|
|
71
|
+
label: `${skill.enabled ? "Disable" : "Enable"} ${skill.id}`,
|
|
72
|
+
reason: `${skill.enabled ? "Stop" : "Allow"} SkillBoard from selecting this skill.`,
|
|
73
|
+
risk: "medium",
|
|
74
|
+
dryRun,
|
|
75
|
+
apply: command([...policyArgs, "--json"]),
|
|
76
|
+
appliesTo: { kind: "skill", id: skill.id },
|
|
77
|
+
blockedReason: null,
|
|
78
|
+
advanced: { enabled: !skill.enabled, policy_projection_version: 2 }
|
|
79
|
+
}));
|
|
80
|
+
actions.push(v2SharingAction(skill, paths, skill.shared ? "unshare" : "share"));
|
|
81
|
+
const intent = options?.intent?.trim();
|
|
82
|
+
if (intent !== undefined && intent.length > 0
|
|
83
|
+
&& skill.enabled
|
|
84
|
+
&& !samePreference(skill.preference, intent)) {
|
|
85
|
+
const intents = [...new Set([...(skill.preference?.intents ?? []), intent])]
|
|
86
|
+
.sort((left, right) => left.localeCompare(right));
|
|
87
|
+
const preferenceArgs = ["skillboard", "skill", "preference", skill.id, "--intent", intents.join(","), "--priority", "100", "--config", paths.configPath,
|
|
88
|
+
...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
|
|
89
|
+
actions.push(makeAction({
|
|
90
|
+
kind: "v2:prefer-skill",
|
|
91
|
+
targetId: `${skill.id}:${encodeURIComponent(intent)}`,
|
|
92
|
+
label: `Prefer ${skill.id} for ${intent}`,
|
|
93
|
+
reason: "Rank this enabled skill higher when it is installed for the current agent and the intent matches.",
|
|
94
|
+
risk: "medium",
|
|
95
|
+
dryRun: command([...preferenceArgs, "--dry-run", "--json"]),
|
|
96
|
+
apply: command([...preferenceArgs, "--json"]),
|
|
97
|
+
appliesTo: { kind: "skill", id: skill.id },
|
|
98
|
+
blockedReason: null,
|
|
99
|
+
advanced: { intents, priority: 100, policy_projection_version: 2 }
|
|
100
|
+
}));
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return withApplicationCommands(actions.sort(sortActions), { options, paths, workflow, workspace });
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function v2SharingAction(skill, paths, operation) {
|
|
107
|
+
const args = ["skillboard", "skill", operation, skill.id, "--config", paths.configPath];
|
|
108
|
+
const unshare = operation === "unshare";
|
|
109
|
+
return makeAction({
|
|
110
|
+
kind: unshare ? "v2:unshare-skill" : "v2:share-skill",
|
|
111
|
+
targetId: skill.id,
|
|
112
|
+
label: `${unshare ? "Stop sharing" : "Share"} ${skill.id}`,
|
|
113
|
+
reason: unshare
|
|
114
|
+
? "Stop SkillBoard-managed cross-agent sharing while preserving agent-owned originals."
|
|
115
|
+
: "Make this skill available through the user shared-skill layer.",
|
|
116
|
+
risk: "medium",
|
|
117
|
+
dryRun: command([...args, "--dry-run", "--json"]),
|
|
118
|
+
apply: command([...args, "--json"]),
|
|
119
|
+
appliesTo: { kind: "skill", id: skill.id },
|
|
120
|
+
blockedReason: null,
|
|
121
|
+
advanced: { shared: !unshare, policy_projection_version: 2 }
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function v2ForgetAction(skill, paths) {
|
|
126
|
+
const args = ["skillboard", "skill", "forget", skill.id, "--config", paths.configPath,
|
|
127
|
+
...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot])];
|
|
128
|
+
return makeAction({
|
|
129
|
+
kind: "v2:forget-skill",
|
|
130
|
+
targetId: skill.id,
|
|
131
|
+
label: `Forget removed skill ${skill.id}`,
|
|
132
|
+
reason: "Remove stale SkillBoard policy after the owning installer removed this unshared skill.",
|
|
133
|
+
risk: "medium",
|
|
134
|
+
dryRun: command([...args, "--dry-run", "--json"]),
|
|
135
|
+
apply: command([...args, "--json"]),
|
|
136
|
+
appliesTo: { kind: "skill", id: skill.id },
|
|
137
|
+
blockedReason: null,
|
|
138
|
+
advanced: { policy_only: true, policy_projection_version: 2 }
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function samePreference(preference, intent) {
|
|
143
|
+
return preference?.priority === 100 && preference.intents.includes(intent);
|
|
144
|
+
}
|
|
145
|
+
|
|
50
146
|
function reviewInstallUnitActions({ paths, workflow, reviewQueue }) {
|
|
51
147
|
const units = new Map();
|
|
52
148
|
for (const entry of reviewQueue) {
|
|
@@ -3,17 +3,17 @@ import {
|
|
|
3
3
|
workflowResolved
|
|
4
4
|
} from "./action-core.mjs";
|
|
5
5
|
|
|
6
|
-
export function withApplicationCommands(actions, { paths, workflow }) {
|
|
6
|
+
export function withApplicationCommands(actions, { paths, workflow, workspace, options }) {
|
|
7
7
|
return actions.map((action) => {
|
|
8
8
|
return {
|
|
9
9
|
...action,
|
|
10
|
-
application: applicationCommand(action, paths, workflow)
|
|
10
|
+
application: applicationCommand(action, paths, workflow, workspace, options)
|
|
11
11
|
};
|
|
12
12
|
});
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
function applicationCommand(action, paths, workflow) {
|
|
16
|
-
const blockedReason = applicationBlockedReason(action, workflow);
|
|
15
|
+
function applicationCommand(action, paths, workflow, workspace, options) {
|
|
16
|
+
const blockedReason = applicationBlockedReason(action, workflow, workspace);
|
|
17
17
|
if (blockedReason !== null) {
|
|
18
18
|
return {
|
|
19
19
|
preview: null,
|
|
@@ -24,11 +24,12 @@ function applicationCommand(action, paths, workflow) {
|
|
|
24
24
|
|
|
25
25
|
const base = [
|
|
26
26
|
"skillboard", "apply-action", action.id,
|
|
27
|
-
...workflowArgs(workflow),
|
|
27
|
+
...(workspace.version === 2 ? [] : workflowArgs(workflow)),
|
|
28
28
|
"--dir", paths.root,
|
|
29
29
|
"--config", paths.configPath,
|
|
30
|
-
"--skills", paths.skillsRoot,
|
|
31
|
-
"--json"
|
|
30
|
+
...(paths.skillsRoot === undefined ? [] : ["--skills", paths.skillsRoot]),
|
|
31
|
+
"--json",
|
|
32
|
+
...(workspace.version === 2 && options?.agent !== undefined ? ["--agent", options.agent] : [])
|
|
32
33
|
];
|
|
33
34
|
return {
|
|
34
35
|
preview: command([...base, "--dry-run"]),
|
|
@@ -37,14 +38,14 @@ function applicationCommand(action, paths, workflow) {
|
|
|
37
38
|
};
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
function applicationBlockedReason(action, workflow) {
|
|
41
|
+
function applicationBlockedReason(action, workflow, workspace) {
|
|
41
42
|
if (action.blocked_reason !== null) {
|
|
42
43
|
return action.blocked_reason;
|
|
43
44
|
}
|
|
44
45
|
if (action.apply === null) {
|
|
45
46
|
return "Action cannot be applied directly.";
|
|
46
47
|
}
|
|
47
|
-
if (!workflowResolved(workflow)) {
|
|
48
|
+
if (workspace.version !== 2 && !workflowResolved(workflow)) {
|
|
48
49
|
return workflow.blocked_reason ?? "Select a workflow before applying action cards.";
|
|
49
50
|
}
|
|
50
51
|
return null;
|
|
@@ -5,8 +5,15 @@ import {
|
|
|
5
5
|
installGuardHook,
|
|
6
6
|
removeSkill
|
|
7
7
|
} from "../control.mjs";
|
|
8
|
+
import { setV2SkillEnabled, setV2SkillPreference } from "../control/v2-skill-crud.mjs";
|
|
9
|
+
import { forgetV2Skill } from "../control/v2-skill-forget.mjs";
|
|
10
|
+
import { setSkillSharing } from "../shared-skill.mjs";
|
|
11
|
+
import { resolveUserStatePaths } from "../user-state-paths.mjs";
|
|
8
12
|
import { reviewInstallUnit } from "../review.mjs";
|
|
9
13
|
import { uninstallProject } from "../uninstall.mjs";
|
|
14
|
+
import { isPreV2ActionId, V1_MUTATION_ERROR } from "../compatibility.mjs";
|
|
15
|
+
import { loadWorkspace } from "../workspace.mjs";
|
|
16
|
+
import { resolveProjectPaths } from "./schema.mjs";
|
|
10
17
|
|
|
11
18
|
const INSTALL_UNIT_ACTIONS = new Set([
|
|
12
19
|
"block-install-unit",
|
|
@@ -27,17 +34,30 @@ export async function applyAdvisorAction(actionId, options) {
|
|
|
27
34
|
throw new ApplyActionError("missing-action-id", "Usage: skillboard apply-action <action-id>");
|
|
28
35
|
}
|
|
29
36
|
|
|
37
|
+
const paths = resolveProjectPaths(options);
|
|
38
|
+
options = { ...options, root: paths.root, configPath: paths.configPath, skillsRoot: paths.skillsRoot };
|
|
39
|
+
const workspace = await loadWorkspace({ configPath: options.configPath, skillsRoot: options.skillsRoot });
|
|
40
|
+
if (workspace.version === 1 && options.yes === true && options.dryRun !== true) {
|
|
41
|
+
throw new ApplyActionError("migration-required", V1_MUTATION_ERROR);
|
|
42
|
+
}
|
|
43
|
+
if (workspace.version === 2 && isPreV2ActionId(actionId)) {
|
|
44
|
+
throw new ApplyActionError("stale-policy-version", `Pre-v2 action id is stale: ${actionId}. Run skillboard brief --include-actions to get current actions.`);
|
|
45
|
+
}
|
|
30
46
|
const action = await resolveCurrentAction(actionId, options);
|
|
31
47
|
if (action.blocked_reason !== null) {
|
|
32
48
|
throw new ApplyActionError("blocked-action", action.blocked_reason);
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
if (previewMode(options)) {
|
|
52
|
+
const control = action.kind.startsWith("v2:")
|
|
53
|
+
? await dispatchAction(action, { ...options, dryRun: true })
|
|
54
|
+
: null;
|
|
36
55
|
return {
|
|
37
56
|
ok: true,
|
|
38
57
|
mode: "preview",
|
|
39
58
|
changed: false,
|
|
40
|
-
action
|
|
59
|
+
action,
|
|
60
|
+
control
|
|
41
61
|
};
|
|
42
62
|
}
|
|
43
63
|
|
|
@@ -96,6 +116,8 @@ async function resolveCurrentAction(actionId, options) {
|
|
|
96
116
|
function buildActionBrief(options) {
|
|
97
117
|
return buildSkillBrief({
|
|
98
118
|
includeActions: true,
|
|
119
|
+
agent: options.agent,
|
|
120
|
+
intent: options.intent,
|
|
99
121
|
workflow: options.workflow,
|
|
100
122
|
configPath: options.configPath,
|
|
101
123
|
skillsRoot: options.skillsRoot,
|
|
@@ -108,6 +130,57 @@ function previewMode(options) {
|
|
|
108
130
|
}
|
|
109
131
|
|
|
110
132
|
async function dispatchAction(action, options) {
|
|
133
|
+
if (action.kind === "v2:enable-skill" || action.kind === "v2:disable-skill") {
|
|
134
|
+
return await setV2SkillEnabled({
|
|
135
|
+
skillId: action.applies_to.id,
|
|
136
|
+
enabled: action.kind === "v2:enable-skill",
|
|
137
|
+
configPath: options.configPath,
|
|
138
|
+
skillsRoot: options.skillsRoot,
|
|
139
|
+
dryRun: options.dryRun === true
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
if (action.kind === "v2:share-skill" || action.kind === "v2:unshare-skill") {
|
|
143
|
+
const state = resolveUserStatePaths({
|
|
144
|
+
home: options.home,
|
|
145
|
+
env: options.env ?? process.env,
|
|
146
|
+
configPath: options.configPath,
|
|
147
|
+
inventoryPath: options.inventoryPath
|
|
148
|
+
});
|
|
149
|
+
return await setSkillSharing({
|
|
150
|
+
skillId: action.applies_to.id,
|
|
151
|
+
shared: action.kind === "v2:share-skill",
|
|
152
|
+
configPath: options.configPath,
|
|
153
|
+
inventoryPath: state.inventoryPath,
|
|
154
|
+
home: state.home,
|
|
155
|
+
env: options.env ?? process.env,
|
|
156
|
+
dryRun: options.dryRun === true
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
if (action.kind === "v2:prefer-skill") {
|
|
160
|
+
return await setV2SkillPreference({
|
|
161
|
+
skillId: action.applies_to.id,
|
|
162
|
+
intents: action.advanced.intents,
|
|
163
|
+
priority: action.advanced.priority,
|
|
164
|
+
configPath: options.configPath,
|
|
165
|
+
skillsRoot: options.skillsRoot,
|
|
166
|
+
dryRun: options.dryRun === true
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
if (action.kind === "v2:forget-skill") {
|
|
170
|
+
const state = resolveUserStatePaths({
|
|
171
|
+
home: options.home,
|
|
172
|
+
env: options.env ?? process.env,
|
|
173
|
+
configPath: options.configPath,
|
|
174
|
+
inventoryPath: options.inventoryPath
|
|
175
|
+
});
|
|
176
|
+
return await forgetV2Skill({
|
|
177
|
+
skillId: action.applies_to.id,
|
|
178
|
+
configPath: options.configPath,
|
|
179
|
+
inventoryPath: state.inventoryPath,
|
|
180
|
+
skillsRoot: options.skillsRoot,
|
|
181
|
+
dryRun: options.dryRun === true
|
|
182
|
+
});
|
|
183
|
+
}
|
|
111
184
|
if (INSTALL_UNIT_ACTIONS.has(action.kind)) {
|
|
112
185
|
return await reviewInstallUnit({
|
|
113
186
|
unitId: action.applies_to.id,
|
package/src/advisor/guidance.mjs
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import { command } from "./action-core.mjs";
|
|
2
2
|
|
|
3
|
-
const GUARD_WHEN = "before
|
|
3
|
+
const GUARD_WHEN = "immediately before skill use";
|
|
4
4
|
const GOAL_DOCUMENT = Object.freeze({
|
|
5
5
|
path: "docs/ai-skill-routing-goal.md",
|
|
6
|
-
purpose: "Preserve SkillBoard as a
|
|
6
|
+
purpose: "Preserve SkillBoard v2 as a user-level control plane: enable or disable a skill, opt individual skills into cross-agent sharing, and use optional preference only to rank available overlaps.",
|
|
7
7
|
loop: Object.freeze([
|
|
8
8
|
"observe",
|
|
9
9
|
"route",
|
|
@@ -18,7 +18,7 @@ const GOAL_DOCUMENT = Object.freeze({
|
|
|
18
18
|
"before changing brief output",
|
|
19
19
|
"before changing bridge instructions",
|
|
20
20
|
"before changing policy UX",
|
|
21
|
-
"before changing
|
|
21
|
+
"before changing sharing UX"
|
|
22
22
|
])
|
|
23
23
|
});
|
|
24
24
|
const GUARD_ALLOWED_USE = Object.freeze({
|
|
@@ -71,6 +71,9 @@ function guidanceStatus(brief) {
|
|
|
71
71
|
if (brief.error?.code === "unknown-workflow" || brief.workflow?.unknown === true) {
|
|
72
72
|
return "unknown-workflow";
|
|
73
73
|
}
|
|
74
|
+
if (brief.health?.config?.version === 2 && brief.workflow?.selected === null && (brief.workflow?.candidates?.length ?? 0) === 0) {
|
|
75
|
+
return "ready";
|
|
76
|
+
}
|
|
74
77
|
if (brief.workflow?.needs_selection === true || brief.workflow?.selected === null) {
|
|
75
78
|
return "workflow-selection-needed";
|
|
76
79
|
}
|
|
@@ -92,20 +95,20 @@ function hasInvalidConfig(brief) {
|
|
|
92
95
|
}
|
|
93
96
|
|
|
94
97
|
function summaryForStatus(status, brief) {
|
|
95
|
-
const readyCount = (brief
|
|
98
|
+
const readyCount = availableSkillCount(brief);
|
|
96
99
|
const decisionCount = guidanceDecisionCount(brief);
|
|
97
100
|
const blockedCount = brief.skills?.blocked?.length ?? 0;
|
|
98
101
|
switch (status) {
|
|
99
102
|
case "ready":
|
|
100
|
-
return `SkillBoard is ready; ${readyCount} skills are available
|
|
103
|
+
return `SkillBoard is ready; ${readyCount} skills are available for the current agent.`;
|
|
101
104
|
case "needs-decision":
|
|
102
|
-
return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before
|
|
105
|
+
return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before the requested policy change is applied.`;
|
|
103
106
|
case "blocked":
|
|
104
107
|
return `SkillBoard found blocking policy issues; ${blockedCount} skills are blocked for safety.`;
|
|
105
108
|
case "not-initialized":
|
|
106
|
-
return "SkillBoard
|
|
109
|
+
return "SkillBoard user state has not been set up yet.";
|
|
107
110
|
case "invalid-config":
|
|
108
|
-
return "SkillBoard cannot read the
|
|
111
|
+
return "SkillBoard cannot read the selected user policy configuration.";
|
|
109
112
|
case "workflow-selection-needed":
|
|
110
113
|
return "SkillBoard needs a workflow selection before applying action cards.";
|
|
111
114
|
case "unknown-workflow":
|
|
@@ -115,6 +118,10 @@ function summaryForStatus(status, brief) {
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
|
|
121
|
+
function availableSkillCount(brief) {
|
|
122
|
+
return (brief.skills?.automatic_allowed?.length ?? 0) + (brief.skills?.manual_allowed?.length ?? 0);
|
|
123
|
+
}
|
|
124
|
+
|
|
118
125
|
function guidanceDecisionCount(brief) {
|
|
119
126
|
const skillDecisionCount = brief.skills?.needs_review?.length ?? 0;
|
|
120
127
|
if (skillDecisionCount > 0) {
|
|
@@ -137,7 +144,9 @@ function recommendedNextStep(status, brief, choices, route = null) {
|
|
|
137
144
|
? "Ask a clarifying question; no workflow capability matched this request."
|
|
138
145
|
: `Use ${route.recommended_skill} for this request after the guard check passes${postUsePolicyStep(route)}.`;
|
|
139
146
|
}
|
|
140
|
-
return
|
|
147
|
+
return availableSkillCount(brief) === 0
|
|
148
|
+
? "No policy change is required; valid installed skills default to enabled and agent-local."
|
|
149
|
+
: "Run the guard check immediately before using any selected skill.";
|
|
141
150
|
case "needs-decision":
|
|
142
151
|
if (route?.recommended_skill !== null && route?.guard_allowed === true) {
|
|
143
152
|
return route.post_use_policy_suggestion === null
|
|
@@ -156,7 +165,7 @@ function recommendedNextStep(status, brief, choices, route = null) {
|
|
|
156
165
|
: `Review the blocked item before applying: ${firstChoice.label}.`;
|
|
157
166
|
case "not-initialized":
|
|
158
167
|
return firstChoice === undefined
|
|
159
|
-
? "
|
|
168
|
+
? "Run SkillBoard setup before checking skill availability."
|
|
160
169
|
: `Ask the user whether to approve: ${firstChoice.label}.`;
|
|
161
170
|
case "invalid-config":
|
|
162
171
|
return "Fix the SkillBoard configuration before checking skill availability.";
|
|
@@ -237,16 +246,15 @@ function confirmableAction(action) {
|
|
|
237
246
|
}
|
|
238
247
|
|
|
239
248
|
function guardCommandHint(brief) {
|
|
240
|
-
if (hasInvalidConfig(brief)) {
|
|
241
|
-
return null;
|
|
242
|
-
}
|
|
243
|
-
if (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true) {
|
|
249
|
+
if (hasInvalidConfig(brief) || brief.error?.code === "not-initialized" || availableSkillCount(brief) === 0) {
|
|
244
250
|
return null;
|
|
245
251
|
}
|
|
252
|
+
const version = brief.health?.config?.version;
|
|
253
|
+
if (version === 1 && (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true)) return null;
|
|
246
254
|
return command([
|
|
247
255
|
"skillboard", "guard", "use", "<skill-id>",
|
|
248
|
-
"--workflow", brief.workflow.selected,
|
|
256
|
+
...(version === 1 ? ["--workflow", brief.workflow.selected] : []),
|
|
249
257
|
"--config", brief.health.config_path,
|
|
250
|
-
"--skills", brief.health.skills_root
|
|
258
|
+
...(brief.health.skills_root === undefined || brief.health.skills_root === null ? [] : ["--skills", brief.health.skills_root])
|
|
251
259
|
]).display;
|
|
252
260
|
}
|
package/src/advisor/schema.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { isAbsolute, resolve } from "node:path";
|
|
1
|
+
import { dirname, isAbsolute, resolve } from "node:path";
|
|
2
2
|
import { uninstallProject } from "../uninstall.mjs";
|
|
3
3
|
import { buildAssistantGuidance } from "./guidance.mjs";
|
|
4
4
|
import { sortedStrings } from "./sort.mjs";
|
|
@@ -18,6 +18,7 @@ export function buildBrief(data) {
|
|
|
18
18
|
ok: data.ok,
|
|
19
19
|
schema_version: SCHEMA_VERSION
|
|
20
20
|
};
|
|
21
|
+
if (data.compatibility !== undefined && data.compatibility !== null) brief.compatibility = data.compatibility;
|
|
21
22
|
if (data.error !== undefined) {
|
|
22
23
|
brief.error = data.error;
|
|
23
24
|
}
|
|
@@ -35,11 +36,12 @@ export function buildBrief(data) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
export function resolveProjectPaths(options) {
|
|
38
|
-
const
|
|
39
|
+
const configPath = resolve(options.configPath ?? "skillboard.config.yaml");
|
|
40
|
+
const root = resolve(options.root ?? dirname(configPath));
|
|
39
41
|
return {
|
|
40
42
|
root,
|
|
41
|
-
configPath: resolveUnderRoot(root,
|
|
42
|
-
skillsRoot: resolveUnderRoot(root, options.skillsRoot
|
|
43
|
+
configPath: resolveUnderRoot(root, configPath),
|
|
44
|
+
skillsRoot: options.skillsRoot === undefined ? undefined : resolveUnderRoot(root, options.skillsRoot)
|
|
43
45
|
};
|
|
44
46
|
}
|
|
45
47
|
|
|
@@ -66,14 +68,14 @@ export async function buildCleanup(root) {
|
|
|
66
68
|
}
|
|
67
69
|
|
|
68
70
|
export function healthFromDoctor(doctor, paths) {
|
|
69
|
-
|
|
71
|
+
const health = {
|
|
70
72
|
mode: doctor.mode,
|
|
71
73
|
review_required: doctor.reviewRequired,
|
|
72
74
|
strict_ok: doctor.strictOk,
|
|
73
75
|
initialized: doctor.initialized,
|
|
74
76
|
root: paths.root,
|
|
75
77
|
config_path: paths.configPath,
|
|
76
|
-
skills_root: paths.skillsRoot,
|
|
78
|
+
skills_root: paths.skillsRoot ?? null,
|
|
77
79
|
config: {
|
|
78
80
|
exists: doctor.config.exists,
|
|
79
81
|
valid: doctor.config.valid,
|
|
@@ -86,6 +88,15 @@ export function healthFromDoctor(doctor, paths) {
|
|
|
86
88
|
warnings: sortedStrings(doctor.policy.warnings)
|
|
87
89
|
}
|
|
88
90
|
};
|
|
91
|
+
if (doctor.inventory.required) {
|
|
92
|
+
health.inventory = {
|
|
93
|
+
required: true,
|
|
94
|
+
ok: doctor.inventory.ok,
|
|
95
|
+
path: doctor.inventory.path,
|
|
96
|
+
errors: sortedStrings(doctor.inventory.errors)
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
return health;
|
|
89
100
|
}
|
|
90
101
|
|
|
91
102
|
export function emptyWorkflowState() {
|
package/src/advisor/skills.mjs
CHANGED
|
@@ -12,8 +12,8 @@ export function skillsWithoutWorkflow(workspace) {
|
|
|
12
12
|
return sortSkillGroups(groups);
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units: [] }) {
|
|
16
|
-
if (workflowName === null) {
|
|
15
|
+
export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units: [] }, agentName) {
|
|
16
|
+
if (workflowName === null && workspace.version !== 2) {
|
|
17
17
|
const groups = emptySkillGroups();
|
|
18
18
|
groups.installed_only = installedOnlyEntries(workspace);
|
|
19
19
|
return sortSkillGroups(groups);
|
|
@@ -24,7 +24,7 @@ export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units
|
|
|
24
24
|
for (const summary of listSkills(workspace)) {
|
|
25
25
|
const skill = workspace.skills.find((candidate) => candidate.id === summary.id);
|
|
26
26
|
const explanation = explainSkill(workspace, summary.id);
|
|
27
|
-
const use = canUseSkill(workspace, summary.id, workflowName);
|
|
27
|
+
const use = canUseSkill(workspace, summary.id, workflowName, agentName);
|
|
28
28
|
const group = groupForDeclaredSkill(workspace, skill, use, sourceFindings);
|
|
29
29
|
const entry = declaredSkillEntry(summary, explanation, use, group);
|
|
30
30
|
if (use.allowed) {
|
|
@@ -42,6 +42,16 @@ export function skillsForWorkflow(workspace, workflowName, sourceAudit = { units
|
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
function declaredSkillEntry(summary, explanation, use, group) {
|
|
45
|
+
if (summary.enabled !== undefined) {
|
|
46
|
+
return {
|
|
47
|
+
id: summary.id, label: summary.id, path: summary.path,
|
|
48
|
+
reason: reasonForGroup(group, use?.reasons ?? []),
|
|
49
|
+
advanced: {
|
|
50
|
+
enabled: summary.enabled, shared: summary.shared, installed_on: explanation.inventory?.installed_on ?? [], preference: summary.preference,
|
|
51
|
+
runtime_ready: !(use?.reasons ?? []).some((reason) => reason.includes("Inventory integrity error"))
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
}
|
|
45
55
|
return {
|
|
46
56
|
id: summary.id,
|
|
47
57
|
label: summary.id,
|
|
@@ -79,8 +89,9 @@ function groupForDeclaredSkill(workspace, skill, use, sourceFindings) {
|
|
|
79
89
|
|
|
80
90
|
function installedOnlyEntries(workspace) {
|
|
81
91
|
const declaredPaths = new Set(workspace.skills.map((skill) => skill.path));
|
|
92
|
+
const declaredIds = new Set(workspace.skills.map((skill) => skill.id));
|
|
82
93
|
return workspace.installedSkills
|
|
83
|
-
.filter((skill) => !declaredPaths.has(skill.path))
|
|
94
|
+
.filter((skill) => !declaredPaths.has(skill.path) && !declaredIds.has(skill.id))
|
|
84
95
|
.map((skill) => ({
|
|
85
96
|
id: skill.id,
|
|
86
97
|
label: skill.name ?? skill.id,
|
|
@@ -151,7 +162,9 @@ function findingsBySource(sourceAudit) {
|
|
|
151
162
|
}
|
|
152
163
|
|
|
153
164
|
function isNotInWorkflowReason(reason) {
|
|
154
|
-
return reason.includes("is not active, preferred, or fallback in workflow")
|
|
165
|
+
return reason.includes("is not active, preferred, or fallback in workflow")
|
|
166
|
+
|| reason.includes("is not available in workflow")
|
|
167
|
+
|| reason.includes("requires workflow");
|
|
155
168
|
}
|
|
156
169
|
|
|
157
170
|
function isReviewReason(reason) {
|