agent-skillboard 0.2.18 → 0.3.1
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 +85 -0
- package/README.md +161 -255
- package/bin/postinstall.mjs +2 -1
- package/docs/adapters.md +37 -113
- package/docs/ai-skill-routing-goal.md +41 -108
- package/docs/capabilities.md +17 -104
- package/docs/install.md +70 -473
- package/docs/policy-model.md +50 -280
- package/docs/positioning.md +19 -86
- package/docs/profiles.md +21 -153
- package/docs/reference.md +133 -362
- package/docs/rollout-runbook.md +23 -25
- package/docs/routing.md +23 -90
- package/docs/user-flow.md +68 -279
- package/docs/value-proof.md +23 -181
- package/docs/variant-lifecycle.md +47 -67
- package/docs/versioning.md +49 -269
- 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 +96 -13
- package/src/agent-integration-content.mjs +21 -11
- package/src/agent-integration-files.mjs +1 -1
- package/src/agent-integration-home.mjs +14 -1
- package/src/agent-inventory-platforms.mjs +21 -8
- package/src/agent-inventory.mjs +44 -16
- package/src/agent-root-registry.mjs +127 -0
- package/src/agent-skill-import.mjs +2 -2
- package/src/agent-skill-roots.mjs +70 -13
- 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 +521 -235
- 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 +71 -5
- 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/install-health.mjs +177 -0
- package/src/inventory-install-units.mjs +63 -0
- package/src/inventory-json.mjs +279 -0
- package/src/inventory-refresh.mjs +168 -19
- 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-reconcile.mjs +97 -0
- package/src/shared-skill.mjs +325 -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 +161 -0
- package/src/workspace.mjs +119 -79
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) {
|
package/src/advisor.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import {
|
|
|
4
4
|
} from "./control.mjs";
|
|
5
5
|
import { doctorProject } from "./doctor.mjs";
|
|
6
6
|
import { routeSkill } from "./route.mjs";
|
|
7
|
-
import { buildActionCards, buildInitActions } from "./advisor/actions.mjs";
|
|
7
|
+
import { buildActionCards, buildInitActions, buildV2ActionCards } from "./advisor/actions.mjs";
|
|
8
8
|
import {
|
|
9
9
|
buildBrief,
|
|
10
10
|
buildCleanup,
|
|
@@ -25,14 +25,14 @@ import { loadWorkspace } from "./workspace.mjs";
|
|
|
25
25
|
|
|
26
26
|
export async function buildSkillBrief(options = {}) {
|
|
27
27
|
const paths = resolveProjectPaths(options);
|
|
28
|
-
const cleanup = await buildCleanup(paths.root);
|
|
29
28
|
const configDoctor = await doctorProject({
|
|
30
29
|
root: paths.root,
|
|
31
30
|
configPath: paths.configPath,
|
|
32
31
|
skillsRoot: paths.skillsRoot
|
|
33
32
|
});
|
|
34
33
|
|
|
35
|
-
if (!configDoctor.config.exists
|
|
34
|
+
if (!configDoctor.config.exists) {
|
|
35
|
+
const cleanup = userStateCleanup();
|
|
36
36
|
return buildBrief({
|
|
37
37
|
ok: false,
|
|
38
38
|
error: {
|
|
@@ -50,6 +50,7 @@ export async function buildSkillBrief(options = {}) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
if (!configDoctor.config.valid) {
|
|
53
|
+
const cleanup = userStateCleanup();
|
|
53
54
|
return buildExpectedConfigError(configDoctor, paths, cleanup, {
|
|
54
55
|
code: "invalid-config",
|
|
55
56
|
message: configDoctor.config.error ?? "skillboard.config.yaml is invalid"
|
|
@@ -60,12 +61,15 @@ export async function buildSkillBrief(options = {}) {
|
|
|
60
61
|
try {
|
|
61
62
|
workspace = await loadWorkspace({ configPath: paths.configPath, skillsRoot: paths.skillsRoot });
|
|
62
63
|
} catch (error) {
|
|
64
|
+
const cleanup = userStateCleanup();
|
|
63
65
|
return buildExpectedConfigError(configDoctor, paths, cleanup, {
|
|
64
66
|
code: "invalid-config",
|
|
65
67
|
message: error instanceof Error ? error.message : String(error)
|
|
66
68
|
}, options);
|
|
67
69
|
}
|
|
68
70
|
|
|
71
|
+
const cleanup = workspace.version === 2 ? userStateCleanup() : await buildCleanup(paths.root);
|
|
72
|
+
|
|
69
73
|
const doctor = await doctorProject({
|
|
70
74
|
root: paths.root,
|
|
71
75
|
configPath: paths.configPath,
|
|
@@ -74,13 +78,16 @@ export async function buildSkillBrief(options = {}) {
|
|
|
74
78
|
verifySources: options.verifySources
|
|
75
79
|
});
|
|
76
80
|
const sourceAudit = auditSources(workspace);
|
|
77
|
-
const workflow =
|
|
78
|
-
|
|
81
|
+
const workflow = workspace.version === 2
|
|
82
|
+
? emptyWorkflowState()
|
|
83
|
+
: resolveWorkflow(listWorkflows(workspace), options.workflow);
|
|
84
|
+
const reviewQueue = workspace.version === 2 ? [] : buildReviewQueue(workspace, sourceAudit);
|
|
79
85
|
|
|
80
86
|
if (workflow.unknown) {
|
|
81
87
|
const skills = skillsWithoutWorkflow(workspace);
|
|
82
88
|
const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
|
|
83
89
|
return buildBrief({
|
|
90
|
+
compatibility: workspace.compatibility,
|
|
84
91
|
ok: false,
|
|
85
92
|
error: {
|
|
86
93
|
code: "unknown-workflow",
|
|
@@ -96,11 +103,13 @@ export async function buildSkillBrief(options = {}) {
|
|
|
96
103
|
});
|
|
97
104
|
}
|
|
98
105
|
|
|
99
|
-
const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit);
|
|
106
|
+
const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit, options.agent);
|
|
100
107
|
const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
|
|
101
108
|
const route = routeForBrief({ options, paths, workflow, workspace });
|
|
102
|
-
const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.
|
|
109
|
+
const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.inventory.ok
|
|
110
|
+
&& (workspace.version === 2 || doctor.sources.ok);
|
|
103
111
|
return buildBrief({
|
|
112
|
+
compatibility: workspace.compatibility,
|
|
104
113
|
ok: availabilityOk,
|
|
105
114
|
health: healthForBrief(doctor, paths, availabilityOk),
|
|
106
115
|
workflow,
|
|
@@ -113,14 +122,22 @@ export async function buildSkillBrief(options = {}) {
|
|
|
113
122
|
});
|
|
114
123
|
}
|
|
115
124
|
|
|
125
|
+
function userStateCleanup() {
|
|
126
|
+
return {
|
|
127
|
+
conservative: { dryRun: true, removed: [], updated: [], preserved: [] },
|
|
128
|
+
full_reset: { dryRun: true, removed: [], updated: [], preserved: [] }
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
116
132
|
function routeForBrief({ options, paths, workflow, workspace }) {
|
|
117
133
|
const intent = options.intent?.trim();
|
|
118
|
-
|
|
134
|
+
const workflowRequired = workspace.version !== 2 && workflow.selected === null;
|
|
135
|
+
if (intent === undefined || intent.length === 0 || workflowRequired || workflow.unknown || workflow.needs_selection) {
|
|
119
136
|
return undefined;
|
|
120
137
|
}
|
|
121
138
|
return routeSkill(workspace, {
|
|
122
139
|
intent,
|
|
123
|
-
workflow: workflow.selected,
|
|
140
|
+
...(workspace.version === 2 ? { agent: options.agent } : workflow.selected === null ? {} : { workflow: workflow.selected }),
|
|
124
141
|
configPath: paths.configPath,
|
|
125
142
|
skillsRoot: paths.skillsRoot
|
|
126
143
|
});
|
|
@@ -155,6 +172,7 @@ function actionsForBrief(context) {
|
|
|
155
172
|
if (!requestedActions(context.options)) {
|
|
156
173
|
return { reviewQueue: context.reviewQueue, actions: undefined };
|
|
157
174
|
}
|
|
175
|
+
if (context.workspace.version === 2) return { reviewQueue: [], actions: buildV2ActionCards(context) };
|
|
158
176
|
return buildActionCards(context);
|
|
159
177
|
}
|
|
160
178
|
|
|
@@ -1,16 +1,30 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve } from "node:path";
|
|
2
|
+
import { mkdir } from "node:fs/promises";
|
|
2
3
|
import { createInterface } from "node:readline";
|
|
3
4
|
import { installAgentIntegration, uninstallAgentIntegration } from "./agent-integration-files.mjs";
|
|
4
|
-
import { resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
|
|
5
|
+
import { applyOwnership, applyOwnershipTree, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
|
|
5
6
|
import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
|
|
7
|
+
import {
|
|
8
|
+
agentRootRegistryPath,
|
|
9
|
+
loadRegisteredAgentRoots,
|
|
10
|
+
mergeRegisteredAgentRoots,
|
|
11
|
+
proposedAgentRoot,
|
|
12
|
+
writeRegisteredAgentRoots
|
|
13
|
+
} from "./agent-root-registry.mjs";
|
|
14
|
+
import { refreshAgentInventory } from "./inventory-refresh.mjs";
|
|
15
|
+
import { reconcileSharedSkills } from "./shared-skill-reconcile.mjs";
|
|
6
16
|
|
|
7
17
|
export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
|
|
18
|
+
assertSetupOptions(options);
|
|
8
19
|
if (options.get("dir") !== undefined) {
|
|
9
20
|
throw new Error("skillboard setup is agent-layer setup and does not accept --dir");
|
|
10
21
|
}
|
|
11
22
|
const env = runtime.env ?? process.env;
|
|
12
23
|
const home = await resolveSetupHome(env, runtime);
|
|
13
|
-
const
|
|
24
|
+
const existingRoots = await loadRegisteredAgentRoots(home);
|
|
25
|
+
const proposedRoot = await setupSkillRoot(options, home, runtime.cwd ?? process.cwd());
|
|
26
|
+
const registeredRoots = mergeRegisteredAgentRoots(existingRoots, proposedRoot);
|
|
27
|
+
const targets = await agentSetupTargets(options, runtime, home, registeredRoots);
|
|
14
28
|
if (targets.length === 0) {
|
|
15
29
|
stdout.write("No supported agent user skill roots were detected.\n");
|
|
16
30
|
stdout.write(`Create a supported agent home or pass --agent ${supportedAgentNames().join(",")} to choose targets.\n`);
|
|
@@ -27,15 +41,53 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
|
|
|
27
41
|
return 1;
|
|
28
42
|
}
|
|
29
43
|
}
|
|
30
|
-
const
|
|
44
|
+
const ownership = setupOwnership(env, runtime, home);
|
|
45
|
+
const result = await installAgentIntegration(targets, ownership);
|
|
46
|
+
await mkdir(home, { recursive: true });
|
|
47
|
+
let inventory = await refreshAgentInventory({
|
|
48
|
+
root: home,
|
|
49
|
+
home,
|
|
50
|
+
env,
|
|
51
|
+
registeredRoots,
|
|
52
|
+
preserveLegacyPolicy: true
|
|
53
|
+
});
|
|
54
|
+
const configPath = resolve(home, inventory.configPath);
|
|
55
|
+
const inventoryPath = inventory.inventoryPath === null ? null : resolve(home, inventory.inventoryPath);
|
|
56
|
+
const shared = inventoryPath === null
|
|
57
|
+
? { created: [], unchanged: [], preserved: [], blocked: [] }
|
|
58
|
+
: await reconcileSharedSkills({ home, env, targets, configPath, inventoryPath });
|
|
59
|
+
if (shared.created.length > 0) {
|
|
60
|
+
inventory = await refreshAgentInventory({ root: home, home, env, registeredRoots });
|
|
61
|
+
}
|
|
62
|
+
for (const entry of [...shared.created, ...shared.unchanged]) {
|
|
63
|
+
await applyOwnershipTree(entry.path, ownership);
|
|
64
|
+
}
|
|
65
|
+
if (proposedRoot !== undefined) {
|
|
66
|
+
await writeRegisteredAgentRoots(home, registeredRoots);
|
|
67
|
+
await applyOwnership(agentRootRegistryPath(home), ownership);
|
|
68
|
+
}
|
|
69
|
+
await applyOwnership(configPath, ownership);
|
|
70
|
+
if (inventoryPath !== null) await applyOwnership(inventoryPath, ownership);
|
|
31
71
|
stdout.write("SkillBoard agent integration installed.\n");
|
|
32
72
|
writeList(stdout, "Created", result.created);
|
|
33
73
|
writeList(stdout, "Updated", result.updated);
|
|
34
74
|
writeList(stdout, "Unchanged", result.unchanged);
|
|
35
75
|
writeList(stdout, "Preserved", result.preserved);
|
|
76
|
+
if (proposedRoot !== undefined) stdout.write(`Registered agent roots: ${registeredRoots.length}\n`);
|
|
77
|
+
if (shared.created.length > 0) stdout.write(`Created shared copies: ${shared.created.length}\n`);
|
|
78
|
+
if (shared.unchanged.length > 0) stdout.write(`Unchanged shared copies: ${shared.unchanged.length}\n`);
|
|
79
|
+
writeList(stdout, "Preserved shared copies", shared.preserved.map(formatSharedEntry));
|
|
80
|
+
writeList(stdout, "Blocked shared copies", shared.blocked.map(formatBlockedEntry));
|
|
81
|
+
stdout.write(`User policy: ${inventory.configPath}\n`);
|
|
82
|
+
stdout.write(`Observed skills: ${inventory.scan.scannedSkills}\n`);
|
|
83
|
+
if (inventory.inventoryPath === null) {
|
|
84
|
+
stdout.write("Policy version 1 remains read-only during SkillBoard v0.3.x.\n");
|
|
85
|
+
stdout.write(`Preview migration: ${commandPrefix(runtime)} migrate v2 --config ${shellQuote(configPath, runtime.platform)} --json\n`);
|
|
86
|
+
}
|
|
36
87
|
stdout.write("Next:\n");
|
|
37
88
|
stdout.write("- Restart or refresh agents that cache user skills.\n");
|
|
38
|
-
stdout.write("-
|
|
89
|
+
stdout.write("- Run skillboard doctor --summary to check policy and executable paths.\n");
|
|
90
|
+
stdout.write("- User-level policy and inventory were refreshed; no project was initialized.\n");
|
|
39
91
|
stdout.write('- Ask the agent in a workspace: "Review this plan and point out weak assumptions."\n');
|
|
40
92
|
stdout.write("- SkillBoard will step in when skills overlap, routing is ambiguous, or you ask for a skill decision.\n");
|
|
41
93
|
return 0;
|
|
@@ -76,7 +128,7 @@ function writeList(stdout, label, values) {
|
|
|
76
128
|
function writeSetupConfirmation(stdout, targets, command) {
|
|
77
129
|
stdout.write("SkillBoard setup installs agent-layer integration, not project files.\n");
|
|
78
130
|
stdout.write("It writes a SkillBoard guidance skill into detected user agent skill roots so agents can resolve skill priority when choices overlap.\n");
|
|
79
|
-
stdout.write("It
|
|
131
|
+
stdout.write("It creates ~/skillboard.config.yaml and ~/.skillboard/inventory.json as one user-level control plane; skillboard init is not needed for normal use.\n");
|
|
80
132
|
stdout.write("Targets:\n");
|
|
81
133
|
for (const target of targets) {
|
|
82
134
|
stdout.write(`- ${target.agent}: ${target.skillPath}\n`);
|
|
@@ -127,7 +179,7 @@ function defaultRuntime() {
|
|
|
127
179
|
};
|
|
128
180
|
}
|
|
129
181
|
|
|
130
|
-
async function agentSetupTargets(options, runtime, setupHome) {
|
|
182
|
+
async function agentSetupTargets(options, runtime, setupHome, registeredRoots) {
|
|
131
183
|
const env = runtime.env ?? process.env;
|
|
132
184
|
const home = setupHome ?? await resolveSetupHome(env, runtime);
|
|
133
185
|
const requested = readCsv(options.get("agent"));
|
|
@@ -138,20 +190,48 @@ async function agentSetupTargets(options, runtime, setupHome) {
|
|
|
138
190
|
if (!supported.has(name)) {
|
|
139
191
|
throw new Error(`Unsupported setup agent: ${name}`);
|
|
140
192
|
}
|
|
141
|
-
targets.push(...await setupAgentSkillTargets(name, home, env, {
|
|
193
|
+
targets.push(...await setupAgentSkillTargets(name, home, env, {
|
|
194
|
+
includeFallback: requested.length > 0,
|
|
195
|
+
registeredRoots
|
|
196
|
+
}));
|
|
142
197
|
}
|
|
143
198
|
return targets;
|
|
144
199
|
}
|
|
145
200
|
|
|
201
|
+
async function setupSkillRoot(options, home, cwd) {
|
|
202
|
+
const skillRoot = options.get("skill-root");
|
|
203
|
+
if (skillRoot === undefined) return undefined;
|
|
204
|
+
const requested = readCsv(options.get("agent"));
|
|
205
|
+
if (requested.length !== 1) {
|
|
206
|
+
throw new Error("--skill-root requires exactly one --agent value.");
|
|
207
|
+
}
|
|
208
|
+
return await proposedAgentRoot(home, requested[0], skillRoot, cwd);
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function assertSetupOptions(options) {
|
|
212
|
+
const allowed = new Set(["yes", "agent", "skill-root"]);
|
|
213
|
+
for (const option of options.keys()) {
|
|
214
|
+
if (!allowed.has(option)) throw new Error(`Unknown setup option: --${option}`);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function formatSharedEntry(entry) {
|
|
219
|
+
return `${entry.agent}:${entry.skill}:${entry.path}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function formatBlockedEntry(entry) {
|
|
223
|
+
return `${entry.agent ?? "unknown"}:${entry.skill}:${entry.reason}`;
|
|
224
|
+
}
|
|
225
|
+
|
|
146
226
|
function commandPrefix(runtime) {
|
|
147
227
|
const entrypoint = runtime.entrypointPath ?? "";
|
|
148
228
|
const normalized = entrypoint.replace(/\\/g, "/");
|
|
149
229
|
if (normalized.includes("/_npx/")) {
|
|
150
230
|
const packageSpec = runtime.packageSpec ?? "agent-skillboard";
|
|
151
|
-
return `npx --yes --package ${shellQuote(packageSpec)} skillboard`;
|
|
231
|
+
return `npx --yes --package ${shellQuote(packageSpec, runtime.platform)} skillboard`;
|
|
152
232
|
}
|
|
153
233
|
if (isSourceTreeEntrypoint(entrypoint)) {
|
|
154
|
-
return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd())}`;
|
|
234
|
+
return `node ${sourceTreeEntrypoint(entrypoint, runtime.cwd ?? process.cwd(), runtime.platform)}`;
|
|
155
235
|
}
|
|
156
236
|
return "skillboard";
|
|
157
237
|
}
|
|
@@ -167,18 +247,21 @@ function isSourceTreeEntrypoint(entrypoint) {
|
|
|
167
247
|
&& !normalized.includes("/.npm/");
|
|
168
248
|
}
|
|
169
249
|
|
|
170
|
-
function sourceTreeEntrypoint(entrypoint, cwd) {
|
|
250
|
+
function sourceTreeEntrypoint(entrypoint, cwd, platform) {
|
|
171
251
|
const absoluteEntrypoint = isAbsolute(entrypoint) ? entrypoint : resolve(cwd, entrypoint);
|
|
172
252
|
const relativeEntrypoint = relative(cwd, absoluteEntrypoint).replace(/\\/g, "/");
|
|
173
253
|
if (!relativeEntrypoint.startsWith("../") && relativeEntrypoint !== ".." && !isAbsolute(relativeEntrypoint)) {
|
|
174
|
-
return shellQuote(relativeEntrypoint);
|
|
254
|
+
return shellQuote(relativeEntrypoint, platform);
|
|
175
255
|
}
|
|
176
|
-
return shellQuote(absoluteEntrypoint);
|
|
256
|
+
return shellQuote(absoluteEntrypoint, platform);
|
|
177
257
|
}
|
|
178
258
|
|
|
179
|
-
function shellQuote(value) {
|
|
259
|
+
function shellQuote(value, platform = process.platform) {
|
|
180
260
|
if (/^[A-Za-z0-9_./:@%+=,-]+$/.test(value)) {
|
|
181
261
|
return value;
|
|
182
262
|
}
|
|
263
|
+
if (platform === "win32") {
|
|
264
|
+
return `"${value.replace(/"/g, '""')}"`;
|
|
265
|
+
}
|
|
183
266
|
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
184
267
|
}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
export const AGENT_INTEGRATION_START = "<!-- BEGIN SKILLBOARD AGENT INTEGRATION -->";
|
|
2
2
|
export const AGENT_INTEGRATION_END = "<!-- END SKILLBOARD AGENT INTEGRATION -->";
|
|
3
3
|
|
|
4
|
-
export function agentIntegrationSkill() {
|
|
4
|
+
export function agentIntegrationSkill(agent = "<current-agent>") {
|
|
5
5
|
return `---
|
|
6
6
|
name: skillboard
|
|
7
|
-
description: Use SkillBoard when several installed skills could apply, a skill choice is ambiguous,
|
|
7
|
+
description: Use SkillBoard when several installed skills could apply, a skill choice is ambiguous, or the user explicitly asks which skill should be used, shared, imported, preferred, avoided, or prioritized.
|
|
8
8
|
---
|
|
9
9
|
${AGENT_INTEGRATION_START}
|
|
10
10
|
# SkillBoard Agent Integration
|
|
@@ -13,21 +13,24 @@ Use this skill to let SkillBoard guide skill selection above individual projects
|
|
|
13
13
|
|
|
14
14
|
## Layering
|
|
15
15
|
|
|
16
|
-
- SkillBoard is the user-level control plane for skill priority, overlap resolution, and
|
|
16
|
+
- SkillBoard is the user-level control plane for skill priority, overlap resolution, and opt-in cross-agent sharing.
|
|
17
17
|
- Project management belongs to the agent or workspace layer. Do not initialize, attach, rewrite, or manage a project just because SkillBoard is installed.
|
|
18
|
-
- Package install and \`skillboard setup\`
|
|
18
|
+
- Package install and \`skillboard setup\` refresh user-agent guidance plus the home policy and inventory.
|
|
19
19
|
- \`skillboard init\` is deprecated project-local policy bootstrap. It is not needed for normal use; use it only when maintaining an existing workspace that intentionally keeps local policy files such as \`skillboard.config.yaml\`, \`.skillboard/\`, \`AGENTS.md\`, or \`CLAUDE.md\`.
|
|
20
20
|
|
|
21
21
|
## Default Behavior
|
|
22
22
|
|
|
23
|
-
- Installed
|
|
24
|
-
-
|
|
23
|
+
- Installed skills stay agent-local by default. Only a skill the user explicitly shares is promoted across supported agents.
|
|
24
|
+
- This integration is running for agent \`${agent}\`; pass \`--agent ${agent}\` to brief, route, can-use, and guard.
|
|
25
|
+
- For ordinary user requests, work normally; invoke SkillBoard only when skill choice is ambiguous, skills overlap, or the user asks for a SkillBoard or skill decision.
|
|
25
26
|
- If the user explicitly asks for a specific installed skill, honor that request when the guard allows it instead of rerouting away solely because other skills also match.
|
|
26
27
|
- Do not ask for permission merely because you selected a skill.
|
|
27
28
|
- When you use a skill, disclose it briefly at the start and completion. If SkillBoard says remembered or configured policy selected this skill while other allowed skills were available, mention that at completion.
|
|
28
29
|
|
|
29
30
|
## Cross-Agent Skill Reuse
|
|
30
31
|
|
|
32
|
+
- When the user wants one installed skill available to all supported agents, confirm that persistent policy change once and run \`skillboard skill share <skill> --json\`.
|
|
33
|
+
- To stop SkillBoard-managed propagation, confirm once and run \`skillboard skill unshare <skill> --json\`. This preserves agent-owned originals and removes only SkillBoard-managed copies.
|
|
31
34
|
- When the user wants to use a skill from another agent, run \`skillboard import-skill --from <source-agent> --to <this-agent> --skill <skill> --json\`.
|
|
32
35
|
- If SkillBoard reports the skill is compatible, install it with \`--yes\` and use the copied target-agent skill.
|
|
33
36
|
- If SkillBoard reports \`needs-adaptation\`, explain the compatibility reasons and ask before changing the skill body for this agent.
|
|
@@ -37,11 +40,18 @@ Use this skill to let SkillBoard guide skill selection above individual projects
|
|
|
37
40
|
## Ambiguity Resolution
|
|
38
41
|
|
|
39
42
|
1. Start from the user's task, not from a pre-task inventory prompt.
|
|
40
|
-
2. Identify candidate skills only when multiple installed skills plausibly match
|
|
41
|
-
3. Prefer the skill whose explicit request, description,
|
|
42
|
-
4.
|
|
43
|
-
5.
|
|
44
|
-
6.
|
|
43
|
+
2. Identify candidate skills only when multiple installed skills plausibly match or a manual skill-control request is present.
|
|
44
|
+
3. Prefer the skill whose explicit request, description, and local instructions most directly match the user's task.
|
|
45
|
+
4. Use \`skillboard brief --agent ${agent} --intent <request> --json\` or \`skillboard route <intent> --agent ${agent} --json\` to break ties.
|
|
46
|
+
5. Run \`skillboard guard use <skill-id> --agent ${agent} --json\` immediately before invoking the selected skill.
|
|
47
|
+
6. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
|
|
48
|
+
7. Continue with the selected skill; do not stop only because other candidate skills exist.
|
|
49
|
+
|
|
50
|
+
## Removal
|
|
51
|
+
|
|
52
|
+
- If an owning installer removed a skill and inventory refresh reports stale policy, confirm once before running \`skillboard skill forget <skill-id>\`. Forget never deletes skill files and refuses installed or shared skills.
|
|
53
|
+
- When the user wants to remove SkillBoard itself, run \`skillboard uninstall --user --dry-run\`, show the managed cleanup plan, confirm once, then run \`skillboard uninstall --user --yes\`. Package removal comes afterward.
|
|
54
|
+
- User cleanup preserves agent-owned and unmanaged skills while removing marker-owned shared copies, managed guidance, and SkillBoard home state.
|
|
45
55
|
|
|
46
56
|
${AGENT_INTEGRATION_END}
|
|
47
57
|
`;
|
|
@@ -8,8 +8,8 @@ export async function installAgentIntegration(targets, ownership = null) {
|
|
|
8
8
|
const updated = [];
|
|
9
9
|
const unchanged = [];
|
|
10
10
|
const preserved = [];
|
|
11
|
-
const content = agentIntegrationSkill();
|
|
12
11
|
for (const target of targets) {
|
|
12
|
+
const content = agentIntegrationSkill(target.agent);
|
|
13
13
|
if (!await isSafeManagedSkillTarget(target)) {
|
|
14
14
|
preserved.push(`${target.agent}:${target.skillPath}`);
|
|
15
15
|
continue;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { execFile } from "node:child_process";
|
|
2
|
-
import { access, chown, lstat, readFile } from "node:fs/promises";
|
|
2
|
+
import { access, chown, lstat, readFile, readdir } from "node:fs/promises";
|
|
3
3
|
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
|
|
@@ -53,6 +53,19 @@ export async function applyOwnership(path, ownership) {
|
|
|
53
53
|
}
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
+
export async function applyOwnershipTree(path, ownership) {
|
|
57
|
+
if (ownership === null) return;
|
|
58
|
+
const stats = await pathStats(path);
|
|
59
|
+
if (stats === null || stats.isSymbolicLink()) return;
|
|
60
|
+
await applyOwnership(path, ownership);
|
|
61
|
+
if (!stats.isDirectory()) return;
|
|
62
|
+
const entries = await readdir(path, { withFileTypes: true });
|
|
63
|
+
for (const entry of entries) {
|
|
64
|
+
if (entry.isSymbolicLink()) continue;
|
|
65
|
+
await applyOwnershipTree(resolve(path, entry.name), ownership);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
56
69
|
function canApplyProcessOwnership(uid, gid) {
|
|
57
70
|
if (typeof process.getuid !== "function") {
|
|
58
71
|
return false;
|