claude-task-worker 0.27.1 → 0.29.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 (3) hide show
  1. package/README.md +10 -1
  2. package/dist/index.js +178 -52
  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,
@@ -529,6 +534,49 @@ var init_herdr_runner = __esm({
529
534
  }
530
535
  });
531
536
 
537
+ // src/commands/run-command.ts
538
+ var run_command_exports = {};
539
+ __export(run_command_exports, {
540
+ runCommand: () => runCommand
541
+ });
542
+ import { spawn as spawn2 } from "node:child_process";
543
+ function runCommand(command, args) {
544
+ return new Promise((resolve3, reject) => {
545
+ const child = spawn2(command, args, {
546
+ stdio: "inherit",
547
+ shell: process.platform === "win32"
548
+ });
549
+ const forwardSignal = (signal) => {
550
+ child.kill(signal);
551
+ };
552
+ const onSigint = () => forwardSignal("SIGINT");
553
+ const onSigterm = () => forwardSignal("SIGTERM");
554
+ const cleanup = () => {
555
+ process.removeListener("SIGINT", onSigint);
556
+ process.removeListener("SIGTERM", onSigterm);
557
+ };
558
+ process.once("SIGINT", onSigint);
559
+ process.once("SIGTERM", onSigterm);
560
+ child.on("error", (err) => {
561
+ cleanup();
562
+ reject(err);
563
+ });
564
+ child.on("close", (code) => {
565
+ cleanup();
566
+ if (code === 0) {
567
+ resolve3();
568
+ } else {
569
+ reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
570
+ }
571
+ });
572
+ });
573
+ }
574
+ var init_run_command = __esm({
575
+ "src/commands/run-command.ts"() {
576
+ "use strict";
577
+ }
578
+ });
579
+
532
580
  // src/dispatcher.ts
533
581
  var dispatcher_exports = {};
