@hwip/cli 0.1.11 → 0.1.13
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 +8 -3
- package/dist/templates.d.ts +10 -1
- package/dist/templates.js +13 -4
- package/dist/turn-recorder-wire.js +1 -1
- package/dist/turn-recorder.d.ts +2 -2
- package/dist/turn-recorder.js +10 -2
- package/dist/worktree.d.ts +9 -3
- package/dist/worktree.js +16 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -239,13 +239,16 @@ back when it writes again.
|
|
|
239
239
|
Detection is exact-match: a terminal keystroke racing a phone message
|
|
240
240
|
can never false-flag. Muted projects are never flagged.
|
|
241
241
|
|
|
242
|
-
Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider
|
|
242
|
+
Agents without that hook (Cursor, Windsurf, Copilot, Cline, Aider) have one
|
|
243
243
|
other way in: a `hwip_ask` answer reporting `hwipState: "REMOTE"`. Their rule
|
|
244
244
|
files therefore keep the after-real-work `hwip_ask` — the "Entering REMOTE
|
|
245
245
|
without a prompt hook" preamble in `src/templates.ts` — so the phone always
|
|
246
246
|
has a button to tap that starts the loop. The one exception is an answer sent
|
|
247
247
|
with the phone's "send and exit": that turn carries out the instruction and
|
|
248
|
-
ends without an ask.
|
|
248
|
+
ends without an ask. OpenCode has no prompt hook either but leaves the
|
|
249
|
+
preamble out: its after-work ask reached the phone on turns typed at the
|
|
250
|
+
terminal, so it owes no ask in NORMAL, and a phone message to an OpenCode
|
|
251
|
+
session gets the completion push rather than buttons. Agents with the hook drop that
|
|
249
252
|
obligation while at the terminal — and their rule file carries only the
|
|
250
253
|
NORMAL branch of the core (about a quarter of it), since it is read on every
|
|
251
254
|
turn; the hook injects the REMOTE sections in full on the turn a phone
|
|
@@ -685,7 +688,9 @@ A tmux name is used again when an agent starts in the same folder, so each
|
|
|
685
688
|
chunk also names its **run** — the tmux session it was recorded in (`runId`,
|
|
686
689
|
tmux's `session_created` in epoch seconds). The app uses it to keep one run's
|
|
687
690
|
conversation apart from the one before it under the same name; a chunk read
|
|
688
|
-
after a restart keeps the run it was recorded in.
|
|
691
|
+
after a restart keeps the run it was recorded in. What a session writes with no
|
|
692
|
+
run to name (a name whose tmux session has ended while its file is still written
|
|
693
|
+
to) is not recorded: the server takes no chunk without a run. The listener's record of past
|
|
689
694
|
sessions keeps runs apart the same way: closing `hwip cc` and starting it again
|
|
690
695
|
in the same folder leaves the first run as a past session of its own (the
|
|
691
696
|
newest ten runs of a name are kept, 100 in all, and up to 50 are reported to
|
package/dist/templates.d.ts
CHANGED
|
@@ -16,7 +16,16 @@ export declare const CLINE_RULE: string;
|
|
|
16
16
|
export declare const AIDER_RULE: string;
|
|
17
17
|
/** Pi — managed block in ~/.pi/agent/AGENTS.md. Extension = Stop-equivalent + prompt hook + waiting-on-you push (PI_EXTENSION), so NORMAL-only core. */
|
|
18
18
|
export declare const PI_RULE: string;
|
|
19
|
-
/**
|
|
19
|
+
/**
|
|
20
|
+
* OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook.
|
|
21
|
+
*
|
|
22
|
+
* No REMOTE_ENTRY_NO_HOOK, unlike the other hook-less agents: its after-work
|
|
23
|
+
* ask landed on the phone while the user sat at the terminal (2026-10-29,
|
|
24
|
+
* OpenCode 2.1.3), and the 2.x plugin API (event stream only) has no per-turn
|
|
25
|
+
* hook to tell the two apart. So a NORMAL session owes no ask, and REMOTE
|
|
26
|
+
* starts only from an ask the agent chose to send. Full core, because no hook
|
|
27
|
+
* delivers the REMOTE sections on entry.
|
|
28
|
+
*/
|
|
20
29
|
export declare const OPENCODE_RULE: string;
|
|
21
30
|
export declare const CURSOR_HOOKS: {
|
|
22
31
|
version: number;
|
package/dist/templates.js
CHANGED
|
@@ -21,7 +21,8 @@
|
|
|
21
21
|
// hook note (a phone message) or by a `hwip_ask` answer. Only Gemini, Codex
|
|
22
22
|
// and Pi have that hook (remoteHookCmd below). Every other agent's only door is the
|
|
23
23
|
// ask itself — so for them the NORMAL branch must still send one after real
|
|
24
|
-
// work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK
|
|
24
|
+
// work, or the phone loop can never begin. That is REMOTE_ENTRY_NO_HOOK
|
|
25
|
+
// (OpenCode excepted — OPENCODE_RULE says why).
|
|
25
26
|
//
|
|
26
27
|
// The same axis decides how much of the core the static rule file carries.
|
|
27
28
|
// That file is read on every turn, and a session is NORMAL nearly all of the
|
|
@@ -125,7 +126,7 @@ refactor, multi-file changes) call hwip_notify. Skip it for trivial
|
|
|
125
126
|
operations (file reads, simple searches). Set priority "high" for
|
|
126
127
|
errors/blockers.`;
|
|
127
128
|
// REMOTE-entry preamble — agents with NO prompt-submit hook (Cursor, Windsurf,
|
|
128
|
-
// Copilot, Cline, Aider,
|
|
129
|
+
// Copilot, Cline, Aider; OpenCode opts out, see OPENCODE_RULE). The shared core scopes Rules 1/2/8/9 to REMOTE
|
|
129
130
|
// and tells a NORMAL session it owes no `hwip_ask`. That is right where a
|
|
130
131
|
// hook can announce the phone message that starts REMOTE; here nothing can,
|
|
131
132
|
// and the only remaining entry is a `hwip_ask` answer reporting REMOTE.
|
|
@@ -266,10 +267,18 @@ exports.PI_RULE = buildRule({
|
|
|
266
267
|
pushSignal: PI_PUSH_SIGNAL,
|
|
267
268
|
core: PROMPT_HOOK_CORE,
|
|
268
269
|
});
|
|
269
|
-
/**
|
|
270
|
+
/**
|
|
271
|
+
* OpenCode — managed block in ~/.config/opencode/AGENTS.md. Stop hook via plugin, no prompt hook.
|
|
272
|
+
*
|
|
273
|
+
* No REMOTE_ENTRY_NO_HOOK, unlike the other hook-less agents: its after-work
|
|
274
|
+
* ask landed on the phone while the user sat at the terminal (2026-10-29,
|
|
275
|
+
* OpenCode 2.1.3), and the 2.x plugin API (event stream only) has no per-turn
|
|
276
|
+
* hook to tell the two apart. So a NORMAL session owes no ask, and REMOTE
|
|
277
|
+
* starts only from an ask the agent chose to send. Full core, because no hook
|
|
278
|
+
* delivers the REMOTE sections on entry.
|
|
279
|
+
*/
|
|
270
280
|
exports.OPENCODE_RULE = buildRule({
|
|
271
281
|
notify: HOOK_DRIVEN_NOTIFY,
|
|
272
|
-
remoteEntry: REMOTE_ENTRY_NO_HOOK,
|
|
273
282
|
core: hwip_core_generated_js_1.HWIP_CORE_HOOK_DRIVEN,
|
|
274
283
|
});
|
|
275
284
|
// ── Hook configs ─────────────────────────────────────────────────
|
|
@@ -98,7 +98,7 @@ const createChunkUploader = (opts) => {
|
|
|
98
98
|
try {
|
|
99
99
|
res = await (0, api_js_1.apiFetch)(fetchFn, (0, api_js_1.apiUrl)(opts.baseUrl, `/sessions/${encodeURIComponent(sessionKey)}/turns`), opts.apiKey, UPLOAD_TIMEOUT_MS, {
|
|
100
100
|
method: 'POST',
|
|
101
|
-
body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt,
|
|
101
|
+
body: JSON.stringify({ seq: chunk.seq, firstAt: chunk.firstAt, lastAt: chunk.lastAt, runId: chunk.runId, sealed: chunk.sealed }),
|
|
102
102
|
});
|
|
103
103
|
}
|
|
104
104
|
catch (err) {
|
package/dist/turn-recorder.d.ts
CHANGED
|
@@ -98,8 +98,8 @@ export interface OutboxChunk {
|
|
|
98
98
|
seq: number;
|
|
99
99
|
firstAt: string;
|
|
100
100
|
lastAt: string;
|
|
101
|
-
/** The run the events were recorded in
|
|
102
|
-
runId
|
|
101
|
+
/** The run the events were recorded in: the server takes no chunk without one. */
|
|
102
|
+
runId: string;
|
|
103
103
|
sealed: SealedChunk;
|
|
104
104
|
}
|
|
105
105
|
export type UploadOutcome =
|
package/dist/turn-recorder.js
CHANGED
|
@@ -90,7 +90,7 @@ const isOutboxChunk = (c) => {
|
|
|
90
90
|
const o = c;
|
|
91
91
|
const s = o.sealed;
|
|
92
92
|
return (Number.isSafeInteger(o.seq) && typeof o.firstAt === 'string' && typeof o.lastAt === 'string' &&
|
|
93
|
-
!!s && typeof s.body === 'string' && typeof s.senderPublicKey === 'string' &&
|
|
93
|
+
typeof o.runId === 'string' && !!o.runId && !!s && typeof s.body === 'string' && typeof s.senderPublicKey === 'string' &&
|
|
94
94
|
!!s.deviceKeyMap && typeof s.deviceKeyMap === 'object');
|
|
95
95
|
};
|
|
96
96
|
/** What is on disk, or null when it is missing or not ours (a bad file is a fresh start, never a crash). */
|
|
@@ -273,13 +273,21 @@ const createTurnRecorder = (deps) => {
|
|
|
273
273
|
const chunks = [];
|
|
274
274
|
const runId = runOf(live, live.tailPath);
|
|
275
275
|
let seq = base.seq;
|
|
276
|
+
// No run to name (a session name that ended, its file still written to): the server takes no such chunk,
|
|
277
|
+
// so what was read is passed over rather than sealed and sent to be refused.
|
|
278
|
+
if (!runId && !live.noRunWarned) {
|
|
279
|
+
live.noRunWarned = true;
|
|
280
|
+
deps.log(`⧉ turn-recorder ${live.name}: no run to tag its chunks with — what it writes is not recorded`);
|
|
281
|
+
}
|
|
276
282
|
try {
|
|
277
283
|
for (const page of pages) {
|
|
284
|
+
if (!runId)
|
|
285
|
+
break;
|
|
278
286
|
seq += 1;
|
|
279
287
|
const firstAt = eventAt(page, 'first', fallbackAt);
|
|
280
288
|
const lastAt = eventAt(page, 'last', fallbackAt);
|
|
281
289
|
const sealed = await deps.seal(JSON.stringify({ sessionKey: live.sessionKey, seq, events: page }), recipients);
|
|
282
|
-
chunks.push({ seq, firstAt, lastAt,
|
|
290
|
+
chunks.push({ seq, firstAt, lastAt, runId, sealed });
|
|
283
291
|
}
|
|
284
292
|
}
|
|
285
293
|
catch (err) {
|
package/dist/worktree.d.ts
CHANGED
|
@@ -136,9 +136,15 @@ export interface WorktreeStatus {
|
|
|
136
136
|
/** What `hwip worktree` shows: every row with its state now. Read-only. */
|
|
137
137
|
export declare const worktreeStatuses: (paneCwds: string[], run?: GitRun) => Promise<WorktreeStatus[]>;
|
|
138
138
|
/**
|
|
139
|
-
*
|
|
140
|
-
* tmux could not be asked. The difference matters: an
|
|
141
|
-
* empty would make every worktree in use look abandoned.
|
|
139
|
+
* Where every live tmux pane started and where it is now, `[]` when there is no
|
|
140
|
+
* tmux server, null when tmux could not be asked. The difference matters: an
|
|
141
|
+
* unreadable list read as empty would make every worktree in use look abandoned.
|
|
142
|
+
*
|
|
143
|
+
* The start path (`new -c`) is read as well as the current one because the
|
|
144
|
+
* current one is not yet the pane's right after it starts: measured on tmux 3.6
|
|
145
|
+
* (ubuntu-26.04 runner), a pane read within ~10 ms of `new-session -c <dir>`
|
|
146
|
+
* reports the tmux server's cwd. The start path is the `-c` text unresolved,
|
|
147
|
+
* unlike the current one, so it is resolved like the worktree paths.
|
|
142
148
|
*/
|
|
143
149
|
export declare const readPaneCwds: (tmux?: GitRun) => Promise<string[] | null>;
|
|
144
150
|
//# sourceMappingURL=worktree.d.ts.map
|
package/dist/worktree.js
CHANGED
|
@@ -297,14 +297,23 @@ exports.worktreeStatuses = worktreeStatuses;
|
|
|
297
297
|
// `error connecting to <socket> (No such file or directory)`; `(Permission denied)` and the rest are not "no panes".
|
|
298
298
|
const NO_TMUX_SERVER = /^(no server running on |error connecting to .+ \(No such file or directory\)$)/;
|
|
299
299
|
/**
|
|
300
|
-
*
|
|
301
|
-
* tmux could not be asked. The difference matters: an
|
|
302
|
-
* empty would make every worktree in use look abandoned.
|
|
300
|
+
* Where every live tmux pane started and where it is now, `[]` when there is no
|
|
301
|
+
* tmux server, null when tmux could not be asked. The difference matters: an
|
|
302
|
+
* unreadable list read as empty would make every worktree in use look abandoned.
|
|
303
|
+
*
|
|
304
|
+
* The start path (`new -c`) is read as well as the current one because the
|
|
305
|
+
* current one is not yet the pane's right after it starts: measured on tmux 3.6
|
|
306
|
+
* (ubuntu-26.04 runner), a pane read within ~10 ms of `new-session -c <dir>`
|
|
307
|
+
* reports the tmux server's cwd. The start path is the `-c` text unresolved,
|
|
308
|
+
* unlike the current one, so it is resolved like the worktree paths.
|
|
303
309
|
*/
|
|
304
310
|
const readPaneCwds = async (tmux = exports.tmuxRun) => {
|
|
305
|
-
const out = await tmux(['list-panes', '-a', '-F', '#{pane_current_path}']);
|
|
306
|
-
if (out.ok)
|
|
307
|
-
return
|
|
308
|
-
return
|
|
311
|
+
const out = await tmux(['list-panes', '-a', '-F', '#{pane_start_path}\t#{pane_current_path}']);
|
|
312
|
+
if (!out.ok)
|
|
313
|
+
return NO_TMUX_SERVER.test(out.error) ? [] : null;
|
|
314
|
+
return out.stdout.split('\n').filter(Boolean).flatMap((line) => {
|
|
315
|
+
const [start = '', current = ''] = line.split('\t');
|
|
316
|
+
return [start && realOrSelf(start), current].filter(Boolean);
|
|
317
|
+
});
|
|
309
318
|
};
|
|
310
319
|
exports.readPaneCwds = readPaneCwds;
|