@pushary/agent-hooks 0.99.0 → 1.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/CHANGELOG.md +14 -0
- package/data/SKILL.md +250 -49
- package/data/cursor-plugin/scripts/pushary-gate.mjs +24 -0
- package/data/cursor-plugin/skills/pushary/SKILL.md +250 -49
- package/data/vscode-plugin/scripts/pushary-gate.mjs +24 -0
- package/data/vscode-plugin/skills/pushary/SKILL.md +250 -49
- package/dist/bin/pushary-bell-hook.js +18 -18
- package/dist/bin/pushary-bell.js +6 -6
- package/dist/bin/pushary-claude.js +22 -24
- package/dist/bin/pushary-clean.js +60 -18
- package/dist/bin/pushary-codex-bridge.js +10 -10
- package/dist/bin/pushary-codex-hook.js +33 -35
- package/dist/bin/pushary-codex.js +6 -7
- package/dist/bin/pushary-connect.js +6 -6
- package/dist/bin/pushary-cowork.js +3 -3
- package/dist/bin/pushary-daemon.js +22 -22
- package/dist/bin/pushary-disconnect.js +48 -7
- package/dist/bin/pushary-doctor.js +88 -19
- package/dist/bin/pushary-elicitation-hook.js +17 -17
- package/dist/bin/pushary-gemini-bridge.js +9 -9
- package/dist/bin/pushary-gemini-hook.js +25 -27
- package/dist/bin/pushary-hook.js +61 -33
- package/dist/bin/pushary-login.js +7 -7
- package/dist/bin/pushary-logout.js +4 -4
- package/dist/bin/pushary-mode.js +3 -3
- package/dist/bin/pushary-notification-hook.js +26 -21
- package/dist/bin/pushary-opencode-hook.js +27 -29
- package/dist/bin/pushary-permission-denied-hook.js +24 -23
- package/dist/bin/pushary-permission-hook.js +32 -24
- package/dist/bin/pushary-post-hook.js +30 -22
- package/dist/bin/pushary-prompt-hook.js +26 -21
- package/dist/bin/pushary-session-end-hook.js +26 -21
- package/dist/bin/pushary-session-start-hook.js +30 -25
- package/dist/bin/pushary-setup.js +145 -37
- package/dist/bin/pushary-stats.js +2 -2
- package/dist/bin/pushary-status.js +10 -10
- package/dist/bin/pushary-stop-hook.js +27 -22
- package/dist/bin/pushary-stopfailure-hook.js +26 -21
- package/dist/bin/pushary-transcript-register.js +16 -16
- package/dist/bin/pushary-transcripts.js +4 -4
- package/dist/bin/pushary-upgrade.js +14 -14
- package/dist/bin/pushary-wait.js +3 -3
- package/dist/{chunk-ID5FDKDZ.js → chunk-2MOQUNFL.js} +1 -1
- package/dist/{chunk-CRFLU455.js → chunk-6JUCFMSF.js} +3 -3
- package/dist/{chunk-QS55LDX4.js → chunk-6T4OINZB.js} +16 -16
- package/dist/{chunk-6C6MS57P.js → chunk-76ZCJZBK.js} +1 -1
- package/dist/{chunk-JXELRCU5.js → chunk-AH5OO2YZ.js} +3 -3
- package/dist/{chunk-XPPXOKIG.js → chunk-BBUZD7MU.js} +1 -1
- package/dist/{chunk-T3JZZUAR.js → chunk-DKTFZCLQ.js} +30 -29
- package/dist/{chunk-RALVI7OF.js → chunk-DZYIAMKR.js} +9 -6
- package/dist/{chunk-GCD3SQSV.js → chunk-E3WQMDCE.js} +1 -1
- package/dist/{chunk-JBJBVERL.js → chunk-EWQZISYV.js} +1 -1
- package/dist/{chunk-LWKPMSBB.js → chunk-EZ3GX3NS.js} +5 -5
- package/dist/{chunk-5KLAETSU.js → chunk-FO4HHPMO.js} +1 -1
- package/dist/{chunk-6KLCP7VF.js → chunk-IH7YUPFN.js} +3 -3
- package/dist/{chunk-OPR2BPD4.js → chunk-INBTKFT6.js} +1 -1
- package/dist/{chunk-K4XFCEVI.js → chunk-IPDBFJ2M.js} +1 -1
- package/dist/{chunk-6IFUXNFX.js → chunk-ISELLVBP.js} +1 -1
- package/dist/{chunk-562UJ2DA.js → chunk-J7PJALS2.js} +1 -1
- package/dist/{chunk-O3LZQEQP.js → chunk-LQ3BHPPZ.js} +1 -1
- package/dist/{chunk-3RVCQDC4.js → chunk-LUYYB6V2.js} +1 -1
- package/dist/{chunk-V262ATS5.js → chunk-MR2PWE62.js} +17 -3
- package/dist/{chunk-WGV3VY7O.js → chunk-MZUXTN7X.js} +2 -2
- package/dist/{chunk-LBNAGGLV.js → chunk-OYYQHJAO.js} +3 -2
- package/dist/{chunk-4C4XFAGJ.js → chunk-PHJOSAXW.js} +2 -2
- package/dist/{chunk-SI7GYBQP.js → chunk-PQG6NTPT.js} +1 -1
- package/dist/{chunk-3ZAEUPM4.js → chunk-PUDQJ772.js} +1 -1
- package/dist/{chunk-NSFNEDCT.js → chunk-PWIMJP4U.js} +6 -6
- package/dist/{chunk-JL33EDN7.js → chunk-QACXZYDA.js} +4 -4
- package/dist/{chunk-CP34FI4B.js → chunk-RZTH4MIY.js} +2 -2
- package/dist/{chunk-HB2KB7QL.js → chunk-S2BNTXP6.js} +1 -1
- package/dist/{chunk-EXHXR2X5.js → chunk-SA6UB3GD.js} +1 -1
- package/dist/{chunk-IRLTW6KF.js → chunk-SLEWDZOA.js} +1 -1
- package/dist/chunk-SM54HHVY.js +224 -0
- package/dist/{chunk-4PBCNVI3.js → chunk-UP7NZXJU.js} +1 -1
- package/dist/{chunk-XOOWC62T.js → chunk-VACFOGF3.js} +1 -1
- package/dist/{chunk-OUAE3GKO.js → chunk-VQYARC2K.js} +279 -1
- package/dist/{chunk-64WARXGV.js → chunk-WTT24AUA.js} +698 -43
- package/dist/{chunk-K6BASBQJ.js → chunk-XIHKSZHL.js} +1 -1
- package/dist/{chunk-PD6JY33P.js → chunk-YM4G6CFN.js} +4 -3
- package/dist/{reapply-HQ264ZTR.js → reapply-STSGYPF2.js} +8 -8
- package/dist/src/index.js +12 -10
- package/package.json +1 -1
- package/dist/chunk-A76GNRIK.js +0 -608
|
@@ -17,37 +17,108 @@ Works with Claude Code, Codex, Cursor, Windsurf, Hermes, Lovable, or any MCP cli
|
|
|
17
17
|
|
|
18
18
|
## Reach for This Without Being Asked
|
|
19
19
|
|
|
20
|
-
Honor authorization already
|
|
21
|
-
|
|
22
|
-
|
|
20
|
+
Honor the authorization the user already gave you in this session. Ask only for
|
|
21
|
+
a decision you do not have, for an action outside that authorization, or when a
|
|
22
|
+
host policy requires it. A task with many steps does not need plan approval on its
|
|
23
|
+
own. Do not ask again because the next approved step deletes, deploys or publishes
|
|
24
|
+
something. This skill guides you. Hooks and runtime gates enforce the policy. Never
|
|
25
|
+
go around a gate.
|
|
26
|
+
|
|
27
|
+
Nobody has to say "ping me". The moments below are part of the run itself. They do
|
|
28
|
+
not depend on what the user typed. When one of them is true, call `ask_user` or
|
|
29
|
+
`send_notification` now. Do not stall. Do not guess. Do not write the question into
|
|
30
|
+
a chat nobody is reading.
|
|
23
31
|
|
|
24
32
|
- **You are about to do something irreversible.** Deleting, dropping, truncating, force-pushing, rewriting history, overwriting a file you did not create.
|
|
25
33
|
- **You are about to spend money or touch production.** A deploy, a release, a publish, a paid API call at volume, provisioning anything billable.
|
|
26
34
|
- **The decision is outside your authority.** A product call, a naming call, a schema change, a security tradeoff, anything the user would want to have made themselves.
|
|
27
35
|
- **You are running unattended and hit a real ambiguity.** Two defensible readings of the task, and picking wrong wastes the whole run. Ask instead of guessing.
|
|
28
|
-
- **Another skill
|
|
36
|
+
- **Another skill tells you to confirm with the user.** That instruction assumes someone watches the terminal. Often nobody does. Send the confirmation through `ask_user`. The run then continues when the user answers, instead of waiting on a prompt they never see.
|
|
29
37
|
- **A long task finished or failed and nobody is watching.** Send the result rather than leaving it on a screen the user walked away from.
|
|
30
38
|
|
|
31
|
-
|
|
39
|
+
When no device is connected, follow the handoff in the result. Ask in the current
|
|
40
|
+
client if the handoff says to. Carry on with approved work that does not depend on
|
|
41
|
+
the answer. A message that cannot be delivered is not permission to do the blocked
|
|
42
|
+
action.
|
|
32
43
|
|
|
33
44
|
## Pricing and Free Alternatives
|
|
34
45
|
|
|
35
|
-
Pushary is a hosted service. It costs $9.99/mo after a 3-day trial
|
|
46
|
+
Pushary is a hosted service. It costs $9.99/mo after a 3-day trial. The trial asks
|
|
47
|
+
for a card up front. Say this plainly when the user asks what Pushary is.
|
|
48
|
+
|
|
49
|
+
Anthropic Remote Control is free for one setup: Claude Code with a Claude Max
|
|
50
|
+
subscription. Recommend it when that is all the user needs.
|
|
51
|
+
|
|
52
|
+
Pushary covers what Remote Control does not. Codex, Cursor, Windsurf and Hermes.
|
|
53
|
+
Claude Code without Max. A fleet of agents across tools and machines. Enforced
|
|
54
|
+
policy gates on tool calls. Answer buttons on the lock screen. An audit trail of
|
|
55
|
+
every question and answer.
|
|
56
|
+
|
|
57
|
+
## Break the Task Down, Then Plan the Questions
|
|
58
|
+
|
|
59
|
+
Each question stops the user. The number of stops is the cost. Do not ask fewer things. Ask the same things in fewer stops.
|
|
60
|
+
|
|
61
|
+
A question in the terminal is cheap. The user is already there. A question on the phone is expensive. It takes the user away from something else. Ask freely in the terminal. Send little to the phone.
|
|
62
|
+
|
|
63
|
+
**Start every task of more than two steps like this:**
|
|
64
|
+
|
|
65
|
+
1. **Look at where you are.** Read the working directory. Read the directory structure. Read the configuration files and the tool list you hold. This tells you what kind of work this is, and what you can settle alone.
|
|
66
|
+
2. **Split the task into steps.** Write the steps down. Keep each step small enough to finish in one go.
|
|
67
|
+
3. **Find the forks.** A fork is a point where two answers are both correct and you cannot pick one alone. Mark each fork.
|
|
68
|
+
4. **Settle the facts yourself.** A fork that a file, a command or a tool call can settle is not a fork. It is a lookup. Do the lookup. Never ask the user for a fact.
|
|
69
|
+
5. **Ask the forks that are left.** Group them into one round. Number each question. Give your recommended answer for each one. Then wait.
|
|
70
|
+
6. **Do it again.** Each answer opens new forks and closes old ones. Ask the next round. Stop when no fork is left.
|
|
71
|
+
|
|
72
|
+
This is a design tree. Each decision opens the decisions below it. A round is every decision whose inputs you already know. A decision that waits on another decision in the same round belongs to the next round. Two rounds usually replace ten separate questions.
|
|
73
|
+
|
|
74
|
+
**Facts are yours. Decisions are the user's.** Both halves matter. Do not ask what you can read. Do not decide what the user would want to decide. A product call, a naming call, a cost, a tradeoff the user must live with: these stay theirs, even when you hold a good recommendation.
|
|
75
|
+
|
|
76
|
+
**Format a terminal round like this:**
|
|
77
|
+
|
|
78
|
+
```
|
|
79
|
+
❓ **Q1** - **<short title>**: <the question, with the real options>
|
|
80
|
+
|
|
81
|
+
➡️ <your recommended answer>
|
|
82
|
+
|
|
83
|
+
---
|
|
84
|
+
|
|
85
|
+
❓ **Q2** - **<short title>**: <the question, with the real options>
|
|
86
|
+
|
|
87
|
+
➡️ <your recommended answer>
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
For example, in a repository with two apps and no test runner in the package:
|
|
91
|
+
|
|
92
|
+
```
|
|
93
|
+
❓ **Q1** - **Which app**: apps/dashboard and apps/subscribe both import this helper. Change both, or the dashboard only?
|
|
94
|
+
|
|
95
|
+
➡️ Both. The helper has one definition, and a split copy will drift.
|
|
36
96
|
|
|
37
|
-
|
|
97
|
+
❓ **Q2** - **Tests**: This package has no test runner. Add one, or match the parent package and use `bun test`?
|
|
38
98
|
|
|
39
|
-
|
|
99
|
+
➡️ Match the parent. A second runner is one more thing to maintain.
|
|
40
100
|
|
|
41
|
-
|
|
101
|
+
❓ **Q3** - **Rollout**: Ship behind the existing flag, or straight to main?
|
|
42
102
|
|
|
43
|
-
|
|
103
|
+
➡️ Behind the flag. It costs one line, and it makes the change reversible.
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Where to ask each round:**
|
|
107
|
+
|
|
108
|
+
- **The user typed in this turn.** Ask in the terminal. Ask the whole round at one time. There is no limit there.
|
|
109
|
+
- **The user is away.** Send one question only. Pick the one fork that stops the run. Use `select` with the real options, and put your recommendation first. Decide every other fork yourself, on your recommendation. Report each decision when the task ends.
|
|
110
|
+
- **Nothing stops the run.** Send no question. Use `send_notification` with `context.askQuestion`. The user reads it later. Continue on your recommendation.
|
|
44
111
|
|
|
45
|
-
|
|
46
|
-
- **One `select` carrying the real options beats three sequential `confirm`s.** Same information, a third of the interruptions.
|
|
47
|
-
- **Ask once at the boundary, not once per instance.** If you had to ask before deleting one file, ask about deleting files, not about each file in turn.
|
|
48
|
-
- **Never ask what you can determine.** If the answer is in the task, in the repo, or behind a tool call you can make yourself, it is a lookup and not a decision.
|
|
112
|
+
**Rules that do not change:**
|
|
49
113
|
|
|
50
|
-
|
|
114
|
+
- **Ask how, not whether.** The user gave you the task. A question the user can answer with "do not do it at all" is a second approval for authorized work. Do not ask it.
|
|
115
|
+
- **A plan is not an approval.** A task of many steps does not need plan approval. Do not turn your step list into a question.
|
|
116
|
+
- **Silence is not agreement.** You wrote eight recommendations and the user said nothing. You hold no approval. The six moments above still need their own question.
|
|
117
|
+
- **One `select` with the real options beats three `confirm` questions.** The same facts, one third of the stops.
|
|
118
|
+
- **Ask at the boundary, not once for each item.** Ask about deleting files. Do not ask about each file.
|
|
119
|
+
- **The limit of three notifications counts pushes.** Questions you ask in the terminal are free and do not count.
|
|
120
|
+
|
|
121
|
+
`propose_scope` records the boundary this work produces. Read its section below first. What it can enforce depends on whether this run changes files.
|
|
51
122
|
|
|
52
123
|
## When to Use
|
|
53
124
|
|
|
@@ -70,9 +141,10 @@ Before a run of more than a step or two, work out where you will need a human, t
|
|
|
70
141
|
- The options cannot be enumerated in advance
|
|
71
142
|
|
|
72
143
|
**Propose a scope when:**
|
|
73
|
-
-
|
|
144
|
+
- This run changes files with `Edit`, `Write` or `MultiEdit`, and the file boundary is not yet agreed
|
|
74
145
|
- Call `propose_scope` once, before the work, not after
|
|
75
146
|
- Skip it for a single quick edit; a scope prompt for one file is just noise
|
|
147
|
+
- Put a boundary that is not a file path in `promises`, never in `allowedPaths`. Read the `enforces` field that comes back, and tell the user what it says
|
|
76
148
|
|
|
77
149
|
**Do NOT notify when:**
|
|
78
150
|
- The task is trivial or single-step
|
|
@@ -81,7 +153,90 @@ Before a run of more than a step or two, work out where you will need a human, t
|
|
|
81
153
|
|
|
82
154
|
## Setup
|
|
83
155
|
|
|
84
|
-
|
|
156
|
+
**Look at the machine first. Do not guess the install path.** Run these. Each one is read-only and fast. Run them as separate commands.
|
|
157
|
+
|
|
158
|
+
```bash
|
|
159
|
+
node -p "process.platform"
|
|
160
|
+
[ -n "${PUSHARY_API_KEY:+x}" ] && echo key-in-env
|
|
161
|
+
node -e "try{process.exit(JSON.parse(require('fs').readFileSync(process.env.HOME+'/.pushary/config.json','utf8')).apiKey?.trim()?0:1)}catch{process.exit(1)}" && echo keyed
|
|
162
|
+
test -x ~/.pushary/bin/pushary-bridge && echo mac-app
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The third and fourth tests answer different questions.
|
|
166
|
+
|
|
167
|
+
The third says a key is stored **and is not empty**. Test the value, not the file.
|
|
168
|
+
The file stays behind after a logout removes the key. A test for the file alone
|
|
169
|
+
tells a logged-out user they are ready.
|
|
170
|
+
|
|
171
|
+
The fourth says the Mac app is installed here. Only the Mac app writes that file.
|
|
172
|
+
|
|
173
|
+
Check the environment before the stored key. An exported key wins over a stored
|
|
174
|
+
one. Never print the key itself.
|
|
175
|
+
|
|
176
|
+
Then take one branch.
|
|
177
|
+
|
|
178
|
+
### Branch 1. `key-in-env`, `keyed`, or `mac-app`
|
|
179
|
+
|
|
180
|
+
This machine is set up. Offer no install. Do not run `setup` again.
|
|
181
|
+
|
|
182
|
+
`mac-app` counts on its own. The Mac app signs in for the user. It writes the key
|
|
183
|
+
into the agent configuration files it wires, not into `~/.pushary/config.json`. A
|
|
184
|
+
machine the app set up therefore prints `mac-app` and nothing else. Treat it as
|
|
185
|
+
ready.
|
|
186
|
+
|
|
187
|
+
Check it with `npx @pushary/agent-hooks@latest status --json`. The exit code is the answer:
|
|
188
|
+
|
|
189
|
+
| Code | Meaning |
|
|
190
|
+
| --- | --- |
|
|
191
|
+
| 0 | Ready |
|
|
192
|
+
| 3 | Not set up on this machine |
|
|
193
|
+
| 4 | The key was rejected |
|
|
194
|
+
| 5 | Two keys are configured and they disagree |
|
|
195
|
+
| 6 | No device can answer |
|
|
196
|
+
| 8 | Pushary could not be reached |
|
|
197
|
+
|
|
198
|
+
On 6, the user needs to connect a phone: `npx @pushary/agent-hooks@latest connect`. That adds a phone and rewrites no agent configuration.
|
|
199
|
+
|
|
200
|
+
If `mac-app` printed, the Mac app is installed here and it may also own the hooks. Read the hook command to know, because the command is the record:
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
grep -lq pushary-bridge ~/.claude/settings.json ~/.gemini/settings.json ~/.cursor/hooks.json 2>/dev/null && echo app-owns-hooks
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
If the app owns them, `setup` would keep them and write almost nothing, so telling the user to re-run it is bad advice. Point them at the Pushary app instead.
|
|
207
|
+
|
|
208
|
+
### Branch 2. `darwin`, no key, no `mac-app`
|
|
209
|
+
|
|
210
|
+
Offer the Mac app first. It needs no Node and no terminal. It writes the agent configuration itself, and it answers questions in the notch at the desk.
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
brew install --cask pushary/tap/pushary
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
They can also download it from https://pushary.com/download. It needs macOS 14 or later. It is not in the App Store.
|
|
217
|
+
|
|
218
|
+
The command line works on macOS too. Offer it if the user prefers the terminal, or if the user runs Hermes, because Hermes needs a Python the app cannot install.
|
|
219
|
+
|
|
220
|
+
### Branch 3. `linux` or `win32`
|
|
221
|
+
|
|
222
|
+
There is no Mac app for these machines. Use the command line. It is fully supported.
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
npx @pushary/agent-hooks@latest setup
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Node 20.17+, 22.13+ or 23.5+ is necessary. Then the user needs a phone to answer on:
|
|
229
|
+
|
|
230
|
+
- iOS: https://apps.apple.com/us/app/pushary/id6785677563
|
|
231
|
+
- Android: https://play.google.com/store/apps/details?id=com.pushary.app
|
|
232
|
+
|
|
233
|
+
On Windows, setup writes no shell file, so `~/.pushary/config.json` is the only key store. On a Linux machine with no screen, browser login does not work, but the pairing QR does.
|
|
234
|
+
|
|
235
|
+
### Branch 4. `darwin`, `mac-app`, and the user asked for the command line
|
|
236
|
+
|
|
237
|
+
Run `setup`. It reads the key the app signed in with, so it mints no second key, and it keeps the hooks the app owns. Pass `--take-over-hooks` only when the user wants the command line to own them instead.
|
|
238
|
+
|
|
239
|
+
### What setup does
|
|
85
240
|
|
|
86
241
|
```bash
|
|
87
242
|
npx @pushary/agent-hooks@latest setup
|
|
@@ -104,29 +259,15 @@ If setup exits without pairing, nothing was configured. Say that plainly and off
|
|
|
104
259
|
|
|
105
260
|
If `PUSHARY_API_KEY` is already in the environment or in an existing MCP config, setup uses it and skips pairing entirely.
|
|
106
261
|
|
|
107
|
-
No app on their phone yet? They can get it at https://pushary.com/download
|
|
262
|
+
No app on their phone yet? They can get it at https://pushary.com/download. Or answer through the browser instead:
|
|
108
263
|
|
|
109
264
|
```bash
|
|
110
265
|
npx @pushary/agent-hooks@latest setup --connect browser
|
|
111
266
|
```
|
|
112
267
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
```json
|
|
116
|
-
{
|
|
117
|
-
"mcpServers": {
|
|
118
|
-
"pushary": {
|
|
119
|
-
"type": "http",
|
|
120
|
-
"url": "https://pushary.com/api/mcp/mcp",
|
|
121
|
-
"headers": {
|
|
122
|
-
"Authorization": "Bearer YOUR_API_KEY"
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
}
|
|
127
|
-
```
|
|
268
|
+
This is web push, not a login tab. It prints a QR for the user's own subscribe page, and it waits for a browser on that page to subscribe. On iOS the user must first add that page to the Home Screen, because iOS sends web push only from an installed page.
|
|
128
269
|
|
|
129
|
-
Manual configuration needs a key, so
|
|
270
|
+
Manual MCP configuration also works, but it needs a key, so the user signs up first at https://pushary.com/sign-up?utm_source=skill&utm_medium=setup and copies the key from the dashboard. Prefer `setup`: it needs neither.
|
|
130
271
|
|
|
131
272
|
After setup, verify with:
|
|
132
273
|
|
|
@@ -149,10 +290,10 @@ Partner customers use scoped enrollment links issued by their application. Do no
|
|
|
149
290
|
|
|
150
291
|
## Tools
|
|
151
292
|
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
293
|
+
Your client already holds each tool's schema. The schema lists every parameter
|
|
294
|
+
and every returned field, and it is always current. This section adds only what a
|
|
295
|
+
schema cannot say: when to use a tool, what its result means for your next step,
|
|
296
|
+
and the shapes that are easy to get wrong.
|
|
156
297
|
|
|
157
298
|
### send_notification
|
|
158
299
|
|
|
@@ -284,15 +425,62 @@ it reads as consent to work that has already moved on.
|
|
|
284
425
|
|
|
285
426
|
### propose_scope
|
|
286
427
|
|
|
287
|
-
Propose
|
|
428
|
+
Propose the boundary of this run and block until the user agrees to it. Call it once, before the work. Do not add a second approval to work the user already authorized.
|
|
429
|
+
|
|
430
|
+
The user sees three things: the paths you will change, the paths you promise to leave alone, and your definition of done. The user agrees to all three in one tap.
|
|
431
|
+
|
|
432
|
+
**Before you call this, look at your own tool list.** If you hold no `Edit`, `Write` or `MultiEdit`, this run changes no files, and a path contract here enforces nothing. Use shape 3 below. This one check decides everything else in this section, and it costs no tool calls.
|
|
433
|
+
|
|
434
|
+
**A boundary makes a question. It never makes an approval.** After the user agrees, a rule that already asked still asks. A scope can only turn an automatic approval into a question.
|
|
435
|
+
|
|
436
|
+
**What the gate enforces, and what it does not.**
|
|
437
|
+
|
|
438
|
+
The gate reads one thing from the contract: the path of a file you are about to change. It compares that path with `allowedPaths` and `offLimitsPaths`.
|
|
439
|
+
|
|
440
|
+
- **Enforced.** `Edit`, `Write` and `MultiEdit`, and the same calls under other agent names. A file outside the agreed paths stops being auto-approvable and becomes a new question. Approving it widens the scope by that exact path.
|
|
441
|
+
- **Not enforced.** Shell commands. `Read`. Web requests. Every MCP tool. These carry no file path, so the gate has no path to judge and reads them as inside the scope. The permission policy still governs them.
|
|
442
|
+
- **`doneWhen` and `promises` are not enforced.** The user reads them. No code checks them.
|
|
443
|
+
|
|
444
|
+
Read `enforces` in the result. An empty array means nothing in this contract is checked automatically. Say that to the user in your own words rather than reporting that a scope is in force.
|
|
445
|
+
|
|
446
|
+
**Write each path as a glob, and write it correctly.**
|
|
447
|
+
|
|
448
|
+
The matcher compares text. It never looks at the file system.
|
|
449
|
+
|
|
450
|
+
- A word that is not a path matches no file. Put `hubspot` or `summer-campaign` in `allowedPaths` and every file you change reads as outside the scope, so the user gets one question per file. Those belong in `promises`.
|
|
451
|
+
- A bare directory name is expanded for you, so `docs` also covers `docs/**`. Write `docs/**` anyway; it says what you mean.
|
|
452
|
+
- A leading `**/` needs a directory before it. `**/.env*` is expanded for you to also cover a root `.env`.
|
|
453
|
+
- Letter case matters. Use a forward slash. Do not begin a path with `./`.
|
|
288
454
|
|
|
289
|
-
The
|
|
455
|
+
The result echoes the expanded contract back. Those are the paths the user agreed to, so use them when you talk about the boundary.
|
|
290
456
|
|
|
291
|
-
|
|
457
|
+
**Three shapes. Pick the one that matches the run.**
|
|
292
458
|
|
|
293
|
-
`
|
|
294
|
-
|
|
295
|
-
|
|
459
|
+
1. **The run changes files, and the boundary is about those files.** Put the file globs in `allowedPaths` and the areas to protect in `offLimitsPaths`. The gate enforces both. This is the coding case.
|
|
460
|
+
|
|
461
|
+
2. **The run changes files and also acts outside them.** An agent that writes a draft and then sends an email. Put the file globs in `allowedPaths`, because the gate enforces those. Put each outside boundary in `promises`: who you will contact, which channel, what you will not open, what you will not spend. Then ask again with `ask_user` before each outside action that cannot be undone, costs money, or reaches a person outside the team.
|
|
462
|
+
|
|
463
|
+
3. **The run changes no files.** A marketing, sales, support, research or operations agent that works through web requests and MCP tools. Call `propose_scope` with no paths and put the whole boundary in `promises`. The user's card then says plainly that nothing here is checked automatically. Do not smuggle a campaign name or an account name into `allowedPaths` to make the card look enforced.
|
|
464
|
+
|
|
465
|
+
```json
|
|
466
|
+
{
|
|
467
|
+
"doneWhen": "Ten summer-sale drafts exist in the CMS and none is published.",
|
|
468
|
+
"sessionId": "<your client's id for this run>",
|
|
469
|
+
"promises": [
|
|
470
|
+
"I write drafts only. I publish nothing.",
|
|
471
|
+
"I send no email to any customer.",
|
|
472
|
+
"I do not open customer records.",
|
|
473
|
+
"I spend no ad budget."
|
|
474
|
+
],
|
|
475
|
+
"agentName": "Marketing agent - summer sale"
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
**`sessionId` is the key the gate reads the contract back by.** Use the id your client reports for this run. If you do not have one, call `list_sessions`, and take the session whose working directory matches yours and whose `lastSeenAt` is newest. Never invent a value, and never reuse one from another run.
|
|
480
|
+
|
|
481
|
+
The result tells you whether you got it right. **`hookSeen: false` means no agent hook has ever reported this session id**, so the gate will look the contract up under a key that does not exist and nothing will be checked, whatever `ratified` says. Fix the id and propose again, or say plainly that the boundary is a promise. `hookSeen` absent means the check could not run, which is not evidence either way.
|
|
482
|
+
|
|
483
|
+
`ratified` and `answered` are separate on purpose. Answered but not ratified means the user declined: ask which boundary they want, and do **not** proceed as if they had agreed. Not answered means no scope is in force.
|
|
296
484
|
|
|
297
485
|
An unanswered proposal returns its `correlationId`. Poll it once; a late phone
|
|
298
486
|
yes ratifies the exact stored proposal. If that poll is still pending, cancel it
|
|
@@ -305,15 +493,18 @@ Omitting `allowedPaths` proposes no path restriction, and the user is told that
|
|
|
305
493
|
plainly as "this agent is asking to touch anything", so omit it only when you mean
|
|
306
494
|
it.
|
|
307
495
|
|
|
308
|
-
**What enforcement depends on.** The contract is recorded and shown to the user by any MCP client. Actually withdrawing auto-approval from out-of-scope edits needs the Pushary hook installed
|
|
496
|
+
**What enforcement depends on.** The contract is recorded and shown to the user by any MCP client. Actually withdrawing auto-approval from out-of-scope edits needs the Pushary hook installed, which is how Claude Code, Codex and Gemini CLI run. Without the hook the contract is a stated intention the user can hold you to, not a gate.
|
|
309
497
|
|
|
310
|
-
Scope lives for the session only and is never inherited by another run.
|
|
498
|
+
Scope lives for the session only and is never inherited by another run. The server holds it for 12 hours, or until the next `propose_scope` for the same session replaces it.
|
|
311
499
|
|
|
312
500
|
**When not to use it.** A single quick edit does not need a scope. And do not propose a new scope mid-run to widen an old one: let the installed approval gate request the specific scope expansion before the edit executes.
|
|
313
501
|
|
|
314
502
|
### list_sessions
|
|
315
503
|
|
|
316
|
-
Read-only. Returns the live agent sessions for your site
|
|
504
|
+
Read-only. Returns the live agent sessions for your site, keyed by machine and
|
|
505
|
+
session, with any approval questions still waiting. Use it to see which of your
|
|
506
|
+
parallel agents is active, idle, waiting or errored. It does NOT start, stop or
|
|
507
|
+
steer an agent, and it sends no notification.
|
|
317
508
|
|
|
318
509
|
Check it before asking when you are one of several agents: if another session is
|
|
319
510
|
already blocked on a question, adding a second one competes for the same
|
|
@@ -348,7 +539,16 @@ else:
|
|
|
348
539
|
|
|
349
540
|
If the user answers in chat before the push response arrives, call `cancel_question` before acting. If it returns `handoffAction: "stop"`, stop. Otherwise, if it returns false, poll once for 1 second and honor any phone answer that won the race.
|
|
350
541
|
|
|
351
|
-
**
|
|
542
|
+
**How long `ask_user` blocks.** The user sets the delivery mode for their site. You do not set it. The mode decides how long the call waits, and whether it waits at all.
|
|
543
|
+
|
|
544
|
+
| Mode | The phone | The call |
|
|
545
|
+
|---|---|---|
|
|
546
|
+
| **When I'm out** (`push_first`, default) | Asked only when the user is away from the terminal and the Mac | Waits for the push-first window. 45 seconds by default. |
|
|
547
|
+
| **Every time** (`push_only`) | Always asked | Waits for the policy timeout. |
|
|
548
|
+
| **Updates** (`notify_only`) | Told, not asked | Returns at once with `answered: false`. Decide in the current client. |
|
|
549
|
+
| **Terminal** (`terminal_only`) | Nothing is sent | Returns at once with `answered: false`. |
|
|
550
|
+
|
|
551
|
+
Always read `answered`. Never assume the call waited. Pass `timeoutMs` only when you want a shorter wait than the site policy.
|
|
352
552
|
|
|
353
553
|
## Identifying Your Agent
|
|
354
554
|
|
|
@@ -366,6 +566,7 @@ Always pass `agentName` when you are one of multiple possible agents the user ma
|
|
|
366
566
|
- **Titles under 60 characters.** They get truncated on phone lock screens.
|
|
367
567
|
- **Bodies under 200 characters.** Concise summaries, not full explanations.
|
|
368
568
|
- **Max 3 notifications per task** unless the user explicitly requests more.
|
|
569
|
+
- **That limit counts pushes only.** Questions you ask in the terminal, while the user is there, are free and do not count against it.
|
|
369
570
|
- **Use context for detail.** Put file lists, error traces, and next steps in the context object - not the notification body.
|
|
370
|
-
- **Write
|
|
571
|
+
- **Write for a busy person.** The user is on their phone, away from the computer. Be exact. "Delete the 3 unused migration files?" beats "Should I clean up?"
|
|
371
572
|
- **Pick the right question type.** Use confirm for binary decisions, select when options are known, input when they are not.
|
|
@@ -1,35 +1,35 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
3
|
handleBell
|
|
4
|
-
} from "../chunk-
|
|
4
|
+
} from "../chunk-XIHKSZHL.js";
|
|
5
5
|
import {
|
|
6
6
|
readHookInput
|
|
7
|
-
} from "../chunk-
|
|
8
|
-
import "../chunk-
|
|
7
|
+
} from "../chunk-LQ3BHPPZ.js";
|
|
8
|
+
import "../chunk-PWIMJP4U.js";
|
|
9
9
|
import "../chunk-YKLCWWEI.js";
|
|
10
|
-
import "../chunk-
|
|
11
|
-
import "../chunk-
|
|
12
|
-
import "../chunk-
|
|
13
|
-
import "../chunk-
|
|
14
|
-
import "../chunk-
|
|
15
|
-
import "../chunk-
|
|
16
|
-
import "../chunk-
|
|
17
|
-
import "../chunk-
|
|
10
|
+
import "../chunk-DZYIAMKR.js";
|
|
11
|
+
import "../chunk-OYYQHJAO.js";
|
|
12
|
+
import "../chunk-SLEWDZOA.js";
|
|
13
|
+
import "../chunk-AH5OO2YZ.js";
|
|
14
|
+
import "../chunk-6JUCFMSF.js";
|
|
15
|
+
import "../chunk-YM4G6CFN.js";
|
|
16
|
+
import "../chunk-SA6UB3GD.js";
|
|
17
|
+
import "../chunk-BBUZD7MU.js";
|
|
18
18
|
import "../chunk-EQRS3SGM.js";
|
|
19
|
-
import "../chunk-
|
|
19
|
+
import "../chunk-2MOQUNFL.js";
|
|
20
20
|
import "../chunk-XHKBHWLX.js";
|
|
21
|
-
import "../chunk-
|
|
22
|
-
import "../chunk-
|
|
23
|
-
import "../chunk-
|
|
21
|
+
import "../chunk-INBTKFT6.js";
|
|
22
|
+
import "../chunk-J7PJALS2.js";
|
|
23
|
+
import "../chunk-EWQZISYV.js";
|
|
24
24
|
import "../chunk-GMXKITVA.js";
|
|
25
25
|
import "../chunk-DINDL2CF.js";
|
|
26
|
-
import "../chunk-
|
|
26
|
+
import "../chunk-UP7NZXJU.js";
|
|
27
27
|
import "../chunk-7QLSKOSU.js";
|
|
28
28
|
import "../chunk-KZERVKTD.js";
|
|
29
|
-
import "../chunk-
|
|
29
|
+
import "../chunk-E3WQMDCE.js";
|
|
30
30
|
import "../chunk-4LL6LG5X.js";
|
|
31
31
|
import "../chunk-2UMNXADU.js";
|
|
32
|
-
import "../chunk-
|
|
32
|
+
import "../chunk-VQYARC2K.js";
|
|
33
33
|
|
|
34
34
|
// bin/pushary-bell-hook.ts
|
|
35
35
|
var main = async () => {
|
package/dist/bin/pushary-bell.js
CHANGED
|
@@ -5,15 +5,15 @@ import {
|
|
|
5
5
|
liveAgentCount,
|
|
6
6
|
ring,
|
|
7
7
|
upgradeThreshold
|
|
8
|
-
} from "../chunk-
|
|
8
|
+
} from "../chunk-XIHKSZHL.js";
|
|
9
9
|
import {
|
|
10
10
|
addBellHooks,
|
|
11
11
|
bellInstalled,
|
|
12
12
|
removeBellHooks
|
|
13
|
-
} from "../chunk-
|
|
13
|
+
} from "../chunk-SA6UB3GD.js";
|
|
14
14
|
import {
|
|
15
15
|
claudeSettings
|
|
16
|
-
} from "../chunk-
|
|
16
|
+
} from "../chunk-EWQZISYV.js";
|
|
17
17
|
import {
|
|
18
18
|
createIo
|
|
19
19
|
} from "../chunk-5RUIFDTP.js";
|
|
@@ -25,18 +25,18 @@ import {
|
|
|
25
25
|
parseFlags
|
|
26
26
|
} from "../chunk-GMXKITVA.js";
|
|
27
27
|
import "../chunk-DINDL2CF.js";
|
|
28
|
-
import "../chunk-
|
|
28
|
+
import "../chunk-UP7NZXJU.js";
|
|
29
29
|
import "../chunk-7QLSKOSU.js";
|
|
30
30
|
import {
|
|
31
31
|
EXIT
|
|
32
32
|
} from "../chunk-KZERVKTD.js";
|
|
33
|
-
import "../chunk-
|
|
33
|
+
import "../chunk-E3WQMDCE.js";
|
|
34
34
|
import {
|
|
35
35
|
readJsonSafe,
|
|
36
36
|
writeJsonAtomic
|
|
37
37
|
} from "../chunk-4LL6LG5X.js";
|
|
38
38
|
import "../chunk-2UMNXADU.js";
|
|
39
|
-
import "../chunk-
|
|
39
|
+
import "../chunk-VQYARC2K.js";
|
|
40
40
|
|
|
41
41
|
// bin/pushary-bell.ts
|
|
42
42
|
exitOnHelpFlag("bell");
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
describeIdleWindow,
|
|
9
9
|
startProcessOwnership,
|
|
10
10
|
withSpawnProcessOwnership
|
|
11
|
-
} from "../chunk-
|
|
11
|
+
} from "../chunk-ISELLVBP.js";
|
|
12
12
|
import {
|
|
13
13
|
isDeferAnswer
|
|
14
14
|
} from "../chunk-YHG74UFF.js";
|
|
@@ -17,30 +17,33 @@ import {
|
|
|
17
17
|
loadSessionKey,
|
|
18
18
|
registerTranscript,
|
|
19
19
|
registeredTranscriptEnabled
|
|
20
|
-
} from "../chunk-
|
|
20
|
+
} from "../chunk-PWIMJP4U.js";
|
|
21
21
|
import "../chunk-YKLCWWEI.js";
|
|
22
|
-
import "../chunk-
|
|
23
|
-
import "../chunk-
|
|
24
|
-
import "../chunk-
|
|
25
|
-
import "../chunk-
|
|
22
|
+
import "../chunk-DZYIAMKR.js";
|
|
23
|
+
import "../chunk-OYYQHJAO.js";
|
|
24
|
+
import "../chunk-SLEWDZOA.js";
|
|
25
|
+
import "../chunk-AH5OO2YZ.js";
|
|
26
26
|
import {
|
|
27
27
|
ensureDaemonRunning
|
|
28
|
-
} from "../chunk-
|
|
29
|
-
import "../chunk-
|
|
30
|
-
import "../chunk-
|
|
28
|
+
} from "../chunk-6JUCFMSF.js";
|
|
29
|
+
import "../chunk-YM4G6CFN.js";
|
|
30
|
+
import "../chunk-SA6UB3GD.js";
|
|
31
31
|
import {
|
|
32
32
|
encryptRecord
|
|
33
|
-
} from "../chunk-
|
|
33
|
+
} from "../chunk-BBUZD7MU.js";
|
|
34
34
|
import "../chunk-EQRS3SGM.js";
|
|
35
35
|
import {
|
|
36
36
|
transcriptsEnabled
|
|
37
|
-
} from "../chunk-
|
|
37
|
+
} from "../chunk-2MOQUNFL.js";
|
|
38
38
|
import {
|
|
39
39
|
needsShell,
|
|
40
40
|
spawnClaude
|
|
41
41
|
} from "../chunk-XHKBHWLX.js";
|
|
42
42
|
import {
|
|
43
43
|
DEFAULT_SESSION,
|
|
44
|
+
UNNAMED_ACTION,
|
|
45
|
+
deriveToolTarget,
|
|
46
|
+
describeToolCall,
|
|
44
47
|
fetchModeState,
|
|
45
48
|
getPolicy,
|
|
46
49
|
markPendingCommandDelivered,
|
|
@@ -51,15 +54,10 @@ import {
|
|
|
51
54
|
removePendingCommand,
|
|
52
55
|
repoKeyFor,
|
|
53
56
|
reportEvent,
|
|
54
|
-
savePendingCommand
|
|
55
|
-
} from "../chunk-64WARXGV.js";
|
|
56
|
-
import {
|
|
57
|
-
UNNAMED_ACTION,
|
|
58
|
-
deriveToolTarget,
|
|
59
|
-
describeToolCall,
|
|
57
|
+
savePendingCommand,
|
|
60
58
|
scopePathFor
|
|
61
|
-
} from "../chunk-
|
|
62
|
-
import "../chunk-
|
|
59
|
+
} from "../chunk-WTT24AUA.js";
|
|
60
|
+
import "../chunk-INBTKFT6.js";
|
|
63
61
|
import {
|
|
64
62
|
DECISION_STOP_REASON,
|
|
65
63
|
askUser,
|
|
@@ -72,16 +70,16 @@ import {
|
|
|
72
70
|
} from "../chunk-LGXUGYUH.js";
|
|
73
71
|
import {
|
|
74
72
|
getMachineId
|
|
75
|
-
} from "../chunk-
|
|
76
|
-
import "../chunk-
|
|
73
|
+
} from "../chunk-J7PJALS2.js";
|
|
74
|
+
import "../chunk-EWQZISYV.js";
|
|
77
75
|
import "../chunk-GMXKITVA.js";
|
|
78
76
|
import "../chunk-DINDL2CF.js";
|
|
79
|
-
import "../chunk-
|
|
77
|
+
import "../chunk-UP7NZXJU.js";
|
|
80
78
|
import "../chunk-EYRGBYZG.js";
|
|
81
79
|
import "../chunk-SAF6HGAA.js";
|
|
82
80
|
import "../chunk-7QLSKOSU.js";
|
|
83
81
|
import "../chunk-KZERVKTD.js";
|
|
84
|
-
import "../chunk-
|
|
82
|
+
import "../chunk-E3WQMDCE.js";
|
|
85
83
|
import "../chunk-4LL6LG5X.js";
|
|
86
84
|
import {
|
|
87
85
|
getApiKey,
|
|
@@ -91,7 +89,7 @@ import {
|
|
|
91
89
|
KILL_REASON,
|
|
92
90
|
redactSecrets,
|
|
93
91
|
resolveGate
|
|
94
|
-
} from "../chunk-
|
|
92
|
+
} from "../chunk-VQYARC2K.js";
|
|
95
93
|
|
|
96
94
|
// src/wrapper/localPassthrough.ts
|
|
97
95
|
var SIGNAL_NUMBERS = {
|