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 docs/reference/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ augmentcode: # augmentcode specific parameters
takt: # takt specific parameters (optional; emitted under .takt/facets/policies/ — frontmatter is dropped on emit)
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
extends: "base" # (optional) emit a leading `{extends:<parent>}` facet-inheritance directive (Takt 0.39.0+)
facet: "output-contracts" # (optional) "policies" (default) or "output-contracts": redirect this rule to Takt's output-structure/report-template facet
---

# Rulesync Project Overview
Expand Down
34 changes: 19 additions & 15 deletions docs/tools/takt.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,34 +4,38 @@

## Output mapping

Each rulesync feature maps one-to-one onto a dedicated Takt facet directory. There is **no `takt.facet` override** — the target directory is fixed per feature.
Each rulesync feature maps onto a dedicated Takt facet directory. The target directory is fixed per feature, except that **rules** may opt into Takt's fifth facet — `output-contracts` — via the `takt.facet` override (see below).

| Rulesync feature | Takt facet directory |
| ---------------- | ---------------------------- |
| `rules` | `.takt/facets/policies/` |
| `commands` | `.takt/facets/instructions/` |
| `subagents` | `.takt/facets/personas/` |
| `skills` | `.takt/facets/knowledge/` |
| Rulesync feature | Takt facet directory |
| ---------------- | --------------------------------------------------------------------------------------- |
| `rules` | `.takt/facets/policies/` (default) or `.takt/facets/output-contracts/` via `takt.facet` |
| `commands` | `.takt/facets/instructions/` |
| `subagents` | `.takt/facets/personas/` |
| `skills` | `.takt/facets/knowledge/` |

The only Takt-specific frontmatter knob is `takt.name`, which renames the emitted filename stem:
Takt-specific frontmatter knobs:

```yaml
---
takt:
name: my-renamed-stem
name: my-renamed-stem # rename the emitted filename stem
extends: base # emit a leading {extends:base} facet-inheritance directive
facet: output-contracts # "policies" (default) or "output-contracts"
---
```

- `takt.name` is **optional**; the source filename stem is used by default.
- Unsafe values (path separators, `..` segments, etc.) raise a hard validation error at `generate` time.
- `takt.name` is **optional**; the source filename stem is used by default. Unsafe values (path separators, `..` segments, etc.) raise a hard validation error at `generate` time.
- `takt.facet` is **optional** and defaults to `policies`. Setting it to `output-contracts` redirects the rule to Takt's output-structure / report-template facet, which has no dedicated rulesync feature. Both `policies` and `output-contracts` support `{extends:...}` inheritance. The other facets (`instructions`, `personas`, `knowledge`) are owned by the commands, subagents, and skills features and are not selectable via `takt.facet`.
- Like `takt.name` and `takt.extends`, `takt.facet` is a generate-side authoring control. Because Takt facet files are plain Markdown with no frontmatter, the facet selection cannot be recovered on import (see [Importing](#importing-existing-takt-files-into-rulesync) below).

Output files are **plain Markdown** — the source frontmatter is dropped entirely and the body is written verbatim:

```
.rulesync/rules/style.md → .takt/facets/policies/style.md
.rulesync/commands/review.md → .takt/facets/instructions/review.md
.rulesync/subagents/coder.md → .takt/facets/personas/coder.md
.rulesync/skills/oncall/SKILL.md → .takt/facets/knowledge/oncall.md
.rulesync/rules/style.md → .takt/facets/policies/style.md
.rulesync/rules/review-format.md → .takt/facets/output-contracts/review-format.md (with takt.facet: output-contracts)
.rulesync/commands/review.md → .takt/facets/instructions/review.md
.rulesync/subagents/coder.md → .takt/facets/personas/coder.md
.rulesync/skills/oncall/SKILL.md → .takt/facets/knowledge/oncall.md
```

## Scope
Expand Down
1 change: 1 addition & 0 deletions skills/rulesync/file-formats.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ augmentcode: # augmentcode specific parameters
takt: # takt specific parameters (optional; emitted under .takt/facets/policies/ — frontmatter is dropped on emit)
name: "renamed-stem" # (optional) override the emitted filename stem (no path separators or "..")
extends: "base" # (optional) emit a leading `{extends:<parent>}` facet-inheritance directive (Takt 0.39.0+)
facet: "output-contracts" # (optional) "policies" (default) or "output-contracts": redirect this rule to Takt's output-structure/report-template facet
---

# Rulesync Project Overview
Expand Down
26 changes: 26 additions & 0 deletions src/e2e/e2e-takt.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,30 @@ Skill body for review.
await readFileContent(join(testDir, ".takt", "facets", "knowledge", "review.md")),
).toContain("Skill body for review.");
});

