@zeph-to/cli 2.9.0 → 2.10.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 +11 -4
- package/dist/cli.js +2 -2
- package/dist/gate.d.ts +9 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +10 -1
- package/dist/remote-hook.js +4 -4
- package/dist/templates.d.ts.map +1 -1
- package/dist/templates.js +155 -31
- package/dist/zeph-core.generated.d.ts +4 -4
- package/dist/zeph-core.generated.d.ts.map +1 -1
- package/dist/zeph-core.generated.js +4 -4
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -605,10 +605,17 @@ this order — first hit wins:
|
|
|
605
605
|
It is now `quiet`, so upgrading turns the routine per-turn push off until
|
|
606
606
|
you run `/zeph-normal`. Row 4 is why the hooks this CLI installs are
|
|
607
607
|
unaffected: they name `normal` themselves, since a hook that supplies no
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
608
|
+
`high` marker would be permanently silent under `quiet` rather than
|
|
609
|
+
merely quieter. Row 4 sits *below* the state files on purpose — the flag
|
|
610
|
+
names a default, it does not override a dial the user set.
|
|
611
|
+
|
|
612
|
+
Under `normal` the JSON hook configs push on every turn, because they see
|
|
613
|
+
no per-tool events and so pass no counts — the gate then assumes real
|
|
614
|
+
work. The Pi extension and the OpenCode plugin do see those events, so
|
|
615
|
+
they pass real `--tools` / `--nonreadonly` counts and go quiet where the
|
|
616
|
+
others cannot: a turn with no tool calls, a turn with exactly one (a
|
|
617
|
+
lone edit included — the gate wants two), and a turn whose calls were
|
|
618
|
+
all reads. `/zeph-loud` still pushes on all of them.
|
|
612
619
|
|
|
613
620
|
A dial file that exists but reads empty resolves to `normal`, not to row
|
|
614
621
|
5: an empty file is a failed write, and resolving breakage to silence
|
package/dist/cli.js
CHANGED
|
@@ -279,8 +279,8 @@ const handleNotify = async (args) => {
|
|
|
279
279
|
// With no dial the mode falls back to --pushmode-default, then to quiet.
|
|
280
280
|
if (args.auto === true) {
|
|
281
281
|
const verdict = (0, gate_js_1.decidePush)({
|
|
282
|
-
toolCount: gateCount(args.
|
|
283
|
-
nonReadonlyCount: gateCount(args.
|
|
282
|
+
toolCount: gateCount(args[gate_js_1.TOOL_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.toolCount),
|
|
283
|
+
nonReadonlyCount: gateCount(args[gate_js_1.NONREADONLY_COUNT_FLAG], gate_js_1.GATE_DEFAULTS.nonReadonlyCount),
|
|
284
284
|
alreadyAsked: gate_js_1.GATE_DEFAULTS.alreadyAsked,
|
|
285
285
|
marker: (0, gate_js_1.normalizeMarker)(typeof args.marker === 'string' ? args.marker : undefined),
|
|
286
286
|
pushMode: (0, gate_js_1.autoPushMode)(projectDir, args[gate_js_1.PUSHMODE_DEFAULT_FLAG]),
|
package/dist/gate.d.ts
CHANGED
|
@@ -88,6 +88,15 @@ export declare const PUSHMODE_DEFAULT: GatePushMode;
|
|
|
88
88
|
* read back in cli.ts — shared so the two can never drift apart.
|
|
89
89
|
*/
|
|
90
90
|
export declare const PUSHMODE_DEFAULT_FLAG = "pushmode-default";
|
|
91
|
+
/**
|
|
92
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
93
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
94
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
95
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
96
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
97
|
+
*/
|
|
98
|
+
export declare const TOOL_COUNT_FLAG = "tools";
|
|
99
|
+
export declare const NONREADONLY_COUNT_FLAG = "nonreadonly";
|
|
91
100
|
/**
|
|
92
101
|
* The user's session push-mode dial (/zeph-quiet | /zeph-loud | /zeph-normal).
|
|
93
102
|
*
|
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;;;;;;;;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"}
|
|
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;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,UAAU,CAAC;AACvC,eAAO,MAAM,sBAAsB,gBAAgB,CAAC;AAEpD;;;;;;;;;;;;;;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.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;
|
|
3
|
+
exports.autoPushMode = exports.readPushMode = exports.NONREADONLY_COUNT_FLAG = exports.TOOL_COUNT_FLAG = 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
|
*
|
|
@@ -244,6 +244,15 @@ exports.PUSHMODE_DEFAULT = 'quiet';
|
|
|
244
244
|
* read back in cli.ts — shared so the two can never drift apart.
|
|
245
245
|
*/
|
|
246
246
|
exports.PUSHMODE_DEFAULT_FLAG = 'pushmode-default';
|
|
247
|
+
/**
|
|
248
|
+
* `notify` flags carrying the turn's tool counts. Only the drop-in artifacts
|
|
249
|
+
* (pi extension, opencode plugin) can supply them — they are the only installed
|
|
250
|
+
* hooks that see per-tool events. Shared for the same reason as the flag above:
|
|
251
|
+
* templates.ts writes them, cli.ts reads them, and a rename on one side alone
|
|
252
|
+
* would send `gateCount` back to GATE_DEFAULTS with nothing to show for it.
|
|
253
|
+
*/
|
|
254
|
+
exports.TOOL_COUNT_FLAG = 'tools';
|
|
255
|
+
exports.NONREADONLY_COUNT_FLAG = 'nonreadonly';
|
|
247
256
|
/**
|
|
248
257
|
* The user's session push-mode dial (/zeph-quiet | /zeph-loud | /zeph-normal).
|
|
249
258
|
*
|
package/dist/remote-hook.js
CHANGED
|
@@ -14,7 +14,7 @@ exports.runRemoteHook = exports.isRemoteHookAgent = exports.REMOTE_HOOK_AGENTS =
|
|
|
14
14
|
* record — same project (cksum of the cwd), fresh (≤15 min), and
|
|
15
15
|
* byte-identical trimmed text — the hook emits additionalContext telling
|
|
16
16
|
* the model the user is driving the session from their phone, which enters
|
|
17
|
-
* sticky REMOTE mode
|
|
17
|
+
* sticky REMOTE mode.
|
|
18
18
|
*
|
|
19
19
|
* Detection is exact: a terminal keystroke racing a phone message can
|
|
20
20
|
* never false-match. Parity with the writer (listener.ts writeRemoteMarker)
|
|
@@ -54,7 +54,7 @@ const HOOK_EVENT_NAME = {
|
|
|
54
54
|
const FRESH_WINDOW_SEC = 900;
|
|
55
55
|
const TWO_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
56
56
|
|
|
57
|
-
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 (
|
|
57
|
+
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 (the "Sticky REMOTE mode" rule in your Zeph rules): 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.`;
|
|
58
58
|
const ONE_WAY_CONTEXT = `# System note (Zeph remote-origin detect)
|
|
59
59
|
|
|
60
60
|
This user message arrived from the user's phone via Zeph agent chat (verified by the listener — exact text match), but no hook id is configured (neither \`ZEPH_HOOK_ID\` nor \`hookId\` in ~/.zeph/config.json), 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).`;
|
|
@@ -66,7 +66,7 @@ This user message arrived from the user's phone via Zeph agent chat (verified by
|
|
|
66
66
|
*/
|
|
67
67
|
const EXIT_CONTEXT = `# System note (Zeph)
|
|
68
68
|
|
|
69
|
-
The user typed this prompt at the terminal, so this session has LEFT sticky REMOTE mode — answer normally (
|
|
69
|
+
The user typed this prompt at the terminal, so this session has LEFT sticky REMOTE mode — answer normally (the NORMAL branch of your Zeph rules) and do not end this response with \`zeph_ask\` just to keep the loop alive. One rule 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.`;
|
|
70
70
|
const isRemoteHookAgent = (raw) => exports.REMOTE_HOOK_AGENTS.includes(raw);
|
|
71
71
|
exports.isRemoteHookAgent = isRemoteHookAgent;
|
|
72
72
|
/**
|
|
@@ -87,7 +87,7 @@ const runRemoteHook = (agent, stdin, env = process.env, now = Date.now) => {
|
|
|
87
87
|
// `cwd` keys every state file, so without it there is nothing to look up.
|
|
88
88
|
if (!cwd)
|
|
89
89
|
return null;
|
|
90
|
-
// Mute outranks everything (
|
|
90
|
+
// Mute outranks everything (the mute rule) — stay silent and leave both the marker
|
|
91
91
|
// and the state untouched (the next inject overwrites the marker anyway).
|
|
92
92
|
if ((0, gate_js_1.isMuted)(cwd))
|
|
93
93
|
return null;
|
package/dist/templates.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"templates.d.ts","sourceRoot":"","sources":["../src/templates.ts"],"names":[],"mappings":"AA4KA,6FAA6F;AAC7F,eAAO,MAAM,WAAW,QAKtB,CAAC;AAEH,6FAA6F;AAC7F,eAAO,MAAM,aAAa,QAIxB,CAAC;AAEH,0FAA0F;AAC1F,eAAO,MAAM,WAAW,QAAyE,CAAC;AAElG,uFAAuF;AACvF,eAAO,MAAM,UAAU,QAAyE,CAAC;AAEjG,oGAAoG;AACpG,eAAO,MAAM,YAAY,QAIvB,CAAC;AAEH,gFAAgF;AAChF,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,sGAAsG;AACtG,eAAO,MAAM,UAAU,QAIrB,CAAC;AAEH,6GAA6G;AAC7G,eAAO,MAAM,OAAO,QAIlB,CAAC;AAEH,2GAA2G;AAC3G,eAAO,MAAM,aAAa,QAIxB,CAAC;AAIH,eAAO,MAAM,YAAY,QAKd,CAAC;AAEZ,eAAO,MAAM,cAAc,QAOhB,CAAC;AAEZ,eAAO,MAAM,YAAY;;;;;;;;;;;;;;;;;;;;;;;CAuBxB,CAAC;AAYF,eAAO,MAAM,WAAW;;;;;;;;;;;;;;;;CASvB,CAAC;AAEF;;;;;;;GAOG;AACH,eAAO,MAAM,eAAe,GAAI,OAAO,OAAO,KAAG,OAUhD,CAAC;AAEF,eAAO,MAAM,aAAa,QASf,CAAC;AAQZ,2FAA2F;AAC3F,eAAO,MAAM,YAAY,QAsDxB,CAAC;AAEF,kGAAkG;AAClG,eAAO,MAAM,eAAe,QAwG3B,CAAC;AASF,eAAO,MAAM,eAAe,oFAA+E,CAAC;AAC5G,eAAO,MAAM,aAAa,sBAAsB,CAAC;AAQjD;;;;GAIG;AACH,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,EAAE,MAAM,MAAM,KAAG,MAWnE,CAAC;AAEF,uEAAuE;AACvE,eAAO,MAAM,kBAAkB,GAAI,UAAU,MAAM,KAAG,MAOrD,CAAC"}
|
package/dist/templates.js
CHANGED
|
@@ -6,7 +6,8 @@
|
|
|
6
6
|
// generated core (src/zeph-core.generated.ts) plus a per-agent
|
|
7
7
|
// notification preamble:
|
|
8
8
|
//
|
|
9
|
-
// - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot
|
|
9
|
+
// - Hook-driven agents (Cursor, Windsurf, Gemini, Codex, Copilot, Pi,
|
|
10
|
+
// OpenCode) have
|
|
10
11
|
// a Stop-equivalent hook installed that auto-pushes on completion, so
|
|
11
12
|
// they must NOT manually call zeph_notify for "done".
|
|
12
13
|
// - Rule-only agents (Cline, Aider) have no Stop hook, so they DO call
|
|
@@ -16,9 +17,9 @@
|
|
|
16
17
|
// all of them — that is the whole point of the shared generated core.
|
|
17
18
|
//
|
|
18
19
|
// One more axis, and it decides who can enter REMOTE at all: the core's
|
|
19
|
-
// Rules
|
|
20
|
-
// hook note (a phone message) or by a `zeph_ask` answer. Only Gemini
|
|
21
|
-
// have that hook (remoteHookCmd below). Every other agent's only door is the
|
|
20
|
+
// Rules 1/2/8/9 are REMOTE-scoped, and REMOTE is entered by a prompt-submit
|
|
21
|
+
// hook note (a phone message) or by a `zeph_ask` answer. Only Gemini, Codex
|
|
22
|
+
// and Pi have that hook (remoteHookCmd below). Every other agent's only door is the
|
|
22
23
|
// ask itself — so for them the NORMAL branch must still send one after real
|
|
23
24
|
// work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK.
|
|
24
25
|
//
|
|
@@ -35,8 +36,9 @@ const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
|
35
36
|
// pattern in plugin/hooks/zeph-{stop,ask}.sh.
|
|
36
37
|
//
|
|
37
38
|
// `--auto` applies the shared push-gate before sending (see src/gate.ts):
|
|
38
|
-
//
|
|
39
|
-
// /zeph-quiet | /zeph-loud dial works for every hook-driven
|
|
39
|
+
// a caller that names no turn counts still pushes (gate defaults assume real
|
|
40
|
+
// work), and the /zeph-quiet | /zeph-loud dial works for every hook-driven
|
|
41
|
+
// agent. The two drop-in artifacts do name counts — see `turnFacts` below.
|
|
40
42
|
//
|
|
41
43
|
// `--pushmode-default normal` is what keeps the first half of that true. The
|
|
42
44
|
// built-in default for a project with no dial is quiet, and quiet only lets a
|
|
@@ -47,8 +49,23 @@ const zeph_core_generated_js_1 = require("./zeph-core.generated.js");
|
|
|
47
49
|
// Older installed `zeph` versions parse both flags as unknown booleans and
|
|
48
50
|
// ignore them — graceful backward compatibility, and for `--pushmode-default`
|
|
49
51
|
// specifically the old build's behavior was already "normal when no dial".
|
|
50
|
-
|
|
51
|
-
|
|
52
|
+
//
|
|
53
|
+
// `turnFacts` carries the `--tools` / `--nonreadonly` counts, which only the
|
|
54
|
+
// two drop-in artifacts can supply — a JSON hook config sees no per-tool
|
|
55
|
+
// events, so it passes nothing and keeps riding GATE_DEFAULTS.
|
|
56
|
+
const notifyCmd = (turnFacts = '') => '$(command -v zeph || echo "npx -y @zeph-to/cli") notify --title "Task done" --auto'
|
|
57
|
+
+ turnFacts
|
|
58
|
+
+ ` --${gate_js_1.PUSHMODE_DEFAULT_FLAG} normal 2>/dev/null || true`;
|
|
59
|
+
// The same command as a JS template literal, so an artifact's runtime counters
|
|
60
|
+
// interpolate into it. JSON.stringify would emit a double-quoted string and
|
|
61
|
+
// ship the placeholders verbatim. Safe as long as the command holds no
|
|
62
|
+
// backtick and no `${` of its own — `$(command -v zeph …)` is shell, not JS.
|
|
63
|
+
const notifyCmdLiteral = (turnFacts) => '`' + notifyCmd(turnFacts) + '`';
|
|
64
|
+
// The turn-fact fragment, as JS template-literal placeholders. Both artifacts
|
|
65
|
+
// name their counters `tools` / `nonReadonly`, so one fragment serves both —
|
|
66
|
+
// templates.test.ts pins those identifiers, since a rename on the artifact side
|
|
67
|
+
// would leave `${tools}` unresolved and kill the push with a ReferenceError.
|
|
68
|
+
const TURN_FACT_FLAGS = ` --${gate_js_1.TOOL_COUNT_FLAG} \${tools} --${gate_js_1.NONREADONLY_COUNT_FLAG} \${nonReadonly}`;
|
|
52
69
|
// Prompt-submit hook command — remote-origin detection (ADR-0002). Reads
|
|
53
70
|
// the hook JSON on stdin and prints additionalContext JSON on a marker
|
|
54
71
|
// match (see src/remote-hook.ts). stdout IS the hook response, so only
|
|
@@ -81,7 +98,7 @@ refactor, multi-file changes) call zeph_notify. Skip it for trivial
|
|
|
81
98
|
operations (file reads, simple searches). Set priority "high" for
|
|
82
99
|
errors/blockers.`;
|
|
83
100
|
// REMOTE-entry preamble — agents with NO prompt-submit hook (Cursor, Windsurf,
|
|
84
|
-
// Copilot, Cline, Aider). The shared core scopes Rules
|
|
101
|
+
// Copilot, Cline, Aider, OpenCode). The shared core scopes Rules 1/2/8/9 to REMOTE
|
|
85
102
|
// and tells a NORMAL session it owes no `zeph_ask`. That is right where a
|
|
86
103
|
// hook can announce the phone message that starts REMOTE; here nothing can,
|
|
87
104
|
// and the only remaining entry is a `zeph_ask` answer that is not Done-like.
|
|
@@ -91,7 +108,7 @@ errors/blockers.`;
|
|
|
91
108
|
// says about NORMAL still holds (no ask on trivial turns, questions may go
|
|
92
109
|
// to the local picker, no `zeph_ask` just to mark a turn finished).
|
|
93
110
|
//
|
|
94
|
-
// This overrides the core's Rule
|
|
111
|
+
// This overrides the core's Rule 2 NORMAL clause for these agents. It is a
|
|
95
112
|
// per-agent preamble, not a fork of the core, for the reason the core's
|
|
96
113
|
// header gives: the rule text must stay one thing.
|
|
97
114
|
const REMOTE_ENTRY_NO_HOOK = `## Entering REMOTE without a prompt hook
|
|
@@ -99,14 +116,14 @@ const REMOTE_ENTRY_NO_HOOK = `## Entering REMOTE without a prompt hook
|
|
|
99
116
|
This agent has no prompt-submit hook, so nothing can tell you when a
|
|
100
117
|
message arrived from the user's phone. The ONLY way this session enters
|
|
101
118
|
REMOTE is a \`zeph_ask\` answer that is not a Done-like button. So — and
|
|
102
|
-
this overrides the "In NORMAL, end with nothing" clause
|
|
119
|
+
this overrides the "In NORMAL, end with nothing" clause below —
|
|
103
120
|
**after substantial work in NORMAL, end the response with \`zeph_ask\`**:
|
|
104
121
|
2–4 \`actions\` carrying the next-step candidates plus a Done-like
|
|
105
122
|
\`fallback\`, \`timeout\` 300–600 s. "Substantial" = file changes, commits,
|
|
106
123
|
builds, tests, deploys, destructive ops, milestone completions. Skip it on
|
|
107
124
|
trivial turns (read-only exploration, a mid-step in an approved plan, a
|
|
108
|
-
typo-sized fix). Once the answer reports \`zephState: "REMOTE"\`,
|
|
109
|
-
takes over.`;
|
|
125
|
+
typo-sized fix). Once the answer reports \`zephState: "REMOTE"\`, sticky REMOTE
|
|
126
|
+
mode takes over.`;
|
|
110
127
|
// Tool-access preamble — pi only.
|
|
111
128
|
const PI_TOOL_ACCESS = `## Zeph tools via the CLI (no MCP)
|
|
112
129
|
|
|
@@ -191,13 +208,13 @@ exports.OPENCODE_RULE = buildRule({
|
|
|
191
208
|
exports.CURSOR_HOOKS = JSON.stringify({
|
|
192
209
|
version: 1,
|
|
193
210
|
hooks: {
|
|
194
|
-
stop: [{ command:
|
|
211
|
+
stop: [{ command: notifyCmd() }],
|
|
195
212
|
},
|
|
196
213
|
}, null, 2);
|
|
197
214
|
exports.WINDSURF_HOOKS = JSON.stringify({
|
|
198
215
|
hooks: {
|
|
199
216
|
post_cascade_response: [{
|
|
200
|
-
command:
|
|
217
|
+
command: notifyCmd(),
|
|
201
218
|
show_output: false,
|
|
202
219
|
}],
|
|
203
220
|
},
|
|
@@ -220,7 +237,7 @@ exports.GEMINI_HOOKS = {
|
|
|
220
237
|
hooks: [{
|
|
221
238
|
name: 'zeph-notify',
|
|
222
239
|
type: 'command',
|
|
223
|
-
command:
|
|
240
|
+
command: notifyCmd(),
|
|
224
241
|
}],
|
|
225
242
|
}],
|
|
226
243
|
},
|
|
@@ -242,7 +259,7 @@ exports.CODEX_HOOKS = {
|
|
|
242
259
|
hooks: [{ type: 'command', command: remoteHookCmd('codex'), timeout: 5 }],
|
|
243
260
|
}],
|
|
244
261
|
Stop: [{
|
|
245
|
-
hooks: [{ type: 'command', command:
|
|
262
|
+
hooks: [{ type: 'command', command: notifyCmd() }],
|
|
246
263
|
}],
|
|
247
264
|
},
|
|
248
265
|
};
|
|
@@ -270,7 +287,7 @@ exports.COPILOT_HOOKS = JSON.stringify({
|
|
|
270
287
|
hooks: {
|
|
271
288
|
sessionEnd: [{
|
|
272
289
|
type: 'command',
|
|
273
|
-
bash:
|
|
290
|
+
bash: notifyCmd(),
|
|
274
291
|
timeoutSec: 10,
|
|
275
292
|
}],
|
|
276
293
|
},
|
|
@@ -295,17 +312,36 @@ const sh = (cmd: string, cwd: string, stdin?: string): Promise<string> =>
|
|
|
295
312
|
child.stdin.end(stdin ?? "");
|
|
296
313
|
});
|
|
297
314
|
|
|
315
|
+
// Read-only tools, mirroring what the Claude Code Stop hook excludes
|
|
316
|
+
// (Read/Grep/Glob). Names read from the installed package's dist/core/tools on
|
|
317
|
+
// 2026-08-28: bash, edit, find, grep, ls, powershell, read, write — \`find\` is
|
|
318
|
+
// pi's Glob. Anything absent here, MCP tools included, counts as real work, so
|
|
319
|
+
// an unknown tool errs toward pushing rather than toward silence.
|
|
320
|
+
const READ_ONLY = new Set(["read", "grep", "find", "ls"]);
|
|
321
|
+
|
|
298
322
|
export default function (pi: ExtensionAPI) {
|
|
323
|
+
// Turn facts for the push gate. One agent per process, so plain counters
|
|
324
|
+
// suffice — no session keying.
|
|
325
|
+
let tools = 0;
|
|
326
|
+
let nonReadonly = 0;
|
|
327
|
+
|
|
328
|
+
pi.on("tool_execution_end", (event) => {
|
|
329
|
+
tools += 1;
|
|
330
|
+
if (!READ_ONLY.has(event.toolName)) nonReadonly += 1;
|
|
331
|
+
});
|
|
299
332
|
// Stop-equivalent: agent_settled fires once per user turn, after retries/compaction.
|
|
300
333
|
// Fire-and-forget: never block pi's turn-end on the notify network call.
|
|
301
334
|
pi.on("agent_settled", (_event, ctx) => {
|
|
302
|
-
const child = spawn("sh", ["-c", ${
|
|
335
|
+
const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS)}], { cwd: ctx.cwd, stdio: "ignore", detached: true });
|
|
303
336
|
child.on("error", () => {});
|
|
304
337
|
child.unref();
|
|
305
338
|
});
|
|
306
339
|
// Prompt-submit: remote-origin detection (ADR-0002) — additionalContext comes
|
|
307
340
|
// back as a persistent injected message (this extension is the consumer).
|
|
341
|
+
// Doubles as the turn boundary: this is where the counters start over.
|
|
308
342
|
pi.on("before_agent_start", async (event, ctx) => {
|
|
343
|
+
tools = 0;
|
|
344
|
+
nonReadonly = 0;
|
|
309
345
|
const out = await sh(${JSON.stringify(remoteHookCmd('pi'))}, ctx.cwd,
|
|
310
346
|
JSON.stringify({ prompt: event.prompt, cwd: ctx.cwd }));
|
|
311
347
|
try {
|
|
@@ -320,20 +356,108 @@ export default function (pi: ExtensionAPI) {
|
|
|
320
356
|
/** OpenCode plugin source — written to ~/.config/opencode/plugins/zeph.ts (drop-in auto-load). */
|
|
321
357
|
exports.OPENCODE_PLUGIN = `// Generated by @zeph-to/cli — reinstall overwrites; \`zeph uninstall\` removes.
|
|
322
358
|
import { spawn } from "node:child_process";
|
|
359
|
+
import type { Plugin } from "@opencode-ai/plugin";
|
|
323
360
|
|
|
324
|
-
|
|
361
|
+
// Read-only tools, mirroring what the Claude Code Stop hook excludes
|
|
362
|
+
// (Read/Grep/Glob). Names read from the installed opencode binary's tool
|
|
363
|
+
// registry on 2026-08-28: read, list, glob, grep, webfetch, websearch, task,
|
|
364
|
+
// shell, bash, edit, write, patch, skill, lsp, todowrite. Anything absent
|
|
365
|
+
// here, MCP tools included, counts as real work — an unknown tool errs toward
|
|
366
|
+
// pushing rather than toward silence.
|
|
367
|
+
const READ_ONLY = new Set(["read", "grep", "glob", "list"]);
|
|
325
368
|
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
369
|
+
type TurnFacts = { tools: number; nonReadonly: number };
|
|
370
|
+
const zeroFacts = (): TurnFacts => ({ tools: 0, nonReadonly: 0 });
|
|
371
|
+
|
|
372
|
+
// Typed against the real plugin contract, the way the pi extension is: the
|
|
373
|
+
// hook names and payload shapes this file asserts by hand are then checked by
|
|
374
|
+
// the compiler, so an upstream rename fails loudly instead of silently
|
|
375
|
+
// handing every handler an \`any\`.
|
|
376
|
+
export const ZephPlugin: Plugin = async ({ client, directory }) => {
|
|
377
|
+
// Turn facts, keyed by session. The plugin instance is per-project, not
|
|
378
|
+
// per-session, and the \`task\` tool runs subagents in their own child
|
|
379
|
+
// sessions with their own full lifecycle — one shared counter would bill
|
|
380
|
+
// their tool calls to whatever else the project had running.
|
|
381
|
+
const facts = new Map<string, TurnFacts>();
|
|
382
|
+
|
|
383
|
+
// A child session's id is all \`session.idle\` carries; the parent link lives
|
|
384
|
+
// on the session record. This is the one blocking call in the idle handler —
|
|
385
|
+
// the notify spawn below stays fire-and-forget, but the push decision cannot
|
|
386
|
+
// be made without knowing whether this session is somebody's subagent. The
|
|
387
|
+
// generated client defaults to \`throwOnError: false\`, so a non-2xx arrives as
|
|
388
|
+
// a result with no \`data\` rather than as a throw; both paths land on
|
|
389
|
+
// undefined, which pushes — the same direction every other unknown in this
|
|
390
|
+
// file leans.
|
|
391
|
+
const parentOf = async (id: string): Promise<string | undefined> => {
|
|
392
|
+
try {
|
|
393
|
+
return (await client.session.get({ path: { id } })).data?.parentID;
|
|
394
|
+
} catch {
|
|
395
|
+
return undefined;
|
|
396
|
+
}
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
return {
|
|
400
|
+
// Turn start. Create-if-absent, never reset: this fires per *message*, and
|
|
401
|
+
// a second message queued while the agent is still working would otherwise
|
|
402
|
+
// zero the counts already earned. session.idle deletes the entry, so the
|
|
403
|
+
// next turn always starts from zero anyway.
|
|
404
|
+
"chat.message": async ({ sessionID }) => {
|
|
405
|
+
if (!facts.has(sessionID)) facts.set(sessionID, zeroFacts());
|
|
406
|
+
},
|
|
407
|
+
"tool.execute.after": async ({ tool, sessionID }) => {
|
|
408
|
+
const entry = facts.get(sessionID) ?? zeroFacts();
|
|
409
|
+
entry.tools += 1;
|
|
410
|
+
if (!READ_ONLY.has(tool)) entry.nonReadonly += 1;
|
|
411
|
+
facts.set(sessionID, entry);
|
|
412
|
+
},
|
|
413
|
+
// Stop-equivalent. Unlike the named hooks above, session.idle arrives only
|
|
414
|
+
// through the generic \`event\` hook — there is no per-event key for it, so
|
|
415
|
+
// filter by type.
|
|
416
|
+
event: async ({ event }) => {
|
|
417
|
+
// A deleted session never idles again, so nothing would ever consume its
|
|
418
|
+
// entry. Drop it here or it outlives the turn for the life of the
|
|
419
|
+
// process. An *aborted* turn needs no such handling: the entry survives
|
|
420
|
+
// to the session's next chat.message, which adds to it rather than
|
|
421
|
+
// resetting, so the interrupted work is reported with the retry.
|
|
422
|
+
if (event.type === "session.deleted") {
|
|
423
|
+
facts.delete(event.properties.info.id);
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
if (event.type !== "session.idle") return;
|
|
427
|
+
const { sessionID } = event.properties;
|
|
428
|
+
// No entry means no user message was seen for this session, so there is
|
|
429
|
+
// no turn to report — the zeroes gate the push out, which is correct.
|
|
430
|
+
const settled = facts.get(sessionID) ?? zeroFacts();
|
|
431
|
+
facts.delete(sessionID);
|
|
432
|
+
|
|
433
|
+
// A \`task\` subagent settles first, in its own session. Pushing there
|
|
434
|
+
// would announce "Task done" for a slice of a turn still in flight, and
|
|
435
|
+
// leave the parent crediting the whole delegation as the single
|
|
436
|
+
// \`task\` call it saw — one tool, below the gate, so the real
|
|
437
|
+
// completion would go silent. Roll the counts up and stay quiet.
|
|
438
|
+
const parentID = await parentOf(sessionID);
|
|
439
|
+
const parent = parentID === undefined ? undefined : facts.get(parentID);
|
|
440
|
+
if (parent) {
|
|
441
|
+
parent.tools += settled.tools;
|
|
442
|
+
parent.nonReadonly += settled.nonReadonly;
|
|
443
|
+
return;
|
|
444
|
+
}
|
|
445
|
+
// Falling through means no live parent entry to roll into — either this
|
|
446
|
+
// is a top-level session, or a \`background\` task outlived the turn that
|
|
447
|
+
// spawned it and the parent settled and pushed already, or the lookup
|
|
448
|
+
// above failed. The three are indistinguishable from here, and all three
|
|
449
|
+
// want the same thing: report this session's own work rather than write
|
|
450
|
+
// it into an entry nobody will ever read. Never conjure that entry — that
|
|
451
|
+
// would leak one per background task.
|
|
452
|
+
|
|
453
|
+
const { tools, nonReadonly } = settled;
|
|
454
|
+
// Fire-and-forget: never block opencode's event pipeline on the notify call.
|
|
455
|
+
const child = spawn("sh", ["-c", ${notifyCmdLiteral(TURN_FACT_FLAGS)}], { cwd: directory, stdio: "ignore", detached: true });
|
|
456
|
+
child.on("error", () => {});
|
|
457
|
+
child.unref();
|
|
458
|
+
},
|
|
459
|
+
};
|
|
460
|
+
};
|
|
337
461
|
`;
|
|
338
462
|
// ── Marker-section helpers for shared global rule files ──────────
|
|
339
463
|
//
|
|
@@ -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 = "
|
|
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\
|
|
2
|
+
export declare const ZEPH_CORE_SOURCE_HASH = "32a111f3b27446031bc6972595c9b4cb13d86aa2a2ff3ff78a450eddc44d0a14";
|
|
3
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot, Pi, OpenCode). */
|
|
4
|
+
export declare const ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires \u2014 that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** \u2014 not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2\u20134 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id \u2014 never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done \u2014 what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done \u2014 next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first \u2014 title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **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** 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 installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin 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 phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard \u2014 staying in REMOTE would answer the terminal with a phone loop, and 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, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2\u20134 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` \u2014 never a destructive one, since an unanswered ask resolves to it \u2014 a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300\u2013600 s so silence degrades cleanly: an unanswered ask 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.\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 (no zeph_ask is owed)\n\nThe user is at the terminal \u2014 that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device \u2014 it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While 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?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README \u2192 \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through \u2014 the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE 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 code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. 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\
|
|
6
|
+
export declare const ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires \u2014 that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** \u2014 not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` \u2014 it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2\u20134 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id \u2014 never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done \u2014 what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done \u2014 next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first \u2014 title \"About to force-push main \u2014 proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **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** 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 installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin 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 phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard \u2014 staying in REMOTE would answer the terminal with a phone loop, and 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, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2\u20134 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` \u2014 never a destructive one, since an unanswered ask resolves to it \u2014 a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300\u2013600 s so silence degrades cleanly: an unanswered ask 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.\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 (no zeph_ask is owed)\n\nThe user is at the terminal \u2014 that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device \u2014 it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While 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?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE \u2014 so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README \u2192 \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through \u2014 the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE 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 code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. 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,
|
|
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,wHAAwH;AACxH,eAAO,MAAM,qBAAqB,24PAAsuP,CAAC;AAEzwP,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,24PAAsuP,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 = "
|
|
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\
|
|
10
|
+
exports.ZEPH_CORE_SOURCE_HASH = "32a111f3b27446031bc6972595c9b4cb13d86aa2a2ff3ff78a450eddc44d0a14";
|
|
11
|
+
/** Shared rule core for agents with a Stop-equivalent hook (Cursor, Windsurf, Gemini, Codex, Copilot, Pi, OpenCode). */
|
|
12
|
+
exports.ZEPH_CORE_HOOK_DRIVEN = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires — that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** — not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2–4 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id — never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done — what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done — next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first — title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **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** 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 installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin 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 phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard — staying in REMOTE would answer the terminal with a phone loop, and 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, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2–4 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` — never a destructive one, since an unanswered ask resolves to it — a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300–600 s so silence degrades cleanly: an unanswered ask 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.\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 (no zeph_ask is owed)\n\nThe user is at the terminal — that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device — it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While 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?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE — so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README → \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through — the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE 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 code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. 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\
|
|
14
|
+
exports.ZEPH_CORE_RULE_ONLY = "### When zeph_ask is MANDATORY\n\n1. **While in REMOTE, 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 holds even on research / analysis / planning turns where no files were touched.\n\n **In NORMAL it does not apply.** Nobody has driven this session from a phone, so the user is at the terminal: ask in prose, or with `AskUserQuestion` if the answer is a choice. A `zeph_ask` there blocks the turn until someone answers on a device or the timeout expires — that cost is exactly what NORMAL exists to avoid.\n\n### When zeph_ask is the DEFAULT (substantial work)\n\n2. **In REMOTE, `zeph_ask` is the DEFAULT end of EVERY response** — not conditioned on the work being substantial (Rule 7).\n\n **In NORMAL, end with nothing.** The Stop hook's push is the completion signal. Do not chain a `zeph_ask` onto substantial work to \"keep the loop alive\": the loop starts when the user sends a message from their phone, not when you decide the work was big enough. `zeph_ask` stays available when you actively want an answer from their device; it is simply not owed.\n\n3. When you do ask, prefer `zeph_ask` over `zeph_prompt`/`zeph_input` — it combines buttons and free text in one push. **`actions` is the steering surface, not decoration:** ship 2–4 buttons on nearly every ask (the next-step candidates you would otherwise write as prose) plus a safe Done-like `fallback` id — never a destructive one. Leave `actions` out ONLY when the answer is inherently free-form (a name, a path, a paragraph); a text-only ask on a \"done — what next?\" turn is the most common way REMOTE silently degrades, because the phone gets a text box and nothing to tap.\n\n ```\n zeph_ask({\n title: \"Slice done — next?\",\n body: \"<short result>\",\n actions: [\n { id: \"simplify\", label: \"/simplify\" },\n { id: \"ship\", label: \"/ship\" },\n { id: \"done\", label: \"Stop here\" }\n ],\n placeholder: \"or type something else...\",\n fallback: \"done\"\n })\n ```\n\n4. Rule 7 says when to send one. Never send an ask just to mark a turn finished.\n\n### Handling the response\n\n5. 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\n6. Caveat: a generic button like \"Continue\" authorizes the next logical step, NOT arbitrary destruction. If that step would destroy user code, data, or infrastructure (force-push to a shared branch, `rm -rf` outside the workdir, dropping a database, deleting prod resources), surface that specific risk in a targeted `zeph_ask` first — title \"About to force-push main — proceed?\", actions `[ok, cancel]`, fallback `cancel`.\n\n### Sticky REMOTE mode (Rule 7)\n\n7. **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** 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 installed (this plugin, or the hooks `zeph setup` writes for Gemini/Codex): a remote-origin 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 phone answer to a `zeph_ask` comes back as a `tool_result` and never reaches a prompt hook, so a prompt with no phone marker is demonstrably the user's own keyboard — staying in REMOTE would answer the terminal with a phone loop, and 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, and it is separate from any push-volume marker, which says nothing about the mode.\n\n#### Behavior in REMOTE (sticky, zeph_ask MANDATORY)\n\nEnd EVERY response with `zeph_ask`, carrying 2–4 `actions` (the next-step candidates as buttons) plus a Done-like `fallback` — never a destructive one, since an unanswered ask resolves to it — a text-only ask is only for inherently free-form answers. Non-negotiable while in REMOTE, independent of whether the work was substantial or routine.\n\nSet `timeout` 300–600 s so silence degrades cleanly: an unanswered ask 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.\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 (no zeph_ask is owed)\n\nThe user is at the terminal — that is what NORMAL means. Nothing here obliges an ask:\n\n- Questions go to `AskUserQuestion` or plain prose. The Ask hook still pushes them to the user's device, so a question is never lost.\n- Completion is the Stop hook's push.\n- `zeph_ask` remains available when you actively want an answer from their device — it is not owed, and never as a way to mark a turn finished.\n\nREMOTE begins the moment the user sends a message from their phone; from that turn on, Rules 1, 2, 8 and 9 are in force.\n\n### When to use AskUserQuestion vs zeph_ask\n\n8. **While 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?\", \"proceed?\"). `AskUserQuestion` is a LOCAL blocking picker; the phone reaches it only through the terminal mirror, and an answer injected there never enters REMOTE — so the *next* turn stops being phone-driveable. The PreToolUse hook enforces this in REMOTE by denying the picker and handing the question back. (Why the mirror is the worse channel on every axis: README → \"AskUserQuestion vs zeph_ask\".)\n\n **In NORMAL the picker is the right tool** and the hook lets it through — the user is at the terminal, and the Ask hook still pushes the question to their device so they know one is waiting.\n\n9. **In REMOTE 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 code or logs that won't fit in a push body, or (b) the answer is plausibly multi-paragraph. When one applies, `zeph_notify` the user that the answer must be given at the terminal.\n\n### Persistence\n\n10. These rules persist for the entire session. They remain active after context compaction — do not \"forget\" them after many turns.";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@zeph-to/cli",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.10.0",
|
|
4
4
|
"description": "Zeph CLI + push notification SDK for AI agents",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -30,6 +30,7 @@
|
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.0.0",
|
|
32
32
|
"@types/ws": "^8.18.1",
|
|
33
|
+
"esbuild": "^0.21.5",
|
|
33
34
|
"typescript": "^5.8.0",
|
|
34
35
|
"vitest": "^2.1.9"
|
|
35
36
|
},
|