agent-skillboard 0.2.17 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +62 -0
  2. package/README.md +128 -260
  3. package/bin/postinstall.mjs +2 -2
  4. package/docs/adapters.md +37 -113
  5. package/docs/ai-skill-routing-goal.md +35 -109
  6. package/docs/capabilities.md +17 -104
  7. package/docs/install.md +39 -493
  8. package/docs/policy-model.md +50 -280
  9. package/docs/positioning.md +19 -86
  10. package/docs/profiles.md +21 -153
  11. package/docs/reference.md +117 -356
  12. package/docs/rollout-runbook.md +23 -25
  13. package/docs/routing.md +23 -90
  14. package/docs/user-flow.md +60 -292
  15. package/docs/value-proof.md +23 -181
  16. package/docs/variant-lifecycle.md +47 -67
  17. package/docs/versioning.md +31 -264
  18. package/examples/v2-multi-source.config.yaml +35 -0
  19. package/examples/v2-policy-error.config.yaml +6 -0
  20. package/package.json +1 -1
  21. package/src/advisor/actions.mjs +102 -6
  22. package/src/advisor/application-commands.mjs +10 -9
  23. package/src/advisor/apply-action.mjs +74 -1
  24. package/src/advisor/guidance.mjs +24 -16
  25. package/src/advisor/schema.mjs +17 -6
  26. package/src/advisor/skills.mjs +18 -5
  27. package/src/advisor.mjs +27 -9
  28. package/src/agent-integration-cli.mjs +13 -4
  29. package/src/agent-integration-content.mjs +22 -12
  30. package/src/agent-integration-files.mjs +1 -1
  31. package/src/agent-inventory-platforms.mjs +10 -0
  32. package/src/agent-inventory.mjs +23 -1
  33. package/src/agent-skill-import.mjs +2 -2
  34. package/src/audit-paths.mjs +42 -0
  35. package/src/brief-cli.mjs +3 -2
  36. package/src/brief-renderer.mjs +1 -0
  37. package/src/cli.mjs +398 -127
  38. package/src/compatibility.mjs +24 -0
  39. package/src/control/can-use-guard.mjs +21 -1
  40. package/src/control/config-write.mjs +32 -2
  41. package/src/control/skill-crud.mjs +5 -0
  42. package/src/control/v2-guard.mjs +175 -0
  43. package/src/control/v2-skill-crud.mjs +32 -0
  44. package/src/control/v2-skill-forget.mjs +38 -0
  45. package/src/control/variant-status.mjs +47 -1
  46. package/src/control.mjs +55 -0
  47. package/src/doctor.mjs +65 -6
  48. package/src/domain/v2-policy.mjs +111 -0
  49. package/src/hook-plan.mjs +33 -3
  50. package/src/impact.mjs +52 -29
  51. package/src/index.mjs +25 -1
  52. package/src/init.mjs +50 -34
  53. package/src/inventory-install-units.mjs +63 -0
  54. package/src/inventory-json.mjs +279 -0
  55. package/src/inventory-refresh.mjs +163 -18
  56. package/src/lifecycle-cli.mjs +40 -12
  57. package/src/lifecycle-content.mjs +52 -67
  58. package/src/migration/v1-to-v2.mjs +212 -0
  59. package/src/migration/v2-files.mjs +211 -0
  60. package/src/migration/v2-journal.mjs +169 -0
  61. package/src/migration/v2-projection.mjs +108 -0
  62. package/src/migration/v2-transaction.mjs +205 -0
  63. package/src/policy.mjs +3 -0
  64. package/src/reconcile.mjs +139 -111
  65. package/src/report.mjs +168 -148
  66. package/src/review.mjs +2 -0
  67. package/src/route-advisory.mjs +47 -2
  68. package/src/route-selection.mjs +38 -2
  69. package/src/route.mjs +62 -2
  70. package/src/shared-skill.mjs +301 -0
  71. package/src/source-digest.mjs +42 -0
  72. package/src/source-profiles.mjs +27 -0
  73. package/src/source-verification.mjs +32 -48
  74. package/src/uninstall.mjs +22 -0
  75. package/src/user-state-paths.mjs +19 -0
  76. package/src/user-uninstall.mjs +146 -0
  77. package/src/workspace.mjs +41 -1
