Work>work chooses its own task and merges from the interface; panes find their tools; shell follows the project folder

This commit is contained in:
Sam
2026-11-11 12:02:12 +11:00
parent 678c497064
commit 7198c34e42
13 changed files with 441 additions and 15 deletions

View File

@@ -0,0 +1,80 @@
---
type: context
id: context.paperclip-harness.agents
title: agents
status: active
source_of_truth: false
freshness: current
last_reviewed: 2026-11-11
tags:
- pack/paperclip-harness
- agent-memory/context
- autolearn
- rich-discovery
- repo/agents
---
# agents
## Rich discovery update (2026-11-11 00:28:33)
You are right on both counts, and I will be precise about what was invented and whose rule it was.
## Where the shell command came from — and why I was wrong to force it
It is **not** a Paperclip rule. It is your own project rule, written in `docs/OPERATING-RULES.md` before this attempt:
> **How a task is judged finished.** The test is a **command**, kept with the project, and agreed **before** the work starts.
That came out of the old project, along with `.harness/verify.sh`. Paperclip has no such field. An agent sets a task's status itself; the reviewer or a review stage checks it. I took a project convention and turned it into a mandatory form field and a shell prompt in a test plan, which made the agent look like a dumb executor. That is my error, and the rule is now marked in `OPERATING-RULES.md` as ours, not Paperclip's.
**What changed in the program:**
- The field is now called **"A command that proves it (optional)"**, and its hint says: *"leave empty and the agent decides how to check its own work."*
- The guidance panel no longer asks you to write shell prompts; it says most tasks need nothing there and to fill it only for a gate no agent can talk its way past.
- `bin/merge` already worked without one — it says *"no verify command: this would pass on the agent's word"* and continues to the review.
## What Paperclip actually does, and what you actually write
You write a task in your own words. Pi reads it, works out how to do it, runs the project's own tests, comments with evidence, and sets the status. Paperclip adds the parts you do not want to run by hand: blockers as links between tasks, review stages (`in_review` with a participant), approvals, run history, cost, and a worktree per task.
So the autonomy is in the *work*, not in your instructions. My test plan asked for a file name and an exact line, which is why it read like dictating a patch.
## The error you saw was a real bug, not your mistake
`AUD-167` **was created and completed** — `ADd a file named STATUS.md`, status `done`, assigned to Builder. The "[The program exited with status 0.]" was the pane dying: the New task pane ran the form alone, so when the form quit after creating, nothing was left to run. Two fixes:
- the form now **stays open** and shows `created AUD-1xx, handed to an agent which will start on its own`, with the fields cleared for the next one;
- every pane now **falls back to a shell**, so a program that exits returns to a prompt instead of leaving a dead pane.
I proved both by creating `AUD-168` — *"Tidy the README introduction"*, assigned, no verify command. It shows `in_progress` with Builder working, and the form stayed usable.
## On "have you fully read and understood Paperclip"
No, and I should have said so earlier. I have read the agent skill, the API specification, the agents/heartbeats/routines documentation, and I have probed the API extensively. What I got wrong was mixing your old project's conventions with Paperclip's model and presenting the mixture as if it were Paperclip's. Where I am unsure — review stages, execution policy, approvals — I will read before advising, and say when I have not.
## The test plan, corrected
`docs/TEST-PLAN.md` no longer asks for a shell command anywhere. Step 1 is now: title, plain-language description, no blocker, no command, `yes` to assign — and expect the agent to pick it up within seconds and decide for itself how to check the work. Step 4 keeps `bin/merge` but explains that "no verify command" is a normal outcome, not a failure.
Committed as `678c497`. `AUD-168` is running now if you want to watch it in `Agents>agents` — that is the honest demonstration of autonomy: you said what you wanted, it is working out how.
## Source paths observed
- `../bin/harness`
- `agents/heartbeats/routines`
## Evidence
- Confidence: 84%
- Reason: Update component/repository context with detailed discovered structure and configuration
## Related
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
## Related learned memories <!-- same-turn-2026-11-11 00:28:33 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-agents-2026-11-11-00-28-33|Rich discovery: agents 2026-11-11 00:28:33]]
- context: [[packs/paperclip-harness/20-context/agents|agents]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|agents repository structure and configuration patterns]]

