claude-task-worker 0.19.0 → 0.20.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 +160 -13
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -572,11 +572,14 @@ async function listIssuesByNumbers(assignee, labels, excludeLabels, numbers) {
572
572
  return results;
573
573
  }
574
574
  async function findOpenPrNumberByHeadRef(headRefName) {
575
+ return findPrNumberByHeadRef(headRefName, "open");
576
+ }
577
+ async function findPrNumberByHeadRef(headRefName, state = "open") {
575
578
  const output = await execGh([
576
579
  "pr",
577
580
  "list",
578
581
  "--state",
579
- "open",
582
+ state,
580
583
  "--head",
581
584
  headRefName,
582
585
  "--json",
@@ -587,6 +590,37 @@ async function findOpenPrNumberByHeadRef(headRefName) {
587
590
  const prs = JSON.parse(output);
588
591
  return prs.length > 0 ? prs[0].number : null;
589
592
  }
593
+ async function getIssueState(issueNumber) {
594
+ const output = await execGh(["issue", "view", String(issueNumber), "--json", "state"]);
595
+ const parsed = JSON.parse(output);
596
+ return parsed.state;
597
+ }
598
+ async function findPrNumberClosingIssue(issueNumber) {
599
+ const { owner, name } = await getRepoInfo();
600
+ const query = `query($owner: String!, $name: String!, $number: Int!) {
601
+ repository(owner: $owner, name: $name) {
602
+ issue(number: $number) {
603
+ closedByPullRequestsReferences(first: 10, includeClosedPrs: true) { nodes { number state } }
604
+ }
605
+ }
606
+ }`;
607
+ const output = await execGh([
608
+ "api",
609
+ "graphql",
610
+ "-f",
611
+ `query=${query}`,
612
+ "-F",
613
+ `owner=${owner}`,
614
+ "-F",
615
+ `name=${name}`,
616
+ "-F",
617
+ `number=${issueNumber}`
618
+ ]);
619
+ const parsed = JSON.parse(output);
620
+ const nodes = parsed?.data?.repository?.issue?.closedByPullRequestsReferences?.nodes ?? [];
621
+ const validPr = nodes.find((node) => node.state === "MERGED" || node.state === "OPEN");
622
+ return validPr ? validPr.number : null;
623
+ }
590
624
  async function getIssueSubIssuesSummary(issueNumber) {
591
625
  const output = await execGh(["issue", "view", String(issueNumber), "--json", "subIssuesSummary"]);
592
626
  const parsed = JSON.parse(output);
@@ -694,6 +728,9 @@ async function removeLabel(type, number, label) {
694
728
  async function commentOnPR(prNumber, body) {
695
729
  await execGh(["pr", "comment", String(prNumber), "--body", body]);
696
730
  }
731
+ async function commentOnIssue(issueNumber, body) {
732
+ await execGh(["issue", "comment", String(issueNumber), "--body", body]);
733
+ }
697
734
  async function createLabel(name, color, force) {
698
735
  try {
699
736
  const args = ["label", "create", name];
@@ -712,6 +749,11 @@ var DISALLOWED_TOOLS = [
712
749
  // 呼ぶと処理未完のままプロセスが終了する。
713
750
  "Monitor",
714
751
  "ScheduleWakeup",
752
+ // SendMessage は過去に起動したサブエージェントをバックグラウンドでしか再開できず、
753
+ // 「完了通知を待ちます」でターンを終える誘因になる(通知が届く前にプロセスが正常終了し、
754
+ // PR未作成のまま完了扱いになる事故の実例あり)。未完のサブエージェントへの追加指示は
755
+ // フォアグラウンドの新規 Agent 起動で代替させる。
756
+ "SendMessage",
715
757
  // 対話 / 承認: 自律実行セッションには回答・承認するユーザーが存在しない。
716
758
  "AskUserQuestion",
717
759
  "EnterPlanMode",
@@ -972,8 +1014,32 @@ async function assertRemoteTrackingExists(epicBranch) {
972
1014
  // src/process-manager.ts
973
1015
  import { spawn } from "node:child_process";
974
1016
  init_table();
975
- var childProcesses = /* @__PURE__ */ new Map();
1017
+
1018
+ // src/task-result.ts
976
1019
  var TASK_TIMEOUT_MS = 90 * 60 * 1e3;
1020
+ var STDERR_TAIL_LIMIT = 8 * 1024;
1021
+ function buildTaskResult(code, timedOut, stdout, stderrTail) {
1022
+ const emptyOutput = stdout.trim() === "";
1023
+ const completed = !timedOut && code === 0 && !emptyOutput;
1024
+ let output = stdout;
1025
+ if (timedOut) {
1026
+ output += `
1027
+ [worker] task timed out after ${TASK_TIMEOUT_MS / 1e3}s`;
1028
+ } else if (code === 0 && emptyOutput) {
1029
+ output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
1030
+ } else if (!completed) {
1031
+ output += `
1032
+ [worker] claude exited with code ${code}`;
1033
+ }
1034
+ if (!completed && stderrTail.trim() !== "") {
1035
+ output += `
1036
+ [stderr] ${stderrTail.trim()}`;
1037
+ }
1038
+ return { status: completed ? "completed" : "failed", output };
1039
+ }
1040
+
1041
+ // src/process-manager.ts
1042
+ var childProcesses = /* @__PURE__ */ new Map();
977
1043
  var tasks = /* @__PURE__ */ new Map();
978
1044
  var shuttingDown = false;
979
1045
  function setShuttingDown() {
@@ -1105,11 +1171,20 @@ function run(command, args, id, title, workerName, path, onComplete, cwd) {
1105
1171
  ...cwd ? { cwd } : {}
1106
1172
  });
1107
1173
  childProcesses.set(id, child);
1108
- child.stderr?.resume();
1109
1174
  const outputChunks = [];
1110
1175
  child.stdout?.on("data", (chunk) => {
1111
1176
  outputChunks.push(chunk);
1112
1177
  });
1178
+ const stderrChunks = [];
1179
+ let stderrLen = 0;
1180
+ child.stderr?.on("data", (chunk) => {
1181
+ stderrChunks.push(chunk);
1182
+ stderrLen += chunk.length;
1183
+ while (stderrChunks.length > 1 && stderrLen - stderrChunks[0].length >= STDERR_TAIL_LIMIT) {
1184
+ stderrLen -= stderrChunks[0].length;
1185
+ stderrChunks.shift();
1186
+ }
1187
+ });
1113
1188
  let timedOut = false;
1114
1189
  const timeoutHandle = setTimeout(() => {
1115
1190
  timedOut = true;
@@ -1128,9 +1203,12 @@ function run(command, args, id, title, workerName, path, onComplete, cwd) {
1128
1203
  timeoutHandle.unref();
1129
1204
  child.on("close", async (code) => {
1130
1205
  clearTimeout(timeoutHandle);
1131
- const output = Buffer.concat(outputChunks).toString("utf-8") + (timedOut ? `
1132
- [worker] task timed out after ${TASK_TIMEOUT_MS / 1e3}s` : "");
1133
- const finalStatus = !timedOut && code === 0 ? "completed" : "failed";
1206
+ const { status: finalStatus, output } = buildTaskResult(
1207
+ code,
1208
+ timedOut,
1209
+ Buffer.concat(outputChunks).toString("utf-8"),
1210
+ Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
1211
+ );
1134
1212
  try {
1135
1213
  await Promise.race([
1136
1214
  onComplete?.(finalStatus, output) ?? Promise.resolve(),
@@ -1651,6 +1729,14 @@ async function deleteLocalBranch(branchName) {
1651
1729
  console.error(`[worktree] Failed to delete local branch ${branchName}: ${stderr.trim()}`);
1652
1730
  }
1653
1731
  }
1732
+ async function localBranchExists(branchName) {
1733
+ try {
1734
+ await execFileAsync2("git", ["rev-parse", "--verify", "--quiet", `refs/heads/${branchName}`]);
1735
+ return true;
1736
+ } catch {
1737
+ return false;
1738
+ }
1739
+ }
1654
1740
  function getWorktreePath(worktreeId) {
1655
1741
  return `${WORKTREES_DIR}/${worktreeId}`;
1656
1742
  }
@@ -1814,13 +1900,20 @@ function createIssuePollingWorker(config) {
1814
1900
  }
1815
1901
  try {
1816
1902
  if (status === "completed") {
1817
- await config.onCompleted?.(issue.number);
1818
- await notifyTaskCompleted(config.name, name, issue.number, issue.title, issueUrl, output);
1903
+ const verified = await config.onCompleted?.(issue.number, worktreeId) ?? true;
1904
+ if (verified === false) {
1905
+ await notifyTaskFailed(config.name, name, issue.number, issue.title, issueUrl, output);
1906
+ } else {
1907
+ await notifyTaskCompleted(config.name, name, issue.number, issue.title, issueUrl, output);
1908
+ }
1819
1909
  } else {
1820
1910
  await notifyTaskFailed(config.name, name, issue.number, issue.title, issueUrl, output);
1821
1911
  }
1822
1912
  } catch (err) {
1823
1913
  console.error(`[${config.name}] post-task error for #${issue.number}: ${err}`);
1914
+ await notifyTaskFailed(config.name, name, issue.number, issue.title, issueUrl, output).catch(
1915
+ (notifyErr) => console.error(`[${config.name}] notifyTaskFailed failed for #${issue.number}: ${notifyErr}`)
1916
+ );
1824
1917
  } finally {
1825
1918
  await removeLabel("issue", issue.number, "cc-in-progress").catch(
1826
1919
  (err) => console.error(`[${config.name}] removeLabel cc-in-progress failed for #${issue.number}: ${err}`)
@@ -1852,18 +1945,47 @@ function createIssuePollingWorker(config) {
1852
1945
  }
1853
1946
 
1854
1947
  // src/workers/exec-issue.ts
1948
+ function prMissingComment(worktreeId) {
1949
+ return [
1950
+ "## PR\u672A\u4F5C\u6210\u306E\u307E\u307E\u81EA\u52D5\u5B9F\u884C\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
1951
+ `exec-issue \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u6B63\u5E38\u7D42\u4E86\uFF08exit 0\uFF09\u3057\u307E\u3057\u305F\u304C\u3001\u3053\u306E\u5B9F\u884C\u306E\u4F5C\u696D\u30D6\u30E9\u30F3\u30C1\uFF08\`${worktreeId}\`\uFF09\u3092 head \u3068\u3059\u308BPR\u3082\u3001\u672CIssue\u3092 closing \u53C2\u7167\u3059\u308BPR\u3082\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002PR\u4F5C\u6210\u524D\u306B\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`,
1952
+ "",
1953
+ "## \u72B6\u614B\u306E\u78BA\u8A8D",
1954
+ `- \u5909\u66F4\u304C push \u6E08\u307F\u306E\u5834\u5408\u306F\u30EA\u30E2\u30FC\u30C8\u30D6\u30E9\u30F3\u30C1 \`${worktreeId}\` \u304C\u6B8B\u3063\u3066\u3044\u307E\u3059\u3002\u5185\u5BB9\u3092\u78BA\u8A8D\u3057\u3001\u5FC5\u8981\u306A\u3089\u624B\u52D5\u3067PR\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044`,
1955
+ "",
1956
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
1957
+ "- \u81EA\u52D5\u5B9F\u884C\u3092\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-exec-issue` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u76F4\u3057\u3066\u304F\u3060\u3055\u3044",
1958
+ "- \u624B\u52D5\u3067PR\u3092\u4F5C\u6210\u3057\u305F\u5834\u5408\u306A\u3069\u5BFE\u5FDC\u6E08\u307F\u306E\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3066\u304F\u3060\u3055\u3044"
1959
+ ].join("\n");
1960
+ }
1855
1961
  var execIssueWorker = (opts = {}) => createIssuePollingWorker({
1856
1962
  name: "exec-issue",
1857
1963
  command: "/claude-task-worker:exec-issue",
1858
1964
  triggerLabels: ["cc-exec-issue"],
1859
1965
  epicFilters: opts.epicFilters,
1860
1966
  labelFilters: opts.labelFilters,
1861
- onCompleted: async (issueNumber) => {
1967
+ onCompleted: async (issueNumber, worktreeId) => {
1862
1968
  if (await hasLabel("issue", issueNumber, "cc-need-human-check")) {
1863
1969
  console.log(`[exec-issue] #${issueNumber}: cc-need-human-check present, skip cc-pr-created`);
1970
+ return false;
1971
+ }
1972
+ if (await getIssueState(issueNumber) === "CLOSED") {
1973
+ console.log(`[exec-issue] #${issueNumber}: issue closed by skill (no-change path), skip cc-pr-created`);
1864
1974
  return;
1865
1975
  }
1866
- await addLabel("issue", issueNumber, "cc-pr-created");
1976
+ const prNumber = await findPrNumberByHeadRef(worktreeId, "all") ?? await findPrNumberClosingIssue(issueNumber);
1977
+ if (prNumber !== null) {
1978
+ await addLabel("issue", issueNumber, "cc-pr-created");
1979
+ return;
1980
+ }
1981
+ console.error(
1982
+ `[exec-issue] #${issueNumber}: session exited without a PR (branch: ${worktreeId}); marking cc-need-human-check`
1983
+ );
1984
+ await addLabel("issue", issueNumber, "cc-need-human-check");
1985
+ await commentOnIssue(issueNumber, prMissingComment(worktreeId)).catch(
1986
+ (err) => console.error(`[exec-issue] commentOnIssue failed for #${issueNumber}: ${err}`)
1987
+ );
1988
+ return false;
1867
1989
  }
1868
1990
  })();
1869
1991
 
@@ -1897,6 +2019,14 @@ function createPrPollingWorker(config) {
1897
2019
  try {
1898
2020
  await removeWorktreeByBranch(pr.headRefName);
1899
2021
  await deleteLocalBranch(pr.headRefName);
2022
+ if (await localBranchExists(pr.headRefName)) {
2023
+ console.error(
2024
+ `[${config.name}] PR #${pr.number}: branch ${pr.headRefName} is still checked out by another worktree; skipping this tick`
2025
+ );
2026
+ await removeLabel("pr", pr.number, LABEL_IN_PROGRESS).catch(() => {
2027
+ });
2028
+ continue;
2029
+ }
1900
2030
  syncDefaultBranch(defaultBranch);
1901
2031
  await createWorktreeFromBranch(worktreeId, defaultBranch);
1902
2032
  const cwd = getWorktreePath(worktreeId);
@@ -2074,6 +2204,19 @@ var checkDependabotWorker = createPrPollingWorker({
2074
2204
  });
2075
2205
 
2076
2206
  // src/workers/epic-issue.ts
2207
+ function epicPrMissingComment(issueNumber) {
2208
+ return [
2209
+ "## Epic PR\u672A\u4F5C\u6210\u306E\u307E\u307E\u81EA\u52D5\u5B9F\u884C\u304C\u7D42\u4E86\u3057\u307E\u3057\u305F\uFF08\u8981\u4EBA\u624B\u78BA\u8A8D\uFF09",
2210
+ `create-epic-pr \u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F\u6B63\u5E38\u7D42\u4E86\uFF08exit 0\uFF09\u3057\u307E\u3057\u305F\u304C\u3001Epic \u30D6\u30E9\u30F3\u30C1 \`cc-epic-${issueNumber}\` \u3092 head \u3068\u3059\u308B\u30AA\u30FC\u30D7\u30F3\u306APR\u304C\u898B\u3064\u304B\u308A\u307E\u305B\u3093\u3067\u3057\u305F\u3002PR\u4F5C\u6210\u524D\u306B\u30BB\u30C3\u30B7\u30E7\u30F3\u304C\u7D42\u4E86\u3057\u305F\u53EF\u80FD\u6027\u304C\u3042\u308A\u307E\u3059\u3002`,
2211
+ "",
2212
+ "## \u72B6\u614B\u306E\u78BA\u8A8D",
2213
+ `- \u5909\u66F4\u304C push \u6E08\u307F\u306E\u5834\u5408\u306F\u30EA\u30E2\u30FC\u30C8\u30D6\u30E9\u30F3\u30C1 \`cc-epic-${issueNumber}\` \u304C\u6B8B\u3063\u3066\u3044\u307E\u3059\u3002\u5185\u5BB9\u3092\u78BA\u8A8D\u3057\u3001\u5FC5\u8981\u306A\u3089\u624B\u52D5\u3067PR\u3092\u4F5C\u6210\u3057\u3066\u304F\u3060\u3055\u3044`,
2214
+ "",
2215
+ "## \u5BFE\u5FDC\u5F8C\u306E\u9032\u3081\u65B9",
2216
+ "- \u81EA\u52D5\u5B9F\u884C\u3092\u3084\u308A\u76F4\u3059\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3001`cc-epic-issue` \u30E9\u30D9\u30EB\u3092\u4ED8\u3051\u76F4\u3057\u3066\u304F\u3060\u3055\u3044",
2217
+ "- \u624B\u52D5\u3067PR\u3092\u4F5C\u6210\u3057\u305F\u5834\u5408\u306A\u3069\u5BFE\u5FDC\u6E08\u307F\u306E\u5834\u5408: `cc-need-human-check` \u30E9\u30D9\u30EB\u3092\u5916\u3057\u3066\u304F\u3060\u3055\u3044"
2218
+ ].join("\n");
2219
+ }
2077
2220
  var epicIssueWorker = (opts = {}) => createIssuePollingWorker({
2078
2221
  name: "epic-issue",
2079
2222
  command: "/claude-task-worker:create-epic-pr",
@@ -2088,12 +2231,16 @@ var epicIssueWorker = (opts = {}) => createIssuePollingWorker({
2088
2231
  return "proceed";
2089
2232
  },
2090
2233
  onCompleted: async (issueNumber) => {
2091
- await addLabel("issue", issueNumber, "cc-pr-created");
2092
2234
  const prNumber = await findOpenPrNumberByHeadRef(`cc-epic-${issueNumber}`);
2093
2235
  if (prNumber === null) {
2094
- console.error(`[epic-issue] Epic PR for branch cc-epic-${issueNumber} not found; skip labeling`);
2095
- return;
2236
+ console.error(`[epic-issue] Epic PR for branch cc-epic-${issueNumber} not found; skip cc-pr-created`);
2237
+ await addLabel("issue", issueNumber, "cc-need-human-check");
2238
+ await commentOnIssue(issueNumber, epicPrMissingComment(issueNumber)).catch(
2239
+ (err) => console.error(`[epic-issue] commentOnIssue failed for #${issueNumber}: ${err}`)
2240
+ );
2241
+ return false;
2096
2242
  }
2243
+ await addLabel("issue", issueNumber, "cc-pr-created");
2097
2244
  await addLabel("pr", prNumber, "cc-epic-issue");
2098
2245
  await addLabel("pr", prNumber, "cc-triage-scope");
2099
2246
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.19.0",
3
+ "version": "0.20.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",