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>
This commit is contained in:
CroneKorkN 2026-05-10 20:24:03 +02:00
parent d49259ff07
commit 3ed0264be6
Signed by: cronekorkn
SSH key fingerprint: SHA256:v0410ZKfuO1QHdgKBsdQNF64xmTxOF8osF1LIqwTcVw

View file

@ -0,0 +1,253 @@
# 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.