claude-task-worker 0.23.0 → 0.25.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 (2) hide show
  1. package/dist/index.js +141 -35
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -61,6 +61,7 @@ __export(herdr_exports, {
61
61
  HerdrUnavailableError: () => HerdrUnavailableError,
62
62
  checkHerdrAvailable: () => checkHerdrAvailable,
63
63
  paneProcessInfo: () => paneProcessInfo,
64
+ paneRead: () => paneRead,
64
65
  paneSendKeys: () => paneSendKeys,
65
66
  paneSendText: () => paneSendText,
66
67
  tabClose: () => tabClose,
@@ -129,6 +130,50 @@ async function tabList() {
129
130
  workspaceId: tab.workspace_id
130
131
  }));
131
132
  }
133
+ function isPaneReadErrorPayload(value) {
134
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
135
+ return false;
136
+ }
137
+ const keys = Object.keys(value);
138
+ if (keys.some((key) => !PANE_READ_ERROR_ALLOWED_KEYS.has(key))) {
139
+ return false;
140
+ }
141
+ const payload = value;
142
+ if (typeof payload.code !== "string") {
143
+ return false;
144
+ }
145
+ if ("message" in payload && typeof payload.message !== "string") {
146
+ return false;
147
+ }
148
+ return true;
149
+ }
150
+ async function paneRead(paneId, options) {
151
+ const args = ["pane", "read", paneId, "--source", options?.source ?? "visible"];
152
+ if (options?.lines !== void 0) {
153
+ args.push("--lines", String(options.lines));
154
+ }
155
+ const { execError, stdout, stderr } = await runHerdr(args);
156
+ const trimmed = stdout.trim();
157
+ if (trimmed.startsWith("{")) {
158
+ let parsed;
159
+ try {
160
+ parsed = JSON.parse(trimmed);
161
+ } catch {
162
+ parsed = void 0;
163
+ }
164
+ if (isPaneReadErrorPayload(parsed)) {
165
+ throw new HerdrError(
166
+ `herdr ${args.join(" ")} failed: [${parsed.code}] ${parsed.message ?? ""}`.trim(),
167
+ parsed.code
168
+ );
169
+ }
170
+ }
171
+ if (execError) {
172
+ const stderrSuffix = stderr.trim() ? `: ${stderr.trim()}` : "";
173
+ throw new Error(`herdr ${args.join(" ")} failed: ${execError.message}${stderrSuffix}`);
174
+ }
175
+ return stdout;
176
+ }
132
177
  async function paneSendText(paneId, text) {
133
178
  await execHerdr(["pane", "send-text", paneId, text], { allowEmptyResult: true });
134
179
  }
@@ -162,7 +207,7 @@ async function checkHerdrAvailable() {
162
207
  );
163
208
  }
164
209
  }
165
- var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS;
210
+ var childProcess2, HerdrUnavailableError, HerdrError, HERDR_TIMEOUT_MS, PANE_READ_ERROR_ALLOWED_KEYS;
166
211
  var init_herdr = __esm({
167
212
  "src/herdr.ts"() {
168
213
  "use strict";
@@ -184,25 +229,36 @@ var init_herdr = __esm({
184
229
  }
185
230
  };
186
231
  HERDR_TIMEOUT_MS = 15 * 1e3;
232
+ PANE_READ_ERROR_ALLOWED_KEYS = /* @__PURE__ */ new Set(["code", "message"]);
187
233
  }
188
234
  });
189
235
 
190
236
  // src/dispatcher.ts
191
237
  var dispatcher_exports = {};
