Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,19 +416,20 @@ Primary is Skywalker. Bundled skill bodies that are operator slashes are **actio
| `.claude/skills/` | Claude Code workspace skills |
| `.codex/skills/` | Codex workspace skills |

Each `<base>/<skill-name>/SKILL.md` is one skill. Discovery dedupes by directory name: the first base dir that provides a given name wins, so an enabled plugin skill shadows a project-local skill of the same name. Plugin dirs are passed in discovery order (repo first), so a first-party catalog name wins over a later marketplace or project skill of the same name. `resolveSkillBody(cwd, ref, pluginDirs)` resolves a skill's body using the same ordered list (it accepts a bare name or a `plugin:name` ref, keying on the name).
Each `<base>/<skill-name>/SKILL.md` is one skill. Discovery dedupes by directory name: the first base dir that provides a given name wins, so an enabled plugin skill shadows a project-local skill of the same name. Plugin dirs are passed in discovery order (repo first), so a first-party catalog name wins over a later marketplace or project skill of the same name. Skills with `disable-model-invocation: true` are omitted from the returned listing but still claim the name (first-wins), so a lower-priority same-name skill cannot leak into the listing. `resolveSkillBody(cwd, ref, pluginDirs)` resolves a skill's body using the same ordered list (it accepts a bare name or a `plugin:name` ref, keying on the name) and **does not** hard-fail on `disable-model-invocation` — explicit `use_skill("name")` still loads background libraries.

#### SKILL.md format

A skill file begins with a YAML frontmatter block, followed by the body that holds the instructions. Discovery parses `description`; `loadSkillCommands` also reads `user-invocable`. The skill's identifier (what `use_skill` and `/<skill-name>` take) is its directory name. A skill with no `SKILL.md` or an empty body is skipped.
A skill file begins with a YAML frontmatter block, followed by the body that holds the instructions. Discovery parses `description` and `disable-model-invocation`; `loadSkillCommands` also reads `user-invocable`. The skill's identifier (what `use_skill` and `/<skill-name>` take) is its directory name. A skill with no `SKILL.md` or an empty body is skipped.

| Field | Required | Description |
| ---------------- | ------------ | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | yes | One-line summary shown in the prompt's lazy skills listing and the slash picker |
| `name` | conventional | Conventionally matches the directory name; the directory name is what is actually used as the identifier |
| `user-invocable` | no | When `false`, `loadSkillCommands` skips slash synthesis; the skill remains `use_skill` only. Untagged skills still become slashes (marketplace BC) |
| Field | Required | Description |
| -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `description` | yes | One-line summary shown in the prompt's lazy skills listing and the slash picker |
| `name` | conventional | Conventionally matches the directory name; the directory name is what is actually used as the identifier |
| `user-invocable` | no | When `false`, `loadSkillCommands` skips slash synthesis; the skill remains `use_skill` only. Untagged skills still become slashes (marketplace BC) |
| `disable-model-invocation` | no | When `true`, `discoverSkills` omits the skill from the lazy listing (but still claims the name for first-wins). Explicit `resolveSkillBody` / `use_skill("name")` still loads the body. Does not affect slash emission. |

There are no `type` or `disable-model-invocation` fields required for model invocation — a skill body is plain instruction text. `argument-hint` on frontmatter is preserved for the slash picker (greyed arg guidance). Multi-step orchestration is a separate mechanism (see Workflows above), not a skill `type`.
There is no skill `type` field required for model invocation — a skill body is plain instruction text. Background libraries (e.g. `git-worktrees`) set both `user-invocable: false` and `disable-model-invocation: true` so they are absent from slash and listing, yet recipes can still `use_skill("git-worktrees")`. `argument-hint` on frontmatter is preserved for the slash picker (greyed arg guidance). Multi-step orchestration is a separate mechanism (see Workflows above), not a skill `type`.

#### Loading (model and operator)

