@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.
- package/CONTRIBUTING.md +193 -0
- package/README.md +154 -41
- package/dist/cli/index.js +66474 -29775
- package/dist/cli/index.js.map +1 -1
- package/dist/index.d.ts +43671 -0
- package/dist/index.js +53581 -21797
- package/dist/index.js.map +1 -1
- package/dist/templates/architect.yaml +15 -13
- package/dist/templates/domain-owner.yaml +5 -4
- package/dist/templates/skills/sdd-architect.md +44 -26
- package/dist/templates/skills/sdd-auditor.md +5 -4
- package/dist/templates/skills/sdd-delegate.md +43 -4
- package/dist/templates/skills/sdd-implement.md +83 -13
- package/dist/templates/skills/sdd-narrative.md +7 -4
- package/dist/templates/variants/logic-shapes.yaml +59 -0
- package/dist/templates/variants/portal-shapes.yaml +13 -0
- package/dist/webapp.html +771 -0
- package/docs/architecture.md +83 -0
- package/docs/cli.md +1208 -0
- package/docs/design/approval-baseline.md +257 -0
- package/docs/design/connecting-agent-entrypoint.md +167 -0
- package/docs/design/execution-budgets.md +199 -0
- package/docs/design/pack-scoping.md +581 -0
- package/docs/design/supervisor-doctrine.md +110 -0
- package/docs/design-export.md +313 -0
- package/docs/extending-wairon.md +616 -0
- package/docs/network.md +109 -0
- package/docs/requirements.md +65 -0
- package/docs/roadmap.md +133 -0
- package/docs/standards/INDEX.md +23 -0
- package/docs/standards/architecture.md +1042 -0
- package/docs/standards/language-bindings.md +120 -0
- package/docs/templates.md +61 -0
- package/docs/vision.md +59 -0
- package/docs/wairon-feature-requests-hosted-mcp.md +74 -0
- package/package.json +18 -5
- package/schemas/design-export-2.json +1186 -0
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# Approval baselines — what `lock` approves, and why it stopped writing
|
|
2
|
+
|
|
3
|
+
> Status: **implemented**. Written against wairon 5.1.x. The "problem" section
|
|
4
|
+
> describes the behaviour as it was BEFORE this work, not as it is now.
|
|
5
|
+
|
|
6
|
+
---
|
|
7
|
+
|
|
8
|
+
## The problem
|
|
9
|
+
|
|
10
|
+
`wairon lock` had grown two jobs fused into one. The first is the one it was
|
|
11
|
+
designed for: **a human reviews the design and approves it**. The second was
|
|
12
|
+
bookkeeping — and the bookkeeping is what people actually experienced.
|
|
13
|
+
|
|
14
|
+
Three symptoms, all the same cause:
|
|
15
|
+
|
|
16
|
+
**It flooded the working tree.** Approving ratcheted every spec's `status` from
|
|
17
|
+
`draft` to `complete` on disk. On this project's own tree that is **786 files**
|
|
18
|
+
rewritten for a decision that changed no design. A lock scoped to one subsystem
|
|
19
|
+
still rewrote everything it could reach, and the specs a human had actually
|
|
20
|
+
edited were buried under files whose content had not changed.
|
|
21
|
+
|
|
22
|
+
**It reported a verdict nobody could act on.** `wairon status` printed:
|
|
23
|
+
|
|
24
|
+
```
|
|
25
|
+
Lock: STALE — the specs or the governing doctrine changed since 2026-08-12,
|
|
26
|
+
so this lock no longer holds. Re-run `wairon lock`.
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
…on a tree validating **0 errors / 0 warnings**. The banner named nothing to
|
|
30
|
+
look at, offered no way to approve part of the change, and asked for work that
|
|
31
|
+
produced no new information. It could only ever say "something moved", because
|
|
32
|
+
a `StateId` is a hash: it answers *did anything change*, never *what*.
|
|
33
|
+
|
|
34
|
+
**It fought the maintainer's own workflow.** `.wai/phased_design.md` records the
|
|
35
|
+
blanket draft→complete freeze being reverted by hand **four times** (lines 18,
|
|
36
|
+
27, 34, 41), once annotated *"product gap: lock needs phase awareness"*.
|
|
37
|
+
|
|
38
|
+
And a second gate had grown on top of the first — see
|
|
39
|
+
[the removed `promote` step](https://github.com/SYW-Apps/Waffle-AIron/blob/main/docs/design/hosted-mcp-server.md#4-state-scoped-lock).
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The model
|
|
44
|
+
|
|
45
|
+
One addition retires five concepts: **record what was approved per spec, not one
|
|
46
|
+
hash of the whole tree.**
|
|
47
|
+
|
|
48
|
+
A review needs two things — the current tree and the tree as it stood when
|
|
49
|
+
someone approved it. wairon had the first and only a whole-tree fingerprint of
|
|
50
|
+
the second. With **one content digest per spec file**, the question changes from
|
|
51
|
+
*"did anything move?"* to *"what moved?"*, which is the only form a human can
|
|
52
|
+
review.
|
|
53
|
+
|
|
54
|
+
`src/core/approval.ts` derives it; `.wai/lock.json` carries it. Three properties
|
|
55
|
+
are load-bearing.
|
|
56
|
+
|
|
57
|
+
### It is committed, and it is one file
|
|
58
|
+
|
|
59
|
+
The approval rides in the lock record, which was always committed. That is what
|
|
60
|
+
lets a **teammate, a fresh clone and CI** see the same approval the approver saw
|
|
61
|
+
— an earlier revision of this design kept it in `~/.wairon/baselines/`, where
|
|
62
|
+
nobody else could read it, so every other checkout judged an approved tree as
|
|
63
|
+
unapproved.
|
|
64
|
+
|
|
65
|
+
Digests rather than content is what makes that affordable: for this project's
|
|
66
|
+
786 specs, **~90 KB instead of ~2 MB**. And no consumer ever needed the content
|
|
67
|
+
— `diffAgainstApproval` and `settledSpecPaths` only ever ask whether a spec
|
|
68
|
+
still matches. For the content itself there is already git: the record is
|
|
69
|
+
committed, so `git diff <lock commit> -- .wai/specs` is the real diff, and
|
|
70
|
+
better.
|
|
71
|
+
|
|
72
|
+
Keys are written **sorted**, so re-approving a one-spec change produces a
|
|
73
|
+
**two-line diff** — the timestamp and that spec's digest — in a 96 KB file.
|
|
74
|
+
Measured on this repo. That is a good signal in a PR rather than noise, and it
|
|
75
|
+
is categorically different from the ratchet this replaced, which rewrote
|
|
76
|
+
hundreds of *spec* files for a decision that changed no design. A test asserts
|
|
77
|
+
the spec tree is byte-identical after `lock`.
|
|
78
|
+
|
|
79
|
+
Digests **normalize line endings** before hashing. This is not tidiness: git
|
|
80
|
+
rewrites line endings on checkout (`core.autocrlf`), so an approval taken on
|
|
81
|
+
Windows would otherwise report every spec as drifted on a Linux CI runner. The
|
|
82
|
+
defect was unreachable while the record was machine-local — sharing it is what
|
|
83
|
+
made it live.
|
|
84
|
+
|
|
85
|
+
### It is per project root, and members are pinned by their subjects
|
|
86
|
+
|
|
87
|
+
Every `.wai` owns its own lock record, including each member project. A
|
|
88
|
+
parent's approval never freezes a member's in-flight work, a member cloned on
|
|
89
|
+
its own carries its approval with it, and a parent lock writes nothing below
|
|
90
|
+
itself.
|
|
91
|
+
|
|
92
|
+
What crosses between them is a **composition subject**: the `stateId` in the
|
|
93
|
+
member's own lock record, read and never recomputed. Each direct member's
|
|
94
|
+
subject is an input of the parent's gate identity, and the parent's record
|
|
95
|
+
(format 2) lists it under `members` with the member's state as the parent saw
|
|
96
|
+
it — `approved` (its lock matches its tree), `drifted` (it has a lock, its tree
|
|
97
|
+
changed) or `never` (no lock). Approval is transitive by hashing: a member's
|
|
98
|
+
subject already includes its own members' subjects, so nothing recurses into a
|
|
99
|
+
member's files. A member editing its own specs does not appear in the parent's
|
|
100
|
+
diff or move the parent's identity — the decisions are separate — until the
|
|
101
|
+
member re-locks; then its subject moves and the parent's approval reads stale.
|
|
102
|
+
`wairon status` prints the pin tree (each member's state from its own root,
|
|
103
|
+
and whether the parent's pin `matches`, has `moved`, or is `unpinned`).
|
|
104
|
+
|
|
105
|
+
A parent may require its members' approvals with
|
|
106
|
+
`composition.requireApprovedMembers: true`: its lock then refuses while a
|
|
107
|
+
direct member is drifted or never approved, naming each. By default it proceeds
|
|
108
|
+
and records their states.
|
|
109
|
+
|
|
110
|
+
This replaced a stage-1 `children` map that copied each child's `StateId` into
|
|
111
|
+
the parent's record without ever re-verifying it; `children` is still read for
|
|
112
|
+
one release and never written.
|
|
113
|
+
|
|
114
|
+
### Settledness is derived, not stored
|
|
115
|
+
|
|
116
|
+
This is what replaced the on-disk `status` ratchet, and it needed care, because
|
|
117
|
+
the ratchet was doing real work. The MCP authoring tools **always** write
|
|
118
|
+
`status: 'draft'` (`src/mcp/server.ts:825, 1027, 1125, 1331`); draft specs get
|
|
119
|
+
`COMPLETENESS_RULES` findings downgraded from error to warning; and `lock` was
|
|
120
|
+
the only thing that ever promoted. Delete it naively and the gate goes
|
|
121
|
+
**permanently soft** — which is exactly how an `UNEXPECTED_IMPLEMENTATION_METHOD`
|
|
122
|
+
came to surface days late in real use.
|
|
123
|
+
|
|
124
|
+
The record already knows which specs were approved and which have moved, so a
|
|
125
|
+
spec that is *approved and unchanged* is presented to the rules as `complete` —
|
|
126
|
+
in memory, restored in `finally`, the same mechanism `treatAllAsComplete`
|
|
127
|
+
already used (`src/core/validation.ts`). Nothing is written.
|
|
128
|
+
|
|
129
|
+
Two things get strictly better than the ratchet:
|
|
130
|
+
|
|
131
|
+
- **It is bidirectional.** The on-disk version was one-way by design
|
|
132
|
+
(`allowStatusDemotion` exists to stop a re-add reopening a locked spec), so an
|
|
133
|
+
edited spec stayed marked complete and in-flux work kept being judged at full
|
|
134
|
+
strictness with no way back short of a manual demotion. A spec that drifts
|
|
135
|
+
after approval now returns to draft context by itself.
|
|
136
|
+
- **It cannot drift.** Derived state has no second copy to fall out of sync.
|
|
137
|
+
|
|
138
|
+
---
|
|
139
|
+
|
|
140
|
+
## What `lock` does now
|
|
141
|
+
|
|
142
|
+
1. Capture the gate identity (own specs, design doctrine, declared inputs,
|
|
143
|
+
`composition`, each direct member's subject).
|
|
144
|
+
2. Validate as complete (full strictness, no draft relaxation, mutates nothing).
|
|
145
|
+
Only the **design** half may refuse; code-conformance results are recorded
|
|
146
|
+
beside the claim (`code`, with the analyzer that produced them) and enforced
|
|
147
|
+
by `validate --ci`, not by the lock.
|
|
148
|
+
3. Report what moved since the last approval, the code results, and each direct
|
|
149
|
+
member's state; refuse under `composition.requireApprovedMembers` if a member
|
|
150
|
+
is not approved.
|
|
151
|
+
4. Ask.
|
|
152
|
+
5. Confirm the gate identity is still the captured one (refuse, writing nothing,
|
|
153
|
+
if an input moved while the lock ran).
|
|
154
|
+
6. Record the approval — a digest per own spec, `members`, `code` — on
|
|
155
|
+
`.wai/lock.json` (format 2), together with who approved and how that identity
|
|
156
|
+
was established.
|
|
157
|
+
7. Regenerate this project's derived topology (`generate --family` walks the
|
|
158
|
+
members; a lock never does).
|
|
159
|
+
|
|
160
|
+
A hosted lock runs the same steps. A hosted lock *request* also records the
|
|
161
|
+
gate identity it was made about; if the tree moved after the request, the
|
|
162
|
+
approved execution refuses, writes nothing, and cancels the request with the
|
|
163
|
+
reason.
|
|
164
|
+
|
|
165
|
+
A scoped approval (`--subsystem x`) approves **only what it covers**; everything
|
|
166
|
+
outside keeps the approval it already had. Whole-tree capture under a scoped
|
|
167
|
+
flag would silently mark the rest of the tree reviewed, which is a worse bug
|
|
168
|
+
than the one being removed.
|
|
169
|
+
|
|
170
|
+
---
|
|
171
|
+
|
|
172
|
+
## Who approved
|
|
173
|
+
|
|
174
|
+
`lockedBy` is `{ id, name?, source }`, and the `source` is the point. A `hosted`
|
|
175
|
+
identity was **authenticated** by the instance that issued the caller's
|
|
176
|
+
credential. `git` and `os` are self-declared, read from the machine's own
|
|
177
|
+
config. Recording which keeps the record honest instead of leaving a bare name
|
|
178
|
+
to imply more than it can.
|
|
179
|
+
|
|
180
|
+
Locally the preference is the **git author identity**, because it is the one
|
|
181
|
+
identity the repository already attributes work to — a reviewer can match it
|
|
182
|
+
against the author of the commit that carries the lock, and that matching is the
|
|
183
|
+
whole value. The fallback is `user@hostname`, which is what Terraform records as
|
|
184
|
+
a lock's holder and what git itself synthesizes when no identity is configured; a
|
|
185
|
+
bare OS username is strictly worse, because in CI it is `runner` and identifies
|
|
186
|
+
nobody. Deliberately not a MAC address or other hardware id: modern systems
|
|
187
|
+
randomize MACs, they differ per interface and VPN, and collecting one is device
|
|
188
|
+
fingerprinting for a field that is not authentication anyway.
|
|
189
|
+
|
|
190
|
+
Hosted is the one surface where wairon genuinely authenticated the approver, and
|
|
191
|
+
it was **throwing that away** — `executeApprovedLock` wrote the constant
|
|
192
|
+
`admin:master` while its caller held a resolved principal. It now records the
|
|
193
|
+
subject. On the approval path the **decider** is recorded, not the requester:
|
|
194
|
+
the requester did not have the authority, and the decision is what conferred it.
|
|
195
|
+
The requester is not lost — the `ApprovalRequest` and the audit event carry them.
|
|
196
|
+
|
|
197
|
+
None of this is proof. The trust anchor is the commit that introduces
|
|
198
|
+
`lock.json` — signed commits, a protected branch, a reviewed PR. `lockedBy` is a
|
|
199
|
+
convenience copy that earns its place only where git does not follow: a
|
|
200
|
+
`.waitree` archive, a `remote push` into a hosted instance, a tarball.
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Reversals worth recording
|
|
205
|
+
|
|
206
|
+
**Keeping the approval outside the repo was wrong, and only sharing it showed
|
|
207
|
+
why.** The first revision put it in `~/.wairon/baselines/` on the reasoning that
|
|
208
|
+
"approving must not dirty the repo". That reasoning conflated two very different
|
|
209
|
+
writes: rewriting 786 *spec* files, and one lockfile changing the way lockfiles
|
|
210
|
+
do. The cost was that no teammate, clone or CI run could see the approval at
|
|
211
|
+
all. Moving it into the committed record — as digests, so it fits — keeps the
|
|
212
|
+
property that mattered and drops the one that did not.
|
|
213
|
+
|
|
214
|
+
**Two defects were only reachable once the record was shared.** Hashing raw
|
|
215
|
+
bytes meant a Windows approval reported all 786 specs as drifted on a Linux
|
|
216
|
+
checkout; digests now normalize line endings, and a test rewrites the whole
|
|
217
|
+
fixture tree to CRLF to hold that. And the hosted lock **published before
|
|
218
|
+
writing the record**, so the commit shipping the specs did not contain the
|
|
219
|
+
approval certifying them — harmless when the record was four bookkeeping fields,
|
|
220
|
+
not when it *is* the approval. Neither was observable while the approval sat on
|
|
221
|
+
one machine.
|
|
222
|
+
|
|
223
|
+
**The signature change was caught by a test, not by the compiler.** Adding an
|
|
224
|
+
`approver` parameter to `executeApprovedLock` silently re-bound an existing
|
|
225
|
+
three-argument call to the new slot. `tsconfig.json` excludes `tests/`, so
|
|
226
|
+
nothing typechecks the suite — vitest transpiles without checking. The test
|
|
227
|
+
failed loudly and the fix was obvious, but the compiler should have said so
|
|
228
|
+
first.
|
|
229
|
+
|
|
230
|
+
**`--subsystem` nearly became a silent whole-tree approval.** Removing the
|
|
231
|
+
ratchet left nothing scoping the capture. Two existing lock tests failed and
|
|
232
|
+
named it. Scoped capture was added rather than discovered later.
|
|
233
|
+
|
|
234
|
+
**The first e2e assertion was written before it could pass.** *"Approving writes
|
|
235
|
+
nothing into the project"* is the property the design exists for, so it was
|
|
236
|
+
asserted first; it failed on the status ratchet, was narrowed to what was true
|
|
237
|
+
that day with a comment saying where it stopped, and tightened to byte-identity
|
|
238
|
+
once the ratchet was gone. A test that encodes a temporary truth is worse than
|
|
239
|
+
one that admits its scope.
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
## Deliberately out of scope
|
|
244
|
+
|
|
245
|
+
**A local wairon account.** The CLI has no identity of its own: `remote attach`
|
|
246
|
+
stores a bearer token, but resolving it to a person would need a `whoami`
|
|
247
|
+
endpoint and a network call, and `lock` is deliberately an offline command. The
|
|
248
|
+
git identity is the honest local answer until that changes.
|
|
249
|
+
|
|
250
|
+
**Storing the approved content.** Digests cannot show what changed *inside* a
|
|
251
|
+
spec. Git already can, against the lock's own commit, so keeping a second copy
|
|
252
|
+
of the tree to duplicate it is not worth its weight.
|
|
253
|
+
|
|
254
|
+
**Separation of duties.** `promote` was the hook for "two people must sign off".
|
|
255
|
+
It never worked, and it was removed rather than kept as a placeholder. If it
|
|
256
|
+
returns, it belongs on the approval — where *"approved by X at StateId S"* is a
|
|
257
|
+
reviewable, committed fact — not as a status string nothing reads.
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
# Teaching the connecting agent — the MCP entrypoint
|
|
2
|
+
|
|
3
|
+
> Status: 7.1–7.5 all shipped. Written against wairon 5.1.0.
|
|
4
|
+
> Source request: "Teaching the connecting agent: wairon needs the entrypoint,
|
|
5
|
+
> packs only adjust it" (2026-07-27).
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The governing principle
|
|
10
|
+
|
|
11
|
+
**wairon owns the SDD model, so wairon must teach it.** If an agent connecting to
|
|
12
|
+
a wairon MCP server does not know what SDD is, what the stereotypes mean, or in
|
|
13
|
+
what order to use the `sdd_*` tools, supplying that is wairon's default — not
|
|
14
|
+
each wrapper's. A pack appends its **platform delta** ("and here is what each of
|
|
15
|
+
those means in Make.com") and never restates the model, which would drift on
|
|
16
|
+
every wairon release.
|
|
17
|
+
|
|
18
|
+
Every item below is shaped by that split.
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Shipped
|
|
23
|
+
|
|
24
|
+
### 7.1 — `instructions` on `initialize`
|
|
25
|
+
|
|
26
|
+
`createMcpServer` returns MCP `instructions`, the protocol's field for "how to
|
|
27
|
+
use this server", which clients inject into the agent's system prompt. Composed
|
|
28
|
+
by `instructions_specialist` (`src/core/instructions.ts`), it carries the L0→L5
|
|
29
|
+
tree shape, the authoring order by tool name, the fact that the `sdd_*` schemas
|
|
30
|
+
are self-describing, the bound project's governing `projectType`/profile, the
|
|
31
|
+
loaded packs, and the directive to read `wairon-skill://sdd-architect` **before**
|
|
32
|
+
authoring — with a pointer per published skill resource.
|
|
33
|
+
|
|
34
|
+
Deliberately short and pointer-heavy (~2.9 KB for this repo; a test pins a 6 KB
|
|
35
|
+
ceiling): the depth lives in `templates/skills/*.md`, and this is the map that
|
|
36
|
+
sends an agent to read them.
|
|
37
|
+
|
|
38
|
+
Composed **per `createMcpServer` call**, so the hosted per-request scoped server
|
|
39
|
+
(`server/request.ts:652`) reports its own bound project rather than a snapshot.
|
|
40
|
+
Never throws — a failed pack load or an uninitialized project degrades to
|
|
41
|
+
wairon's own briefing, because a connecting agent must always be taught
|
|
42
|
+
something.
|
|
43
|
+
|
|
44
|
+
Path: `mcp_server` → `mcp_skills_adapter` → `skills_portal` →
|
|
45
|
+
`skills_resource_orchestrator` → `instructions_specialist`. `sdd_mcp` stays a
|
|
46
|
+
transport subsystem.
|
|
47
|
+
|
|
48
|
+
### 7.2 — pack-contributed instruction blocks
|
|
49
|
+
|
|
50
|
+
`DeclarativePackSchema.instructions` accepts a bare string, one block, or a list,
|
|
51
|
+
normalized to `PackInstructionBlock[]`:
|
|
52
|
+
|
|
53
|
+
```yaml
|
|
54
|
+
instructions:
|
|
55
|
+
- text: Applies to every project this pack governs.
|
|
56
|
+
- text: Router branches never appear in a spec — model them as narrative branch steps.
|
|
57
|
+
profile: [make-automation]
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Appended after wairon's text under `## From pack "<name>"`, in **pack load
|
|
61
|
+
order** — the one merged field whose order is semantic, so blocks are never
|
|
62
|
+
deduped or overwritten. `profile` scopes a block to the governing profile exactly
|
|
63
|
+
as declarative assertions scope theirs; a block scoped to a profile that cannot
|
|
64
|
+
be resolved is **withheld**, so platform doctrine never leaks into an unrelated
|
|
65
|
+
project.
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Shipped (continued)
|
|
70
|
+
|
|
71
|
+
### 7.4 — skills as MCP prompts — SHIPPED
|
|
72
|
+
|
|
73
|
+
Resources are pull-only; prompts are what makes a skill *discoverable* to a human
|
|
74
|
+
driving an agent (as a slash command or attachable context).
|
|
75
|
+
|
|
76
|
+
- Register a prompts capability mirroring `listSkillResources()` — the SDK
|
|
77
|
+
exposes `registerPrompt`, so the set cannot drift from the resource mirror.
|
|
78
|
+
- Prompt name = skill id (`sdd-architect`, `appenser-make-implementer`); the
|
|
79
|
+
handler returns the skill markdown as a message.
|
|
80
|
+
- Resources stay for programmatic reads. Hosted inherits this via the shared
|
|
81
|
+
factory, as the resource mirror already does.
|
|
82
|
+
- Test over an in-memory transport (`client.listPrompts()` / `getPrompt()`),
|
|
83
|
+
matching `tests/mcp/skill-resources.test.ts`.
|
|
84
|
+
|
|
85
|
+
### 7.5 — variant guidance over MCP — SHIPPED
|
|
86
|
+
|
|
87
|
+
`agent_resolver.buildVariantGuidance` (`src/core/agent_resolver.ts:216`) is the
|
|
88
|
+
right hook for platform implementation guidance — it attaches to the component
|
|
89
|
+
the implementer is holding, with no doctrine duplication. But it only reaches
|
|
90
|
+
**generated agent files**, so hosted/cloud agents never see it.
|
|
91
|
+
|
|
92
|
+
- **Lift the composition into `src/core/variants.ts`** so the agent renderer and
|
|
93
|
+
the MCP path share one implementation instead of a second copy that drifts.
|
|
94
|
+
- `sdd_get_spec` on a variant-tagged component returns the resolved variant
|
|
95
|
+
guidance and its same-variant siblings as a clearly-derived, **read-only**
|
|
96
|
+
field, documented in the tool description as not part of the spec.
|
|
97
|
+
- Naming the active variants in the `initialize` instructions was **not** built —
|
|
98
|
+
see the outcome note below. The request said "and/or", and the `sdd_get_spec`
|
|
99
|
+
half is the substantive one.
|
|
100
|
+
|
|
101
|
+
### 7.3 — pack skills that EXTEND a builtin — SHIPPED
|
|
102
|
+
|
|
103
|
+
Today `PackSkillSchema` is `{id, source, targets}` and pack skills are namespaced
|
|
104
|
+
`<pack>-<id>` so they cannot shadow `sdd-*`. A platform delta can therefore only
|
|
105
|
+
exist as a **parallel** skill (`appenser-make-implementer` beside `sdd-implement`),
|
|
106
|
+
leaving the agent to notice both and reconcile them — and tempting every wrapper
|
|
107
|
+
to fork the builtin wholesale, which is worse for everyone.
|
|
108
|
+
|
|
109
|
+
```yaml
|
|
110
|
+
skills:
|
|
111
|
+
- extends: sdd-implement # builtin skill name
|
|
112
|
+
source: skills/make-implementer/SKILL.md
|
|
113
|
+
- id: make-control-plane # unchanged: a genuinely new skill
|
|
114
|
+
source: skills/make-control-plane/SKILL.md
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
- `PackSkillSchema` becomes a union; `extends` must name a builtin
|
|
118
|
+
(`SKILL_NAMES`), and an unknown target **fails the pack load loudly** — the
|
|
119
|
+
`assertions` precedent: an old wairon must never silently not-apply a newer
|
|
120
|
+
pack's doctrine.
|
|
121
|
+
- The pack section is appended to the builtin under `## Platform: <pack>`
|
|
122
|
+
(multiple packs in load order), on install **and** on `resources/read`. The
|
|
123
|
+
builtin stays wairon's, so an upgrade still updates it.
|
|
124
|
+
- An extending skill contributes **no separate resource** — it composes into the
|
|
125
|
+
builtin's descriptor.
|
|
126
|
+
|
|
127
|
+
**The trap:** `checkSkillFreshness` (`src/core/skills.ts:166`) byte-compares the
|
|
128
|
+
installed file against the raw builtin template. Left alone, every extended skill
|
|
129
|
+
reports permanently **stale** in `wairon doctor`. It must compare against the
|
|
130
|
+
*composed* expectation.
|
|
131
|
+
|
|
132
|
+
Also touched: `exportSddSkills` (write composed), `readSkillResource` /
|
|
133
|
+
`readResource` (return composed), `listSkillResources` (descriptors), and the
|
|
134
|
+
`pack_skill` type spec.
|
|
135
|
+
|
|
136
|
+
---
|
|
137
|
+
|
|
138
|
+
## Sequencing (as built)
|
|
139
|
+
|
|
140
|
+
7.4 → 7.5 → 7.3, in ascending order of blast radius, after the pack-scoping work
|
|
141
|
+
(`docs/design/pack-scoping.md`) had settled the selection model — 7.3's
|
|
142
|
+
composition runs over whatever pack set resolution yields.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Outcome notes
|
|
147
|
+
|
|
148
|
+
**7.5 — one half deliberately not built.** The request said "include the resolved
|
|
149
|
+
variant guidance in `sdd_get_spec` … **and/or** name the active variants in the
|
|
150
|
+
`initialize` instructions". The first is done. The second is not: reading the
|
|
151
|
+
variant registry from the instructions composer would be an `sdd_skills` →
|
|
152
|
+
`sdd_core` hop onto an internal Adapter, which the boundary rules refuse, and
|
|
153
|
+
routing it through the core portal would mean a barrel import cycle for what is
|
|
154
|
+
only a pointer. The substantive requirement — guidance reaching a hosted agent —
|
|
155
|
+
is satisfied by the tool that hands an implementer its component.
|
|
156
|
+
|
|
157
|
+
**7.3 — the trap was real.** `checkSkillFreshness` byte-compared installed skills
|
|
158
|
+
against the raw builtin template. Left alone, every extended skill would have
|
|
159
|
+
reported permanently *stale* in `wairon doctor`, telling users to run `generate`
|
|
160
|
+
on a loop. It now compares against what `exportSddSkills` would write.
|
|
161
|
+
|
|
162
|
+
**A latent bug found while testing 7.5.** `requireSpecs()` in `mcp/server.ts`
|
|
163
|
+
lazily required a relative path, which does not resolve under the test runner — so
|
|
164
|
+
every `sdd_*` tool built on it failed with a module error instead of running. No
|
|
165
|
+
test had ever driven one through an MCP client, so nothing caught it. The
|
|
166
|
+
remaining lazy requires in that file (loader / validation / provision) are the
|
|
167
|
+
same latent landmine for any future test that exercises those tools.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
# Execution budgets — the resource axis of the topology
|
|
2
|
+
|
|
3
|
+
> Status: **implemented**. Written against wairon 5.1.x. The measurements below
|
|
4
|
+
> come from three archived Claude Code sessions of this project's own
|
|
5
|
+
> development, read out of the session transcripts rather than estimated.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## The problem
|
|
10
|
+
|
|
11
|
+
The derived topology answered one question about each agent — *what does it own*
|
|
12
|
+
— and nothing about a second one that turns out to cost real money: *what does
|
|
13
|
+
its work cost to do*.
|
|
14
|
+
|
|
15
|
+
In a delegating workflow that gap has a specific, measurable shape. A Claude
|
|
16
|
+
Code subagent's `model` field defaults to `inherit`. An agent file that omits it
|
|
17
|
+
silently adopts the parent session's model. With a frontier-tier parent and no
|
|
18
|
+
agent files on disk at all, **1,860 of 2,176 subagent turns ran on the most
|
|
19
|
+
expensive tier** across the three sessions measured — not by anyone's decision,
|
|
20
|
+
but by a default nobody had reason to look at.
|
|
21
|
+
|
|
22
|
+
Two things are worth stating because they are commonly assumed and were both
|
|
23
|
+
wrong here:
|
|
24
|
+
|
|
25
|
+
- **Per-spawn startup duplication is not the problem.** Subagents do reload
|
|
26
|
+
CLAUDE.md and tool schemas from scratch — measured at 13.5k–32.5k tokens
|
|
27
|
+
against a main session's 44–46k. But the whole CLAUDE.md chain came to ~2,400
|
|
28
|
+
tokens: 8% of a subagent's startup and **0.2% of subagent spend**. Eliminating
|
|
29
|
+
it entirely would save ~2%.
|
|
30
|
+
- **Delegation was still correct.** Running the same work inline in a session
|
|
31
|
+
already averaging 271k context would have added a large context-carry
|
|
32
|
+
surcharge on every turn, and would have forced repeated compaction. The
|
|
33
|
+
problem was never *whether* to delegate; it was that every delegation was
|
|
34
|
+
constituted by default.
|
|
35
|
+
|
|
36
|
+
A default that expensive cannot be fixed by instruction text. A prompt asking an
|
|
37
|
+
orchestrator to choose models deliberately competes for attention with the task,
|
|
38
|
+
and loses — the sessions measured had exactly such a policy, stated by the
|
|
39
|
+
maintainer, and it lost 85% of the time. It has to be configuration.
|
|
40
|
+
|
|
41
|
+
---
|
|
42
|
+
|
|
43
|
+
## The model
|
|
44
|
+
|
|
45
|
+
Four layers, tool-agnostic until the last. The split is what lets one derivation
|
|
46
|
+
serve a local Claude Code session, a hosted MCP client, and a tool that cannot
|
|
47
|
+
select models at all.
|
|
48
|
+
|
|
49
|
+
| Layer | File | Holds |
|
|
50
|
+
|-------|------|-------|
|
|
51
|
+
| **Execution profile** | `models/execution.ts`, `core/execution_profile.ts` | What the work *is like*: `breadth`, `writes`, `reasoningDepth`, `delegates`. Says nothing about models. |
|
|
52
|
+
| **Budget policy** | `core/budget_policy.ts` | What that *earns*: capability tier, effort, turn ceiling, tool class, delegation rights, MCP access. Named in tiers, never vendor models. |
|
|
53
|
+
| **Target emitter** | `exporters/claude.ts` | The per-tool *encoding*. Deliberately not generic. |
|
|
54
|
+
| **Brief** | `core/agent_resolver.ts` | The same budget, carried advisory to whoever spawns from it. |
|
|
55
|
+
|
|
56
|
+
### Derivation needs no spec authoring
|
|
57
|
+
|
|
58
|
+
Every input already exists on a resolved `AgentRecord`:
|
|
59
|
+
|
|
60
|
+
- `reasoningDepth` reads the **component stereotype**, which the vocabulary
|
|
61
|
+
already encodes. A Store, Index, Registry or Adapter is plumbing that its L3
|
|
62
|
+
contract and L5 narrative fully describe. An Orchestrator, Supervisor or
|
|
63
|
+
Specialist carries the decisions. Inventing a second classification alongside
|
|
64
|
+
the stereotype would have meant two things to keep in agreement.
|
|
65
|
+
- `breadth` reads owned-path spread and whether `readPaths` sweeps the tree.
|
|
66
|
+
- `writes` reads the **role**, not the path count — see the reversals below.
|
|
67
|
+
- `delegates` reads the template.
|
|
68
|
+
|
|
69
|
+
### Tiers, not model names
|
|
70
|
+
|
|
71
|
+
`ExecutionBudget.modelTier` is `small | standard | large | frontier`. Mapping a
|
|
72
|
+
tier onto a real model is the *consumer's* job, because only the consumer knows
|
|
73
|
+
what its host tool understands. The Claude Code exporter maps to model
|
|
74
|
+
**aliases** (`haiku`, `sonnet`, `opus`, `fable`) rather than pinned ids, so
|
|
75
|
+
generated files keep working across model releases.
|
|
76
|
+
|
|
77
|
+
### The dial
|
|
78
|
+
|
|
79
|
+
`execution.tier` defaults to `off`, and each step is a strict superset of the
|
|
80
|
+
constraints below it — raising it can only tighten a budget, never loosen one.
|
|
81
|
+
That property is what makes the dial safe to turn without auditing every agent.
|
|
82
|
+
|
|
83
|
+
`free` is defined as having *no* quality tradeoff, which is why it expresses no
|
|
84
|
+
model choice at all: picking a model is a quality decision. An absent
|
|
85
|
+
`modelTier` is not the same as a default one, and the exporter omits the field
|
|
86
|
+
rather than substituting.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## Enforce versus advise
|
|
91
|
+
|
|
92
|
+
Both delivery paths carry the budget, and they are not equivalent:
|
|
93
|
+
|
|
94
|
+
- A **generated agent file** can *enforce*. Front-matter binds: `model`,
|
|
95
|
+
`effort`, `maxTurns`, `tools`, `mcpServers` are read by the host before the
|
|
96
|
+
agent runs.
|
|
97
|
+
- A **live brief** can only *advise*. The caller spawning from the brief is what
|
|
98
|
+
applies it.
|
|
99
|
+
|
|
100
|
+
That asymmetry is deliberate rather than a gap — a brief is consumed by tools
|
|
101
|
+
wairon does not control. It also matters practically: `materializeAgentFiles`
|
|
102
|
+
defaults to **off**, so the brief is the path most projects actually use, and a
|
|
103
|
+
budget that only reached generated files would have reached almost nobody.
|
|
104
|
+
|
|
105
|
+
The same line explains the hosted story. Budgets are declarative data and travel
|
|
106
|
+
fine over the hosted MCP surface as an advisory block on the brief. Front-matter,
|
|
107
|
+
hooks and settings are filesystem artifacts and only the local CLI can write
|
|
108
|
+
them. This is the same split hosted pack management already draws — declarative
|
|
109
|
+
over the API, code packs via filesystem — rather than a second rule.
|
|
110
|
+
|
|
111
|
+
---
|
|
112
|
+
|
|
113
|
+
## Two things derivation deliberately never produces
|
|
114
|
+
|
|
115
|
+
### `frontier` is not an owner tier
|
|
116
|
+
|
|
117
|
+
Derivation caps deep reasoning at `large`, at every dial setting including
|
|
118
|
+
`aggressive`. `frontier` is reachable only by an explicit per-agent override.
|
|
119
|
+
|
|
120
|
+
The reasoning is that a frontier-capable model is a **sparring partner** for a
|
|
121
|
+
question the specs do not settle, not a way to operate a component day to day.
|
|
122
|
+
Given a tree that is already specced and decided, an owner that genuinely needs
|
|
123
|
+
frontier capability to function is usually a component doing too much — the same
|
|
124
|
+
smell `GOD_COMPONENT` reports, arrived at from a different direction. So it stays
|
|
125
|
+
a decision only the user can make, and `wairon execution show` surfaces every
|
|
126
|
+
agent pinned there.
|
|
127
|
+
|
|
128
|
+
### `orchestrate` is not derived either
|
|
129
|
+
|
|
130
|
+
The `orchestrate` tool class — the thin grant that withholds bulk-content tools
|
|
131
|
+
from a pure router — exists because a manager that can read files will read
|
|
132
|
+
them, and then it accumulates context exactly like a main session, which is the
|
|
133
|
+
whole thing delegation was supposed to avoid.
|
|
134
|
+
|
|
135
|
+
But **every agent in a wairon topology owns and authors something**. Domain
|
|
136
|
+
owners write the specs and source they own; even a chained-subproject owner
|
|
137
|
+
writes its mount spec. Stripping their write tools would not make them cheaper,
|
|
138
|
+
it would make them broken. The genuine pure router in this workflow is the
|
|
139
|
+
human's own main session, which is not a wairon agent at all.
|
|
140
|
+
|
|
141
|
+
So `orchestrate` stays in the vocabulary as an override-selectable class for a
|
|
142
|
+
manager someone defines by hand, and derivation never assigns it.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Reversals worth recording
|
|
147
|
+
|
|
148
|
+
Both were found by generating into a real project rather than by reading the
|
|
149
|
+
code, and both are pinned by regression tests.
|
|
150
|
+
|
|
151
|
+
**`writes` was inferred from owned-path count.** An implementer whose source
|
|
152
|
+
path could not be inferred came out with zero owned paths, therefore
|
|
153
|
+
`writes: false`, therefore `tools: Read, Grep, Glob` — an implementer that
|
|
154
|
+
cannot implement, produced silently. `writes` is now a property of the role. A
|
|
155
|
+
missing path is a topology gap to fix, not a reason to demote an agent.
|
|
156
|
+
|
|
157
|
+
**Budget front-matter leaked into other targets.** `cursor`, `copilot` and
|
|
158
|
+
`codex` all reuse `ClaudeExporter` for the agent-file shape. Enabling budgets
|
|
159
|
+
therefore wrote `model`, `maxTurns` and `mcpServers` into files for tools that do
|
|
160
|
+
not implement Claude Code's subagent contract. Those keys would be ignored, which
|
|
161
|
+
is worse than absent: a budget sitting in the file reads as enforced when nothing
|
|
162
|
+
enforces it. The shape is still shared; the encoding is now per-tool, opt-in at
|
|
163
|
+
the exporter registry once a target's own fields are verified.
|
|
164
|
+
|
|
165
|
+
---
|
|
166
|
+
|
|
167
|
+
## Deliberately out of scope
|
|
168
|
+
|
|
169
|
+
**Session-hygiene hooks and a context statusline.** Both would cut real tokens —
|
|
170
|
+
rewriting noisy commands so their output does not enter context verbatim, and
|
|
171
|
+
showing live context size instead of periodically re-measuring it. Neither
|
|
172
|
+
belongs here. They are identical for every project regardless of its spec tree,
|
|
173
|
+
so wairon would be deriving them from nothing, and wiring them means writing into
|
|
174
|
+
a hand-maintained `settings.json` with no marker-based reconcile available,
|
|
175
|
+
unlike every other file wairon generates. Generated skills are not a precedent:
|
|
176
|
+
those *are* wairon's domain.
|
|
177
|
+
|
|
178
|
+
**A conformance rule for frontier overrides.** `validate` gates the spec tree;
|
|
179
|
+
an override lives in project config, and `lint.allow` attaches to specs. The
|
|
180
|
+
finding actually wanted — "this component is doing too much" — is already
|
|
181
|
+
`GOD_COMPONENT`. The override warning lives in `wairon execution show`, where the
|
|
182
|
+
config it reports on lives.
|
|
183
|
+
|
|
184
|
+
---
|
|
185
|
+
|
|
186
|
+
## Open
|
|
187
|
+
|
|
188
|
+
- **Precedence between an agent file's `model:` and the
|
|
189
|
+
`CLAUDE_CODE_SUBAGENT_MODEL` environment variable is unverified.** The env var
|
|
190
|
+
short-circuits Claude Code's model-resolution chain when set to anything but
|
|
191
|
+
`inherit`; whether explicit front-matter still wins decides whether a global
|
|
192
|
+
floor and per-role pinning compose or conflict.
|
|
193
|
+
- **Whether `effort`, `maxTurns` and `mcpServers` in agent front-matter are
|
|
194
|
+
honoured** by a given Claude Code build. Documented, not yet measured.
|
|
195
|
+
- **Topology scale.** A large project generates 30–50 subsystem agents (and would
|
|
196
|
+
generate 1000+ with component implementers on). Because briefs resolve on
|
|
197
|
+
demand by id, nothing needs a materialized catalog — but agent *discovery*
|
|
198
|
+
still enumerates everything, which argues for scoping discovery to a domain.
|
|
199
|
+
Separate concern from budgets.
|