View File

@@ -2159,3 +2159,67 @@ Committed as `c9e027d`.
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-root-2026-11-11-00-03-58|Rich discovery: root. 2026-11-11 00:03:58]]
- context: [[packs/paperclip-harness/20-context/home|root.]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|root. repository structure and configuration patterns]]
---
## Update (2026-11-11 00:28:33)
## Structural/configuration observations (2026-11-11 00:28:33)
You are right on both counts, and I will be precise about what was invented and whose rule it was.
## Where the shell command came from — and why I was wrong to force it
It is **not** a Paperclip rule. It is your own project rule, written in `docs/OPERATING-RULES.md` before this attempt:
> **How a task is judged finished.** The test is a **command**, kept with the project, and agreed **before** the work starts.
That came out of the old project, along with `.harness/verify.sh`. Paperclip has no such field. An agent sets a task's status itself; the reviewer or a review stage checks it. I took a project convention and turned it into a mandatory form field and a shell prompt in a test plan, which made the agent look like a dumb executor. That is my error, and the rule is now marked in `OPERATING-RULES.md` as ours, not Paperclip's.
**What changed in the program:**
- The field is now called **"A command that proves it (optional)"**, and its hint says: *"leave empty and the agent decides how to check its own work."*
- The guidance panel no longer asks you to write shell prompts; it says most tasks need nothing there and to fill it only for a gate no agent can talk its way past.
- `bin/merge` already worked without one — it says *"no verify command: this would pass on the agent's word"* and continues to the review.
## What Paperclip actually does, and what you actually write
You write a task in your own words. Pi reads it, works out how to do it, runs the project's own tests, comments with evidence, and sets the status. Paperclip adds the parts you do not want to run by hand: blockers as links between tasks, review stages (`in_review` with a participant), approvals, run history, cost, and a worktree per task.
So the autonomy is in the *work*, not in your instructions. My test plan asked for a file name and an exact line, which is why it read like dictating a patch.
## The error you saw was a real bug, not your mistake
`AUD-167` **was created and completed** — `ADd a file named STATUS.md`, status `done`, assigned to Builder. The "[The program exited with status 0.]" was the pane dying: the New task pane ran the form alone, so when the form quit after creating, nothing was left to run. Two fixes:
- the form now **stays open** and shows `created AUD-1xx, handed to an agent which will start on its own`, with the fields cleared for the next one;
- every pane now **falls back to a shell**, so a program that exits returns to a prompt instead of leaving a dead pane.
I proved both by creating `AUD-168` — *"Tidy the README introduction"*, assigned, no verify command. It shows `in_progress` with Builder working, and the form stayed usable.
## On "have you fully read and understood Paperclip"
No, and I should have said so earlier. I have read the agent skill, the API specification, the agents/heartbeats/routines documentation, and I have probed the API extensively. What I got wrong was mixing your old project's conventions with Paperclip's model and presenting the mixture as if it were Paperclip's. Where I am unsure — review stages, execution policy, approvals — I will read before advising, and say when I have not.
## The test plan, corrected
`docs/TEST-PLAN.md` no longer asks for a shell command anywhere. Step 1 is now: title, plain-language description, no blocker, no command, `yes` to assign — and expect the agent to pick it up within seconds and decide for itself how to check the work. Step 4 keeps `bin/merge` but explains that "no verify command" is a normal outcome, not a failure.
Committed as `678c497`. `AUD-168` is running now if you want to watch it in `Agents>agents` — that is the honest demonstration of autonomy: you said what you wanted, it is working out how.
## Source paths observed
- `../bin/harness`
- `agents/heartbeats/routines`
## Evidence
- Confidence: 82%
- Reason: Captured factual repo structure/configuration patterns from rich discovery
## Related learned memories <!-- same-turn-2026-11-11 00:28:33 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-agents-2026-11-11-00-28-33|Rich discovery: agents 2026-11-11 00:28:33]]
- context: [[packs/paperclip-harness/20-context/agents|agents]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|agents repository structure and configuration patterns]]

