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
21 changes: 20 additions & 1 deletion docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,26 @@ Attention, again, you are just the planner, so though you can read any files and

> **Gemini CLI note (as of 2026-04-01):** Subagents are generated to `.gemini/agents/`. To enable the agents feature, set `"experimental": { "enableAgents": true }` in your `.gemini/settings.json`.

> **Kilo note (as of 2026-05-13):** Kilo's documented default for user-defined agents is `mode: all`, which makes the agent available both as a top-level pick and as a subagent. Set `kilo.mode: subagent` to opt into hidden/subagent-only behavior. Other supported fields include `displayName`, `temperature`, `top_p`, `model`, `permission`, `prompt`, `color`, `native`, `hidden`, `variant`, `disable`, `deprecated`, `steps`, and `options`.
> **Kilo note (as of 2026-05-13):** Kilo's documented default for user-defined agents is `mode: all`, which makes the agent available both as a top-level pick and as a subagent. Set `kilo.mode: subagent` to opt into hidden/subagent-only behavior.

Besides `mode`, the `kilo` subagent block accepts these optional fields (all preserved on round-trip):

| Field | Type | Notes |
| ------------- | --------------- | ------------------------------------ |
| `displayName` | string | Human-friendly name shown in pickers |
| `model` | string | Model id |
| `variant` | string | Model variant |
| `temperature` | number | Sampling temperature |
| `top_p` | number | Nucleus-sampling parameter |
| `permission` | string | Permission profile |
| `prompt` | string | Inline system prompt |
| `color` | string | UI color |
| `native` | boolean | Native (built-in) agent flag |
| `hidden` | boolean | Hide from top-level picker |
| `disable` | boolean | Disable the agent |
| `deprecated` | boolean | Mark as deprecated |
| `steps` | array of object | Ordered step definitions |
| `options` | object | Free-form key/value options |

## `.rulesync/skills/*/SKILL.md`

Expand Down
21 changes: 20 additions & 1 deletion skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -270,7 +270,26 @@ Attention, again, you are just the planner, so though you can read any files and

> **Gemini CLI note (as of 2026-04-01):** Subagents are generated to `.gemini/agents/`. To enable the agents feature, set `"experimental": { "enableAgents": true }` in your `.gemini/settings.json`.

> **Kilo note (as of 2026-05-13):** Kilo's documented default for user-defined agents is `mode: all`, which makes the agent available both as a top-level pick and as a subagent. Set `kilo.mode: subagent` to opt into hidden/subagent-only behavior. Other supported fields include `displayName`, `temperature`, `top_p`, `model`, `permission`, `prompt`, `color`, `native`, `hidden`, `variant`, `disable`, `deprecated`, `steps`, and `options`.
> **Kilo note (as of 2026-05-13):** Kilo's documented default for user-defined agents is `mode: all`, which makes the agent available both as a top-level pick and as a subagent. Set `kilo.mode: subagent` to opt into hidden/subagent-only behavior.

Besides `mode`, the `kilo` subagent block accepts these optional fields (all preserved on round-trip):

| Field | Type | Notes |
| ------------- | --------------- | ------------------------------------ |
| `displayName` | string | Human-friendly name shown in pickers |
| `model` | string | Model id |
| `variant` | string | Model variant |
| `temperature` | number | Sampling temperature |
| `top_p` | number | Nucleus-sampling parameter |
| `permission` | string | Permission profile |
| `prompt` | string | Inline system prompt |
| `color` | string | UI color |
| `native` | boolean | Native (built-in) agent flag |
| `hidden` | boolean | Hide from top-level picker |
| `disable` | boolean | Disable the agent |
| `deprecated` | boolean | Mark as deprecated |
| `steps` | array of object | Ordered step definitions |
| `options` | object | Free-form key/value options |

## `.rulesync/skills/*/SKILL.md`

