Skip to content

ADR-008: Native Per-Tool Artifacts for AI Assistant Configuration

Field Value
Date 2026-07-20
Status Accepted
Deciders Holger Zahnleiter
Supersedes
Superseded by

Context and Problem Statement

Claude Code configuration (CLAUDE.md, .claude/agents/, .claude/commands/, .claude/skills/, .claude/rules/, .claude/settings.json) is generated through the capability registry described in ADR-007: dedicated Capability constants, project-type-aware, always on. GitHub Copilot, GitLab Duo, and Cursor support — selected via the optional, repeatable --assistant flag — deliberately sits outside that registry, on its own axis (internal.AssistantType), handled by a single finalizeExtras function (internal/templating/general_generator.go). Until now, each opted-in assistant received exactly one file: a hand-written, condensed digest of a subset of the guideline directives (.github/copilot-instructions.md, .cursor/rules/guidelines.md, .gitlab/duo_instructions.md) — no per-guideline scoped rules, no commands, no agents, no MCP config beyond the always-on .mcp.json. The digests duplicated content already mechanically extracted from specs/guidelines/*.md's ## Directives sections into .claude/rules/*.md, with no mechanism keeping the two in sync — the same "hand-maintained mapping with no drift protection" problem ADR-007 solved for the Language/ProjectType axis, recurring one level down for the assistant axis.

GitHub Copilot and Cursor have since grown native mechanisms that map reasonably well onto what Claude Code already provides for a generated project: scoped, glob-fronted instruction/rule files (.github/instructions/*.instructions.md, .cursor/rules/*.mdc), reusable prompt/command files (.github/prompts/*.prompt.md, .cursor/commands/*.md), and, for Copilot, custom chat modes (.github/chatmodes/*.chatmode.md) approximating an agent persona. Neither tool has a confirmed native equivalent to Claude Code's isolated-context subagents (context: fork skills backed by a .claude/agents/*.md definition), and neither has a repo-portable equivalent of .claude/settings.json's command-approval policy.

Decision Drivers

  • Single source of truth: a guideline's rule content must not be able to drift between .claude/rules/, .github/instructions/, and .cursor/rules/ — they describe the same rule
  • Respect the ADR-007 boundary: the assistant axis ([]AssistantType) is orthogonal to the (Language, ProjectType) axis the capability registry models; folding assistant selection into that registry would mix two independent axes into one table, the exact nested-map failure mode ADR-007 rejected in its Option 3, one level removed
  • Honesty about capability gaps: Copilot and Cursor genuinely lack an isolated-review mechanism and a repo-portable approval-policy format; generating a misleading artifact (or silently degrading feature parity without saying so) is worse than a documented gap
  • Incremental, reviewable delivery: two independently useful, independently revertible units of work (Copilot, then Cursor), not one large cross-cutting change

Considered Options

  1. Keep the status quo — one hand-maintained condensed digest per assistant
  2. Fold assistant selection into the (Language, ProjectType) capability registry as a third axis
  3. Keep assistant generation on its own axis (finalizeExtras), but generalize the guideline extraction engine so a single extraction feeds multiple tool-native rule targets, and add per-tool translations of the existing Claude commands/skills/agents ← chosen

Decision Outcome

Chosen option: Option 3. engine.GenerateSpec.RulesTargetDir (one target) became RuleTargets []RuleTarget (any number of targets, each with its own directory, filename, and frontmatter-rendering function). .claude/rules/*.md is one RuleTarget among what is now up to three; .github/instructions/*.instructions.md and .cursor/rules/*.mdc are the other two, selected by general.GuidelineRuleTargets reading project.Assistants — the same *internal.ProjectDescriptor every capability Action already receives, so no registry change was needed to reach it. All three are always extracted from the same rendered specs/guidelines/*.md content by the same extractDirectives; only the frontmatter shape differs per tool (paths: YAML list, applyTo: "<glob>", globs:/alwaysApply:).

Commands and skills are translated by dedicated functions (general.WriteCopilotPrompts, general.WriteCursorCommands) that render the existing Claude command/skill templates and mechanically strip/replace what doesn't carry over ($ARGUMENTS → Copilot's named ${input:argument} or dropped outright for Cursor's free-text argument style; the "start in plan mode" instruction; Claude-specific YAML frontmatter). review/security-review are generated in degraded form — the same checklist plus an explicit disclaimer that the isolation guarantee does not hold — rather than silently omitted, so users are not misled about parity but also are not left with strictly less. Chat modes (.github/chatmodes/) are the closest available Copilot approximation of the code-reviewer/security-auditor Claude agents, again with an explicit in-file note that they share conversation history rather than running isolated. Cursor has no confirmed equivalent, so no artifact is generated for it — Option 3 treats an honestly documented gap as an acceptable, and preferable, outcome to inventing a speculative file format.

Positive Consequences

  • .claude/rules/foo.md, .github/instructions/foo.instructions.md, and .cursor/rules/foo.mdc are now provably identical in body content (asserted directly in internal/templating/general/capabilities_test.go) — a guideline edit can never desync them
  • Adding a fourth assistant with a similar rule-file mechanism means one more RuleTarget branch in GuidelineRuleTargets and one more frontmatter function — no specs.go/generate.go change, mirroring the additive-extension property ADR-007 established for the other axis
  • The two commits (Copilot, then Cursor) were independently buildable, testable, and revertible; Cursor's commit touched no engine code, only additive wiring, confirming the mechanism actually generalized rather than merely relocating Copilot-specific logic
  • Documented gaps (.claude/settings.json equivalents, subagent isolation) live in the generated files themselves (copilot-instructions.md, .cursor/rules/guidelines.md), not only in this ADR or a commit message — a developer reads the gap where they'd look for the feature

Negative Consequences / Risks

  • The condensed root digests (copilot-instructions.md, .cursor/rules/guidelines.md) still exist as a second, smaller hand-maintained surface (project structure/architecture prose) — trimmed rather than eliminated, since that prose has no other generated source of truth to derive from; a future project-metadata capability could absorb it, not attempted here
  • Copilot chat-mode tool-restriction frontmatter keys were authored from best available knowledge of VS Code's schema, flagged in-file for verification, not from a live-tested integration — lower confidence than the rest of this change, which is exercised end-to-end via repo init
  • Degrading review/security-review instead of omitting them means Copilot/Cursor users get a prompt that reads like a full skill but performs worse than Claude's isolated version if the disclaimer is skimmed past — mitigated by placing the disclaimer as the first line of the file, not a footnote

Pros and Cons of the Options

Option 1 — Status quo (one condensed digest per assistant)

  • Pro: Simplest possible implementation; a single static file per assistant
  • Con: Duplicated, hand-maintained content with no drift protection against specs/guidelines/*.md changes — the exact problem ADR-007 solved one axis over
  • Con: No commands, skills, or agent approximation — large capability gap vs. Claude Code that neither tool's actual feature set justifies

Option 2 — Fold assistant selection into the capability registry

  • Pro: One dispatch mechanism instead of two (Registry + finalizeExtras)
  • Con: Mixes two independent axes ((Language, ProjectType) and []AssistantType) into one table — profiles would need Language × ProjectType × AssistantType membership, or the registry would need per-capability assistant-conditionals, undermining the "no Action branches on the profile axis" invariant ADR-007 established
  • Con: []AssistantType is a set (0 or more selected simultaneously), not a single enum value like Language/ProjectType — a poor fit for ProfileKey's single-value-per-axis shape

Option 3 — Own axis, generalized extraction, per-tool translation (chosen)

  • Pro: Reuses project.Assistants, already available to every capability Action, without registry changes
  • Pro: One extraction (extractDirectives), many frontmatter shapes — single source of truth by construction, not by convention
  • Pro: Degrade-with-disclaimer for gaps keeps the generated output honest without silently reducing functionality
  • Con: Two dispatch mechanisms remain (Registry for Language/ProjectType, finalizeExtras/GuidelineRuleTargets for assistants) — accepted as the correct reflection of two genuinely independent axes, not a shortcoming to fix
  • internal/templating/engine/{specs.go,generate.go,frontmatter.go}RuleTarget, the generalized extraction loop, and the three frontmatter builders
  • internal/templating/general/capabilities.goGuidelineRuleTargets, HasAssistant
  • internal/templating/general/{copilot.go,cursor.go} — command/skill translation
  • internal/templating/general_generator.gofinalizeExtras
  • specs/usage.md — user-facing description of what each --assistant value generates
  • ADR-007 — the capability-registry boundary this decision deliberately stays outside of