A build system for AI-coding harnesses.

A harness (the rules, agents, commands, skills, hooks and MCP servers that shape coding agents) kept as source in one Git repository, the Forge. Craftar composes it per team, writes each tool's own files and records them in a lockfile (P1, P2).

craftar (the CLI and this plugin) is built with Orca, which is why the example runs in it. Get the Orca plugin →

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.

The same agents, rules, commands, hooks and MCP servers live as T copies in K formats (.claude/, .kiro/, AGENTS.md, .cursor/). A change costs T × K manual edits, with no record of what came from what.

requiresone source; emission per target; a record per file.

P2

One developer, many workspaces, separate remotes.

One workspace per team, each on that team's Git host, its harness copied in by hand. Nothing records which version each one carries, or whether a file was 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
Today: one harness copied by hand into four team workspaces, each in a different AI coder format Mechanism: a single harness (about 12 rules, 7 agents, 9 commands, skills, hooks and 4 MCP servers) exists only as a folder on the developer's machine. It is copied and translated by hand into four team workspaces: team A uses Claude Code, team B uses Kimi via AGENTS.md, team C uses Kiro, team D uses Cursor. Because the copies are manual, they diverge: team A's copy is two months old, team B has one stale agent, team C is missing one agent, team D carries version 3 of 5. No edge in this graph is recorded anywhere, so nothing can report which copy is current. harness (yours) rules ×12 agents ×7 commands ×9 skills · hooks mcp ×4 a folder + memory copy translate by hand team A · workspace .claude/ 2 months old team B · workspace AGENTS.md 1 stale team C · workspace .kiro/ 1 missing team D · workspace .cursor/ harness v3 of 5

dashed edges are manual work; no edge records provenance. Each small block is one agent (reviewer, author, executor…). Faded = diverged or absent.

What breaks when the harness is duplicated: observed, not hypothetical.

Eight failures seen in two earlier generators and in hand-synced workspaces: FM-1 to FM-5 from P1, FM-6 to FM-8 from P2. Each becomes a constraint in Invariants.

idFailureRoot cause
FM-1Regeneration deletes hand-written agents and rulesThe generator owned the whole .claude/ folder.
FM-2Every file reads as hand-edited; the team lives on --forceRaw-byte hashes; a Windows checkout rewrites LF→CRLF.
FM-3A deleted rule keeps loading into sessionsThe generator only wrote, never removed.
FM-4Kiro mirrors drift 4–52 lines from their sourceOnly rules were generated; the rest were hand mirrors.
FM-5A rule and its reviewer agent contradict each otherTwo files stating the same constraints, edited apart.
FM-6A model pin or MCP version breaks in three places at onceVolatile values inlined instead of declared once.
FM-7"Everything in English" contradicted by 18 filesLanguage treated as the workspace's, not the ingredient's.
FM-8Real package-feed tokens in a staged settings.jsonSettings carried values instead of variable references.

Source, resolution, emission.

Ingredients are source; recipes and profiles compose them; craftar sync resolves them for one workspace, writes each target's files and records them in a lockfile. Generated files are committed, so a team that only uses Kiro never needs Craftar installed.

With Craftar: one Forge resolved per workspace and emitted to each team's AI coders Mechanism: the Forge is one Git repository containing ingredients (rules, agents, commands, skills, hooks, MCP servers, scripts, steering files), recipes (base, backend-*, frontend-*, aws-local, pm-*) and one profile per team. Running craftar sync in a workspace loads the Forge (by path, or by git URL at a ref), resolves layers, emits files for each of the workspace's targets and writes craftar.lock. Team A emits to .claude/ with 7 agents; team B swaps the frontend recipe and has three workspaces, one target each: .claude/, .kiro/ and AGENTS.md, which carries the rules only; team C emits .kiro/ and pins the Forge at tag v2.3, and its lock is up to date; team D sets its targets to agents-md in its local file, so Cursor reads the emitted AGENTS.md, which carries the rules only, and has one drifted file. Every edge is generated and recorded in the lock. Forge · git repo ingredients/ rules · agents · commands skills · hooks · mcp · scripts recipes/ base · backend-* · frontend-* aws-local · pm-* profiles/ team-a · team-b · team-c · team-d single source of truth craftar sync resolve layers emit per target write craftar.lock @ref team Acraftar.yaml · profile: team-a .claude/ 7 agents · 1 target · lock ok team Brecipes: −frontend-angular +frontend-react ws-1.claude/ ws-2.kiro/ ws-3AGENTS.md 6 agents · 3 workspaces · ws-3 rules only · lock ok team Cforge pinned @ v2.3 .kiro/ 5 agents · kiro · lock ok team Dcraftar.local.yaml: targets agents-md AGENTS.md rules only · agents-md · drift 1

