@hwip/cli 0.0.1 → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (245) hide show
  1. package/LICENSE +190 -0
  2. package/NOTICE +11 -0
  3. package/README.md +1979 -0
  4. package/dist/agent-list.d.ts +76 -0
  5. package/dist/agent-list.js +102 -0
  6. package/dist/agent-read.d.ts +105 -0
  7. package/dist/agent-read.js +309 -0
  8. package/dist/agent-rules-fetch.d.ts +51 -0
  9. package/dist/agent-rules-fetch.js +275 -0
  10. package/dist/agent-rules.default.d.ts +30 -0
  11. package/dist/agent-rules.default.js +292 -0
  12. package/dist/agent-session.d.ts +23 -0
  13. package/dist/agent-session.js +44 -0
  14. package/dist/agent-state.d.ts +84 -0
  15. package/dist/agent-state.js +188 -0
  16. package/dist/agent-target.d.ts +60 -0
  17. package/dist/agent-target.js +81 -0
  18. package/dist/agents.d.ts +31 -0
  19. package/dist/agents.js +75 -0
  20. package/dist/api.d.ts +28 -0
  21. package/dist/api.js +76 -0
  22. package/dist/args.d.ts +74 -0
  23. package/dist/args.js +254 -0
  24. package/dist/ask-context-attach.d.ts +73 -0
  25. package/dist/ask-context-attach.js +151 -0
  26. package/dist/ask-context-screen.d.ts +21 -0
  27. package/dist/ask-context-screen.js +49 -0
  28. package/dist/ask-context-switch.d.ts +31 -0
  29. package/dist/ask-context-switch.js +74 -0
  30. package/dist/ask-context.d.ts +84 -0
  31. package/dist/ask-context.js +172 -0
  32. package/dist/ask-pending.d.ts +47 -0
  33. package/dist/ask-pending.js +221 -0
  34. package/dist/ask-seal.d.ts +120 -0
  35. package/dist/ask-seal.js +179 -0
  36. package/dist/ask.d.ts +107 -0
  37. package/dist/ask.js +551 -0
  38. package/dist/blocked-prompt-seal.d.ts +35 -0
  39. package/dist/blocked-prompt-seal.js +89 -0
  40. package/dist/blocked-prompt.d.ts +57 -0
  41. package/dist/blocked-prompt.js +181 -0
  42. package/dist/channel.d.ts +67 -0
  43. package/dist/channel.js +136 -0
  44. package/dist/check-update.d.ts +28 -0
  45. package/dist/check-update.js +153 -0
  46. package/dist/cli.d.ts +3 -0
  47. package/dist/cli.js +813 -0
  48. package/dist/codex-config.d.ts +24 -0
  49. package/dist/codex-config.js +169 -0
  50. package/dist/codex-transcript.d.ts +59 -0
  51. package/dist/codex-transcript.js +373 -0
  52. package/dist/command-refusal.d.ts +56 -0
  53. package/dist/command-refusal.js +127 -0
  54. package/dist/command-scan.d.ts +46 -0
  55. package/dist/command-scan.js +154 -0
  56. package/dist/command-seal.d.ts +57 -0
  57. package/dist/command-seal.js +88 -0
  58. package/dist/command-seq.d.ts +30 -0
  59. package/dist/command-seq.js +150 -0
  60. package/dist/config.d.ts +156 -0
  61. package/dist/config.js +392 -0
  62. package/dist/core-sections.d.ts +18 -0
  63. package/dist/core-sections.js +28 -0
  64. package/dist/crypto.d.ts +278 -0
  65. package/dist/crypto.js +698 -0
  66. package/dist/desktop-notify.d.ts +33 -0
  67. package/dist/desktop-notify.js +49 -0
  68. package/dist/device-directory.d.ts +36 -0
  69. package/dist/device-directory.js +97 -0
  70. package/dist/device-presence.d.ts +17 -0
  71. package/dist/device-presence.js +62 -0
  72. package/dist/downloads.d.ts +29 -0
  73. package/dist/downloads.js +106 -0
  74. package/dist/errors.d.ts +12 -0
  75. package/dist/errors.js +28 -0
  76. package/dist/file-crypto.d.ts +94 -0
  77. package/dist/file-crypto.js +272 -0
  78. package/dist/file-view-budget.d.ts +15 -0
  79. package/dist/file-view-budget.js +34 -0
  80. package/dist/file-view-switch.d.ts +28 -0
  81. package/dist/file-view-switch.js +122 -0
  82. package/dist/file-view.d.ts +108 -0
  83. package/dist/file-view.js +327 -0
  84. package/dist/gate.d.ts +141 -0
  85. package/dist/gate.js +336 -0
  86. package/dist/gemini-transcript.d.ts +32 -0
  87. package/dist/gemini-transcript.js +189 -0
  88. package/dist/http-failure.d.ts +98 -0
  89. package/dist/http-failure.js +349 -0
  90. package/dist/hwip-core.generated.d.ts +7 -0
  91. package/dist/hwip-core.generated.js +14 -0
  92. package/dist/hwip-hook.d.ts +77 -0
  93. package/dist/hwip-hook.js +340 -0
  94. package/dist/index.d.ts +7 -0
  95. package/dist/index.js +14 -0
  96. package/dist/input-sequencer.d.ts +65 -0
  97. package/dist/input-sequencer.js +142 -0
  98. package/dist/insecure-url.d.ts +16 -0
  99. package/dist/insecure-url.js +76 -0
  100. package/dist/installer.d.ts +96 -0
  101. package/dist/installer.js +850 -0
  102. package/dist/inventory-offload.d.ts +49 -0
  103. package/dist/inventory-offload.js +209 -0
  104. package/dist/inventory-worker.d.ts +2 -0
  105. package/dist/inventory-worker.js +27 -0
  106. package/dist/jsonc-doc.d.ts +106 -0
  107. package/dist/jsonc-doc.js +354 -0
  108. package/dist/keep-awake.d.ts +32 -0
  109. package/dist/keep-awake.js +46 -0
  110. package/dist/key-changes.d.ts +48 -0
  111. package/dist/key-changes.js +140 -0
  112. package/dist/lan-auth.d.ts +140 -0
  113. package/dist/lan-auth.js +217 -0
  114. package/dist/lan-endpoint.d.ts +41 -0
  115. package/dist/lan-endpoint.js +115 -0
  116. package/dist/lan-receiver.d.ts +122 -0
  117. package/dist/lan-receiver.js +510 -0
  118. package/dist/lan-sender.d.ts +76 -0
  119. package/dist/lan-sender.js +184 -0
  120. package/dist/lan-transfer.d.ts +36 -0
  121. package/dist/lan-transfer.js +54 -0
  122. package/dist/listener-device-id.d.ts +20 -0
  123. package/dist/listener-device-id.js +133 -0
  124. package/dist/listener-log.d.ts +4 -0
  125. package/dist/listener-log.js +23 -0
  126. package/dist/listener-process.d.ts +200 -0
  127. package/dist/listener-process.js +470 -0
  128. package/dist/listener-service.d.ts +225 -0
  129. package/dist/listener-service.js +682 -0
  130. package/dist/listener-worktree-cleanup.d.ts +27 -0
  131. package/dist/listener-worktree-cleanup.js +62 -0
  132. package/dist/listener.d.ts +1325 -0
  133. package/dist/listener.js +5680 -0
  134. package/dist/login-address.d.ts +25 -0
  135. package/dist/login-address.js +97 -0
  136. package/dist/login-page.d.ts +29 -0
  137. package/dist/login-page.js +93 -0
  138. package/dist/login.d.ts +98 -0
  139. package/dist/login.js +388 -0
  140. package/dist/mcp-command.d.ts +45 -0
  141. package/dist/mcp-command.js +50 -0
  142. package/dist/mcp.d.ts +19 -0
  143. package/dist/mcp.js +29 -0
  144. package/dist/node-support.d.ts +19 -0
  145. package/dist/node-support.js +38 -0
  146. package/dist/notify-failure.d.ts +57 -0
  147. package/dist/notify-failure.js +150 -0
  148. package/dist/opencode-transcript.d.ts +139 -0
  149. package/dist/opencode-transcript.js +390 -0
  150. package/dist/optional-features.d.ts +6 -0
  151. package/dist/optional-features.js +19 -0
  152. package/dist/pane-fit.d.ts +40 -0
  153. package/dist/pane-fit.js +119 -0
  154. package/dist/peer-key.d.ts +33 -0
  155. package/dist/peer-key.js +34 -0
  156. package/dist/pi-transcript.d.ts +26 -0
  157. package/dist/pi-transcript.js +255 -0
  158. package/dist/presence.d.ts +14 -0
  159. package/dist/presence.js +124 -0
  160. package/dist/private-files.d.ts +51 -0
  161. package/dist/private-files.js +218 -0
  162. package/dist/project-switch.d.ts +19 -0
  163. package/dist/project-switch.js +60 -0
  164. package/dist/redact.d.ts +10 -0
  165. package/dist/redact.js +21 -0
  166. package/dist/remote-agents.d.ts +410 -0
  167. package/dist/remote-agents.js +1218 -0
  168. package/dist/remote-hook.d.ts +12 -0
  169. package/dist/remote-hook.js +196 -0
  170. package/dist/replace-file.d.ts +50 -0
  171. package/dist/replace-file.js +328 -0
  172. package/dist/schedule-command.d.ts +35 -0
  173. package/dist/schedule-command.js +260 -0
  174. package/dist/schedule-launch.d.ts +15 -0
  175. package/dist/schedule-launch.js +24 -0
  176. package/dist/schedule-ready.d.ts +44 -0
  177. package/dist/schedule-ready.js +71 -0
  178. package/dist/schedule-remote.d.ts +77 -0
  179. package/dist/schedule-remote.js +167 -0
  180. package/dist/schedule-runner.d.ts +89 -0
  181. package/dist/schedule-runner.js +452 -0
  182. package/dist/schedule.d.ts +149 -0
  183. package/dist/schedule.js +428 -0
  184. package/dist/secret-mask.d.ts +29 -0
  185. package/dist/secret-mask.js +245 -0
  186. package/dist/self-register.d.ts +45 -0
  187. package/dist/self-register.js +67 -0
  188. package/dist/send.d.ts +56 -0
  189. package/dist/send.js +104 -0
  190. package/dist/session-diff.d.ts +161 -0
  191. package/dist/session-diff.js +445 -0
  192. package/dist/session-registry.d.ts +143 -0
  193. package/dist/session-registry.js +216 -0
  194. package/dist/sessions-report.d.ts +59 -0
  195. package/dist/sessions-report.js +99 -0
  196. package/dist/shell-audit.d.ts +34 -0
  197. package/dist/shell-audit.js +47 -0
  198. package/dist/subagent-transcripts.d.ts +167 -0
  199. package/dist/subagent-transcripts.js +437 -0
  200. package/dist/subscriber-trust.d.ts +128 -0
  201. package/dist/subscriber-trust.js +397 -0
  202. package/dist/templates.d.ts +105 -0
  203. package/dist/templates.js +871 -0
  204. package/dist/tmux-install.d.ts +44 -0
  205. package/dist/tmux-install.js +65 -0
  206. package/dist/tmux-liveness.d.ts +45 -0
  207. package/dist/tmux-liveness.js +71 -0
  208. package/dist/tmux-pane.d.ts +55 -0
  209. package/dist/tmux-pane.js +126 -0
  210. package/dist/tmux-socket.d.ts +20 -0
  211. package/dist/tmux-socket.js +292 -0
  212. package/dist/todo-fold.d.ts +31 -0
  213. package/dist/todo-fold.js +102 -0
  214. package/dist/transcript-tail.d.ts +312 -0
  215. package/dist/transcript-tail.js +718 -0
  216. package/dist/trust-command.d.ts +49 -0
  217. package/dist/trust-command.js +200 -0
  218. package/dist/trust-lock.d.ts +26 -0
  219. package/dist/trust-lock.js +94 -0
  220. package/dist/trust-pins.d.ts +37 -0
  221. package/dist/trust-pins.js +82 -0
  222. package/dist/turn-recorder-wire.d.ts +70 -0
  223. package/dist/turn-recorder-wire.js +165 -0
  224. package/dist/turn-recorder.d.ts +190 -0
  225. package/dist/turn-recorder.js +695 -0
  226. package/dist/turn-ring.d.ts +93 -0
  227. package/dist/turn-ring.js +418 -0
  228. package/dist/turn-watch.d.ts +177 -0
  229. package/dist/turn-watch.js +630 -0
  230. package/dist/types.d.ts +63 -0
  231. package/dist/types.js +2 -0
  232. package/dist/uninstall.d.ts +55 -0
  233. package/dist/uninstall.js +507 -0
  234. package/dist/usage.d.ts +17 -0
  235. package/dist/usage.js +421 -0
  236. package/dist/verify.d.ts +47 -0
  237. package/dist/verify.js +396 -0
  238. package/dist/worktree.d.ts +144 -0
  239. package/dist/worktree.js +310 -0
  240. package/dist/wrapper.d.ts +254 -0
  241. package/dist/wrapper.js +704 -0
  242. package/dist/ws-url.d.ts +109 -0
  243. package/dist/ws-url.js +202 -0
  244. package/package.json +78 -10
  245. package/bin/hwip.js +0 -3
