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/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 || !configDoctor.initialized) {
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 = resolveWorkflow(listWorkflows(workspace), options.workflow);
78
- const reviewQueue = buildReviewQueue(workspace, sourceAudit);
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.sources.ok;
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
- if (intent === undefined || intent.length === 0 || workflow.selected === null || workflow.unknown || workflow.needs_selection) {
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,8 +1,10 @@
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, resolveSetupHome, setupOwnership } from "./agent-integration-home.mjs";
5
6
  import { setupAgentSkillTargets, supportedAgentNames } from "./agent-skill-roots.mjs";
7
+ import { refreshAgentInventory } from "./inventory-refresh.mjs";
6
8
 
7
9
  export async function runSetupCommand(options, stdout, runtime = defaultRuntime()) {
8
10
  if (options.get("dir") !== undefined) {
@@ -27,15 +29,22 @@ export async function runSetupCommand(options, stdout, runtime = defaultRuntime(
27
29
  return 1;
28
30
  }
29
31
  }
30
- const result = await installAgentIntegration(targets, setupOwnership(env, runtime, home));
32
+ const ownership = setupOwnership(env, runtime, home);
33
+ const result = await installAgentIntegration(targets, ownership);
34
+ await mkdir(home, { recursive: true });
35
+ const inventory = await refreshAgentInventory({ root: home, home, env });
36
+ await applyOwnership(resolve(home, inventory.configPath), ownership);
37
+ if (inventory.inventoryPath !== null) await applyOwnership(resolve(home, inventory.inventoryPath), ownership);
31
38
  stdout.write("SkillBoard agent integration installed.\n");
32
39
  writeList(stdout, "Created", result.created);
33
40
  writeList(stdout, "Updated", result.updated);
34
41
  writeList(stdout, "Unchanged", result.unchanged);
35
42
  writeList(stdout, "Preserved", result.preserved);
43
+ stdout.write(`User policy: ${inventory.configPath}\n`);
44
+ stdout.write(`Observed skills: ${inventory.scan.scannedSkills}\n`);
36
45
  stdout.write("Next:\n");
37
46
  stdout.write("- Restart or refresh agents that cache user skills.\n");
38
- stdout.write("- No project was initialized; run skillboard init only inside a workspace that needs local SkillBoard policy.\n");
47
+ stdout.write("- User-level policy and inventory were refreshed; no project was initialized.\n");
39
48
  stdout.write('- Ask the agent in a workspace: "Review this plan and point out weak assumptions."\n');
40
49
  stdout.write("- SkillBoard will step in when skills overlap, routing is ambiguous, or you ask for a skill decision.\n");
41
50
  return 0;
@@ -76,7 +85,7 @@ function writeList(stdout, label, values) {
76
85
  function writeSetupConfirmation(stdout, targets, command) {
77
86
  stdout.write("SkillBoard setup installs agent-layer integration, not project files.\n");
78
87
  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 does not create skillboard.config.yaml or .skillboard/; run skillboard init later only for a project that needs local policy.\n");
88
+ 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
89
  stdout.write("Targets:\n");
81
90
  for (const target of targets) {
82
91
  stdout.write(`- ${target.agent}: ${target.skillPath}\n`);
@@ -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, workflow priority matters, or the user explicitly asks which skill should be used, imported, preferred, avoided, reviewed, or prioritized.
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 workflow-aware routing.
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\` install user-agent guidance only.
19
- - \`skillboard init\` is a separate project command. Use it only when the user wants project-local policy files such as \`skillboard.config.yaml\`, \`.skillboard/\`, \`AGENTS.md\`, or \`CLAUDE.md\`.
18
+ - Package install and \`skillboard setup\` refresh user-agent guidance plus the home policy and inventory.
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 user skills are usable by default unless the runtime, user, or local instructions disable them.
24
- - For ordinary user requests, work normally; invoke SkillBoard only when skill choice is ambiguous, skills overlap, workflow priority matters, or the user asks for a SkillBoard or skill decision.
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, a manual skill-control request is present, or workflow priority could change the route.
41
- 3. Prefer the skill whose explicit request, description, workflow guidance, and local instructions most directly match the user's task.
42
- 4. If a project or agent has explicit SkillBoard policy, use \`skillboard brief --intent <request> --json\` or \`skillboard route <intent> --workflow <name> --json\` to break ties.
43
- 5. If the best choice is still ambiguous or the choice would change persistent policy, ask the user which priority to remember.
44
- 6. Continue with the selected skill; do not stop only because other candidate skills exist.
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;
@@ -6,15 +6,21 @@ export async function defaultScanRoots(home, env) {
6
6
  const agentRoots = await Promise.all([
7
7
  agentSkillRootCandidates("codex", home, env),
8
8
  agentSkillRootCandidates("claude", home, env),
9
+ agentSkillRootCandidates("opencode", home, env),
9
10
  agentSkillRootCandidates("hermes", home, env)
10
11
  ]);
11
12
  return [
12
13
  join(codexHome, "skills", ".system"),
13
14
  join(codexHome, "plugins", "cache"),
15
+ join(home, ".agents", "shared-skills"),
14
16
  ...agentRoots.flat().map((root) => root.skillRoot)
15
17
  ];
16
18
  }
17
19
 
20
+ export function sharedUserUnit(path, home) {
21
+ return userSkillUnit("shared.user-skills", path, home);
22
+ }
23
+
18
24
  export function systemCodexUnit(path, home) {
19
25
  return {
20
26
  id: "codex.system-skills",
@@ -38,6 +44,10 @@ export function userClaudeUnit(path, home) {
38
44
  return userSkillUnit("claude.user-skills", path, home);
39
45
  }
40
46
 
47
+ export function userOpenCodeUnit(path, home) {
48
+ return userSkillUnit("opencode.user-skills", path, home);
49
+ }
50
+
41
51
  export function userHermesUnit(path, home) {
42
52
  return userSkillUnit("hermes.user-skills", path, home);
43
53
  }
@@ -9,14 +9,25 @@ import {
9
9
  hermesProfileUnit,
10
10
  isHermesProfileSkillsPath,
11
11
  safeSegment,
12
+ sharedUserUnit,
12
13
  systemCodexUnit,
13
14
  userClaudeUnit,
14
15
  userCodexUnit,
15
- userHermesUnit
16
+ userHermesUnit,
17
+ userOpenCodeUnit
16
18
  } from "./agent-inventory-platforms.mjs";
17
19
  import { readString, requireRecord } from "./config-helpers.mjs";
18
20
 
19
21
  export const agentInventoryDetectors = Object.freeze([
22
+ {
23
+ id: "shared-user-skills",
24
+ matches(path) {
25
+ return path.endsWith("/.agents/shared-skills") || path.endsWith("\\.agents\\shared-skills");
26
+ },
27
+ async discover(path, home) {
28
+ return await discoverSkillDirectory(path, sharedUserUnit(path, home), { excludeSystem: true });
29
+ }
30
+ },
20
31
  {
21
32
  id: "codex-plugin-cache",
22
33
  matches(path) {
@@ -56,6 +67,16 @@ export const agentInventoryDetectors = Object.freeze([
56
67
  return await discoverSkillDirectory(path, userClaudeUnit(path, home), { excludeSystem: true });
57
68
  }
58
69
  },
70
+ {
71
+ id: "opencode-user-skills",
72
+ matches(path) {
73
+ const normalized = path.replace(/\\/g, "/");
74
+ return normalized.endsWith("/.config/opencode/skills") || normalized.endsWith("/.opencode/skills");
75
+ },
76
+ async discover(path, home) {
77
+ return await discoverSkillDirectory(path, userOpenCodeUnit(path, home), { excludeSystem: true });
78
+ }
79
+ },
59
80
  {
60
81
  id: "hermes-user-skills",
61
82
  matches(path) {
@@ -416,6 +437,7 @@ async function inventoryFromGroups(groups, home, initialWarnings = []) {
416
437
  const skill = {
417
438
  id,
418
439
  path: sourceAlias.path,
440
+ skillFile: resolve(file),
419
441
  status: "quarantined",
420
442
  invocation: "blocked",
421
443
  exposure: "exported",
@@ -157,7 +157,7 @@ async function findSourceSkill(options) {
157
157
  throw new Error(`Source skill not found: ${options.skill} under ${root}`);
158
158
  }
159
159
 
160
- async function findSourceSkillInRoots(options) {
160
+ export async function findSourceSkillInRoots(options) {
161
161
  const misses = [];
162
162
  for (const root of options.roots) {
163
163
  try {
@@ -200,7 +200,7 @@ function parseSkillFrontmatter(text) {
200
200
  return parsed !== null && typeof parsed === "object" ? parsed : {};
201
201
  }
202
202
 
203
- function analyzeAgentCompatibility(content, options) {
203
+ export function analyzeAgentCompatibility(content, options) {
204
204
  const reasons = [];
205
205
  for (const [agent, markers] of Object.entries(AGENT_MARKERS)) {
206
206
  if (agent === options.targetAgent) {
@@ -0,0 +1,42 @@
1
+ import { homedir } from "node:os";
2
+ import { basename, isAbsolute, relative, resolve } from "node:path";
3
+
4
+ export function expandPortablePath(value, paths) {
5
+ if (value === "${PROJECT}") return paths.rootDir;
6
+ if (value.startsWith("${PROJECT}/")) return resolve(paths.rootDir, value.slice("${PROJECT}/".length));
7
+ if (value === "${HOME}") return homedir();
8
+ if (value.startsWith("${HOME}/")) return resolve(homedir(), value.slice("${HOME}/".length));
9
+ if (value === "<external>" || value.startsWith("<external>/")) return null;
10
+ return value;
11
+ }
12
+
13
+ export function portableObservation(value, options) {
14
+ if (value.startsWith("${PROJECT}") || value.startsWith("${HOME}") || value.startsWith("<external>")) return value;
15
+ if (value.startsWith("~/")) return `\${HOME}/${value.slice(2)}`;
16
+ return isAbsolute(value) ? auditPath(value, options) : value;
17
+ }
18
+
19
+ export function auditPath(path, options) {
20
+ const project = relative(options.rootDir, path);
21
+ if (!project.startsWith("..") && !isAbsolute(project)) {
22
+ return project === "" ? "${PROJECT}" : `\${PROJECT}/${project.replace(/\\/g, "/")}`;
23
+ }
24
+ const home = relative(homedir(), path);
25
+ if (!home.startsWith("..") && !isAbsolute(home)) {
26
+ return home === "" ? "${HOME}" : `\${HOME}/${home.replace(/\\/g, "/")}`;
27
+ }
28
+ return `<external>/${basename(path)}`;
29
+ }
30
+
31
+ export function redactPathError(error, options) {
32
+ const message = error instanceof Error ? error.message : String(error);
33
+ return replaceText(
34
+ replaceText(replaceText(message, options.rootDir, "${PROJECT}"), options.configDir, "${PROJECT}"),
35
+ homedir(),
36
+ "${HOME}"
37
+ );
38
+ }
39
+
40
+ function replaceText(value, search, replacement) {
41
+ return value.split(search).join(replacement);
42
+ }
package/src/brief-cli.mjs CHANGED
@@ -5,11 +5,12 @@ import { renderSkillBrief } from "./brief-renderer.mjs";
5
5
  export async function runBriefCommand(options, stdout, paths) {
6
6
  const json = options.get("json") === "true";
7
7
  const result = await buildSkillBrief({
8
- root: briefRoot(options),
8
+ root: briefRoot(options) ?? dirname(paths.configPath),
9
9
  configPath: paths.configPath,
10
10
  skillsRoot: paths.skillsRoot,
11
11
  workflow: options.get("workflow"),
12
12
  intent: options.get("intent"),
13
+ agent: options.get("agent"),
13
14
  includeActions: options.get("include-actions") === "true" || !json
14
15
  });
15
16
  writeBriefOutput(stdout, result, options);
@@ -22,7 +23,7 @@ function briefRoot(options) {
22
23
  return dir;
23
24
  }
24
25
  const config = options.get("config");
25
- return config !== undefined && isAbsolute(config) ? dirname(config) : ".";
26
+ return config !== undefined && isAbsolute(config) ? dirname(config) : undefined;
26
27
  }
27
28
 
28
29
  function briefExitCode(result) {
@@ -47,6 +47,7 @@ export function renderSkillBrief(brief, options = {}) {
47
47
  `Workflow: ${workflowSummary(brief.workflow)}`,
48
48
  ""
49
49
  ];
50
+ if (brief.compatibility?.notice !== undefined) lines.push(brief.compatibility.notice, "");
50
51
  if (brief.error !== undefined) {
51
52
  lines.push(`Error: ${safeText(brief.error.message)}`, "");
52
53
  }