@zeph-to/cli 2.1.1 → 2.3.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 +44 -16
- package/dist/ask.d.ts +52 -0
- package/dist/ask.d.ts.map +1 -0
- package/dist/ask.js +165 -0
- package/dist/cli.js +7 -0
- package/dist/gate.d.ts +21 -0
- package/dist/gate.d.ts.map +1 -1
- package/dist/gate.js +75 -1
- package/dist/listener.d.ts +262 -4
- package/dist/listener.d.ts.map +1 -1
- package/dist/listener.js +0 -0
- package/dist/remote-agents.d.ts +12 -0
- package/dist/remote-agents.d.ts.map +1 -1
- package/dist/remote-agents.js +1 -0
- package/dist/remote-hook.d.ts.map +1 -1
- package/dist/remote-hook.js +61 -21
- package/dist/session-registry.d.ts +63 -0
- package/dist/session-registry.d.ts.map +1 -0
- package/dist/session-registry.js +110 -0
- package/dist/test-setup.d.ts +22 -0
- package/dist/test-setup.d.ts.map +1 -0
- package/dist/test-setup.js +29 -0
- 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
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* What a session WAS, kept so it can be started again.
|
|
4
|
+
*
|
|
5
|
+
* tmux is the only record of a live session, and it forgets one the moment it
|
|
6
|
+
* ends — which is exactly when the phone wants it back. So the listener writes
|
|
7
|
+
* down what it saw while the session was alive: where it ran and which agent it
|
|
8
|
+
* ran, keyed by the tmux name the phone already addresses it by.
|
|
9
|
+
*
|
|
10
|
+
* This file is the whitelist that makes remote resume safe. A resume request
|
|
11
|
+
* carries a session NAME and nothing else; the directory and the binary come
|
|
12
|
+
* from here, from what this machine observed itself. Nothing a phone (or a
|
|
13
|
+
* relay posing as one) sends can point the daemon at another directory or
|
|
14
|
+
* another program.
|
|
15
|
+
*/
|
|
16
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
|
+
exports.sessionDirectoryExists = exports.knownSessionsPath = exports.isKnownSession = exports.rememberSessions = exports.recallSession = exports.knownSessions = exports.KNOWN_SESSION_TTL_MS = exports.MAX_KNOWN_SESSIONS = void 0;
|
|
18
|
+
const fs_1 = require("fs");
|
|
19
|
+
const path_1 = require("path");
|
|
20
|
+
const gate_js_1 = require("./gate.js");
|
|
21
|
+
/**
|
|
22
|
+
* Ceiling on remembered sessions. tmux names come from a small reused pool
|
|
23
|
+
* (`zeph-<project>`, `-2`, …), so this holds far more distinct projects than it
|
|
24
|
+
* looks; the cap only stops the file from growing without bound on a machine
|
|
25
|
+
* that has run agents for years.
|
|
26
|
+
*/
|
|
27
|
+
exports.MAX_KNOWN_SESSIONS = 100;
|
|
28
|
+
/** Forgotten after this long unseen — a directory from months ago is more
|
|
29
|
+
* likely to have moved than to be what the user meant. */
|
|
30
|
+
exports.KNOWN_SESSION_TTL_MS = 30 * 24 * 60 * 60 * 1000;
|
|
31
|
+
const registryPath = () => (0, path_1.join)((0, gate_js_1.stateDir)(), 'known-sessions.json');
|
|
32
|
+
const readAll = () => {
|
|
33
|
+
try {
|
|
34
|
+
const raw = (0, fs_1.readFileSync)(registryPath(), 'utf-8');
|
|
35
|
+
const parsed = JSON.parse(raw);
|
|
36
|
+
if (!Array.isArray(parsed))
|
|
37
|
+
return [];
|
|
38
|
+
// Written by this process, but a half-written or hand-edited file must
|
|
39
|
+
// not take the listener down — keep the rows that still make sense.
|
|
40
|
+
return parsed.filter((e) => !!e &&
|
|
41
|
+
typeof e === 'object' &&
|
|
42
|
+
typeof e.name === 'string' &&
|
|
43
|
+
typeof e.cwd === 'string' &&
|
|
44
|
+
typeof e.agentKind === 'string' &&
|
|
45
|
+
typeof e.lastSeenAt === 'string');
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
const writeAll = (entries) => {
|
|
52
|
+
const path = registryPath();
|
|
53
|
+
try {
|
|
54
|
+
(0, fs_1.mkdirSync)((0, path_1.dirname)(path), { recursive: true, mode: 0o700 });
|
|
55
|
+
// 0600: it names the user's project directories, which is not something
|
|
56
|
+
// every account on the machine needs to read.
|
|
57
|
+
(0, fs_1.writeFileSync)(path, JSON.stringify(entries, null, 2), { mode: 0o600 });
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// A registry that cannot be written costs the resume affordance, not
|
|
61
|
+
// the daemon — every other path keeps working.
|
|
62
|
+
}
|
|
63
|
+
};
|
|
64
|
+
/** Sessions this machine has seen, newest first, expired ones dropped. */
|
|
65
|
+
const knownSessions = (now = Date.now()) => readAll()
|
|
66
|
+
.filter((e) => now - Date.parse(e.lastSeenAt) < exports.KNOWN_SESSION_TTL_MS)
|
|
67
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt));
|
|
68
|
+
exports.knownSessions = knownSessions;
|
|
69
|
+
/** One remembered session, or null when this machine never saw that name. */
|
|
70
|
+
const recallSession = (name, now = Date.now()) => (0, exports.knownSessions)(now).find((e) => e.name === name) ?? null;
|
|
71
|
+
exports.recallSession = recallSession;
|
|
72
|
+
/**
|
|
73
|
+
* Write down the sessions running right now, replacing what was known about
|
|
74
|
+
* each. Called from the inventory sweep, so the record follows a session that
|
|
75
|
+
* moves directory or changes agent rather than pinning its first sighting.
|
|
76
|
+
*
|
|
77
|
+
* A session with no readable cwd is skipped rather than remembered without
|
|
78
|
+
* one: an entry that cannot say where to start the agent is not a resume
|
|
79
|
+
* target, only a row that would fail when tapped.
|
|
80
|
+
*/
|
|
81
|
+
const rememberSessions = (live, now = Date.now()) => {
|
|
82
|
+
const usable = live.filter((s) => !!s.name && !!s.cwd);
|
|
83
|
+
if (usable.length === 0)
|
|
84
|
+
return;
|
|
85
|
+
const seenAt = new Date(now).toISOString();
|
|
86
|
+
const byName = new Map((0, exports.knownSessions)(now).map((e) => [e.name, e]));
|
|
87
|
+
for (const s of usable) {
|
|
88
|
+
byName.set(s.name, {
|
|
89
|
+
name: s.name,
|
|
90
|
+
cwd: s.cwd,
|
|
91
|
+
agentKind: s.agentKind,
|
|
92
|
+
...(s.project ? { project: s.project } : {}),
|
|
93
|
+
...(s.label ? { label: s.label } : {}),
|
|
94
|
+
lastSeenAt: seenAt,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
const entries = [...byName.values()]
|
|
98
|
+
.sort((a, b) => b.lastSeenAt.localeCompare(a.lastSeenAt))
|
|
99
|
+
.slice(0, exports.MAX_KNOWN_SESSIONS);
|
|
100
|
+
writeAll(entries);
|
|
101
|
+
};
|
|
102
|
+
exports.rememberSessions = rememberSessions;
|
|
103
|
+
/** Whether the registry knows this name — the resume whitelist check. */
|
|
104
|
+
const isKnownSession = (name, now = Date.now()) => (0, exports.recallSession)(name, now) !== null;
|
|
105
|
+
exports.isKnownSession = isKnownSession;
|
|
106
|
+
/** Test seam: the file this module reads and writes. */
|
|
107
|
+
exports.knownSessionsPath = registryPath;
|
|
108
|
+
/** True when the recorded directory still exists to start an agent in. */
|
|
109
|
+
const sessionDirectoryExists = (entry) => (0, fs_1.existsSync)(entry.cwd);
|
|
110
|
+
exports.sessionDirectoryExists = sessionDirectoryExists;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Point every test at a throwaway home before any of them runs.
|
|
3
|
+
*
|
|
4
|
+
* The daemon keeps real state under `$HOME` and `$XDG_STATE_HOME` — the config,
|
|
5
|
+
* the remote marker, the registry of sessions this machine has run. Tests reach
|
|
6
|
+
* that state without meaning to: a test that only wanted to check a lease calls
|
|
7
|
+
* into the inventory sweep, and the sweep writes down what it saw. With fake
|
|
8
|
+
* tmux answering, what it wrote down was fiction, in the developer's own
|
|
9
|
+
* registry, on the machine they use.
|
|
10
|
+
*
|
|
11
|
+
* That already happened. Four invented sessions from the lease test's fake tmux
|
|
12
|
+
* ended up in a real registry and then on a real phone, each offering to start
|
|
13
|
+
* an agent in a directory that never existed.
|
|
14
|
+
*
|
|
15
|
+
* Eleven test files had guarded themselves individually. That is the wrong
|
|
16
|
+
* shape for this: it is a rule every future test has to remember, and the ones
|
|
17
|
+
* that forget do not fail — they quietly write somewhere real. Setting it here
|
|
18
|
+
* makes the safe thing the default. A file that still wants its own directory
|
|
19
|
+
* assigns one in its module body, which runs after this and wins.
|
|
20
|
+
*/
|
|
21
|
+
export {};
|
|
22
|
+
//# sourceMappingURL=test-setup.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"test-setup.d.ts","sourceRoot":"","sources":["../src/test-setup.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG"}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Point every test at a throwaway home before any of them runs.
|
|
4
|
+
*
|
|
5
|
+
* The daemon keeps real state under `$HOME` and `$XDG_STATE_HOME` — the config,
|
|
6
|
+
* the remote marker, the registry of sessions this machine has run. Tests reach
|
|
7
|
+
* that state without meaning to: a test that only wanted to check a lease calls
|
|
8
|
+
* into the inventory sweep, and the sweep writes down what it saw. With fake
|
|
9
|
+
* tmux answering, what it wrote down was fiction, in the developer's own
|
|
10
|
+
* registry, on the machine they use.
|
|
11
|
+
*
|
|
12
|
+
* That already happened. Four invented sessions from the lease test's fake tmux
|
|
13
|
+
* ended up in a real registry and then on a real phone, each offering to start
|
|
14
|
+
* an agent in a directory that never existed.
|
|
15
|
+
*
|
|
16
|
+
* Eleven test files had guarded themselves individually. That is the wrong
|
|
17
|
+
* shape for this: it is a rule every future test has to remember, and the ones
|
|
18
|
+
* that forget do not fail — they quietly write somewhere real. Setting it here
|
|
19
|
+
* makes the safe thing the default. A file that still wants its own directory
|
|
20
|
+
* assigns one in its module body, which runs after this and wins.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
const fs_1 = require("fs");
|
|
24
|
+
const os_1 = require("os");
|
|
25
|
+
const path_1 = require("path");
|
|
26
|
+
const sandbox = (0, fs_1.mkdtempSync)((0, path_1.join)((0, os_1.tmpdir)(), 'zeph-test-home-'));
|
|
27
|
+
process.env.HOME = sandbox;
|
|
28
|
+
process.env.XDG_STATE_HOME = (0, path_1.join)(sandbox, 'state');
|
|
29
|
+
process.env.XDG_CONFIG_HOME = (0, path_1.join)(sandbox, 'config');
|
|
@@ -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 = "36f68d9ee22d475f362c126a3002eb0d368879c67381d0c93ce2fcf8d232a480";
|
|
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.**
|
|
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 one-line reminder on every later turn the session is still remote \u2014 including turns the user types at the terminal.\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 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:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\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\nThe only way to leave REMOTE is the user signalling exit.\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.**
|
|
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 one-line reminder on every later turn the session is still remote \u2014 including turns the user types at the terminal.\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 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:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\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\nThe only way to leave REMOTE is the user signalling exit.\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,k0PAA4qP,CAAC;AAE/sP,gFAAgF;AAChF,eAAO,MAAM,mBAAmB,k0PAA4qP,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 = "36f68d9ee22d475f362c126a3002eb0d368879c67381d0c93ce2fcf8d232a480";
|
|
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.**
|
|
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 one-line reminder on every later turn the session is still remote — including turns the user types at the terminal.\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 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:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\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\nThe only way to leave REMOTE is the user signalling exit.\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.**
|
|
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 one-line reminder on every later turn the session is still remote — including turns the user types at the terminal.\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 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:\n- Whether the next user message arrived as a `tool_result` or as a typed terminal message. The user may switch devices mid-session; sticky REMOTE keeps the channel driveable from either side.\n- Whether the work was substantial or routine.\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\nThe only way to leave REMOTE is the user signalling exit.\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.";
|