@zswarm/core 0.2.6 → 0.2.7

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/dist/harness.d.ts CHANGED
@@ -1,6 +1,17 @@
1
1
  export type SubmitStrategy = "auto" | "double-enter";
2
+ /**
3
+ * How to ask a harness to quit its own UI. A slash command is typed and
4
+ * submitted; keys are sent raw. `null` means the restart op falls back to
5
+ * Ctrl+C instead of guessing.
6
+ */
7
+ export type HarnessExitRecipe = {
8
+ /** A command line to type into the harness composer, e.g. "/exit". */
9
+ command?: string;
10
+ /** Raw keys to send, e.g. ["Ctrl c"]. */
11
+ keys?: readonly string[];
12
+ };
2
13
  export type HarnessProfile = {
3
- /** Stable id: "codex" | "cursor" | "opencode" | "gemini" | "pi" | "unknown" */
14
+ /** Stable id: "codex" | "cursor" | "opencode" | "gemini" | "pi" | "claude" | "unknown" */
4
15
  name: string;
5
16
  /** What `send` should do when the caller did not pass submit= explicitly. */
6
17
  submit: SubmitStrategy;
@@ -19,6 +30,12 @@ export type HarnessProfile = {
19
30
  * these against the trailing lines to avoid treating a busy pane as idle.
20
31
  */
21
32
  busy: readonly RegExp[];
33
+ /** Program to type in a shell pane to relaunch this harness; null when unknown. */
34
+ launch: string | null;
35
+ /** How to ask the harness to quit; null falls back to Ctrl+C. */
36
+ exit: HarnessExitRecipe | null;
37
+ /** Trailing lines showing the harness finished starting and is ready for input. */
38
+ ready: readonly RegExp[];
22
39
  };
23
40
  /**
24
41
  * Identify the harness from a pane's command and title, case-insensitively.
@@ -35,7 +52,15 @@ export declare function resolveHarness(pane: {
35
52
  */
36
53
  export declare function queuedPrompt(screen: string, profile: HarnessProfile): boolean;
37
54
  /**
38
- * Does this screen's trailing lines show the harness actively working? Only
55
+ * The busy marker regex a screen shows on its trailing lines, or null. Only
39
56
  * the last few lines count: a quoted marker in scrollback is not a live turn.
40
57
  */
58
+ export declare function busyMarker(screen: string, profile: HarnessProfile): RegExp | null;
59
+ /** Does this screen's trailing lines show the harness actively working? */
41
60
  export declare function busyPrompt(screen: string, profile: HarnessProfile): boolean;
61
+ /**
62
+ * Does this screen's trailing lines show the harness ready for input? An empty
63
+ * ready set is unknown, never a match: a profile that never declared a marker
64
+ * must not make the restart op wait forever.
65
+ */
66
+ export declare function readyPrompt(screen: string, profile: HarnessProfile): boolean;
package/dist/harness.js CHANGED
@@ -5,6 +5,7 @@ const PROFILES = [
5
5
  { name: "opencode", command: ["opencode"], title: ["agent-opencode"] },
6
6
  { name: "gemini", command: ["agy", "gemini"], title: ["agent-gemini"] },
7
7
  { name: "pi", command: ["pi"], title: ["agent-pi"] },
8
+ { name: "claude", command: ["claude"], title: ["agent-claude"] },
8
9
  ];
9
10
  /**
10
11
  * Only codex needs a second Enter to submit: its composer keeps the pasted
@@ -17,6 +18,7 @@ const SUBMIT = {
17
18
  opencode: "auto",
18
19
  gemini: "auto",
19
20
  pi: "auto",
21
+ claude: "auto",
20
22
  };
21
23
  /**
22
24
  * Prompt shapes any CLI can show, so every profile carries these as a base.
@@ -50,6 +52,7 @@ const WAITING = {
50
52
  /Accept this file edit\?/i,
51
53
  ],
52
54
  pi: GENERIC_WAITING,
55
+ claude: [...GENERIC_WAITING, /Do you want to (?:proceed|continue)\?/i],
53
56
  };
54
57
  /**
55
58
  * Queued-message hints seen live. Claude Code prints
@@ -68,6 +71,7 @@ const QUEUED = {
68
71
  opencode: GENERIC_QUEUED,
69
72
  gemini: GENERIC_QUEUED,
70
73
  pi: GENERIC_QUEUED,
74
+ claude: GENERIC_QUEUED,
71
75
  };
72
76
  /**
73
77
  * Busy (working) markers seen live. OpenCode draws a progress bar of filled
@@ -80,6 +84,47 @@ const BUSY = {
80
84
  opencode: [/[■⬝]{4,}/],
81
85
  gemini: [],
82
86
  pi: [],
87
+ claude: [/esc to interrupt/i],
88
+ };
89
+ /**
90
+ * Program to type in a shell pane to start this harness. The first entry of the
91
+ * harness's command needle is the executable a shell would accept.
92
+ */
93
+ const LAUNCH = {
94
+ codex: "codex",
95
+ cursor: "cursor-agent",
96
+ opencode: "opencode",
97
+ gemini: "agy",
98
+ pi: "pi",
99
+ claude: "claude",
100
+ };
101
+ /**
102
+ * How to ask a harness to quit its own UI. `/exit` is documented for OpenCode
103
+ * and Claude Code; the rest have no equally reliable in-band quit, so they carry
104
+ * `null` and the restart op falls back to Ctrl+C (twice) after a bounded wait.
105
+ */
106
+ const EXIT = {
107
+ codex: null,
108
+ cursor: null,
109
+ opencode: { command: "/exit" },
110
+ gemini: null,
111
+ pi: null,
112
+ claude: { command: "/exit" },
113
+ };
114
+ /**
115
+ * A trailing line that means the harness finished starting and accepts input.
116
+ * OpenCode's idle screen pins its composer placeholder ("Ask anything…") and
117
+ * footer ("ctrl+p commands"); Claude Code pins "? for shortcuts". The profiles
118
+ * whose composer is not known well enough carry `[]`, and the restart op then
119
+ * falls back to waiting for the screen to settle instead of guessing.
120
+ */
121
+ const READY = {
122
+ codex: [],
123
+ cursor: [],
124
+ opencode: [/ask anything/i, /ctrl\+p commands/i],
125
+ gemini: [],
126
+ pi: [],
127
+ claude: [/\? for shortcuts/i, /bypass permissions on/i],
83
128
  };
84
129
  const UNKNOWN = {
85
130
  name: "unknown",
@@ -87,6 +132,9 @@ const UNKNOWN = {
87
132
  waiting: GENERIC_WAITING,
88
133
  queued: GENERIC_QUEUED,
89
134
  busy: [/esc to interrupt/i],
135
+ launch: null,
136
+ exit: null,
137
+ ready: [],
90
138
  };
91
139
  function escapeRegExp(text) {
92
140
  return text.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
@@ -137,6 +185,9 @@ export function resolveHarness(pane) {
137
185
  waiting: WAITING[name],
138
186
  queued: QUEUED[name],
139
187
  busy: BUSY[name],
188
+ launch: LAUNCH[name],
189
+ exit: EXIT[name],
190
+ ready: READY[name],
140
191
  };
141
192
  }
142
193
  /**
@@ -148,12 +199,27 @@ export function queuedPrompt(screen, profile) {
148
199
  return profile.queued.some((re) => lines.some((line) => re.test(line)));
149
200
  }
150
201
  /**
151
- * Does this screen's trailing lines show the harness actively working? Only
202
+ * The busy marker regex a screen shows on its trailing lines, or null. Only
152
203
  * the last few lines count: a quoted marker in scrollback is not a live turn.
153
204
  */
205
+ export function busyMarker(screen, profile) {
206
+ const lines = trailingLines(screen);
207
+ return profile.busy.find((re) => lines.some((line) => re.test(line))) ?? null;
208
+ }
209
+ /** Does this screen's trailing lines show the harness actively working? */
154
210
  export function busyPrompt(screen, profile) {
211
+ return busyMarker(screen, profile) !== null;
212
+ }
213
+ /**
214
+ * Does this screen's trailing lines show the harness ready for input? An empty
215
+ * ready set is unknown, never a match: a profile that never declared a marker
216
+ * must not make the restart op wait forever.
217
+ */
218
+ export function readyPrompt(screen, profile) {
219
+ if (profile.ready.length === 0)
220
+ return false;
155
221
  const lines = trailingLines(screen);
156
- return profile.busy.some((re) => lines.some((line) => re.test(line)));
222
+ return profile.ready.some((re) => lines.some((line) => re.test(line)));
157
223
  }
158
224
  /** The six last non-empty, right-trimmed lines of a screen, CR stripped. */
159
225
  function trailingLines(screen) {
package/dist/host/cli.js CHANGED
@@ -114,7 +114,7 @@ function leaseTable(leases) {
114
114
  return [header, ...body].map((row) => row.map((cell, i) => cell.padEnd(widths[i])).join(" ").trimEnd()).join("\n");
115
115
  }
116
116
  async function relay(argv) {
117
- const { flags } = parseHostArgs(argv, ["--foreground", "--prune", "--list", "--leases", "--cancel-all", "--json", "--queue", "--force"]);
117
+ const { flags } = parseHostArgs(argv, ["--foreground", "--prune", "--list", "--leases", "--cancel-all", "--json", "--queue", "--force", "--if-idle"]);
118
118
  const run = str(flags["--run"]);
119
119
  if (run)
120
120
  return runRelayDir(run);
@@ -182,6 +182,7 @@ async function relay(argv) {
182
182
  foreground: flags["--foreground"] === true,
183
183
  queue: flags["--queue"] === true,
184
184
  force: flags["--force"] === true,
185
+ ifIdle: flags["--if-idle"] === true,
185
186
  }));
186
187
  }
187
188
  async function host(argv) {
@@ -245,12 +246,14 @@ const COMMANDS = {
245
246
  usage: [
246
247
  "zswarm relay --to PANE --done REGEX --reply-to PANE [--serve TARGET] [--session S] [--body-file F]",
247
248
  " [--reply-session S] [--from NAME] [--check SCRIPT] [--message TEMPLATE] [--id ID]",
248
- " [--timeout-min 240] [--max-age MIN] [--queue] [--force] [--foreground]",
249
+ " [--timeout-min 240] [--max-age MIN] [--queue] [--force] [--foreground] [--if-idle]",
249
250
  " send the body to PANE over serve (ZSWARM_SERVE / ZSWARM_SERVE_TOKEN), then a detached relay",
250
251
  " waits for REGEX (and SCRIPT exit 0) and pastes a message into the local --reply-to pane;",
251
252
  " --max-age (default --timeout-min + 60) is a hard end that always yields a final status",
252
253
  " it leases its target pane until it ends; a second relay to a leased pane fails pane_leased",
253
254
  " unless --queue waits for the lease to free or --force shares it",
255
+ " --if-idle applies send's busy check before the lease: a harness that shows it is working fails pane_busy",
256
+ " without sending; --queue then waits for the pane to go idle as well as for the lease, and --force skips the check",
254
257
  "zswarm relay --list [--json] id, state, target, session, age, last check, job and lease per relay dir",
255
258
  "zswarm relay --leases [--json] live pane leases and the relay that holds each",
256
259
  "zswarm relay --cancel ID | --cancel-all --to PANE stop the job, mark it CANCELLED, paste nothing",
@@ -108,6 +108,8 @@ export type RelayStartInput = {
108
108
  queue?: boolean;
109
109
  /** Send to a leased pane anyway; the lease is shared and noted. */
110
110
  force?: boolean;
111
+ /** With --if-idle, check the target harness before the lease: a visibly working pane fails pane_busy. */
112
+ ifIdle?: boolean;
111
113
  env?: NodeJS.ProcessEnv;
112
114
  deps?: RelayDeps;
113
115
  /** State store for the pane lease; injected in tests. */
@@ -2,7 +2,7 @@ import { spawn, spawnSync } from "node:child_process";
2
2
  import { appendFileSync, chmodSync, copyFileSync, existsSync, mkdirSync, openSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
3
3
  import { basename, join } from "node:path";
4
4
  import { ZellijError } from "../errors.js";
5
- import { dispatchZswarm } from "../ops/dispatch.js";
5
+ import { dispatchZswarm, paneBusyMarker } from "../ops/dispatch.js";
6
6
  import { resolveServeCliLaunch } from "../ops/serve-install.js";
7
7
  import { createStateStore, defaultStateDir } from "../state.js";
8
8
  import { findLease, leaseKey, releaseLeaseKey, takeLease } from "./lease.js";
@@ -404,15 +404,69 @@ export async function startRelay(input) {
404
404
  let how;
405
405
  const leaseNow = input.deps?.now ?? Date.now;
406
406
  const leaseSleep = input.deps?.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
407
+ const call = input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
408
+ const remote = serveEnv(cfg, env);
409
+ const target = (args) => cfg.session ? { ...args, session: cfg.session } : args;
407
410
  const take = () => takeLease(store, { relayId: id, from: cfg.from, at: leaseNow(), dir, target: leaseTarget, force: input.force });
408
411
  const leaseKeyValue = leaseKey(leaseTarget);
409
- let taken = take();
410
- if (!taken.ok && input.queue) {
411
- const deadline = leaseNow() + cfg.timeoutMin * 60_000;
412
- while (!taken.ok && leaseNow() < deadline) {
412
+ /** The pane --to names, with the command/title the harness check needs. */
413
+ const resolveRemotePane = async () => {
414
+ const args = { op: "list" };
415
+ if (cfg.session)
416
+ args.session = cfg.session;
417
+ const res = await call(args, remote).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
418
+ const panes = res.ok
419
+ ? res.data.panes ?? []
420
+ : [];
421
+ const pane = panes.find((p) => p.title === cfg.to || p.id === cfg.to);
422
+ return pane ? { command: pane.command, title: pane.title } : { title: cfg.to };
423
+ };
424
+ // --if-idle: the same busy check send --if-idle uses, one dump before the
425
+ // lease is taken. --force skips it, as it skips the lease.
426
+ const checkIdle = input.ifIdle === true && input.force !== true;
427
+ const idlePane = checkIdle ? await resolveRemotePane() : undefined;
428
+ /** The harness working marker one dump of the target shows, or null. */
429
+ const busyMarkerNow = async () => {
430
+ const res = await call(target({ op: "dump", to: cfg.to, max: 400 }), remote).catch((err) => ({ ok: false, error: { code: "failed", message: String(err) } }));
431
+ if (!res.ok)
432
+ return null; // cannot see the screen: the lease is the only guard
433
+ return paneBusyMarker(String(res.data?.text ?? ""), idlePane ?? { title: cfg.to });
434
+ };
435
+ const deadline = leaseNow() + cfg.timeoutMin * 60_000;
436
+ let busy = null;
437
+ let taken;
438
+ if (!input.queue) {
439
+ if (checkIdle) {
440
+ busy = await busyMarkerNow();
441
+ if (busy) {
442
+ rmSync(dir, { recursive: true, force: true });
443
+ throw new ZellijError("pane_busy", `pane ${cfg.to} is busy with a task (marker: ${busy}); retry when it is idle`);
444
+ }
445
+ }
446
+ taken = take();
447
+ }
448
+ else {
449
+ // --queue polls the lease and, under --if-idle, the busy marker. The lease
450
+ // is only taken once the pane is not visibly working.
451
+ let attempt;
452
+ for (;;) {
453
+ if (checkIdle)
454
+ busy = await busyMarkerNow();
455
+ attempt = busy ? undefined : take();
456
+ if (attempt?.ok)
457
+ break;
458
+ if (leaseNow() >= deadline)
459
+ break;
413
460
  await leaseSleep(Math.min(LEASE_QUEUE_POLL_MS, Math.max(1, deadline - leaseNow())));
414
- taken = take();
415
461
  }
462
+ if (attempt?.ok)
463
+ taken = attempt;
464
+ else if (busy) {
465
+ rmSync(dir, { recursive: true, force: true });
466
+ throw new ZellijError("pane_busy", `pane ${cfg.to} is busy with a task (marker: ${busy}) and did not free before --timeout-min`);
467
+ }
468
+ else
469
+ taken = attempt ?? take();
416
470
  }
417
471
  if (!taken.ok) {
418
472
  rmSync(dir, { recursive: true, force: true });
@@ -423,9 +477,6 @@ export async function startRelay(input) {
423
477
  if (taken.shared) {
424
478
  warning = `pane ${cfg.to} is already leased by relay ${taken.lease.relayId}; --force shares its lease`;
425
479
  }
426
- const call = input.deps?.call ?? ((args, e) => dispatchZswarm(args, undefined, { env: e }));
427
- const remote = serveEnv(cfg, env);
428
- const target = (args) => cfg.session ? { ...args, session: cfg.session } : args;
429
480
  try {
430
481
  if (body !== undefined) {
431
482
  const res = await call(target({ op: "send", to: cfg.to, body, from: cfg.from, settleMs: 800 }), remote);
package/dist/index.d.ts CHANGED
@@ -12,6 +12,8 @@ export { createZellijClient, type ZellijClient, type ZellijClientOptions, type Z
12
12
  export { createStateStore, defaultStateDir, type LeaseRecord, type LogEntry, type SignalChannel, type StateStore, type StateStoreOptions, } from "./state.js";
13
13
  export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, type LeaseTakeInput, type LeaseTakeResult, type LeaseTarget, } from "./host/lease.js";
14
14
  export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
15
+ export { EXIT_WAIT_MS, HANDOFF_INLINE_MAX, HANDOFF_MARKER, HANDOFF_SELF_WAIT_MS, READY_SETTLE_MS, READY_STABLE_GAP_MS, READY_WAIT_MS, restartLaunchCommand, restartPane, restartPaneKind, type RestartPaneKind, type RestartStep, } from "./ops/restart.js";
16
+ export { busyPrompt, queuedPrompt, readyPrompt, resolveHarness, type HarnessExitRecipe, type HarnessProfile, type SubmitStrategy, } from "./harness.js";
15
17
  export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, type Policy, } from "./policy.js";
16
18
  export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, type IpcDiscoveryState, type SshExecFn, type SshTarget, } from "./exec.js";
17
19
  export { resolveSshTarget } from "./zellij/binary.js";
package/dist/index.js CHANGED
@@ -12,6 +12,8 @@ export { createZellijClient, } from "./zellij/client.js";
12
12
  export { createStateStore, defaultStateDir, } from "./state.js";
13
13
  export { findLease, leaseKey, leaseStale, listLeases, releaseLease, releaseLeaseKey, takeLease, } from "./host/lease.js";
14
14
  export { dispatchZswarm, resolveInvocationEnv } from "./ops/dispatch.js";
15
+ export { EXIT_WAIT_MS, HANDOFF_INLINE_MAX, HANDOFF_MARKER, HANDOFF_SELF_WAIT_MS, READY_SETTLE_MS, READY_STABLE_GAP_MS, READY_WAIT_MS, restartLaunchCommand, restartPane, restartPaneKind, } from "./ops/restart.js";
16
+ export { busyPrompt, queuedPrompt, readyPrompt, resolveHarness, } from "./harness.js";
15
17
  export { assertOpAllowed, assertPaneAllowed, isWriteOp, loadPolicy, } from "./policy.js";
16
18
  export { buildSshRemoteCommand, createSshExec, quoteRemoteArg, shellQuote, } from "./exec.js";
17
19
  export { resolveSshTarget } from "./zellij/binary.js";
@@ -1,5 +1,14 @@
1
1
  import { type ZellijClient } from "../zellij/client.js";
2
2
  import type { DispatchDeps, OpsResult } from "./types.js";
3
+ /**
4
+ * The harness busy marker a dumped screen shows for `pane`, or null. The one
5
+ * busy check behind `send --if-idle` and `relay --if-idle`; the marker names
6
+ * itself (its regex source) in the `pane_busy` error.
7
+ */
8
+ export declare function paneBusyMarker(screen: string, pane: {
9
+ command?: string | null;
10
+ title?: string | null;
11
+ }): string | null;
3
12
  /**
4
13
  * Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
5
14
  * and serve (and remote IPC). `--ssh` sets the destination for this call and
@@ -1,6 +1,6 @@
1
1
  import { ZellijError } from "../errors.js";
2
2
  import { createGitClient } from "../git.js";
3
- import { busyPrompt, resolveHarness } from "../harness.js";
3
+ import { busyMarker, resolveHarness } from "../harness.js";
4
4
  import { findLease } from "../host/lease.js";
5
5
  import { normalizeKeys } from "../keys.js";
6
6
  import { createZellijClient } from "../zellij/client.js";
@@ -16,6 +16,7 @@ import { attachKnownSender, deliverTo, scaleBudgets, withSenderLabel, selfPaneTi
16
16
  import { assertNotPlugin, assertNotSelf, assertExpectedScreen, assertWritable, } from "./guards.js";
17
17
  import { readDeliveryLog } from "./log.js";
18
18
  import { awaitSignal, listSignals, postSignal } from "./signals.js";
19
+ import { restartPane } from "./restart.js";
19
20
  import { spawnPane } from "./spawn.js";
20
21
  import { observationBudget } from "./observation.js";
21
22
  import { invocationContext } from "./routing.js";
@@ -30,6 +31,14 @@ import { serveCallTimeout, } from "./serve.js";
30
31
  import { doctorOp } from "./doctor.js";
31
32
  import { forwardServe } from "./serve-tunnel.js";
32
33
  import { installServeService, uninstallServeService } from "../host/serve-service.js";
34
+ /**
35
+ * The harness busy marker a dumped screen shows for `pane`, or null. The one
36
+ * busy check behind `send --if-idle` and `relay --if-idle`; the marker names
37
+ * itself (its regex source) in the `pane_busy` error.
38
+ */
39
+ export function paneBusyMarker(screen, pane) {
40
+ return busyMarker(screen, resolveHarness(pane))?.source ?? null;
41
+ }
33
42
  /**
34
43
  * Per-invocation routing from `--local` / `--ssh`. `--local` clears both SSH
35
44
  * and serve (and remote IPC). `--ssh` sets the destination for this call and
@@ -270,6 +279,9 @@ async function dispatchOperation(args, injected, deps, context, env) {
270
279
  if (isTrue(args.clear)) {
271
280
  const cleared = await uninstallServeLogon({
272
281
  platform: deps.serveInstall?.platform ?? process.platform,
282
+ // --session names the task to clear: its own per-session task, or the
283
+ // legacy root task only when that task records the same session.
284
+ session: optionalString(args.session),
273
285
  timeoutMs,
274
286
  signal,
275
287
  env,
@@ -399,8 +411,11 @@ async function dispatchOperation(args, injected, deps, context, env) {
399
411
  throw new ZellijError("pane_leased", `pane ${to} is leased by relay ${held.relayId} (from ${held.from}, since ${new Date(held.at).toISOString()})`, { relayId: held.relayId, from: held.from, at: held.at, dir: held.dir });
400
412
  }
401
413
  const screen = await client.dumpPane({ session, paneId: pane.id }).then((res) => res.text, () => null);
402
- if (screen !== null && busyPrompt(screen, resolveHarness(pane))) {
403
- throw new ZellijError("pane_busy", `pane ${to} is busy with a task; retry when it is idle`);
414
+ if (screen !== null) {
415
+ const marker = paneBusyMarker(screen, pane);
416
+ if (marker) {
417
+ throw new ZellijError("pane_busy", `pane ${to} is busy with a task (marker: ${marker}); retry when it is idle`);
418
+ }
404
419
  }
405
420
  }
406
421
  const labeled = withSenderLabel(args, {
@@ -652,6 +667,13 @@ async function dispatchOperation(args, injected, deps, context, env) {
652
667
  return await peerCheckpoint(git(), args, clock);
653
668
  case "spawn":
654
669
  return await spawnPane(client, args, deps.git, clock, signal);
670
+ case "restart": {
671
+ const { session, pane, panes } = await resolveTarget(client, args, state(), clock, env, policy, op, signal);
672
+ return await restartPane(client, state(), args, { session, pane, panes }, clock, {
673
+ env,
674
+ signal,
675
+ });
676
+ }
655
677
  case "worktrees":
656
678
  return await listPeerWorktrees(git(), client, args);
657
679
  case "unworktree":
@@ -0,0 +1,57 @@
1
+ import { type HarnessProfile } from "../harness.js";
2
+ import type { StateStore } from "../state.js";
3
+ import type { ZellijClient } from "../zellij/client.js";
4
+ import type { ZellijPane } from "../zellij/panes.js";
5
+ import type { Clock, OpsResult } from "./types.js";
6
+ /**
7
+ * Restart the agent in one pane, in place: same pane id, title and position.
8
+ *
9
+ * A Zellij command pane (a `command=` layout entry or `zellij run`) stays open
10
+ * when its command exits and offers ENTER to re-run the same command — that is
11
+ * the in-place relaunch. A shell pane has no such prompt, so the profile's
12
+ * launch command (or `--command`) is typed instead.
13
+ */
14
+ /** The agent prints this once its handoff file is written. */
15
+ export declare const HANDOFF_MARKER = "ZSWARM-HANDOFF-DONE";
16
+ /** A handoff at or under this many chars is pasted; longer ones get a pointer. */
17
+ export declare const HANDOFF_INLINE_MAX = 2000;
18
+ /** Bound on waiting for a self-written handoff marker. */
19
+ export declare const HANDOFF_SELF_WAIT_MS = 60000;
20
+ /** Bound on waiting for the pane's command to exit before the Ctrl+C fallback. */
21
+ export declare const EXIT_WAIT_MS = 10000;
22
+ /** Bound on waiting for readiness after relaunch (marker or settled screen). */
23
+ export declare const READY_WAIT_MS = 45000;
24
+ /** Extra settle after a ready marker before the handoff is pasted. */
25
+ export declare const READY_SETTLE_MS = 500;
26
+ /** A screen that has not changed for this long counts as settled. */
27
+ export declare const READY_STABLE_GAP_MS = 1000;
28
+ /** Screen poll interval while a step waits. */
29
+ export declare const RESTART_POLL_MS = 250;
30
+ export type RestartPaneKind = "command" | "shell";
31
+ export type RestartStep = {
32
+ step: string;
33
+ ok: boolean;
34
+ detail: string;
35
+ elapsedMs: number;
36
+ };
37
+ /**
38
+ * A command pane ran the agent itself, so ENTER re-runs it. A shell pane had the
39
+ * agent started inside a shell, so the launch command has to be typed again.
40
+ * A pane with no command information is treated as a shell: typing a launch
41
+ * command is recoverable, pressing ENTER into a live agent is not.
42
+ */
43
+ export declare function restartPaneKind(pane: ZellijPane): RestartPaneKind;
44
+ /** The command line a shell pane gets to relaunch the agent. */
45
+ export declare function restartLaunchCommand(args: Record<string, unknown>, profile: HarnessProfile): string | null;
46
+ /**
47
+ * Restart one already-resolved pane. `target` comes from dispatch's
48
+ * `resolveTarget`, so policy and pane lookup are shared with the other ops.
49
+ */
50
+ export declare function restartPane(client: ZellijClient, state: StateStore, args: Record<string, unknown>, target: {
51
+ session: string;
52
+ pane: ZellijPane;
53
+ panes: ZellijPane[];
54
+ }, clock: Clock, opts?: {
55
+ env?: NodeJS.ProcessEnv;
56
+ signal?: AbortSignal;
57
+ }): Promise<OpsResult>;