192
238
  __export(dispatcher_exports, {
239
+ PANE_READY_POLL_INTERVAL_MS: () => PANE_READY_POLL_INTERVAL_MS,
240
+ PANE_READY_TIMEOUT_MS: () => PANE_READY_TIMEOUT_MS,
193
241
  POLL_INTERVAL_MS: () => POLL_INTERVAL_MS,
242
+ SEND_MAX_ATTEMPTS: () => SEND_MAX_ATTEMPTS,
194
243
  SHUTDOWN_RETRY_TIMEOUT_MS: () => SHUTDOWN_RETRY_TIMEOUT_MS,
195
244
  SHUTDOWN_TIMEOUT_MS: () => SHUTDOWN_TIMEOUT_MS,
196
245
  TAB_LABEL_PREFIX: () => TAB_LABEL_PREFIX,
246
+ WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
247
+ WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
197
248
  createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
198
249
  formatUptime: () => formatUptime,
250
+ isShellProcess: () => isShellProcess,
251
+ isWorkerProcess: () => isWorkerProcess,
199
252
  monitorSessions: () => monitorSessions,
200
253
  pollOnce: () => pollOnce,
201
254
  removeSession: () => removeSession,
202
255
  renderSessionTable: () => renderSessionTable,
203
256
  runDispatcher: () => runDispatcher,
204
257
  shutdownDispatcher: () => shutdownDispatcher,
205
- tabLabelFor: () => tabLabelFor
258
+ startWorkerInPane: () => startWorkerInPane,
259
+ tabLabelFor: () => tabLabelFor,
260
+ waitForPaneReady: () => waitForPaneReady,
261
+ waitForWorkerStartup: () => waitForWorkerStartup
206
262
  });
