A harness is everything that shapes how coding agents behave in a repository: rules, agents, commands, skills, hooks and MCP servers. Craftar addresses two problems with it: keeping one harness consistent across many teams that use different AI coders (P1), and keeping one developer's many workspaces — each on a different team's Git remote — on the same version of it (P2). It does so by treating the harness as source in a Git repository, the Forge, composed per team and emitted in each tool's native format with a lockfile.
$ craftar sync
craftar sync — profile team-a · recipes base → backend-clean-arch → frontend-angular → aws-local → pm-azure-devops · targets claude-code, kiro
wrote 4, removed 1 orphan(s), skipped 1+ .claude/rules/frontend-angular.md
+ .claude/agents/frontend-reviewer.md
+ .claude/commands/create-component.md
+ .kiro/steering/frontend-angular.md
- .kiro/steering/frontend-react.md (orphan: no longer produced by the Forge)! .kiro/agents/frontend-reviewer.json hand-edited since last sync — run `craftar diff` and either `--overwrite-drift` or promote the change to the Forge$
Problem statement
Two problems, one root: the harness has no source and no record.
P1
One harness, T teams, K AI coders, A agents each — maintained by hand.
A harness is a roster of agents — a reviewer per architecture, a docs author, an executor — plus the rules they are bound to, the workflow commands, hooks and MCP servers. Across T teams it exists as T copies in K formats (.claude/, .kiro/, AGENTS.md, .cursor/), each holding the same A agents. A change costs T × K manual edits, with no record of what was generated from what.
requiresone source; emission per target; a record per file.
P2
One developer, many workspaces, separate remotes.
One machine, one workspace per team. Each workspace groups that team's repositories on that team's Git host; no repository is common to all of them. The harness in each was copied in by hand at a different time. Nothing on the machine records which version each workspace carries, or whether a file in .claude/ is still the copy or has been edited in place.
requiresa channel that is no team repository's remote; a lock per workspace; a registry.
state before Craftar — the resolved state of each is in Model
dashed edges are manual work; no edge records provenance. Each small block is one agent (reviewer, author, executor…). Faded = diverged or absent.
four copies of the harness, four ages, no common repository, no lock; the dashed links are the developer comparing folders by hand.
Failure modes
What breaks when the harness is duplicated — observed, not hypothetical.
Two earlier attempts at generating .claude/ from templates were adopted and later removed by the teams using them. Their post-mortems, plus the tech-debt notes of hand-synchronised workspaces, reduce to eight recurring failures — FM-1 to FM-5 stem from P1, FM-6 to FM-8 from maintaining P2 by hand. Each one becomes a design constraint in Invariants.
id
Failure
Root cause
FM-1
Regeneration deletes hand-written agents and rules
Generator treats the whole .claude/ folder as its output; anything not produced by a template is collateral.
FM-2
Every file reports as hand-edited on every run; team runs permanently on --force
Drift guard hashes raw bytes; Git rewrites LF→CRLF on Windows checkout, so the hash never matches.
FM-3
A deleted rule keeps loading into sessions
Generator only writes; it never removes the outputs of ingredients that left the set. The checker compares present files only, so it still reports "in sync".
FM-4
Kiro steering, agent JSON and command mirrors diverge from their source by 4–52 lines
Only rules were generated; agents and commands for the second target were kept as manual mirrors of the first.
FM-5
Architecture rule and its reviewer agent contradict each other
Rule and reviewer are two files stating the same constraints, edited independently.
FM-6
A model pin or MCP version breaks in three places at once; two sessions patch the same line the same day
Volatile values are inlined in a rule, a script and a steering file rather than declared once.
FM-7
Language policy "everything in English" contradicted by 18 files
Language treated as a property of the workspace instead of each ingredient; re-syncing became a bilingual merge.
FM-8
Real package-feed tokens in a staged settings.json
Generated settings carried values instead of references to environment variables.
Model
Source, resolution, emission.
Craftar borrows the shape of a build system. Ingredients are source; recipes and profiles compose them; craftar sync resolves the composition for one workspace and emits target-specific outputs, recording every output in a lockfile. The generated files are committed to the workspace, so a team that only uses Kiro never needs Craftar installed.
P1 resolved — solid edges are generated and recorded in each workspace's lockfile; status reads the lock to report behind / drift per workspace.
P2 resolved — one Forge, one lock per workspace, one registry
Same machine, same four workspaces, same four remotes. The Forge is a fifth repository that none of the team repositories reference; each workspace's craftar.yaml points to it. What the workspaces now share is the harness: every workspace is generated from the same Forge, so base rules, commands and agents are identical across all of them, and the lockfile in each records exactly which Forge commit produced it. craftar ls will read the registry and report, per workspace, whether it is at the Forge head, behind, or drifted.
the Forge is never a remote of any team repository; generated files are committed inside each workspace, so a team's own clone of its repo already contains the harness in its tool's format.
~
$ craftar ls # plannedWORKSPACE PROFILE STACK TARGETS FORGE STATUS
team-a-workspace team-a angular · dotnet claude-code, kiro 7c1e9a2 up to date
team-b-workspace team-b react · dotnet kimi 7c1e9a2 up to date
team-c-workspace team-c angular · dotnet codex 5b0d3f1 behind 2 (pinned v2.3)
team-d-workspace team-d angular · dotnet cursor 7c1e9a2 drift 1 .claude/rules/workflow.md
$
Layer resolution
From layers to files — how sync works.
A workspace is the result of layers applied in a fixed order, weakest first. Ingredients with the same name resolve to the strongest layer; stack recipes that declare the same slot are mutually exclusive; {{params}} are substituted from the winning layer. craftar explain <file> prints the layer that decided each output.
All layers are versioned in the Forge except the last two, which live in the workspace.
resolve
Pipelinecraftar sync
$ craftar sync
Read craftar.yaml; load the Forge (a local path today; a pinned git ref is planned).
Resolve layers, exclusive slots and params into the final ingredient set.
Emit open standards verbatim: AGENTS.md, Agent Skills (SKILL.md), mcp.json.
Emit tool-specific formats (inclusion modes, subagents, hooks) through Craftar's own emitters and post-processors; delegating further formats to rulesync is planned.
Diff against craftar.lock; classify each file as new / update / unchanged / adopt / drift / collision / orphan; write, remove orphans, refresh the lock.
emit
Targets, native formatgenerated · committed
Claude Code.claude/ · CLAUDE.md
Kiro.kiro/steering · agents/*.json
KimiAGENTS.md · skills
CodexAGENTS.md · [hooks]
Cursor.cursor/rules/*.mdc
Warpworkflows · mcp
Every generated file carries a generated by craftar from forge@sha header where the format allows comments; JSON gets a _craftar field.
In practice — Forge → Workspace by stack
The diagram above shows the logical flow — layers resolving into targets. Below is the physical mapping: how a real Forge for a given stack becomes actual files in the workspace. Select a stack, then a target to see exactly what craftar sync produces.
dotnet-enterprise
A Forge for .NET backend teams following Clean Architecture — see how its ingredients map to each target's workspace structure.
Craftar generates files inside the AI coder folder (.claude/, .kiro/, .cursor/) and outside it (docs/, local-stack/, AGENTS.md). Files outside are tool-agnostic — the same docs/architecture/ structure serves Claude Code, Kiro and Cursor users alike. Client-owned folders like src/ or infra/ are never touched by craftar sync.
Forge
forge/ · one Git repo
Repository holding every ingredient, recipe and profile. Versioned; workspaces point at it by local path today — referencing a branch, tag or SHA is planned.
Ingredient
ingredients/rules/frontend-angular/
Smallest reusable unit: a rule, agent, command, skill, hook, MCP server, local service or template. A folder with an ingredient.yaml declaring type, targets, inclusion mode and globs.
Recipe
recipes/frontend-angular.yaml
Named set of ingredients with parameters. Recipes extend others; stack recipes declare a slot so two front-end stacks cannot coexist.
Profile
profiles/team-a/profile.yaml
One team: recipes, repositories, template feeds, PM tool, internal platform, and which AI coders that team uses. Parameters for the whole stack live here.
Workspace
craftar.yaml
Local folder — one repo or a folder of repos — declaring profile, recipe add/remove and targets, plus a git-ignored craftar.local.yaml for per-developer overrides.
Target
claude-code · kiro · kimi · codex · cursor
An AI coder to emit for. Each will declare its coverage in a capabilities.yaml (planned); today the Kiro emitter warns and skips what it cannot express — a fallback is planned.
Lockfile
craftar.lock
Forge SHA, the resolved profile name and a normalized content hash for every generated file. The only authority on what Craftar owns in a workspace.
Registry
~/.craftar/registry.json
Every workspace on the machine: path, profile, last sync, status. Will back craftar ls and the local dashboard — planned.
Capabilities
Twelve capabilities, each tied to the problem it addresses.
Ordered by roadmap phase. working marks what runs today and is covered by tests; a # planned comment in a snippet marks a command that is designed, not built; mvp marks the minimum first release.
F1 workingmvp
Forge → workspace sync
problem · P1 · P2A change to any of A agents or their rules must reach T workspaces in K formats; today that is T × K manual edits with no provenance.
$ craftar sync [--dry-run] [--check] [--overwrite-drift]$ craftar diff # what sync would change, writes nothing$ craftar status # per file: new · update · unchanged · adopt · drift · collision · orphan · orphan-drift$ craftar sync --forge-ref v2.3 --targets kiro · craftar update # planned
One command resolves and emits for every active target and prints what it wrote, removed and skipped, file by file. Following a Forge branch or pinning a tag is planned.
F2 workingmvp
Multi-target emission
problem · P1Each team's AI coder has its own file layout, frontmatter and agent schema. Translating by hand yields FM-4: mirrors that diverge.
Three layers: open standards emitted verbatim (AGENTS.md, Agent Skills, Agent Plugins mcp.json); tool-specific formats; Craftar post-processors for known gaps, e.g. injecting resources: file://.kiro/steering/*.md into Kiro custom agents. Today Craftar's own emitters cover Claude Code, Kiro and AGENTS.md, and the Kiro emitter reproduces a hand-built workspace byte for byte, including agent JSON and command mirrors. Delegating Codex, Kimi and Cursor formats to rulesync is planned.
F3 resolution working
Layers and stack slots
problem · P1Teams differ in stack. Without composition, a React team inherits Angular reviewers, or every team gets a hand-pruned fork of the harness.
Recipes declaring the same slot are mutually exclusive. The profile sets the team default; the workspace may override; the local file (craftar.local.yaml, git-ignored) overrides the workspace for one developer; its arrays replace the workspace's.
F4
Workspace bootstrap
problem · P2Starting with a new team means reassembling repos, harness, templates and local services from memory.
Reads the profile's repos, templates and services; registers the workspace in the registry.
F5
PM tool and internal platform per profile
problem · P1Agents need to read user stories and talk to the team's internal developer platform; each team uses a different tool for both.
integrations.pm.kind ∈ {github, azure-devops, notion, jira} selects the matching MCP ingredient plus a rule describing how stories are located and interpreted for that team. integrations.idp registers the platform's MCP server in every target that supports MCP and adds a rule describing its operations. Tokens are referenced by authEnv, never stored (FM-8).
F6
Local services per workspace
problem · P1Cloud dependencies (queues, buckets) need a local stand-in, and the agent has to know its endpoints and fake credentials.
$ craftar services up | down | status # planned
A service ingredient carries a docker-compose fragment, default variables and a rule for the agent. Fragments merge into .craftar/compose.yaml. First ingredient: LocalStack-style SQS/S3.
F7
Project templates
problem · P1New projects should use the team's own dotnet new (or npm create, nx, cookiecutter) templates; agents need to know which.
The profile declares feeds and packages; Craftar installs them and generates a rule mapping archetype → template plus a wrapping command.
F8 detection working
Drift and reverse contribution
problem · P2Generated files get edited in place. Either the edit is lost on the next sync, or the generator is abandoned (FM-2).
$ craftar status # drifted files listed; never overwritten without --overwrite-drift$ craftar drift show <file> · drift discard · drift promote # planned
Normalized hashes from the lock are compared with disk. Planned: promote will convert the edit back into an ingredient via import and open a Forge branch. Sync direction is Forge → workspace; reverse flow is always explicit.
F9 --check working
CI
problem · P2Sync that depends on someone remembering will lag by workspace.
$ craftar sync --check # exit ≠ 0 when behind or drifted
Planned: a GitHub Action and an Azure Pipelines task that open a PR with the change summary when the Forge moves; optional Forge-side dispatch to registered workspaces; a pre-commit hook running --check.
F10
Registry and dashboard
problem · P2With a dozen workspaces there is no view of which is behind, drifted or on which Forge version.
$ craftar ls · craftar ui # planned — today's ls lists the recipes and ingredients resolved for one workspace
Table or local web page: workspace, profile, stack, targets, Forge SHA, status; actions sync / diff / explain; Forge view of recipes, ingredients and who uses what.
F11
Craftar as an MCP server
problem · P1 · P2The agent itself should be able to check status, sync, or add a recipe without leaving the session.
$ craftar mcp # planned
Exposes status, sync, diff, explain, recipes, add recipe, services up. Packaged as a Claude Code plugin, an Agent Plugin, and Warp workflows — thin shells over the CLI.
F12
Diagnostics
problem · P2Sync failures usually come from the environment: missing env vars, wrong Node, no Docker, unreachable Forge.
$ craftar doctor # planned
Checks Node and rulesync versions, Git access to the Forge, every authEnv required by MCP ingredients, Docker when services exist, dotnet when templates exist, and lockfile consistency.
Invariants
Constraints derived from the failure modes.
These are the constraints Craftar is built against, in every command and every emitter — what distinguishes it from a template renderer. The two marked planned are design commitments, not yet enforced by the code.
never
Touch a file not in the lockfile
The lock is the complete list of managed outputs. Unknown files inside .claude/ or .kiro/ are preserved and never removed; one at a path the Forge would write is reported by status as a collision and never touched. Regeneration is per file.
← FM-1
never
Hash raw bytes
Content hashes are computed after normalization: LF line endings, UTF-8, BOM stripped; JSON compared structurally. Claude Code and AGENTS.md outputs keep the EOL and BOM of the file they replace; Kiro output is always CRLF without BOM.
← FM-2
never
Leave an orphan on disk
An output whose ingredient left the resolved set is removed on the next sync and named in the summary, in every target. --check fails on orphans.
← FM-3
never
Require a manual mirror
Everything a second target needs — steering, agent JSON, command copies — is emitted from the same ingredient. If two files must say the same thing, both are generated.
← FM-4
never planned
Split a rule from its reviewer
An architecture rule and the reviewer agent for that architecture are meant to be one ingredient with two outputs, so they cannot disagree. Planned: today the rule and its reviewer are separate ingredients (rule/<name>, agent/<name>-reviewer) grouped by a stack recipe.
← FM-5
never
Inline a volatile value
Model pins, MCP versions, ports and organisation names are declared once in the profile and injected as {{params}} wherever cited.
← FM-6
never
Translate
Language is an attribute of the ingredient. The Forge may hold rules in one language and docs in another; Craftar emits text as written.
← FM-7
never planned
Store a secret
Ingredients are meant to declare authEnv; settings would be emitted as settings.example.json; doctor would verify the variables exist on the machine. Planned: not built yet, though craftar import already refuses ingredients holding secret-like values.
← FM-8
Targets
Capability matrix.
This table is the design target, not current coverage: today only the Claude Code, Kiro and AGENTS.md emitters exist. A per-target capabilities.yaml is planned. Nothing may be dropped silently: today the Kiro emitter warns and skips what it cannot express; closing the remaining gaps is planned.
Target
Emits to
Rules & inclusion modes
Commands
Subagents
Hooks
Skills
MCP
Claude Code
.claude/ · CLAUDE.md
yes
yes
yes
yes
yes
yes
Kiro
.kiro/steering · .kiro/agents/*.json
always · fileMatch · manual
yes
custom agents
partial
yes
yes
Kimi
AGENTS.md · SKILL.md
AGENTS.md only
fallback
as commands
—
yes
yes
Codex
AGENTS.md · config
AGENTS.md only
fallback
—
[hooks] block
yes
yes
Cursor
.cursor/rules/*.mdc
globs · always
fallback
—
—
Agent Plugin
yes
Warp
workflows · mcp
AGENTS.md
workflows
—
—
partial
yes
AGENTS.md
root and per-folder
plain
—
—
—
yes
mcp.json
yes — nativepartial / fallback — emitted with a warning— unsupported; reported and skippedKimi and Codex rows pending verification against the tools.
Status
Phase 0 — validated against a hand-built workspace with an oracle test.
Before generating anything new, Craftar has to reproduce what teams already have. The oracle imports a hand-built workspace (Claude Code + Kiro), syncs it, and asserts byte equality of every generated file; a second sync must be a no-op; a Forge edit must reach both targets; a manual edit must surface as drift and survive; a removed ingredient must disappear; an unmanaged file must be untouched.
Phase 0in progress
Foundations
Forge schema: ingredients, recipes, profiles
Layer resolver with exclusive slots and params
sync, diff, status, explain, ls, lockfile
import --from claude-code
Emitters: Claude Code, Kiro, AGENTS.md
Oracle suite against a private hand-built workspace, plus a synthetic suite in CI on Linux and Windows
Phase 1
Team profile
Profiles and craftar init
Remote Forge (Git URL)
PM tool → MCP, platform → MCP
doctor, registry
Phase 2
Environment
service ingredient
Project templates
drift show / discard / promote
docs validate
Phase 3
Automation
GitHub Action, Azure Pipelines task
craftar ui
Phase 4
Ecosystem
craftar mcp
Claude Code plugin, Agent Plugin
Warp workflows
Kimi, Codex, Cursor emitters
Documentation
Stack
TypeScript / Node CLI (commander, zod, yaml) with its own emitters for Claude Code, Kiro and AGENTS.md; rulesync is planned as the conversion dependency for further tool-specific formats; Craftar owns the layer above — Forge, resolution, lockfile, drift, registry.
Open questions
Forge as a separate repo (current) vs. monorepo with team repos
What Kimi and Codex read beyond AGENTS.md
Multiple Forges per workspace (schema reserved, not implemented)
Trying it
Install with npm install -g craftar (Node ≥ 22), or prefix any command with npx. Then, inside an existing Claude Code workspace, run craftar import --from claude-code --forge <dir> --profile <name> --write-config to obtain a Forge and a craftar.yaml, and craftar diff to see what a sync would change. Nothing in .claude/ or .kiro/ is rewritten until sync.
This page was written by a language model, from the project's specification and the notes of the workspaces that motivated it, and reviewed by a person. It is also meant to be read by language models: every diagram carries a prose description, the metadata is structured, and a plain-text version lives at llms.txt.