bundlewrap/docs/superpowers/specs/2026-05-10-ckn-bw-agents-md-refactor-round-1-design.md
CroneKorkN 3ed0264be6
docs/specs: round-1 agents-md refactor design (gaps 1-6)
Captures the brainstorm + per-commit wording for the first six
gaps from the left4me-integration handoff, plus a side-quest
read-only command cheat sheet for docs/agents/commands.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:24:03 +02:00

253 lines
11 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Round 1 — agent-doc refactor (gaps 16 + cmd cheat sheet)
## Why
A previous session integrated `bundles/left4me/` and brought
`ovh.left4me` live. The integration produced a handoff (at
`~/.claude/plans/2026-05-10-ckn-bw-docs-improvements-handoff.md`)
listing 12 documentation gaps surfaced by the work. This spec covers
the first six (the cross-cutting ones) plus a useful side-quest:
adding a read-only command cheat sheet to `docs/agents/commands.md`.
Gaps 712 (item-specific, bundle READMEs) are deferred to a follow-up
round.
## Scope
In:
- Gap 1 — drop `bw bundles` (doesn't exist), add `bw verify` to the
read-only allowlist.
- Gap 2 — bundle-validation workflow needs a node attached.
- Gap 3 — nodes carry only node-specific metadata (split across
`bundles/AGENTS.md` and `nodes/AGENTS.md`).
- Gap 4 — reactors must read metadata or be defaults.
- Gap 5 — `triggers``triggered: True` invariant + self-healing
pattern.
- Gap 6 — `unless` semantics (folded into Gap 5's second bullet).
- Side-quest: read-only command cheat sheet in `commands.md` (`bw
test` flag matrix + selectors, `bw metadata -k/-b/-f`, `bw items
--blame/-f`, `bw verify -o bundle:`, `bw hash -m/-d`).
Out:
- Gaps 712 (`source` implicit, `git_deploy` chown, `git_deploy` URL
form, letsencrypt/bind/nginx READMEs).
- Any change to bundle behaviour. This is pure docs; if a doc claim
feels wrong, push back to the maintainer rather than editing
`.py`.
## Verification approach
For each gap, find current line numbers in the target doc (handoff
line numbers are May 2026; some have drifted). Verify code-level
claims against the fork source under `.venv/src/bundlewrap/` before
quoting them.
Already verified during brainstorm:
- Gap 1: `bw bundles` is not a subcommand of the installed fork
(`.venv/bin/bw --help` lists only
`apply, debug, diff, groups, hash, ipmi, items, lock, metadata,
nodes, plot, pw, repo, run, stats, test, verify, zen`). `bw verify`
is read-only.
- Gap 2: `bw test` default flag set differs by mode. Whole-repo:
`-HIJKMSp`. Node-targeted: `-IJKMp`. The repo-mode adds `-H`
(repo hooks) and `-S` (subgroup-loops); the node-mode adds `-J`
(node hooks). Reactors only resolve when a node's metadata is
built, which only happens when a node opts into the bundle.
- Gap 4: exact wording at `metagen.py:428`:
`"{reactor_name} on {node_name} did not request any metadata, you
might want to use defaults instead"`.
- Gap 5: exact wording at `deps.py:340`:
`"'{item1}' in bundle '{bundle1}' triggered by '{item2}' in bundle
'{bundle2}', but missing 'triggered' attribute"`.
- Gap 3 precedent: `bundles/left4me/metadata.py:10` is the canonical
random-bytes-in-defaults example. `bundles/postgresql/metadata.py:4`
is the password_for-at-module-scope example. (The handoff cites
postgresql for the random-bytes pattern; that's a misattribution —
postgresql uses `password_for`.)
After every commit: `.venv/bin/bw test` must pass with the same
output as before. Pure-docs edits cannot break it unless a `.py` is
touched accidentally.
## Commits
Six iterative commits, matching repo style.
### Commit 1 — drop `bw bundles`, add `bw verify` (Gap 1)
`AGENTS.md` rule 1 only. The handoff also flagged
`bundles/AGENTS.md:60-64`, but that list no longer references
`bw bundles` (it currently reads `bw test` / `bw items` / `bw hash`).
That section gets rewritten in commit 3, not here.
```diff
- to `bw test`, `bw nodes`, `bw groups`, `bw bundles`,
- `bw items`, `bw metadata`, `bw hash`, `bw debug`. See
+ to `bw test`, `bw nodes`, `bw groups`, `bw items`,
+ `bw metadata`, `bw hash`, `bw verify`, `bw debug`. See
```
### Commit 2 — read-only command cheat sheet
Append to `docs/agents/commands.md`. New H2 section, table format
to match the existing voice.
```markdown
## Read-only commands — useful flag combinations
The fork's [`AGENTS.md`][fork] documents the canonical safety envelope.
These are the flag combinations agents reach for most often in this repo:
| Want to … | Run |
|---|---|
| Sanity-check the whole repo (parse + cross-cutting hooks) | `bw test` (defaults to `-HIJKMSp`) |
| Exercise reactors and item-graph for one node | `bw test <node>` (defaults to `-IJKMp`) |
| Same, but every node that has a given bundle | `bw test bundle:<name>` |
| Print one metadata key for one node | `bw metadata <node> -k <a/b>` (repeat `-k` for more) |
| Show where each metadata value comes from | `bw metadata <node> -b` |
| Resolve Faults (vault values) into the dump | `bw metadata <node> -f`**may print secrets, avoid** |
| List a node's items, with the bundle that defines each | `bw items <node> --blame` |
| Preview a rendered file's content | `bw items <node> file:<path> -f` |
| Verify against the live host, scoped to one bundle | `bw verify <node> -o bundle:<name>` |
| Hash metadata only (faster than full config hash) | `bw hash <node> -m` |
| Inspect the data backing a hash | `bw hash <node> -d` |
`bw test`, `bw verify`, `bw nodes`, `bw metadata` all share a target-
selector grammar: bare node name, group name, `bundle:<name>`,
`!bundle:<name>`, or `"lambda:node.metadata_get('foo/bar', 0) < 3"`.
[fork]: https://github.com/CroneKorkN/bundlewrap/blob/main/AGENTS.md
```
### Commit 3 — bundle validation needs a node attached (Gap 2)
Two file changes.
**`bundles/AGENTS.md` lines 59-64** — replace the Verify list:
```markdown
5. **Verify, in this order:**
- `bw test` — repo-wide parse + cross-cutting hooks. Loads every
bundle, but reactors don't fire for nodes that haven't opted into
the bundle yet — bugs in new reactors stay hidden here.
- **Attach the bundle to a node** (via the node's `bundles` list, or
a group it belongs to). Until you do, the next steps don't actually
exercise the bundle.
- `bw test <node>` — exercises every reactor and item-graph edge for
that node. This is where most new-bundle bugs surface.
- `bw items <node> --blame` — confirm items materialise with the right
paths, authored by the expected bundle.
- `bw metadata <node> -k <a/b>` — spot-check derived metadata.
- `bw hash <node>` — preview vs current host state.
See [`docs/agents/commands.md#bundle-validation-workflow`](../docs/agents/commands.md#bundle-validation-workflow)
for the rationale.
```
**`docs/agents/commands.md`** — new section after the cheat sheet:
```markdown
## Bundle-validation workflow
`bw test` (no args) is a *parsing* gate, not a *behaviour* gate. It
loads every bundle, but a bundle's reactors only resolve when a node's
metadata is actually built — and that happens only for nodes that
opt in. Until then, reactor bugs stay dormant. bw rejects reactors that
don't read any metadata, but the rejection only fires once *some* node
consumes the bundle.
When developing a new bundle:
1. Scaffold + `bw test` — confirms parsing.
2. **Attach the bundle to one node** (or a stub node) by adding it to
`nodes/<n>.py`'s `bundles` list, or to a group the node is in.
3. `bw test <node>` — now reactors fire. This is where bundle bugs
surface.
4. `bw items <node> --blame` and `bw metadata <node> -k <key>` — confirm
items materialise and derived metadata looks right.
5. `bw hash <node>` — preview against the live host.
Step 2 is non-optional. A bundle that "passes `bw test`" with no consumer
is proven only to parse.
```
### Commit 4 — nodes carry only node-specific metadata (Gap 3)
**`bundles/AGENTS.md` Conventions** — new bullet:
```markdown
- **Bundles own application-wide knowledge; nodes carry only the few
per-host knobs the bundle actually needs.** When designing a bundle,
identify the per-node knobs (e.g. domain, uplink interface, a
vault-id suffix) and put everything else in `defaults`, or in a
reactor that derives from those knobs. Per-node random secrets
belong in `defaults` via `repo.vault.random_bytes_as_base64_for(...)`
keyed on the node — not in the node file. See
`bundles/left4me/metadata.py:10` (`secret_key` derived in defaults)
and `bundles/postgresql/metadata.py:4` (vault-derived `password_for`
at module scope).
```
**`nodes/AGENTS.md` Pitfalls** — new bullet:
```markdown
- **Bloated per-node metadata is usually a bundle smell.** If a
bundle's metadata block in the node file has more than 3-5 keys,
the bundle is probably under-using `defaults` / reactors. Push the
contribution into the bundle (see
[`bundles/AGENTS.md`](../bundles/AGENTS.md#conventions)) rather than
growing the node file.
```
### Commit 5 — reactors must read metadata or be defaults (Gap 4)
**`bundles/AGENTS.md` Pitfalls** — new bullet:
```markdown
- **Reactors must read metadata.** If a reactor body returns a static
dict without calling `metadata.get(...)`, bw raises
`ValueError: <reactor> on <node> did not request any metadata, you
might want to use defaults instead` once a node consumes the bundle.
Fix: fold the contribution into `defaults`. The rule applies even
when the reactor writes into another bundle's namespace — a static
contribution to e.g. `nftables/output` belongs in `defaults`, where
bw merges it with other bundles' contributions.
```
### Commit 6 — `triggers` invariant + self-healing + `unless` (Gaps 5+6)
**`bundles/AGENTS.md` Pitfalls** — two new bullets (Gap 6's `unless`
semantics fold into the second; cleaner than three bullets):
```markdown
- **`triggers` ↔ `triggered: True` invariant.** Any item listed in
another's `triggers` list must declare `triggered: True`. bw
enforces this at `bw test` time: *"…triggered by …, but missing
'triggered' attribute"*. Corollary: an action can't be both in an
upstream `triggers` list AND self-healing every apply — pick one.
- **Triggered actions don't recover from partial failure.** When an
upstream item's apply succeeds but its triggered downstream action
fails, subsequent applies can't recover via the trigger chain —
upstream is "already in desired state" and never re-triggers. For
actions that must self-heal (pip installs, chowns, migrations),
drop `triggered: True` and gate the command with `unless:
<fast-check>`. `unless` is a shell command on the target host whose
exit status decides whether the main command runs (exit 0 = skip);
it's checked at fire time, after `triggered:` filtering.
```
## Out of scope
- Gaps 712 — deferred. The maintainer re-engages after this round.
- Bundle behaviour changes. Pure docs.
- `bw apply` / `bw run` — not authorised this session.
## Constraints
- Don't echo decrypted secrets in commit messages or new doc text.
- Don't restore `*.py_` parked nodes.
- After each commit, `.venv/bin/bw test` must pass.
- No push.