Expand Down
12 changes: 8 additions & 4 deletions docs/PLUGINS.md
Original file line number Diff line number Diff line change
Expand Up @@ -309,10 +309,14 @@ shape.
become slashes (marketplace backward compatibility). Frontmatter
`argument-hint` is preserved so the TUI can show greyed arg guidance (e.g.
`/create-issue` → `[description] [--from-doc]`). This is an additional
surface: `discoverSkills` is unchanged, so the model can still auto-invoke any
skill via `use_skill` — including first-party recipes that are not operator
slashes (`dispatch`, `git-rebase`, `linear-issue-workflow`, `style`,
`philosophy`, `typescript`, `opsh`). The slash command is a direct user entry
surface: `discoverSkills` skips skills with `disable-model-invocation: true`
from the lazy listing (those stay loadable via explicit `use_skill` /
`resolveSkillBody`), so the model does not auto-suggest background libraries.
First-party recipes that are not operator slashes remain listed for
`use_skill` when they only set `user-invocable: false` (`dispatch`,
`git-rebase`, `linear-issue-workflow`, `style`, `philosophy`, `typescript`,
`opsh`). Background libs such as `git-worktrees` set both flags. The slash
command is a direct user entry
point on top.
- **First-party catalog.** `plugins/corbits-skills/` (id `corbits-skills`,
kind `command`, `defaultEnabled: true`) is the bundled skill catalog. Origin
Expand Down
31 changes: 31 additions & 0 deletions plugins/corbits-skills/skills/git-worktrees/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
---
name: git-worktrees
user-invocable: false
disable-model-invocation: true
description: Create a git worktree from origin/<default-branch> and tear it down. Background library — load via use_skill("git-worktrees"); absent from slash and use_skill listing.
---

# git-worktrees

Background recipe. Skywalker loads via `use_skill("git-worktrees")` and copies commands into an intern brief. Intern executes via `run_shell`. Skywalker does not run the git.

## Create from origin/<default-branch>

```bash
git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
git fetch origin
git worktree add ../worktree/<branch-name> -b <branch-name> origin/<default-branch>
```

Always base new branches on `origin/<default-branch>` (whatever the repository uses). After creating the worktree, intern `cd`s into it and installs local dependencies (`bun install` when the project uses Bun; otherwise follow developer docs). Worktrees do not share `node_modules`.

## Teardown

```bash
cd <path-to-main-repo>
git fetch origin
git worktree remove ../worktree/<branch-name>
git branch -d <branch-name>
```

If the worktree directory was already deleted: `git worktree prune`.
25 changes: 4 additions & 21 deletions plugins/corbits-skills/skills/linear-issue-workflow/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,23 +21,15 @@ If the scope is unclear, `ask_operator` before proceeding. Do not guess.

Read `branchName` from the issue (call `mcp__linear__get_issue` again if needed).

Spawn `task(agent="intern")` with this sequenced `run_shell` list copied into the brief. Intern executes; Skywalker does not run the git.
Load `use_skill("git-worktrees")`. Copy the create-from-origin/<default-branch> recipe into an intern brief (substitute `<branch-name>`). Spawn `task(agent="intern")`. Intern executes; Skywalker does not run the git.

```bash
git symbolic-ref refs/remotes/origin/HEAD | sed 's@^refs/remotes/origin/@@'
git fetch origin
git worktree add ../worktree/<branch-name> -b <branch-name> origin/<default-branch>
```

Always base new branches on `origin/<default-branch>` (whatever the repository uses). After creating the worktree, intern `cd`s into it and installs local dependencies from developer documentation. Worktrees do not share `node_modules`.

If intern fails, stop and `ask_operator`. If the operator rejects the issue before implementation, intern tears down the worktree (Phase 7 commands) rather than leaving it stranded.
If intern fails, stop and `ask_operator`. If the operator rejects the issue before implementation, intern tears down the worktree via the git-worktrees teardown recipe rather than leaving it stranded.

## Phase 3: Plan, attach, mark In Progress

1. Spawn `task(agent="explore")` if the codebase map is not already known. Brief it with the absolute worktree path (it must work there) and the issue: where changes go, existing patterns, related code.
2. Follow the `/implement` loop's greybeard step (Phase 4) for the approach. Present the plan to the operator and `ask_operator` whether to proceed. Do not start implementation until approved.
3. If the operator rejects the plan and the issue cannot be salvaged, intern tears down the worktree (Phase 7) rather than leaving it stranded.
3. If the operator rejects the plan and the issue cannot be salvaged, intern tears down the worktree via the git-worktrees teardown recipe rather than leaving it stranded.
4. Attach the plan to the Linear issue. **Do not post the plan as a comment** — comments are for discussion, not archives.

