agent-skillboard 0.2.15 → 0.2.17
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 +35 -0
- package/README.md +103 -45
- package/bin/postinstall.mjs +5 -3
- package/docs/ai-skill-routing-goal.md +15 -3
- package/docs/install.md +98 -68
- package/docs/positioning.md +8 -6
- package/docs/reference.md +22 -10
- package/docs/routing.md +15 -0
- package/docs/user-flow.md +61 -32
- package/docs/value-proof.md +6 -3
- package/package.json +3 -2
- package/src/advisor/guidance.mjs +23 -3
- package/src/agent-integration-cli.mjs +184 -0
- package/src/agent-integration-content.mjs +48 -0
- package/src/agent-integration-files.mjs +202 -0
- package/src/agent-integration-home.mjs +185 -0
- package/src/agent-skill-roots.mjs +1 -0
- package/src/brief-renderer.mjs +36 -57
- package/src/cli.mjs +168 -169
- package/src/control/can-use-guard.mjs +12 -0
- package/src/doctor.mjs +4 -1
- package/src/lifecycle-cli.mjs +23 -303
- package/src/lifecycle-content.mjs +4 -2
- package/src/route-advisory.mjs +214 -0
- package/src/route-renderer.mjs +146 -0
- package/src/route-selection.mjs +220 -0
- package/src/route-tokens.mjs +68 -0
- package/src/route.mjs +48 -413
- package/src/source-profiles.mjs +151 -150
- package/src/uninstall.mjs +56 -9
- package/src/workspace.mjs +79 -79
|
@@ -0,0 +1,202 @@
|
|
|
1
|
+
import { lstat, mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { AGENT_INTEGRATION_END, AGENT_INTEGRATION_START, agentIntegrationSkill } from "./agent-integration-content.mjs";
|
|
4
|
+
import { applyOwnership, isInside } from "./agent-integration-home.mjs";
|
|
5
|
+
|
|
6
|
+
export async function installAgentIntegration(targets, ownership = null) {
|
|
7
|
+
const created = [];
|
|
8
|
+
const updated = [];
|
|
9
|
+
const unchanged = [];
|
|
10
|
+
const preserved = [];
|
|
11
|
+
const content = agentIntegrationSkill();
|
|
12
|
+
for (const target of targets) {
|
|
13
|
+
if (!await isSafeManagedSkillTarget(target)) {
|
|
14
|
+
preserved.push(`${target.agent}:${target.skillPath}`);
|
|
15
|
+
continue;
|
|
16
|
+
}
|
|
17
|
+
const existing = await readFile(target.skillPath, "utf8").catch((error) => {
|
|
18
|
+
if (error?.code === "ENOENT") {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
throw error;
|
|
22
|
+
});
|
|
23
|
+
if (existing === content) {
|
|
24
|
+
await applyOwnership(target.skillPath, ownership);
|
|
25
|
+
unchanged.push(`${target.agent}:${target.skillPath}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (existing !== null && !existing.includes(AGENT_INTEGRATION_START)) {
|
|
29
|
+
preserved.push(`${target.agent}:${target.skillPath}`);
|
|
30
|
+
continue;
|
|
31
|
+
}
|
|
32
|
+
await mkdir(dirname(target.skillPath), { recursive: true });
|
|
33
|
+
await writeFile(target.skillPath, content, "utf8");
|
|
34
|
+
await applyOwnership(target.skillPath, ownership);
|
|
35
|
+
(existing === null ? created : updated).push(`${target.agent}:${target.skillPath}`);
|
|
36
|
+
}
|
|
37
|
+
return { created, updated, unchanged, preserved };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function uninstallAgentIntegration(targets, dryRun) {
|
|
41
|
+
const removed = [];
|
|
42
|
+
const updated = [];
|
|
43
|
+
const preserved = [];
|
|
44
|
+
const absent = [];
|
|
45
|
+
for (const target of targets) {
|
|
46
|
+
const result = await removeAgentIntegration(target, dryRun);
|
|
47
|
+
const value = `${target.agent}:${target.skillPath}`;
|
|
48
|
+
if (result === "removed") {
|
|
49
|
+
removed.push(value);
|
|
50
|
+
} else if (result === "updated") {
|
|
51
|
+
updated.push(value);
|
|
52
|
+
} else if (result === "preserved") {
|
|
53
|
+
preserved.push(value);
|
|
54
|
+
} else {
|
|
55
|
+
absent.push(value);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return { removed, updated, preserved, absent };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
async function removeAgentIntegration(target, dryRun) {
|
|
62
|
+
if (!await isSafeManagedSkillTarget(target)) {
|
|
63
|
+
return "preserved";
|
|
64
|
+
}
|
|
65
|
+
const path = target.skillPath;
|
|
66
|
+
const stats = await pathStats(path);
|
|
67
|
+
if (stats === null) {
|
|
68
|
+
return "absent";
|
|
69
|
+
}
|
|
70
|
+
if (stats.isSymbolicLink() || !stats.isFile()) {
|
|
71
|
+
return "preserved";
|
|
72
|
+
}
|
|
73
|
+
const current = await readFile(path, "utf8");
|
|
74
|
+
const next = withoutAgentIntegrationBlock(current);
|
|
75
|
+
if (next === null) {
|
|
76
|
+
return "preserved";
|
|
77
|
+
}
|
|
78
|
+
if (shouldRemoveAgentIntegrationFile(next)) {
|
|
79
|
+
if (!dryRun) {
|
|
80
|
+
await rm(path);
|
|
81
|
+
await removeEmptyManagedSkillDir(dirname(path));
|
|
82
|
+
}
|
|
83
|
+
return "removed";
|
|
84
|
+
}
|
|
85
|
+
if (!dryRun) {
|
|
86
|
+
await writeFile(path, next, "utf8");
|
|
87
|
+
}
|
|
88
|
+
return "updated";
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function isSafeManagedSkillTarget(target) {
|
|
92
|
+
const root = resolve(target.root);
|
|
93
|
+
const skillPath = resolve(target.skillPath);
|
|
94
|
+
const skillDir = dirname(skillPath);
|
|
95
|
+
if (!isInside(skillDir, root) || !isInside(skillPath, root)) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
const boundary = managedBoundary(target, root);
|
|
99
|
+
if (await hasUnsafeManagedDirectoryComponent(boundary, skillDir)) {
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
const skillStats = await pathStats(skillPath);
|
|
103
|
+
return skillStats === null || !skillStats.isSymbolicLink();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function managedBoundary(target, root) {
|
|
107
|
+
const home = target.home === undefined ? null : resolve(target.home);
|
|
108
|
+
if (home === null) {
|
|
109
|
+
return root;
|
|
110
|
+
}
|
|
111
|
+
return isAtOrInside(root, home) ? home : root;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async function hasUnsafeManagedDirectoryComponent(root, skillDir) {
|
|
115
|
+
for (const component of managedDirectoryComponents(root, skillDir)) {
|
|
116
|
+
const stats = await pathStats(component);
|
|
117
|
+
if (stats !== null && (stats.isSymbolicLink() || !stats.isDirectory())) {
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return false;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function managedDirectoryComponents(root, skillDir) {
|
|
125
|
+
const fromRoot = [];
|
|
126
|
+
let current = skillDir;
|
|
127
|
+
while (isAtOrInside(current, root)) {
|
|
128
|
+
fromRoot.push(current);
|
|
129
|
+
const parent = dirname(current);
|
|
130
|
+
if (parent === current) {
|
|
131
|
+
break;
|
|
132
|
+
}
|
|
133
|
+
current = parent;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return uniquePaths(fromRoot.reverse());
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isAtOrInside(path, parent) {
|
|
140
|
+
return path === parent || isInside(path, parent);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function uniquePaths(paths) {
|
|
144
|
+
const seen = new Set();
|
|
145
|
+
const unique = [];
|
|
146
|
+
for (const path of paths) {
|
|
147
|
+
if (!seen.has(path)) {
|
|
148
|
+
seen.add(path);
|
|
149
|
+
unique.push(path);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
return unique;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function withoutAgentIntegrationBlock(text) {
|
|
156
|
+
const start = text.indexOf(AGENT_INTEGRATION_START);
|
|
157
|
+
if (start === -1) {
|
|
158
|
+
return null;
|
|
159
|
+
}
|
|
160
|
+
const end = text.indexOf(AGENT_INTEGRATION_END, start);
|
|
161
|
+
if (end === -1) {
|
|
162
|
+
return null;
|
|
163
|
+
}
|
|
164
|
+
const afterBlock = end + AGENT_INTEGRATION_END.length;
|
|
165
|
+
let before = text.slice(0, start);
|
|
166
|
+
const after = text.slice(afterBlock).replace(/^\r?\n/u, "");
|
|
167
|
+
if (before.endsWith("\r\n\r\n")) {
|
|
168
|
+
before = before.slice(0, -2);
|
|
169
|
+
} else if (before.endsWith("\n\n")) {
|
|
170
|
+
before = before.slice(0, -1);
|
|
171
|
+
}
|
|
172
|
+
return `${before}${after}`;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function shouldRemoveAgentIntegrationFile(text) {
|
|
176
|
+
const trimmed = text.trim();
|
|
177
|
+
if (trimmed.length === 0) {
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
return /^---\s*\r?\nname:\s*skillboard\s*\r?\ndescription:[\s\S]*?\r?\n---\s*$/u.test(trimmed);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
async function removeEmptyManagedSkillDir(path) {
|
|
184
|
+
try {
|
|
185
|
+
await rmdir(path);
|
|
186
|
+
} catch (error) {
|
|
187
|
+
if (error?.code !== "ENOENT" && error?.code !== "ENOTEMPTY" && error?.code !== "EEXIST") {
|
|
188
|
+
throw error;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
async function pathStats(path) {
|
|
194
|
+
try {
|
|
195
|
+
return await lstat(path);
|
|
196
|
+
} catch (error) {
|
|
197
|
+
if (error?.code === "ENOENT") {
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
throw error;
|
|
201
|
+
}
|
|
202
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { access, chown, lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { dirname, isAbsolute, relative, resolve } from "node:path";
|
|
4
|
+
import { promisify } from "node:util";
|
|
5
|
+
|
|
6
|
+
const execFileAsync = promisify(execFile);
|
|
7
|
+
const GETENT_PATHS = ["/usr/bin/getent", "/bin/getent"];
|
|
8
|
+
|
|
9
|
+
export async function resolveSetupHome(env, runtime) {
|
|
10
|
+
const explicit = nonEmpty(env.SKILLBOARD_SETUP_HOME);
|
|
11
|
+
if (explicit !== null) {
|
|
12
|
+
return explicit;
|
|
13
|
+
}
|
|
14
|
+
if (shouldUseSudoUserHome(env)) {
|
|
15
|
+
const sudoHome = nonEmpty(env.SUDO_HOME)
|
|
16
|
+
?? await passwdHome(env.SUDO_USER, runtime.passwdPath ?? "/etc/passwd")
|
|
17
|
+
?? await getentHome(env.SUDO_USER, runtime)
|
|
18
|
+
?? await conventionalUserHome(env.SUDO_USER);
|
|
19
|
+
if (sudoHome !== null) {
|
|
20
|
+
return sudoHome;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return env.HOME ?? env.USERPROFILE;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function setupOwnership(env, runtime, home) {
|
|
27
|
+
if (process.platform === "win32" || !shouldUseSudoUserHome(env)) {
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
const uid = parseNonNegativeInteger(env.SUDO_UID);
|
|
31
|
+
const gid = parseNonNegativeInteger(env.SUDO_GID);
|
|
32
|
+
if (uid === null || gid === null) {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
const chownFunction = runtime.chown ?? chown;
|
|
36
|
+
if (runtime.chown === undefined && !canApplyProcessOwnership(uid, gid)) {
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
return {
|
|
40
|
+
uid,
|
|
41
|
+
gid,
|
|
42
|
+
home: resolve(home),
|
|
43
|
+
chown: chownFunction
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export async function applyOwnership(path, ownership) {
|
|
48
|
+
if (ownership === null) {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
for (const ownedPath of await ownershipPaths(path, ownership.home)) {
|
|
52
|
+
await ownership.chown(ownedPath, ownership.uid, ownership.gid);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function canApplyProcessOwnership(uid, gid) {
|
|
57
|
+
if (typeof process.getuid !== "function") {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
60
|
+
if (process.getuid() === 0) {
|
|
61
|
+
return true;
|
|
62
|
+
}
|
|
63
|
+
if (typeof process.getgid !== "function") {
|
|
64
|
+
return process.getuid() === uid;
|
|
65
|
+
}
|
|
66
|
+
return process.getuid() === uid && process.getgid() === gid;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function parseNonNegativeInteger(value) {
|
|
70
|
+
if (value === undefined || !/^\d+$/.test(value)) {
|
|
71
|
+
return null;
|
|
72
|
+
}
|
|
73
|
+
const parsed = Number(value);
|
|
74
|
+
return Number.isSafeInteger(parsed) ? parsed : null;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function shouldUseSudoUserHome(env) {
|
|
78
|
+
const sudoUser = nonEmpty(env.SUDO_USER);
|
|
79
|
+
return process.platform !== "win32"
|
|
80
|
+
&& sudoUser !== null
|
|
81
|
+
&& sudoUser !== "root"
|
|
82
|
+
&& (
|
|
83
|
+
env.SUDO_UID !== undefined
|
|
84
|
+
|| env.SUDO_GID !== undefined
|
|
85
|
+
|| env.USER === "root"
|
|
86
|
+
|| env.LOGNAME === "root"
|
|
87
|
+
|| env.HOME === "/root"
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function passwdHome(user, passwdPath) {
|
|
92
|
+
const text = await readFile(passwdPath, "utf8").catch(() => "");
|
|
93
|
+
for (const line of text.split("\n")) {
|
|
94
|
+
if (line.startsWith(`${user}:`)) {
|
|
95
|
+
return nonEmpty(line.split(":")[5]);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return null;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async function getentHome(user, runtime) {
|
|
102
|
+
const candidates = runtime.getentPath === undefined ? GETENT_PATHS : [runtime.getentPath];
|
|
103
|
+
for (const candidate of candidates) {
|
|
104
|
+
try {
|
|
105
|
+
const { stdout } = await execFileAsync(candidate, ["passwd", user], {
|
|
106
|
+
env: { PATH: "/usr/sbin:/usr/bin:/sbin:/bin" },
|
|
107
|
+
timeout: 1000
|
|
108
|
+
});
|
|
109
|
+
const home = nonEmpty(stdout.trim().split(":")[5]);
|
|
110
|
+
if (home !== null) {
|
|
111
|
+
return home;
|
|
112
|
+
}
|
|
113
|
+
} catch {
|
|
114
|
+
// Try the next trusted absolute path; fall back to conventional home paths below.
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
return null;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function conventionalUserHome(user) {
|
|
121
|
+
const candidates = process.platform === "darwin"
|
|
122
|
+
? [`/Users/${user}`]
|
|
123
|
+
: [`/home/${user}`];
|
|
124
|
+
for (const candidate of candidates) {
|
|
125
|
+
if (await exists(candidate)) {
|
|
126
|
+
return candidate;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
async function exists(path) {
|
|
133
|
+
return access(path).then(() => true, () => false);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function nonEmpty(value) {
|
|
137
|
+
return value === undefined || value.trim() === "" ? null : value;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
async function ownershipPaths(path, home) {
|
|
141
|
+
const resolvedHome = resolve(home);
|
|
142
|
+
const resolvedPath = resolve(path);
|
|
143
|
+
if (!isInside(resolvedPath, resolvedHome)) {
|
|
144
|
+
return [];
|
|
145
|
+
}
|
|
146
|
+
const homeStats = await pathStats(resolvedHome);
|
|
147
|
+
if (homeStats === null || homeStats.isSymbolicLink()) {
|
|
148
|
+
return [];
|
|
149
|
+
}
|
|
150
|
+
const directories = [];
|
|
151
|
+
let current = dirname(resolvedPath);
|
|
152
|
+
while (current !== resolvedHome && isInside(current, resolvedHome)) {
|
|
153
|
+
directories.push(current);
|
|
154
|
+
const parent = dirname(current);
|
|
155
|
+
if (parent === current) {
|
|
156
|
+
break;
|
|
157
|
+
}
|
|
158
|
+
current = parent;
|
|
159
|
+
}
|
|
160
|
+
const safePaths = [];
|
|
161
|
+
for (const candidate of [...directories.reverse(), resolvedPath]) {
|
|
162
|
+
const stats = await pathStats(candidate);
|
|
163
|
+
if (stats === null || stats.isSymbolicLink()) {
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
safePaths.push(candidate);
|
|
167
|
+
}
|
|
168
|
+
return safePaths;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function pathStats(path) {
|
|
172
|
+
try {
|
|
173
|
+
return await lstat(path);
|
|
174
|
+
} catch (error) {
|
|
175
|
+
if (error?.code === "ENOENT") {
|
|
176
|
+
return null;
|
|
177
|
+
}
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
export function isInside(path, parent) {
|
|
183
|
+
const relativePath = relative(parent, path);
|
|
184
|
+
return relativePath !== "" && !relativePath.startsWith("..") && !isAbsolute(relativePath);
|
|
185
|
+
}
|
|
@@ -30,6 +30,7 @@ export async function setupAgentSkillTargets(agent, home = homedir(), env = proc
|
|
|
30
30
|
const roots = await detectedAgentSkillRoots(agent, home, env, { includeFallback: options.includeFallback === true });
|
|
31
31
|
return roots.map((root) => ({
|
|
32
32
|
agent,
|
|
33
|
+
home: resolve(home),
|
|
33
34
|
skillPath: join(root.skillRoot, "skillboard", "SKILL.md"),
|
|
34
35
|
root: root.skillRoot,
|
|
35
36
|
source: root.source
|
package/src/brief-renderer.mjs
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
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.
|
|
2
|
+
import { renderRouteSectionLines } from "./route-renderer.mjs";
|
|
3
|
+
|
|
2
4
|
const SKILL_SECTIONS = [
|
|
3
5
|
["Needs your decision", "needs_review"],
|
|
4
6
|
["Blocked for safety", "blocked"]
|
|
@@ -82,63 +84,13 @@ function emitIntentRoute(lines, brief) {
|
|
|
82
84
|
return;
|
|
83
85
|
}
|
|
84
86
|
lines.push("## Suggested skill for this request", "");
|
|
85
|
-
lines.push(
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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
|
-
}
|
|
87
|
+
lines.push(...renderRouteSectionLines(route, {
|
|
88
|
+
format: "brief",
|
|
89
|
+
nextStep: brief.assistant_guidance?.recommended_next_step
|
|
90
|
+
}));
|
|
115
91
|
lines.push("");
|
|
116
92
|
}
|
|
117
93
|
|
|
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
94
|
function emitPolicyHealth(lines, brief) {
|
|
143
95
|
const policy = brief.health?.policy;
|
|
144
96
|
if (policy === undefined) {
|
|
@@ -271,6 +223,7 @@ function emitDecisionSection(lines, brief, options) {
|
|
|
271
223
|
lines.push("- none", "");
|
|
272
224
|
return;
|
|
273
225
|
}
|
|
226
|
+
emitDeferredDecisionLead(lines, brief);
|
|
274
227
|
if (skillEntries.length > 0) {
|
|
275
228
|
emitSkillEntries(lines, skillEntries, {
|
|
276
229
|
...options,
|
|
@@ -287,6 +240,13 @@ function emitDecisionSection(lines, brief, options) {
|
|
|
287
240
|
lines.push("");
|
|
288
241
|
}
|
|
289
242
|
|
|
243
|
+
function emitDeferredDecisionLead(lines, brief) {
|
|
244
|
+
if (!hasUsableRoutedSkill(brief)) {
|
|
245
|
+
return;
|
|
246
|
+
}
|
|
247
|
+
lines.push("A routed skill is already usable for this request; handle these decisions after the task unless a policy-changing action is needed now.", "");
|
|
248
|
+
}
|
|
249
|
+
|
|
290
250
|
function reviewEntriesForDecisionSection(brief, skillEntries) {
|
|
291
251
|
if (skillEntries.length === 0) {
|
|
292
252
|
return brief.review_queue ?? [];
|
|
@@ -384,7 +344,7 @@ function workflowOption(brief) {
|
|
|
384
344
|
function emitActions(lines, brief, options) {
|
|
385
345
|
const actions = actionsForTextBrief(brief);
|
|
386
346
|
lines.push("## Suggested next actions", "");
|
|
387
|
-
lines.push(
|
|
347
|
+
lines.push(suggestedActionsLead(brief), "");
|
|
388
348
|
if (actions.length === 0) {
|
|
389
349
|
lines.push("- none", "");
|
|
390
350
|
return;
|
|
@@ -401,7 +361,7 @@ function emitActions(lines, brief, options) {
|
|
|
401
361
|
if (!options.verbose) {
|
|
402
362
|
const hiddenSecondary = other.length + cleanup.length;
|
|
403
363
|
if (hiddenSecondary > 0) {
|
|
404
|
-
if (lines.
|
|
364
|
+
if (lines[lines.length - 1] === "") {
|
|
405
365
|
lines.pop();
|
|
406
366
|
}
|
|
407
367
|
lines.push(`- ${hiddenSecondary} safety and cleanup actions hidden. Run ${code(`skillboard brief --verbose${workflowOption(brief)}`)}.`);
|
|
@@ -419,9 +379,15 @@ function emitActions(lines, brief, options) {
|
|
|
419
379
|
}
|
|
420
380
|
}
|
|
421
381
|
|
|
382
|
+
function suggestedActionsLead(brief) {
|
|
383
|
+
return hasUsableRoutedSkill(brief)
|
|
384
|
+
? "After the routed task, use current action ids from this brief for policy changes; ask for one user confirmation before applying one action."
|
|
385
|
+
: "AI/automation operations should use current action ids from this brief, then ask for user confirmation before applying one action.";
|
|
386
|
+
}
|
|
387
|
+
|
|
422
388
|
function emitNextAction(lines, brief, options) {
|
|
423
389
|
lines.push("## Next safe action", "");
|
|
424
|
-
lines.push(
|
|
390
|
+
lines.push(nextActionLead(brief), "");
|
|
425
391
|
const action = nextSafeAction(brief);
|
|
426
392
|
if (action === null) {
|
|
427
393
|
lines.push("- none", "");
|
|
@@ -431,6 +397,19 @@ function emitNextAction(lines, brief, options) {
|
|
|
431
397
|
lines.push("");
|
|
432
398
|
}
|
|
433
399
|
|
|
400
|
+
function nextActionLead(brief) {
|
|
401
|
+
return hasUsableRoutedSkill(brief)
|
|
402
|
+
? "A routed skill is already usable for this request; handle this policy action after the task unless a policy-changing action is needed now."
|
|
403
|
+
: "AI/automation should present this as the next confirmable operation, not as an automatic mutation.";
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
function hasUsableRoutedSkill(brief) {
|
|
407
|
+
const route = brief.assistant_guidance?.route;
|
|
408
|
+
return route?.recommended_skill !== null
|
|
409
|
+
&& route?.recommended_skill !== undefined
|
|
410
|
+
&& route.guard_allowed === true;
|
|
411
|
+
}
|
|
412
|
+
|
|
434
413
|
function nextSafeAction(brief) {
|
|
435
414
|
const actions = actionsForTextBrief(brief);
|
|
436
415
|
if (actions.length === 0) {
|