it("redirects a rule to the output-contracts facet via takt.facet", async () => {
const testDir = getTestDir();

await writeFileContent(
join(testDir, RULESYNC_RULES_RELATIVE_DIR_PATH, "review-format.md"),
`---
targets: ["*"]
takt:
facet: output-contracts
---
Review output contract body.
`,
);

await runGenerate({
target: "takt",
features: "rules",
});

expect(
await readFileContent(
join(testDir, ".takt", "facets", "output-contracts", "review-format.md"),
),
).toContain("Review output contract body.");
});
});
5 changes: 5 additions & 0 deletions src/features/rules/rulesync-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ export const RulesyncRuleFrontmatterSchema = z.object({
// Facet inheritance: emit a leading `{extends:<parent>}` directive (Takt 0.39.0+).
// Rules map to the `policies` facet, which supports inheritance.
extends: z.optional(z.string()),
// Redirect the rule to a different writable Takt facet. Rules default to the
// `policies` facet; set `facet: "output-contracts"` to author an output-contract
// facet (output structure / report templates) instead. Both facets support
// `{extends:...}` inheritance. See docs/reference/file-formats.md.
facet: z.optional(z.enum(["policies", "output-contracts"])),
}),
),
});
Expand Down
55 changes: 55 additions & 0 deletions src/features/rules/takt-rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,61 @@ describe("TaktRule", () => {
/Invalid takt\.extends/,
);
});

it("redirects the rule to the output-contracts facet when takt.facet is set", () => {
const rulesyncRule = new RulesyncRule({
outputRoot: testDir,
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
relativeFilePath: "review-format.md",
frontmatter: {
targets: ["*"],
takt: { facet: "output-contracts" },
},
body: "# Review format",
});

const rule = TaktRule.fromRulesyncRule({ outputRoot: testDir, rulesyncRule });
expect(rule.getRelativeDirPath()).toBe(join(".takt", "facets", "output-contracts"));
expect(rule.getRelativeFilePath()).toBe("review-format.md");
expect(rule.getFileContent()).toBe("# Review format");
});

it("defaults to the policies facet when takt.facet is policies or absent", () => {
const explicit = new RulesyncRule({
outputRoot: testDir,
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
relativeFilePath: "style.md",
frontmatter: {
targets: ["*"],
takt: { facet: "policies" },
},
body: "x",
});
expect(
TaktRule.fromRulesyncRule({
outputRoot: testDir,
rulesyncRule: explicit,
}).getRelativeDirPath(),
).toBe(join(".takt", "facets", "policies"));
});

it("composes takt.facet with takt.name and takt.extends", () => {
const rulesyncRule = new RulesyncRule({
outputRoot: testDir,
relativeDirPath: RULESYNC_RULES_RELATIVE_DIR_PATH,
relativeFilePath: "source.md",
frontmatter: {
targets: ["*"],
takt: { facet: "output-contracts", name: "report", extends: "base" },
},
body: "# Report contract",
});

const rule = TaktRule.fromRulesyncRule({ outputRoot: testDir, rulesyncRule });
expect(rule.getRelativeDirPath()).toBe(join(".takt", "facets", "output-contracts"));
expect(rule.getRelativeFilePath()).toBe("report.md");
expect(rule.getFileContent()).toBe("{extends:base}\n\n# Report contract");
});
});

