bosun 0.41.2 → 0.41.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/.env.example +1 -1
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +28 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/ui-server.mjs +1194 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +21 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +334 -80
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +21 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +785 -140
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +304 -52
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +20 -9
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
|
@@ -24,13 +24,19 @@ import {
|
|
|
24
24
|
registerNodeType,
|
|
25
25
|
unregisterNodeType,
|
|
26
26
|
} from "./workflow-engine.mjs";
|
|
27
|
+
import {
|
|
28
|
+
_completedWithPR,
|
|
29
|
+
_noCommitCounts,
|
|
30
|
+
_skipUntil,
|
|
31
|
+
MAX_NO_COMMIT_ATTEMPTS,
|
|
32
|
+
} from "./workflow-nodes/transforms.mjs";
|
|
27
33
|
import { existsSync, readFileSync, writeFileSync, mkdirSync, rmSync } from "node:fs";
|
|
28
34
|
import { resolve, dirname } from "node:path";
|
|
29
35
|
import { execSync, execFileSync, spawn, spawnSync } from "node:child_process";
|
|
30
36
|
import { createHash, randomUUID } from "node:crypto";
|
|
31
37
|
import { getAgentToolConfig, getEffectiveTools } from "../agent/agent-tool-config.mjs";
|
|
32
38
|
import { getToolsPromptBlock } from "../agent/agent-custom-tools.mjs";
|
|
33
|
-
import { buildRelevantSkillsPromptBlock, findRelevantSkills } from "../agent/bosun-skills.mjs";
|
|
39
|
+
import { buildRelevantSkillsPromptBlock, emitSkillInvokeEvent, findRelevantSkills } from "../agent/bosun-skills.mjs";
|
|
34
40
|
import { readBenchmarkModeState, taskMatchesBenchmarkMode } from "../bench/benchmark-mode.mjs";
|
|
35
41
|
import { getSessionTracker } from "../infra/session-tracker.mjs";
|
|
36
42
|
import {
|
|
@@ -38,8 +44,9 @@ import {
|
|
|
38
44
|
loadWorkflowContract,
|
|
39
45
|
validateWorkflowContract,
|
|
40
46
|
} from "./workflow-contract.mjs";
|
|
47
|
+
import { loadConfig } from "../config/config.mjs";
|
|
41
48
|
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
42
|
-
import { clearBlockedWorktreeIdentity } from "../git/git-safety.mjs";
|
|
49
|
+
import { clearBlockedWorktreeIdentity, normalizeBaseBranch } from "../git/git-safety.mjs";
|
|
43
50
|
import { getGitHubToken, invalidateTokenType } from "../github/github-auth-manager.mjs";
|
|
44
51
|
import {
|
|
45
52
|
CUSTOM_NODE_DIR_NAME,
|
|
@@ -55,6 +62,7 @@ let customLoadPromise = null;
|
|
|
55
62
|
let customDiscoveryStarted = false;
|
|
56
63
|
const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
57
64
|
const PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND = "node -e \"const cp=require('node:child_process');cp.execSync('git worktree prune',{stdio:'ignore'});const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
|
|
65
|
+
const DEFAULT_NON_RETRYABLE_WORKTREE_RECOVERY_MS = 15 * 60 * 1000;
|
|
58
66
|
const WORKFLOW_AGENT_HEARTBEAT_MS = (() => {
|
|
59
67
|
const raw = Number(process.env.WORKFLOW_AGENT_HEARTBEAT_MS || 30000);
|
|
60
68
|
if (!Number.isFinite(raw)) return 30000;
|
|
@@ -67,6 +75,19 @@ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
|
|
|
67
75
|
})();
|
|
68
76
|
const BOSUN_ATTACHED_PR_LABEL = "bosun-attached";
|
|
69
77
|
|
|
78
|
+
function getNonRetryableWorktreeRecoveryMs() {
|
|
79
|
+
try {
|
|
80
|
+
const config = loadConfig();
|
|
81
|
+
const minutes = Number(config?.workflowWorktreeRecoveryCooldownMin);
|
|
82
|
+
if (!Number.isFinite(minutes)) {
|
|
83
|
+
return DEFAULT_NON_RETRYABLE_WORKTREE_RECOVERY_MS;
|
|
84
|
+
}
|
|
85
|
+
return Math.max(1, Math.min(1440, Math.trunc(minutes))) * 60 * 1000;
|
|
86
|
+
} catch {
|
|
87
|
+
return DEFAULT_NON_RETRYABLE_WORKTREE_RECOVERY_MS;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
70
91
|
const HTML_TEXT_BREAK_TAGS = new Set([
|
|
71
92
|
"address",
|
|
72
93
|
"article",
|
|
@@ -1334,6 +1355,45 @@ function parseBooleanSetting(value, defaultValue = false) {
|
|
|
1334
1355
|
return defaultValue;
|
|
1335
1356
|
}
|
|
1336
1357
|
|
|
1358
|
+
async function recoverTimedBlockedWorkflowTasks({ kanban, ctx, node, projectId }) {
|
|
1359
|
+
if (!kanban || typeof kanban.listTasks !== "function" || typeof kanban.updateTask !== "function") {
|
|
1360
|
+
return { recoveredTaskIds: [], recoveredCount: 0 };
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
const blockedTasks = await kanban.listTasks(projectId, { status: "blocked" });
|
|
1364
|
+
const nowMs = Date.now();
|
|
1365
|
+
const recoveredTaskIds = [];
|
|
1366
|
+
for (const task of Array.isArray(blockedTasks) ? blockedTasks : []) {
|
|
1367
|
+
const autoRecovery = task?.meta?.autoRecovery;
|
|
1368
|
+
if (!autoRecovery || typeof autoRecovery !== "object") continue;
|
|
1369
|
+
if (autoRecovery.active === false) continue;
|
|
1370
|
+
if (String(autoRecovery.reason || "").trim() !== "worktree_failure") continue;
|
|
1371
|
+
const retryAtMs = Date.parse(String(autoRecovery.retryAt || task?.cooldownUntil || ""));
|
|
1372
|
+
if (!Number.isFinite(retryAtMs) || retryAtMs > nowMs) continue;
|
|
1373
|
+
await kanban.updateTask(task.id, {
|
|
1374
|
+
status: "todo",
|
|
1375
|
+
cooldownUntil: null,
|
|
1376
|
+
blockedReason: null,
|
|
1377
|
+
meta: {
|
|
1378
|
+
...(task?.meta && typeof task.meta === "object" ? task.meta : {}),
|
|
1379
|
+
autoRecovery: {
|
|
1380
|
+
...autoRecovery,
|
|
1381
|
+
active: false,
|
|
1382
|
+
recoveredAt: new Date(nowMs).toISOString(),
|
|
1383
|
+
recoveredStatus: "todo",
|
|
1384
|
+
},
|
|
1385
|
+
},
|
|
1386
|
+
});
|
|
1387
|
+
recoveredTaskIds.push(task.id);
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
if (recoveredTaskIds.length > 0) {
|
|
1391
|
+
ctx.log(node.id, `Recovered ${recoveredTaskIds.length} blocked task(s): ${recoveredTaskIds.join(", ")}`);
|
|
1392
|
+
}
|
|
1393
|
+
|
|
1394
|
+
return { recoveredTaskIds, recoveredCount: recoveredTaskIds.length };
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1337
1397
|
function getPathValue(value, pathExpression) {
|
|
1338
1398
|
const path = String(pathExpression || "").trim();
|
|
1339
1399
|
if (!path) return undefined;
|
|
@@ -3936,7 +3996,7 @@ registerBuiltinNodeType("action.update_task_status", {
|
|
|
3936
3996
|
type: "object",
|
|
3937
3997
|
properties: {
|
|
3938
3998
|
taskId: { type: "string", description: "Task ID (supports {{variables}})" },
|
|
3939
|
-
status: { type: "string", enum: ["todo", "inprogress", "inreview", "done", "archived"] },
|
|
3999
|
+
status: { type: "string", enum: ["todo", "inprogress", "inreview", "done", "blocked", "archived"] },
|
|
3940
4000
|
taskTitle: { type: "string", description: "Optional task title for downstream event payloads" },
|
|
3941
4001
|
previousStatus: { type: "string", description: "Optional explicit previous status" },
|
|
3942
4002
|
workflowEvent: { type: "string", description: "Optional follow-up workflow event to emit after status update" },
|
|
@@ -4213,7 +4273,7 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4213
4273
|
autoMergeMethod: {
|
|
4214
4274
|
type: "string",
|
|
4215
4275
|
enum: ["merge", "squash", "rebase"],
|
|
4216
|
-
default: "
|
|
4276
|
+
default: "merge",
|
|
4217
4277
|
description: "Merge method used with gh pr merge --auto",
|
|
4218
4278
|
},
|
|
4219
4279
|
mergeMethod: {
|
|
@@ -4229,7 +4289,12 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4229
4289
|
async execute(node, ctx) {
|
|
4230
4290
|
const title = ctx.resolve(node.config?.title || "");
|
|
4231
4291
|
const body = ctx.resolve(node.config?.body || "");
|
|
4232
|
-
const
|
|
4292
|
+
const baseInput = ctx.resolve(node.config?.base || node.config?.baseBranch || "main");
|
|
4293
|
+
let base = String(baseInput || "main").trim() || "main";
|
|
4294
|
+
try {
|
|
4295
|
+
base = normalizeBaseBranch(base).branch;
|
|
4296
|
+
} catch {
|
|
4297
|
+
}
|
|
4233
4298
|
const branch = ctx.resolve(node.config?.branch || "");
|
|
4234
4299
|
const repoSlug = String(
|
|
4235
4300
|
ctx.resolve(node.config?.repoSlug || ctx.data?.repoSlug || ctx.data?.repository || ""),
|
|
@@ -4241,11 +4306,11 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4241
4306
|
false,
|
|
4242
4307
|
);
|
|
4243
4308
|
const autoMergeMethodRaw = String(
|
|
4244
|
-
ctx.resolve(node.config?.autoMergeMethod || node.config?.mergeMethod || "
|
|
4309
|
+
ctx.resolve(node.config?.autoMergeMethod || node.config?.mergeMethod || process.env.BOSUN_MERGE_METHOD || "merge"),
|
|
4245
4310
|
).trim().toLowerCase();
|
|
4246
4311
|
const autoMergeMethod = ["merge", "squash", "rebase"].includes(autoMergeMethodRaw)
|
|
4247
4312
|
? autoMergeMethodRaw
|
|
4248
|
-
: "
|
|
4313
|
+
: (process.env.BOSUN_MERGE_METHOD || "merge");
|
|
4249
4314
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
4250
4315
|
|
|
4251
4316
|
// Normalize labels/reviewers to arrays
|
|
@@ -9690,6 +9755,88 @@ function hasUnresolvedGitOperation(worktreePath) {
|
|
|
9690
9755
|
}
|
|
9691
9756
|
}
|
|
9692
9757
|
|
|
9758
|
+
function hasTrackedGitChanges(worktreePath) {
|
|
9759
|
+
if (!worktreePath || !existsSync(worktreePath)) return false;
|
|
9760
|
+
try {
|
|
9761
|
+
const status = execGitArgsSync(
|
|
9762
|
+
["status", "--porcelain", "--untracked-files=no"],
|
|
9763
|
+
{
|
|
9764
|
+
cwd: worktreePath,
|
|
9765
|
+
encoding: "utf8",
|
|
9766
|
+
timeout: 5000,
|
|
9767
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9768
|
+
},
|
|
9769
|
+
).trim();
|
|
9770
|
+
return Boolean(status);
|
|
9771
|
+
} catch {
|
|
9772
|
+
return true;
|
|
9773
|
+
}
|
|
9774
|
+
}
|
|
9775
|
+
|
|
9776
|
+
function countCommitsBehindBase(worktreePath, baseBranch) {
|
|
9777
|
+
if (!worktreePath || !existsSync(worktreePath) || !baseBranch) return 0;
|
|
9778
|
+
try {
|
|
9779
|
+
const counts = execGitArgsSync(
|
|
9780
|
+
["rev-list", "--left-right", "--count", `HEAD...${baseBranch}`],
|
|
9781
|
+
{
|
|
9782
|
+
cwd: worktreePath,
|
|
9783
|
+
encoding: "utf8",
|
|
9784
|
+
timeout: 5000,
|
|
9785
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9786
|
+
},
|
|
9787
|
+
).trim();
|
|
9788
|
+
const match = counts.match(/^(\d+)\s+(\d+)$/);
|
|
9789
|
+
return match ? Number(match[2]) : 0;
|
|
9790
|
+
} catch {
|
|
9791
|
+
return 0;
|
|
9792
|
+
}
|
|
9793
|
+
}
|
|
9794
|
+
|
|
9795
|
+
function refreshManagedWorktreeReuse(
|
|
9796
|
+
nodeId,
|
|
9797
|
+
ctx,
|
|
9798
|
+
repoRoot,
|
|
9799
|
+
worktreePath,
|
|
9800
|
+
baseBranch,
|
|
9801
|
+
baseBranchShort,
|
|
9802
|
+
fetchTimeout,
|
|
9803
|
+
) {
|
|
9804
|
+
if (!existsSync(worktreePath) || shouldSkipGitRefreshForTests()) return existsSync(worktreePath);
|
|
9805
|
+
let refreshError = "";
|
|
9806
|
+
try {
|
|
9807
|
+
execSync(`git pull --rebase origin ${baseBranchShort}`, {
|
|
9808
|
+
cwd: worktreePath,
|
|
9809
|
+
encoding: "utf8",
|
|
9810
|
+
timeout: fetchTimeout,
|
|
9811
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9812
|
+
});
|
|
9813
|
+
} catch (error) {
|
|
9814
|
+
refreshError = formatExecSyncError(error);
|
|
9815
|
+
}
|
|
9816
|
+
if (!existsSync(worktreePath)) return false;
|
|
9817
|
+
if (hasUnresolvedGitOperation(worktreePath)) {
|
|
9818
|
+
const detail = refreshError ? ` (${refreshError})` : "";
|
|
9819
|
+
ctx.log(
|
|
9820
|
+
nodeId,
|
|
9821
|
+
`Managed worktree refresh left unresolved git state, recreating: ${worktreePath}${detail}`,
|
|
9822
|
+
);
|
|
9823
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
9824
|
+
return false;
|
|
9825
|
+
}
|
|
9826
|
+
const reasons = [];
|
|
9827
|
+
if (hasTrackedGitChanges(worktreePath)) reasons.push("tracked changes after refresh");
|
|
9828
|
+
const behindCount = countCommitsBehindBase(worktreePath, baseBranch);
|
|
9829
|
+
if (behindCount > 0) reasons.push(`${behindCount} commit(s) behind ${baseBranch}`);
|
|
9830
|
+
if (reasons.length === 0) return true;
|
|
9831
|
+
if (refreshError) reasons.unshift(`refresh failed: ${refreshError}`);
|
|
9832
|
+
ctx.log(
|
|
9833
|
+
nodeId,
|
|
9834
|
+
`Managed worktree refresh did not yield a clean up-to-date branch, recreating: ${worktreePath} (${reasons.join("; ")})`,
|
|
9835
|
+
);
|
|
9836
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
9837
|
+
return false;
|
|
9838
|
+
}
|
|
9839
|
+
|
|
9693
9840
|
function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
9694
9841
|
if (!worktreePath) return;
|
|
9695
9842
|
const linkedGitDir = resolveGitDirForWorktree(worktreePath);
|
|
@@ -9728,13 +9875,9 @@ function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
|
9728
9875
|
}
|
|
9729
9876
|
|
|
9730
9877
|
/**
|
|
9731
|
-
* Anti-thrash state —
|
|
9732
|
-
*
|
|
9878
|
+
* Anti-thrash state — imported from transforms.mjs (single source of truth).
|
|
9879
|
+
* Shared between monolithic workflow-nodes.mjs and modular triggers.mjs.
|
|
9733
9880
|
*/
|
|
9734
|
-
const _noCommitCounts = new Map();
|
|
9735
|
-
const _skipUntil = new Map();
|
|
9736
|
-
const _completedWithPR = new Set();
|
|
9737
|
-
const MAX_NO_COMMIT_ATTEMPTS = 3;
|
|
9738
9881
|
const NO_COMMIT_BASE_COOLDOWN_MS = 15 * 60 * 1000; // 15 min
|
|
9739
9882
|
const NO_COMMIT_MAX_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
9740
9883
|
const STRICT_START_GUARD_MISSING_TASK = /^(1|true|yes|on)$/i.test(
|
|
@@ -9795,6 +9938,13 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
9795
9938
|
for (let attempt = 0; attempt <= listRetries; attempt++) {
|
|
9796
9939
|
try {
|
|
9797
9940
|
const kanban = ctx.data?._services?.kanban || engine?.services?.kanban;
|
|
9941
|
+
if (status === "todo") {
|
|
9942
|
+
try {
|
|
9943
|
+
await recoverTimedBlockedWorkflowTasks({ kanban, ctx, node, projectId });
|
|
9944
|
+
} catch (recoveryErr) {
|
|
9945
|
+
ctx.log(node.id, `Blocked task recovery warning: ${recoveryErr?.message || recoveryErr}`);
|
|
9946
|
+
}
|
|
9947
|
+
}
|
|
9798
9948
|
if (kanban?.listTasks) {
|
|
9799
9949
|
tasks = await kanban.listTasks(projectId, { status });
|
|
9800
9950
|
} else {
|
|
@@ -10108,6 +10258,9 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
10108
10258
|
if (baseBranch) ctx.data.baseBranch = baseBranch;
|
|
10109
10259
|
const branch = deriveTaskBranch(primaryTask);
|
|
10110
10260
|
if (branch) ctx.data.branch = branch;
|
|
10261
|
+
ctx.data.taskMeta = primaryTask?.meta && typeof primaryTask.meta === "object"
|
|
10262
|
+
? { ...primaryTask.meta }
|
|
10263
|
+
: {};
|
|
10111
10264
|
}
|
|
10112
10265
|
|
|
10113
10266
|
ctx.log(node.id, `Found ${toDispatch.length} task(s) ready (${remaining} slot(s) free)`);
|
|
@@ -10479,8 +10632,26 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10479
10632
|
|| ctx.data?.agentProfile
|
|
10480
10633
|
|| "",
|
|
10481
10634
|
).trim();
|
|
10635
|
+
const taskText = [task.title, task.description].filter(Boolean).join("\n");
|
|
10636
|
+
const inferredResolutionTags = [];
|
|
10637
|
+
if (/\btest(?:s|ing)?\b/i.test(taskText)) inferredResolutionTags.push("test", "tests");
|
|
10638
|
+
if (/\b(?:ci|cd|pipeline|workflow|github actions?)\b/i.test(taskText)) inferredResolutionTags.push("ci", "cd", "pipeline");
|
|
10639
|
+
if (/\b(?:merge conflict|conflicts|rebase|cherry-pick)\b/i.test(taskText)) inferredResolutionTags.push("conflict", "merge");
|
|
10640
|
+
if (/\b(?:implement|implementation|feature|build|ship)\b/i.test(taskText)) inferredResolutionTags.push("implementation");
|
|
10641
|
+
if (/\b(?:docs?|documentation|readme)\b/i.test(taskText)) inferredResolutionTags.push("docs", "documentation");
|
|
10642
|
+
const resolutionTags = Array.from(new Set([
|
|
10643
|
+
...task.tags,
|
|
10644
|
+
...inferredResolutionTags,
|
|
10645
|
+
String(ctx.data?.task?.type || "").trim(),
|
|
10646
|
+
String(ctx.data?.task?.agentType || "").trim(),
|
|
10647
|
+
String(ctx.data?.task?.assignedAgentType || "").trim(),
|
|
10648
|
+
String(ctx.data?.agentType || "").trim(),
|
|
10649
|
+
String(ctx.data?.assignedAgentType || "").trim(),
|
|
10650
|
+
].map((value) => String(value || "").trim()).filter(Boolean)));
|
|
10482
10651
|
let profileDecision = null;
|
|
10483
10652
|
let configuredExecutorPreference = null;
|
|
10653
|
+
ctx.data.resolvedSkillIds = [];
|
|
10654
|
+
ctx.data.resolvedLibraryPlan = null;
|
|
10484
10655
|
|
|
10485
10656
|
// Check env var overrides (mirrors TaskExecutor behavior)
|
|
10486
10657
|
const envModel =
|
|
@@ -10497,25 +10668,30 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10497
10668
|
|
|
10498
10669
|
try {
|
|
10499
10670
|
const library = await ensureLibraryManagerMod();
|
|
10500
|
-
const
|
|
10671
|
+
const criteria = {
|
|
10672
|
+
title: task.title,
|
|
10673
|
+
description: task.description,
|
|
10674
|
+
tags: resolutionTags,
|
|
10675
|
+
agentType: ctx.data?.task?.agentType || ctx.data?.agentType || "",
|
|
10501
10676
|
repoRoot,
|
|
10677
|
+
changedFiles: Array.isArray(ctx.data?.changedFiles) ? ctx.data.changedFiles : [],
|
|
10678
|
+
};
|
|
10679
|
+
const planResult = library.resolveLibraryPlan?.(
|
|
10680
|
+
repoRoot,
|
|
10681
|
+
criteria,
|
|
10502
10682
|
{
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
tags: task.tags,
|
|
10506
|
-
agentType: ctx.data?.task?.agentType || ctx.data?.agentType || "",
|
|
10507
|
-
repoRoot,
|
|
10683
|
+
topN: Math.max(10, requestedAgentProfileId ? 25 : 10),
|
|
10684
|
+
skillTopN: 5,
|
|
10508
10685
|
},
|
|
10509
|
-
{ topN: Math.max(10, requestedAgentProfileId ? 25 : 10) },
|
|
10510
10686
|
);
|
|
10511
|
-
const candidates = Array.isArray(
|
|
10512
|
-
const bestCandidate =
|
|
10513
|
-
const autoMinScore = Number(
|
|
10687
|
+
const candidates = Array.isArray(planResult?.candidates) ? planResult.candidates : [];
|
|
10688
|
+
const bestCandidate = planResult?.best || null;
|
|
10689
|
+
const autoMinScore = Number(planResult?.auto?.thresholds?.minScore || 12);
|
|
10514
10690
|
const scoreQualified = Number(bestCandidate?.score || 0) >= autoMinScore;
|
|
10515
10691
|
const matchedCandidate = requestedAgentProfileId
|
|
10516
10692
|
? candidates.find((candidate) => String(candidate?.id || "").trim() === requestedAgentProfileId) || null
|
|
10517
|
-
: ((
|
|
10518
|
-
if (!requestedAgentProfileId && bestCandidate && !
|
|
10693
|
+
: ((planResult?.auto?.shouldAutoApply || scoreQualified) ? bestCandidate : null);
|
|
10694
|
+
if (!requestedAgentProfileId && bestCandidate && !planResult?.auto?.shouldAutoApply) {
|
|
10519
10695
|
ctx.log(
|
|
10520
10696
|
node.id,
|
|
10521
10697
|
`Profile match below auto threshold; ignoring candidate ${String(bestCandidate.id || "unknown")}`,
|
|
@@ -10534,13 +10710,19 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10534
10710
|
ctx.data.agentProfile = profileId;
|
|
10535
10711
|
ctx.data.resolvedAgentProfile = {
|
|
10536
10712
|
id: profileId,
|
|
10537
|
-
name:
|
|
10713
|
+
name: matchedCandidate?.name || profile?.name || profileId,
|
|
10538
10714
|
...profile,
|
|
10539
10715
|
};
|
|
10540
|
-
const
|
|
10541
|
-
?
|
|
10542
|
-
:
|
|
10716
|
+
const resolvedPlan = planResult?.plan && planResult.plan.agentProfileId === profileId
|
|
10717
|
+
? planResult.plan
|
|
10718
|
+
: null;
|
|
10719
|
+
const skillIds = resolvedPlan && Array.isArray(resolvedPlan.skillIds)
|
|
10720
|
+
? resolvedPlan.skillIds.map((value) => String(value || "").trim()).filter(Boolean)
|
|
10721
|
+
: Array.isArray(profile.skills)
|
|
10722
|
+
? profile.skills.map((value) => String(value || "").trim()).filter(Boolean)
|
|
10723
|
+
: [];
|
|
10543
10724
|
ctx.data.resolvedSkillIds = skillIds;
|
|
10725
|
+
ctx.data.resolvedLibraryPlan = resolvedPlan;
|
|
10544
10726
|
}
|
|
10545
10727
|
} catch (err) {
|
|
10546
10728
|
ctx.log(node.id, `Library profile resolution failed: ${err.message}`);
|
|
@@ -10739,22 +10921,15 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10739
10921
|
}
|
|
10740
10922
|
|
|
10741
10923
|
if (existsSync(worktreePath)) {
|
|
10742
|
-
|
|
10743
|
-
|
|
10744
|
-
|
|
10745
|
-
|
|
10746
|
-
|
|
10747
|
-
|
|
10748
|
-
|
|
10749
|
-
|
|
10750
|
-
|
|
10751
|
-
/* rebase failures are non-fatal for reuse */
|
|
10752
|
-
}
|
|
10753
|
-
if (existsSync(worktreePath) && hasUnresolvedGitOperation(worktreePath)) {
|
|
10754
|
-
ctx.log(node.id, `Managed worktree refresh left unresolved git state, recreating: ${worktreePath}`);
|
|
10755
|
-
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
10756
|
-
}
|
|
10757
|
-
}
|
|
10924
|
+
refreshManagedWorktreeReuse(
|
|
10925
|
+
node.id,
|
|
10926
|
+
ctx,
|
|
10927
|
+
repoRoot,
|
|
10928
|
+
worktreePath,
|
|
10929
|
+
baseBranch,
|
|
10930
|
+
baseBranchShort,
|
|
10931
|
+
fetchTimeout,
|
|
10932
|
+
);
|
|
10758
10933
|
if (existsSync(worktreePath)) {
|
|
10759
10934
|
ctx.data.worktreePath = worktreePath;
|
|
10760
10935
|
ctx.data._worktreeCreated = false;
|
|
@@ -10767,6 +10942,7 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10767
10942
|
}
|
|
10768
10943
|
|
|
10769
10944
|
// Create fresh worktree
|
|
10945
|
+
let attachedExistingBranch = false;
|
|
10770
10946
|
try {
|
|
10771
10947
|
execSync(
|
|
10772
10948
|
`git worktree add "${worktreePath}" -b "${branch}" "${baseBranch}" 2>&1`,
|
|
@@ -10782,21 +10958,39 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10782
10958
|
`git worktree add "${worktreePath}" "${branch}" 2>&1`,
|
|
10783
10959
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
10784
10960
|
);
|
|
10961
|
+
attachedExistingBranch = true;
|
|
10785
10962
|
} catch (reuseErr) {
|
|
10786
10963
|
const existingBranchWorktree = findExistingWorktreePathForBranch(repoRoot, branch);
|
|
10787
10964
|
if (existingBranchWorktree && existsSync(existingBranchWorktree)) {
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
10965
|
+
const existingWorktreeIsBroken = (
|
|
10966
|
+
!isValidGitWorktreePath(existingBranchWorktree) ||
|
|
10967
|
+
hasUnresolvedGitOperation(existingBranchWorktree)
|
|
10968
|
+
) && isManagedBosunWorktree(existingBranchWorktree, repoRoot);
|
|
10969
|
+
if (existingWorktreeIsBroken) {
|
|
10791
10970
|
ctx.log(
|
|
10792
10971
|
node.id,
|
|
10793
|
-
`Existing branch worktree is invalid, recreating managed path: ${existingBranchWorktree}`,
|
|
10972
|
+
`Existing branch worktree is invalid or unresolved, recreating managed path: ${existingBranchWorktree}`,
|
|
10794
10973
|
);
|
|
10795
10974
|
cleanupBrokenManagedWorktree(repoRoot, existingBranchWorktree);
|
|
10796
10975
|
}
|
|
10797
10976
|
}
|
|
10798
10977
|
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
10799
|
-
isValidGitWorktreePath(existingBranchWorktree)
|
|
10978
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
10979
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
10980
|
+
) {
|
|
10981
|
+
refreshManagedWorktreeReuse(
|
|
10982
|
+
node.id,
|
|
10983
|
+
ctx,
|
|
10984
|
+
repoRoot,
|
|
10985
|
+
existingBranchWorktree,
|
|
10986
|
+
baseBranch,
|
|
10987
|
+
baseBranchShort,
|
|
10988
|
+
fetchTimeout,
|
|
10989
|
+
);
|
|
10990
|
+
}
|
|
10991
|
+
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
10992
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
10993
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
10800
10994
|
) {
|
|
10801
10995
|
ctx.data.worktreePath = existingBranchWorktree;
|
|
10802
10996
|
ctx.data._worktreeCreated = false;
|
|
@@ -10820,6 +11014,22 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10820
11014
|
);
|
|
10821
11015
|
}
|
|
10822
11016
|
}
|
|
11017
|
+
if (attachedExistingBranch) {
|
|
11018
|
+
refreshManagedWorktreeReuse(
|
|
11019
|
+
node.id,
|
|
11020
|
+
ctx,
|
|
11021
|
+
repoRoot,
|
|
11022
|
+
worktreePath,
|
|
11023
|
+
baseBranch,
|
|
11024
|
+
baseBranchShort,
|
|
11025
|
+
fetchTimeout,
|
|
11026
|
+
);
|
|
11027
|
+
if (!existsSync(worktreePath)) {
|
|
11028
|
+
throw new Error(
|
|
11029
|
+
`Worktree refresh failed for existing branch ${branch}; managed worktree was removed after stale refresh state`,
|
|
11030
|
+
);
|
|
11031
|
+
}
|
|
11032
|
+
}
|
|
10823
11033
|
fixGitConfigCorruption(repoRoot);
|
|
10824
11034
|
const cleared3 = clearBlockedWorktreeIdentity(worktreePath);
|
|
10825
11035
|
if (cleared3) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
|
|
@@ -10830,8 +11040,28 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10830
11040
|
ctx.log(node.id, `Worktree created: ${worktreePath} (branch: ${branch}, base: ${baseBranch})`);
|
|
10831
11041
|
return { success: true, worktreePath, created: true, branch, baseBranch };
|
|
10832
11042
|
} catch (err) {
|
|
10833
|
-
|
|
10834
|
-
|
|
11043
|
+
const errorMessage = String(err?.message || err || "worktree_acquisition_failed");
|
|
11044
|
+
const retryable = !/managed worktree was removed after stale refresh state/i.test(errorMessage);
|
|
11045
|
+
const recordedAt = new Date().toISOString();
|
|
11046
|
+
const autoRecoverDelayMs = retryable ? 0 : getNonRetryableWorktreeRecoveryMs();
|
|
11047
|
+
const retryAt = retryable ? null : new Date(Date.now() + autoRecoverDelayMs).toISOString();
|
|
11048
|
+
const blockedReason = retryable
|
|
11049
|
+
? errorMessage
|
|
11050
|
+
: "Managed worktree refresh conflict detected; Bosun will retry automatically after cooldown.";
|
|
11051
|
+
ctx.log(node.id, `Worktree acquisition failed: ${errorMessage}`);
|
|
11052
|
+
return {
|
|
11053
|
+
success: false,
|
|
11054
|
+
error: errorMessage,
|
|
11055
|
+
branch,
|
|
11056
|
+
baseBranch,
|
|
11057
|
+
retryable,
|
|
11058
|
+
failureKind: retryable ? "worktree_acquisition_failed" : "branch_refresh_conflict",
|
|
11059
|
+
recordedAt,
|
|
11060
|
+
autoRecoverDelayMs,
|
|
11061
|
+
retryAt,
|
|
11062
|
+
blockedReason,
|
|
11063
|
+
recoveryNote: retryable || !retryAt ? "" : ` — blocked until ${retryAt}`,
|
|
11064
|
+
};
|
|
10835
11065
|
}
|
|
10836
11066
|
},
|
|
10837
11067
|
});
|
|
@@ -10887,6 +11117,8 @@ registerBuiltinNodeType("action.release_worktree", {
|
|
|
10887
11117
|
} catch { /* best-effort */ }
|
|
10888
11118
|
}
|
|
10889
11119
|
|
|
11120
|
+
fixGitConfigCorruption(repoRoot);
|
|
11121
|
+
|
|
10890
11122
|
ctx.data._worktreeCreated = false;
|
|
10891
11123
|
ctx.data._worktreeManaged = false;
|
|
10892
11124
|
ctx.log(node.id, `Worktree released: ${worktreePath}`);
|
|
@@ -11412,6 +11644,11 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11412
11644
|
const content = library.getEntryContent?.(libraryRoot, entry);
|
|
11413
11645
|
if (!content || (typeof content === "string" && !content.trim())) continue;
|
|
11414
11646
|
const body = typeof content === "string" ? content.trim() : JSON.stringify(content, null, 2);
|
|
11647
|
+
emitSkillInvokeEvent(skillId, entry.name || skillId, {
|
|
11648
|
+
taskId,
|
|
11649
|
+
executor: ctx.data?.resolvedSdk,
|
|
11650
|
+
source: "library",
|
|
11651
|
+
});
|
|
11415
11652
|
librarySkillParts.push(`### Skill: ${entry.name || skillId} (\`${skillId}\`)`);
|
|
11416
11653
|
librarySkillParts.push(body);
|
|
11417
11654
|
librarySkillParts.push("");
|
|
@@ -11749,6 +11986,21 @@ registerBuiltinNodeType("action.push_branch", {
|
|
|
11749
11986
|
}
|
|
11750
11987
|
}
|
|
11751
11988
|
|
|
11989
|
+
// ── Hard zero-diff guard (always active) ──
|
|
11990
|
+
try {
|
|
11991
|
+
const headSha = execSync("git rev-parse HEAD", {
|
|
11992
|
+
cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
|
|
11993
|
+
}).trim();
|
|
11994
|
+
const mainSha = execSync("git rev-parse origin/main", {
|
|
11995
|
+
cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
|
|
11996
|
+
}).trim();
|
|
11997
|
+
if (headSha && mainSha && headSha === mainSha) {
|
|
11998
|
+
ctx.log(node.id, "HEAD is identical to origin/main — aborting push to prevent PR wipe");
|
|
11999
|
+
ctx.data._pushSkipped = true;
|
|
12000
|
+
return { success: false, error: "HEAD matches origin/main — refusing push", pushed: false };
|
|
12001
|
+
}
|
|
12002
|
+
} catch { /* best-effort */ }
|
|
12003
|
+
|
|
11752
12004
|
// ── Push ──
|
|
11753
12005
|
const pushFlags = [];
|
|
11754
12006
|
if (forceWithLease) pushFlags.push("--force-with-lease");
|