bosun 0.41.2 → 0.41.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-prompt-catalog.mjs +971 -0
  3. package/agent/agent-prompts.mjs +2 -970
  4. package/agent/agent-supervisor.mjs +6 -3
  5. package/agent/autofix-git.mjs +33 -0
  6. package/agent/autofix-prompts.mjs +151 -0
  7. package/agent/autofix.mjs +11 -175
  8. package/agent/bosun-skills.mjs +3 -2
  9. package/bosun.config.example.json +17 -0
  10. package/bosun.schema.json +87 -188
  11. package/cli.mjs +34 -1
  12. package/config/config-doctor.mjs +5 -250
  13. package/config/config-file-names.mjs +5 -0
  14. package/config/config.mjs +89 -493
  15. package/config/executor-config.mjs +493 -0
  16. package/config/repo-root.mjs +1 -2
  17. package/config/workspace-health.mjs +242 -0
  18. package/git/git-safety.mjs +15 -0
  19. package/github/github-oauth-portal.mjs +46 -0
  20. package/infra/library-manager-utils.mjs +22 -0
  21. package/infra/library-manager-well-known-sources.mjs +578 -0
  22. package/infra/library-manager.mjs +512 -1030
  23. package/infra/monitor.mjs +28 -9
  24. package/infra/session-tracker.mjs +10 -7
  25. package/kanban/kanban-adapter.mjs +17 -1
  26. package/lib/codebase-audit-manifests.mjs +117 -0
  27. package/lib/codebase-audit.mjs +18 -115
  28. package/package.json +18 -3
  29. package/server/ui-server.mjs +1194 -79
  30. package/shell/codex-config-file.mjs +178 -0
  31. package/shell/codex-config.mjs +538 -575
  32. package/task/task-cli.mjs +54 -3
  33. package/task/task-executor.mjs +143 -13
  34. package/task/task-store.mjs +409 -1
  35. package/telegram/telegram-bot.mjs +127 -0
  36. package/tools/apply-pr-suggestions.mjs +401 -0
  37. package/tools/syntax-check.mjs +21 -9
  38. package/ui/app.js +3 -14
  39. package/ui/components/kanban-board.js +227 -4
  40. package/ui/components/session-list.js +85 -5
  41. package/ui/demo-defaults.js +334 -80
  42. package/ui/demo.html +155 -0
  43. package/ui/modules/session-api.js +96 -0
  44. package/ui/modules/settings-schema.js +1 -2
  45. package/ui/modules/state.js +21 -3
  46. package/ui/setup.html +4 -5
  47. package/ui/styles/components.css +58 -4
  48. package/ui/tabs/agents.js +12 -15
  49. package/ui/tabs/control.js +1 -0
  50. package/ui/tabs/library.js +484 -22
  51. package/ui/tabs/manual-flows.js +105 -29
  52. package/ui/tabs/tasks.js +785 -140
  53. package/ui/tabs/telemetry.js +129 -11
  54. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  55. package/ui/tabs/workflows.js +293 -23
  56. package/voice/voice-tool-definitions.mjs +757 -0
  57. package/voice/voice-tools.mjs +34 -778
  58. package/workflow/manual-flow-audit.mjs +165 -0
  59. package/workflow/manual-flows.mjs +164 -259
  60. package/workflow/workflow-engine.mjs +147 -58
  61. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  62. package/workflow/workflow-nodes/transforms.mjs +612 -0
  63. package/workflow/workflow-nodes.mjs +304 -52
  64. package/workflow/workflow-templates.mjs +313 -191
  65. package/workflow-templates/_helpers.mjs +154 -0
  66. package/workflow-templates/agents.mjs +61 -4
  67. package/workflow-templates/code-quality.mjs +7 -7
  68. package/workflow-templates/github.mjs +20 -10
  69. package/workflow-templates/task-batch.mjs +20 -9
  70. package/workflow-templates/task-lifecycle.mjs +31 -6
  71. package/workspace/worktree-manager.mjs +277 -3
