@wairon/cli 5.1.1-dev.11 → 5.1.1-dev.110

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.
Files changed (37) hide show
  1. package/CONTRIBUTING.md +193 -0
  2. package/README.md +154 -41
  3. package/dist/cli/index.js +66474 -29775
  4. package/dist/cli/index.js.map +1 -1
  5. package/dist/index.d.ts +43671 -0
  6. package/dist/index.js +53581 -21797
  7. package/dist/index.js.map +1 -1
  8. package/dist/templates/architect.yaml +15 -13
  9. package/dist/templates/domain-owner.yaml +5 -4
  10. package/dist/templates/skills/sdd-architect.md +44 -26
  11. package/dist/templates/skills/sdd-auditor.md +5 -4
  12. package/dist/templates/skills/sdd-delegate.md +43 -4
  13. package/dist/templates/skills/sdd-implement.md +83 -13
  14. package/dist/templates/skills/sdd-narrative.md +7 -4
  15. package/dist/templates/variants/logic-shapes.yaml +59 -0
  16. package/dist/templates/variants/portal-shapes.yaml +13 -0
  17. package/dist/webapp.html +771 -0
  18. package/docs/architecture.md +83 -0
  19. package/docs/cli.md +1208 -0
  20. package/docs/design/approval-baseline.md +257 -0
  21. package/docs/design/connecting-agent-entrypoint.md +167 -0
  22. package/docs/design/execution-budgets.md +199 -0
  23. package/docs/design/pack-scoping.md +581 -0
  24. package/docs/design/supervisor-doctrine.md +110 -0
  25. package/docs/design-export.md +313 -0
  26. package/docs/extending-wairon.md +616 -0
  27. package/docs/network.md +109 -0
  28. package/docs/requirements.md +65 -0
  29. package/docs/roadmap.md +133 -0
  30. package/docs/standards/INDEX.md +23 -0
  31. package/docs/standards/architecture.md +1042 -0
  32. package/docs/standards/language-bindings.md +120 -0
  33. package/docs/templates.md +61 -0
  34. package/docs/vision.md +59 -0
  35. package/docs/wairon-feature-requests-hosted-mcp.md +74 -0
  36. package/package.json +18 -5
  37. package/schemas/design-export-2.json +1186 -0
