agent-skillboard 0.1.1 → 0.2.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 +20 -0
- package/README.md +154 -630
- package/docs/adapters.md +96 -96
- package/docs/ai-skill-routing-goal.md +112 -0
- package/docs/capabilities.md +6 -0
- package/docs/install.md +155 -105
- package/docs/plans/skillboard-variant-lifecycle-handoff.md +56 -0
- package/docs/policy-model.md +266 -214
- package/docs/positioning.md +94 -94
- package/docs/reference.md +349 -0
- package/docs/routing.md +85 -0
- package/docs/user-flow.md +75 -16
- package/docs/value-proof.md +190 -0
- package/docs/variant-lifecycle.md +86 -0
- package/docs/versioning.md +157 -138
- package/examples/multi-source-skills/anthropic/docx/SKILL.md +8 -8
- package/examples/multi-source-skills/anthropic/skill-creator/SKILL.md +8 -8
- package/examples/multi-source-skills/matt/grill-me/SKILL.md +8 -8
- package/examples/multi-source-skills/matt/tdd/SKILL.md +8 -8
- package/examples/multi-source-skills/omo/review-work/SKILL.md +8 -8
- package/examples/multi-source-skills/omo/ulw-plan/SKILL.md +8 -8
- package/examples/multi-source-skills/private/tdd-work-continuity/SKILL.md +8 -8
- package/examples/multi-source-skills/private/workflow-router/SKILL.md +8 -8
- package/examples/multi-source-skills/wshobson/python-testing/SKILL.md +8 -8
- package/examples/multi-source-skills/wshobson/security-review/SKILL.md +8 -8
- package/examples/skills/grill-me/SKILL.md +9 -9
- package/examples/skills/grill-with-docs/SKILL.md +9 -9
- package/examples/skills/requirement-intake/SKILL.md +9 -9
- package/examples/skills/tdd/SKILL.md +8 -8
- package/package.json +25 -20
- package/src/advisor/guidance.mjs +232 -0
- package/src/advisor/schema.mjs +2 -0
- package/src/advisor/skills.mjs +2 -0
- package/src/advisor.mjs +36 -7
- package/src/brief-cli.mjs +6 -5
- package/src/brief-renderer.mjs +225 -8
- package/src/cli.mjs +589 -34
- package/src/config-helpers.mjs +34 -18
- package/src/conflicts.mjs +70 -0
- package/src/control/can-use-guard.mjs +8 -3
- package/src/control/skill-crud.mjs +142 -0
- package/src/control/skill-variants.mjs +221 -0
- package/src/control/source-trust.mjs +1 -0
- package/src/control/variant-files.mjs +265 -0
- package/src/control/variant-lifecycle-config.mjs +156 -0
- package/src/control/variant-reset.mjs +171 -0
- package/src/control/variant-status.mjs +75 -0
- package/src/control.mjs +13 -1
- package/src/domain/rules/skills.mjs +60 -0
- package/src/domain/rules/workflows.mjs +13 -0
- package/src/impact.mjs +21 -12
- package/src/index.mjs +13 -1
- package/src/lifecycle-cli.mjs +18 -7
- package/src/lifecycle-content.mjs +29 -22
- package/src/route.mjs +537 -0
- package/src/source-verification.mjs +7 -3
- package/src/workspace.mjs +141 -43
- package/tsconfig.lsp.json +1 -1
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { command } from "./action-core.mjs";
|
|
2
|
+
|
|
3
|
+
const GUARD_WHEN = "before invoking a skill";
|
|
4
|
+
const GOAL_DOCUMENT = Object.freeze({
|
|
5
|
+
path: "docs/ai-skill-routing-goal.md",
|
|
6
|
+
purpose: "Preserve SkillBoard as a non-blocking AI skill routing control plane: route and work first when safe, explain briefly, ask after use when policy learning helps, and remember usage policy without rewriting skill bodies.",
|
|
7
|
+
when_to_read: Object.freeze([
|
|
8
|
+
"before changing routing",
|
|
9
|
+
"before changing brief output",
|
|
10
|
+
"before changing bridge instructions",
|
|
11
|
+
"before changing policy UX",
|
|
12
|
+
"before changing workflow UX"
|
|
13
|
+
])
|
|
14
|
+
});
|
|
15
|
+
const GUARD_ALLOWED_USE = Object.freeze({
|
|
16
|
+
confirmation_required: false,
|
|
17
|
+
start: "State at the start which selected skill is being used for this request.",
|
|
18
|
+
finish: "State at completion which selected skill was used.",
|
|
19
|
+
start_message_template: "I will use <skill-id> for this request.",
|
|
20
|
+
finish_message_template: "I used <skill-id> for this request.",
|
|
21
|
+
ask_user_when: "Ask the user only if the guard denies use or a policy-changing action is needed."
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
export function buildAssistantGuidance(brief, options = {}) {
|
|
25
|
+
const status = guidanceStatus(brief);
|
|
26
|
+
const choices = status === "invalid-config" || hasPolicyErrors(brief) ? [] : choicesFromActions(brief.actions ?? []);
|
|
27
|
+
const route = options.route === undefined ? null : routeGuidance(options.route);
|
|
28
|
+
const guidance = {
|
|
29
|
+
status,
|
|
30
|
+
summary: summaryForStatus(status, brief),
|
|
31
|
+
goal_document: goalDocument(),
|
|
32
|
+
recommended_next_step: recommendedNextStep(status, brief, choices, route),
|
|
33
|
+
choices,
|
|
34
|
+
guard: {
|
|
35
|
+
required: true,
|
|
36
|
+
when: GUARD_WHEN,
|
|
37
|
+
command_hint: guardCommandHint(brief),
|
|
38
|
+
allowed_use: GUARD_ALLOWED_USE
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
if (route !== null) {
|
|
42
|
+
guidance.route = route;
|
|
43
|
+
}
|
|
44
|
+
return guidance;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function goalDocument() {
|
|
48
|
+
return {
|
|
49
|
+
...GOAL_DOCUMENT,
|
|
50
|
+
when_to_read: [...GOAL_DOCUMENT.when_to_read]
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function guidanceStatus(brief) {
|
|
55
|
+
if (hasInvalidConfig(brief)) {
|
|
56
|
+
return "invalid-config";
|
|
57
|
+
}
|
|
58
|
+
if (brief.error?.code === "not-initialized") {
|
|
59
|
+
return "not-initialized";
|
|
60
|
+
}
|
|
61
|
+
if (brief.error?.code === "unknown-workflow" || brief.workflow?.unknown === true) {
|
|
62
|
+
return "unknown-workflow";
|
|
63
|
+
}
|
|
64
|
+
if (brief.workflow?.needs_selection === true || brief.workflow?.selected === null) {
|
|
65
|
+
return "workflow-selection-needed";
|
|
66
|
+
}
|
|
67
|
+
if (hasPolicyErrors(brief)) {
|
|
68
|
+
return "blocked";
|
|
69
|
+
}
|
|
70
|
+
if ((brief.review_queue ?? []).length > 0 || brief.health?.review_required === true) {
|
|
71
|
+
return "needs-decision";
|
|
72
|
+
}
|
|
73
|
+
if (brief.health?.policy?.ok === false || brief.ok === false) {
|
|
74
|
+
return "blocked";
|
|
75
|
+
}
|
|
76
|
+
return "ready";
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function hasInvalidConfig(brief) {
|
|
80
|
+
return brief.error?.code === "invalid-config"
|
|
81
|
+
|| (brief.health?.config?.exists === true && brief.health.config.valid === false);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function summaryForStatus(status, brief) {
|
|
85
|
+
const readyCount = (brief.skills?.automatic_allowed?.length ?? 0) + (brief.skills?.manual_allowed?.length ?? 0);
|
|
86
|
+
const decisionCount = guidanceDecisionCount(brief);
|
|
87
|
+
const blockedCount = brief.skills?.blocked?.length ?? 0;
|
|
88
|
+
switch (status) {
|
|
89
|
+
case "ready":
|
|
90
|
+
return `SkillBoard is ready; ${readyCount} skills are available in this workflow.`;
|
|
91
|
+
case "needs-decision":
|
|
92
|
+
return `SkillBoard needs ${decisionCount} user ${decisionWord(decisionCount)} before this workflow is fully ready.`;
|
|
93
|
+
case "blocked":
|
|
94
|
+
return `SkillBoard found blocking policy issues; ${blockedCount} skills are blocked for safety.`;
|
|
95
|
+
case "not-initialized":
|
|
96
|
+
return "SkillBoard is not initialized in this project.";
|
|
97
|
+
case "invalid-config":
|
|
98
|
+
return "SkillBoard cannot read the project configuration.";
|
|
99
|
+
case "workflow-selection-needed":
|
|
100
|
+
return "SkillBoard needs a workflow selection before applying action cards.";
|
|
101
|
+
case "unknown-workflow":
|
|
102
|
+
return `SkillBoard does not know workflow ${brief.workflow?.selected ?? "the requested workflow"}.`;
|
|
103
|
+
default:
|
|
104
|
+
return "SkillBoard could not determine the current guidance state.";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function guidanceDecisionCount(brief) {
|
|
109
|
+
const skillDecisionCount = brief.skills?.needs_review?.length ?? 0;
|
|
110
|
+
if (skillDecisionCount > 0) {
|
|
111
|
+
return skillDecisionCount;
|
|
112
|
+
}
|
|
113
|
+
const reviewCount = brief.review_queue?.length ?? 0;
|
|
114
|
+
return reviewCount === 0 ? (brief.actions?.length ?? 0) : reviewCount;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
function decisionWord(count) {
|
|
118
|
+
return count === 1 ? "decision" : "decisions";
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function recommendedNextStep(status, brief, choices, route = null) {
|
|
122
|
+
const firstChoice = choices[0];
|
|
123
|
+
switch (status) {
|
|
124
|
+
case "ready":
|
|
125
|
+
if (route !== null) {
|
|
126
|
+
return route.recommended_skill === null
|
|
127
|
+
? "Ask a clarifying question; no workflow capability matched this request."
|
|
128
|
+
: `Use ${route.recommended_skill} for this request after the guard check passes.`;
|
|
129
|
+
}
|
|
130
|
+
return "Run the guard check before invoking any selected skill.";
|
|
131
|
+
case "needs-decision":
|
|
132
|
+
if (route?.recommended_skill !== null && route?.guard_allowed === true) {
|
|
133
|
+
return `Use ${route.recommended_skill} for this request after the guard check passes; handle pending review decisions after the task unless a policy-changing action is needed now.`;
|
|
134
|
+
}
|
|
135
|
+
return firstChoice === undefined
|
|
136
|
+
? "Ask the user which pending review decision to make."
|
|
137
|
+
: `Ask the user whether to approve: ${firstChoice.label}.`;
|
|
138
|
+
case "blocked":
|
|
139
|
+
if (hasPolicyErrors(brief)) {
|
|
140
|
+
return "Fix the SkillBoard policy errors before applying actions or invoking skills.";
|
|
141
|
+
}
|
|
142
|
+
return firstChoice === undefined
|
|
143
|
+
? "Resolve the blocking policy issue before invoking skills."
|
|
144
|
+
: `Review the blocked item before applying: ${firstChoice.label}.`;
|
|
145
|
+
case "not-initialized":
|
|
146
|
+
return firstChoice === undefined
|
|
147
|
+
? "Initialize SkillBoard before checking skill availability."
|
|
148
|
+
: `Ask the user whether to approve: ${firstChoice.label}.`;
|
|
149
|
+
case "invalid-config":
|
|
150
|
+
return "Fix the SkillBoard configuration before checking skill availability.";
|
|
151
|
+
case "workflow-selection-needed":
|
|
152
|
+
return (brief.workflow?.candidates?.length ?? 0) === 0
|
|
153
|
+
? "Set up SkillBoard by refreshing inventory, then add a harness and workflow before applying action cards."
|
|
154
|
+
: "Ask the user which workflow to use.";
|
|
155
|
+
case "unknown-workflow":
|
|
156
|
+
return "Ask the user to choose one of the configured workflows.";
|
|
157
|
+
default:
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function routeGuidance(route) {
|
|
163
|
+
return {
|
|
164
|
+
intent: route.intent,
|
|
165
|
+
workflow: route.workflow,
|
|
166
|
+
matched_capability: route.matched_capability,
|
|
167
|
+
matched_skill: route.matched_skill ?? null,
|
|
168
|
+
match_source: route.match_source,
|
|
169
|
+
confidence: route.confidence,
|
|
170
|
+
matched_terms: route.matched_terms,
|
|
171
|
+
recommendation_reason: route.recommendation_reason,
|
|
172
|
+
recommended_skill: route.recommended_skill,
|
|
173
|
+
fallback_skills: route.fallback_skills,
|
|
174
|
+
route_candidates: (route.route_candidates ?? []).map((candidate) => ({
|
|
175
|
+
skill: candidate.skill,
|
|
176
|
+
role: candidate.role,
|
|
177
|
+
selected: candidate.selected,
|
|
178
|
+
guard_allowed: candidate.guard_allowed,
|
|
179
|
+
guard_reasons: candidate.guard_reasons,
|
|
180
|
+
guard_roles: candidate.guard_roles,
|
|
181
|
+
capability_roles: candidate.capability_roles
|
|
182
|
+
})),
|
|
183
|
+
usage_disclosure: route.usage_disclosure ?? null,
|
|
184
|
+
post_use_policy_suggestion: route.post_use_policy_suggestion ?? null,
|
|
185
|
+
guard_command: route.guard_command,
|
|
186
|
+
guard_allowed: route.guard?.allowed ?? null,
|
|
187
|
+
guard_reasons: route.guard?.reasons ?? [],
|
|
188
|
+
possible_skills: route.possible_skills.map((skill) => ({
|
|
189
|
+
id: skill.id,
|
|
190
|
+
category: skill.category,
|
|
191
|
+
allowed: skill.allowed
|
|
192
|
+
}))
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function hasPolicyErrors(brief) {
|
|
197
|
+
return (brief.health?.policy?.errors?.length ?? 0) > 0;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function choicesFromActions(actions) {
|
|
201
|
+
return actions.filter(confirmableAction).map((action) => ({
|
|
202
|
+
label: action.label,
|
|
203
|
+
action_id: action.id,
|
|
204
|
+
kind: action.kind,
|
|
205
|
+
applies_to: action.applies_to ?? null,
|
|
206
|
+
risk: action.risk,
|
|
207
|
+
requires_confirmation: action.requires_user_confirmation,
|
|
208
|
+
effect: action.reason,
|
|
209
|
+
blocked_reason: action.blocked_reason ?? action.application?.blocked_reason ?? null
|
|
210
|
+
}));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function confirmableAction(action) {
|
|
214
|
+
return action.blocked_reason === null
|
|
215
|
+
&& (action.application?.blocked_reason ?? null) === null
|
|
216
|
+
&& (action.application?.apply ?? null) !== null;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function guardCommandHint(brief) {
|
|
220
|
+
if (hasInvalidConfig(brief)) {
|
|
221
|
+
return null;
|
|
222
|
+
}
|
|
223
|
+
if (brief.workflow?.selected === null || brief.workflow?.unknown === true || brief.workflow?.needs_selection === true) {
|
|
224
|
+
return null;
|
|
225
|
+
}
|
|
226
|
+
return command([
|
|
227
|
+
"skillboard", "guard", "use", "<skill-id>",
|
|
228
|
+
"--workflow", brief.workflow.selected,
|
|
229
|
+
"--config", brief.health.config_path,
|
|
230
|
+
"--skills", brief.health.skills_root
|
|
231
|
+
]).display;
|
|
232
|
+
}
|
package/src/advisor/schema.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { isAbsolute, resolve } from "node:path";
|
|
2
2
|
import { uninstallProject } from "../uninstall.mjs";
|
|
3
|
+
import { buildAssistantGuidance } from "./guidance.mjs";
|
|
3
4
|
import { sortedStrings } from "./sort.mjs";
|
|
4
5
|
|
|
5
6
|
const SCHEMA_VERSION = 1;
|
|
@@ -29,6 +30,7 @@ export function buildBrief(data) {
|
|
|
29
30
|
if (data.actions !== undefined) {
|
|
30
31
|
brief.actions = data.actions;
|
|
31
32
|
}
|
|
33
|
+
brief.assistant_guidance = buildAssistantGuidance(brief, { route: data.route });
|
|
32
34
|
return brief;
|
|
33
35
|
}
|
|
34
36
|
|
package/src/advisor/skills.mjs
CHANGED
|
@@ -52,6 +52,7 @@ function declaredSkillEntry(summary, explanation, use, group) {
|
|
|
52
52
|
invocation: summary.invocation,
|
|
53
53
|
exposure: summary.exposure,
|
|
54
54
|
category: summary.category,
|
|
55
|
+
variant: summary.variant ?? null,
|
|
55
56
|
source_class: explanation.source.class,
|
|
56
57
|
owner_install_unit: explanation.source.ownerInstallUnit,
|
|
57
58
|
workflow_roles: use?.roles ?? summary.workflowRoles,
|
|
@@ -165,6 +166,7 @@ function isHardReason(reason) {
|
|
|
165
166
|
return normalized.includes("policy check failed")
|
|
166
167
|
|| normalized.includes("unknown skill")
|
|
167
168
|
|| normalized.includes("unknown workflow")
|
|
169
|
+
|| normalized.includes("conflicts with active skill")
|
|
168
170
|
|| normalized.includes("blocks skill")
|
|
169
171
|
|| normalized.includes("source trust policy")
|
|
170
172
|
|| (normalized.includes("install unit") && normalized.includes("disabled"))
|
package/src/advisor.mjs
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
listWorkflows
|
|
4
4
|
} from "./control.mjs";
|
|
5
5
|
import { doctorProject } from "./doctor.mjs";
|
|
6
|
+
import { routeSkill } from "./route.mjs";
|
|
6
7
|
import { buildActionCards, buildInitActions } from "./advisor/actions.mjs";
|
|
7
8
|
import {
|
|
8
9
|
buildBrief,
|
|
@@ -52,7 +53,7 @@ export async function buildSkillBrief(options = {}) {
|
|
|
52
53
|
return buildExpectedConfigError(configDoctor, paths, cleanup, {
|
|
53
54
|
code: "invalid-config",
|
|
54
55
|
message: configDoctor.config.error ?? "skillboard.config.yaml is invalid"
|
|
55
|
-
});
|
|
56
|
+
}, options);
|
|
56
57
|
}
|
|
57
58
|
|
|
58
59
|
let workspace;
|
|
@@ -62,7 +63,7 @@ export async function buildSkillBrief(options = {}) {
|
|
|
62
63
|
return buildExpectedConfigError(configDoctor, paths, cleanup, {
|
|
63
64
|
code: "invalid-config",
|
|
64
65
|
message: error instanceof Error ? error.message : String(error)
|
|
65
|
-
});
|
|
66
|
+
}, options);
|
|
66
67
|
}
|
|
67
68
|
|
|
68
69
|
const doctor = await doctorProject({
|
|
@@ -97,19 +98,46 @@ export async function buildSkillBrief(options = {}) {
|
|
|
97
98
|
|
|
98
99
|
const skills = skillsForWorkflow(workspace, workflow.selected, sourceAudit);
|
|
99
100
|
const actionData = actionsForBrief({ options, paths, workflow, skills, reviewQueue, cleanup, workspace });
|
|
101
|
+
const route = routeForBrief({ options, paths, workflow, workspace });
|
|
102
|
+
const availabilityOk = doctor.config.valid && doctor.policy.ok && doctor.sources.ok;
|
|
100
103
|
return buildBrief({
|
|
101
|
-
ok:
|
|
102
|
-
health:
|
|
104
|
+
ok: availabilityOk,
|
|
105
|
+
health: healthForBrief(doctor, paths, availabilityOk),
|
|
103
106
|
workflow,
|
|
104
107
|
skills,
|
|
105
108
|
sources: summarizeSources(sourceAudit),
|
|
106
109
|
reviewQueue: actionData.reviewQueue,
|
|
107
110
|
cleanup,
|
|
108
|
-
actions: actionData.actions
|
|
111
|
+
actions: actionData.actions,
|
|
112
|
+
route
|
|
109
113
|
});
|
|
110
114
|
}
|
|
111
115
|
|
|
112
|
-
function
|
|
116
|
+
function routeForBrief({ options, paths, workflow, workspace }) {
|
|
117
|
+
const intent = options.intent?.trim();
|
|
118
|
+
if (intent === undefined || intent.length === 0 || workflow.selected === null || workflow.unknown || workflow.needs_selection) {
|
|
119
|
+
return undefined;
|
|
120
|
+
}
|
|
121
|
+
return routeSkill(workspace, {
|
|
122
|
+
intent,
|
|
123
|
+
workflow: workflow.selected,
|
|
124
|
+
configPath: paths.configPath,
|
|
125
|
+
skillsRoot: paths.skillsRoot
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function healthForBrief(doctor, paths, availabilityOk) {
|
|
130
|
+
const health = healthFromDoctor(doctor, paths);
|
|
131
|
+
if (!availabilityOk || health.mode !== "failed") {
|
|
132
|
+
return health;
|
|
133
|
+
}
|
|
134
|
+
return {
|
|
135
|
+
...health,
|
|
136
|
+
mode: health.review_required ? "safe-mode" : "passed"
|
|
137
|
+
};
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function buildExpectedConfigError(doctor, paths, cleanup, error, options = {}) {
|
|
113
141
|
return buildBrief({
|
|
114
142
|
ok: false,
|
|
115
143
|
error,
|
|
@@ -118,7 +146,8 @@ function buildExpectedConfigError(doctor, paths, cleanup, error) {
|
|
|
118
146
|
skills: emptySkillGroups(),
|
|
119
147
|
sources: sourcesFromDoctor(doctor),
|
|
120
148
|
reviewQueue: [],
|
|
121
|
-
cleanup
|
|
149
|
+
cleanup,
|
|
150
|
+
actions: requestedActions(options) ? [] : undefined
|
|
122
151
|
});
|
|
123
152
|
}
|
|
124
153
|
|
package/src/brief-cli.mjs
CHANGED
|
@@ -6,11 +6,12 @@ export async function runBriefCommand(options, stdout, paths) {
|
|
|
6
6
|
const json = options.get("json") === "true";
|
|
7
7
|
const result = await buildSkillBrief({
|
|
8
8
|
root: briefRoot(options),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
9
|
+
configPath: paths.configPath,
|
|
10
|
+
skillsRoot: paths.skillsRoot,
|
|
11
|
+
workflow: options.get("workflow"),
|
|
12
|
+
intent: options.get("intent"),
|
|
13
|
+
includeActions: options.get("include-actions") === "true" || !json
|
|
14
|
+
});
|
|
14
15
|
writeBriefOutput(stdout, result, options);
|
|
15
16
|
return briefExitCode(result);
|
|
16
17
|
}
|
package/src/brief-renderer.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
+
// SIZE_OK: src/brief-renderer.mjs is pre-existing renderer debt; this change only adds narrow AI/automation copy until a broader renderer split.
|
|
1
2
|
const SKILL_SECTIONS = [
|
|
2
|
-
["What your AI can use now", "automatic_allowed"],
|
|
3
|
-
["Manual only", "manual_allowed"],
|
|
4
3
|
["Needs your decision", "needs_review"],
|
|
5
4
|
["Blocked for safety", "blocked"]
|
|
6
5
|
];
|
|
@@ -20,6 +19,7 @@ const CLEANUP_ACTION_KINDS = new Set([
|
|
|
20
19
|
]);
|
|
21
20
|
const COMPACT_SKILL_LIMIT = 5;
|
|
22
21
|
const TOP_CATEGORY_LIMIT = 5;
|
|
22
|
+
const POLICY_DIAGNOSTIC_LIMIT = 3;
|
|
23
23
|
const MAX_ACTIONS_PER_TEXT_SECTION = 5;
|
|
24
24
|
const ACTION_KIND_RANK = new Map([
|
|
25
25
|
["setup-guidance", 0],
|
|
@@ -48,9 +48,16 @@ export function renderSkillBrief(brief, options = {}) {
|
|
|
48
48
|
if (brief.error !== undefined) {
|
|
49
49
|
lines.push(`Error: ${safeText(brief.error.message)}`, "");
|
|
50
50
|
}
|
|
51
|
+
emitIntentRoute(lines, brief);
|
|
52
|
+
emitPolicyHealth(lines, brief);
|
|
51
53
|
emitCategorySummary(lines, brief);
|
|
52
54
|
emitNextAction(lines, brief, { verbose });
|
|
55
|
+
emitAvailableNowSection(lines, brief, { verbose });
|
|
53
56
|
for (const [title, key] of SKILL_SECTIONS) {
|
|
57
|
+
if (key === "needs_review") {
|
|
58
|
+
emitDecisionSection(lines, brief, { verbose });
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
54
61
|
emitSkillSection(lines, title, brief.skills[key], {
|
|
55
62
|
brief,
|
|
56
63
|
groupLabel: compactGroupLabel(key),
|
|
@@ -69,6 +76,109 @@ export function renderSkillBrief(brief, options = {}) {
|
|
|
69
76
|
return `${lines.join("\n")}\n`;
|
|
70
77
|
}
|
|
71
78
|
|
|
79
|
+
function emitIntentRoute(lines, brief) {
|
|
80
|
+
const route = brief.assistant_guidance?.route;
|
|
81
|
+
if (route === undefined) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
lines.push("## Suggested skill for this request", "");
|
|
85
|
+
lines.push(`- Intent: ${safeText(route.intent)}`);
|
|
86
|
+
lines.push(`- Match source: ${route.match_source}`);
|
|
87
|
+
lines.push(`- Matched capability: ${route.matched_capability ?? "none"}`);
|
|
88
|
+
lines.push(`- Matched skill: ${route.matched_skill === null ? "none" : code(route.matched_skill)}`);
|
|
89
|
+
lines.push(`- Confidence: ${route.confidence}`);
|
|
90
|
+
lines.push(`- Why: ${safeText(route.recommendation_reason, 320)}`);
|
|
91
|
+
lines.push(`- Matched terms: ${formatCodeList(route.matched_terms)}`);
|
|
92
|
+
if (route.recommended_skill === null) {
|
|
93
|
+
lines.push("- Recommended skill: none");
|
|
94
|
+
lines.push("- Next step: ask a clarifying question before choosing a skill.");
|
|
95
|
+
} else {
|
|
96
|
+
lines.push(`- Recommended skill: ${code(route.recommended_skill)}`);
|
|
97
|
+
lines.push(`- Fallback skills: ${formatCodeList(route.fallback_skills)}`);
|
|
98
|
+
if ((route.route_candidates ?? []).length > 0) {
|
|
99
|
+
lines.push("- Route candidates:");
|
|
100
|
+
for (const candidate of route.route_candidates) {
|
|
101
|
+
lines.push(` - ${code(candidate.skill)} (${routeCandidateStatus(candidate)})`);
|
|
102
|
+
if (!candidate.guard_allowed && candidate.guard_reasons.length > 0) {
|
|
103
|
+
lines.push(` - ${safeText(candidate.guard_reasons[0])}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
lines.push(`- Guard: ${code(route.guard_command, Number.POSITIVE_INFINITY)}`);
|
|
108
|
+
if (route.usage_disclosure !== null && route.usage_disclosure !== undefined) {
|
|
109
|
+
lines.push(`- Disclosure: ${routeDisclosureText(code(route.recommended_skill))}`);
|
|
110
|
+
lines.push(`- Say before use: "${safeText(route.usage_disclosure.start_message)}"`);
|
|
111
|
+
lines.push(`- Say after completion: "${safeText(route.usage_disclosure.finish_message)}"`);
|
|
112
|
+
}
|
|
113
|
+
emitPostUsePolicySuggestion(lines, route.post_use_policy_suggestion);
|
|
114
|
+
}
|
|
115
|
+
lines.push("");
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function routeCandidateStatus(candidate) {
|
|
119
|
+
return [
|
|
120
|
+
candidate.role,
|
|
121
|
+
candidate.selected ? "selected" : null,
|
|
122
|
+
candidate.guard_allowed ? "allowed" : "denied"
|
|
123
|
+
].filter((value) => value !== null).join(", ");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function routeDisclosureText(skillLabel) {
|
|
127
|
+
return `run the guard automatically, state at the start that ${skillLabel} is being used, and state at completion that it was used. No extra user approval is needed when the guard allows it.`;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function emitPostUsePolicySuggestion(lines, suggestion) {
|
|
131
|
+
if (suggestion === null || suggestion === undefined) {
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
lines.push(`- After completion: ${safeText(afterUsePromptText(suggestion.question))}`);
|
|
135
|
+
lines.push(`- Policy command after confirmation: ${code(suggestion.suggested_policy.command_hint, Number.POSITIVE_INFINITY)}`);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function afterUsePromptText(question) {
|
|
139
|
+
return question.replace(/^Should I /u, "ask whether to ").replace(/\?$/u, ".");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function emitPolicyHealth(lines, brief) {
|
|
143
|
+
const policy = brief.health?.policy;
|
|
144
|
+
if (policy === undefined) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const errors = policy.errors ?? [];
|
|
148
|
+
const warnings = policy.warnings ?? [];
|
|
149
|
+
if (errors.length === 0 && warnings.length === 0) {
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
lines.push("## Policy health", "");
|
|
153
|
+
emitPolicyDiagnostics(lines, "Policy errors", errors);
|
|
154
|
+
emitPolicyDiagnostics(lines, "Policy warnings", warnings);
|
|
155
|
+
lines.push(`- check with: ${code(policyCheckCommand(brief), Number.POSITIVE_INFINITY)}`);
|
|
156
|
+
lines.push("");
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function emitPolicyDiagnostics(lines, label, diagnostics) {
|
|
160
|
+
if (diagnostics.length === 0) {
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
lines.push(`- ${label}: ${diagnostics.length}`);
|
|
164
|
+
for (const diagnostic of diagnostics.slice(0, POLICY_DIAGNOSTIC_LIMIT)) {
|
|
165
|
+
lines.push(` - ${safeText(diagnostic)}`);
|
|
166
|
+
}
|
|
167
|
+
const hidden = diagnostics.length - POLICY_DIAGNOSTIC_LIMIT;
|
|
168
|
+
if (hidden > 0) {
|
|
169
|
+
lines.push(` - ${hidden} more hidden. Run ${code("skillboard check")}.`);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function policyCheckCommand(brief) {
|
|
174
|
+
const config = brief.health?.config_path;
|
|
175
|
+
const skills = brief.health?.skills_root;
|
|
176
|
+
if (config === undefined || skills === undefined) {
|
|
177
|
+
return "skillboard check";
|
|
178
|
+
}
|
|
179
|
+
return `skillboard check --config ${config} --skills ${skills}`;
|
|
180
|
+
}
|
|
181
|
+
|
|
72
182
|
function briefCounts(brief) {
|
|
73
183
|
const automatic = brief.skills.automatic_allowed.length;
|
|
74
184
|
const manual = brief.skills.manual_allowed.length;
|
|
@@ -76,32 +186,133 @@ function briefCounts(brief) {
|
|
|
76
186
|
automatic,
|
|
77
187
|
manual,
|
|
78
188
|
usable: automatic + manual,
|
|
79
|
-
needsDecision: brief
|
|
189
|
+
needsDecision: decisionCount(brief),
|
|
80
190
|
blocked: brief.skills.blocked.length
|
|
81
191
|
};
|
|
82
192
|
}
|
|
83
193
|
|
|
194
|
+
function decisionCount(brief) {
|
|
195
|
+
const skillDecisionCount = brief.skills.needs_review.length;
|
|
196
|
+
if (skillDecisionCount > 0) {
|
|
197
|
+
return skillDecisionCount;
|
|
198
|
+
}
|
|
199
|
+
return brief.review_queue?.length ?? 0;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function emitAvailableNowSection(lines, brief, options) {
|
|
203
|
+
lines.push("## What your AI can use now", "");
|
|
204
|
+
const entries = [
|
|
205
|
+
...brief.skills.automatic_allowed.map((entry) => ({ entry, mode: "automatic" })),
|
|
206
|
+
...brief.skills.manual_allowed.map((entry) => ({ entry, mode: "manual-only" }))
|
|
207
|
+
];
|
|
208
|
+
if (entries.length === 0) {
|
|
209
|
+
lines.push("- none", "");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
lines.push("Automatic skills can be selected by the AI. On-request skills can be used when the user asks the AI; the AI runs the guard first.");
|
|
213
|
+
lines.push("When the guard allows use, disclose the selected skill at the start and completion instead of asking again.", "");
|
|
214
|
+
const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
|
|
215
|
+
for (const item of visibleEntries) {
|
|
216
|
+
lines.push(formatSkillEntry(item.entry, [availableModeLabel(item.mode)]));
|
|
217
|
+
}
|
|
218
|
+
const hiddenEntries = entries.slice(visibleEntries.length);
|
|
219
|
+
if (hiddenEntries.length > 0) {
|
|
220
|
+
lines.push(`- ${hiddenEntries.length} more ${availableHiddenLabel(hiddenEntries)} hidden. Run ${code(verboseCommand(brief))} or ${code(listCommand(brief))}.`);
|
|
221
|
+
}
|
|
222
|
+
lines.push("");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
function availableHiddenLabel(entries) {
|
|
226
|
+
const modes = new Set(entries.map((entry) => entry.mode));
|
|
227
|
+
if (modes.size !== 1) {
|
|
228
|
+
return "available skills";
|
|
229
|
+
}
|
|
230
|
+
return modes.has("manual-only") ? "on-request skills" : "automatic skills";
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
function availableModeLabel(mode) {
|
|
234
|
+
return mode === "manual-only" ? "on request" : mode;
|
|
235
|
+
}
|
|
236
|
+
|
|
84
237
|
function emitSkillSection(lines, title, entries, options) {
|
|
85
238
|
lines.push(`## ${title}`, "");
|
|
86
239
|
if (entries.length === 0) {
|
|
87
240
|
lines.push("- none", "");
|
|
88
241
|
return;
|
|
89
242
|
}
|
|
243
|
+
emitSkillEntries(lines, entries, options);
|
|
244
|
+
lines.push("");
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function emitSkillEntries(lines, entries, options) {
|
|
90
248
|
const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
|
|
91
249
|
for (const entry of visibleEntries) {
|
|
92
|
-
|
|
93
|
-
const reason = entry.reason === null || entry.reason === undefined
|
|
94
|
-
? ""
|
|
95
|
-
: ` - ${safeText(entry.reason)}`;
|
|
96
|
-
lines.push(`- ${code(entry.id)}${path}${reason}`);
|
|
250
|
+
lines.push(formatSkillEntry(entry));
|
|
97
251
|
}
|
|
98
252
|
const hidden = entries.length - visibleEntries.length;
|
|
99
253
|
if (hidden > 0) {
|
|
100
254
|
lines.push(`- ${hidden} more ${options.groupLabel} hidden. Run ${code(verboseCommand(options.brief))} or ${code(listCommand(options.brief))}.`);
|
|
101
255
|
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
function formatSkillEntry(entry, labels = []) {
|
|
259
|
+
const path = entry.path === undefined ? "" : ` (${safeText(entry.path)})`;
|
|
260
|
+
const reason = entry.reason === null || entry.reason === undefined ? null : safeText(entry.reason);
|
|
261
|
+
const details = [...labels, reason].filter((value) => value !== null && value !== "");
|
|
262
|
+
const suffix = details.length === 0 ? "" : ` - ${details.join("; ")}`;
|
|
263
|
+
return `- ${code(entry.id)}${path}${suffix}`;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
function emitDecisionSection(lines, brief, options) {
|
|
267
|
+
lines.push("## Needs your decision", "");
|
|
268
|
+
const skillEntries = brief.skills.needs_review;
|
|
269
|
+
const reviewEntries = reviewEntriesForDecisionSection(brief, skillEntries);
|
|
270
|
+
if (skillEntries.length === 0 && reviewEntries.length === 0) {
|
|
271
|
+
lines.push("- none", "");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
if (skillEntries.length > 0) {
|
|
275
|
+
emitSkillEntries(lines, skillEntries, {
|
|
276
|
+
...options,
|
|
277
|
+
brief,
|
|
278
|
+
groupLabel: "decision items"
|
|
279
|
+
});
|
|
280
|
+
}
|
|
281
|
+
if (reviewEntries.length > 0) {
|
|
282
|
+
emitReviewQueueEntries(lines, reviewEntries, {
|
|
283
|
+
...options,
|
|
284
|
+
brief
|
|
285
|
+
});
|
|
286
|
+
}
|
|
102
287
|
lines.push("");
|
|
103
288
|
}
|
|
104
289
|
|
|
290
|
+
function reviewEntriesForDecisionSection(brief, skillEntries) {
|
|
291
|
+
if (skillEntries.length === 0) {
|
|
292
|
+
return brief.review_queue ?? [];
|
|
293
|
+
}
|
|
294
|
+
return [];
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
function emitReviewQueueEntries(lines, entries, options) {
|
|
298
|
+
const visibleEntries = options.verbose ? entries : entries.slice(0, COMPACT_SKILL_LIMIT);
|
|
299
|
+
for (const entry of visibleEntries) {
|
|
300
|
+
const label = entry.label ?? entry.title ?? entry.id;
|
|
301
|
+
const action = firstActionId(entry);
|
|
302
|
+
const actionText = action === null ? "" : ` - action: ${code(action)}`;
|
|
303
|
+
lines.push(`- ${safeText(label)} - ${safeText(entry.reason)}${actionText}`);
|
|
304
|
+
}
|
|
305
|
+
const hidden = entries.length - visibleEntries.length;
|
|
306
|
+
if (hidden > 0) {
|
|
307
|
+
lines.push(`- ${hidden} more review decisions hidden. Run ${code(verboseCommand(options.brief))}.`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function firstActionId(entry) {
|
|
312
|
+
const [action] = entry.action_ids ?? [];
|
|
313
|
+
return typeof action === "string" ? action : null;
|
|
314
|
+
}
|
|
315
|
+
|
|
105
316
|
function emitCategorySummary(lines, brief) {
|
|
106
317
|
const categories = categoryCounts(brief);
|
|
107
318
|
lines.push("## Top categories", "");
|
|
@@ -173,6 +384,7 @@ function workflowOption(brief) {
|
|
|
173
384
|
function emitActions(lines, brief, options) {
|
|
174
385
|
const actions = actionsForTextBrief(brief);
|
|
175
386
|
lines.push("## Suggested next actions", "");
|
|
387
|
+
lines.push("AI/automation operations should use current action ids from this brief, then ask for user confirmation before applying one action.", "");
|
|
176
388
|
if (actions.length === 0) {
|
|
177
389
|
lines.push("- none", "");
|
|
178
390
|
return;
|
|
@@ -209,6 +421,7 @@ function emitActions(lines, brief, options) {
|
|
|
209
421
|
|
|
210
422
|
function emitNextAction(lines, brief, options) {
|
|
211
423
|
lines.push("## Next safe action", "");
|
|
424
|
+
lines.push("AI/automation should present this as the next confirmable operation, not as an automatic mutation.", "");
|
|
212
425
|
const action = nextSafeAction(brief);
|
|
213
426
|
if (action === null) {
|
|
214
427
|
lines.push("- none", "");
|
|
@@ -360,3 +573,7 @@ function safeText(value, maxLength = 180) {
|
|
|
360
573
|
function code(value, maxLength = 180) {
|
|
361
574
|
return `\`${safeText(value, maxLength)}\``;
|
|
362
575
|
}
|
|
576
|
+
|
|
577
|
+
function formatCodeList(values) {
|
|
578
|
+
return values.length === 0 ? "none" : values.map((value) => code(value)).join(", ");
|
|
579
|
+
}
|