bosun 0.33.0 → 0.33.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/.env.example +6 -0
- package/agent-event-bus.mjs +1 -0
- package/agent-prompts.mjs +6 -0
- package/config.mjs +84 -22
- package/kanban-adapter.mjs +258 -3
- package/library-manager.mjs +727 -0
- package/monitor.mjs +3 -0
- package/package.json +13 -1
- package/primary-agent.mjs +46 -1
- package/repo-root.mjs +39 -20
- package/review-agent.mjs +9 -4
- package/session-tracker.mjs +1 -0
- package/setup-web-server.mjs +97 -10
- package/setup.mjs +31 -0
- package/shared-state-manager.mjs +83 -10
- package/task-attachments.mjs +187 -0
- package/task-executor.mjs +186 -4
- package/ui/app.js +3 -1
- package/ui/components/chat-view.js +132 -5
- package/ui/demo.html +600 -30
- package/ui/index.html +0 -1
- package/ui/modules/api.js +4 -1
- package/ui/modules/icon-utils.js +7 -0
- package/ui/modules/icons.js +13 -0
- package/ui/modules/router.js +3 -2
- package/ui/modules/streaming.js +4 -3
- package/ui/setup.html +22 -8
- package/ui/styles/components.css +119 -11
- package/ui/styles/layout.css +31 -0
- package/ui/styles/sessions.css +117 -30
- package/ui/styles/variables.css +1 -1
- package/ui/styles.css +1 -0
- package/ui/tabs/agents.js +14 -4
- package/ui/tabs/control.js +30 -3
- package/ui/tabs/library.js +695 -0
- package/ui/tabs/tasks.js +213 -0
- package/ui/tabs/workflows.js +27 -2
- package/ui-server.mjs +575 -21
- package/workflow-engine.mjs +822 -0
- package/workflow-migration.mjs +396 -0
- package/workflow-nodes.mjs +1811 -0
- package/workflow-templates/_helpers.mjs +61 -0
- package/workflow-templates/agents.mjs +425 -0
- package/workflow-templates/ci-cd.mjs +77 -0
- package/workflow-templates/github.mjs +432 -0
- package/workflow-templates/planning.mjs +266 -0
- package/workflow-templates/reliability.mjs +351 -0
- package/workflow-templates.mjs +243 -0
- package/workspace-manager.mjs +29 -10
package/.env.example
CHANGED
|
@@ -435,6 +435,12 @@ TELEGRAM_MINIAPP_ENABLED=false
|
|
|
435
435
|
# BOSUN_ENFORCE_TASK_LABEL=true
|
|
436
436
|
# Optional issue fetch cap per sync/poll cycle (default: 1000)
|
|
437
437
|
# GITHUB_ISSUES_LIST_LIMIT=1000
|
|
438
|
+
# Task context limits (comments + attachments)
|
|
439
|
+
# BOSUN_TASK_CONTEXT_MAX_COMMENTS=8
|
|
440
|
+
# BOSUN_TASK_CONTEXT_MAX_COMMENT_CHARS=1200
|
|
441
|
+
# BOSUN_TASK_CONTEXT_MAX_ATTACHMENTS=20
|
|
442
|
+
# Max upload size for task/chat attachments (MB)
|
|
443
|
+
# BOSUN_ATTACHMENT_MAX_MB=25
|
|
438
444
|
|
|
439
445
|
# Jira backend (KANBAN_BACKEND=jira)
|
|
440
446
|
# Jira Cloud site URL (no trailing slash)
|
package/agent-event-bus.mjs
CHANGED
|
@@ -785,6 +785,7 @@ export class AgentEventBus {
|
|
|
785
785
|
branchName:
|
|
786
786
|
result?.branch || task?.branchName || task?.meta?.branch_name,
|
|
787
787
|
description: task?.description || "",
|
|
788
|
+
taskContext: task?._taskContextBlock || task?.meta?.taskContextBlock || "",
|
|
788
789
|
});
|
|
789
790
|
this.emit(AGENT_EVENT.AUTO_REVIEW, taskId, {
|
|
790
791
|
title: task?.title || "",
|
package/agent-prompts.mjs
CHANGED
|
@@ -240,6 +240,7 @@ You are the always-on reliability guardian for bosun in devmode.
|
|
|
240
240
|
|
|
241
241
|
## Description
|
|
242
242
|
{{TASK_DESCRIPTION}}
|
|
243
|
+
{{TASK_CONTEXT}}
|
|
243
244
|
|
|
244
245
|
## Environment
|
|
245
246
|
- Working Directory: {{WORKTREE_PATH}}
|
|
@@ -326,6 +327,7 @@ Please:
|
|
|
326
327
|
|
|
327
328
|
Original task description:
|
|
328
329
|
{{TASK_DESCRIPTION}}
|
|
330
|
+
{{TASK_CONTEXT}}
|
|
329
331
|
`,
|
|
330
332
|
taskExecutorContinueHasCommits: `# {{TASK_ID}} — CONTINUE (Verify and Push)
|
|
331
333
|
|
|
@@ -336,6 +338,7 @@ You already made commits.
|
|
|
336
338
|
2. If passing, push: git push origin HEAD
|
|
337
339
|
3. If failing, fix issues, commit, and push.
|
|
338
340
|
4. Task is not complete until push succeeds.
|
|
341
|
+
{{TASK_CONTEXT}}
|
|
339
342
|
`,
|
|
340
343
|
taskExecutorContinueHasEdits: `# {{TASK_ID}} — CONTINUE (Commit and Push)
|
|
341
344
|
|
|
@@ -346,6 +349,7 @@ You made file edits but no commit yet.
|
|
|
346
349
|
2. Run relevant tests.
|
|
347
350
|
3. Commit with conventional format.
|
|
348
351
|
4. Push: git push origin HEAD
|
|
352
|
+
{{TASK_CONTEXT}}
|
|
349
353
|
`,
|
|
350
354
|
taskExecutorContinueNoProgress: `# CONTINUE - Resume Implementation
|
|
351
355
|
|
|
@@ -360,6 +364,7 @@ Execute now:
|
|
|
360
364
|
|
|
361
365
|
Task: {{TASK_TITLE}}
|
|
362
366
|
Description: {{TASK_DESCRIPTION}}
|
|
367
|
+
{{TASK_CONTEXT}}
|
|
363
368
|
`,
|
|
364
369
|
reviewer: `You are a senior code reviewer for a production software project.
|
|
365
370
|
|
|
@@ -385,6 +390,7 @@ Review the following PR diff for CRITICAL issues ONLY.
|
|
|
385
390
|
|
|
386
391
|
## Task Description
|
|
387
392
|
{{TASK_DESCRIPTION}}
|
|
393
|
+
{{TASK_CONTEXT}}
|
|
388
394
|
|
|
389
395
|
## Response Format
|
|
390
396
|
Respond with JSON only:
|
package/config.mjs
CHANGED
|
@@ -65,7 +65,8 @@ function isWslInteropRuntime() {
|
|
|
65
65
|
}
|
|
66
66
|
|
|
67
67
|
function resolveConfigDir(repoRoot) {
|
|
68
|
-
// 1. Explicit env override
|
|
68
|
+
// 1. Explicit env override (BOSUN_HOME supersedes BOSUN_DIR; both are aliases)
|
|
69
|
+
if (process.env.BOSUN_HOME) return resolve(process.env.BOSUN_HOME);
|
|
69
70
|
if (process.env.BOSUN_DIR) return resolve(process.env.BOSUN_DIR);
|
|
70
71
|
|
|
71
72
|
// 2. Platform-aware user home
|
|
@@ -86,6 +87,55 @@ function resolveConfigDir(repoRoot) {
|
|
|
86
87
|
return resolve(baseDir, "bosun");
|
|
87
88
|
}
|
|
88
89
|
|
|
90
|
+
function getConfigSearchDirs(repoRoot) {
|
|
91
|
+
const dirs = new Set();
|
|
92
|
+
if (process.env.BOSUN_HOME) dirs.add(resolve(process.env.BOSUN_HOME));
|
|
93
|
+
if (process.env.BOSUN_DIR) dirs.add(resolve(process.env.BOSUN_DIR));
|
|
94
|
+
dirs.add(resolveConfigDir(repoRoot));
|
|
95
|
+
if (process.env.APPDATA) dirs.add(resolve(process.env.APPDATA, "bosun"));
|
|
96
|
+
if (process.env.LOCALAPPDATA) dirs.add(resolve(process.env.LOCALAPPDATA, "bosun"));
|
|
97
|
+
if (process.env.USERPROFILE) dirs.add(resolve(process.env.USERPROFILE, "bosun"));
|
|
98
|
+
if (process.env.HOME) dirs.add(resolve(process.env.HOME, "bosun"));
|
|
99
|
+
return [...dirs].filter(Boolean);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function collectRepoPathsFromConfig(cfg, configDir) {
|
|
103
|
+
const paths = [];
|
|
104
|
+
const pushPath = (path) => {
|
|
105
|
+
if (!path) return;
|
|
106
|
+
paths.push(isAbsolute(path) ? path : resolve(configDir, path));
|
|
107
|
+
};
|
|
108
|
+
|
|
109
|
+
const repos = cfg.repositories || cfg.repos || [];
|
|
110
|
+
if (Array.isArray(repos)) {
|
|
111
|
+
for (const repo of repos) {
|
|
112
|
+
const repoPath = typeof repo === "string" ? repo : (repo?.path || repo?.repoRoot);
|
|
113
|
+
pushPath(repoPath);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const workspaces = cfg.workspaces || [];
|
|
118
|
+
if (Array.isArray(workspaces)) {
|
|
119
|
+
for (const ws of workspaces) {
|
|
120
|
+
const wsBase = ws?.path
|
|
121
|
+
? (isAbsolute(ws.path) ? ws.path : resolve(configDir, ws.path))
|
|
122
|
+
: (ws?.id ? resolve(configDir, "workspaces", ws.id) : null);
|
|
123
|
+
const wsRepos = ws?.repos || ws?.repositories || [];
|
|
124
|
+
if (!Array.isArray(wsRepos)) continue;
|
|
125
|
+
for (const repo of wsRepos) {
|
|
126
|
+
const repoPath = typeof repo === "string" ? repo : (repo?.path || repo?.repoRoot);
|
|
127
|
+
if (repoPath) {
|
|
128
|
+
pushPath(repoPath);
|
|
129
|
+
continue;
|
|
130
|
+
}
|
|
131
|
+
if (wsBase && repo?.name) pushPath(resolve(wsBase, repo.name));
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return paths;
|
|
137
|
+
}
|
|
138
|
+
|
|
89
139
|
function ensurePromptWorkspaceGitIgnore(repoRoot) {
|
|
90
140
|
const gitignorePath = resolve(repoRoot, ".gitignore");
|
|
91
141
|
const entry = "/.bosun/";
|
|
@@ -575,33 +625,26 @@ function detectRepoRoot() {
|
|
|
575
625
|
}
|
|
576
626
|
|
|
577
627
|
// 4. Check bosun config for workspace repos
|
|
578
|
-
const
|
|
628
|
+
const configDirs = getConfigSearchDirs();
|
|
629
|
+
let fallbackRepo = null;
|
|
579
630
|
for (const cfgName of CONFIG_FILES) {
|
|
580
|
-
const
|
|
581
|
-
|
|
631
|
+
for (const configDir of configDirs) {
|
|
632
|
+
const cfgPath = resolve(configDir, cfgName);
|
|
633
|
+
if (!existsSync(cfgPath)) continue;
|
|
582
634
|
try {
|
|
583
635
|
const cfg = JSON.parse(readFileSync(cfgPath, "utf8"));
|
|
584
|
-
|
|
585
|
-
const
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
if (repoPath && existsSync(resolve(repoPath))) return resolve(repoPath);
|
|
636
|
+
const repoPaths = collectRepoPathsFromConfig(cfg, configDir);
|
|
637
|
+
for (const repoPath of repoPaths) {
|
|
638
|
+
if (!repoPath || !existsSync(repoPath)) continue;
|
|
639
|
+
if (existsSync(resolve(repoPath, ".git"))) return repoPath;
|
|
640
|
+
fallbackRepo ??= repoPath;
|
|
590
641
|
}
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
const ws = workspaces[0];
|
|
595
|
-
const wsRepos = ws?.repos || ws?.repositories || [];
|
|
596
|
-
if (Array.isArray(wsRepos) && wsRepos.length > 0) {
|
|
597
|
-
const primary = wsRepos.find((r) => r.primary) || wsRepos[0];
|
|
598
|
-
const repoPath = primary?.path || primary?.repoRoot;
|
|
599
|
-
if (repoPath && existsSync(resolve(repoPath))) return resolve(repoPath);
|
|
600
|
-
}
|
|
601
|
-
}
|
|
602
|
-
} catch { /* invalid config */ }
|
|
642
|
+
} catch {
|
|
643
|
+
/* invalid config */
|
|
644
|
+
}
|
|
603
645
|
}
|
|
604
646
|
}
|
|
647
|
+
if (fallbackRepo) return fallbackRepo;
|
|
605
648
|
|
|
606
649
|
// 5. Final fallback — warn and return cwd. This is unlikely to be a valid
|
|
607
650
|
// git repo (e.g. when the daemon spawns with cwd=homedir), but returning
|
|
@@ -2069,6 +2112,25 @@ export function loadConfig(argv = process.argv, options = {}) {
|
|
|
2069
2112
|
|
|
2070
2113
|
// First run
|
|
2071
2114
|
isFirstRun,
|
|
2115
|
+
|
|
2116
|
+
// Security controls
|
|
2117
|
+
security: Object.freeze({
|
|
2118
|
+
// List of trusted issue creators (primary GitHub account or configured list)
|
|
2119
|
+
trustedCreators:
|
|
2120
|
+
configData.trustedCreators ||
|
|
2121
|
+
process.env.BOSUN_TRUSTED_CREATORS?.split(",") ||
|
|
2122
|
+
[],
|
|
2123
|
+
// Enforce all new tasks go to backlog unless planner config allows auto-push
|
|
2124
|
+
enforceBacklog:
|
|
2125
|
+
typeof configData.enforceBacklog === "boolean"
|
|
2126
|
+
? configData.enforceBacklog
|
|
2127
|
+
: true,
|
|
2128
|
+
// Control agent triggers: restrict agent activation to trusted sources
|
|
2129
|
+
agentTriggerControl:
|
|
2130
|
+
typeof configData.agentTriggerControl === "boolean"
|
|
2131
|
+
? configData.agentTriggerControl
|
|
2132
|
+
: true,
|
|
2133
|
+
}),
|
|
2072
2134
|
};
|
|
2073
2135
|
|
|
2074
2136
|
return Object.freeze(config);
|
package/kanban-adapter.mjs
CHANGED
|
@@ -63,6 +63,10 @@ import {
|
|
|
63
63
|
removeTask as removeInternalTask,
|
|
64
64
|
updateTask as patchInternalTask,
|
|
65
65
|
} from "./task-store.mjs";
|
|
66
|
+
import {
|
|
67
|
+
listTaskAttachments,
|
|
68
|
+
mergeTaskAttachments,
|
|
69
|
+
} from "./task-attachments.mjs";
|
|
66
70
|
import { randomUUID } from "node:crypto";
|
|
67
71
|
|
|
68
72
|
const TAG = "[kanban]";
|
|
@@ -269,6 +273,149 @@ function extractPrFromText(text) {
|
|
|
269
273
|
return null;
|
|
270
274
|
}
|
|
271
275
|
|
|
276
|
+
function isBosunStateComment(text) {
|
|
277
|
+
const raw = String(text || "").toLowerCase();
|
|
278
|
+
if (!raw) return false;
|
|
279
|
+
return raw.includes("bosun-state") || raw.includes("codex:ignore");
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
function normalizeAttachmentName(name, url) {
|
|
283
|
+
const raw = String(name || "").trim();
|
|
284
|
+
if (raw && !/^(?:https?:)?\/\//i.test(raw)) return raw;
|
|
285
|
+
if (!url) return raw || "attachment";
|
|
286
|
+
try {
|
|
287
|
+
const parsed = new URL(url);
|
|
288
|
+
const base = decodeURIComponent(parsed.pathname.split("/").pop() || "");
|
|
289
|
+
return base || raw || "attachment";
|
|
290
|
+
} catch {
|
|
291
|
+
const parts = String(url).split("/").filter(Boolean);
|
|
292
|
+
return parts[parts.length - 1] || raw || "attachment";
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
function guessAttachmentKind(url, name, isImage) {
|
|
297
|
+
const text = `${url || ""} ${name || ""}`.toLowerCase();
|
|
298
|
+
if (isImage) return "image";
|
|
299
|
+
if (/\.(png|jpe?g|gif|webp|bmp|svg)(\?|#|$)/i.test(text)) return "image";
|
|
300
|
+
return "file";
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function guessContentType(name, kind) {
|
|
304
|
+
const text = String(name || "").toLowerCase();
|
|
305
|
+
if (kind === "image") {
|
|
306
|
+
if (text.endsWith(".png")) return "image/png";
|
|
307
|
+
if (text.endsWith(".jpg") || text.endsWith(".jpeg")) return "image/jpeg";
|
|
308
|
+
if (text.endsWith(".gif")) return "image/gif";
|
|
309
|
+
if (text.endsWith(".webp")) return "image/webp";
|
|
310
|
+
if (text.endsWith(".svg")) return "image/svg+xml";
|
|
311
|
+
}
|
|
312
|
+
if (text.endsWith(".pdf")) return "application/pdf";
|
|
313
|
+
if (text.endsWith(".json")) return "application/json";
|
|
314
|
+
if (text.endsWith(".csv")) return "text/csv";
|
|
315
|
+
if (text.endsWith(".txt") || text.endsWith(".log")) return "text/plain";
|
|
316
|
+
if (text.endsWith(".zip")) return "application/zip";
|
|
317
|
+
if (text.endsWith(".xlsx")) return "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
|
|
318
|
+
if (text.endsWith(".docx")) return "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
|
|
319
|
+
return kind === "image" ? "image/*" : "application/octet-stream";
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
function isLikelyAttachmentUrl(url, isImage = false) {
|
|
323
|
+
if (!url) return false;
|
|
324
|
+
const text = String(url).toLowerCase();
|
|
325
|
+
if (isImage) return true;
|
|
326
|
+
if (text.includes("user-images.githubusercontent.com")) return true;
|
|
327
|
+
if (text.includes("github.com/user-attachments")) return true;
|
|
328
|
+
if (text.includes("/attachments/")) return true;
|
|
329
|
+
if (text.includes("/files/")) return true;
|
|
330
|
+
return /\.(png|jpe?g|gif|webp|bmp|svg|pdf|json|csv|txt|log|zip|gz|tgz|xz|tar|rar|7z|docx?|xlsx?|pptx?)(\?|#|$)/i.test(text);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
function extractMarkdownLinks(text) {
|
|
334
|
+
const raw = String(text || "");
|
|
335
|
+
if (!raw) return [];
|
|
336
|
+
const results = [];
|
|
337
|
+
const imageRe = /!\[([^\]]*)\]\(([^)]+)\)/g;
|
|
338
|
+
const linkRe = /\[([^\]]+)\]\(([^)]+)\)/g;
|
|
339
|
+
const htmlImgRe = /<img[^>]+src=["']([^"']+)["'][^>]*>/gi;
|
|
340
|
+
const htmlLinkRe = /<a[^>]+href=["']([^"']+)["'][^>]*>/gi;
|
|
341
|
+
const urlRe = /\bhttps?:\/\/[^\s<>()]+/gi;
|
|
342
|
+
|
|
343
|
+
let match;
|
|
344
|
+
while ((match = imageRe.exec(raw))) {
|
|
345
|
+
const url = String(match[2] || "").trim().split(/\s+/)[0];
|
|
346
|
+
if (url) results.push({ url, label: match[1] || "", isImage: true });
|
|
347
|
+
}
|
|
348
|
+
while ((match = linkRe.exec(raw))) {
|
|
349
|
+
const url = String(match[2] || "").trim().split(/\s+/)[0];
|
|
350
|
+
if (url) results.push({ url, label: match[1] || "", isImage: false });
|
|
351
|
+
}
|
|
352
|
+
while ((match = htmlImgRe.exec(raw))) {
|
|
353
|
+
const url = String(match[1] || "").trim();
|
|
354
|
+
if (url) results.push({ url, label: "", isImage: true });
|
|
355
|
+
}
|
|
356
|
+
while ((match = htmlLinkRe.exec(raw))) {
|
|
357
|
+
const url = String(match[1] || "").trim();
|
|
358
|
+
if (url) results.push({ url, label: "", isImage: false });
|
|
359
|
+
}
|
|
360
|
+
while ((match = urlRe.exec(raw))) {
|
|
361
|
+
const url = String(match[0] || "").trim();
|
|
362
|
+
if (url) results.push({ url, label: "", isImage: false });
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
return results;
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
function extractAttachmentsFromText(text, meta = {}) {
|
|
369
|
+
const links = extractMarkdownLinks(text);
|
|
370
|
+
const attachments = [];
|
|
371
|
+
for (const link of links) {
|
|
372
|
+
if (!isLikelyAttachmentUrl(link.url, link.isImage)) continue;
|
|
373
|
+
const name = normalizeAttachmentName(link.label, link.url);
|
|
374
|
+
const kind = guessAttachmentKind(link.url, name, link.isImage);
|
|
375
|
+
attachments.push({
|
|
376
|
+
id: meta?.id || null,
|
|
377
|
+
name,
|
|
378
|
+
url: link.url,
|
|
379
|
+
kind,
|
|
380
|
+
contentType: meta?.contentType || guessContentType(name, kind),
|
|
381
|
+
size: meta?.size ?? null,
|
|
382
|
+
source: meta?.source || "unknown",
|
|
383
|
+
sourceType: meta?.sourceType || null,
|
|
384
|
+
commentId: meta?.commentId || null,
|
|
385
|
+
author: meta?.author || null,
|
|
386
|
+
createdAt: meta?.createdAt || null,
|
|
387
|
+
});
|
|
388
|
+
}
|
|
389
|
+
return attachments;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function normalizeCommentList(comments) {
|
|
393
|
+
if (!Array.isArray(comments)) return [];
|
|
394
|
+
return comments
|
|
395
|
+
.map((comment) => {
|
|
396
|
+
const body =
|
|
397
|
+
comment?.body ||
|
|
398
|
+
comment?.bodyText ||
|
|
399
|
+
comment?.body_html ||
|
|
400
|
+
comment?.text ||
|
|
401
|
+
"";
|
|
402
|
+
const trimmed = String(body || "").trim();
|
|
403
|
+
if (!trimmed || isBosunStateComment(trimmed)) return null;
|
|
404
|
+
return {
|
|
405
|
+
id: comment?.id || comment?.databaseId || null,
|
|
406
|
+
author:
|
|
407
|
+
comment?.author?.login ||
|
|
408
|
+
comment?.author?.displayName ||
|
|
409
|
+
comment?.author?.name ||
|
|
410
|
+
null,
|
|
411
|
+
createdAt: comment?.createdAt || comment?.created || null,
|
|
412
|
+
body: trimmed,
|
|
413
|
+
url: comment?.url || null,
|
|
414
|
+
};
|
|
415
|
+
})
|
|
416
|
+
.filter(Boolean);
|
|
417
|
+
}
|
|
418
|
+
|
|
272
419
|
class InternalAdapter {
|
|
273
420
|
constructor() {
|
|
274
421
|
this.name = "internal";
|
|
@@ -291,6 +438,18 @@ class InternalAdapter {
|
|
|
291
438
|
extractBaseBranchFromLabels(labelBag) ||
|
|
292
439
|
extractBaseBranchFromText(task.description || task.body || ""),
|
|
293
440
|
);
|
|
441
|
+
const rawComments = Array.isArray(task.meta?.comments)
|
|
442
|
+
? task.meta.comments
|
|
443
|
+
: [];
|
|
444
|
+
const normalizedComments = normalizeCommentList(rawComments);
|
|
445
|
+
const existingAttachments = []
|
|
446
|
+
.concat(Array.isArray(task.attachments) ? task.attachments : [])
|
|
447
|
+
.concat(Array.isArray(task.meta?.attachments) ? task.meta.attachments : []);
|
|
448
|
+
const localAttachments = listTaskAttachments(task.id, "internal");
|
|
449
|
+
const mergedAttachments = mergeTaskAttachments(
|
|
450
|
+
existingAttachments,
|
|
451
|
+
localAttachments,
|
|
452
|
+
);
|
|
294
453
|
return {
|
|
295
454
|
id: String(task.id || ""),
|
|
296
455
|
title: task.title || "",
|
|
@@ -317,7 +476,13 @@ class InternalAdapter {
|
|
|
317
476
|
createdAt: task.createdAt || null,
|
|
318
477
|
updatedAt: task.updatedAt || null,
|
|
319
478
|
backend: "internal",
|
|
320
|
-
|
|
479
|
+
attachments: mergedAttachments,
|
|
480
|
+
comments: normalizedComments,
|
|
481
|
+
meta: {
|
|
482
|
+
...(task.meta || {}),
|
|
483
|
+
comments: normalizedComments,
|
|
484
|
+
attachments: mergedAttachments,
|
|
485
|
+
},
|
|
321
486
|
};
|
|
322
487
|
}
|
|
323
488
|
|
|
@@ -3027,6 +3192,28 @@ To re-enable bosun for this task, remove the \`${this._codexLabels.ignore}\` lab
|
|
|
3027
3192
|
extractBaseBranchFromLabels(labels) ||
|
|
3028
3193
|
extractBaseBranchFromText(issue.body || ""),
|
|
3029
3194
|
);
|
|
3195
|
+
const comments = normalizeCommentList(
|
|
3196
|
+
Array.isArray(issue.comments) ? issue.comments : [],
|
|
3197
|
+
);
|
|
3198
|
+
const descriptionAttachments = extractAttachmentsFromText(issue.body || "", {
|
|
3199
|
+
source: "github",
|
|
3200
|
+
sourceType: "issue",
|
|
3201
|
+
createdAt: issue.createdAt || issue.created_at || null,
|
|
3202
|
+
});
|
|
3203
|
+
const commentAttachments = comments.flatMap((comment) =>
|
|
3204
|
+
extractAttachmentsFromText(comment.body, {
|
|
3205
|
+
source: "github",
|
|
3206
|
+
sourceType: "comment",
|
|
3207
|
+
commentId: comment.id,
|
|
3208
|
+
author: comment.author,
|
|
3209
|
+
createdAt: comment.createdAt,
|
|
3210
|
+
}),
|
|
3211
|
+
);
|
|
3212
|
+
const localAttachments = listTaskAttachments(issue.number, "github");
|
|
3213
|
+
const mergedAttachments = mergeTaskAttachments(
|
|
3214
|
+
mergeTaskAttachments(descriptionAttachments, commentAttachments),
|
|
3215
|
+
localAttachments,
|
|
3216
|
+
);
|
|
3030
3217
|
|
|
3031
3218
|
return {
|
|
3032
3219
|
id: String(issue.number || ""),
|
|
@@ -3045,10 +3232,14 @@ To re-enable bosun for this task, remove the \`${this._codexLabels.ignore}\` lab
|
|
|
3045
3232
|
baseBranch,
|
|
3046
3233
|
branchName: branchMatch?.[1] || null,
|
|
3047
3234
|
prNumber: prMatch?.[1] || null,
|
|
3235
|
+
attachments: mergedAttachments,
|
|
3236
|
+
comments,
|
|
3048
3237
|
meta: {
|
|
3049
3238
|
...issue,
|
|
3050
3239
|
task_url: issue.url || null,
|
|
3051
3240
|
tags,
|
|
3241
|
+
comments,
|
|
3242
|
+
attachments: mergedAttachments,
|
|
3052
3243
|
...(baseBranch ? { base_branch: baseBranch, baseBranch } : {}),
|
|
3053
3244
|
codex: codexMeta,
|
|
3054
3245
|
},
|
|
@@ -3358,6 +3549,66 @@ class JiraAdapter {
|
|
|
3358
3549
|
normalizedFieldValues[lcKey] = fieldValue;
|
|
3359
3550
|
}
|
|
3360
3551
|
}
|
|
3552
|
+
const rawComments = Array.isArray(fields.comment?.comments)
|
|
3553
|
+
? fields.comment.comments
|
|
3554
|
+
: [];
|
|
3555
|
+
const comments = rawComments
|
|
3556
|
+
.map((comment) => {
|
|
3557
|
+
const body = this._commentToText(comment?.body).trim();
|
|
3558
|
+
if (!body || isBosunStateComment(body)) return null;
|
|
3559
|
+
return {
|
|
3560
|
+
id: comment?.id || null,
|
|
3561
|
+
author:
|
|
3562
|
+
comment?.author?.displayName ||
|
|
3563
|
+
comment?.author?.emailAddress ||
|
|
3564
|
+
comment?.author?.accountId ||
|
|
3565
|
+
null,
|
|
3566
|
+
createdAt: comment?.created || null,
|
|
3567
|
+
body,
|
|
3568
|
+
url: comment?.self || null,
|
|
3569
|
+
};
|
|
3570
|
+
})
|
|
3571
|
+
.filter(Boolean);
|
|
3572
|
+
const commentAttachments = comments.flatMap((comment) =>
|
|
3573
|
+
extractAttachmentsFromText(comment.body, {
|
|
3574
|
+
source: "jira",
|
|
3575
|
+
sourceType: "comment",
|
|
3576
|
+
commentId: comment.id,
|
|
3577
|
+
author: comment.author,
|
|
3578
|
+
createdAt: comment.createdAt,
|
|
3579
|
+
}),
|
|
3580
|
+
);
|
|
3581
|
+
const rawAttachments = Array.isArray(fields.attachment)
|
|
3582
|
+
? fields.attachment
|
|
3583
|
+
: [];
|
|
3584
|
+
const jiraAttachments = rawAttachments
|
|
3585
|
+
.map((attachment) => {
|
|
3586
|
+
const name = attachment?.filename || attachment?.name || "attachment";
|
|
3587
|
+
const url = attachment?.content || attachment?.self || attachment?.thumbnail;
|
|
3588
|
+
if (!url) return null;
|
|
3589
|
+
const kind = guessAttachmentKind(
|
|
3590
|
+
url,
|
|
3591
|
+
name,
|
|
3592
|
+
String(attachment?.mimeType || "").startsWith("image/"),
|
|
3593
|
+
);
|
|
3594
|
+
return {
|
|
3595
|
+
id: attachment?.id || null,
|
|
3596
|
+
name,
|
|
3597
|
+
url,
|
|
3598
|
+
kind,
|
|
3599
|
+
contentType: attachment?.mimeType || guessContentType(name, kind),
|
|
3600
|
+
size: attachment?.size ?? null,
|
|
3601
|
+
source: "jira",
|
|
3602
|
+
sourceType: "attachment",
|
|
3603
|
+
createdAt: attachment?.created || null,
|
|
3604
|
+
};
|
|
3605
|
+
})
|
|
3606
|
+
.filter(Boolean);
|
|
3607
|
+
const localAttachments = listTaskAttachments(issueKey, "jira");
|
|
3608
|
+
const mergedAttachments = mergeTaskAttachments(
|
|
3609
|
+
mergeTaskAttachments(jiraAttachments, commentAttachments),
|
|
3610
|
+
localAttachments,
|
|
3611
|
+
);
|
|
3361
3612
|
return {
|
|
3362
3613
|
id: issueKey,
|
|
3363
3614
|
title: fields.summary || "",
|
|
@@ -3378,11 +3629,15 @@ class JiraAdapter {
|
|
|
3378
3629
|
taskUrl: issueKey ? `${this._baseUrl}/browse/${issueKey}` : null,
|
|
3379
3630
|
createdAt: fields.created || null,
|
|
3380
3631
|
updatedAt: fields.updated || null,
|
|
3632
|
+
attachments: mergedAttachments,
|
|
3633
|
+
comments,
|
|
3381
3634
|
meta: {
|
|
3382
3635
|
...issue,
|
|
3383
3636
|
labels,
|
|
3384
3637
|
fields: normalizedFieldValues,
|
|
3385
3638
|
tags,
|
|
3639
|
+
comments,
|
|
3640
|
+
attachments: mergedAttachments,
|
|
3386
3641
|
...(baseBranch ? { base_branch: baseBranch, baseBranch } : {}),
|
|
3387
3642
|
codex: codexMeta,
|
|
3388
3643
|
},
|
|
@@ -3546,7 +3801,7 @@ class JiraAdapter {
|
|
|
3546
3801
|
const fieldList =
|
|
3547
3802
|
fields.length > 0
|
|
3548
3803
|
? fields.join(",")
|
|
3549
|
-
: "summary,description,status,assignee,priority,project,labels,comment,created,updated";
|
|
3804
|
+
: "summary,description,status,assignee,priority,project,labels,comment,attachment,created,updated";
|
|
3550
3805
|
return this._jira(
|
|
3551
3806
|
`/rest/api/3/issue/${encodeURIComponent(issueKey)}?fields=${encodeURIComponent(fieldList)}`,
|
|
3552
3807
|
);
|
|
@@ -3735,7 +3990,7 @@ class JiraAdapter {
|
|
|
3735
3990
|
const data = await this._searchIssues(
|
|
3736
3991
|
jql,
|
|
3737
3992
|
maxResults,
|
|
3738
|
-
"summary,description,status,assignee,priority,project,labels,comment,created,updated",
|
|
3993
|
+
"summary,description,status,assignee,priority,project,labels,comment,attachment,created,updated",
|
|
3739
3994
|
);
|
|
3740
3995
|
let tasks = (Array.isArray(data?.issues) ? data.issues : []).map((issue) =>
|
|
3741
3996
|
this._normaliseIssue(issue),
|