craftar 0.35.0 → 0.36.0

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.
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*).
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), 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), `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*. |
@@ -346,6 +346,14 @@ steering = `inclusion` frontmatter + `GENERATED` banner + rule body, with `.clau
346
346
 
347
347
  **agents-md** — one `AGENTS.md` with the always-on rules concatenated and the scoped rules after them — each listed at the file a target of the workspace writes for it (`.claude/rules/<name>.md` when `claude-code` writes it, else `.kiro/steering/<name>.md` when `kiro` does), or, when no target writes it, embedded in full under a `> Scoped rule — <scope>` line, for tools that read the open standard (Codex, Cursor, Warp, Copilot, Kimi…). A `.claude/rules/<x>.md` reference inside a body follows the rule it names: it is kept when `claude-code` writes that rule, becomes `.kiro/steering/<x>.md` when only `kiro` writes that file (from the rule or from a `steering` ingredient of that name), becomes `AGENTS.md (rule: <x>)` when the rule's text is in this file, and otherwise becomes `<x> (rule not in this workspace)` with a warning (an unknown name is left as written when `claude-code` is a target); a reference inside a link's text is resolved the same way, and a link whose target reads "rule not in this workspace" and whose text is exactly its own path becomes `<x> (rule not in this workspace)` (backticks kept); without `claude-code`, the warning also names references to other `.claude/` files, which are left as written. Every other ingredient type aimed at `agents-md` — including through the default `targets: "*"` — has no `AGENTS.md` equivalent: it is skipped with a warning — one line per type, naming every skipped ingredient. The file keeps the line endings and BOM of the one it replaces; a new one is LF without a BOM. **A size budget, `agents-md.maxBytes`** (default 32768 — Codex's `project_doc_max_bytes`; `0` turns it off), set in one of the layers that give params: a recipe's `params` (its `default` counts as set), the profile's `params`, or `overrides.params` in `craftar.yaml` or `craftar.local.yaml`; a value that is not a whole number of bytes is a warning, and the default applies. The size is the planned `AGENTS.md` in bytes; each rule body the file holds — always-on, and a scoped one embedded because no target writes it — is counted as its UTF-8 bytes after reference resolution and the trailing-newline trim. Over the budget every plan warns, naming at most 5 rules by name and bytes, never their content: `AGENTS.md is N bytes, over the default budget of 32768 (agents-md.maxBytes) — largest rules: a (n bytes), b (scoped, n bytes)`, or, when the budget is set, `… over the budget of M set by agents-md.maxBytes; sync --check fails — …`. Only a budget that is set fails anything: `sync --check`, `status` (and `status --json`, whose keys do not change — the reason is that warning) and `diff --exit-code` with no `[path]` exit 1 and print `error: AGENTS.md is N bytes, over the budget of M set by agents-md.maxBytes` with its fix; a writing `sync` still writes — so it never turns a `craftar workspaces` row into `error` (the table lists the warning under the workspace's path), `forge impact` does not show it, and the `next sync:` line does not count it (`add recipe`, `remove recipe` and `init --no-sync` still print the warning beside it). `doctor` reports it once, as `agents-md-size`.
348
348
 
