bosun 0.28.2 → 0.28.4
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/.env.example +68 -0
- package/README.md +1 -1
- package/agent-prompts.mjs +12 -6
- package/agent-work-analyzer.mjs +39 -15
- package/cli.mjs +4 -1
- package/codex-config.mjs +7 -0
- package/monitor.mjs +83 -24
- package/package.json +2 -1
- package/preflight.mjs +3 -1
- package/primary-agent.mjs +5 -1
- package/pwsh-runtime.mjs +62 -0
- package/setup.mjs +70 -3
- package/task-executor.mjs +125 -2
- package/telegram-bot.mjs +45 -8
- package/ui/app.js +2 -16
- package/ui/components/workspace-switcher.js +25 -32
- package/ui/modules/settings-schema.js +7 -0
- package/ui/styles/base.css +3 -28
- package/ui/styles/components.css +309 -73
- package/ui/styles/kanban.css +10 -16
- package/ui/styles/layout.css +81 -101
- package/ui/styles/sessions.css +27 -32
- package/ui/styles/variables.css +8 -8
- package/ui/styles/workspace-switcher.css +2 -4
- package/ui/tabs/control.js +40 -71
- package/ui/tabs/settings.js +207 -0
- package/ui/tabs/tasks.js +116 -129
- package/ui-server.mjs +487 -0
- package/workspace-manager.mjs +57 -11
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) {
|
|
@@ -1584,7 +1627,13 @@ function buildDefaultWritableRoots(repoRoot) {
|
|
|
1584
1627
|
if (parent && parent !== repo) roots.add(parent);
|
|
1585
1628
|
roots.add(repo);
|
|
1586
1629
|
roots.add(resolve(repo, ".git"));
|
|
1630
|
+
// Worktree checkout paths (used by task-executor)
|
|
1631
|
+
roots.add(resolve(repo, ".cache", "worktrees"));
|
|
1632
|
+
// Cache directories for agent work logs, build artifacts, etc.
|
|
1633
|
+
roots.add(resolve(repo, ".cache"));
|
|
1587
1634
|
}
|
|
1635
|
+
// /tmp needed for sandbox temp files, pip installs, etc.
|
|
1636
|
+
roots.add("/tmp");
|
|
1588
1637
|
return Array.from(roots).join(",");
|
|
1589
1638
|
}
|
|
1590
1639
|
|
|
@@ -2222,10 +2271,27 @@ async function main() {
|
|
|
2222
2271
|
let repoIdx = 0;
|
|
2223
2272
|
|
|
2224
2273
|
while (addMoreRepos) {
|
|
2225
|
-
|
|
2274
|
+
let repoUrl = await prompt.ask(
|
|
2226
2275
|
` Repo ${repoIdx + 1} — git URL (SSH or HTTPS)`,
|
|
2227
|
-
repoIdx === 0
|
|
2276
|
+
repoIdx === 0
|
|
2277
|
+
? buildDefaultGitUrl(env.GITHUB_REPO || slug, repoRoot)
|
|
2278
|
+
: "",
|
|
2228
2279
|
);
|
|
2280
|
+
if (repoUrl && isSshGitUrl(repoUrl) && !hasSshKeyMaterial()) {
|
|
2281
|
+
warn(
|
|
2282
|
+
"SSH URL detected but no SSH agent/keys found. Cloning may fail unless SSH is configured.",
|
|
2283
|
+
);
|
|
2284
|
+
const switchToHttps = await prompt.confirm(
|
|
2285
|
+
"Use HTTPS URL instead?",
|
|
2286
|
+
true,
|
|
2287
|
+
);
|
|
2288
|
+
if (switchToHttps) {
|
|
2289
|
+
const parsedSlug = parseRepoSlugFromUrl(repoUrl);
|
|
2290
|
+
if (parsedSlug) {
|
|
2291
|
+
repoUrl = `https://github.com/${parsedSlug}.git`;
|
|
2292
|
+
}
|
|
2293
|
+
}
|
|
2294
|
+
}
|
|
2229
2295
|
const parsedSlug = parseRepoSlugFromUrl(repoUrl);
|
|
2230
2296
|
const parsedRepoName = parsedSlug ? parsedSlug.split("/")[1] : "";
|
|
2231
2297
|
const defaultNameFromUrl = repoUrl
|
|
@@ -2872,11 +2938,12 @@ async function main() {
|
|
|
2872
2938
|
if (ghStatus.ok) break;
|
|
2873
2939
|
|
|
2874
2940
|
warn(
|
|
2875
|
-
|
|
2941
|
+
`GitHub auth is required to auto-detect projects, create boards, and sync issues. ${ghStatus.reason || ""}`.trim(),
|
|
2876
2942
|
);
|
|
2877
2943
|
info(
|
|
2878
2944
|
"If you do not plan to use GitHub as the task manager, pick Internal, Jira, or Vibe-Kanban.",
|
|
2879
2945
|
);
|
|
2946
|
+
info("Authenticate with GitHub using: gh auth login");
|
|
2880
2947
|
const ghActionIdx = await prompt.choose(
|
|
2881
2948
|
"How do you want to proceed?",
|
|
2882
2949
|
[
|
package/task-executor.mjs
CHANGED
|
@@ -113,6 +113,35 @@ const CODEX_TASK_LABELS = (() => {
|
|
|
113
113
|
|
|
114
114
|
/** Watchdog interval: how often to check for stalled agent slots */
|
|
115
115
|
const WATCHDOG_INTERVAL_MS = 60_000; // 1 minute
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Returns the Co-authored-by trailer for bosun-botswain[bot], or empty string
|
|
119
|
+
* if the GitHub App ID is not configured. Used to attribute agent commits to
|
|
120
|
+
* the Bosun GitHub App so the bot shows up as a contributor.
|
|
121
|
+
*
|
|
122
|
+
* To enable: set BOSUN_GITHUB_APP_ID=<your-app-id> in .env
|
|
123
|
+
* App noreply email format: <id>+bosun-botswain[bot]@users.noreply.github.com
|
|
124
|
+
*/
|
|
125
|
+
function getBosunCoAuthorLine() {
|
|
126
|
+
const appId = String(process.env.BOSUN_GITHUB_APP_ID || "").trim();
|
|
127
|
+
if (!appId) return "";
|
|
128
|
+
return `Co-authored-by: bosun-botswain[bot] <${appId}+bosun-botswain[bot]@users.noreply.github.com>`;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Returns a prompt instruction block telling the agent to append the Bosun
|
|
133
|
+
* co-author trailer to every commit. Empty string when app ID not configured.
|
|
134
|
+
*/
|
|
135
|
+
function getBosunCoAuthorInstruction() {
|
|
136
|
+
const line = getBosunCoAuthorLine();
|
|
137
|
+
if (!line) return "";
|
|
138
|
+
return `\n**Attribution (required — do not omit):**
|
|
139
|
+
Every commit message MUST end with a blank line then this exact trailer:
|
|
140
|
+
\`\`\`
|
|
141
|
+
${line}
|
|
142
|
+
\`\`\`
|
|
143
|
+
`;
|
|
144
|
+
}
|
|
116
145
|
/** Grace period after task timeout before watchdog force-kills the slot */
|
|
117
146
|
const WATCHDOG_GRACE_MS = 10 * 60_000; // 10 minutes — generous buffer, stream analysis handles real issues
|
|
118
147
|
/** Max age for in-progress tasks to auto-resume after monitor restart */
|
|
@@ -212,11 +241,30 @@ function normalizeBranchName(value) {
|
|
|
212
241
|
function extractScopeFromTitle(title) {
|
|
213
242
|
if (!title) return null;
|
|
214
243
|
const match = String(title).match(
|
|
215
|
-
/(?:^\[
|
|
244
|
+
/(?:^\[[^\]]+\]\s*)?(?:feat|fix|docs|style|refactor|perf|test|build|ci|chore|revert)\(([^)]+)\)/i,
|
|
216
245
|
);
|
|
217
246
|
return match ? match[1].toLowerCase().trim() : null;
|
|
218
247
|
}
|
|
219
248
|
|
|
249
|
+
/**
|
|
250
|
+
* If TASK_BRANCH_AUTO_MODULE=true (default), extract the conventional-commit
|
|
251
|
+
* scope from a task title and return the corresponding module branch ref.
|
|
252
|
+
*
|
|
253
|
+
* e.g. "[m] feat(veid): add verification" → "origin/veid"
|
|
254
|
+
* "[s] fix(market): resolve bid race" → "origin/market"
|
|
255
|
+
*/
|
|
256
|
+
function resolveModuleAutoBaseBranch(title) {
|
|
257
|
+
const enabled = (process.env.TASK_BRANCH_AUTO_MODULE ?? "true") !== "false";
|
|
258
|
+
if (!enabled) return null;
|
|
259
|
+
const scope = extractScopeFromTitle(title);
|
|
260
|
+
if (!scope) return null;
|
|
261
|
+
// Exclude generic scopes that don't map to real module branches
|
|
262
|
+
const generic = new Set(["deps", "app", "sdk", "cli", "api"]);
|
|
263
|
+
if (generic.has(scope)) return null;
|
|
264
|
+
const prefix = (process.env.MODULE_BRANCH_PREFIX || "origin/").replace(/\/*$/, "/");
|
|
265
|
+
return `${prefix}${scope}`;
|
|
266
|
+
}
|
|
267
|
+
|
|
220
268
|
function collectTaskLabels(task) {
|
|
221
269
|
const labels = [];
|
|
222
270
|
if (!task) return labels;
|
|
@@ -350,6 +398,10 @@ function resolveTaskBaseBranch(task, branchRouting, defaultTargetBranch) {
|
|
|
350
398
|
const fromConfig = resolveUpstreamFromConfig(task, branchRouting);
|
|
351
399
|
if (fromConfig) return normalizeBranchName(fromConfig);
|
|
352
400
|
|
|
401
|
+
// Auto-detect module branch from conventional commit scope in title
|
|
402
|
+
const moduleAuto = resolveModuleAutoBaseBranch(task.title || task.name);
|
|
403
|
+
if (moduleAuto) return normalizeBranchName(moduleAuto);
|
|
404
|
+
|
|
353
405
|
return normalizeBranchName(defaultTargetBranch);
|
|
354
406
|
}
|
|
355
407
|
|
|
@@ -3285,12 +3337,25 @@ class TaskExecutor {
|
|
|
3285
3337
|
const requestedBranch = String(
|
|
3286
3338
|
options?.branch || options?.resumeBranch || "",
|
|
3287
3339
|
).trim();
|
|
3340
|
+
// Resolve base branch first so direct-mode can reuse it for the working branch
|
|
3341
|
+
const baseBranch = this._resolveTaskBaseBranch(task);
|
|
3342
|
+
// Branch name strategy:
|
|
3343
|
+
// TASK_BRANCH_MODE=direct → work directly on the module branch (no ve/ sub-branch),
|
|
3344
|
+
// enabling sequential in-place work with real-time conflict
|
|
3345
|
+
// resolution by the same team of agents.
|
|
3346
|
+
// TASK_BRANCH_MODE=worktree (default) → create an isolated ve/<id>-<slug> sub-branch
|
|
3347
|
+
// that PRs back into the module/base branch.
|
|
3348
|
+
const branchMode = (process.env.TASK_BRANCH_MODE || "worktree").trim().toLowerCase();
|
|
3349
|
+
const directModeBranch =
|
|
3350
|
+
branchMode === "direct" && baseBranch
|
|
3351
|
+
? normalizeBranchName(baseBranch)?.replace(/^origin\//, "") || null
|
|
3352
|
+
: null;
|
|
3288
3353
|
const branch =
|
|
3289
3354
|
requestedBranch ||
|
|
3290
3355
|
task.branchName ||
|
|
3291
3356
|
task.meta?.branch_name ||
|
|
3357
|
+
directModeBranch ||
|
|
3292
3358
|
`ve/${taskId.substring(0, 8)}-${slugify(taskTitle)}`;
|
|
3293
|
-
const baseBranch = this._resolveTaskBaseBranch(task);
|
|
3294
3359
|
const taskRepoContext = this._resolveTaskRepoContext(task);
|
|
3295
3360
|
const executionRepoRoot = taskRepoContext.repoRoot || this.repoRoot;
|
|
3296
3361
|
const executionRepoSlug = taskRepoContext.repoSlug || this.repoSlug;
|
|
@@ -4124,6 +4189,7 @@ class TaskExecutor {
|
|
|
4124
4189
|
REPO_ROOT: promptRepoRoot,
|
|
4125
4190
|
TASK_WORKSPACE: promptWorkspace,
|
|
4126
4191
|
TASK_REPOSITORY: promptRepository,
|
|
4192
|
+
COAUTHOR_INSTRUCTION: getBosunCoAuthorInstruction(),
|
|
4127
4193
|
},
|
|
4128
4194
|
fallbackPrompt,
|
|
4129
4195
|
);
|
|
@@ -4196,6 +4262,11 @@ class TaskExecutor {
|
|
|
4196
4262
|
`3. Re-run tests to verify`,
|
|
4197
4263
|
`4. Commit and push your fixes`,
|
|
4198
4264
|
``,
|
|
4265
|
+
...(getBosunCoAuthorLine() ? [
|
|
4266
|
+
`Attribution: append this trailer after each commit message body (blank line before it):`,
|
|
4267
|
+
getBosunCoAuthorLine(),
|
|
4268
|
+
``,
|
|
4269
|
+
] : []),
|
|
4199
4270
|
`Original task description:`,
|
|
4200
4271
|
task.description || "See task URL for details.",
|
|
4201
4272
|
].join("\n");
|
|
@@ -5101,6 +5172,58 @@ class TaskExecutor {
|
|
|
5101
5172
|
/* best-effort upstream rebase */
|
|
5102
5173
|
}
|
|
5103
5174
|
|
|
5175
|
+
// Additionally sync with origin/main (or DEFAULT_TARGET_BRANCH) so that
|
|
5176
|
+
// module branches continuously absorb upstream changes before each push.
|
|
5177
|
+
// This surfaces conflicts early — if a merge conflict occurs, we abort the
|
|
5178
|
+
// merge and push as-is; the NEXT agent working on this branch will encounter
|
|
5179
|
+
// the conflict immediately and resolve it inline (no separate conflict-fix task).
|
|
5180
|
+
try {
|
|
5181
|
+
const mainBranch =
|
|
5182
|
+
process.env.DEFAULT_TARGET_BRANCH ||
|
|
5183
|
+
process.env.VK_TARGET_BRANCH?.replace(/^origin\//, "") ||
|
|
5184
|
+
"main";
|
|
5185
|
+
const syncEnabled =
|
|
5186
|
+
(process.env.TASK_UPSTREAM_SYNC_MAIN ?? "true") !== "false";
|
|
5187
|
+
// Only sync main when base branch is NOT already main itself
|
|
5188
|
+
const isMainBase = baseInfo.branch === mainBranch || baseBranch === "main";
|
|
5189
|
+
if (syncEnabled && !isMainBase) {
|
|
5190
|
+
spawnSync("git", ["fetch", "origin", mainBranch, "--quiet"], {
|
|
5191
|
+
cwd: worktreePath,
|
|
5192
|
+
encoding: "utf8",
|
|
5193
|
+
timeout: 30_000,
|
|
5194
|
+
});
|
|
5195
|
+
const mainMergeResult = spawnSync(
|
|
5196
|
+
"git",
|
|
5197
|
+
["merge", `origin/${mainBranch}`, "--no-edit", "--no-ff", "-X", "ours"],
|
|
5198
|
+
{
|
|
5199
|
+
cwd: worktreePath,
|
|
5200
|
+
encoding: "utf8",
|
|
5201
|
+
timeout: 60_000,
|
|
5202
|
+
},
|
|
5203
|
+
);
|
|
5204
|
+
if (mainMergeResult.status !== 0) {
|
|
5205
|
+
// Conflicts or error — abort and continue with push as-is.
|
|
5206
|
+
// The unresolved divergence will be visible to the next agent.
|
|
5207
|
+
console.warn(
|
|
5208
|
+
`${TAG} upstream main merge (origin/${mainBranch}) had conflicts on ` +
|
|
5209
|
+
`${branch} — aborting merge, pushing branch as-is. ` +
|
|
5210
|
+
`Next agent on this branch will resolve conflicts.`,
|
|
5211
|
+
);
|
|
5212
|
+
spawnSync("git", ["merge", "--abort"], {
|
|
5213
|
+
cwd: worktreePath,
|
|
5214
|
+
encoding: "utf8",
|
|
5215
|
+
timeout: 10_000,
|
|
5216
|
+
});
|
|
5217
|
+
} else {
|
|
5218
|
+
console.log(
|
|
5219
|
+
`${TAG} synced ${branch} with origin/${mainBranch} before push.`,
|
|
5220
|
+
);
|
|
5221
|
+
}
|
|
5222
|
+
}
|
|
5223
|
+
} catch {
|
|
5224
|
+
/* best-effort upstream main sync */
|
|
5225
|
+
}
|
|
5226
|
+
|
|
5104
5227
|
const safety = evaluateBranchSafetyForPush(worktreePath, {
|
|
5105
5228
|
baseBranch,
|
|
5106
5229
|
remote: "origin",
|
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/app.js
CHANGED
|
@@ -255,25 +255,11 @@ function Header() {
|
|
|
255
255
|
return html`
|
|
256
256
|
<header class="app-header">
|
|
257
257
|
<div class="app-header-left">
|
|
258
|
-
<div class="app-header-brand">
|
|
259
|
-
<div class="app-header-logo">
|
|
260
|
-
<img src="logo.png" alt="Bosun" class="app-logo-img" />
|
|
261
|
-
</div>
|
|
262
|
-
<div class="app-header-titles">
|
|
263
|
-
<div class="app-header-title">Bosun</div>
|
|
264
|
-
${subLabel
|
|
265
|
-
? html`<div class="app-header-subtitle">${subLabel}</div>`
|
|
266
|
-
: null}
|
|
267
|
-
${navHint
|
|
268
|
-
? html`<div class="app-header-hint">${navHint}</div>`
|
|
269
|
-
: null}
|
|
270
|
-
</div>
|
|
271
|
-
</div>
|
|
272
|
-
</div>
|
|
273
|
-
<div class="app-header-right">
|
|
274
258
|
<div class="app-header-workspace">
|
|
275
259
|
<${WorkspaceSwitcher} />
|
|
276
260
|
</div>
|
|
261
|
+
</div>
|
|
262
|
+
<div class="app-header-right">
|
|
277
263
|
<div class="header-actions">
|
|
278
264
|
<div class="header-status">
|
|
279
265
|
<div class="connection-pill ${connClass}">
|
|
@@ -10,6 +10,7 @@ import { signal } from "@preact/signals";
|
|
|
10
10
|
import htm from "htm";
|
|
11
11
|
import { apiFetch } from "../modules/api.js";
|
|
12
12
|
import { haptic } from "../modules/telegram.js";
|
|
13
|
+
import { Modal } from "./shared.js";
|
|
13
14
|
|
|
14
15
|
const html = htm.bind(h);
|
|
15
16
|
|
|
@@ -416,42 +417,34 @@ export function WorkspaceManager({ open, onClose }) {
|
|
|
416
417
|
const loading = workspacesLoading.value;
|
|
417
418
|
|
|
418
419
|
return html`
|
|
419
|
-
|
|
420
|
-
<div class="ws-manager-
|
|
421
|
-
<
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
title="Scan disk for workspaces"
|
|
429
|
-
>${scanning ? html`<${Spinner} /> Scanning…` : "🔍 Scan Disk"}</button>
|
|
430
|
-
<button class="ws-manager-close-btn" onClick=${onClose} title="Close">✕</button>
|
|
431
|
-
</div>
|
|
432
|
-
</div>
|
|
420
|
+
<${Modal} title="Manage Workspaces" open=${open} onClose=${onClose}>
|
|
421
|
+
<div class="ws-manager-modal-toolbar">
|
|
422
|
+
<button
|
|
423
|
+
class="btn btn-ghost btn-sm"
|
|
424
|
+
onClick=${handleScan}
|
|
425
|
+
disabled=${scanning}
|
|
426
|
+
title="Scan disk for workspaces"
|
|
427
|
+
>${scanning ? "Scanning…" : "🔍 Scan Disk"}</button>
|
|
428
|
+
</div>
|
|
433
429
|
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
}
|
|
430
|
+
${loading && !wsList.length
|
|
431
|
+
? html`<div class="ws-manager-loading">Loading workspaces…</div>`
|
|
432
|
+
: null
|
|
433
|
+
}
|
|
439
434
|
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
435
|
+
<div class="ws-manager-list">
|
|
436
|
+
${wsList.map((ws) => html`
|
|
437
|
+
<${WorkspaceCard} key=${ws.id} ws=${ws} />
|
|
438
|
+
`)}
|
|
439
|
+
</div>
|
|
445
440
|
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
441
|
+
${!wsList.length && !loading
|
|
442
|
+
? html`<div class="ws-manager-empty-state">No workspaces found. Create one or scan disk.</div>`
|
|
443
|
+
: null
|
|
444
|
+
}
|
|
450
445
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
</div>
|
|
454
|
-
</div>
|
|
446
|
+
<${AddWorkspaceForm} />
|
|
447
|
+
<//>
|
|
455
448
|
`;
|
|
456
449
|
}
|
|
457
450
|
|
|
@@ -111,6 +111,13 @@ export const SETTINGS_SCHEMA = [
|
|
|
111
111
|
{ key: "STALE_TASK_AGE_HOURS", label: "Stale Task Age", category: "kanban", type: "number", defaultVal: 3, min: 1, max: 168, unit: "hours", description: "Hours before an in-progress task with no activity is considered stale and eligible for recovery." },
|
|
112
112
|
{ key: "TASK_PLANNER_MODE", label: "Task Planner Mode", category: "kanban", type: "select", defaultVal: "kanban", options: ["kanban", "codex-sdk", "disabled"], description: "How the autonomous task planner operates. 'disabled' turns off automatic task generation." },
|
|
113
113
|
{ key: "TASK_PLANNER_DEDUP_HOURS", label: "Planner Dedup Window", category: "kanban", type: "number", defaultVal: 6, min: 1, max: 72, unit: "hours", description: "Hours to look back for duplicate task detection.", advanced: true },
|
|
114
|
+
|
|
115
|
+
// ── Branch Strategy ──────────────────────────────────────────────────
|
|
116
|
+
{ key: "TASK_BRANCH_MODE", label: "Branch Mode", category: "branching", type: "select", defaultVal: "worktree", options: ["worktree", "direct"], description: "worktree: agents use isolated ve/<id>-<slug> sub-branches that PR into the module/base branch (default). direct: agents push directly onto the module branch — sequential, real-time conflict resolution.", restart: true },
|
|
117
|
+
{ key: "TASK_BRANCH_AUTO_MODULE", label: "Auto Module Branch", category: "branching", type: "boolean", defaultVal: true, description: "Automatically detect origin/<module> as base_branch from conventional commit titles (e.g. feat(veid): → origin/veid). Enables parallel branch-per-module strategy." },
|
|
118
|
+
{ key: "MODULE_BRANCH_PREFIX", label: "Module Branch Prefix", category: "branching", type: "string", defaultVal: "origin/", description: "Prefix prepended to module scope when building module branch refs (default: origin/).", advanced: true },
|
|
119
|
+
{ key: "DEFAULT_TARGET_BRANCH", label: "Default Target Branch", category: "branching", type: "string", defaultVal: "main", description: "The main upstream branch agents sync with before pushing (e.g. main). Used for upstream sync and PR base detection." },
|
|
120
|
+
{ key: "TASK_UPSTREAM_SYNC_MAIN", label: "Upstream Main Sync", category: "branching", type: "boolean", defaultVal: true, description: "Before each push, merge origin/main into the task branch to keep it continuously in sync with upstream. If a conflict occurs, it is aborted and the next agent on the branch resolves it." },
|
|
114
121
|
{ key: "BOSUN_PROMPT_PLANNER", label: "Planner Prompt Path", category: "advanced", type: "string", description: "Override the task planner prompt file path.", advanced: true },
|
|
115
122
|
|
|
116
123
|
// ── Logging / Telemetry ──────────────────────────────────────
|
package/ui/styles/base.css
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
box-sizing: border-box;
|
|
9
9
|
margin: 0;
|
|
10
10
|
padding: 0;
|
|
11
|
+
min-width: 0;
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
html,
|
|
@@ -21,8 +22,6 @@ body {
|
|
|
21
22
|
line-height: 1.5;
|
|
22
23
|
color: var(--text-primary);
|
|
23
24
|
background: var(--bg-primary);
|
|
24
|
-
background-image: var(--gradient-mesh);
|
|
25
|
-
background-attachment: fixed;
|
|
26
25
|
-webkit-font-smoothing: antialiased;
|
|
27
26
|
-moz-osx-font-smoothing: grayscale;
|
|
28
27
|
-webkit-tap-highlight-color: transparent;
|
|
@@ -34,8 +33,6 @@ body {
|
|
|
34
33
|
}
|
|
35
34
|
|
|
36
35
|
#app {
|
|
37
|
-
/* height: 100% (not min-height) anchors the flex tree so .main-content gets
|
|
38
|
-
a real bounded height to scroll within instead of growing indefinitely */
|
|
39
36
|
height: 100%;
|
|
40
37
|
overflow: hidden;
|
|
41
38
|
display: flex;
|
|
@@ -44,32 +41,10 @@ body {
|
|
|
44
41
|
z-index: 1;
|
|
45
42
|
}
|
|
46
43
|
|
|
44
|
+
/* Remove body glow pseudo-elements — they cause visual noise */
|
|
47
45
|
body::before,
|
|
48
46
|
body::after {
|
|
49
|
-
|
|
50
|
-
position: fixed;
|
|
51
|
-
pointer-events: none;
|
|
52
|
-
z-index: 0;
|
|
53
|
-
inset: auto;
|
|
54
|
-
opacity: 0.45;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
body::before {
|
|
58
|
-
top: -20%;
|
|
59
|
-
left: -10%;
|
|
60
|
-
right: -10%;
|
|
61
|
-
height: 55%;
|
|
62
|
-
background: var(--gradient-hero);
|
|
63
|
-
filter: blur(12px);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
body::after {
|
|
67
|
-
bottom: -25%;
|
|
68
|
-
left: -20%;
|
|
69
|
-
right: -20%;
|
|
70
|
-
height: 60%;
|
|
71
|
-
background: var(--gradient-radial-glow);
|
|
72
|
-
filter: blur(18px);
|
|
47
|
+
display: none;
|
|
73
48
|
}
|
|
74
49
|
|
|
75
50
|
/* Selection styling */
|