claude-task-worker 0.30.0 → 0.32.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.
- package/README.md +26 -1
- package/dist/index.js +201 -114
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -274,7 +274,7 @@ claude-task-worker yolo --epic 100 --epic 200 --label priority-high
|
|
|
274
274
|
}
|
|
275
275
|
```
|
|
276
276
|
|
|
277
|
-
`mode` については [`mode`(タスクの実行形態)](#modeタスクの実行形態) を参照。
|
|
277
|
+
`mode` については [`mode`(タスクの実行形態)](#modeタスクの実行形態) を、`headroom` については [`headroom`(Headroom 経由でのタスク実行)](#headroomheadroom-経由でのタスク実行) を参照。
|
|
278
278
|
|
|
279
279
|
`--project` は繰り返し指定可能で、複数指定した場合は解決後のプロジェクト集合の和集合が対象になる(重複は一意化される)。`--epic` / `--label` と併用でき、ディスパッチ先の各プロジェクトで実行されるコマンドにそのまま引き継がれる。
|
|
280
280
|
|
|
@@ -331,6 +331,31 @@ claude-task-worker exec-issue --project app-a --epic 100 --label priority-high
|
|
|
331
331
|
|
|
332
332
|
適用は `herdr server reload-config`。この設定は herdr サーバー全体に効くため、ワーカー以外の対話セッションの完了音も鳴らなくなる(`[ui.sound.agents] claude = "off"` でも実質同じ範囲)。ワーカーだけを無音にしたい場合は、`HERDR_DISABLE_SOUND=1 herdr --session <name>` で別セッションを起動し、その中でディスパッチャーを動かす
|
|
333
333
|
|
|
334
|
+
### `headroom`(Headroom 経由でのタスク実行)
|
|
335
|
+
|
|
336
|
+
`config.json` のトップレベルに `"headroom": true` を書くと、ワーカーは各タスクの claude を [Headroom](https://github.com/headroom-ai/headroom) 経由(`headroom wrap claude`)で起動する。Headroom がローカルプロキシを立ててコンテキストを圧縮し、`ANTHROPIC_BASE_URL` を差し替えた状態で claude を起動する。既定は `false`(`claude` を直接起動する)。
|
|
337
|
+
|
|
338
|
+
`mode` と同じくトップレベル一括の設定で、プロジェクト単位・ワーカー単位の指定はできない。
|
|
339
|
+
|
|
340
|
+
```json
|
|
341
|
+
{
|
|
342
|
+
"headroom": true,
|
|
343
|
+
"projects": { "app-a": "/Users/me/repos/app-a" }
|
|
344
|
+
}
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
| `headroom` | 実行されるコマンド |
|
|
348
|
+
|-----------|------------------|
|
|
349
|
+
| `false`(既定) | `claude <引数>` |
|
|
350
|
+
| `true` | `headroom wrap claude -- <引数>` |
|
|
351
|
+
|
|
352
|
+
補足:
|
|
353
|
+
|
|
354
|
+
- `mode` とは独立して組み合わせられる(`mode: "herdr"` の TUI 起動も `headroom wrap claude` になる)
|
|
355
|
+
- claude へ渡す引数はすべて `--` の後ろに置かれる。`headroom wrap claude` 自身も `--port` / `--memory` などのオプションを持ち、`-p` のように衝突しうるフラグは `--` の後ろでないと claude に届かないため
|
|
356
|
+
- `headroom: true` で headroom コマンドが PATH に無い場合、ワーカーは起動時にエラー終了する(`mode: "herdr"` と同じく、サイレントに直接起動へフォールバックはしない)
|
|
357
|
+
- `headroom wrap claude` は claude を起動する前に起動バナー(枠線・`ANTHROPIC_BASE_URL=...` など)を stdout へ出力し、これを抑止するオプションは無い。ワーカーは空振りセッション検知(exit 0 かつ無出力を失敗とみなす判定)の前にこのバナーを取り除くため、検知は `headroom: true` でも従来どおり機能する。ただし Slack 通知の本文にはバナーがそのまま含まれる
|
|
358
|
+
|
|
334
359
|
### exec-issue
|
|
335
360
|
|
|
336
361
|
`cc-exec-issue` ラベルが付いた自分にアサインされたIssueを定期取得し、Claude Codeで処理を実行する。(デフォルト1分間隔)
|
package/dist/index.js
CHANGED
|
@@ -12,6 +12,7 @@ var __export = (target, all) => {
|
|
|
12
12
|
// src/table.ts
|
|
13
13
|
var table_exports = {};
|
|
14
14
|
__export(table_exports, {
|
|
15
|
+
buildTaskTableLines: () => buildTaskTableLines,
|
|
15
16
|
getDisplayWidth: () => getDisplayWidth,
|
|
16
17
|
padToWidth: () => padToWidth,
|
|
17
18
|
truncateToWidth: () => truncateToWidth
|
|
@@ -47,12 +48,122 @@ function padToWidth(str, targetWidth) {
|
|
|
47
48
|
const padding = targetWidth - currentWidth;
|
|
48
49
|
return padding > 0 ? str + " ".repeat(padding) : str;
|
|
49
50
|
}
|
|
51
|
+
function formatDuration(start, end = /* @__PURE__ */ new Date()) {
|
|
52
|
+
const diffMs = end.getTime() - start.getTime();
|
|
53
|
+
const totalSeconds = Math.floor(diffMs / 1e3);
|
|
54
|
+
const minutes = Math.floor(totalSeconds / 60);
|
|
55
|
+
const seconds = totalSeconds % 60;
|
|
56
|
+
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
57
|
+
}
|
|
58
|
+
function formatTime(date) {
|
|
59
|
+
const h = String(date.getHours()).padStart(2, "0");
|
|
60
|
+
const m = String(date.getMinutes()).padStart(2, "0");
|
|
61
|
+
const s = String(date.getSeconds()).padStart(2, "0");
|
|
62
|
+
return `${h}:${m}:${s}`;
|
|
63
|
+
}
|
|
64
|
+
function buildTaskTableLines(entries, now = /* @__PURE__ */ new Date()) {
|
|
65
|
+
if (entries.length === 0) return [];
|
|
66
|
+
const runningTasks = entries.filter((t) => t.status === "running");
|
|
67
|
+
const finishedTasks = entries.filter((t) => t.status !== "running");
|
|
68
|
+
const maxTitleWidth = 40;
|
|
69
|
+
const maxPathWidth = 40;
|
|
70
|
+
const runningRows = runningTasks.map((t) => ({
|
|
71
|
+
id: `#${t.id}`,
|
|
72
|
+
title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
|
|
73
|
+
worker: t.workerName,
|
|
74
|
+
path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
|
|
75
|
+
// herdr モードでは agent ステータス(working / blocked 等)を併記し、
|
|
76
|
+
// 人の介入が必要な blocked にも気づけるようにする。
|
|
77
|
+
status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
|
|
78
|
+
time: formatTime(t.startedAt),
|
|
79
|
+
duration: formatDuration(t.startedAt, now)
|
|
80
|
+
}));
|
|
81
|
+
const finishedRows = finishedTasks.map((t) => ({
|
|
82
|
+
id: `#${t.id}`,
|
|
83
|
+
title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
|
|
84
|
+
worker: t.workerName,
|
|
85
|
+
path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
|
|
86
|
+
status: t.status,
|
|
87
|
+
time: formatTime(t.finishedAt ?? t.startedAt),
|
|
88
|
+
duration: formatDuration(t.startedAt, t.finishedAt ?? now)
|
|
89
|
+
}));
|
|
90
|
+
const allRows = [...runningRows, ...finishedRows];
|
|
91
|
+
const hasPath = allRows.some((r) => r.path !== "");
|
|
92
|
+
const colWidths = {
|
|
93
|
+
id: Math.max(3, ...allRows.map((r) => r.id.length)),
|
|
94
|
+
title: Math.max(5, ...allRows.map((r) => getDisplayWidth(r.title))),
|
|
95
|
+
worker: Math.max(6, ...allRows.map((r) => r.worker.length)),
|
|
96
|
+
...hasPath ? { path: Math.max(8, ...allRows.map((r) => r.path.length)) } : {},
|
|
97
|
+
status: Math.max(6, ...allRows.map((r) => r.status.length)),
|
|
98
|
+
time: Math.max(4, ...allRows.map((r) => r.time.length)),
|
|
99
|
+
duration: Math.max(8, ...allRows.map((r) => r.duration.length))
|
|
100
|
+
};
|
|
101
|
+
const pad = (s, w, useDisplayWidth = false) => useDisplayWidth ? padToWidth(s, w) : s + " ".repeat(w - s.length);
|
|
102
|
+
const cols = hasPath ? [
|
|
103
|
+
colWidths.id,
|
|
104
|
+
colWidths.title,
|
|
105
|
+
colWidths.worker,
|
|
106
|
+
colWidths.path,
|
|
107
|
+
colWidths.status,
|
|
108
|
+
colWidths.time,
|
|
109
|
+
colWidths.duration
|
|
110
|
+
] : [colWidths.id, colWidths.title, colWidths.worker, colWidths.status, colWidths.time, colWidths.duration];
|
|
111
|
+
const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
|
|
112
|
+
const row = (id, title, worker, path, status, time, duration) => hasPath ? `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(path, colWidths.path)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502` : `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502`;
|
|
113
|
+
const lines = [];
|
|
114
|
+
lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
|
|
115
|
+
lines.push(row("#", "Title", "Worker", "Worktree", "Status", "Time", "Duration"));
|
|
116
|
+
lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
|
|
117
|
+
for (const r of runningRows) {
|
|
118
|
+
lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
|
|
119
|
+
}
|
|
120
|
+
if (runningRows.length > 0 && finishedRows.length > 0) {
|
|
121
|
+
lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
|
|
122
|
+
}
|
|
123
|
+
for (const r of finishedRows) {
|
|
124
|
+
lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
|
|
125
|
+
}
|
|
126
|
+
lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
|
|
127
|
+
return lines;
|
|
128
|
+
}
|
|
50
129
|
var init_table = __esm({
|
|
51
130
|
"src/table.ts"() {
|
|
52
131
|
"use strict";
|
|
53
132
|
}
|
|
54
133
|
});
|
|
55
134
|
|
|
135
|
+
// src/task-result.ts
|
|
136
|
+
function stripHeadroomBanner(stdout) {
|
|
137
|
+
const lines = stdout.split("\n");
|
|
138
|
+
let i = 0;
|
|
139
|
+
while (i < lines.length && (lines[i].trim() === "" || lines[i].startsWith(" "))) i++;
|
|
140
|
+
return lines.slice(i).join("\n");
|
|
141
|
+
}
|
|
142
|
+
function buildTaskResult(code, stdout, stderrTail, options) {
|
|
143
|
+
const meaningful = options?.headroom ? stripHeadroomBanner(stdout) : stdout;
|
|
144
|
+
const emptyOutput = meaningful.trim() === "";
|
|
145
|
+
const completed = code === 0 && !emptyOutput;
|
|
146
|
+
let output = stdout;
|
|
147
|
+
if (code === 0 && emptyOutput) {
|
|
148
|
+
output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
|
|
149
|
+
} else if (!completed) {
|
|
150
|
+
output += `
|
|
151
|
+
[worker] claude exited with code ${code}`;
|
|
152
|
+
}
|
|
153
|
+
if (!completed && stderrTail.trim() !== "") {
|
|
154
|
+
output += `
|
|
155
|
+
[stderr] ${stderrTail.trim()}`;
|
|
156
|
+
}
|
|
157
|
+
return { status: completed ? "completed" : "failed", output };
|
|
158
|
+
}
|
|
159
|
+
var STDERR_TAIL_LIMIT;
|
|
160
|
+
var init_task_result = __esm({
|
|
161
|
+
"src/task-result.ts"() {
|
|
162
|
+
"use strict";
|
|
163
|
+
STDERR_TAIL_LIMIT = 8 * 1024;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
|
|
56
167
|
// src/herdr.ts
|
|
57
168
|
var herdr_exports = {};
|
|
58
169
|
__export(herdr_exports, {
|
|
@@ -411,8 +522,9 @@ function observeAgentStatus(tracker, status) {
|
|
|
411
522
|
}
|
|
412
523
|
return { tracker, decision: "running" };
|
|
413
524
|
}
|
|
414
|
-
function buildHerdrTaskResult(paneOutput) {
|
|
415
|
-
|
|
525
|
+
function buildHerdrTaskResult(paneOutput, options) {
|
|
526
|
+
const meaningful = options?.headroom ? stripHeadroomBanner(paneOutput) : paneOutput;
|
|
527
|
+
if (meaningful.trim() === "") {
|
|
416
528
|
return {
|
|
417
529
|
status: "failed",
|
|
418
530
|
output: "[worker] the claude session became idle but its pane produced no output (session aborted before the model ran; e.g. a skill preamble command failed)"
|
|
@@ -470,7 +582,7 @@ async function waitForHerdrTask(paneId, options) {
|
|
|
470
582
|
tracker = observed.tracker;
|
|
471
583
|
if (observed.decision === "completed") {
|
|
472
584
|
const output = await readPaneOutput(paneId, mod);
|
|
473
|
-
return buildHerdrTaskResult(output);
|
|
585
|
+
return buildHerdrTaskResult(output, { headroom: options?.headroom });
|
|
474
586
|
}
|
|
475
587
|
if (observed.decision === "blocked-first-seen") {
|
|
476
588
|
options?.onBlocked?.();
|
|
@@ -530,6 +642,7 @@ var AGENT_POLL_INTERVAL_MS, PANE_OUTPUT_LINES, CLAUDE_EXIT_TIMEOUT_MS, CLAUDE_EX
|
|
|
530
642
|
var init_herdr_runner = __esm({
|
|
531
643
|
"src/herdr-runner.ts"() {
|
|
532
644
|
"use strict";
|
|
645
|
+
init_task_result();
|
|
533
646
|
AGENT_POLL_INTERVAL_MS = 3 * 1e3;
|
|
534
647
|
PANE_OUTPUT_LINES = 300;
|
|
535
648
|
CLAUDE_EXIT_TIMEOUT_MS = 15 * 1e3;
|
|
@@ -1299,6 +1412,8 @@ var SYSTEM_PROMPT = `\u3053\u306E\u30BB\u30C3\u30B7\u30E7\u30F3\u306F \`claude-t
|
|
|
1299
1412
|
- CodeGraph \u304C\u8FD4\u3057\u305F\u30BD\u30FC\u30B9\u306F\u300C\u8AAD\u307F\u7D42\u3048\u305F\u3082\u306E\u300D\u3068\u3057\u3066\u6271\u3044\u3001\u540C\u3058\u7B87\u6240\u3092 \`Grep\`/\`Read\` \u3067\u88CF\u53D6\u308A\u3057\u76F4\u3055\u306A\u3044\u3002\u305F\u3060\u3057\u51FA\u529B\u306B staleness\uFF08\u30A4\u30F3\u30C7\u30C3\u30AF\u30B9\u304C\u53E4\u3044\u65E8\uFF09\u306E\u8B66\u544A\u304C\u51FA\u3066\u3044\u308B\u5834\u5408\u306F\u8A72\u5F53\u30D5\u30A1\u30A4\u30EB\u3092 \`Read\` \u3057\u3066\u73FE\u7269\u3092\u78BA\u8A8D\u3059\u308B
|
|
1300
1413
|
- \u8A2D\u5B9A\u30D5\u30A1\u30A4\u30EB\u30FB\u30C9\u30AD\u30E5\u30E1\u30F3\u30C8\u30FB\u30B3\u30E1\u30F3\u30C8/\u6587\u5B57\u5217\u30EA\u30C6\u30E9\u30EB\u30FB\u672A\u5BFE\u5FDC\u8A00\u8A9E\u306A\u3069 CodeGraph \u304C\u6271\u308F\u306A\u3044\u5BFE\u8C61\u306F\u3001\u5F93\u6765\u3069\u304A\u308A\u30C6\u30AD\u30B9\u30C8\u691C\u7D22\u3067\u88DC\u3046
|
|
1301
1414
|
- \u63A2\u7D22\u3092\u30B5\u30D6\u30A8\u30FC\u30B8\u30A7\u30F3\u30C8\u3078\u59D4\u8B72\u3059\u308B\u5834\u5408\u306F\u3001\u3053\u306E\u65B9\u91DD\u3082\u59D4\u8B72\u30D7\u30ED\u30F3\u30D7\u30C8\u306B\u660E\u8A18\u3057\u3066\u4F1D\u3048\u308B`;
|
|
1415
|
+
var CLAUDE_COMMAND = "claude";
|
|
1416
|
+
var HEADROOM_COMMAND = "headroom";
|
|
1302
1417
|
function buildClaudeArgs({ mode, prompt, model, effort }) {
|
|
1303
1418
|
return [
|
|
1304
1419
|
...mode === "herdr" ? [] : ["-p"],
|
|
@@ -1315,6 +1430,13 @@ function buildClaudeArgs({ mode, prompt, model, effort }) {
|
|
|
1315
1430
|
effort
|
|
1316
1431
|
];
|
|
1317
1432
|
}
|
|
1433
|
+
function buildClaudeExecution(invocation) {
|
|
1434
|
+
const args = buildClaudeArgs(invocation);
|
|
1435
|
+
if (!invocation.headroom) {
|
|
1436
|
+
return { command: CLAUDE_COMMAND, args };
|
|
1437
|
+
}
|
|
1438
|
+
return { command: HEADROOM_COMMAND, args: ["wrap", CLAUDE_COMMAND, "--", ...args] };
|
|
1439
|
+
}
|
|
1318
1440
|
function buildClaudeEnv(mode) {
|
|
1319
1441
|
if (mode === "herdr") {
|
|
1320
1442
|
return {
|
|
@@ -1569,25 +1691,7 @@ async function assertRemoteTrackingExists(epicBranch) {
|
|
|
1569
1691
|
import { spawn } from "node:child_process";
|
|
1570
1692
|
import { basename } from "node:path";
|
|
1571
1693
|
init_table();
|
|
1572
|
-
|
|
1573
|
-
// src/task-result.ts
|
|
1574
|
-
var STDERR_TAIL_LIMIT = 8 * 1024;
|
|
1575
|
-
function buildTaskResult(code, stdout, stderrTail) {
|
|
1576
|
-
const emptyOutput = stdout.trim() === "";
|
|
1577
|
-
const completed = code === 0 && !emptyOutput;
|
|
1578
|
-
let output = stdout;
|
|
1579
|
-
if (code === 0 && emptyOutput) {
|
|
1580
|
-
output += "[worker] claude exited with code 0 but produced no output (session aborted before the model ran; e.g. a skill preamble command failed)";
|
|
1581
|
-
} else if (!completed) {
|
|
1582
|
-
output += `
|
|
1583
|
-
[worker] claude exited with code ${code}`;
|
|
1584
|
-
}
|
|
1585
|
-
if (!completed && stderrTail.trim() !== "") {
|
|
1586
|
-
output += `
|
|
1587
|
-
[stderr] ${stderrTail.trim()}`;
|
|
1588
|
-
}
|
|
1589
|
-
return { status: completed ? "completed" : "failed", output };
|
|
1590
|
-
}
|
|
1694
|
+
init_task_result();
|
|
1591
1695
|
|
|
1592
1696
|
// src/user-config.ts
|
|
1593
1697
|
import { readFileSync as readFileSync2, statSync } from "node:fs";
|
|
@@ -1595,6 +1699,7 @@ import { homedir } from "node:os";
|
|
|
1595
1699
|
import { isAbsolute, join as join2, resolve } from "node:path";
|
|
1596
1700
|
var RESERVED_ALL = "all";
|
|
1597
1701
|
var DEFAULT_RUN_MODE = "default";
|
|
1702
|
+
var DEFAULT_HEADROOM = false;
|
|
1598
1703
|
var UserConfigError = class extends Error {
|
|
1599
1704
|
constructor(message) {
|
|
1600
1705
|
super(message);
|
|
@@ -1643,6 +1748,13 @@ function parseMode(raw, path) {
|
|
|
1643
1748
|
console.warn(`[config] invalid mode: ${JSON.stringify(value)} in ${path}, using "${DEFAULT_RUN_MODE}"`);
|
|
1644
1749
|
return DEFAULT_RUN_MODE;
|
|
1645
1750
|
}
|
|
1751
|
+
function parseHeadroom(raw, path) {
|
|
1752
|
+
if (!("headroom" in raw)) return DEFAULT_HEADROOM;
|
|
1753
|
+
const value = raw["headroom"];
|
|
1754
|
+
if (typeof value === "boolean") return value;
|
|
1755
|
+
console.warn(`[config] invalid headroom: ${JSON.stringify(value)} in ${path}, using ${DEFAULT_HEADROOM}`);
|
|
1756
|
+
return DEFAULT_HEADROOM;
|
|
1757
|
+
}
|
|
1646
1758
|
function loadUserConfig() {
|
|
1647
1759
|
const path = getUserConfigPath();
|
|
1648
1760
|
const raw = readRawConfig();
|
|
@@ -1659,6 +1771,7 @@ function loadUserConfig() {
|
|
|
1659
1771
|
throw new UserConfigError(`config.json "projectGroups" must be an object: ${path}`);
|
|
1660
1772
|
}
|
|
1661
1773
|
const mode = parseMode(raw, path);
|
|
1774
|
+
const headroom = parseHeadroom(raw, path);
|
|
1662
1775
|
const rawProjects = raw["projects"];
|
|
1663
1776
|
const rawProjectGroups = "projectGroups" in raw ? raw["projectGroups"] : {};
|
|
1664
1777
|
const projectKeys = Object.keys(rawProjects);
|
|
@@ -1709,7 +1822,7 @@ function loadUserConfig() {
|
|
|
1709
1822
|
}
|
|
1710
1823
|
projectGroups[groupName] = members;
|
|
1711
1824
|
}
|
|
1712
|
-
return { mode, projects, projectGroups };
|
|
1825
|
+
return { mode, headroom, projects, projectGroups };
|
|
1713
1826
|
}
|
|
1714
1827
|
var cachedRunMode;
|
|
1715
1828
|
function getRunMode() {
|
|
@@ -1719,18 +1832,35 @@ function getRunMode() {
|
|
|
1719
1832
|
return cachedRunMode;
|
|
1720
1833
|
}
|
|
1721
1834
|
function readRunMode() {
|
|
1835
|
+
const raw = readTopLevelConfig(`using "${DEFAULT_RUN_MODE}" mode`);
|
|
1836
|
+
if (raw === void 0) return DEFAULT_RUN_MODE;
|
|
1837
|
+
return parseMode(raw, getUserConfigPath());
|
|
1838
|
+
}
|
|
1839
|
+
var cachedHeadroom;
|
|
1840
|
+
function getHeadroomEnabled() {
|
|
1841
|
+
if (cachedHeadroom === void 0) {
|
|
1842
|
+
cachedHeadroom = readHeadroom();
|
|
1843
|
+
}
|
|
1844
|
+
return cachedHeadroom;
|
|
1845
|
+
}
|
|
1846
|
+
function readHeadroom() {
|
|
1847
|
+
const raw = readTopLevelConfig(`using headroom=${DEFAULT_HEADROOM}`);
|
|
1848
|
+
if (raw === void 0) return DEFAULT_HEADROOM;
|
|
1849
|
+
return parseHeadroom(raw, getUserConfigPath());
|
|
1850
|
+
}
|
|
1851
|
+
function readTopLevelConfig(fallbackDescription) {
|
|
1722
1852
|
let raw;
|
|
1723
1853
|
try {
|
|
1724
1854
|
raw = readRawConfig();
|
|
1725
1855
|
} catch (err) {
|
|
1726
|
-
console.warn(`[config] failed to read config file,
|
|
1727
|
-
return
|
|
1856
|
+
console.warn(`[config] failed to read config file, ${fallbackDescription}: ${err}`);
|
|
1857
|
+
return void 0;
|
|
1728
1858
|
}
|
|
1729
|
-
if (raw === void 0) return
|
|
1859
|
+
if (raw === void 0) return void 0;
|
|
1730
1860
|
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
1731
|
-
return
|
|
1861
|
+
return void 0;
|
|
1732
1862
|
}
|
|
1733
|
-
return
|
|
1863
|
+
return raw;
|
|
1734
1864
|
}
|
|
1735
1865
|
function findProjectNameByPath(path) {
|
|
1736
1866
|
let config;
|
|
@@ -1812,84 +1942,9 @@ function isWorkerAtCapacity(workerName) {
|
|
|
1812
1942
|
}
|
|
1813
1943
|
return count >= getWorkerConfig(workerName).maxConcurrentTasks;
|
|
1814
1944
|
}
|
|
1815
|
-
function formatDuration(start, end = /* @__PURE__ */ new Date()) {
|
|
1816
|
-
const diffMs = end.getTime() - start.getTime();
|
|
1817
|
-
const totalSeconds = Math.floor(diffMs / 1e3);
|
|
1818
|
-
const minutes = Math.floor(totalSeconds / 60);
|
|
1819
|
-
const seconds = totalSeconds % 60;
|
|
1820
|
-
return `${minutes}m ${String(seconds).padStart(2, "0")}s`;
|
|
1821
|
-
}
|
|
1822
|
-
function formatTime(date) {
|
|
1823
|
-
const h = String(date.getHours()).padStart(2, "0");
|
|
1824
|
-
const m = String(date.getMinutes()).padStart(2, "0");
|
|
1825
|
-
const s = String(date.getSeconds()).padStart(2, "0");
|
|
1826
|
-
return `${h}:${m}:${s}`;
|
|
1827
|
-
}
|
|
1828
1945
|
function renderTable() {
|
|
1829
|
-
const
|
|
1830
|
-
if (
|
|
1831
|
-
const runningTasks = entries.filter((t) => t.status === "running");
|
|
1832
|
-
const finishedTasks = entries.filter((t) => t.status !== "running");
|
|
1833
|
-
const maxTitleWidth = 40;
|
|
1834
|
-
const maxPathWidth = 40;
|
|
1835
|
-
const allRows = [
|
|
1836
|
-
...runningTasks.map((t) => ({
|
|
1837
|
-
id: `#${t.id}`,
|
|
1838
|
-
title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
|
|
1839
|
-
worker: t.workerName,
|
|
1840
|
-
path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
|
|
1841
|
-
// herdr モードでは agent ステータス(working / blocked 等)を併記し、
|
|
1842
|
-
// 人の介入が必要な blocked にも気づけるようにする。
|
|
1843
|
-
status: t.agentStatus ? `${t.status}:${t.agentStatus}` : t.status,
|
|
1844
|
-
time: formatTime(t.startedAt),
|
|
1845
|
-
duration: formatDuration(t.startedAt)
|
|
1846
|
-
})),
|
|
1847
|
-
...finishedTasks.map((t) => ({
|
|
1848
|
-
id: `#${t.id}`,
|
|
1849
|
-
title: getDisplayWidth(t.title) > maxTitleWidth ? truncateToWidth(t.title, maxTitleWidth) : t.title,
|
|
1850
|
-
worker: t.workerName,
|
|
1851
|
-
path: t.path ? t.path.length > maxPathWidth ? truncateToWidth(t.path, maxPathWidth) : t.path : "",
|
|
1852
|
-
status: t.status,
|
|
1853
|
-
time: formatTime(t.finishedAt ?? t.startedAt),
|
|
1854
|
-
duration: formatDuration(t.startedAt, t.finishedAt)
|
|
1855
|
-
}))
|
|
1856
|
-
];
|
|
1857
|
-
const hasPath = allRows.some((r) => r.path !== "");
|
|
1858
|
-
const colWidths = {
|
|
1859
|
-
id: Math.max(3, ...allRows.map((r) => r.id.length)),
|
|
1860
|
-
title: Math.max(5, ...allRows.map((r) => getDisplayWidth(r.title))),
|
|
1861
|
-
worker: Math.max(6, ...allRows.map((r) => r.worker.length)),
|
|
1862
|
-
...hasPath ? { path: Math.max(8, ...allRows.map((r) => r.path.length)) } : {},
|
|
1863
|
-
status: Math.max(6, ...allRows.map((r) => r.status.length)),
|
|
1864
|
-
time: Math.max(4, ...allRows.map((r) => r.time.length)),
|
|
1865
|
-
duration: Math.max(8, ...allRows.map((r) => r.duration.length))
|
|
1866
|
-
};
|
|
1867
|
-
const pad = (s, w, useDisplayWidth = false) => useDisplayWidth ? padToWidth(s, w) : s + " ".repeat(w - s.length);
|
|
1868
|
-
const cols = hasPath ? [
|
|
1869
|
-
colWidths.id,
|
|
1870
|
-
colWidths.title,
|
|
1871
|
-
colWidths.worker,
|
|
1872
|
-
colWidths.path,
|
|
1873
|
-
colWidths.status,
|
|
1874
|
-
colWidths.time,
|
|
1875
|
-
colWidths.duration
|
|
1876
|
-
] : [colWidths.id, colWidths.title, colWidths.worker, colWidths.status, colWidths.time, colWidths.duration];
|
|
1877
|
-
const line = (l, m, r, f) => `${l}${cols.map((w) => f.repeat(w + 2)).join(m)}${r}`;
|
|
1878
|
-
const row = (id, title, worker, path, status, time, duration) => hasPath ? `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(path, colWidths.path)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502` : `\u2502 ${pad(id, colWidths.id)} \u2502 ${pad(title, colWidths.title, true)} \u2502 ${pad(worker, colWidths.worker)} \u2502 ${pad(status, colWidths.status)} \u2502 ${pad(time, colWidths.time)} \u2502 ${pad(duration, colWidths.duration)} \u2502`;
|
|
1879
|
-
const lines = [];
|
|
1880
|
-
lines.push(line("\u250C", "\u252C", "\u2510", "\u2500"));
|
|
1881
|
-
lines.push(row("#", "Title", "Worker", "Worktree", "Status", "Time", "Duration"));
|
|
1882
|
-
lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
|
|
1883
|
-
for (const r of allRows.filter((r2) => r2.status === "running")) {
|
|
1884
|
-
lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
|
|
1885
|
-
}
|
|
1886
|
-
if (runningTasks.length > 0 && finishedTasks.length > 0) {
|
|
1887
|
-
lines.push(line("\u251C", "\u253C", "\u2524", "\u2500"));
|
|
1888
|
-
}
|
|
1889
|
-
for (const r of allRows.filter((r2) => r2.status !== "running")) {
|
|
1890
|
-
lines.push(row(r.id, r.title, r.worker, r.path, r.status, r.time, r.duration));
|
|
1891
|
-
}
|
|
1892
|
-
lines.push(line("\u2514", "\u2534", "\u2518", "\u2500"));
|
|
1946
|
+
const lines = buildTaskTableLines([...tasks.values()]);
|
|
1947
|
+
if (lines.length === 0) return;
|
|
1893
1948
|
console.clear();
|
|
1894
1949
|
console.log(lines.join("\n"));
|
|
1895
1950
|
}
|
|
@@ -1941,6 +1996,7 @@ async function runViaHerdr(command, args, id, onComplete, cwd, env) {
|
|
|
1941
1996
|
herdrTasks.set(id, task);
|
|
1942
1997
|
result = await waitForHerdrTask2(task.paneId, {
|
|
1943
1998
|
signal: herdrAbortSignal,
|
|
1999
|
+
headroom: getHeadroomEnabled(),
|
|
1944
2000
|
onBlocked: () => console.warn(`[worker] #${id} is blocked and waiting for input in herdr tab "${label}"`),
|
|
1945
2001
|
onStatus: (status) => {
|
|
1946
2002
|
const task2 = tasks.get(id);
|
|
@@ -1998,7 +2054,8 @@ function run(command, args, id, title, workerName, path, onComplete, cwd, env) {
|
|
|
1998
2054
|
const result = buildTaskResult(
|
|
1999
2055
|
code,
|
|
2000
2056
|
Buffer.concat(outputChunks).toString("utf-8"),
|
|
2001
|
-
Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT)
|
|
2057
|
+
Buffer.concat(stderrChunks).toString("utf-8").slice(-STDERR_TAIL_LIMIT),
|
|
2058
|
+
{ headroom: getHeadroomEnabled() }
|
|
2002
2059
|
);
|
|
2003
2060
|
await finishTask(id, result, onComplete);
|
|
2004
2061
|
childProcesses.delete(id);
|
|
@@ -2707,7 +2764,13 @@ function createIssuePollingWorker(config) {
|
|
|
2707
2764
|
const command = skill || config.command;
|
|
2708
2765
|
const parentNumber = issue.parent?.number;
|
|
2709
2766
|
const mode = getRunMode();
|
|
2710
|
-
const
|
|
2767
|
+
const execution = buildClaudeExecution({
|
|
2768
|
+
mode,
|
|
2769
|
+
prompt: `${command} ${issue.number}`,
|
|
2770
|
+
model,
|
|
2771
|
+
effort,
|
|
2772
|
+
headroom: getHeadroomEnabled()
|
|
2773
|
+
});
|
|
2711
2774
|
let baseBranch = defaultBranch;
|
|
2712
2775
|
if (parentNumber !== void 0) {
|
|
2713
2776
|
baseBranch = `cc-epic-${parentNumber}`;
|
|
@@ -2717,8 +2780,8 @@ function createIssuePollingWorker(config) {
|
|
|
2717
2780
|
const cwd = getWorktreePath(worktreeId);
|
|
2718
2781
|
console.log(`[${config.name}] #${issue.number}: created worktree ${worktreeId} from ${baseBranch}`);
|
|
2719
2782
|
run(
|
|
2720
|
-
|
|
2721
|
-
|
|
2783
|
+
execution.command,
|
|
2784
|
+
execution.args,
|
|
2722
2785
|
issue.number,
|
|
2723
2786
|
issue.title,
|
|
2724
2787
|
config.name,
|
|
@@ -2873,9 +2936,16 @@ function createPrPollingWorker(config) {
|
|
|
2873
2936
|
const { model, effort, skill } = getWorkerConfig(config.name);
|
|
2874
2937
|
const command = skill || config.command;
|
|
2875
2938
|
const mode = getRunMode();
|
|
2939
|
+
const execution = buildClaudeExecution({
|
|
2940
|
+
mode,
|
|
2941
|
+
prompt: `${command} ${pr.number}`,
|
|
2942
|
+
model,
|
|
2943
|
+
effort,
|
|
2944
|
+
headroom: getHeadroomEnabled()
|
|
2945
|
+
});
|
|
2876
2946
|
run(
|
|
2877
|
-
|
|
2878
|
-
|
|
2947
|
+
execution.command,
|
|
2948
|
+
execution.args,
|
|
2879
2949
|
pr.number,
|
|
2880
2950
|
`PR #${pr.number} (${pr.headRefName})`,
|
|
2881
2951
|
config.name,
|
|
@@ -3539,6 +3609,23 @@ async function assertRunModeAvailable() {
|
|
|
3539
3609
|
}
|
|
3540
3610
|
console.log("[worker] run mode: herdr (each task runs as a TUI session in its own herdr tab)");
|
|
3541
3611
|
}
|
|
3612
|
+
async function assertHeadroomAvailable() {
|
|
3613
|
+
if (!getHeadroomEnabled()) return;
|
|
3614
|
+
const { execFile: execFile3 } = await import("node:child_process");
|
|
3615
|
+
const { promisify: promisify4 } = await import("node:util");
|
|
3616
|
+
try {
|
|
3617
|
+
await promisify4(execFile3)(HEADROOM_COMMAND, ["--version"]);
|
|
3618
|
+
} catch (err) {
|
|
3619
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
3620
|
+
console.error(`[worker] config.json has headroom: true but the headroom CLI is unavailable: ${message}`);
|
|
3621
|
+
process.exit(1);
|
|
3622
|
+
}
|
|
3623
|
+
console.log("[worker] headroom: enabled (each task runs as `headroom wrap claude`)");
|
|
3624
|
+
}
|
|
3625
|
+
async function assertRunPrerequisites() {
|
|
3626
|
+
await assertRunModeAvailable();
|
|
3627
|
+
await assertHeadroomAvailable();
|
|
3628
|
+
}
|
|
3542
3629
|
if (!hasProjectFilter()) {
|
|
3543
3630
|
process.on("SIGTERM", async () => {
|
|
3544
3631
|
if (isShuttingDown()) return;
|
|
@@ -3627,7 +3714,7 @@ if (hasProjectFilter()) {
|
|
|
3627
3714
|
const epicFilters = parseEpicFilters();
|
|
3628
3715
|
const labelFilters = parseLabelFilters();
|
|
3629
3716
|
(async () => {
|
|
3630
|
-
await
|
|
3717
|
+
await assertRunPrerequisites();
|
|
3631
3718
|
await removeStaleWorktrees();
|
|
3632
3719
|
await Promise.all([
|
|
3633
3720
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3643,7 +3730,7 @@ if (hasProjectFilter()) {
|
|
|
3643
3730
|
const epicFilters = parseEpicFilters();
|
|
3644
3731
|
const labelFilters = parseLabelFilters();
|
|
3645
3732
|
(async () => {
|
|
3646
|
-
await
|
|
3733
|
+
await assertRunPrerequisites();
|
|
3647
3734
|
await removeStaleWorktrees();
|
|
3648
3735
|
await Promise.all([
|
|
3649
3736
|
execIssueWorker({ epicFilters, labelFilters }),
|
|
@@ -3662,7 +3749,7 @@ if (hasProjectFilter()) {
|
|
|
3662
3749
|
const epicFilters = parseEpicFilters();
|
|
3663
3750
|
const labelFilters = parseLabelFilters();
|
|
3664
3751
|
(async () => {
|
|
3665
|
-
await
|
|
3752
|
+
await assertRunPrerequisites();
|
|
3666
3753
|
await removeStaleWorktrees();
|
|
3667
3754
|
await WORKERS[workerType]({ epicFilters, labelFilters });
|
|
3668
3755
|
})();
|