bosun 0.41.7 → 0.41.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -1
- package/agent/agent-event-bus.mjs +31 -2
- package/agent/agent-pool.mjs +251 -11
- package/agent/agent-prompts.mjs +5 -1
- package/agent/agent-supervisor.mjs +22 -0
- package/agent/primary-agent.mjs +115 -5
- package/cli.mjs +3 -2
- package/config/config.mjs +4 -1
- package/desktop/main.mjs +350 -25
- package/desktop/preload.cjs +8 -0
- package/desktop/preload.mjs +19 -0
- package/entrypoint.mjs +332 -0
- package/infra/health-status.mjs +72 -0
- package/infra/library-manager.mjs +58 -1
- package/infra/maintenance.mjs +1 -2
- package/infra/monitor.mjs +25 -7
- package/infra/session-tracker.mjs +30 -3
- package/package.json +10 -4
- package/server/bosun-mcp-server.mjs +1004 -0
- package/server/setup-web-server.mjs +287 -258
- package/server/ui-server.mjs +218 -23
- package/shell/claude-shell.mjs +14 -1
- package/shell/codex-model-profiles.mjs +166 -29
- package/shell/codex-shell.mjs +56 -18
- package/shell/opencode-providers.mjs +20 -8
- package/task/task-executor.mjs +28 -0
- package/task/task-store.mjs +13 -4
- package/tools/list-todos.mjs +7 -1
- package/ui/app.js +3 -2
- package/ui/components/agent-selector.js +127 -0
- package/ui/components/session-list.js +2 -0
- package/ui/demo-defaults.js +6 -6
- package/ui/modules/router.js +2 -0
- package/ui/modules/state.js +13 -5
- package/ui/tabs/chat.js +3 -0
- package/ui/tabs/library.js +284 -52
- package/ui/tabs/tasks.js +5 -13
- package/workflow/workflow-engine.mjs +16 -4
- package/workflow/workflow-nodes/definitions.mjs +37 -0
- package/workflow/workflow-nodes.mjs +489 -153
- package/workflow/workflow-templates.mjs +0 -5
- package/workflow-templates/github.mjs +106 -16
- package/workspace/worktree-manager.mjs +1 -1
|
@@ -48,6 +48,7 @@ import { resolveAutoCommand } from "./project-detection.mjs";
|
|
|
48
48
|
import { loadConfig } from "../config/config.mjs";
|
|
49
49
|
import { fixGitConfigCorruption } from "../workspace/worktree-manager.mjs";
|
|
50
50
|
import { clearBlockedWorktreeIdentity, normalizeBaseBranch } from "../git/git-safety.mjs";
|
|
51
|
+
import { getBosunCoAuthorTrailer, shouldAddBosunCoAuthor } from "../git/git-commit-helpers.mjs";
|
|
51
52
|
import { getGitHubToken, invalidateTokenType } from "../github/github-auth-manager.mjs";
|
|
52
53
|
import {
|
|
53
54
|
CUSTOM_NODE_DIR_NAME,
|
|
@@ -58,6 +59,8 @@ import {
|
|
|
58
59
|
stopCustomNodeDiscovery,
|
|
59
60
|
} from "./workflow-nodes/custom-loader.mjs";
|
|
60
61
|
|
|
62
|
+
// CLAUDE:SUMMARY — workflow-nodes
|
|
63
|
+
// Registers built-in workflow node types and shared prompt/runtime actions for Bosun workflows.
|
|
61
64
|
const TAG = "[workflow-nodes]";
|
|
62
65
|
let customLoadPromise = null;
|
|
63
66
|
let customDiscoveryStarted = false;
|
|
@@ -966,6 +969,43 @@ async function createKanbanTaskWithProject(kanban, taskData = {}, projectIdValue
|
|
|
966
969
|
payload.projectId = resolvedProjectId;
|
|
967
970
|
}
|
|
968
971
|
|
|
972
|
+
const createTaskParamNames = (() => {
|
|
973
|
+
try {
|
|
974
|
+
const inspectTarget =
|
|
975
|
+
typeof kanban.createTask?.getMockImplementation === "function"
|
|
976
|
+
? kanban.createTask.getMockImplementation() || kanban.createTask
|
|
977
|
+
: kanban.createTask;
|
|
978
|
+
const source = Function.prototype.toString.call(inspectTarget);
|
|
979
|
+
const parenMatch = source.match(/^[^(]*\(([^)]*)\)/s);
|
|
980
|
+
if (parenMatch) {
|
|
981
|
+
return String(parenMatch[1] || "")
|
|
982
|
+
.split(",")
|
|
983
|
+
.map((entry) =>
|
|
984
|
+
String(entry || "")
|
|
985
|
+
.trim()
|
|
986
|
+
.replace(/^\.{3}/, "")
|
|
987
|
+
.replace(/\s*=.*$/s, "")
|
|
988
|
+
.trim(),
|
|
989
|
+
)
|
|
990
|
+
.filter(Boolean);
|
|
991
|
+
}
|
|
992
|
+
const arrowMatch = source.match(/^(?:async\s+)?([A-Za-z_$][\w$]*)\s*=>/);
|
|
993
|
+
if (arrowMatch?.[1]) return [arrowMatch[1]];
|
|
994
|
+
} catch {
|
|
995
|
+
// Fall back to the project-aware signature when adapter source is opaque.
|
|
996
|
+
}
|
|
997
|
+
return [];
|
|
998
|
+
})();
|
|
999
|
+
const firstParamName = String(createTaskParamNames[0] || "").toLowerCase();
|
|
1000
|
+
const payloadOnlyCreateTask =
|
|
1001
|
+
createTaskParamNames.length === 1 &&
|
|
1002
|
+
/(task|payload|spec|data)/i.test(firstParamName) &&
|
|
1003
|
+
!/project/i.test(firstParamName);
|
|
1004
|
+
|
|
1005
|
+
if (payloadOnlyCreateTask) {
|
|
1006
|
+
return kanban.createTask(payload);
|
|
1007
|
+
}
|
|
1008
|
+
|
|
969
1009
|
const taskPayload = { ...payload };
|
|
970
1010
|
delete taskPayload.projectId;
|
|
971
1011
|
return kanban.createTask(resolvedProjectId, taskPayload);
|
|
@@ -1368,7 +1408,7 @@ async function recoverTimedBlockedWorkflowTasks({ kanban, ctx, node, projectId }
|
|
|
1368
1408
|
const autoRecovery = task?.meta?.autoRecovery;
|
|
1369
1409
|
if (!autoRecovery || typeof autoRecovery !== "object") continue;
|
|
1370
1410
|
if (autoRecovery.active === false) continue;
|
|
1371
|
-
|
|
1411
|
+
// Accept any auto-recovery reason (worktree_failure, consecutive_errors, etc.)
|
|
1372
1412
|
const retryAtMs = Date.parse(String(autoRecovery.retryAt || task?.cooldownUntil || ""));
|
|
1373
1413
|
if (!Number.isFinite(retryAtMs) || retryAtMs > nowMs) continue;
|
|
1374
1414
|
await kanban.updateTask(task.id, {
|
|
@@ -2305,7 +2345,9 @@ registerBuiltinNodeType("action.run_agent", {
|
|
|
2305
2345
|
const timeoutMs = Number.isFinite(Number(resolvedTimeoutMs))
|
|
2306
2346
|
? Math.max(1000, Math.trunc(Number(resolvedTimeoutMs)))
|
|
2307
2347
|
: 3600000;
|
|
2308
|
-
const includeTaskContext =
|
|
2348
|
+
const includeTaskContext =
|
|
2349
|
+
node.config?.includeTaskContext !== false &&
|
|
2350
|
+
ctx.data?._taskIncludeContext !== false;
|
|
2309
2351
|
const configuredSystemPrompt =
|
|
2310
2352
|
ctx.resolve(node.config?.systemPrompt || "") ||
|
|
2311
2353
|
ctx.data?._taskSystemPrompt ||
|
|
@@ -2316,7 +2358,10 @@ registerBuiltinNodeType("action.run_agent", {
|
|
|
2316
2358
|
.filter(Boolean)
|
|
2317
2359
|
.join("\n\n");
|
|
2318
2360
|
let finalPrompt = prompt;
|
|
2319
|
-
|
|
2361
|
+
const promptHasTaskContext =
|
|
2362
|
+
ctx.data?._taskPromptIncludesTaskContext === true ||
|
|
2363
|
+
String(finalPrompt || "").includes("## Task Context");
|
|
2364
|
+
if (includeTaskContext && !promptHasTaskContext) {
|
|
2320
2365
|
const explicitContext =
|
|
2321
2366
|
ctx.data?.taskContext ||
|
|
2322
2367
|
ctx.data?.taskContextBlock ||
|
|
@@ -8243,10 +8288,34 @@ const BOSUN_FUNCTION_REGISTRY = Object.freeze({
|
|
|
8243
8288
|
async invoke(args, ctx) {
|
|
8244
8289
|
const cwd = args.cwd || ctx.data?.worktreePath || ctx.data?.repoRoot || process.cwd();
|
|
8245
8290
|
try {
|
|
8246
|
-
const
|
|
8247
|
-
|
|
8248
|
-
|
|
8249
|
-
|
|
8291
|
+
const lines = execFileSync("git", ["for-each-ref", "--format=%(HEAD)|%(refname:short)", "refs/heads"], {
|
|
8292
|
+
encoding: "utf8",
|
|
8293
|
+
cwd,
|
|
8294
|
+
timeout: 4000,
|
|
8295
|
+
stdio: "pipe",
|
|
8296
|
+
})
|
|
8297
|
+
.trim()
|
|
8298
|
+
.split("\n")
|
|
8299
|
+
.map((line) => line.trim())
|
|
8300
|
+
.filter(Boolean);
|
|
8301
|
+
const branches = [];
|
|
8302
|
+
let current = "";
|
|
8303
|
+
for (const line of lines) {
|
|
8304
|
+
const [headMarker, ...rest] = line.split("|");
|
|
8305
|
+
const branchName = rest.join("|").trim();
|
|
8306
|
+
if (!branchName) continue;
|
|
8307
|
+
branches.push(branchName);
|
|
8308
|
+
if (headMarker === "*") current = branchName;
|
|
8309
|
+
}
|
|
8310
|
+
if (!current) {
|
|
8311
|
+
current = execFileSync("git", ["branch", "--show-current"], {
|
|
8312
|
+
encoding: "utf8",
|
|
8313
|
+
cwd,
|
|
8314
|
+
timeout: 2000,
|
|
8315
|
+
stdio: "pipe",
|
|
8316
|
+
}).trim();
|
|
8317
|
+
}
|
|
8318
|
+
return { current, branches, branchCount: branches.length };
|
|
8250
8319
|
} catch (err) {
|
|
8251
8320
|
return { current: "", branches: [], branchCount: 0, error: err.message };
|
|
8252
8321
|
}
|
|
@@ -9429,6 +9498,7 @@ registerBuiltinNodeType("transform.mcp_extract", {
|
|
|
9429
9498
|
/** Module-scope lazy caches for task lifecycle imports. */
|
|
9430
9499
|
let _taskClaimsMod = null;
|
|
9431
9500
|
let _taskClaimsInitPromise = null;
|
|
9501
|
+
let _taskClaimsInitRepoRoot = "";
|
|
9432
9502
|
let _taskComplexityMod = null;
|
|
9433
9503
|
let _kanbanAdapterMod = null;
|
|
9434
9504
|
let _agentPoolMod = null;
|
|
@@ -9436,11 +9506,27 @@ let _libraryManagerMod = null;
|
|
|
9436
9506
|
let _configMod = null;
|
|
9437
9507
|
let _gitSafetyMod = null;
|
|
9438
9508
|
let _diffStatsMod = null;
|
|
9509
|
+
let _sharedStateManagerMod = null;
|
|
9510
|
+
const SHARED_STATE_ACTIVE_STALE_THRESHOLD_MS =
|
|
9511
|
+
Number(process.env.SHARED_STATE_STALE_THRESHOLD_MS) || 300_000;
|
|
9512
|
+
const TERMINAL_SHARED_STATE_STATUSES = new Set([
|
|
9513
|
+
"complete",
|
|
9514
|
+
"completed",
|
|
9515
|
+
"failed",
|
|
9516
|
+
"abandoned",
|
|
9517
|
+
"released",
|
|
9518
|
+
]);
|
|
9439
9519
|
|
|
9440
9520
|
async function ensureTaskClaimsMod() {
|
|
9441
9521
|
if (!_taskClaimsMod) _taskClaimsMod = await import("../task/task-claims.mjs");
|
|
9442
9522
|
return _taskClaimsMod;
|
|
9443
9523
|
}
|
|
9524
|
+
async function ensureSharedStateManagerMod() {
|
|
9525
|
+
if (!_sharedStateManagerMod) {
|
|
9526
|
+
_sharedStateManagerMod = await import("../workspace/shared-state-manager.mjs");
|
|
9527
|
+
}
|
|
9528
|
+
return _sharedStateManagerMod;
|
|
9529
|
+
}
|
|
9444
9530
|
function pickTaskString(...values) {
|
|
9445
9531
|
for (const value of values) {
|
|
9446
9532
|
const normalized = String(value || "").trim();
|
|
@@ -9512,21 +9598,91 @@ function resolveTaskRepositoryRoot(taskRepository, currentRepoRoot) {
|
|
|
9512
9598
|
}
|
|
9513
9599
|
return "";
|
|
9514
9600
|
}
|
|
9515
|
-
|
|
9601
|
+
function sameResolvedPath(leftPath, rightPath) {
|
|
9602
|
+
const left = resolve(String(leftPath || ""));
|
|
9603
|
+
const right = resolve(String(rightPath || ""));
|
|
9604
|
+
if (process.platform === "win32") {
|
|
9605
|
+
return left.toLowerCase() === right.toLowerCase();
|
|
9606
|
+
}
|
|
9607
|
+
return left === right;
|
|
9608
|
+
}
|
|
9609
|
+
async function ensureTaskClaimsInitialized(ctx, claims, explicitRepoRoot = "") {
|
|
9516
9610
|
if (typeof claims?.initTaskClaims !== "function") return;
|
|
9517
|
-
|
|
9518
|
-
|
|
9519
|
-
|
|
9520
|
-
|
|
9521
|
-
|
|
9522
|
-
|
|
9611
|
+
const requestedRepoRoot = pickTaskString(
|
|
9612
|
+
explicitRepoRoot,
|
|
9613
|
+
ctx?.data?.repoRoot,
|
|
9614
|
+
ctx?.data?.workspace,
|
|
9615
|
+
process.cwd(),
|
|
9616
|
+
);
|
|
9617
|
+
const repoRoot =
|
|
9618
|
+
resolveTaskRepositoryRoot("", requestedRepoRoot)
|
|
9619
|
+
|| requestedRepoRoot
|
|
9620
|
+
|| process.cwd();
|
|
9621
|
+
if (!_taskClaimsInitPromise || !sameResolvedPath(_taskClaimsInitRepoRoot, repoRoot)) {
|
|
9622
|
+
_taskClaimsInitRepoRoot = repoRoot;
|
|
9523
9623
|
_taskClaimsInitPromise = claims.initTaskClaims({ repoRoot }).catch((err) => {
|
|
9524
9624
|
_taskClaimsInitPromise = null;
|
|
9625
|
+
_taskClaimsInitRepoRoot = "";
|
|
9525
9626
|
throw err;
|
|
9526
9627
|
});
|
|
9527
9628
|
}
|
|
9528
9629
|
await _taskClaimsInitPromise;
|
|
9529
9630
|
}
|
|
9631
|
+
function isSharedStateOwnershipActive(state, now = Date.now()) {
|
|
9632
|
+
if (!state || typeof state !== "object") return false;
|
|
9633
|
+
const ownerId = pickTaskString(state.ownerId, state.owner_id);
|
|
9634
|
+
if (!ownerId) return false;
|
|
9635
|
+
const attemptStatus = pickTaskString(state.attemptStatus, state.attempt_status).toLowerCase();
|
|
9636
|
+
if (attemptStatus && TERMINAL_SHARED_STATE_STATUSES.has(attemptStatus)) return false;
|
|
9637
|
+
const heartbeatText = pickTaskString(state.ownerHeartbeat, state.owner_heartbeat);
|
|
9638
|
+
const heartbeatMs = Date.parse(heartbeatText);
|
|
9639
|
+
if (!Number.isFinite(heartbeatMs)) return false;
|
|
9640
|
+
if (now - heartbeatMs > SHARED_STATE_ACTIVE_STALE_THRESHOLD_MS) return false;
|
|
9641
|
+
return true;
|
|
9642
|
+
}
|
|
9643
|
+
async function getPersistedOwnedTaskIds(node, ctx) {
|
|
9644
|
+
const requestedRepoRoot = pickTaskString(
|
|
9645
|
+
cfgOrCtx(node, ctx, "repoRoot"),
|
|
9646
|
+
ctx?.data?.repoRoot,
|
|
9647
|
+
ctx?.data?.workspace,
|
|
9648
|
+
process.cwd(),
|
|
9649
|
+
);
|
|
9650
|
+
const repoRoot =
|
|
9651
|
+
resolveTaskRepositoryRoot("", requestedRepoRoot)
|
|
9652
|
+
|| requestedRepoRoot
|
|
9653
|
+
|| process.cwd();
|
|
9654
|
+
const activeTaskIds = new Set();
|
|
9655
|
+
try {
|
|
9656
|
+
const claims = await ensureTaskClaimsMod();
|
|
9657
|
+
await ensureTaskClaimsInitialized(ctx, claims, repoRoot);
|
|
9658
|
+
if (typeof claims.listClaims === "function") {
|
|
9659
|
+
const persistedClaims = await claims.listClaims();
|
|
9660
|
+
for (const claim of persistedClaims || []) {
|
|
9661
|
+
const taskId = pickTaskString(claim?.task_id, claim?.taskId);
|
|
9662
|
+
if (taskId) activeTaskIds.add(taskId);
|
|
9663
|
+
}
|
|
9664
|
+
}
|
|
9665
|
+
} catch (err) {
|
|
9666
|
+
ctx?.log?.(node.id, `Persisted claim filter warning: ${err?.message || err}`);
|
|
9667
|
+
}
|
|
9668
|
+
try {
|
|
9669
|
+
const sharedStateManager = await ensureSharedStateManagerMod();
|
|
9670
|
+
if (typeof sharedStateManager.getAllSharedStates === "function") {
|
|
9671
|
+
const sharedStates = await sharedStateManager.getAllSharedStates(repoRoot);
|
|
9672
|
+
const now = Date.now();
|
|
9673
|
+
for (const [rawTaskId, state] of Object.entries(sharedStates || {})) {
|
|
9674
|
+
const taskId = pickTaskString(state?.taskId, state?.task_id, rawTaskId);
|
|
9675
|
+
if (!taskId) continue;
|
|
9676
|
+
if (isSharedStateOwnershipActive(state, now)) {
|
|
9677
|
+
activeTaskIds.add(taskId);
|
|
9678
|
+
}
|
|
9679
|
+
}
|
|
9680
|
+
}
|
|
9681
|
+
} catch (err) {
|
|
9682
|
+
ctx?.log?.(node.id, `Shared state filter warning: ${err?.message || err}`);
|
|
9683
|
+
}
|
|
9684
|
+
return activeTaskIds;
|
|
9685
|
+
}
|
|
9530
9686
|
async function ensureTaskComplexityMod() {
|
|
9531
9687
|
if (!_taskComplexityMod) _taskComplexityMod = await import("../task/task-complexity.mjs");
|
|
9532
9688
|
return _taskComplexityMod;
|
|
@@ -9877,7 +10033,7 @@ function refreshManagedWorktreeReuse(
|
|
|
9877
10033
|
if (!existsSync(worktreePath) || shouldSkipGitRefreshForTests()) return existsSync(worktreePath);
|
|
9878
10034
|
let refreshError = "";
|
|
9879
10035
|
try {
|
|
9880
|
-
|
|
10036
|
+
execGitArgsSync(["pull", "--rebase", "origin", baseBranchShort], {
|
|
9881
10037
|
cwd: worktreePath,
|
|
9882
10038
|
encoding: "utf8",
|
|
9883
10039
|
timeout: fetchTimeout,
|
|
@@ -10078,8 +10234,32 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
10078
10234
|
return true;
|
|
10079
10235
|
});
|
|
10080
10236
|
|
|
10237
|
+
let persistedOwnershipFilteredCount = 0;
|
|
10238
|
+
if (status === "todo" && tasks.length > 0) {
|
|
10239
|
+
const persistedOwnedTaskIds = await getPersistedOwnedTaskIds(node, ctx);
|
|
10240
|
+
if (persistedOwnedTaskIds.size > 0) {
|
|
10241
|
+
const beforeFilterCount = tasks.length;
|
|
10242
|
+
tasks = tasks.filter((task) => {
|
|
10243
|
+
const taskId = pickTaskString(task?.id, task?.task_id);
|
|
10244
|
+
return taskId && !persistedOwnedTaskIds.has(taskId);
|
|
10245
|
+
});
|
|
10246
|
+
persistedOwnershipFilteredCount = beforeFilterCount - tasks.length;
|
|
10247
|
+
if (persistedOwnershipFilteredCount > 0) {
|
|
10248
|
+
ctx.log(
|
|
10249
|
+
node.id,
|
|
10250
|
+
`Persisted ownership filtered ${persistedOwnershipFilteredCount} task(s) with live claims/shared state`,
|
|
10251
|
+
);
|
|
10252
|
+
}
|
|
10253
|
+
}
|
|
10254
|
+
}
|
|
10255
|
+
|
|
10081
10256
|
if (tasks.length === 0) {
|
|
10082
|
-
return {
|
|
10257
|
+
return {
|
|
10258
|
+
triggered: false,
|
|
10259
|
+
reason: "all_filtered",
|
|
10260
|
+
taskCount: 0,
|
|
10261
|
+
persistedOwnershipFilteredCount,
|
|
10262
|
+
};
|
|
10083
10263
|
}
|
|
10084
10264
|
|
|
10085
10265
|
let benchmarkMode = null;
|
|
@@ -10343,6 +10523,7 @@ registerBuiltinNodeType("trigger.task_available", {
|
|
|
10343
10523
|
taskCount: toDispatch.length,
|
|
10344
10524
|
availableSlots: remaining,
|
|
10345
10525
|
selectedTaskId: primaryTask ? pickTaskString(primaryTask.id, primaryTask.task_id) : "",
|
|
10526
|
+
persistedOwnershipFilteredCount,
|
|
10346
10527
|
auditEvents: startGuardAuditEvents,
|
|
10347
10528
|
benchmarkMode,
|
|
10348
10529
|
};
|
|
@@ -10956,7 +11137,7 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10956
11137
|
const baseBranchShort = baseBranch.replace(/^origin\//, "");
|
|
10957
11138
|
if (!shouldSkipGitRefreshForTests()) {
|
|
10958
11139
|
try {
|
|
10959
|
-
|
|
11140
|
+
execGitArgsSync(["fetch", "origin", baseBranchShort, "--no-tags"], {
|
|
10960
11141
|
cwd: repoRoot, encoding: "utf8",
|
|
10961
11142
|
timeout: fetchTimeout,
|
|
10962
11143
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -10973,7 +11154,7 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
10973
11154
|
|
|
10974
11155
|
// Ensure long paths are enabled for this repo before checkout.
|
|
10975
11156
|
try {
|
|
10976
|
-
|
|
11157
|
+
execGitArgsSync(["config", "--local", "core.longpaths", "true"], {
|
|
10977
11158
|
cwd: repoRoot,
|
|
10978
11159
|
encoding: "utf8",
|
|
10979
11160
|
timeout: 5000,
|
|
@@ -11017,18 +11198,66 @@ registerBuiltinNodeType("action.acquire_worktree", {
|
|
|
11017
11198
|
// Create fresh worktree
|
|
11018
11199
|
let attachedExistingBranch = false;
|
|
11019
11200
|
try {
|
|
11020
|
-
|
|
11021
|
-
|
|
11201
|
+
execGitArgsSync(
|
|
11202
|
+
["worktree", "add", worktreePath, "-b", branch, baseBranch],
|
|
11022
11203
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
11023
11204
|
);
|
|
11024
11205
|
} catch (createErr) {
|
|
11025
11206
|
if (!isExistingBranchWorktreeError(createErr)) {
|
|
11026
11207
|
throw new Error(`Worktree creation failed: ${formatExecSyncError(createErr)}`);
|
|
11027
11208
|
}
|
|
11209
|
+
const existingBranchWorktree = findExistingWorktreePathForBranch(repoRoot, branch);
|
|
11210
|
+
if (existingBranchWorktree && existsSync(existingBranchWorktree)) {
|
|
11211
|
+
const existingWorktreeIsBroken = (
|
|
11212
|
+
!isValidGitWorktreePath(existingBranchWorktree) ||
|
|
11213
|
+
hasUnresolvedGitOperation(existingBranchWorktree)
|
|
11214
|
+
) && isManagedBosunWorktree(existingBranchWorktree, repoRoot);
|
|
11215
|
+
if (existingWorktreeIsBroken) {
|
|
11216
|
+
ctx.log(
|
|
11217
|
+
node.id,
|
|
11218
|
+
`Existing branch worktree is invalid or unresolved, recreating managed path: ${existingBranchWorktree}`,
|
|
11219
|
+
);
|
|
11220
|
+
cleanupBrokenManagedWorktree(repoRoot, existingBranchWorktree);
|
|
11221
|
+
}
|
|
11222
|
+
}
|
|
11223
|
+
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
11224
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
11225
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
11226
|
+
) {
|
|
11227
|
+
refreshManagedWorktreeReuse(
|
|
11228
|
+
node.id,
|
|
11229
|
+
ctx,
|
|
11230
|
+
repoRoot,
|
|
11231
|
+
existingBranchWorktree,
|
|
11232
|
+
baseBranch,
|
|
11233
|
+
baseBranchShort,
|
|
11234
|
+
fetchTimeout,
|
|
11235
|
+
);
|
|
11236
|
+
}
|
|
11237
|
+
if (existingBranchWorktree && existsSync(existingBranchWorktree) &&
|
|
11238
|
+
isValidGitWorktreePath(existingBranchWorktree) &&
|
|
11239
|
+
!hasUnresolvedGitOperation(existingBranchWorktree)
|
|
11240
|
+
) {
|
|
11241
|
+
ctx.data.worktreePath = existingBranchWorktree;
|
|
11242
|
+
ctx.data._worktreeCreated = false;
|
|
11243
|
+
ctx.data._worktreeManaged = true;
|
|
11244
|
+
ctx.log(node.id, `Reusing existing branch worktree: ${existingBranchWorktree}`);
|
|
11245
|
+
const cleared2 = clearBlockedWorktreeIdentity(existingBranchWorktree);
|
|
11246
|
+
if (cleared2) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${existingBranchWorktree}`);
|
|
11247
|
+
return {
|
|
11248
|
+
success: true,
|
|
11249
|
+
worktreePath: existingBranchWorktree,
|
|
11250
|
+
created: false,
|
|
11251
|
+
reused: true,
|
|
11252
|
+
reusedExistingBranch: true,
|
|
11253
|
+
branch,
|
|
11254
|
+
baseBranch,
|
|
11255
|
+
};
|
|
11256
|
+
}
|
|
11028
11257
|
// Branch already exists — attach worktree to existing branch.
|
|
11029
11258
|
try {
|
|
11030
|
-
|
|
11031
|
-
|
|
11259
|
+
execGitArgsSync(
|
|
11260
|
+
["worktree", "add", worktreePath, branch],
|
|
11032
11261
|
{ cwd: repoRoot, encoding: "utf8", timeout: worktreeTimeout },
|
|
11033
11262
|
);
|
|
11034
11263
|
attachedExistingBranch = true;
|
|
@@ -11171,22 +11400,22 @@ registerBuiltinNodeType("action.release_worktree", {
|
|
|
11171
11400
|
}
|
|
11172
11401
|
|
|
11173
11402
|
try {
|
|
11174
|
-
|
|
11175
|
-
|
|
11176
|
-
|
|
11177
|
-
|
|
11178
|
-
|
|
11179
|
-
|
|
11403
|
+
if (existsSync(worktreePath)) {
|
|
11404
|
+
try {
|
|
11405
|
+
execGitArgsSync(["worktree", "remove", String(worktreePath), "--force"], {
|
|
11406
|
+
cwd: repoRoot, encoding: "utf8", timeout: removeTimeout,
|
|
11407
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
11408
|
+
});
|
|
11180
11409
|
} catch {
|
|
11181
11410
|
/* best-effort — directory might already be gone */
|
|
11182
11411
|
}
|
|
11183
11412
|
}
|
|
11184
11413
|
|
|
11185
|
-
|
|
11186
|
-
|
|
11187
|
-
|
|
11188
|
-
|
|
11189
|
-
|
|
11414
|
+
if (shouldPrune) {
|
|
11415
|
+
try {
|
|
11416
|
+
execGitArgsSync(["worktree", "prune"], {
|
|
11417
|
+
cwd: repoRoot, encoding: "utf8", timeout: 15000,
|
|
11418
|
+
});
|
|
11190
11419
|
} catch { /* best-effort */ }
|
|
11191
11420
|
}
|
|
11192
11421
|
|
|
@@ -11380,6 +11609,7 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11380
11609
|
retryReason: { type: "string", description: "Reason for retry (if retrying)" },
|
|
11381
11610
|
includeAgentsMd: { type: "boolean", default: true },
|
|
11382
11611
|
includeComments: { type: "boolean", default: true },
|
|
11612
|
+
includeGitContext: { type: "boolean", default: true },
|
|
11383
11613
|
includeStatusEndpoint: { type: "boolean", default: true },
|
|
11384
11614
|
promptTemplate: { type: "string", description: "Custom template (overrides)" },
|
|
11385
11615
|
},
|
|
@@ -11396,7 +11626,10 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11396
11626
|
const repoSlug = cfgOrCtx(node, ctx, "repoSlug");
|
|
11397
11627
|
const retryReason = cfgOrCtx(node, ctx, "retryReason");
|
|
11398
11628
|
const includeAgentsMd = node.config?.includeAgentsMd !== false;
|
|
11629
|
+
const includeComments = node.config?.includeComments !== false;
|
|
11630
|
+
const includeGitContext = node.config?.includeGitContext !== false;
|
|
11399
11631
|
const includeStatusEndpoint = node.config?.includeStatusEndpoint !== false;
|
|
11632
|
+
ctx.data._taskIncludeContext = includeComments;
|
|
11400
11633
|
const customTemplate = cfgOrCtx(node, ctx, "promptTemplate");
|
|
11401
11634
|
const taskPayload =
|
|
11402
11635
|
ctx.data?.task && typeof ctx.data.task === "object"
|
|
@@ -11407,9 +11640,19 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11407
11640
|
? taskPayload.meta
|
|
11408
11641
|
: null;
|
|
11409
11642
|
|
|
11643
|
+
const TASK_TEMPLATE_PLACEHOLDER_RE = /^\{\{\s*[\w.-]+\s*\}\}$/;
|
|
11644
|
+
const TASK_PROMPT_INVALID_VALUES = new Set([
|
|
11645
|
+
"internal server error",
|
|
11646
|
+
"{\"ok\":false,\"error\":\"internal server error\"}",
|
|
11647
|
+
"{\"error\":\"internal server error\"}",
|
|
11648
|
+
]);
|
|
11410
11649
|
const normalizeString = (value) => {
|
|
11411
11650
|
if (value == null) return "";
|
|
11412
|
-
|
|
11651
|
+
const text = String(value).trim();
|
|
11652
|
+
if (!text) return "";
|
|
11653
|
+
if (TASK_TEMPLATE_PLACEHOLDER_RE.test(text)) return "";
|
|
11654
|
+
if (TASK_PROMPT_INVALID_VALUES.has(text.toLowerCase())) return "";
|
|
11655
|
+
return text;
|
|
11413
11656
|
};
|
|
11414
11657
|
const pickFirstString = (...values) => {
|
|
11415
11658
|
for (const value of values) {
|
|
@@ -11454,6 +11697,32 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11454
11697
|
if (ctxValue != null && ctxValue !== "") return ctxValue;
|
|
11455
11698
|
return null;
|
|
11456
11699
|
};
|
|
11700
|
+
const normalizedTaskId = pickFirstString(
|
|
11701
|
+
resolvePromptValue("taskId"),
|
|
11702
|
+
taskPayload?.id,
|
|
11703
|
+
taskPayload?.taskId,
|
|
11704
|
+
taskMeta?.taskId,
|
|
11705
|
+
taskId,
|
|
11706
|
+
);
|
|
11707
|
+
const normalizedTaskTitle = pickFirstString(
|
|
11708
|
+
resolvePromptValue("taskTitle"),
|
|
11709
|
+
taskPayload?.title,
|
|
11710
|
+
taskMeta?.taskTitle,
|
|
11711
|
+
taskTitle,
|
|
11712
|
+
) || (normalizedTaskId ? `Task ${normalizedTaskId}` : "Untitled task");
|
|
11713
|
+
const normalizedTaskDescription = pickFirstString(
|
|
11714
|
+
resolvePromptValue("taskDescription"),
|
|
11715
|
+
taskPayload?.description,
|
|
11716
|
+
taskPayload?.body,
|
|
11717
|
+
taskMeta?.taskDescription,
|
|
11718
|
+
taskDescription,
|
|
11719
|
+
);
|
|
11720
|
+
const normalizedBranch = normalizeString(branch);
|
|
11721
|
+
const normalizedBaseBranch = normalizeString(baseBranch);
|
|
11722
|
+
const normalizedWorktreePath = normalizeString(worktreePath);
|
|
11723
|
+
const normalizedRepoRoot = normalizeString(repoRoot) || process.cwd();
|
|
11724
|
+
const normalizedRepoSlug = normalizeString(repoSlug);
|
|
11725
|
+
const normalizedRetryReason = normalizeString(retryReason);
|
|
11457
11726
|
const workspace = pickFirstString(
|
|
11458
11727
|
resolvePromptValue("workspace"),
|
|
11459
11728
|
taskPayload?.workspace,
|
|
@@ -11470,76 +11739,69 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11470
11739
|
taskPayload?.repositories,
|
|
11471
11740
|
taskMeta?.repositories,
|
|
11472
11741
|
);
|
|
11473
|
-
const primaryRepository = pickFirstString(repository,
|
|
11742
|
+
const primaryRepository = pickFirstString(repository, normalizedRepoSlug);
|
|
11474
11743
|
const allowedRepositories = normalizeStringArray(repositories, primaryRepository);
|
|
11475
|
-
const matchedSkills = findRelevantSkills(
|
|
11744
|
+
const matchedSkills = findRelevantSkills(
|
|
11745
|
+
normalizedRepoRoot,
|
|
11746
|
+
normalizedTaskTitle,
|
|
11747
|
+
normalizedTaskDescription || "",
|
|
11748
|
+
{},
|
|
11749
|
+
);
|
|
11476
11750
|
const activeSkillFiles = matchedSkills.map((skill) => skill.filename);
|
|
11477
11751
|
const strictCacheAnchoring =
|
|
11478
11752
|
String(process.env.BOSUN_CACHE_ANCHOR_MODE || "")
|
|
11479
11753
|
.trim()
|
|
11480
11754
|
.toLowerCase() === "strict";
|
|
11755
|
+
const customTemplateValues = {
|
|
11756
|
+
taskId: normalizedTaskId,
|
|
11757
|
+
taskTitle: normalizedTaskTitle,
|
|
11758
|
+
taskDescription: normalizedTaskDescription,
|
|
11759
|
+
branch: normalizedBranch,
|
|
11760
|
+
baseBranch: normalizedBaseBranch,
|
|
11761
|
+
worktreePath: normalizedWorktreePath,
|
|
11762
|
+
repoRoot: normalizedRepoRoot,
|
|
11763
|
+
repoSlug: normalizedRepoSlug,
|
|
11764
|
+
workspace,
|
|
11765
|
+
repository: primaryRepository,
|
|
11766
|
+
repositories: allowedRepositories.join(", "),
|
|
11767
|
+
retryReason: normalizedRetryReason,
|
|
11768
|
+
};
|
|
11769
|
+
const renderCustomTemplate = (template) => {
|
|
11770
|
+
const lookup = new Map();
|
|
11771
|
+
const register = (key, value) => {
|
|
11772
|
+
const normalizedKey = String(key || "").trim();
|
|
11773
|
+
if (!normalizedKey) return;
|
|
11774
|
+
const normalizedValue = normalizeString(value);
|
|
11775
|
+
lookup.set(normalizedKey, normalizedValue);
|
|
11776
|
+
lookup.set(normalizedKey.toLowerCase(), normalizedValue);
|
|
11777
|
+
lookup.set(normalizedKey.toUpperCase(), normalizedValue);
|
|
11778
|
+
};
|
|
11779
|
+
for (const [key, value] of Object.entries(customTemplateValues)) {
|
|
11780
|
+
register(key, value);
|
|
11781
|
+
register(key.replace(/([a-z0-9])([A-Z])/g, "$1_$2"), value);
|
|
11782
|
+
}
|
|
11783
|
+
return String(template || "")
|
|
11784
|
+
.replace(/\{\{\s*([A-Za-z0-9_.-]+)\s*\}\}/g, (_full, key) => {
|
|
11785
|
+
const lookupKey = String(key || "").trim();
|
|
11786
|
+
if (!lookupKey) return "";
|
|
11787
|
+
if (lookup.has(lookupKey)) return lookup.get(lookupKey);
|
|
11788
|
+
if (lookup.has(lookupKey.toLowerCase())) return lookup.get(lookupKey.toLowerCase());
|
|
11789
|
+
if (lookup.has(lookupKey.toUpperCase())) return lookup.get(lookupKey.toUpperCase());
|
|
11790
|
+
return "";
|
|
11791
|
+
})
|
|
11792
|
+
.split("\n")
|
|
11793
|
+
.map((line) => line.replace(/[ \t]+$/g, ""))
|
|
11794
|
+
.join("\n")
|
|
11795
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
11796
|
+
.trim();
|
|
11797
|
+
};
|
|
11481
11798
|
|
|
11482
11799
|
const buildStableSystemPrompt = () => {
|
|
11483
11800
|
const systemParts = [];
|
|
11484
|
-
|
|
11485
|
-
|
|
11486
|
-
const docFiles = ["AGENTS.md", ".github/copilot-instructions.md"];
|
|
11487
|
-
const loaded = new Set();
|
|
11488
|
-
for (const dir of searchDirs) {
|
|
11489
|
-
for (const doc of docFiles) {
|
|
11490
|
-
if (loaded.has(doc)) continue;
|
|
11491
|
-
const fullPath = resolve(dir, doc);
|
|
11492
|
-
try {
|
|
11493
|
-
if (!existsSync(fullPath)) continue;
|
|
11494
|
-
const content = readFileSync(fullPath, "utf8").trim();
|
|
11495
|
-
if (!content || content.length <= 10) continue;
|
|
11496
|
-
loaded.add(doc);
|
|
11497
|
-
systemParts.push(`## ${doc}`);
|
|
11498
|
-
systemParts.push(content);
|
|
11499
|
-
systemParts.push("");
|
|
11500
|
-
} catch {
|
|
11501
|
-
// best-effort only
|
|
11502
|
-
}
|
|
11503
|
-
}
|
|
11504
|
-
}
|
|
11505
|
-
}
|
|
11506
|
-
|
|
11507
|
-
if (includeStatusEndpoint) {
|
|
11508
|
-
const port = process.env.AGENT_ENDPOINT_PORT || process.env.BOSUN_AGENT_ENDPOINT_PORT || "";
|
|
11509
|
-
if (port) {
|
|
11510
|
-
systemParts.push("## Agent Status Endpoint");
|
|
11511
|
-
systemParts.push(`POST http://127.0.0.1:${port}/status — Report progress`);
|
|
11512
|
-
systemParts.push(`POST http://127.0.0.1:${port}/heartbeat — Heartbeat ping`);
|
|
11513
|
-
systemParts.push(`POST http://127.0.0.1:${port}/error — Report errors`);
|
|
11514
|
-
systemParts.push(`POST http://127.0.0.1:${port}/complete — Signal completion`);
|
|
11515
|
-
systemParts.push("");
|
|
11516
|
-
}
|
|
11517
|
-
}
|
|
11518
|
-
|
|
11519
|
-
systemParts.push("## Tool Discovery");
|
|
11520
|
-
systemParts.push(
|
|
11521
|
-
"Bosun uses a compact MCP discovery layer for external MCP servers and the custom tool library.",
|
|
11522
|
-
);
|
|
11523
|
-
systemParts.push(
|
|
11524
|
-
"Preferred flow: `search` -> `get_schema` -> `execute`.",
|
|
11525
|
-
);
|
|
11526
|
-
systemParts.push(
|
|
11527
|
-
"Only eager tools are preloaded below to keep context small. Use `call_discovered_tool` only as a direct fallback when orchestration code is unnecessary.",
|
|
11528
|
-
);
|
|
11801
|
+
systemParts.push("You are an autonomous software engineering agent inside the Bosun orchestrator.");
|
|
11802
|
+
systemParts.push("Follow the project guidance provided in the user message and execute tasks end-to-end.");
|
|
11529
11803
|
systemParts.push("");
|
|
11530
11804
|
|
|
11531
|
-
const eagerToolBlock = getToolsPromptBlock(repoRoot, {
|
|
11532
|
-
includeBuiltins: true,
|
|
11533
|
-
eagerOnly: true,
|
|
11534
|
-
discoveryMode: true,
|
|
11535
|
-
emitReflectHint: true,
|
|
11536
|
-
limit: 12,
|
|
11537
|
-
});
|
|
11538
|
-
if (eagerToolBlock) {
|
|
11539
|
-
systemParts.push(eagerToolBlock);
|
|
11540
|
-
systemParts.push("");
|
|
11541
|
-
}
|
|
11542
|
-
|
|
11543
11805
|
systemParts.push("## Instructions");
|
|
11544
11806
|
systemParts.push(
|
|
11545
11807
|
"1. Follow the project instructions in AGENTS.md.\n" +
|
|
@@ -11550,23 +11812,78 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11550
11812
|
"6. Never ask for user input — you are autonomous.\n" +
|
|
11551
11813
|
"7. Use all available tools to verify your work.",
|
|
11552
11814
|
);
|
|
11553
|
-
systemParts.push("");
|
|
11554
|
-
systemParts.push("## Git Attribution");
|
|
11555
|
-
systemParts.push("Add this trailer to all commits:");
|
|
11556
|
-
systemParts.push("Co-authored-by: bosun[bot] <bosun@virtengine.com>");
|
|
11557
11815
|
return systemParts.join("\n").trim();
|
|
11558
11816
|
};
|
|
11559
11817
|
|
|
11818
|
+
const assertStableSystemPrompt = (candidate) => {
|
|
11819
|
+
if (!strictCacheAnchoring) return;
|
|
11820
|
+
const dynamicMarkers = [
|
|
11821
|
+
normalizedTaskId,
|
|
11822
|
+
normalizedTaskTitle,
|
|
11823
|
+
normalizedTaskDescription,
|
|
11824
|
+
normalizedRetryReason,
|
|
11825
|
+
normalizedBranch,
|
|
11826
|
+
normalizedBaseBranch,
|
|
11827
|
+
normalizedWorktreePath,
|
|
11828
|
+
]
|
|
11829
|
+
.map((value) => String(value || "").trim())
|
|
11830
|
+
.filter(Boolean);
|
|
11831
|
+
const leaked = dynamicMarkers.find((marker) => candidate.includes(marker));
|
|
11832
|
+
if (leaked) {
|
|
11833
|
+
throw new Error(
|
|
11834
|
+
`BOSUN_CACHE_ANCHOR_MODE=strict violation: system prompt leaked task-specific marker "${leaked}"`,
|
|
11835
|
+
);
|
|
11836
|
+
}
|
|
11837
|
+
};
|
|
11838
|
+
|
|
11839
|
+
const buildGitContextBlock = async () => {
|
|
11840
|
+
if (!includeGitContext) return "";
|
|
11841
|
+
const root = normalizedWorktreePath || normalizedRepoRoot;
|
|
11842
|
+
if (!root) return "";
|
|
11843
|
+
if (!existsSync(resolve(root, ".git"))) return "";
|
|
11844
|
+
|
|
11845
|
+
try {
|
|
11846
|
+
const diffStatsMod = await ensureDiffStatsMod();
|
|
11847
|
+
const commits =
|
|
11848
|
+
diffStatsMod.getRecentCommits?.(root, 8) || [];
|
|
11849
|
+
let diffSummary =
|
|
11850
|
+
diffStatsMod.getCompactDiffSummary?.(root, {
|
|
11851
|
+
baseBranch: normalizedBaseBranch || "origin/main",
|
|
11852
|
+
}) || "";
|
|
11853
|
+
|
|
11854
|
+
if (diffSummary && diffSummary.length > 2000) {
|
|
11855
|
+
diffSummary = `${diffSummary.slice(0, 2000)}…`;
|
|
11856
|
+
}
|
|
11857
|
+
|
|
11858
|
+
const lines = ["## Git Context"];
|
|
11859
|
+
if (Array.isArray(commits) && commits.length > 0) {
|
|
11860
|
+
lines.push("### Recent Commits");
|
|
11861
|
+
for (const commit of commits) lines.push(`- ${commit}`);
|
|
11862
|
+
}
|
|
11863
|
+
if (diffSummary && diffSummary !== "(no diff stats available)") {
|
|
11864
|
+
lines.push("### Diff Summary");
|
|
11865
|
+
lines.push("```");
|
|
11866
|
+
lines.push(diffSummary);
|
|
11867
|
+
lines.push("```");
|
|
11868
|
+
}
|
|
11869
|
+
return lines.length > 1 ? lines.join("\n") : "";
|
|
11870
|
+
} catch {
|
|
11871
|
+
return "";
|
|
11872
|
+
}
|
|
11873
|
+
};
|
|
11874
|
+
|
|
11560
11875
|
if (customTemplate) {
|
|
11876
|
+
const renderedTemplate = renderCustomTemplate(customTemplate);
|
|
11561
11877
|
const stableSystemPrompt = buildStableSystemPrompt();
|
|
11562
|
-
|
|
11563
|
-
ctx.data.
|
|
11878
|
+
assertStableSystemPrompt(stableSystemPrompt);
|
|
11879
|
+
ctx.data._taskPrompt = renderedTemplate;
|
|
11880
|
+
ctx.data._taskUserPrompt = renderedTemplate;
|
|
11564
11881
|
ctx.data._taskSystemPrompt = stableSystemPrompt;
|
|
11565
|
-
ctx.log(node.id, `Prompt from custom template (${
|
|
11882
|
+
ctx.log(node.id, `Prompt from custom template (${renderedTemplate.length} chars)`);
|
|
11566
11883
|
return {
|
|
11567
11884
|
success: true,
|
|
11568
|
-
prompt:
|
|
11569
|
-
userPrompt:
|
|
11885
|
+
prompt: renderedTemplate,
|
|
11886
|
+
userPrompt: renderedTemplate,
|
|
11570
11887
|
systemPrompt: stableSystemPrompt,
|
|
11571
11888
|
source: "custom",
|
|
11572
11889
|
};
|
|
@@ -11575,33 +11892,48 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11575
11892
|
const userParts = [];
|
|
11576
11893
|
|
|
11577
11894
|
// Header
|
|
11578
|
-
userParts.push(`# Task: ${
|
|
11579
|
-
if (
|
|
11895
|
+
userParts.push(`# Task: ${normalizedTaskTitle}`);
|
|
11896
|
+
if (normalizedTaskId) userParts.push(`Task ID: ${normalizedTaskId}`);
|
|
11580
11897
|
userParts.push("");
|
|
11581
11898
|
|
|
11582
11899
|
// Retry context (if applicable)
|
|
11583
|
-
if (
|
|
11900
|
+
if (normalizedRetryReason) {
|
|
11584
11901
|
userParts.push("## Retry Context");
|
|
11585
|
-
userParts.push(`Previous attempt failed: ${
|
|
11902
|
+
userParts.push(`Previous attempt failed: ${normalizedRetryReason}`);
|
|
11586
11903
|
userParts.push("Try a different approach this time.");
|
|
11587
11904
|
userParts.push("");
|
|
11588
11905
|
}
|
|
11589
11906
|
|
|
11590
11907
|
// Description
|
|
11591
|
-
if (
|
|
11908
|
+
if (normalizedTaskDescription) {
|
|
11592
11909
|
userParts.push("## Description");
|
|
11593
|
-
userParts.push(
|
|
11910
|
+
userParts.push(normalizedTaskDescription);
|
|
11911
|
+
userParts.push("");
|
|
11912
|
+
}
|
|
11913
|
+
|
|
11914
|
+
if (includeComments) {
|
|
11915
|
+
const taskContextBlock = buildTaskContextBlock(taskPayload);
|
|
11916
|
+
if (taskContextBlock) {
|
|
11917
|
+
userParts.push(taskContextBlock);
|
|
11918
|
+
userParts.push("");
|
|
11919
|
+
ctx.data._taskPromptIncludesTaskContext = true;
|
|
11920
|
+
}
|
|
11921
|
+
}
|
|
11922
|
+
|
|
11923
|
+
const gitContextBlock = await buildGitContextBlock();
|
|
11924
|
+
if (gitContextBlock) {
|
|
11925
|
+
userParts.push(gitContextBlock);
|
|
11594
11926
|
userParts.push("");
|
|
11595
11927
|
}
|
|
11596
11928
|
|
|
11597
11929
|
// Environment context
|
|
11598
11930
|
userParts.push("## Environment");
|
|
11599
11931
|
const envLines = [];
|
|
11600
|
-
if (
|
|
11601
|
-
if (
|
|
11602
|
-
if (
|
|
11603
|
-
if (
|
|
11604
|
-
if (
|
|
11932
|
+
if (normalizedWorktreePath) envLines.push(`- **Working Directory:** ${normalizedWorktreePath}`);
|
|
11933
|
+
if (normalizedBranch) envLines.push(`- **Branch:** ${normalizedBranch}`);
|
|
11934
|
+
if (normalizedBaseBranch) envLines.push(`- **Base Branch:** ${normalizedBaseBranch}`);
|
|
11935
|
+
if (normalizedRepoSlug) envLines.push(`- **Repository:** ${normalizedRepoSlug}`);
|
|
11936
|
+
if (normalizedRepoRoot) envLines.push(`- **Repo Root:** ${normalizedRepoRoot}`);
|
|
11605
11937
|
if (envLines.length) userParts.push(envLines.join("\n"));
|
|
11606
11938
|
userParts.push("");
|
|
11607
11939
|
|
|
@@ -11617,11 +11949,11 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11617
11949
|
} else {
|
|
11618
11950
|
userParts.push("- **Allowed Repositories:** (not declared)");
|
|
11619
11951
|
}
|
|
11620
|
-
if (
|
|
11952
|
+
if (normalizedWorktreePath) userParts.push(`- **Write Scope Root:** ${normalizedWorktreePath}`);
|
|
11621
11953
|
userParts.push("");
|
|
11622
11954
|
userParts.push("Hard boundaries:");
|
|
11623
|
-
if (
|
|
11624
|
-
userParts.push(`1. Modify files only inside \`${
|
|
11955
|
+
if (normalizedWorktreePath) {
|
|
11956
|
+
userParts.push(`1. Modify files only inside \`${normalizedWorktreePath}\`.`);
|
|
11625
11957
|
} else {
|
|
11626
11958
|
userParts.push("1. Modify files only inside the active repository working directory.");
|
|
11627
11959
|
}
|
|
@@ -11653,7 +11985,7 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11653
11985
|
|
|
11654
11986
|
// AGENTS.md + copilot-instructions.md
|
|
11655
11987
|
if (includeAgentsMd) {
|
|
11656
|
-
const searchDirs = [
|
|
11988
|
+
const searchDirs = [normalizedWorktreePath || normalizedRepoRoot, normalizedRepoRoot].filter(Boolean);
|
|
11657
11989
|
const docFiles = ["AGENTS.md", ".github/copilot-instructions.md"];
|
|
11658
11990
|
const loaded = new Set();
|
|
11659
11991
|
for (const dir of searchDirs) {
|
|
@@ -11688,10 +12020,36 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11688
12020
|
}
|
|
11689
12021
|
}
|
|
11690
12022
|
|
|
12023
|
+
userParts.push("## Tool Discovery");
|
|
12024
|
+
userParts.push(
|
|
12025
|
+
"Bosun uses a compact MCP discovery layer for external MCP servers and the custom tool library.",
|
|
12026
|
+
);
|
|
12027
|
+
userParts.push(
|
|
12028
|
+
"Preferred flow: `search` -> `get_schema` -> `execute`.",
|
|
12029
|
+
);
|
|
12030
|
+
userParts.push(
|
|
12031
|
+
"Only eager tools are preloaded below to keep context small. Use `call_discovered_tool` only as a direct fallback when orchestration code is unnecessary.",
|
|
12032
|
+
);
|
|
12033
|
+
userParts.push("");
|
|
12034
|
+
|
|
12035
|
+
// Skill-driven eager tools belong with task context to preserve cache anchoring.
|
|
12036
|
+
const taskScopedEagerTools = getToolsPromptBlock(normalizedRepoRoot, {
|
|
12037
|
+
activeSkills: activeSkillFiles,
|
|
12038
|
+
includeBuiltins: true,
|
|
12039
|
+
eagerOnly: true,
|
|
12040
|
+
discoveryMode: true,
|
|
12041
|
+
emitReflectHint: true,
|
|
12042
|
+
limit: 12,
|
|
12043
|
+
});
|
|
12044
|
+
if (taskScopedEagerTools) {
|
|
12045
|
+
userParts.push(taskScopedEagerTools);
|
|
12046
|
+
userParts.push("");
|
|
12047
|
+
}
|
|
12048
|
+
|
|
11691
12049
|
const relevantSkillsBlock = buildRelevantSkillsPromptBlock(
|
|
11692
|
-
|
|
11693
|
-
|
|
11694
|
-
|
|
12050
|
+
normalizedRepoRoot,
|
|
12051
|
+
normalizedTaskTitle,
|
|
12052
|
+
normalizedTaskDescription || "",
|
|
11695
12053
|
{},
|
|
11696
12054
|
);
|
|
11697
12055
|
if (relevantSkillsBlock) {
|
|
@@ -11707,7 +12065,7 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11707
12065
|
if (librarySkillIds.length > 0) {
|
|
11708
12066
|
try {
|
|
11709
12067
|
const library = await ensureLibraryManagerMod();
|
|
11710
|
-
const libraryRoot =
|
|
12068
|
+
const libraryRoot = normalizedRepoRoot || process.cwd();
|
|
11711
12069
|
const fsSkillNames = new Set(matchedSkills.map((s) => String(s.filename || "").replace(/\.md$/i, "").toLowerCase()));
|
|
11712
12070
|
const librarySkillParts = [];
|
|
11713
12071
|
for (const skillId of librarySkillIds) {
|
|
@@ -11718,7 +12076,7 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11718
12076
|
if (!content || (typeof content === "string" && !content.trim())) continue;
|
|
11719
12077
|
const body = typeof content === "string" ? content.trim() : JSON.stringify(content, null, 2);
|
|
11720
12078
|
emitSkillInvokeEvent(skillId, entry.name || skillId, {
|
|
11721
|
-
taskId,
|
|
12079
|
+
taskId: normalizedTaskId,
|
|
11722
12080
|
executor: ctx.data?.resolvedSdk,
|
|
11723
12081
|
source: "library",
|
|
11724
12082
|
});
|
|
@@ -11734,42 +12092,20 @@ registerBuiltinNodeType("action.build_task_prompt", {
|
|
|
11734
12092
|
ctx.log(node.id, `Library skill injection failed (non-fatal): ${err.message}`);
|
|
11735
12093
|
}
|
|
11736
12094
|
}
|
|
11737
|
-
|
|
11738
|
-
const
|
|
11739
|
-
|
|
11740
|
-
|
|
11741
|
-
|
|
11742
|
-
|
|
11743
|
-
|
|
11744
|
-
|
|
11745
|
-
});
|
|
11746
|
-
if (taskScopedEagerTools) {
|
|
11747
|
-
userParts.push(taskScopedEagerTools);
|
|
12095
|
+
|
|
12096
|
+
const coAuthorTrailer = shouldAddBosunCoAuthor({ taskId: normalizedTaskId })
|
|
12097
|
+
? getBosunCoAuthorTrailer()
|
|
12098
|
+
: "";
|
|
12099
|
+
if (coAuthorTrailer) {
|
|
12100
|
+
userParts.push("## Git Attribution");
|
|
12101
|
+
userParts.push("Add this trailer to all commits:");
|
|
12102
|
+
userParts.push(coAuthorTrailer);
|
|
11748
12103
|
userParts.push("");
|
|
11749
12104
|
}
|
|
11750
12105
|
|
|
11751
12106
|
const userPrompt = userParts.join("\n").trim();
|
|
11752
12107
|
const systemPrompt = buildStableSystemPrompt();
|
|
11753
|
-
|
|
11754
|
-
if (strictCacheAnchoring) {
|
|
11755
|
-
const dynamicMarkers = [
|
|
11756
|
-
taskId,
|
|
11757
|
-
taskTitle,
|
|
11758
|
-
taskDescription,
|
|
11759
|
-
retryReason,
|
|
11760
|
-
branch,
|
|
11761
|
-
baseBranch,
|
|
11762
|
-
worktreePath,
|
|
11763
|
-
]
|
|
11764
|
-
.map((value) => String(value || "").trim())
|
|
11765
|
-
.filter(Boolean);
|
|
11766
|
-
const leaked = dynamicMarkers.find((marker) => systemPrompt.includes(marker));
|
|
11767
|
-
if (leaked) {
|
|
11768
|
-
throw new Error(
|
|
11769
|
-
`BOSUN_CACHE_ANCHOR_MODE=strict violation: system prompt leaked task-specific marker "${leaked}"`,
|
|
11770
|
-
);
|
|
11771
|
-
}
|
|
11772
|
-
}
|
|
12108
|
+
assertStableSystemPrompt(systemPrompt);
|
|
11773
12109
|
|
|
11774
12110
|
ctx.data._taskPrompt = userPrompt;
|
|
11775
12111
|
ctx.data._taskUserPrompt = userPrompt;
|