Spawn `task(agent="build")` with a mechanical brief to write the approved plan to the worktree's `tmp/plan-<ISSUE-ID>.md` (do not commit it). Intern captures byte size with `wc -c`. Primary then:
Expand Down Expand Up @@ -130,16 +122,7 @@ Phase 6 ends when the PR is open. Phase 7 runs **after the PR is merged** and **
2. Re-read the issue with `mcp__linear__get_issue`. Flip checkboxes the merged PR actually completed on `main` via `mcp__linear__save_issue`. Never check a box on intent.
3. `mcp__linear__save_comment` with PR URL, merge SHA, and CI-green confirmation. Short. Present-tense facts.
4. If every outcome checkbox is checked, set state to `Done` with `mcp__linear__save_issue`. Otherwise leave In Progress.
5. Only then intern cleans up:

```bash
cd <path-to-main-repo>
git fetch origin
git worktree remove ../worktree/<branch-name>
git branch -d <branch-name>
```

If the worktree directory was already deleted: `git worktree prune`.
5. Only then intern cleans up: load `use_skill("git-worktrees")` and copy the teardown recipe into an intern brief (substitute `<branch-name>` and `<path-to-main-repo>`).

## Linear MCP tool reference

Expand Down
4 changes: 2 additions & 2 deletions src/agent/codex-tool-proxies.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,8 +597,8 @@ describe("update_plan proxy", () => {
});