View File

@@ -0,0 +1,83 @@
---
type: session
id: session.paperclip-harness.rich-discovery-agents-2026-11-11-00-28-33
title: Rich discovery: agents 2026-11-11 00:28:33
status: active
source_of_truth: false
freshness: current
last_reviewed: 2026-11-11
tags:
- pack/paperclip-harness
- agent-memory/session
- autolearn
- rich-discovery
- repo/agents
---
# Rich discovery: agents 2026-11-11 00:28:33
Sanitized rich discovery snapshot captured automatically from the completed turn.
## Source paths observed
- `../bin/harness`
- `agents/heartbeats/routines`
## Final answer snapshot
You are right on both counts, and I will be precise about what was invented and whose rule it was.
## Where the shell command came from — and why I was wrong to force it
It is **not** a Paperclip rule. It is your own project rule, written in `docs/OPERATING-RULES.md` before this attempt:
> **How a task is judged finished.** The test is a **command**, kept with the project, and agreed **before** the work starts.
That came out of the old project, along with `.harness/verify.sh`. Paperclip has no such field. An agent sets a task's status itself; the reviewer or a review stage checks it. I took a project convention and turned it into a mandatory form field and a shell prompt in a test plan, which made the agent look like a dumb executor. That is my error, and the rule is now marked in `OPERATING-RULES.md` as ours, not Paperclip's.
**What changed in the program:**
- The field is now called **"A command that proves it (optional)"**, and its hint says: *"leave empty and the agent decides how to check its own work."*
- The guidance panel no longer asks you to write shell prompts; it says most tasks need nothing there and to fill it only for a gate no agent can talk its way past.
- `bin/merge` already worked without one — it says *"no verify command: this would pass on the agent's word"* and continues to the review.
## What Paperclip actually does, and what you actually write
You write a task in your own words. Pi reads it, works out how to do it, runs the project's own tests, comments with evidence, and sets the status. Paperclip adds the parts you do not want to run by hand: blockers as links between tasks, review stages (`in_review` with a participant), approvals, run history, cost, and a worktree per task.
So the autonomy is in the *work*, not in your instructions. My test plan asked for a file name and an exact line, which is why it read like dictating a patch.
## The error you saw was a real bug, not your mistake
`AUD-167` **was created and completed** — `ADd a file named STATUS.md`, status `done`, assigned to Builder. The "[The program exited with status 0.]" was the pane dying: the New task pane ran the form alone, so when the form quit after creating, nothing was left to run. Two fixes:
- the form now **stays open** and shows `created AUD-1xx, handed to an agent which will start on its own`, with the fields cleared for the next one;
- every pane now **falls back to a shell**, so a program that exits returns to a prompt instead of leaving a dead pane.
I proved both by creating `AUD-168` — *"Tidy the README introduction"*, assigned, no verify command. It shows `in_progress` with Builder working, and the form stayed usable.
## On "have you fully read and understood Paperclip"
No, and I should have said so earlier. I have read the agent skill, the API specification, the agents/heartbeats/routines documentation, and I have probed the API extensively. What I got wrong was mixing your old project's conventions with Paperclip's model and presenting the mixture as if it were Paperclip's. Where I am unsure — review stages, execution policy, approvals — I will read before advising, and say when I have not.
## The test plan, corrected
`docs/TEST-PLAN.md` no longer asks for a shell command anywhere. Step 1 is now: title, plain-language description, no blocker, no command, `yes` to assign — and expect the agent to pick it up within seconds and decide for itself how to check the work. Step 4 keeps `bin/merge` but explains that "no verify command" is a normal outcome, not a failure.
Committed as `678c497`. `AUD-168` is running now if you want to watch it in `Agents>agents` — that is the honest demonstration of autonomy: you said what you wanted, it is working out how.
## Evidence
- Confidence: 86%
- Reason: Preserve full sanitized discovery detail from a rich investigation turn
## Related
- [[packs/paperclip-harness/00-system/pi-agent/memory-manifest|Memory Manifest]]
## Related learned memories <!-- same-turn-2026-11-11 00:28:33 -->
- session: [[packs/paperclip-harness/80-sessions/rich-discovery-agents-2026-11-11-00-28-33|Rich discovery: agents 2026-11-11 00:28:33]]
- context: [[packs/paperclip-harness/20-context/agents|agents]]
- observation: [[packs/paperclip-harness/60-observations/tmp-repository-structure-and-configuration-patterns|agents repository structure and configuration patterns]]

