Open-source CLI · TypeScript / Node · design notes

A build system for AI-coding harnesses.

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.

Read the model $npm install -g craftar
claude-codekiroagents-md
~/work/team-a/platform-workspace
$ 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 — `craftar drift show <path>`, then `craftar drift discard` or `craftar drift promote`
$ 
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
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.

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.

idFailureRoot cause
FM-1Regeneration deletes hand-written agents and rulesGenerator treats the whole .claude/ folder as its output; anything not produced by a template is collateral.
FM-2Every file reports as hand-edited on every run; team runs permanently on --forceDrift guard hashes raw bytes; Git rewrites LF→CRLF on Windows checkout, so the hash never matches.
FM-3A deleted rule keeps loading into sessionsGenerator 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-4Kiro steering, agent JSON and command mirrors diverge from their source by 4–52 linesOnly rules were generated; agents and commands for the second target were kept as manual mirrors of the first.
FM-5Architecture rule and its reviewer agent contradict each otherRule and reviewer are two files stating the same constraints, edited independently.
FM-6A model pin or MCP version breaks in three places at once; two sessions patch the same line the same dayVolatile values are inlined in a rule, a script and a steering file rather than declared once.
FM-7Language policy "everything in English" contradicted by 18 filesLanguage treated as a property of the workspace instead of each ingredient; re-syncing became a bilingual merge.
FM-8Real package-feed tokens in a staged settings.jsonGenerated 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.

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 the lock to report drift per workspace, and, with a remote Forge, names the Forge commit next to the lock's when it has moved.

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 workspaces reads the registry every writing sync keeps and reports, per workspace, whether it is up to date, outdated — with forge moved when the Forge's commit is no longer the lock's — 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 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 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
$ 
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.

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 local path, or a git URL at a branch, tag or full SHA, fetched into a per-machine cache).
  2. Resolve layers, exclusive slots and params into the final ingredient set.
  3. Emit the open standards: AGENTS.md — the always-on rules, then each scoped rule listed at the file a target writes for it, or embedded in full when none does — Agent Skills (SKILL.md) and mcp.json through the target emitters.
  4. Emit tool-specific formats (inclusion modes, subagents, hooks) through Craftar's own emitters and post-processors; delegating further formats to rulesync is planned.
  5. Diff against craftar.lock; classify each file as new / update / unchanged / adopt / drift / collision / orphan; write, remove orphans, refresh the lock.