349
+ **One home for the always-on rules, `agents-md.primary`** (a Forge at `schema: 5`). With `claude-code`, `kiro` and `agents-md` all targets, an always-on rule with the default `targets` is written three times — `.claude/rules/<x>.md`, `.kiro/steering/<x>.md` and its section of `AGENTS.md` — and Claude Code and Kiro each load two of them. Set the param `agents-md.primary: true` (a recipe default, the profile's `params` or `overrides.params`; a YAML boolean) in a Forge that declares `schema: 5` and a workspace whose targets include `agents-md`, and such a rule is written to `AGENTS.md` alone:
350
+ - **Who follows it:** a rule with `inclusion: always` whose `targets` is the default (`"*"`, written or omitted). A rule with a `targets` list keeps it — list `targets: [claude-code, agents-md]` to keep a rule in `.claude/rules/` too. Scoped rules, `steering` and every other type are untouched. The files the mode stops writing become orphans: `sync` removes them (its line reads `orphan: written to AGENTS.md only (agents-md.primary)`, and `diff`'s header `(forge: written to AGENTS.md only — sync removes it)`), and a hand-edited one is kept as an `orphan-drift` until `drift discard` or a delete.
351
+ - **References follow the rule:** in the `.claude/` files the `claude-code` emitter writes — a scoped rule's body, an agent's and a command's body and its `frontmatterRaw` (else its `description`), a skill's `.md` files — a `.claude/rules/<x>.md` reference is resolved as in `AGENTS.md`: `AGENTS.md (rule: <x>)` for a rule held there (a link loses its fragment), `.kiro/steering/<x>.md` for a rule only `kiro` writes. The `kiro` files reword such a reference to `AGENTS.md (rule: <x>)` too. Scripts, hooks and files copied as bytes are never rewritten: a reference there is warned with its file and line (`claude-code: … left as written in files craftar copies: .claude/scripts/lint.sh:2 (style)`), never with the line.
352
+ - **Kiro `resources`:** a generated list names only the steering files the plan writes, and never `file://AGENTS.md` (a custom agent inherits `AGENTS.md` — measured with kiro-cli 2.29.1). A hand-written `file://` entry with no glob under `.kiro/steering/`, `.claude/rules/` or naming `AGENTS.md` is checked: one the plan does not write and the disk lacks fails `sync --check`, `status` and `diff --exit-code` (a writing `sync` still writes); one only on disk is a warning. Measured with kiro-cli 2.29.1: a missing entry is no error to Kiro, and a `manual` steering file is not loaded through `resources` — neither by the glob `file://.kiro/steering/**/*.md` nor named alone.
353
+ - **`CLAUDE.md`:** Claude Code (2.1.277 or later) reads `AGENTS.md` only when no `CLAUDE.md` — at the root, in `.claude/`, or `CLAUDE.local.md` — is in the way, unless one imports it: a line `@AGENTS.md` (or `@./AGENTS.md`) in the root's `CLAUDE.md`, or `@../AGENTS.md` in `.claude/CLAUDE.md` (an import is relative to the file that holds it — measured with Claude Code 2.1.310). craftar never writes a `CLAUDE.md`; when the root holds one that blocks, every plan warns, naming the file and never a line of it. A Claude Code before 2.1.277 reads only `CLAUDE.md` and `.claude/rules/`, and loses the always-on rules without a word from craftar.
354
+ - `doctor` gains `agents-md-primary` (a blocking `CLAUDE.md` at the root, or one in a directory above the workspace — never above `warn`) and `kiro-resources` (`error` for an unplanned entry, `warn` for one on disk only), both only under the mode. Over the `AGENTS.md` budget, the warning adds `these rules are in no other file`. `drift promote` refuses a file the mode reworded (`drift show`: `reworded by agents-md.primary`), and `import` into a Forge that sets the mode compares each base as the mode writes it.
355
+ - Below `schema: 5` the key is not read (set to `true` it warns, and the mode is off); a value that is not a boolean, or the mode with no `agents-md` target, warns and leaves it off. **A workspace that does not set the key sees no change at all.** The key is reserved: `forge unify` never extracts into it and `import` never infers it.
356
+
349
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.
350
358
 
351
359
  ## Design rules learned from the previous generator
@@ -525,6 +533,13 @@ Later: rulesync bridge for Codex/Kimi/Cursor specifics; `craftar docs validate`;
525
533
 
526
534
  ## Upgrading
527
535
 
536
+ ### to 0.36.0
537
+
538
+ - **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.
539
+ - **`agents-md.primary` is opt-in.** A workspace that does not set it sees no change: no byte, no lock entry, no warning, no exit code, `doctor --strict` included. Turning it on changes bytes by design: the `.claude/rules/` and `.kiro/steering/` files of the always-on rules with default targets become orphans that `sync` removes; the `.claude/`, `.kiro/` and `AGENTS.md` texts citing them are reworded; generated Kiro `resources` name only the steering files the plan writes.
540
+ - **`doctor --json` gains the check ids `agents-md-primary` and `kiro-resources`** (additive, present only under the mode). `drift show --json` can give the reason `reworded by agents-md.primary`. The `sync` tool of `craftar mcp` can give the `removed` reason `orphan: written to AGENTS.md only (agents-md.primary)`, the words `craftar sync` prints.
541
+ - **The key `agents-md.primary` is reserved** at every schema: `forge unify` never extracts a profile value into it and `import` never infers it.
542
+
528
543
  ### to 0.35.0
529
544
 
530
545
  - **New flag: `craftar mcp --allow-writes`**, and two commands, `craftar mcp allow-writes` and `craftar mcp revoke-writes` (see *Commands* and *Craftar inside the AI coder*). Without the flag, `craftar mcp` answers byte for byte as before — `initialize`, `tools/list` and every tool. Its `--help` description now reads `Reads only unless --allow-writes`.
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import { serve } from "./mcp.js";
17
17
  import { mcpWriters } from "./core/write-actions.js";
18
18
  import { pruneCache } from "./core/cache.js";
19
19
  import { resolveHome } from "./core/home-lock.js";
20
- import { loadWorkspace, loadForgeSource, readWorkspaceConfig, plan, readLock, status, unsetDeclared, unsetRefusal, agentsMdOverBudget, agentsMdRefusal, resolveForge, resolveForgeSource, WORKSPACE_FILE } from "./core/sync.js";
20
+ import { loadWorkspace, loadForgeSource, readWorkspaceConfig, plan, readLock, status, unsetDeclared, unsetRefusal, agentsMdOverBudget, agentsMdRefusal, kiroResourcesUnplanned, kiroResourcesRefusal, orphanReason, primaryOrphan, resolveForge, resolveForgeSource, WORKSPACE_FILE } from "./core/sync.js";
21
21
  import { sectionKey } from "./core/resolve.js";
22
22
  import { catalogueContext, listRecipes, listIngredients, checkType, withLoadWarnings } from "./core/catalogue.js";
23
23
  import { canonicalValue } from "./core/sections.js";
@@ -59,7 +59,7 @@ process.stdout.on("error", (e) => { if (e.code === "EPIPE")
59
59
  */
60
60
  const DIFF_EXIT_CODE_FETCH_MODE = "sync";
61
61
  const program = new Command();
62
- program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.35.0");
62
+ program.name("craftar").description("Craft, sync and convert AI-coding workspace harnesses.").version("0.36.0");
63
63
  const SAVE_PLAN_GATE = { what: "plan", who: "unify", outside: "save plans outside the Forge", never: "unify never overwrites" };
64
64
  const REPORT_GATE = { what: "report", who: "import", outside: "write the report outside the Forge", never: "import never overwrites" };
65
65
  program
@@ -165,7 +165,7 @@ program
165
165
  console.log(JSON.stringify(report, null, 2));
166
166
  // exitCode, not process.exit: a piped object is not cut (spec 29 §4.1)
167
167
  // The object carries both reasons (unsetParams, the budget line in warnings): nothing on stderr
168
- if (unset.length || agentsMdOverBudget(p))
168
+ if (unset.length || agentsMdOverBudget(p) || kiroResourcesUnplanned(p)?.length)
169
169
  process.exitCode = 1;
170
170
  return;
171
171
  }
@@ -192,7 +192,10 @@ function budgetRefusalBlock(p) {
192
192
  const over = agentsMdOverBudget(p);
193
193
  if (over)
194
194
  errorLine(agentsMdRefusal(over));
195
- return over !== null;
195
+ const kiroUnplanned = kiroResourcesUnplanned(p);
196
+ if (kiroUnplanned)
197
+ errorLine(kiroResourcesRefusal(kiroUnplanned));
198
+ return over !== null || kiroUnplanned !== null;
196
199
  }
197
200
  /**
198
201
  * A writing sync on a planned workspace, then its report: `apply`, the registration of spec 21 (unless
@@ -208,7 +211,7 @@ async function applyAndReport(ws, p, st, lock, opts = {}) {
208
211
  for (const f of o.result.written)
209
212
  console.log(` ${pc.green("+")} ${f}`);
210
213
  for (const f of o.result.removed)
211
- console.log(` ${pc.magenta("-")} ${f} (${opts.overwritePaths?.has(f) ? "hand-edited orphan: discarded" : "orphan: no longer produced by the Forge"})`);
214
+ console.log(` ${pc.magenta("-")} ${f} (${opts.overwritePaths?.has(f) ? "hand-edited orphan: discarded" : orphanReason(p, st, f)})`);
212
215
  for (const s of o.result.skipped)
213
216
  console.log(` ${pc.yellow("!")} ${s.path} ${explainSkip(s)}`);
214
217
  for (const w of p.warnings)
@@ -782,7 +785,8 @@ program
782
785
  if (only && s.path !== only)
783
786
  continue;
784
787
  // Spec 19 §3.2: the six states sync --check refuses are shown; which those are is diffReport's to say
785
- if (await printFileDiff(ws.root, s))
788
+ const isPrimary = primaryOrphan(p, s);
789
+ if (await printFileDiff(ws.root, s, { isPrimary }))
786
790
  shown++;
787
791
  }
788
792
  if (!shown)
@@ -799,7 +803,7 @@ program
799
803
  });
800
804
  const DIFF_PAINT = { paint: { same: pc.dim, del: pc.red, add: pc.green } };
801
805
  /** What `craftar diff` prints for one file `sync --check` refuses; `drift show <path>` prints the same for a drifted one. */
802
- async function printFileDiff(root, s) {
806
+ async function printFileDiff(root, s, opts = {}) {
803
807
  // What to show is decided in the core (`diffReport`); the headers and the paint are this command's.
804
808
  const [row] = await diffReport({ root }, [s], { withholdMcp: false });
805
809
  // Nothing to show for `unchanged` and `adopt`: says so, for the caller's count
@@ -812,7 +816,8 @@ async function printFileDiff(root, s) {
812
816
  else if (s.state === "orphan") {
813
817
  // A removal: the file exists on disk but the Forge no longer produces it
814
818
  console.log(pc.bold(`--- ${s.path} (disk, orphan)`));
815
- console.log(pc.bold(`+++ ${s.path} (forge: no longer produced — sync removes it)`));
819
+ const orphanReason = opts.isPrimary ? "written to AGENTS.md only — sync removes it" : "no longer produced — sync removes it";
820
+ console.log(pc.bold(`+++ ${s.path} (forge: ${orphanReason})`));
816
821
  }
817
822
  else {
818
823
  console.log(pc.bold(`--- ${s.path} (disk, ${s.state})`));
@@ -854,7 +859,7 @@ drift
854
859
  // Below schema 3, providedRefs returns early (line "if (!eff.selected.length) return new Map()") before resolve(), so it cannot throw.
855
860
  providedWarnings.push(`profile ${ws.config.profile} does not resolve on its own (${e.message}) — what its integrations provide was not computed, and craftar drift promote will refuse`);
856
861
  }
857
- const rows = driftList(named ? [named] : st, provided);
862
+ const rows = driftList(named ? [named] : st, provided, new Set(p.reworded));
858
863
  // Spec 30r5: drift show decides a refused sync as status does — unsetParams in --json, the refusal block on stderr, exit 1.
859
864
  const unset = unsetDeclared(p);
860
865
  if (o.json) {