diff --git a/bin/harness b/bin/harness index 21e2ec2..e582618 100755 Binary files a/bin/harness and b/bin/harness differ diff --git a/bin/lib/harness.py b/bin/lib/harness.py index 3dd90c3..c9f7184 100644 --- a/bin/lib/harness.py +++ b/bin/lib/harness.py @@ -126,9 +126,11 @@ def verify_command(issue, worktree): m = VERIFY_RE.search(issue.get("description") or "") if m: return m.group("cmd").strip(), "from the task description" - project_script = os.path.join(worktree, ".harness", "verify.sh") - if os.path.isfile(project_script): - return ".harness/verify.sh", "the project's .harness/verify.sh" + # The project's own check. check.sh is the current name; verify.sh is what + # the earlier project wrote, and is still honoured. + for name in (".harness/check.sh", ".harness/verify.sh"): + if os.path.isfile(os.path.join(worktree, name)): + return name, "the project's " + name return None, "none" diff --git a/bin/office b/bin/office index 5b7d666..fc3347b 100755 --- a/bin/office +++ b/bin/office @@ -49,23 +49,27 @@ def scaffold(folder, name): harness = os.path.join(folder, ".harness") os.makedirs(harness, exist_ok=True) - verify = os.path.join(harness, "verify.sh") - if not os.path.exists(verify): - with open(verify, "w") as f: + check = os.path.join(harness, "check.sh") + if not os.path.exists(check): + with open(check, "w") as f: f.write("""#!/bin/sh -# The command that decides a task in this project is finished. +# This project's own check, run by bin/merge before and after it puts a task's +# work on the base branch. Exit 0 means "the project still passes". # -# Leave it as it is to accept that there is no agreed command: bin/merge will -# say so and let the review decide. Replace the body with the real one when the -# project has tests, for example: +# It does not decide that a task is finished: an agent sets a task's status, and +# the review decides whether the change is good. This is the regression check, +# the same thing a CI pipeline would run. +# +# Leave it as it is while the project has no tests. bin/merge will say there is +# no check and let the review decide. Replace the body when tests exist: # # exec go test ./... # exec make test -echo "no verify command agreed for this project" +echo "this project has no check yet" exit 0 """) - os.chmod(verify, 0o755) - written.append(verify) + os.chmod(check, 0o755) + written.append(check) notes = os.path.join(harness, "NOTES.md") if not os.path.exists(notes): diff --git a/docs/NEXT.md b/docs/NEXT.md index 1191080..7ab8ef1 100644 --- a/docs/NEXT.md +++ b/docs/NEXT.md @@ -104,9 +104,21 @@ step; a switch is only worth adding after one agent has been run both ways and m ## 9. MOUSE-1 -Corrected: Mouse-1 is a hosted API, not software to install. `MOUSE_API_KEY` is present; what is -missing is a client call from our side. The old project's policy stands: triage only, never a -gate, never a permit — it could order `Board>needs`, never decide a merge. +It is a hosted API, not software: `POST https://api.mouse.dev/v1/decisions` with the key already +in the environment. Measured facts are in `docs/MOUSE-1-NOTES.md` and summarised in +`docs/PLAN-INTERFACE.md`: cold start of minutes, ~490 ms warm, stable answers at high +confidence, and four proven uses — failure routing first among them. + +The one place it would earn its keep here: a failed run in `Agents>agents` offers `f`, which +asks "what does this failure need" with four options and records the answer, the confidence and +the options on the task. A person still decides, and it never decides a merge. + +## Done: Headroom + +A provider entry in `~/.pi/agent/settings.json` pointing at `http://192.168.20.13:8787/v1`, and +an agent's model set to `headroom/`. Per agent, because the model is per agent; per +project is not possible. The exact steps and the one unknown to measure are in +`docs/PLAN-INTERFACE.md`. ## Unproven, and worth a test diff --git a/docs/PLAN-INTERFACE.md b/docs/PLAN-INTERFACE.md index 8649c5a..4f14092 100644 --- a/docs/PLAN-INTERFACE.md +++ b/docs/PLAN-INTERFACE.md @@ -576,48 +576,92 @@ A transcript tab inside `Agents>agents` is the integration option: read the JSON messages, no second process. Worth doing only if the file browser feels like a detour after a week of use. Recommendation: do the cheap one now, decide later. -## verify.sh, plainly +## The project's check: `.harness/check.sh` -`.harness/verify.sh` is a shell script in the project folder. Its only job is to answer one -question: **is this project's work finished?** It exits 0 when the answer is yes. +This is a script in the project folder that exits 0 when the project still passes its own +tests. It answers "did this change break the project", not "is this task finished". -- It is **not** Paperclip's. Paperclip has no such concept; an agent sets a task's status. -- It is **our** convention, from the old project, and `bin/office` now writes a template: +**What decides a task is finished**, in order of authority: - ```sh - #!/bin/sh - echo "no verify command agreed for this project" - exit 0 - ``` +1. the agent, by setting the task's status — this is Paperclip's own model and nothing in our + software can overrule it; +2. a reviewer, if a review stage is configured on the task (Paperclip's execution policy); +3. `ocr`, at merge time, which can refuse the change. - As it stands it says yes to everything, which is honest: there is no agreed check. Replace - the body with the project's real tests when they exist: +The project's check is a **regression check** at the same point as a CI pipeline: `bin/merge` +runs it in the task's worktree before merging, and again on the base branch after merging, and +rolls the merge back if it fails. It never decides that a task is finished. - ```sh - exec go test ./... - ``` +Why keep it at all: an agent says it is done, a review says the diff looks right, and neither +proves the build still works. One command does. If a project has no check, `bin/merge` says so +and the review decides alone — that is the current state of this sandbox. -- `bin/merge` runs it in the task's worktree, and again on the base branch after merging. -- A task may carry its own command in its description; that one wins for that task only. +Naming: `check.sh` is the current name. `.harness/verify.sh` was the earlier project's name and +is still honoured if present, so an adopted folder keeps working. `bin/office` writes +`check.sh` with a comment explaining what to replace. -So: a per-project default, a per-task override, both optional. If neither exists, the merge -says so and lets the `ocr` review decide. +## Headroom, concretely -## Headroom, and whether it is good for a project +Headroom is a proxy that accepts the same HTTP API as the model router and shortens long +prompts before forwarding. Measured here: -Headroom is a **compression proxy**: requests pass through it and long prompts are shortened -before they reach the model. It is running here at `192.168.20.13:8787` (v0.24.0, healthy, -memory backend disabled, compression on). +``` +GET http://192.168.20.13:8787/v1/models → 200, 486 models (auto/best-coding, deepseek/…) +GET http://192.168.20.13:8787/health → 200, healthy, compression on, memory off +upstream (config `openai_api_url`) → http://192.168.20.13:20129/v1 +``` -It is good where prompts are long and repetitive — a big repository, a long thread, many tool -results. It is pointless for a small task whose prompt is already short, and it adds a hop and a -failure mode. +The agents' provider `omni` already points at `http://192.168.20.13:20129/v1`, which is the +same upstream headroom forwards to. So headroom can sit in front of it. -The lever is per **agent**, not per project, because a Paperclip agent's provider is configured -on the agent. `adapterConfig.env` accepts variables, so an agent can be pointed at the proxy -without touching anything else. That is why the interface offers a read-out first: until one -agent has run both ways and you have compared cost and quality, "good for some projects" stays -a hypothesis rather than a setting. +**How to use it for one agent.** Two steps, and only the second is per agent: + +1. Add a provider to `~/.pi/agent/settings.json` (global, once): + + ```json + "headroom": { + "baseUrl": "http://192.168.20.13:8787/v1", + "apiKey": "", + "api": "openai-completions" + } + ``` + +2. Set that agent's model in `Agents>settings` to `headroom/auto/best-coding` (or any model id + from the list above). Paperclip stores the model per agent, so exactly one agent changes. + +**Would it work.** The endpoint answers the OpenAI-style API that Pi's `openai-completions` +provider speaks, and lists the models Pi would ask for, so the pieces line up. It is still a +hypothesis until one run is made through it: the unknowns are whether the proxy passes Pi's +tool-calling requests unchanged, and what compression does to a prompt with large tool results. +The measurement is one run: compare cost, duration and result quality for the same task with and +without the provider. + +**Per project is not possible.** The provider is chosen by the agent's model, and agents belong +to the company, not to a project. The finest granularity is one agent, which is what the +`Agents>settings` model field already gives. + +## Mouse-1, measured + +The person supplied `docs/MOUSE-1-NOTES.md`, written from measurements on 2026-11-08/09. The +facts that shape the design: + +- `POST https://api.mouse.dev/v1/decisions`, `Authorization: Bearer $MOUSE_API_KEY`, body + `{model, state, questions:{id:{type:"choice", instructions, criteria}}}`; the answer returns + `choice`, `confidence`, `probabilities`. +- **Cold start is minutes.** The first call after idle returned `503 engine_timeout` after 30 s; + the retry returned 200 in 13 s; warm calls were 1044 ms then ~489 ms. +- The same state and options gave the same choice at ~0.98 confidence. It behaves as a + classifier, not a writer. +- Proven useful: **failure routing** (redo / stronger model / environment / person), unblock + triage, intake clarity, task sizing. Measured in the old project as `bin/route-failure`. +- Ruled out: the merge decision, judging test results, anything on a keystroke path, vague + options, and being the only record of a decision. + +Where it would go in this interface, if added: `Agents>agents` → a failure on a run offers +`f`, which asks Mouse-1 one question with four options and writes the recommendation, the +confidence and the options as a comment on the task. A person still decides. Expected cost of +the first call after idle is 30 seconds, so `f` must show "asking, this can take half a minute" +rather than appear to hang. ## Mouse-1, corrected diff --git a/tui/frame.go b/tui/frame.go index afd1c63..ca2f474 100644 --- a/tui/frame.go +++ b/tui/frame.go @@ -278,15 +278,47 @@ func wrap(s string, w int) []string { // screen. It replaces the whole view rather than floating over it, so nothing // behind it can be misread as still active. func helpOverlay(title string, entries [][2]string, w, h int) string { - boxW := min(64, max(30, w-8)) - boxH := min(len(entries)+4, max(8, h-4)) - lines := []string{""} + // The box is as wide as the screen allows, and every line of text is wrapped + // inside it. Cutting a sentence in half hides the reason a key exists. + boxW := min(92, max(40, w-6)) + if boxW > w { + boxW = w + } + keyCol := 16 + inner := boxW - 4 - keyCol + if inner < 20 { + keyCol = 8 + inner = boxW - 4 - keyCol + } + + var lines []string + lines = append(lines, "") for _, e := range entries { - key := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Accent)).Bold(true). - Width(16).Render(e[0]) - lines = append(lines, " "+key+lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(e[1])) + key := e[0] + if key == "" { + for _, cont := range wrap(e[1], max(12, inner)) { + lines = append(lines, " "+strings.Repeat(" ", keyCol)+ + lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(cont)) + } + continue + } + wrapped := wrap(e[1], max(12, inner)) + styled := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Accent)).Bold(true). + Render(padTo(key, keyCol)) + text := lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(wrapped[0]) + lines = append(lines, " "+styled+text) + for _, cont := range wrapped[1:] { + lines = append(lines, " "+strings.Repeat(" ", keyCol)+ + lipgloss.NewStyle().Foreground(lipgloss.Color(active.Text)).Render(cont)) + } } lines = append(lines, "", dimStyle().Render(" esc or ? closes this")) + + boxH := len(lines) + 2 + if boxH > h-2 { + boxH = h - 2 + lines = lines[:max(1, boxH-2)] + } p := panel{title: title, width: boxW, height: boxH, focused: true} return lipgloss.Place(w, h, lipgloss.Center, lipgloss.Center, p.render(lines)) } diff --git a/tui/work.go b/tui/work.go index 53d0399..20bb9ff 100644 --- a/tui/work.go +++ b/tui/work.go @@ -467,9 +467,9 @@ func (m workModel) View() string { } if m.confirm { - verify := "no verify command: the review decides" + check := "no project check: the review decides" if cmd := verifyFromDescription(m.task.Description); cmd != "" { - verify = "verify command: " + cmd + check = "project check: " + cmd } branch := "(no branch yet)" if m.task.Workspace != nil && m.task.Workspace.Branch != "" { @@ -478,7 +478,7 @@ func (m workModel) View() string { return helpOverlay("merge "+m.task.Identifier+" into main?", [][2]string{ {"what is merged", branch + " → " + baseRefOf(m.task)}, {"what happens", "rebase the branch onto the base, then merge it"}, - {"", verify}, + {"", check}, {"", "ocr reviews the change; a critical finding refuses the merge"}, {"", "merge, then re-run the checks on the base branch"}, {"y", "do it"},