craftar 0.36.0 → 0.37.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  Craft, sync and convert AI-coding workspace harnesses — rules, agents, commands, skills, MCP servers — across every client workspace you maintain and every AI coder each team uses.
4
4
 
5
- > Status: **phase 1 in progress** — phase 0, the foundations, is done. `import`, `sync`, `status`, `diff`, `explain`, `ls` work end-to-end for the `claude-code` and `kiro` targets and are validated byte-for-byte against a real workspace (see *Oracle*). The Forge commands `forge variants`, `forge diff` (with a suggested class per hunk) and `forge unify` (`--take base|variant`, `take: param` to turn a value into a `{{key}}` parameter, and `take: section` to turn a client block into a section) and `forge promote` (a profile's variant becomes the base) also work, covered by the unit and golden suites rather than the oracle, and `import` is render-aware: importing into an existing Forge reuses a base whose render equals the workspace file, and edits an existing profile and the recipes it owns in place. Ingredient bodies can hold client-specific **sections** (`<!-- craftar:section <name> -->` … `<!-- /craftar:section -->`), overridden by a profile's `sections` and a workspace's `overrides.sections`; import infers each client's content and `forge unify` can extract a section from a variant, and a Forge that uses them declares `schema: 2` or above (see *Forge layout*). The read-only catalogue — `recipes`, `ingredients` and `targets` with the capability matrix — works too, and a workspace can name its Forge by git URL with an optional `ref`, read from a per-machine cache, with a lock that records the ref, commit, recipes and targets it was generated from (see *Remote Forge*). `add recipe` / `remove recipe` change a workspace's recipe choice in `craftar.yaml`, refusing an edit that would not resolve. A writing `sync` records each workspace in a per-machine registry, and `craftar workspaces` lists them all with their status (see *Workspace registry*). `craftar forge impact` lists the workspaces registered on this machine that read a Forge (by path, by a git remote of the clone, or through another clone of the same remote) with what their next sync would do against it, and `forge unify` plans them before and after its writes, names the ones its warnings concern, and with `--prune-recipes` deletes a recipe left identical to its sibling once every one of them plans the same. `craftar doctor` reports, one line per finding, what is off on this machine and in this workspace. `craftar cache prune` removes the cache entries and trees nothing uses. An MCP ingredient can declare `authEnv`, the environment variables its server needs (names only, never a value), and `craftar doctor` checks them; with `claude-code`, `sync` writes an example file listing them. A recipe can bring **local services**: a `service` ingredient is a Docker Compose fragment that `sync` writes under `.craftar/`, and `craftar services up | down | status` run Compose on what the lock records (see *Local services*). A profile's `integrations` block can select recipes and give params in a Forge that declares `schema: 3` or above (see *Integrations*), since `0.27.0`. `craftar templates list | install` shows and runs the profile's `templates` block — `install` runs `dotnet new install` per entry into the user's template store; nothing installs implicitly. An MCP `server` takes the params its ingredient declares, in a Forge that declares `schema: 4` (see *Server params*), since `0.28.0`. `craftar mcp` serves a workspace to an AI coder over the Model Context Protocol — seven reading tools with the data of `status`, `diff`, `explain`, `recipes`, `ingredients`, `targets` and `doctor`; started with `--allow-writes`, it also serves `sync`, `add_recipe` and `remove_recipe`, which write only in a workspace where a person typed `craftar mcp allow-writes` and never discard a hand edit. `craftar ui` serves a local, read-only dashboard of every registered workspace — the table of `craftar workspaces`, and for one workspace its file states, a file's diff and explanation, and its Forge's recipes and ingredients — on `127.0.0.1` only. `craftar init --clone` and `craftar repos clone` clone a profile's repositories (its `repos` list) into the workspace, and `craftar repos` lists them. `craftar init --new-profile` creates a profile in a Forge directory (one file, not committed) and starts the workspace on it. With `agents-md.primary`, in a Forge that declares `schema: 5`, an always-on rule lives in `AGENTS.md` alone, and the files that cite it follow it (see *Targets*).
5
+ > Status: **phase 1 in progress** — phase 0, the foundations, is done. `import`, `sync`, `status`, `diff`, `explain`, `ls` work end-to-end for the `claude-code` and `kiro` targets and are validated byte-for-byte against a real workspace (see *Oracle*). The Forge commands `forge variants`, `forge diff` (with a suggested class per hunk) and `forge unify` (`--take base|variant`, `take: param` to turn a value into a `{{key}}` parameter, and `take: section` to turn a client block into a section) and `forge promote` (a profile's variant becomes the base) also work, covered by the unit and golden suites rather than the oracle, and `import` is render-aware: importing into an existing Forge reuses a base whose render equals the workspace file, and edits an existing profile and the recipes it owns in place. Ingredient bodies can hold client-specific **sections** (`<!-- craftar:section <name> -->` … `<!-- /craftar:section -->`), overridden by a profile's `sections` and a workspace's `overrides.sections`; import infers each client's content and `forge unify` can extract a section from a variant, and a Forge that uses them declares `schema: 2` or above (see *Forge layout*). The read-only catalogue — `recipes`, `ingredients` and `targets` with the capability matrix — works too, and a workspace can name its Forge by git URL with an optional `ref`, read from a per-machine cache, with a lock that records the ref, commit, recipes and targets it was generated from (see *Remote Forge*). `add recipe` / `remove recipe` change a workspace's recipe choice in `craftar.yaml`, refusing an edit that would not resolve. A writing `sync` records each workspace in a per-machine registry, and `craftar workspaces` lists them all with their status (see *Workspace registry*). `craftar forge impact` lists the workspaces registered on this machine that read a Forge (by path, by a git remote of the clone, or through another clone of the same remote) with what their next sync would do against it, and `forge unify` plans them before and after its writes, names the ones its warnings concern, and with `--prune-recipes` deletes a recipe left identical to its sibling once every one of them plans the same. `craftar doctor` reports, one line per finding, what is off on this machine and in this workspace. `craftar cache prune` removes the cache entries and trees nothing uses. An MCP ingredient can declare `authEnv`, the environment variables its server needs (names only, never a value), and `craftar doctor` checks them; with `claude-code`, `sync` writes an example file listing them. A recipe can bring **local services**: a `service` ingredient is a Docker Compose fragment that `sync` writes under `.craftar/`, and `craftar services up | down | status` run Compose on what the lock records (see *Local services*). A profile's `integrations` block can select recipes and give params in a Forge that declares `schema: 3` or above (see *Integrations*), since `0.27.0`. `craftar templates list | install` shows and runs the profile's `templates` block — `install` runs `dotnet new install` per entry into the user's template store; nothing installs implicitly. An MCP `server` takes the params its ingredient declares, in a Forge that declares `schema: 4` (see *Server params*), since `0.28.0`. `craftar mcp` serves a workspace to an AI coder over the Model Context Protocol — seven reading tools with the data of `status`, `diff`, `explain`, `recipes`, `ingredients`, `targets` and `doctor`; started with `--allow-writes`, it also serves `sync`, `add_recipe` and `remove_recipe`, which write only in a workspace where a person typed `craftar mcp allow-writes` and never discard a hand edit. `craftar ui` serves a local, read-only dashboard of every registered workspace — the table of `craftar workspaces`, and for one workspace its file states, a file's diff and explanation, and its Forge's recipes and ingredients — on `127.0.0.1` only; started with `--allow-writes`, it also syncs a workspace or discards one hand-edited file, each confirmed in the page and never with `--overwrite-drift`. `craftar init --clone` and `craftar repos clone` clone a profile's repositories (its `repos` list) into the workspace, and `craftar repos` lists them. `craftar init --new-profile` creates a profile in a Forge directory (one file, not committed) and starts the workspace on it. With `agents-md.primary`, in a Forge that declares `schema: 5`, an always-on rule lives in `AGENTS.md` alone, and the files that cite it follow it (see *Targets*).
6
6
 
7
7
  ## The idea in one paragraph
8
8
 
@@ -90,7 +90,7 @@ From then on, change a rule in `forge/ingredients/rules/<name>/rule.md`, run `cr
90
90
  | `craftar explain <path>` | Which ingredient, recipe chain, target and origin produced a file and, for an ingredient with sections, which layer filled each one: `sections flavors (profile globex), extra (default)` (`default`, `profile <p>` or `workspace`). For an ingredient whose files cite parameters, each cited key with the layer that filled it for this ingredient: `params org (profile globex), port (default), root (unset)` — `default` (the ingredient's own), `recipe <name>`, `profile <p>`, `workspace` (either workspace file) or `unset`. `AGENTS.md` gets neither line: it is not one ingredient. |
91
91
  | `craftar ls` | Recipes and ingredients resolved for this workspace. |
92
92
  | `craftar workspaces [--fetch] [--json]` | Every workspace a writing `sync` registered on this machine, sorted by path: name, path, profile, stack (`slot=recipe`), targets, the Forge, the last sync, and a status computed now — `up to date`, `outdated` (`new`, `update` or `orphan` pending), `drift` (a `drift`, `orphan-drift` or `collision`), `no lock`, `missing` (directory or `craftar.yaml` gone) or `error` (it no longer loads, plans or reads its lock, or its `sync` would refuse for a declared parameter with no value — the reason is one of the row's warnings: `sync refused: declared parameter(s) with no value: <keys>`), plus `forge moved` when the Forge's commit differs from the lock's. Read without the network: a remote Forge comes from the cache; `--fetch` fetches first. Each row's warnings print after the table, prefixed with its path. Times are UTC. Writes nothing. Exits 0 whenever the table prints. Takes no `--workspace`. `--json` is a contract: `{registry, fetch, workspaces, warnings}`. |
93
- | `craftar doctor [--workspace <dir>] [--fetch] [--strict] [--json]` | Checks this machine — Node ≥ 22, `git`, `$CRAFTAR_HOME`, the registry (entries whose workspace is gone), the Forge cache (its size, entries nothing names, a fetch that never completed) — and, when run inside one (the current directory, or `--workspace`), this workspace: the configuration and its Forge (cached, fetched or pinned), whether the workspace sits inside its Forge, the plan's warnings, declared parameters with no value, the `${NAME}` variables a written MCP server expects and this environment lacks — `mcp-env` reads `authEnv` next to `${NAME}` (names only, never values), `templates` when the profile declares a `templates` block (an empty `templates:` line declares none), `dotnet` when that block is accepted and declares a `dotnet-new` entry (the .NET SDK answering `dotnet --version` at the workspace root); neither goes above `warn` — `agents-md-size` when the plan writes `AGENTS.md` (`ok` within the budget or with the budget off, `warn` over the default budget, `error` over an explicit one — the cause is reported there, not by the plan's warnings), under `agents-md.primary` `agents-md-primary` (a `CLAUDE.md` that keeps Claude Code from reading `AGENTS.md`, at the root or above it; at most `warn`) and `kiro-resources` (a hand-written Kiro agent `resources` entry the plan does not write: `error`, or `warn` when the file is only on disk), the lock, `status`, and registration; in a workspace with a service: `docker` — Docker and its Compose plugin, 2.20.0 or later — and `services-env` — the variables a fragment needs. One line per finding — `ok`, `warn` or `error` — each problem with the command that fixes it; each cause is reported once. A declared parameter with no value is an `error` when a file the plan writes cites it (`sync` refuses that workspace) and a `warn` when nothing cites it. It only reports: nothing is written to a workspace, a lock or the registry, and the Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; under `--fetch`, a fetch and the cleanup of trees unused for 14 days). Read without the network; `--fetch` fetches a remote Forge first. Exits 1 on an `error`, or on a `warn` under `--strict`; an explicit `--workspace` without `craftar.yaml` exits 1, while the current directory without one runs the machine checks only. `--json` is a contract: `{version, workspace, fetch, strict, checks: [{id, scope, level, message, fix}], summary}`. In a schema 4 Forge `mcp-env` reads the server as rendered, so a `${NAME}` a param brings into `env` is checked. |
93
+ | `craftar doctor [--workspace <dir>] [--fetch] [--strict] [--json]` | Checks this machine — Node ≥ 22, `git`, `$CRAFTAR_HOME`, the registry (entries whose workspace is gone), the Forge cache (its size, entries nothing names, a fetch that never completed) — and, when run inside one (the current directory, or `--workspace`), this workspace: the configuration and its Forge (cached, fetched or pinned), whether the workspace sits inside its Forge, the plan's warnings, declared parameters with no value, the `${NAME}` variables a written MCP server expects and this environment lacks — `mcp-env` reads `authEnv` next to `${NAME}` (names only, never values), `mcp-launcher` when the plan writes an MCP server (a `warn` per server whose command is `cmd` (or a path to `cmd.exe`) and whose first argument is `/c` or `/k` — it starts on Windows only — naming the server and the files, never its command or arguments; see *MCP servers on Windows and Linux*), `templates` when the profile declares a `templates` block (an empty `templates:` line declares none), `dotnet` when that block is accepted and declares a `dotnet-new` entry (the .NET SDK answering `dotnet --version` at the workspace root); neither goes above `warn` — `agents-md-size` when the plan writes `AGENTS.md` (`ok` within the budget or with the budget off, `warn` over the default budget, `error` over an explicit one — the cause is reported there, not by the plan's warnings), under `agents-md.primary` `agents-md-primary` (a `CLAUDE.md` that keeps Claude Code from reading `AGENTS.md`, at the root or above it; at most `warn`) and `kiro-resources` (a hand-written Kiro agent `resources` entry the plan does not write: `error`, or `warn` when the file is only on disk), the lock, `status`, and registration; in a workspace with a service: `docker` — Docker and its Compose plugin, 2.20.0 or later — and `services-env` — the variables a fragment needs. One line per finding — `ok`, `warn` or `error` — each problem with the command that fixes it; each cause is reported once. A declared parameter with no value is an `error` when a file the plan writes cites it (`sync` refuses that workspace) and a `warn` when nothing cites it. It only reports: nothing is written to a workspace, a lock or the registry, and the Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; under `--fetch`, a fetch and the cleanup of trees unused for 14 days). Read without the network; `--fetch` fetches a remote Forge first. Exits 1 on an `error`, or on a `warn` under `--strict`; an explicit `--workspace` without `craftar.yaml` exits 1, while the current directory without one runs the machine checks only. `--json` is a contract: `{version, workspace, fetch, strict, checks: [{id, scope, level, message, fix}], summary}`. In a schema 4 Forge `mcp-env` reads the server as rendered, so a `${NAME}` a param brings into `env` is checked. |
94
94
  | `craftar templates list [--json] [--workspace <dir>] [--offline]` | What the profile's `templates` block declares: one line per package — kind, package, version or `latest`, source or `default feeds` — and one per entry this craftar cannot install. Runs no program and does not know what is installed. `--json` is a contract: `{profile, templates: [{entry, kind, package, version, source, installable}], warnings}`. Exits 1 when the block is refused, naming the position and the rule, never a value. |
95
95
  | `craftar templates install [--dry-run] [--force] [--interactive] [--workspace <dir>] [--offline]` | Runs `dotnet new install <pkg>[::<version>]... [--add-source <source>]` once per `dotnet-new` entry, in the profile's order, at the workspace root. `--dry-run` prints the exact lines, needs no `dotnet` and exits 0 over an accepted block, even with a skipped entry. A real run exits 1 when the .NET SDK is not available. Both exit 1 when the block is refused or a package ID exists as a file or folder at the workspace root. `--force` and `--interactive` are passed to `dotnet` as typed, never otherwise; `--interactive` also gives `dotnet` the terminal's input. An entry that fails does not stop the others; a real run exits 1 when any failed or was skipped. When an entry declares a `source` and the workspace root has no `NuGet.Config`, a warning line is printed before the runs. Writes nothing in the workspace, the lock or the registry; nothing else (`sync`, `init`, `doctor`) runs it. See *Project templates*. |
96
96
  | `craftar mcp [--workspace <dir>] [--fetch] [--allow-writes]` | An MCP server (Model Context Protocol, revision `2025-06-18`) over stdin and stdout, started by an AI coder as a subprocess — not typed in a terminal, where it refuses to start. It serves **one workspace**, the one it was started in, through seven tools that only read: `status`, `diff` (the pending files; with a `path`, that file's diff), `explain`, `recipes`, `ingredients`, `targets` and `doctor` (the checks about this workspace, plus Node and `git`; like the command, it asks `docker compose` for its version when the workspace has a service and `dotnet` for its version when the profile declares a template it can install) — each answers with the data its command prints, as one JSON object. Without `--allow-writes` nothing is written to the workspace, the lock, `craftar.yaml`, the registry or the Forge. With `--allow-writes` it also serves three writing tools — `sync` (a `craftar sync` that skips and lists every `drift`, `orphan-drift` and `collision`; a remote Forge needs `--fetch`), `add_recipe` and `remove_recipe` (an `add recipe` / `remove recipe`, which do not sync) — and each call writes only when this machine's registry holds the consent `craftar mcp allow-writes` recorded for this workspace, for the `forge`, `ref` and `profile` the server started with; otherwise the call is an error and nothing is written. A writing result carries what the command prints, never a file's content or the registry's path. The Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; under `--fetch`, a fetch and the cleanup of trees unused for 14 days). Without `--fetch` no tool touches the network: a remote Forge is read from the cached copy, and the answer says so — for `doctor` in its `forge` check, for the others but `targets` in `warnings` unless `ref` is a full commit SHA, which cannot go stale (`targets` reads only which targets the configuration resolves, never fetches and carries no such line). The `forge`, `ref` and `profile` of `craftar.yaml` / `craftar.local.yaml` are fixed when the server starts: after one of them changes, every tool but `targets` answers with an error until it is put back or the server is restarted (`targets` answers the matrix unmarked, with a warning). A hand-edited `.mcp.json`, `.kiro/settings/mcp.json` or `.claude/settings.craftar.example.json` is never returned — a note stands in for its diff. A line that is not JSON, or longer than 1 MiB, is not answered — the server reads on. Exits 0 when stdin closes; exits 1, with one line on stderr, in a terminal, without a `craftar.yaml`, or when the configuration does not read. See *Craftar inside the AI coder*. |
@@ -98,7 +98,7 @@ From then on, change a rule in `forge/ingredients/rules/<name>/rule.md`, run `cr
98
98
  | `craftar mcp revoke-writes [--workspace <dir>]` | Removes that consent; needs no terminal; takes effect on the server's next call, without a restart. Exits 0 also when there was none (`no consent recorded for <path>`); exits 1 without a `craftar.yaml`, with `CRAFTAR_NO_REGISTRY` set, or when the registry does not read or cannot be written. |
99
99
  | `craftar cache prune [--dry-run] [--workspace <dir>] [--json]` | Removes the Forge cache entries nothing uses — no registered workspace (`missing` ones included until `craftar workspaces prune`) and not the current workspace — entries whose first fetch never completed (even when named), and trees unused for 14 days. Whole entries are kept with a warning when the registry is off or unreadable, or the current `craftar.yaml` does not read. Deletes by default; `--dry-run` shows. Never touches a workspace, a lock or the registry; no network. One removing prune at a time per `$CRAFTAR_HOME` (`forges/prune.lock`). An explicit `--workspace` without `craftar.yaml` exits 1. `--json` is a contract: `{home, dryRun, removed: [{path, kind, reason, bytes}], kept: [{path, reason, namedBy}], freedBytes, warnings}`. |
100
100
  | `craftar workspaces forget <dir>` / `craftar workspaces prune` | `forget` removes one entry (the directory is not touched; an unregistered path exits 1); `prune` removes every `missing` entry and never an `error` one. Nothing else removes an entry. |
101
- | `craftar ui [--port <n>]` | A local dashboard, 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. It prints one address and serves until interrupted (`Ctrl-C` exits 0). **Read-only**: nothing is written to a workspace, a lock, the registry or a Forge — the Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; `git` runs as for `craftar workspaces`) — and the page never runs `sync` — it prints the command to copy. **Loopback only**: it listens on `127.0.0.1` (no `--host`; `localhost` is refused), answers `GET` only, and every answer that carries data needs a token generated for that run — it travels in the address's fragment, which the browser does not send, so open the address exactly as printed (pasted, not clicked from a web page; a reload keeps working, a new tab needs the address again); it stays in the terminal's scrollback (and in a file, if the output is redirected) and may stay in the browser's history, and is worth nothing once the command stops. **No network**: a remote Forge is read from the cached copy, never fetched, and the page says so. `--port` (default `4747`; `0` takes a free one); a taken port is refused, never replaced. A Forge URL the registry recorded with credentials is shown as `(a URL with credentials — not shown)`, with no cache key — as `craftar workspaces --json` gives it since `0.28.1` (see *Upgrading › to 0.28.1*). Takes no `--workspace`; exits 1 when the registry is off (`CRAFTAR_NO_REGISTRY`) or cannot be read. The page's routes are not a contract. |
101
+ | `craftar ui [--port <n>] [--allow-writes]` | A local dashboard, 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. It prints one address and serves until interrupted (`Ctrl-C` exits 0). **Read-only unless `--allow-writes`**: without the flag nothing is written to a workspace, a lock, the registry or a Forge — the Forge cache is used as every reader uses it (`.used` stamps, a rebuilt tree; `git` runs as for `craftar workspaces`) — and the page never runs `sync` — it prints the command to copy. With `--allow-writes` (typing it 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; never for `.claude/settings.craftar.example.json`, never for a workspace whose Forge is remote, and refused when the workspace changed since the page showed it (refresh and confirm again). **Loopback only**: it listens on `127.0.0.1` (no `--host`; `localhost` is refused), answers `GET` only (and, with `--allow-writes`, `POST` to its two writing routes, with an `Origin` of its own address), and every answer that carries data needs a token generated for that run — it travels in the address's fragment, which the browser does not send, so open the address exactly as printed (pasted, not clicked from a web page; a reload keeps working, a new tab needs the address again); it stays in the terminal's scrollback (and in a file, if the output is redirected) and may stay in the browser's history, and is worth nothing once the command stops. **No network**: a remote Forge is read from the cached copy, never fetched, and the page says so. `--port` (default `4747`; `0` takes a free one); a taken port is refused, never replaced. A Forge URL the registry recorded with credentials is shown as `(a URL with credentials — not shown)`, with no cache key — as `craftar workspaces --json` gives it since `0.28.1` (see *Upgrading › to 0.28.1*). Takes no `--workspace`; exits 1 when the registry is off (`CRAFTAR_NO_REGISTRY`) or cannot be read. The page's routes are not a contract. |
102
102
  | `craftar recipes [--forge <dir> [--profile <name>] \| --workspace <dir>] [--json]` | Every recipe of the Forge, sorted by name: slot, the parents it extends, the ingredients it lists itself and the profiles that reach it. In a workspace (or with `--forge <dir> --profile <name>`) each recipe says whether it is in use and why — `profile` (listed in `recipes` or selected by the profile's `integrations` block), `workspace` (`recipes.add`), `extends <child>` — and a recipe `recipes.remove` names reads `removed by this workspace`. A context that does not resolve (two recipes on one slot, an unknown recipe, a cycle) still lists everything, marks nothing and warns. Read-only: it resolves, never plans. `--json` is a contract: `{forge, context, recipes, warnings}`. |
103
103
  | `craftar ingredients [--recipe <name>] [--type <type>] [--forge <dir> [--profile <name>] \| --workspace <dir>] [--json]` | Every ingredient of the Forge, grouped by type: output name, targets, the recipes that list it, and whether the context uses it or the workspace disables it; a reference a recipe makes to an ingredient the Forge does not hold is listed under `missing`, never dropped. `--recipe <name>` lists what choosing that recipe brings, with the parents it extends, parents first; two of them on one slot is a warning. `--type` keeps one type. Read-only. `--json` is a contract: `{forge, context, recipe, ingredients, missing, warnings}`. |
104
104
  | `craftar targets [--workspace <dir>] [--json]` | The capability matrix (see *Targets*): what each target does with each ingredient type, the paths it writes and what changes. Needs no Forge; inside a workspace it marks the targets in use, and when that workspace does not load or resolve it prints the matrix unmarked with a warning. An explicit `--workspace <dir>` without a `craftar.yaml` exits 1; the current directory without one just prints the matrix unmarked. `--json` is a contract: `{ingredientTypes, targets, warnings}`. |
@@ -356,6 +356,43 @@ steering = `inclusion` frontmatter + `GENERATED` banner + rule body, with `.clau
356
356
 
357
357
  Conversion of hooks/subagents to other tools is out of scope here: the plan is to delegate that to [rulesync](https://github.com/dyoshikawa/rulesync) rather than reimplement it.
358
358
 
359
+ ## MCP servers on Windows and Linux
360
+
361
+ `.mcp.json` and `.kiro/settings/mcp.json` are committed, and one file serves every machine. Write an `npx` server in
362
+ the **portable form** — the program as `command`, the rest as `args`:
363
+
364
+ ```yaml
365
+ # ingredients/mcp/example/ingredient.yaml
366
+ type: mcp
367
+ name: example
368
+ server:
369
+ command: npx
370
+ args: ["-y", "@acme/example-mcp"]
371
+ ```
372
+
373
+ Not `command: cmd` with `args: ["/c", "npx", …]`: that form was once required on native Windows, starts on Windows only
374
+ (`cmd` does not exist on Linux or macOS), and is no longer needed — a bare `npx` starts on native Windows in both tools
375
+ (measured below). Move every such server to the portable form in the Forge; the next `sync` writes `update` for the two
376
+ files in every workspace of that Forge. craftar never rewrites it for you: `craftar doctor` warns for each one
377
+ (`mcp-launcher`) and `craftar import` warns when it reads one, keeping it as written. `command: "cmd /c npx"` as one
378
+ string starts in neither tool.
379
+
380
+ What was measured, by running each tool (stdio servers; Claude Code read from a project `.mcp.json`, Kiro CLI from the
381
+ workspace `.kiro/settings/mcp.json`):
382
+
383
+ | | Claude Code | Kiro CLI |
384
+ |---|---|---|
385
+ | `command: npx`, `args: ["-y", <package>]`, on native Windows | starts | starts |
386
+ | `command: cmd`, `args: ["/c", "npx", …]`, on native Windows | starts | starts |
387
+ | `command: "cmd /c npx"` (one string), on native Windows | fails | fails |
388
+ | `${VAR}` / `${VAR:-default}` in `command` | expanded | expanded |
389
+ | `${VAR}` / `${VAR:-default}` in `args` | expanded | **not expanded** — the text reaches the program as written |
390
+
391
+ Measured on Linux on 2026-10-23 (Claude Code 2.1.309, Kiro CLI 2.29.1) and on native Windows on 2026-10-24 (Claude Code
392
+ 2.1.310, Kiro CLI 2.33.1, Node 22); the `${VAR:-default}` cases agree on both, and plain `${VAR}` was run on Linux only. The Kiro IDE and macOS were not measured. Later
393
+ versions may differ. So a `${…}` default in `args` works for Claude Code and not for Kiro: keep variables out of `args`
394
+ of a server Kiro writes.
395
+
359
396
  ## Design rules learned from the previous generator
360
397
 
361
398
  These come straight from workspaces that removed an earlier generator, and the code enforces them:
@@ -525,7 +562,7 @@ The server answers for the directory it is started in; a client that starts its
525
562
 
526
563
  Phase 0 — foundations (done): schema, import, sync/status/diff/explain, claude-code + kiro + agents-md targets, lock, drift, orphans; `forge variants`, `forge diff` and `forge unify` (`take: param`, `take: section`); sections.
527
564
 
528
- Phase 1 — client profile (in progress). Shipped: the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock) and `craftar cache prune`; the workspace registry and `craftar workspaces`; `craftar add recipe` / `remove recipe`; `craftar init` with flags; `craftar doctor`; `craftar forge impact`, `forge unify --prune-recipes` and `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` / `drift discard` / `drift promote` (list the files edited by hand, discard an edit, or carry one into the Forge as a profile value or a variant); a sync refused when a declared parameter has no value; `craftar import --report` (a Markdown report of an import, as paths and line counts) and `import --prefer-kiro` (a rule's source taken from its Kiro steering file); a profile's `integrations` block (PM tool and IDP 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, over stdin and stdout); `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).
565
+ Phase 1 — client profile (in progress). Shipped: the read-only catalogue (`recipes`, `ingredients`, `targets` with the capability matrix); remote Forge (git URL + `ref`, a per-machine cache, the schema 2 lock) and `craftar cache prune`; the workspace registry and `craftar workspaces`; `craftar add recipe` / `remove recipe`; `craftar init` with flags; `craftar doctor`; `craftar forge impact`, `forge unify --prune-recipes` and `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` / `drift discard` / `drift promote` (list the files edited by hand, discard an edit, or carry one into the Forge as a profile value or a variant); a sync refused when a declared parameter has no value; `craftar import --report` (a Markdown report of an import, as paths and line counts) and `import --prefer-kiro` (a rule's source taken from its Kiro steering file); a profile's `integrations` block (PM tool and IDP 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, over stdin and stdout); `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).
529
566
 
530
567
  Phase 2 — environment (in progress). Shipped: local services (`service` ingredients, `.craftar/`, `craftar services up | down | status`); `craftar templates list | install` (`dotnet new` project templates declared in a profile).
531
568
 
@@ -533,6 +570,18 @@ Later: rulesync bridge for Codex/Kimi/Cursor specifics; `craftar docs validate`;
533
570
 
534
571
  ## Upgrading
535
572
 
573
+ ### to 0.37.1
574
+
575
+ - **`craftar doctor` gains the check `mcp-launcher`**, present when the plan writes an MCP server: `ok`, or a `warn` per server whose command is `cmd` (or a path to `cmd.exe`) and whose first argument is `/c` or `/k` (it starts on Windows only — see *MCP servers on Windows and Linux*). A workspace that writes such a server now has a warning, so **`doctor --strict` exits 1 where it exited 0**; move the server to the portable form in the Forge. `doctor --json` gains the id; the `doctor` tool of `craftar mcp` returns it too.
576
+ - **`craftar import` warns** when it reads such a server from `.mcp.json`, and writes it as read.
577
+ - No emitted byte, lock, registry entry or `--json` shape changes, and no exit code but `doctor --strict`'s.
578
+
579
+ ### to 0.37.0
580
+
581
+ - **New flag: `craftar ui --allow-writes`** (see *Commands*). Without it, `craftar ui` is the read-only dashboard — the page, its script and stylesheet and the three terminal lines byte for byte, and every answer too except one: `GET /api/workspaces` gains the key `writes` (`false`). With it the terminal's first line reads `writes allowed` in place of `read-only`, a second line names the two actions, and the page offers Sync and Discard edit for a workspace with a path Forge.
582
+ - **The `ui` command's `--help` description now reads `Reads only unless --allow-writes`.**
583
+ - No emitted byte, lock, registry entry, `--json` shape or exit code of an existing command changes.
584
+
536
585
  ### to 0.36.0
537
586
 
538
587
  - **A Forge can declare `schema: 5`**, which includes 4, 3 and 2. Every craftar before `0.36.0` refuses such a Forge at `craftar.forge.yaml`, so no older craftar writes the three copies of a rule the mode keeps in `AGENTS.md` alone. Upgrade every machine and CI that reads the Forge before setting it. Nothing changes for a Forge at 1 to 4.
package/dist/cli.js CHANGED
@@ -24,7 +24,7 @@ import { canonicalValue } from "./core/sections.js";
24
24
  import { renderDiff, NO_EOF_NEWLINE_MARKER } from "./core/diff.js";
25
25
  import { preferredLine, renderImportReport } from "./core/import-report.js";
26
26
  import { driftList, unmanaged } from "./core/drift.js";
27
- import { EXAMPLE_CONTENT_HIDDEN, diffReport, explainFile, explainSkip, forgeLine, nextSyncLine, notProduced, readText, statusPath, statusReport, targetsReport } from "./core/report.js";
27
+ import { diffReport, explainFile, explainSkip, forgeLine, nextSyncLine, notProduced, orphanDriftBody, statusPath, statusReport, targetsReport } from "./core/report.js";
28
28
  import { diffIngredients, listVariants, profileOf } from "./core/variants.js";
29
29
  import { exists, gitDirty, gitIgnored, gitIsRepo, gitUnheld, loadForge, realpathOfNearest } from "./core/forge.js";
30
30
  import { applyBasePromotion, planBasePromotion, proveBasePromotion } from "./core/promote.js";
@@ -40,10 +40,9 @@ import { findGit, cloneRepos, listRepos, planClone } from "./core/clone.js";
40
40
  import { byHand, profileRepos } from "./core/repos.js";
41
41
  import { readlineIo } from "./prompt.js";
42
42
  import { recipeDiffLine } from "./core/recipe-edit.js";
43
- import { syncWorkspace, editRecipes, confirmWrites } from "./core/write-actions.js";
43
+ import { syncWorkspace, editRecipes, confirmWrites, DISCARDED_ORPHAN } from "./core/write-actions.js";
44
44
  import { readLocalFile } from "./core/workspace-yaml.js";
45
45
  import { HUNK_CLASSES, INGREDIENT_TYPES, UnifyPlanSchema } from "./schema/index.js";
46
- import { EXAMPLE_SETTINGS } from "./emitters/shared.js";
47
46
  import { YamlSyntaxError, parseYamlText } from "./core/yaml-read.js";
48
47
  import { recordedServices, pendingLine, upRefusal, composeArgv, downArgv, readPs, dockerVersion, dockerFault, dockerRunner, projectName, statusReport as servicesStatusReport } from "./core/services.js";
49
48
  import { errorText } from "./core/schema-fault.js";
@@ -59,7 +58,7 @@ process.stdout.on("error", (e) => { if (e.code === "EPIPE")
59
58
  */
60
59
  const DIFF_EXIT_CODE_FETCH_MODE = "sync";
61
60
  const program = new Command();
62
- program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.36.0");
61
+ program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.37.1");
63
62
  const SAVE_PLAN_GATE = { what: "plan", who: "unify", outside: "save plans outside the Forge", never: "unify never overwrites" };
64
63
  const REPORT_GATE = { what: "report", who: "import", outside: "write the report outside the Forge", never: "import never overwrites" };
65
64
  program
@@ -211,7 +210,7 @@ async function applyAndReport(ws, p, st, lock, opts = {}) {
211
210
  for (const f of o.result.written)
212
211
  console.log(` ${pc.green("+")} ${f}`);
213
212
  for (const f of o.result.removed)
214
- console.log(` ${pc.magenta("-")} ${f} (${opts.overwritePaths?.has(f) ? "hand-edited orphan: discarded" : orphanReason(p, st, f)})`);
213
+ console.log(` ${pc.magenta("-")} ${f} (${opts.overwritePaths?.has(f) ? DISCARDED_ORPHAN : orphanReason(p, st, f)})`);
215
214
  for (const s of o.result.skipped)
216
215
  console.log(` ${pc.yellow("!")} ${s.path} ${explainSkip(s)}`);
217
216
  for (const w of p.warnings)
@@ -895,12 +894,12 @@ drift
895
894
  // The whole file as a removal: what `drift discard` would do to it.
896
895
  console.log(pc.bold(`--- ${named.path} (disk, orphan-drift)`));
897
896
  console.log(pc.bold(`+++ ${named.path} (forge: no longer produced — \`craftar drift discard\` removes it)`));
898
- // Spec 27 Ruling 4: the on-disk content of the example file is never printed.
899
- if (named.path === EXAMPLE_SETTINGS) {
900
- console.log(` ${EXAMPLE_CONTENT_HIDDEN}`);
897
+ const { body, note } = await orphanDriftBody(ws.root, named);
898
+ if (note) {
899
+ console.log(` ${note}`);
901
900
  }
902
901
  else {
903
- console.log(renderDiff((await readText(path.join(ws.root, named.path))) ?? "", "", DIFF_PAINT));
902
+ console.log(renderDiff(body ?? "", "", DIFF_PAINT));
904
903
  }
905
904
  }
906
905
  else
@@ -1252,8 +1251,9 @@ workspaces
1252
1251
  /* ---------------------------------------------------------------- ui */
1253
1252
  program
1254
1253
  .command("ui")
1255
- .description("A local dashboard: the table of `craftar workspaces` and, for one workspace, its file states, a file's diff and explanation, and its Forge's recipes and ingredients — in a browser, on 127.0.0.1 only, behind an address that works for this run. Reads only — writes no workspace, lock, registry or Forge — and reaches no network; Ctrl-C stops it")
1254
+ .description("A local dashboard: the table of `craftar workspaces` and, for one workspace, its file states, a file's diff and explanation, and its Forge's recipes and ingredients — in a browser, on 127.0.0.1 only, behind an address that works for this run. Reads only unless --allow-writes — then a sync or the discard of one hand-edited file, confirmed in the page, never --overwrite-drift — and reaches no network; Ctrl-C stops it")
1256
1255
  .option("--port <n>", "the port to listen on, 0 to 65535; 0 takes a free one", "4747")
1256
+ .option("--allow-writes", "serve Sync and Discard edit on the page — a sync of one workspace, or the discard of one hand-edited file, each confirmed in the page; workspaces with a path Forge only")
1257
1257
  .action(async (o) => {
1258
1258
  // Every refusal comes before the bind (spec 36 §4.2).
1259
1259
  const port = parsePort(o.port);
@@ -1268,10 +1268,11 @@ program
1268
1268
  fail(errorText(e));
1269
1269
  }
1270
1270
  const version = program.version() ?? "unknown";
1271
+ const allowWrites = o.allowWrites === true;
1271
1272
  let ui;
1272
1273
  try {
1273
1274
  // The one line a failing request leaves in the terminal; nothing is printed per request otherwise.
1274
- ui = await startUi({ home: craftarHome(), version, port, onError: (line) => console.error(line) });
1275
+ ui = await startUi({ home: craftarHome(), version, port, allowWrites, onError: (line) => console.error(line) });
1275
1276
  }
1276
1277
  catch (e) {
1277
1278
  // The code only, never the message.
@@ -1280,7 +1281,13 @@ program
1280
1281
  fail(`port ${port} is in use on 127.0.0.1 — pass --port <other>, or --port 0 for a free one`);
1281
1282
  fail(`cannot listen on 127.0.0.1:${port}: ${code ?? "unknown"}`);
1282
1283
  }
1283
- console.log(pc.bold(`craftar ui — craftar ${version} · read-only · ${reg.workspaces.length} registered`));
1284
+ if (allowWrites) {
1285
+ console.log(pc.bold(`craftar ui — craftar ${version} · writes allowed · ${reg.workspaces.length} registered`));
1286
+ console.log(" writes allowed: sync, discard one hand-edited file");
1287
+ }
1288
+ else {
1289
+ console.log(pc.bold(`craftar ui — craftar ${version} · read-only · ${reg.workspaces.length} registered`));
1290
+ }
1284
1291
  console.log(` open ${ui.url}`);
1285
1292
  console.log(" the address works for this run only, on this machine — Ctrl-C stops it");
1286
1293
  // The first signal closes the listener and lets the process end; a second one, while closing, ends it at once.
@@ -1300,7 +1307,7 @@ program
1300
1307
  /* ---------------------------------------------------------------- doctor */
1301
1308
  program
1302
1309
  .command("doctor")
1303
- .description("Check this machine (Node, git, $CRAFTAR_HOME, the registry, the Forge cache) and, inside one, this workspace (configuration and Forge, plan, declared parameters, MCP environment variables, the profile's templates block and dotnet when it has one, lock, status, registration): one line per finding — ok, warn or error — each with its fix. Reports only; reads without the network unless --fetch")
1310
+ .description("Check this machine (Node, git, $CRAFTAR_HOME, the registry, the Forge cache) and, inside one, this workspace (configuration and Forge, plan, declared parameters, MCP environment variables, Windows-only MCP launchers, the profile's templates block and dotnet when it has one, lock, status, registration): one line per finding — ok, warn or error — each with its fix. Reports only; reads without the network unless --fetch")
1304
1311
  // No commander default: an explicit --workspace without craftar.yaml exits 1, the current directory without one runs the machine checks (spec 24 §4.1).
1305
1312
  .option("--workspace <dir>", "the workspace to check (default: the current directory, when it holds craftar.yaml)")
1306
1313
  .option("--fetch", "fetch a remote Forge first instead of reading the cached copy", false)