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.
Files changed (94) hide show
  1. package/CONTRIBUTING.md +83 -0
  2. package/LICENSE +21 -0
  3. package/README.md +645 -0
  4. package/bin/skillboard.mjs +4 -0
  5. package/docs/adapters.md +127 -0
  6. package/docs/capabilities.md +107 -0
  7. package/docs/install.md +346 -0
  8. package/docs/plans/20260625-080025-skillboard-mvp-review.md +189 -0
  9. package/docs/plans/README.md +20 -0
  10. package/docs/policy-model.md +251 -0
  11. package/docs/positioning.md +94 -0
  12. package/docs/profiles.md +166 -0
  13. package/docs/rollout-runbook.md +60 -0
  14. package/docs/user-flow.md +231 -0
  15. package/docs/versioning.md +201 -0
  16. package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -0
  17. package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -0
  18. package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -0
  19. package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -0
  20. package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -0
  21. package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -0
  22. package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -0
  23. package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -0
  24. package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -0
  25. package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -0
  26. package/examples/multi-source.config.yaml +271 -0
  27. package/examples/skillboard.config.yaml +194 -0
  28. package/examples/skills/grill-me/SKILL.md +9 -0
  29. package/examples/skills/grill-with-docs/SKILL.md +9 -0
  30. package/examples/skills/requirement-intake/SKILL.md +9 -0
  31. package/examples/skills/tdd/SKILL.md +8 -0
  32. package/package.json +58 -0
  33. package/profiles/anthropics-skills.yaml +17 -0
  34. package/profiles/mattpocock-skills.yaml +26 -0
  35. package/profiles/oh-my-openagent.yaml +32 -0
  36. package/profiles/voltagent-awesome-agent-skills.yaml +18 -0
  37. package/profiles/wshobson-agents.yaml +19 -0
  38. package/src/advisor/action-core.mjs +74 -0
  39. package/src/advisor/actions.mjs +256 -0
  40. package/src/advisor/application-commands.mjs +59 -0
  41. package/src/advisor/apply-action.mjs +183 -0
  42. package/src/advisor/schema.mjs +112 -0
  43. package/src/advisor/setup-actions.mjs +42 -0
  44. package/src/advisor/skills.mjs +191 -0
  45. package/src/advisor/sort.mjs +15 -0
  46. package/src/advisor/sources.mjs +160 -0
  47. package/src/advisor/trust-policy.mjs +65 -0
  48. package/src/advisor/workflow.mjs +41 -0
  49. package/src/advisor.mjs +134 -0
  50. package/src/agent-inventory-platforms.mjs +100 -0
  51. package/src/agent-inventory.mjs +804 -0
  52. package/src/brief-cli.mjs +40 -0
  53. package/src/brief-renderer.mjs +362 -0
  54. package/src/change-plan.mjs +169 -0
  55. package/src/cli.mjs +1171 -0
  56. package/src/config-helpers.mjs +72 -0
  57. package/src/control/can-use-guard.mjs +91 -0
  58. package/src/control/config-write.mjs +163 -0
  59. package/src/control/skill-crud.mjs +394 -0
  60. package/src/control/source-trust.mjs +161 -0
  61. package/src/control/workflow-crud.mjs +104 -0
  62. package/src/control.mjs +197 -0
  63. package/src/doctor.mjs +299 -0
  64. package/src/domain/constants.mjs +47 -0
  65. package/src/domain/indexes.mjs +29 -0
  66. package/src/domain/rules/capabilities.mjs +33 -0
  67. package/src/domain/rules/harnesses.mjs +16 -0
  68. package/src/domain/rules/install-units.mjs +95 -0
  69. package/src/domain/rules/skills.mjs +105 -0
  70. package/src/domain/rules/workflows.mjs +105 -0
  71. package/src/domain/skill-state-matrix.mjs +79 -0
  72. package/src/domain/source-classes.mjs +99 -0
  73. package/src/hook-plan.mjs +152 -0
  74. package/src/impact.mjs +41 -0
  75. package/src/index.mjs +82 -0
  76. package/src/init.mjs +136 -0
  77. package/src/install-output-detector.mjs +337 -0
  78. package/src/install-units.mjs +62 -0
  79. package/src/inventory-refresh.mjs +45 -0
  80. package/src/lifecycle-cli.mjs +166 -0
  81. package/src/lifecycle-content.mjs +87 -0
  82. package/src/policy.mjs +42 -0
  83. package/src/reconcile.mjs +111 -0
  84. package/src/report.mjs +151 -0
  85. package/src/review.mjs +88 -0
  86. package/src/rollout.mjs +453 -0
  87. package/src/skill-paths.mjs +17 -0
  88. package/src/source-cache.mjs +178 -0
  89. package/src/source-profile-loader.mjs +160 -0
  90. package/src/source-profiles.mjs +299 -0
  91. package/src/source-verification.mjs +252 -0
  92. package/src/uninstall.mjs +258 -0
  93. package/src/workspace.mjs +219 -0
  94. 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
+ }