claude-task-worker 0.80.0 → 0.82.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 +267 -10
  2. package/package.json +1 -1
package/dist/index.js CHANGED
@@ -1525,6 +1525,15 @@ async function commentOnPR(prNumber, body) {
1525
1525
  async function commentOnIssue(issueNumber, body) {
1526
1526
  await execGh(["issue", "comment", String(issueNumber), "--body", body]);
1527
1527
  }
1528
+ async function createPullRequest(base, head, title, body) {
1529
+ const url = await execGh(["pr", "create", "--base", base, "--head", head, "--title", title, "--body", body]);
1530
+ const matched = url.match(/\/pull\/(\d+)/);
1531
+ if (!matched) throw new Error(`gh pr create returned an unexpected output: ${url}`);
1532
+ return Number(matched[1]);
1533
+ }
1534
+ async function mergePullRequest(prNumber) {
1535
+ await execGh(["pr", "merge", String(prNumber), "--merge", "--delete-branch"]);
1536
+ }
1528
1537
  async function createLabel(name, color, force) {
1529
1538
  try {
1530
1539
  const args = ["label", "create", name];
@@ -1892,7 +1901,7 @@ function buildClaudeEnv(mode) {
1892
1901
  }
1893
1902
 
1894
1903
  // src/config.ts
1895
- import { readFileSync as readFileSync2 } from "node:fs";
1904
+ import { readFileSync as readFileSync2, writeFileSync as writeFileSync2 } from "node:fs";
1896
1905
  import { isAbsolute as isAbsolute2, join as join2, normalize, sep as SEP } from "node:path";
1897
1906
  var DEFAULT_UI_DESIGN_CONFIG = {
1898
1907
  enabled: false,
@@ -2016,11 +2025,44 @@ var WORKER_DEFAULTS = {
2016
2025
  pollingIntervalSeconds: 300,
2017
2026
  cooldownSeconds: 0,
2018
2027
  maxConcurrentTasks: 1
2028
+ },
2029
+ // 以下3つは定期ワーカー(createScheduledWorker)。実行間隔そのものは
2030
+ // SCHEDULE_INTERVAL_HOURS(24時間)と実行ログで決まり、pollingIntervalSeconds は
2031
+ // 「24時間経過したかを確認する頻度」でしかない。
2032
+ // model が opus なのは、成果物(CODING_GUIDELINES.md / .claude/requirements/ / DESIGN.md)が
2033
+ // 後続の全 Issue・全デザインの前提として読まれ、誤った一般化がそのまま下流の手戻りになるため。
2034
+ "update-coding-guidelines": {
2035
+ skill: "/claude-task-worker:update-coding-guidelines",
2036
+ model: "opus",
2037
+ advisorModel: "",
2038
+ effort: "high",
2039
+ pollingIntervalSeconds: 3600,
2040
+ cooldownSeconds: 0,
2041
+ maxConcurrentTasks: 1
2042
+ },
2043
+ "update-requirement-rules": {
2044
+ skill: "/claude-task-worker:update-requirement-rules",
2045
+ model: "opus",
2046
+ advisorModel: "",
2047
+ effort: "high",
2048
+ pollingIntervalSeconds: 3600,
2049
+ cooldownSeconds: 0,
2050
+ maxConcurrentTasks: 1
2051
+ },
2052
+ "update-design-md": {
2053
+ skill: "/claude-task-worker:update-design-md",
2054
+ model: "opus",
2055
+ advisorModel: "",
2056
+ effort: "high",
2057
+ pollingIntervalSeconds: 3600,
2058
+ cooldownSeconds: 0,
2059
+ maxConcurrentTasks: 1
2019
2060
  }
2020
2061
  };
2021
2062
  var DEFAULT_CONFIG = {
2022
2063
  fixReviewPointCallbackCommentMessage: "",
2023
2064
  uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG },
2065
+ lastRun: {},
2024
2066
  workers: {}
2025
2067
  };
2026
2068
  var CONFIG_PATH = join2(process.cwd(), "claude-task-worker.json");
@@ -2135,6 +2177,21 @@ function parseUiDesignEntry(val) {
2135
2177
  }
2136
2178
  return result;
2137
2179
  }
2180
+ function parseLastRunEntry(val) {
2181
+ if (typeof val !== "object" || val === null || Array.isArray(val)) {
2182
+ console.warn(`[config] invalid lastRun: expected object, ignoring`);
2183
+ return {};
2184
+ }
2185
+ const result = {};
2186
+ for (const [name, at] of Object.entries(val)) {
2187
+ if (typeof at === "string" && !Number.isNaN(Date.parse(at))) {
2188
+ result[name] = at;
2189
+ } else {
2190
+ console.warn(`[config] invalid lastRun.${name}: ${String(at)}, ignoring`);
2191
+ }
2192
+ }
2193
+ return result;
2194
+ }
2138
2195
  function loadConfig() {
2139
2196
  const configPath = CONFIG_PATH;
2140
2197
  let raw;
@@ -2142,11 +2199,14 @@ function loadConfig() {
2142
2199
  raw = JSON.parse(readFileSync2(configPath, "utf-8"));
2143
2200
  } catch (err) {
2144
2201
  if (err.code === "ENOENT") {
2145
- return { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, workers: {} };
2202
+ return { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, lastRun: {}, workers: {} };
2146
2203
  }
2147
2204
  throw err;
2148
2205
  }
2149
- const result = { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, workers: {} };
2206
+ const result = { ...DEFAULT_CONFIG, uiDesign: { ...DEFAULT_UI_DESIGN_CONFIG }, lastRun: {}, workers: {} };
2207
+ if ("lastRun" in raw) {
2208
+ result.lastRun = parseLastRunEntry(raw["lastRun"]);
2209
+ }
2150
2210
  if ("fixReviewPointCallbackCommentMessage" in raw) {
2151
2211
  const val = raw["fixReviewPointCallbackCommentMessage"];
2152
2212
  if (typeof val === "string") {
@@ -2173,6 +2233,36 @@ function getWorkerConfig(workerName) {
2173
2233
  const config = loadConfig();
2174
2234
  return config.workers[workerName] ?? { ...defaultsFor(workerName) };
2175
2235
  }
2236
+ function getLastRunAt(workerName) {
2237
+ let at;
2238
+ try {
2239
+ at = loadConfig().lastRun[workerName];
2240
+ } catch (err) {
2241
+ console.warn(`[config] failed to load lastRun, treating ${workerName} as never run: ${err}`);
2242
+ return void 0;
2243
+ }
2244
+ if (at === void 0) return void 0;
2245
+ const parsed = Date.parse(at);
2246
+ return Number.isNaN(parsed) ? void 0 : parsed;
2247
+ }
2248
+ function writeLastRun(repoRoot, workerName, at = /* @__PURE__ */ new Date()) {
2249
+ const path2 = join2(repoRoot, "claude-task-worker.json");
2250
+ let raw = {};
2251
+ try {
2252
+ const parsed = JSON.parse(readFileSync2(path2, "utf-8"));
2253
+ if (typeof parsed === "object" && parsed !== null && !Array.isArray(parsed)) {
2254
+ raw = parsed;
2255
+ }
2256
+ } catch (err) {
2257
+ if (err.code !== "ENOENT") {
2258
+ console.warn(`[config] failed to read ${path2}, rewriting it with lastRun only: ${err}`);
2259
+ }
2260
+ }
2261
+ const current = typeof raw["lastRun"] === "object" && raw["lastRun"] !== null ? raw["lastRun"] : {};
2262
+ raw["lastRun"] = { ...current, [workerName]: at.toISOString() };
2263
+ writeFileSync2(path2, `${JSON.stringify(raw, null, 2)}
2264
+ `, "utf-8");
2265
+ }
2176
2266
  function getUiDesignConfig() {
2177
2267
  try {
2178
2268
  return loadConfig().uiDesign;
@@ -2722,13 +2812,13 @@ function isGeneratedWorktreeName(name) {
2722
2812
 
2723
2813
  // src/slack.ts
2724
2814
  import { exec } from "node:child_process";
2725
- import { readFileSync as readFileSync4, writeFileSync as writeFileSync3 } from "node:fs";
2815
+ import { readFileSync as readFileSync4, writeFileSync as writeFileSync4 } from "node:fs";
2726
2816
  import { homedir as homedir4 } from "node:os";
2727
2817
  import { join as join5 } from "node:path";
2728
2818
  import { promisify as promisify2 } from "node:util";
2729
2819
 
2730
2820
  // src/runcat.ts
2731
- import { mkdirSync as mkdirSync2, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync2 } from "node:fs";
2821
+ import { mkdirSync as mkdirSync2, renameSync as renameSync2, rmSync, writeFileSync as writeFileSync3 } from "node:fs";
2732
2822
  import { homedir as homedir3 } from "node:os";
2733
2823
  import { dirname, join as join4 } from "node:path";
2734
2824
  var RUNCAT_OUT_FILE = process.env.RUNCAT_OUT_FILE ?? join4(process.env.HOME ?? homedir3(), ".claude", "runcat-usage.json");
@@ -2798,7 +2888,7 @@ function writeRuncatUsage(usage, outFile = RUNCAT_OUT_FILE) {
2798
2888
  const tmp = join4(dir, `.runcat-${process.pid}-${Date.now()}.json`);
2799
2889
  try {
2800
2890
  mkdirSync2(dir, { recursive: true });
2801
- writeFileSync2(tmp, JSON.stringify(snapshot), "utf-8");
2891
+ writeFileSync3(tmp, JSON.stringify(snapshot), "utf-8");
2802
2892
  renameSync2(tmp, outFile);
2803
2893
  } catch (err) {
2804
2894
  rmSync(tmp, { force: true });
@@ -2849,7 +2939,7 @@ function readUsageCache() {
2849
2939
  }
2850
2940
  function writeUsageCache(data) {
2851
2941
  try {
2852
- writeFileSync3(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
2942
+ writeFileSync4(USAGE_CACHE_PATH, JSON.stringify({ timestamp: Date.now(), data }));
2853
2943
  } catch {
2854
2944
  }
2855
2945
  }
@@ -3816,6 +3906,159 @@ var applyUiDesignWorker = async (opts = {}) => {
3816
3906
  })();
3817
3907
  };
3818
3908
 
3909
+ // src/last-run-pr.ts
3910
+ import { execFile as execFile3 } from "node:child_process";
3911
+ import { promisify as promisify4 } from "node:util";
3912
+ var execFileAsync3 = promisify4(execFile3);
3913
+ var CONFIG_FILE = "claude-task-worker.json";
3914
+ function lastRunBranchName(workerName) {
3915
+ return `ctw-last-run-${workerName}`;
3916
+ }
3917
+ function hasLastRunChange(porcelain) {
3918
+ return porcelain.trim().length > 0;
3919
+ }
3920
+ function lastRunPrTitle(workerName) {
3921
+ return `chore: ${workerName} \u306E\u5B9F\u884C\u8A18\u9332\u3092\u66F4\u65B0`;
3922
+ }
3923
+ function lastRunPrBody(workerName, at) {
3924
+ return [
3925
+ `\`claude-task-worker\` \u306E \`${workerName}\` \u30EF\u30FC\u30AB\u30FC\u304C\u3001\u5B9A\u671F\u5B9F\u884C\u306E\u8A18\u9332\uFF08\`lastRun.${workerName}\`\uFF09\u3092\u66F4\u65B0\u3057\u307E\u3057\u305F\u3002`,
3926
+ "",
3927
+ `- \u5B9F\u884C\u6642\u523B: ${at.toISOString()}`,
3928
+ "",
3929
+ `\u3053\u306EPR\u306F\u5B9F\u884C\u8A18\u9332\u306E\u307F\u3092\u66F4\u65B0\u3057\u307E\u3059\uFF08\u6210\u679C\u7269\u306E\u5909\u66F4\u306F\u540C\u30EF\u30FC\u30AB\u30FC\u304C\u8D77\u52D5\u3057\u305F\u30B9\u30AD\u30EB\u304C\u5225PR\u3067\u51FA\u3057\u307E\u3059\uFF09\u3002`,
3930
+ `\u30EC\u30D3\u30E5\u30FC\u5BFE\u8C61\u3067\u306F\u306A\u3044\u305F\u3081\u3001\u30EF\u30FC\u30AB\u30FC\u304C\u305D\u306E\u307E\u307E\u30DE\u30FC\u30B8\u3057\u307E\u3059\u3002`
3931
+ ].join("\n");
3932
+ }
3933
+ async function git(cwd, args) {
3934
+ const { stdout } = await execFileAsync3("git", ["-C", cwd, ...args]);
3935
+ return stdout;
3936
+ }
3937
+ async function publishLastRunPr(workerName, defaultBranch, at) {
3938
+ const branch = lastRunBranchName(workerName);
3939
+ const cwd = getWorktreePath(branch);
3940
+ try {
3941
+ await createWorktreeFromBranch(branch, defaultBranch);
3942
+ writeLastRun(cwd, workerName, at);
3943
+ if (!hasLastRunChange(await git(cwd, ["status", "--porcelain", CONFIG_FILE]))) {
3944
+ console.log(`[${workerName}] lastRun unchanged, skipping PR`);
3945
+ return;
3946
+ }
3947
+ await git(cwd, ["add", CONFIG_FILE]);
3948
+ await git(cwd, ["commit", "-m", lastRunPrTitle(workerName)]);
3949
+ await git(cwd, ["push", "--force", "origin", `HEAD:refs/heads/${branch}`]);
3950
+ const existing = await findOpenPrNumberByHeadRef(branch);
3951
+ const prNumber = existing ?? await createPullRequest(defaultBranch, branch, lastRunPrTitle(workerName), lastRunPrBody(workerName, at));
3952
+ await mergePullRequest(prNumber);
3953
+ console.log(`[${workerName}] merged lastRun PR #${prNumber}`);
3954
+ } finally {
3955
+ await removeWorktree(branch).catch((err) => console.error(`[${workerName}] removeWorktree failed: ${err}`));
3956
+ }
3957
+ }
3958
+
3959
+ // src/workers/scheduled-worker.ts
3960
+ var SCHEDULE_INTERVAL_HOURS = 24;
3961
+ var SCHEDULE_INTERVAL_MS = SCHEDULE_INTERVAL_HOURS * 60 * 60 * 1e3;
3962
+ var SCOPE_DAYS = SCHEDULE_INTERVAL_HOURS / 24;
3963
+ function createScheduledWorker(config) {
3964
+ return async () => {
3965
+ const { owner, name: repoName, defaultBranch } = await getRepoInfo();
3966
+ const { pollingIntervalSeconds } = getWorkerConfig(config.name);
3967
+ console.log(
3968
+ `[${config.name}] Checking every ${pollingIntervalSeconds} seconds (runs at most once per ${SCHEDULE_INTERVAL_HOURS}h for ${owner}/${repoName})`
3969
+ );
3970
+ let startedAt = 0;
3971
+ const tick = async () => {
3972
+ if (isShuttingDown()) return;
3973
+ if (config.enabled && !config.enabled()) return;
3974
+ if (isRunning(config.taskId)) return;
3975
+ const now = Date.now();
3976
+ const lastRunAt = Math.max(getLastRunAt(config.name) ?? 0, startedAt);
3977
+ if (lastRunAt > 0 && now - lastRunAt < SCHEDULE_INTERVAL_MS) return;
3978
+ const worktreeId = generateWorktreeName();
3979
+ startedAt = now;
3980
+ try {
3981
+ syncDefaultBranch(defaultBranch);
3982
+ const { model, effort, skill, advisorModel } = getWorkerConfig(config.name);
3983
+ const command = skill || config.command;
3984
+ const mode = getRunMode();
3985
+ const execution = buildClaudeExecution({
3986
+ mode,
3987
+ prompt: `${command} ${SCOPE_DAYS}`,
3988
+ model,
3989
+ effort,
3990
+ advisorModel: isAdvisorEnabled() ? advisorModel : "",
3991
+ permissionMode: getPermissionMode()
3992
+ });
3993
+ await publishLastRunPr(config.name, defaultBranch, new Date(now)).catch(
3994
+ (err) => console.error(`[${config.name}] publishLastRunPr failed: ${err}`)
3995
+ );
3996
+ await createWorktreeFromBranch(worktreeId, defaultBranch);
3997
+ const cwd = getWorktreePath(worktreeId);
3998
+ console.log(`[${config.name}] created worktree ${worktreeId} from ${defaultBranch}`);
3999
+ const repoUrl = `https://github.com/${owner}/${repoName}`;
4000
+ run(
4001
+ execution.command,
4002
+ execution.args,
4003
+ config.taskId,
4004
+ config.name,
4005
+ config.name,
4006
+ worktreeId,
4007
+ async (status, output) => {
4008
+ try {
4009
+ if (status === "completed") {
4010
+ await notifyTaskCompleted(config.name, repoName, config.taskId, config.name, repoUrl, output);
4011
+ } else {
4012
+ await notifyTaskFailed(config.name, repoName, config.taskId, config.name, repoUrl, output);
4013
+ }
4014
+ } catch (err) {
4015
+ console.error(`[${config.name}] post-task error: ${err}`);
4016
+ } finally {
4017
+ await removeWorktree(worktreeId).catch(
4018
+ (err) => console.error(`[${config.name}] removeWorktree failed: ${err}`)
4019
+ );
4020
+ }
4021
+ },
4022
+ cwd,
4023
+ buildClaudeEnv(mode)
4024
+ );
4025
+ } catch (err) {
4026
+ console.error(`[${config.name}] setup error: ${err}`);
4027
+ await removeWorktree(worktreeId).catch(() => {
4028
+ });
4029
+ await notifyError(config.name, repoName, err);
4030
+ }
4031
+ };
4032
+ await tick();
4033
+ setInterval(tick, pollingIntervalSeconds * 1e3);
4034
+ };
4035
+ }
4036
+
4037
+ // src/workers/update-coding-guidelines.ts
4038
+ var updateCodingGuidelinesWorker = createScheduledWorker({
4039
+ name: "update-coding-guidelines",
4040
+ command: "/claude-task-worker:update-coding-guidelines",
4041
+ taskId: -1
4042
+ });
4043
+
4044
+ // src/workers/update-requirement-rules.ts
4045
+ var updateRequirementRulesWorker = createScheduledWorker({
4046
+ name: "update-requirement-rules",
4047
+ command: "/claude-task-worker:update-requirement-rules",
4048
+ taskId: -2
4049
+ });
4050
+
4051
+ // src/workers/update-design-md.ts
4052
+ var updateDesignMdWorker = createScheduledWorker({
4053
+ name: "update-design-md",
4054
+ command: "/claude-task-worker:update-design-md",
4055
+ taskId: -3,
4056
+ // DESIGN.md の材料は `cc-ui-design` ラベル付きのマージ済みデザインPRで、それを作るのは
4057
+ // uiDesign.enabled のデザイン先行フローだけ。無効なリポジトリでは収集対象が原理的に
4058
+ // 存在しないため、空振りのセッションを毎日起動しないようここで止める。
4059
+ enabled: () => getUiDesignConfig().enabled
4060
+ });
4061
+
3819
4062
  // src/index.ts
3820
4063
  init_table();
3821
4064
 
@@ -4223,7 +4466,10 @@ var WORKERS = {
4223
4466
  "check-dependabot": checkDependabotWorker,
4224
4467
  "epic-issue": epicIssueWorker,
4225
4468
  "create-ui-design": createUiDesignWorker,
4226
- "apply-ui-design": applyUiDesignWorker
4469
+ "apply-ui-design": applyUiDesignWorker,
4470
+ "update-coding-guidelines": updateCodingGuidelinesWorker,
4471
+ "update-requirement-rules": updateRequirementRulesWorker,
4472
+ "update-design-md": updateDesignMdWorker
4227
4473
  };
4228
4474
  function printUsage() {
4229
4475
  console.log(`Usage: claude-task-worker <command> [--project <name>] [--epic <issue-number>] [--label <label-name>]
@@ -4248,6 +4494,9 @@ Workers:
4248
4494
  epic-issue Poll cc-epic-issue issues and create epic PR when all sub-issues are closed
4249
4495
  create-ui-design Poll cc-create-ui-design issues and create a Pencil design PR (requires uiDesign.enabled)
4250
4496
  apply-ui-design Poll cc-ui-design-pr-created issues and write the design reference back once the design PR is merged (requires uiDesign.enabled)
4497
+ update-coding-guidelines Run /update-coding-guidelines once every 24 hours over the last 24 hours
4498
+ update-requirement-rules Run /update-requirement-rules once every 24 hours over the last 24 hours
4499
+ update-design-md Run /update-design-md once every 24 hours over the last 24 hours (requires uiDesign.enabled)
4251
4500
  all Poll all workers except triage-created-issue, triage-pr, check-dependabot
4252
4501
  yolo Poll all workers including triage-created-issue, triage-pr, check-dependabot
4253
4502
 
@@ -4433,7 +4682,12 @@ if (hasProjectFilter()) {
4433
4682
  resolveConflictWorker(),
4434
4683
  epicIssueWorker({ epicFilters, labelFilters }),
4435
4684
  createUiDesignWorker({ epicFilters, labelFilters }),
4436
- applyUiDesignWorker({ epicFilters, labelFilters })
4685
+ applyUiDesignWorker({ epicFilters, labelFilters }),
4686
+ // 24時間おきの定期ワーカー。update-design-md は uiDesign.enabled が false のとき
4687
+ // 自身で no-op になる(create-ui-design / apply-ui-design と同じ扱い)。
4688
+ updateCodingGuidelinesWorker(),
4689
+ updateRequirementRulesWorker(),
4690
+ updateDesignMdWorker()
4437
4691
  ]);
4438
4692
  })();
4439
4693
  } else if (workerType === "yolo") {
@@ -4454,7 +4708,10 @@ if (hasProjectFilter()) {
4454
4708
  resolveConflictWorker(),
4455
4709
  epicIssueWorker({ epicFilters, labelFilters }),
4456
4710
  createUiDesignWorker({ epicFilters, labelFilters }),
4457
- applyUiDesignWorker({ epicFilters, labelFilters })
4711
+ applyUiDesignWorker({ epicFilters, labelFilters }),
4712
+ updateCodingGuidelinesWorker(),
4713
+ updateRequirementRulesWorker(),
4714
+ updateDesignMdWorker()
4458
4715
  ]);
4459
4716
  })();
4460
4717
  } else {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-task-worker",
3
- "version": "0.80.0",
3
+ "version": "0.82.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",