bosun 0.41.2 → 0.41.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 +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- 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 +35 -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/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -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 +28 -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 +338 -84
- 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 +43 -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 +848 -141
- 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 +358 -63
- 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 +44 -11
- 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;
|
|
@@ -2917,11 +2977,19 @@ registerBuiltinNodeType("action.run_command", {
|
|
|
2917
2977
|
type: "object",
|
|
2918
2978
|
properties: {
|
|
2919
2979
|
command: { type: "string", description: "Shell command to run" },
|
|
2980
|
+
args: {
|
|
2981
|
+
description: "Optional argv passed to the command without shell interpolation",
|
|
2982
|
+
oneOf: [
|
|
2983
|
+
{ type: "array", items: { type: ["string", "number", "boolean"] } },
|
|
2984
|
+
{ type: "string" },
|
|
2985
|
+
],
|
|
2986
|
+
},
|
|
2920
2987
|
cwd: { type: "string", description: "Working directory" },
|
|
2921
2988
|
env: { type: "object", description: "Environment variables passed to the command (supports templates)", additionalProperties: true },
|
|
2922
2989
|
timeoutMs: { type: "number", default: 300000 },
|
|
2923
2990
|
shell: { type: "string", default: "auto", enum: ["auto", "bash", "pwsh", "cmd"] },
|
|
2924
2991
|
captureOutput: { type: "boolean", default: true },
|
|
2992
|
+
parseJson: { type: "boolean", default: false, description: "Parse JSON output automatically" },
|
|
2925
2993
|
failOnError: { type: "boolean", default: false, description: "Throw on non-zero exit status (enables workflow retries)" },
|
|
2926
2994
|
},
|
|
2927
2995
|
required: ["command"],
|
|
@@ -2945,28 +3013,63 @@ registerBuiltinNodeType("action.run_command", {
|
|
|
2945
3013
|
}
|
|
2946
3014
|
|
|
2947
3015
|
const timeout = node.config?.timeoutMs || 300000;
|
|
3016
|
+
const resolvedArgsConfig = resolveWorkflowNodeValue(node.config?.args ?? [], ctx);
|
|
3017
|
+
const commandArgs = Array.isArray(resolvedArgsConfig)
|
|
3018
|
+
? resolvedArgsConfig.map((value) => String(value))
|
|
3019
|
+
: typeof resolvedArgsConfig === "string" && resolvedArgsConfig.trim()
|
|
3020
|
+
? [resolvedArgsConfig]
|
|
3021
|
+
: [];
|
|
3022
|
+
const shouldParseJson = node.config?.parseJson === true;
|
|
3023
|
+
const parseOutput = (rawOutput) => {
|
|
3024
|
+
const trimmed = rawOutput?.trim?.() ?? "";
|
|
3025
|
+
if (!shouldParseJson || !trimmed) return trimmed;
|
|
3026
|
+
try {
|
|
3027
|
+
return JSON.parse(trimmed);
|
|
3028
|
+
} catch {
|
|
3029
|
+
const lines = String(trimmed)
|
|
3030
|
+
.split(/\r?\n/)
|
|
3031
|
+
.map((line) => line.trim())
|
|
3032
|
+
.filter(Boolean);
|
|
3033
|
+
const candidate = lines.length > 0 ? lines[lines.length - 1] : trimmed;
|
|
3034
|
+
try {
|
|
3035
|
+
return JSON.parse(candidate);
|
|
3036
|
+
} catch {
|
|
3037
|
+
return trimmed;
|
|
3038
|
+
}
|
|
3039
|
+
}
|
|
3040
|
+
};
|
|
3041
|
+
const usedArgv = commandArgs.length > 0;
|
|
2948
3042
|
|
|
2949
3043
|
if (command !== resolvedCommand) {
|
|
2950
3044
|
ctx.log(node.id, `Normalized legacy command for portability: ${command}`);
|
|
2951
3045
|
}
|
|
2952
|
-
ctx.log(node.id, `Running: ${command}`);
|
|
3046
|
+
ctx.log(node.id, `Running: ${usedArgv ? `${command} ${commandArgs.join(" ")}`.trim() : command}`);
|
|
2953
3047
|
try {
|
|
2954
|
-
const output =
|
|
2955
|
-
|
|
2956
|
-
|
|
2957
|
-
|
|
2958
|
-
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
3048
|
+
const output = usedArgv
|
|
3049
|
+
? execFileSync(command, commandArgs, {
|
|
3050
|
+
cwd,
|
|
3051
|
+
timeout,
|
|
3052
|
+
encoding: "utf8",
|
|
3053
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
3054
|
+
stdio: node.config?.captureOutput !== false ? "pipe" : "inherit",
|
|
3055
|
+
env: commandEnv,
|
|
3056
|
+
})
|
|
3057
|
+
: execSync(command, {
|
|
3058
|
+
cwd,
|
|
3059
|
+
timeout,
|
|
3060
|
+
encoding: "utf8",
|
|
3061
|
+
maxBuffer: 10 * 1024 * 1024,
|
|
3062
|
+
stdio: node.config?.captureOutput !== false ? "pipe" : "inherit",
|
|
3063
|
+
env: commandEnv,
|
|
3064
|
+
});
|
|
2962
3065
|
ctx.log(node.id, `Command succeeded`);
|
|
2963
|
-
return { success: true, output: output
|
|
3066
|
+
return { success: true, output: parseOutput(output), exitCode: 0 };
|
|
2964
3067
|
} catch (err) {
|
|
2965
3068
|
const output = err.stdout?.toString() || "";
|
|
2966
3069
|
const stderr = err.stderr?.toString() || "";
|
|
2967
3070
|
const result = {
|
|
2968
3071
|
success: false,
|
|
2969
|
-
output,
|
|
3072
|
+
output: parseOutput(output),
|
|
2970
3073
|
stderr,
|
|
2971
3074
|
exitCode: err.status,
|
|
2972
3075
|
error: err.message,
|
|
@@ -3936,7 +4039,7 @@ registerBuiltinNodeType("action.update_task_status", {
|
|
|
3936
4039
|
type: "object",
|
|
3937
4040
|
properties: {
|
|
3938
4041
|
taskId: { type: "string", description: "Task ID (supports {{variables}})" },
|
|
3939
|
-
status: { type: "string", enum: ["todo", "inprogress", "inreview", "done", "archived"] },
|
|
4042
|
+
status: { type: "string", enum: ["todo", "inprogress", "inreview", "done", "blocked", "archived"] },
|
|
3940
4043
|
taskTitle: { type: "string", description: "Optional task title for downstream event payloads" },
|
|
3941
4044
|
previousStatus: { type: "string", description: "Optional explicit previous status" },
|
|
3942
4045
|
workflowEvent: { type: "string", description: "Optional follow-up workflow event to emit after status update" },
|
|
@@ -4213,7 +4316,7 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4213
4316
|
autoMergeMethod: {
|
|
4214
4317
|
type: "string",
|
|
4215
4318
|
enum: ["merge", "squash", "rebase"],
|
|
4216
|
-
default: "
|
|
4319
|
+
default: "merge",
|
|
4217
4320
|
description: "Merge method used with gh pr merge --auto",
|
|
4218
4321
|
},
|
|
4219
4322
|
mergeMethod: {
|
|
@@ -4229,7 +4332,12 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4229
4332
|
async execute(node, ctx) {
|
|
4230
4333
|
const title = ctx.resolve(node.config?.title || "");
|
|
4231
4334
|
const body = ctx.resolve(node.config?.body || "");
|
|
4232
|
-
const
|
|
4335
|
+
const baseInput = ctx.resolve(node.config?.base || node.config?.baseBranch || "main");
|
|
4336
|
+
let base = String(baseInput || "main").trim() || "main";
|
|
4337
|
+
try {
|
|
4338
|
+
base = normalizeBaseBranch(base).branch;
|
|
4339
|
+
} catch {
|
|
4340
|
+
}
|
|
4233
4341
|
const branch = ctx.resolve(node.config?.branch || "");
|
|
4234
4342
|
const repoSlug = String(
|
|
4235
4343
|
ctx.resolve(node.config?.repoSlug || ctx.data?.repoSlug || ctx.data?.repository || ""),
|
|
@@ -4241,11 +4349,11 @@ registerBuiltinNodeType("action.create_pr", {
|
|
|
4241
4349
|
false,
|
|
4242
4350
|
);
|
|
4243
4351
|
const autoMergeMethodRaw = String(
|
|
4244
|
-
ctx.resolve(node.config?.autoMergeMethod || node.config?.mergeMethod || "
|
|
4352
|
+
ctx.resolve(node.config?.autoMergeMethod || node.config?.mergeMethod || process.env.BOSUN_MERGE_METHOD || "merge"),
|
|
4245
4353
|
).trim().toLowerCase();
|
|
4246
4354
|
const autoMergeMethod = ["merge", "squash", "rebase"].includes(autoMergeMethodRaw)
|
|
4247
4355
|
? autoMergeMethodRaw
|
|
4248
|
-
: "
|
|
4356
|
+
: (process.env.BOSUN_MERGE_METHOD || "merge");
|
|
4249
4357
|
const cwd = ctx.resolve(node.config?.cwd || ctx.data?.worktreePath || process.cwd());
|
|
4250
4358
|
|
|
4251
4359
|
// Normalize labels/reviewers to arrays
|
|
@@ -9690,6 +9798,88 @@ function hasUnresolvedGitOperation(worktreePath) {
|
|
|
9690
9798
|
}
|
|
9691
9799
|
}
|
|
9692
9800
|
|
|
9801
|
+
function hasTrackedGitChanges(worktreePath) {
|
|
9802
|
+
if (!worktreePath || !existsSync(worktreePath)) return false;
|
|
9803
|
+
try {
|
|
9804
|
+
const status = execGitArgsSync(
|
|
9805
|
+
["status", "--porcelain", "--untracked-files=no"],
|
|
9806
|
+
{
|
|
9807
|
+
cwd: worktreePath,
|
|
9808
|
+
encoding: "utf8",
|
|
9809
|
+
timeout: 5000,
|
|
9810
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9811
|
+
},
|
|
9812
|
+
).trim();
|
|
9813
|
+
return Boolean(status);
|
|
9814
|
+
} catch {
|
|
9815
|
+
return true;
|
|
9816
|
+
}
|
|
9817
|
+
}
|
|
9818
|
+
|
|
9819
|
+
function countCommitsBehindBase(worktreePath, baseBranch) {
|
|
9820
|
+
if (!worktreePath || !existsSync(worktreePath) || !baseBranch) return 0;
|
|
9821
|
+
try {
|
|
9822
|
+
const counts = execGitArgsSync(
|
|
9823
|
+
["rev-list", "--left-right", "--count", `HEAD...${baseBranch}`],
|
|
9824
|
+
{
|
|
9825
|
+
cwd: worktreePath,
|
|
9826
|
+
encoding: "utf8",
|
|
9827
|
+
timeout: 5000,
|
|
9828
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9829
|
+
},
|
|
9830
|
+
).trim();
|
|
9831
|
+
const match = counts.match(/^(\d+)\s+(\d+)$/);
|
|
9832
|
+
return match ? Number(match[2]) : 0;
|
|
9833
|
+
} catch {
|
|
9834
|
+
return 0;
|
|
9835
|
+
}
|
|
9836
|
+
}
|
|
9837
|
+
|
|
9838
|
+
function refreshManagedWorktreeReuse(
|
|
9839
|
+
nodeId,
|
|
9840
|
+
ctx,
|
|
9841
|
+
repoRoot,
|
|
9842
|
+
worktreePath,
|
|
9843
|
+
baseBranch,
|
|
9844
|
+
baseBranchShort,
|
|
9845
|
+
fetchTimeout,
|
|
9846
|
+
) {
|
|
9847
|
+
if (!existsSync(worktreePath) || shouldSkipGitRefreshForTests()) return existsSync(worktreePath);
|
|
9848
|
+
let refreshError = "";
|
|
9849
|
+
try {
|
|
9850
|
+
execSync(`git pull --rebase origin ${baseBranchShort}`, {
|
|
9851
|
+
cwd: worktreePath,
|
|
9852
|
+
encoding: "utf8",
|
|
9853
|
+
timeout: fetchTimeout,
|
|
9854
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
9855
|
+
});
|
|
9856
|
+
} catch (error) {
|
|
9857
|
+
refreshError = formatExecSyncError(error);
|
|
9858
|
+
}
|
|
9859
|
+
if (!existsSync(worktreePath)) return false;
|
|
9860
|
+
if (hasUnresolvedGitOperation(worktreePath)) {
|
|
9861
|
+
const detail = refreshError ? ` (${refreshError})` : "";
|
|
9862
|
+
ctx.log(
|
|
9863
|
+
nodeId,
|
|
9864
|
+
`Managed worktree refresh left unresolved git state, recreating: ${worktreePath}${detail}`,
|
|
9865
|
+
);
|
|
9866
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
9867
|
+
return false;
|
|
9868
|
+
}
|
|
9869
|
+
const reasons = [];
|
|
9870
|
+
if (hasTrackedGitChanges(worktreePath)) reasons.push("tracked changes after refresh");
|
|
9871
|
+
const behindCount = countCommitsBehindBase(worktreePath, baseBranch);
|
|
9872
|
+
if (behindCount > 0) reasons.push(`${behindCount} commit(s) behind ${baseBranch}`);
|
|
9873
|
+
if (reasons.length === 0) return true;
|
|
9874
|
+
if (refreshError) reasons.unshift(`refresh failed: ${refreshError}`);
|
|
9875
|
+
ctx.log(
|
|
9876
|
+
nodeId,
|
|
9877
|
+
`Managed worktree refresh did not yield a clean up-to-date branch, recreating: ${worktreePath} (${reasons.join("; ")})`,
|
|
9878
|
+
);
|
|
9879
|
+
cleanupBrokenManagedWorktree(repoRoot, worktreePath);
|
|
9880
|
+
return false;
|
|
9881
|
+
}
|
|
9882
|
+
|
|
9693
9883
|
function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
9694
9884
|
if (!worktreePath) return;
|
|
9695
9885
|
const linkedGitDir = resolveGitDirForWorktree(worktreePath);
|
|
@@ -9728,13 +9918,9 @@ function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
|
|
|
9728
9918
|
}
|
|
9729
9919
|
|
|
9730
9920
|
/**
|
|
9731
|
-
* Anti-thrash state —
|
|
9732
|
-
*
|
|
9921
|
+
* Anti-thrash state — imported from transforms.mjs (single source of truth).
|
|
9922
|
+
* Shared between monolithic workflow-nodes.mjs and modular triggers.mjs.
|
|
9733
9923
|
*/
|
|
9734
|
-
const _noCommitCounts = new Map();
|
|
9735
|
-
const _skipUntil = new Map();
|
|
9736
|
-
const _completedWithPR = new Set();
|
|
9737
|
-
const MAX_NO_COMMIT_ATTEMPTS = 3;
|
|
9738
9924
|
const NO_COMMIT_BASE_COOLDOWN_MS = 15 * 60 * 1000; // 15 min
|
|
9739
9925
|
const NO_COMMIT_MAX_COOLDOWN_MS = 2 * 60 * 60 * 1000; // 2 hours
|
|
9740
9926
|
const STRICT_START_GUARD_MISSING_TASK = /^(1|true|yes|on)$/i.test(
|
|
@@ -9795,6 +9981,13 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
9795
9981
|
for (let attempt = 0; attempt <= listRetries; attempt++) {
|
|
9796
9982
|
try {
|
|
9797
9983
|
const kanban = ctx.data?._services?.kanban || engine?.services?.kanban;
|
|
9984
|
+
if (status === "todo") {
|
|
9985
|
+
try {
|
|
9986
|
+
await recoverTimedBlockedWorkflowTasks({ kanban, ctx, node, projectId });
|
|
9987
|
+
} catch (recoveryErr) {
|
|
9988
|
+
ctx.log(node.id, `Blocked task recovery warning: ${recoveryErr?.message || recoveryErr}`);
|
|
9989
|
+
}
|
|
9990
|
+
}
|
|
9798
9991
|
if (kanban?.listTasks) {
|
|
9799
9992
|
tasks = await kanban.listTasks(projectId, { status });
|
|
9800
9993
|
} else {
|
|
@@ -10108,6 +10301,9 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
10108
10301
|
if (baseBranch) ctx.data.baseBranch = baseBranch;
|
|
10109
10302
|
const branch = deriveTaskBranch(primaryTask);
|
|
10110
10303
|
if (branch) ctx.data.branch = branch;
|
|
10304
|
+
ctx.data.taskMeta = primaryTask?.meta && typeof primaryTask.meta === "object"
|
|
10305
|
+
? { ...primaryTask.meta }
|
|
10306
|
+
: {};
|
|
10111
10307
|
}
|
|
10112
10308
|
|
|
10113
10309
|
ctx.log(node.id, `Found ${toDispatch.length} task(s) ready (${remaining} slot(s) free)`);
|
|
@@ -10479,8 +10675,26 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10479
10675
|
|| ctx.data?.agentProfile
|
|
10480
10676
|
|| "",
|
|
10481
10677
|
).trim();
|
|
10678
|
+
const taskText = [task.title, task.description].filter(Boolean).join("\n");
|
|
10679
|
+
const inferredResolutionTags = [];
|
|
10680
|
+
if (/\btest(?:s|ing)?\b/i.test(taskText)) inferredResolutionTags.push("test", "tests");
|
|
10681
|
+
if (/\b(?:ci|cd|pipeline|workflow|github actions?)\b/i.test(taskText)) inferredResolutionTags.push("ci", "cd", "pipeline");
|
|
10682
|
+
if (/\b(?:merge conflict|conflicts|rebase|cherry-pick)\b/i.test(taskText)) inferredResolutionTags.push("conflict", "merge");
|
|
10683
|
+
if (/\b(?:implement|implementation|feature|build|ship)\b/i.test(taskText)) inferredResolutionTags.push("implementation");
|
|
10684
|
+
if (/\b(?:docs?|documentation|readme)\b/i.test(taskText)) inferredResolutionTags.push("docs", "documentation");
|
|
10685
|
+
const resolutionTags = Array.from(new Set([
|
|
10686
|
+
...task.tags,
|
|
10687
|
+
...inferredResolutionTags,
|
|
10688
|
+
String(ctx.data?.task?.type || "").trim(),
|
|
10689
|
+
String(ctx.data?.task?.agentType || "").trim(),
|
|
10690
|
+
String(ctx.data?.task?.assignedAgentType || "").trim(),
|
|
10691
|
+
String(ctx.data?.agentType || "").trim(),
|
|
10692
|
+
String(ctx.data?.assignedAgentType || "").trim(),
|
|
10693
|
+
].map((value) => String(value || "").trim()).filter(Boolean)));
|
|
10482
10694
|
let profileDecision = null;
|
|
10483
10695
|
let configuredExecutorPreference = null;
|
|
10696
|
+
ctx.data.resolvedSkillIds = [];
|
|
10697
|
+
ctx.data.resolvedLibraryPlan = null;
|
|
10484
10698
|
|
|
10485
10699
|
// Check env var overrides (mirrors TaskExecutor behavior)
|
|
10486
10700
|
const envModel =
|
|
@@ -10497,25 +10711,30 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10497
10711
|
|
|
10498
10712
|
try {
|
|
10499
10713
|
const library = await ensureLibraryManagerMod();
|
|
10500
|
-
const
|
|
10714
|
+
const criteria = {
|
|
10715
|
+
title: task.title,
|
|
10716
|
+
description: task.description,
|
|
10717
|
+
tags: resolutionTags,
|
|
10718
|
+
agentType: ctx.data?.task?.agentType || ctx.data?.agentType || "",
|
|
10719
|
+
repoRoot,
|
|
10720
|
+
changedFiles: Array.isArray(ctx.data?.changedFiles) ? ctx.data.changedFiles : [],
|
|
10721
|
+
};
|
|
10722
|
+
const planResult = library.resolveLibraryPlan?.(
|
|
10501
10723
|
repoRoot,
|
|
10724
|
+
criteria,
|
|
10502
10725
|
{
|
|
10503
|
-
|
|
10504
|
-
|
|
10505
|
-
tags: task.tags,
|
|
10506
|
-
agentType: ctx.data?.task?.agentType || ctx.data?.agentType || "",
|
|
10507
|
-
repoRoot,
|
|
10726
|
+
topN: Math.max(10, requestedAgentProfileId ? 25 : 10),
|
|
10727
|
+
skillTopN: 5,
|
|
10508
10728
|
},
|
|
10509
|
-
{ topN: Math.max(10, requestedAgentProfileId ? 25 : 10) },
|
|
10510
10729
|
);
|
|
10511
|
-
const candidates = Array.isArray(
|
|
10512
|
-
const bestCandidate =
|
|
10513
|
-
const autoMinScore = Number(
|
|
10730
|
+
const candidates = Array.isArray(planResult?.candidates) ? planResult.candidates : [];
|
|
10731
|
+
const bestCandidate = planResult?.best || null;
|
|
10732
|
+
const autoMinScore = Number(planResult?.auto?.thresholds?.minScore || 12);
|
|
10514
10733
|
const scoreQualified = Number(bestCandidate?.score || 0) >= autoMinScore;
|
|
10515
10734
|
const matchedCandidate = requestedAgentProfileId
|
|
10516
10735
|
? candidates.find((candidate) => String(candidate?.id || "").trim() === requestedAgentProfileId) || null
|
|
10517
|
-
: ((
|
|
10518
|
-
if (!requestedAgentProfileId && bestCandidate && !
|
|
10736
|
+
: ((planResult?.auto?.shouldAutoApply || scoreQualified) ? bestCandidate : null);
|
|
10737
|
+
if (!requestedAgentProfileId && bestCandidate && !planResult?.auto?.shouldAutoApply) {
|
|
10519
10738
|
ctx.log(
|
|
10520
10739
|
node.id,
|
|
10521
10740
|
`Profile match below auto threshold; ignoring candidate ${String(bestCandidate.id || "unknown")}`,
|
|
@@ -10534,13 +10753,19 @@ registerBuiltinNodeType("action.resolve_executor", {
|
|
|
10534
10753
|
ctx.data.agentProfile = profileId;
|
|
10535
10754
|
ctx.data.resolvedAgentProfile = {
|
|
10536
10755
|
id: profileId,
|
|
10537
|
-
name:
|
|
10756
|
+
name: matchedCandidate?.name || profile?.name || profileId,
|
|
10538
10757
|
...profile,
|
|
10539
10758
|
};
|
|
10540
|
-
const
|
|
10541
|
-
?
|
|
10542
|
-
:
|
|
10759
|
+
const resolvedPlan = planResult?.plan && planResult.plan.agentProfileId === profileId
|
|
10760
|
+
? planResult.plan
|
|
10761
|
+
: null;
|
|
10762
|
+
const skillIds = resolvedPlan && Array.isArray(resolvedPlan.skillIds)
|
|
10763
|
+
? resolvedPlan.skillIds.map((value) => String(value || "").trim()).filter(Boolean)
|
|
10764
|
+
: Array.isArray(profile.skills)
|
|
10765
|
+
? profile.skills.map((value) => String(value || "").trim()).filter(Boolean)
|
|
10766
|
+
: [];
|
|
10543
10767
|
ctx.data.resolvedSkillIds = skillIds;
|
|
10768
|
+
ctx.data.resolvedLibraryPlan = resolvedPlan;
|
|
10544
10769
|
}
|
|
10545
10770
|
} catch (err) {
|
|
10546
10771
|
ctx.log(node.id, `Library profile resolution failed: ${err.message}`);
|
|
@@ -10739,22 +10964,15 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10739
10964
|
}
|
|
10740
10965
|
|
|
10741
10966
|
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
|
-
}
|
|
10967
|
+
refreshManagedWorktreeReuse(
|
|
10968
|
+
node.id,
|
|
10969
|
+
ctx,
|
|
10970
|
+
repoRoot,
|
|
10971
|
+
worktreePath,
|
|
10972
|
+
baseBranch,
|
|
10973
|
+
baseBranchShort,
|
|
10974
|
+
fetchTimeout,
|
|
10975
|
+
);
|
|
10758
10976
|
if (existsSync(worktreePath)) {
|
|
10759
10977
|
ctx.data.worktreePath = worktreePath;
|
|
10760
10978
|
ctx.data._worktreeCreated = false;
|
|
@@ -10767,6 +10985,7 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10767
10985
|
}
|
|
10768
10986
|
|
|
10769
10987
|
// Create fresh worktree
|
|
10988
|
+
let attachedExistingBranch = false;
|
|
10770
10989
|
try {
|
|
10771
10990
|
execSync(
|
|
10772
10991
|
`git worktree add "${worktreePath}" -b "${branch}" "${baseBranch}" 2>&1`,
|
|
@@ -10782,21 +11001,39 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10782
11001
|
`git worktree add "${worktreePath}" "${branch}" 2>&1`,
|
|
10783
11002
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
10784
11003
|
);
|
|
11004
|
+
attachedExistingBranch = true;
|
|
10785
11005
|
} catch (reuseErr) {
|
|
10786
11006
|
const existingBranchWorktree = findExistingWorktreePathForBranch(repoRoot, branch);
|
|
10787
11007
|
if (existingBranchWorktree && existsSync(existingBranchWorktree)) {
|
|
10788
|
-
|
|
10789
|
-
|
|
10790
|
-
|
|
11008
|
+
const existingWorktreeIsBroken = (
|
|
11009
|
+
!isValidGitWorktreePath(existingBranchWorktree) ||
|
|
11010
|
+
hasUnresolvedGitOperation(existingBranchWorktree)
|
|
11011
|
+
) && isManagedBosunWorktree(existingBranchWorktree, repoRoot);
|
|
11012
|
+
if (existingWorktreeIsBroken) {
|
|
10791
11013
|
ctx.log(
|
|
10792
11014
|
node.id,
|
|
10793
|
-
`Existing branch worktree is invalid, recreating managed path: ${existingBranchWorktree}`,
|
|
11015
|
+
`Existing branch worktree is invalid or unresolved, recreating managed path: ${existingBranchWorktree}`,
|
|
10794
11016
|
);
|
|
10795
11017
|
cleanupBrokenManagedWorktree(repoRoot, existingBranchWorktree);
|
|
10796
11018
|
}
|
|
10797
11019
|
}
|
|
10798
11020
|
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
10799
|
-
isValidGitWorktreePath(existingBranchWorktree)
|
|
11021
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
11022
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
11023
|
+
) {
|
|
11024
|
+
refreshManagedWorktreeReuse(
|
|
11025
|
+
node.id,
|
|
11026
|
+
ctx,
|
|
11027
|
+
repoRoot,
|
|
11028
|
+
existingBranchWorktree,
|
|
11029
|
+
baseBranch,
|
|
11030
|
+
baseBranchShort,
|
|
11031
|
+
fetchTimeout,
|
|
11032
|
+
);
|
|
11033
|
+
}
|
|
11034
|
+
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
11035
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
11036
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
10800
11037
|
) {
|
|
10801
11038
|
ctx.data.worktreePath = existingBranchWorktree;
|
|
10802
11039
|
ctx.data._worktreeCreated = false;
|
|
@@ -10820,6 +11057,22 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10820
11057
|
);
|
|
10821
11058
|
}
|
|
10822
11059
|
}
|
|
11060
|
+
if (attachedExistingBranch) {
|
|
11061
|
+
refreshManagedWorktreeReuse(
|
|
11062
|
+
node.id,
|
|
11063
|
+
ctx,
|
|
11064
|
+
repoRoot,
|
|
11065
|
+
worktreePath,
|
|
11066
|
+
baseBranch,
|
|
11067
|
+
baseBranchShort,
|
|
11068
|
+
fetchTimeout,
|
|
11069
|
+
);
|
|
11070
|
+
if (!existsSync(worktreePath)) {
|
|
11071
|
+
throw new Error(
|
|
11072
|
+
`Worktree refresh failed for existing branch ${branch}; managed worktree was removed after stale refresh state`,
|
|
11073
|
+
);
|
|
11074
|
+
}
|
|
11075
|
+
}
|
|
10823
11076
|
fixGitConfigCorruption(repoRoot);
|
|
10824
11077
|
const cleared3 = clearBlockedWorktreeIdentity(worktreePath);
|
|
10825
11078
|
if (cleared3) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
|
|
@@ -10830,8 +11083,28 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10830
11083
|
ctx.log(node.id, `Worktree created: ${worktreePath} (branch: ${branch}, base: ${baseBranch})`);
|
|
10831
11084
|
return { success: true, worktreePath, created: true, branch, baseBranch };
|
|
10832
11085
|
} catch (err) {
|
|
10833
|
-
|
|
10834
|
-
|
|
11086
|
+
const errorMessage = String(err?.message || err || "worktree_acquisition_failed");
|
|
11087
|
+
const retryable = !/managed worktree was removed after stale refresh state/i.test(errorMessage);
|
|
11088
|
+
const recordedAt = new Date().toISOString();
|
|
11089
|
+
const autoRecoverDelayMs = retryable ? 0 : getNonRetryableWorktreeRecoveryMs();
|
|
11090
|
+
const retryAt = retryable ? null : new Date(Date.now() + autoRecoverDelayMs).toISOString();
|
|
11091
|
+
const blockedReason = retryable
|
|
11092
|
+
? errorMessage
|
|
11093
|
+
: "Managed worktree refresh conflict detected; Bosun will retry automatically after cooldown.";
|
|
11094
|
+
ctx.log(node.id, `Worktree acquisition failed: ${errorMessage}`);
|
|
11095
|
+
return {
|
|
11096
|
+
success: false,
|
|
11097
|
+
error: errorMessage,
|
|
11098
|
+
branch,
|
|
11099
|
+
baseBranch,
|
|
11100
|
+
retryable,
|
|
11101
|
+
failureKind: retryable ? "worktree_acquisition_failed" : "branch_refresh_conflict",
|
|
11102
|
+
recordedAt,
|
|
11103
|
+
autoRecoverDelayMs,
|
|
11104
|
+
retryAt,
|
|
11105
|
+
blockedReason,
|
|
11106
|
+
recoveryNote: retryable || !retryAt ? "" : ` — blocked until ${retryAt}`,
|
|
11107
|
+
};
|
|
10835
11108
|
}
|
|
10836
11109
|
},
|
|
10837
11110
|
});
|
|
@@ -10887,6 +11160,8 @@ registerBuiltinNodeType("action.release_worktree", {
|
|
|
10887
11160
|
} catch { /* best-effort */ }
|
|
10888
11161
|
}
|
|
10889
11162
|
|
|
11163
|
+
fixGitConfigCorruption(repoRoot);
|
|
11164
|
+
|
|
10890
11165
|
ctx.data._worktreeCreated = false;
|
|
10891
11166
|
ctx.data._worktreeManaged = false;
|
|
10892
11167
|
ctx.log(node.id, `Worktree released: ${worktreePath}`);
|
|
@@ -11412,6 +11687,11 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11412
11687
|
const content = library.getEntryContent?.(libraryRoot, entry);
|
|
11413
11688
|
if (!content || (typeof content === "string" && !content.trim())) continue;
|
|
11414
11689
|
const body = typeof content === "string" ? content.trim() : JSON.stringify(content, null, 2);
|
|
11690
|
+
emitSkillInvokeEvent(skillId, entry.name || skillId, {
|
|
11691
|
+
taskId,
|
|
11692
|
+
executor: ctx.data?.resolvedSdk,
|
|
11693
|
+
source: "library",
|
|
11694
|
+
});
|
|
11415
11695
|
librarySkillParts.push(`### Skill: ${entry.name || skillId} (\`${skillId}\`)`);
|
|
11416
11696
|
librarySkillParts.push(body);
|
|
11417
11697
|
librarySkillParts.push("");
|
|
@@ -11749,6 +12029,21 @@ registerBuiltinNodeType("action.push_branch", {
|
|
|
11749
12029
|
}
|
|
11750
12030
|
}
|
|
11751
12031
|
|
|
12032
|
+
// ── Hard zero-diff guard (always active) ──
|
|
12033
|
+
try {
|
|
12034
|
+
const headSha = execSync("git rev-parse HEAD", {
|
|
12035
|
+
cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
|
|
12036
|
+
}).trim();
|
|
12037
|
+
const mainSha = execSync("git rev-parse origin/main", {
|
|
12038
|
+
cwd: worktreePath, encoding: "utf8", timeout: 5_000, stdio: ["pipe", "pipe", "pipe"],
|
|
12039
|
+
}).trim();
|
|
12040
|
+
if (headSha && mainSha && headSha === mainSha) {
|
|
12041
|
+
ctx.log(node.id, "HEAD is identical to origin/main — aborting push to prevent PR wipe");
|
|
12042
|
+
ctx.data._pushSkipped = true;
|
|
12043
|
+
return { success: false, error: "HEAD matches origin/main — refusing push", pushed: false };
|
|
12044
|
+
}
|
|
12045
|
+
} catch { /* best-effort */ }
|
|
12046
|
+
|
|
11752
12047
|
// ── Push ──
|
|
11753
12048
|
const pushFlags = [];
|
|
11754
12049
|
if (forceWithLease) pushFlags.push("--force-with-lease");
|