Targets, native formatgenerated · committed
Claude Code.claude/ · .mcp.json
Kiro.kiro/steering · agents/*.json
AGENTS.mdroot · read by Codex, Cursor, Warp, Copilot, Kimi

Composed files carry a GENERATED banner — Kiro steering's is configurable, AGENTS.md's is fixed. Claude Code text files are emitted verbatim, which is what lets an existing workspace be adopted byte for byte. What Craftar owns is recorded in craftar.lock, not in a header; a generated by craftar from forge@sha marker on every target is planned.

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 what craftar sync writes today.

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)
What syncs where

Craftar writes the AI coder's own files — .claude/ plus .mcp.json for Claude Code, .kiro/ for Kiro — and a root AGENTS.md for the tools that read the open standard: Codex, Cursor, Warp, Copilot and Kimi consume that file; Craftar writes no tool-agnostic output such as docs/. For a workspace whose recipes bring a service ingredient, sync also writes .craftar/ — .craftar/compose.yaml, .craftar/.gitattributes and .craftar/services/<name>/… — for no target. Client-owned folders like src/ or infra/ are never touched — only paths recorded in craftar.lock are ever written or removed. Outside a workspace and a Forge, Craftar writes on its own only under ~/.craftar (by default): the Forge cache and the registry.

Growing the Forge — a second client, one base

Importing a second client into an existing Forge keeps every ingredient that already matches and turns the ones that differ into variants named after the profile, so both workspaces still reproduce byte for byte. The forge commands then close the gap: forge variants lists them nearest first, forge diff shows each hunk with a suggested class, and forge unify resolves one — keeping the base or the variant, turning the differing value into a parameter the profile carries, or turning a differing block into a section whose content the profile carries. Before and after it writes, forge unify plans every workspace registered on this machine that reads the Forge and prints which of each one's files the unification changes; with --prune-recipes it also deletes a recipe the unification left identical to its sibling and repoints the profiles that name it — only when every one of those workspaces plans the same files. craftar forge impact lists the same workspaces with what each one's next sync would do against the Forge as it stands.

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 never merges ingredient.yaml and refuses when another layer, profile or ingredient already uses the key. Sections ship too — markers placed by hand (each client's content inferred by import) or extracted from a variant by forge unify with take: section; forge promote ships as well — a profile's variant becomes the base, and every other profile that resolved the old base keeps it as a variant of its own. The interactive walk is planned.

Forge

forge/ · one Git repo

Repository holding every ingredient, recipe and profile. Versioned; workspaces point at it by local path or by git URL, with an optional branch, tag or full SHA; a remote Forge is read from a per-machine cache.

Ingredient

ingredients/rules/frontend-angular/

Smallest reusable unit: a rule, agent, command, skill, hook, MCP server, script, steering file or local service. A folder with an ingredient.yaml declaring type, targets, inclusion mode and globs. A service is aimed at no AI coder: sync writes it under .craftar/. Project templates are not an ingredient: a profile declares them.

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 — and, in a Forge at schema: 8, its own ingredients and recipes. Parameters and section overrides for the whole stack live here.

Workspace

craftar.yaml

Local folder — one repo or a folder of repos — declaring profile, recipe add/remove (edited by craftar add recipe / remove recipe) and targets, plus a git-ignored craftar.local.yaml for per-developer overrides.

Target

claude-code · kiro · agents-md

An AI coder to emit for. These three are the targets; Codex, Cursor, Warp, Copilot and Kimi read the emitted AGENTS.md. Coverage is a table in the CLI — craftar targets prints it, per ingredient type: native, converted or unsupported. Sync skips an ingredient aimed at a target that the table marks unsupported, and warns, naming it — a fallback is planned.

Lockfile

craftar.lock

The Forge's source, requested ref and commit (null when the Forge is not a git repository or has no commits yet), the resolved profile, recipes and targets, and a normalized content hash for every generated file; the lock is rewritten only when its own content changes. The only authority on what Craftar owns in a workspace.

Registry

~/.craftar/registry.json

Every workspace a writing sync ran in on this machine: path, profile, Forge, recipes, targets, last sync. craftar workspaces lists them with a status computed on the spot; forget and prune remove entries, and CRAFTAR_NO_REGISTRY turns the registry off. craftar ui shows them as a local page on 127.0.0.1, read-only unless started with --allow-writes. craftar ls stays the view of one workspace's resolved recipes and ingredients.

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 [--exit-code] # what sync would change, orphans included; writes nothing $ craftar status # per file: new · update · unchanged · adopt · drift · collision · orphan · orphan-drift $ craftar sync --offline # remote Forge: fetched first; --offline reads the cached copy $ craftar cache prune [--dry-run] # cached Forges nothing uses, unfinished fetches, trees unused for 14 days $ craftar sync --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. With a git-URL Forge, ref follows a branch or pins a tag or full SHA; sync fetches first and fails when it cannot, so CI never syncs from a stale copy unless it passes --offline. craftar cache prune removes the Forge cache entries nothing uses — no registered workspace and not the current one — entries whose first fetch never completed, and trees unused for 14 days; it never touches a workspace, a lock or the registry, and --dry-run shows what it would remove. A symbolic link never carries sync out of the tree it works on: a Forge file read through a link that leaves the Forge or leads into its .git, and a workspace path that leaves through one, refuse the run before anything is written — there is no flag; a link that stays inside works as before. In a git repository, sync and sync --check warn for each generated file git ignores, naming the ignore rule by file, line and pattern; craftar never edits an ignore file, and mayBeIgnored in craftar.yaml lists the paths kept out of git on purpose.

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.

$ craftar import --from claude-code # existing .claude/ → ingredients + recipes + profile $ craftar forge variants # where clients differ, nearest first $ craftar forge diff rule/deploy # hunks, each with a suggested class $ craftar forge unify rule/deploy --profile acme --plan plan.yaml # base, variant, param or section $ craftar forge impact # every registered workspace reading this Forge, and what its next sync would do $ craftar targets # the capability matrix: what each target does with each ingredient type

Three layers: the open standards — AGENTS.md (the always-on rules, then each scoped rule listed at the file a target writes for it or embedded when none does), Agent Skills (SKILL.md) and mcp.json — written by the target emitters; 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. Importing a second client into an existing Forge reuses every ingredient whose render for that profile equals the workspace file, infers declared {{key}} values into the profile, and makes the rest variants; forge unify resolves each one back into its base (see Growing the Forge). craftar forge promote <type/name> --profile <p> goes the other way: it makes a profile's variant the base and gives the old base back, as a variant of its own, to every other profile that resolved it — gated on git, proved before the first write, never a commit. craftar import --report <file.md> also writes a Markdown report of the import: what was created, reused, made a variant or rejected, and the .kiro/ files that differ from what sync would generate — as paths and line counts, never content. craftar import --prefer-kiro <rule,…> takes the named rules from .kiro/steering/<name>.md instead of .claude/rules/<name>.md, for a steering mirror that was edited by hand and is the text to keep; the text is accepted only when it is proved to render back to that steering file's body for Kiro. Rules only: commands, agents and skills are not taken from Kiro. Import reads a Claude rule's paths: frontmatter as its scope, so a scoped rule stays scoped — fileMatch for Kiro, and a scoped rule in AGENTS.md — and keeps that frontmatter verbatim, so .claude/rules/<name>.md comes back byte for byte. At schema: 7 import never writes a generated rules index over its placeholder silently: a variant that holds the placeholder and whose file differs from what sync writes is refused, a new variant that freezes the list is warned, and --prefer-kiro refuses a rule whose ingredient holds the placeholder. Import takes nothing from behind a symbolic link that leaves the workspace or leads into its .git: such a source is rejected and listed, never adopted. In a Forge at schema: 8, a source equal to the importing profile's own local is reused there, one that differs is refused, and the Forge it would write is first loaded on a scratch copy. With --local=auto, a new source of the profile becomes a local there instead of a root ingredient, and a local that differs is rewritten; --local=always writes no ingredient at the root. A new local no recipe of the profile's folder cites is wired into the recipe <p>-local. With agents-md.primary: true, in a Forge that declares schema: 5 and a workspace whose targets include agents-md, an always-on rule with the default targets is written to AGENTS.md alone — with all three targets, instead of three times (.claude/rules/, .kiro/steering/ and AGENTS.md): its other files become orphans sync removes; the .claude/, .kiro/ and AGENTS.md texts that cite it point at its place in AGENTS.md; scripts, hooks and files copied as bytes are never rewritten — a reference left there is warned with its file and line; and generated Kiro resources name only the steering files the plan writes, while a hand-written entry naming a file the plan does not write and the disk lacks fails sync --check (a writing sync still writes). Craftar writes no CLAUDE.md; when one keeps Claude Code from reading AGENTS.md, every plan says so. The mode is opt-in — a workspace that does not set it sees no change — and every craftar before 0.36.0 refuses a schema: 5 Forge. A hook ingredient can declare its registration — the event, matcher and command Claude Code runs it with — and claude-code writes it to .claude/settings.craftar.example.json, to copy into .claude/settings.json; Craftar never writes a settings file Claude Code reads. Every craftar before 0.38.0 refuses a Forge whose hook holds registration.

F3 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.

$ craftar add recipe frontend-react --replace · craftar remove recipe aws-local # edits craftar.yaml, never syncs $ craftar recipes · craftar ingredients --recipe frontend-angular # the catalogue: every recipe and ingredient, and what this workspace uses $ craftar explain .kiro/steering/workflow.md # which layer decided this file $ craftar forge localize rule/acme-deploy --profile acme · craftar forge share rule/acme-deploy --profile acme # schema 8: into a profile's folder and back

Recipes declaring the same slot are mutually exclusive. craftar add recipe and craftar remove recipe change the workspace's choice in craftar.yaml in place — refusing a recipe that would share a slot (unless --replace), removing one another recipe still brings in through extends, or an edit that would not resolve — and print what the next sync would do; they never sync. craftar recipes and craftar ingredients list the whole Forge read-only, mark what the workspace uses (and, for recipes, why), and --recipe shows what choosing one would bring. 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. An ingredient body can also mark a client-specific block as a section (<!-- craftar:section flavors --> … <!-- /craftar:section -->): the block is the default, a profile's sections or the workspace's overrides.sections replace or empty it, no rendered body holds the markers, explain names the layer each section came from, and forge unify can extract a section from a variant (take: section). A Forge that uses sections declares schema: 2 or above, so an older CLI refuses it rather than leak the markers. In a Forge that declares schema: 6, an opening marker can name the targets that receive the block (<!-- craftar:section flavors targets=claude-code,kiro -->): a target it names gets the section as any other, a target it does not name gets nothing of it, whatever a profile or the workspace sets, and a marker with no targets= is read as before. A Forge that writes no targets= sees no change, and every craftar before 0.39.0 refuses a schema: 6 Forge. In a Forge that declares schema: 7, a line of a rule or steering body that holds {{craftar.rules.index}} alone becomes the list of the rules the profile resolves, as each target names them: up to three groups — Always on:, Scoped:, On request: — one line per rule with its description, or by its reference alone, warned, when it has none; a rule no target of the workspace writes is left out. explain names an indexed file's list, AGENTS.md aside. A placeholder not alone on its line, or in an agent, a command or a skill, is left as written and warned; so is one alone on its line below schema: 7. Either warning makes doctor --strict exit 1. The index is generated, never a parameter, and every key starting with craftar. is reserved: forge unify never extracts into one and import never infers one. A Forge that writes no placeholder and sets no craftar. key sees no change from the index, and every craftar before 0.40.0 refuses a schema: 7 Forge. In a Forge that declares schema: 8, a profile's folder holds its own ingredients and recipes (profiles/<p>/ingredients/, profiles/<p>/recipes/), which only that profile sees: they render exactly as they would at the root, a root recipe can never reach them, and a broken reference fails the load, naming the files. explain names where a file's ingredient lives. Every craftar before 0.45.0 refuses a schema: 8 Forge. craftar forge localize moves a root ingredient only one profile resolves into that profile's folder, with the root recipes that cite it, and craftar forge share moves a local back to the root — each gated on git and proved, before the first write, to change no byte any profile or registered workspace plans; neither commits. In a Forge that declares schema: 9, an ingredient's metadata takes {{param}} too — a rule's paths: and pattern, an agent's or a command's description — a value that would break the file it lands in fails the plan, and forge diff and forge unify work on those fields; every craftar before 0.47.0 refuses a schema: 9 Forge.

F4 working

Workspace bootstrap

problem · P2Starting with a new team means reassembling repos, harness, templates and local services from memory.

$ craftar init --forge ../forge --profile team-a --targets claude-code,kiro # writes craftar.yaml, then runs the first sync $ craftar init # in a terminal: asks for the Forge, the profile, then the recipes and targets; confirms once $ craftar init --forge ../forge --new-profile team-e --like team-a # writes profiles/team-e/profile.yaml (not committed), then the workspace $ craftar repos · craftar repos clone # the profile's repositories, present or missing — then clone the missing ones

Writes craftar.yaml — the Forge, the profile, and the recipe and target choices, given as flags or as answers — only once the workspace resolves and plans, then runs the first sync, which registers the workspace. Files already on disk are adopted when they equal the plan and otherwise left as they are and reported; an existing craftar.yaml is never edited. In a terminal, craftar init asks for a --forge or a --profile it was not given — the Forge last synced on this machine offered, the profile from the Forge's list, then the recipes and targets to keep or adjust — confirms once before anything is written, and ends by printing the same run as flags, for CI; off a terminal a missing flag is refused. When a declared parameter a file the plan writes cites has no value, craftar.yaml is still written and the first sync is not run. With --clone it then clones each repository the profile's repos lists into the workspace — git clone over https or ssh only, with the git found on PATH, never over a file sync writes, into the Forge or out of the workspace; when init asks for what the flags do not give, it lists the URLs first and asks, the default no. A clone is recorded nowhere, and sync never updates or removes it. craftar repos lists those repositories in an existing workspace — present, missing or blocked, with their URLs — and craftar repos clone clones the missing ones. With --new-profile <name> (optionally --like <profile>), init first writes one new file in a Forge directory, profiles/<name>/profile.yaml — the recipes and targets chosen, nothing else of the template — behind a git gate and never committed, then starts the workspace on it; in a terminal the profile list offers 0) a new profile.

F5 working

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.

In a Forge that declares schema: 3 or above, a profile's integrations block selects recipes and gives params. integrations.<slot>.kind: <kind> selects the recipe named <slot>-<kind> — integrations.pm.kind: azure-devops selects pm-azure-devops, a recipe that lists, say, the MCP ingredient and a rule describing where stories live — and every scalar of the block is the param integrations.<slot>.<key>, cited in a body as {{integrations.pm.org}}. An integration with no kind gives params only. A schema 3 Forge is read only by craftar 0.27.0 and later; below schema: 3 the block is ignored, without a warning; a kind with no recipe stops sync. In a Forge that declares schema: 4, an MCP server takes the params its ingredient declares, so one server serves every client: url: "{{integrations.idp.url}}", with integrations.idp.url declared in the ingredient's params, renders each profile's own URL, and .mcp.json and .kiro/settings/mcp.json get the same rendered server. A declared key with no value stops sync; a key the ingredient does not declare is left as written, with a warning when some layer gives it a value; below schema: 4 a server is written exactly as the Forge holds it. A schema 4 Forge is read only by craftar 0.28.0 and later. Tokens are referenced by authEnv, never stored (FM-8).

F6 working

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 # Docker Compose on what the lock records

A service ingredient is a Docker Compose fragment plus the files it mounts. sync writes it under .craftar/services/<name>/, with the stack .craftar/compose.yaml — a Compose include of every fragment — for no target, text always LF without a BOM (a file listed as binary is copied as bytes), and records each file in craftar.lock (schema 3 only for a workspace that has a service). They are generated files like any other: status, diff and sync --check treat them the same. craftar services up reads the lock, not the Forge, and refuses to start a stack with a missing or hand-edited file, or with a stack or fragment the lock does not record; down stops it and status shows it. sync never runs Docker; up needs Docker Compose 2.20.0 or later. A Forge that holds a service ingredient is read only by craftar 0.25.0 and later.

F7 list | install working

Project templates

problem · P1New projects should use the team's own dotnet new (or npm create, nx, cookiecutter) templates; agents need to know which.

$ craftar templates list | install # the dotnet new templates the profile declares$ /create-project # planned

A profile's templates block lists packages per kind — dotnet-new today — with an optional source feed. craftar templates list shows what the profile declares; craftar templates install runs dotnet new install once per entry, into the user's own template store. Nothing installs implicitly — not sync, not init, not doctor. An entry of a kind this CLI cannot install (npm-create, nx) is listed, skipped and warned. A source carrying a user or a password is refused; authentication is NuGet's. Planned: a generated rule mapping archetype → template, and a wrapping /create-project command.

F8 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 # the hand-edited files, whether the Forge also changed each, whether the edit can be promoted; with a path, its diff $ craftar drift discard <path...> # a sync that also regenerates the named hand-edited files; their edits are lost $ craftar drift promote <path> # carry one hand edit of a .claude/ file into the Forge

Normalized hashes from the lock are compared with disk. drift discard is a sync that also regenerates the named hand-edited files and removes a named hand-edited file the Forge no longer produces. drift promote carries one hand edit of a .claude/ file into the Forge's working tree — a value in the profile's params or sections when the edit is only that, otherwise a variant for the workspace's profile — gated on git and proved before the first write; it never syncs and never commits. A file that holds an expanded rules index is not promotable: edit the Forge's rule, or the description of the rules it lists. So is a hand edit of a metadata field the Forge renders from a parameter, at schema: 9; drift show says why. At schema: 8, a hand edit of a profile-local file is promoted into the local itself: a value inference proves goes to the profile's params or sections, otherwise the local's own files are edited in place, with no variant. Sync direction is Forge → workspace; reverse flow is always explicit.

F9 working

CI

problem · P2Sync that depends on someone remembering will lag by workspace.

$ craftar sync --check # exit ≠ 0 when out of sync or drifted$ craftar diff --exit-code # the same verdict as --check, with the diff that explains it$ craftar sync --check --committed [rev] · --check --index # the same check over a commit or git's index$ craftar git-hooks install [--pre-push] [--pre-commit] # an optional hook that runs it, only when typeduses: llima/craftar-cli/ci/github@<SHA or v-tag> # GitHub Action · Azure: template ci/azure/check.yml@craftar

AGENTS.md has a byte budget, agents-md.maxBytes (default 32768, Codex's project_doc_max_bytes; 0 turns it off): over it every plan warns, naming the largest rules, and when a recipe, the profile or the workspace sets it, sync --check, status and diff --exit-code with no path fail — a writing sync still writes. Two thin wrappers in the CLI's repository make a pull request fail when a workspace is out of step with its Forge: a GitHub Action (ci/github/action.yml) and an Azure Pipelines step template (ci/azure/check.yml). Each installs an exact craftar version, outside the checkout, runs sync --check — its exit code is the step's — and names the files out of sync as annotations and a job summary, never a file's content; they write nothing to the workspace. A private Forge's token comes only from the workflow, for an origin the workflow names. Pin them to a commit SHA or a v<version> tag: a change under ci/ reaches a branch reference the moment it merges. The action ran on GitHub's Linux and Windows runners; the Azure template has not run on a real agent. craftar sync --check --committed [rev] and --check --index give the same verdict over what git holds — a commit or the index — instead of the folder, writing nothing and starting no hook, filter or core.fsmonitor of the repository. craftar git-hooks install writes, only when typed, a pre-push (or pre-commit) hook that runs that check, only inside the repository, starting only the craftar on PATH; uninstall and status complete it.

F10 working

Registry and dashboard

problem · P2With a dozen workspaces there is no view of which is behind, drifted or on which Forge version.

$ craftar workspaces [--fetch] # every registered workspace, its status computed now$ craftar workspaces forget <dir> · craftar workspaces prune$ craftar ui [--port <n>] [--allow-writes] # a local dashboard on 127.0.0.1, read-only unless --allow-writes

A writing sync records its workspace in ~/.craftar/registry.json. craftar workspaces prints each one — profile, stack, targets, Forge, last sync — with a status computed on the spot: up to date, outdated, drift, no lock, missing or error, plus forge moved when the Forge's commit is no longer the lock's; it reads a remote Forge from the cache unless --fetch is given. CRAFTAR_NO_REGISTRY turns the registry off, for CI runners and throwaway checkouts. From the Forge's side, craftar forge impact lists every registered workspace that reads it — by its path, by a git remote, or through another clone — with what its next sync would do; a workspace synced on CI, on another machine or with the registry off is not in it. craftar ui serves these views as a local page in a browser: the table of craftar workspaces and, for a registered workspace, what status, diff and explain say about it and what recipes and ingredients say about its Forge. Without --allow-writes it is read-only — it writes nothing (the Forge cache is used as every reader uses it) and never runs sync: it prints the command to copy. It reaches no network either way: a remote Forge is read from the cached copy, and the page says so. Started with --allow-writes — typing the flag is the consent — the page also offers Sync for a workspace and Discard edit for one hand-edited file (drift or orphan-drift, from its open diff), each confirmed in the page: a sync that never passes --overwrite-drift — every hand edit and collision is skipped and listed — or a craftar drift discard of that one path. Neither is offered for .claude/settings.craftar.example.json or for a workspace whose Forge is remote, and either is refused when the workspace changed since the page showed it. It listens on 127.0.0.1 only (port 4747 by default; --port 0 takes a free one), answers GET only — with --allow-writes, also POST to its two writing routes, from its own address — and every answer that carries data needs a token generated for that run, carried in the address it prints — whoever holds that address can read what the reading commands print, so open it only in a browser of the same user on the same machine, exactly as printed. It takes no --workspace, and exits 1 when the registry is off or cannot be read. craftar ls stays the view of one workspace's resolution.

F11 working

Craftar as an MCP server

problem · P1 · P2The agent itself should be able to ask what is pending, why a file exists and what the Forge offers without leaving the session — and say "run craftar sync" instead of editing a generated file.

$ craftar mcp [--workspace <dir>] [--fetch] [--allow-writes] # started by the AI coder, over stdin and stdout> /plugin marketplace add llima/craftar-cli · /plugin install craftar@craftar # in Claude Code: the /craftar skill and the reading tools$ craftar mcp allow-writes · craftar mcp revoke-writes # the consent, typed by a person in a terminal, per workspace

An MCP server the AI coder starts — registered in its own MCP configuration, or for a whole team as an ordinary mcp ingredient that sync writes like any other server. It serves the one workspace it was started in through seven tools that only read — status, diff, explain, recipes, ingredients, targets, doctor — each answering with the data its command prints, so the agent can say "run craftar sync" instead of editing a generated file. Without --allow-writes it writes nothing — no workspace file, lock, registry or Forge (the Forge cache is used as every reader uses it) — reads no network unless started with --fetch, and never returns a hand-edited MCP configuration file; a result can carry the Forge's profile names, so register it only where that is acceptable. Started with --allow-writes, it also serves sync, add_recipe and remove_recipe. The flag grants nothing by itself: a writing call acts only in a workspace where a person typed craftar mcp allow-writes in a terminal, for the Forge, ref and profile the server is pinned to — the shell tools of Claude Code and Kiro give a program no terminal, so an agent cannot grant it through them (one that drives a real terminal could) — and craftar mcp revoke-writes takes it back without a restart. The sync tool never overwrites or removes a hand edit: every drift, orphan-drift and collision is skipped and listed, for a person to resolve with drift discard or drift promote, and a remote Forge needs --fetch; add_recipe and remove_recipe never sync. A result carries what the commands print, never a file's content. Upgrade every machine to craftar 0.29.0 or later before adding it to a shared Forge, and to 0.35.0 or later before registering it with --allow-writes. In Claude Code, a plugin in the CLI's repository holds a /craftar skill and starts the craftar on PATH — install 0.40.1 or later — as craftar mcp with its reading tools only — never --allow-writes, never --fetch; in a project with no craftar.yaml it does not start. When craftar comes from a version manager's shim (asdf, volta, mise once trusted), a project's files can choose what runs when the server starts. Planned: an Agent Plugin.

F12 working

Diagnostics

problem · P2Sync failures usually come from the environment: missing env vars, wrong Node, no Docker, unreachable Forge.

$ craftar doctor [--fetch] [--strict] [--json] # one line per finding, each with its fix$ craftar docs validate [path] [--fetch] [--strict] [--json] # the workspace's Markdown: links, leaked tool-call tags, encoding, the profile's docs: block

Checks this machine — Node ≥ 22, git, ~/.craftar, the registry, the Forge cache — and, inside a workspace, its configuration and Forge, the plan's warnings, declared parameters with no value (an error when a file the plan writes cites one, a warn when nothing does), the variables a written MCP server declares in authEnv or references as ${NAME} and the environment lacks (names only, never values), the lock, status and registration, and — in a workspace with a local service — Docker with its Compose plugin and the variables a service fragment needs that nothing sets (names only), and — when the profile declares templates — the templates block and, for a dotnet-new entry, the .NET SDK — and the planned AGENTS.md against its byte budget — and, under agents-md.primary, a CLAUDE.md, CLAUDE.local.md or .claude/CLAUDE.md — at the root or in a directory above it — that keeps Claude Code from reading AGENTS.md and the Kiro agents' hand-written resources — and, when the plan writes a hook's registration, a planned handler that is in no settings file Claude Code reads (at most a warn, so --strict exits 1 until the block is copied) — and, when claude-code writes a skill as one file, that Claude Code (measured with 2.1.310) does not list it (a warn per skill, with the fix, so --strict exits 1; the file is still written) — and, in a git repository, a generated file git ignores (generated-ignored, a warn, unless the path is listed under mayBeIgnored) — and a hook craftar git-hooks wrote (git-hooks) — and a path that leaves the workspace through a symbolic link (workspace-links, an error). Each finding is one line — ok, warn or error — with the command that fixes it. It only reports: nothing is written to a workspace, a lock or the registry, and a remote Forge is read from the cache unless --fetch is given. It exits 1 on an error, or on a warn under --strict; --json is a contract for tooling. craftar docs validate checks a workspace's Markdown under docs/, .claude/ and at its root — links that resolve inside the workspace, tool-call tags leaked outside code, files that are not UTF-8 text and, where the profile declares a docs: block, its categories, required frontmatter, dated file names and related: entries — one line per finding, naming a file, a line and a column, never a line of the file; it writes nothing and reads without the network unless --fetch, which concerns the Forge only. Planned: a rulesync check once it is a dependency.

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 one marked planned is a design commitment, 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, and diff shows an orphan as the removal of the whole file (a hand-edited one as a header, since sync keeps it).

← 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 (in a schema 4 Forge, inside an MCP server too; at any schema, inside a hook's registration, for the keys the hook declares). A parameter an ingredient declares with no default, that a file the plan writes cites and no layer sets, stops sync: nothing is written until it has a value.

← 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

Store a secret

An MCP ingredient declares authEnv — the environment variables its server needs, names only, never a value; an item shaped like a known token fails the load. With claude-code, sync writes .claude/settings.craftar.example.json, each declared name with an empty value, for the developer to copy into their own untracked .claude/settings.json. craftar doctor names the variables a written MCP server declares in authEnv or references as ${NAME} and the environment lacks, never their values; craftar import refuses ingredients holding secret-like values, and a forge: URL holding credentials is refused. In a schema 4 Forge, a value rendered into an MCP server that is a URL with credentials or a token of a known format fails the plan, and the message names the field, never the value — a check, not a guarantee: a token of an unknown format still passes. A literal token in a hook's registration fails the Forge load, and a credential substituted into one fails the plan. A Forge file that is not valid YAML is reported by an error code and a position at most, never by quoting the line; one its schema refuses is reported by key, never by a value it holds.

← FM-8

Targets

Capability matrix.

craftar targets prints this matrix from the CLI's own table; sync decides from the same table which ingredients a target does not write, and a test fails when a cell and its emitter disagree. Nothing is dropped silently: an ingredient aimed at a target that cannot express it is skipped with a warning that names it; a fallback is planned.

Ingredient typeclaude-codekiroagents-md
rulenative
.claude/rules/<name>.md
converted
.kiro/steering/<name>.md
converted
AGENTS.md
agentnative
.claude/agents/<name>.md
converted
.kiro/agents/<name>.json
unsupported
commandnative
.claude/commands/<name>.md
converted
.kiro/steering/commands/<name>.md
unsupported
skillnative
.claude/skills/<name>/<file> · .claude/skills/<name>.md
converted
.kiro/skills/<name>/<file>
unsupported
mcpnative
.mcp.json · .claude/settings.craftar.example.json
native
.kiro/settings/mcp.json
unsupported
scriptnative
.claude/scripts/<file>
unsupportedunsupported
steeringunsupportednative
.kiro/steering/<name>.md
unsupported
hooknative
.claude/hooks/<file> · .claude/settings.craftar.example.json
unsupportedunsupported
serviceunsupportedunsupportedunsupported
native — written in the tool's own place for that type, as the Forge holds itconverted — written in another form: frontmatter or a banner added, references rewritten, another container format, or folded into a shared fileunsupported — not written; sync warns and names the ingredientservice is aimed at no AI coder: sync writes it under .craftar/ for no target.craftar targets also prints what each converted cell changes.
Status

Phase 0 is done; phases 1 and 2 — the client layer and the environment — are in progress.

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 0done

Foundations

  • Forge schema: ingredients, recipes, profiles
  • Layer resolver with exclusive slots and params
  • sync, diff, status, explain, ls, lockfile, drift and orphans
  • import --from claude-code, reusing a base whose render matches when a second client joins the Forge
  • forge variants, forge diff, forge unify (base, variant, take: param or take: section)
  • Sections: client-specific blocks in ingredient bodies, overridden per profile and workspace, each client's content inferred by import or extracted from a variant by forge unify
  • 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 1in progress

Client profile

  • The read-only catalogue: recipes, ingredients and targets with the capability matrix
  • Remote Forge: a git URL and ref, a per-machine cache (craftar cache prune), the schema 2 lock
  • Workspace registry and craftar workspaces
  • add recipe / remove recipe, editing the workspace's recipe choice
  • craftar init with flags
  • craftar doctor: what is off on the machine and in the workspace
  • forge impact; forge unify planning the registered workspaces before and after, --prune-recipes; forge promote
  • An interactive craftar init: choose the Forge and the profile
  • authEnv on MCP ingredients: the environment variables a server needs, by name, and the generated .claude/settings.craftar.example.json
  • craftar drift: show, discard, promote
  • A sync refused when a declared parameter has no value
  • craftar import --report: a Markdown report of an import
  • craftar import --prefer-kiro: a rule's source taken from its Kiro steering file
  • A profile's integrations block: PM tool and platform as recipes selected by the profile
  • {{param}} inside an MCP server
  • craftar mcp: a read-only MCP server, seven reading tools for an AI coder
  • craftar ui: a local, read-only dashboard of the registered workspaces, on 127.0.0.1 only
  • Cloning a profile's repositories: init --clone, craftar repos
  • A new profile from craftar init: --new-profile, --like
  • Writing tools for craftar mcp: --allow-writes — sync, add_recipe, remove_recipe, behind a consent typed per workspace
  • Writing actions in craftar ui: --allow-writes — Sync and Discard edit, confirmed in the page
  • The Claude Code plugin of craftar mcp: plugins/craftar/ — a /craftar skill and the reading tools
  • The CI wrappers: ci/github/action.yml, ci/azure/check.yml — sync --check as a pull request's gate
Phase 2in progress

Environment

  • Local services: service ingredients, .craftar/, craftar services up | down | status
  • craftar templates list | install: dotnet new project templates declared in a profile
  • craftar docs validate: links, leaked tool-call tags and encoding always, and a profile's declared docs: taxonomy
Not in a phase

Later

  • A rulesync bridge for Codex, Kimi and Cursor specifics
  • The Agent Plugin packaging of craftar mcp

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
  • 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. Import a second workspace into the same Forge under another profile, and craftar forge variants --forge <dir> lists where the two differ. To start an empty workspace from a Forge instead, run craftar init --forge <dir> --profile <name>: it writes craftar.yaml and runs the first sync — or, in a terminal, run craftar init alone and answer its questions. When something fails, craftar doctor lists what is off on the machine and in the workspace, each finding with the command that fixes it.

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.