Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ed0ea64

Browse files
Prefix controller-managed skills with controller- and hide them from the slash picker (#173)
The five app-managed skills installed into every provider's user home on startup are agent-facing, not user-invokable. Today they live under unprefixed names (`browser`, `integrations`, `search-skills`, `skill-creator`, plus the partially-prefixed `controller-scripts`) and render in the `/` picker as ordinary `scope: "user"` skills, so a user typing `/` sees half the visible items being app-managed with no visual cue. Rename them under a consistent `controller-` prefix (`controller-browser`, `controller-integrations`, `controller-search-skills`, `controller-skill-creator`, and `controller-scripts` grandfathered as the only exception to avoid a double prefix), update each `name:` frontmatter to match the directory name, refresh the bodies' self-references and collision examples, and bump the `MANAGED_MARKER` comment to reference this issue so future renames can detect unowned files. The disk provider now detects `MANAGED_MARKER` and tags the resulting metadata with a new `scope: "managed"` value (extends `SkillScope` on both server and client). `mergeSkillMetadata` keeps managed entries reachable for body lookup — the agent still needs to `readBody` them — and the chat composer's `/` picker filters them out of the popover so they no longer mix in with user-invokable skills. A user who types `/controller-browser` manually and submits still gets the body prepended; the server-side slash path is intentionally left permissive. After upgrading, manually remove the old `~/.{anita,codex,claude}/skills/` directories named `browser`, `integrations`, `search-skills`, or `skill-creator` — their previous `MANAGED_MARKER` would otherwise leave them visible as regular user-authored skills. Closes #159
1 parent e63b2ed commit ed0ea64

7 files changed

Lines changed: 236 additions & 24 deletions

File tree

CHANGELOG.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,33 @@ All notable changes to this project are documented here.
44

55
## [Unreleased]
66

7+
### Changed
8+
9+
- **Renamed controller-managed skills to a `controller-` prefix and hid them
10+
from the `/` picker** (#159). The five app-managed skills installed into
11+
each provider's user home on startup
12+
(`browser``controller-browser`,
13+
`integrations``controller-integrations`,
14+
`controller-scripts` (grandfathered — no double prefix),
15+
`search-skills``controller-search-skills`,
16+
`skill-creator``controller-skill-creator`) now live under
17+
`controller-`-prefixed directories, with matching `name:` frontmatter and
18+
`MANAGED_MARKER` (now references issue #159 so future renames can detect
19+
unowned files). The disk provider tags any `SKILL.md` carrying the
20+
marker with `scope: "managed"`, and the chat composer filters
21+
`scope: "managed"` entries out of the `/` autocomplete popover so users
22+
no longer see agent-facing skills mixed in with their own. The agent
23+
still discovers the body through the filesystem location, so a user who
24+
types `/controller-browser` manually and submits still gets the body
25+
prepended. Existing per-agent and unified skills are unaffected. **Note:**
26+
after upgrading, manually remove the old `~/.{anita,codex,claude}/skills/`
27+
directories named `browser`, `integrations`, `search-skills`, or
28+
`skill-creator` (or simply `rm -rf ~/.anita/skills/browser` etc.); they
29+
carry the previous marker comment and would otherwise be re-read as
30+
regular user-authored skills.
31+
32+
33+
734
### Changed
835

936
- **Updated session-file ownership comments to reflect post-#152 / #163

client/src/api.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -668,7 +668,12 @@ export async function fetchAgentProviders(): Promise<AgentProviderInfo[]> {
668668
return res.json();
669669
}
670670

671-
export type SkillScope = "unified" | "user" | "system" | "repo";
671+
export type SkillScope =
672+
| "unified"
673+
| "user"
674+
| "system"
675+
| "repo"
676+
| "managed";
672677

673678
export interface AgentSkill {
674679
name: string;

client/src/pages/SessionView.tsx

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3666,9 +3666,16 @@ export function SessionView({
36663666
}, [message]);
36673667
const filteredSkills = useMemo(() => {
36683668
if (skillQuery === null) return [];
3669+
// Hide controller-managed skills (e.g. `controller-browser`) from the
3670+
// `/` picker. They are still in `availableSkills` so a user can type the
3671+
// name manually and submit; the server-side slash path will prepend the
3672+
// body either way. Managed skills are agent-facing, not user-invokable.
3673+
const userVisible = availableSkills.filter(
3674+
(entry) => entry.scope !== "managed"
3675+
);
36693676
const needle = skillQuery.token.toLowerCase();
3670-
if (!needle) return availableSkills;
3671-
return availableSkills.filter((entry) =>
3677+
if (!needle) return userVisible;
3678+
return userVisible.filter((entry) =>
36723679
entry.name.toLowerCase().startsWith(needle)
36733680
);
36743681
}, [availableSkills, skillQuery]);

server/lib/__tests__/managed-skills.test.ts

Lines changed: 77 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,10 @@ test("managed skills use a single `<cliPath> <surface> <command>` convention", a
7676
// `<cliPath> <surface> <command>`. Never a bare subcommand, never a
7777
// double-prefixed surface.
7878
const cases = [
79-
{ name: "browser", surface: "browser" },
80-
{ name: "integrations", surface: "integrations" },
81-
{ name: "search-skills", surface: "skills" },
82-
{ name: "skill-creator", surface: "skills" },
79+
{ name: "controller-browser", surface: "browser" },
80+
{ name: "controller-integrations", surface: "integrations" },
81+
{ name: "controller-search-skills", surface: "skills" },
82+
{ name: "controller-skill-creator", surface: "skills" },
8383
] as const;
8484

8585
for (const provider of [".anita", ".codex", ".claude"]) {
@@ -143,7 +143,7 @@ test("browser, integrations, and skills bodies advertise concrete commands", asy
143143
);
144144

145145
const anitaBrowser = readFileSync(
146-
path.join(os.homedir(), ".anita", "skills", "browser", "SKILL.md"),
146+
path.join(os.homedir(), ".anita", "skills", "controller-browser", "SKILL.md"),
147147
"utf-8"
148148
);
149149
assert.match(
@@ -156,7 +156,13 @@ test("browser, integrations, and skills bodies advertise concrete commands", asy
156156
);
157157

158158
const anitaIntegrations = readFileSync(
159-
path.join(os.homedir(), ".anita", "skills", "integrations", "SKILL.md"),
159+
path.join(
160+
os.homedir(),
161+
".anita",
162+
"skills",
163+
"controller-integrations",
164+
"SKILL.md"
165+
),
160166
"utf-8"
161167
);
162168
assert.match(
@@ -169,7 +175,13 @@ test("browser, integrations, and skills bodies advertise concrete commands", asy
169175
);
170176

171177
const anitaSearchSkills = readFileSync(
172-
path.join(os.homedir(), ".anita", "skills", "search-skills", "SKILL.md"),
178+
path.join(
179+
os.homedir(),
180+
".anita",
181+
"skills",
182+
"controller-search-skills",
183+
"SKILL.md"
184+
),
173185
"utf-8"
174186
);
175187
assert.match(
@@ -182,7 +194,13 @@ test("browser, integrations, and skills bodies advertise concrete commands", asy
182194
);
183195

184196
const anitaSkillCreator = readFileSync(
185-
path.join(os.homedir(), ".anita", "skills", "skill-creator", "SKILL.md"),
197+
path.join(
198+
os.homedir(),
199+
".anita",
200+
"skills",
201+
"controller-skill-creator",
202+
"SKILL.md"
203+
),
186204
"utf-8"
187205
);
188206
assert.match(
@@ -191,3 +209,54 @@ test("browser, integrations, and skills bodies advertise concrete commands", asy
191209
);
192210
});
193211
});
212+
213+
test("managed skills install under controller-prefixed directory names", async () => {
214+
await withIsolatedHomes(async () => {
215+
await installManagedSkills();
216+
217+
// Each managed skill should land in a `controller-`-prefixed directory
218+
// (or stay as the grandfathered `controller-scripts`). The marker comment
219+
// references issue #159 so future renames can detect unowned files.
220+
const expected: Array<{ dir: string; name: string }> = [
221+
{ dir: "controller-browser", name: "controller-browser" },
222+
{ dir: "controller-integrations", name: "controller-integrations" },
223+
{ dir: "controller-scripts", name: "controller-scripts" },
224+
{ dir: "controller-search-skills", name: "controller-search-skills" },
225+
{ dir: "controller-skill-creator", name: "controller-skill-creator" },
226+
];
227+
228+
for (const provider of [".anita", ".codex", ".claude"]) {
229+
for (const { dir, name } of expected) {
230+
const skillFile = path.join(
231+
os.homedir(),
232+
provider,
233+
"skills",
234+
dir,
235+
"SKILL.md"
236+
);
237+
assert.ok(
238+
existsSync(skillFile),
239+
`expected ${skillFile} to be installed`
240+
);
241+
const body = readFileSync(skillFile, "utf-8");
242+
// The `name:` frontmatter must match the directory name so
243+
// `extractSkillInvocation` and the disk loader see consistent names.
244+
const frontmatterMatch = /^---\nname:\s*([^\n]+)\n/m.exec(body);
245+
assert.ok(
246+
frontmatterMatch,
247+
`${skillFile} is missing a 'name:' frontmatter line`
248+
);
249+
assert.equal(
250+
frontmatterMatch[1].trim(),
251+
name,
252+
`${skillFile} frontmatter name does not match the directory name`
253+
);
254+
// The body must carry the up-to-date managed marker.
255+
assert.ok(
256+
body.includes("<!-- managed-by: coding-orchestrator (issue #159) -->"),
257+
`${skillFile} is missing the current MANAGED_MARKER`
258+
);
259+
}
260+
}
261+
});
262+
});

server/lib/__tests__/skills.test.ts

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -458,3 +458,71 @@ test("unified skills appear in listMetadata above per-agent skills", async () =>
458458
rmSync(orchestratorHome, { recursive: true, force: true });
459459
});
460460
});
461+
462+
// ---------------------------------------------------------------------------
463+
// Controller-managed skills: scope and dedupe behavior (issue #159)
464+
// ---------------------------------------------------------------------------
465+
466+
const MANAGED_MARKER = "<!-- managed-by: coding-orchestrator (issue #159) -->";
467+
468+
test("per-agent SKILL.md with MANAGED_MARKER is surfaced as scope 'managed'", async () => {
469+
await withProviderHome("anita", async (home, provider) => {
470+
// A managed skill lives in the user home with the marker comment in the
471+
// body. The loader should tag its metadata as `scope: "managed"` so the
472+
// `/` picker can hide it.
473+
makeSkillFile(
474+
path.join(home, ".anita/skills"),
475+
"controller-browser",
476+
{ name: "controller-browser", description: "Drive the preview browser" },
477+
`${MANAGED_MARKER}\n# controller-browser\nbody`
478+
);
479+
const metadata = await provider.listMetadata(os.tmpdir());
480+
assert.equal(metadata.length, 1);
481+
assert.equal(metadata[0].name, "controller-browser");
482+
assert.equal(metadata[0].scope, "managed");
483+
});
484+
});
485+
486+
test("a user-authored SKILL.md in the same directory keeps scope 'user'", async () => {
487+
await withProviderHome("anita", async (home, provider) => {
488+
// No marker → regular user skill. The scope should not be silently
489+
// upgraded to "managed" just because the directory looks controlled.
490+
makeSkillFile(
491+
path.join(home, ".anita/skills"),
492+
"my-skill",
493+
{ name: "my-skill", description: "Mine" },
494+
"# my-skill\nbody"
495+
);
496+
const metadata = await provider.listMetadata(os.tmpdir());
497+
assert.equal(metadata.length, 1);
498+
assert.equal(metadata[0].scope, "user");
499+
});
500+
});
501+
502+
test("mergeSkillMetadata keeps managed entries for body lookup (unified still wins)", () => {
503+
const unified = [skillMeta("imagegen", "unified", "u")];
504+
const perAgent = [
505+
skillMeta("controller-browser", "managed", "managed browser"),
506+
skillMeta("github-issues", "user", "user skill"),
507+
];
508+
const merged = mergeSkillMetadata(unified, perAgent);
509+
// Unified renders first; managed + user entries follow in their original
510+
// order. The managed entry must remain reachable so the agent can
511+
// `readBody` it on demand.
512+
assert.deepEqual(merged.map((entry) => [entry.name, entry.scope]), [
513+
["imagegen", "unified"],
514+
["controller-browser", "managed"],
515+
["github-issues", "user"],
516+
]);
517+
});
518+
519+
test("extractSkillInvocation matches the renamed controller- names", () => {
520+
assert.deepEqual(extractSkillInvocation("/controller-browser do it"), {
521+
skillName: "controller-browser",
522+
rest: "do it",
523+
});
524+
assert.deepEqual(extractSkillInvocation("/controller-search-skills find x"), {
525+
skillName: "controller-search-skills",
526+
rest: "find x",
527+
});
528+
});

server/lib/managed-skills.ts

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,12 @@ import os from "node:os";
1111
import path from "node:path";
1212
import { controllerCliInstalledPath } from "./controller-cli.js";
1313

14-
const MANAGED_MARKER = "<!-- managed-by: coding-orchestrator (issue #109) -->";
14+
export const MANAGED_MARKER =
15+
"<!-- managed-by: coding-orchestrator (issue #159) -->";
1516

1617
function buildIntegrationsSkillBody(cliPath: string): string {
1718
return `---
18-
name: integrations
19+
name: controller-integrations
1920
description: Discover and use the third-party services the user connected in Controller (APIs, MCP servers, native CLIs) through a uniform gateway. Use whenever a task needs an external service — search for a capability, then call it. Credentials are injected by Controller; you never see or handle secrets.
2021
---
2122
@@ -28,6 +29,11 @@ Integrations. You reach them through the Controller CLI, invoked by its absolute
2829
path (it is not on your PATH). Every command below is run as
2930
\`${cliPath} integrations <command>\`.
3031
32+
This skill is managed by the Controller app (directory name
33+
\`controller-integrations\`). It is hidden from the \`/\` picker — the agent
34+
discovers the body through the filesystem location, not via a user-invoked
35+
slash command.
36+
3137
Controller holds the credentials and injects them server-side when making the
3238
call — there is no token for you to read, and you must never ask the user to
3339
paste one into the chat. Authentication is set up in the UI.
@@ -93,7 +99,7 @@ the integration, then retry.
9399

94100
function buildBrowserSkillBody(cliPath: string): string {
95101
return `---
96-
name: browser
102+
name: controller-browser
97103
description: Drive the visible in-app preview browser to open pages, read the rendered DOM, and click or type — use it to verify UI/web work instead of guessing.
98104
---
99105
@@ -106,6 +112,11 @@ browser CLI. Use it to verify front-end and web work: open a localhost dev
106112
server or a project HTML file, read what actually rendered, and interact with
107113
the page. The user sees everything you do in the Preview tab.
108114
115+
This skill is managed by the Controller app (directory name
116+
\`controller-browser\`). It is hidden from the \`/\` picker — the agent
117+
discovers the body through the filesystem location, not via a user-invoked
118+
slash command.
119+
109120
Invoke the CLI by its absolute path — it is not on your PATH. Every command
110121
below is run as \`${cliPath} browser <command>\`:
111122
@@ -147,6 +158,11 @@ ${MANAGED_MARKER}
147158
148159
# Controller Scripts
149160
161+
This skill is managed by the Controller app (directory name
162+
\`controller-scripts\`). It is hidden from the \`/\` picker — the agent
163+
discovers the body through the filesystem location, not via a user-invoked
164+
slash command.
165+
150166
Controller resolves native scripts from the project's root
151167
\`.coding-orchestrator/\` directory:
152168
@@ -322,14 +338,19 @@ When migrating, translate those JSON commands into native shell scripts at
322338

323339
function buildSearchSkillsBody(cliPath: string): string {
324340
return `---
325-
name: search-skills
341+
name: controller-search-skills
326342
description: Search and activate unified skills from the Controller catalog. Use when the user asks for a capability that might already be configured as a unified skill, or when you want to reuse an existing skill for the current turn.
327343
---
328344
329345
${MANAGED_MARKER}
330346
331347
# Search Skills
332348
349+
This skill is managed by the Controller app (directory name
350+
\`controller-search-skills\`). It is hidden from the \`/\` picker — the agent
351+
discovers the body through the filesystem location, not via a user-invoked
352+
slash command.
353+
333354
Controller hosts an app-owned catalog of unified skills in Settings → Skills.
334355
These skills are available to every agent and take precedence over per-agent
335356
skills with the same name. You can search the catalog and activate a skill so
@@ -395,14 +416,19 @@ interface ManagedSkill {
395416

396417
function buildSkillCreatorSkillBody(cliPath: string): string {
397418
return `---
398-
name: skill-creator
419+
name: controller-skill-creator
399420
description: Create a new unified skill in the Controller catalog by interviewing the user, drafting a SKILL.md, and writing it via the Controller CLI. Use when the user asks to build a new skill, document a recurring workflow, or turn a one-off conversation into a reusable skill.
400421
---
401422
402423
${MANAGED_MARKER}
403424
404425
# Skill Creator
405426
427+
This skill is managed by the Controller app (directory name
428+
\`controller-skill-creator\`). It is hidden from the \`/\` picker — the agent
429+
discovers the body through the filesystem location, not via a user-invoked
430+
slash command.
431+
406432
You help the user create a new unified skill in Controller's app-owned
407433
catalog at \`~/coding-orchestrator/skills/<name>/SKILL.md\`. Skills created
408434
here are available to every agent immediately and surface in the \`/\`
@@ -493,8 +519,8 @@ fails on duplicate names so it cannot be used as an edit path.
493519
494520
- Unified skills take precedence over per-agent skills with the same name,
495521
so creating a skill called \`browser\` would shadow the managed
496-
\`browser\` skill. Confirm with the user before claiming a name that
497-
might collide with a built-in.
522+
\`controller-browser\` skill. Confirm with the user before claiming a name
523+
that might collide with a built-in.
498524
- The CLI writes to \`~/coding-orchestrator/skills/<name>/SKILL.md\`. Do
499525
not try to write the file directly — let the CLI perform validation.
500526
- Skill activation (\`/name\`) prepends the body to the user's next
@@ -535,11 +561,11 @@ export async function installManagedSkills(): Promise<void> {
535561
// bare CLI path here.
536562
const cli = controllerCliInstalledPath();
537563
const skills: ManagedSkill[] = [
538-
{ name: "browser", body: buildBrowserSkillBody(cli) },
539-
{ name: "integrations", body: buildIntegrationsSkillBody(cli) },
564+
{ name: "controller-browser", body: buildBrowserSkillBody(cli) },
565+
{ name: "controller-integrations", body: buildIntegrationsSkillBody(cli) },
540566
{ name: "controller-scripts", body: CONTROLLER_SCRIPTS_SKILL_BODY },
541-
{ name: "search-skills", body: buildSearchSkillsBody(cli) },
542-
{ name: "skill-creator", body: buildSkillCreatorSkillBody(cli) },
567+
{ name: "controller-search-skills", body: buildSearchSkillsBody(cli) },
568+
{ name: "controller-skill-creator", body: buildSkillCreatorSkillBody(cli) },
543569
];
544570

545571
for (const { dir } of providerHomes()) {

0 commit comments

Comments
 (0)