bosun 0.41.1 → 0.41.2

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "bosun",
3
- "version": "0.41.1",
3
+ "version": "0.41.2",
4
4
  "description": "Bosun Autonomous Engineering — manages AI agent executors with failover, extremely powerful workflow builder, and a massive amount of included default workflow templates for autonomous engineering, creates PRs via Vibe-Kanban API, and sends Telegram notifications. Supports N executors with weighted distribution, multi-repo projects, and auto-setup.",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
@@ -248,4 +248,3 @@ export function scaffoldCustomNodeFile(name, options = {}) {
248
248
  return { filePath, type, customDir, repoRoot };
249
249
  }
250
250
 
251
-
@@ -67,6 +67,131 @@ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
67
67
  })();
68
68
  const BOSUN_ATTACHED_PR_LABEL = "bosun-attached";
69
69
 
70
+ const HTML_TEXT_BREAK_TAGS = new Set([
71
+ "address",
72
+ "article",
73
+ "aside",
74
+ "blockquote",
75
+ "br",
76
+ "dd",
77
+ "div",
78
+ "dl",
79
+ "dt",
80
+ "figcaption",
81
+ "figure",
82
+ "footer",
83
+ "form",
84
+ "h1",
85
+ "h2",
86
+ "h3",
87
+ "h4",
88
+ "h5",
89
+ "h6",
90
+ "header",
91
+ "hr",
92
+ "li",
93
+ "main",
94
+ "nav",
95
+ "ol",
96
+ "p",
97
+ "pre",
98
+ "section",
99
+ "table",
100
+ "tbody",
101
+ "td",
102
+ "tfoot",
103
+ "th",
104
+ "thead",
105
+ "tr",
106
+ "ul",
107
+ ]);
108
+
109
+ function decodeHtmlEntities(value = "") {
110
+ return String(value).replace(/&(?:nbsp|amp|lt|gt|quot|apos|#39|#\d+|#x[0-9a-f]+);/gi, (entity) => {
111
+ const normalized = entity.toLowerCase();
112
+ switch (normalized) {
113
+ case " ":
114
+ return " ";
115
+ case "&":
116
+ return "&";
117
+ case "<":
118
+ return "<";
119
+ case "&gt;":
120
+ return ">";
121
+ case "&quot;":
122
+ return '"';
123
+ case "&apos;":
124
+ case "&#39;":
125
+ return "'";
126
+ default:
127
+ if (normalized.startsWith("&#x")) {
128
+ return String.fromCodePoint(Number.parseInt(normalized.slice(3, -1), 16));
129
+ }
130
+ if (normalized.startsWith("&#")) {
131
+ return String.fromCodePoint(Number.parseInt(normalized.slice(2, -1), 10));
132
+ }
133
+ return entity;
134
+ }
135
+ });
136
+ }
137
+
138
+ function stripHtmlToText(html = "") {
139
+ const input = String(html ?? "");
140
+ let plain = "";
141
+ let index = 0;
142
+ let skippedTagName = null;
143
+
144
+ while (index < input.length) {
145
+ const tagStart = input.indexOf("<", index);
146
+ if (tagStart === -1) {
147
+ if (!skippedTagName) plain += input.slice(index);
148
+ break;
149
+ }
150
+
151
+ if (!skippedTagName && tagStart > index) {
152
+ plain += input.slice(index, tagStart);
153
+ }
154
+
155
+ const tagEnd = input.indexOf(">", tagStart + 1);
156
+ if (tagEnd === -1) {
157
+ if (!skippedTagName) plain += input.slice(tagStart).replace(/</g, " ");
158
+ break;
159
+ }
160
+
161
+ const rawTag = input.slice(tagStart + 1, tagEnd).trim();
162
+ const loweredTag = rawTag.toLowerCase();
163
+ const isClosingTag = loweredTag.startsWith("/");
164
+ const normalizedTag = isClosingTag ? loweredTag.slice(1).trimStart() : loweredTag;
165
+ const tagName = normalizedTag.match(/^[a-z0-9]+/i)?.[0] ?? "";
166
+
167
+ if (skippedTagName) {
168
+ if (isClosingTag && tagName === skippedTagName) {
169
+ skippedTagName = null;
170
+ plain += " ";
171
+ }
172
+ index = tagEnd + 1;
173
+ continue;
174
+ }
175
+
176
+ if (tagName === "script" || tagName === "style") {
177
+ if (!isClosingTag && !normalizedTag.endsWith("/")) {
178
+ skippedTagName = tagName;
179
+ }
180
+ index = tagEnd + 1;
181
+ continue;
182
+ }
183
+
184
+ if (HTML_TEXT_BREAK_TAGS.has(tagName)) {
185
+ plain += " ";
186
+ }
187
+
188
+ index = tagEnd + 1;
189
+ }
190
+
191
+ return decodeHtmlEntities(plain);
192
+ }
193
+
194
+
70
195
  const PORT_TYPE_DESCRIPTIONS = Object.freeze({
71
196
  Any: "Wildcard payload",
72
197
  TaskDef: "Task definition/context payload",
@@ -9517,8 +9642,57 @@ function isValidGitWorktreePath(worktreePath) {
9517
9642
  }
9518
9643
  }
9519
9644
 
9645
+ function resolveGitDirForWorktree(worktreePath) {
9646
+ if (!worktreePath || !existsSync(worktreePath)) return "";
9647
+ try {
9648
+ const topLevel = execGitArgsSync(["rev-parse", "--show-toplevel"], {
9649
+ cwd: worktreePath,
9650
+ encoding: "utf8",
9651
+ timeout: 5000,
9652
+ stdio: ["ignore", "pipe", "pipe"],
9653
+ }).trim();
9654
+ const normalize = (value) =>
9655
+ resolve(String(value || ""))
9656
+ .replace(/\\/g, "/")
9657
+ .replace(/\/+$/, "")
9658
+ .toLowerCase();
9659
+ if (normalize(topLevel) !== normalize(worktreePath)) return "";
9660
+ const gitDir = execGitArgsSync(["rev-parse", "--git-dir"], {
9661
+ cwd: worktreePath,
9662
+ encoding: "utf8",
9663
+ timeout: 5000,
9664
+ stdio: ["ignore", "pipe", "pipe"],
9665
+ }).trim();
9666
+ if (!gitDir) return "";
9667
+ return resolve(worktreePath, gitDir);
9668
+ } catch {
9669
+ return "";
9670
+ }
9671
+ }
9672
+
9673
+ function hasUnresolvedGitOperation(worktreePath) {
9674
+ if (!worktreePath || !existsSync(worktreePath)) return false;
9675
+ try {
9676
+ const gitDir = resolveGitDirForWorktree(worktreePath);
9677
+ if (!gitDir || !existsSync(gitDir)) return true;
9678
+ for (const marker of ["rebase-merge", "rebase-apply", "MERGE_HEAD", "CHERRY_PICK_HEAD", "REVERT_HEAD"]) {
9679
+ if (existsSync(resolve(gitDir, marker))) return true;
9680
+ }
9681
+ const unmerged = execGitArgsSync(["diff", "--name-only", "--diff-filter=U"], {
9682
+ cwd: worktreePath,
9683
+ encoding: "utf8",
9684
+ timeout: 5000,
9685
+ stdio: ["ignore", "pipe", "pipe"],
9686
+ }).trim();
9687
+ return Boolean(unmerged);
9688
+ } catch {
9689
+ return true;
9690
+ }
9691
+ }
9692
+
9520
9693
  function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
9521
9694
  if (!worktreePath) return;
9695
+ const linkedGitDir = resolveGitDirForWorktree(worktreePath);
9522
9696
  try {
9523
9697
  execGitArgsSync(["worktree", "remove", String(worktreePath), "--force"], {
9524
9698
  cwd: repoRoot,
@@ -9534,6 +9708,13 @@ function cleanupBrokenManagedWorktree(repoRoot, worktreePath) {
9534
9708
  } catch {
9535
9709
  /* best-effort */
9536
9710
  }
9711
+ try {
9712
+ if (linkedGitDir && existsSync(linkedGitDir)) {
9713
+ rmSync(linkedGitDir, { recursive: true, force: true });
9714
+ }
9715
+ } catch {
9716
+ /* best-effort */
9717
+ }
9537
9718
  try {
9538
9719
  execGitArgsSync(["worktree", "prune"], {
9539
9720
  cwd: repoRoot,
@@ -10551,6 +10732,9 @@ registerBuiltinNodeType("action.acquire_worktree", {
10551
10732
  if (!isValidGitWorktreePath(worktreePath)) {
10552
10733
  ctx.log(node.id, `Managed worktree is invalid, recreating: ${worktreePath}`);
10553
10734
  cleanupBrokenManagedWorktree(repoRoot, worktreePath);
10735
+ } else if (hasUnresolvedGitOperation(worktreePath)) {
10736
+ ctx.log(node.id, `Managed worktree has unresolved git state, recreating: ${worktreePath}`);
10737
+ cleanupBrokenManagedWorktree(repoRoot, worktreePath);
10554
10738
  }
10555
10739
  }
10556
10740
 
@@ -10566,14 +10750,20 @@ registerBuiltinNodeType("action.acquire_worktree", {
10566
10750
  } catch {
10567
10751
  /* rebase failures are non-fatal for reuse */
10568
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
+ }
10758
+ if (existsSync(worktreePath)) {
10759
+ ctx.data.worktreePath = worktreePath;
10760
+ ctx.data._worktreeCreated = false;
10761
+ ctx.data._worktreeManaged = true;
10762
+ ctx.log(node.id, `Reusing worktree: ${worktreePath}`);
10763
+ const cleared1 = clearBlockedWorktreeIdentity(worktreePath);
10764
+ if (cleared1) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
10765
+ return { success: true, worktreePath, created: false, reused: true, branch, baseBranch };
10569
10766
  }
10570
- ctx.data.worktreePath = worktreePath;
10571
- ctx.data._worktreeCreated = false;
10572
- ctx.data._worktreeManaged = true;
10573
- ctx.log(node.id, `Reusing worktree: ${worktreePath}`);
10574
- const cleared1 = clearBlockedWorktreeIdentity(worktreePath);
10575
- if (cleared1) ctx.log(node.id, `Cleared blocked test git identity from worktree: ${worktreePath}`);
10576
- return { success: true, worktreePath, created: false, reused: true, branch, baseBranch };
10577
10767
  }
10578
10768
 
10579
10769
  // Create fresh worktree