@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.
- package/README.md +214 -0
- package/dist/cli.js +14079 -0
- package/dist/main.js +13952 -0
- 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`.
|