View File

@@ -60,7 +60,7 @@ program running in the Board workspace.
|---|---|---|
| Shell>shell | a prompt in the project folder: the pane that never exits, and the escape hatch for anything no panel covers | — |
| Board>board | tasks by group · the chosen task · needs you | ↑↓ j k · enter · a · u · / · t (tools) · tab |
| Work>work | runs · change · facts · review | [ ] tab · ↑↓ j k · g G · t (tools) |
| Work>work | runs · change · facts · review · merge | [ ] tab · ↑↓ j k · g G · o (choose task) · m (merge) · t (tools) |
| Reply>reply | the task · the thread · the reply box | enter · s · esc |
| New task>new | the form · what a task needs · what will be sent | tab · enter · ctrl+n |
| Project>project | overview · facts · costs · settings | [ ] tab · ↑↓ j k |

Binary file not shown.

View File

@@ -14,6 +14,20 @@ import sys
import urllib.error
import urllib.request
# A program started from a TUIOS pane gets a smaller PATH than an interactive
# shell, so the places a person's tools actually live are added before anything
# is looked for. Without this, ocr is "not installed" inside a merge.
_HOME = os.path.expanduser("~")
_USER = os.environ.get("USER", "")
for _p in (
os.path.join(_HOME, ".local", "bin"),
os.path.join(_HOME, ".nix-profile", "bin"),
"/run/current-system/sw/bin",
"/etc/profiles/per-user/" + _USER + "/bin",
):
if _p not in os.environ.get("PATH", ""):
os.environ["PATH"] = _p + ":" + os.environ.get("PATH", "")
API = os.environ.get("PAPERCLIP_API_URL", "http://127.0.0.1:3100").rstrip("/")
COMPANY = os.environ.get("PAPERCLIP_COMPANY_ID", "d4921013-e5da-4d88-8016-57d5746e2d03")
KEY = os.environ.get("PAPERCLIP_BOARD_KEY", "")

View File

@@ -157,6 +157,14 @@ def arrange(order, session=None):
return moved
def window_cwd(name, session=None):
"""The working directory of a named pane, as TUIOS reports it."""
for w in windows(session):
if (w.get("custom_name") or "") == name:
return w.get("cwd") or ""
return ""
def close_pane(name, session=None):
for w in windows(session):
if (w.get("custom_name") or "") == name:

View File

@@ -86,24 +86,33 @@ Go to `Work>work`. It follows the task you chose on the board.
## Step 4 — merge it
This is the one step that needs a shell, because a merge takes a lock and re-runs the checks.
No terminal. Stay in `Work>work`.
1. Go to `Shell>shell`. You are in the project folder.
2. Type: `bin/merge AUD-1xx --check` (your identifier), press `Enter`.
1. Press `m`. The **merge** tab opens by itself and shows what is happening:
**Expected:** it rebases, runs the review, and prints either `checked only; nothing merged` or
`no verify command: this would pass on the agent's word` followed by the same. Both are normal:
nothing is merged yet.
```
merge of AUD-1xx started 12:01:39
rebased onto main
no verify command: this would pass on the agent's word
review saw nothing to review (0 files); recorded, not hidden
merged into main at 4a44473
merge finished 12:01:43
```
3. Type: `bin/merge AUD-1xx`, press `Enter`.
2. Wait for `merged into main at <commit>`. That commit is the task's work, on the project's
base branch.
**Expected:** the same checks, then `merged into main at <commit>`. A review that finds
something serious refuses the merge and says so; that is the gate working, not a failure.
**Expected:** a green refusal line, not a red one. If it says `refused: the review found 1
blocking item(s)`, the gate worked — the findings are on the task and in `Work>review`.
4. Type `git log --oneline -3`, press `Enter`. Your task's merge commit is at the top.
**Starting a merge twice by accident is safe**: the merge takes a lock and the second one is
refused with `another merge is in progress`.
**Stop if:** it says `refused`. The reason is printed and also written on the task, where
`Work>review` shows it.
## Step 4b — choose a task without leaving Work
In `Work>work`, press `o`. A list of this project's tasks appears. `↑`/`↓` to one, `Enter`, and
the runs, change, facts, review and merge tabs all switch to it. You never have to go to
`Board>board` first.
## Step 5 — a task that must wait for another

