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.
Files changed (43) hide show
  1. package/README.md +23 -1
  2. package/agent/agent-event-bus.mjs +31 -2
  3. package/agent/agent-pool.mjs +251 -11
  4. package/agent/agent-prompts.mjs +5 -1
  5. package/agent/agent-supervisor.mjs +22 -0
  6. package/agent/primary-agent.mjs +115 -5
  7. package/cli.mjs +3 -2
  8. package/config/config.mjs +4 -1
  9. package/desktop/main.mjs +350 -25
  10. package/desktop/preload.cjs +8 -0
  11. package/desktop/preload.mjs +19 -0
  12. package/entrypoint.mjs +332 -0
  13. package/infra/health-status.mjs +72 -0
  14. package/infra/library-manager.mjs +58 -1
  15. package/infra/maintenance.mjs +1 -2
  16. package/infra/monitor.mjs +25 -7
  17. package/infra/session-tracker.mjs +30 -3
  18. package/package.json +10 -4
  19. package/server/bosun-mcp-server.mjs +1004 -0
  20. package/server/setup-web-server.mjs +287 -258
  21. package/server/ui-server.mjs +218 -23
  22. package/shell/claude-shell.mjs +14 -1
  23. package/shell/codex-model-profiles.mjs +166 -29
  24. package/shell/codex-shell.mjs +56 -18
  25. package/shell/opencode-providers.mjs +20 -8
  26. package/task/task-executor.mjs +28 -0
  27. package/task/task-store.mjs +13 -4
  28. package/tools/list-todos.mjs +7 -1
  29. package/ui/app.js +3 -2
  30. package/ui/components/agent-selector.js +127 -0
  31. package/ui/components/session-list.js +2 -0
  32. package/ui/demo-defaults.js +6 -6
  33. package/ui/modules/router.js +2 -0
  34. package/ui/modules/state.js +13 -5
  35. package/ui/tabs/chat.js +3 -0
  36. package/ui/tabs/library.js +284 -52
  37. package/ui/tabs/tasks.js +5 -13
  38. package/workflow/workflow-engine.mjs +16 -4
  39. package/workflow/workflow-nodes/definitions.mjs +37 -0
  40. package/workflow/workflow-nodes.mjs +489 -153
  41. package/workflow/workflow-templates.mjs +0 -5
  42. package/workflow-templates/github.mjs +106 -16
  43. 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
- if (String(autoRecovery.reason || "").trim() !== "worktree_failure") continue;
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 = node.config?.includeTaskContext !== false;
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
- if (includeTaskContext) {
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 current = execSync("git branch --show-current", { encoding: "utf8", cwd, timeout: 15000, stdio: "pipe" }).trim();
8247
- const allBranches = execSync("git branch --list --format='%(refname:short)'", { encoding: "utf8", cwd, timeout: 15000, stdio: "pipe" })
8248
- .trim().split("\n").filter(Boolean);
8249
- return { current, branches: allBranches, branchCount: allBranches.length };
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
- async function ensureTaskClaimsInitialized(ctx, claims) {
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
- if (!_taskClaimsInitPromise) {
9518
- const repoRoot = pickTaskString(
9519
- ctx?.data?.repoRoot,
9520
- ctx?.data?.workspace,
9521
- process.cwd(),
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
- execSync(`git pull --rebase origin ${baseBranchShort}`, {
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 { triggered: false, reason: "all_filtered", taskCount: 0 };
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
- execSync(`git fetch origin ${baseBranchShort} --no-tags`, {
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
- execSync("git config --local core.longpaths true", {
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
- execSync(
11021
- `git worktree add "${worktreePath}" -b "${branch}" "${baseBranch}" 2>&1`,
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
- execSync(
11031
- `git worktree add "${worktreePath}" "${branch}" 2>&1`,
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
- if (existsSync(worktreePath)) {
11175
- try {
11176
- execSync(`git worktree remove "${worktreePath}" --force`, {
11177
- cwd: repoRoot, encoding: "utf8", timeout: removeTimeout,
11178
- stdio: ["ignore", "pipe", "pipe"],
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
- if (shouldPrune) {
11186
- try {
11187
- execSync("git worktree prune", {
11188
- cwd: repoRoot, encoding: "utf8", timeout: 15000,
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
- return String(value).trim();
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, repoSlug);
11742
+ const primaryRepository = pickFirstString(repository, normalizedRepoSlug);
11474
11743
  const allowedRepositories = normalizeStringArray(repositories, primaryRepository);
11475
- const matchedSkills = findRelevantSkills(repoRoot, taskTitle, taskDescription || "", {});
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
- if (includeAgentsMd) {
11485
- const searchDirs = [repoRoot].filter(Boolean);
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
- ctx.data._taskPrompt = customTemplate;
11563
- ctx.data._taskUserPrompt = customTemplate;
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 (${customTemplate.length} chars)`);
11882
+ ctx.log(node.id, `Prompt from custom template (${renderedTemplate.length} chars)`);
11566
11883
  return {
11567
11884
  success: true,
11568
- prompt: customTemplate,
11569
- userPrompt: customTemplate,
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: ${taskTitle}`);
11579
- if (taskId) userParts.push(`Task ID: ${taskId}`);
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 (retryReason) {
11900
+ if (normalizedRetryReason) {
11584
11901
  userParts.push("## Retry Context");
11585
- userParts.push(`Previous attempt failed: ${retryReason}`);
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 (taskDescription) {
11908
+ if (normalizedTaskDescription) {
11592
11909
  userParts.push("## Description");
11593
- userParts.push(taskDescription);
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 (worktreePath) envLines.push(`- **Working Directory:** ${worktreePath}`);
11601
- if (branch) envLines.push(`- **Branch:** ${branch}`);
11602
- if (baseBranch) envLines.push(`- **Base Branch:** ${baseBranch}`);
11603
- if (repoSlug) envLines.push(`- **Repository:** ${repoSlug}`);
11604
- if (repoRoot) envLines.push(`- **Repo Root:** ${repoRoot}`);
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 (worktreePath) userParts.push(`- **Write Scope Root:** ${worktreePath}`);
11952
+ if (normalizedWorktreePath) userParts.push(`- **Write Scope Root:** ${normalizedWorktreePath}`);
11621
11953
  userParts.push("");
11622
11954
  userParts.push("Hard boundaries:");
11623
- if (worktreePath) {
11624
- userParts.push(`1. Modify files only inside \`${worktreePath}\`.`);
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 = [worktreePath || repoRoot, repoRoot].filter(Boolean);
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
- repoRoot,
11693
- taskTitle,
11694
- taskDescription || "",
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 = repoRoot || process.cwd();
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
- // Skill-driven eager tools belong with task context to preserve cache anchoring.
11738
- const taskScopedEagerTools = getToolsPromptBlock(repoRoot, {
11739
- activeSkills: activeSkillFiles,
11740
- includeBuiltins: true,
11741
- eagerOnly: true,
11742
- discoveryMode: true,
11743
- emitReflectHint: false,
11744
- limit: 12,
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;