534
582
  __export(dispatcher_exports, {
@@ -542,6 +590,7 @@ __export(dispatcher_exports, {
542
590
  WORKER_STARTUP_POLL_INTERVAL_MS: () => WORKER_STARTUP_POLL_INTERVAL_MS,
543
591
  WORKER_STARTUP_TIMEOUT_MS: () => WORKER_STARTUP_TIMEOUT_MS,
544
592
  createDispatcherShutdownHandler: () => createDispatcherShutdownHandler,
593
+ focusedWorkspaceId: () => focusedWorkspaceId,
545
594
  formatUptime: () => formatUptime,
546
595
  isShellProcess: () => isShellProcess,
547
596
  isWorkerProcess: () => isWorkerProcess,
@@ -549,6 +598,7 @@ __export(dispatcher_exports, {
549
598
  pollOnce: () => pollOnce,
550
599
  removeSession: () => removeSession,
551
600
  renderSessionTable: () => renderSessionTable,
601
+ restoreWorkspaceFocus: () => restoreWorkspaceFocus,
552
602
  runDispatcher: () => runDispatcher,
553
603
  shutdownDispatcher: () => shutdownDispatcher,
554
604
  startWorkerInPane: () => startWorkerInPane,
@@ -565,6 +615,25 @@ async function loadTable() {
565
615
  function workspaceLabelFor(projectName) {
566
616
  return `${LABEL_PREFIX}${projectName}`;
567
617
  }
618
+ async function focusedWorkspaceId(herdr) {
619
+ try {
620
+ const workspaces = await herdr.workspaceList();
621
+ return workspaces.find((workspace) => workspace.focused)?.workspaceId;
622
+ } catch (error) {
623
+ console.error(`[dispatcher] failed to read the focused workspace: ${error}`);
624
+ return void 0;
625
+ }
626
+ }
627
+ async function restoreWorkspaceFocus(herdr, previousWorkspaceId, closingWorkspaceIds) {
628
+ if (!previousWorkspaceId || closingWorkspaceIds.has(previousWorkspaceId)) return;
629
+ const currentWorkspaceId = await focusedWorkspaceId(herdr);
630
+ if (currentWorkspaceId === previousWorkspaceId) return;
631
+ try {
632
+ await herdr.workspaceFocus(previousWorkspaceId);
633
+ } catch (error) {
634
+ console.error(`[dispatcher] failed to restore focus to workspace "${previousWorkspaceId}": ${error}`);
635
+ }
636
+ }
568
637
  function sleep2(ms) {
569
638
  return new Promise((resolve3) => setTimeout(resolve3, ms));
570
639
  }
@@ -676,23 +745,27 @@ async function runDispatcher(projects, forwardedCommand, timing) {
676
745
  console.error(`[dispatcher] failed to dispatch project "${project.name}": ${error}`);
677
746
  sessions.delete(project.name);
678
747
  if (createdWorkspaceId !== void 0) {
748
+ const focusedBefore = await focusedWorkspaceId(herdr);
679
749
  try {
680
750
  await workspaceClose2(createdWorkspaceId);
681
751
  } catch (closeError) {
682
752
  console.error(`[dispatcher] failed to close dangling workspace for project "${project.name}": ${closeError}`);
683
753
  }
754
+ await restoreWorkspaceFocus(herdr, focusedBefore, /* @__PURE__ */ new Set([createdWorkspaceId]));
684
755
  }
685
756
  }
686
757
  }
687
758
  return sessions;
688
759
  }
689
- async function removeSession(sessions, name, { closeWorkspace }) {
760
+ async function removeSession(sessions, name, { closeWorkspace }, herdrModule) {
690
761
  const session = sessions.get(name);
691
762
  if (!session) return;
692
763
  sessions.delete(name);
693
764
  if (closeWorkspace) {
694
- const { workspaceClose: workspaceClose2 } = await loadHerdr2();
695
- await workspaceClose2(session.workspaceId);
765
+ const herdr = herdrModule ?? await loadHerdr2();
766
+ const focusedBefore = await focusedWorkspaceId(herdr);
767
+ await herdr.workspaceClose(session.workspaceId);
768
+ await restoreWorkspaceFocus(herdr, focusedBefore, /* @__PURE__ */ new Set([session.workspaceId]));
696
769
  }
697
770
  }
698
771
  async function pollOnce(sessions, herdr) {
@@ -700,11 +773,11 @@ async function pollOnce(sessions, herdr) {
700
773
  try {
701
774
  const { foregroundProcesses } = await herdr.paneProcessInfo(session.paneId);
702
775
  if (!foregroundProcesses.some(isWorkerProcess)) {
703
- await removeSession(sessions, name, { closeWorkspace: true });
776
+ await removeSession(sessions, name, { closeWorkspace: true }, herdr);
704
777
  }
705
778
  } catch (error) {
706
779
  if (error instanceof herdr.HerdrError && error.code === "pane_not_found") {
707
- await removeSession(sessions, name, { closeWorkspace: false });
780
+ await removeSession(sessions, name, { closeWorkspace: false }, herdr);
708
781
  continue;
709
782
  }
710
783
  console.error(`[dispatcher] failed to poll session "${name}": ${error}`);
@@ -802,6 +875,8 @@ async function waitUntilSessionsEmpty(sessions, herdr, pollIntervalMs, timeoutMs
802
875
  return true;
803
876
  }
804
877
  async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
878
+ const closingWorkspaceIds = new Set([...sessions.values()].map((session) => session.workspaceId));
879
+ const focusedBefore = await focusedWorkspaceId(herdr);
805
880
  const results = await Promise.all(
806
881
  [...sessions.values()].map(async (session) => {
807
882
  try {
@@ -820,6 +895,7 @@ async function closeRemainingWorkspaces(sessions, herdr, timeoutMs) {
820
895
  }
821
896
  })
822
897
  );
898
+ await restoreWorkspaceFocus(herdr, focusedBefore, closingWorkspaceIds);
823
899
  return results.every((closed) => closed);
824
900
  }
825
901
  async function forceKillAllSessions(sessions, herdr, workspaceCloseTimeoutMs) {
@@ -1230,8 +1306,7 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
1230
1306
  function buildClaudeEnv(mode) {
1231
1307
  if (mode === "herdr") {
1232
1308
  return {
1233
- CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS,
1234
- HERDR_DISABLE_SOUND: "1"
1309
+ CLAUDE_CODE_DISABLE_BACKGROUND_TASKS: CLAUDE_SPAWN_ENV.CLAUDE_CODE_DISABLE_BACKGROUND_TASKS
1235
1310
  };
1236
1311
  }
1237
1312
  return { ...CLAUDE_SPAWN_ENV };
@@ -2992,7 +3067,85 @@ var epicIssueWorker = (opts = {}) => createIssuePollingWorker({
2992
3067
  })();
2993
3068
 
2994
3069
  // src/commands/init.ts
2995
- import { mkdir, writeFile, access } from "node:fs/promises";
3070
+ import { mkdir as mkdir2, writeFile as writeFile2, access } from "node:fs/promises";
3071
+
3072
+ // src/commands/codegraph.ts
3073
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
3074
+ import { homedir as homedir4 } from "node:os";
3075
+ import { dirname as dirname2, join as join5 } from "node:path";
3076
+ async function loadRunCommand() {
3077
+ return await Promise.resolve().then(() => (init_run_command(), run_command_exports));
3078
+ }
3079
+ var CODEGRAPH_PACKAGE = "@colbymchenry/codegraph";
3080
+ var CODEGRAPH_IGNORE_ENTRY = ".codegraph/";
3081
+ function globalGitIgnorePath(env = process.env, home = homedir4()) {
3082
+ const xdg = env.XDG_CONFIG_HOME?.trim();
3083
+ const base = xdg ? xdg : join5(home, ".config");
3084
+ return join5(base, "git", "ignore");
3085
+ }
3086
+ function appendIgnoreEntry(current, entry) {
3087
+ const bare = entry.replace(/\/$/, "");
3088
+ const alreadyListed = current.split("\n").map((line) => line.trim()).some((line) => line === entry || line === bare);
3089
+ if (alreadyListed) return null;
3090
+ if (current === "") return `${entry}
3091
+ `;
3092
+ return current.endsWith("\n") ? `${current}${entry}
3093
+ ` : `${current}
3094
+ ${entry}
3095
+ `;
3096
+ }
3097
+ async function installCodegraphCli(logPrefix, mode) {
3098
+ const progressive = mode === "install" ? "Installing" : "Updating";
3099
+ const past = mode === "install" ? "installed" : "updated";
3100
+ console.log(`[${logPrefix}] ${progressive} CodeGraph CLI (npm install -g ${CODEGRAPH_PACKAGE}@latest)...`);
3101
+ try {
3102
+ const { runCommand: runCommand2 } = await loadRunCommand();
3103
+ await runCommand2("npm", ["install", "-g", `${CODEGRAPH_PACKAGE}@latest`]);
3104
+ console.log(`[${logPrefix}] CodeGraph CLI ${past}.`);
3105
+ return true;
3106
+ } catch (err) {
3107
+ console.error(`[${logPrefix}] Failed to ${mode} CodeGraph CLI: ${err.message}`);
3108
+ return false;
3109
+ }
3110
+ }
3111
+ async function runCodegraphInit(logPrefix) {
3112
+ console.log(`[${logPrefix}] Initializing CodeGraph index (codegraph init)...`);
3113
+ try {
3114
+ const { runCommand: runCommand2 } = await loadRunCommand();
3115
+ await runCommand2("codegraph", ["init"]);
3116
+ console.log(`[${logPrefix}] CodeGraph index initialized.`);
3117
+ return true;
3118
+ } catch (err) {
3119
+ console.error(
3120
+ `[${logPrefix}] Failed to run codegraph init (install it with \`claude-task-worker install\`): ${err.message}`
3121
+ );
3122
+ return false;
3123
+ }
3124
+ }
3125
+ async function ensureCodegraphGitIgnore(logPrefix) {
3126
+ const path = globalGitIgnorePath();
3127
+ try {
3128
+ let current = "";
3129
+ try {
3130
+ current = await readFile(path, "utf-8");
3131
+ } catch {
3132
+ }
3133
+ const next = appendIgnoreEntry(current, CODEGRAPH_IGNORE_ENTRY);
3134
+ if (next === null) {
3135
+ console.log(`[${logPrefix}] Already ignored: ${CODEGRAPH_IGNORE_ENTRY} (${path})`);
3136
+ return true;
3137
+ }
3138
+ await mkdir(dirname2(path), { recursive: true });
3139
+ await writeFile(path, next, "utf-8");
3140
+ console.log(`[${logPrefix}] Added ${CODEGRAPH_IGNORE_ENTRY} to ${path}`);
3141
+ return true;
3142
+ } catch (err) {
3143
+ console.error(`[${logPrefix}] Failed to update ${path}: ${err.message}`);
3144
+ return false;
3145
+ }
3146
+ }
3147
+
3148
+ // src/commands/init.ts
2996
3149
  var LABELS = [
2997
3150
  { name: "cc-update-issue", color: "e4e669" },
2998
3151
  { name: "cc-answer-issue-questions", color: "5319e7" },
@@ -3048,10 +3201,10 @@ async function writeFileWithMode(path, content, force) {
3048
3201
  try {
3049
3202
  await access(path);
3050
3203
  if (!force) return "skipped";
3051
- await writeFile(path, content, "utf-8");
3204
+ await writeFile2(path, content, "utf-8");
3052
3205
  return "overwritten";
3053
3206
  } catch {
3054
- await writeFile(path, content, "utf-8");
3207
+ await writeFile2(path, content, "utf-8");
3055
3208
  return "created";
3056
3209
  }
3057
3210
  }
@@ -3077,53 +3230,23 @@ async function init(options = {}) {
3077
3230
  }
3078
3231
  }
3079
3232
  console.log("[init] Creating issue template...");
3080
- await mkdir(".github/ISSUE_TEMPLATE", { recursive: true });
3233
+ await mkdir2(".github/ISSUE_TEMPLATE", { recursive: true });
3081
3234
  const templatePath = ".github/ISSUE_TEMPLATE/cc-triage-scope.yml";
3082
3235
  logWriteResult(await writeFileWithMode(templatePath, ISSUE_TEMPLATE, force), templatePath);
3083
3236
  console.log("[init] Creating GitHub Actions workflow...");
3084
- await mkdir(".github/workflows", { recursive: true });
3237
+ await mkdir2(".github/workflows", { recursive: true });
3085
3238
  const workflowPath = ".github/workflows/assign-creator-on-cc-triage-scope.yml";
3086
3239
  logWriteResult(await writeFileWithMode(workflowPath, ASSIGN_CREATOR_WORKFLOW, force), workflowPath);
3087
3240
  console.log("[init] Creating config file...");
3088
3241
  await createConfig(force);
3242
+ console.log("[init] Setting up CodeGraph...");
3243
+ await ensureCodegraphGitIgnore("init");
3244
+ await runCodegraphInit("init");
3089
3245
  console.log("[init] Done.");
3090
3246
  }
3091
3247
 
3092
- // src/commands/run-command.ts
3093
- import { spawn as spawn2 } from "node:child_process";
3094
- function runCommand(command, args) {
3095
- return new Promise((resolve3, reject) => {
3096
- const child = spawn2(command, args, {
3097
- stdio: "inherit",
3098
- shell: process.platform === "win32"
3099
- });
3100
- const forwardSignal = (signal) => {
3101
- child.kill(signal);
3102
- };
3103
- const onSigint = () => forwardSignal("SIGINT");
3104
- const onSigterm = () => forwardSignal("SIGTERM");
3105
- const cleanup = () => {
3106
- process.removeListener("SIGINT", onSigint);
3107
- process.removeListener("SIGTERM", onSigterm);
3108
- };
3109
- process.once("SIGINT", onSigint);
3110
- process.once("SIGTERM", onSigterm);
3111
- child.on("error", (err) => {
3112
- cleanup();
3113
- reject(err);
3114
- });
3115
- child.on("close", (code) => {
3116
- cleanup();
3117
- if (code === 0) {
3118
- resolve3();
3119
- } else {
3120
- reject(new Error(`${command} ${args.join(" ")} exited with code ${code}`));
3121
- }
3122
- });
3123
- });
3124
- }
3125
-
3126
3248
  // src/commands/install.ts
3249
+ init_run_command();
3127
3250
  var PLUGIN_NAME = "claude-task-worker";
3128
3251
  var MARKETPLACE_NAME = "claude-task-worker";
3129
3252
  var MARKETPLACE_SOURCE = "getty104/claude-task-worker";
@@ -3165,13 +3288,15 @@ async function install() {
3165
3288
  await addMarketplace();
3166
3289
  const pluginOk = await installPlugin();
3167
3290
  const cliOk = await installCli();
3168
- if (!pluginOk || !cliOk) {
3291
+ const codegraphOk = await installCodegraphCli("install", "install");
3292
+ if (!pluginOk || !cliOk || !codegraphOk) {
3169
3293
  process.exitCode = 1;
3170
3294
  }
3171
3295
  console.log("[install] Done.");
3172
3296
  }
3173
3297
 
3174
3298
  // src/commands/update.ts
3299
+ init_run_command();
3175
3300
  var PLUGIN_NAME2 = "claude-task-worker";
3176
3301
  var MARKETPLACE_NAME2 = "claude-task-worker";
3177
3302
  async function updateMarketplace() {
@@ -3212,7 +3337,8 @@ async function update() {
3212
3337
  const marketplaceOk = await updateMarketplace();
3213
3338
  const pluginOk = await updatePlugin();
3214
3339
  const cliOk = await updateCli();
3215
- if (!marketplaceOk || !pluginOk || !cliOk) {
3340
+ const codegraphOk = await installCodegraphCli("update", "update");
3341
+ if (!marketplaceOk || !pluginOk || !cliOk || !codegraphOk) {
3216
3342
  process.exitCode = 1;
3217
3343
  }
3218
3344
  console.log("[update] Done.");
@@ -3220,11 +3346,11 @@ async function update() {
3220
3346
 
3221
3347
  // src/commands/version.ts
3222
3348
  import { readFileSync as readFileSync4 } from "node:fs";
3223
- import { dirname as dirname2, join as join5 } from "node:path";
3349
+ import { dirname as dirname3, join as join6 } from "node:path";
3224
3350
  import { fileURLToPath } from "node:url";
3225
3351
  function version() {
3226
- const here = dirname2(fileURLToPath(import.meta.url));
3227
- const pkgPath = join5(here, "..", "package.json");
3352
+ const here = dirname3(fileURLToPath(import.meta.url));
3353
+ const pkgPath = join6(here, "..", "package.json");
3228
3354
  try {
3229
3355
  const pkg = JSON.parse(readFileSync4(pkgPath, "utf8"));
3230
3356
  console.log(pkg.version ?? "unknown");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.27.1",
3
+ "version": "0.29.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",