bosun 0.28.2 → 0.28.3
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 +1 -1
- package/monitor.mjs +43 -19
- package/package.json +2 -1
- package/preflight.mjs +3 -1
- package/pwsh-runtime.mjs +62 -0
- package/setup.mjs +64 -3
- package/telegram-bot.mjs +45 -8
- package/ui/tabs/control.js +48 -3
- package/workspace-manager.mjs +57 -11
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@ Bosun is a production-grade supervisor for AI coding agents. It routes tasks acr
|
|
|
10
10
|
</p>
|
|
11
11
|
|
|
12
12
|
<p align="center">
|
|
13
|
-
<img src="site/social-
|
|
13
|
+
<img src="site/social-card.png" alt="bosun — AI agent supervisor" width="100%" />
|
|
14
14
|
</p>
|
|
15
15
|
|
|
16
16
|
<p align="center">
|
package/monitor.mjs
CHANGED
|
@@ -157,6 +157,7 @@ import { WorkspaceMonitor } from "./workspace-monitor.mjs";
|
|
|
157
157
|
import { VkLogStream } from "./vk-log-stream.mjs";
|
|
158
158
|
import { VKErrorResolver } from "./vk-error-resolver.mjs";
|
|
159
159
|
import { createAnomalyDetector } from "./anomaly-detector.mjs";
|
|
160
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
160
161
|
import {
|
|
161
162
|
getWorktreeManager,
|
|
162
163
|
acquireWorktree,
|
|
@@ -7555,12 +7556,21 @@ function buildPlannerTaskDescription({
|
|
|
7555
7556
|
reason,
|
|
7556
7557
|
numTasks,
|
|
7557
7558
|
runtimeContext,
|
|
7559
|
+
userPrompt,
|
|
7558
7560
|
}) {
|
|
7559
7561
|
return [
|
|
7560
7562
|
"## Task Planner — Auto-created by bosun",
|
|
7561
7563
|
"",
|
|
7562
7564
|
`**Trigger reason:** ${reason || "manual"}`,
|
|
7563
7565
|
`**Requested task count:** ${numTasks}`,
|
|
7566
|
+
...(userPrompt
|
|
7567
|
+
? [
|
|
7568
|
+
"",
|
|
7569
|
+
"### User Planning Prompt",
|
|
7570
|
+
"",
|
|
7571
|
+
userPrompt,
|
|
7572
|
+
]
|
|
7573
|
+
: []),
|
|
7564
7574
|
"",
|
|
7565
7575
|
"### Planner Prompt (Injected by bosun)",
|
|
7566
7576
|
"",
|
|
@@ -8689,6 +8699,7 @@ async function triggerTaskPlanner(
|
|
|
8689
8699
|
details,
|
|
8690
8700
|
{
|
|
8691
8701
|
taskCount,
|
|
8702
|
+
userPrompt,
|
|
8692
8703
|
notify = true,
|
|
8693
8704
|
preferredMode,
|
|
8694
8705
|
allowCodexWhenDisabled = false,
|
|
@@ -8724,6 +8735,7 @@ async function triggerTaskPlanner(
|
|
|
8724
8735
|
try {
|
|
8725
8736
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8726
8737
|
taskCount,
|
|
8738
|
+
userPrompt,
|
|
8727
8739
|
notify,
|
|
8728
8740
|
});
|
|
8729
8741
|
} catch (kanbanErr) {
|
|
@@ -8753,6 +8765,7 @@ async function triggerTaskPlanner(
|
|
|
8753
8765
|
}
|
|
8754
8766
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8755
8767
|
taskCount,
|
|
8768
|
+
userPrompt,
|
|
8756
8769
|
notify,
|
|
8757
8770
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8758
8771
|
});
|
|
@@ -8761,6 +8774,7 @@ async function triggerTaskPlanner(
|
|
|
8761
8774
|
try {
|
|
8762
8775
|
result = await triggerTaskPlannerViaCodex(reason, details, {
|
|
8763
8776
|
taskCount,
|
|
8777
|
+
userPrompt,
|
|
8764
8778
|
notify,
|
|
8765
8779
|
allowWhenDisabled: allowCodexWhenDisabled,
|
|
8766
8780
|
});
|
|
@@ -8784,6 +8798,7 @@ async function triggerTaskPlanner(
|
|
|
8784
8798
|
|
|
8785
8799
|
result = await triggerTaskPlannerViaKanban(reason, details, {
|
|
8786
8800
|
taskCount,
|
|
8801
|
+
userPrompt,
|
|
8787
8802
|
notify,
|
|
8788
8803
|
});
|
|
8789
8804
|
}
|
|
@@ -8818,7 +8833,7 @@ async function triggerTaskPlanner(
|
|
|
8818
8833
|
async function triggerTaskPlannerViaKanban(
|
|
8819
8834
|
reason,
|
|
8820
8835
|
details,
|
|
8821
|
-
{ taskCount, notify = true } = {},
|
|
8836
|
+
{ taskCount, userPrompt, notify = true } = {},
|
|
8822
8837
|
) {
|
|
8823
8838
|
const defaultPlannerTaskCount = Number(
|
|
8824
8839
|
process.env.TASK_PLANNER_DEFAULT_COUNT || "30",
|
|
@@ -8844,12 +8859,15 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8844
8859
|
);
|
|
8845
8860
|
}
|
|
8846
8861
|
|
|
8847
|
-
const desiredTitle =
|
|
8862
|
+
const desiredTitle = userPrompt
|
|
8863
|
+
? `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"}) — ${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? "…" : ""}`
|
|
8864
|
+
: `[${plannerTaskSizeLabel}] Plan next tasks (${reason || "backlog-empty"})`;
|
|
8848
8865
|
const desiredDescription = buildPlannerTaskDescription({
|
|
8849
8866
|
plannerPrompt,
|
|
8850
8867
|
reason,
|
|
8851
8868
|
numTasks,
|
|
8852
8869
|
runtimeContext,
|
|
8870
|
+
userPrompt,
|
|
8853
8871
|
});
|
|
8854
8872
|
|
|
8855
8873
|
// Check for existing planner tasks to avoid duplicates
|
|
@@ -8941,7 +8959,7 @@ async function triggerTaskPlannerViaKanban(
|
|
|
8941
8959
|
async function triggerTaskPlannerViaCodex(
|
|
8942
8960
|
reason,
|
|
8943
8961
|
details,
|
|
8944
|
-
{ taskCount, notify = true, allowWhenDisabled = false } = {},
|
|
8962
|
+
{ taskCount, userPrompt, notify = true, allowWhenDisabled = false } = {},
|
|
8945
8963
|
) {
|
|
8946
8964
|
if (!codexEnabled && !allowWhenDisabled) {
|
|
8947
8965
|
throw new Error(
|
|
@@ -8973,6 +8991,17 @@ async function triggerTaskPlannerViaCodex(
|
|
|
8973
8991
|
"## Execution Context",
|
|
8974
8992
|
`- Trigger reason: ${reason || "manual"}`,
|
|
8975
8993
|
`- Requested task count: ${numTasks}`,
|
|
8994
|
+
...(userPrompt
|
|
8995
|
+
? [
|
|
8996
|
+
"",
|
|
8997
|
+
"## User Planning Prompt",
|
|
8998
|
+
"",
|
|
8999
|
+
userPrompt,
|
|
9000
|
+
"",
|
|
9001
|
+
"Incorporate the above user prompt into any relevant planning decisions.",
|
|
9002
|
+
]
|
|
9003
|
+
: []),
|
|
9004
|
+
"",
|
|
8976
9005
|
"Context JSON:",
|
|
8977
9006
|
"```json",
|
|
8978
9007
|
safeJsonBlock(runtimeContext),
|
|
@@ -10590,21 +10619,16 @@ async function startProcess() {
|
|
|
10590
10619
|
let orchestratorArgs = [...scriptArgs];
|
|
10591
10620
|
|
|
10592
10621
|
if (scriptLower.endsWith(".ps1")) {
|
|
10593
|
-
const
|
|
10594
|
-
|
|
10595
|
-
|
|
10596
|
-
|
|
10597
|
-
|
|
10598
|
-
|
|
10599
|
-
|
|
10600
|
-
|
|
10601
|
-
|
|
10602
|
-
|
|
10603
|
-
const pwshLabel = configuredPwsh
|
|
10604
|
-
? `PWSH_PATH (${configuredPwsh})`
|
|
10605
|
-
: bundledPwshExists
|
|
10606
|
-
? `bundled pwsh (${bundledPwsh})`
|
|
10607
|
-
: "pwsh on PATH";
|
|
10622
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
10623
|
+
if (!pwshRuntime.exists) {
|
|
10624
|
+
const pwshLabel =
|
|
10625
|
+
pwshRuntime.source === "env"
|
|
10626
|
+
? `PWSH_PATH (${pwshRuntime.command})`
|
|
10627
|
+
: pwshRuntime.source === "bundled"
|
|
10628
|
+
? `bundled pwsh (${pwshRuntime.command})`
|
|
10629
|
+
: pwshRuntime.source === "powershell"
|
|
10630
|
+
? `powershell on PATH`
|
|
10631
|
+
: "pwsh on PATH";
|
|
10608
10632
|
const pauseMs = Math.max(orchestratorPauseMs, 60_000);
|
|
10609
10633
|
const pauseMin = Math.max(1, Math.round(pauseMs / 60_000));
|
|
10610
10634
|
monitorSafeModeUntil = Math.max(monitorSafeModeUntil, Date.now() + pauseMs);
|
|
@@ -10622,7 +10646,7 @@ async function startProcess() {
|
|
|
10622
10646
|
setTimeout(startProcess, pauseMs);
|
|
10623
10647
|
return;
|
|
10624
10648
|
}
|
|
10625
|
-
orchestratorCmd =
|
|
10649
|
+
orchestratorCmd = pwshRuntime.command;
|
|
10626
10650
|
orchestratorArgs = ["-File", scriptPath, ...scriptArgs];
|
|
10627
10651
|
} else if (scriptLower.endsWith(".sh")) {
|
|
10628
10652
|
const shellCmd =
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "bosun",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.3",
|
|
4
4
|
"description": "AI-powered orchestrator supervisor — manages AI agent executors with failover, auto-restarts on failure, analyzes crashes with Codex SDK, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "Apache 2.0",
|
|
@@ -152,6 +152,7 @@
|
|
|
152
152
|
"sdk-conflict-resolver.mjs",
|
|
153
153
|
"session-tracker.mjs",
|
|
154
154
|
"setup.mjs",
|
|
155
|
+
"pwsh-runtime.mjs",
|
|
155
156
|
"shared-knowledge.mjs",
|
|
156
157
|
"shared-state-manager.mjs",
|
|
157
158
|
"shared-workspace-cli.mjs",
|
package/preflight.mjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { spawnSync } from "node:child_process";
|
|
2
2
|
import { resolve } from "node:path";
|
|
3
3
|
import os from "node:os";
|
|
4
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
4
5
|
|
|
5
6
|
const isWindows = process.platform === "win32";
|
|
6
7
|
const MIN_FREE_GB = Number(process.env.BOSUN_MIN_FREE_GB || "10");
|
|
@@ -179,6 +180,7 @@ function checkToolchain() {
|
|
|
179
180
|
"node",
|
|
180
181
|
shellMode ? "shell" : "pwsh",
|
|
181
182
|
]);
|
|
183
|
+
const pwshRuntime = resolvePwshRuntime({ preferBundled: true });
|
|
182
184
|
|
|
183
185
|
const tools = [
|
|
184
186
|
checkToolVersion(
|
|
@@ -213,7 +215,7 @@ function checkToolchain() {
|
|
|
213
215
|
),
|
|
214
216
|
checkToolVersion(
|
|
215
217
|
"pwsh",
|
|
216
|
-
|
|
218
|
+
pwshRuntime.command,
|
|
217
219
|
["-NoProfile", "-Command", "$PSVersionTable.PSVersion.ToString()"],
|
|
218
220
|
"Install PowerShell 7+ (pwsh) and ensure it is on PATH.",
|
|
219
221
|
),
|
package/pwsh-runtime.mjs
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { existsSync } from "node:fs";
|
|
2
|
+
import { dirname, resolve } from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
4
|
+
import { execSync } from "node:child_process";
|
|
5
|
+
|
|
6
|
+
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
+
const BUNDLED_PWSH_PATH = resolve(__dirname, ".cache", "bosun", "pwsh", "pwsh");
|
|
8
|
+
|
|
9
|
+
function commandExists(cmd) {
|
|
10
|
+
try {
|
|
11
|
+
execSync(`${process.platform === "win32" ? "where" : "which"} ${cmd}`, {
|
|
12
|
+
stdio: "ignore",
|
|
13
|
+
});
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function isPathLike(value) {
|
|
21
|
+
return value.includes("/") || value.includes("\\");
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export function resolvePwshRuntime({ preferBundled = true } = {}) {
|
|
25
|
+
const configured = String(process.env.PWSH_PATH || "").trim();
|
|
26
|
+
if (configured) {
|
|
27
|
+
if (isPathLike(configured)) {
|
|
28
|
+
if (existsSync(configured)) {
|
|
29
|
+
return { command: configured, source: "env", exists: true };
|
|
30
|
+
}
|
|
31
|
+
return { command: configured, source: "env", exists: false };
|
|
32
|
+
}
|
|
33
|
+
if (commandExists(configured)) {
|
|
34
|
+
return { command: configured, source: "env", exists: true };
|
|
35
|
+
}
|
|
36
|
+
return { command: configured, source: "env", exists: false };
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
if (preferBundled && existsSync(BUNDLED_PWSH_PATH)) {
|
|
40
|
+
return { command: BUNDLED_PWSH_PATH, source: "bundled", exists: true };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
if (commandExists("pwsh")) {
|
|
44
|
+
return { command: "pwsh", source: "path", exists: true };
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (process.platform === "win32" && commandExists("powershell")) {
|
|
48
|
+
return { command: "powershell", source: "powershell", exists: true };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return { command: "pwsh", source: "missing", exists: false };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function resolvePwshCommand(options = {}) {
|
|
55
|
+
return resolvePwshRuntime(options).command;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function hasPwshRuntime(options = {}) {
|
|
59
|
+
return resolvePwshRuntime(options).exists;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export { BUNDLED_PWSH_PATH };
|
package/setup.mjs
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
|
|
23
23
|
import { createInterface } from "node:readline";
|
|
24
24
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
25
|
+
import { homedir } from "node:os";
|
|
25
26
|
import { resolve, dirname, basename, relative, isAbsolute } from "node:path";
|
|
26
27
|
import { execSync } from "node:child_process";
|
|
27
28
|
import { execFileSync } from "node:child_process";
|
|
@@ -654,6 +655,18 @@ function detectRepoSlug(cwd) {
|
|
|
654
655
|
}
|
|
655
656
|
}
|
|
656
657
|
|
|
658
|
+
function detectRepoRemoteUrl(cwd) {
|
|
659
|
+
try {
|
|
660
|
+
return execSync("git remote get-url origin", {
|
|
661
|
+
encoding: "utf8",
|
|
662
|
+
cwd: cwd || process.cwd(),
|
|
663
|
+
stdio: ["pipe", "pipe", "ignore"],
|
|
664
|
+
}).trim();
|
|
665
|
+
} catch {
|
|
666
|
+
return "";
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
657
670
|
function detectRepoRoot(cwd) {
|
|
658
671
|
try {
|
|
659
672
|
return execSync("git rev-parse --show-toplevel", {
|
|
@@ -679,6 +692,36 @@ function detectProjectName(repoRoot) {
|
|
|
679
692
|
return basename(repoRoot);
|
|
680
693
|
}
|
|
681
694
|
|
|
695
|
+
function hasSshKeyMaterial() {
|
|
696
|
+
if (process.env.SSH_AUTH_SOCK) return true;
|
|
697
|
+
const home = homedir();
|
|
698
|
+
if (!home) return false;
|
|
699
|
+
const candidates = [
|
|
700
|
+
".ssh/id_rsa.pub",
|
|
701
|
+
".ssh/id_ed25519.pub",
|
|
702
|
+
".ssh/id_ecdsa.pub",
|
|
703
|
+
".ssh/id_dsa.pub",
|
|
704
|
+
];
|
|
705
|
+
return candidates.some((rel) => existsSync(resolve(home, rel)));
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
function isSshGitUrl(value) {
|
|
709
|
+
const text = String(value || "").trim();
|
|
710
|
+
if (!text) return false;
|
|
711
|
+
return text.startsWith("git@") || text.startsWith("ssh://");
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
function buildDefaultGitUrl(slug, repoRoot) {
|
|
715
|
+
const normalizedSlug = String(slug || "").trim();
|
|
716
|
+
if (!normalizedSlug) return "";
|
|
717
|
+
const remote = detectRepoRemoteUrl(repoRoot);
|
|
718
|
+
if (remote) return remote;
|
|
719
|
+
const preferSsh = hasSshKeyMaterial();
|
|
720
|
+
return preferSsh
|
|
721
|
+
? `git@github.com:${normalizedSlug}.git`
|
|
722
|
+
: `https://github.com/${normalizedSlug}.git`;
|
|
723
|
+
}
|
|
724
|
+
|
|
682
725
|
function formatModelVariant(profile) {
|
|
683
726
|
if (!profile?.model && !profile?.variant) return "";
|
|
684
727
|
if (profile?.model && profile?.variant) {
|
|
@@ -2222,10 +2265,27 @@ async function main() {
|
|
|
2222
2265
|
let repoIdx = 0;
|
|
2223
2266
|
|
|
2224
2267
|
while (addMoreRepos) {
|
|
2225
|
-
|
|
2268
|
+
let repoUrl = await prompt.ask(
|
|
2226
2269
|
` Repo ${repoIdx + 1} — git URL (SSH or HTTPS)`,
|
|
2227
|
-
repoIdx === 0
|
|
2270
|
+
repoIdx === 0
|
|
2271
|
+
? buildDefaultGitUrl(env.GITHUB_REPO || slug, repoRoot)
|
|
2272
|
+
: "",
|
|
2228
2273
|
);
|
|
2274
|
+
if (repoUrl && isSshGitUrl(repoUrl) && !hasSshKeyMaterial()) {
|
|
2275
|
+
warn(
|
|
2276
|
+
"SSH URL detected but no SSH agent/keys found. Cloning may fail unless SSH is configured.",
|
|
2277
|
+
);
|
|
2278
|
+
const switchToHttps = await prompt.confirm(
|
|
2279
|
+
"Use HTTPS URL instead?",
|
|
2280
|
+
true,
|
|
2281
|
+
);
|
|
2282
|
+
if (switchToHttps) {
|
|
2283
|
+
const parsedSlug = parseRepoSlugFromUrl(repoUrl);
|
|
2284
|
+
if (parsedSlug) {
|
|
2285
|
+
repoUrl = `https://github.com/${parsedSlug}.git`;
|
|
2286
|
+
}
|
|
2287
|
+
}
|
|
2288
|
+
}
|
|
2229
2289
|
const parsedSlug = parseRepoSlugFromUrl(repoUrl);
|
|
2230
2290
|
const parsedRepoName = parsedSlug ? parsedSlug.split("/")[1] : "";
|
|
2231
2291
|
const defaultNameFromUrl = repoUrl
|
|
@@ -2872,11 +2932,12 @@ async function main() {
|
|
|
2872
2932
|
if (ghStatus.ok) break;
|
|
2873
2933
|
|
|
2874
2934
|
warn(
|
|
2875
|
-
|
|
2935
|
+
`GitHub auth is required to auto-detect projects, create boards, and sync issues. ${ghStatus.reason || ""}`.trim(),
|
|
2876
2936
|
);
|
|
2877
2937
|
info(
|
|
2878
2938
|
"If you do not plan to use GitHub as the task manager, pick Internal, Jira, or Vibe-Kanban.",
|
|
2879
2939
|
);
|
|
2940
|
+
info("Authenticate with GitHub using: gh auth login");
|
|
2880
2941
|
const ghActionIdx = await prompt.choose(
|
|
2881
2942
|
"How do you want to proceed?",
|
|
2882
2943
|
[
|
package/telegram-bot.mjs
CHANGED
|
@@ -59,6 +59,7 @@ import {
|
|
|
59
59
|
getWorktreeStats,
|
|
60
60
|
} from "./worktree-manager.mjs";
|
|
61
61
|
import { loadExecutorConfig } from "./config.mjs";
|
|
62
|
+
import { resolvePwshRuntime } from "./pwsh-runtime.mjs";
|
|
62
63
|
import {
|
|
63
64
|
getTelegramUiUrl,
|
|
64
65
|
startTelegramUiServer,
|
|
@@ -2701,7 +2702,7 @@ const COMMANDS = {
|
|
|
2701
2702
|
},
|
|
2702
2703
|
"/plan": {
|
|
2703
2704
|
handler: cmdPlan,
|
|
2704
|
-
desc: "Trigger task planner: /plan [count] (
|
|
2705
|
+
desc: "Trigger task planner: /plan [count] [prompt] (e.g. /plan 5 fix auth bugs)",
|
|
2705
2706
|
},
|
|
2706
2707
|
"/cleanup": {
|
|
2707
2708
|
handler: cmdCleanupMerged,
|
|
@@ -3237,9 +3238,24 @@ const UI_INPUT_HANDLERS = {
|
|
|
3237
3238
|
buildCommand: (input) => `/shell ${input}`,
|
|
3238
3239
|
},
|
|
3239
3240
|
plan_count: {
|
|
3240
|
-
prompt: "How many tasks should the planner generate?",
|
|
3241
|
+
prompt: "How many tasks should the planner generate? (e.g. 5)",
|
|
3241
3242
|
buildCommand: (input) => `/plan ${input}`,
|
|
3242
3243
|
},
|
|
3244
|
+
plan_prompt: {
|
|
3245
|
+
prompt:
|
|
3246
|
+
"Describe what you want the planner to focus on.\n" +
|
|
3247
|
+
"You can prefix with a count: e.g. '10 fix auth bugs and add tests'\n" +
|
|
3248
|
+
"Or just a topic: 'improve error handling across API layer'",
|
|
3249
|
+
buildCommand: (input) => {
|
|
3250
|
+
const trimmed = input.trim();
|
|
3251
|
+
const firstWord = trimmed.split(/\s+/)[0];
|
|
3252
|
+
const maybeCount = parseInt(firstWord, 10);
|
|
3253
|
+
if (Number.isFinite(maybeCount) && maybeCount > 0) {
|
|
3254
|
+
return `/plan ${trimmed}`;
|
|
3255
|
+
}
|
|
3256
|
+
return `/plan 5 ${trimmed}`;
|
|
3257
|
+
},
|
|
3258
|
+
},
|
|
3243
3259
|
starttask: {
|
|
3244
3260
|
prompt:
|
|
3245
3261
|
"Enter the task ID to start manually.\nNext you'll pick executor → SDK → model.",
|
|
@@ -4418,7 +4434,10 @@ Object.assign(UI_SCREENS, {
|
|
|
4418
4434
|
uiButton("Plan 5", uiCmdAction("/plan 5")),
|
|
4419
4435
|
uiButton("Plan 10", uiCmdAction("/plan 10")),
|
|
4420
4436
|
],
|
|
4421
|
-
[
|
|
4437
|
+
[
|
|
4438
|
+
uiButton("Custom Count", uiInputAction("plan_count")),
|
|
4439
|
+
uiButton("With Prompt", uiInputAction("plan_prompt")),
|
|
4440
|
+
],
|
|
4422
4441
|
uiNavRow("tasks"),
|
|
4423
4442
|
]),
|
|
4424
4443
|
},
|
|
@@ -7097,11 +7116,26 @@ async function cmdPlan(chatId, args) {
|
|
|
7097
7116
|
return;
|
|
7098
7117
|
}
|
|
7099
7118
|
|
|
7100
|
-
// Parse optional task count
|
|
7101
|
-
|
|
7102
|
-
|
|
7119
|
+
// Parse optional task count and/or free-form prompt:
|
|
7120
|
+
// /plan → 5 tasks, no prompt
|
|
7121
|
+
// /plan 10 → 10 tasks, no prompt
|
|
7122
|
+
// /plan fix auth → 5 tasks, userPrompt="fix auth"
|
|
7123
|
+
// /plan 10 fix auth → 10 tasks, userPrompt="fix auth"
|
|
7124
|
+
const rawArgs = (args || "").trim();
|
|
7125
|
+
const firstToken = rawArgs.split(/\s+/)[0];
|
|
7126
|
+
const parsedCount = parseInt(firstToken, 10);
|
|
7127
|
+
let taskCount = 5;
|
|
7128
|
+
let userPrompt;
|
|
7129
|
+
if (Number.isFinite(parsedCount) && parsedCount > 0) {
|
|
7130
|
+
taskCount = parsedCount;
|
|
7131
|
+
const remainder = rawArgs.slice(firstToken.length).trim();
|
|
7132
|
+
if (remainder) userPrompt = remainder;
|
|
7133
|
+
} else if (rawArgs) {
|
|
7134
|
+
userPrompt = rawArgs;
|
|
7135
|
+
}
|
|
7103
7136
|
|
|
7104
|
-
|
|
7137
|
+
const promptSuffix = userPrompt ? ` — "${userPrompt.slice(0, 60)}${userPrompt.length > 60 ? "…" : ""}"` : "";
|
|
7138
|
+
await sendReply(chatId, `📋 Triggering task planner (${taskCount} tasks${promptSuffix})...`);
|
|
7105
7139
|
|
|
7106
7140
|
try {
|
|
7107
7141
|
const result = await _triggerTaskPlanner(
|
|
@@ -7109,6 +7143,7 @@ async function cmdPlan(chatId, args) {
|
|
|
7109
7143
|
{ source: "telegram /plan command" },
|
|
7110
7144
|
{
|
|
7111
7145
|
taskCount,
|
|
7146
|
+
userPrompt,
|
|
7112
7147
|
notify: false,
|
|
7113
7148
|
preferredMode: "codex-sdk",
|
|
7114
7149
|
allowCodexWhenDisabled: true,
|
|
@@ -7398,7 +7433,9 @@ async function cmdShell(chatId, shellArgs) {
|
|
|
7398
7433
|
|
|
7399
7434
|
function runPwsh(psScript, timeoutMs = 15000) {
|
|
7400
7435
|
const isWin = process.platform === "win32";
|
|
7401
|
-
const pwsh = isWin
|
|
7436
|
+
const pwsh = isWin
|
|
7437
|
+
? "powershell.exe"
|
|
7438
|
+
: resolvePwshRuntime({ preferBundled: true }).command;
|
|
7402
7439
|
const script = `& { ${psScript} }`;
|
|
7403
7440
|
const result = spawnSync(pwsh, ["-NoProfile", "-Command", script], {
|
|
7404
7441
|
cwd: repoRoot,
|
package/ui/tabs/control.js
CHANGED
|
@@ -83,6 +83,8 @@ export function ControlTab() {
|
|
|
83
83
|
const [tasksLoading, setTasksLoading] = useState(false);
|
|
84
84
|
const [startTaskError, setStartTaskError] = useState("");
|
|
85
85
|
const [retryTaskError, setRetryTaskError] = useState("");
|
|
86
|
+
const [planPrompt, setPlanPrompt] = useState("");
|
|
87
|
+
const [planCount, setPlanCount] = useState("5");
|
|
86
88
|
const startTaskIdRef = useRef("");
|
|
87
89
|
const retryTaskIdRef = useRef("");
|
|
88
90
|
|
|
@@ -844,9 +846,52 @@ export function ControlTab() {
|
|
|
844
846
|
>
|
|
845
847
|
Retry Task
|
|
846
848
|
</button>
|
|
847
|
-
<
|
|
848
|
-
|
|
849
|
-
|
|
849
|
+
<div class="form-group" style="margin-top:0.5rem">
|
|
850
|
+
<div class="card-subtitle" style="margin-bottom:0.25rem">Task Planner</div>
|
|
851
|
+
<div class="input-row" style="display:flex;gap:0.4rem;align-items:center;flex-wrap:wrap">
|
|
852
|
+
<input
|
|
853
|
+
type="number"
|
|
854
|
+
class="input"
|
|
855
|
+
style="width:4.5rem;flex-shrink:0"
|
|
856
|
+
min="1"
|
|
857
|
+
max="50"
|
|
858
|
+
placeholder="5"
|
|
859
|
+
value=${planCount}
|
|
860
|
+
onInput=${(e) => setPlanCount(e.target.value)}
|
|
861
|
+
title="Number of tasks to generate"
|
|
862
|
+
/>
|
|
863
|
+
<input
|
|
864
|
+
class="input"
|
|
865
|
+
style="flex:1;min-width:10rem"
|
|
866
|
+
placeholder="Optional: focus on X, fix Y issues…"
|
|
867
|
+
value=${planPrompt}
|
|
868
|
+
onInput=${(e) => setPlanPrompt(e.target.value)}
|
|
869
|
+
onKeyDown=${(e) => {
|
|
870
|
+
if (e.key === "Enter") {
|
|
871
|
+
const count = parseInt(planCount, 10);
|
|
872
|
+
const n = Number.isFinite(count) && count > 0 ? count : 5;
|
|
873
|
+
const cmd = planPrompt.trim()
|
|
874
|
+
? `/plan ${n} ${planPrompt.trim()}`
|
|
875
|
+
: `/plan ${n}`;
|
|
876
|
+
sendCmd(cmd);
|
|
877
|
+
}
|
|
878
|
+
}}
|
|
879
|
+
/>
|
|
880
|
+
<button
|
|
881
|
+
class="btn btn-ghost btn-sm"
|
|
882
|
+
onClick=${() => {
|
|
883
|
+
const count = parseInt(planCount, 10);
|
|
884
|
+
const n = Number.isFinite(count) && count > 0 ? count : 5;
|
|
885
|
+
const cmd = planPrompt.trim()
|
|
886
|
+
? `/plan ${n} ${planPrompt.trim()}`
|
|
887
|
+
: `/plan ${n}`;
|
|
888
|
+
sendCmd(cmd);
|
|
889
|
+
}}
|
|
890
|
+
>
|
|
891
|
+
📋 Plan
|
|
892
|
+
</button>
|
|
893
|
+
</div>
|
|
894
|
+
</div>
|
|
850
895
|
</div>
|
|
851
896
|
${retryTaskError
|
|
852
897
|
? html`<div class="form-hint error">${retryTaskError}</div>`
|
package/workspace-manager.mjs
CHANGED
|
@@ -432,12 +432,24 @@ export function pullWorkspaceRepos(configDir, workspaceId) {
|
|
|
432
432
|
stdio: ["pipe", "pipe", "pipe"],
|
|
433
433
|
});
|
|
434
434
|
if (clone.status !== 0) {
|
|
435
|
+
const stderr = String(clone.stderr || clone.stdout || "");
|
|
436
|
+
let hint = "";
|
|
437
|
+
if (/permission denied \(publickey\)/i.test(stderr)) {
|
|
438
|
+
hint =
|
|
439
|
+
"SSH auth failed. Configure SSH keys or use an HTTPS URL instead.";
|
|
440
|
+
} else if (/authentication failed|fatal: authentication failed/i.test(stderr)) {
|
|
441
|
+
hint =
|
|
442
|
+
"HTTPS auth failed. Use a PAT/credential helper or switch to SSH.";
|
|
443
|
+
} else if (/repository .* not found|not found/i.test(stderr)) {
|
|
444
|
+
hint =
|
|
445
|
+
"Repository not found or access denied. Verify the org/repo and permissions.";
|
|
446
|
+
}
|
|
435
447
|
results.push({
|
|
436
448
|
name: repo.name,
|
|
437
449
|
success: false,
|
|
438
|
-
error: `git clone failed: ${
|
|
439
|
-
|
|
440
|
-
}`,
|
|
450
|
+
error: `git clone failed (${repoUrl}): ${
|
|
451
|
+
stderr || clone.error?.message || "unknown error"
|
|
452
|
+
}${hint ? ` — ${hint}` : ""}`,
|
|
441
453
|
});
|
|
442
454
|
continue;
|
|
443
455
|
}
|
|
@@ -446,18 +458,52 @@ export function pullWorkspaceRepos(configDir, workspaceId) {
|
|
|
446
458
|
results.push({
|
|
447
459
|
name: repo.name,
|
|
448
460
|
success: false,
|
|
449
|
-
error: `git clone failed: ${err.message || err}`,
|
|
461
|
+
error: `git clone failed (${repoUrl}): ${err.message || err}`,
|
|
450
462
|
});
|
|
451
463
|
continue;
|
|
452
464
|
}
|
|
453
465
|
}
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
466
|
+
const gitDir = resolve(repoPath, ".git");
|
|
467
|
+
if (!existsSync(gitDir)) {
|
|
468
|
+
try {
|
|
469
|
+
const contents = existsSync(repoPath) ? readdirSync(repoPath) : [];
|
|
470
|
+
const isEmpty = contents.length === 0;
|
|
471
|
+
const repoUrl =
|
|
472
|
+
repo.url ||
|
|
473
|
+
(repo.slug ? `https://github.com/${repo.slug.replace(/\.git$/i, "")}.git` : "");
|
|
474
|
+
if (isEmpty && repoUrl) {
|
|
475
|
+
console.log(TAG, `Cloning ${repoUrl} into existing empty directory ${repoPath}...`);
|
|
476
|
+
const clone = spawnSync("git", ["clone", repoUrl, "."], {
|
|
477
|
+
encoding: "utf8",
|
|
478
|
+
timeout: 300000,
|
|
479
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
480
|
+
cwd: repoPath,
|
|
481
|
+
});
|
|
482
|
+
if (clone.status !== 0) {
|
|
483
|
+
const stderr = String(clone.stderr || clone.stdout || "");
|
|
484
|
+
results.push({
|
|
485
|
+
name: repo.name,
|
|
486
|
+
success: false,
|
|
487
|
+
error: `git clone failed (${repoUrl}): ${stderr || clone.error?.message || "unknown error"}`,
|
|
488
|
+
});
|
|
489
|
+
continue;
|
|
490
|
+
}
|
|
491
|
+
} else {
|
|
492
|
+
results.push({
|
|
493
|
+
name: repo.name,
|
|
494
|
+
success: false,
|
|
495
|
+
error: "Directory exists but is not a git repository",
|
|
496
|
+
});
|
|
497
|
+
continue;
|
|
498
|
+
}
|
|
499
|
+
} catch (err) {
|
|
500
|
+
results.push({
|
|
501
|
+
name: repo.name,
|
|
502
|
+
success: false,
|
|
503
|
+
error: `Directory check failed: ${err.message || err}`,
|
|
504
|
+
});
|
|
505
|
+
continue;
|
|
506
|
+
}
|
|
461
507
|
}
|
|
462
508
|
try {
|
|
463
509
|
execSync("git pull --rebase", {
|