describe("fromFile", () => {
Expand Down
42 changes: 34 additions & 8 deletions src/features/rules/takt-rule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,13 +17,32 @@ import {
} from "./tool-rule.js";

/**
* Fixed facet directory for TAKT rule files.
* Default facet directory for TAKT rule files.
*
* Rulesync rules map one-to-one to TAKT's `policies/` facet. No override
* is supported; the directory is always `.takt/facets/policies/`.
* Rulesync rules map to TAKT's `policies/` facet by default. The source
* frontmatter may redirect a rule to another writable facet via `takt.facet`
* (currently `policies` or `output-contracts`); see {@link TAKT_RULE_FACETS}.
*/
export const DEFAULT_TAKT_RULE_DIR = "policies";

/**
* Writable Takt facets a rulesync rule may target via `takt.facet`.
*
* `policies` is the default. `output-contracts` lets users author Takt's
* output-structure / report-template facet (which has no dedicated rulesync
* feature) through the rules feature. Both are plain-Markdown facets that
* support `{extends:...}` inheritance. Other facets (`personas`,
* `instructions`, `knowledge`) are owned by the subagents, commands, and skills
* features respectively and are intentionally not selectable here.
*/
export const TAKT_RULE_FACETS = ["policies", "output-contracts"] as const;

export type TaktRuleFacet = (typeof TAKT_RULE_FACETS)[number];

function resolveTaktRuleFacet(facet: unknown): TaktRuleFacet {
return facet === "output-contracts" ? "output-contracts" : DEFAULT_TAKT_RULE_DIR;
}

export type TaktRuleParams = Omit<ToolRuleParams, "fileContent"> & {
body: string;
};
Expand All @@ -32,12 +51,18 @@ export type TaktRuleParams = Omit<ToolRuleParams, "fileContent"> & {
* Rule generator for TAKT (https://github.com/nrslib/takt).
*
* TAKT organizes prompts into faceted directories under `.takt/facets/`.
* Rulesync rules always map to TAKT's `policies/` facet; the source
* frontmatter may rename the emitted stem via `takt.name`, but the facet
* directory is fixed.
* Rulesync rules map to TAKT's `policies/` facet by default; the source
* frontmatter may rename the emitted stem via `takt.name` and redirect the rule
* to another writable facet via `takt.facet` (`policies` or `output-contracts`).
* `output-contracts` lets users author Takt's output-structure / report-template
* facet — which has no dedicated rulesync feature — through the rules feature.
*
* The emitted files are plain Markdown — frontmatter is always dropped, and
* the body is written verbatim.
* the body is written verbatim. Like `takt.name` and `takt.extends`, the
* `takt.facet` selection is a generate-side authoring control and is not
* reconstructed on import (Takt facet files carry no frontmatter to recover it
* from). Import only scans the default `.takt/facets/policies/` facet and yields
* plain rules; files under `.takt/facets/output-contracts/` are not imported.
*/
export class TaktRule extends ToolRule {
static getSettablePaths({
Expand Down Expand Up @@ -130,7 +155,8 @@ export class TaktRule extends ToolRule {
assertSafeTaktName({ name: stem, featureLabel: "rule", sourceLabel });
const relativeFilePath = `${stem}.md`;

const relativeDirPath = join(".takt", "facets", DEFAULT_TAKT_RULE_DIR);
const facet = resolveTaktRuleFacet(taktSection?.facet);
const relativeDirPath = join(".takt", "facets", facet);

const body = prependTaktExtends({
extendsName: typeof taktSection?.extends === "string" ? taktSection.extends : undefined,
Expand Down
Loading