claude-task-worker 0.28.0 → 0.29.1

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 (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +44 -9
  3. package/package.json +1 -1
package/README.md CHANGED
@@ -312,7 +312,7 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
312
312
 
313
313
  `mode: "herdr"` のときの1タスクの流れ:
314
314
 
315
- 1. worktree を作成し、`ctw:<プロジェクト名>:#<Issue/PR番号>` ラベルのタブで claude を TUI 起動する(`HERDR_DISABLE_SOUND=1` を設定するため通知音は鳴らない)
315
+ 1. worktree を作成し、`ctw:<プロジェクト名>:#<Issue/PR番号>` ラベルのタブで claude を TUI 起動する
316
316
  2. herdr が持つ agent ステータスを監視し、`working` → `idle` の遷移をタスク完了とみなす
317
317
  3. 完了したらペインの内容を回収して通知に使い、タブを閉じてラベル・worktree を後片付けする
318
318
 
@@ -321,6 +321,15 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
321
321
  - タブは `--project` で起動した場合そのプロジェクトのワークスペース内に作られる(herdrが各ペインへ注入する `HERDR_WORKSPACE_ID` を利用する)
322
322
  - `blocked`(claudeが入力待ち)になってもタスクは自動失敗にせず待機し続ける。ステータステーブルに `running:blocked` と表示されるので、herdrのタブを開いて直接対応できる
323
323
  - `mode: "herdr"` でherdrが未インストール・未起動の場合、ワーカーは起動時にエラー終了する(`"default"` へ勝手にフォールバックしない)
324
+ - **タスク完了時の通知音はワーカー側から止められない**。herdr のエージェント状態遷移音を再生するのは herdr サーバープロセスで、`HERDR_DISABLE_SOUND` もそのプロセスの環境変数として読まれるため、タスクペインへ渡しても効かない(socket API にもペイン単位のミュートは無い)。無音にしたい場合は herdr 側の設定で行う:
325
+
326
+ ```toml
327
+ # ~/.config/herdr/config.toml
328
+ [ui.sound]
329
+ enabled = false
330
+ ```
331
+
332
+ 適用は `herdr server reload-config`。この設定は herdr サーバー全体に効くため、ワーカー以外の対話セッションの完了音も鳴らなくなる(`[ui.sound.agents] claude = "off"` でも実質同じ範囲)。ワーカーだけを無音にしたい場合は、`HERDR_DISABLE_SOUND=1 herdr --session <name>` で別セッションを起動し、その中でディスパッチャーを動かす
324
333
 
325
334
  ### exec-issue
326
335
 
package/dist/index.js CHANGED
@@ -75,6 +75,7 @@ __export(herdr_exports, {
75
75
  tabRename: () => tabRename,
76
76
  workspaceClose: () => workspaceClose,
77
77
  workspaceCreate: () => workspaceCreate,
78
+ workspaceFocus: () => workspaceFocus,
78
79
  workspaceList: () => workspaceList
79
80
  });
80
81
  import { createRequire as createRequire2 } from "node:module";
@@ -210,12 +211,16 @@ async function workspaceList() {
210
211
  }
211
212
  return result.workspaces.map((workspace) => ({
212
213
  workspaceId: workspace.workspace_id,
213
- label: workspace.label ?? ""
214
+ label: workspace.label ?? "",
215
+ focused: workspace.focused === true
214
216
  }));
215
217
  }
216
218
  async function workspaceClose(workspaceId) {
217
219
  await execHerdr(["workspace", "close", workspaceId]);
218
220
  }