207
263
  async function loadHerdr() {
208
264
  return await Promise.resolve().then(() => (init_herdr(), herdr_exports));
@@ -213,8 +269,72 @@ async function loadTable() {
213
269
  function tabLabelFor(projectName) {
214
270
  return `${TAB_LABEL_PREFIX}${projectName}`;
215
271
  }
216
- async function runDispatcher(projects, forwardedCommand) {
217
- const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2, paneSendText: paneSendText2, paneSendKeys: paneSendKeys2 } = await loadHerdr();
272
+ function sleep(ms) {
273
+ return new Promise((resolve2) => setTimeout(resolve2, ms));
274
+ }
275
+ function isWorkerProcess(process2) {
276
+ return process2.cmdline?.includes("claude-task-worker") ?? false;
277
+ }
278
+ function isShellProcess(process2) {
279
+ return SHELL_NAME_PATTERN.test(process2.name ?? "") || SHELL_NAME_PATTERN.test(process2.cmdline ?? "");
280
+ }
281
+ async function waitForPaneReady(paneId, herdr, options) {
282
+ const timeoutMs = options?.timeoutMs ?? PANE_READY_TIMEOUT_MS;
283
+ const pollIntervalMs = options?.pollIntervalMs ?? PANE_READY_POLL_INTERVAL_MS;
284
+ const deadline = Date.now() + timeoutMs;
285
+ for (; ; ) {
286
+ const content = await herdr.paneRead(paneId, { source: "visible" });
287
+ if (content.trim() !== "") return true;
288
+ if (Date.now() >= deadline) return false;
289
+ await sleep(pollIntervalMs);
290
+ }
291
+ }
292
+ async function waitForWorkerStartup(paneId, herdr, options) {
293
+ const timeoutMs = options?.timeoutMs ?? WORKER_STARTUP_TIMEOUT_MS;
294
+ const pollIntervalMs = options?.pollIntervalMs ?? WORKER_STARTUP_POLL_INTERVAL_MS;
295
+ const deadline = Date.now() + timeoutMs;
296
+ for (; ; ) {
297
+ const { foregroundProcesses } = await herdr.paneProcessInfo(paneId);
298
+ if (foregroundProcesses.some(isWorkerProcess)) return "started";
299
+ if (foregroundProcesses.length > 0 && !foregroundProcesses.every(isShellProcess)) return "other";
300
+ if (Date.now() >= deadline) return "shell";
301
+ await sleep(pollIntervalMs);
302
+ }
303
+ }
304
+ async function startWorkerInPane(paneId, forwardedCommand, herdr, options) {
305
+ const maxAttempts = options?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS;
306
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
307
+ const ready = await waitForPaneReady(paneId, herdr, {
308
+ timeoutMs: options?.paneReadyTimeoutMs,
309
+ pollIntervalMs: options?.paneReadyPollIntervalMs
310
+ });
311
+ if (!ready) {
312
+ console.warn(`[dispatcher] pane ${paneId} produced no prompt before the timeout, sending the command anyway`);
313
+ }
314
+ await herdr.paneSendText(paneId, forwardedCommand);
315
+ await herdr.paneSendKeys(paneId, "enter");
316
+ const result = await waitForWorkerStartup(paneId, herdr, {
317
+ timeoutMs: options?.workerStartupTimeoutMs,
318
+ pollIntervalMs: options?.workerStartupPollIntervalMs
319
+ });
320
+ if (result === "started") return true;
321
+ if (result === "other") {
322
+ console.warn(
323
+ `[dispatcher] pane ${paneId} foreground is neither the shell nor the worker, giving up without resending`
324
+ );
325
+ return false;
326
+ }
327
+ if (attempt < maxAttempts) {
328
+ console.warn(
329
+ `[dispatcher] worker did not start in pane ${paneId} (attempt ${attempt}/${maxAttempts}), resending the command`
330
+ );
331
+ }
332
+ }
333
+ return false;
334
+ }
335
+ async function runDispatcher(projects, forwardedCommand, timing) {
336
+ const herdr = await loadHerdr();
337
+ const { checkHerdrAvailable: checkHerdrAvailable2, tabCreate: tabCreate2, tabClose: tabClose2, tabList: tabList2 } = herdr;
218
338
  try {
219
339
  await checkHerdrAvailable2();
220
340
  } catch (error) {
@@ -241,8 +361,12 @@ async function runDispatcher(projects, forwardedCommand) {
241
361
  startedAt: /* @__PURE__ */ new Date(),
242
362
  status: "running"
243
363
  });
244
- await paneSendText2(paneId, forwardedCommand);
245
- await paneSendKeys2(paneId, "enter");
364
+ const started = await startWorkerInPane(paneId, forwardedCommand, herdr, timing);
365
+ if (!started) {
366
+ throw new Error(
367
+ `worker did not start in pane ${paneId} after ${timing?.sendMaxAttempts ?? SEND_MAX_ATTEMPTS} attempt(s)`
368
+ );
369
+ }
246
370
  } catch (error) {
247
371
  console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
248
372
  sessions.delete(project.name);
@@ -270,8 +394,7 @@ async function pollOnce(sessions, herdr) {
270
394
  for (const [name, session] of [...sessions.entries()]) {
271
395
  try {
272
396
  const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
273
- const isAlive = foregroundProcesses.some((process2) => process2.cmdline?.includes("claude-task-worker"));
274
- if (!isAlive) {
397
+ if (!foregroundProcesses.some(isWorkerProcess)) {
275
398
  await removeSession(sessions, name, { closeTab: true });
276
399
  }
277
400
  } catch (error) {
@@ -467,14 +590,20 @@ function createDispatcherShutdownHandler(shutdown2) {
467
590
  };
468
591
  return { handle, isShuttingDown: () => shuttingDown2 };
469
592
  }
470
- var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, TAB_LABEL_PREFIX, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
593
+ var getDisplayWidth2, truncateToWidth2, padToWidth2, POLL_INTERVAL_MS, SHUTDOWN_TIMEOUT_MS, PANE_READY_TIMEOUT_MS, PANE_READY_POLL_INTERVAL_MS, WORKER_STARTUP_TIMEOUT_MS, WORKER_STARTUP_POLL_INTERVAL_MS, SEND_MAX_ATTEMPTS, TAB_LABEL_PREFIX, SHELL_NAME_PATTERN, SHUTDOWN_RETRY_TIMEOUT_MS, CTRL_C_KEY, shutdownPromise;
471
594
  var init_dispatcher = __esm({
472
595
  async "src/dispatcher.ts"() {
473
596
  "use strict";
474
597
  ({ getDisplayWidth: getDisplayWidth2, truncateToWidth: truncateToWidth2, padToWidth: padToWidth2 } = await loadTable());
475
598
  POLL_INTERVAL_MS = 7 * 1e3;
476
599
  SHUTDOWN_TIMEOUT_MS = 10 * 60 * 1e3;
600
+ PANE_READY_TIMEOUT_MS = 30 * 1e3;
601
+ PANE_READY_POLL_INTERVAL_MS = 200;
602
+ WORKER_STARTUP_TIMEOUT_MS = 30 * 1e3;
603
+ WORKER_STARTUP_POLL_INTERVAL_MS = 500;
604
+ SEND_MAX_ATTEMPTS = 3;
477
605
  TAB_LABEL_PREFIX = "ctw:";
606
+ SHELL_NAME_PATTERN = /^-?(zsh|bash|sh)$/;
478
607
  SHUTDOWN_RETRY_TIMEOUT_MS = 90 * 1e3;
479
608
  CTRL_C_KEY = "ctrl+c";
480
609
  }
@@ -1024,16 +1153,12 @@ import { spawn } from "node:child_process";
1024
1153
  init_table();
1025
1154
 
1026
1155
  // src/task-result.ts
1027
- var TASK_TIMEOUT_MS = 120 * 60 * 1e3;
1028
1156
  var STDERR_TAIL_LIMIT = 8 * 1024;
1029
- function buildTaskResult(code, timedOut, stdout, stderrTail) {
1157
+ function buildTaskResult(code, stdout, stderrTail) {
1030
1158
  const emptyOutput = stdout.trim() === "";
1031
- const completed = !timedOut && code === 0 && !emptyOutput;
1159
+ const completed = code === 0 && !emptyOutput;
1032
1160
  let output = stdout;
1033
- if (timedOut) {
1034
- output += `
1035
- [worker] task timed out after ${TASK_TIMEOUT_MS / 1e3}s`;
1036
- } else if (code === 0 && emptyOutput) {
1161
+ if (code === 0 && emptyOutput) {
1037
1162
  output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
1038
1163
  } else if (!completed) {
1039
1164
  output += `
@@ -1194,27 +1319,9 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1194
1319
  stderrChunks.shift();
1195
1320
  }
1196
1321
  });
1197
- let timedOut = false;
1198
- const timeoutHandle = setTimeout(() => {
1199
- timedOut = true;
1200
- console.error(`[worker] task #${id} timed out after ${TASK_TIMEOUT_MS / 1e3}s, terminating`);
1201
- if (child.pid) {
1202
- try {
1203
- process.kill(-child.pid, "SIGTERM");
1204
- } catch {
1205
- try {
1206
- child.kill("SIGTERM");
1207
- } catch {
1208
- }
1209
- }
1210
- }
1211
- }, TASK_TIMEOUT_MS);
1212
- timeoutHandle.unref();
1213
1322
  child.on("close", async (code) => {
1214
- clearTimeout(timeoutHandle);
1215
1323
  const { status: finalStatus, output } = buildTaskResult(
1216
1324
  code,
1217
- timedOut,
1218
1325
  Buffer.concat(outputChunks).toString("utf-8"),
1219
1326
  Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
1220
1327
  );
@@ -1237,7 +1344,6 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
1237
1344
  renderTable();
1238
1345
  });
1239
1346
  child.on("error", async (err) => {
1240
- clearTimeout(timeoutHandle);
1241
1347
  console.error(`[worker] failed to spawn process for #${id}: ${err.message}`);
1242
1348
  try {
1243
1349
  await Promise.race([
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.23.0",
3
+ "version": "0.25.0",
4
4
  "description": "CLI tool that polls GitHub Issues/PRs and delegates work to Claude CLI",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",