@@ -0,0 +1,1207 @@
1
+ /**
2
+ * workflow-nodes.mjs — Built-in Workflow Node Types for Bosun
3
+ *
4
+ * Registers all standard node types that can be used in workflow definitions.
5
+ * Node types are organized by category:
6
+ *
7
+ * TRIGGERS — Events that start workflow execution
8
+ * CONDITIONS — Branching logic / gates
9
+ * ACTIONS — Side-effect operations (run agent, create task, etc.)
10
+ * VALIDATION — Verification gates (screenshots, tests, model review)
11
+ * TRANSFORM — Data transformation / aggregation
12
+ * NOTIFY — Notifications (telegram, log, etc.)
13
+ *
14
+ * Each node type must export:
15
+ * execute(node, ctx, engine) → Promise<any> — The node's logic
16
+ * describe() → string — Human-readable description
17
+ * schema → object — JSON Schema for node config
18
+ */
19
+
20
+ import { existsSync, readFileSync, writeFileSync, mkdirSync } from "node:fs";
21
+ import { resolve, dirname } from "node:path";
22
+ import { execSync, execFileSync, spawn } from "node:child_process";
23
+ import { createHash, randomUUID } from "node:crypto";
24
+ import { getAgentToolConfig, getEffectiveTools } from "../../agent/agent-tool-config.mjs";
25
+ import { getToolsPromptBlock } from "../../agent/agent-custom-tools.mjs";
26
+ import { buildRelevantSkillsPromptBlock, findRelevantSkills } from "../../agent/bosun-skills.mjs";
27
+ import { getSessionTracker } from "../../infra/session-tracker.mjs";
28
+ import { fixGitConfigCorruption } from "../../workspace/worktree-manager.mjs";
29
+
30
+ const TAG = "[workflow-nodes]";
31
+ const PORTABLE_WORKTREE_COUNT_COMMAND = "node -e \"const cp=require('node:child_process');const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
32
+ const PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND = "node -e \"const cp=require('node:child_process');cp.execSync('git worktree prune',{stdio:'ignore'});const wt=cp.execSync('git worktree list --porcelain',{encoding:'utf8'});const count=(wt.match(/^worktree /gm)||[]).length;process.stdout.write(String(count)+'\\\\n');\"";
33
+ const WORKFLOW_AGENT_HEARTBEAT_MS = (() => {
34
+ const raw = Number(process.env.WORKFLOW_AGENT_HEARTBEAT_MS || 30000);
35
+ if (!Number.isFinite(raw)) return 30000;
36
+ return Math.max(5000, Math.min(120000, Math.trunc(raw)));
37
+ })();
38
+ const WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT = (() => {
39
+ const raw = Number(process.env.WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT || 80);
40
+ if (!Number.isFinite(raw)) return 80;
41
+ return Math.max(20, Math.min(500, Math.trunc(raw)));
42
+ })();
43
+ const BOSUN_ATTACHED_PR_LABEL = "bosun-attached";
44
+
45
+ const _builtinNodeDefinitions = new Map();
46
+ function registerNodeType(type, handler) {
47
+ if (!handler || typeof handler.execute !== "function") {
48
+ throw new Error(`${TAG} Node type "${type}" must have an execute function`);
49
+ }
50
+ _builtinNodeDefinitions.set(type, handler);
51
+ }
52
+
53
+ export function getBuiltinNodeDefinition(type) {
54
+ return _builtinNodeDefinitions.get(type) || null;
55
+ }
56
+
57
+ export function listBuiltinNodeDefinitions() {
58
+ return [..._builtinNodeDefinitions.entries()].map(([type, handler]) => ({
59
+ type,
60
+ handler,
61
+ }));
62
+ }
63
+
64
+ function makeIsolatedGitEnv(extra = {}) {
65
+ const env = { ...process.env, ...extra };
66
+ for (const key of [
67
+ "GIT_DIR",
68
+ "GIT_WORK_TREE",
69
+ "GIT_COMMON_DIR",
70
+ "GIT_INDEX_FILE",
71
+ "GIT_OBJECT_DIRECTORY",
72
+ "GIT_ALTERNATE_OBJECT_DIRECTORIES",
73
+ "GIT_PREFIX",
74
+ ]) {
75
+ delete env[key];
76
+ }
77
+ return env;
78
+ }
79
+
80
+ function resolveGitCandidates(env = process.env) {
81
+ const candidates = [];
82
+ const envGitExe = env?.GIT_EXE || process.env.GIT_EXE;
83
+ if (envGitExe) candidates.push(envGitExe);
84
+ if (process.platform === "win32") {
85
+ candidates.push(
86
+ "C:\\Program Files\\Git\\cmd\\git.exe",
87
+ "C:\\Program Files\\Git\\bin\\git.exe",
88
+ "C:\\Program Files (x86)\\Git\\cmd\\git.exe",
89
+ "C:\\Program Files (x86)\\Git\\bin\\git.exe",
90
+ );
91
+ } else {
92
+ candidates.push(
93
+ "/usr/bin/git",
94
+ "/usr/local/bin/git",
95
+ "/bin/git",
96
+ "/opt/homebrew/bin/git",
97
+ );
98
+ }
99
+
100
+ if (process.platform === "win32") {
101
+ try {
102
+ const whereOutput = execFileSync("where.exe", ["git"], {
103
+ encoding: "utf8",
104
+ env,
105
+ stdio: ["ignore", "pipe", "ignore"],
106
+ windowsHide: true,
107
+ });
108
+ for (const line of String(whereOutput || "").split(/\r?\n/)) {
109
+ const candidate = line.trim();
110
+ if (!candidate) continue;
111
+ candidates.push(candidate);
112
+ }
113
+ } catch {
114
+ /* best-effort */
115
+ }
116
+ }
117
+
118
+ candidates.push("git");
119
+ const deduped = [];
120
+ const seen = new Set();
121
+ for (const candidate of candidates) {
122
+ const key = process.platform === "win32"
123
+ ? String(candidate || "").toLowerCase()
124
+ : String(candidate || "");
125
+ if (!candidate || seen.has(key)) continue;
126
+ seen.add(key);
127
+ deduped.push(candidate);
128
+ }
129
+ return deduped;
130
+ }
131
+
132
+ function buildGitExecutionEnv(baseEnv, gitBinary) {
133
+ if (process.platform !== "win32") return baseEnv;
134
+ const normalizedBinary = String(gitBinary || "").replace(/\//g, "\\");
135
+ if (!normalizedBinary.includes("\\") || !normalizedBinary.toLowerCase().endsWith("\\git.exe")) {
136
+ return baseEnv;
137
+ }
138
+ const env = { ...baseEnv };
139
+ const pathKey = Object.prototype.hasOwnProperty.call(env, "Path")
140
+ ? "Path"
141
+ : "PATH";
142
+ const existing = String(env[pathKey] ?? env.PATH ?? env.Path ?? "");
143
+ const parts = existing
144
+ .split(";")
145
+ .map((part) => part.trim())
146
+ .filter(Boolean);
147
+ const seen = new Set(parts.map((part) => part.toLowerCase()));
148
+ const binaryDir = dirname(normalizedBinary);
149
+ const gitRoot = dirname(binaryDir);
150
+ for (const dir of [
151
+ binaryDir,
152
+ `${gitRoot}\\cmd`,
153
+ `${gitRoot}\\bin`,
154
+ `${gitRoot}\\mingw64\\bin`,
155
+ `${gitRoot}\\usr\\bin`,
156
+ ]) {
157
+ const normalizedDir = String(dir || "").replace(/\//g, "\\");
158
+ if (!normalizedDir || seen.has(normalizedDir.toLowerCase())) continue;
159
+ seen.add(normalizedDir.toLowerCase());
160
+ parts.unshift(normalizedDir);
161
+ }
162
+ env[pathKey] = parts.join(";");
163
+ if (pathKey === "PATH") env.Path = env[pathKey];
164
+ else env.PATH = env[pathKey];
165
+ return env;
166
+ }
167
+
168
+ function execGitArgsSync(args, options = {}) {
169
+ if (!Array.isArray(args) || !args.length) {
170
+ throw new Error("execGitArgsSync requires a non-empty args array");
171
+ }
172
+ const env = makeIsolatedGitEnv(options.env);
173
+ const gitArgs = args.map((arg) => String(arg));
174
+ let lastEnoent = null;
175
+ for (const gitBinary of resolveGitCandidates(env)) {
176
+
177
+ try {
178
+ return execFileSync(gitBinary, gitArgs, {
179
+ ...options,
180
+ env: buildGitExecutionEnv(env, gitBinary),
181
+ });
182
+ } catch (error) {
183
+ if (error?.code === "ENOENT") {
184
+ lastEnoent = error;
185
+ continue;
186
+ }
187
+ throw error;
188
+ }
189
+ }
190
+ if (lastEnoent) throw lastEnoent;
191
+ throw new Error("Git executable not found");
192
+ }
193
+
194
+ function trimLogText(value, max = 180) {
195
+ const text = String(value || "").replace(/\s+/g, " ").trim();
196
+ if (!text) return "";
197
+ return text.length > max ? `${text.slice(0, max - 1)}…` : text;
198
+ }
199
+
200
+ function normalizeLineEndings(value) {
201
+ if (value == null) return "";
202
+ return String(value)
203
+ .replace(/\\r\\n/g, "\n")
204
+ .replace(/\\n/g, "\n")
205
+ .replace(/\r\n/g, "\n")
206
+ .replace(/\r/g, "\n");
207
+ }
208
+
209
+ function simplifyPathLabel(filePath) {
210
+ const normalized = String(filePath || "").replace(/\\/g, "/");
211
+ if (!normalized) return "";
212
+ const parts = normalized.split("/").filter(Boolean);
213
+ if (parts.length >= 2) return parts.slice(-2).join("/");
214
+ return parts[0] || normalized;
215
+ }
216
+
217
+ const PR_EVENT_ALIAS_MAP = Object.freeze({
218
+ open: "opened",
219
+ opened: "opened",
220
+ reopen: "opened",
221
+ reopened: "opened",
222
+ ready_for_review: "opened",
223
+ readyforreview: "opened",
224
+ synchronize: "opened",
225
+ synchronized: "opened",
226
+ edited: "opened",
227
+ merge: "merged",
228
+ merged: "merged",
229
+ review_requested: "review_requested",
230
+ reviewrequest: "review_requested",
231
+ review_requested_event: "review_requested",
232
+ changes_requested: "changes_requested",
233
+ change_requested: "changes_requested",
234
+ requested_changes: "changes_requested",
235
+ approved: "approved",
236
+ approval: "approved",
237
+ close: "closed",
238
+ closed: "closed",
239
+ });
240
+
241
+ function normalizePrEventName(value) {
242
+ const raw = String(value || "").trim().toLowerCase();
243
+ if (!raw) return "";
244
+ const normalized = raw.replace(/[\s-]+/g, "_");
245
+ return PR_EVENT_ALIAS_MAP[normalized] || normalized;
246
+ }
247
+
248
+ function evaluateTaskAssignedTriggerConfig(config = {}, eventData = {}) {
249
+ let triggered = eventData?.eventType === "task.assigned";
250
+ if (!triggered) return false;
251
+
252
+ const task = eventData?.task || eventData || {};
253
+ const expectedAgentType = String(config?.agentType || "").trim().toLowerCase();
254
+ if (expectedAgentType) {
255
+ const candidateTypes = new Set(
256
+ [
257
+ eventData?.agentType,
258
+ eventData?.assignedAgentType,
259
+ eventData?.task?.agentType,
260
+ eventData?.task?.assignedAgentType,
261
+ eventData?.task?.agentProfile,
262
+ task?.agentType,
263
+ task?.assignedAgentType,
264
+ task?.agentProfile,
265
+ ]
266
+ .map((value) => String(value || "").trim().toLowerCase())
267
+ .filter(Boolean),
268
+ );
269
+ triggered = candidateTypes.has(expectedAgentType);
270
+ }
271
+
272
+ if (triggered && config?.taskPattern) {
273
+ try {
274
+ const regex = new RegExp(String(config.taskPattern), "i");
275
+ const searchableText = [
276
+ eventData?.taskTitle,
277
+ task?.title,
278
+ ...(Array.isArray(task?.tags) ? task.tags : []),
279
+ ]
280
+ .map((value) => String(value || "").trim())
281
+ .filter(Boolean)
282
+ .join(" ");
283
+ triggered = regex.test(searchableText);
284
+ } catch {
285
+ triggered = false;
286
+ }
287
+ }
288
+
289
+ if (triggered && config?.filter) {
290
+ try {
291
+ const fn = new Function("task", "$data", `return !!(${config.filter});`);
292
+ triggered = Boolean(fn(task, eventData));
293
+ } catch {
294
+ triggered = false;
295
+ }
296
+ }
297
+
298
+ return triggered;
299
+ }
300
+
301
+ function isManagedBosunWorktree(worktreePath, repoRoot) {
302
+ const resolvedWorktree = resolve(String(worktreePath || ""));
303
+ const managedRoot = resolve(String(repoRoot || process.cwd()), ".bosun", "worktrees");
304
+ return (
305
+ resolvedWorktree === managedRoot ||
306
+ resolvedWorktree.startsWith(`${managedRoot}\\`) ||
307
+ resolvedWorktree.startsWith(`${managedRoot}/`)
308
+ );
309
+ }
310
+
311
+ function deriveManagedWorktreeDirName(taskId, branch) {
312
+ const taskToken = String(taskId || "task")
313
+ .replace(/[^a-zA-Z0-9]/g, "")
314
+ .slice(0, 12)
315
+ || "task";
316
+ const branchHash = createHash("sha1")
317
+ .update(String(branch || "branch"))
318
+ .digest("hex")
319
+ .slice(0, 10);
320
+ return `task-${taskToken}-${branchHash}`;
321
+ }
322
+
323
+ const WORKFLOW_TELEGRAM_ICON_MAP = Object.freeze({
324
+ check: "✅",
325
+ close: "❌",
326
+ alert: "⚠️",
327
+ warning: "⚠️",
328
+ help: "❓",
329
+ info: "ℹ️",
330
+ dot: "•",
331
+ folder: "📁",
332
+ refresh: "🔄",
333
+ lock: "🔒",
334
+ unlock: "🔓",
335
+ play: "▶️",
336
+ pause: "⏸️",
337
+ stop: "⏹️",
338
+ rocket: "🚀",
339
+ gear: "⚙️",
340
+ wrench: "🔧",
341
+ search: "🔍",
342
+ clipboard: "📋",
343
+ chart: "📊",
344
+ hourglass: "⏳",
345
+ fire: "🔥",
346
+ bug: "🐛",
347
+ sparkles: "✨",
348
+ });
349
+
350
+ function decodeWorkflowUnicodeIconToken(name) {
351
+ const raw = String(name || "").trim().toLowerCase();
352
+ if (!raw) return "";
353
+ const normalized = raw.startsWith("u") ? raw.slice(1) : raw;
354
+ if (!/^[0-9a-f]{4,6}$/.test(normalized)) return "";
355
+ try {
356
+ return String.fromCodePoint(parseInt(normalized, 16));
357
+ } catch {
358
+ return "";
359
+ }
360
+ }
361
+
362
+ function normalizeWorkflowTelegramText(value) {
363
+ const text = String(value || "");
364
+ if (!text) return "";
365
+ return text.replace(/:([a-zA-Z0-9_+-]{2,}):/g, (token, iconName) => {
366
+ const key = String(iconName || "").trim().toLowerCase();
367
+ if (!key) return token;
368
+ const squashed = key.replace(/[-+]/g, "");
369
+ const glyph = WORKFLOW_TELEGRAM_ICON_MAP[key]
370
+ || WORKFLOW_TELEGRAM_ICON_MAP[squashed]
371
+ || decodeWorkflowUnicodeIconToken(key)
372
+ || decodeWorkflowUnicodeIconToken(squashed);
373
+ return glyph || token;
374
+ });
375
+ }
376
+
377
+ function parsePathListingLine(line) {
378
+ const raw = String(line || "").trim();
379
+ if (!raw) return null;
380
+ const windowsMatch = raw.match(/^([A-Za-z]:\\[^:]+):(\d+):\s*(.+)?$/);
381
+ if (windowsMatch) {
382
+ return {
383
+ path: windowsMatch[1],
384
+ line: Number(windowsMatch[2]),
385
+ detail: String(windowsMatch[3] || "").trim(),
386
+ };
387
+ }
388
+ const unixMatch = raw.match(/^(\/[^:]+):(\d+):\s*(.+)?$/);
389
+ if (unixMatch) {
390
+ return {
391
+ path: unixMatch[1],
392
+ line: Number(unixMatch[2]),
393
+ detail: String(unixMatch[3] || "").trim(),
394
+ };
395
+ }
396
+ return null;
397
+ }
398
+
399
+ function extractSymbolHint(detail) {
400
+ const text = String(detail || "");
401
+ if (!text) return "";
402
+ const patterns = [
403
+ /\b(?:async\s+)?function\s+([A-Za-z0-9_$]+)/i,
404
+ /\bclass\s+([A-Za-z0-9_$]+)/i,
405
+ /\b(?:const|let|var)\s+([A-Za-z0-9_$]+)\s*=\s*(?:async\s*)?\(/i,
406
+ /\b([A-Za-z0-9_$]+)\s*:\s*function\b/i,
407
+ ];
408
+ for (const pattern of patterns) {
409
+ const match = text.match(pattern);
410
+ if (match?.[1]) return match[1];
411
+ }
412
+ return "";
413
+ }
414
+
415
+ function summarizePathListingBlock(value) {
416
+ const lines = normalizeLineEndings(value)
417
+ .split("\n")
418
+ .map((line) => String(line || "").trim())
419
+ .filter(Boolean);
420
+ if (!lines.length) return "";
421
+
422
+ const entries = [];
423
+ for (const line of lines) {
424
+ const parsed = parsePathListingLine(line);
425
+ if (parsed) entries.push(parsed);
426
+ }
427
+
428
+ if (entries.length < 3) return "";
429
+ const fileStats = new Map();
430
+ const symbols = new Set();
431
+ for (const entry of entries) {
432
+ const label = simplifyPathLabel(entry.path) || entry.path;
433
+ const current = fileStats.get(label) || { count: 0 };
434
+ current.count += 1;
435
+ fileStats.set(label, current);
436
+ const symbol = extractSymbolHint(entry.detail);
437
+ if (symbol) symbols.add(symbol);
438
+ }
439
+
440
+ const fileList = Array.from(fileStats.entries())
441
+ .sort((a, b) => b[1].count - a[1].count)
442
+ .slice(0, 4)
443
+ .map(([label, stat]) => `${label} (${stat.count})`)
444
+ .join(", ");
445
+ const symbolList = Array.from(symbols).slice(0, 6).join(", ");
446
+
447
+ const summaryParts = [
448
+ `Indexed ${entries.length} code references across ${fileStats.size} file${fileStats.size === 1 ? "" : "s"}`,
449
+ ];
450
+ if (fileList) summaryParts.push(`Top files: ${fileList}`);
451
+ if (symbolList) summaryParts.push(`Symbols: ${symbolList}`);
452
+
453
+ return trimLogText(summaryParts.join(". "), 320);
454
+ }
455
+
456
+ function normalizeNarrativeText(value, options = {}) {
457
+ const maxParagraphs = Number.isFinite(options.maxParagraphs) ? options.maxParagraphs : 4;
458
+ const maxChars = Number.isFinite(options.maxChars) ? options.maxChars : 2200;
459
+ const raw = normalizeLineEndings(value);
460
+ if (!raw) return "";
461
+
462
+ const pathSummary = summarizePathListingBlock(raw);
463
+ if (pathSummary) return pathSummary;
464
+
465
+ const paragraphs = raw
466
+ .split(/\n{2,}/)
467
+ .map((paragraph) =>
468
+ paragraph
469
+ .split("\n")
470
+ .map((line) => String(line || "").trim())
471
+ .filter(Boolean)
472
+ .join(" "),
473
+ )
474
+ .map((paragraph) => paragraph.replace(/\s+/g, " ").trim())
475
+ .filter(Boolean)
476
+ .slice(0, Math.max(1, maxParagraphs));
477
+
478
+ const text = paragraphs.join("\n\n").trim();
479
+ if (!text) return "";
480
+ return text.length > maxChars ? `${text.slice(0, maxChars - 1)}…` : text;
481
+ }
482
+
483
+ function summarizeAssistantUsage(data = {}) {
484
+ const usage = data?.usage && typeof data.usage === "object" ? data.usage : data;
485
+ if (!usage || typeof usage !== "object") return "";
486
+
487
+ const pickNumber = (...keys) => {
488
+ for (const key of keys) {
489
+ const candidate = Number(usage?.[key]);
490
+ if (Number.isFinite(candidate) && candidate >= 0) return candidate;
491
+ }
492
+ return null;
493
+ };
494
+
495
+ const model = trimLogText(usage?.model || data?.model || "", 60);
496
+ const prompt = pickNumber("prompt_tokens", "inputTokens", "promptTokens");
497
+ const completion = pickNumber("completion_tokens", "outputTokens", "completionTokens");
498
+ const total = pickNumber("total_tokens", "totalTokens");
499
+ const durationMs = pickNumber("duration", "durationMs");
500
+ const parts = [];
501
+
502
+ if (model) parts.push(`model=${model}`);
503
+ if (prompt != null) parts.push(`prompt=${prompt}`);
504
+ if (completion != null) parts.push(`completion=${completion}`);
505
+ if (total != null) parts.push(`total=${total}`);
506
+ if (durationMs != null) parts.push(`duration=${Math.round(durationMs)}ms`);
507
+ if (!parts.length) return "";
508
+ return `Usage: ${parts.join(" · ")}`;
509
+ }
510
+
511
+ function bindTaskContext(ctx, { taskId, taskTitle, task = null } = {}) {
512
+ if (!ctx || typeof ctx !== "object") return;
513
+ if (!ctx.data || typeof ctx.data !== "object") {
514
+ ctx.data = {};
515
+ }
516
+
517
+ const normalizedTaskId = String(taskId || task?.id || task?.task_id || "").trim();
518
+ if (normalizedTaskId) {
519
+ ctx.data.taskId = normalizedTaskId;
520
+ ctx.data.activeTaskId = normalizedTaskId;
521
+ }
522
+
523
+ const normalizedTaskTitle = String(taskTitle || task?.title || "").trim();
524
+ if (normalizedTaskTitle) {
525
+ ctx.data.taskTitle = normalizedTaskTitle;
526
+ }
527
+
528
+ if (task && typeof task === "object") {
529
+ ctx.data.task = task;
530
+ }
531
+ }
532
+ async function createKanbanTaskWithProject(kanban, taskData = {}, projectIdValue = "") {
533
+ if (!kanban || typeof kanban.createTask !== "function") {
534
+ throw new Error("Kanban adapter not available");
535
+ }
536
+
537
+ const payload =
538
+ taskData && typeof taskData === "object" ? { ...taskData } : {};
539
+ const resolvedProjectId = String(projectIdValue || payload.projectId || "").trim();
540
+
541
+ if (resolvedProjectId) {
542
+ payload.projectId = resolvedProjectId;
543
+ }
544
+
545
+ const taskPayload = { ...payload };
546
+ delete taskPayload.projectId;
547
+ return kanban.createTask(resolvedProjectId, taskPayload);
548
+ }
549
+
550
+ function summarizeAssistantMessageData(data = {}) {
551
+ const messageText = normalizeNarrativeText(
552
+ extractStreamText(data?.content) ||
553
+ extractStreamText(data?.text) ||
554
+ extractStreamText(data?.deltaContent),
555
+ { maxParagraphs: 1, maxChars: 260 },
556
+ );
557
+ if (messageText) return `Agent: ${trimLogText(messageText, 220)}`;
558
+
559
+ const detailText = normalizeNarrativeText(data?.detailedContent, {
560
+ maxParagraphs: 1,
561
+ maxChars: 260,
562
+ });
563
+ if (detailText) return `Agent detail: ${trimLogText(detailText, 220)}`;
564
+
565
+ const toolRequests = Array.isArray(data?.toolRequests)
566
+ ? data.toolRequests
567
+ .map((req) => String(req?.name || "").trim())
568
+ .filter(Boolean)
569
+ : [];
570
+ if (toolRequests.length) {
571
+ const unique = Array.from(new Set(toolRequests)).slice(0, 4).join(", ");
572
+ return `Agent requested tools: ${unique}`;
573
+ }
574
+
575
+ const reasoningText = normalizeNarrativeText(data?.reasoningOpaque, {
576
+ maxParagraphs: 1,
577
+ maxChars: 220,
578
+ });
579
+ if (reasoningText) return `Thinking: ${trimLogText(reasoningText, 220)}`;
580
+
581
+ return "";
582
+ }
583
+
584
+ function extractStreamText(value) {
585
+ if (value == null) return "";
586
+ if (typeof value === "string") return value;
587
+ if (typeof value === "number" || typeof value === "boolean") return String(value);
588
+ if (Array.isArray(value)) {
589
+ const parts = value
590
+ .map((entry) => {
591
+ if (entry == null) return "";
592
+ if (typeof entry === "string") return entry;
593
+ if (typeof entry?.text === "string") return entry.text;
594
+ if (typeof entry?.content === "string") return entry.content;
595
+ if (typeof entry?.deltaContent === "string") return entry.deltaContent;
596
+ return "";
597
+ })
598
+ .filter(Boolean);
599
+ return parts.join(" ");
600
+ }
601
+ if (typeof value === "object") {
602
+ if (typeof value?.text === "string") return value.text;
603
+ if (typeof value?.content === "string") return value.content;
604
+ if (typeof value?.deltaContent === "string") return value.deltaContent;
605
+ if (typeof value?.detailedContent === "string") return value.detailedContent;
606
+ if (typeof value?.summary === "string") return value.summary;
607
+ if (typeof value?.reasoning === "string") return value.reasoning;
608
+ if (typeof value?.reasoningOpaque === "string") return value.reasoningOpaque;
609
+ }
610
+ return "";
611
+ }
612
+
613
+ function summarizeAgentStreamEvent(event) {
614
+ const type = String(event?.type || "").trim();
615
+ if (!type) return "";
616
+
617
+ // Ignore token-level deltas that create noisy duplicate run logs.
618
+ if (
619
+ /reasoning(?:_|[.])delta/i.test(type) ||
620
+ /(?:^|[.])delta$/i.test(type) ||
621
+ /(?:_|[.])delta(?:_|[.])/i.test(type)
622
+ ) {
623
+ return "";
624
+ }
625
+
626
+ if (type === "item.updated") {
627
+ return "";
628
+ }
629
+
630
+ if (type === "tool_call") {
631
+ return `Tool call: ${event?.tool_name || event?.data?.tool_name || "unknown"}`;
632
+ }
633
+
634
+ if (type === "function_call") {
635
+ return `Tool call: ${event?.name || event?.tool_name || "unknown"}`;
636
+ }
637
+
638
+ if (type === "tool_result") {
639
+ const name = event?.tool_name || event?.data?.tool_name || "unknown";
640
+ return `Tool result: ${name}`;
641
+ }
642
+
643
+ if (type === "function_call_output" || type === "tool_output") {
644
+ const name = event?.name || event?.tool_name || event?.data?.tool_name || "unknown";
645
+ return `Tool result: ${name}`;
646
+ }
647
+
648
+ if (type === "error") {
649
+ return `Agent error: ${trimLogText(event?.error || event?.message || "unknown error", 220)}`;
650
+ }
651
+
652
+ if (type === "assistant.usage") {
653
+ const usageLine = summarizeAssistantUsage(event?.data || {});
654
+ return usageLine || "Usage update";
655
+ }
656
+
657
+ if (type === "assistant.message") {
658
+ return summarizeAssistantMessageData(event?.data || {});
659
+ }
660
+
661
+ const item = event?.item;
662
+ if (item && (type === "item.completed" || type === "item.started")) {
663
+ const itemType = String(item?.type || "").trim().toLowerCase();
664
+ const toolName =
665
+ item?.tool_name ||
666
+ item?.toolName ||
667
+ item?.name ||
668
+ item?.call?.tool_name ||
669
+ item?.call?.name ||
670
+ item?.function?.name ||
671
+ null;
672
+
673
+ if (
674
+ itemType === "tool_call" ||
675
+ itemType === "mcp_tool_call" ||
676
+ itemType === "function_call" ||
677
+ itemType === "tool_use"
678
+ ) {
679
+ return `Tool call: ${toolName || "unknown"}`;
680
+ }
681
+ if (
682
+ itemType === "tool_result" ||
683
+ itemType === "mcp_tool_result" ||
684
+ itemType === "tool_output"
685
+ ) {
686
+ return `Tool result: ${toolName || "unknown"}`;
687
+ }
688
+
689
+ const itemText = trimLogText(
690
+ extractStreamText(item?.text) ||
691
+ extractStreamText(item?.summary) ||
692
+ extractStreamText(item?.content) ||
693
+ extractStreamText(item?.message?.content) ||
694
+ extractStreamText(item?.message?.text),
695
+ 220,
696
+ );
697
+
698
+ if (itemType.includes("reason") || itemType.includes("thinking")) {
699
+ return itemText ? `Thinking: ${itemText}` : "Thinking...";
700
+ }
701
+
702
+ if (
703
+ itemType === "agent_message" ||
704
+ itemType === "assistant_message" ||
705
+ itemType === "message"
706
+ ) {
707
+ return itemText ? `Agent: ${itemText}` : "";
708
+ }
709
+
710
+ if (itemText) {
711
+ return `${itemType || "item"}: ${itemText}`;
712
+ }
713
+ }
714
+
715
+ const messageText = trimLogText(
716
+ extractStreamText(event?.message?.content) ||
717
+ extractStreamText(event?.message?.text) ||
718
+ extractStreamText(event?.content) ||
719
+ extractStreamText(event?.text) ||
720
+ extractStreamText(event?.data?.content) ||
721
+ extractStreamText(event?.data?.text) ||
722
+ extractStreamText(event?.data?.deltaContent) ||
723
+ normalizeNarrativeText(event?.data?.detailedContent, {
724
+ maxParagraphs: 1,
725
+ maxChars: 220,
726
+ }) ||
727
+ "",
728
+ 220,
729
+ );
730
+
731
+ if (messageText) {
732
+ if (
733
+ type === "agent_message" ||
734
+ type === "assistant_message" ||
735
+ type === "message" ||
736
+ type === "item.completed"
737
+ ) {
738
+ return `Agent: ${messageText}`;
739
+ }
740
+ return `${type}: ${messageText}`;
741
+ }
742
+
743
+ if (
744
+ type === "turn.complete" ||
745
+ type === "session.completed" ||
746
+ type === "response.completed"
747
+ ) {
748
+ return `Agent event: ${type}`;
749
+ }
750
+
751
+ return "";
752
+ }
753
+
754
+ function buildAgentEventPreview(items = [], streamLines = [], maxEvents = WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT) {
755
+ const lines = [];
756
+ if (Array.isArray(streamLines) && streamLines.length) {
757
+ lines.push(...streamLines);
758
+ }
759
+
760
+ if (Array.isArray(items) && items.length) {
761
+ for (const entry of items) {
762
+ const line = summarizeAgentStreamEvent(entry);
763
+ if (line) lines.push(line);
764
+ }
765
+ }
766
+
767
+ const deduped = [];
768
+ const seen = new Set();
769
+ for (const line of lines) {
770
+ const normalized = trimLogText(line, 260);
771
+ if (!normalized) continue;
772
+ const key = normalized.toLowerCase();
773
+ if (seen.has(key)) continue;
774
+ seen.add(key);
775
+ deduped.push(normalized);
776
+ }
777
+
778
+ const limit = Number.isFinite(maxEvents)
779
+ ? Math.max(10, Math.min(500, Math.trunc(maxEvents)))
780
+ : WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT;
781
+ return deduped.slice(-limit);
782
+ }
783
+
784
+ function condenseAgentItems(items = [], maxEvents = WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT) {
785
+ if (!Array.isArray(items) || items.length === 0) return [];
786
+ const limit = Number.isFinite(maxEvents)
787
+ ? Math.max(10, Math.min(500, Math.trunc(maxEvents)))
788
+ : WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT;
789
+ const slice = items.slice(-limit);
790
+ return slice.map((entry) => ({
791
+ type: String(entry?.type || entry?.item?.type || "event"),
792
+ summary:
793
+ summarizeAgentStreamEvent(entry) ||
794
+ trimLogText(
795
+ normalizeNarrativeText(
796
+ extractStreamText(entry?.message?.content) ||
797
+ extractStreamText(entry?.content) ||
798
+ extractStreamText(entry?.text) ||
799
+ extractStreamText(entry?.data?.content) ||
800
+ extractStreamText(entry?.item?.text) ||
801
+ extractStreamText(entry?.item?.content),
802
+ { maxParagraphs: 1, maxChars: 220 },
803
+ ),
804
+ 220,
805
+ ) ||
806
+ "event",
807
+ timestamp: entry?.timestamp || entry?.data?.timestamp || null,
808
+ }));
809
+ }
810
+
811
+ function buildAgentExecutionDigest(result = {}, streamLines = [], maxEvents = WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT) {
812
+ const eventPreview = buildAgentEventPreview(result?.items || [], streamLines, maxEvents);
813
+ const thoughts = eventPreview
814
+ .filter((line) => line.startsWith("Thinking:"))
815
+ .map((line) => line.replace(/^Thinking:\s*/i, "").trim())
816
+ .filter(Boolean);
817
+ const actionLines = eventPreview
818
+ .filter(
819
+ (line) =>
820
+ line.startsWith("Tool call:") ||
821
+ line.startsWith("Tool result:") ||
822
+ line.startsWith("Agent requested tools:"),
823
+ )
824
+ .map((line) =>
825
+ line
826
+ .replace(/^Tool call:\s*/i, "called ")
827
+ .replace(/^Tool result:\s*/i, "received result from ")
828
+ .replace(/^Agent requested tools:\s*/i, "requested tools ")
829
+ .trim(),
830
+ )
831
+ .filter(Boolean);
832
+ const agentMessages = eventPreview
833
+ .filter((line) => line.startsWith("Agent:"))
834
+ .map((line) => line.replace(/^Agent:\s*/i, "").trim())
835
+ .filter(Boolean);
836
+
837
+ let summary = normalizeNarrativeText(result?.output || "", { maxParagraphs: 2, maxChars: 900 });
838
+ if (!summary || summary === "(Agent completed with no text output)") {
839
+ summary = agentMessages[agentMessages.length - 1] || "";
840
+ }
841
+ if (!summary && eventPreview.length > 0) {
842
+ summary = eventPreview[eventPreview.length - 1];
843
+ }
844
+ summary = trimLogText(summary, 900);
845
+
846
+ const narrativeParts = [];
847
+ if (summary && summary !== "(Agent completed with no text output)") {
848
+ narrativeParts.push(summary);
849
+ }
850
+ if (thoughts.length) {
851
+ narrativeParts.push(`Thought process: ${thoughts.slice(0, 4).join(" ")}`);
852
+ }
853
+ if (actionLines.length) {
854
+ narrativeParts.push(`Actions: ${actionLines.slice(0, 8).join("; ")}`);
855
+ }
856
+ if (!narrativeParts.length && eventPreview.length) {
857
+ narrativeParts.push(eventPreview.slice(-3).join(" "));
858
+ }
859
+
860
+ const itemCount = Array.isArray(result?.items) ? result.items.length : 0;
861
+ const retainedItems = condenseAgentItems(result?.items || [], maxEvents);
862
+ const omittedItemCount = Math.max(0, itemCount - retainedItems.length);
863
+
864
+ return {
865
+ summary,
866
+ narrative: narrativeParts.join("\n\n").trim(),
867
+ thoughts: thoughts.slice(0, 8),
868
+ stream: eventPreview,
869
+ items: retainedItems,
870
+ itemCount,
871
+ omittedItemCount,
872
+ };
873
+ }
874
+
875
+ function normalizeLegacyWorkflowCommand(command) {
876
+ let normalized = String(command || "");
877
+ if (!normalized) return normalized;
878
+ if (/--json\s+name,state,conclusion\b/i.test(normalized)) {
879
+ normalized = normalized.replace(/--json\s+name,state,conclusion\b/gi, "--json name,state");
880
+ }
881
+ if (/grep\s+-c\s+worktree/i.test(normalized)) {
882
+ normalized = /git\s+worktree\s+prune/i.test(normalized)
883
+ ? PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND
884
+ : PORTABLE_WORKTREE_COUNT_COMMAND;
885
+ }
886
+ return normalized;
887
+ }
888
+
889
+ function resolveWorkflowNodeValue(value, ctx) {
890
+ if (typeof value === "string") return ctx.resolve(value);
891
+ if (Array.isArray(value)) {
892
+ return value.map((item) => resolveWorkflowNodeValue(item, ctx));
893
+ }
894
+ if (value && typeof value === "object") {
895
+ const resolved = {};
896
+ for (const [key, entry] of Object.entries(value)) {
897
+ resolved[key] = resolveWorkflowNodeValue(entry, ctx);
898
+ }
899
+ return resolved;
900
+ }
901
+ return value;
902
+ }
903
+
904
+ function parseBooleanSetting(value, defaultValue = false) {
905
+ if (value == null || value === "") return defaultValue;
906
+ if (typeof value === "boolean") return value;
907
+ if (typeof value === "number") return value !== 0;
908
+ if (typeof value === "string") {
909
+ const normalized = value.trim().toLowerCase();
910
+ if (!normalized) return defaultValue;
911
+ if (["true", "1", "yes", "y", "on"].includes(normalized)) return true;
912
+ if (["false", "0", "no", "n", "off"].includes(normalized)) return false;
913
+ }
914
+ return defaultValue;
915
+ }
916
+
917
+ function getPathValue(value, pathExpression) {
918
+ const path = String(pathExpression || "").trim();
919
+ if (!path) return undefined;
920
+ const parts = path
921
+ .split(".")
922
+ .map((part) => String(part || "").trim())
923
+ .filter(Boolean);
924
+ if (parts.length === 0) return undefined;
925
+
926
+ let cursor = value;
927
+ for (const part of parts) {
928
+ if (cursor == null) return undefined;
929
+ if (Array.isArray(cursor)) {
930
+ const idx = Number.parseInt(part, 10);
931
+ if (!Number.isFinite(idx)) return undefined;
932
+ cursor = cursor[idx];
933
+ continue;
934
+ }
935
+ if (typeof cursor !== "object") return undefined;
936
+ cursor = cursor[part];
937
+ }
938
+ return cursor;
939
+ }
940
+
941
+ function collectWakePhraseCandidates(payload, payloadField = "") {
942
+ const candidates = [];
943
+ const seen = new Set();
944
+
945
+ const appendCandidate = (field, rawValue) => {
946
+ if (rawValue == null) return;
947
+ if (Array.isArray(rawValue)) {
948
+ rawValue.forEach((entry, idx) => appendCandidate(`${field}[${idx}]`, entry));
949
+ return;
950
+ }
951
+ if (typeof rawValue === "object") {
952
+ if (typeof rawValue.content === "string") {
953
+ appendCandidate(`${field}.content`, rawValue.content);
954
+ }
955
+ if (typeof rawValue.text === "string") {
956
+ appendCandidate(`${field}.text`, rawValue.text);
957
+ }
958
+ if (typeof rawValue.transcript === "string") {
959
+ appendCandidate(`${field}.transcript`, rawValue.transcript);
960
+ }
961
+ return;
962
+ }
963
+
964
+ const text = String(rawValue).trim();
965
+ if (!text) return;
966
+ const key = `${field}::${text}`;
967
+ if (seen.has(key)) return;
968
+ seen.add(key);
969
+ candidates.push({ field, text });
970
+ };
971
+
972
+ if (payloadField) {
973
+ appendCandidate(payloadField, getPathValue(payload, payloadField));
974
+ return candidates;
975
+ }
976
+
977
+ const commonFields = [
978
+ "content",
979
+ "text",
980
+ "transcript",
981
+ "message",
982
+ "utterance",
983
+ "payload.content",
984
+ "payload.text",
985
+ "payload.transcript",
986
+ "event.content",
987
+ "event.text",
988
+ "event.transcript",
989
+ "voice.content",
990
+ "voice.transcript",
991
+ "meta.transcript",
992
+ ];
993
+ for (const field of commonFields) {
994
+ appendCandidate(field, getPathValue(payload, field));
995
+ }
996
+
997
+ const messages = Array.isArray(payload?.messages) ? payload.messages : [];
998
+ messages.forEach((entry, idx) => appendCandidate(`messages[${idx}]`, entry));
999
+
1000
+ const transcriptEvents = Array.isArray(payload?.transcriptEvents) ? payload.transcriptEvents : [];
1001
+ transcriptEvents.forEach((entry, idx) => appendCandidate(`transcriptEvents[${idx}]`, entry));
1002
+
1003
+ return candidates;
1004
+ }
1005
+
1006
+ function detectWakePhraseMatch(text, phrase, options = {}) {
1007
+ const mode = String(options.mode || "contains").trim().toLowerCase() || "contains";
1008
+ const caseSensitive = options.caseSensitive === true;
1009
+ const source = String(text || "");
1010
+ const target = String(phrase || "");
1011
+
1012
+ if (!source || !target) return { matched: false, mode };
1013
+
1014
+ const sourceNormalized = caseSensitive ? source : source.toLowerCase();
1015
+ const targetNormalized = caseSensitive ? target : target.toLowerCase();
1016
+
1017
+ if (mode === "exact") {
1018
+ return { matched: sourceNormalized.trim() === targetNormalized.trim(), mode };
1019
+ }
1020
+ if (mode === "starts_with") {
1021
+ return { matched: sourceNormalized.trimStart().startsWith(targetNormalized), mode };
1022
+ }
1023
+ if (mode === "regex") {
1024
+ try {
1025
+ const regex = new RegExp(target, caseSensitive ? "" : "i");
1026
+ return { matched: regex.test(source), mode };
1027
+ } catch (err) {
1028
+ return {
1029
+ matched: false,
1030
+ mode,
1031
+ error: `invalid regex: ${err?.message || err}`,
1032
+ };
1033
+ }
1034
+ }
1035
+ return { matched: sourceNormalized.includes(targetNormalized), mode: "contains" };
1036
+ }
1037
+
1038
+ function normalizeWorkflowStack(value) {
1039
+ if (!Array.isArray(value)) return [];
1040
+ return value
1041
+ .map((entry) => String(entry || "").trim())
1042
+ .filter(Boolean);
1043
+ }
1044
+
1045
+ function isBosunStateComment(text) {
1046
+ const raw = String(text || "").toLowerCase();
1047
+ return raw.includes("bosun-state") || raw.includes("codex:ignore");
1048
+ }
1049
+
1050
+ function normalizeTaskComments(task, maxComments = 6) {
1051
+ if (!task) return [];
1052
+ const raw = Array.isArray(task.comments)
1053
+ ? task.comments
1054
+ : Array.isArray(task.meta?.comments)
1055
+ ? task.meta.comments
1056
+ : [];
1057
+ const normalized = raw
1058
+ .map((comment) => {
1059
+ const body = typeof comment === "string"
1060
+ ? comment
1061
+ : comment.body || comment.text || comment.content || "";
1062
+ const trimmed = String(body || "").trim();
1063
+ if (!trimmed || isBosunStateComment(trimmed)) return null;
1064
+ return {
1065
+ author: comment?.author || comment?.user || null,
1066
+ createdAt: comment?.createdAt || comment?.created_at || null,
1067
+ body: trimmed.replace(/\s+/g, " ").slice(0, 600),
1068
+ };
1069
+ })
1070
+ .filter(Boolean);
1071
+ if (normalized.length <= maxComments) return normalized;
1072
+ return normalized.slice(-maxComments);
1073
+ }
1074
+
1075
+ function normalizeTaskAttachments(task, maxAttachments = 10) {
1076
+ if (!task) return [];
1077
+ const combined = []
1078
+ .concat(Array.isArray(task.attachments) ? task.attachments : [])
1079
+ .concat(Array.isArray(task.meta?.attachments) ? task.meta.attachments : []);
1080
+ if (combined.length <= maxAttachments) return combined;
1081
+ return combined.slice(0, maxAttachments);
1082
+ }
1083
+
1084
+ function formatAttachmentLine(att) {
1085
+ const name = att.name || att.filename || att.title || "attachment";
1086
+ const kind = att.kind ? ` (${att.kind})` : "";
1087
+ const location = att.url || att.filePath || att.path || "";
1088
+ const suffix = location ? ` — ${location}` : "";
1089
+ return `- ${name}${kind}${suffix}`;
1090
+ }
1091
+
1092
+ function formatCommentLine(comment) {
1093
+ const author = comment.author ? `@${comment.author}` : "comment";
1094
+ const when = comment.createdAt ? ` (${comment.createdAt})` : "";
1095
+ return `- ${author}${when}: ${comment.body}`;
1096
+ }
1097
+
1098
+ function buildTaskContextBlock(task) {
1099
+ if (!task) return "";
1100
+ const comments = normalizeTaskComments(task);
1101
+ const attachments = normalizeTaskAttachments(task);
1102
+ if (!comments.length && !attachments.length) return "";
1103
+ const lines = ["## Task Context"];
1104
+ if (comments.length) {
1105
+ lines.push("### Comments");
1106
+ for (const comment of comments) lines.push(formatCommentLine(comment));
1107
+ }
1108
+ if (attachments.length) {
1109
+ lines.push("### Attachments");
1110
+ for (const attachment of attachments) lines.push(formatAttachmentLine(attachment));
1111
+ }
1112
+ return lines.join("\n");
1113
+ }
1114
+
1115
+ function buildWorkflowAgentToolContract(rootDir, agentProfileId = "") {
1116
+ const profileId = String(agentProfileId || "").trim();
1117
+ const effective = profileId
1118
+ ? getEffectiveTools(rootDir, profileId)
1119
+ : getEffectiveTools(rootDir, "__default__");
1120
+ const rawCfg = profileId ? getAgentToolConfig(rootDir, profileId) : null;
1121
+ const enabledBuiltinTools = (Array.isArray(effective?.builtinTools) ? effective.builtinTools : [])
1122
+ .filter((tool) => tool?.enabled)
1123
+ .map((tool) => ({
1124
+ id: String(tool?.id || "").trim(),
1125
+ name: String(tool?.name || "").trim(),
1126
+ description: String(tool?.description || "").trim(),
1127
+ }))
1128
+ .filter((tool) => tool.id);
1129
+ const enabledMcpServers = Array.isArray(rawCfg?.enabledMcpServers)
1130
+ ? rawCfg.enabledMcpServers.map((id) => String(id || "").trim()).filter(Boolean)
1131
+ : [];
1132
+ const manifest = {
1133
+ agentProfileId: profileId || null,
1134
+ enabledBuiltinTools,
1135
+ enabledMcpServers,
1136
+ toolBridge: {
1137
+ module: "./voice-tools.mjs",
1138
+ function: "executeToolCall(toolName, args, context)",
1139
+ quickUse: "node -e \"import('../voice/voice-tools.mjs').then(async m=>{const r=await m.executeToolCall('get_workspace_context', {}, {});console.log(r?.result||r);})\"",
1140
+ },
1141
+ };
1142
+ return [
1143
+ "## Tool Capability Contract",
1144
+ "Use enabled tools by default before claiming work is blocked.",
1145
+ "Enabled tools JSON:",
1146
+ "```json",
1147
+ JSON.stringify(manifest, null, 2),
1148
+ "```",
1149
+ "When uncertain about arguments, call get_admin_help via executeToolCall.",
1150
+ ].join("\n");
1151
+ }
1152
+
1153
+ // ═══════════════════════════════════════════════════════════════════════════
1154
+ // TRIGGERS — Events that initiate a workflow
1155
+ // ═══════════════════════════════════════════════════════════════════════════
1156
+
1157
+ export { registerNodeType };
1158
+ export {
1159
+ BOSUN_ATTACHED_PR_LABEL,
1160
+ PORTABLE_PRUNE_AND_COUNT_WORKTREES_COMMAND,
1161
+ PORTABLE_WORKTREE_COUNT_COMMAND,
1162
+ TAG,
1163
+ WORKFLOW_AGENT_EVENT_PREVIEW_LIMIT,
1164
+ WORKFLOW_AGENT_HEARTBEAT_MS,
1165
+ WORKFLOW_TELEGRAM_ICON_MAP,
1166
+ bindTaskContext,
1167
+ buildAgentEventPreview,
1168
+ buildAgentExecutionDigest,
1169
+ buildGitExecutionEnv,
1170
+ buildTaskContextBlock,
1171
+ buildWorkflowAgentToolContract,
1172
+ collectWakePhraseCandidates,
1173
+ condenseAgentItems,
1174
+ createKanbanTaskWithProject,
1175
+ decodeWorkflowUnicodeIconToken,
1176
+ deriveManagedWorktreeDirName,
1177
+ detectWakePhraseMatch,
1178
+ execGitArgsSync,
1179
+ extractStreamText,
1180
+ extractSymbolHint,
1181
+ formatAttachmentLine,
1182
+ formatCommentLine,
1183
+ getPathValue,
1184
+ isBosunStateComment,
1185
+ isManagedBosunWorktree,
1186
+ makeIsolatedGitEnv,
1187
+ normalizeLegacyWorkflowCommand,
1188
+ normalizeLineEndings,
1189
+ normalizeNarrativeText,
1190
+ normalizePrEventName,
1191
+ normalizeTaskAttachments,
1192
+ normalizeTaskComments,
1193
+ normalizeWorkflowStack,
1194
+ normalizeWorkflowTelegramText,
1195
+ evaluateTaskAssignedTriggerConfig,
1196
+ parseBooleanSetting,
1197
+ parsePathListingLine,
1198
+ resolveGitCandidates,
1199
+ resolveWorkflowNodeValue,
1200
+ simplifyPathLabel,
1201
+ summarizeAgentStreamEvent,
1202
+ summarizeAssistantMessageData,
1203
+ summarizeAssistantUsage,
1204
+ summarizePathListingBlock,
1205
+ trimLogText,
1206
+ };
1207
+