@phnx-labs/agents-cli 1.20.54 → 1.20.56

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.
@@ -123,9 +123,10 @@ export declare function paneExitStatus(pane: string, socket?: string): Promise<P
123
123
  * Bind a per-session hook. Used by the spawn-wrap path to install a `pane-died`
124
124
  * hook that detaches the attach client the instant the wrapped agent exits (the
125
125
  * global `remain-on-exit on` otherwise leaves the client staring at a dead pane).
126
- * Best-effort — a failed hook just means the user Ctrl-b d's out manually.
126
+ * Best-effort — returns false when tmux rejects the hook so callers do not
127
+ * stamp a schema marker and daemon reconciliation can retry later.
127
128
  */
128
- export declare function setSessionHook(name: string, hook: string, command: string, socket?: string): Promise<void>;
129
+ export declare function setSessionHook(name: string, hook: string, command: string, socket?: string): Promise<boolean>;
129
130
  /**
130
131
  * Schema version of the `pane-died` hook installed on managed `agents run`
131
132
  * sessions. Bump whenever the hook's SHAPE changes so the daemon reconcile
@@ -135,15 +136,30 @@ export declare function setSessionHook(name: string, hook: string, command: stri
135
136
  * user exiting a split they opened) tore down the whole client.
136
137
  * v2 — `#{hook_pane}`-guarded: only the AGENT pane dying detaches; a user
137
138
  * split's death runs `kill-pane`, closing just that split.
139
+ * v3 — the else-branch pins its target via
140
+ * `run-shell "tmux -S <socket> kill-pane -t #{hook_pane}"`. Untargeted
141
+ * kill-pane resolves "current pane" inside the hook context, which goes
142
+ * nondeterministic on a loaded detached server (CI flake #965: the dead
143
+ * split survived as a husk); run-shell format-expands its command at
144
+ * fire time, so the event pane is always the target.
145
+ * v4 — `run-shell -C "kill-pane -t #{hook_pane}"` executes the targeted command
146
+ * in the tmux server instead of launching a second tmux client against
147
+ * the same socket from inside the hook. That self-client could race the
148
+ * server under load and leave the dead split behind.
138
149
  */
139
- export declare const AGENT_HOOK_SCHEMA = 2;
150
+ export declare const AGENT_HOOK_SCHEMA = 4;
140
151
  /**
141
152
  * The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
142
153
  * (so the blocking attach in runInTmux returns and the exit status can be read);
143
- * a user split's death falls through to `kill-pane`, which because the hook
144
- * runs in the dead pane's context closes that split in place. Single source of
145
- * truth: both the spawn-wrap (exec.ts) and the daemon reconcile build the hook
146
- * here, so the two can never drift.
154
+ * a user split's death runs the else-branch, closing just that split. The
155
+ * else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
156
+ * target: tmux format-expands the command at fire time and executes it inside
157
+ * the server command queue, so the event pane is always the one killed without
158
+ * launching a second tmux client against the same socket. A bare `kill-pane`
159
+ * relied on the hook context supplying a "current pane", while an external
160
+ * self-client could race the server under load. Single source of truth: both
161
+ * the spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
162
+ * two can never drift.
147
163
  */
148
164
  export declare function agentPaneDiedHook(sessionName: string, agentPane: string): string;
149
165
  /** Stamp a session's hook-schema marker to the current version. */