package/src/reconcile.mjs CHANGED
@@ -1,111 +1,139 @@
1
- const SAFE_HARNESS_STATUSES = new Set(["available", "configured", "primary", "fallback"]);
2
-
3
- export function reconcileWorkspace(workspace, options = {}) {
4
- const configuredSkills = new Set(workspace.skills.map((skill) => skill.id));
5
- const actualHarnesses = options.actualHarnesses ?? [];
6
- const plan = {
7
- skillChanges: [],
8
- harnessChanges: [],
9
- autoActions: [],
10
- decisionsRequired: [],
11
- warnings: []
12
- };
13
-
14
- for (const skill of workspace.installedSkills) {
15
- if (configuredSkills.has(skill.id)) {
16
- continue;
17
- }
18
- const capability = findCapabilityForSkill(workspace, skill.id);
19
- const recommendedInvocation = recommendedInvocationFor(capability);
20
- const change = {
21
- type: "new-skill",
22
- skillId: skill.id,
23
- capability: capability?.name ?? "uncategorized",
24
- recommendedStatus: "quarantined",
25
- recommendedInvocation
26
- };
27
- plan.skillChanges.push(change);
28
- plan.autoActions.push({
29
- action: "quarantine-skill",
30
- skillId: skill.id,
31
- capability: capability?.name ?? "uncategorized",
32
- recommendedStatus: "quarantined",
33
- recommendedInvocation
34
- });
35
- plan.decisionsRequired.push(
36
- `Classify ${skill.id}: keep quarantined, approve manual-only for a workflow, or archive as duplicate.`
37
- );
38
- }
39
-
40
- if (actualHarnesses.length === 0) {
41
- plan.warnings.push("Actual harness inventory was not provided; harness reconciliation skipped.");
42
- } else {
43
- reconcileHarnesses(workspace, new Set(actualHarnesses), plan);
44
- }
45
-
46
- return plan;
47
- }
48
-
49
- function reconcileHarnesses(workspace, actualHarnesses, plan) {
50
- const desiredHarnesses = new Set(workspace.harnesses.map((harness) => harness.name));
51
-
52
- for (const harness of workspace.harnesses) {
53
- if (!SAFE_HARNESS_STATUSES.has(harness.status) || actualHarnesses.has(harness.name)) {
54
- continue;
55
- }
56
- const affectedWorkflows = harness.workflows.length === 0
57
- ? workflowsUsingHarness(workspace, harness.name)
58
- : harness.workflows;
59
- const change = {
60
- type: "removed-harness",
61
- harness: harness.name,
62
- affectedWorkflows,
63
- missingCommands: harness.commands,
64
- recommendations: [
65
- "assign a fallback harness before applying workflow changes",
66
- "replace missing commands with capability-backed workflow steps"
67
- ]
68
- };
69
- plan.harnessChanges.push(change);
70
- plan.decisionsRequired.push(
71
- `Migrate ${harness.name}: ${affectedWorkflows.length} workflow(s) need a fallback harness or command mapping.`
72
- );
73
- }
74
-
75
- for (const harness of actualHarnesses) {
76
- if (desiredHarnesses.has(harness)) {
77
- continue;
78
- }
79
- plan.harnessChanges.push({
80
- type: "new-harness",
81
- harness,
82
- affectedWorkflows: [],
83
- missingCommands: [],
84
- recommendations: ["record the harness as disabled until workflows explicitly opt in"]
85
- });
86
- plan.autoActions.push({
87
- action: "disable-harness",
88
- harness,
89
- recommendedStatus: "disabled"
90
- });
91
- }
92
- }
93
-
94
- function findCapabilityForSkill(workspace, skillId) {
95
- return workspace.capabilities.find((candidate) => {
96
- return candidate.canonical === skillId || candidate.alternatives.includes(skillId);
97
- });
98
- }
99
-
100
- function recommendedInvocationFor(capability) {
101
- if (capability === undefined || capability.defaultPolicy === "global-auto") {
102
- return "blocked";
103
- }
104
- return capability.defaultPolicy;
105
- }
106
-
107
- function workflowsUsingHarness(workspace, harnessName) {
108
- return workspace.workflows
109
- .filter((workflow) => workflow.harness === harnessName)
110
- .map((workflow) => workflow.name);
111
- }
1
+ const SAFE_HARNESS_STATUSES = new Set(["available", "configured", "primary", "fallback"]);
2
+
3
+ export function reconcileWorkspace(workspace, options = {}) {
4
+ if (workspace.version === 2) {
5
+ return reconcileV2Workspace(workspace);
6
+ }
7
+ const configuredSkills = new Set(workspace.skills.map((skill) => skill.id));
8
+ const actualHarnesses = options.actualHarnesses ?? [];
9
+ const plan = {
10
+ skillChanges: [],
11
+ harnessChanges: [],
12
+ autoActions: [],
13
+ decisionsRequired: [],
14
+ warnings: []
15
+ };
16
+
17
+ for (const skill of workspace.installedSkills) {
18
+ if (configuredSkills.has(skill.id)) {
19
+ continue;
20
+ }
21
+ const capability = findCapabilityForSkill(workspace, skill.id);
22
+ const recommendedInvocation = recommendedInvocationFor(capability);
23
+ const change = {
24
+ type: "new-skill",
25
+ skillId: skill.id,
26
+ capability: capability?.name ?? "uncategorized",
27
+ recommendedStatus: "quarantined",
28
+ recommendedInvocation
29
+ };
30
+ plan.skillChanges.push(change);
31
+ plan.autoActions.push({
32
+ action: "quarantine-skill",
33
+ skillId: skill.id,
34
+ capability: capability?.name ?? "uncategorized",
35
+ recommendedStatus: "quarantined",
36
+ recommendedInvocation
37
+ });
38
+ plan.decisionsRequired.push(
39
+ `Classify ${skill.id}: keep quarantined, approve manual-only for a workflow, or archive as duplicate.`
40
+ );
41
+ }
42
+
43
+ if (actualHarnesses.length === 0) {
44
+ plan.warnings.push("Actual harness inventory was not provided; harness reconciliation skipped.");
45
+ } else {
46
+ reconcileHarnesses(workspace, new Set(actualHarnesses), plan);
47
+ }
48
+
49
+ return plan;
50
+ }
51
+
52
+ function reconcileV2Workspace(workspace) {
53
+ const configuredSkills = new Set(workspace.skills.map((skill) => skill.id));
54
+ const observedSkills = workspace.inventory?.skills ?? workspace.installedSkills;
55
+ const skillChanges = observedSkills
56
+ .filter((skill) => !configuredSkills.has(skill.id))
57
+ .map((skill) => ({
58
+ type: "new-skill",
59
+ skillId: skill.id,
60
+ recommendedEnabled: true,
61
+ recommendedShared: false
62
+ }));
63
+ return {
64
+ skillChanges,
65
+ harnessChanges: [],
66
+ autoActions: skillChanges.map((change) => ({
67
+ action: "enable-skill-local",
68
+ skillId: change.skillId,
69
+ enabled: true,
70
+ shared: false
71
+ })),
72
+ decisionsRequired: [],
73
+ warnings: workspace.inventory?.integrityErrors ?? []
74
+ };
75
+ }
76
+
77
+ function reconcileHarnesses(workspace, actualHarnesses, plan) {
78
+ const desiredHarnesses = new Set(workspace.harnesses.map((harness) => harness.name));
79
+
80
+ for (const harness of workspace.harnesses) {
81
+ if (!SAFE_HARNESS_STATUSES.has(harness.status) || actualHarnesses.has(harness.name)) {
82
+ continue;
83
+ }
84
+ const affectedWorkflows = harness.workflows.length === 0
85
+ ? workflowsUsingHarness(workspace, harness.name)
86
+ : harness.workflows;
87
+ const change = {
88
+ type: "removed-harness",
89
+ harness: harness.name,
90
+ affectedWorkflows,
91
+ missingCommands: harness.commands,
92
+ recommendations: [
93
+ "assign a fallback harness before applying workflow changes",
94
+ "replace missing commands with capability-backed workflow steps"
95
+ ]
96
+ };
97
+ plan.harnessChanges.push(change);
98
+ plan.decisionsRequired.push(
99
+ `Migrate ${harness.name}: ${affectedWorkflows.length} workflow(s) need a fallback harness or command mapping.`
100
+ );
101
+ }
102
+
103
+ for (const harness of actualHarnesses) {
104
+ if (desiredHarnesses.has(harness)) {
105
+ continue;
106
+ }
107
+ plan.harnessChanges.push({
108
+ type: "new-harness",
109
+ harness,
110
+ affectedWorkflows: [],
111
+ missingCommands: [],
112
+ recommendations: ["record the harness as disabled until workflows explicitly opt in"]
113
+ });
114
+ plan.autoActions.push({
115
+ action: "disable-harness",
116
+ harness,
117
+ recommendedStatus: "disabled"
118
+ });
119
+ }
120
+ }
121
+
122
+ function findCapabilityForSkill(workspace, skillId) {
123
+ return workspace.capabilities.find((candidate) => {
124
+ return candidate.canonical === skillId || candidate.alternatives.includes(skillId);
125
+ });
126
+ }
127
+
128
+ function recommendedInvocationFor(capability) {
129
+ if (capability === undefined || capability.defaultPolicy === "global-auto") {
130
+ return "blocked";
131
+ }
132
+ return capability.defaultPolicy;
133
+ }
134
+
135
+ function workflowsUsingHarness(workspace, harnessName) {
136
+ return workspace.workflows
137
+ .filter((workflow) => workflow.harness === harnessName)
138
+ .map((workflow) => workflow.name);
139
+ }
package/src/report.mjs CHANGED
@@ -1,151 +1,171 @@
1
- export function renderDashboard(workspace) {
2
- const lines = ["# SkillBoard", "", "## Defaults", ""];
3
- lines.push(`- Invocation policy: \`${workspace.defaults.invocationPolicy}\``);
4
- lines.push(`- Model auto invocation: \`${workspace.defaults.allowModelInvocation}\``);
5
- lines.push(`- Explicit workflow required: \`${workspace.defaults.requireExplicitWorkflow}\``);
6
- lines.push("", "## Workflows", "");
7
-
8
- for (const workflow of workspace.workflows) {
9
- lines.push(`### ${workflow.name}`, "");
10
- lines.push(`- Harness: \`${workflow.harness}\``);
11
- lines.push(`- required outputs: ${formatList(workflow.requiredOutputs)}`);
12
- emitSkillGroup(lines, "active", workflow.activeSkills, workspace);
13
- emitSkillGroup(lines, "blocked", workflow.blockedSkills, workspace);
14
- emitCapabilityRequirements(lines, workflow.requiredCapabilities);
15
- lines.push("");
16
- }
17
-
18
- lines.push("## Capabilities", "");
19
- if (workspace.capabilities.length === 0) {
20
- lines.push("- none");
21
- }
22
- for (const capability of workspace.capabilities) {
23
- lines.push(`- \`${capability.name}\` canonical: \`${capability.canonical || "none"}\`, alternatives: ${formatList(capability.alternatives)}`);
24
- }
25
- lines.push("", "## Harnesses", "");
26
- if (workspace.harnesses.length === 0) {
27
- lines.push("- none");
28
- }
29
- for (const harness of workspace.harnesses) {
30
- lines.push(`- \`${harness.name}\` ${harness.status}, workflows: ${formatList(harness.workflows)}`);
31
- }
32
- lines.push("");
33
-
34
- lines.push("## Agent Runtime Install Units", "");
35
- if (workspace.installUnits.length === 0) {
36
- lines.push("- none");
37
- }
38
- for (const unit of workspace.installUnits) {
39
- lines.push(`### ${unit.id}`, "");
40
- lines.push(`- Kind: \`${unit.kind}\``);
41
- lines.push(`- Scope: \`${unit.scope}\``);
42
- lines.push(`- Source: \`${unit.source || "unknown"}\``);
43
- lines.push(`- Enabled: \`${unit.enabled}\``);
44
- lines.push(`- Auto update: \`${unit.autoUpdate}\``);
45
- lines.push(`- Manifest: \`${unit.manifestPath || "none"}\``);
46
- lines.push(`- Cache: \`${unit.cachePath || "none"}\``);
47
- lines.push(`- Provides: ${formatList(unit.providedComponents)}`);
48
- lines.push(`- Skills: ${formatList(unit.components.skills)}`);
49
- lines.push(`- Commands: ${formatList(unit.components.commands)}`);
50
- lines.push(`- Hooks: ${formatList(unit.components.hooks)}`);
51
- lines.push(`- MCP servers: ${formatList(unit.components.mcpServers)}`);
52
- lines.push(`- Modifies: ${formatList(unit.modifiedConfigFiles)}`);
1
+ export function renderDashboard(workspace) {
2
+ if (workspace.version === 2) return renderV2Dashboard(workspace);
3
+ const lines = ["# SkillBoard", "", "## Defaults", ""];
4
+ lines.push(`- Invocation policy: \`${workspace.defaults.invocationPolicy}\``);
5
+ lines.push(`- Model auto invocation: \`${workspace.defaults.allowModelInvocation}\``);
6
+ lines.push(`- Explicit workflow required: \`${workspace.defaults.requireExplicitWorkflow}\``);
7
+ lines.push("", "## Workflows", "");
8
+
9
+ for (const workflow of workspace.workflows) {
10
+ lines.push(`### ${workflow.name}`, "");
11
+ lines.push(`- Harness: \`${workflow.harness}\``);
12
+ lines.push(`- required outputs: ${formatList(workflow.requiredOutputs)}`);
13
+ emitSkillGroup(lines, "active", workflow.activeSkills, workspace);
14
+ emitSkillGroup(lines, "blocked", workflow.blockedSkills, workspace);
15
+ emitCapabilityRequirements(lines, workflow.requiredCapabilities);
16
+ lines.push("");
17
+ }
18
+
19
+ lines.push("## Capabilities", "");
20
+ if (workspace.capabilities.length === 0) {
21
+ lines.push("- none");
22
+ }
23
+ for (const capability of workspace.capabilities) {
24
+ lines.push(`- \`${capability.name}\` — canonical: \`${capability.canonical || "none"}\`, alternatives: ${formatList(capability.alternatives)}`);
25
+ }
26
+ lines.push("", "## Harnesses", "");
27
+ if (workspace.harnesses.length === 0) {
28
+ lines.push("- none");
29
+ }
30
+ for (const harness of workspace.harnesses) {
31
+ lines.push(`- \`${harness.name}\` — ${harness.status}, workflows: ${formatList(harness.workflows)}`);
32
+ }
33
+ lines.push("");
34
+
35
+ lines.push("## Agent Runtime Install Units", "");
36
+ if (workspace.installUnits.length === 0) {
37
+ lines.push("- none");
38
+ }
39
+ for (const unit of workspace.installUnits) {
40
+ lines.push(`### ${unit.id}`, "");
41
+ lines.push(`- Kind: \`${unit.kind}\``);
42
+ lines.push(`- Scope: \`${unit.scope}\``);
43
+ lines.push(`- Source: \`${unit.source || "unknown"}\``);
44
+ lines.push(`- Enabled: \`${unit.enabled}\``);
45
+ lines.push(`- Auto update: \`${unit.autoUpdate}\``);
46
+ lines.push(`- Manifest: \`${unit.manifestPath || "none"}\``);
47
+ lines.push(`- Cache: \`${unit.cachePath || "none"}\``);
48
+ lines.push(`- Provides: ${formatList(unit.providedComponents)}`);
49
+ lines.push(`- Skills: ${formatList(unit.components.skills)}`);
50
+ lines.push(`- Commands: ${formatList(unit.components.commands)}`);
51
+ lines.push(`- Hooks: ${formatList(unit.components.hooks)}`);
52
+ lines.push(`- MCP servers: ${formatList(unit.components.mcpServers)}`);
53
+ lines.push(`- Modifies: ${formatList(unit.modifiedConfigFiles)}`);
53
54
  lines.push(`- Workflow dependencies: ${formatList(unit.workflowDependencies)}`);
54
55
  lines.push(`- Trust level: \`${unit.trustLevel}\``);
55
56
  lines.push(`- permission risk: \`${unit.permissionRisk}\``);
56
- lines.push(`- Rollback: \`${unit.rollback}\``);
57
- lines.push("");
58
- }
59
-
60
- lines.push("## Skill Inventory", "");
61
- for (const skill of workspace.skills) {
62
- lines.push(`- \`${skill.id}\` — ${skill.status}, ${skill.invocation}, ${skill.exposure}, ${skill.category}, owner: \`${skill.ownerInstallUnit ?? "none"}\``);
63
- }
64
- lines.push("", "## Installed Skill Files", "");
65
- if (workspace.installedSkills.length === 0) {
66
- lines.push("- none");
67
- }
68
- for (const skill of workspace.installedSkills) {
69
- lines.push(`- \`${skill.path}\` — ${skill.name}: ${skill.description}`);
70
- }
71
- lines.push("");
72
- return `${lines.join("\n")}\n`;
73
- }
74
-
75
- export function renderReconcilePlan(plan) {
76
- const lines = ["# SkillBoard Reconcile Plan", "", "## Warnings", ""];
77
- if (plan.warnings.length === 0) {
78
- lines.push("- none");
79
- }
80
- for (const warning of plan.warnings) {
81
- lines.push(`- ${warning}`);
82
- }
83
- lines.push("", "## Automatic Actions", "");
84
- if (plan.autoActions.length === 0) {
85
- lines.push("- none");
86
- }
87
- for (const action of plan.autoActions) {
88
- if (action.action === "quarantine-skill") {
89
- lines.push(`- quarantine \`${action.skillId}\` as \`${action.recommendedStatus}\` / \`${action.recommendedInvocation}\` for capability \`${action.capability}\``);
90
- } else if (action.action === "disable-harness") {
91
- lines.push(`- disable new harness \`${action.harness}\` until a workflow opts in`);
92
- }
93
- }
94
-
95
- lines.push("", "## Skill Changes", "");
96
- if (plan.skillChanges.length === 0) {
97
- lines.push("- none");
98
- }
99
- for (const change of plan.skillChanges) {
100
- lines.push(`- \`${change.skillId}\`: ${change.type}, capability \`${change.capability}\`, recommend \`${change.recommendedStatus}\``);
101
- }
102
-
103
- lines.push("", "## Harness Changes", "");
104
- if (plan.harnessChanges.length === 0) {
105
- lines.push("- none");
106
- }
107
- for (const change of plan.harnessChanges) {
108
- lines.push(`- \`${change.harness}\`: ${change.type}`);
109
- lines.push(` - affected workflows: ${formatList(change.affectedWorkflows)}`);
110
- lines.push(` - missing commands: ${formatList(change.missingCommands)}`);
111
- lines.push(` - recommendations: ${formatList(change.recommendations)}`);
112
- }
113
-
114
- lines.push("", "## Decisions Required", "");
115
- if (plan.decisionsRequired.length === 0) {
116
- lines.push("- none");
117
- }
118
- for (const decision of plan.decisionsRequired) {
119
- lines.push(`- ${decision}`);
120
- }
121
- lines.push("");
122
- return `${lines.join("\n")}\n`;
123
- }
124
-
125
- function emitSkillGroup(lines, label, skillIds, workspace) {
126
- lines.push(`- ${label}:`);
127
- if (skillIds.length === 0) {
128
- lines.push(" - none");
129
- return;
130
- }
131
- for (const skillId of skillIds) {
132
- const skill = workspace.skills.find((candidate) => candidate.id === skillId);
133
- const invocation = skill === undefined ? "missing" : skill.invocation;
134
- lines.push(` - \`${skillId}\` (${invocation})`);
135
- }
136
- }
137
-
138
- function emitCapabilityRequirements(lines, capabilities) {
139
- lines.push("- required capabilities:");
140
- if (capabilities.length === 0) {
141
- lines.push(" - none");
142
- return;
143
- }
144
- for (const capability of capabilities) {
145
- lines.push(` - \`${capability.name}\` preferred \`${capability.preferred || "none"}\`, fallback: ${formatList(capability.fallback)}`);
146
- }
147
- }
148
-
149
- function formatList(values) {
150
- return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
151
- }
57
+ lines.push(`- Rollback: \`${unit.rollback}\``);
58
+ lines.push("");
59
+ }
60
+
61
+ lines.push("## Skill Inventory", "");
62
+ for (const skill of workspace.skills) {
63
+ lines.push(`- \`${skill.id}\` — ${skill.status}, ${skill.invocation}, ${skill.exposure}, ${skill.category}, owner: \`${skill.ownerInstallUnit ?? "none"}\``);
64
+ }
65
+ lines.push("", "## Installed Skill Files", "");
66
+ if (workspace.installedSkills.length === 0) {
67
+ lines.push("- none");
68
+ }
69
+ for (const skill of workspace.installedSkills) {
70
+ lines.push(`- \`${skill.path}\` — ${skill.name}: ${skill.description}`);
71
+ }
72
+ lines.push("");
73
+ return `${lines.join("\n")}\n`;
74
+ }
75
+
76
+ function renderV2Dashboard(workspace) {
77
+ const lines = ["# SkillBoard", "", "## Version 2 Policy", ""];
78
+ for (const skill of workspace.skills) {
79
+ const state = skill.enabled ? "enabled" : "disabled";
80
+ lines.push(`- \`${skill.id}\` — ${state}, ${skill.shared ? "shared across agents" : "agent-local"}`);
81
+ }
82
+ lines.push("", "## Runtime Readiness", "");
83
+ const errors = workspace.inventory?.integrityErrors ?? [];
84
+ lines.push(errors.length === 0 ? "- inventory ready" : `- inventory unavailable: ${errors.join("; ")}`);
85
+ lines.push("");
86
+ return `${lines.join("\n")}\n`;
87
+ }
88
+
89
+ export function renderReconcilePlan(plan) {
90
+ const lines = ["# SkillBoard Reconcile Plan", "", "## Warnings", ""];
91
+ if (plan.warnings.length === 0) {
92
+ lines.push("- none");
93
+ }
94
+ for (const warning of plan.warnings) {
95
+ lines.push(`- ${warning}`);
96
+ }
97
+ lines.push("", "## Automatic Actions", "");
98
+ if (plan.autoActions.length === 0) {
99
+ lines.push("- none");
100
+ }
101
+ for (const action of plan.autoActions) {
102
+ if (action.action === "enable-skill-local") {
103
+ lines.push(`- enable \`${action.skillId}\` where it is installed`);
104
+ } else if (action.action === "quarantine-skill") {
105
+ lines.push(`- quarantine \`${action.skillId}\` as \`${action.recommendedStatus}\` / \`${action.recommendedInvocation}\` for capability \`${action.capability}\``);
106
+ } else if (action.action === "disable-harness") {
107
+ lines.push(`- disable new harness \`${action.harness}\` until a workflow opts in`);
108
+ }
109
+ }
110
+
111
+ lines.push("", "## Skill Changes", "");
112
+ if (plan.skillChanges.length === 0) {
113
+ lines.push("- none");
114
+ }
115
+ for (const change of plan.skillChanges) {
116
+ if (change.recommendedEnabled !== undefined) {
117
+ lines.push(`- \`${change.skillId}\`: ${change.type}, recommend enabled \`${change.recommendedEnabled}\`, shared \`${change.recommendedShared}\``);
118
+ } else {
119
+ lines.push(`- \`${change.skillId}\`: ${change.type}, capability \`${change.capability}\`, recommend \`${change.recommendedStatus}\``);
120
+ }
121
+ }
122
+
123
+ lines.push("", "## Harness Changes", "");
124
+ if (plan.harnessChanges.length === 0) {
125
+ lines.push("- none");
126
+ }
127
+ for (const change of plan.harnessChanges) {
128
+ lines.push(`- \`${change.harness}\`: ${change.type}`);
129
+ lines.push(` - affected workflows: ${formatList(change.affectedWorkflows)}`);
130
+ lines.push(` - missing commands: ${formatList(change.missingCommands)}`);
131
+ lines.push(` - recommendations: ${formatList(change.recommendations)}`);
132
+ }
133
+
134
+ lines.push("", "## Decisions Required", "");
135
+ if (plan.decisionsRequired.length === 0) {
136
+ lines.push("- none");
137
+ }
138
+ for (const decision of plan.decisionsRequired) {
139
+ lines.push(`- ${decision}`);
140
+ }
141
+ lines.push("");
142
+ return `${lines.join("\n")}\n`;
143
+ }
144
+
145
+ function emitSkillGroup(lines, label, skillIds, workspace) {
146
+ lines.push(`- ${label}:`);
147
+ if (skillIds.length === 0) {
148
+ lines.push(" - none");
149
+ return;
150
+ }
151
+ for (const skillId of skillIds) {
152
+ const skill = workspace.skills.find((candidate) => candidate.id === skillId);
153
+ const invocation = skill === undefined ? "missing" : skill.invocation;
154
+ lines.push(` - \`${skillId}\` (${invocation})`);
155
+ }
156
+ }
157
+
158
+ function emitCapabilityRequirements(lines, capabilities) {
159
+ lines.push("- required capabilities:");
160
+ if (capabilities.length === 0) {
161
+ lines.push(" - none");
162
+ return;
163
+ }
164
+ for (const capability of capabilities) {
165
+ lines.push(` - \`${capability.name}\` preferred \`${capability.preferred || "none"}\`, fallback: ${formatList(capability.fallback)}`);
166
+ }
167
+ }
168
+
169
+ function formatList(values) {
170
+ return values.length === 0 ? "none" : values.map((value) => `\`${value}\``).join(", ");
171
+ }
package/src/review.mjs CHANGED
@@ -6,6 +6,7 @@ import { textChangePlan } from "./change-plan.mjs";
6
6
  import { TRUST_LEVEL_VALUES } from "./domain/constants.mjs";
7
7
  import { checkPolicy } from "./policy.mjs";
8
8
  import { loadWorkspace } from "./workspace.mjs";
9
+ import { assertV2MutationVersion } from "./compatibility.mjs";
9
10
 
10
11
  export async function reviewInstallUnit(options) {
11
12
  const trustLevel = options.trustLevel ?? "reviewed";
@@ -14,6 +15,7 @@ export async function reviewInstallUnit(options) {
14
15
  }
15
16
 
16
17
  const { document, originalText } = await loadConfig(options.configPath);
18
+ assertV2MutationVersion(document.get("version") ?? 1);
17
19
  const installUnits = requireMapAt(document, ["install_units"], "install_units");
18
20
  const unit = installUnits.get(options.unitId, true);
19
21
  if (unit === undefined) {