docs(AGENTS): write down the wave workflow
CI / Test and lint (host) (push) Has been cancelled
CI / wasm32 check (browser bundle crates) (push) Has been cancelled
CI / Knowledge base consistency (push) Has been cancelled

Most work here is done by waves of agents in worktrees, and every rule in
this section was learned by breaking it: the three orchestrator-only files
and why, that kb.pl additions must be batched because loadProgram replaces a
whole predicate, that tickets sharing a file belong to one agent, that a
worktree does not reliably branch from the orchestrator's HEAD, that direnv
status reports 0 for allowed, and that the code review's characteristic find
here is a test that passes while the thing it names is broken.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EoW5BB87ZZdm96SvBTWawH
This commit is contained in:
Edward Langley
2026-09-19 17:42:46 -07:00
parent cad914faab
commit 74ac4cb6ab
2 changed files with 106 additions and 0 deletions
+1
View File
@@ -23,6 +23,7 @@
{"_type":"issue","id":"improvise-km8","title":"1.2 Fix Cargo.toml metadata","description":"Add missing package fields: description, repository, homepage, documentation, readme, keywords, categories, license. Description: 'Terminal pivot-table modeling in the spirit of Lotus Improv'. Keywords: tui, pivot, spreadsheet, data, improv. Categories: command-line-utilities, visualization.","status":"closed","priority":1,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:06:48Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:20:17Z","close_reason":"Metadata already present: description, license, repository, homepage, readme, keywords, categories all populated.","dependency_count":0,"dependent_count":2,"comment_count":0}
{"_type":"issue","id":"improvise-902","title":"1.1 Audit and remove or update context/SPEC.md","description":"Compare context/SPEC.md against actual code. If spec contradicts code significantly, delete it. Optionally move salvageable conceptual content to docs/design-notes.md with a staleness disclaimer. Do not try to bring spec into sync.","status":"closed","priority":1,"issue_type":"task","assignee":"Edward Langley","owner":"el-github@elangley.org","created_at":"2026-04-09T04:06:46Z","created_by":"Edward Langley","updated_at":"2026-07-30T23:06:31Z","closed_at":"2026-04-09T07:10:05Z","close_reason":"Audited SPEC.md against code. Found minor inaccuracies (storage internals, wizard step count, search mode representation) but no major architectural contradictions. Deleted SPEC.md since it was largely redundant with repo-map.md and design-principles.md. Salvaged product vision and non-goals into docs/design-notes.md with staleness disclaimer.","dependency_count":0,"dependent_count":1,"comment_count":0}
{"_type":"issue","id":"improvise-tik","title":"Phase 1: Repository hygiene","description":"Raise the quality floor of what a stranger sees when they land on the repo. Subtasks cover spec audit, Cargo.toml metadata, publish-readiness, CSV audit, and example files.","status":"closed","priority":1,"issue_type":"feature","owner":"el-github@elangley.org","created_at":"2026-04-09T04:05:45Z","created_by":"Edward Langley","updated_at":"2026-04-09T07:22:34Z","closed_at":"2026-04-09T07:22:34Z","close_reason":"All 5 subtasks complete: SPEC.md audited, Cargo.toml metadata fixed, publish dry-run clean, CSV audit done, example files created.","dependencies":[{"issue_id":"improvise-tik","depends_on_id":"improvise-2fr","type":"blocks","created_at":"2026-04-08T23:37:31Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-902","type":"blocks","created_at":"2026-04-08T23:37:30Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-bv1","type":"blocks","created_at":"2026-04-08T23:37:32Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-km8","type":"blocks","created_at":"2026-04-08T23:37:30Z","created_by":"Edward Langley","metadata":"{}"},{"issue_id":"improvise-tik","depends_on_id":"improvise-n1h","type":"blocks","created_at":"2026-04-08T23:37:32Z","created_by":"Edward Langley","metadata":"{}"}],"dependency_count":5,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-hwpi","title":"The browser shell fails quietly, and never reads a key","description":"Found by the improvise-cr3 code review (2026-09-19) by READING web/app.js. There is no browser and no headless DOM in this environment, so none of this is observed -- it is what the code says will happen, and it is filed rather than fixed because each item is a judgement call about the shell rather than a defect in the Rust.\n\nFOUR THINGS, in descending order of how much they would cost a person at a demo:\n\n1. NO KEYBOARD AT ALL. #grid carries tabindex=\"0\" but the page registers no keydown handler, so Client::on_key_event -- and the whole keys.rs classifier and the lockstep machinery behind it -- is UNREACHED from the browser. Consistent with improvise-bpq2 (ClientMsg has no Key variant server-side) but larger than it: even a LOCAL key, one that needs no round trip, does nothing. improvise-1ey acceptance (3) is \"arrow keys move the cursor locally with no round-trip\", and nothing in the page can do it.\n\n2. A MALFORMED FRAME IS QUIET. client.on_frame(event.data) returns Result\u003c_, JsError\u003e, which wasm-bindgen turns into a THROWN EXCEPTION. It is thrown inside the message listener with no try, so it surfaces as an uncaught error in the devtools console and the following repaint() is skipped. Nothing in the UI says so: the grid simply stops updating. A try/catch writing to el.status would make it visible.\n\n3. NO error LISTENER ON THE SOCKET. A connection that never opens leaves #status blank -- only close writes \"disconnected\". So \"server not running\" and \"still connecting\" look the same.\n\n4. close() RUNS TWICE ON COMMIT, benignly. Enter -\u003e close(true) -\u003e td.replaceChildren(original) removes the focused \u003cinput\u003e -\u003e blur fires -\u003e close(false). editing is already null so it is harmless (one extra repaint), but there is no re-entry guard and only a real browser would show it.\n\nNOTE WHAT MAKES THIS TICKET DIFFERENT: items 2-4 are the kind of thing a test would catch instantly and we have no way to run one. Before fixing them, consider whether the right first move is a headless DOM in CI (jsdom or playwright) -- otherwise the fixes are as unverified as the bugs. improvise-xf1 (verify OpfsFs against a real browser) wants the same harness.","status":"open","priority":2,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-20T00:40:36Z","created_by":"Edward Langley","updated_at":"2026-09-20T00:40:36Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-zv4m","title":"improvise-protocol should own the websocket framing, so both ends stop mirroring it","description":"Raised by improvise-cr3 (2026-09-19) as the thing to push back on, and filed rather than fixed there.\n\nTODAY: improvise-wasm-client/src/socket.rs declares ClientFrame/ServerFrame, which MIRROR improvise-ws-server/src/wire.rs ClientMsg/ServerMsg field for field. The client cannot depend on the server crate -- tokio, tungstenite, Box\u003cdyn Storage\u003e, all native -- so the duplication was the only way to speak the protocol from wasm.\n\nWHAT HOLDS THE LINE MEANWHILE: improvise-ws-server is a DEV-dependency of the client (the same argument the crate already makes for improvise-command), and the_socket_framing_matches_the_server round-trips every variant THROUGH THE SERVERS OWN TYPES in both directions; a_frame_named_the_other_envelopes_way_does_not_parse is its check-the-checker. So a drift is caught -- but only by a test whose job is to notice, rather than by a shared definition that cannot drift.\n\nTHE FIX: move the framing into improvise-protocol, which both ends already depend on and which is the crate whose charter is exactly this -- \"the wire vocabulary and the portable session slice\". Then the mirror, the dev-dependency and both compatibility tests all delete.\n\nNOTE THE THIRD ENVELOPE while you are in here: improvise-wasm-client/src/wire.rs has ClientMessage/ServerMessage framing the WORKER deployment (improvise-tm6). Three envelopes for two deployments is worth resolving in one pass -- decide whether the worker and the socket want the same vocabulary before moving either.","status":"closed","priority":2,"issue_type":"task","owner":"el-github@elangley.org","created_at":"2026-09-20T00:37:29Z","created_by":"Edward Langley","updated_at":"2026-09-20T00:37:50Z","closed_at":"2026-09-20T00:37:50Z","close_reason":"Duplicate of improvise-xks6 ('One home for the client/server frame envelopes'), filed minutes apart from the improvise-cr3 hand-back without spotting the existing ticket. Closing this one; xks6 is the survivor and its description should absorb two facts this one carried:\n\n * WHAT HOLDS THE LINE MEANWHILE: improvise-ws-server is a DEV-dependency of improvise-wasm-client, and the_socket_framing_matches_the_server round-trips every variant THROUGH THE SERVERS OWN TYPES in both directions, with a_frame_named_the_other_envelopes_way_does_not_parse as its check-the-checker. The drift is caught by a test whose job is to notice, rather than prevented by a shared definition.\n * THERE ARE THREE ENVELOPES, NOT TWO: improvise-wasm-client/src/wire.rs frames the WORKER deployment (improvise-tm6) separately again. Decide whether the worker and the socket want the same vocabulary before moving either, or the move produces two homes instead of one.\n\n(See also memory agent-issue-drift-pattern -- search before filing.)","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-8xhy","title":"[bug] A fit over a categorical feature emits an unevaluable formula","description":"Found during improvise-7na (2026-09-19), verified by probe, and pinned as a characterization test rather than fixed.\n\ngp_tree::data::preprocess one-hots a categorical column into a feature named Region=North. That is an ENCODING ARTEFACT, not an item of the document, so the emitted formula Expr::Ref fails Model::find_item_category and EVERY CELL of the fitted measure evaluates to Error(\"ref:Region=North\"). col.missing and the col.year / col.month date sub-features are in exactly the same position.\n\nSo \"fit a tree and get a working formula\" holds ONLY for a fit over numeric measures.\n\nTHE TEST THAT SHOULD HAVE CAUGHT IT WAS LYING: improvise_evaluation_matches_gp_tree_eval cannot see this, because its improvise_eval helper FABRICATES a _Measure item for every feature name, including outlook=rainy. It asserts the emitter and gp_tree::eval agree, which they do -- against a model that could not exist. Worth remembering as a shape: a helper that builds the world the code wants is not a test of whether that world exists.\n\nPinned as a_split_on_a_one_hot_feature_does_not_evaluate -- characterization, flip it when fixed.\n\nTHREE REMEDIES, pick deliberately: restrict fits to numeric features (smallest, and honest about what works); materialise one-hot columns as real _Measure items (makes the formula evaluable but pollutes the document with encoding artefacts); or teach the emitter improvise WHERE vocabulary so a categorical split becomes a real predicate over the category (largest, and the only one that produces a formula a human would have written). The third is probably right and should be costed before the first is shipped as permanent.","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-20T00:35:05Z","created_by":"Edward Langley","updated_at":"2026-09-20T00:35:05Z","dependency_count":0,"dependent_count":0,"comment_count":0}
{"_type":"issue","id":"improvise-shyt","title":"[bug] Records-mode row order is not stable across processes","description":"Found during improvise-4s8 (2026-09-19). A real latent bug in improvise-core, out of that ticket scope, worked around there by sorting.\n\nDataStore::matching_cells returns cells in HashSet iteration order, so RecordRow::group -- and therefore RECORDS-MODE ROW ORDER IN THE LIVE UI -- is not stable across processes. The same document opened twice can list its records in different orders.\n\nWHY IT MATTERS BEYOND TIDINESS:\n * gotcha 18 says IndexMap insertion order is load-bearing for persistence, and DataStore::cells is IndexMap precisely so \"records mode can display rows in the order they were entered\" (its own doc comment). The SECONDARY INDEX walk defeats that.\n * f32 accumulation is order-dependent, so anything summing over matching_cells is reproducible only within one process. improvise-4s8 had to sort before building a feature matrix or a SEEDED FIT would not reproduce across runs -- see the known-answer vectors in gp-tree, which exist because that class of drift is silent.\n * A user watching rows reorder between sessions has no way to tell whether the document changed.\n\nFIX: make matching_cells (or its callers that care) yield in the primary IndexMap order rather than the index HashSet order. Note the index exists for O(1) LOOKUP, not for iteration, so the fix is probably to intersect the index result back against the insertion-ordered primary rather than to change the index.\n\nTEST IT THE WAY THE INSTABILITY ACTUALLY SHOWS: a single-process test cannot distinguish \"sorted\" from \"hashed that way today\". Build the same document twice with different insertion histories that produce the same set, or assert directly against the IndexMap order.","status":"open","priority":2,"issue_type":"bug","owner":"el-github@elangley.org","created_at":"2026-09-20T00:35:04Z","created_by":"Edward Langley","updated_at":"2026-09-20T00:35:04Z","dependency_count":0,"dependent_count":0,"comment_count":0}
+105
View File
@@ -53,6 +53,111 @@ cp -rf source dest # NOT: cp -r source dest
- `apt-get` - use `-y` flag
- `brew` - use `HOMEBREW_NO_AUTO_UPDATE=1` env var
## The Wave Workflow
Most work here is done by **waves**: an orchestrator chunks the open backlog,
fans several agents out into their own git worktrees, merges what comes back,
reviews it, and pushes the wave as one PR. This section is what that costs to
get right. Everything in it was learned by getting it wrong first.
### Who owns what
Three files are **orchestrator-only**, and the rule exists because they are
guaranteed conflicts — every agent has a reason to touch them and they merge
badly:
| File | Why |
|---|---|
| `.beads/` (incl. `issues.jsonl`) | bd owns it. See "Never hand-edit" below. |
| `prolog-sessions/kb.pl` | MCP-maintained, whole-predicate replacement. |
| `context/repo-map.md` | Every agent wants to add a gotcha; every wave would conflict. |
Wave agents **read** all three freely (`bd show`, `bd ready`, grep `kb.pl`) and
**write** none of them. They report proposed repo-map lines and `kb.pl` facts as
*text* in their hand-back, and the orchestrator applies them — batched, at the
end of the wave, after the review. Batching matters for `kb.pl` specifically:
`loadProgram` **replaces a whole predicate**, so every addition means re-sending
every existing clause of `doctrine/2`, `decision/3`, etc. Doing that once per
wave instead of once per fact is the difference between cheap and painful. It
has already silently wiped 22 decisions and 19 doctrines once — count with
`findall/length` before and after, and check the count before saving.
`saveSession` re-adds the MCP's own `host_rpc` shims to `kb.pl` every time.
Strip them after each save.
### Partitioning a wave
Partition by **crate or directory**, not by ticket count, and tell each agent
explicitly which ground is *not* theirs. Two rules that fall out of the ones we
broke:
- **Tickets that share a file belong to one agent.** The four TUI widget
migrations all touch `src/draw.rs`; split across agents they are a guaranteed
conflict, so they go to one agent to do sequentially. Group first, then count.
- **An agent that must stray says so rather than guessing.** Give the boundary
and ask for a report when it has to be crossed.
Two agents each adding a workspace member will still collide in
`Cargo.toml`/`Cargo.lock`. That one is expected; keep both sides.
### Every wave agent's brief must include
1. **Verify your base commit first.** Worktrees do **not** reliably branch from
the orchestrator's HEAD — one was created three merges behind and only
noticed because it was told to look. `git log --oneline -1` against a named
commit, reset onto `main` if behind. Name the expected commit in the brief.
2. **The baseline test count**, so "1090 passing" is checkable rather than
asserted, and a drop is visible.
3. **Do not push, do not merge to `main`.** The orchestrator merges.
4. The commit-message trailers, spelled out.
5. **What to report**: branch and commits, what changed per ticket, *what could
not be verified and why*, and proposed repo-map/`kb.pl` text. Ask explicitly
for the thing the agent learned that the ticket did not predict — that is
consistently the most valuable part of a hand-back, and agents omit it unless
asked.
`bd` works in any worktree, but `.envrc` must be allowed there or you get a
**version mismatch** rather than a clean failure — `direnv allow` in the
worktree, or rely on the `[whitelist] prefix` in `~/.config/direnv/direnv.toml`.
Note `direnv status` reports `Found RC allowed 0`, and `0` means **allowed**
(the enum is 0=Allowed, 1=NotAllowed). It reads like the opposite.
### Between waves
Run the **code-review skill** on the merged range and iterate until its backlog
is empty. This is not ceremony: in this repo the review's characteristic find is
not a wrong line, it is **a test that passes while the thing it names is
broken** — a vacuous fold test, a bit-witness that cannot see the bit, a
one-category fixture, a helper that fabricates the `_Measure` items the code
under test needs, a check-the-checker that injected malformed JSON and so proved
nothing. A green suite is not evidence. Ask the reviewer to *inject the fault*
and watch the check fail.
### Shipping a wave
One PR per wave, built by a **dedicated PR agent in its own worktree**, so waves
pipeline against it: the next wave runs while the PR absorbs review fixes.
Copilot review must be **re-requested after every fix push** (GraphQL
`requestReviews`); it does not re-review on its own. Across three PRs it found
five genuine bugs and zero false positives, so its findings are worth the loop.
Merge only once it passes.
### Never hand-edit `.beads/issues.jsonl`
bd manipulates that file and auto-exports to it. Editing it — or even
`git checkout`-ing it, which fires the post-checkout hook — breaks auto-export
and desynchronises `export-state.json`. Use `bd` commands, let auto-export run,
and commit whatever it leaves.
### Before filing a ticket, search
Agent-filed issues duplicate work that is already open, sometimes already
in progress. Two duplicates were filed in this repo within minutes of the
hand-backs that prompted them. `bd search` the core concept *and* scan
in-progress issues first.
<!-- BEGIN BEADS INTEGRATION v:1 profile:minimal hash:ca08a54f -->
## Beads Issue Tracker