@@ -262,12 +262,18 @@ export async function paneExitStatus(pane, socket) {
262
262
  * Bind a per-session hook. Used by the spawn-wrap path to install a `pane-died`
263
263
  * hook that detaches the attach client the instant the wrapped agent exits (the
264
264
  * global `remain-on-exit on` otherwise leaves the client staring at a dead pane).
265
- * Best-effort — a failed hook just means the user Ctrl-b d's out manually.
265
+ * Best-effort — returns false when tmux rejects the hook so callers do not
266
+ * stamp a schema marker and daemon reconciliation can retry later.
266
267
  */
267
268
  export async function setSessionHook(name, hook, command, socket) {
268
269
  assertValidSessionName(name);
269
270
  const sock = socket ?? getDefaultSocketPath();
270
- await runTmux({ socket: sock, args: ['set-hook', '-t', name, hook, command], throwOnError: false }).catch(() => { });
271
+ const result = await runTmux({
272
+ socket: sock,
273
+ args: ['set-hook', '-t', name, hook, command],
274
+ throwOnError: false,
275
+ }).catch(() => null);
276
+ return result?.code === 0;
271
277
  }
272
278
  /**
273
279
  * Schema version of the `pane-died` hook installed on managed `agents run`
@@ -278,20 +284,35 @@ export async function setSessionHook(name, hook, command, socket) {
278
284
  * user exiting a split they opened) tore down the whole client.
279
285
  * v2 — `#{hook_pane}`-guarded: only the AGENT pane dying detaches; a user
280
286
  * split's death runs `kill-pane`, closing just that split.
287
+ * v3 — the else-branch pins its target via
288
+ * `run-shell "tmux -S <socket> kill-pane -t #{hook_pane}"`. Untargeted
289
+ * kill-pane resolves "current pane" inside the hook context, which goes
290
+ * nondeterministic on a loaded detached server (CI flake #965: the dead
291
+ * split survived as a husk); run-shell format-expands its command at
292
+ * fire time, so the event pane is always the target.
293
+ * v4 — `run-shell -C "kill-pane -t #{hook_pane}"` executes the targeted command
294
+ * in the tmux server instead of launching a second tmux client against
295
+ * the same socket from inside the hook. That self-client could race the
296
+ * server under load and leave the dead split behind.
281
297
  */
282
- export const AGENT_HOOK_SCHEMA = 2;
298
+ export const AGENT_HOOK_SCHEMA = 4;
283
299
  /** Per-session tmux user-option that records which AGENT_HOOK_SCHEMA a session's hook is at. */
284
300
  const HOOK_SCHEMA_OPTION = '@ag_hook_schema';
285
301
  /**
286
302
  * The guarded `pane-died` hook. Detach the client ONLY when the agent pane dies
287
303
  * (so the blocking attach in runInTmux returns and the exit status can be read);
288
- * a user split's death falls through to `kill-pane`, which because the hook
289
- * runs in the dead pane's context closes that split in place. Single source of
290
- * truth: both the spawn-wrap (exec.ts) and the daemon reconcile build the hook
291
- * here, so the two can never drift.
304
+ * a user split's death runs the else-branch, closing just that split. The
305
+ * else-branch goes through `run-shell -C` with an explicit `-t #{hook_pane}`
306
+ * target: tmux format-expands the command at fire time and executes it inside
307
+ * the server command queue, so the event pane is always the one killed without
308
+ * launching a second tmux client against the same socket. A bare `kill-pane`
309
+ * relied on the hook context supplying a "current pane", while an external
310
+ * self-client could race the server under load. Single source of truth: both
311
+ * the spawn-wrap (exec.ts) and the daemon reconcile build the hook here, so the
312
+ * two can never drift.
292
313
  */
293
314
  export function agentPaneDiedHook(sessionName, agentPane) {
294
- return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'kill-pane'`;
315
+ return `if -F '#{==:#{hook_pane},${agentPane}}' 'detach-client -s =${sessionName}' 'run-shell -C "kill-pane -t #{hook_pane}"'`;
295
316
  }
296
317
  /** Stamp a session's hook-schema marker to the current version. */
297
318
  export async function markSessionHookSchema(name, socket) {
@@ -355,7 +376,9 @@ export async function reconcileSessionHooks(socket) {
355
376
  const agentPane = s.meta?.pane ?? await lowestPaneId(s.name, sock);
356
377
  if (!agentPane)
357
378
  continue;
358
- await setSessionHook(s.name, 'pane-died', agentPaneDiedHook(s.name, agentPane), sock);
379
+ const installed = await setSessionHook(s.name, 'pane-died', agentPaneDiedHook(s.name, agentPane), sock);
380
+ if (!installed)
381
+ continue;
359
382
  await markSessionHookSchema(s.name, sock);
360
383
  reconciled++;
361
384
  }
@@ -12,7 +12,7 @@
12
12
  * The optional http listener (`startWebhookServer`) is a thin adapter over it.
13
13
  */
14
14
  import * as http from 'http';
15
- import { listJobs } from '../routines.js';
15
+ import { listJobs, jobRunsOnThisDevice } from '../routines.js';
16
16
  import { executeJobDetached } from '../runner.js';
17
17
  /** Read `repository.full_name` (`owner/name`) from a webhook payload, if present. */
18
18
  export function webhookRepo(payload) {
@@ -85,7 +85,7 @@ export function jobMatchesWebhook(job, webhook) {
85
85
  * time-based jobs are unaffected by webhook delivery.
86
86
  */
87
87
  export function matchJobsToWebhook(jobs, webhook) {
88
- return jobs.filter((job) => job.enabled !== false && jobMatchesWebhook(job, webhook));
88
+ return jobs.filter((job) => job.enabled !== false && jobRunsOnThisDevice(job) && jobMatchesWebhook(job, webhook));
89
89
  }
90
90
  /**
91
91
  * Match an incoming webhook against the persisted routines and fire each match
@@ -439,6 +439,12 @@ export interface SkillEntry {
439
439
  tags?: string[];
440
440
  /** Registry-specific trust signal (e.g. 'builtin', 'trusted', 'community'). */
441
441
  trustLevel?: string;
442
+ /**
443
+ * Lowercase hex sha256 of the skill's SKILL.md, as recorded by
444
+ * `agents publish`. When present, install verifies the cloned SKILL.md
445
+ * against it and aborts on mismatch.
446
+ */
447
+ sha256?: string;
442
448
  }
443
449
  /** Paginated response from a skill registry search endpoint. */
444
450
  export interface SkillRegistryResponse {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.54",
3
+ "version": "1.20.56",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",