Expand Down
52 changes: 52 additions & 0 deletions src/features/subagents/kilo-subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -460,4 +460,56 @@ Agent body`,
expect(result.data.options).toEqual({ key: "value" });
}
});

it("should round-trip the options and steps fields via fromRulesyncSubagent", () => {
const rulesyncSubagent = new RulesyncSubagent({
outputRoot: testDir,
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
relativeFilePath: "with-steps.md",
frontmatter: {
targets: ["kilo"],
name: "with-steps",
description: "Agent with options/steps",
kilo: {
options: { temperature: 0.2 },
steps: [{ name: "step1" }, { name: "step2" }],
},
},
body: "Body",
validate: false,
});

const fm = (
KiloSubagent.fromRulesyncSubagent({
rulesyncSubagent,
outputRoot: testDir,
relativeDirPath: RULESYNC_SUBAGENTS_RELATIVE_DIR_PATH,
}) as KiloSubagent
).getFrontmatter();
expect(fm.options).toEqual({ temperature: 0.2 });
expect(fm.steps).toEqual([{ name: "step1" }, { name: "step2" }]);
});

describe("forDeletion", () => {
it("should create a deletable placeholder with the default mode", () => {
const subagent = KiloSubagent.forDeletion({
outputRoot: testDir,
relativeDirPath: ".kilo/agents",
relativeFilePath: "obsolete.md",
});
expect(subagent.isDeletable()).toBe(true);
expect(subagent.getBody()).toBe("");
expect(subagent.getFrontmatter().mode).toBe("all");
});

it("should preserve the global flag passthrough (regression for #1639)", () => {
const subagent = KiloSubagent.forDeletion({
outputRoot: testDir,
relativeDirPath: join(".config", "kilo", "agents"),
relativeFilePath: "obsolete.md",
global: true,
});
expect((subagent as unknown as { global: boolean }).global).toBe(true);
});
});
});
34 changes: 25 additions & 9 deletions src/features/subagents/kilo-subagent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,12 @@ import {
ToolSubagentSettablePaths,
} from "./tool-subagent.js";

/** Default `mode` applied to Kilo subagents (single source of truth). */
export const KILO_DEFAULT_MODE = "all";

export const KiloSubagentFrontmatterSchema = z.looseObject({
description: z.optional(z.string()),
mode: z._default(z.string(), "all"),
mode: z._default(z.string(), KILO_DEFAULT_MODE),
name: z.optional(z.string()),
displayName: z.optional(z.string()),
deprecated: z.optional(z.boolean()),
Expand All @@ -32,8 +35,8 @@ export const KiloSubagentFrontmatterSchema = z.looseObject({
model: z.optional(z.string()),
variant: z.optional(z.string()),
prompt: z.optional(z.string()),
options: z.optional(z.looseObject({})),
steps: z.optional(z.array(z.looseObject({}))),
options: z.optional(z.record(z.string(), z.unknown())),
steps: z.optional(z.array(z.record(z.string(), z.unknown()))),
disable: z.optional(z.boolean()),
});
export type KiloSubagentFrontmatter = z.infer<typeof KiloSubagentFrontmatterSchema>;
Expand All @@ -45,13 +48,23 @@ export class KiloSubagent extends OpenCodeStyleSubagent {
declare protected readonly frontmatter: KiloSubagentFrontmatter;

constructor(params: KiloSubagentParams) {
super(params);
// Apply the Kilo schema (which also fills the `mode` default) up front so the
// stored frontmatter is normalized and `validate()` can stay side-effect-free.
// Kilo's schema is a strict superset of the parent's, so the parent's own
// validation is redundant — pass `validate: false` to skip it (the check has
// already happened here).
let frontmatter = params.frontmatter;
if (params.validate !== false) {
const result = this.validate();
const result = KiloSubagentFrontmatterSchema.safeParse(params.frontmatter);
if (!result.success) {
throw result.error;
throw new Error(
`Invalid frontmatter in ${join(params.relativeDirPath, params.relativeFilePath)}: ${formatError(result.error)}`,
);
}
frontmatter = result.data;
}

super({ ...params, frontmatter, validate: false });
}

protected getToolTarget(): Extract<ToolTarget, "opencode" | "kilo"> {
Expand All @@ -62,11 +75,14 @@ export class KiloSubagent extends OpenCodeStyleSubagent {
return this.frontmatter;
}

/**
* Pure validation (matches every sibling subagent): checks the stored
* frontmatter against the Kilo schema without mutating it. Default application
* happens in the constructor, not here.
*/
validate(): ValidationResult {
const result = KiloSubagentFrontmatterSchema.safeParse(this.frontmatter);
if (result.success) {
// @ts-expect-error - readonly
this.frontmatter = result.data;
return { success: true, error: null };
}

Expand Down Expand Up @@ -171,7 +187,7 @@ export class KiloSubagent extends OpenCodeStyleSubagent {
outputRoot,
relativeDirPath,
relativeFilePath,
frontmatter: { description: "", mode: "all" },
frontmatter: { description: "", mode: KILO_DEFAULT_MODE },
body: "",
fileContent: "",
validate: false,
Expand Down
22 changes: 22 additions & 0 deletions src/features/subagents/opencode-subagent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -243,4 +243,26 @@ Body content`,

expect(subagent.getFrontmatter().mode).toBe("all");
});

describe("forDeletion", () => {
it("should create a deletable placeholder", () => {
const subagent = OpenCodeSubagent.forDeletion({
outputRoot: testDir,
relativeDirPath: ".opencode/agents",
relativeFilePath: "obsolete.md",
});
expect(subagent.isDeletable()).toBe(true);
expect(subagent.getBody()).toBe("");
});

it("should preserve the global flag passthrough (regression for #1639)", () => {
const subagent = OpenCodeSubagent.forDeletion({
outputRoot: testDir,
relativeDirPath: join(".config", "opencode", "agents"),
relativeFilePath: "obsolete.md",
global: true,
});
expect((subagent as unknown as { global: boolean }).global).toBe(true);
});
});
});
Loading