package/docs/cli.md ADDED
@@ -0,0 +1,1208 @@
1
+ # wairon — CLI Reference
2
+
3
+ All commands operate on the `.wai/` directory in the current project. Global
4
+ flags: `--verbose`, `--silent`, `-v`/`--version`.
5
+
6
+ ---
7
+
8
+ ## Project
9
+
10
+ ### `wairon init [-y, --yes] [--pack <source>]`
11
+ Bootstrap `.wai/` in the current project: project config, the SDD spec tree
12
+ (an L0 `.index.yaml` is seeded), the shared `.wai/context/` and the AI guide
13
+ (`CLAUDE.md` / `GEMINI.md`), the project-local MCP registration, and the SDD
14
+ skills installed into each selected target tool. No agent files are written:
15
+ agents are served as live briefs (`sdd_get_agent_brief`, `wairon agent brief
16
+ <id>`) unless the project opts into `rules.materializeAgentFiles: true`. `--yes`
17
+ uses defaults without prompts (a shell with no terminal needs it): the project name from the folder, profile backend, target claude only — another tool is one interactive answer or one `targets` entry away;
18
+ `--pack <source>` (repeatable) vendors + registers an extension pack right after
19
+ init (see `wairon pack`). Re-running on an
20
+ initialized project is a no-op that points you back to the SDD flow. Run in a
21
+ folder an existing project binds (see *Which project a command acts on* below),
22
+ it asks on a terminal whether to make that folder a **member** of the parent —
23
+ which edits the parent's `project.yaml` — and with `--yes` it refuses, writing
24
+ nothing and printing the exact `wairon member add <alias> <path> --project` to
25
+ run from the parent (or: make the folder its own repository with `git init` and
26
+ re-run `wairon init` for an independent project).
27
+
28
+ **What to commit.** All of `.wai/` — the specs, `project.yaml`, `lock.json`,
29
+ `externals/` and `externals.lock.yaml`, vendored packs: the gate reads committed
30
+ files only (a migration's scratch folder, `.wai/transactions/`, ignores itself).
31
+ The generated `CLAUDE.md`/`GEMINI.md`, `.claude/` (or `.gemini/`) skills and
32
+ `.mcp.json` hold no machine path: commit them and every clone's assistant starts
33
+ with them, or ignore them and run `wairon generate` after a clone — either way
34
+ `wairon generate` / `wairon doctor --fix` refreshes them.
35
+ wairon writes no `.gitignore` — your toolchain's own entries (`node_modules/`,
36
+ `dist/`, …) are all it needs.
37
+
38
+ #### Which project a command acts on
39
+ Every command, the MCP server (`wairon mcp serve`) and `wairon dev` bind the same
40
+ project: from the folder you are in, the nearest folder whose `.wai/specs` holds
41
+ an L0 — but the walk **never climbs past the repository root** (the nearest folder
42
+ holding `.git`) unless a project above it declares the crossing as a member
43
+ (`members` naming the repository's folder, or a folder inside it). A stray `.wai`
44
+ in a parent folder therefore binds nothing in a child repository. Outside any
45
+ repository the walk is unbounded. Each command that acts on a project prints
46
+ `project <id> at <root>` on stderr first, so `wairon export` and every `--json`
47
+ output keep a pure stdout.
48
+
49
+ #### When a command refuses
50
+ A refusal is one `✖` line and a non-zero exit — never a Node stack trace, whatever
51
+ the input. A `.wai/project.yaml` that does not parse as YAML (most often a key
52
+ written twice, e.g. a second `rules:` block appended by hand) stops every command
53
+ the same way, naming the file, the line and the key —
54
+ `✖ .wai/project.yaml:33: duplicated mapping key "rules" — …` — and `wairon doctor`
55
+ diagnoses it too; no command runs on defaults instead. To report a bug, set
56
+ `WAIRON_DEBUG=1` and the stack is printed after the line.
57
+
58
+ #### Ids and names
59
+ Every id and name the design holds follows one grammar, and every writer, the
60
+ loader and the CLI judge it the same way. Spec ids (subsystems, components,
61
+ interfaces, implementations, types), project ids, and member and external
62
+ aliases, as well as method, parameter and field names, are never empty, at most
63
+ 64 characters, never start with `-` (no command line could name one), hold no
64
+ control, NUL or zero-width character, and are never `__proto__`. Spec ids and
65
+ aliases are `[a-z0-9-_]+`, project ids `[a-z0-9-_.]` starting and ending
66
+ alphanumeric; these become folder and file names, so a name Windows reserves for
67
+ a device (`con`, `aux`, `nul`, `com1`, …) is refused on every platform, and a
68
+ project id or an alias is never `super`, the `::` grammar's namespace hop.
69
+ Method, parameter and field names are ASCII identifiers that never start with a
70
+ digit, and never a word the tree's `targetLanguage` reserves for that kind of
71
+ identifier: TypeScript and JavaScript allow a keyword as a method or a field but
72
+ not as a parameter, and no method named `constructor`; Rust, Python, Go, Java, C#,
73
+ C and Kotlin refuse their keywords everywhere (`RESERVED_IDENTIFIER`, naming the
74
+ language and an alternative). A display `name` is never blank and holds no
75
+ invisible character.
76
+
77
+ ### `wairon status [--subsystem <id>] [--no-recursive] [--all]`
78
+ Print the SDD spec tree as a hierarchy. Its percentages measure **authoring
79
+ progress** — 80% once a component's component, contract and implementation specs
80
+ are written, 100% once its implementation names source files that exist, capped
81
+ at 50% while any of them is `draft` or `design` — and are
82
+ separate from **approval**, which is the lock record in `.wai/lock.json` and is
83
+ printed on its own line. `wairon lock` never rewrites a spec's status, so an
84
+ approved tree can still show draft specs. `--subsystem <id>` shows one
85
+ subsystem; `--no-recursive` shows this project without its members; `--all` lists
86
+ every spec that moved since the approval instead of the first few.
87
+ Each member project prints as `[Project] alias (id)` holding its own subsystems,
88
+ with its **approval state** computed at the member's own root (`approved`,
89
+ `drifted`, `never`) and how this project's lock pinned it (`matches`, `moved`,
90
+ `unpinned`). The report closes with this project's own state. Asked at the parent
91
+ or at the member, the answer is the same. When an external moved in its live
92
+ producer, drifted or could not be compared, an **Externals** section follows
93
+ the verdict (the same advisory comparison `validate` prints); it never changes
94
+ the exit code.
95
+
96
+ ### `wairon validate [--ci] [--all] [--subsystem <id>] [--family] [--no-recursive]`
97
+ Run the architecture-conformance gate over the spec tree: reference integrity,
98
+ contract↔implementation method symmetry, narrative-call resolution, component
99
+ stereotype dependency rules, and dependency-cycle detection. `--ci` treats
100
+ warnings as errors (notices are printed and counted, never fatal).
101
+
102
+ - The first 100 findings per severity are printed; the rest are counted ("N
103
+ more not shown") and a per-code total follows. `--all` prints every finding.
104
+ - `--subsystem <id>` validates one subsystem.
105
+ - It ends with one **code-reading line**, the same sentence `sdd_validate_tree`
106
+ carries in its hint: the analysis grade the code was read at (the weakest any
107
+ file got — see `wairon lock` for the grades) and, at `exact`, the TypeScript
108
+ compiler that read it — `Code read at grade exact with TypeScript 5.9.3
109
+ (wairon's own copy)` on a project with TypeScript 7 or none, `(the project's
110
+ own)` on TypeScript 5 or 6 — or `Code: none read yet` before any file the design
111
+ names exists.
112
+ - At a project that declares members, `validate` is the **family run**: every
113
+ member's own gate, and this project's externals composed against their live
114
+ producers. `--no-recursive` runs this project's gate alone; `--family` runs
115
+ the family run from a member.
116
+ - **Where the network proofs run.** The proofs that need the whole family —
117
+ `ENTRY_UNPROVEN` (a `network` entry no modelled caller reaches), the gateway
118
+ rules for members, cross-project reach — are judged by the family run at the
119
+ **family root** (the project that declares the members). A member validated on
120
+ its own judges its own gate with its `network` entries counted as declared, and
121
+ says so in one line, so a green member run is never read as a proven one.
122
+ **In CI, run `wairon validate --ci` at the family root**, after installing
123
+ the project's dependencies (`npm ci` or your package manager's equivalent —
124
+ the gate reads TypeScript/JavaScript code with the project's own TypeScript
125
+ when it is 5 or 6, and with the copy wairon ships otherwise;
126
+ see [Using it in GitHub Actions](#using-it-in-github-actions)); a member in
127
+ its own repository adds the root's run to its pipeline or relies on the root
128
+ repository's.
129
+ - **Externals, compared live (advisory: the pin gates).** The gate judges each
130
+ external against its pin. Then every external the run did not compose is
131
+ compared with its **live** producer, offline, and printed in its own section:
132
+ `EXTERNAL_LIVE_INCOMPATIBLE` (warning: a used member changed at signature
133
+ level, was renamed — the new name is given — or is gone; it names the specs
134
+ that use it and the fix: adapt the uses, then `wairon externals pin <alias>`),
135
+ `EXTERNAL_DRIFTED` (notice: the producer moved, nothing used did) and
136
+ `EXTERNAL_LIVE_UNCOMPARED` (notice: the producer could not be read — a git
137
+ external is never fetched here; `wairon externals status` compares it). These
138
+ findings are **advisory**: printed and counted, but neither the exit code nor
139
+ `--ci` ever decides on them. A family run composes the externals in its reach
140
+ as its gate (`EXTERNAL_INCOMPATIBLE`, error) and gives them no second word.
141
+ - **Planned code is a notice.** An implementation whose `sourcePath` names a
142
+ file not written yet is `SOURCE_FILE_PLANNED`, and one naming no file is
143
+ `MISSING_SOURCE_PATH` — both notices — until realization begins (any file the
144
+ implementation names exists). From then on a missing file is
145
+ `MISSING_SOURCE_FILE` (error) and a contract method naming no file is
146
+ `METHOD_SOURCE_PATH_MISSING` (warning). `rules.conformance.requireCode: true`
147
+ in `project.yaml` reports the two notices as errors, for a CI that must say
148
+ every designed implementation has code.
149
+ - **Types are linked to code too.** A type spec without a `sourcePath` is
150
+ `MISSING_TYPE_SOURCE_PATH` — a notice before its subsystem has code, a
151
+ warning after. Plan a type's `sourcePath` when you plan the implementations'.
152
+ - **What the closing line says.** Plain `validate` passes with warnings and
153
+ says so — `Passed with N warning(s)` (`--ci` fails on them); `All checks
154
+ passed` means none. A tree holding no design yet (no L0, or an L0 with nothing
155
+ below it) says there is nothing to check.
156
+ - **Unknown keys are reported.** A key a spec's schema does not know (an
157
+ `exports:` table in the L0 — the export table is `publicInterfaces`; a
158
+ misspelt field) is `UNKNOWN_SPEC_KEY`, and an unknown setting in
159
+ `.wai/project.yaml` is `UNKNOWN_CONFIG_KEY` (both warnings): the parse would
160
+ otherwise drop them without a word.
161
+ - **Code that crosses the design.** A call through a constructor-injected
162
+ collaborator is collaboration whatever the import's form: when its type comes
163
+ from a component the caller never declared, even through `import type`, it is
164
+ `UNDECLARED_DEPENDENCY`; a Portal calling a write- or lifecycle-effect method
165
+ of a Repository, Index, Store or Registry is
166
+ `PORTAL_WRITE_SHORTCUT_IN_CODE` (error, the code twin of
167
+ `PORTAL_WRITE_SHORTCUT`). A Portal's calls are resolved by the TypeScript
168
+ type checker, so how the receiver is written does not matter: the call lands
169
+ on the method the checker resolves it to, and a call through an interface or
170
+ type — a port declared in the Portal's own contracts file included — lands on
171
+ every class of the project that realizes it (the ones that say `implements`
172
+ when any does, else every class the checker finds assignable). Taking a
173
+ method as a value counts as using it (`Reflect.apply`, a `Function`-typed
174
+ variable, `.bind`, a callback), and code no component maps — a helper in a
175
+ module no spec names, or a function that is none of its file's components'
176
+ modelled methods — is read as if it were inlined where it is called; the
177
+ finding names the path. A call the checker cannot resolve because its
178
+ receiver is typed `any` or `unknown`, cast to either or to an index
179
+ signature, picked by a computed key, or typed nothing, under the name of a
180
+ data component's write (or with a computed key on a receiver that was a data
181
+ component before the cast), is `PORTAL_CALL_UNRESOLVED` (a warning, so `--ci`
182
+ fails): give the receiver its type. An Orchestrator (or any component) whose
183
+ code calls a write- or lifecycle-effect method of another component that its
184
+ narrative never claims is `UNDECLARED_WRITE_CALL`, and so is a Portal calling
185
+ a workflow verb its narrative never names whose effect is not declared `read`
186
+ or `none`; the same unclaimed write through a receiver the checker cannot
187
+ follow is `CALL_ORIGIN_UNRESOLVED` (a warning). The narrative↔code and
188
+ declared-edge checks read the checker too, so a type-only `ports.ts` barrel,
189
+ `export *`, a port declared in a shared module and a dependency object typed
190
+ inline realize the calls and edges they carry. And a file importing a technology's package
191
+ outside the components that bind it is `TECH_LEAKAGE_IN_CODE`. A technology's
192
+ packages are its declared tokens (its name, or its `matches`) plus the common
193
+ packages wairon knows for it — see [Technologies and their
194
+ packages](#technologies-and-their-packages). An Adapter's call step to a verb
195
+ of a Portal that declares an out-of-process transport (HTTP, gRPC, a
196
+ database, a bus, a CLI…) is the link the design models: it is never resolved
197
+ to the remote Portal's file, so it never reads as `CALL_ORIGIN_UNRESOLVED`;
198
+ the call to the Adapter stays checked. Likewise the Adapter's `dependsOn` to
199
+ that Portal is realized by the link, never reported `UNREALIZED_DEPENDENCY`:
200
+ never add an import across a network boundary to satisfy a check.
201
+
202
+ #### Technologies and their packages
203
+
204
+ `TECH_LEAKAGE_IN_CODE` compares each import's package with the packages of the
205
+ technologies an implementation binds. A design writes the technology the way
206
+ people say it (`postgres`) while the code imports its driver (`pg`), so wairon
207
+ knows the common packages of the common technologies by default:
208
+
209
+ | Technology (any of these names) | Packages |
210
+ | --- | --- |
211
+ | `postgres`, `postgresql`, `pg` | `pg`, `postgres`, `pg-promise`, `@neondatabase/serverless`, `@vercel/postgres` |
212
+ | `mysql`, `mariadb` | `mysql2`, `mysql`, `mariadb` |
213
+ | `redis`, `valkey` | `redis`, `ioredis`, `@redis/client` |
214
+ | `mongodb`, `mongo` | `mongodb`, `mongoose` |
215
+ | `sqlite`, `sqlite3` | `better-sqlite3`, `sqlite3`, `sqlite` |
216
+ | `kafka` | `kafkajs`, `node-rdkafka` |
217
+ | `rabbitmq`, `amqp` | `amqplib`, `amqp-connection-manager` |
218
+
219
+ An HTTP client (`fetch`, `axios`, `got`, `undici`) is never a technology — it is
220
+ how an Adapter reaches anything — so no entry names one. To extend the list,
221
+ write the technology as `{ name, matches }` (each `matches` token is a package
222
+ too), or have a pack contribute `technologyPackages: { <technology>: [<package>,
223
+ …] }`.
224
+
225
+ A package of a technology that **no** implementation binds is reported too:
226
+ the code uses a technology the design never placed, so every importer is
227
+ outside its (missing) home. Bind it on the data-layer component that owns the
228
+ vendor call (`technologies: [redis]`), and that component becomes its home.
229
+
230
+ ### `wairon generate [--target <name>] [--domain <id>] [--domains <ids>] [--root] [--family] [--no-prune] [--global] [--dry-run]`
231
+ Reconcile the generated guides, skills and context, and — only when the project
232
+ opts in — write the agent files from the spec-derived topology.
233
+
234
+ **Agent files are opt-in.** With `rules.materializeAgentFiles` off (the default
235
+ for a new project) no agent file is written, and a run removes the ones an
236
+ earlier version wrote; agents are served as live briefs instead
237
+ (`sdd_get_agent_brief`, `wairon agent brief <id>`). Set
238
+ `rules.materializeAgentFiles: true` in `.wai/project.yaml` to keep agent files
239
+ on disk (`rules.generateComponentImplementers: true` adds one implementer per
240
+ component).
241
+
242
+ - `--target <name>` limits generation to one of the project's configured
243
+ targets (`claude`, `agy`, …); an unknown name exits non-zero and lists the
244
+ targets the project configures.
245
+ - `--domain <id>` / `--domains <ids>` / `--root` limit it to some domains.
246
+ - `--no-prune` keeps wairon-managed agent files that left the topology.
247
+ - `--global` also writes a target whose output directory is outside the project
248
+ root, backing up each file it replaces.
249
+ - `--dry-run` previews without writing.
250
+
251
+ `generate` writes **only this project's** outputs. A parent's topology lists a
252
+ member's agents by reference (`delegatesTo: <alias>::<agentId>`) instead of
253
+ copying them, and a brief for `<alias>::<agent>` composes at the member's own
254
+ root. `--family` also generates each member's own layer, in its own root.
255
+ (`--no-recurse` is accepted for one release; not cascading is now the default.)
256
+
257
+ ### `wairon lock [-y, --yes] [--subsystem <id>]`
258
+ Review and approve the design. Validates the spec tree **as if complete** (full
259
+ strictness, no draft-status relaxation) and — only if the **design** passes —
260
+ records the current tree as approved and refreshes this project's generated
261
+ outputs (agent files only when `rules.materializeAgentFiles` is on).
262
+
263
+ **It approves; it does not change statuses.** No spec's `status` is rewritten:
264
+ approval lives in `.wai/lock.json`. Implementation gates on that approval —
265
+ `wairon lock-check` in CI, `sdd_get_status` for an AI tool — not on
266
+ `status: complete`. Commit `.wai/lock.json` with the change it approves.
267
+ When any spec is still `draft` or `design`, the lock says how many of how many,
268
+ per kind (subsystems, components, contracts, implementations, types), and that
269
+ it approves them as they stand. Re-running it when nothing it records moved
270
+ keeps the record on file — its `lockedAt` included — so `.wai/lock.json` stays
271
+ unmodified; a change of approver, verdict, member pin or wairon release writes
272
+ a new record.
273
+
274
+ **What it certifies is the design.** Only design findings can refuse a lock.
275
+ Code-conformance findings (the code↔spec checks) are recorded **beside** the
276
+ claim in the record's `code` block, with the analyzer that produced them, and
277
+ printed as `code: N error(s), … recorded beside the claim`, with the analysis
278
+ **grade** — the weakest any analyzed file got: `exact` (read with the project's
279
+ TypeScript compiler API: declarations, imports and calls resolved), `pattern`
280
+ (read through the language's declaration and import patterns — recognized, not
281
+ resolved), `generic` (word-boundary identifiers only, the floor for any
282
+ language), or `none` (no file analyzed yet: no code, or none the specs name).
283
+ CI enforces them:
284
+ `wairon validate --ci` still fails on a code error. A design can be approved
285
+ before its code exists.
286
+
287
+ **It approves this project only.** The gate identity it records covers this
288
+ project's own specs, the design doctrine, its declared inputs, its `composition`
289
+ block, and each direct member's **composition subject** — the `stateId` in the
290
+ member's own lock record. The record (format 3) lists each direct member under
291
+ `members` with its subject and state (`approved`, `drifted`, `never`). Nothing is
292
+ written below the project: each member locks at its own root. With
293
+ `composition.requireApprovedMembers: true` in `project.yaml`, the lock refuses
294
+ while a direct member is drifted or never approved, naming each.
295
+
296
+ The identity is captured **before** validating and confirmed **before** writing:
297
+ if a spec, the doctrine, an input or a member's approval moves while the lock
298
+ runs, it refuses and writes nothing. (`--no-recursive` is accepted for one
299
+ release and changes nothing: a lock never reaches below its project.)
300
+
301
+ **Code linkage is not part of the approval.** The gate identity and the per-spec
302
+ digests read each spec's **design**: where it is realized — `sourcePath`,
303
+ `symbol`, `exportedVia`, `simPath`, `injectedParams`, conformance tiers, a Portal's
304
+ `router`, `externalLinks` — and the `createdAt`/`updatedAt` timestamps are left
305
+ out (`specsReading: design`). Linking code to an approved design, or a change and
306
+ its revert, never stales the lock; declare planned `sourcePath`s at design time.
307
+
308
+ **Readiness is not part of the approval either.** A spec's `status` (`draft`,
309
+ `design`, `complete`) says how far authoring is, never what the design says, so
310
+ it is left out of the design view: promoting a spec to `complete` after the lock
311
+ never reopens it. A format-3 record taken while the view still carried `status`
312
+ keeps passing when nothing but a status moved — it is judged in the reading it
313
+ was taken in, each spec's approved status recovered from its digest — and
314
+ `wairon doctor --fix` carries it into the current reading without a review.
315
+
316
+ A **format-2** record (written before code linkage left the approval) still passes
317
+ `lock-check` while nothing it covered moved. `wairon doctor --fix` re-expresses it
318
+ as format 3 in place — before any other repair touches a spec — keeping
319
+ `lockedAt`, `lockedBy`, the results and the member pins, and recording
320
+ `reexpressed: { at, fromAlgorithm, fromReading, by }`. When no spec file moved
321
+ but the record still does not match, what moved is the **gate** it was judged
322
+ under — the design rules of a newer wairon release, the project's rule tuning,
323
+ `composition`, the network declaration, consumed contracts or a member's
324
+ approval — never your design: `lock-check` and `doctor` say exactly that, `doctor
325
+ --fix` still carries the record into the design reading (its claim stays as
326
+ certified, so it reads stale for that stated cause), and one `wairon lock`
327
+ re-approves the unchanged design. A format-2 record whose specs moved reads stale
328
+ once; one `wairon lock` clears it for good.
329
+
330
+ It writes **nothing into your spec tree**. The approval is one sha256 per spec
331
+ (over its design view) on `.wai/lock.json`, the record that was always committed — so your
332
+ teammates, a fresh clone and CI all see the same approval you gave, and
333
+ `wairon status` elsewhere can name what has drifted from it. Keys are sorted, so
334
+ re-approving a one-spec change shows up as a two-line diff. A failed or
335
+ cancelled lock changes nothing at all.
336
+
337
+ `lockedBy` records who approved **and how that identity was established**: your
338
+ git author identity where there is one (so a reviewer can match it against the
339
+ author of the commit carrying the lock), else `user@hostname`. On a hosted
340
+ instance it is the authenticated subject. None of it is proof — the commit that
341
+ introduces `lock.json` is what carries that.
342
+
343
+ Before approving, it reports what moved since the last approval — the question
344
+ a human is actually answering:
345
+
346
+ ```
347
+ 3 spec(s) changed since the last approval:
348
+ ~ sdd_core/spec_loader/.index.yaml
349
+ ~ sdd_core/spec_loader/.interface.yaml
350
+ + sdd_core/spec_index/.index.yaml
351
+ ```
352
+
353
+ `--yes` skips the confirmation (for CI); `--subsystem`
354
+ limits the scope. A shell with no terminal and no `--yes` writes nothing.
355
+
356
+ ### `wairon lock-check [--strict]`
357
+ The **merge gate**. One question, one exit code: *is the design in this working
358
+ tree the design that was approved?* It compares the tree's gate identity against
359
+ the one recorded in `.wai/lock.json` **as it is in the working tree**. In CI that
360
+ is the file committed in the checked-out revision; locally an uncommitted lock
361
+ record counts too, so commit it before you rely on a local pass.
362
+
363
+ | What it finds | Default | `--strict` |
364
+ | --- | --- | --- |
365
+ | **`locked`** — the approval still covers this design | pass (0) | pass (0) |
366
+ | **`stale`** — the design moved past its approval | **fail (1)** | **fail (1)** |
367
+ | **`unlocked`** — nothing was ever approved | pass, with a notice (0) | **fail (1)** |
368
+ | no `.wai/specs` in this directory at all | pass, saying so (0) | **fail (1)** |
369
+ | a member project has spec changes nobody approved at its own root | **fail (1)** | **fail (1)** |
370
+ | a member project was never approved by anyone | pass, naming it as *not gated* (0) | **fail (1)** |
371
+ | only a member's own approval moved (it re-locked) | **fail (1)**, naming the member: re-lock here to pin its new approval | **fail (1)** |
372
+
373
+ **What plain `lock-check` guarantees:** that a *committed* approval still covers the
374
+ design, and that no member project carries unapproved changes. Nothing more: a
375
+ project with no `.wai/lock.json` — never locked, *or the record deleted in the
376
+ pull request* — passes, because it reads exactly like one that never opted in.
377
+ **In CI, run `wairon lock-check --strict`** (or the reusable workflow with
378
+ `strict: true`) once the project has been locked: then a missing record, or a
379
+ member nobody ever approved, fails.
380
+
381
+ **Members.** The gate identity pins each direct member's *recorded* approval, so
382
+ the root reads every member project's own approval at its own root too. A member
383
+ whose specs moved past its own approval fails the root's check — run `wairon lock`
384
+ in that member's folder. A member that re-locked is expected (its own approver
385
+ signed it off), and the root is judged by whether its pin of the member is still
386
+ the member's approved one: after a member re-locks it is not, and the verdict says
387
+ exactly that — no own spec moved, run `wairon lock` here to pin the new approval.
388
+
389
+ A lock taken by a wairon release before 6.0.0 reads `stale` once, because the
390
+ gate identity gained inputs. The message says so — the approval *was taken under
391
+ an earlier gate identity* — and whether any own spec file changed since; re-lock
392
+ once and commit `.wai/lock.json`.
393
+
394
+ **It is optional by construction.** By default only a moved approval (or a
395
+ member's unapproved changes) refuses, and neither can happen in a project that
396
+ never locked — so adding this to an existing repository's CI cannot make it start
397
+ failing. `--strict` is what turns "never approved" into a failure, and a project
398
+ has to ask for it — which is why a project that has locked should run it strict.
399
+
400
+ It is **not** `wairon validate`. Validate asks whether the design is *legal* and
401
+ runs the whole rule set to answer; this asks whether it is *approved* and reads
402
+ one JSON file, which is why it is safe to run on every pull request. The two are
403
+ independent — a tree can be approved and illegal, or legal and unapproved — so
404
+ run both.
405
+
406
+ It gates on the **gate identity** (the hashed parsed tree plus the governing
407
+ doctrine), not on the per-spec content digests the same lock record carries.
408
+ Those answer "has this file changed since you approved it": a whitespace-only
409
+ edit moves them, and a gate that demands a re-lock for reformatting is one people
410
+ learn to bypass.
411
+
412
+ **What it proves:** that the design in the commit being merged is the design that
413
+ was approved. **What it does not prove:** *who* approved. `lockedBy` is a claim,
414
+ not an attestation — anyone who can run the CLI can write a lock record. Pull
415
+ request review is what establishes who reviewed; this establishes that the thing
416
+ merging is the thing that was reviewed.
417
+
418
+ #### Using it in GitHub Actions
419
+
420
+ This repository publishes it as a **reusable workflow**. Pin it — and the CLI it
421
+ installs — to **the version you lock with**: the one `wairon --version` prints
422
+ where you run `wairon lock`. Every published version has a git tag `v<version>`
423
+ and the same version on npm: a release `X.Y.Z` is `@vX.Y.Z` with
424
+ `wairon-version: 'X.Y.Z'`; a dev build `X.Y.Z-dev.N` is `@vX.Y.Z-dev.N` with
425
+ `wairon-version: 'X.Y.Z-dev.N'` (the npm `dev` dist-tag names the newest dev
426
+ build, but it moves — pin the exact version). Check that the version you pin is
427
+ on npm before you commit the job: `npm view @wairon/cli@<version> version`. The
428
+ workflow and the `lock-check` command first ship with 6.0.0 and its dev builds;
429
+ an older version has neither. Below, `<version>` stands for yours. Add one job
430
+ to your own workflow:
431
+
432
+ ```yaml
433
+ # .github/workflows/ci.yml in YOUR repository
434
+ on: [pull_request]
435
+
436
+ jobs:
437
+ approved-design:
438
+ uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@v<version>
439
+ with:
440
+ wairon-version: '<version>'
441
+ strict: true # a deleted .wai/lock.json fails instead of switching the gate off
442
+ ```
443
+
444
+ With inputs (all optional):
445
+
446
+ ```yaml
447
+ approved-design:
448
+ uses: SYW-Apps/Waffle-AIron/.github/workflows/lock-check.yml@v<version>
449
+ with:
450
+ working-directory: packages/api # where the .wai/ tree lives (default: .)
451
+ wairon-version: '<version>' # the version you lock with, or an npm dist-tag (default: latest)
452
+ strict: true # fail when nothing was approved — use it once you have locked (default: false)
453
+ validate: true # also run `wairon validate --ci` there (default: true)
454
+ install: auto # the project's dependencies before validate: auto | none | <command> (default: auto)
455
+ node-version: '20' # (default: '20')
456
+ runs-on: ubuntu-latest # (default: ubuntu-latest)
457
+ ```
458
+
459
+ Pin `@<ref>` to a **tag**, never to `main` or `dev`. A moving branch means the
460
+ check that gates your merges can change under you between two runs of the same
461
+ commit — and this one decides whether code merges. Pin `wairon-version` too, to
462
+ the version you lock with: the gate identity's algorithm can change between
463
+ releases, and the default `latest` moves.
464
+
465
+ On a `pull_request` event the default checkout is the merge commit, so what the
466
+ gate judges is literally the design that would land.
467
+
468
+ After the approval check the job runs `wairon validate --ci` in the same
469
+ directory (`validate: false` turns it off). Point `working-directory` at the
470
+ **family root** — the project that declares the members — so that run is the
471
+ family run, which judges the network proofs no member's own gate can.
472
+
473
+ **The project's dependencies come first.** The conformance gate reads
474
+ TypeScript/JavaScript code with the project's own TypeScript compiler when it is
475
+ TypeScript 5 or 6, and with the copy wairon ships otherwise — a fresh checkout,
476
+ a project without TypeScript, or one on TypeScript 7 (whose native compiler has
477
+ no JavaScript API) is still read at exact grade, and `CONFORMANCE_DEGRADED` is
478
+ left for a file the compiler could not read at all. Installing the project's
479
+ dependencies keeps the analysis on the project's own compiler. So before
480
+ `validate --ci` the job
481
+ installs them with the project's own package manager (`install: auto`, the
482
+ default): nothing without a `package.json`; otherwise the nearest lockfile from
483
+ `working-directory` up to the repository root decides — `package-lock.json` →
484
+ `npm ci`, `pnpm-lock.yaml` → `pnpm install --frozen-lockfile`, `yarn.lock` →
485
+ `yarn install` (`--immutable`, or `--frozen-lockfile` for Yarn 1). `install:
486
+ none` skips it; any other value is run as the install command. The approval
487
+ check alone (`validate: false`) installs nothing.
488
+
489
+ #### Without the reusable workflow
490
+
491
+ The same gate as plain steps — for another CI system, or a job of your own. For
492
+ a TypeScript/JavaScript project with code, **install the project's dependencies
493
+ before `wairon validate --ci`** (`npm ci`, `pnpm install --frozen-lockfile` or
494
+ `yarn install --immutable`, whichever your lockfile is for):
495
+
496
+ ```yaml
497
+ # .github/workflows/design.yml in YOUR repository
498
+ on: [pull_request]
499
+
500
+ jobs:
501
+ design:
502
+ runs-on: ubuntu-latest
503
+ steps:
504
+ - uses: actions/checkout@v4
505
+ - uses: actions/setup-node@v4
506
+ with:
507
+ node-version: '20'
508
+ - run: npm install --global @wairon/cli@<version> # the version you lock with (wairon --version)
509
+ - run: wairon lock-check --strict
510
+ - run: npm ci # the project's dependencies: the gate reads your code with your TypeScript (5 or 6)
511
+ - run: wairon validate --ci
512
+ ```
513
+
514
+ **A failing job does not block a merge on its own.** A workflow can only fail;
515
+ making a failing job stop a merge is a branch-protection / ruleset setting on
516
+ your repository ("Require status checks to pass" → add this job). No workflow
517
+ can declare that for itself.
518
+
519
+ ### `wairon doctor [--fix] [-y, --yes] [--global] [--report <section>]`
520
+ Health check: flags stale generated guides/skills, an unregistered MCP server,
521
+ spec-tree issues and the member/reference migration still pending. `--fix`
522
+ regenerates stale in-project guides/context/skills, registers the MCP server and
523
+ applies the spec repairs, then the member migration once confirmed (`--yes`
524
+ answers that confirmation in a script). A write outside the project root (a
525
+ machine-wide MCP config) also needs `--global`; each file it replaces is backed
526
+ up beside itself. `--report <section>` prints one section's report and writes
527
+ nothing: `chaining` (the member/reference migration plan) or
528
+ `composed-validation` (what the family run changes about each member's
529
+ findings) or `reachability` (the reachability migration's plan); it never
530
+ combines with `--fix`.
531
+
532
+ Among its spec repairs, `--fix` rewrites every stored type position that is an
533
+ alias of its canonical spelling (`string[]` becomes `list<string>`, `boolean`
534
+ becomes `bool`, `T | null` becomes `T?`, `Promise<T>` becomes `async T`) — what
535
+ any later save would write. It never guesses: a `number` position whose name
536
+ says a whole number (`count`, `maxDepth`, `port`) gets `int` *proposed* and
537
+ listed, and every position no rewrite can settle (an inline function type, a
538
+ literal union, a union mixing in a primitive, a `number` with no proposal) is
539
+ listed with its replacement for an author. Plain `wairon doctor` prints the same
540
+ plan without writing it.
541
+
542
+ **The reachability migration.** `--fix` also moves the tree onto the
543
+ reachability model: `portalType` becomes `transport` (`HTTP_API` → `HTTP`, a
544
+ `Custom` Portal that binds no wire address → `InProcess`), a listener's `mounts`
545
+ become a Portal-level entry on each mounted Portal and its implementation's
546
+ `router` (the mount's `via`), `invokedBy: external` becomes `entry` on a Portal
547
+ verb and `runtime` elsewhere, an authored export `type` that agrees with the derived kind is dropped,
548
+ in-process `Custom` endpoint addresses are removed, and allows of retired codes
549
+ are dropped or renamed. It never invents design: a Portal that was neither a
550
+ listener nor mounted gets no entry, and a `sibling-subsystem` caller, an export
551
+ type that disagrees, an endpoint outside every old prefix and a router already
552
+ set otherwise are listed for an author. Plain `wairon doctor` prints the plan;
553
+ `--report reachability` lists every rewrite and every form left for an author.
554
+ The design changes, so one `wairon lock` follows. After it, declare the entries
555
+ the migration cannot know — a CLI, a stdio tool surface (`JSONRPC`), a library
556
+ (`InProcess`) — and model a caller or remove each verb still reported unreached.
557
+
558
+ `--fix` also re-expresses an earlier lock record — format 2, or format 3 taken
559
+ while `status` was still in the approval — in the current reading, first, before
560
+ any repair touches a spec (see `wairon lock`), keeping who approved it and when.
561
+ A format-2 record over a tree no file of which moved is carried into the design
562
+ reading even when the gate it was judged under moved; doctor then says the cause
563
+ (the gate, not the design) and that one `wairon lock` re-approves it.
564
+
565
+ ### `wairon list` (alias `ls`) / `wairon show <id>`
566
+ List, or show full details of, the agents resolved from the spec tree
567
+ (`system-architect`, `<subsystem>-owner`, owners for free-standing domains, and
568
+ a `<component>-implementer` per component when
569
+ `rules.generateComponentImplementers` is on).
570
+
571
+ ### `wairon agent brief <id>` / `wairon agent customize <id>`
572
+ `brief` prints the live delegation brief for one agent — the same composition
573
+ `sdd_get_agent_brief` returns, rendered from the current spec tree on every
574
+ call. `customize` scaffolds the user-owned guidance file `.wai/agents/<id>.md`,
575
+ which later briefs for that agent include.
576
+
577
+ ### `wairon rules list`
578
+ List the SDD conformance rule registry — every rule group, the issue codes it
579
+ can emit, default severities, and any per-project overrides from
580
+ `rules.sddRuleSeverity`. The gate is a documented architecture linter: each
581
+ rule is a self-contained module in `src/core/rules/`. Rules injected by
582
+ extension packs are listed too, tagged with their pack.
583
+
584
+ ### `wairon execution show` (alias `ls`) / `wairon execution set-tier <tier>`
585
+ Execution budgets — the resource axis of the derived topology. Where authority
586
+ says which paths an agent owns, a budget says what its work costs to do:
587
+ a capability tier, a reasoning effort, a turn ceiling, a tool class, and
588
+ whether it may delegate further or load MCP servers.
589
+
590
+ `show` lists every agent's allowance next to the rationale that produced it
591
+ (component stereotype, owned-path breadth, role), so a tier choice is auditable
592
+ rather than magic. It also flags any agent an override has pinned to the
593
+ `frontier` tier — derivation never selects it.
594
+
595
+ `set-tier` moves the aggressiveness dial and states what the new tier costs:
596
+
597
+ | Tier | What it does |
598
+ |------|--------------|
599
+ | `off` (default) | No budgets derived. Output is byte-identical to before this feature existed. |
600
+ | `free` | Structural constraints only — tool class, MCP access, nested delegation. No model or effort selection, so no quality tradeoff at all. |
601
+ | `default` | Adds capability-tier selection per role and turn ceilings. Mechanical work (Store, Index, Registry, Adapter) runs smaller; work carrying decisions (Orchestrator, Supervisor) keeps the capable tier. |
602
+ | `trade` | Adds effort reduction on mechanical work and steps standard work down a tier. Real but bounded quality cost. |
603
+ | `aggressive` | Small tier for everything but deep reasoning, halved turn ceilings. Expect partial results and worse judgment. |
604
+
605
+ Raising the dial can only tighten a budget, so it is safe to turn without
606
+ auditing every agent. Per-agent overrides live in `.wai/project.yaml` under
607
+ `execution.overrides`.
608
+
609
+ ### `wairon pack …`
610
+ Extension packs — plain config files (YAML, or a JS module for programmatic
611
+ rules) injecting custom profiles, language/platform tables, and conformance
612
+ rules (see [Extending wairon](extending-wairon.md)). `wairon packs` is a
613
+ deprecated alias of `wairon pack`.
614
+
615
+ | Command | Description |
616
+ |---------|-------------|
617
+ | `wairon pack init <name> [--kind declarative\|code] [--dir <path>] [--skill]` | Scaffold a new pack project |
618
+ | `wairon pack build [source] [--out <file>]` | Build an installable `.wpack` archive from a pack directory |
619
+ | `wairon pack install <source> [-y]` | Install a pack (`.wpack`/`.zip` or a directory) into this machine's pack store. It applies to nothing until a project selects it |
620
+ | `wairon pack uninstall <name>[@version]` | Remove a pack from the store |
621
+ | `wairon pack use <name>[@version] [--source <url>] [--pin] [--bundle] [-y]` | Select an installed pack for this project, recorded in `.wai/project.yaml` |
622
+ | `wairon pack unuse <name> [-y]` | Deselect a pack for this project (it stays installed) |
623
+ | `wairon pack impact <name>[@version]` | What a pack changes here, writing nothing (below) |
624
+ | `wairon pack sync` | Install every declared-but-missing pack from the source its selection records — what a fresh machine or CI runner needs |
625
+ | `wairon pack bundle [name] [--all]` | Commit a copy of a selected pack under `.wai/packs/`, so a clone and CI need no pack store |
626
+ | `wairon pack which <name>` | Which installed pack a name resolves to: version, path, digest, origin |
627
+ | `wairon pack list` | Global and project packs and what each provides, with load errors inline |
628
+ | `wairon pack add <source> [--global]` / `wairon pack remove <name> [--global]` | Vendor a pack file or directory into `.wai/packs/` (or install it machine-wide), or remove it |
629
+
630
+ The vendoring commands in detail:
631
+
632
+ - `add <source>`: verify the pack loads, then vendor it into `.wai/packs/`
633
+ and register it in `project.yaml → extensions.packs` (commit `.wai/` so CI
634
+ and every clone enforce it). With `--global`, install machine-wide into
635
+ `WAIRON_PACKS_DIR` / `~/.wairon/packs` — auto-loaded for every project on
636
+ this machine (project packs win on collision; a project opts out via
637
+ `extensions.useGlobalPacks: false`). A source may be a file or a directory
638
+ with a pack entry file (`pack.yaml` | `pack.cjs` | `index.cjs` | …).
639
+ - `list`: global + project packs and what each provides (profiles,
640
+ languages, rules), with load errors inline.
641
+ - `remove <name>`: deregister by pack name and delete vendored files under
642
+ `.wai/packs/` (files elsewhere are left in place); `--global` removes a
643
+ machine-wide pack.
644
+
645
+ The store and selection commands (`install | uninstall | which | use | unuse |
646
+ impact | bundle | sync`) are described in
647
+ [Extending wairon](extending-wairon.md#installing-and-selecting-packs).
648
+
649
+ `wairon patterns list` lists the reusable, versioned architecture patterns the
650
+ loaded packs declare; `wairon variants list` lists the component variants (each
651
+ anchored on a base block, with its implementation guidance).
652
+
653
+ ### `wairon pack impact <name>[@version]` — and confirm before every pack write
654
+ Packs exist to adjust wairon's checks and behaviour, and a pack may loosen or
655
+ remove them by design (an automation platform cannot hold every concept a full
656
+ backend can). So installing a pack is never judged — but it is always
657
+ intentional, and its impact is shown first.
658
+
659
+ `wairon pack impact` shows, writing nothing, what a pack changes on this
660
+ project:
661
+
662
+ - its **doctrine against wairon's defaults**, by the profile that carries each
663
+ change: rules loosened, raised, turned off or added; profiles added or a
664
+ builtin redefined; stereotypes removed or discouraged, edges licensed,
665
+ patterns, guarantee tokens and language tables added;
666
+ - the **profiles of it that would govern** here (the `projectType` and each
667
+ subsystem profile that names one);
668
+ - the **findings that change** on this project — introduced, resolved and
669
+ regraded, from a dry validate of the current and the candidate
670
+ configuration — with both totals.
671
+
672
+ A pack this project does not apply (or applies at another version) is measured
673
+ as applied; one it applies exactly as asked is measured as removed, so the
674
+ report reads as what the pack accounts for now. Nothing in the report is a
675
+ finding.
676
+
677
+ Every command that selects, updates or removes a project's pack — `pack use`,
678
+ `pack unuse`, `pack add`, `pack remove`, and `pack install` when it moves this
679
+ project's floating selection — **shows the same report and asks before it
680
+ writes**; anything but yes writes nothing. In a script, pass `-y, --yes`: the
681
+ write happens without the report, and the command says it applied without
682
+ showing it. A run with no terminal to ask on (CI, a pipe) behaves the same way.
683
+
684
+ ### `wairon diagram [--format <fmt>] [--subsystem <id>] [--sequence <component:method>] [--depth <n>] [--all] [--out <path>] [--no-health]`
685
+ Generate architecture diagrams derived from the spec tree — living
686
+ documentation from the same source of truth as the conformance gate. Every
687
+ format writes a file (by default under `.wai/docs/diagrams/`; `--out` names
688
+ another file or directory); nothing opens automatically.
689
+
690
+ - **default (no flags): the interactive canvas** (`canvas.html`).
691
+ - `--format mermaid` (or `--subsystem <id>`): system-wide or scoped Mermaid
692
+ **component diagram** as a markdown file (subsystems as subgraphs,
693
+ `dependsOn` edges, thick edges for boundary hops, dashed `owns`
694
+ containment, bold border on the published surface). Use a `.mmd` `--out`
695
+ path for raw Mermaid.
696
+ - `--subsystem <id>`: scope to one subsystem plus its directly-connected
697
+ external neighbors.
698
+ - `--sequence <component:method>`: a **sequence diagram** derived from the
699
+ method's L5 narrative, recursively expanding `call` steps (cycle-guarded,
700
+ `--depth` limits expansion; default 3).
701
+ - `--canvas`: an **interactive HTML canvas** — the whole system on one page.
702
+ Subsystems as containers, pattern compounds nested inside, components laid
703
+ out in dependency layers (entrypoints left → data right), subsystems in
704
+ topological order (callers left of providers) with barycenter
705
+ crossing-reduction so links stay short and untangled. Pan/zoom,
706
+ double-click a boundary to collapse it (its external edges aggregate into
707
+ labeled "tubes"), click anything for a spec-derived detail panel
708
+ (description, interfaces, methods, endpoints, narratives, dependencies,
709
+ trusted links), search, and a validation-issue overlay. The computed
710
+ layout is locked by default — enable "rearrange" to drag, "reset layout"
711
+ to undo — and "export PNG" renders the full graph to a high-res image for
712
+ Miro/docs/slides. Fully self-contained (Cytoscape.js embedded inline,
713
+ works offline).
714
+ - `--format <mermaid|canvas|drawio|excalidraw>`: friendly alias for the flags below/above.
715
+ - `--drawio` / `--excalidraw`: **editable** exports in open formats
716
+ (draw.io / diagrams.net XML, Excalidraw scene JSON) with the exact same
717
+ computed layout as the canvas — import into diagrams.net, excalidraw.com,
718
+ VS Code extensions, or any whiteboard tool that accepts these formats.
719
+ - `--all`: write the full set (system, per-subsystem, one sequence per
720
+ entrypoint method with a narrative, plus `canvas.html`,
721
+ `architecture.drawio`, and `architecture.excalidraw`) into
722
+ `.wai/docs/diagrams/` (or `--out`).
723
+ - `--no-health`: skip comparing each consumption relation with its live
724
+ producer; the canvas draws those edges as not checked.
725
+
726
+ The canvas is a small single-page app with **C4-style scoped navigation**:
727
+ each view renders one scope's direct children (System → subsystems →
728
+ components → pattern members, infinitely deep by ownership); double-click
729
+ drills in, the breadcrumb navigates back, "Internals" previews children
730
+ inside boxes, "Externals" shows out-of-scope references as ghosts. Plus
731
+ SYW/light themes, presentation mode, per-view layout persistence,
732
+ narrative flowcharts with call drill-down, and an Export menu
733
+ (PNG / draw.io / Excalidraw) capturing the current view and layout.
734
+
735
+ ### `wairon export [--out <file>]`
736
+ The whole design, resolved, as one JSON document for generators,
737
+ translators and documentation tools (format `wairon-design`; the
738
+ [format page](design-export.md) describes every field, the compatibility
739
+ promise and how a consumer follows renames).
740
+
741
+ - It first decides the approval verdict exactly as `wairon lock-check` does
742
+ (not strict) and stamps it as `source.approval` (`locked`, `stale` or
743
+ `unlocked`) and `source.approved`. An unapproved tree is exported too,
744
+ carrying its state.
745
+ - Without `--out` it prints the JSON to stdout and nothing else, so it pipes
746
+ cleanly (`wairon export | jq .components`). With `--out` it writes the file
747
+ and reports the path and the approval state.
748
+ - Member projects are listed under `dependencies`, never inlined: export each
749
+ from its own root. So the top of a family that holds no design of its own
750
+ exports no components. It names its members (on stderr without `--out`), so
751
+ that does not read as an empty design. A part's subsystems are the project's
752
+ own and are exported with it.
753
+ - Deterministic: the same tree and verdict give byte-identical output.
754
+ - A tree without an L0 is refused. The JSON Schema ships in the package as
755
+ `schemas/design-export-1.json`, and the library twin is `exportDesign()`.
756
+
757
+ ### `wairon type rename-field <type> <field> <new-name>`
758
+ Renames a field of a type and respells every reference to it — each foreign
759
+ key `references: <type>.<field>` naming it, the type's own included. The field
760
+ keeps its place, type and description; its old name joins the field's rename
761
+ trace (`previousNames`), which `wairon export` shows as `formerly`, so a
762
+ consumer holding the old name reads a rename, not a removal and an addition.
763
+ Traces and prose are never rewritten. It refuses, writing nothing, a type or
764
+ field that does not exist, a name that is not an identifier or that the target
765
+ language reserves for a field (`fn` in Rust), and a name another field of the
766
+ type holds or retired. The MCP twin is `sdd_rename_field`. As with
767
+ any design edit, the next `wairon lock` approves it.
768
+
769
+ ### `wairon method rename <component> <method> <new-name> [--dry-run] [--search <dirs...>] [--no-pin-symbol]`
770
+ Renames a contract method and retargets every reference to it in the bound
771
+ tree. The method moves on every interface of the component that declares it,
772
+ and on the implementations of those contracts with its narrative, source path
773
+ and symbol; call, register and dispatch steps, dispatch-table bindings,
774
+ lifecycle entrypoints, the lint allows keyed on it and the debt register
775
+ (`rules.conformance.carried`) follow. Its old name joins the method's rename
776
+ trace (`previousNames`), which `wairon export` shows as `formerly`, so a
777
+ consumer reads a rename, not a removal and an addition. Prose and a gRPC wire
778
+ method are never rewritten; they are listed as mentions.
779
+
780
+ - `--dry-run` prints what the rename would move, retarget and break — every
781
+ consumer that calls the method through an export, by project and spec — and
782
+ writes nothing. Run it first on a published verb.
783
+ - `--search <dirs...>` also scans these folders for consumer checkouts outside
784
+ the family (for example the folder holding sibling checkouts), so the dry run
785
+ names a sibling project the rename breaks.
786
+ - `--no-pin-symbol`: by default an implementation that declares no `symbol` is
787
+ pinned to the OLD name, so the function its code already holds keeps binding.
788
+ Pass it when the code is not written yet, or will be renamed with the
789
+ contract, so the implementation follows the new name.
790
+
791
+ It refuses, writing nothing: a component that does not exist, one in another
792
+ project (rename it from that project's root), a method the component does not
793
+ declare, a name a moving contract already declares or retired, and a new name
794
+ that is not an identifier in the tree's method casing (snake_case in a Rust or
795
+ Python tree, camelCase in a TypeScript one), that is longer than 64 characters
796
+ or starts with a digit, or that the target language reserves for a method
797
+ (`constructor` in TypeScript, `fn` or `type` in Rust — the refusal names the
798
+ language and offers an alternative). A contract that implements another
799
+ project's extension point takes its method names from that producer, so its
800
+ casing is not judged. The MCP twin is `sdd_rename_method` (with `dryRun`). The
801
+ next `wairon lock` approves the rename.
802
+
803
+ ### `wairon method rename-param <component> <method> <param> <new-name>`
804
+ Renames a parameter of a contract method. The parameter moves on every
805
+ interface of the component that declares the method, keeping its place, type,
806
+ description and optionality; its old name joins the parameter's rename trace
807
+ (`previousNames`), which `wairon export` shows as `formerly`, so a generator or
808
+ consumer reads a rename, not a signature change. Each signature is re-derived
809
+ from the params (a method whose `signatureFrom` names this one follows by
810
+ itself), and an HTTP endpoint path placeholder `{old}` on the method's own
811
+ binding is respelled — the URL a caller sends is unchanged. Prose is never
812
+ rewritten. It refuses, writing nothing, a component, method or parameter that
813
+ does not exist (or a parameter the method takes from a `signatureFrom`), a name
814
+ that is not an identifier or that the target language reserves for a parameter
815
+ (`class` in TypeScript, `type` in Rust), and a name another parameter of the
816
+ method holds or retired. The MCP twin is `sdd_rename_param`. The next `wairon lock` approves it.
817
+
818
+ ### `wairon network flows|policy|diagram|check|why|declare|undeclare`
819
+ Networking derived from the design: the reach the validator already models
820
+ (modelled calls, Portal entries and their scopes, declared networks and their
821
+ gateways), never addresses. Run at the family root.
822
+
823
+ - `flows [--format json|csv|markdown] [--out <file>]` prints the allowed-flows
824
+ matrix, narrowed to the modelled callers wherever the family proves a
825
+ `network` entry.
826
+ - `policy --bindings <file> [--out <file>]` generates Kubernetes
827
+ `NetworkPolicy` from the matrix and a bindings file you keep outside
828
+ `.wai/`. Names it cannot bind are listed, never guessed.
829
+ - `diagram [--out <file>]` prints a Mermaid flowchart of the networks as
830
+ trust boundaries, with the gateways, outside and the flows.
831
+ - `check --observed <file> [--bindings <file>]` compares observed live flows
832
+ (CSV or JSON) with the matrix. It exits 1 on an unexpected flow or an
833
+ unknown verb.
834
+ - `why <from> <to>` prints the modelled chain behind one flow. It exits 1
835
+ when nothing allows it.
836
+ - `declare [--description <text>]` writes the project's `network` declaration
837
+ into `.wai/project.yaml`; `undeclare` removes it (the assistant's twin is
838
+ `sdd_set_network`). It changes which rules fire: validate, then re-lock.
839
+
840
+ [Derived networking](network.md) describes every output, the bindings file
841
+ and the observed-flow format.
842
+
843
+ ---
844
+
845
+ ## Members
846
+
847
+ A project's **members** are declared in its `.wai/project.yaml`, each by one
848
+ location key:
849
+
850
+ ```yaml
851
+ members:
852
+ scheduler: services/scheduler # contained
853
+ admin: ../admin # a sibling checkout
854
+ payments: git@host:acme/payments.git#<full commit> # a git repository at a commit
855
+ ledger: { source: services/ledger, description: The books, as: project } # long form
856
+ ```
857
+
858
+ A member is a **part** or a **project**, and what it is follows from its
859
+ content — `as:` only asserts it (a contradiction is `MEMBER_KIND_MISMATCH`):
860
+
861
+ - A **part** (the default) is a piece of this project stored in another folder
862
+ or repository. Its subsystems are this project's own: written by local id,
863
+ judged by the ordinary subsystem rules, covered by this project's lock, and
864
+ generated and briefed with this project's agents. It holds only a specs folder
865
+ — plus, when it is not contained, a `project.yaml` holding just
866
+ `partOf: { project: <parent id>, path: <parent root> }`.
867
+ - A **project** is an independent boundary: its tree declares an id, holds an L0
868
+ or carries a lock. It is reached only as `alias::name` through its L0 exports,
869
+ judged by its own gate and locked at its own root. A project stored outside
870
+ this project (`../x`, git, `hosted:`) is a **referenced** project: this
871
+ project's gate judges it against its pin (`wairon externals pin`), and the
872
+ family run opens it where it is stored and composes it per use.
873
+
874
+ Boundaries are earned: grow a system from subsystems, to parts when a piece
875
+ needs its own folder or repository, to projects only when it needs its own team,
876
+ release, approval or public surface.
877
+
878
+ A git member is pinned at a full commit; it is read from the fetch cache
879
+ (`WAIRON_CACHE_DIR`, else the OS user cache directory), immutable and offline
880
+ once fetched. A git part is read-only here — change it in its own repository,
881
+ then move the pin with `wairon member update`. An uncached git part offline is
882
+ `PART_UNAVAILABLE`; an uncached git project falls back to its pin and is
883
+ `EXTERNAL_CHECK_UNAVAILABLE` — never a pass. Every cross-project reference is
884
+ `alias::name`: the alias is one of the referring project's project members or
885
+ declared `externals`, the name a public name in that project's L0 export table.
886
+ An id without `::` is local — a part's subsystems included.
887
+
888
+ **Authoring the L0 export table.** What another project may reference is the
889
+ project's **L0 export table**: the `publicInterfaces` list of the L0 spec
890
+ (`.wai/specs/.index.yaml`). It is authored in two steps, both through the MCP
891
+ tools (an assistant runs them; a human may edit the YAML the same way):
892
+
893
+ 1. Each subsystem publishes its own surface — its inbound Portal, and the types
894
+ it shares — with `sdd_set_public_interfaces` (an L1 `publicInterfaces`).
895
+ 2. The L0 **re-exports** from those subsystems with `sdd_update_spec` on kind
896
+ `system`, delta `publicInterfaces`. Each entry names a subsystem in `from`;
897
+ `from` alone re-exports everything it publishes, `component` (optionally
898
+ narrowed with `interface`) or `typeDef` picks one item, `as` renames it
899
+ (the public name consumers write after `alias::`), `role: implement` marks
900
+ an extension point consumers implement, and `audience` decides who may see
901
+ it (below).
902
+
903
+ ```yaml
904
+ # .wai/specs/.index.yaml (excerpt)
905
+ publicInterfaces:
906
+ - from: distance # everything the distance subsystem publishes
907
+ - from: tiles
908
+ component: tile_portal
909
+ as: tiles # consumers write geo::tiles
910
+ audience: external
911
+ - from: shared
912
+ typeDef: coordinate # a shared type, as geo::coordinate
913
+ ```
914
+
915
+ The key is `publicInterfaces` — an `exports:` (or any other key the schema does
916
+ not know) is reported as `UNKNOWN_SPEC_KEY`, never silently ignored, and an
917
+ unknown setting in `.wai/project.yaml` as `UNKNOWN_CONFIG_KEY`. A reference to a
918
+ name the producer does not export is `EXTERNAL_NOT_EXPORTED`.
919
+
920
+ **Export audiences.** Each L0 entry carries an `audience` (default `instance`)
921
+ that decides who may see the public name. Narrowest first:
922
+
923
+ | Audience | Who sees it |
924
+ |----------|-------------|
925
+ | `project` | Only this project's family: its members, its parent and their siblings. |
926
+ | `department` | The family, and — on a hosted instance — the projects of the owning department. A local sibling checkout or git consumer does **not** see it. |
927
+ | `instance` | The default. Any project on the instance, and any local consumer by path or git. |
928
+ | `partner` | Also consumers outside the instance that a partner grant admits. |
929
+ | `external` | Publicly consumable. |
930
+
931
+ A consumer is read at one audience: a family project at `project`, so it sees
932
+ everything; a path, git or hosted consumer outside the family at `instance`, so
933
+ it sees `instance`, `partner` and `external` entries only. A name exported to a
934
+ narrower audience is refused by `externals add` (and `externals use`), naming
935
+ the export's audience, the consumer's and this ranking. Because a project cannot
936
+ tell from its own tree whether its consumers are hosted department units,
937
+ `validate` notes every `department` export (`EXPORT_AUDIENCE_NARROW`, a
938
+ notice): widen it to `instance` when a sibling checkout or a git consumer uses
939
+ it, or keep it and allow the notice. A re-export can narrow an audience, never
940
+ widen it (`EXPORT_WIDENS_AUDIENCE`).
941
+
942
+ | Command | Description |
943
+ |---------|-------------|
944
+ | `wairon member add <alias> <source> [--project] [--description <text>]` | Create a member at `<source>` (`path`, `../path` or `git-url[#commit]`) and declare it by the shorthand. A **part** by default: a specs folder (and, outside this project, its `partOf`). With `--project` a project: its `project.yaml` declaring `<alias>` as its id and its L0, each only when absent. A git member is never scaffolded: it is pinned at the commit given, else the default branch head, and its content decides what it is |
945
+ | `wairon member promote <alias> [--id <id>]` | Make a part an independent project **in place**: its id, an L0 exporting exactly what this project uses of it, references across the new boundary respelled `alias::name` (`<parent id>::name` the other way), the parent declared as its external, pins on both sides. Refused for a part fetched from git, a trustedLink that would cross the boundary, a component used across it that its subsystem does not publish, or an id already taken |
946
+ | `wairon member demote <alias> [--home <subsystem>] [--packs adopt\|drop]` | Make a project member a part **in place** — promote's inverse: its own metadata goes home (as `internalize` sends it), its L0, lock and pins end, references across the old boundary become local ids. Refused while another family project consumes it, or for a member fetched from git |
947
+ | `wairon member update <alias> [--ref <ref> \| --commit <sha>] [--report]` | Move a git member's pinned commit to its ref's head (or the ref or commit given), printing the spec files it adds, changes and removes; `--report` writes nothing. The only way a git member's content changes; this project's lock reads stale until re-locked when the content moved |
948
+ | `wairon member move <alias> <path>` | Move a contained member's directory and point its `members` entry there (a legacy L1 mount is moved into `members` first) |
949
+ | `wairon member attach <alias> <path> [--description <text>]` | Make the **existing** project at `<path>` a member, keeping its L0, subsystems, packs and lock; its id is declared when it only defaulted one (the id its lock approved, else its effective id). Refused when its id collides with a family project's |
950
+ | `wairon member detach <alias> [--widen]` | Take a member out of the family: this project and every family consumer reach it as an external by `source.path`, this project's pinned. Refused (`audience-too-narrow`, each export named with its users) while a family project uses a name the member exports only to the family; `--widen` instead widens exactly those used exports to `instance` in the member's L0, each shown in the plan |
951
+ | `wairon member adopt <alias>` | Make this project's external found by a path inside it a member again — detach's inverse |
952
+ | `wairon member rename-alias <old> <new>` | Rename one alias of this project (a member or an external) and respell this project's references through it; no member or sibling changes |
953
+ | `wairon member internalize <alias> [--into <subsystem>] [--packs adopt\|drop] [--export <name>…]` | Fold a member into this project's own specs folder. A **part** is a storage move: its subsystems move in and nothing else changes. A **project** is demoted first — its own metadata goes to a home (its L0 vision onto the `--into` subsystem, its boundaries, requirements and databases into this L0, its language, profile and depth onto the moved subsystems, its members and externals into this configuration, its packs adopted or dropped), what has no home (its lock, pins, derived outputs) is deleted and listed, and every family project that consumed it is re-pointed here |
954
+ | `wairon project rename <new-id> [--project <alias path>]` | Move a project's id — this project's, or a member's named by its alias path — and every reference to the old id family-wide; the old id is kept in `previousIds`. Lists every project it writes to re-lock |
955
+ | `wairon subsystem externalize <id> --path <dir> [--as part\|project]` | Move an internal subsystem's specs into a **part** at `<dir>` (a new one, declared under the subsystem id, or the existing part there) — a storage move: no reference, export or pin changes, the same verdict. With `--as project` the move is followed by a promote, family-wide: references across the new boundary become `alias::name`, what crosses it is exported and imported, and every other family project's names are checked to keep resolving. You move the source code |
956
+
957
+ On a hosted instance a project member is a project record of its own and
958
+ inherits access from its parent, while a **part is part of its parent's record**:
959
+ it gets no record, and a demote retires the member's record (disabled as "part of
960
+ `<parent>`", audited `member.retired`, its own-scope settings kept) and a promote
961
+ re-enables it; the record's `disabledReason` (shown in the web app and `host
962
+ project list`) says why. Hosted roots are isolated, so a member with a `../` or git source
963
+ is refused there; a `hosted:` project member is its own top-level record.
964
+ `sdd_detach_member` also **moves** the member to an isolated root of its own
965
+ (writing `source: { hosted: <id> }` for its consumers) and `sdd_adopt_member`
966
+ with `path` moves it back. A `source.hosted` producer outside a hosted server is
967
+ reported as "hosted-only producer `<id>`: available only through the hosted
968
+ server".
969
+
970
+ **A part on its own.** A part stored outside its parent can be validated in its
971
+ own repository's CI: run `wairon externals pin` at the part (with the parent on
972
+ disk at `partOf.path`) to pin the excerpt of the parent it uses; `wairon validate`
973
+ there then judges the part against that pin (`PART_JUDGED_ALONE`). Unpinned, it
974
+ says "part of `<parent>`; validate from the parent" (`PART_UNPINNED`).
975
+
976
+ **Family migrations.** `attach`, `detach`, `adopt`, `promote`, `demote`,
977
+ `rename-alias`, `internalize`, `project rename` and `subsystem externalize` all run
978
+ one flow: plan, print the plan (each project's edits, every refusal, the notes,
979
+ the file changes, the projects to re-lock), then apply it all or nothing.
980
+ `--report` prints the plan and writes nothing; otherwise the command asks —
981
+ `--yes` answers, and a shell with no terminal and no `--yes` writes nothing. A
982
+ refused plan exits non-zero and writes nothing. The plan is computed by running
983
+ the verb's writes on a private copy of the family's `.wai` trees (its parts'
984
+ included); applying it stages every change with a backup under each project's
985
+ `.wai/transactions/<id>/` (never committed) and swaps them in, restoring every
986
+ backup on any failure. A crash mid-swap leaves a journal: `wairon status` and
987
+ `wairon validate` show it as a notice, and `wairon doctor --fix` rolls it back.
988
+ No verb ever locks — each names the projects to re-lock. A promote (or `externalize --as project`) re-roots every
989
+ source path that moves with the new project: a path inside the new project's folder becomes
990
+ member-relative, and one outside it (source code you have not moved yet) becomes a member-relative
991
+ *planned* path — `SOURCE_FILE_PLANNED`, never a path that escapes the root — so the new project
992
+ validates on day one and you move the code when you are ready. A topic finding
993
+ (`UNCONSUMED_TOPIC`, `UNSOURCED_SUBSCRIPTION`) is sited at its topic: an allow names it
994
+ with `at: <topic>`. A member stored in a
995
+ `../` sibling checkout on the same volume joins the same transaction: its files
996
+ change all-or-nothing with the family's, the coordinator's journal names every
997
+ owner (so `wairon doctor --fix` recovers from either root), and the plan says
998
+ plainly "files: all-or-nothing; commits: one per repository" — commit each
999
+ repository yourself; no commit across repositories is ever atomic. A sibling on
1000
+ another volume is refused (`cross-volume`), and a git member always is (its files
1001
+ are a read-only cache). `member demote` names every L0 export entry it removes.
1002
+
1003
+ **Required packs.** A project may require packs of the members below it with
1004
+ `composition.requirePolicies` (see
1005
+ [Extending wairon](extending-wairon.md#governance--what-a-pack-changes-and-what-a-parent-requires)).
1006
+ `wairon member add --project` — and `wairon init` run in a subdirectory when you
1007
+ answer yes on a terminal, which creates a project member the same way — writes those packs into the new member's selection once,
1008
+ each pinned to the highest installed version its range admits, sets the
1009
+ `projectType` a requirement names, and prints what it applied and each
1010
+ requirement nothing installed satisfies. Scaffolding is an unattended pack write,
1011
+ so it shows no impact report; run `wairon pack impact <name>` at the member's
1012
+ root to see one.
1013
+
1014
+ **Deprecated forms.** For one release wairon still reads, and reports: a leading
1015
+ `::` (`::shared::money`), `super::` (`super::sibling`), a member path
1016
+ (`billing::invoice::invoice_portal`) and an L1 subsystem carrying
1017
+ `projectPath` (`DEPRECATED_MOUNT_FORM`), and a member's long-form `path` key
1018
+ (`{ path: services/x }`; the one location key is `source`). `wairon doctor
1019
+ --fix` rewrites them to `members`, the one location key and `alias::name`.
1020
+
1021
+ ---
1022
+
1023
+ ## Domains
1024
+
1025
+ A domain is a unit of agent ownership. Subsystem-derived domains come from the
1026
+ spec tree (read-only); free-standing domains live in `.wai/topology.yaml`.
1027
+
1028
+ | Command | Description |
1029
+ |---------|-------------|
1030
+ | `wairon domains list` | List all domains (subsystem-derived + free-standing) |
1031
+ | `wairon domains scan [--add]` | Detect physical directory candidates; `--add` adds selected ones as free-standing domains |
1032
+ | `wairon domains add [--path] [--id]` | Manually add a free-standing domain |
1033
+ | `wairon domains remove <id>` | Remove a free-standing domain (subsystem-derived domains cannot be removed here) |
1034
+
1035
+ ---
1036
+
1037
+ ## Skills
1038
+
1039
+ The five SDD skills (`sdd-architect`, `sdd-narrative`, `sdd-auditor`,
1040
+ `sdd-implement`, `sdd-delegate`) drive the spec-driven workflow inside your AI
1041
+ tool.
1042
+
1043
+ | Command | Description |
1044
+ |---------|-------------|
1045
+ | `wairon skills list` | List the built-in SDD skills |
1046
+ | `wairon skills install` (alias `sync`) | Install/refresh the skills into each active target's skills dir |
1047
+
1048
+ ---
1049
+
1050
+ ## MCP
1051
+
1052
+ The wairon MCP server exposes topology and `sdd_*` tools so AI tools can query
1053
+ and author specs directly.
1054
+
1055
+ | Command | Description |
1056
+ |---------|-------------|
1057
+ | `wairon mcp serve` | Start the MCP server (stdio transport) |
1058
+ | `wairon mcp install [--global] [--config-dir <path>] [--backend claude\|gemini]` | Register the server. Default: project-local. `--global` uses the home config (respects `CLAUDE_CONFIG_DIR`/`GEMINI_CONFIG_DIR`); `--config-dir` installs into an explicit, validated config dir (requires `--backend`) |
1059
+ | `wairon mcp install --hosted <url> [--project <id>] [--token <token>]` | Register a **hosted** entry against an instance instead of the local stdio server; the token defaults to the credential stored by `wairon login` |
1060
+ | `wairon mcp status` | Show whether the server is registered |
1061
+
1062
+ A project-local registration (`.mcp.json`, `.gemini/settings.json`) is committed with the
1063
+ project, so it holds nothing machine-specific: it runs `wairon mcp serve` from the PATH (or
1064
+ `node ./<path>` when the CLI lives inside the project, as in a checkout of wairon itself),
1065
+ and the server attaches to the project it is started in. A `--global` registration is
1066
+ machine-wide and names the running CLI by its absolute path.
1067
+
1068
+ The local server offers 47 tools:
1069
+
1070
+ | Group | Tools |
1071
+ |-------|-------|
1072
+ | Topology (read) | `listAgents`, `getAgent`, `listDomains`, `validateTopology`, `getProjectConfig` |
1073
+ | Authoring | `sdd_initialize_system`, `sdd_add_subsystem`, `sdd_set_public_interfaces`, `sdd_add_component`, `sdd_define_interface`, `sdd_set_endpoints`, `sdd_write_narrative`, `sdd_add_type`, `sdd_update_spec`, `sdd_delete_spec` |
1074
+ | Reading and checking | `sdd_get_spec`, `sdd_get_status`, `sdd_validate_tree` |
1075
+ | Renames and moves (in this tree) | `sdd_rename_component`, `sdd_rename_method`, `sdd_rename_param`, `sdd_rename_type`, `sdd_rename_field`, `sdd_move_methods` |
1076
+ | Members and family migrations (each takes `dryRun`) | `sdd_add_member`, `sdd_move_member`, `sdd_externalize_subsystem`, `sdd_promote_member`, `sdd_demote_member`, `sdd_internalize_member`, `sdd_attach_member`, `sdd_detach_member`, `sdd_adopt_member`, `sdd_rename_project`, `sdd_rename_member_alias` |
1077
+ | Externals | `sdd_add_external`, `sdd_update_external`, `sdd_remove_external`, `sdd_pin_externals`, `sdd_get_externals_status` |
1078
+ | Public surface (the producer's side) | `sdd_list_consumers`, `sdd_surface_diff` |
1079
+ | Network | `sdd_set_network`, `sdd_get_network_flows`, `sdd_explain_flow` |
1080
+ | Packs | `sdd_pack_impact` |
1081
+ | Delegation | `sdd_get_agent_brief` (the live brief for one agent; also served as the `wairon-agent://` resource) |
1082
+
1083
+ A hosted server adds its own `sdd_host_*` and `sdd_landscape_*` tools; see the
1084
+ [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md).
1085
+
1086
+ ---
1087
+
1088
+ ## Hosting (self-hosted server)
1089
+
1090
+ `wairon serve` runs wairon as an HTTP server that hosts many **fully-isolated**
1091
+ projects behind one endpoint — a public **data plane** (the `sdd_*` tools over
1092
+ streamable HTTP, per-project API-key scoped) and an admin **control plane**
1093
+ (project & key lifecycle, state-scoped lock). See the
1094
+ [hosted server guide](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md) for the architecture, Docker
1095
+ self-hosting, auth, and sizing.
1096
+
1097
+ ### `wairon serve [--host <h>] [--port <p>] [--admin-host <h>] [--admin-port <p>] [--data-dir <path>] [--no-auth]`
1098
+ Start the hosting server. Data plane on `0.0.0.0:8080` (`POST /mcp`, `/healthz`,
1099
+ `/readyz`); admin plane on `127.0.0.1:8081` (`/admin/*`). Auth is **on by
1100
+ default** and requires `WAIRON_ADMIN_TOKEN` (the master credential) — the server
1101
+ refuses to start without it; `--no-auth` disables data-plane auth for a trusted
1102
+ network. `--data-dir` (or `WAIRON_DATA_DIR`, default `~/.wairon/data`) is the data
1103
+ root holding `projects/` and `auth/`. `WAIRON_AUDIT_POLICY` (optional) sets the
1104
+ durable audit policy as a JSON object of overrides laid over the secure default
1105
+ (capture `info` and above, keep events 90 days and `security` events 365, skip read
1106
+ events, redacted metadata) — e.g. `{"retentionDays":30,"includeReadEvents":true}`.
1107
+ Its fields are `enabled` and `includeReadEvents` (booleans), `retentionDays` and
1108
+ `securityRetentionDays` (non-negative numbers), `minimumLevel` (`debug`, `info`,
1109
+ `warning`, `error` or `security`) and `metadataMode` (`none` or `redacted`; the
1110
+ retired `full-redacted` still loads as `redacted`, with a deprecation warning); an unknown field, a wrong type or invalid JSON stops the server at
1111
+ startup with a message naming the variable. `WAIRON_QUOTA_POLICY` (optional) sets the
1112
+ advisory quota policy the same way, as a JSON object of overrides laid over the
1113
+ disabled default (`enabled: false`, mode `observe`, no limits) — e.g.
1114
+ `{"enabled":true,"mode":"warn","maxProjectsPerUser":20}`. Its fields are `enabled`
1115
+ (boolean), `mode` (`observe` or `warn`, the label an exceeded limit is reported
1116
+ with; quotas never block or throttle yet, so the retired `block` loads as `observe`,
1117
+ with a deprecation warning) and the non-negative integer
1118
+ limits `maxProjectsPerUser`, `maxMcpRequestsPerMinute`, `maxProjectBytes` and
1119
+ `maxAuditEventsPerDay`; it is refused at startup exactly as the audit policy is.
1120
+
1121
+ ### `wairon host …` — control plane
1122
+ Runs **in-process** (no running server needed), so it works over SSH /
1123
+ `docker exec`. Reads the master credential from `WAIRON_ADMIN_TOKEN`.
1124
+
1125
+ Every project lives in an **organization unit**, and access is a grid of
1126
+ **permission assignments** (subject × scope × capability). An API key acts as its
1127
+ **owner's** live permissions, narrowed to the projects it names. The first
1128
+ project on a new instance:
1129
+
1130
+ ```sh
1131
+ wairon host unit create --slug acme # a root unit (business_entity)
1132
+ wairon host project create --id shop --unit acme
1133
+ wairon host permission set --user alice --capability project:read --project shop
1134
+ wairon host permission set --user alice --capability project:write --project shop
1135
+ wairon host key mint --project shop --owner alice # → wk_… (shown once)
1136
+ ```
1137
+
1138
+ `project:write` does not include `project:read`: a key whose owner can only
1139
+ write is refused every read tool. `host permission set` warns when it grants
1140
+ `project:write` to a user who cannot read that scope, and `host key mint --owner`
1141
+ warns when the owner cannot read the project — each naming the grant that fixes
1142
+ it. An owner needs no user record — an id with no record acts as a service
1143
+ principal.
1144
+
1145
+ | Command | Description |
1146
+ |---------|-------------|
1147
+ | `wairon host unit create --slug <slug> [--kind <kind>] [--name <name>] [--parent <unitId>]` | Create an organization unit. A root unit (no `--parent`) must be a `business_entity`, which is its default kind; under `--parent` the default is `team`, and `department`, … are allowed where the hierarchy admits them |
1148
+ | `wairon host project create --id <id> --unit <unitId>` | Provision a new isolated project (its own `.wai/` tree) in a unit |
1149
+ | `wairon host project list` | List hosted projects |
1150
+ | `wairon host project destroy --id <id>` | Remove a project and its tree |
1151
+ | `wairon host demo [--id <id>] [--unit <unitId>] [--force]` | Provision a project seeded with an example spec tree (default id and unit `demo`; the unit is created if absent), so the canvas has content |
1152
+ | `wairon host permission set --user <userId> --capability <cap> [--value yes\|approval\|no\|inherit] [--project <id> [--subsystem <id>] \| --unit <unitId> \| --instance]` | Set one assignment. Capabilities: `project:read`, `project:create`, `project:write`, `project:admin`, `approval:decide`. The scope defaults to the instance |
1153
+ | `wairon host permission list [--user <userId>] [scope flags]` / `remove --id <assignmentId>` | List or remove assignments |
1154
+ | `wairon host key mint --project <id\|*> --owner <userId> [--label <label>]` | Mint an API key acting as the owner's live permissions (plaintext shown once). The project must exist (`*` aside); a token naming a project covers its members. Without `--owner` (the legacy `--role editor\|admin` mint) the key resolves to **zero** permissions and the command warns |
1155
+ | `wairon host doctor [--fix]` | Inspect the data dir and, with `--fix`, migrate it: roll back a transaction a crash left unfinished there, apply the permission-model migration, migrate every hosted project's spec tree onto the reachability model (each rewritten project is audited as `migration.reachability` and owes one re-lock by its approvers; the server never does this on its own when it binds a project, because the rewrite moves the approved design and a read-only request must not write), then register every hosted family's members as records of their own (no grant written — access is inherited through the parent chain — and every member-qualified key entry rewritten to a record id), all or nothing, audited |
1156
+ | `wairon host key list [--project <id>]` | List API keys |
1157
+ | `wairon host key revoke --id <id>` | Revoke a key |
1158
+ | `wairon host lock --project <id>` | The same lock flow as `wairon lock` (design gate, `members`, `code` beside the claim, format 3) against the hosted project |
1159
+ | `wairon host git enable \| disable \| sync \| commit \| status \| sync-config` | Bind a hosted project to its real repository (wairon commits only `.wai/`) |
1160
+ | `wairon host producer configure \| produce \| remove \| list` | Project a hosted project to Notion or Miro |
1161
+ | `wairon host secret set \| list` | Set integration secrets at runtime (`git-token`, `notion-token`, `miro-token`, `signing-secret`) — no restart |
1162
+ | `wairon host packs list [--project <id>]` | List the server-global packs, or one hosted project's |
1163
+ | `wairon host packs install --file <pack.yaml> [--name <n>] [--project <id>] [-y]` | Install a declarative pack server-wide, or into a hosted project. Into a project it first shows the pack's impact on that project (measured by the host, writing nothing) and asks; `--yes`, or no terminal, installs without the report and says so |
1164
+ | `wairon host packs remove --name <n> [--project <id>] [-y]` | Remove a pack; from a project it first shows what the pack accounts for there and asks, as install does |
1165
+
1166
+ The hosted API and web UI preview the same way:
1167
+ `POST /admin/projects/{id}/packs/{name}/impact` (a declarative pack's YAML as
1168
+ `content`, or no body to preview adopting the server-global pack of that name)
1169
+ and `POST /admin/projects/{id}/packs/{name}/removal-impact`; both need
1170
+ `project:read` and write nothing. The hosted MCP data plane offers the same
1171
+ preview as `sdd_host_pack_impact`. The unattended hosted policy writes —
1172
+ reconcile, a project type change, and a policy-governed project creation —
1173
+ return the impact of every pack they applied in their results.
1174
+
1175
+ ---
1176
+
1177
+ ## Connecting to a hosted instance
1178
+
1179
+ | Command | Description |
1180
+ |---------|-------------|
1181
+ | `wairon login <url> [--token <token>] [--project <id>]` | Store a bearer credential for a hosted instance on this machine (`--project` verifies it against that project first; the token defaults to `WAIRON_REMOTE_TOKEN`) |
1182
+ | `wairon logout [url]` | Forget a stored credential (local only — it does not revoke it), or list what is stored |
1183
+ | `wairon remote push \| pull [--url] [--project] [--token] [--unit <id>] [--force] …` | Migrate a spec tree to or from a hosted project (`--unit` creates the destination project first) |
1184
+ | `wairon remote attach \| detach \| status` | Bind this checkout to a hosted project; while attached, `wairon lock` locks the hosted project |
1185
+
1186
+ ## Surfaces, externals and producers
1187
+
1188
+ | Command | Description |
1189
+ |---------|-------------|
1190
+ | `wairon externals add <alias> [<source>] [--project <id>] [--ref <ref>] [--dir <dir>] [--use a,b\|'*'] [--description <text>] [--no-pin] [--dry-run] [--json]` | Declare one external in `.wai/project.yaml`. The source is the location grammar members use: `../sibling`, `hosted:<id>`, `<git url>` or `<git url>#<commit>` (the commit is the ref the pin follows, fixed there); omit it when the family provides the producer. Text that is no location (braces, quotes, whitespace) is refused. The declaration is checked against the producer it reaches **before** anything is written — `--dry-run` included: one answering to another id, or not exporting a `use` name to this project, is refused naming the id, the closest exported names, or the narrower audience the name is exported to (see Export audiences). Pins by default; a producer that cannot be read leaves it declared and unpinned, saying why, and exits 2 (nothing was pinned). A refusal is one sentence naming the accepted form, and exits 1 |
1191
+ | `wairon externals use <alias> [--add a,b\|'*'] [--remove c] [--dry-run] [--json]` | Change one declared external's `use` imports, so the specs may name the producer's public names bare (e.g. so an `implements` of the producer's trait spells its types as the producer does — though `alias::name` and the bare imported name compare as one type either way). An added name the producer does not export to this project is refused. The pin is untouched. Exits 1 on a refusal |
1192
+ | `wairon externals remove <alias> [--dry-run] [--json]` | Remove one external: its declaration and its pin (`.wai/externals/<alias>.yaml` and its lock entry) together. An orphaned pin — one whose declaration was deleted by hand, which `externals list` shows — is removed too. Exits 1 when the alias is neither declared nor pinned |
1193
+ | `wairon externals consumers [--search <dirs...>] [--json]` | From a producer's root: the projects that consume it — each with the alias and section it declares it under and the public names its specs use — so a producer sees who breaks before it changes its surface. Without `--search`, the family read from the highest root in reach. `--search ..` (any folders) also scans those folders and the project roots directly under them for projects declaring this one as an external by a path that resolves here, or by git — a sibling checkout is found this way. A hosted consumer, or one on another machine, declares its dependency on its own side and is not visible here (`sdd_list_consumers` is the same answer for an assistant) |
1194
+ | `wairon externals pin [alias…] [--json]` | Pin declared externals into `.wai/externals/<alias>.yaml` and `.wai/externals.lock.yaml`. The snapshot is rewritten whenever anything it carries moved — not only the signatures the digest covers: a producer that added `abi: c`, changed a transport or a role, or recorded a rename is refreshed by a re-pin. Exits 1 when an alias could not be pinned (unresolved or unreachable — its previous pin stays) |
1195
+ | `wairon externals status [--json]` | Each pin compared with its live producer per used member — `unchanged`, `changed`, `renamed` (with the new name), `removed`, `unlocked`, `unavailable` — and each external's health (`incompatible`, `not compared`, `drifted`, `ok`); a pinned snapshot that no longer carries what the producer says (a stale `abi`, transport or role) is `drifted`, never `ok`, and names the stale facts. A use the lock does not hold is still compared with the live producer: gone from it, it is `removed` or `renamed`. Git producers are fetched. The opt-in **live** gate: exits 1 when any external is incompatible, 2 when nothing is incompatible but something could not be compared (never a pass), 0 otherwise |
1196
+ | `wairon externals list [--json]` | The declared externals, how each resolves and what is pinned; a malformed declaration, and an orphaned pin whose declaration is gone, are listed with their problem, never hidden |
1197
+ | `wairon surface export \| import \| list [--audience <level>] [--format native\|openapi] [--portal <id>] [--out <path>] [--source <path>]` | Exchange a public surface document: export this project's (native snapshot or one OpenAPI 3.1 document per portal), import one, or list them. OpenAPI defaults to the `project` audience, so every HTTP Portal of the project's own is described — one its L0 export table never exports included (a wider `--audience` narrows to what the table shares there); the native snapshot defaults to `instance`. In the OpenAPI document a path placeholder — `{name}`, or the Express spelling `:name`, which is rewritten to `{name}` — is a parameter `in: path`, the other params the query of a GET/DELETE or the JSON body of a POST/PUT/PATCH. The Portal's `basePath` is joined into every path (the document carries no `servers` entry: where it is served is deployment, not design). The parameter that carries the credential the Portal's `auth` binds (a bearer `token`, say) is left out of the parameters and the body — `security` describes it — and named under `x-wairon-credential-param` so a wairon reader can restore it. An unknown `--portal` is refused naming the portals the surface renders |
1198
+ | `wairon surface diff [--against <ref\|file>] [--json]` | The public-surface changelog: this project's export table now against the same table at its last **committed** approval (or at a git revision, or in a saved native snapshot from `surface export`) — every exported name and contract method `added`, `removed`, `renamed` (from its rename trace) or `changed` (signature or type shape), and how many a consumer may have to follow. What a producer writes release notes from before it re-locks; `wairon externals consumers --search <dir>` then says who uses what. Read-only. With no approval ever committed it says so (lock and commit first, or name `--against`). `sdd_surface_diff` is the same answer for an assistant |
1199
+ | `wairon produce <notion\|miro> [--page <id>] [--token <token>]` | Project the local spec tree to Notion or Miro (the token comes from `--token`, the environment, else a prompt; nothing is stored) |
1200
+
1201
+ ## Tooling
1202
+
1203
+ | Command | Description |
1204
+ |---------|-------------|
1205
+ | `wairon dev [--port <port>] [--open]` | A local single-project dev server: the wairon web UI over the current project, no login or tenancy (loopback only). It serves the same project over MCP (streamable HTTP) at `POST http://127.0.0.1:<port>/mcp?project=local` — the current project is registered as `local`; without `?project=local` the endpoint answers 403. For an AI tool on the same machine the stdio server (`wairon mcp serve`, what `wairon mcp install` registers) remains the usual route |
1206
+ | `wairon update [--check] [--channel <name>]` | Check/install the latest release; switch channel |
1207
+ | `wairon aliases list` | Show command aliases (`wai`) and their status |
1208
+ | `wairon aliases enable <name>` / `disable <name>` | Create / remove an alias |