@@ -0,0 +1,275 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.refreshManifest = exports.rulesUrl = exports.syncManifestFromCache = exports.loadManifestFromCache = exports.resetActiveManifest = exports.getActiveManifestSource = exports.getActiveManifest = exports.compareManifestVersions = exports.validateManifest = exports.RULES_REFRESH_INTERVAL_MS = exports.RULES_CACHE_FILE = void 0;
4
+ /**
5
+ * OTA delivery for agent detection rules (SPEC-AGENT-AWARENESS §S7).
6
+ *
7
+ * Agent UIs change on their release cadence, not ours: when Claude
8
+ * Code reshapes its status line, detection must be fixable by shipping
9
+ * DATA, not a new daemon. The listener therefore resolves its manifest
10
+ * as: valid remote fetch → valid disk cache → bundled defaults. Every
11
+ * tier fails closed to the next — a dead endpoint, corrupt cache, or
12
+ * hostile payload can never leave the listener without rules, and
13
+ * `disabledRuleIds` in a fetched manifest acts as a same-day
14
+ * kill-switch for a misfiring rule (no release, no restart).
15
+ *
16
+ * Version precedence: a fetched/cached manifest only activates when
17
+ * its version is >= the bundled one. npm publishes land in minutes
18
+ * while the server manifest ships with a full deploy, so a fresh cli
19
+ * routinely carries NEWER rules than the endpoint — without the gate,
20
+ * the older remote manifest would shadow every bundled rule fix until
21
+ * the server caught up. Server-side rollback still works: the server
22
+ * bumps the version even when reverting rule content.
23
+ */
24
+ const fs_1 = require("fs");
25
+ const path_1 = require("path");
26
+ const config_js_1 = require("./config.js");
27
+ const agent_state_js_1 = require("./agent-state.js");
28
+ const agent_rules_default_js_1 = require("./agent-rules.default.js");
29
+ const private_files_js_1 = require("./private-files.js");
30
+ exports.RULES_CACHE_FILE = (0, path_1.join)(config_js_1.CONFIG_DIR, 'agent-rules.json');
31
+ exports.RULES_REFRESH_INTERVAL_MS = 6 * 60 * 60 * 1000;
32
+ // Matches the server-side serving cap; anything bigger is not a manifest.
33
+ const MAX_MANIFEST_BYTES = 256 * 1024;
34
+ const FETCH_TIMEOUT_MS = 10_000;
35
+ const VALID_STATES = new Set(['working', 'blocked', 'idle', 'unknown']);
36
+ const isStringArray = (v) => Array.isArray(v) && v.every((s) => typeof s === 'string');
37
+ const isCondition = (v) => {
38
+ if (typeof v !== 'object' || v === null)
39
+ return false;
40
+ const c = v;
41
+ if (c.contains !== undefined && !isStringArray(c.contains))
42
+ return false;
43
+ if (c.regex !== undefined && !isStringArray(c.regex))
44
+ return false;
45
+ return true;
46
+ };
47
+ const isRule = (v) => {
48
+ if (typeof v !== 'object' || v === null)
49
+ return false;
50
+ const r = v;
51
+ if (typeof r.id !== 'string' || r.id.length === 0)
52
+ return false;
53
+ if (typeof r.state !== 'string' || !VALID_STATES.has(r.state))
54
+ return false;
55
+ // Safe integers, not merely finite. These numbers arrive from the server
56
+ // and decide behavior: a fractional priority orders rules by values no
57
+ // author can reason about, 1e21 outranks every rule ever written including
58
+ // the kill-switch, and a fractional tailLines slices a capture at a
59
+ // fraction of a line.
60
+ if (!Number.isSafeInteger(r.priority))
61
+ return false;
62
+ if (r.region !== undefined && r.region !== 'tail' && r.region !== 'whole')
63
+ return false;
64
+ if (r.tailLines !== undefined && (!Number.isSafeInteger(r.tailLines) || r.tailLines < 1 || r.tailLines > 200))
65
+ return false;
66
+ if (r.contains !== undefined && !isStringArray(r.contains))
67
+ return false;
68
+ if (r.regex !== undefined && !isStringArray(r.regex))
69
+ return false;
70
+ if (r.any !== undefined && (!Array.isArray(r.any) || !r.any.every(isCondition)))
71
+ return false;
72
+ if (r.not !== undefined && (!Array.isArray(r.not) || !r.not.every(isCondition)))
73
+ return false;
74
+ if (r.skipStateUpdate !== undefined && typeof r.skipStateUpdate !== 'boolean')
75
+ return false;
76
+ return true;
77
+ };
78
+ /**
79
+ * Structural validation of an untrusted manifest. Engine-version gate
80
+ * included: a manifest authored for a future engine is rejected whole —
81
+ * partial interpretation of unknown semantics is worse than falling
82
+ * back to bundled rules.
83
+ */
84
+ const validateManifest = (value) => {
85
+ if (typeof value !== 'object' || value === null)
86
+ return null;
87
+ const m = value;
88
+ if (m.engineVersion !== agent_state_js_1.ENGINE_VERSION)
89
+ return null;
90
+ if (typeof m.version !== 'string' || m.version.length === 0)
91
+ return null;
92
+ if (m.disabledRuleIds !== undefined && !isStringArray(m.disabledRuleIds))
93
+ return null;
94
+ if (typeof m.agents !== 'object' || m.agents === null)
95
+ return null;
96
+ for (const rules of Object.values(m.agents)) {
97
+ if (!Array.isArray(rules) || !rules.every(isRule))
98
+ return null;
99
+ }
100
+ return value;
101
+ };
102
+ exports.validateManifest = validateManifest;
103
+ /**
104
+ * Segment-wise numeric compare of dotted date versions
105
+ * ("2026.07.04.3"). Plain string compare fails on ".10" vs ".9";
106
+ * non-numeric segments compare as equal so a malformed version can
107
+ * never crash the refresh path.
108
+ */
109
+ const compareManifestVersions = (a, b) => {
110
+ const as = a.split('.').map(Number);
111
+ const bs = b.split('.').map(Number);
112
+ const len = Math.max(as.length, bs.length);
113
+ for (let i = 0; i < len; i++) {
114
+ const d = (as[i] ?? 0) - (bs[i] ?? 0);
115
+ if (Number.isNaN(d))
116
+ continue;
117
+ if (d !== 0)
118
+ return d < 0 ? -1 : 1;
119
+ }
120
+ return 0;
121
+ };
122
+ exports.compareManifestVersions = compareManifestVersions;
123
+ let activeManifest = agent_rules_default_js_1.DEFAULT_MANIFEST;
124
+ let activeSource = 'bundled';
125
+ const getActiveManifest = () => activeManifest;
126
+ exports.getActiveManifest = getActiveManifest;
127
+ const getActiveManifestSource = () => activeSource;
128
+ exports.getActiveManifestSource = getActiveManifestSource;
129
+ const activateManifest = (manifest, source) => {
130
+ activeManifest = manifest;
131
+ activeSource = source;
132
+ // Old manifest's compiled patterns must not pin memory forever.
133
+ (0, agent_state_js_1.clearRegexCache)();
134
+ };
135
+ /** Test hook. */
136
+ const resetActiveManifest = () => {
137
+ activateManifest(agent_rules_default_js_1.DEFAULT_MANIFEST, 'bundled');
138
+ };
139
+ exports.resetActiveManifest = resetActiveManifest;
140
+ /**
141
+ * Startup path (synchronous): promote the disk cache if it validates,
142
+ * else stay on bundled. Never throws — detection must work offline.
143
+ */
144
+ const loadManifestFromCache = () => {
145
+ try {
146
+ const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
147
+ const manifest = (0, exports.validateManifest)(cache.manifest);
148
+ if (manifest && (0, exports.compareManifestVersions)(manifest.version, agent_rules_default_js_1.DEFAULT_MANIFEST.version) >= 0) {
149
+ activateManifest(manifest, 'cache');
150
+ }
151
+ }
152
+ catch {
153
+ // Missing/corrupt cache — bundled rules carry the session.
154
+ }
155
+ return activeSource;
156
+ };
157
+ exports.loadManifestFromCache = loadManifestFromCache;
158
+ /** mtime of the cache file the last time `syncManifestFromCache` looked; null = no file. */
159
+ let cacheSeenMtime;
160
+ /**
161
+ * Worker-thread path. A `worker_threads` Worker gets its own module instance,
162
+ * so `loadManifestFromCache` on the main thread leaves the worker on
163
+ * `DEFAULT_MANIFEST`, which would miss a fetched manifest's newer rules and its
164
+ * `disabledRuleIds` kill-switch for the life of the daemon (2026-09-14: a pi
165
+ * session whose pane the same rules classified `working` in-thread, back when
166
+ * the bundle carried claude rules only). Call this before each sweep: it
167
+ * reloads the cache only when the file changed, so the main thread's refresh
168
+ * reaches the worker on its next sweep without any message-protocol change.
169
+ */
170
+ const syncManifestFromCache = () => {
171
+ let mtime;
172
+ try {
173
+ mtime = (0, fs_1.statSync)(exports.RULES_CACHE_FILE).mtimeMs;
174
+ }
175
+ catch {
176
+ mtime = null;
177
+ }
178
+ if (mtime === cacheSeenMtime)
179
+ return activeSource;
180
+ if (mtime === null) {
181
+ cacheSeenMtime = null;
182
+ return activeSource;
183
+ }
184
+ const source = (0, exports.loadManifestFromCache)();
185
+ // Latch only a read that landed. The cache is written without tmp+rename,
186
+ // so a stat/read from this thread can catch it half-written; a 304 refresh
187
+ // never rewrites the file, so latching that torn read would leave this
188
+ // thread on bundled rules for the daemon's life.
189
+ if (source === 'cache')
190
+ cacheSeenMtime = mtime;
191
+ return source;
192
+ };
193
+ exports.syncManifestFromCache = syncManifestFromCache;
194
+ const readCachedEtag = () => {
195
+ try {
196
+ const cache = JSON.parse((0, fs_1.readFileSync)(exports.RULES_CACHE_FILE, 'utf-8'));
197
+ return typeof cache.etag === 'string' ? cache.etag : undefined;
198
+ }
199
+ catch {
200
+ return undefined;
201
+ }
202
+ };
203
+ const rulesUrl = () => {
204
+ const override = (0, config_js_1.resolvedEnv)('HWIP_AGENT_RULES_URL');
205
+ if (override)
206
+ return override;
207
+ // The base the key goes to: a config file that cannot be read stops this (the refresh then keeps the
208
+ // current manifest) rather than fetching from the default stage while the account is on another.
209
+ const base = (0, config_js_1.resolveBaseUrl)(undefined, (0, config_js_1.loadConfig)());
210
+ return `${base.replace(/\/$/, '')}/agent-detection/manifest`;
211
+ };
212
+ exports.rulesUrl = rulesUrl;
213
+ /**
214
+ * One refresh attempt: conditional GET, validate, persist, activate.
215
+ * All failure modes degrade to the current active manifest.
216
+ */
217
+ const refreshManifest = async (fetchImpl = fetch) => {
218
+ let url;
219
+ try {
220
+ url = (0, exports.rulesUrl)();
221
+ }
222
+ catch (err) {
223
+ // The address itself is broken: saying so once beats an 'error' that reads as offline,
224
+ // and retrying a broken address on every tick would only repeat it.
225
+ console.error(`agent rules: ${err instanceof Error ? err.message : String(err)} — keeping the current manifest`);
226
+ return { source: activeSource, outcome: 'config-error' };
227
+ }
228
+ try {
229
+ const etag = readCachedEtag();
230
+ const res = await fetchImpl(url, {
231
+ headers: etag ? { 'If-None-Match': etag } : {},
232
+ signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
233
+ });
234
+ if (res.status === 304) {
235
+ return { source: activeSource, outcome: 'not-modified', version: activeManifest.version };
236
+ }
237
+ if (!res.ok) {
238
+ return { source: activeSource, outcome: 'error' };
239
+ }
240
+ const body = await res.text();
241
+ if (Buffer.byteLength(body, 'utf-8') > MAX_MANIFEST_BYTES) {
242
+ return { source: activeSource, outcome: 'invalid' };
243
+ }
244
+ const manifest = (0, exports.validateManifest)(JSON.parse(body));
245
+ if (!manifest) {
246
+ return { source: activeSource, outcome: 'invalid' };
247
+ }
248
+ const cache = {
249
+ etag: res.headers.get('etag') ?? undefined,
250
+ fetchedAt: new Date().toISOString(),
251
+ manifest,
252
+ };
253
+ try {
254
+ (0, private_files_js_1.mkdirHandedBack)(config_js_1.CONFIG_DIR);
255
+ (0, fs_1.writeFileSync)(exports.RULES_CACHE_FILE, JSON.stringify(cache, null, 2) + '\n');
256
+ (0, private_files_js_1.handBackToSudoUser)(exports.RULES_CACHE_FILE);
257
+ }
258
+ catch {
259
+ // Cache write failure is non-fatal: the manifest still
260
+ // activates for this process lifetime.
261
+ }
262
+ if ((0, exports.compareManifestVersions)(manifest.version, agent_rules_default_js_1.DEFAULT_MANIFEST.version) < 0) {
263
+ // Endpoint hasn't caught up to this build's bundled rules.
264
+ // Cache is still written above (the ETag stops refetch churn)
265
+ // but startup applies the same gate, so bundled stays active.
266
+ return { source: activeSource, outcome: 'stale-ignored', version: manifest.version };
267
+ }
268
+ activateManifest(manifest, 'remote');
269
+ return { source: 'remote', outcome: 'updated', version: manifest.version };
270
+ }
271
+ catch {
272
+ return { source: activeSource, outcome: 'error' };
273
+ }
274
+ };
275
+ exports.refreshManifest = refreshManifest;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Bundled default detection rules — the fallback when no OTA manifest
3
+ * has been fetched (or the fetched one fails validation, §S7).
4
+ *
5
+ * Rule content is authored HERE, from observation of each agent's UI.
6
+ * Do not import or transcribe rules from third-party projects: the
7
+ * bundled set is original work under this repo's license.
8
+ *
9
+ * Claude Code observations behind these rules (UI as of mid-2026):
10
+ * - While running, the status line shows an "esc to interrupt" hint
11
+ * next to the spinner/progress text.
12
+ * - Blocking dialogs (permission requests, plan approval, question
13
+ * forms) render a numbered/arrow-key option list with "esc" as the
14
+ * cancel affordance and Enter as the confirm affordance.
15
+ * - At rest, the input box renders a `❯` prompt and the footer offers
16
+ * "? for shortcuts".
17
+ * - The transcript viewer (ctrl+o) overlays the pane; the agent may
18
+ * still be working underneath, so it must not change the state.
19
+ *
20
+ * Every agent here carries the same rules the server manifest publishes
21
+ * (hwip server/src/agent-rules/manifest.json, version 2026.10.15.1 — hwip#484),
22
+ * so a listener that never reaches the endpoint classifies them too. A fetched
23
+ * manifest replaces this bundle whole for state reports; schedules read only
24
+ * this bundle. An agent without rules here
25
+ * reports `unknown`: honest ignorance beats guessed state, and
26
+ * harness-bench.test.ts fails the moment a rule set is half-filled.
27
+ */
28
+ import { type DetectionManifest } from './agent-state.js';
29
+ export declare const DEFAULT_MANIFEST: DetectionManifest;
30
+ //# sourceMappingURL=agent-rules.default.d.ts.map
@@ -0,0 +1,292 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_MANIFEST = void 0;
4
+ /**
5
+ * Bundled default detection rules — the fallback when no OTA manifest
6
+ * has been fetched (or the fetched one fails validation, §S7).
7
+ *
8
+ * Rule content is authored HERE, from observation of each agent's UI.
9
+ * Do not import or transcribe rules from third-party projects: the
10
+ * bundled set is original work under this repo's license.
11
+ *
12
+ * Claude Code observations behind these rules (UI as of mid-2026):
13
+ * - While running, the status line shows an "esc to interrupt" hint
14
+ * next to the spinner/progress text.
15
+ * - Blocking dialogs (permission requests, plan approval, question
16
+ * forms) render a numbered/arrow-key option list with "esc" as the
17
+ * cancel affordance and Enter as the confirm affordance.
18
+ * - At rest, the input box renders a `❯` prompt and the footer offers
19
+ * "? for shortcuts".
20
+ * - The transcript viewer (ctrl+o) overlays the pane; the agent may
21
+ * still be working underneath, so it must not change the state.
22
+ *
23
+ * Every agent here carries the same rules the server manifest publishes
24
+ * (hwip server/src/agent-rules/manifest.json, version 2026.10.15.1 — hwip#484),
25
+ * so a listener that never reaches the endpoint classifies them too. A fetched
26
+ * manifest replaces this bundle whole for state reports; schedules read only
27
+ * this bundle. An agent without rules here
28
+ * reports `unknown`: honest ignorance beats guessed state, and
29
+ * harness-bench.test.ts fails the moment a rule set is half-filled.
30
+ */
31
+ const agent_state_js_1 = require("./agent-state.js");
32
+ exports.DEFAULT_MANIFEST = {
33
+ engineVersion: agent_state_js_1.ENGINE_VERSION,
34
+ version: '2026.10.15.1',
35
+ agents: {
36
+ claude: [
37
+ {
38
+ // Transcript / verbose-output overlay: freeze state.
39
+ id: 'claude-transcript-overlay',
40
+ state: 'unknown',
41
+ priority: 1000,
42
+ skipStateUpdate: true,
43
+ any: [
44
+ { contains: ['showing detailed transcript'] },
45
+ { contains: ['ctrl+o to toggle'] },
46
+ ],
47
+ },
48
+ {
49
+ // Blocking dialog: an option list waiting on the user.
50
+ // "esc" alone is ambiguous (working shows "esc to
51
+ // interrupt"), so require a selection affordance too.
52
+ id: 'claude-blocked-dialog',
53
+ state: 'blocked',
54
+ priority: 900,
55
+ contains: ['esc'],
56
+ any: [
57
+ { contains: ['do you want'] },
58
+ { contains: ['enter to select'] },
59
+ { contains: ['enter to confirm'] },
60
+ { contains: ['to navigate'] },
61
+ ],
62
+ not: [{ contains: ['esc to interrupt'] }],
63
+ },
64
+ {
65
+ id: 'claude-working-interrupt-hint',
66
+ state: 'working',
67
+ priority: 800,
68
+ contains: ['esc to interrupt'],
69
+ },
70
+ {
71
+ // Newer CC skins drop the "esc to interrupt" hint; the
72
+ // live token counter on the spinner line
73
+ // ("✶ Churning… (1m 2s · ↓ 1.2k tokens)") only renders
74
+ // while the agent is actually running.
75
+ id: 'claude-working-token-counter',
76
+ state: 'working',
77
+ priority: 790,
78
+ regex: ['[↓↑]\\s?[\\d.,]+k? tokens'],
79
+ },
80
+ {
81
+ // Elapsed-time spinner suffix ("(12s · ..." / "(1m 2s ·")
82
+ // — second fallback for skins that hide the token counter
83
+ // early in a turn.
84
+ id: 'claude-working-elapsed-spinner',
85
+ state: 'working',
86
+ priority: 780,
87
+ regex: ['…\\s*\\((?:\\d+m )?\\d+s ·'],
88
+ },
89
+ {
90
+ // Compact pass ("✱ Compacting conversation… (51s)")
91
+ // renders neither the interrupt hint nor a token
92
+ // counter, but the session is busy and can't take
93
+ // input — without this it reads as idle.
94
+ id: 'claude-working-compacting',
95
+ state: 'working',
96
+ priority: 770,
97
+ contains: ['compacting conversation'],
98
+ },
99
+ {
100
+ // Idle prompt: `❯` at line start in the tail, with no
101
+ // dialog affordances around it.
102
+ id: 'claude-idle-prompt',
103
+ state: 'idle',
104
+ priority: 600,
105
+ regex: ['^\\s*❯'],
106
+ not: [
107
+ { contains: ['do you want'] },
108
+ { contains: ['enter to select'] },
109
+ ],
110
+ },
111
+ {
112
+ // Fallback idle signal when the prompt glyph is themed
113
+ // away: the shortcuts footer only renders at rest.
114
+ id: 'claude-idle-shortcuts-footer',
115
+ state: 'idle',
116
+ priority: 500,
117
+ contains: ['? for shortcuts'],
118
+ },
119
+ ],
120
+ codex: [
121
+ // Codex CLI v0.160.1, captured 2026-10-08 and 2026-10-15 (see
122
+ // harness-bench.test.ts). Its dialogs replace the composer and its
123
+ // footer with a `› 1.` option list over an `enter <verb> · esc …`
124
+ // line. At rest the composer sits over a `? for shortcuts` footer;
125
+ // while a turn runs both stay drawn under a `• Working (… • esc to
126
+ // interrupt)` line, so working must outrank idle.
127
+ //
128
+ // KNOWN GAP — no tool ever ran (the workspace was out of credits),
129
+ // so the tool-approval dialog was never drawn and has no rule. The
130
+ // idle rule refuses a tail holding the markers those dialogs and
131
+ // older Codex approval prompts draw — a `› 1.` option list, a
132
+ // `(y)`/`(n)` key hint, a `· esc …` footer, `enter to confirm` or
133
+ // `esc to cancel` — so an approval dialog that kept the footer
134
+ // reads `unknown` as long as it draws one of them. One that draws
135
+ // none would read `idle`; capture it before trusting this rule
136
+ // further.
137
+ {
138
+ id: 'codex-blocked-hooks-review',
139
+ state: 'blocked',
140
+ priority: 900,
141
+ contains: ['hooks need review', 'enter confirm'],
142
+ },
143
+ {
144
+ id: 'codex-blocked-usage-limit',
145
+ state: 'blocked',
146
+ priority: 900,
147
+ contains: ['usage limit reached', 'enter select'],
148
+ },
149
+ {
150
+ id: 'codex-blocked-update',
151
+ state: 'blocked',
152
+ priority: 900,
153
+ contains: ['update available', 'enter continue'],
154
+ },
155
+ {
156
+ id: 'codex-blocked-trust-dialog',
157
+ state: 'blocked',
158
+ priority: 900,
159
+ contains: ['trust this folder', 'enter continue'],
160
+ },
161
+ {
162
+ id: 'codex-working-interrupt-hint',
163
+ state: 'working',
164
+ priority: 800,
165
+ contains: ['esc to interrupt'],
166
+ },
167
+ {
168
+ // The footer is gone while a draft is typed, so a pane with
169
+ // unsent text reads `unknown` — the safe side for a schedule.
170
+ // 30 lines, not the default 10: a dialog marker drawn high
171
+ // above a kept footer must still be in view of `not`.
172
+ id: 'codex-idle-shortcuts-footer',
173
+ state: 'idle',
174
+ priority: 600,
175
+ tailLines: 30,
176
+ contains: ['? for shortcuts'],
177
+ not: [
178
+ { regex: ['^\\s*›\\s*\\d+\\.'] },
179
+ { regex: ['·\\s*esc '] },
180
+ { regex: ['enter (?:to )?(?:confirm|select|continue)'] },
181
+ { regex: ['esc to (?:cancel|deny)'] },
182
+ { regex: ['\\([yn]\\)\\s*$'] },
183
+ ],
184
+ },
185
+ ],
186
+ gemini: [
187
+ {
188
+ // KNOWN GAP — `blocked` here means the folder-trust dialog,
189
+ // the only blocked screen that was actually captured. Gemini's
190
+ // tool-approval prompt is the common blocked case in practice
191
+ // and has NO rule: the probe session had shell auto-approved,
192
+ // so it never rendered. Until someone captures it, a gemini
193
+ // session waiting on tool approval reports `working` (the
194
+ // spinner rule) or `idle`, not `blocked`. Do not read the
195
+ // bench matrix's green `blocked` cell as full coverage.
196
+ // Observed on Gemini CLI v0.46.0.
197
+ id: 'gemini-blocked-trust-dialog',
198
+ state: 'blocked',
199
+ priority: 900,
200
+ tailLines: 30,
201
+ contains: ['do you trust the files in this folder?'],
202
+ },
203
+ {
204
+ id: 'gemini-working-cancel-hint',
205
+ state: 'working',
206
+ priority: 800,
207
+ contains: ['esc to cancel'],
208
+ },
209
+ {
210
+ id: 'gemini-idle-input-placeholder',
211
+ state: 'idle',
212
+ priority: 600,
213
+ contains: ['type your message'],
214
+ not: [{ contains: ['esc to cancel'] }],
215
+ },
216
+ ],
217
+ opencode: [
218
+ // Hand-written from the screen strings recorded in the C136
219
+ // backlog row (opencode 1.18.35) and a user's permission screen
220
+ // (`Allow once Always allow Reject … enter confirm`), not
221
+ // from a pane capture, so the bench has no fixture for it and
222
+ // agent-state.test.ts pins the strings instead. The button
223
+ // wording differs between versions (`Allow always` vs `Always
224
+ // allow`), hence the rule keys on the `Permission required`
225
+ // title line, guarded by any button or footer string so the
226
+ // phrase in tool output or agent text alone is not a dialog. tailLines 30 because an edit request draws its diff
227
+ // between that line and the buttons. Other prompts (the agent
228
+ // asking a question) are not captured and report `unknown`.
229
+ // No idle rule: at rest the footer is only `ctrl+p commands`,
230
+ // which says nothing the absence of `esc interrupt` does not.
231
+ {
232
+ id: 'opencode-blocked-permission',
233
+ state: 'blocked',
234
+ priority: 900,
235
+ tailLines: 30,
236
+ contains: ['permission required'],
237
+ any: [
238
+ { contains: ['allow once'] },
239
+ { contains: ['reject'] },
240
+ { contains: ['enter confirm'] },
241
+ ],
242
+ },
243
+ {
244
+ id: 'opencode-working-interrupt-hint',
245
+ state: 'working',
246
+ priority: 800,
247
+ contains: ['esc interrupt'],
248
+ },
249
+ ],
250
+ pi: [
251
+ {
252
+ // Observed on pi v0.85.1. `blocked` was captured on a picker
253
+ // drawn through ctx.ui.custom by an ask_user_question
254
+ // extension (hint `↑↓ navigate • Enter select • Esc cancel`).
255
+ // pi core's own extension dialogs end their hint with
256
+ // `escape/ctrl+c cancel` under the default keybindings —
257
+ // matched by the same pattern but read from the vendor file,
258
+ // not captured. A user who rebinds tui.select.cancel changes
259
+ // that text and falls back to `working` or `idle`.
260
+ id: 'pi-blocked-dialog',
261
+ state: 'blocked',
262
+ priority: 900,
263
+ any: [
264
+ { regex: ['enter\\b.*\\besc(?:ape)?(?:/ctrl\\+c)?\\s+cancel\\s*$'] },
265
+ { regex: ['enter to \\w+ • esc to go back\\s*$'] },
266
+ ],
267
+ },
268
+ {
269
+ // pi's default Loader frames at the start of a line, bare or
270
+ // inside the editor's top border — anchored so a braille glyph
271
+ // mid-line in tool output does not count. An extension that
272
+ // swaps the indicator frames reports `idle` while working.
273
+ id: 'pi-working-spinner',
274
+ state: 'working',
275
+ priority: 800,
276
+ regex: ['^\\s*(?:─+\\s*)?[⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏] +[a-z]'],
277
+ },
278
+ {
279
+ // Keys on the footer's context-usage figure (`5.0%/1.0M`, or
280
+ // `?/1.0M` right after a compaction), which renders from the
281
+ // first frame, before any token count exists. The footer
282
+ // truncates its stats line with `...` when wider than the
283
+ // pane and this figure is its last item, so a pane too narrow
284
+ // for the stats reports `unknown` rather than `idle`.
285
+ id: 'pi-idle-context-footer',
286
+ state: 'idle',
287
+ priority: 600,
288
+ regex: ['(?:^|\\s)(?:\\d+(?:\\.\\d+)?%|\\?)/\\d+(?:\\.\\d+)?[km]?\\b'],
289
+ },
290
+ ],
291
+ },
292
+ };
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Stable agent-session grouping when running inside a tmux agent session, so a
3
+ * push or hook event files under the same session key as the listener's own
4
+ * pushes — surviving Claude session-UUID rotation, and letting the phone open
5
+ * the agent chat (with its live terminal and key row) instead of a bare push
6
+ * screen: `buildNotificationDeepLink` needs BOTH fields
7
+ * (apps/mobile/src/navigation/linking.ts).
8
+ *
9
+ * The device id MUST equal the listener's `computeListenerDeviceId`, so it's
10
+ * resolved the same way (machine-id hash → sticky file → hostname) via the
11
+ * shared read-only helper — NOT a bare hostname hash, which drifts from the
12
+ * listener's id when a machine id is readable and files the push under a
13
+ * non-matching session key.
14
+ *
15
+ * One definition for every sender in this package: `hwip notify` (hwip-hook.ts)
16
+ * and `hwip ask` (ask.ts). The MCP server has its own copy of the same rule
17
+ * (mcp-server/src/config.ts) because it is a separate package.
18
+ */
19
+ export declare const agentSessionContext: () => {
20
+ agentDeviceId: string;
21
+ agentSessionName: string;
22
+ } | null;
23
+ //# sourceMappingURL=agent-session.d.ts.map
@@ -0,0 +1,44 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.agentSessionContext = void 0;
4
+ const node_child_process_1 = require("node:child_process");
5
+ const listener_device_id_js_1 = require("./listener-device-id.js");
6
+ /**
7
+ * Stable agent-session grouping when running inside a tmux agent session, so a
8
+ * push or hook event files under the same session key as the listener's own
9
+ * pushes — surviving Claude session-UUID rotation, and letting the phone open
10
+ * the agent chat (with its live terminal and key row) instead of a bare push
11
+ * screen: `buildNotificationDeepLink` needs BOTH fields
12
+ * (apps/mobile/src/navigation/linking.ts).
13
+ *
14
+ * The device id MUST equal the listener's `computeListenerDeviceId`, so it's
15
+ * resolved the same way (machine-id hash → sticky file → hostname) via the
16
+ * shared read-only helper — NOT a bare hostname hash, which drifts from the
17
+ * listener's id when a machine id is readable and files the push under a
18
+ * non-matching session key.
19
+ *
20
+ * One definition for every sender in this package: `hwip notify` (hwip-hook.ts)
21
+ * and `hwip ask` (ask.ts). The MCP server has its own copy of the same rule
22
+ * (mcp-server/src/config.ts) because it is a separate package.
23
+ */
24
+ const agentSessionContext = () => {
25
+ if (!process.env.TMUX)
26
+ return null;
27
+ // A pi subagent pane carries its own wire name (<#S>.<pane> — the
28
+ // listener's naming) in this env, set by the extension for its notify
29
+ // children. It wins over #S, which would file the push under the parent.
30
+ const fromEnv = process.env.HWIP_AGENT_SESSION_NAME?.trim();
31
+ if (fromEnv)
32
+ return { agentDeviceId: (0, listener_device_id_js_1.listenerDeviceId)(), agentSessionName: fromEnv };
33
+ let name;
34
+ try {
35
+ name = (0, node_child_process_1.execFileSync)('tmux', ['display-message', '-p', '#S'], { encoding: 'utf-8' }).trim();
36
+ }
37
+ catch {
38
+ return null;
39
+ }
40
+ if (!name)
41
+ return null;
42
+ return { agentDeviceId: (0, listener_device_id_js_1.listenerDeviceId)(), agentSessionName: name };
43
+ };
44
+ exports.agentSessionContext = agentSessionContext;