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
2 changes: 1 addition & 1 deletion docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| GitHub Copilot CLI | copilotcli | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | |
| Goose | goose | ✅ 🌏 | ✅ | | | | | ✅ 🌏 | |
| Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| deepagents-cli | deepagents | | | ✅ 🌏 | | | ✅ | 🌏 | |
| deepagents-cli | deepagents | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | 🌏 | |
| Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| OpenCode | opencode | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| Cline | cline | ✅ 🌏 | ✅ | 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | ✅ |
Expand Down
2 changes: 1 addition & 1 deletion skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| GitHub Copilot CLI | copilotcli | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | |
| Goose | goose | ✅ 🌏 | ✅ | | | | | ✅ 🌏 | |
| Cursor | cursor | ✅ | ✅ | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| deepagents-cli | deepagents | | | ✅ 🌏 | | | ✅ | 🌏 | |
| deepagents-cli | deepagents | ✅ 🌏 | | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | 🌏 | |
| Factory Droid | factorydroid | ✅ 🌏 | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| OpenCode | opencode | ✅ 🌏 | | ✅ 🌏 🔧 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 |
| Cline | cline | ✅ 🌏 | ✅ | 🌏 | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | ✅ |
Expand Down
1 change: 1 addition & 0 deletions src/e2e/e2e-rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,7 @@ describe("E2E: rules (global mode)", () => {
{ target: "antigravity-cli", outputPath: join(".gemini", "GEMINI.md") },
{ target: "goose", outputPath: join(".config", "goose", ".goosehints") },
{ target: "copilotcli", outputPath: join(".copilot", "copilot-instructions.md") },
{ target: "deepagents", outputPath: join(".deepagents", "deepagents", "AGENTS.md") },
{ target: "factorydroid", outputPath: join(".factory", "AGENTS.md") },
{ target: "kilo", outputPath: join(".config", "kilo", "AGENTS.md") },
{ target: "rovodev", outputPath: join(".rovodev", "AGENTS.md") },
Expand Down
4 changes: 4 additions & 0 deletions src/e2e/e2e-skills.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -295,6 +295,10 @@ describe("E2E: skills (global mode)", () => {
target: "amp",
outputPath: join(".config", "agents", "skills", "test-skill", "SKILL.md"),
},
{
target: "deepagents",
outputPath: join(".deepagents", "deepagents", "skills", "test-skill", "SKILL.md"),
},
{
target: "codexcli",
outputPath: join(".agents", "skills", "test-skill", "SKILL.md"),
Expand Down
4 changes: 4 additions & 0 deletions src/e2e/e2e-subagents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,10 @@ describe("E2E: subagents (global mode)", () => {
{ target: "takt", outputPath: join(".takt", "facets", "personas", "planner.md") },
{ target: "factorydroid", outputPath: join(".factory", "droids", "planner.md") },
{ target: "cline", outputPath: join(".cline", "agents", "planner.yaml") },
{
target: "deepagents",
outputPath: join(".deepagents", "deepagents", "agents", "planner", "AGENTS.md"),
},
])("should generate $target subagents in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
27 changes: 27 additions & 0 deletions src/features/hooks/deepagents-hooks.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,33 @@ describe("DeepagentsHooks", () => {
expect(stopEntry.command).toEqual(["bash", "-c", "echo task done"]);
});

it("should map the canonical contextOffload event to dcode's context.offload", () => {
const rulesyncHooks = new RulesyncHooks({
outputRoot: testDir,
relativeDirPath: ".rulesync",
relativeFilePath: "hooks.json",
fileContent: JSON.stringify({
version: 1,
hooks: {
contextOffload: [{ type: "command", command: "echo offloaded" }],
},
}),
});

const hooks = DeepagentsHooks.fromRulesyncHooks({ outputRoot: testDir, rulesyncHooks });
const parsed = JSON.parse(hooks.getFileContent());

const offloadEntry = parsed.hooks.find((h: { events?: string[] }) =>
h.events?.includes("context.offload"),
);
expect(offloadEntry).toBeDefined();
expect(offloadEntry.command).toEqual(["bash", "-c", "echo offloaded"]);

// And it should round-trip back to the canonical name.
const roundTripped = JSON.parse(hooks.toRulesyncHooks().getFileContent());
expect(roundTripped.hooks.contextOffload).toBeDefined();
});

it("should map the canonical notification event to dcode input.required", () => {
const rulesyncHooks = new RulesyncHooks({
outputRoot: testDir,
Expand Down
6 changes: 6 additions & 0 deletions src/features/rules/deepagents-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,12 @@ describe("DeepagentsRule", () => {
const paths = DeepagentsRule.getSettablePaths();
expect(paths.nonRoot).toBeUndefined();
});

it("should return the user-level root path for global mode", () => {
const paths = DeepagentsRule.getSettablePaths({ global: true });
expect(paths.root.relativeDirPath).toBe(join(".deepagents", "deepagents"));
expect(paths.root.relativeFilePath).toBe("AGENTS.md");
});
});

describe("fromFile", () => {
Expand Down
39 changes: 25 additions & 14 deletions src/features/rules/deepagents-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,19 @@ export class DeepagentsRule extends ToolRule {
});
}

static getSettablePaths(
_options: {
global?: boolean;
excludeToolDir?: boolean;
} = {},
): DeepagentsRuleSettablePaths {
static getSettablePaths({
global = false,
}: {
global?: boolean;
excludeToolDir?: boolean;
} = {}): DeepagentsRuleSettablePaths {
// dcode reads user-level context from `~/.deepagents/<agent_name>/AGENTS.md`
// (default agent_name `deepagents`); the home directory is resolved by the
// processor through outputRoot in global mode. Project context lives in
// `<project>/.deepagents/AGENTS.md`.
return {
root: {
relativeDirPath: ".deepagents",
relativeDirPath: global ? join(".deepagents", "deepagents") : ".deepagents",
relativeFilePath: "AGENTS.md",
},
};
Expand All @@ -63,8 +67,9 @@ export class DeepagentsRule extends ToolRule {
// so the incoming `relativeFilePath` is ignored and the root file is read.
relativeFilePath: _relativeFilePath,
validate = true,
global = false,
}: ToolRuleFromFileParams): Promise<DeepagentsRule> {
const settablePaths = this.getSettablePaths();
const settablePaths = this.getSettablePaths({ global });
const relativePath = join(
settablePaths.root.relativeDirPath,
settablePaths.root.relativeFilePath,
Expand All @@ -86,7 +91,11 @@ export class DeepagentsRule extends ToolRule {
relativeDirPath,
relativeFilePath,
}: ToolRuleForDeletionParams): DeepagentsRule {
const isRoot = relativeFilePath === "AGENTS.md" && relativeDirPath === ".deepagents";
// The deepagents root file is always `AGENTS.md`, under `.deepagents`
// (project) or `.deepagents/deepagents` (global).
const isRoot =
relativeFilePath === "AGENTS.md" &&
(relativeDirPath === ".deepagents" || relativeDirPath === join(".deepagents", "deepagents"));

return new DeepagentsRule({
outputRoot,
Expand All @@ -102,14 +111,16 @@ export class DeepagentsRule extends ToolRule {
outputRoot = process.cwd(),
rulesyncRule,
validate = true,
global = false,
}: ToolRuleFromRulesyncRuleParams): DeepagentsRule {
const rootPath = this.getSettablePaths().root;
const rootPath = this.getSettablePaths({ global }).root;
const isRoot = rulesyncRule.getFrontmatter().root ?? false;

// deepagents reads project context only from the fixed `.deepagents/AGENTS.md`
// file. Both root and non-root rules therefore target that single path; the
// RulesProcessor folds the non-root bodies (`root: false`) into the root rule
// and drops the redundant non-root instances before writing.
// deepagents reads context only from the fixed root AGENTS.md file
// (`.deepagents/AGENTS.md` for project, `.deepagents/deepagents/AGENTS.md`
// for global). Both root and non-root rules therefore target that single
// path; the RulesProcessor folds the non-root bodies (`root: false`) into
// the root rule and drops the redundant non-root instances before writing.
return new DeepagentsRule({
outputRoot,
relativeDirPath: rootPath.relativeDirPath,
Expand Down
4 changes: 3 additions & 1 deletion src/features/rules/rules-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -900,6 +900,7 @@ Content that would fail parsing`;
"codexcli",
"copilot",
"copilotcli",
"deepagents",
"factorydroid",
"geminicli",
"goose",
Expand Down Expand Up @@ -941,6 +942,7 @@ Content that would fail parsing`;
expect(globalTargets).toContain("codexcli");
expect(globalTargets).toContain("copilot");
expect(globalTargets).toContain("copilotcli");
expect(globalTargets).toContain("deepagents");
expect(globalTargets).toContain("factorydroid");
expect(globalTargets).toContain("geminicli");
expect(globalTargets).toContain("junie");
Expand All @@ -952,7 +954,7 @@ Content that would fail parsing`;
expect(globalTargets).toContain("takt");
expect(globalTargets).toContain("devin");
expect(globalTargets).toContain("zed");
expect(globalTargets.length).toBe(21);
expect(globalTargets.length).toBe(22);

// These targets should NOT be in global mode
expect(globalTargets).not.toContain("cursor");
Expand Down
3 changes: 2 additions & 1 deletion src/features/rules/rules-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,8 @@ const toolRuleFactories = new Map<RulesProcessorToolTarget, ToolRuleFactory>([
class: DeepagentsRule,
meta: {
extension: "md",
supportsGlobal: false,
// dcode reads user-level context from `~/.deepagents/<agent_name>/AGENTS.md`.
supportsGlobal: true,
ruleDiscoveryMode: "auto",
},
},
Expand Down
4 changes: 2 additions & 2 deletions src/features/skills/deepagents-skill.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,9 +28,9 @@ describe("DeepagentsSkill", () => {
expect(paths.relativeDirPath).toBe(join(".deepagents", "skills"));
});

it("should return project path for global mode", () => {
it("should return the user-level path for global mode", () => {
const paths = DeepagentsSkill.getSettablePaths({ global: true });
expect(paths.relativeDirPath).toBe(join(".deepagents", "skills"));
expect(paths.relativeDirPath).toBe(join(".deepagents", "deepagents", "skills"));
});
});

Expand Down
9 changes: 7 additions & 2 deletions src/features/skills/deepagents-skill.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,9 +66,14 @@ export class DeepagentsSkill extends ToolSkill {
}
}

static getSettablePaths(_options?: { global?: boolean }): ToolSkillSettablePaths {
static getSettablePaths({ global = false }: { global?: boolean } = {}): ToolSkillSettablePaths {
// dcode discovers user-level skills in `~/.deepagents/<agent_name>/skills/`
// (default agent_name `deepagents`); the home directory is resolved by the
// processor through outputRoot in global mode.
return {
relativeDirPath: join(".deepagents", "skills"),
relativeDirPath: global
? join(".deepagents", "deepagents", "skills")
: join(".deepagents", "skills"),
};
}

Expand Down
2 changes: 2 additions & 0 deletions src/features/skills/skills-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -982,6 +982,7 @@ Content that would fail parsing`;
"cline",
"codexcli",
"cursor",
"deepagents",
"factorydroid",
"geminicli",
"junie",
Expand Down Expand Up @@ -1015,6 +1016,7 @@ Content that would fail parsing`;
"cline",
"codexcli",
"cursor",
"deepagents",
"factorydroid",
"geminicli",
"junie",
Expand Down
3 changes: 2 additions & 1 deletion src/features/skills/skills-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -216,8 +216,9 @@ const toolSkillFactories = new Map<SkillsProcessorToolTarget, ToolSkillFactory>(
[
"deepagents",
{
// dcode discovers user-level skills in `~/.deepagents/<agent_name>/skills/`.
class: DeepagentsSkill,
meta: { supportsProject: true, supportsSimulated: false, supportsGlobal: false },
meta: { supportsProject: true, supportsSimulated: false, supportsGlobal: true },
},
],
[
Expand Down
5 changes: 5 additions & 0 deletions src/features/subagents/deepagents-subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ describe("DeepagentsSubagent", () => {
const paths = DeepagentsSubagent.getSettablePaths();
expect(paths.relativeDirPath).toBe(join(".deepagents", "agents"));
});

it("should return the user-level path for global mode", () => {
const paths = DeepagentsSubagent.getSettablePaths({ global: true });
expect(paths.relativeDirPath).toBe(join(".deepagents", "deepagents", "agents"));
});
});

describe("constructor", () => {
Expand Down
11 changes: 9 additions & 2 deletions src/features/subagents/deepagents-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,9 +61,16 @@ export class DeepagentsSubagent extends ToolSubagent {
this.body = body;
}

static getSettablePaths(_options: { global?: boolean } = {}): ToolSubagentSettablePaths {
static getSettablePaths({
global = false,
}: { global?: boolean } = {}): ToolSubagentSettablePaths {
// dcode discovers user-level subagents in `~/.deepagents/<agent_name>/agents/`
// (default agent_name `deepagents`); the home directory is resolved by the
// processor through outputRoot in global mode.
return {
relativeDirPath: join(".deepagents", "agents"),
relativeDirPath: global
? join(".deepagents", "deepagents", "agents")
: join(".deepagents", "agents"),
};
}

Expand Down
1 change: 1 addition & 0 deletions src/features/subagents/subagents-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -963,6 +963,7 @@ Second global content`;
"codexcli",
"copilotcli",
"cursor",
"deepagents",
"factorydroid",
"geminicli",
"junie",
Expand Down
3 changes: 2 additions & 1 deletion src/features/subagents/subagents-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,8 @@ const toolSubagentFactories = new Map<SubagentsProcessorToolTarget, ToolSubagent
// https://github.com/langchain-ai/deepagents/blob/main/libs/code/deepagents_code/subagents.py
meta: {
supportsSimulated: false,
supportsGlobal: false,
// dcode discovers user-level subagents in `~/.deepagents/<agent_name>/agents/`.
supportsGlobal: true,
filePattern: join("*", "AGENTS.md"),
},
},
Expand Down
3 changes: 3 additions & 0 deletions src/types/hooks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@ export type HookEvent =
| "stop"
| "subagentStop"
| "preCompact"
| "contextOffload"
| "postToolUseFailure"
| "subagentStart"
| "beforeShellExecution"
Expand Down Expand Up @@ -189,6 +190,7 @@ export const DEEPAGENTS_HOOK_EVENTS: readonly HookEvent[] = [
"postToolUseFailure",
"stop",
"preCompact",
"contextOffload",
"notification",
];

Expand Down Expand Up @@ -568,6 +570,7 @@ export const CANONICAL_TO_DEEPAGENTS_EVENT_NAMES: Record<string, string> = {
postToolUseFailure: "tool.error",
stop: "task.complete",
preCompact: "context.compact",
contextOffload: "context.offload",
// dcode's human-in-the-loop interrupt; canonical `notification` is the closest fit.
notification: "input.required",
};
Expand Down
Loading