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 README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ See [Quick Start guide](https://dyoshikawa.github.io/rulesync/getting-started/qu
| Kilo Code | kilo | ✅ 🌏 | ✅ | ✅ | ✅ 🌏 | | ✅ 🌏 | | |
| Roo Code | roo | ✅ | ✅ | ✅ | ✅ | 🎮 | ✅ 🌏 | | |
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
| Qwen Code | qwencode | ✅ | ✅ | | | | | | |
| Kiro | kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ |
| Google Antigravity | antigravity | ✅ | | | ✅ | | ✅ 🌏 | | |
Expand Down
3 changes: 3 additions & 0 deletions cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -257,6 +257,9 @@
"stylelintcache",
"sugarss",
"Tabnine",
"takt",
"TAKT",
"Takt",
"testignore",
"textextensions",
"tfstate",
Expand Down
10 changes: 10 additions & 0 deletions docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ antigravity: # antigravity specific parameters
trigger: "always_on" # always_on, glob, manual, or model_decision
globs: ["**/*"] # (optional) file patterns to match when trigger is "glob"
description: "When to apply this rule" # (optional) used with "model_decision" trigger
takt: # takt specific parameters (optional, plain Markdown only — frontmatter is dropped on emit)
facet: "policy" # (optional) "policy" (default), "knowledge", or "output-contract"
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

# Rulesync Project Overview
Expand Down Expand Up @@ -183,6 +186,8 @@ copilot: # copilot specific parameters (optional)
antigravity: # antigravity specific parameters
trigger: "/review" # Specific trigger for workflow (renames file to review.md)
turbo: true # (Optional, default: true) Append // turbo for auto-execution
takt: # takt specific parameters (optional; only `name` is honored — `facet` is fixed to "instruction")
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

target_pr = $ARGUMENTS
Expand Down Expand Up @@ -222,6 +227,8 @@ opencode: # for OpenCode-specific parameters
permission:
bash:
"git diff": allow
takt: # takt specific parameters (optional; only `name` is honored — `facet` is fixed to "persona")
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

You are the planner for any tasks.
Expand Down Expand Up @@ -253,6 +260,9 @@ claudecode: # for claudecode-specific parameters
disable-model-invocation: true # (optional) disable model invocation for this skill
codexcli: # for codexcli-specific parameters
short-description: A brief user-facing description
takt: # takt specific parameters (optional, plain Markdown only — frontmatter is dropped on emit)
facet: "instruction" # (optional) "instruction" (default), "knowledge", or "output-contract"
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

This is the skill body content.
Expand Down
1 change: 1 addition & 0 deletions docs/reference/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Kilo Code | kilo | ✅ 🌏 | ✅ | ✅ | ✅ 🌏 | | ✅ 🌏 | | |
| Roo Code | roo | ✅ | ✅ | ✅ | ✅ | 🎮 | ✅ 🌏 | | |
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
| Qwen Code | qwencode | ✅ | ✅ | | | | | | |
| Kiro | kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ |
| Google Antigravity | antigravity | ✅ | | | ✅ | | ✅ 🌏 | | |
Expand Down
47 changes: 47 additions & 0 deletions docs/tools/takt.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Takt

[Takt](https://github.com/dyoshikawa/takt) is a faceted-prompting AI coding workflow tool. Rulesync generates plain-Markdown facet files into Takt's `.takt/facets/` layout (or `~/.takt/facets/` in global mode).

## Output mapping

| Rulesync feature | Default facet directory | Allowed `takt.facet` overrides |
| ---------------- | ---------------------------- | ------------------------------------------------------- |
| `subagents` | `.takt/facets/personas/` | `persona` only (override is a no-op) |
| `rules` | `.takt/facets/policies/` | `policy` (default), `knowledge`, `output-contract` |
| `commands` | `.takt/facets/instructions/` | `instruction` only (override is a no-op) |
| `skills` | `.takt/facets/instructions/` | `instruction` (default), `knowledge`, `output-contract` |

The facet override is read from the rulesync source frontmatter under the `takt:` key:

```yaml
---
takt:
facet: knowledge # rules + skills only
name: my-renamed-stem
---
```

- `takt.facet` is **optional**; the per-feature default is used when absent.
- A disallowed value (for example `takt.facet: persona` on a rule) raises a hard validation error at `generate` time.
- `takt.name` is also optional and lets you rename the emitted filename stem to escape collisions.

Output files are **plain Markdown** — the source frontmatter is dropped entirely and the body is written verbatim. The filename stem of the source is preserved unless `takt.name` is set:

```
.rulesync/subagents/coder.md → .takt/facets/personas/coder.md
.rulesync/rules/style.md → .takt/facets/policies/style.md
.rulesync/commands/review.md → .takt/facets/instructions/review.md
.rulesync/skills/oncall/SKILL.md → .takt/facets/instructions/oncall.md
```

## Scope

Both project mode (`.takt/facets/...`) and global mode (`~/.takt/facets/...`) are supported.

## Filename collisions

Because commands and skills can both write to `.takt/facets/instructions/`, two source files that share a stem may collide. When this happens, Rulesync logs a warning naming both source files plus the conflicting target path and SKIPS writing both colliding files for that target — the rest of the run continues. Other (non-colliding) takt files are still written, and other targets are unaffected. Rename one of the colliding sources via `takt.name` to disambiguate.

## Importing existing TAKT files into rulesync

Reverse import (`rulesync import --targets takt`) is **not supported**. TAKT facet files are plain Markdown with no frontmatter, so the original skill / command / subagent metadata cannot be recovered. Attempting to import a TAKT skill raises a clear error rather than silently producing a stub that round-trips badly.
10 changes: 10 additions & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,9 @@ antigravity: # antigravity specific parameters
trigger: "always_on" # always_on, glob, manual, or model_decision
globs: ["**/*"] # (optional) file patterns to match when trigger is "glob"
description: "When to apply this rule" # (optional) used with "model_decision" trigger
takt: # takt specific parameters (optional, plain Markdown only — frontmatter is dropped on emit)
facet: "policy" # (optional) "policy" (default), "knowledge", or "output-contract"
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

# Rulesync Project Overview
Expand Down Expand Up @@ -183,6 +186,8 @@ copilot: # copilot specific parameters (optional)
antigravity: # antigravity specific parameters
trigger: "/review" # Specific trigger for workflow (renames file to review.md)
turbo: true # (Optional, default: true) Append // turbo for auto-execution
takt: # takt specific parameters (optional; only `name` is honored — `facet` is fixed to "instruction")
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

target_pr = $ARGUMENTS
Expand Down Expand Up @@ -222,6 +227,8 @@ opencode: # for OpenCode-specific parameters
permission:
bash:
"git diff": allow
takt: # takt specific parameters (optional; only `name` is honored — `facet` is fixed to "persona")
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

You are the planner for any tasks.
Expand Down Expand Up @@ -253,6 +260,9 @@ claudecode: # for claudecode-specific parameters
disable-model-invocation: true # (optional) disable model invocation for this skill
codexcli: # for codexcli-specific parameters
short-description: A brief user-facing description
takt: # takt specific parameters (optional, plain Markdown only — frontmatter is dropped on emit)
facet: "instruction" # (optional) "instruction" (default), "knowledge", or "output-contract"
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
---

This is the skill body content.
Expand Down
1 change: 1 addition & 0 deletions skills/rulesync/supported-tools.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ Rulesync supports both **generation** and **import** for All of the major AI cod
| Kilo Code | kilo | ✅ 🌏 | ✅ | ✅ | ✅ 🌏 | | ✅ 🌏 | | |
| Roo Code | roo | ✅ | ✅ | ✅ | ✅ | 🎮 | ✅ 🌏 | | |
| Rovodev (Atlassian) | rovodev | ✅ 🌏 | | 🌏 | | ✅ 🌏 | ✅ 🌏 | | |
| Takt | takt | ✅ 🌏 | | | ✅ 🌏 | ✅ 🌏 | ✅ 🌏 | | |
| Qwen Code | qwencode | ✅ | ✅ | | | | | | |
| Kiro | kiro | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | | ✅ |
| Google Antigravity | antigravity | ✅ | | | ✅ | | ✅ 🌏 | | |
Expand Down
31 changes: 25 additions & 6 deletions src/cli/commands/gitignore-entries.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,25 @@ const TARGETS_WITHOUT_GITIGNORE_ENTRIES = new Set([
]);

describe("GITIGNORE_ENTRY_REGISTRY", () => {
it("should have no duplicate entries", () => {
const entries = GITIGNORE_ENTRY_REGISTRY.map((tag) => tag.entry);
const unique = new Set(entries);
expect(entries.length).toBe(unique.size);
it("should have no duplicate entries within a single feature tag", () => {
// The registry intentionally allows the SAME entry to be registered under
// different feature tags (e.g. `.takt/facets/instructions/` is shared by
// both `commands` and `skills` for the takt target). The `resolveGitignoreEntries`
// writer dedupes the final output. What we want to forbid is the same
// (target, feature, entry) triple appearing twice.
const seen = new Set<string>();
const collisions: string[] = [];
for (const tag of GITIGNORE_ENTRY_REGISTRY) {
const targets = Array.isArray(tag.target) ? tag.target : [tag.target];
for (const target of targets) {
const key = `${target}::${tag.feature}::${tag.entry}`;
if (seen.has(key)) {
collisions.push(key);
}
seen.add(key);
}
}
expect(collisions).toEqual([]);
});

it("should cover all tool targets except intentionally excluded ones", () => {
Expand All @@ -45,8 +60,12 @@ describe("GITIGNORE_ENTRY_REGISTRY", () => {
});

describe("ALL_GITIGNORE_ENTRIES", () => {
it("should contain all entries from the registry", () => {
expect(ALL_GITIGNORE_ENTRIES.length).toBe(GITIGNORE_ENTRY_REGISTRY.length);
it("should contain every distinct entry from the registry", () => {
// The registry can register the same entry under multiple feature tags;
// `ALL_GITIGNORE_ENTRIES` is the deduplicated view, so its length matches
// the unique entry count rather than the raw registry length.
const distinctRegistryEntries = new Set(GITIGNORE_ENTRY_REGISTRY.map((tag) => tag.entry));
expect(ALL_GITIGNORE_ENTRIES.length).toBe(distinctRegistryEntries.size);
for (const tag of GITIGNORE_ENTRY_REGISTRY) {
expect(ALL_GITIGNORE_ENTRIES).toContain(tag.entry);
}
Expand Down
38 changes: 35 additions & 3 deletions src/cli/commands/gitignore-entries.ts
Original file line number Diff line number Diff line change
Expand Up @@ -223,6 +223,27 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [
},
{ target: "rovodev", feature: "skills", entry: "**/.agents/skills/" },

// TAKT
// The `knowledge/` and `output-contracts/` facets are shared between the
// `rules` and `skills` features (both can opt into them via `takt.facet`).
// Register them under both feature tags so the entry survives when the
// user enables only one of the two.
{ target: "takt", feature: "rules", entry: "**/.takt/facets/policies/" },
{ target: "takt", feature: "rules", entry: "**/.takt/facets/knowledge/" },
{ target: "takt", feature: "skills", entry: "**/.takt/facets/knowledge/" },
{ target: "takt", feature: "rules", entry: "**/.takt/facets/output-contracts/" },
{ target: "takt", feature: "skills", entry: "**/.takt/facets/output-contracts/" },
{ target: "takt", feature: "subagents", entry: "**/.takt/facets/personas/" },
// Both commands and skills emit into `.takt/facets/instructions/`; register
// under both features so enabling either one is enough to gitignore the dir.
// The gitignore writer dedupes the entries on output.
{ target: "takt", feature: "commands", entry: "**/.takt/facets/instructions/" },
{ target: "takt", feature: "skills", entry: "**/.takt/facets/instructions/" },
{ target: "takt", feature: "general", entry: "**/.takt/runs/" },
{ target: "takt", feature: "general", entry: "**/.takt/tasks/" },
{ target: "takt", feature: "general", entry: "**/.takt/.cache/" },
{ target: "takt", feature: "general", entry: "**/.takt/config.yaml" },

// Windsurf
{ target: "windsurf", feature: "skills", entry: "**/.windsurf/skills/" },
{ target: "windsurf", feature: "skills", entry: "**/.codeium/windsurf/skills/" },
Expand All @@ -232,9 +253,20 @@ export const GITIGNORE_ENTRY_REGISTRY: ReadonlyArray<GitignoreEntryTag> = [
{ target: "warp", feature: "rules", entry: "**/WARP.md" },
] as const;

export const ALL_GITIGNORE_ENTRIES: ReadonlyArray<string> = GITIGNORE_ENTRY_REGISTRY.map(
(tag) => tag.entry,
);
export const ALL_GITIGNORE_ENTRIES: ReadonlyArray<string> = (() => {
// The registry may register the SAME entry under multiple feature tags
// (e.g. `.takt/facets/instructions/` is shared by both `commands` and
// `skills` for the takt target). The exported list dedupes while
// preserving the original insertion order.
const seen = new Set<string>();
const result: string[] = [];
for (const tag of GITIGNORE_ENTRY_REGISTRY) {
if (seen.has(tag.entry)) continue;
seen.add(tag.entry);
result.push(tag.entry);
}
return result;
})();

type FilterGitignoreEntriesParams = {
readonly targets?: ReadonlyArray<string>;
Expand Down
5 changes: 5 additions & 0 deletions src/e2e/e2e-commands.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ describe("E2E: commands", () => {
{ target: "kiro", outputPath: join(".kiro", "prompts", "review-pr.md") },
{ target: "antigravity", outputPath: join(".agent", "workflows", "review-pr.md") },
{ target: "junie", outputPath: join(".junie", "commands", "review-pr.md") },
{ target: "takt", outputPath: join(".takt", "facets", "instructions", "review-pr.md") },
])("should generate $target commands", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -158,6 +159,10 @@ describe("E2E: commands (global mode)", () => {
{ target: "cline", outputPath: join("Documents", "Cline", "Workflows", "review-pr.md") },
{ target: "kilo", outputPath: join(".config", "kilo", "commands", "review-pr.md") },
{ target: "junie", outputPath: join(".junie", "commands", "review-pr.md") },
{
target: "takt",
outputPath: join(".takt", "facets", "instructions", "review-pr.md"),
},
])("should generate $target commands in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
2 changes: 2 additions & 0 deletions src/e2e/e2e-rules.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ This is a test rule for E2E testing.
{ target: "antigravity", outputPath: join(".agent", "rules", "overview.md") },
{ target: "augmentcode", outputPath: join(".augment", "rules", "overview.md") },
{ target: "windsurf", outputPath: join(".windsurf", "rules", "overview.md") },
{ target: "takt", outputPath: join(".takt", "facets", "policies", "overview.md") },
])("should generate $target rules (non-root)", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -262,6 +263,7 @@ describe("E2E: rules (global mode)", () => {
{ target: "factorydroid", outputPath: join(".factory", "AGENTS.md") },
{ target: "kilo", outputPath: join(".config", "kilo", "AGENTS.md") },
{ target: "rovodev", outputPath: join(".rovodev", "AGENTS.md") },
{ target: "takt", outputPath: join(".takt", "facets", "policies", "overview.md") },
])("should generate $target rules in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
8 changes: 8 additions & 0 deletions src/e2e/e2e-skills.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,10 @@ describe("E2E: skills", () => {
target: "agentsskills",
outputPath: join(".agents", "skills", "test-skill", "SKILL.md"),
},
{
target: "takt",
outputPath: join(".takt", "facets", "instructions", "test-skill.md"),
},
])("should generate $target skills", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -265,6 +269,10 @@ describe("E2E: skills (global mode)", () => {
target: "antigravity",
outputPath: join(".gemini", "antigravity", "skills", "test-skill", "SKILL.md"),
},
{
target: "takt",
outputPath: join(".takt", "facets", "instructions", "test-skill.md"),
},
])("should generate $target skills in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
5 changes: 5 additions & 0 deletions src/e2e/e2e-subagents.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ describe("E2E: subagents", () => {
target: "junie",
outputPath: join(".junie", "agents", "planner.md"),
},
{
target: "takt",
outputPath: join(".takt", "facets", "personas", "planner.md"),
},
])("should generate $target subagents", async ({ target, outputPath }) => {
const testDir = getTestDir();

Expand Down Expand Up @@ -229,6 +233,7 @@ describe("E2E: subagents (global mode)", () => {
{ target: "cursor", outputPath: join(".cursor", "agents", "planner.md") },
{ target: "opencode", outputPath: join(".config", "opencode", "agent", "planner.md") },
{ target: "rovodev", outputPath: join(".rovodev", "subagents", "planner.md") },
{ target: "takt", outputPath: join(".takt", "facets", "personas", "planner.md") },
])("should generate $target subagents in home directory", async ({ target, outputPath }) => {
const projectDir = getProjectDir();
const homeDir = getHomeDir();
Expand Down
Loading
Loading