View File

@@ -37,6 +37,7 @@ what it shows is never a second copy of anything.
| `Work>change` | worktree, branch, base, commits, changed files, the diff | built |
| `Work>facts` | task, state, worktree path, branch, base, cost | built |
| `Work>review` | the verdicts recorded on this task: review findings, refused merges, merged results, blocking ones in red | built |
| `Work>merge` | the last merge this screen ran, live, with `m` to start one — no terminal needed | built |
## 4. Reply

View File

@@ -88,3 +88,55 @@ func splitLines(s string) []string {
}
return out
}
// mergeLogPath is where a merge writes what it did, so a panel can show it
// without the person opening a shell or remembering a command.
func mergeLogPath() string {
return filepath.Join(os.TempDir(), "paperclip-harness-merge.log")
}
// startMerge runs bin/merge for one task, detached, and returns at once. The
// Work workspace tails the log; the merge itself takes a lock, so starting one
// twice by accident is refused rather than doubled.
func startMerge(folder, identifier string) error {
merge := filepath.Join(exeDir(), "merge")
if _, err := os.Stat(merge); err != nil {
return fmt.Errorf("no bin/merge next to the program (%s)", merge)
}
log, err := os.Create(mergeLogPath())
if err != nil {
return err
}
fmt.Fprintf(log, "merge of %s started %s\n", identifier, time.Now().Format("15:04:05"))
cmd := exec.Command(merge, identifier)
cmd.Dir = folder
cmd.Stdout = log
cmd.Stderr = log
cmd.SysProcAttr = &syscall.SysProcAttr{Setsid: true}
if err := cmd.Start(); err != nil {
log.Close()
return err
}
go func() {
err := cmd.Wait()
if err != nil {
fmt.Fprintf(log, "\nmerge stopped: %v\n", err)
} else {
fmt.Fprintf(log, "\nmerge finished %s\n", time.Now().Format("15:04:05"))
}
log.Close()
}()
return nil
}
// mergeLogTail returns the end of the last merge, for the panel that shows it.
func mergeLogTail(lines int) []string {
b, err := os.ReadFile(mergeLogPath())
if err != nil {
return []string{"", " no merge has been run from this screen yet"}
}
all := splitLines(string(b))
start := max(0, len(all)-lines)
return all[start:]
}

View File

