Milestones 4 and 5: project layer, review gate and serial merge, all measured

This commit is contained in:
Sam
2026-11-10 12:45:23 +11:00
parent 50fde87450
commit d03636ead3
14 changed files with 1115 additions and 5 deletions

View File

@@ -390,3 +390,87 @@ status, and our screens should show that second run rather than hide it.
under the board identity by mistake". So `authorType: user` is not proof that the person
typed it. Screen 4 currently prints `user` as "you"; that label is wrong often enough to
matter, and it needs a better rule than the author type alone.
---
# Milestones 4 and 5, measured, 2026-11-10
## The project layer
`bin/office <folder>` writes exactly one file, `<folder>/.harness/harness.toml`:
```toml
session = "paperclip-harness"
watching = "AUD-163"
[paperclip]
company = "d4921013-e5da-4d88-8016-57d5746e2d03"
project = "17adbb47-f966-4a82-a8ef-e48c941443ea"
[machine]
host = "nixos-desktop"
folder = "/tmp/pc-sandbox-2026-11-10"
```
That is the whole of what we store: which folder, on which machine, in which session,
showing which task. Everything else is read from Paperclip.
Measured, in this order, on one project:
| Step | Result |
|---|---|
| `bin/office` | wrote the file, built five panes on workspaces 1–5 |
| run again | five panes adopted, none created |
| `bin/board --replace` | five closed, five created, then placed 1–5 in order |
| `bin/project-reset --dry-run` | listed the panes, three worktrees and three branches |
| `bin/project-reset --yes` | removed them; `git worktree list` left only the main checkout |
| `bin/office` afterwards | five panes built again from nothing |
Two failures found by doing it:
- **A rebuild scatters the workspace order.** Closing panes renumbers the rest, so the
pages landed as shell 2, tree 3, reply 4, log 5, new 1. The fix is a final pass that
moves each pane once to the workspace it belongs on (`tuios move-window`) and names it
again. Without that pass the rail order is meaningless.
- **Adopting a project must not invent a session.** The first version defaulted the session
to the folder name, wrote that into the file, then failed because no such session
existed — and the good session name was already overwritten. Sessions are made by a
person, so `bin/office` now reads the recorded session first, checks it exists, and
refuses with the exact `tuios new` command when it does not.
## The merge gate
`bin/merge` takes a lock (`flock` on `/tmp/paperclip-harness-merge.lock`), rebases the
task's branch onto the base, runs the task's own verify command, runs `ocr` over the
rebased range, and only then merges — then runs the verify command again on the base
branch and rolls the merge back if it fails.
Every branch of that path was exercised, not reasoned about:
| Case | What happened |
|---|---|
| clean task (`AUD-163`, docs only) | merged at `2899981`; `ocr` reported `skipped`, 0 files, and said so instead of pretending to have reviewed |
| verify command fails | exit 4, task left alone, comment written |
| review finds a critical | `AUD-165` added `cleanup.sh` with `rm -rf "$HOME/tmp"`; exit 5, finding quoted, `cleanup.sh` never reached `main` |
| review cannot run | exit 5, refused |
| another merge holds the lock | exit 6, refused immediately |
`ocr`'s JSON has `status`, `summary.files_reviewed` and `comments[]` with `severity`
(`critical`, `high`, …), `path`, `start_line` and `content`. Blocking severities are
`critical` and `high`; `skipped` with zero files is "nothing to review", recorded
plainly, while any other non-`complete` status refuses.
## Two API behaviours that shape the code
**A comment on an assigned task wakes its agent.** The merge tool's first refusal comment
went onto a task still assigned to `Builder`, and `Builder` immediately woke and took the
task back to `in_progress` — paying for a run to read a message meant for a person. So a
tool's verdict is written only after the task is handed back:
`PATCH {"assigneeAgentId": null, "assigneeUserId": "<the board user>"}`. A person's reply
from screen 4 is the opposite case: there the wake is the point.
**The issue list is a summary.** `GET /api/companies/{id}/issues` carries no
`currentExecutionWorkspace` and no blockers; the single-task read has both. Any code that
needs a worktree path, a branch or a blocker must do a second `GET /api/issues/{id}`.
That is the third time this list-versus-record difference has bitten, after blockers and
after the earlier probe.