Skip to content
Merged
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
1 change: 1 addition & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@
"src/generated/**"
],
"words": [
"aaif",
"diffable",
"extensionless",
"LASTEXITCODE",
Expand Down
8 changes: 4 additions & 4 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -1056,17 +1056,17 @@ Goose configures MCP servers in two locations depending on scope:
- **Global (`--global`):** MCP servers are written as **extensions** in the shared user config `~/.config/goose/config.yaml`. The schema is non-standard, so Rulesync maps canonical MCP fields to Goose's: `command` → `cmd` (an array `command` folds its tail into `args`), `env` → `envs`, `url`/`httpUrl` → `uri`, and `disabled: true` → `enabled: false`. The `type` is derived — `command` ⇒ `stdio`, a remote `url` ⇒ `streamable_http` (or `sse` when the canonical `type` is `sse`). Each extension also carries its own `name`. A canonical server with no `command` and no `url` is **skipped with a warning** rather than written as a `stdio` extension with no `cmd`, which Goose cannot start. Generation merges the `extensions:` block into the existing `config.yaml`, preserving other Goose settings (model, provider, ...), and the file is never deleted. The `extensions:` map itself is co-owned: Goose's own `builtin`/`platform`/`frontend`/`inline_python` extensions (`developer`, `memory`, ...) live there alongside MCP servers and are **carried over untouched**, as is any entry Rulesync cannot read as an MCP server, while every entry it positively identifies as one (`stdio`/`streamable_http`/`sse`) is Rulesync-owned, so a server deleted from `.rulesync/.mcp.json` is retracted with a warning naming it. Import mirrors this: a non-MCP extension type is skipped with a warning instead of being imported as a server (importing a `builtin` used to strip the type that makes it work). This location supports **both stdio and remote** (http/sse) servers.
- **Project:** Goose v1.39.0+ discovers MCP extensions in **open plugins** at `<project>/.agents/plugins/<name>/.mcp.json` (and `~/.agents/plugins/<name>/.mcp.json` at user scope). Rulesync emits `.agents/plugins/rulesync/.mcp.json`, reusing the same `.agents/plugins/rulesync/` tree already used for Goose hooks. The manifest uses the **Claude-style** `{ "mcpServers": { "<name>": { "command", "args", "env", "cwd" } } }` shape. This manifest is **stdio-only** — it cannot express `url`/`headers`, so **remote (http/sse) servers are skipped with a warning** in project mode; sync them with `--global` to `~/.config/goose/config.yaml` instead. The `.mcp.json` manifest is owned by Rulesync and is deleted when no servers remain.

See the [Goose extensions docs](https://block.github.io/goose/docs/getting-started/using-extensions/) and [open-plugins MCP PR #9471](https://github.com/block/goose/pull/9471).
See the [Goose extensions docs](https://goose-docs.ai/docs/getting-started/using-extensions/) and [open-plugins MCP PR #9471](https://github.com/aaif-goose/goose/pull/9471).

### Goose-specific: commands as recipes, subagents as custom agents

Goose [recipes](https://block.github.io/goose/docs/guides/recipes/recipe-reference/) are reusable YAML workflow files. **Commands** map to top-level recipes at `.goose/recipes/<name>.yaml` (project) and `~/.config/goose/recipes/<name>.yaml` (global); the command body becomes the recipe `prompt`, `title` defaults to the file name and `description` to the rulesync `description` (falling back to `title`), `version` defaults to `1.0.0`, and any other recipe field round-trips through the rulesync `goose` section of a command.
Goose [recipes](https://goose-docs.ai/docs/guides/recipes/recipe-reference/) are reusable YAML workflow files. **Commands** map to top-level recipes at `.goose/recipes/<name>.yaml` (project) and `~/.config/goose/recipes/<name>.yaml` (global); the command body becomes the recipe `prompt`, `title` defaults to the file name and `description` to the rulesync `description` (falling back to `title`), `version` defaults to `1.0.0`, and any other recipe field round-trips through the rulesync `goose` section of a command.

A recipe on disk is not invocable as `/name` on its own: Goose resolves slash commands from the `slash_commands` list in the user config (`~/.config/goose/config.yaml`), whose entries are `{ command, recipe_path }` pairs. In **global mode** Rulesync therefore registers every generated recipe there. `recipe_path` is written as an **absolute** path, because Goose resolves it with a bare `PathBuf::from(...)` on this code path (the tilde expansion used by `goose run --recipe` does not apply, so a `~/…` registration would never resolve), and the command name is lowercased, because Goose lowercases the typed command and compares it against the stored value verbatim. There is no project-level registration surface upstream, so project-scope recipes must still be run with `goose run --recipe`.

The list is co-owned: entries whose `recipe_path` points outside `~/.config/goose/recipes/` — and sub-recipes under `recipes/subagents/` — are carried over untouched, while **every** entry pointing directly into that directory is Rulesync-owned and recomputed on each `--global` generate. That retracts a deleted command's registration and drops the key once nothing is registered, but it also means a slash command you registered yourself (via Goose's own UI or `goose recipe`) for a recipe living in that directory is removed on the next generate — keep such recipes elsewhere, or author them in `.rulesync/commands/`. Command names must be unique, contain no spaces, and must not shadow a built-in command such as `/recipe`, `/compact`, or `/help`; Rulesync does not check the built-in names for you. See the [slash-command mapping in the Goose source](https://github.com/block/goose/blob/main/crates/goose/src/slash_commands/recipe_slash_command.rs).
The list is co-owned: entries whose `recipe_path` points outside `~/.config/goose/recipes/` — and sub-recipes under `recipes/subagents/` — are carried over untouched, while **every** entry pointing directly into that directory is Rulesync-owned and recomputed on each `--global` generate. That retracts a deleted command's registration and drops the key once nothing is registered, but it also means a slash command you registered yourself (via Goose's own UI or `goose recipe`) for a recipe living in that directory is removed on the next generate — keep such recipes elsewhere, or author them in `.rulesync/commands/`. Command names must be unique, contain no spaces, and must not shadow a built-in command such as `/recipe`, `/compact`, or `/help`; Rulesync does not check the built-in names for you. See the [slash-command mapping in the Goose source](https://github.com/aaif-goose/goose/blob/main/crates/goose/src/slash_commands/recipe_slash_command.rs).

**Subagents** map to Goose's [custom agents](https://block.github.io/goose/docs/guides/context-engineering/custom-agents/) (v1.34.0+): Markdown files with `name` (required) / `description` / `model` frontmatter whose body is the agent instructions, invocable via `@name` or delegation. They are emitted to the goose-specific discovery dirs `.goose/agents/<name>.md` (project) and `~/.config/goose/agents/<name>.md` (global), so the output cannot collide with a future shared `.agents/agents/` target; `model` and unknown future fields round-trip through the rulesync `goose` subagent section. Earlier rulesync versions emitted subagents as sub-recipe YAML under `.goose/recipes/subagents/` — a location Goose's agent discovery never scans, so those files were inert; they are no longer generated (stale outputs stay gitignored but are not cleaned up automatically).
**Subagents** map to Goose's [custom agents](https://goose-docs.ai/docs/guides/context-engineering/custom-agents/) (v1.34.0+): Markdown files with `name` (required) / `description` / `model` frontmatter whose body is the agent instructions, invocable via `@name` or delegation. They are emitted to the goose-specific discovery dirs `.goose/agents/<name>.md` (project) and `~/.config/goose/agents/<name>.md` (global), so the output cannot collide with a future shared `.agents/agents/` target; `model` and unknown future fields round-trip through the rulesync `goose` subagent section. Earlier rulesync versions emitted subagents as sub-recipe YAML under `.goose/recipes/subagents/` — a location Goose's agent discovery never scans, so those files were inert; they are no longer generated (stale outputs stay gitignored but are not cleaned up automatically).

### Vibe-specific: stdio `cwd` and MCP `[auth]` block

Expand Down
14 changes: 7 additions & 7 deletions src/constants/goose-paths.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ export const GOOSE_GLOBAL_DIR = join(".config", "goose");
export const GOOSE_RULE_FILE_NAME = ".goosehints";
export const GOOSE_MCP_FILE_NAME = "config.yaml";
// Goose persists per-tool permission overrides in the global user config dir.
// https://github.com/block/goose/blob/main/crates/goose/src/config/permission.rs
// https://github.com/aaif-goose/goose/blob/main/crates/goose/src/config/permission.rs
export const GOOSE_PERMISSIONS_FILE_NAME = "permission.yaml";
export const GOOSE_HOOKS_DIR_PATH = join(".agents", "plugins", "rulesync", "hooks");
export const GOOSE_HOOKS_FILE_NAME = "hooks.json";
Expand All @@ -16,8 +16,8 @@ export const GOOSE_HOOKS_FILE_NAME = "hooks.json";
// `{ "mcpServers": { "<name>": { command, args, env, cwd } } }`. The manifest is
// stdio-only (no `url`/`headers`). rulesync reuses the same `.agents/plugins/rulesync/`
// tree already used for Goose hooks.
// @see https://github.com/block/goose/pull/9471
// @see https://github.com/block/goose/releases/tag/v1.39.0
// @see https://github.com/aaif-goose/goose/pull/9471
// @see https://github.com/aaif-goose/goose/releases/tag/v1.39.0
export const GOOSE_PLUGIN_MCP_DIR_PATH = join(".agents", "plugins", "rulesync");
export const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";

Expand All @@ -33,8 +33,8 @@ export const GOOSE_PLUGIN_MCP_FILE_NAME = ".mcp.json";
// `.agents/skills/` location is already the canonical Goose skill target via
// `agentsskills`, and migrating the dedicated `goose` target would only
// duplicate that output. See the agentsskills target for the recommended path.
// @see https://block.github.io/goose/docs/mcp/skills-mcp/
// @see https://block.github.io/goose/docs/guides/context-engineering/using-skills/
// @see https://goose-docs.ai/docs/mcp/skills-mcp/
// @see https://goose-docs.ai/docs/guides/context-engineering/using-skills/
export const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");

// Recipes are reusable YAML workflow files. Goose discovers project recipes in
Expand All @@ -43,12 +43,12 @@ export const GOOSE_SKILLS_DIR_PATH = join(GOOSE_DIR, "skills");
// files under the `subagents/` subdirectory (referenced from a parent recipe via
// a relative `path`). Keeping subagents in a subdirectory makes the command and
// subagent file sets disjoint so import/orphan-deletion never cross over.
// @see https://block.github.io/goose/docs/guides/recipes/recipe-reference/
// @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
export const GOOSE_RECIPES_DIR_PATH = join(GOOSE_DIR, "recipes");
export const GOOSE_GLOBAL_RECIPES_DIR_PATH = join(GOOSE_GLOBAL_DIR, "recipes");
// Goose custom agents (Markdown + YAML frontmatter, v1.34.0+): the
// goose-specific discovery dirs are used so the output does not collide with a
// future shared `.agents/agents/` target.
// @see https://block.github.io/goose/docs/guides/context-engineering/custom-agents/
// @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
export const GOOSE_AGENTS_DIR_PATH = join(GOOSE_DIR, "agents");
export const GOOSE_GLOBAL_AGENTS_DIR_PATH = join(GOOSE_GLOBAL_DIR, "agents");
4 changes: 2 additions & 2 deletions src/features/commands/goose-command.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,7 @@ const RECIPE_VERSION = "1.0.0";
//
// The registration surface exists at user scope only — there is no project-level
// `slash_commands` list — so it is written in global mode only.
// @see https://github.com/block/goose/blob/main/crates/goose/src/slash_commands/recipe_slash_command.rs
// @see https://github.com/aaif-goose/goose/blob/main/crates/goose/src/slash_commands/recipe_slash_command.rs
const SLASH_COMMANDS_KEY = "slash_commands";
const GOOSE_GLOBAL_RECIPES_POSIX_DIR = toPosixPath(GOOSE_GLOBAL_RECIPES_DIR_PATH);

Expand Down Expand Up @@ -180,7 +180,7 @@ class GooseCommandConfigFile extends ToolFile {
* The whole file is a YAML mapping (not frontmatter + markdown body), so the
* class stores the parsed recipe object rather than a frontmatter/body split.
*
* @see https://block.github.io/goose/docs/guides/recipes/recipe-reference/
* @see https://goose-docs.ai/docs/guides/recipes/recipe-reference/
*/
const GooseCommandRecipeSchema = z.looseObject({
version: z.optional(z.string()),
Expand Down
2 changes: 1 addition & 1 deletion src/features/hooks/goose-hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ const GOOSE_CONVERTER_CONFIG: ToolHooksConverterConfig = {
*
* The JSON shape matches Claude Code's: each PascalCase event maps to an array of
* `{ matcher, hooks: [{ type: "command", command }] }` entries.
* @see https://block.github.io/goose/docs/guides/context-engineering/hooks/
* @see https://goose-docs.ai/docs/guides/context-engineering/hooks/
*/
export class GooseHooks extends ToolHooks {
constructor(params: AiFileParams) {
Expand Down
4 changes: 2 additions & 2 deletions src/features/mcp/goose-mcp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ function convertToGoosePluginMcpServers(
* shape and cannot express `url`/`headers`, so remote servers are skipped with
* a warning in project mode (use `--global` to sync them instead).
*
* @see https://block.github.io/goose/docs/getting-started/using-extensions/
* @see https://github.com/block/goose/pull/9471
* @see https://goose-docs.ai/docs/getting-started/using-extensions/
* @see https://github.com/aaif-goose/goose/pull/9471
*/
export class GooseMcp extends ToolMcp {
private readonly config: Record<string, unknown>;
Expand Down
4 changes: 2 additions & 2 deletions src/features/mcp/mcp-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,8 +312,8 @@ export const toolMcpFactories = new Map<McpProcessorToolTarget, ToolMcpFactory>(
// `~/.config/goose/config.yaml`, and (since v1.39.0) discovers stdio-only
// MCP extensions in open plugins at project scope
// `.agents/plugins/rulesync/.mcp.json` (Claude-style `mcpServers`).
// https://block.github.io/goose/docs/getting-started/using-extensions/
// https://github.com/block/goose/pull/9471
// https://goose-docs.ai/docs/getting-started/using-extensions/
// https://github.com/aaif-goose/goose/pull/9471
class: GooseMcp,
meta: {
supportsProject: true,
Expand Down
4 changes: 2 additions & 2 deletions src/features/permissions/goose-permissions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ const GOOSE_GLOBAL_ONLY_MESSAGE =
// Goose stores user-set permission decisions under the top-level `user` key of
// permission.yaml; other keys (e.g. `smart_approve`) hold cached LLM/annotation
// decisions and are preserved verbatim.
// https://github.com/block/goose/blob/main/crates/goose/src/config/permission.rs
// https://github.com/aaif-goose/goose/blob/main/crates/goose/src/config/permission.rs
const GOOSE_USER_KEY = "user";

// The catch-all rulesync pattern. Goose permission lists hold whole tool names
Expand All @@ -35,7 +35,7 @@ const CATCH_ALL_PATTERN = "*";

// Goose's built-in Developer extension tools are namespaced `extension__tool`.
// rulesync's canonical categories map onto the matching Developer tool name.
// https://block.github.io/goose/docs/mcp/developer-mcp/
// https://goose-docs.ai/docs/mcp/developer-mcp/
const RULESYNC_TO_GOOSE_TOOL_NAME: Record<string, string> = {
bash: "developer__shell",
edit: "developer__text_editor",
Expand Down
4 changes: 2 additions & 2 deletions src/features/rules/goose-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export type GooseRuleSettablePathsGlobal = ToolRuleSettablePathsGlobal;
* touches during a session. The separate `.goose/memories/` tree is the Memory
* extension's storage and is NOT auto-loaded as session context.
* (Verified against the official docs:
* https://block.github.io/goose/docs/guides/context-engineering/using-goosehints/)
* https://goose-docs.ai/docs/guides/context-engineering/using-goosehints/)
*
* rulesync's topic-based non-root rules have no project subdirectory to map onto,
* so writing them under `.goose/memories/` made them effectively invisible to
Expand All @@ -45,7 +45,7 @@ export type GooseRuleSettablePathsGlobal = ToolRuleSettablePathsGlobal;
* Goose uses plain markdown files (.goosehints) without frontmatter.
*
* Global scope emits only `~/.config/goose/.goosehints`. Goose v1.41.0 (PR
* block/goose#9736) additionally loads the vendor-neutral
* aaif-goose/goose#9736) additionally loads the vendor-neutral
* `~/.agents/AGENTS.md` alongside the config-dir hints, but rulesync
* deliberately does not emit that shared path from the goose target: the
* config-dir hints remain fully loaded (no capability loss), and the
Expand Down
2 changes: 1 addition & 1 deletion src/features/skills/goose-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ import {

const GooseSkillFrontmatterSchema = z.looseObject({
// Goose SKILL.md documents only `name` and `description` as required fields.
// https://block.github.io/goose/docs/guides/context-engineering/using-skills/
// https://goose-docs.ai/docs/guides/context-engineering/using-skills/
// Any additional fields (e.g. `metadata`) pass through via z.looseObject.
name: z.string(),
description: z.string(),
Expand Down
2 changes: 1 addition & 1 deletion src/features/subagents/goose-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ import {
* `sub_recipes` list, which rulesync never wrote) — so those files were inert.
* The custom-agent surface is the one Goose actually reads.
*
* @see https://block.github.io/goose/docs/guides/context-engineering/custom-agents/
* @see https://goose-docs.ai/docs/guides/context-engineering/custom-agents/
*/
export const GooseSubagentFrontmatterSchema = z.looseObject({
name: z.string(),
Expand Down
Loading
Loading