221
+ async function workspaceFocus(workspaceId) {
222
+ await execHerdr(["workspace", "focus", workspaceId]);
223
+ }
219
224
  async function agentStart({
220
225
  name,
221
226
  cwd,
@@ -244,7 +249,7 @@ async function agentStart({
244
249
  return { paneId, tabId };
245
250
  }
246
251
  function toAgentStatus(value) {
247
- return value === "working" || value === "idle" || value === "blocked" ? value : "unknown";
252
+ return value === "working" || value === "idle" || value === "blocked" || value === "done" ? value : "unknown";
248
253
  }
249
254
  async function agentGet(target) {
250
255
  const result = await execHerdr(["agent", "get", target]);
@@ -394,6 +399,9 @@ function observeAgentStatus(tracker, status) {
394
399
  if (status === "working") {
395
400
  return { tracker: { ...tracker, seenWorking: true }, decision: "running" };
396
401
  }
402
+ if (status === "done") {
403
+ return { tracker, decision: "completed" };
404
+ }
397
405
  if (status === "blocked") {
398
406
  if (tracker.warnedBlocked) return { tracker, decision: "running" };
399
407
  return { tracker: { ...tracker, warnedBlocked: true }, decision: "blocked-first-seen" };
@@ -585,6 +593,7 @@ __export(dispatcher_exports, {
585
593
  WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
586
594
  WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
587
595
  createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
596
+ focusedWorkspaceId: () => focusedWorkspaceId,
588
597
  formatUptime: () => formatUptime,
589
598
  isShellProcess: () => isShellProcess,
590
599
  isWorkerProcess: () => isWorkerProcess,
@@ -592,6 +601,7 @@ __export(dispatcher_exports, {
592
601
  pollOnce: () => pollOnce,
593
602
  removeSession: () => removeSession,
594
603
  renderSessionTable: () => renderSessionTable,
604
+ restoreWorkspaceFocus: () => restoreWorkspaceFocus,
595
605
  runDispatcher: () => runDispatcher,
596
606
  shutdownDispatcher: () => shutdownDispatcher,
597
607
  startWorkerInPane: () => startWorkerInPane,
@@ -608,6 +618,25 @@ async function loadTable() {
608
618
  function workspaceLabelFor(projectName) {
609
619
  return `${LABEL_PREFIX}${projectName}`;
610
620
  }
621
+ async function focusedWorkspaceId(herdr) {
622
+ try {
623
+ const workspaces = await herdr.workspaceList();
624
+ return workspaces.find((workspace) => workspace.focused)?.workspaceId;
625
+ } catch (error) {
626
+ console.error(`[dispatcher] failed to read the focused workspace: ${error}`);
627
+ return void 0;
628
+ }
629
+ }
630
+ async function restoreWorkspaceFocus(herdr, previousWorkspaceId, closingWorkspaceIds) {
631
+ if (!previousWorkspaceId || closingWorkspaceIds.has(previousWorkspaceId)) return;
632
+ const currentWorkspaceId = await focusedWorkspaceId(herdr);
633
+ if (currentWorkspaceId === previousWorkspaceId) return;
634
+ try {
635
+ await herdr.workspaceFocus(previousWorkspaceId);
636
+ } catch (error) {
637
+ console.error(`[dispatcher] failed to restore focus to workspace "${previousWorkspaceId}": ${error}`);
638
+ }
639
+ }
611
640
  function sleep2(ms) {
612
641
  return new Promise((resolve3) => setTimeout(resolve3, ms));
613
642
  }
@@ -719,23 +748,27 @@ async function runDispatcher(projects, forwardedCommand, timing) {
719
748
  console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
720
749
  sessions.delete(project.name);
721
750
  if (createdWorkspaceId !== void 0) {
751
+ const focusedBefore = await focusedWorkspaceId(herdr);
722
752
  try {
723
753
  await workspaceClose2(createdWorkspaceId);
724
754
  } catch (closeError) {
725
755
  console.error(`[dispatcher] failed to close dangling workspace for project "${project.name}": ${closeError}`);
726
756
  }
757
+ await restoreWorkspaceFocus(herdr, focusedBefore, /* @__PURE__ */ new Set([createdWorkspaceId]));
727
758
  }
728
759
  }
729
760
  }
730
761
  return sessions;
731
762
  }
732
- async function removeSession(sessions, name, { closeWorkspace }) {
763
+ async function removeSession(sessions, name, { closeWorkspace }, herdrModule) {
733
764
  const session = sessions.get(name);
734
765
  if (!session) return;
735
766
  sessions.delete(name);
736
767
  if (closeWorkspace) {
737
- const { workspaceClose: workspaceClose2 } = await loadHerdr2();
738
- await workspaceClose2(session.workspaceId);
768
+ const herdr = herdrModule ?? await loadHerdr2();
769
+ const focusedBefore = await focusedWorkspaceId(herdr);
770
+ await herdr.workspaceClose(session.workspaceId);
771
+ await restoreWorkspaceFocus(herdr, focusedBefore, /* @__PURE__ */ new Set([session.workspaceId]));
739
772
  }
740
773
  }
741
774
  async function pollOnce(sessions, herdr) {
@@ -743,11 +776,11 @@ async function pollOnce(sessions, herdr) {
743
776
  try {
744
777
  const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
745
778
  if (!foregroundProcesses.some(isWorkerProcess)) {
746
- await removeSession(sessions, name, { closeWorkspace: true });
779
+ await removeSession(sessions, name, { closeWorkspace: true }, herdr);
747
780
  }
748
781
  } catch (error) {
749
782
  if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
750
- await removeSession(sessions, name, { closeWorkspace: false });
783
+ await removeSession(sessions, name, { closeWorkspace: false }, herdr);
751
784
  continue;
752
785
  }
753
786
  console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
@@ -845,6 +878,8 @@ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs
845
878
  return true;
846
879
  }
847
880
  async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
881
+ const closingWorkspaceIds = new Set([...sessions.values()].map((session) => session.workspaceId));
882
+ const focusedBefore = await focusedWorkspaceId(herdr);
848
883
  const results = await Promise.all(
849
884
  [...sessions.values()].map(async (session) => {
850
885
  try {
@@ -863,6 +898,7 @@ async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
863
898
  }
864
899
  })
865
900
  );
901
+ await restoreWorkspaceFocus(herdr, focusedBefore, closingWorkspaceIds);
866
902
  return results.every((closed) => closed);
867
903
  }
868
904
  async function forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs) {
@@ -1273,8 +1309,7 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
1273
1309
  function buildClaudeEnv(mode) {
1274
1310
  if (mode === "herdr") {
1275
1311
  return {
1276
- CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS,
1277
- HERDR_DISABLE_SOUND: "1"
1312
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
1278
1313
  };
1279
1314
  }
1280
1315
  return { ...CLAUDE_SPAWN_ENV };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.28.0",
3
+ "version": "0.29.1",
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",