@zeph-to/cli 2.3.0 → 2.4.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 +41 -5
- package/dist/gate.d.ts +11 -3
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +23 -4
- package/dist/remote-agents.d.ts +9 -3
- package/dist/remote-agents.d.ts.map +1 -1
- package/dist/remote-agents.js +14 -2
- package/dist/remote-hook.d.ts.map +1 -1
- package/dist/remote-hook.js +43 -36
- package/dist/zeph-core.generated.d.ts +3 -3
- package/dist/zeph-core.generated.d.ts.map +1 -1
- package/dist/zeph-core.generated.js +3 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
|
|
9
9
|
**Your agent works, hits a decision, and asks your phone. You tap a button (or type a reply), and the answer lands back in the live session — so the agent keeps going.**
|
|
10
10
|
|
|
11
|
-
`@zeph-to/cli` is the terminal side of that round trip: a zero-dependency push SDK, a `zeph` CLI that wires up 8 AI agents in one command, and a resident listener that lets your phone **drive
|
|
11
|
+
`@zeph-to/cli` is the terminal side of that round trip: a zero-dependency push SDK, a `zeph` CLI that wires up 8 AI agents in one command, and a resident listener that lets your phone **drive any agent session it can start** by typing straight into named tmux sessions.
|
|
12
12
|
|
|
13
13
|
<p align="center">
|
|
14
14
|
<img src="https://zeph.to/readme/demo.gif" alt="Agent asks 'Deploy to prod?' on your phone; you tap Deploy; the session ships" width="560"><br>
|
|
@@ -83,13 +83,13 @@ To **send** notifications:
|
|
|
83
83
|
zeph notify --title "Deploy done" --body "v2.1.0 shipped"
|
|
84
84
|
```
|
|
85
85
|
|
|
86
|
-
To **drive
|
|
86
|
+
To **drive an agent session from your phone**, see
|
|
87
87
|
[Remote Control](#remote-control) below.
|
|
88
88
|
|
|
89
89
|
## Remote Control
|
|
90
90
|
|
|
91
|
-
> Send messages from your phone *into* a live
|
|
92
|
-
>
|
|
91
|
+
> Send messages from your phone *into* a live agent session — even after
|
|
92
|
+
> a `zeph_ask` polling window has expired.
|
|
93
93
|
|
|
94
94
|
<p align="center">
|
|
95
95
|
<img src="https://zeph.to/readme/ask-phone.png" alt="A Zeph hook on the phone: a question with tappable answer buttons and a text field" width="300">
|
|
@@ -116,7 +116,7 @@ tmux session via `tmux send-keys`.
|
|
|
116
116
|
[zeph listener — resident daemon, started by `zeph cc` automatically]
|
|
117
117
|
│ tmux send-keys -l -t zeph-myapp "리팩토링 마무리해줘" + Enter
|
|
118
118
|
▼
|
|
119
|
-
[tmux session "zeph-myapp" running claude / codex / cursor-agent / gemini]
|
|
119
|
+
[tmux session "zeph-myapp" running claude / codex / cursor-agent / gemini / hermes]
|
|
120
120
|
```
|
|
121
121
|
|
|
122
122
|
The listener polls its tmux session inventory every 5 seconds and
|
|
@@ -164,6 +164,26 @@ through. Same reason the `stop`-hook auto-push `zeph setup` installs
|
|
|
164
164
|
covers the Cursor **IDE** but not `zeph cursor` panes; ask for a
|
|
165
165
|
`zeph_notify` when you want one.
|
|
166
166
|
|
|
167
|
+
`zeph hermes` is in the same position, and asking for it works the same
|
|
168
|
+
way. `zeph install` does not wire Hermes either — add the MCP server to
|
|
169
|
+
`~/.hermes/config.yaml` by hand, as one entry under `mcp_servers`:
|
|
170
|
+
|
|
171
|
+
```yaml
|
|
172
|
+
mcp_servers:
|
|
173
|
+
zeph:
|
|
174
|
+
command: "npx"
|
|
175
|
+
args: ["-y", "@zeph-to/mcp-server"]
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
Add the entry; do not replace the file. `mcp_servers` is shared with
|
|
179
|
+
every other server you have configured, and nothing under `~/.hermes` is
|
|
180
|
+
in version control. Then `/reload-mcp` inside Hermes.
|
|
181
|
+
|
|
182
|
+
`zeph_ask` needs a hook id, which the MCP server reads from
|
|
183
|
+
`ZEPH_HOOK_ID` or from `hookId` in `~/.zeph/config.json` — `zeph install`
|
|
184
|
+
writes the latter, so a machine that has run it once needs no `env`
|
|
185
|
+
block here.
|
|
186
|
+
|
|
167
187
|
### Setup
|
|
168
188
|
|
|
169
189
|
1. **Install tmux.** The listener uses `send-keys`; the wrapper spawns
|
|
@@ -190,12 +210,27 @@ covers the Cursor **IDE** but not `zeph cursor` panes; ask for a
|
|
|
190
210
|
zeph codex # codex → tmux session "zeph-<project>"
|
|
191
211
|
zeph cursor # cursor-agent → tmux session "zeph-<project>"
|
|
192
212
|
zeph gemini # gemini → tmux session "zeph-<project>"
|
|
213
|
+
zeph hermes # hermes → tmux session "zeph-<project>"
|
|
193
214
|
```
|
|
194
215
|
|
|
195
216
|
`zeph cursor` runs **`cursor-agent`**, Cursor's terminal agent — a
|
|
196
217
|
separate install from the Cursor IDE (the bare `cursor` on your PATH
|
|
197
218
|
is the editor launcher, which exits immediately and can't be driven).
|
|
198
219
|
|
|
220
|
+
`zeph hermes` runs Hermes in its classic REPL, which is the default.
|
|
221
|
+
Launching it in TUI mode instead (`zeph hermes --tui`, or `HERMES_TUI=1`
|
|
222
|
+
in the environment) puts the pane on the terminal's alternate screen,
|
|
223
|
+
and the alternate screen has no scrollback — the live mirror on your
|
|
224
|
+
phone still draws, but scrolling up in it comes back empty.
|
|
225
|
+
|
|
226
|
+
If a Hermes session disappears from the phone's picker, the cause is
|
|
227
|
+
tmux having dropped the pane's `start_command`: all the listener sees
|
|
228
|
+
then is the interpreter Hermes execs (`python3.11` at time of writing),
|
|
229
|
+
which names no agent. `zeph listener` run in the foreground prints the
|
|
230
|
+
reason (`no agent in pane (start=…, current=python3.11)`). Kill the
|
|
231
|
+
session and start it again with `zeph hermes` to recover. `claude` sits
|
|
232
|
+
on the same edge for the same reason — it execs node.
|
|
233
|
+
|
|
199
234
|
The first `zeph cc` on a machine **auto-spawns a background
|
|
200
235
|
listener** (singleton, PID file at `~/.zeph/listener.pid`,
|
|
201
236
|
stdout/stderr at `~/.zeph/listener.log`). You never run
|
|
@@ -407,6 +442,7 @@ zeph cc # claude
|
|
|
407
442
|
zeph codex # codex
|
|
408
443
|
zeph cursor # cursor-agent (Cursor CLI, not the IDE)
|
|
409
444
|
zeph gemini # gemini
|
|
445
|
+
zeph hermes # hermes
|
|
410
446
|
|
|
411
447
|
# Run the resident listener (foreground; background it as you like)
|
|
412
448
|
zeph listener
|
package/dist/gate.d.ts
CHANGED
|
@@ -47,9 +47,11 @@ export declare const remoteDigest: (text: string) => string;
|
|
|
47
47
|
/**
|
|
48
48
|
* How long REMOTE stays live without a refresh. Generous on purpose: the state
|
|
49
49
|
* is refreshed on every phone prompt and every answered `zeph_ask`, so it only
|
|
50
|
-
* has to outlive a working session, never an idle user. It
|
|
51
|
-
*
|
|
52
|
-
*
|
|
50
|
+
* has to outlive a working session, never an idle user. It is the backstop and
|
|
51
|
+
* not the usual exit — a terminal-typed prompt, a Done-like answer and the
|
|
52
|
+
* model's `<!-- zeph: exit -->` all delete the file outright. What none of them
|
|
53
|
+
* covers is a session that simply died: there is no SessionEnd hook, so without
|
|
54
|
+
* a TTL a crash or Ctrl-C would latch REMOTE forever.
|
|
53
55
|
*/
|
|
54
56
|
export declare const REMOTE_TTL_SEC = 14400;
|
|
55
57
|
/** State path for a project hash: `<stateDir>/remote-active-<cksum(dir)>`. */
|
|
@@ -65,6 +67,12 @@ export declare const isRemoteActive: (dir: string, now?: () => number) => boolea
|
|
|
65
67
|
* hooks that call this must never fail a prompt over state IO.
|
|
66
68
|
*/
|
|
67
69
|
export declare const touchRemoteActive: (dir: string, now?: () => number) => void;
|
|
70
|
+
/**
|
|
71
|
+
* Leave REMOTE. Best-effort like its twin: an absent file is success, and the
|
|
72
|
+
* caller is a hook with its own job to finish. Bash twin: gate.sh
|
|
73
|
+
* zeph_remote_clear.
|
|
74
|
+
*/
|
|
75
|
+
export declare const clearRemoteActive: (dir: string) => void;
|
|
68
76
|
/** True when the user ran /zeph-mute for this project. */
|
|
69
77
|
export declare const isMuted: (dir: string) => boolean;
|
|
70
78
|
/**
|
package/dist/gate.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,UACS,CAAC;AAEpE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,YACR,CAAC;AAErD,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,KAAG,WAW7C,CAAC;AAeF,eAAO,MAAM,QAAQ,QAAO,MACoD,CAAC;AA2BjF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAWF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAA4C,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAG1B,CAAC;AAiBnB
|
|
1
|
+
{"version":3,"file":"gate.d.ts","sourceRoot":"","sources":["../src/gate.ts"],"names":[],"mappings":"AAyBA,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,GAAG,MAAM,CAAC;AAC3D,MAAM,MAAM,YAAY,GAAG,OAAO,GAAG,MAAM,GAAG,QAAQ,CAAC;AAEvD,MAAM,WAAW,SAAS;IACxB,uCAAuC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,qDAAqD;IACrD,gBAAgB,EAAE,MAAM,CAAC;IACzB,yDAAyD;IACzD,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,UAAU,CAAC;IACnB,QAAQ,EAAE,YAAY,CAAC;CACxB;AAED,MAAM,WAAW,WAAW;IAC1B,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,MAAM,GAAG,QAAQ,CAAC;CAC7B;AAED;;;;;;;;;;GAUG;AACH,eAAO,MAAM,aAAa;;;;CAIhB,CAAC;AAEX,eAAO,MAAM,eAAe,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,UACS,CAAC;AAEpE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,GAAG,SAAS,KAAG,YACR,CAAC;AAErD,eAAO,MAAM,UAAU,GAAI,OAAO,SAAS,KAAG,WAW7C,CAAC;AAeF,eAAO,MAAM,QAAQ,QAAO,MACoD,CAAC;AA2BjF,eAAO,MAAM,WAAW,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAOlD,CAAC;AAWF,+EAA+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,MAAM,MAAM,KAAG,MAA4C,CAAC;AAE7F;;;;;GAKG;AACH,eAAO,MAAM,YAAY,GAAI,MAAM,MAAM,KAAG,MAG1B,CAAC;AAiBnB;;;;;;;;GAQG;AACH,eAAO,MAAM,cAAc,QAAQ,CAAC;AAEpC,8EAA8E;AAC9E,eAAO,MAAM,eAAe,GAAI,MAAM,MAAM,KAAG,MACJ,CAAC;AAE5C;;;;GAIG;AACH,eAAO,MAAM,cAAc,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,OAsB1E,CAAC;AAEF;;;GAGG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,EAAE,MAAK,MAAM,MAAiB,KAAG,IAS7E,CAAC;AAEF;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,IAQ/C,CAAC;AAEF,0DAA0D;AAC1D,eAAO,MAAM,OAAO,GAAI,KAAK,MAAM,KAAG,OAGrC,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,gBAAgB,EAAE,YAAsB,CAAC;AAEtD;;;;GAIG;AACH,eAAO,MAAM,qBAAqB,qBAAqB,CAAC;AAExD;;;;;;;;;;;;;;GAcG;AACH,eAAO,MAAM,YAAY,GACvB,KAAK,MAAM,EACX,WAAU,YAA+B,KACxC,YAUF,CAAC;AAEF;;;;;;;;;;;GAWG;AACH,eAAO,MAAM,YAAY,GAAI,KAAK,MAAM,EAAE,MAAM,MAAM,GAAG,OAAO,GAAG,SAAS,KAAG,YACW,CAAC"}
|
package/dist/gate.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.autoPushMode = exports.readPushMode = exports.PUSHMODE_DEFAULT_FLAG = exports.PUSHMODE_DEFAULT = exports.isMuted = exports.touchRemoteActive = exports.isRemoteActive = exports.remoteStatePath = exports.REMOTE_TTL_SEC = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
3
|
+
exports.autoPushMode = exports.readPushMode = exports.PUSHMODE_DEFAULT_FLAG = exports.PUSHMODE_DEFAULT = exports.isMuted = exports.clearRemoteActive = exports.touchRemoteActive = exports.isRemoteActive = exports.remoteStatePath = exports.REMOTE_TTL_SEC = exports.remoteDigest = exports.remoteMarkerPath = exports.projectHash = exports.stateDir = exports.decidePush = exports.normalizePushMode = exports.normalizeMarker = exports.GATE_DEFAULTS = void 0;
|
|
4
4
|
/**
|
|
5
5
|
* Push-gate decision — the portable half of the Zeph Stop-hook logic.
|
|
6
6
|
*
|
|
@@ -149,9 +149,11 @@ exports.remoteDigest = remoteDigest;
|
|
|
149
149
|
/**
|
|
150
150
|
* How long REMOTE stays live without a refresh. Generous on purpose: the state
|
|
151
151
|
* is refreshed on every phone prompt and every answered `zeph_ask`, so it only
|
|
152
|
-
* has to outlive a working session, never an idle user. It
|
|
153
|
-
*
|
|
154
|
-
*
|
|
152
|
+
* has to outlive a working session, never an idle user. It is the backstop and
|
|
153
|
+
* not the usual exit — a terminal-typed prompt, a Done-like answer and the
|
|
154
|
+
* model's `<!-- zeph: exit -->` all delete the file outright. What none of them
|
|
155
|
+
* covers is a session that simply died: there is no SessionEnd hook, so without
|
|
156
|
+
* a TTL a crash or Ctrl-C would latch REMOTE forever.
|
|
155
157
|
*/
|
|
156
158
|
exports.REMOTE_TTL_SEC = 14400;
|
|
157
159
|
/** State path for a project hash: `<stateDir>/remote-active-<cksum(dir)>`. */
|
|
@@ -206,6 +208,23 @@ const touchRemoteActive = (dir, now = Date.now) => {
|
|
|
206
208
|
}
|
|
207
209
|
};
|
|
208
210
|
exports.touchRemoteActive = touchRemoteActive;
|
|
211
|
+
/**
|
|
212
|
+
* Leave REMOTE. Best-effort like its twin: an absent file is success, and the
|
|
213
|
+
* caller is a hook with its own job to finish. Bash twin: gate.sh
|
|
214
|
+
* zeph_remote_clear.
|
|
215
|
+
*/
|
|
216
|
+
const clearRemoteActive = (dir) => {
|
|
217
|
+
const hash = (0, exports.projectHash)(dir);
|
|
218
|
+
if (!hash)
|
|
219
|
+
return;
|
|
220
|
+
try {
|
|
221
|
+
(0, fs_1.unlinkSync)((0, exports.remoteStatePath)(hash));
|
|
222
|
+
}
|
|
223
|
+
catch {
|
|
224
|
+
/* already gone, or unwritable — the prompt still goes through */
|
|
225
|
+
}
|
|
226
|
+
};
|
|
227
|
+
exports.clearRemoteActive = clearRemoteActive;
|
|
209
228
|
/** True when the user ran /zeph-mute for this project. */
|
|
210
229
|
const isMuted = (dir) => {
|
|
211
230
|
const hash = (0, exports.projectHash)(dir);
|
package/dist/remote-agents.d.ts
CHANGED
|
@@ -23,8 +23,9 @@ export interface RemoteAgent {
|
|
|
23
23
|
/**
|
|
24
24
|
* Resolve the agent's own session id from the pane's cwd (+ pane pid
|
|
25
25
|
* when the caller knows it — enables exact process-tree matching).
|
|
26
|
-
* EXTENSION POINT:
|
|
27
|
-
*
|
|
26
|
+
* EXTENSION POINT: carried only by Claude Code. Every other row omits
|
|
27
|
+
* it until that agent's session-file format is confirmed — the listener
|
|
28
|
+
* then reports agentSessionId: null.
|
|
28
29
|
*/
|
|
29
30
|
resolveSessionId?: (paneCwd: string, panePid?: number) => string | null;
|
|
30
31
|
}
|
|
@@ -76,8 +77,13 @@ declare const REMOTE_AGENT_TABLE: readonly [{
|
|
|
76
77
|
readonly displayName: "Gemini CLI";
|
|
77
78
|
readonly binary: "gemini";
|
|
78
79
|
readonly subcommands: readonly ["gemini"];
|
|
80
|
+
}, {
|
|
81
|
+
readonly kind: "hermes";
|
|
82
|
+
readonly displayName: "Hermes";
|
|
83
|
+
readonly binary: "hermes";
|
|
84
|
+
readonly subcommands: readonly ["hermes"];
|
|
79
85
|
}];
|
|
80
|
-
/** Closed union of remote-controllable agent kinds
|
|
86
|
+
/** Closed union of remote-controllable agent kinds, derived from the table above. */
|
|
81
87
|
export type AgentKind = (typeof REMOTE_AGENT_TABLE)[number]['kind'];
|
|
82
88
|
/** A registry row: the uniform RemoteAgent shape with `kind` narrowed to the closed union. */
|
|
83
89
|
export type RegisteredRemoteAgent = RemoteAgent & {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IACxB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC
|
|
1
|
+
{"version":3,"file":"remote-agents.d.ts","sourceRoot":"","sources":["../src/remote-agents.ts"],"names":[],"mappings":"AAoBA,MAAM,WAAW,WAAW;IACxB,qEAAqE;IACrE,IAAI,EAAE,MAAM,CAAC;IACb,kCAAkC;IAClC,WAAW,EAAE,MAAM,CAAC;IACpB,2EAA2E;IAC3E,MAAM,EAAE,MAAM,CAAC;IACf,0DAA0D;IAC1D,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IAC/B;;;;;;;;;OASG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,2EAA2E;IAC3E,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;IACrC;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,MAAM,KAAK,MAAM,GAAG,IAAI,CAAC;CAC3E;AAyCD;;;;;;;GAOG;AACH,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,MAAM,GAAG,IAiB5D,CAAC;AAaF,UAAU,gBAAgB;IACtB,GAAG,EAAE,MAAM,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,GAAG,EAAE,MAAM,CAAC;CACf;AA4ED,4CAA4C;AAC5C,MAAM,WAAW,cAAc;IAC3B,OAAO,CAAC,EAAE,gBAAgB,EAAE,CAAC;IAC7B,WAAW,CAAC,EAAE,GAAG,CAAC,MAAM,CAAC,CAAC;CAC7B;AAED;;;;;GAKG;AACH,eAAO,MAAM,0BAA0B,GACnC,SAAS,MAAM,EACf,SAAS,MAAM,GAAG,IAAI,EACtB,OAAM,cAAmB,KAC1B,MAAM,GAAG,IAOX,CAAC;AAIF,QAAA,MAAM,kBAAkB;;;;;;;;;;;;;;;;;;;;;;;;;;;EA+CmB,CAAC;AAE5C,qFAAqF;AACrF,MAAM,MAAM,SAAS,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC,MAAM,CAAC,CAAC;AAEpE,8FAA8F;AAC9F,MAAM,MAAM,qBAAqB,GAAG,WAAW,GAAG;IAAE,IAAI,EAAE,SAAS,CAAA;CAAE,CAAC;AAEtE,eAAO,MAAM,aAAa,EAAE,SAAS,qBAAqB,EAAuB,CAAC;AAElF,eAAO,MAAM,qBAAqB,GAAI,KAAK,MAAM,KAAG,qBAAqB,GAAG,SAClB,CAAC;AAE3D,eAAO,MAAM,uBAAuB,GAAI,MAAM,MAAM,KAAG,qBAAqB,GAAG,SAG9E,CAAC"}
|
package/dist/remote-agents.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.matchAgentByPaneCommand = exports.findAgentBySubcommand = exports.REMOTE_AGENTS = exports.detectClaudeSessionIdByPid = exports.detectClaudeSessionId = void 0;
|
|
4
4
|
/**
|
|
5
|
-
* Remote-control agent registry — the single table behind
|
|
6
|
-
* `zeph
|
|
5
|
+
* Remote-control agent registry — the single table behind every
|
|
6
|
+
* `zeph <agent>` subcommand, the listener's pane matching, and the
|
|
7
7
|
* per-agent session-id enrichment. Adding a remote-controllable agent is
|
|
8
8
|
* one row here (plus, for a genuinely new kind, backend/phone support:
|
|
9
9
|
* `kind` is a wire contract — AgentSession.agentKind flows to the server
|
|
@@ -217,6 +217,18 @@ const REMOTE_AGENT_TABLE = [
|
|
|
217
217
|
binary: 'gemini',
|
|
218
218
|
subcommands: ['gemini'],
|
|
219
219
|
},
|
|
220
|
+
{
|
|
221
|
+
// `hermes` on PATH is a bash script that execs a venv Python, so the
|
|
222
|
+
// pane's *current* command reads the interpreter (`python3.11` as
|
|
223
|
+
// measured). Only `pane_start_command` identifies it, which is the
|
|
224
|
+
// same bargain `claude` (node) already makes. Deliberately no
|
|
225
|
+
// paneMatchAliases: matching interpreter names would adopt every
|
|
226
|
+
// Python REPL on the machine as a Hermes session.
|
|
227
|
+
kind: 'hermes',
|
|
228
|
+
displayName: 'Hermes',
|
|
229
|
+
binary: 'hermes',
|
|
230
|
+
subcommands: ['hermes'],
|
|
231
|
+
},
|
|
220
232
|
];
|
|
221
233
|
exports.REMOTE_AGENTS = REMOTE_AGENT_TABLE;
|
|
222
234
|
const findAgentBySubcommand = (cmd) => exports.REMOTE_AGENTS.find((a) => a.subcommands.includes(cmd));
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"remote-hook.d.ts","sourceRoot":"","sources":["../src/remote-hook.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"remote-hook.d.ts","sourceRoot":"","sources":["../src/remote-hook.ts"],"names":[],"mappings":"AAwCA;wEACwE;AACxE,eAAO,MAAM,kBAAkB,8BAA+B,CAAC;AAC/D,MAAM,MAAM,eAAe,GAAG,CAAC,OAAO,kBAAkB,CAAC,CAAC,MAAM,CAAC,CAAC;AAoClE,eAAO,MAAM,iBAAiB,GAAI,KAAK,MAAM,KAAG,GAAG,IAAI,eACE,CAAC;AAE1D;;;GAGG;AACH,eAAO,MAAM,aAAa,GACxB,OAAO,eAAe,EACtB,OAAO,MAAM,EACb,MAAK,MAAM,CAAC,UAAwB,EACpC,MAAK,MAAM,MAAiB,KAC3B,MAAM,GAAG,IAwCX,CAAC"}
|
package/dist/remote-hook.js
CHANGED
|
@@ -19,11 +19,13 @@ exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS =
|
|
|
19
19
|
* never false-match. Parity with the writer (listener.ts writeRemoteMarker)
|
|
20
20
|
* is by construction — both sides share gate.ts remoteDigest/remoteMarkerPath.
|
|
21
21
|
*
|
|
22
|
-
* The marker is one-shot, but REMOTE is not
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
22
|
+
* The marker is one-shot, but REMOTE is not: entry records the mode (gate.ts
|
|
23
|
+
* touchRemoteActive) so it outlives the entry turn and survives compaction.
|
|
24
|
+
* A prompt that reaches this hook without a marker was typed at the terminal —
|
|
25
|
+
* the only way text becomes a prompt without one is the user's own keyboard,
|
|
26
|
+
* since a phone answer to a zeph_ask comes back as a tool_result and never
|
|
27
|
+
* reaches a prompt hook. The user is back, so the mode ends here (gate.ts
|
|
28
|
+
* clearRemoteActive) and the hook says so once.
|
|
27
29
|
*
|
|
28
30
|
* No marker and no live state → null (silent no-op); this hook only ever adds
|
|
29
31
|
* context and must never block a prompt.
|
|
@@ -49,19 +51,19 @@ const HOOK_EVENT_NAME = {
|
|
|
49
51
|
const FRESH_WINDOW_SEC = 900;
|
|
50
52
|
const TWO_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
51
53
|
|
|
52
|
-
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match). The user is driving this session remotely and is NOT at the terminal. Enter sticky REMOTE mode now (CORE_RULES Rule 9): end EVERY response with \`zeph_ask\` (buttons + free-text) until the user
|
|
54
|
+
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match). The user is driving this session remotely and is NOT at the terminal. Enter sticky REMOTE mode now (CORE_RULES Rule 9): end EVERY response with \`zeph_ask\` (buttons + free-text) until the user exits — an exit signal (done/stop/exit), or a prompt they type at the terminal, which this hook will tell you about. Plain-text questions are invisible to them.`;
|
|
53
55
|
const ONE_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
54
56
|
|
|
55
57
|
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match), but ZEPH_HOOK_ID is not set, so two-way tools (zeph_ask/zeph_prompt/zeph_input) are unavailable. Make your final message self-contained — the completion push is the user's only feedback channel. If you have not already mentioned it this session, tell the user once that running \`npx @zeph-to/cli setup\` upgrades this into a two-way remote session (buttons + text replies from the phone).`;
|
|
56
58
|
/**
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
59
|
+
* The first prompt a remote session's user types at the terminal. Emitted
|
|
60
|
+
* once — the state is cleared with it, so every later terminal turn is a
|
|
61
|
+
* silent no-op. Kept in step with the wording in
|
|
62
|
+
* plugin/hooks/zeph-remote.sh.
|
|
61
63
|
*/
|
|
62
|
-
const
|
|
64
|
+
const EXIT_CONTEXT = `# System note (Zeph)
|
|
63
65
|
|
|
64
|
-
|
|
66
|
+
The user typed this prompt at the terminal, so this session has LEFT sticky REMOTE mode — answer normally (CORE_RULES Rule 4) and do not end this response with \`zeph_ask\` just to keep the loop alive. Rule 3 still holds: if you actually ask the user something, ask it with \`zeph_ask\`. Re-entry is automatic the moment they send another message from their phone.`;
|
|
65
67
|
const isRemoteHookAgent = (raw) => exports.REMOTE_HOOK_AGENTS.includes(raw);
|
|
66
68
|
exports.isRemoteHookAgent = isRemoteHookAgent;
|
|
67
69
|
/**
|
|
@@ -79,9 +81,7 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
79
81
|
catch {
|
|
80
82
|
return null;
|
|
81
83
|
}
|
|
82
|
-
// `
|
|
83
|
-
// gets its reminder either way. `cwd` keys every state file, so without it
|
|
84
|
-
// there is nothing to look up.
|
|
84
|
+
// `cwd` keys every state file, so without it there is nothing to look up.
|
|
85
85
|
if (!cwd)
|
|
86
86
|
return null;
|
|
87
87
|
// Mute outranks everything (Rule 12) — stay silent and leave both the marker
|
|
@@ -91,9 +91,11 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
91
91
|
const emit = (additionalContext) => JSON.stringify({
|
|
92
92
|
hookSpecificOutput: { hookEventName: HOOK_EVENT_NAME[agent], additionalContext },
|
|
93
93
|
});
|
|
94
|
-
// Exactly one additionalContext per invocation
|
|
95
|
-
//
|
|
96
|
-
|
|
94
|
+
// Exactly one additionalContext per invocation, and the verdict decides
|
|
95
|
+
// which: 'phone' enters, 'keyboard' leaves, 'unclear' says nothing and
|
|
96
|
+
// leaves the mode as it was.
|
|
97
|
+
const origin = remoteOrigin(prompt, cwd, now);
|
|
98
|
+
if (origin === 'phone') {
|
|
97
99
|
if (!env.ZEPH_HOOK_ID)
|
|
98
100
|
return emit(ONE_WAY_CONTEXT);
|
|
99
101
|
// Only a two-way session has a mode to stay in — without zeph_ask there is
|
|
@@ -101,35 +103,40 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
101
103
|
(0, gate_js_1.touchRemoteActive)(cwd, now);
|
|
102
104
|
return emit(TWO_WAY_CONTEXT);
|
|
103
105
|
}
|
|
104
|
-
if (env.ZEPH_HOOK_ID && (0, gate_js_1.isRemoteActive)(cwd, now))
|
|
105
|
-
|
|
106
|
+
if (origin === 'keyboard' && env.ZEPH_HOOK_ID && (0, gate_js_1.isRemoteActive)(cwd, now)) {
|
|
107
|
+
(0, gate_js_1.clearRemoteActive)(cwd);
|
|
108
|
+
return emit(EXIT_CONTEXT);
|
|
109
|
+
}
|
|
106
110
|
return null;
|
|
107
111
|
};
|
|
108
112
|
exports.runRemoteHook = runRemoteHook;
|
|
109
|
-
|
|
110
|
-
* True when this prompt is the phone injection the listener recorded.
|
|
111
|
-
* Consumes the marker on a match, deletes it once stale, and otherwise leaves
|
|
112
|
-
* it in place so a prompt that simply isn't the injected one can still match
|
|
113
|
-
* on a later turn.
|
|
114
|
-
*/
|
|
115
|
-
const matchesRemoteMarker = (prompt, cwd, now) => {
|
|
116
|
-
if (!prompt)
|
|
117
|
-
return false;
|
|
113
|
+
const remoteOrigin = (prompt, cwd, now) => {
|
|
118
114
|
const hash = (0, gate_js_1.projectHash)(cwd);
|
|
119
115
|
if (!hash)
|
|
120
|
-
return
|
|
116
|
+
return 'keyboard';
|
|
121
117
|
const marker = (0, gate_js_1.remoteMarkerPath)(hash);
|
|
122
118
|
let content;
|
|
123
119
|
try {
|
|
124
120
|
content = (0, fs_1.readFileSync)(marker, 'utf-8');
|
|
125
121
|
}
|
|
126
122
|
catch {
|
|
127
|
-
return
|
|
123
|
+
return 'keyboard';
|
|
128
124
|
}
|
|
125
|
+
if (!prompt)
|
|
126
|
+
return 'unclear';
|
|
129
127
|
// Marker format: "<epochSec> <sha256hex>\n" (listener.ts writeRemoteMarker).
|
|
130
128
|
const record = content.match(/^(\d+) ([0-9a-f]{64})\n?$/);
|
|
131
|
-
if (!record)
|
|
132
|
-
|
|
129
|
+
if (!record) {
|
|
130
|
+
// Junk can only ever be re-read and re-rejected — same housekeeping a
|
|
131
|
+
// stale marker gets. Twin: zeph-remote.sh's `rm -f` on the parse branches.
|
|
132
|
+
try {
|
|
133
|
+
(0, fs_1.unlinkSync)(marker);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* best-effort housekeeping */
|
|
137
|
+
}
|
|
138
|
+
return 'keyboard';
|
|
139
|
+
}
|
|
133
140
|
if (Math.floor(now() / 1000) - Number(record[1]) > FRESH_WINDOW_SEC) {
|
|
134
141
|
// Stale markers are dead weight (can never flag) — delete on sight.
|
|
135
142
|
try {
|
|
@@ -138,10 +145,10 @@ const matchesRemoteMarker = (prompt, cwd, now) => {
|
|
|
138
145
|
catch {
|
|
139
146
|
/* best-effort housekeeping */
|
|
140
147
|
}
|
|
141
|
-
return
|
|
148
|
+
return 'keyboard';
|
|
142
149
|
}
|
|
143
150
|
if ((0, gate_js_1.remoteDigest)(prompt) !== record[2])
|
|
144
|
-
return
|
|
151
|
+
return 'unclear';
|
|
145
152
|
// Matched — consume the marker so an identical later prompt (e.g. typed
|
|
146
153
|
// at the terminal) can't re-flag. Housekeeping is not the verdict: the
|
|
147
154
|
// match is valid whether or not the delete lands.
|
|
@@ -151,5 +158,5 @@ const matchesRemoteMarker = (prompt, cwd, now) => {
|
|
|
151
158
|
catch {
|
|
152
159
|
/* emit anyway — the match itself is valid */
|
|
153
160
|
}
|
|
154
|
-
return
|
|
161
|
+
return 'phone';
|
|
155
162
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
2
|
-
export declare const ZEPH_CORE_SOURCE_HASH = "
|
|
2
|
+
export declare const ZEPH_CORE_SOURCE_HASH = "d063dc8986f501725eea0d51d18be4ed2e16480e99dfba93f2a320dc630d5eea";
|
|
3
3
|
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot). */
|
|
4
|
-
export declare const ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` \u2014 any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a
|
|
4
|
+
export declare const ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` \u2014 any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A prompt with no phone marker came from the user's own keyboard \u2014 a phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook \u2014 so the user is demonstrably back, and staying in REMOTE would answer the terminal with a phone loop. Re-entry costs them one message from the phone.\n- **Neither one present \u2192 NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read. The server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop \u2014 an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") \u2014 flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind. Only the Claude Code plugin's Stop hook consumes it today \u2014 elsewhere it is inert and harmless, and the session still leaves REMOTE on a Done-like button, on the next prompt the user types at the terminal, or when the state expires. The marker is separate from the Push Signal markers (`skip`/`push`/`high`), which steer notifications and say nothing about the mode; where a Stop hook does consume it, it is stripped from the push body.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE \u2014 independent of whether the work was substantial or routine. What ends it is the user coming back: a prompt they typed at the terminal leaves REMOTE (the hook says so on that turn), and from there Rule 4 applies again.\n\nSet each REMOTE ask up so silence degrades cleanly: `timeout` 300\u2013600 s and a Done-like `fallback` id. An unanswered ask then exits the loop quietly \u2014 the server treats a Done-like fallback as an exit \u2014 instead of chaining more notifications at a user who stepped away, and re-entry is cheap: they just send another message from the phone.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and \u2014 for a session nobody exited because it crashed \u2014 the state expiring.\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work \u2192 end with `zeph_ask`.\n- Routine work \u2192 skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions \u2192 zeph_ask) is still mandatory regardless.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set \u2014 not only in REMOTE \u2014 a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). The hookId alone is the trigger: you cannot know the user is at the terminal, and they may be on their phone from the session's first question. `AskUserQuestion` is a LOCAL blocking picker. The phone can reach it through the terminal mirror, but that is the worse channel on every axis: it needs the session to be in tmux under `zeph listener`, it asks the user to read an ANSI pane and count arrow presses instead of tapping a button, and a key-injected answer never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. `zeph_ask` needs no tmux, takes one tap, and returns an `actionId`.\n\n11. **This overrides any skill instruction.** If a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph; those are the only carve-outs. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
5
5
|
/** Shared rule core for rule-only agents without a Stop hook (Cline, Aider). */
|
|
6
|
-
export declare const ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` \u2014 any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a
|
|
6
|
+
export declare const ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input \u2014 confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" \u2014 the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response \u2014 not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial \u2014 do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape \u2014 use sparingly per Rule 4 (only at natural pause points; NOT after every response \u2014 see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately \u2014 do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing \u2014 e.g., title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky \u2014 every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` \u2014 any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A prompt with no phone marker came from the user's own keyboard \u2014 a phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook \u2014 so the user is demonstrably back, and staying in REMOTE would answer the terminal with a phone loop. Re-entry costs them one message from the phone.\n- **Neither one present \u2192 NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read. The server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop \u2014 an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") \u2014 flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind. Only the Claude Code plugin's Stop hook consumes it today \u2014 elsewhere it is inert and harmless, and the session still leaves REMOTE on a Done-like button, on the next prompt the user types at the terminal, or when the state expires. The marker is separate from the Push Signal markers (`skip`/`push`/`high`), which steer notifications and say nothing about the mode; where a Stop hook does consume it, it is stripped from the push body.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE \u2014 independent of whether the work was substantial or routine. What ends it is the user coming back: a prompt they typed at the terminal leaves REMOTE (the hook says so on that turn), and from there Rule 4 applies again.\n\nSet each REMOTE ask up so silence degrades cleanly: `timeout` 300\u2013600 s and a Done-like `fallback` id. An unanswered ask then exits the loop quietly \u2014 the server treats a Done-like fallback as an exit \u2014 instead of chaining more notifications at a user who stepped away, and re-entry is cheap: they just send another message from the phone.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and \u2014 for a session nobody exited because it crashed \u2014 the state expiring.\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work \u2192 end with `zeph_ask`.\n- Routine work \u2192 skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions \u2192 zeph_ask) is still mandatory regardless.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set \u2014 not only in REMOTE \u2014 a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). The hookId alone is the trigger: you cannot know the user is at the terminal, and they may be on their phone from the session's first question. `AskUserQuestion` is a LOCAL blocking picker. The phone can reach it through the terminal mirror, but that is the worse channel on every axis: it needs the session to be in tmux under `zeph listener`, it asks the user to read an ANSI pane and count arrow presses instead of tapping a button, and a key-injected answer never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. `zeph_ask` needs no tmux, takes one tap, and returns an `actionId`.\n\n11. **This overrides any skill instruction.** If a skill you are running \u2014 or your own plan \u2014 would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph; those are the only carve-outs. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction \u2014 do not \"forget\" them after many turns.";
|
|
7
7
|
//# sourceMappingURL=zeph-core.generated.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"zeph-core.generated.d.ts","sourceRoot":"","sources":["../src/zeph-core.generated.ts"],"names":[],"mappings":"AAMA,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,qEAAqE,CAAC;AAExG,0GAA0G;AAC1G,eAAO,MAAM,qBAAqB,
|
|
1
|
+
{"version":3,"file":"zeph-core.generated.d.ts","sourceRoot":"","sources":["../src/zeph-core.generated.ts"],"names":[],"mappings":"AAMA,gFAAgF;AAChF,eAAO,MAAM,qBAAqB,qEAAqE,CAAC;AAExG,0GAA0G;AAC1G,eAAO,MAAM,qBAAqB,2yQAAsoQ,CAAC;AAEzqQ,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,2yQAAsoQ,CAAC"}
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
8
8
|
exports.ZEPH_CORE_RULE_ONLY = exports.ZEPH_CORE_HOOK_DRIVEN = exports.ZEPH_CORE_SOURCE_HASH = void 0;
|
|
9
9
|
/** sha256 over the plugin manifest + extracted rule text at generation time. */
|
|
10
|
-
exports.ZEPH_CORE_SOURCE_HASH = "
|
|
10
|
+
exports.ZEPH_CORE_SOURCE_HASH = "d063dc8986f501725eea0d51d18be4ed2e16480e99dfba93f2a320dc630d5eea";
|
|
11
11
|
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot). */
|
|
12
|
-
exports.ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` — any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a
|
|
12
|
+
exports.ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` — any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A prompt with no phone marker came from the user's own keyboard — a phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook — so the user is demonstrably back, and staying in REMOTE would answer the terminal with a phone loop. Re-entry costs them one message from the phone.\n- **Neither one present → NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read. The server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop — an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") — flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind. Only the Claude Code plugin's Stop hook consumes it today — elsewhere it is inert and harmless, and the session still leaves REMOTE on a Done-like button, on the next prompt the user types at the terminal, or when the state expires. The marker is separate from the Push Signal markers (`skip`/`push`/`high`), which steer notifications and say nothing about the mode; where a Stop hook does consume it, it is stripped from the push body.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE — independent of whether the work was substantial or routine. What ends it is the user coming back: a prompt they typed at the terminal leaves REMOTE (the hook says so on that turn), and from there Rule 4 applies again.\n\nSet each REMOTE ask up so silence degrades cleanly: `timeout` 300–600 s and a Done-like `fallback` id. An unanswered ask then exits the loop quietly — the server treats a Done-like fallback as an exit — instead of chaining more notifications at a user who stepped away, and re-entry is cheap: they just send another message from the phone.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and — for a session nobody exited because it crashed — the state expiring.\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work → end with `zeph_ask`.\n- Routine work → skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions → zeph_ask) is still mandatory regardless.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set — not only in REMOTE — a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). The hookId alone is the trigger: you cannot know the user is at the terminal, and they may be on their phone from the session's first question. `AskUserQuestion` is a LOCAL blocking picker. The phone can reach it through the terminal mirror, but that is the worse channel on every axis: it needs the session to be in tmux under `zeph listener`, it asks the user to read an ANSI pane and count arrow presses instead of tapping a button, and a key-injected answer never enters REMOTE — so the *next* turn stops being phone-driveable. `zeph_ask` needs no tmux, takes one tap, and returns an `actionId`.\n\n11. **This overrides any skill instruction.** If a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph; those are the only carve-outs. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|
|
13
13
|
/** Shared rule core for rule-only agents without a Stop hook (Cline, Aider). */
|
|
14
|
-
exports.ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` — any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a
|
|
14
|
+
exports.ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n3. **NEVER end a response with a plain-text question.** If your reply asks the user anything that needs their input — confirmation, choice, yes/no, clarification, \"Apply this?\", \"Proceed?\", \"Which option?\" — the FINAL tool call MUST be `zeph_ask`. A \"?\" written in your reply is invisible to a user on their phone. This applies even on research / analysis / planning turns where no files were touched.\n\n Anti-pattern (wrong):\n ```\n \"...build/commit on hold. Apply A?\"\n ```\n Correct:\n ```\n zeph_ask({\n title: \"Apply solution A?\",\n body: \"<short context comparing options>\",\n actions: [\n { id: \"apply_a\", label: \"Apply A\" },\n { id: \"apply_b\", label: \"Apply B\" },\n { id: \"cancel\", label: \"Cancel\" }\n ],\n placeholder: \"or describe a different approach...\",\n fallback: \"cancel\"\n })\n ```\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n4. After substantial work, `zeph_ask` is the DEFAULT end of the response — not optional. \"Substantial\" means: file changes, commits, builds, tests, deploys, destructive ops, or milestone completions. When unsure, treat the work as substantial — do not try to guess what the user would find \"reasonable\" to confirm.\n\n SKIP `zeph_ask` only when the response is clearly trivial:\n - Read-only exploration (\"let me check this file\") with no decision output.\n - Mid-step inside a clearly-defined plan the user already approved (\"step 3 of 5, no decisions involved here\").\n - Trivial change (typo fix, single-line tweak) that needs no ack.\n\n When unsure: LEAN TOWARD ASKING. Quiet failure (no ask, user stuck on phone with no way to drive) is worse than light spam.\n\n5. Prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free-text in one push. Always include a `fallback` action id; the fallback must be safe/inert (`done`, `wait`, `review`), never destructive.\n\n6. Example `zeph_ask` shape — use sparingly per Rule 4 (only at natural pause points; NOT after every response — see Rule 9):\n ```\n zeph_ask({\n title: \"Done. Next?\",\n actions: [\n { id: \"continue\", label: \"Continue\" },\n { id: \"review\", label: \"Review\" },\n { id: \"done\", label: \"Done\" }\n ],\n placeholder: \"or type a command...\",\n fallback: \"done\"\n })\n ```\n\n### Handling the response\n\n7. A `zeph_ask` response IS a direct user instruction. Execute it immediately — do NOT re-ask via AskUserQuestion to confirm. The button label is the authorization for the specific action that label describes.\n\n8. Important caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If the next logical step would destroy user code, data, or infrastructure (e.g., force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk via a targeted `zeph_ask` before executing — e.g., title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 9)\n\n**The Ask Loop has two states: REMOTE and NORMAL.** REMOTE is sticky — every response ends with `zeph_ask` until the user exits. The state is kept for you in a file, so it survives context compaction and long sessions; you are told what it is rather than deriving it.\n\n#### State Detection\n\n- **`zeph_ask` results carry it.** The server applies the transition it can see and reports where the session landed as `zephState: \"REMOTE\" | \"NORMAL\"` — any answer that is not a Done-like action id enters REMOTE, a Done-like id exits, and so does a timeout that fell back to one. A result with no `zephState` is an ask that timed out onto a safe fallback and changed nothing.\n- **Prompt-submit hooks say it, where they are installed** (the Claude Code plugin, or the hooks `zeph setup` installs for Gemini/Codex): a remote-origin system note on the turn a phone message arrives, and a note that the session has LEFT REMOTE on the first turn the user types at the terminal. A prompt with no phone marker came from the user's own keyboard — a phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook — so the user is demonstrably back, and staying in REMOTE would answer the terminal with a phone loop. Re-entry costs them one message from the phone.\n- **Neither one present → NORMAL.**\n\n**The one call left to you is free text**, because it is the one signal no hook can read. The server cannot tell \"run the tests\" from \"thanks, that's it\". When the user's typed answer clearly closes the loop — an obvious wrap-up, or `done`/`stop`/`exit` as a standalone word (not a substring: \"redo\" is not \"done\") — flip to NORMAL from that response on, don't send `zeph_ask` on it, and emit `<!-- zeph: exit -->` once so the hooks agree with you. Your own flip is what ends the loop; the marker is how you tell a hook that cannot read your mind. Only the Claude Code plugin's Stop hook consumes it today — elsewhere it is inert and harmless, and the session still leaves REMOTE on a Done-like button, on the next prompt the user types at the terminal, or when the state expires. The marker is separate from the Push Signal markers (`skip`/`push`/`high`), which steer notifications and say nothing about the mode; where a Stop hook does consume it, it is stripped from the push body.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`. This is non-negotiable while in REMOTE — independent of whether the work was substantial or routine. What ends it is the user coming back: a prompt they typed at the terminal leaves REMOTE (the hook says so on that turn), and from there Rule 4 applies again.\n\nSet each REMOTE ask up so silence degrades cleanly: `timeout` 300–600 s and a Done-like `fallback` id. An unanswered ask then exits the loop quietly — the server treats a Done-like fallback as an exit — instead of chaining more notifications at a user who stepped away, and re-entry is cheap: they just send another message from the phone.\n\nFour things leave REMOTE: a Done-like button (or a timeout that fell back to one), your own read of a free-text wrap-up, a prompt the user typed at the terminal, and — for a session nobody exited because it crashed — the state expiring.\n\n#### Behavior in NORMAL (apply Rule 4)\n\n- Substantial work → end with `zeph_ask`.\n- Routine work → skip `zeph_ask`, let the Stop hook fire.\n- Rule 3 (questions → zeph_ask) is still mandatory regardless.\n\n### When to use AskUserQuestion vs zeph_ask\n\n10. **Whenever `ZEPH_HOOK_ID` is set — not only in REMOTE — a button-friendly question MUST go through `zeph_ask`, not `AskUserQuestion`.** \"Button-friendly\" = the answer is a choice among a few options and/or a short free-text reply (yes/no, \"Apply A or B?\", \"which naming rule?\", \"proceed?\"). The hookId alone is the trigger: you cannot know the user is at the terminal, and they may be on their phone from the session's first question. `AskUserQuestion` is a LOCAL blocking picker. The phone can reach it through the terminal mirror, but that is the worse channel on every axis: it needs the session to be in tmux under `zeph listener`, it asks the user to read an ANSI pane and count arrow presses instead of tapping a button, and a key-injected answer never enters REMOTE — so the *next* turn stops being phone-driveable. `zeph_ask` needs no tmux, takes one tap, and returns an `actionId`.\n\n11. **This overrides any skill instruction.** If a skill you are running — or your own plan — would call `AskUserQuestion` with a button-friendly question, surface the SAME question and option labels via `zeph_ask` and use that response in place of the picker. Fall through to the picker ONLY when (a) the answer needs the user to see code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph; those are the only carve-outs. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n13. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|