@@ -19,6 +19,11 @@ import (
"paperclip-harness/tui/paperclip"
)
type pickTasksMsg struct {
issues []paperclip.Issue
err error
}
type workMsg struct {
task paperclip.Issue
runs []paperclip.Run
@@ -46,13 +51,19 @@ type workModel struct {
scroll int
tools bool
toolAt int
readAt time.Time
// choosing a task here, so the screen does not depend on the Board
picking bool
pickList []paperclip.Issue
pickNames map[string]string
pickAt int
readAt time.Time
showHelp bool
width, height int
}
var workTabs = []string{"runs", "change", "facts", "review"}
var workTabs = []string{"runs", "change", "facts", "review", "merge"}
func newWorkModel(c *paperclip.Client, folder, issueID string) workModel {
return workModel{client: c, folder: folder, issue: issueID, explicit: issueID != ""}
@@ -125,6 +136,34 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, tea.Batch(m.load(), tickEvery(5*time.Second), tea.WindowSize())
case tea.KeyMsg:
if m.picking {
switch msg.String() {
case "esc", "q", "o":
m.picking = false
return m, nil
case "up", "k":
m.pickAt = max(0, m.pickAt-1)
return m, nil
case "down", "j":
m.pickAt = min(len(m.pickList)-1, m.pickAt+1)
return m, nil
case "enter":
if m.pickAt < len(m.pickList) {
chosen := m.pickList[m.pickAt]
m.picking = false
if err := rememberTask(m.folder, chosen.Identifier); err != nil {
m.err = err
return m, nil
}
m.explicit = true
m.issue = chosen.ID
m.task = chosen
return m, m.load()
}
return m, nil
}
return m, nil
}
if m.tools {
switch msg.String() {
case "esc", "q", "t":
@@ -160,6 +199,39 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
case "t":
m.tools = true
return m, nil
case "o":
c := m.client
folder := m.folder
return m, func() tea.Msg {
cfg, err := readHarnessFile(folder)
if err != nil {
return errMsg{err}
}
issues, err := c.Issues()
if err != nil {
return errMsg{err}
}
var mine []paperclip.Issue
for _, i := range issues {
if i.ProjectID == cfg["paperclip.project"] {
mine = append(mine, i)
}
}
return pickTasksMsg{issues: mine}
}
case "m":
if m.task.Identifier == "" {
return m, nil
}
// The merge runs on its own and writes what it does to a log this
// tab reads, so nobody has to type a command or leave the screen.
if err := startMerge(m.folder, m.task.Identifier); err != nil {
m.err = err
return m, nil
}
m.tab = 4
m.scroll = 1 << 30
return m, m.load()
case "?":
m.showHelp = true
return m, nil
@@ -201,6 +273,15 @@ func (m workModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
return m, nil
}
case pickTasksMsg:
m.pickList = msg.issues
m.pickAt = 0
m.picking = true
if msg.err != nil {
m.err = msg.err
}
return m, nil
case workMsg:
m.task = msg.task
m.runs = msg.runs
@@ -259,10 +340,23 @@ func (m workModel) View() string {
body = m.diffLines(m.width - 4)
case 3:
body = m.reviewLines(m.width - 4)
case 4:
body = m.mergeLines(m.width - 4)
default:
body = m.factsLines(m.width - 4)
}
if m.picking {
entries := make([][2]string, 0, len(m.pickList))
for i, is := range m.pickList {
mark := " "
if i == m.pickAt {
mark = "▸ "
}
entries = append(entries, [2]string{mark + is.Identifier, is.Status + " " + is.Title})
}
return helpOverlay("choose a task to look at", entries, m.width, m.height)
}
if m.tools {
entries := make([][2]string, 0, len(tools))
for i, t := range tools {
@@ -296,6 +390,9 @@ func (m workModel) View() string {
keyHint("[ ]", "switch tab"),
keyHint("↑↓ or j k", "scroll"),
keyHint("t", "open a tool pane"),
keyHint("o", "choose a task"),
keyHint("m", "merge this task"),
keyHint("]", "tabs"),
keyHint("r", "read again"),
keyHint("R", "rebuild every pane"),
keyHint("q", "leave"),
@@ -441,6 +538,24 @@ func (m workModel) reviewLines(width int) []string {
return out
}
// mergeLines shows the last merge this screen started, read from the log the
// merge writes. Pressing m again is safe: the merge takes a lock.
func (m workModel) mergeLines(width int) []string {
out := []string{"", dimStyle().Render(" " + mergeLogPath()), ""}
for _, ln := range mergeLogTail(max(6, m.height-6)) {
colour := active.Text
switch {
case strings.Contains(ln, "refused"), strings.Contains(ln, "stopped"):
colour = active.Error
case strings.Contains(ln, "merged into"), strings.Contains(ln, "finished"):
colour = active.Success
}
out = append(out, " "+lipgloss.NewStyle().Foreground(lipgloss.Color(colour)).Render(truncateVisible(ln, width-2)))
}
out = append(out, "", dimStyle().Render(" m runs the checks and merges this task; the log above is what it did"))
return out
}
func (m workModel) factsLines(width int) []string {
var out []string
out = append(out, "")