@wairon/cli 5.1.1-dev.101 → 5.1.1-dev.103

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.
@@ -0,0 +1,615 @@
1
+ # Extending wairon — packs & wrapper products
2
+
3
+ Wairon core is a spec engine and a rule enforcer. Everything platform- or
4
+ product-specific — custom architectural profiles, language/platform tables,
5
+ extra conformance rules — is injected from outside through **extension
6
+ packs**: plain configuration files (YAML, or a JS module for programmatic
7
+ rules) that wairon reads and imports. A product built this way (a
8
+ *wrapper*) is just packs + an installer on top of the user's existing
9
+ wairon installation: no fork, no build step, and wairon core never learns
10
+ the platform exists.
11
+
12
+ A working wrapper lives at [`examples/wrapper/`](../examples/wrapper/) and
13
+ is guarded by `tests/examples/wrapper-example.test.ts`, so it cannot
14
+ silently rot.
15
+
16
+ ## Installing and selecting packs
17
+
18
+ > **New model (preferred).** Installing a pack makes it *available*; a project
19
+ > then *selects* it. Installing no longer grants a pack authority over every
20
+ > project on the machine — see [pack scoping](design/pack-scoping.md).
21
+
22
+ ```sh
23
+ wairon pack install ./appenser-1.2.0.wpack # into this wairon install's store
24
+ wairon pack which appenser # which version/digest/origin resolves?
25
+ cd my-project
26
+ wairon pack use appenser # THIS project applies it
27
+ ```
28
+
29
+ `pack use` records the selection by name in `.wai/project.yaml`, carrying the
30
+ origin the pack was installed from so a fresh clone or CI runner can obtain it:
31
+
32
+ ```yaml
33
+ extensions:
34
+ packs:
35
+ - name: appenser
36
+ version: 1.2.0 # only when pinned; omit to track latest installed
37
+ integrity: sha256-… # written by --pin
38
+ source: https://…/appenser-{version}.wpack
39
+ ```
40
+
41
+ - `--pin` freezes the resolved version **and** its content digest.
42
+ - `--source <url>` records an explicit fetch URL (overriding the install origin).
43
+ - `--bundle` marks the pack for committing under `.wai/packs/`, so the repo needs
44
+ no machine setup at all — the answer for private packs and air-gapped CI. A
45
+ committed bundle resolves *before* the store.
46
+ - `wairon pack unuse <name>` drops the selection; the pack stays installed.
47
+
48
+ **A declared pack that cannot be resolved is an error**, not a silent skip:
49
+ `validate`, `status`, `lock`, `generate`, and every `sdd_*` MCP call refuse and
50
+ name the pack plus how to install it. A gate that quietly enforces less than the
51
+ project declared is worse than one that fails.
52
+
53
+ The code names the remedy, so the message tells you which fix applies:
54
+
55
+ | Code | Meaning |
56
+ |------|---------|
57
+ | `PACK_NOT_INSTALLED` | absent from both the store and `.wai/packs/` — obtain it (`pack sync` / `pack install`) |
58
+ | `PACK_VERSION_UNSATISFIED` | the pack *is* installed, but not at the pinned version (which the message lists) — correct the pin |
59
+ | `PACK_INTEGRITY_MISMATCH` | resolved content does not match the pinned digest — reinstall, or update the pin if the change is intended |
60
+
61
+ All three are error severity and appear in `wairon rules list`, so a project can
62
+ retune them through `rules.sddRuleSeverity` if it must.
63
+
64
+ If you install from a local path, no fetchable source can be recorded and
65
+ `pack use` says so — bundle it, or pass `--source`.
66
+
67
+ ### Applying a pack to new projects by default
68
+
69
+ A pack in the store can ask to be selected by every project created from then on:
70
+
71
+ ```yaml
72
+ # pack.yaml
73
+ applyByDefault: true
74
+ ```
75
+
76
+ `wairon init` seeds it into the new project's `extensions.packs` as an explicit
77
+ `name@version` selection. That is what a machine-wide install *should* mean — a
78
+ default for new work, written where it is visible in review and removable — rather
79
+ than retroactive authority over projects that never mentioned it.
80
+
81
+ ### Reproducibility
82
+
83
+ With `rules.enforceReproducibility` (the default), a selection that neither pins a
84
+ version nor bundles is reported: `UNPINNED_PACK_SELECTION` resolves to whatever
85
+ this machine happens to have installed, so a clone or CI can enforce a different
86
+ rule set than you do. `PACK_SOURCE_UNFETCHABLE` means nothing can obtain the pack
87
+ elsewhere at all.
88
+
89
+ Both are **warnings while you work** and errors under `wairon validate --ci`, so
90
+ `wairon pack use appenser` stays a one-liner while CI refuses a pack set it cannot
91
+ reproduce. `--pin` or `pack bundle` clears them; `enforceReproducibility: false`
92
+ accepts the drift deliberately.
93
+
94
+ ### CI and a fresh machine
95
+
96
+ `wairon pack install` accepts an HTTP(S) URL, and **`wairon pack sync` takes no
97
+ arguments** — each selection carries its own source, so one command restores a
98
+ project's doctrine anywhere:
99
+
100
+ ```yaml
101
+ - uses: SYW-Apps/Waffle-AIron/.github/actions/setup-wairon@v5
102
+ with:
103
+ packs: sync # sync (default) | none (bundled repos) | explicit sources
104
+ - run: wairon validate --ci
105
+ ```
106
+
107
+ Publishing a pack needs no registry — `wairon pack build` already emits
108
+ `<name>-<version>.wpack`, which is a release asset as-is:
109
+
110
+ ```sh
111
+ wairon pack build && gh release create v1.2.0 appenser-1.2.0.wpack
112
+ ```
113
+
114
+ `source` supports `{version}` for pinned selections, and GitHub's
115
+ `releases/latest/download/<asset>` resolves floating ones with no API call or
116
+ token. `${VAR}` expands from the environment, so a private URL can take a token
117
+ from a CI secret without committing it.
118
+
119
+ Fetching happens **only** in `pack install <url>` and `pack sync` — never during
120
+ `validate`, `status`, `generate`, or an MCP call, because the core workflow is
121
+ required to work offline.
122
+
123
+ ## Governance — what a pack changes, and what a parent requires
124
+
125
+ **Packs may loosen wairon's checks, and that is by design.** Packs exist to
126
+ adjust wairon's checks and behaviour: a pack for an automation or integration
127
+ platform — system design on a far less capable runtime — may have to turn rules
128
+ down or remove concepts that cannot exist there. A pack's loosening is never a
129
+ finding. What wairon insists on is that installing a pack is **intentional**,
130
+ that its **impact is understood** before it applies, and that it applies **per
131
+ project**.
132
+
133
+ ### Pack impact: shown before every pack write
134
+
135
+ ```sh
136
+ wairon pack impact appenser # or appenser@1.2.0 — writes nothing
137
+ ```
138
+
139
+ The report states, and never judges:
140
+
141
+ - the pack's **doctrine against wairon's defaults** — per profile, each rule it
142
+ loosens, raises, turns off or adds and a design depth it changes; each profile
143
+ it adds (or builtin profile it redefines), stereotype it removes or
144
+ discourages and edge it licenses; pack-wide, the patterns, guarantee tokens,
145
+ language tables and assertion codes it adds;
146
+ - which of its **profiles would govern** this project;
147
+ - the **findings that change** on this project if it is applied — a dry
148
+ validate of the current and the candidate configuration, neither written —
149
+ introduced, resolved and regraded, with both totals. What the report says the
150
+ findings will be is what validation reports after the write.
151
+
152
+ `pack use`, `pack unuse`, `pack add`, `pack remove`, and `pack install` when it
153
+ moves this project's floating selection all show this report and **ask before
154
+ writing**; anything but yes writes nothing. `-y, --yes` (or a run with no
155
+ terminal, as in CI) writes without the report and says so. On the MCP server,
156
+ `sdd_pack_impact` answers the same report read-only, so an agent can propose a
157
+ pack and its human applies it with `wairon pack use`.
158
+
159
+ A hosted instance does the same for its projects: the admin API, the web UI
160
+ and the hosted MCP tool `sdd_host_pack_impact` preview an install, an adoption
161
+ from the server-global catalog, or a removal with `project:read`, writing
162
+ nothing, and the web UI asks before it writes (an uploaded `.wpack` archive has
163
+ no preview yet, and the UI says so before asking). The hosted **policy** writes
164
+ — reconcile, setting a project type, and creating a project under the instance
165
+ pack policy — are unattended by nature, so they show nothing first; they return
166
+ the impact of every pack they applied instead, and a policy-governed creation
167
+ also records it in its audit event and, when it ran through an approval, on the
168
+ approval as its `executionSummary`.
169
+
170
+ ### Requiring packs of members — `composition.requirePolicies`
171
+
172
+ A project that contains others ([members](cli.md#members)) can require that
173
+ they run with given packs:
174
+
175
+ ```yaml
176
+ # the parent's .wai/project.yaml
177
+ composition:
178
+ requirePolicies:
179
+ - pack: acme/service-baseline
180
+ version: ^1.2 # a semver range: 1.2.0, ^1.2, ~1.2.3, 1.x, >=1.2.0 <2.0.0, a || b, *
181
+ profile: service # optional: the member must be governed by this profile of the pack
182
+ ```
183
+
184
+ A member **adopts** a requirement by selecting the pack in its own
185
+ `extensions.packs` — the same exact, digested selection `wairon pack use --pin`
186
+ records; there is no separate policy format. `*` also admits a pack that
187
+ declares no version. The hosted instance pack floor and hosted profile
188
+ selections keep working beside it; neither reads the other.
189
+
190
+ Requirements are judged **only in the family run** (`wairon validate` at a
191
+ project that declares members, or `--family`). A member's own gate never reads
192
+ its parent's requirements, so its verdict is the same with and without them.
193
+
194
+ | Code | Severity | Meaning |
195
+ |------|----------|---------|
196
+ | `POLICY_NOT_ADOPTED` | error | A member does not select a required pack, selects it unpinned (a floating selection is judged by nothing a family can reproduce), outside the range, or is not governed by the required profile. The message names the requirement and the member's selection. |
197
+ | `POLICY_DEVIATION` | notice | A member selects the pack but changes one of its settings in its own configuration — a rule severity, the design depth, a subsystem profile, a `lint.allow` over a rule the pack sets, or its naming, complexity or documentation settings. It names the setting, the pack's value and the member's. `--ci` never fails on it; a team that wants it to can raise it with `rules.sddRuleSeverity`. |
198
+ | `POLICY_REQUIREMENT_INVALID` | error | The requiring project's own `composition.requirePolicies` names a range that does not parse — reported at that project's own gate. |
199
+
200
+ The first two are tuned by the **requiring** project's `rules.sddRuleSeverity`.
201
+ A pack's own loosening of wairon's defaults is never reported by any of them.
202
+
203
+ `wairon member add`, and `wairon init` in a subdirectory, write the parent's
204
+ required packs into a new member's selection **once**, at creation — each pinned
205
+ to the highest installed version its range admits — and say what they applied
206
+ and which requirement nothing installed satisfies. Afterwards the selection is
207
+ the member's own.
208
+
209
+ ## Installing packs (legacy vendoring)
210
+
211
+ **Per project (recommended for repo doctrine):**
212
+
213
+ ```sh
214
+ wairon pack add path/to/pack.yaml # or a .cjs file, or a pack directory
215
+ ```
216
+
217
+ This **vendors** the pack into `.wai/packs/` and registers it in
218
+ `.wai/project.yaml → extensions.packs`. Commit `.wai/` — CI and every clone
219
+ then enforce the same doctrine; `wairon validate`, `--ci`, and the MCP
220
+ server's `sdd_validate_tree` all load packs identically, so an AI agent
221
+ authoring specs is held to the same rules as the pipeline.
222
+
223
+ **Machine-wide:**
224
+
225
+ ```sh
226
+ wairon pack add path/to/pack.yaml --global
227
+ ```
228
+
229
+ Installs into the global packs folder (`WAIRON_PACKS_DIR` or
230
+ `~/.wairon/packs`) — every pack there is auto-loaded for **every project on
231
+ this machine**, before the project's own packs (project packs win on
232
+ collision). Dropping pack files into that folder by hand works too; wairon
233
+ discovers `*.yaml`/`*.cjs`/`*.js` files and directories containing a pack
234
+ entry file (`pack.yaml` | `pack.cjs` | `index.cjs` | …). Because global
235
+ packs don't travel with the repo, use them for personal/org-machine
236
+ additions — a project can opt out with `extensions.useGlobalPacks: false`
237
+ for strict reproducibility.
238
+
239
+ **By hand** — packs are just config; you can skip the commands entirely:
240
+
241
+ ```yaml
242
+ # .wai/project.yaml
243
+ extensions:
244
+ packs:
245
+ - .wai/packs/my-doctrine.yaml
246
+ ```
247
+
248
+ Inspect and uninstall:
249
+
250
+ ```sh
251
+ wairon pack list # global + project packs, what each provides
252
+ wairon pack remove <name> # deregister + delete vendored files
253
+ wairon pack remove <name> --global
254
+ wairon init --pack <source> # doctrine applied at project birth
255
+ ```
256
+
257
+ ## Distributing a wrapper product (the ZIP recipe)
258
+
259
+ Wairon is distributed as standalone CLI binaries (GitHub releases) — a
260
+ wrapper product does **not** need npm either. Ship a release ZIP containing
261
+ your pack files plus a small install script; the user unzips and runs it:
262
+
263
+ ```sh
264
+ wairon pack add ./my-product-pack # per project, or:
265
+ wairon pack add ./my-product-pack --global # machine-wide
266
+ ```
267
+
268
+ The installer injects once and leaves — from then on plain `wairon
269
+ validate` enforces the doctrine, and `wairon pack remove` undoes it. See
270
+ [`examples/wrapper/install.js`](../examples/wrapper/install.js) for the
271
+ template. A spec-only product (implementation lives on a cloud platform)
272
+ needs nothing more: the wrapped repository holds specs + docs, wairon holds
273
+ the gate.
274
+
275
+ ## Declarative packs (YAML)
276
+
277
+ Data only — profiles and language/platform tables:
278
+
279
+ ```yaml
280
+ name: my-doctrine
281
+
282
+ profiles:
283
+ my-platform: # becomes a valid `profile` / projectType value
284
+ family: neutral # neutral | backend-like | frontend-like
285
+ forbiddenStereotypes: # error: PROFILE_FORBIDDEN_STEREOTYPE
286
+ - types: [Actor, Supervisor]
287
+ reason: why this platform cannot express them
288
+ discouragedStereotypes: # warning: PROFILE_DISCOURAGED_STEREOTYPE
289
+ - types: [Observer]
290
+ reason: why to avoid them here
291
+
292
+ languages:
293
+ my-platform: # matched against targetLanguage (L0/L1), lowercased
294
+ unsupportedFlow: # warning: LANGUAGE_FOREIGN_FLOW
295
+ # keyspace: branch | switch | forEach | for | while | doWhile | try | throw | jump
296
+ doWhile: remodeling guidance shown to the spec author
297
+ ```
298
+
299
+ Semantics worth knowing:
300
+
301
+ - `family` opts into the built-in stereotype fencing (backend-like ⇒
302
+ frontend stereotypes are violations; frontend-like ⇒ backend runtime
303
+ stereotypes are warned; neutral ⇒ only your explicit lists apply).
304
+ - Language tables **merge over** the built-in ones (Rust/Go/Python/C gaps);
305
+ later packs win on collision, and project packs load after global packs —
306
+ load order is precedence.
307
+ - Flow gating is **warning severity by design**: "possible but not clean"
308
+ is guidance, not prohibition. `lint.allow` (with a reason) covers
309
+ deliberate exceptions; a project can harden a code to `error` via
310
+ `rules.sddRuleSeverity`.
311
+ - Unregistered `profile` / `projectType` names get `UNKNOWN_PROFILE`
312
+ (warning) naming the registered set.
313
+ - `foreignBuiltins` is **deprecated**: contracts speak one language-neutral
314
+ type grammar (see [the architecture standard](standards/architecture.md#6-entities-and-types)), so there is no
315
+ per-language builtin vocabulary left to declare. A pack that still declares
316
+ it loads, ignores the field and gets a `PACK_FIELD_DEPRECATED` notice; the
317
+ field is removed in the next release.
318
+
319
+ ## Pack-provided AI-agent skills
320
+
321
+ A **directory** pack can ship declarative AI-client skills (SKILL.md files)
322
+ alongside its manifest. `wairon skills install` / `wairon generate` install
323
+ them into the supported client targets, and the hosted MCP server publishes
324
+ them through the same `wairon-skill://` resource mirror as the built-ins — so
325
+ a profile can carry platform/domain implementation guidance while wairon stays
326
+ platform-agnostic. These are **AI-client skills, not MCP tools**.
327
+
328
+ ```yaml
329
+ name: appenser # a directory pack: pack.yaml + skills/
330
+ version: 1.2.0 # optional; shown as the skill's provenance version
331
+
332
+ skills:
333
+ - id: domain-implementer
334
+ source: skills/domain-implementer/SKILL.md # relative to the pack directory
335
+ targets: [claude, gemini] # client targets to install into
336
+ ```
337
+
338
+ ### Extending a builtin skill instead of standing beside it
339
+
340
+ A platform delta usually belongs *inside* the relevant builtin, not next to it.
341
+ Declare `extends` instead of `id`:
342
+
343
+ ```yaml
344
+ skills:
345
+ - extends: sdd-implement # sdd-architect | sdd-narrative | sdd-auditor
346
+ source: skills/make-implementer/SKILL.md
347
+ targets: [claude]
348
+ - id: make-control-plane # unchanged: a genuinely new skill
349
+ source: skills/make-control-plane/SKILL.md
350
+ targets: [claude]
351
+ ```
352
+
353
+ The section is appended to the builtin under `## Platform: <pack>`, in pack load
354
+ order, both on install and on `resources/read`. So an implementing agent reads
355
+ **one coherent instruction** with the platform part clearly attributed — instead
356
+ of noticing a parallel `appenser-make-implementer` and reconciling it, or (worse)
357
+ every wrapper forking the builtin wholesale.
358
+
359
+ - The **builtin stays wairon's**: an upgrade still updates the base text, and the
360
+ composed skill keeps the builtin's own frontmatter, so its identity does not
361
+ change. Only the appended sections come from packs.
362
+ - An extending skill publishes **no separate resource** — it *is* part of the
363
+ builtin now. Untouched builtins stay byte-identical to their templates.
364
+ - `extends` pointing at a skill that does not exist is an
365
+ `EXTENSION_LOAD_ERROR`, not a silently dropped section: an older wairon must
366
+ never quietly fail to apply a newer pack's doctrine.
367
+ - Exactly one of `id` / `extends` per entry.
368
+
369
+ - **Namespaced install.** Every *new* pack skill installs as
370
+ `<pack-id>-<skill-id>` (e.g. `appenser-domain-implementer`) — so skills from
371
+ different packs never collide, and provenance is legible in the name. The
372
+ built-in `sdd-*` skill names are reserved; a pack cannot shadow them, only
373
+ extend them.
374
+ - **Provenance + version** come from the pack (`name` + `version`), visible in
375
+ `wairon skills list` and each MCP resource descriptor.
376
+ - **Reproducibility** is automatic — pack skills are vendored under
377
+ `.wai/packs/` and pinned in `project.yaml`, so they travel with the repo.
378
+ - **No change when unused** — projects with no pack skills install exactly the
379
+ built-ins, as before.
380
+
381
+ ## Teaching the connecting agent (`instructions`)
382
+
383
+ An agent that connects to a wairon MCP server is **taught the SDD model on the
384
+ handshake**: wairon returns MCP `instructions` on `initialize` — the protocol's
385
+ field for "how to use this server", which clients inject into the agent's system
386
+ prompt. It states what a spec tree is, the L0→L5 shape, the authoring order, that
387
+ the `sdd_*` schemas are self-describing, the bound project's governing profile,
388
+ the loaded packs, and — pointing rather than repeating — that
389
+ `wairon-skill://sdd-architect` must be read **before** authoring.
390
+
391
+ That default is **wairon's**, deliberately: wairon owns the model, so wairon
392
+ teaches it, and it changes with wairon versions instead of drifting across every
393
+ wrapper that would otherwise reimplement it. A pack does not restate it — a pack
394
+ appends its **platform delta**:
395
+
396
+ ```yaml
397
+ # pack.yaml
398
+ instructions: >-
399
+ Specs in this project stay target-agnostic. Make.com mechanics (method
400
+ selector, JSON argument envelope, Router branches, blueprint patches) never
401
+ enter a spec — read wairon-skill://appenser-architect first.
402
+ ```
403
+
404
+ Blocks are appended after wairon's own text under an attributed heading
405
+ (`## From pack "appenser"`), in **pack load order** — so a reader can always tell
406
+ platform doctrine from wairon doctrine. For finer control, spell blocks out and
407
+ scope them to the governing profile the way assertions scope theirs:
408
+
409
+ ```yaml
410
+ instructions:
411
+ - text: Applies to every project this pack governs.
412
+ - text: Router branches never appear in a spec — model them as narrative branch steps.
413
+ profile: [make-automation] # only when this profile governs the project
414
+ ```
415
+
416
+ - A block with no `profile` always applies; a scoped block applies only when the
417
+ project's `projectType` matches one of its entries (a block scoped to a profile
418
+ that cannot be resolved is withheld, so platform doctrine never leaks into an
419
+ unrelated project).
420
+ - Because the text is composed **per server construction**, a hosted instance
421
+ reports each request's own bound project — its profile, its packs.
422
+ - Pack skills stay the place for depth. `instructions` is the map that makes an
423
+ agent go and read them; keeping it short is the point.
424
+
425
+ ## Reusable, versioned pattern references
426
+
427
+ Packs can declare named, versioned architecture patterns; component specs
428
+ reference them, and wairon resolves + surfaces the reference. This publishes a
429
+ reusable convention across projects without copy-pasting a spec shape, and
430
+ makes adoption/versioning explicit. Core only validates *identity* — the
431
+ pattern's actual constraints are enforced by the **pack's own rules**.
432
+
433
+ ```yaml
434
+ patterns:
435
+ - id: org/domain-pattern
436
+ version: 1.0.0
437
+ description: The organization's canonical domain-module shape.
438
+ ```
439
+
440
+ A component opts in via `patterns` (matching pack + version):
441
+
442
+ ```yaml
443
+ # a component spec
444
+ patterns:
445
+ - id: org/domain-pattern
446
+ version: 1.0.0 # optional; omit to accept any resolved version
447
+ ```
448
+
449
+ - A reference to a pattern no loaded pack declares is `UNKNOWN_PATTERN_REF`; a
450
+ pinned version with no match is `PATTERN_VERSION_MISMATCH` (both warnings,
451
+ tunable via `rules.sddRuleSeverity`).
452
+ - Loaded patterns are listed by `wairon patterns list` (id, version, source
453
+ pack) and exposed to programmatic pack rules via `ctx.ext.patterns`, giving
454
+ them a stable, typed target to enforce against.
455
+
456
+ ## Component variants (a dynamic layer on top of packs)
457
+
458
+ A **variant** is a named, base-anchored specialization of a core stereotype — a
459
+ "kind of `Orchestrator`/`Adapter`/`Portal`/…" (e.g. a `publisher`) — carrying
460
+ implementation guidance so the implementer treats every component of the same
461
+ variant alike, reusing one shared approach instead of reinventing it per instance.
462
+ The base stereotype stays authoritative for all of wairon's generic semantics and
463
+ dependency rules; the variant adds domain vocabulary + a stable rule target + the
464
+ guidance. It adds no dependency rule of its own: what an Orchestrator may depend on
465
+ is its `dependencyClass` field, not its variant.
466
+
467
+ **Built-in variants.** wairon ships five, described in the
468
+ [architecture standard](standards/architecture.md) §8:
469
+
470
+ | Variant | Base | Shape |
471
+ |---|---|---|
472
+ | `arbiter` | Orchestrator | subject + supplied facts → deterministic verdict + reasons |
473
+ | `projector` | Orchestrator | source model → self-contained derived view |
474
+ | `composer` | Orchestrator | templates + values → authored text or files |
475
+ | `codec` | Orchestrator | format ↔ format, both directions in one component |
476
+ | `gateway` | Portal | a Portal that authenticates, authorizes, validates or rate-limits before it dispatches |
477
+
478
+ **Three layers.** Variants load built-in first, then from the global directory
479
+ (`WAIRON_VARIANTS_DIR`, else `~/.wairon/variants`), then from the project's
480
+ `.wai/variants/`; a later layer overrides a variant with the same id, so a team can
481
+ reword a built-in's guidance without forking wairon.
482
+
483
+ Variants deliberately live **outside packs**: define one on demand — no pack edit
484
+ or release — and share it anywhere (a variant is a tiny, portable YAML). A
485
+ machine/org-wide directory makes a good variant reusable across projects, orgs,
486
+ and tenants.
487
+
488
+ ```yaml
489
+ # .wai/variants/publisher.yaml (or WAIRON_VARIANTS_DIR for machine/org-wide)
490
+ id: publisher
491
+ base: Orchestrator # required — the stereotype this variant specializes
492
+ guidance: >
493
+ In-process fan-out emitter. Reuse the shared publisher helper; do not
494
+ reimplement dispatch per instance.
495
+ # target: typescript # optional — only applies for this target language
496
+ # profile: event-driven # optional — only applies under this profile
497
+ ```
498
+ ```yaml
499
+ # on a component
500
+ componentType: Orchestrator
501
+ variant: publisher
502
+ ```
503
+
504
+ - A component's `variant` must resolve to a declared variant (`UNKNOWN_VARIANT`)
505
+ and its stereotype must equal the variant's `base` (`VARIANT_BASE_MISMATCH`,
506
+ error). A variant is always *a kind of a stereotype* — so cross-cutting
507
+ attributes (retriable, cached) can't be variants; those stay method `guarantees`.
508
+ - **One variant per component**: a genuine combination is a *new* combined variant,
509
+ not two stacked (combining is almost always a purity smell).
510
+ - Listed by `wairon variants list`; exposed to programmatic pack rules via
511
+ `ctx.variants` (a stable, typed target); and — the payoff — a component's
512
+ variant guidance and its same-variant siblings are **injected into the generated
513
+ owner/implementer agent context**, so the implementer reuses one shared approach
514
+ across every component of that variant.
515
+
516
+ ## Programmatic packs (JS)
517
+
518
+ A CommonJS module: the same declarative fields, plus `rules`. The pack does
519
+ NOT import wairon — rules are plain objects receiving the rule context:
520
+
521
+ ```js
522
+ module.exports = {
523
+ name: 'my-rules',
524
+ rules: [{
525
+ name: 'my-rule',
526
+ description: 'One paragraph: what it enforces and why.',
527
+ codes: [{ code: 'MY_CODE', defaultSeverity: 'warning', summary: '…' }],
528
+ check(ctx) {
529
+ for (const comp of ctx.components) {
530
+ // ctx: components/interfaces/implementations/types + componentMap,
531
+ // getComponentProfile(id), targetLanguageFor(subsystemId),
532
+ // isComponentDraft(id), ctx.ext.{profiles,languages}, …
533
+ ctx.addIssue('warning', 'MY_CODE', 'message', comp.id, ctx.isComponentDraft(comp.id));
534
+ }
535
+ },
536
+ }],
537
+ };
538
+ ```
539
+
540
+ What `ctx.addIssue` gives you for free:
541
+
542
+ - **Severity overrides** — projects re-tune your code via
543
+ `rules.sddRuleSeverity` (`error | warning | off`).
544
+ - **`lint.allow`** — a spec can suppress your *warning* on itself with a
545
+ reasoned allow; error findings always surface. Your codes are
546
+ automatically part of the known-code set, so allows against them don't
547
+ trip `UNKNOWN_LINT_ALLOW_CODE`, and the staleness audit runs after your
548
+ rules.
549
+ - **Scoping** — granular (`--subsystem`) validation filters your issues by
550
+ the `specId` you pass; always pass one when the finding is about a spec.
551
+ - The `isDraftContext` flag only downgrades codes in the completeness set —
552
+ for pack codes it is currently informational; pass it anyway.
553
+
554
+ Resolution: `./relative` and absolute paths resolve against the project
555
+ root; a directory loads its pack entry file; anything else resolves with
556
+ Node module semantics from the project root. A pack that fails to load or
557
+ is malformed becomes `EXTENSION_LOAD_ERROR` (error severity) — validation
558
+ never silently runs without declared doctrine. Pack rules appear in
559
+ `wairon rules list` tagged with their pack.
560
+
561
+ ## What packs can NOT do — the closed flow algebra
562
+
563
+ Packs can **gate** narrative flow constructs and enforce anything over the
564
+ spec tree, but cannot add new step kinds. Reachability/region analysis
565
+ depends on a closed successor semantics, and step renumbering relocates a
566
+ fixed set of jump fields — an unknown step-reference field would silently
567
+ corrupt narratives on insert/delete. (Same reason ESLint plugins add rules,
568
+ never AST node types.) A construct that genuinely cannot be expressed with
569
+ the existing algebra is essentially never platform-specific — propose it
570
+ for core, where it lands gated-off in profiles that lack it.
571
+
572
+ ## Embedding wairon as a library (advanced)
573
+
574
+ Only needed to build a branded gate binary with doctrine compiled in —
575
+ packs + installer cover everything else. Install the npm package
576
+ (`npm install @wairon/cli`) and require it; the library entry is CommonJS and
577
+ ships no TypeScript declarations:
578
+
579
+ ```js
580
+ const wairon = require('@wairon/cli');
581
+
582
+ wairon.setProjectRoot(projectDir);
583
+ const extensions = wairon.loadExtensions([...packPathsOrModuleIds], projectDir);
584
+ // extensions.errors: string[] — surface these; don't run "open"
585
+ const result = wairon.validateProject({ extensions });
586
+ ```
587
+
588
+ An explicitly passed `extensions` takes precedence over config/global
589
+ auto-loading. The relevant exported API: `validateProject(options)` (the
590
+ owner's gate: one project from its own files), `validateFamily(options)` (the
591
+ family run: every member's own gate verbatim, the composition of each
592
+ project's externals against their live producers, and the family checks),
593
+ `exportDesign` (the `wairon export` document — see [the format](design-export.md)),
594
+ `loadExtensions`, `loadExtensionPacks`, `loadProjectExtensions`,
595
+ `globalPacksDir`, `discoverPacks`, `setProjectRoot`, the `SddRule` /
596
+ `RuleContext` / `LoadedExtensions` / `DeclarativePack` types, `SDD_RULES`,
597
+ `composeRuleSequence`, and the spec loaders/savers. See
598
+ [`examples/wrapper/wrapper.js`](../examples/wrapper/wrapper.js).
599
+
600
+ ## Spec-only wrappers
601
+
602
+ A wrapper whose implementation happens elsewhere (e.g. a cloud automation
603
+ platform) uses wairon purely for **specs + docs in the repository**. That
604
+ works out of the box; the natural configuration:
605
+
606
+ ```yaml
607
+ rules:
608
+ requireOwnedPaths: false # agents own no source paths
609
+ generateComponentImplementers: false
610
+ ```
611
+
612
+ Narratives still carry full value: the detail dial (`full | calls-only |
613
+ intent`) describes platform scenarios as well as code, `sourcePath` is
614
+ simply omitted, and L4 `technologies` still fences vendor specifics (e.g.
615
+ a datastore product) behind their owning Adapter/Store boundaries.
@@ -0,0 +1,65 @@
1
+ # wairon — Requirements
2
+
3
+ > Last updated: 2026-10-05 (for v6.0.0)
4
+
5
+ ---
6
+
7
+ ## Goals
8
+
9
+ 1. **Spec as the source of truth.** A project's architecture lives in a typed
10
+ spec tree under `.wai/specs/`. Agent files and conformance checks are derived
11
+ from it; generated files are outputs, never inputs.
12
+
13
+ 2. **Architecture conformance.** `wairon validate` is a gate that enforces
14
+ reference integrity, contract↔implementation symmetry, component-stereotype
15
+ dependency rules, and dependency-cycle freedom — so two implementers generate
16
+ the same structure and boundaries are not violated.
17
+
18
+ 3. **Equip the AI session, don't orchestrate it.** wairon serves live agent
19
+ briefs, installs SDD skills, and exposes `sdd_*` MCP tools. The host AI tool
20
+ spawns its own subagents and runs the workflow.
21
+
22
+ 4. **Multi-tool support.** Install skills, guides and the MCP registration (and,
23
+ when a project opts in, agent files) for Claude Code, Gemini CLI /
24
+ Antigravity, and other targets from the same spec tree.
25
+
26
+ 5. **Human approval, checkable in CI.** A human approves a design with
27
+ `wairon lock`; the approval is a committed file, and `wairon lock-check`
28
+ tells CI whether what merges is the approved design.
29
+
30
+ 6. **Optional and additive.** If a project has no `.wai/specs/`, wairon is
31
+ inert. When enabled, the workflow is strict.
32
+
33
+ 7. **Observable, file-based state.** Everything lives under `.wai/` as
34
+ human-readable YAML/JSON — no database, works offline, and no daemon for the
35
+ core workflow (`wairon serve` is an opt-in hosting daemon over the same files).
36
+
37
+ ---
38
+
39
+ ## Non-Goals
40
+
41
+ - **Not a session orchestrator.** wairon does not spawn or drive AI sessions,
42
+ run multi-model pipelines, or manage git worktrees. It relies on the host
43
+ tool's own native subagent mechanism. (`wairon serve` *hosts* the `sdd_*` tools
44
+ over HTTP for remote MCP clients, but still does not run or drive the AI
45
+ session.)
46
+ - **Not a hand-maintained agent registry.** Agents are derived from specs, not
47
+ authored in an `agents.json`.
48
+ - **Not a runtime for application code.** wairon manages specs, conformance, and
49
+ agent topology — not execution.
50
+ - **Not a plugin marketplace.** Templates are file-based; there is no registry
51
+ service.
52
+
53
+ ---
54
+
55
+ ## Scope
56
+
57
+ In scope: the SDD spec tree, conformance validation (spec and code↔spec),
58
+ approval and the CI merge gate, members and cross-project references, the
59
+ design export, spec-derived topology and live briefs, domains
60
+ (subsystem-derived + free-standing), SDD skills, extension packs, the MCP
61
+ server, shared context, multi-target generation, and the opt-in hosted server.
62
+
63
+ Still out of scope: deriving specs from existing code, conformance diffs
64
+ between revisions, and org-scale shared standards (see the
65
+ [roadmap](roadmap.md)).