P1 resolved: solid edges are generated and recorded in each workspace's lockfile; status reads it to report drift per workspace.

P2 resolved: one Forge, one lock per workspace, one registry

The Forge is a fifth repository that no team repository references; each workspace's craftar.yaml points to it, and its lock records the Forge commit that produced it. craftar workspaces reports each one as up to date, outdated (forge moved when the Forge has a newer commit) or drifted.

One developer machine with four team workspaces, each with its own repositories on separate Git remotes, all generated from a single Forge Mechanism: same developer machine and same four workspaces as the problem statement, workspace-1 to workspace-4, each still cloning its team's repositories from that team's own remote. Now a fifth repository, the Forge, is referenced by each workspace's craftar.yaml and is not a remote of any team repository. Between them sits craftar itself: it reads the Forge and emits each workspace's harness, so nothing flows from the Forge to a workspace except through it. Each workspace carries a generated harness with the same base ingredients and a craftar.lock naming the Forge commit that produced it (7c1e9a2 for three of them, 5b0d3f1 for the one synced before the Forge moved). Every writing sync records its workspace in the registry at ~/.craftar/registry.json, and craftar workspaces reports per workspace: up to date, outdated with forge moved, or drift. forge sync → workspaces · recorded in each lock main @ 7c1e9a2 · one git repo craftar syncreads the Forge · emits per targetworkspace-1/up to dateprofile team-alock forge@7c1e9a2targets claude-codeharness · generated · baseremote git.team-a.exampleapi-core/web-app/worker-jobs/workspace-2/up to dateprofile team-block forge@7c1e9a2targets agents-mdharness · generated · baseremote github.example/team-bcatalog-api/catalog-web/workspace-3/outdatedprofile team-clock forge@5b0d3f1targets kiroharness · generated · baseremote azure.example/team-cbilling-api/billing-web/billing-cli/workspace-4/driftprofile team-dlock forge@7c1e9a2targets agents-mdharness · generated · baseremote gitlab.team-d.exampleadmin/admin-desktop/ developer machine ~/.craftar/registry.json · 4 workspaces · 10 repositories · 4 remotes · 1 Forge $ craftar workspaces

the Forge is never a remote of a team repository; generated files are committed inside each workspace.

~
$ craftar workspaces
craftar workspaces — 4 registered
  team-a-workspace  /home/dev/work/team-a-workspace
      profile team-a · frontend=frontend-angular · claude-code
      forge ../forge @ 7c1e9a20 · synced 2026-10-07 09:12 UTC · up to date
  team-b-workspace  /home/dev/work/team-b-workspace
      profile team-b · frontend=frontend-react · agents-md
      forge ../forge @ 7c1e9a20 · synced 2026-10-07 09:14 UTC · up to date
  team-c-workspace  /home/dev/work/team-c-workspace
      profile team-c · frontend=frontend-angular · kiro
      forge ../forge @ 7c1e9a20 · synced 2026-10-02 17:40 UTC · outdated, forge moved
  team-d-workspace  /home/dev/work/team-d-workspace
      profile team-d · frontend=frontend-angular · agents-md
      forge ../forge @ 7c1e9a20 · synced 2026-10-07 09:20 UTC · drift
$ 

From layers to files: how sync works.