describe("allowDeleteFromCapabilities", () => {
test("docs allowlist (no delete_file) → false; build → true", () => {
expect(allowDeleteFromCapabilities({ mode: "allow", tools: DOCS_TOOLS })).toBe(false);
test("docs allowlist (includes delete_file) → true; build → true", () => {
expect(allowDeleteFromCapabilities({ mode: "allow", tools: DOCS_TOOLS })).toBe(true);
expect(allowDeleteFromCapabilities({ mode: "allow", tools: BUILD_TOOLS })).toBe(true);
expect(allowDeleteFromCapabilities(undefined)).toBe(true);
expect(allowDeleteFromCapabilities({ mode: "exclude", tools: ["run_shell"] })).toBe(true);
Expand Down
4 changes: 2 additions & 2 deletions src/agent/codex-tool-proxies.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,8 @@ export interface CreateCodexToolProxiesOpts {
runManageTasks: CodexRunManageTasks;
/**
* When false, Delete File and Update+Move refuse without calling `delete_file`.
* Defaults to true (implement / unconstrained). Docs leaves pass false because
* DOCS_TOOLS includes apply_patch but not delete_file.
* Defaults to true (implement / unconstrained). Pass false when the
* director allowlist omits delete_file (docs leaves mount it today).
*/
allowDelete?: boolean;
/**
Expand Down
1 change: 1 addition & 0 deletions src/agent/directors/brand-reviewer/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe("brandReviewerPackage", () => {
const allow = brandReviewerPackage.tools?.allow ?? [];
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("systemPrompt mentions DESIGN.md", () => {
Expand Down
1 change: 1 addition & 0 deletions src/agent/directors/bruckheimer/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("bruckheimerPackage", () => {
const allow = bruckheimerPackage.tools?.allow ?? [];
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is docs", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/critique/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,14 +54,14 @@ describe("critiquePackage", () => {
expect(critiquePackage.spawn.maySpawn).toBe(false);
});

test("tools.allow is review surface without product writes", () => {
test("tools.allow is review surface with product writes", () => {
const allow = critiquePackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).toContain("read_file");
expect(allow).not.toContain("use_skill");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is review", () => {
Expand Down
2 changes: 1 addition & 1 deletion src/agent/directors/critique/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ API contract check (blocking when brief specifies signatures):
- Prefer reading tests/callers; a tiny sync call via run_shell that would hang on a Promise is evidence.
- Rank these as blocking, not style nits.

Write tools are not mounted. Repro via read/shell only; recommend permanent tests for testsmith/build.
Write tools are mounted with no path lock — do not use them. Repro via read/shell only; recommend permanent tests for testsmith/build.

OUT OF LANE → refuse or reclassify under Blockers:
- implementing fixes (route to build)
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/draper/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ describe("draperPackage", () => {
expect(draperPackage.spawn.maySpawn).toBe(false);
});

test("tools.allow is review surface without product writes", () => {
test("tools.allow is review surface with product writes", () => {
const allow = draperPackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is review", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/emil/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ describe("emilPackage", () => {
expect(emilPackage.spawn.maySpawn).toBe(false);
});

test("tools.allow is review surface without product writes", () => {
test("tools.allow is review surface with product writes", () => {
const allow = emilPackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is review", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/explore/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,13 +38,13 @@ describe("explorePackage", () => {
expect(explorePackage.spawn.maySpawn).toBe(false);
});

test("tools.allow is read-only (no product writes)", () => {
test("tools.allow mounts product writes (lane: no product edits)", () => {
const allow = explorePackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).toContain("grep");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is explore", () => {
Expand Down
4 changes: 2 additions & 2 deletions src/agent/directors/explore/package.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { DirectorPackage } from "../types.js";
import { READ_TOOLS } from "../tool-sets.js";
import { REVIEW_TOOLS } from "../tool-sets.js";

export const explorePackage: DirectorPackage = {
id: "explore",
Expand All @@ -22,7 +22,7 @@ FINISH BIAS: Prefer one thorough pass then report. Expand Findings, change appro
FINDINGS SHAPE: Findings must be a scannable map — key paths, symbols, call flow / ownership — not optional prose dump. Cite paths. No drive-by refactors, no feature work, no review severity theater.

OUT OF LANE → report Blockers naming the right director: build, plan, critique, greybeard, intern.`,
tools: { allow: READ_TOOLS },
tools: { allow: REVIEW_TOOLS },
spawn: { maySpawn: false },
tier: "leaf",
modelRole: "explore",
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/gaasbot/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,12 +19,12 @@ describe("gaasbotPackage", () => {
expect(gaasbotPackage.spawn.maySpawn).toBe(false);
});

test("denies product write tools (advice only)", () => {
test("mounts product write tools (lane discipline in prompts)", () => {
const allow = gaasbotPackage.tools?.allow ?? [];
expect(allow).toContain("read_file");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is plan", () => {
Expand Down
8 changes: 4 additions & 4 deletions src/agent/directors/greybeard/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,13 +44,13 @@ describe("greybeardPackage", () => {
expect(greybeardPackage.systemPrompt).toMatch(/never spawn a parallel diagnostic fleet/i);
});

test("tools.allow is orchestrator surface without product writes", () => {
test("tools.allow is orchestrator surface with product writes", () => {
const allow = greybeardPackage.tools?.allow ?? [];
expect(allow).toContain("task");
expect(allow).toContain("search_agents");
expect(allow).not.toContain("write_file");
expect(allow).not.toContain("edit_file");
expect(allow).not.toContain("delete_file");
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
});

test("modelRole is review", () => {
Expand Down
7 changes: 5 additions & 2 deletions src/agent/directors/intern/package.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@ describe("internPackage", () => {
expect(internPackage.spawn.maySpawn).toBe(false);
});

test("tools.allow is shell-first minimal surface", () => {
test("tools.allow is shell-first with path writes", () => {
const allow = internPackage.tools?.allow ?? [];
expect(allow).toContain("run_shell");
expect(allow).toContain("read_file");
expect(allow).toContain("list_dir");
for (const name of ["write_file", "edit_file", "delete_file", "grep", "search_files", "task"]) {
expect(allow).toContain("write_file");
expect(allow).toContain("edit_file");
expect(allow).toContain("delete_file");
for (const name of ["grep", "search_files", "task", "apply_patch"]) {
expect(allow).not.toContain(name);
}
});
Expand Down
2 changes: 1 addition & 1 deletion src/agent/directors/intern/package.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { INTERN_TOOLS } from "../tool-sets.js";

/**
* Mechanical intern leaf (CL-5822).
* Shell/commands only — no judgment, no exploration, no product writes.
* Shell/commands first — no judgment, no exploration; path writes only when the brief requires them.
*/
export const internPackage: DirectorPackage = {
id: "intern",
Expand Down
Loading
Loading