agent-skillboard 0.1.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/CONTRIBUTING.md +83 -0
- package/LICENSE +21 -0
- package/README.md +645 -0
- package/bin/skillboard.mjs +4 -0
- package/docs/adapters.md +127 -0
- package/docs/capabilities.md +107 -0
- package/docs/install.md +346 -0
- package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
- package/docs/plans/README.md +20 -0
- package/docs/policy-model.md +251 -0
- package/docs/positioning.md +94 -0
- package/docs/profiles.md +166 -0
- package/docs/rollout-runbook.md +60 -0
- package/docs/user-flow.md +231 -0
- package/docs/versioning.md +201 -0
- package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
- package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
- package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
- package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
- package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
- package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
- package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
- package/examples/multi-source.config.yaml +271 -0
- package/examples/skillboard.config.yaml +194 -0
- package/examples/skills/grill-me/SKILL.md +9 -0
- package/examples/skills/grill-with-docs/SKILL.md +9 -0
- package/examples/skills/requirement-intake/SKILL.md +9 -0
- package/examples/skills/tdd/SKILL.md +8 -0
- package/package.json +58 -0
- package/profiles/anthropics-skills.yaml +17 -0
- package/profiles/mattpocock-skills.yaml +26 -0
- package/profiles/oh-my-openagent.yaml +32 -0
- package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
- package/profiles/wshobson-agents.yaml +19 -0
- package/src/advisor/action-core.mjs +74 -0
- package/src/advisor/actions.mjs +256 -0
- package/src/advisor/application-commands.mjs +59 -0
- package/src/advisor/apply-action.mjs +183 -0
- package/src/advisor/schema.mjs +112 -0
- package/src/advisor/setup-actions.mjs +42 -0
- package/src/advisor/skills.mjs +191 -0
- package/src/advisor/sort.mjs +15 -0
- package/src/advisor/sources.mjs +160 -0
- package/src/advisor/trust-policy.mjs +65 -0
- package/src/advisor/workflow.mjs +41 -0
- package/src/advisor.mjs +134 -0
- package/src/agent-inventory-platforms.mjs +100 -0
- package/src/agent-inventory.mjs +804 -0
- package/src/brief-cli.mjs +40 -0
- package/src/brief-renderer.mjs +362 -0
- package/src/change-plan.mjs +169 -0
- package/src/cli.mjs +1171 -0
- package/src/config-helpers.mjs +72 -0
- package/src/control/can-use-guard.mjs +91 -0
- package/src/control/config-write.mjs +163 -0
- package/src/control/skill-crud.mjs +394 -0
- package/src/control/source-trust.mjs +161 -0
- package/src/control/workflow-crud.mjs +104 -0
- package/src/control.mjs +197 -0
- package/src/doctor.mjs +299 -0
- package/src/domain/constants.mjs +47 -0
- package/src/domain/indexes.mjs +29 -0
- package/src/domain/rules/capabilities.mjs +33 -0
- package/src/domain/rules/harnesses.mjs +16 -0
- package/src/domain/rules/install-units.mjs +95 -0
- package/src/domain/rules/skills.mjs +105 -0
- package/src/domain/rules/workflows.mjs +105 -0
- package/src/domain/skill-state-matrix.mjs +79 -0
- package/src/domain/source-classes.mjs +99 -0
- package/src/hook-plan.mjs +152 -0
- package/src/impact.mjs +41 -0
- package/src/index.mjs +82 -0
- package/src/init.mjs +136 -0
- package/src/install-output-detector.mjs +337 -0
- package/src/install-units.mjs +62 -0
- package/src/inventory-refresh.mjs +45 -0
- package/src/lifecycle-cli.mjs +166 -0
- package/src/lifecycle-content.mjs +87 -0
- package/src/policy.mjs +42 -0
- package/src/reconcile.mjs +111 -0
- package/src/report.mjs +151 -0
- package/src/review.mjs +88 -0
- package/src/rollout.mjs +453 -0
- package/src/skill-paths.mjs +17 -0
- package/src/source-cache.mjs +178 -0
- package/src/source-profile-loader.mjs +160 -0
- package/src/source-profiles.mjs +299 -0
- package/src/source-verification.mjs +252 -0
- package/src/uninstall.mjs +258 -0
- package/src/workspace.mjs +219 -0
- package/tsconfig.lsp.json +15 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
id: github.voltagent.awesome-agent-skills
|
|
2
|
+
source: VoltAgent/awesome-agent-skills
|
|
3
|
+
kind: marketplace
|
|
4
|
+
namespace: voltagent
|
|
5
|
+
target_path_prefix: voltagent
|
|
6
|
+
scope: cache
|
|
7
|
+
default_status: vendor
|
|
8
|
+
default_invocation: manual-only
|
|
9
|
+
default_exposure: exported
|
|
10
|
+
default_category: uncategorized
|
|
11
|
+
provided_components:
|
|
12
|
+
- marketplace
|
|
13
|
+
- skills
|
|
14
|
+
skill_paths:
|
|
15
|
+
- "skills/**/SKILL.md"
|
|
16
|
+
- "*/SKILL.md"
|
|
17
|
+
permission_risk: low
|
|
18
|
+
rollback: remove cache
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
id: github.wshobson.agents
|
|
2
|
+
source: wshobson/agents
|
|
3
|
+
kind: marketplace
|
|
4
|
+
namespace: wshobson
|
|
5
|
+
target_path_prefix: wshobson
|
|
6
|
+
scope: project
|
|
7
|
+
default_status: candidate
|
|
8
|
+
default_invocation: router-only
|
|
9
|
+
default_exposure: exported
|
|
10
|
+
default_category: uncategorized
|
|
11
|
+
provided_components:
|
|
12
|
+
- skills
|
|
13
|
+
- agents
|
|
14
|
+
- commands
|
|
15
|
+
skill_paths:
|
|
16
|
+
- "skills/**/SKILL.md"
|
|
17
|
+
- "*/SKILL.md"
|
|
18
|
+
permission_risk: medium
|
|
19
|
+
rollback: plugin uninstall
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
const RISK_RANK = { info: 0, low: 1, medium: 2, high: 3, destructive: 4 };
|
|
2
|
+
const INSTALL_UNIT_REVIEW_ACTIONS = new Set([
|
|
3
|
+
"block-install-unit",
|
|
4
|
+
"review-install-unit",
|
|
5
|
+
"trust-install-unit"
|
|
6
|
+
]);
|
|
7
|
+
|
|
8
|
+
export function makeAction(data) {
|
|
9
|
+
return {
|
|
10
|
+
id: `${data.kind}:${data.targetId}`,
|
|
11
|
+
kind: data.kind,
|
|
12
|
+
label: data.label,
|
|
13
|
+
reason: data.reason,
|
|
14
|
+
risk: data.risk,
|
|
15
|
+
requires_user_confirmation: data.requiresUserConfirmation ?? requiresConfirmation(data.risk),
|
|
16
|
+
dry_run: data.dryRun,
|
|
17
|
+
apply: data.apply,
|
|
18
|
+
application: data.application ?? emptyApplication(data.blockedReason),
|
|
19
|
+
applies_to: data.appliesTo,
|
|
20
|
+
blocked_reason: data.blockedReason,
|
|
21
|
+
advanced: data.advanced
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function command(argv) {
|
|
26
|
+
return { argv, display: argv.map(shellQuote).join(" ") };
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function applyOrNull(workflow, dryRun, argv) {
|
|
30
|
+
return workflowResolved(workflow) && dryRun !== null ? command(argv) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function blockedByWorkflow(workflow) {
|
|
34
|
+
return workflowResolved(workflow) ? null : workflow.blocked_reason ?? "Select a workflow before applying action cards.";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function workflowResolved(workflow) {
|
|
38
|
+
return workflow.selected !== null && !workflow.needs_selection && !workflow.unknown;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function sortActions(left, right) {
|
|
42
|
+
return left.kind.localeCompare(right.kind)
|
|
43
|
+
|| left.applies_to.id.localeCompare(right.applies_to.id)
|
|
44
|
+
|| RISK_RANK[left.risk] - RISK_RANK[right.risk]
|
|
45
|
+
|| left.id.localeCompare(right.id);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function linkReviewQueue(reviewQueue, actions) {
|
|
49
|
+
const byUnit = new Map(actions
|
|
50
|
+
.filter((entry) => INSTALL_UNIT_REVIEW_ACTIONS.has(entry.kind))
|
|
51
|
+
.map((entry) => [entry.applies_to.id, entry.id]));
|
|
52
|
+
return reviewQueue.map((entry) => {
|
|
53
|
+
const unitId = entry.advanced.install_unit ?? entry.advanced.source_id;
|
|
54
|
+
return unitId === undefined || !byUnit.has(unitId)
|
|
55
|
+
? entry
|
|
56
|
+
: { ...entry, action_ids: [byUnit.get(unitId)] };
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function requiresConfirmation(risk) {
|
|
61
|
+
return RISK_RANK[risk] >= RISK_RANK.medium;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function emptyApplication(blockedReason) {
|
|
65
|
+
return {
|
|
66
|
+
preview: null,
|
|
67
|
+
apply: null,
|
|
68
|
+
blocked_reason: blockedReason ?? "Action cannot be applied directly."
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function shellQuote(value) {
|
|
73
|
+
return /^[A-Za-z0-9_./:=@-]+$/.test(value) ? value : `'${value.replaceAll("'", "'\\''")}'`;
|
|
74
|
+
}
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import {
|
|
2
|
+
applyOrNull,
|
|
3
|
+
blockedByWorkflow,
|
|
4
|
+
command,
|
|
5
|
+
linkReviewQueue,
|
|
6
|
+
makeAction,
|
|
7
|
+
sortActions,
|
|
8
|
+
workflowResolved
|
|
9
|
+
} from "./action-core.mjs";
|
|
10
|
+
import { withApplicationCommands } from "./application-commands.mjs";
|
|
11
|
+
import { buildSetupGuidanceActions } from "./setup-actions.mjs";
|
|
12
|
+
import { trustRecommendationAction } from "./trust-policy.mjs";
|
|
13
|
+
|
|
14
|
+
const WRITABLE_MODES = new Set(["manual-only", "router-only", "workflow-auto"]);
|
|
15
|
+
|
|
16
|
+
export function buildInitActions(paths) {
|
|
17
|
+
return [makeAction({
|
|
18
|
+
kind: "init-project",
|
|
19
|
+
targetId: paths.root,
|
|
20
|
+
label: "Initialize SkillBoard in this project",
|
|
21
|
+
reason: "SkillBoard is not initialized for this project.",
|
|
22
|
+
risk: "low",
|
|
23
|
+
requiresUserConfirmation: true,
|
|
24
|
+
dryRun: null,
|
|
25
|
+
apply: null,
|
|
26
|
+
appliesTo: { kind: "project", id: paths.root },
|
|
27
|
+
blockedReason: "skillboard init does not have a dry-run preview command.",
|
|
28
|
+
advanced: { root: paths.root }
|
|
29
|
+
})];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function buildActionCards(context) {
|
|
33
|
+
const actions = withApplicationCommands([
|
|
34
|
+
...buildSetupGuidanceActions(context),
|
|
35
|
+
...reviewInstallUnitActions(context),
|
|
36
|
+
...activateSkillActions(context),
|
|
37
|
+
...blockSkillActions(context),
|
|
38
|
+
...hookInstallActions(context),
|
|
39
|
+
...removeSkillForceActions(context),
|
|
40
|
+
resetCleanupAction(context)
|
|
41
|
+
].filter(Boolean).sort(sortActions), context);
|
|
42
|
+
return {
|
|
43
|
+
actions,
|
|
44
|
+
reviewQueue: linkReviewQueue(context.reviewQueue, actions)
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function reviewInstallUnitActions({ paths, workflow, reviewQueue }) {
|
|
49
|
+
const units = new Map();
|
|
50
|
+
for (const entry of reviewQueue) {
|
|
51
|
+
const unitId = entry.advanced.install_unit ?? entry.advanced.source_id;
|
|
52
|
+
if (unitId !== undefined && entry.kind === "install_unit") {
|
|
53
|
+
units.set(unitId, {
|
|
54
|
+
risk: entry.risk,
|
|
55
|
+
recommended: entry.advanced.recommended_trust_level ?? "reviewed"
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
return [...units.entries()].map(([unitId, { risk, recommended }]) => {
|
|
60
|
+
const action = trustRecommendationAction(recommended);
|
|
61
|
+
const dryRun = command([
|
|
62
|
+
"skillboard", "review", "install-unit", unitId, "--trust-level", recommended,
|
|
63
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--dry-run", "--json"
|
|
64
|
+
]);
|
|
65
|
+
return makeAction({
|
|
66
|
+
kind: action.kind,
|
|
67
|
+
targetId: unitId,
|
|
68
|
+
label: `${action.label} ${unitId}`,
|
|
69
|
+
reason: action.reason,
|
|
70
|
+
risk,
|
|
71
|
+
requiresUserConfirmation: true,
|
|
72
|
+
dryRun,
|
|
73
|
+
apply: applyOrNull(workflow, dryRun, [
|
|
74
|
+
"skillboard", "review", "install-unit", unitId, "--trust-level", recommended,
|
|
75
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--json"
|
|
76
|
+
]),
|
|
77
|
+
appliesTo: { kind: "install_unit", id: unitId },
|
|
78
|
+
blockedReason: blockedByWorkflow(workflow),
|
|
79
|
+
advanced: { trust_level: recommended }
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function activateSkillActions({ paths, workflow, skills, workspace }) {
|
|
85
|
+
const candidates = [...skills.not_in_workflow, ...skills.blocked.filter((skill) => missingProvenance(workspace, skill.id))];
|
|
86
|
+
return candidates.flatMap((skill) => {
|
|
87
|
+
if (missingProvenance(workspace, skill.id)) {
|
|
88
|
+
return [blockedActivateAction(paths, workflow, skill)];
|
|
89
|
+
}
|
|
90
|
+
if (!canActivate(skill)) {
|
|
91
|
+
return [];
|
|
92
|
+
}
|
|
93
|
+
const mode = skill.advanced.invocation;
|
|
94
|
+
const risk = riskFromSkill(skill);
|
|
95
|
+
const dryRun = command([
|
|
96
|
+
"skillboard", "activate", skill.id, "--workflow", workflow.selected, "--mode", mode,
|
|
97
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--dry-run", "--json"
|
|
98
|
+
]);
|
|
99
|
+
return [makeAction({
|
|
100
|
+
kind: "activate-skill",
|
|
101
|
+
targetId: skill.id,
|
|
102
|
+
label: `Activate ${skill.id} in this workflow`,
|
|
103
|
+
reason: "Add this reviewed skill to the selected workflow.",
|
|
104
|
+
risk,
|
|
105
|
+
dryRun,
|
|
106
|
+
apply: applyOrNull(workflow, dryRun, [
|
|
107
|
+
"skillboard", "activate", skill.id, "--workflow", workflow.selected, "--mode", mode,
|
|
108
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--json"
|
|
109
|
+
]),
|
|
110
|
+
appliesTo: { kind: "skill", id: skill.id, workflow: workflow.selected },
|
|
111
|
+
blockedReason: blockedByWorkflow(workflow),
|
|
112
|
+
advanced: skillAdvanced(skill, { mode })
|
|
113
|
+
})];
|
|
114
|
+
});
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function blockSkillActions({ paths, workflow, skills }) {
|
|
118
|
+
if (!workflowResolved(workflow)) {
|
|
119
|
+
return [];
|
|
120
|
+
}
|
|
121
|
+
return [...skills.automatic_allowed, ...skills.manual_allowed].map((skill) => {
|
|
122
|
+
const dryRun = command([
|
|
123
|
+
"skillboard", "block", skill.id, "--workflow", workflow.selected,
|
|
124
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--dry-run", "--json"
|
|
125
|
+
]);
|
|
126
|
+
return makeAction({
|
|
127
|
+
kind: "block-skill",
|
|
128
|
+
targetId: skill.id,
|
|
129
|
+
label: `Disable ${skill.id} in this workflow`,
|
|
130
|
+
reason: "Workflow-scoped disable; this does not delete the skill.",
|
|
131
|
+
risk: "medium",
|
|
132
|
+
dryRun,
|
|
133
|
+
apply: command([
|
|
134
|
+
"skillboard", "block", skill.id, "--workflow", workflow.selected,
|
|
135
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--json"
|
|
136
|
+
]),
|
|
137
|
+
appliesTo: { kind: "skill", id: skill.id, workflow: workflow.selected },
|
|
138
|
+
blockedReason: null,
|
|
139
|
+
advanced: skillAdvanced(skill)
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function hookInstallActions({ paths, workflow }) {
|
|
145
|
+
if (workflow.selected === null || workflow.unknown) {
|
|
146
|
+
return [];
|
|
147
|
+
}
|
|
148
|
+
const dryRun = command([
|
|
149
|
+
"skillboard", "hook", "install", "--workflow", workflow.selected,
|
|
150
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--dry-run", "--json"
|
|
151
|
+
]);
|
|
152
|
+
return [makeAction({
|
|
153
|
+
kind: "hook-install",
|
|
154
|
+
targetId: `guard:${workflow.selected}`,
|
|
155
|
+
label: `Preview guard hook install for ${workflow.selected}`,
|
|
156
|
+
reason: "Preview the workflow guard hook install; no files will be changed until you apply it.",
|
|
157
|
+
risk: "high",
|
|
158
|
+
dryRun,
|
|
159
|
+
apply: applyOrNull(workflow, dryRun, [
|
|
160
|
+
"skillboard", "hook", "install", "--workflow", workflow.selected,
|
|
161
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--json"
|
|
162
|
+
]),
|
|
163
|
+
appliesTo: { kind: "hook", id: `guard:${workflow.selected}`, workflow: workflow.selected },
|
|
164
|
+
blockedReason: blockedByWorkflow(workflow),
|
|
165
|
+
advanced: { workflow: workflow.selected }
|
|
166
|
+
})];
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function removeSkillForceActions({ paths, workflow, skills, workspace }) {
|
|
170
|
+
return skills.blocked
|
|
171
|
+
.filter((skill) => !missingProvenance(workspace, skill.id))
|
|
172
|
+
.map((skill) => {
|
|
173
|
+
const dryRun = command([
|
|
174
|
+
"skillboard", "remove", "skill", skill.id, "--force",
|
|
175
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--dry-run", "--json"
|
|
176
|
+
]);
|
|
177
|
+
return makeAction({
|
|
178
|
+
kind: "remove-skill-force",
|
|
179
|
+
targetId: skill.id,
|
|
180
|
+
label: `Remove ${skill.id} from config with force`,
|
|
181
|
+
reason: "Preview forced config-reference removal before applying it.",
|
|
182
|
+
risk: "high",
|
|
183
|
+
dryRun,
|
|
184
|
+
apply: applyOrNull(workflow, dryRun, [
|
|
185
|
+
"skillboard", "remove", "skill", skill.id, "--force",
|
|
186
|
+
"--config", paths.configPath, "--skills", paths.skillsRoot, "--json"
|
|
187
|
+
]),
|
|
188
|
+
appliesTo: { kind: "skill", id: skill.id },
|
|
189
|
+
blockedReason: blockedByWorkflow(workflow),
|
|
190
|
+
advanced: skillAdvanced(skill, { force: true })
|
|
191
|
+
});
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resetCleanupAction({ paths, workflow, cleanup }) {
|
|
196
|
+
const dryRun = command([
|
|
197
|
+
"skillboard", "uninstall", "--dir", paths.root, "--dry-run",
|
|
198
|
+
"--reset-config", "--remove-reports", "--remove-hooks"
|
|
199
|
+
]);
|
|
200
|
+
return makeAction({
|
|
201
|
+
kind: "reset-cleanup",
|
|
202
|
+
targetId: paths.root,
|
|
203
|
+
label: "Reset SkillBoard generated project files",
|
|
204
|
+
reason: "Preview full SkillBoard cleanup before applying it.",
|
|
205
|
+
risk: "destructive",
|
|
206
|
+
dryRun,
|
|
207
|
+
apply: applyOrNull(workflow, dryRun, [
|
|
208
|
+
"skillboard", "uninstall", "--dir", paths.root,
|
|
209
|
+
"--reset-config", "--remove-reports", "--remove-hooks"
|
|
210
|
+
]),
|
|
211
|
+
appliesTo: { kind: "project", id: paths.root },
|
|
212
|
+
blockedReason: blockedByWorkflow(workflow),
|
|
213
|
+
advanced: { full_reset: cleanup.full_reset }
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
function blockedActivateAction(paths, workflow, skill) {
|
|
218
|
+
return makeAction({
|
|
219
|
+
kind: "activate-skill",
|
|
220
|
+
targetId: skill.id,
|
|
221
|
+
label: `Activation blocked for ${skill.id}`,
|
|
222
|
+
reason: "Skill provenance must be declared before activation can be suggested.",
|
|
223
|
+
risk: "high",
|
|
224
|
+
dryRun: null,
|
|
225
|
+
apply: null,
|
|
226
|
+
appliesTo: { kind: "skill", id: skill.id, workflow: workflow.selected },
|
|
227
|
+
blockedReason: `Missing owner_install_unit/provenance for unit-managed skill ${skill.id}.`,
|
|
228
|
+
advanced: skillAdvanced(skill, { config_path: paths.configPath })
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function canActivate(skill) {
|
|
233
|
+
return WRITABLE_MODES.has(skill.advanced.invocation) && skill.advanced.trust.reviewed === true;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function missingProvenance(workspace, skillId) {
|
|
237
|
+
const skill = workspace.skills.find((candidate) => candidate.id === skillId);
|
|
238
|
+
if (skill?.exposure === "unit-managed" && skill.ownerInstallUnit === undefined) {
|
|
239
|
+
return true;
|
|
240
|
+
}
|
|
241
|
+
return skill?.ownerInstallUnit !== undefined
|
|
242
|
+
&& !workspace.installUnits.some((unit) => unit.id === skill.ownerInstallUnit);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function riskFromSkill(skill) {
|
|
246
|
+
return skill.advanced.trust.permissionRisk === "high" ? "high" : "medium";
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function skillAdvanced(skill, extra = {}) {
|
|
250
|
+
return {
|
|
251
|
+
invocation: skill.advanced.invocation,
|
|
252
|
+
owner_install_unit: skill.advanced.owner_install_unit,
|
|
253
|
+
source_class: skill.advanced.source_class,
|
|
254
|
+
...extra
|
|
255
|
+
};
|
|
256
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import {
|
|
2
|
+
command,
|
|
3
|
+
workflowResolved
|
|
4
|
+
} from "./action-core.mjs";
|
|
5
|
+
|
|
6
|
+
export function withApplicationCommands(actions, { paths, workflow }) {
|
|
7
|
+
return actions.map((action) => {
|
|
8
|
+
return {
|
|
9
|
+
...action,
|
|
10
|
+
application: applicationCommand(action, paths, workflow)
|
|
11
|
+
};
|
|
12
|
+
});
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function applicationCommand(action, paths, workflow) {
|
|
16
|
+
const blockedReason = applicationBlockedReason(action, workflow);
|
|
17
|
+
if (blockedReason !== null) {
|
|
18
|
+
return {
|
|
19
|
+
preview: null,
|
|
20
|
+
apply: null,
|
|
21
|
+
blocked_reason: blockedReason
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const base = [
|
|
26
|
+
"skillboard", "apply-action", action.id,
|
|
27
|
+
...workflowArgs(workflow),
|
|
28
|
+
"--dir", paths.root,
|
|
29
|
+
"--config", paths.configPath,
|
|
30
|
+
"--skills", paths.skillsRoot,
|
|
31
|
+
"--json"
|
|
32
|
+
];
|
|
33
|
+
return {
|
|
34
|
+
preview: command([...base, "--dry-run"]),
|
|
35
|
+
apply: command([...base, "--yes", ...destructiveArgs(action)]),
|
|
36
|
+
blocked_reason: null
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function applicationBlockedReason(action, workflow) {
|
|
41
|
+
if (action.blocked_reason !== null) {
|
|
42
|
+
return action.blocked_reason;
|
|
43
|
+
}
|
|
44
|
+
if (action.apply === null) {
|
|
45
|
+
return "Action cannot be applied directly.";
|
|
46
|
+
}
|
|
47
|
+
if (!workflowResolved(workflow)) {
|
|
48
|
+
return workflow.blocked_reason ?? "Select a workflow before applying action cards.";
|
|
49
|
+
}
|
|
50
|
+
return null;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function workflowArgs(workflow) {
|
|
54
|
+
return workflowResolved(workflow) ? ["--workflow", workflow.selected] : [];
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function destructiveArgs(action) {
|
|
58
|
+
return action.kind === "reset-cleanup" ? ["--allow-destructive"] : [];
|
|
59
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { buildSkillBrief } from "../advisor.mjs";
|
|
2
|
+
import {
|
|
3
|
+
activateSkill,
|
|
4
|
+
blockSkill,
|
|
5
|
+
installGuardHook,
|
|
6
|
+
removeSkill
|
|
7
|
+
} from "../control.mjs";
|
|
8
|
+
import { reviewInstallUnit } from "../review.mjs";
|
|
9
|
+
import { uninstallProject } from "../uninstall.mjs";
|
|
10
|
+
|
|
11
|
+
const INSTALL_UNIT_ACTIONS = new Set([
|
|
12
|
+
"block-install-unit",
|
|
13
|
+
"review-install-unit",
|
|
14
|
+
"trust-install-unit"
|
|
15
|
+
]);
|
|
16
|
+
|
|
17
|
+
export class ApplyActionError extends Error {
|
|
18
|
+
constructor(code, message) {
|
|
19
|
+
super(message);
|
|
20
|
+
this.name = "ApplyActionError";
|
|
21
|
+
this.code = code;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export async function applyAdvisorAction(actionId, options) {
|
|
26
|
+
if (actionId === undefined || actionId.length === 0) {
|
|
27
|
+
throw new ApplyActionError("missing-action-id", "Usage: skillboard apply-action <action-id>");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const action = await resolveCurrentAction(actionId, options);
|
|
31
|
+
if (action.blocked_reason !== null) {
|
|
32
|
+
throw new ApplyActionError("blocked-action", action.blocked_reason);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
if (previewMode(options)) {
|
|
36
|
+
return {
|
|
37
|
+
ok: true,
|
|
38
|
+
mode: "preview",
|
|
39
|
+
changed: false,
|
|
40
|
+
action
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
if (action.apply === null) {
|
|
45
|
+
throw new ApplyActionError("action-not-applicable", `Action cannot be applied directly: ${action.id}`);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (action.kind === "reset-cleanup" && options.allowDestructive !== true) {
|
|
49
|
+
throw new ApplyActionError(
|
|
50
|
+
"destructive-confirmation-required",
|
|
51
|
+
"reset-cleanup is destructive; pass --allow-destructive with --yes to apply it."
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const control = await dispatchAction(action, options);
|
|
56
|
+
const brief = await buildActionBrief(options);
|
|
57
|
+
return {
|
|
58
|
+
ok: true,
|
|
59
|
+
mode: "applied",
|
|
60
|
+
changed: controlChanged(action, control),
|
|
61
|
+
action,
|
|
62
|
+
control,
|
|
63
|
+
brief
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function applyActionErrorPayload(error) {
|
|
68
|
+
if (error instanceof ApplyActionError) {
|
|
69
|
+
return { ok: false, error: { code: error.code, message: error.message } };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
ok: false,
|
|
73
|
+
error: {
|
|
74
|
+
code: "apply-action-failed",
|
|
75
|
+
message: error instanceof Error ? error.message : String(error)
|
|
76
|
+
}
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async function resolveCurrentAction(actionId, options) {
|
|
81
|
+
const brief = await buildActionBrief(options);
|
|
82
|
+
const actions = Array.isArray(brief.actions) ? brief.actions : [];
|
|
83
|
+
const matches = actions.filter((action) => action.id === actionId);
|
|
84
|
+
if (matches.length === 1) {
|
|
85
|
+
return matches[0];
|
|
86
|
+
}
|
|
87
|
+
if (matches.length > 1) {
|
|
88
|
+
throw new ApplyActionError("ambiguous-action", `Action id resolved more than once: ${actionId}`);
|
|
89
|
+
}
|
|
90
|
+
throw new ApplyActionError(
|
|
91
|
+
"stale-action",
|
|
92
|
+
`Action is no longer current or was not found: ${actionId}`
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function buildActionBrief(options) {
|
|
97
|
+
return buildSkillBrief({
|
|
98
|
+
includeActions: true,
|
|
99
|
+
workflow: options.workflow,
|
|
100
|
+
configPath: options.configPath,
|
|
101
|
+
skillsRoot: options.skillsRoot,
|
|
102
|
+
root: options.root
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function previewMode(options) {
|
|
107
|
+
return options.dryRun === true || options.yes !== true;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function dispatchAction(action, options) {
|
|
111
|
+
if (INSTALL_UNIT_ACTIONS.has(action.kind)) {
|
|
112
|
+
return await reviewInstallUnit({
|
|
113
|
+
unitId: action.applies_to.id,
|
|
114
|
+
trustLevel: action.advanced.trust_level,
|
|
115
|
+
configPath: options.configPath,
|
|
116
|
+
skillsRoot: options.skillsRoot,
|
|
117
|
+
dryRun: false
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if (action.kind === "activate-skill") {
|
|
122
|
+
return await activateSkill({
|
|
123
|
+
skillId: action.applies_to.id,
|
|
124
|
+
workflow: action.applies_to.workflow,
|
|
125
|
+
mode: action.advanced.mode,
|
|
126
|
+
configPath: options.configPath,
|
|
127
|
+
skillsRoot: options.skillsRoot,
|
|
128
|
+
dryRun: false
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
if (action.kind === "block-skill") {
|
|
133
|
+
return await blockSkill({
|
|
134
|
+
skillId: action.applies_to.id,
|
|
135
|
+
workflow: action.applies_to.workflow,
|
|
136
|
+
configPath: options.configPath,
|
|
137
|
+
skillsRoot: options.skillsRoot,
|
|
138
|
+
dryRun: false
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if (action.kind === "hook-install") {
|
|
143
|
+
return await installGuardHook({
|
|
144
|
+
workflow: action.advanced.workflow,
|
|
145
|
+
out: options.hookOut,
|
|
146
|
+
command: options.skillboardBin,
|
|
147
|
+
configPath: options.configPath,
|
|
148
|
+
skillsRoot: options.skillsRoot
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
if (action.kind === "remove-skill-force") {
|
|
153
|
+
return await removeSkill({
|
|
154
|
+
skillId: action.applies_to.id,
|
|
155
|
+
force: true,
|
|
156
|
+
configPath: options.configPath,
|
|
157
|
+
skillsRoot: options.skillsRoot,
|
|
158
|
+
dryRun: false
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
if (action.kind === "reset-cleanup") {
|
|
163
|
+
return await uninstallProject({
|
|
164
|
+
root: action.applies_to.id,
|
|
165
|
+
dryRun: false,
|
|
166
|
+
resetConfig: true,
|
|
167
|
+
removeReports: true,
|
|
168
|
+
removeHooks: true
|
|
169
|
+
});
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
throw new ApplyActionError("unsupported-action", `Unsupported action kind: ${action.kind}`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function controlChanged(action, control) {
|
|
176
|
+
if (typeof control.changed === "boolean") {
|
|
177
|
+
return control.changed;
|
|
178
|
+
}
|
|
179
|
+
if (action.kind === "reset-cleanup") {
|
|
180
|
+
return control.removed.length > 0 || control.updated.length > 0;
|
|
181
|
+
}
|
|
182
|
+
return true;
|
|
183
|
+
}
|