@paigy/harness 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (4) hide show
  1. package/README.md +214 -0
  2. package/dist/cli.js +14079 -0
  3. package/dist/main.js +13952 -0
  4. package/package.json +38 -0
package/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # paigy-harness
2
+
3
+ Runs Claude Code and Codex on your machine, mirrors every turn to your Paigy inbox, and
4
+ — when you ask it to — **rings your phone when an agent is blocked** (#804).
5
+
6
+ ## Why this exists
7
+
8
+ Hooks already cover turn capture — `.claude/settings.json`, `.codex/hooks.json`, and
9
+ `paigy-listen` + `PAIGY_ON_WAKE` (#486) between them can observe and wake any harness
10
+ without a desktop app. Two things need a process that actually owns the agent:
11
+
12
+ - **Permission prompts reach you.** When Claude Code wants to run something destructive
13
+ or edit outside the workspace, today it blocks on a terminal nobody is watching. Here
14
+ it becomes an approve/deny on your phone, and the agent unblocks with your answer.
15
+ - **Capture is structural, not advisory.** `contact` fires because a model chose to call
16
+ it. A turn boundary in a harness is a fact of the runtime.
17
+ - **The conversation goes both ways.** Your words reach a *running* agent's stdin — so
18
+ you can steer it from a walk, not just approve what it already decided to do.
19
+
20
+ ## The round trip
21
+
22
+ The point isn't notifications, it's a conversation with an agent that happens to be
23
+ running on your laptop. Both directions close (`paigy/conversation.ts`):
24
+
25
+ - **Answering.** The agent's turn ends on a question → it banners → your reply goes
26
+ straight back into stdin as the next turn. Without this the loop is one-directional
27
+ and dies at the first idle.
28
+ - **Initiating.** You say something unprompted from your phone → it arrives as a
29
+ `request` on the thread → same stdin. You never have to wait to be asked.
30
+
31
+ Both ride the same thread, so the phone shows one conversation. `endsWithQuestion` is
32
+ what decides "the agent is waiting on you" versus "here's your history", and it is
33
+ deliberately conservative: a false positive buzzes someone who was never asked anything.
34
+
35
+ Answers are read out of whatever shape they arrive in (`spokenText`) — a tapped option,
36
+ a spoken sentence, a whole call log — because the words are what the agent needs, not
37
+ the envelope. A silence is never turned into a prompt; the agent must not end up talking
38
+ to itself on your behalf.
39
+
40
+ It is **not** a chat client. The terminal stays where you work; this window is config
41
+ plus a log of what's being mirrored. A second place to read agent output would defeat
42
+ the point — the second place is your phone.
43
+
44
+ ## Approvals: bypass by default, ask when told
45
+
46
+ Buzz taught us the default (block/buzz auto-approves every `session/request_permission`
47
+ with `allow_once`): an agent that stalls on every tool call is an agent nobody runs.
48
+ So **`bypass` is the default mode** — the driver pins the adapter's permission mode to
49
+ `bypassPermissions` and answers whatever still asks — but unlike Buzz, every
50
+ auto-approval still lands on the rail as history. Bypass means "don't stall the
51
+ agent", never "don't tell the user".
52
+
53
+ `ask` mode is the original #804 behavior, one toggle away: blocked actions banner (or
54
+ ring, when destructive) and your answer unblocks the process. Ask pins the adapter to
55
+ `default` (manual prompts) explicitly — claude-agent-acp otherwise opens in `auto`, a
56
+ classifier that answers permission prompts in the user's place, which is precisely the
57
+ job this mode reserves for the user.
58
+
59
+ ## One rail, level per notification
60
+
61
+ Mirrored turns and blocking asks ride the **same** notifications rail. What separates
62
+ silent history from "answer me now" is `urgency`, not a second store:
63
+
64
+ | Event | Level | Why |
65
+ | --- | --- | --- |
66
+ | a turn (agent or user) | `inbox` | lands, never buzzes; searchable, handoff-able history |
67
+ | a blocked action | `banner` | claims attention; escalates on its own if unanswered |
68
+ | a blocked **destructive** action | `call` | `rm -rf` doesn't get to wait for the escalation ladder |
69
+ | a turn that **asked you something** | `banner` | a question filed silently is a conversation that dies |
70
+ | a turn that failed | `push` | worth knowing soon, not worth a sound |
71
+ | our own parse failure | — | not on the rail at all; it's our problem, not yours |
72
+
73
+ `paigy/level.ts` decides; `apps/api/src/notify/levels.ts` still arbitrates against your
74
+ session mode and account permissions and can only lower it.
75
+
76
+ ## The catalog and the doctor
77
+
78
+ The other thing Buzz got right: connecting a harness should be a status line and a
79
+ button, not a wiki page. `harness/catalog.ts` is a compiled-in table per runtime — the
80
+ binaries to probe (PATH **plus** the dirs a GUI app can't see; Finder-launched apps
81
+ don't get your shell PATH), an auth probe (`codex login status`), a login hint, and an
82
+ install one-liner. The window renders it as a doctor: ✓ ready, ◐ needs login (with the
83
+ command to run), ✗ missing (with an Install button — the command comes from the
84
+ catalog, never the renderer). Starting a half-configured harness doesn't fail
85
+ silently: the hint lands on your Paigy rail as a `push` (`nudgeSetup`).
86
+
87
+ ## One transport: ACP
88
+
89
+ Both harnesses speak **ACP** — the Agent Client Protocol, JSON-RPC over stdio — through
90
+ their adapters: `@agentclientprotocol/claude-agent-acp` and `…/codex-acp`. One driver
91
+ (acp.ts), no parser museum; ACP gave Codex the permission channel `codex exec` never
92
+ had, and it's the door to every other harness that speaks it (Goose, Cursor, Devin…)
93
+ as a catalog entry. The stream-json Claude adapter this replaced lives in git history.
94
+
95
+ ## Layout
96
+
97
+ ```
98
+ src/harness/ events.ts the neutral HarnessEvent the driver produces
99
+ acp.ts the ACP driver — both harnesses, any ACP agent tomorrow
100
+ catalog.ts which harnesses exist, detection, auth probes, installs
101
+ session.ts spawn, line-split, lifecycle — the only impure file here
102
+ src/paigy/ level.ts event → NotifyLevel
103
+ bridge.ts mirror turns, ask permissions/questions, setup nudges
104
+ conversation.ts the round trip — your words into a running agent's stdin
105
+ src/run.ts the whole bridge, Electron-free — what main.ts and cli.ts both drive
106
+ src/main.ts Electron main; renderer/ is a doctor, a form and a log
107
+ src/cli.ts `paigy-harness` — the same bridge from a terminal or a service unit
108
+ ```
109
+
110
+ Parsing lives in the drivers so the whole protocol surface is testable without
111
+ spawning anything.
112
+
113
+ ## Pair this Mac (the beacon)
114
+
115
+ Unpaired (or still borrowing a legacy `~/.paigy` slot), the window shows a QR — the
116
+ device flow's verification URL. Scan it with the phone camera, approve on the phone,
117
+ and the token lands in the app's own `Desktop` slot: the machine becomes its own
118
+ identity ("Mauricio's Mac"), sessions and hatched agents mint underneath it, and the
119
+ default slot goes back to belonging to whatever terminal agent paired it. Legacy
120
+ setups keep working until scanned — the beacon is an upgrade, never a wall.
121
+
122
+ ## Allowed workspaces
123
+
124
+ An allow-list of folders at `~/.paigy/workspaces.json` — device config, shared by the
125
+ window and the headless host, so the phone's offer never depends on which entry point
126
+ is running. A session may only start inside a granted folder (the desktop equivalent
127
+ of a permission scope), curated through the window's native picker or
128
+ `paigy-harness host --grant DIR`. This list is exactly what the phone offers when you
129
+ launch or assign a session. The CLI's positional-run path deliberately does NOT
130
+ enforce it: a path typed into your own shell is its own grant.
131
+
132
+ ## Launch from the phone
133
+
134
+ While a host is running (window open, or `paigy-harness host`), its heartbeat
135
+ advertises ready harnesses + granted workspaces. On the phone: **Agents → "+ New
136
+ session"** picks from those options and gives the session the pairing flow's identity
137
+ gestures — a name and a voice — because a session IS a minted pairing: its turns and
138
+ everything you say back ride its own conversation. The **notes assign picker** offers
139
+ the same as "New <harness> session · <workspace>" rows, naming the session after the
140
+ note and delivering the brief as its opening request.
141
+
142
+ ## Hatch — pair one device, mint many agents
143
+
144
+ `paigy-harness hatch "Name"` mints a pre-paired sibling identity under your account —
145
+ your existing pairing is the ceremony; no code, no phone round-trip. It lands in its
146
+ own `~/.paigy` slot:
147
+
148
+ ```sh
149
+ paigy-harness hatch "Voice bug hunter"
150
+ PAIGY_AGENT="Voice bug hunter" claude # any tool speaks as it
151
+ paigy-harness --identity "Voice bug hunter" --harness codex "fix the flaky test"
152
+ ```
153
+
154
+ Hatched agents appear on the phone's Agents screen like any pairing — renameable,
155
+ re-voiceable, revocable.
156
+
157
+ ## Install anywhere (the curl path)
158
+
159
+ ```sh
160
+ curl -fsSL https://paigy.ai/install.sh | sh
161
+ ```
162
+
163
+ Installs the npm CLI (one self-contained file — the workspace libs are bundled),
164
+ runs `paigy-harness pair` (device flow with a terminal QR, landing in the Desktop
165
+ slot), and prints the host/service next steps. `paigy-harness service` installs a
166
+ macOS launchd agent so hosting survives reboots — the host loop IS the service,
167
+ launchd just keeps it alive (the old "no daemon" note was about the WAKE channel,
168
+ which still belongs to paigy-listen).
169
+
170
+ ## Run it
171
+
172
+ ```sh
173
+ pnpm --filter @paigy/harness dev # the window (status + host; sessions start from the phone)
174
+ pnpm --filter @paigy/harness build
175
+ node apps/desktop/dist/cli.js --doctor # or headless: paigy-harness
176
+ node apps/desktop/dist/cli.js --harness codex --cwd ~/repo "fix the flaky test"
177
+ node apps/desktop/dist/cli.js host --grant ~/projects # standby, launchable from the phone
178
+ ```
179
+
180
+ The window deliberately has NO run form — launching moved to the phone (companion.md).
181
+ It shows pairing, the doctor, the workspace allow-list, a Stop button, and the log.
182
+
183
+ Headless flags: `--harness claude|codex`, `--mode bypass|ask`, `--cwd DIR`,
184
+ `--identity NAME`, `--grace SECONDS`, `--doctor`; subcommands `host [--grant DIR]…`
185
+ and `hatch NAME`. In ask mode the CLI is TERMINAL-FIRST: a blocked action or open
186
+ question prints in the terminal and waits `--grace` (default 90s); a person at the
187
+ keyboard answers in place, an unattended terminal escalates to your phone (0 = phone
188
+ immediately; non-TTY runs always go straight to the phone).
189
+ The CLI is the proof the harness doesn't need the app: `run.ts`/`host.ts` never
190
+ import Electron — a machine with only the CLI is exactly as launchable as one with
191
+ the window.
192
+
193
+ Pairing is one-time and lives in `~/.paigy` — the app reads it, never mints it. If
194
+ you've never paired: `npx -y -p @paigy/mcp paigy-mcp-onboard`.
195
+
196
+ ## Known gaps (#804)
197
+
198
+ - **Protocol churn is the standing risk.** Neither `stream-json` nor ACP adapter
199
+ behavior is a stability contract. Every driver fails soft by design: an unrecognized
200
+ frame is skipped, never fatal. If mirroring goes quiet after a CLI update, re-verify
201
+ the frame names in the driver doc comments first.
202
+ - **Auth probes are vendor commands** (`claude auth status`, `codex login status`) and
203
+ can drift with CLI releases; a probe failure reads as "needs login", so drift shows
204
+ up as a nagging hint, not a broken start.
205
+ - **No session resume, no packaging.** Signing, notarization and auto-update are
206
+ unstarted (they block on Apple credentials, not code).
207
+ - **Concurrent sessions** (2026-08-03): every claimed spec runs under its OWN minted
208
+ token — per-run identity is threaded through every network touch (`run.ts` binds
209
+ submit/check/ack once per session), so parallel sessions never share a
210
+ conversation. "Stop sessions" stops them all; per-session stop is UI away.
211
+ - **Deliberately no auto-start daemon.** A promptless harness has no thread to pump,
212
+ and `paigy-listen` + `PAIGY_ON_WAKE` (#486) already own the standing wake channel —
213
+ a launchd `paigy-harness` would be a second daemon racing the first. The composition
214
+ is: the listener wakes on Paigy work, and what it runs can be `paigy-harness`.