Layers apply weakest first and the strongest wins; stack recipes that share a slot exclude each other, and {{params}} come from the winning layer. craftar explain <file> names the layer behind each output.

Layers, weakest → strongestsource
baserecipe · every workspace
stack recipesbackend-clean-arch · frontend-angular
team profileprofiles/team-a
craftar.yamlthis workspace · committed
craftar.local.yamlone developer · git-ignored

All layers are versioned in the Forge except the last two, which live in the workspace.

Pipelinecraftar sync
$ craftar sync
  1. Read craftar.yaml; load the Forge: a path, or a git URL at a ref, cached per machine.
  2. Resolve layers, exclusive slots and params into the final ingredient set.
  3. Emit the open standards (AGENTS.md, Agent Skills, mcp.json) through the target emitters.
  4. Emit each tool's own format through one Craftar emitter per target.
  5. Diff against craftar.lock; classify each file (new · update · unchanged · adopt · drift · collision · orphan); write, remove orphans, refresh the lock.
Targets, native formatgenerated · committed
working
Claude Code.claude/ · .mcp.json
Kiro.kiro/steering · agents/*.json
AGENTS.mdroot · read by Codex, Cursor, Copilot, Antigravity, OpenCode, Warp, Kimi
Cursor.cursor/rules/*.mdc · agents/ · mcp.json
Copilot.github/instructions · agents · mcp.json · prompts and .vscode/mcp.json for VS Code
Agent Skills.agents/skills/ · read by Cursor, Copilot, Codex, Antigravity, OpenCode
Codex.codex/agents/*.toml · hooks.json · rules through AGENTS.md
Antigravity.agents/rules · agents · mcp_config.json · hooks.json
mvp
OpenCode.opencode/agents · commands · opencode.json · rules through AGENTS.md

Composed files carry a GENERATED banner; Claude Code files are written verbatim, so an existing workspace is adopted byte for byte. What Craftar owns is recorded in craftar.lock.

In practice: Forge → Workspace by stack

Above, the logical flow; below, how a real Forge for one stack becomes files. Pick a stack, then a target, to see what craftar sync writes.

dotnet-enterprise

A Forge for .NET backend teams following Clean Architecture: see how its ingredients map to each target's workspace structure.

Forge source
forge/
├── ingredients/
│   ├── rules/
│   │   ├── dotnet-clean-architecture/
│   │   ├── dotnet-clean-code/
│   │   ├── dotnet-minimal-api/
│   │   ├── dotnet-entity-framework/
│   │   └── dotnet-security/
│   ├── agents/
│   │   ├── dotnet-arch-reviewer/
│   │   ├── dotnet-api-reviewer/
│   │   ├── dotnet-ef-reviewer/
│   │   └── dotnet-security-reviewer/
│   └── commands/
│       └── create-api-endpoint/
├── recipes/
│   └── dotnet-clean-arch.yaml
└── profiles/
    └── team-dotnet/
sync
Workspace generated
workspace/
├── .claude/                    ← generated
│   ├── rules/
│   │   ├── dotnet-clean-architecture.md
│   │   ├── dotnet-clean-code.md
│   │   ├── dotnet-minimal-api.md
│   │   ├── dotnet-entity-framework.md
│   │   └── dotnet-security.md
│   ├── agents/
│   │   ├── dotnet-arch-reviewer.md
│   │   ├── dotnet-api-reviewer.md
│   │   ├── dotnet-ef-reviewer.md
│   │   └── dotnet-security-reviewer.md
│   └── commands/
│       └── create-api-endpoint.md
├── src/                        ← client code (untouched)
├── craftar.yaml                ← workspace input
└── craftar.lock                ← generated
generated by Craftar client-owned (untouched)

The first three tabs show Claude Code, Kiro and AGENTS.md; Five more targets shows Cursor, Copilot, Agent Skills, Codex and Antigravity in one workspace. All eight are working; the capability matrix lists every path.

What syncs where

Craftar writes only each AI coder's own files: every path is in the capability matrix: and a root AGENTS.md, read by Codex, Cursor, Copilot, Antigravity, OpenCode, Warp and Kimi; plus .craftar/ when the recipes bring a local service. Folders like src/ or infra/ are never touched: only paths recorded in craftar.lock are written or removed. Outside the workspace and the Forge it writes only under ~/.craftar: the Forge cache and the registry.

Growing the Forge: a second client, one base

A second client imported into the Forge reuses every ingredient that matches; the ones that differ become variants named after the profile, so both workspaces still reproduce byte for byte. forge variants lists them, forge diff classifies each hunk, and forge unify resolves one, keep a side, move a differing value into the profile as a parameter, or a block as a section, after planning every registered workspace that reads the Forge.

A second client's differing rule becomes a variant, and forge unify turns the difference into a parameter so both clients share one base Mechanism: the Forge already holds rule/deploy, whose text reads "use globex-api here". Importing a second client, acme, whose copy reads "use acme-api here", keeps the base and adds a variant, rule/deploy--acme, that emits under the original name. craftar forge diff rule/deploy shows one hunk, globex-api against acme-api, and suggests the class value. craftar forge unify rule/deploy --profile acme --plan plan.yaml, with that hunk set to take: param and the key deploy.api, rewrites the base to "use {{deploy.api}} here", declares globex-api as the key's default in the base's ingredient.yaml, writes deploy.api: acme-api into profiles/acme/profile.yaml and removes the variant. Every path it overwrites or deletes must be held by git. Re-importing acme afterwards compares its file with the base as sync renders it for acme, finds them equal and reuses the base: no variant, no change to the profile. 1 · craftar import · acme rule/deploybase use globex-api here rule/deploy--acmevariant use acme-api here the variant emits as rule/deploy 2 · craftar forge diff rule/deploy --against acme · 1 hunk - use globex-api here + use acme-api here suggested: value the rest of the file is identical 3 · craftar forge unify --plan take: param · key deploy.api rule/deploydefault globex-api use {{deploy.api}} here profiles/acme · deploy.api: acme-api variant removed · paths held by git 4 · craftar import · acme again compared with its render for acme rule/deployreused use acme-api here = render no variant · profile unchanged one base, two clients

one rule, two clients. forge unify refuses when another layer, profile or ingredient already uses the key; forge promote makes a profile's variant the base.

Forge

forge/ · one Git repo

Git repository holding every ingredient, recipe and profile; read by path, or by git URL at a ref.

Ingredient

ingredients/rules/frontend-angular/

Smallest reusable unit: a rule, agent, command, skill, hook, MCP server, script, steering file or local service.

Recipe

recipes/frontend-angular.yaml

Named set of ingredients with parameters; stack recipes declare a slot.

Profile

profiles/team-a/profile.yaml

One team: its recipes, targets, parameters and sections.

Workspace

craftar.yaml

A folder with craftar.yaml (profile, recipe changes, targets) and a git-ignored craftar.local.yaml per developer.

Target

claude-code · kiro · agents-md · cursor · copilot · agent-skills · codex · antigravity · opencode

An AI coder to emit for; the matrix lists what each one gets.

Lockfile

craftar.lock

The Forge's source, ref and commit and a normalized hash per generated file: the authority on what Craftar owns.

Registry

~/.craftar/registry.json

Every workspace synced on this machine; craftar workspaces and craftar ui show them.

Twelve capabilities.

Each links to its entry in the documentation: the problem it addresses, every command it adds and how it works. working runs today and is tested; mvp marks the minimum first release (on a target, as for OpenCode in the matrix, it means loaded by the tool but not yet used by a model session).

  1. F1Forge → workspace syncworkingmvp
  2. F2Multi-target emissionworkingmvp
  3. F3Layers and stack slotsworking
  4. F4Workspace bootstrapworking
  5. F5PM tool and internal platform per profileworking
  6. F6Local services per workspaceworking
  7. F7Project templateslist | install working
  8. F8Drift and reverse contributionworking
  9. F9CIworking
  10. F10Registry and dashboardworking
  11. F11Craftar as an MCP serverworking
  12. F12Diagnosticsworking
AI llms.txt

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.