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.
- package/.env.example +1 -1
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +6 -3
- package/agent/autofix-git.mjs +33 -0
- package/agent/autofix-prompts.mjs +151 -0
- package/agent/autofix.mjs +11 -175
- package/agent/bosun-skills.mjs +3 -2
- package/bosun.config.example.json +17 -0
- package/bosun.schema.json +87 -188
- package/cli.mjs +34 -1
- package/config/config-doctor.mjs +5 -250
- package/config/config-file-names.mjs +5 -0
- package/config/config.mjs +89 -493
- package/config/executor-config.mjs +493 -0
- package/config/repo-root.mjs +1 -2
- package/config/workspace-health.mjs +242 -0
- package/git/git-safety.mjs +15 -0
- package/github/github-oauth-portal.mjs +46 -0
- package/infra/library-manager-utils.mjs +22 -0
- package/infra/library-manager-well-known-sources.mjs +578 -0
- package/infra/library-manager.mjs +512 -1030
- package/infra/monitor.mjs +28 -9
- package/infra/session-tracker.mjs +10 -7
- package/kanban/kanban-adapter.mjs +17 -1
- package/lib/codebase-audit-manifests.mjs +117 -0
- package/lib/codebase-audit.mjs +18 -115
- package/package.json +18 -3
- package/server/ui-server.mjs +1194 -79
- package/shell/codex-config-file.mjs +178 -0
- package/shell/codex-config.mjs +538 -575
- package/task/task-cli.mjs +54 -3
- package/task/task-executor.mjs +143 -13
- package/task/task-store.mjs +409 -1
- package/telegram/telegram-bot.mjs +127 -0
- package/tools/apply-pr-suggestions.mjs +401 -0
- package/tools/syntax-check.mjs +21 -9
- package/ui/app.js +3 -14
- package/ui/components/kanban-board.js +227 -4
- package/ui/components/session-list.js +85 -5
- package/ui/demo-defaults.js +334 -80
- package/ui/demo.html +155 -0
- package/ui/modules/session-api.js +96 -0
- package/ui/modules/settings-schema.js +1 -2
- package/ui/modules/state.js +21 -3
- package/ui/setup.html +4 -5
- package/ui/styles/components.css +58 -4
- package/ui/tabs/agents.js +12 -15
- package/ui/tabs/control.js +1 -0
- package/ui/tabs/library.js +484 -22
- package/ui/tabs/manual-flows.js +105 -29
- package/ui/tabs/tasks.js +785 -140
- package/ui/tabs/telemetry.js +129 -11
- package/ui/tabs/workflow-canvas-utils.mjs +130 -0
- package/ui/tabs/workflows.js +293 -23
- package/voice/voice-tool-definitions.mjs +757 -0
- package/voice/voice-tools.mjs +34 -778
- package/workflow/manual-flow-audit.mjs +165 -0
- package/workflow/manual-flows.mjs +164 -259
- package/workflow/workflow-engine.mjs +147 -58
- package/workflow/workflow-nodes/definitions.mjs +1207 -0
- package/workflow/workflow-nodes/transforms.mjs +612 -0
- package/workflow/workflow-nodes.mjs +304 -52
- package/workflow/workflow-templates.mjs +313 -191
- package/workflow-templates/_helpers.mjs +154 -0
- package/workflow-templates/agents.mjs +61 -4
- package/workflow-templates/code-quality.mjs +7 -7
- package/workflow-templates/github.mjs +20 -10
- package/workflow-templates/task-batch.mjs +20 -9
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/server/ui-server.mjs
CHANGED
|
@@ -208,6 +208,7 @@ const TASK_STORE_SPRINT_EXPORTS = Object.freeze({
|
|
|
208
208
|
const TASK_STORE_DAG_EXPORTS = Object.freeze({
|
|
209
209
|
sprint: ["getSprintDag", "getTaskDagForSprint", "buildSprintDag", "buildTaskDag"],
|
|
210
210
|
global: ["getGlobalDagOfDags", "getDagOfDags", "buildGlobalDagOfDags"],
|
|
211
|
+
organize: ["organizeTaskDag"],
|
|
211
212
|
});
|
|
212
213
|
const TASK_STORE_GET_TASK_EXPORTS = ["getTaskById", "getTask"];
|
|
213
214
|
const TASK_STORE_COMMENT_EXPORTS = ["getTaskComments", "listTaskComments"];
|
|
@@ -308,6 +309,16 @@ function addInternalTaskComment(taskId, comment = {}) {
|
|
|
308
309
|
}
|
|
309
310
|
}
|
|
310
311
|
|
|
312
|
+
function unblockInternalTask(taskId, options = {}) {
|
|
313
|
+
const fn = getTaskStoreApiSync()?.unblockTask;
|
|
314
|
+
if (typeof fn !== "function") return null;
|
|
315
|
+
try {
|
|
316
|
+
return fn(taskId, options);
|
|
317
|
+
} catch {
|
|
318
|
+
return null;
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
311
322
|
const __dirname = resolve(fileURLToPath(new URL(".", import.meta.url)));
|
|
312
323
|
const repoRoot = resolveRepoRoot();
|
|
313
324
|
const uiRootPreferred = resolve(__dirname, "..", "ui");
|
|
@@ -1104,6 +1115,73 @@ let _wfInitPromise = null;
|
|
|
1104
1115
|
let _wfInitDone = false;
|
|
1105
1116
|
let _wfLoadedBase = null;
|
|
1106
1117
|
let _wfTaskTraceHookRegistered = false;
|
|
1118
|
+
let _workflowTelegramDigestPromise = null;
|
|
1119
|
+
const workflowTelegramDedup = new Map();
|
|
1120
|
+
|
|
1121
|
+
async function getWorkflowTelegramDigest() {
|
|
1122
|
+
if (_workflowTelegramDigestPromise) {
|
|
1123
|
+
return _workflowTelegramDigestPromise;
|
|
1124
|
+
}
|
|
1125
|
+
_workflowTelegramDigestPromise = (async () => {
|
|
1126
|
+
try {
|
|
1127
|
+
const mod = await import("../telegram/telegram-bot.mjs");
|
|
1128
|
+
if (typeof mod.restoreLiveDigest === "function") {
|
|
1129
|
+
await mod.restoreLiveDigest().catch(() => {});
|
|
1130
|
+
}
|
|
1131
|
+
return mod;
|
|
1132
|
+
} catch (err) {
|
|
1133
|
+
console.warn("[workflows/telegram] live digest unavailable:", err?.message || err);
|
|
1134
|
+
return null;
|
|
1135
|
+
}
|
|
1136
|
+
})();
|
|
1137
|
+
return _workflowTelegramDigestPromise;
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
async function sendWorkflowTelegramMessage(chatId, text, options = {}) {
|
|
1141
|
+
const telegramToken = process.env.TELEGRAM_BOT_TOKEN;
|
|
1142
|
+
const defaultChatId = String(process.env.TELEGRAM_CHAT_ID || "").trim();
|
|
1143
|
+
const target = String(chatId || defaultChatId || "").trim();
|
|
1144
|
+
if (!telegramToken || !target) return;
|
|
1145
|
+
|
|
1146
|
+
const message = String(text || "");
|
|
1147
|
+
const dedupKey = `${target}:${message.trim()}`;
|
|
1148
|
+
const now = Date.now();
|
|
1149
|
+
const lastSentAt = workflowTelegramDedup.get(dedupKey) || 0;
|
|
1150
|
+
if (dedupKey && now - lastSentAt < 5 * 60 * 1000) {
|
|
1151
|
+
return;
|
|
1152
|
+
}
|
|
1153
|
+
workflowTelegramDedup.set(dedupKey, now);
|
|
1154
|
+
|
|
1155
|
+
const parseMode = String(options?.parseMode || "").trim();
|
|
1156
|
+
if (!parseMode && defaultChatId && target === defaultChatId) {
|
|
1157
|
+
const digest = await getWorkflowTelegramDigest();
|
|
1158
|
+
if (typeof digest?.notify === "function") {
|
|
1159
|
+
await digest.notify(message, 4, {
|
|
1160
|
+
silent: Boolean(options?.silent),
|
|
1161
|
+
category: "workflow",
|
|
1162
|
+
});
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
try {
|
|
1168
|
+
await fetch(
|
|
1169
|
+
`https://api.telegram.org/bot${telegramToken}/sendMessage`,
|
|
1170
|
+
{
|
|
1171
|
+
method: "POST",
|
|
1172
|
+
headers: { "Content-Type": "application/json" },
|
|
1173
|
+
body: JSON.stringify({
|
|
1174
|
+
chat_id: target,
|
|
1175
|
+
text: message,
|
|
1176
|
+
parse_mode: parseMode || "HTML",
|
|
1177
|
+
disable_notification: Boolean(options?.silent),
|
|
1178
|
+
}),
|
|
1179
|
+
},
|
|
1180
|
+
);
|
|
1181
|
+
} catch (e) {
|
|
1182
|
+
console.warn("[workflows/telegram] sendMessage failed:", e.message);
|
|
1183
|
+
}
|
|
1184
|
+
}
|
|
1107
1185
|
|
|
1108
1186
|
/**
|
|
1109
1187
|
* Test-only: inject a mock workflow engine module and pre-seed the per-workspace
|
|
@@ -1251,6 +1329,9 @@ async function getWorkflowEngineModule() {
|
|
|
1251
1329
|
_wfEngine = await import(new URL("../workflow/workflow-engine.mjs", base).href);
|
|
1252
1330
|
_wfNodes = await import(new URL("../workflow/workflow-nodes.mjs", base).href);
|
|
1253
1331
|
_wfTemplates = await import(new URL("../workflow/workflow-templates.mjs", base).href);
|
|
1332
|
+
if (typeof _wfNodes?.ensureWorkflowNodeTypesLoaded === "function") {
|
|
1333
|
+
await _wfNodes.ensureWorkflowNodeTypesLoaded({ repoRoot });
|
|
1334
|
+
}
|
|
1254
1335
|
if (_wfLoadedBase !== base) {
|
|
1255
1336
|
console.log(`[workflows] Loaded workflow modules from: ${base}`);
|
|
1256
1337
|
_wfLoadedBase = base;
|
|
@@ -1276,26 +1357,11 @@ async function getWorkflowEngineModule() {
|
|
|
1276
1357
|
const telegramService = telegramToken
|
|
1277
1358
|
? {
|
|
1278
1359
|
async sendMessage(chatId, text, options = {}) {
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
|
|
1284
|
-
`https://api.telegram.org/bot${telegramToken}/sendMessage`,
|
|
1285
|
-
{
|
|
1286
|
-
method: "POST",
|
|
1287
|
-
headers: { "Content-Type": "application/json" },
|
|
1288
|
-
body: JSON.stringify({
|
|
1289
|
-
chat_id: target,
|
|
1290
|
-
text: String(text || ""),
|
|
1291
|
-
parse_mode: parseMode,
|
|
1292
|
-
disable_notification: Boolean(options?.silent),
|
|
1293
|
-
}),
|
|
1294
|
-
}
|
|
1295
|
-
);
|
|
1296
|
-
} catch (e) {
|
|
1297
|
-
console.warn("[workflows/telegram] sendMessage failed:", e.message);
|
|
1298
|
-
}
|
|
1360
|
+
await sendWorkflowTelegramMessage(
|
|
1361
|
+
chatId || telegramChatId,
|
|
1362
|
+
text,
|
|
1363
|
+
options,
|
|
1364
|
+
);
|
|
1299
1365
|
},
|
|
1300
1366
|
}
|
|
1301
1367
|
: null;
|
|
@@ -1541,15 +1607,67 @@ function handleTaskWorkflowTraceEvent(event = {}) {
|
|
|
1541
1607
|
|
|
1542
1608
|
function mergeTaskWorkflowRuns(baseRuns = [], extraRuns = [], limit = 60) {
|
|
1543
1609
|
const merged = [];
|
|
1544
|
-
const
|
|
1610
|
+
const indexByKey = new Map();
|
|
1611
|
+
const resolveLinkedSessionId = (entry) => {
|
|
1612
|
+
const candidates = [
|
|
1613
|
+
entry?.sessionId,
|
|
1614
|
+
entry?.threadId,
|
|
1615
|
+
entry?.agentSessionId,
|
|
1616
|
+
entry?.meta?.sessionId,
|
|
1617
|
+
entry?.meta?.threadId,
|
|
1618
|
+
entry?.data?.sessionId,
|
|
1619
|
+
entry?.data?.threadId,
|
|
1620
|
+
];
|
|
1621
|
+
for (const value of candidates) {
|
|
1622
|
+
const normalized = String(value || "").trim();
|
|
1623
|
+
if (normalized) return normalized;
|
|
1624
|
+
}
|
|
1625
|
+
return null;
|
|
1626
|
+
};
|
|
1627
|
+
const resolveSessionId = (entry) => {
|
|
1628
|
+
const directSessionId = resolveLinkedSessionId(entry);
|
|
1629
|
+
if (directSessionId) return directSessionId;
|
|
1630
|
+
const primarySessionId = String(entry?.primarySessionId || "").trim();
|
|
1631
|
+
return primarySessionId || null;
|
|
1632
|
+
};
|
|
1633
|
+
const mergeEntries = (current, incoming) => {
|
|
1634
|
+
const currentMeta = current?.meta && typeof current.meta === "object" ? current.meta : {};
|
|
1635
|
+
const incomingMeta = incoming?.meta && typeof incoming.meta === "object" ? incoming.meta : {};
|
|
1636
|
+
const mergedMeta = { ...currentMeta, ...incomingMeta };
|
|
1637
|
+
const currentMetaSessionId = String(currentMeta.sessionId || "").trim();
|
|
1638
|
+
const currentMetaThreadId = String(currentMeta.threadId || "").trim();
|
|
1639
|
+
if (currentMetaSessionId) mergedMeta.sessionId = currentMetaSessionId;
|
|
1640
|
+
if (currentMetaThreadId) mergedMeta.threadId = currentMetaThreadId;
|
|
1641
|
+
return {
|
|
1642
|
+
...current,
|
|
1643
|
+
...incoming,
|
|
1644
|
+
runId: incoming.runId || current.runId || null,
|
|
1645
|
+
workflowId: incoming.workflowId || current.workflowId || null,
|
|
1646
|
+
workflowName: incoming.workflowName || current.workflowName || null,
|
|
1647
|
+
status: incoming.status || current.status || null,
|
|
1648
|
+
outcome: incoming.outcome || current.outcome || null,
|
|
1649
|
+
summary: incoming.summary || current.summary || null,
|
|
1650
|
+
startedAt: incoming.startedAt || current.startedAt || null,
|
|
1651
|
+
endedAt: incoming.endedAt || current.endedAt || null,
|
|
1652
|
+
duration: incoming.duration ?? current.duration ?? null,
|
|
1653
|
+
url: incoming.url || current.url || null,
|
|
1654
|
+
nodeId: incoming.nodeId || current.nodeId || null,
|
|
1655
|
+
source: incoming.source || current.source || "workflow",
|
|
1656
|
+
sessionId: resolveLinkedSessionId(incoming) || resolveLinkedSessionId(current) || null,
|
|
1657
|
+
primarySessionId:
|
|
1658
|
+
String(incoming.primarySessionId || "").trim()
|
|
1659
|
+
|| String(current.primarySessionId || "").trim()
|
|
1660
|
+
|| resolveLinkedSessionId(incoming)
|
|
1661
|
+
|| resolveLinkedSessionId(current),
|
|
1662
|
+
meta: mergedMeta,
|
|
1663
|
+
};
|
|
1664
|
+
};
|
|
1545
1665
|
const push = (entry) => {
|
|
1546
1666
|
if (!entry || typeof entry !== "object") return;
|
|
1547
1667
|
const runId = String(entry.runId || "").trim();
|
|
1548
1668
|
const workflowId = String(entry.workflowId || "").trim();
|
|
1549
1669
|
const dedupKey = runId ? `run:${runId}` : `wf:${workflowId}:${entry.startedAt || entry.endedAt || ""}`;
|
|
1550
|
-
|
|
1551
|
-
seen.add(dedupKey);
|
|
1552
|
-
merged.push({
|
|
1670
|
+
const normalized = {
|
|
1553
1671
|
runId: runId || null,
|
|
1554
1672
|
workflowId: workflowId || null,
|
|
1555
1673
|
workflowName: entry.workflowName != null ? String(entry.workflowName) : null,
|
|
@@ -1559,8 +1677,21 @@ function mergeTaskWorkflowRuns(baseRuns = [], extraRuns = [], limit = 60) {
|
|
|
1559
1677
|
startedAt: entry.startedAt || null,
|
|
1560
1678
|
endedAt: entry.endedAt || null,
|
|
1561
1679
|
duration: Number.isFinite(Number(entry.duration)) ? Number(entry.duration) : null,
|
|
1680
|
+
url: entry.url != null ? String(entry.url) : null,
|
|
1681
|
+
nodeId: entry.nodeId != null ? String(entry.nodeId) : null,
|
|
1562
1682
|
source: entry.source ? String(entry.source) : "workflow",
|
|
1563
|
-
|
|
1683
|
+
sessionId: resolveLinkedSessionId(entry),
|
|
1684
|
+
primarySessionId:
|
|
1685
|
+
String(entry.primarySessionId || "").trim() || resolveSessionId(entry),
|
|
1686
|
+
meta: entry.meta && typeof entry.meta === "object" ? { ...entry.meta } : {},
|
|
1687
|
+
};
|
|
1688
|
+
const existingIndex = indexByKey.get(dedupKey);
|
|
1689
|
+
if (existingIndex == null) {
|
|
1690
|
+
indexByKey.set(dedupKey, merged.length);
|
|
1691
|
+
merged.push(normalized);
|
|
1692
|
+
return;
|
|
1693
|
+
}
|
|
1694
|
+
merged[existingIndex] = mergeEntries(merged[existingIndex], normalized);
|
|
1564
1695
|
};
|
|
1565
1696
|
|
|
1566
1697
|
for (const run of Array.isArray(baseRuns) ? baseRuns : []) push(run);
|
|
@@ -1599,6 +1730,33 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1599
1730
|
matches = traceEvents.some((event) => String(event?.taskId || "").trim() === normalizedTaskId);
|
|
1600
1731
|
}
|
|
1601
1732
|
if (!matches) continue;
|
|
1733
|
+
const primarySessionId = (() => {
|
|
1734
|
+
for (const value of [
|
|
1735
|
+
data.sessionId,
|
|
1736
|
+
data.threadId,
|
|
1737
|
+
data?.task?.sessionId,
|
|
1738
|
+
data?.task?.threadId,
|
|
1739
|
+
]) {
|
|
1740
|
+
const normalized = String(value || "").trim();
|
|
1741
|
+
if (normalized) return normalized;
|
|
1742
|
+
}
|
|
1743
|
+
const traceEvents = typeof engine.getTaskTraceEvents === "function"
|
|
1744
|
+
? engine.getTaskTraceEvents(summary.runId) || []
|
|
1745
|
+
: [];
|
|
1746
|
+
for (let index = traceEvents.length - 1; index >= 0; index -= 1) {
|
|
1747
|
+
const event = traceEvents[index];
|
|
1748
|
+
for (const value of [
|
|
1749
|
+
event?.sessionId,
|
|
1750
|
+
event?.threadId,
|
|
1751
|
+
event?.meta?.sessionId,
|
|
1752
|
+
event?.meta?.threadId,
|
|
1753
|
+
]) {
|
|
1754
|
+
const normalized = String(value || "").trim();
|
|
1755
|
+
if (normalized) return normalized;
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
return null;
|
|
1759
|
+
})();
|
|
1602
1760
|
out.push({
|
|
1603
1761
|
runId: detail.runId,
|
|
1604
1762
|
workflowId: detail.workflowId,
|
|
@@ -1611,6 +1769,8 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1611
1769
|
startedAt: detail.startedAt || null,
|
|
1612
1770
|
endedAt: detail.endedAt || null,
|
|
1613
1771
|
duration: detail.duration || null,
|
|
1772
|
+
sessionId: null,
|
|
1773
|
+
primarySessionId,
|
|
1614
1774
|
source: "workflow",
|
|
1615
1775
|
});
|
|
1616
1776
|
if (out.length >= limit) break;
|
|
@@ -1620,6 +1780,230 @@ async function collectWorkflowRunsForTask(taskId, reqUrl, limit = 40) {
|
|
|
1620
1780
|
return [];
|
|
1621
1781
|
}
|
|
1622
1782
|
}
|
|
1783
|
+
|
|
1784
|
+
function sanitizeTaskDiagnosticText(value, maxLength = 240) {
|
|
1785
|
+
const normalized = String(value || "")
|
|
1786
|
+
.replace(/\s+/g, " ")
|
|
1787
|
+
.trim();
|
|
1788
|
+
if (!normalized) return "";
|
|
1789
|
+
if (!Number.isFinite(maxLength) || maxLength <= 0 || normalized.length <= maxLength) {
|
|
1790
|
+
return normalized;
|
|
1791
|
+
}
|
|
1792
|
+
return `${normalized.slice(0, Math.max(0, maxLength - 1)).trimEnd()}…`;
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
function collectTaskTimelineDiagnostics(task, limit = 8) {
|
|
1796
|
+
const timeline = Array.isArray(task?.timeline) ? task.timeline : [];
|
|
1797
|
+
const relevant = [];
|
|
1798
|
+
for (const entry of timeline) {
|
|
1799
|
+
const message = sanitizeTaskDiagnosticText(
|
|
1800
|
+
entry?.message || entry?.reason || entry?.error || "",
|
|
1801
|
+
280,
|
|
1802
|
+
);
|
|
1803
|
+
const status = String(entry?.status || "").trim().toLowerCase();
|
|
1804
|
+
if (
|
|
1805
|
+
!message &&
|
|
1806
|
+
status !== "blocked"
|
|
1807
|
+
) {
|
|
1808
|
+
continue;
|
|
1809
|
+
}
|
|
1810
|
+
const isRelevant =
|
|
1811
|
+
status === "blocked" ||
|
|
1812
|
+
/worktree failed/i.test(message) ||
|
|
1813
|
+
/pre-pr validation failed/i.test(message) ||
|
|
1814
|
+
/claim was stolen/i.test(message) ||
|
|
1815
|
+
/blocked/i.test(message);
|
|
1816
|
+
if (!isRelevant) continue;
|
|
1817
|
+
relevant.push({
|
|
1818
|
+
source: String(entry?.source || entry?.type || "timeline").trim() || "timeline",
|
|
1819
|
+
message: message || `Task entered ${status || "blocked"} state`,
|
|
1820
|
+
timestamp: entry?.timestamp || entry?.createdAt || entry?.updatedAt || null,
|
|
1821
|
+
status: status || null,
|
|
1822
|
+
kind: "timeline",
|
|
1823
|
+
});
|
|
1824
|
+
}
|
|
1825
|
+
return relevant.slice(-Math.max(1, limit));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
function collectTaskLogDiagnostics(task, workspaceDir = "", limit = 8) {
|
|
1829
|
+
const taskId = String(task?.id || task?.taskId || "").trim();
|
|
1830
|
+
if (!taskId) {
|
|
1831
|
+
return {
|
|
1832
|
+
counts: {
|
|
1833
|
+
prePrValidationFailed: 0,
|
|
1834
|
+
worktreeFailed: 0,
|
|
1835
|
+
blockedTransitions: 0,
|
|
1836
|
+
createPrFailed: 0,
|
|
1837
|
+
},
|
|
1838
|
+
entries: [],
|
|
1839
|
+
};
|
|
1840
|
+
}
|
|
1841
|
+
|
|
1842
|
+
const taskBranch = String(task?.branch || task?.branchName || "").trim();
|
|
1843
|
+
const needles = [taskId, taskBranch].filter((value) => value && value.length >= 8);
|
|
1844
|
+
const logPaths = [];
|
|
1845
|
+
const pushLogPath = (candidate) => {
|
|
1846
|
+
if (!candidate || !existsSync(candidate) || logPaths.includes(candidate)) return;
|
|
1847
|
+
logPaths.push(candidate);
|
|
1848
|
+
};
|
|
1849
|
+
if (workspaceDir) {
|
|
1850
|
+
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor-error.log"));
|
|
1851
|
+
pushLogPath(resolve(workspaceDir, ".bosun", "logs", "monitor.log"));
|
|
1852
|
+
}
|
|
1853
|
+
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor-error.log"));
|
|
1854
|
+
pushLogPath(resolve(repoRoot, ".bosun", "logs", "monitor.log"));
|
|
1855
|
+
|
|
1856
|
+
const counts = {
|
|
1857
|
+
prePrValidationFailed: 0,
|
|
1858
|
+
worktreeFailed: 0,
|
|
1859
|
+
blockedTransitions: 0,
|
|
1860
|
+
createPrFailed: 0,
|
|
1861
|
+
};
|
|
1862
|
+
const entries = [];
|
|
1863
|
+
|
|
1864
|
+
for (const logPath of logPaths) {
|
|
1865
|
+
let raw = "";
|
|
1866
|
+
try {
|
|
1867
|
+
raw = readFileSync(logPath, "utf8");
|
|
1868
|
+
} catch {
|
|
1869
|
+
continue;
|
|
1870
|
+
}
|
|
1871
|
+
const logName = /monitor-error\.log$/i.test(logPath) ? "monitor-error.log" : "monitor.log";
|
|
1872
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
1873
|
+
if (!line) continue;
|
|
1874
|
+
if (!needles.some((needle) => line.includes(needle))) continue;
|
|
1875
|
+
const text = sanitizeTaskDiagnosticText(line, 320);
|
|
1876
|
+
let matched = false;
|
|
1877
|
+
if (/pre-PR validation failed/i.test(text)) {
|
|
1878
|
+
counts.prePrValidationFailed += 1;
|
|
1879
|
+
matched = true;
|
|
1880
|
+
}
|
|
1881
|
+
if (/Worktree failed for/i.test(text) || /Worktree acquisition failed/i.test(text)) {
|
|
1882
|
+
counts.worktreeFailed += 1;
|
|
1883
|
+
matched = true;
|
|
1884
|
+
}
|
|
1885
|
+
if (/-> blocked/i.test(text) || /status: .*blocked/i.test(text)) {
|
|
1886
|
+
counts.blockedTransitions += 1;
|
|
1887
|
+
matched = true;
|
|
1888
|
+
}
|
|
1889
|
+
if (/create-pr FAILED/i.test(text)) {
|
|
1890
|
+
counts.createPrFailed += 1;
|
|
1891
|
+
matched = true;
|
|
1892
|
+
}
|
|
1893
|
+
if (!matched) continue;
|
|
1894
|
+
entries.push({
|
|
1895
|
+
source: logName,
|
|
1896
|
+
message: text,
|
|
1897
|
+
kind: "log",
|
|
1898
|
+
});
|
|
1899
|
+
if (entries.length > limit) entries.shift();
|
|
1900
|
+
}
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1903
|
+
return { counts, entries };
|
|
1904
|
+
}
|
|
1905
|
+
|
|
1906
|
+
function buildTaskBlockedContext(task, options = {}) {
|
|
1907
|
+
const currentTask = task && typeof task === "object" ? task : {};
|
|
1908
|
+
const canStart = options.canStart && typeof options.canStart === "object"
|
|
1909
|
+
? options.canStart
|
|
1910
|
+
: null;
|
|
1911
|
+
const normalizedStatus = normalizeTaskStatusKey(currentTask?.status);
|
|
1912
|
+
const explicitReason = sanitizeTaskDiagnosticText(
|
|
1913
|
+
currentTask?.blockedReason
|
|
1914
|
+
|| currentTask?.meta?.worktreeFailure?.blockedReason
|
|
1915
|
+
|| currentTask?.meta?.autoRecovery?.error
|
|
1916
|
+
|| currentTask?.meta?.worktreeFailure?.error
|
|
1917
|
+
|| "",
|
|
1918
|
+
280,
|
|
1919
|
+
);
|
|
1920
|
+
const workflowRuns = Array.isArray(options.workflowRuns)
|
|
1921
|
+
? options.workflowRuns
|
|
1922
|
+
: Array.isArray(currentTask?.workflowRuns)
|
|
1923
|
+
? currentTask.workflowRuns
|
|
1924
|
+
: [];
|
|
1925
|
+
const timelineEvidence = collectTaskTimelineDiagnostics(currentTask, 6);
|
|
1926
|
+
const logDiagnostics = collectTaskLogDiagnostics(
|
|
1927
|
+
currentTask,
|
|
1928
|
+
normalizeCandidatePath(options.workspaceDir),
|
|
1929
|
+
6,
|
|
1930
|
+
);
|
|
1931
|
+
const hasPlannerCorruption = /planner payload corrupted/i.test(explicitReason);
|
|
1932
|
+
const hasWorktreeFailure =
|
|
1933
|
+
Boolean(currentTask?.meta?.worktreeFailure) ||
|
|
1934
|
+
logDiagnostics.counts.worktreeFailed > 0 ||
|
|
1935
|
+
timelineEvidence.some((entry) => /worktree failed/i.test(String(entry?.message || "")));
|
|
1936
|
+
const isDependencyBlocked = canStart?.canStart === false && String(canStart?.reason || "") === "dependency_blocked";
|
|
1937
|
+
|
|
1938
|
+
let category = "";
|
|
1939
|
+
let headline = "";
|
|
1940
|
+
let summary = "";
|
|
1941
|
+
let recommendation = "";
|
|
1942
|
+
|
|
1943
|
+
if (hasPlannerCorruption) {
|
|
1944
|
+
category = "planner_payload_corruption";
|
|
1945
|
+
headline = "Planner payload corruption quarantined this task.";
|
|
1946
|
+
summary = explicitReason;
|
|
1947
|
+
recommendation = "Do not requeue this task as-is. Recreate it from the fixed planner path or repair its payload first.";
|
|
1948
|
+
} else if (hasWorktreeFailure) {
|
|
1949
|
+
category = "worktree_failure";
|
|
1950
|
+
headline = "Task Lifecycle blocked this task after worktree acquisition failed.";
|
|
1951
|
+
summary = explicitReason
|
|
1952
|
+
|| "Bosun could not acquire or refresh a clean managed worktree for this task.";
|
|
1953
|
+
recommendation = "If the worktree guard fix is now deployed, move the task back to todo to retry it on a fresh lifecycle run.";
|
|
1954
|
+
} else if (isDependencyBlocked) {
|
|
1955
|
+
category = "dependency_blocked";
|
|
1956
|
+
headline = "This task cannot start because one or more dependencies are not done yet.";
|
|
1957
|
+
summary = "Bosun will not dispatch this task until every blocking dependency below is resolved.";
|
|
1958
|
+
recommendation = "Complete or unblock the listed dependencies, then dispatch this task again.";
|
|
1959
|
+
} else if (normalizedStatus === "blocked") {
|
|
1960
|
+
category = "blocked";
|
|
1961
|
+
headline = "This task is blocked.";
|
|
1962
|
+
summary = explicitReason || "Bosun marked this task as blocked, but the original blocked reason was not persisted.";
|
|
1963
|
+
recommendation = "Review the recent workflow evidence below. After the underlying issue is fixed, move the task back to todo to clear the block and retry it.";
|
|
1964
|
+
} else if (canStart?.canStart === false) {
|
|
1965
|
+
category = "start_guard_blocked";
|
|
1966
|
+
headline = "This task is currently not startable.";
|
|
1967
|
+
summary = sanitizeTaskDiagnosticText(canStart?.reason || "Bosun start guards rejected dispatch for this task.");
|
|
1968
|
+
recommendation = "Resolve the blocking condition below before dispatching the task.";
|
|
1969
|
+
} else {
|
|
1970
|
+
return null;
|
|
1971
|
+
}
|
|
1972
|
+
|
|
1973
|
+
return {
|
|
1974
|
+
status: normalizedStatus,
|
|
1975
|
+
category,
|
|
1976
|
+
headline,
|
|
1977
|
+
summary,
|
|
1978
|
+
recommendation,
|
|
1979
|
+
reason: explicitReason || sanitizeTaskDiagnosticText(canStart?.reason || ""),
|
|
1980
|
+
workflowRunCount: workflowRuns.length,
|
|
1981
|
+
prePrValidationFailureCount: logDiagnostics.counts.prePrValidationFailed,
|
|
1982
|
+
worktreeFailureCount: logDiagnostics.counts.worktreeFailed,
|
|
1983
|
+
blockedTransitionCount: logDiagnostics.counts.blockedTransitions,
|
|
1984
|
+
createPrFailureCount: logDiagnostics.counts.createPrFailed,
|
|
1985
|
+
blockedBy: Array.isArray(canStart?.blockedBy) ? canStart.blockedBy : [],
|
|
1986
|
+
blockingTaskIds: Array.isArray(canStart?.blockingTaskIds) ? canStart.blockingTaskIds : [],
|
|
1987
|
+
timelineEvidence,
|
|
1988
|
+
logEvidence: logDiagnostics.entries,
|
|
1989
|
+
};
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function buildTaskMetaPatch(previousMeta, metadataPatchMeta, options = {}) {
|
|
1993
|
+
const clearBlockedState = options.clearBlockedState === true;
|
|
1994
|
+
const nextMeta = previousMeta && typeof previousMeta === "object"
|
|
1995
|
+
? { ...previousMeta }
|
|
1996
|
+
: {};
|
|
1997
|
+
if (clearBlockedState) {
|
|
1998
|
+
delete nextMeta.autoRecovery;
|
|
1999
|
+
delete nextMeta.blockedReason;
|
|
2000
|
+
}
|
|
2001
|
+
if (metadataPatchMeta && typeof metadataPatchMeta === "object") {
|
|
2002
|
+
Object.assign(nextMeta, metadataPatchMeta);
|
|
2003
|
+
}
|
|
2004
|
+
return nextMeta;
|
|
2005
|
+
}
|
|
2006
|
+
|
|
1623
2007
|
function maybeBootstrapWorkspaceWorkflowTemplates(engine, workspaceKey, workspaceLabel) {
|
|
1624
2008
|
if (!engine || !_wfTemplates) return;
|
|
1625
2009
|
if (_wfRecommendedInstalledByWorkspace.has(workspaceKey)) return;
|
|
@@ -2450,31 +2834,159 @@ function normalizeTriggerTemplateId(template = {}) {
|
|
|
2450
2834
|
.toLowerCase();
|
|
2451
2835
|
}
|
|
2452
2836
|
|
|
2837
|
+
function sanitizeTriggerTemplateInput(template = {}) {
|
|
2838
|
+
if (!template || typeof template !== "object" || Array.isArray(template)) {
|
|
2839
|
+
return {};
|
|
2840
|
+
}
|
|
2841
|
+
const sanitized = {};
|
|
2842
|
+
for (const key of [
|
|
2843
|
+
"id",
|
|
2844
|
+
"name",
|
|
2845
|
+
"description",
|
|
2846
|
+
"enabled",
|
|
2847
|
+
"action",
|
|
2848
|
+
"minIntervalMinutes",
|
|
2849
|
+
"trigger",
|
|
2850
|
+
"config",
|
|
2851
|
+
]) {
|
|
2852
|
+
if (Object.prototype.hasOwnProperty.call(template, key)) {
|
|
2853
|
+
sanitized[key] = template[key];
|
|
2854
|
+
}
|
|
2855
|
+
}
|
|
2856
|
+
return sanitized;
|
|
2857
|
+
}
|
|
2858
|
+
|
|
2453
2859
|
function normalizeTriggerTemplate(template = {}) {
|
|
2454
|
-
const
|
|
2860
|
+
const source = sanitizeTriggerTemplateInput(template);
|
|
2861
|
+
const id = normalizeTriggerTemplateId(source);
|
|
2455
2862
|
if (!id) return null;
|
|
2456
2863
|
return {
|
|
2457
|
-
...template,
|
|
2458
2864
|
id,
|
|
2459
|
-
name: String(
|
|
2460
|
-
|
|
2461
|
-
|
|
2865
|
+
name: String(source?.name || id).trim() || id,
|
|
2866
|
+
description: String(source?.description || "").trim(),
|
|
2867
|
+
enabled: source?.enabled === true,
|
|
2868
|
+
action: String(source?.action || "create-task").trim(),
|
|
2462
2869
|
minIntervalMinutes:
|
|
2463
|
-
Number.isFinite(Number(
|
|
2464
|
-
Number(
|
|
2465
|
-
? Number(
|
|
2870
|
+
Number.isFinite(Number(source?.minIntervalMinutes)) &&
|
|
2871
|
+
Number(source?.minIntervalMinutes) > 0
|
|
2872
|
+
? Number(source.minIntervalMinutes)
|
|
2466
2873
|
: undefined,
|
|
2467
2874
|
trigger:
|
|
2468
|
-
|
|
2469
|
-
?
|
|
2875
|
+
source?.trigger && typeof source.trigger === "object"
|
|
2876
|
+
? source.trigger
|
|
2470
2877
|
: { anyOf: [] },
|
|
2471
2878
|
config:
|
|
2472
|
-
|
|
2473
|
-
?
|
|
2879
|
+
source?.config && typeof source.config === "object"
|
|
2880
|
+
? source.config
|
|
2474
2881
|
: {},
|
|
2475
2882
|
};
|
|
2476
2883
|
}
|
|
2477
2884
|
|
|
2885
|
+
function buildTaskStateExportPayload(tasks = [], backend = "unknown") {
|
|
2886
|
+
return {
|
|
2887
|
+
schemaVersion: 1,
|
|
2888
|
+
kind: "bosun-task-state-export",
|
|
2889
|
+
exportedAt: new Date().toISOString(),
|
|
2890
|
+
backend,
|
|
2891
|
+
tasks: Array.isArray(tasks) ? tasks : [],
|
|
2892
|
+
};
|
|
2893
|
+
}
|
|
2894
|
+
|
|
2895
|
+
function extractImportedTaskList(body = null) {
|
|
2896
|
+
if (Array.isArray(body)) return body;
|
|
2897
|
+
if (body && typeof body === "object") {
|
|
2898
|
+
if (Array.isArray(body.tasks)) return body.tasks;
|
|
2899
|
+
if (Array.isArray(body.backlog)) return body.backlog;
|
|
2900
|
+
if (body.data && typeof body.data === "object" && Array.isArray(body.data.tasks)) {
|
|
2901
|
+
return body.data.tasks;
|
|
2902
|
+
}
|
|
2903
|
+
}
|
|
2904
|
+
return null;
|
|
2905
|
+
}
|
|
2906
|
+
|
|
2907
|
+
async function importInternalTaskStateSnapshot(body = {}) {
|
|
2908
|
+
const taskStore = await ensureTaskStoreApi();
|
|
2909
|
+
const addTaskFn = typeof taskStore?.addTask === "function" ? taskStore.addTask : null;
|
|
2910
|
+
const updateTaskFn = typeof taskStore?.updateTask === "function" ? taskStore.updateTask : null;
|
|
2911
|
+
if (!addTaskFn || !updateTaskFn) {
|
|
2912
|
+
throw new Error("Internal task store import is unavailable");
|
|
2913
|
+
}
|
|
2914
|
+
|
|
2915
|
+
const tasks = extractImportedTaskList(body);
|
|
2916
|
+
if (!Array.isArray(tasks)) {
|
|
2917
|
+
throw new Error("JSON must contain an array of tasks (top-level or under 'tasks' key)");
|
|
2918
|
+
}
|
|
2919
|
+
|
|
2920
|
+
const mode = String(body?.mode || "merge").trim().toLowerCase();
|
|
2921
|
+
if (!["merge", "upsert"].includes(mode)) {
|
|
2922
|
+
throw new Error("Only merge/upsert import mode is supported");
|
|
2923
|
+
}
|
|
2924
|
+
|
|
2925
|
+
const existingById = new Map(
|
|
2926
|
+
getAllInternalTasks()
|
|
2927
|
+
.filter((task) => task && task.id)
|
|
2928
|
+
.map((task) => [String(task.id), task]),
|
|
2929
|
+
);
|
|
2930
|
+
const summary = {
|
|
2931
|
+
total: tasks.length,
|
|
2932
|
+
created: 0,
|
|
2933
|
+
updated: 0,
|
|
2934
|
+
failed: 0,
|
|
2935
|
+
};
|
|
2936
|
+
const results = [];
|
|
2937
|
+
|
|
2938
|
+
for (const entry of tasks) {
|
|
2939
|
+
const task = entry && typeof entry === "object" && !Array.isArray(entry) ? entry : null;
|
|
2940
|
+
const taskId = String(task?.id || "").trim();
|
|
2941
|
+
if (!task || !taskId) {
|
|
2942
|
+
summary.failed += 1;
|
|
2943
|
+
results.push({ id: taskId || null, status: "failed", error: "task.id is required" });
|
|
2944
|
+
continue;
|
|
2945
|
+
}
|
|
2946
|
+
if (!String(task.title || "").trim()) {
|
|
2947
|
+
summary.failed += 1;
|
|
2948
|
+
results.push({ id: taskId, status: "failed", error: "task.title is required" });
|
|
2949
|
+
continue;
|
|
2950
|
+
}
|
|
2951
|
+
|
|
2952
|
+
try {
|
|
2953
|
+
if (existingById.has(taskId)) {
|
|
2954
|
+
updateTaskFn(taskId, task);
|
|
2955
|
+
summary.updated += 1;
|
|
2956
|
+
results.push({ id: taskId, status: "updated" });
|
|
2957
|
+
} else {
|
|
2958
|
+
addTaskFn(task);
|
|
2959
|
+
existingById.set(taskId, task);
|
|
2960
|
+
summary.created += 1;
|
|
2961
|
+
results.push({ id: taskId, status: "created" });
|
|
2962
|
+
}
|
|
2963
|
+
} catch (err) {
|
|
2964
|
+
summary.failed += 1;
|
|
2965
|
+
results.push({
|
|
2966
|
+
id: taskId,
|
|
2967
|
+
status: "failed",
|
|
2968
|
+
error: err?.message || "import failed",
|
|
2969
|
+
});
|
|
2970
|
+
}
|
|
2971
|
+
}
|
|
2972
|
+
|
|
2973
|
+
if (summary.created > 0 || summary.updated > 0) {
|
|
2974
|
+
const waitForWritesFn = typeof taskStore?.waitForStoreWrites === "function"
|
|
2975
|
+
? taskStore.waitForStoreWrites
|
|
2976
|
+
: null;
|
|
2977
|
+
if (waitForWritesFn) {
|
|
2978
|
+
await waitForWritesFn();
|
|
2979
|
+
}
|
|
2980
|
+
}
|
|
2981
|
+
|
|
2982
|
+
return {
|
|
2983
|
+
backend: "internal",
|
|
2984
|
+
mode,
|
|
2985
|
+
summary,
|
|
2986
|
+
results,
|
|
2987
|
+
};
|
|
2988
|
+
}
|
|
2989
|
+
|
|
2478
2990
|
function readConfigDocument() {
|
|
2479
2991
|
const configPath = resolveConfigPath();
|
|
2480
2992
|
let configData = { $schema: "./bosun.schema.json" };
|
|
@@ -2925,6 +3437,7 @@ async function applySharedStateToTasks(tasks) {
|
|
|
2925
3437
|
function mapTaskStatusToBoardColumn(status) {
|
|
2926
3438
|
const normalized = String(status || "").trim().toLowerCase();
|
|
2927
3439
|
if (normalized === "draft") return "draft";
|
|
3440
|
+
if (["blocked", "error", "failed"].includes(normalized)) return "blocked";
|
|
2928
3441
|
if (["inprogress", "in-progress", "working", "active", "assigned", "running"].includes(normalized)) return "inProgress";
|
|
2929
3442
|
if (["inreview", "in-review", "review", "pr-open", "pr-review"].includes(normalized)) return "inReview";
|
|
2930
3443
|
if (["done", "completed", "closed", "merged", "cancelled"].includes(normalized)) return "done";
|
|
@@ -3017,6 +3530,87 @@ function resolveActiveWorkspaceExecutionContext() {
|
|
|
3017
3530
|
};
|
|
3018
3531
|
}
|
|
3019
3532
|
|
|
3533
|
+
function resolveDefaultRepositoryForWorkspaceContext(workspaceContext = {}) {
|
|
3534
|
+
const configDir = resolveUiConfigDir();
|
|
3535
|
+
if (!configDir) return "";
|
|
3536
|
+
const listed = listManagedWorkspaces(configDir, { repoRoot });
|
|
3537
|
+
const workspaceId = String(workspaceContext?.workspaceId || "").trim().toLowerCase();
|
|
3538
|
+
const workspace =
|
|
3539
|
+
(workspaceId
|
|
3540
|
+
? listed.find((entry) => String(entry?.id || "").trim().toLowerCase() === workspaceId)
|
|
3541
|
+
: null) ||
|
|
3542
|
+
getActiveManagedWorkspace(configDir) ||
|
|
3543
|
+
listed[0] ||
|
|
3544
|
+
null;
|
|
3545
|
+
if (!workspace) return "";
|
|
3546
|
+
return String(
|
|
3547
|
+
workspace?.activeRepo ||
|
|
3548
|
+
workspace?.repos?.find((repo) => repo?.primary)?.name ||
|
|
3549
|
+
workspace?.repos?.[0]?.name ||
|
|
3550
|
+
"",
|
|
3551
|
+
).trim();
|
|
3552
|
+
}
|
|
3553
|
+
|
|
3554
|
+
async function resolveDefaultKanbanProjectId(adapter, requestedProjectId = "") {
|
|
3555
|
+
const explicitProjectId = String(requestedProjectId || "").trim();
|
|
3556
|
+
if (explicitProjectId) return explicitProjectId;
|
|
3557
|
+
if (!adapter || typeof adapter.listProjects !== "function") return "";
|
|
3558
|
+
try {
|
|
3559
|
+
const projects = await adapter.listProjects();
|
|
3560
|
+
return String(projects?.[0]?.id || projects?.[0]?.project_id || "").trim();
|
|
3561
|
+
} catch {
|
|
3562
|
+
return "";
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
function createManualFlowTaskManager(workspaceContext = {}, opts = {}) {
|
|
3567
|
+
return {
|
|
3568
|
+
async createTask(spec = {}) {
|
|
3569
|
+
const title = String(spec?.title || "").trim();
|
|
3570
|
+
if (!title) throw new Error("title is required");
|
|
3571
|
+
|
|
3572
|
+
const adapter = getKanbanAdapter();
|
|
3573
|
+
const projectId = await resolveDefaultKanbanProjectId(
|
|
3574
|
+
adapter,
|
|
3575
|
+
opts?.projectId || spec?.projectId || spec?.project || "",
|
|
3576
|
+
);
|
|
3577
|
+
const labels = normalizeTagsInput(spec?.labels || spec?.tags);
|
|
3578
|
+
const workspace = String(
|
|
3579
|
+
spec?.workspace || opts?.workspaceId || workspaceContext?.workspaceId || "",
|
|
3580
|
+
).trim();
|
|
3581
|
+
const repository = String(
|
|
3582
|
+
spec?.repository ||
|
|
3583
|
+
spec?.meta?.repository ||
|
|
3584
|
+
opts?.repository ||
|
|
3585
|
+
resolveDefaultRepositoryForWorkspaceContext(workspaceContext),
|
|
3586
|
+
).trim();
|
|
3587
|
+
const repositories = Array.isArray(spec?.repositories)
|
|
3588
|
+
? spec.repositories.filter((value) => typeof value === "string" && value.trim())
|
|
3589
|
+
: [];
|
|
3590
|
+
const taskPayload = {
|
|
3591
|
+
title,
|
|
3592
|
+
description: String(spec?.description || ""),
|
|
3593
|
+
status: String(spec?.status || "todo").trim() || "todo",
|
|
3594
|
+
priority: spec?.priority || undefined,
|
|
3595
|
+
...(workspace ? { workspace } : {}),
|
|
3596
|
+
...(repository ? { repository } : {}),
|
|
3597
|
+
...(repositories.length ? { repositories } : {}),
|
|
3598
|
+
...(labels.length ? { labels, tags: labels } : {}),
|
|
3599
|
+
meta: {
|
|
3600
|
+
...(workspace ? { workspace } : {}),
|
|
3601
|
+
...(repository ? { repository } : {}),
|
|
3602
|
+
...(repositories.length ? { repositories } : {}),
|
|
3603
|
+
...(labels.length ? { tags: labels } : {}),
|
|
3604
|
+
manualFlowTemplateId: String(opts?.templateId || "").trim() || undefined,
|
|
3605
|
+
...(spec?.meta && typeof spec.meta === "object" ? spec.meta : {}),
|
|
3606
|
+
},
|
|
3607
|
+
};
|
|
3608
|
+
const createdRaw = await adapter.createTask(projectId, taskPayload);
|
|
3609
|
+
return withTaskMetadataTopLevel(createdRaw);
|
|
3610
|
+
},
|
|
3611
|
+
};
|
|
3612
|
+
}
|
|
3613
|
+
|
|
3020
3614
|
function resolveWorkspaceContextById(workspaceId = "") {
|
|
3021
3615
|
const requestedId = String(workspaceId || "").trim().toLowerCase();
|
|
3022
3616
|
if (!requestedId) return resolveActiveWorkspaceExecutionContext();
|
|
@@ -6371,6 +6965,53 @@ function normalizeJsonResponsePayload(payload) {
|
|
|
6371
6965
|
return scrubStackTraces(payload);
|
|
6372
6966
|
}
|
|
6373
6967
|
|
|
6968
|
+
function makeJsonSafe(value, options = {}) {
|
|
6969
|
+
const depth = Number.isFinite(options.depth) ? options.depth : 0;
|
|
6970
|
+
const maxDepth = Number.isFinite(options.maxDepth) ? options.maxDepth : 5;
|
|
6971
|
+
const seen = options.seen instanceof WeakSet ? options.seen : new WeakSet();
|
|
6972
|
+
|
|
6973
|
+
if (value == null) return value;
|
|
6974
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
6975
|
+
if (typeof value === "number") return Number.isFinite(value) ? value : String(value);
|
|
6976
|
+
if (typeof value === "bigint") return String(value);
|
|
6977
|
+
if (typeof value === "undefined" || typeof value === "function" || typeof value === "symbol") {
|
|
6978
|
+
return undefined;
|
|
6979
|
+
}
|
|
6980
|
+
if (value instanceof Date) {
|
|
6981
|
+
return Number.isFinite(value.getTime()) ? value.toISOString() : String(value);
|
|
6982
|
+
}
|
|
6983
|
+
if (value instanceof Error) {
|
|
6984
|
+
const errorValue = {
|
|
6985
|
+
name: String(value.name || "Error"),
|
|
6986
|
+
message: String(value.message || ""),
|
|
6987
|
+
};
|
|
6988
|
+
if (value.code != null) errorValue.code = String(value.code);
|
|
6989
|
+
return errorValue;
|
|
6990
|
+
}
|
|
6991
|
+
if (depth >= maxDepth) return "[Truncated]";
|
|
6992
|
+
if (typeof value !== "object") return String(value);
|
|
6993
|
+
if (seen.has(value)) return "[Circular]";
|
|
6994
|
+
|
|
6995
|
+
seen.add(value);
|
|
6996
|
+
try {
|
|
6997
|
+
if (Array.isArray(value)) {
|
|
6998
|
+
return value.map((entry) => {
|
|
6999
|
+
const normalized = makeJsonSafe(entry, { depth: depth + 1, maxDepth, seen });
|
|
7000
|
+
return normalized === undefined ? null : normalized;
|
|
7001
|
+
});
|
|
7002
|
+
}
|
|
7003
|
+
|
|
7004
|
+
const out = {};
|
|
7005
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
7006
|
+
const normalized = makeJsonSafe(entry, { depth: depth + 1, maxDepth, seen });
|
|
7007
|
+
if (normalized !== undefined) out[key] = normalized;
|
|
7008
|
+
}
|
|
7009
|
+
return out;
|
|
7010
|
+
} finally {
|
|
7011
|
+
seen.delete(value);
|
|
7012
|
+
}
|
|
7013
|
+
}
|
|
7014
|
+
|
|
6374
7015
|
function extractSafeErrorMessage(payload) {
|
|
6375
7016
|
if (payload == null) return "Internal server error";
|
|
6376
7017
|
if (payload instanceof Error) {
|
|
@@ -6505,7 +7146,7 @@ function normalizeCanStartResult(result, { override = false } = {}) {
|
|
|
6505
7146
|
missingDependencyTaskIds,
|
|
6506
7147
|
blockingSprintIds,
|
|
6507
7148
|
blockingEpicIds,
|
|
6508
|
-
raw,
|
|
7149
|
+
raw: makeJsonSafe(raw),
|
|
6509
7150
|
};
|
|
6510
7151
|
}
|
|
6511
7152
|
|
|
@@ -6658,6 +7299,15 @@ async function getGlobalDagData() {
|
|
|
6658
7299
|
};
|
|
6659
7300
|
}
|
|
6660
7301
|
|
|
7302
|
+
async function organizeDagData(options = {}) {
|
|
7303
|
+
const organizeResult = await callTaskStoreFunction(TASK_STORE_DAG_EXPORTS.organize, [options]);
|
|
7304
|
+
if (!organizeResult.found) return null;
|
|
7305
|
+
return {
|
|
7306
|
+
source: `task-store.${organizeResult.found}`,
|
|
7307
|
+
data: organizeResult.value,
|
|
7308
|
+
};
|
|
7309
|
+
}
|
|
7310
|
+
|
|
6661
7311
|
function normalizeTaskComments(comments = []) {
|
|
6662
7312
|
if (!Array.isArray(comments)) return [];
|
|
6663
7313
|
return comments
|
|
@@ -9056,12 +9706,12 @@ function runGit(args, timeoutMs = 10000) {
|
|
|
9056
9706
|
return String(res.stdout || "").trim();
|
|
9057
9707
|
}
|
|
9058
9708
|
|
|
9059
|
-
async function readJsonBody(req) {
|
|
9709
|
+
async function readJsonBody(req, maxBytes = 1_000_000) {
|
|
9060
9710
|
return new Promise((resolveBody, rejectBody) => {
|
|
9061
9711
|
let data = "";
|
|
9062
9712
|
req.on("data", (chunk) => {
|
|
9063
9713
|
data += chunk;
|
|
9064
|
-
if (data.length >
|
|
9714
|
+
if (data.length > maxBytes) {
|
|
9065
9715
|
rejectBody(new Error("payload too large"));
|
|
9066
9716
|
req.destroy();
|
|
9067
9717
|
}
|
|
@@ -9279,6 +9929,14 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
9279
9929
|
}
|
|
9280
9930
|
}
|
|
9281
9931
|
|
|
9932
|
+
if (hasOwn(input, "blockedReason")) {
|
|
9933
|
+
const blockedReason = normalizeOptionalStringInput(input?.blockedReason);
|
|
9934
|
+
if (blockedReason) {
|
|
9935
|
+
topLevel.blockedReason = blockedReason;
|
|
9936
|
+
meta.blockedReason = blockedReason;
|
|
9937
|
+
}
|
|
9938
|
+
}
|
|
9939
|
+
|
|
9282
9940
|
return { topLevel, meta };
|
|
9283
9941
|
}
|
|
9284
9942
|
|
|
@@ -9464,13 +10122,105 @@ async function readJsonlTail(filePath, maxLines = 2000) {
|
|
|
9464
10122
|
.filter(Boolean);
|
|
9465
10123
|
}
|
|
9466
10124
|
|
|
10125
|
+
function getEntryTimestamp(entry) {
|
|
10126
|
+
const numericCandidates = [
|
|
10127
|
+
entry?.endedAt,
|
|
10128
|
+
entry?.startedAt,
|
|
10129
|
+
];
|
|
10130
|
+
for (const candidate of numericCandidates) {
|
|
10131
|
+
const parsed = Number(candidate);
|
|
10132
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
10133
|
+
}
|
|
10134
|
+
|
|
10135
|
+
const isoCandidates = [
|
|
10136
|
+
entry?.timestamp,
|
|
10137
|
+
entry?.recordedAt,
|
|
10138
|
+
entry?.updatedAt,
|
|
10139
|
+
entry?.createdAt,
|
|
10140
|
+
];
|
|
10141
|
+
for (const candidate of isoCandidates) {
|
|
10142
|
+
const parsed = Date.parse(candidate || "");
|
|
10143
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
10144
|
+
}
|
|
10145
|
+
return Number.NaN;
|
|
10146
|
+
}
|
|
10147
|
+
|
|
10148
|
+
function getEntryDayKey(entry, fallbackTs = Number.NaN) {
|
|
10149
|
+
const isoCandidates = [
|
|
10150
|
+
entry?.timestamp,
|
|
10151
|
+
entry?.recordedAt,
|
|
10152
|
+
entry?.updatedAt,
|
|
10153
|
+
entry?.createdAt,
|
|
10154
|
+
];
|
|
10155
|
+
for (const candidate of isoCandidates) {
|
|
10156
|
+
const value = String(candidate || "").trim();
|
|
10157
|
+
if (value.length >= 10) return value.slice(0, 10);
|
|
10158
|
+
}
|
|
10159
|
+
const ts = Number.isFinite(fallbackTs) ? fallbackTs : getEntryTimestamp(entry);
|
|
10160
|
+
if (!Number.isFinite(ts)) return "";
|
|
10161
|
+
return new Date(ts).toISOString().slice(0, 10);
|
|
10162
|
+
}
|
|
10163
|
+
|
|
9467
10164
|
function withinDays(entry, days) {
|
|
9468
10165
|
if (!days) return true;
|
|
9469
|
-
const ts =
|
|
10166
|
+
const ts = getEntryTimestamp(entry);
|
|
9470
10167
|
if (!Number.isFinite(ts)) return true;
|
|
9471
10168
|
return ts >= Date.now() - days * 24 * 60 * 60 * 1000;
|
|
9472
10169
|
}
|
|
9473
10170
|
|
|
10171
|
+
async function readCompletedSessionEntries(maxLines = 100_000) {
|
|
10172
|
+
const sessionLogPath = resolve(repoRoot, ".cache", "session-accumulator.jsonl");
|
|
10173
|
+
const entries = await readJsonlTail(sessionLogPath, maxLines);
|
|
10174
|
+
return {
|
|
10175
|
+
sessionLogPath,
|
|
10176
|
+
entries: entries.filter((entry) => String(entry?.type || "completed_session") === "completed_session"),
|
|
10177
|
+
};
|
|
10178
|
+
}
|
|
10179
|
+
|
|
10180
|
+
function roundMetric(value, precision = 6) {
|
|
10181
|
+
const numeric = Number(value);
|
|
10182
|
+
if (!Number.isFinite(numeric)) return 0;
|
|
10183
|
+
return Number(numeric.toFixed(precision));
|
|
10184
|
+
}
|
|
10185
|
+
|
|
10186
|
+
const SHREDDING_ESTIMATED_CHARS_PER_TOKEN = 4;
|
|
10187
|
+
|
|
10188
|
+
function estimateTokensFromChars(chars) {
|
|
10189
|
+
const numeric = Number(chars);
|
|
10190
|
+
if (!Number.isFinite(numeric) || numeric <= 0) return 0;
|
|
10191
|
+
return Math.max(0, Math.round(numeric / SHREDDING_ESTIMATED_CHARS_PER_TOKEN));
|
|
10192
|
+
}
|
|
10193
|
+
|
|
10194
|
+
function summarizeObservedSessionCostModel(entries = []) {
|
|
10195
|
+
let totalCostUsd = 0;
|
|
10196
|
+
let totalTokens = 0;
|
|
10197
|
+
let totalInputTokens = 0;
|
|
10198
|
+
let pricedSessions = 0;
|
|
10199
|
+
for (const entry of entries) {
|
|
10200
|
+
const costUsd = numberOrZero(entry?.costUsd);
|
|
10201
|
+
const tokenCount = numberOrZero(entry?.tokenCount);
|
|
10202
|
+
const inputTokens = numberOrZero(entry?.inputTokens);
|
|
10203
|
+
if (costUsd <= 0 || tokenCount <= 0) continue;
|
|
10204
|
+
totalCostUsd += costUsd;
|
|
10205
|
+
totalTokens += tokenCount;
|
|
10206
|
+
totalInputTokens += inputTokens;
|
|
10207
|
+
pricedSessions += 1;
|
|
10208
|
+
}
|
|
10209
|
+
const blendedCostPerToken = totalCostUsd > 0 && totalTokens > 0
|
|
10210
|
+
? totalCostUsd / totalTokens
|
|
10211
|
+
: null;
|
|
10212
|
+
return {
|
|
10213
|
+
pricedSessions,
|
|
10214
|
+
totalCostUsd: roundMetric(totalCostUsd),
|
|
10215
|
+
totalTokens,
|
|
10216
|
+
totalInputTokens,
|
|
10217
|
+
blendedCostPerToken,
|
|
10218
|
+
blendedCostPerMillionTokensUsd: blendedCostPerToken != null
|
|
10219
|
+
? roundMetric(blendedCostPerToken * 1_000_000, 4)
|
|
10220
|
+
: null,
|
|
10221
|
+
};
|
|
10222
|
+
}
|
|
10223
|
+
|
|
9474
10224
|
function summarizeTelemetry(metrics, days) {
|
|
9475
10225
|
const filtered = metrics.filter((m) => withinDays(m, days));
|
|
9476
10226
|
if (filtered.length === 0) return null;
|
|
@@ -9562,7 +10312,10 @@ function isEffectiveShreddingEvent(event) {
|
|
|
9562
10312
|
async function buildUsageAnalytics(days) {
|
|
9563
10313
|
const logDir = resolveAgentWorkLogDir();
|
|
9564
10314
|
const streamPath = resolve(logDir, "agent-work-stream.jsonl");
|
|
9565
|
-
const events = await
|
|
10315
|
+
const [{ entries: completedSessions }, events] = await Promise.all([
|
|
10316
|
+
readCompletedSessionEntries(100_000),
|
|
10317
|
+
readJsonlTail(streamPath, 100_000),
|
|
10318
|
+
]);
|
|
9566
10319
|
|
|
9567
10320
|
const cutoff = days ? Date.now() - days * 24 * 60 * 60 * 1000 : 0;
|
|
9568
10321
|
|
|
@@ -9588,22 +10341,50 @@ async function buildUsageAnalytics(days) {
|
|
|
9588
10341
|
|
|
9589
10342
|
const allDates = new Set();
|
|
9590
10343
|
|
|
10344
|
+
const sessionWindow = completedSessions.filter((session) => {
|
|
10345
|
+
const ts = getEntryTimestamp(session);
|
|
10346
|
+
return !cutoff || (Number.isFinite(ts) && ts >= cutoff);
|
|
10347
|
+
});
|
|
10348
|
+
|
|
10349
|
+
if (sessionWindow.length > 0) {
|
|
10350
|
+
for (const session of sessionWindow) {
|
|
10351
|
+
const ts = getEntryTimestamp(session);
|
|
10352
|
+
if (!Number.isFinite(ts)) continue;
|
|
10353
|
+
if (ts < oldestTs) oldestTs = ts;
|
|
10354
|
+
if (ts > newestTs) newestTs = ts;
|
|
10355
|
+
const day = getEntryDayKey(session, ts);
|
|
10356
|
+
if (day) allDates.add(day);
|
|
10357
|
+
|
|
10358
|
+
agentRuns += 1;
|
|
10359
|
+
const exec = String(session.executor || session.model || "unknown").trim() || "unknown";
|
|
10360
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
10361
|
+
if (day) {
|
|
10362
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
10363
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
10364
|
+
}
|
|
10365
|
+
}
|
|
10366
|
+
}
|
|
10367
|
+
|
|
10368
|
+
let streamSessionStarts = 0;
|
|
9591
10369
|
for (const e of events) {
|
|
9592
|
-
const ts =
|
|
10370
|
+
const ts = getEntryTimestamp(e);
|
|
9593
10371
|
if (!Number.isFinite(ts)) continue;
|
|
9594
10372
|
if (cutoff && ts < cutoff) continue;
|
|
9595
10373
|
if (ts < oldestTs) oldestTs = ts;
|
|
9596
10374
|
if (ts > newestTs) newestTs = ts;
|
|
9597
|
-
const day = (e
|
|
10375
|
+
const day = getEntryDayKey(e, ts);
|
|
9598
10376
|
if (day) allDates.add(day);
|
|
9599
10377
|
|
|
9600
10378
|
if (e.event_type === "session_start") {
|
|
9601
|
-
|
|
9602
|
-
|
|
9603
|
-
|
|
9604
|
-
|
|
9605
|
-
(
|
|
9606
|
-
|
|
10379
|
+
streamSessionStarts += 1;
|
|
10380
|
+
if (sessionWindow.length === 0) {
|
|
10381
|
+
agentRuns++;
|
|
10382
|
+
const exec = e.executor || "unknown";
|
|
10383
|
+
agents.set(exec, (agents.get(exec) || 0) + 1);
|
|
10384
|
+
if (day) {
|
|
10385
|
+
(dailyAgents[day] = dailyAgents[day] || {})[exec] =
|
|
10386
|
+
(dailyAgents[day][exec] || 0) + 1;
|
|
10387
|
+
}
|
|
9607
10388
|
}
|
|
9608
10389
|
} else if (e.event_type === "skill_invoke") {
|
|
9609
10390
|
skillInvocations++;
|
|
@@ -9669,6 +10450,11 @@ async function buildUsageAnalytics(days) {
|
|
|
9669
10450
|
topSkills,
|
|
9670
10451
|
topMcpTools,
|
|
9671
10452
|
trend,
|
|
10453
|
+
diagnostics: {
|
|
10454
|
+
agentRunSource: sessionWindow.length > 0 ? "completed_sessions" : "session_start_events",
|
|
10455
|
+
completedSessions: sessionWindow.length,
|
|
10456
|
+
sessionStarts: streamSessionStarts,
|
|
10457
|
+
},
|
|
9672
10458
|
};
|
|
9673
10459
|
}
|
|
9674
10460
|
|
|
@@ -10391,6 +11177,44 @@ async function handleApi(req, res, url) {
|
|
|
10391
11177
|
return;
|
|
10392
11178
|
}
|
|
10393
11179
|
|
|
11180
|
+
if (path === "/api/tasks/export") {
|
|
11181
|
+
try {
|
|
11182
|
+
const adapter = getKanbanAdapter();
|
|
11183
|
+
const tasks = await listAllTasksForApi(adapter);
|
|
11184
|
+
jsonResponse(res, 200, {
|
|
11185
|
+
ok: true,
|
|
11186
|
+
data: buildTaskStateExportPayload(tasks, getKanbanBackendName()),
|
|
11187
|
+
});
|
|
11188
|
+
} catch (err) {
|
|
11189
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
11190
|
+
}
|
|
11191
|
+
return;
|
|
11192
|
+
}
|
|
11193
|
+
|
|
11194
|
+
if (path === "/api/tasks/import" && req.method === "POST") {
|
|
11195
|
+
try {
|
|
11196
|
+
const backend = getKanbanBackendName();
|
|
11197
|
+
if (backend !== "internal") {
|
|
11198
|
+
jsonResponse(res, 400, {
|
|
11199
|
+
ok: false,
|
|
11200
|
+
error: "Task state import is only supported for the internal backend.",
|
|
11201
|
+
});
|
|
11202
|
+
return;
|
|
11203
|
+
}
|
|
11204
|
+
const body = await readJsonBody(req, 10_000_000);
|
|
11205
|
+
const imported = await importInternalTaskStateSnapshot(body || {});
|
|
11206
|
+
jsonResponse(res, 200, { ok: true, data: imported });
|
|
11207
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
11208
|
+
reason: "task-state-imported",
|
|
11209
|
+
created: imported.summary?.created || 0,
|
|
11210
|
+
updated: imported.summary?.updated || 0,
|
|
11211
|
+
});
|
|
11212
|
+
} catch (err) {
|
|
11213
|
+
jsonResponse(res, 400, { ok: false, error: err.message });
|
|
11214
|
+
}
|
|
11215
|
+
return;
|
|
11216
|
+
}
|
|
11217
|
+
|
|
10394
11218
|
if (path === "/api/tasks") {
|
|
10395
11219
|
const status = url.searchParams.get("status") || "";
|
|
10396
11220
|
const projectId = url.searchParams.get("project") || "";
|
|
@@ -10477,6 +11301,7 @@ async function handleApi(req, res, url) {
|
|
|
10477
11301
|
const statusCounts = {
|
|
10478
11302
|
draft: 0,
|
|
10479
11303
|
backlog: 0,
|
|
11304
|
+
blocked: 0,
|
|
10480
11305
|
inProgress: 0,
|
|
10481
11306
|
inReview: 0,
|
|
10482
11307
|
done: 0,
|
|
@@ -10510,6 +11335,11 @@ async function handleApi(req, res, url) {
|
|
|
10510
11335
|
try {
|
|
10511
11336
|
const taskId =
|
|
10512
11337
|
url.searchParams.get("taskId") || url.searchParams.get("id") || "";
|
|
11338
|
+
const includeDagParam = String(url.searchParams.get("includeDag") || "").trim().toLowerCase();
|
|
11339
|
+
const includeWorkflowRunsParam = String(url.searchParams.get("includeWorkflowRuns") || "").trim().toLowerCase();
|
|
11340
|
+
const includeDag = !["0", "false", "no"].includes(includeDagParam);
|
|
11341
|
+
const includeWorkflowRuns = !["0", "false", "no"].includes(includeWorkflowRunsParam);
|
|
11342
|
+
const workspaceContext = resolveWorkspaceContextFromRequest(url, { allowAll: false });
|
|
10513
11343
|
if (!taskId) {
|
|
10514
11344
|
jsonResponse(res, 400, { ok: false, error: "taskId required" });
|
|
10515
11345
|
return;
|
|
@@ -10519,25 +11349,46 @@ async function handleApi(req, res, url) {
|
|
|
10519
11349
|
const enriched = await applySharedStateToTasks(task ? [task] : []);
|
|
10520
11350
|
let detailTask = enriched[0] || null;
|
|
10521
11351
|
if (detailTask) {
|
|
10522
|
-
const workflowRuns =
|
|
10523
|
-
|
|
11352
|
+
const workflowRuns = includeWorkflowRuns
|
|
11353
|
+
? await collectWorkflowRunsForTask(detailTask.id, url, 40)
|
|
11354
|
+
: [];
|
|
11355
|
+
const mergedWorkflowRuns = includeWorkflowRuns
|
|
11356
|
+
? mergeTaskWorkflowRuns(detailTask.workflowRuns, workflowRuns, 80)
|
|
11357
|
+
: Array.isArray(detailTask.workflowRuns)
|
|
11358
|
+
? detailTask.workflowRuns
|
|
11359
|
+
: [];
|
|
10524
11360
|
detailTask.workflowRuns = mergedWorkflowRuns;
|
|
11361
|
+
const canStart = await evaluateTaskCanStart({
|
|
11362
|
+
taskId: detailTask.id,
|
|
11363
|
+
task: detailTask,
|
|
11364
|
+
reqUrl: url,
|
|
11365
|
+
adapter,
|
|
11366
|
+
});
|
|
10525
11367
|
|
|
10526
11368
|
const sprintId = resolveTaskSprintId(detailTask);
|
|
10527
|
-
const sprintDag = sprintId ? await getSprintDagData(sprintId) : null;
|
|
10528
|
-
const globalDag = await getGlobalDagData();
|
|
11369
|
+
const sprintDag = includeDag && sprintId ? await getSprintDagData(sprintId) : null;
|
|
11370
|
+
const globalDag = includeDag ? await getGlobalDagData() : null;
|
|
11371
|
+
const blockedContext = buildTaskBlockedContext(detailTask, {
|
|
11372
|
+
canStart,
|
|
11373
|
+
workflowRuns: mergedWorkflowRuns,
|
|
11374
|
+
workspaceDir: workspaceContext?.workspaceDir || repoRoot,
|
|
11375
|
+
});
|
|
10529
11376
|
|
|
10530
11377
|
detailTask.meta = {
|
|
10531
11378
|
...(detailTask.meta || {}),
|
|
10532
11379
|
workflowRuns: mergedWorkflowRuns,
|
|
10533
11380
|
historyCount: Array.isArray(detailTask.statusHistory) ? detailTask.statusHistory.length : 0,
|
|
10534
11381
|
timelineCount: Array.isArray(detailTask.timeline) ? detailTask.timeline.length : 0,
|
|
11382
|
+
canStart,
|
|
11383
|
+
blockedContext,
|
|
10535
11384
|
...(sprintId ? { sprintId } : {}),
|
|
10536
11385
|
...(sprintDag ? { sprintDag: sprintDag.data } : {}),
|
|
10537
11386
|
...(globalDag ? { dagOfDags: globalDag.data } : {}),
|
|
10538
11387
|
};
|
|
10539
11388
|
if (sprintDag) detailTask.sprintDag = sprintDag.data;
|
|
10540
11389
|
if (globalDag) detailTask.dagOfDags = globalDag.data;
|
|
11390
|
+
detailTask.canStart = canStart;
|
|
11391
|
+
detailTask.blockedContext = blockedContext;
|
|
10541
11392
|
detailTask = withTaskRuntimeSnapshot(detailTask);
|
|
10542
11393
|
}
|
|
10543
11394
|
jsonResponse(res, 200, { ok: true, data: detailTask });
|
|
@@ -11488,6 +12339,41 @@ async function handleApi(req, res, url) {
|
|
|
11488
12339
|
}
|
|
11489
12340
|
return;
|
|
11490
12341
|
}
|
|
12342
|
+
|
|
12343
|
+
if (path === "/api/tasks/dag/organize" && req.method === "POST") {
|
|
12344
|
+
try {
|
|
12345
|
+
const body = await readJsonBody(req);
|
|
12346
|
+
const sprintId = String(body?.sprintId || body?.sprint || "").trim();
|
|
12347
|
+
const organizeOptions = {
|
|
12348
|
+
...(sprintId ? { sprintId } : {}),
|
|
12349
|
+
...(body?.applyDependencySuggestions != null
|
|
12350
|
+
? { applyDependencySuggestions: Boolean(body.applyDependencySuggestions) }
|
|
12351
|
+
: {}),
|
|
12352
|
+
...(body?.syncEpicDependencies != null
|
|
12353
|
+
? { syncEpicDependencies: Boolean(body.syncEpicDependencies) }
|
|
12354
|
+
: {}),
|
|
12355
|
+
};
|
|
12356
|
+
const organized = await organizeDagData(organizeOptions);
|
|
12357
|
+
if (!organized) {
|
|
12358
|
+
jsonResponse(res, 501, { ok: false, error: "DAG organize API is unavailable." });
|
|
12359
|
+
return;
|
|
12360
|
+
}
|
|
12361
|
+
jsonResponse(res, 200, {
|
|
12362
|
+
ok: true,
|
|
12363
|
+
sprintId: sprintId || null,
|
|
12364
|
+
source: organized.source,
|
|
12365
|
+
data: organized.data,
|
|
12366
|
+
suggestions: Array.isArray(organized.data?.suggestions) ? organized.data.suggestions : [],
|
|
12367
|
+
});
|
|
12368
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
12369
|
+
reason: "dag-organized",
|
|
12370
|
+
sprintId: sprintId || null,
|
|
12371
|
+
});
|
|
12372
|
+
} catch (err) {
|
|
12373
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
12374
|
+
}
|
|
12375
|
+
return;
|
|
12376
|
+
}
|
|
11491
12377
|
if (path === "/api/tasks/attachments/upload" && req.method === "POST") {
|
|
11492
12378
|
try {
|
|
11493
12379
|
const { fields, files } = await readMultipartForm(req);
|
|
@@ -11825,11 +12711,20 @@ async function handleApi(req, res, url) {
|
|
|
11825
12711
|
const tagsProvided = hasOwn(body, "tags");
|
|
11826
12712
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
11827
12713
|
const draftProvided = hasOwn(body, "draft");
|
|
12714
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
12715
|
+
const blockedReason = blockedReasonProvided
|
|
12716
|
+
? String(body?.blockedReason || "").trim() || null
|
|
12717
|
+
: undefined;
|
|
11828
12718
|
const baseBranchProvided = hasOwn(body, "baseBranch") || hasOwn(body, "base_branch");
|
|
11829
12719
|
const baseBranch = baseBranchProvided
|
|
11830
12720
|
? normalizeBranchInput(body?.baseBranch ?? body?.base_branch)
|
|
11831
12721
|
: undefined;
|
|
11832
12722
|
const metadataPatch = buildTaskMetadataPatch(body || {});
|
|
12723
|
+
const requestedStatus = normalizeTaskStatusKey(body?.status);
|
|
12724
|
+
const clearsBlockedState = requestedStatus === "todo";
|
|
12725
|
+
const nextMeta = (Object.keys(metadataPatch.meta).length > 0 || clearsBlockedState)
|
|
12726
|
+
? buildTaskMetaPatch(previousTask?.meta, metadataPatch.meta, { clearBlockedState: clearsBlockedState })
|
|
12727
|
+
: null;
|
|
11833
12728
|
const patch = {
|
|
11834
12729
|
status: body?.status,
|
|
11835
12730
|
title: body?.title,
|
|
@@ -11840,16 +12735,13 @@ async function handleApi(req, res, url) {
|
|
|
11840
12735
|
repositories: Array.isArray(body?.repositories) ? body.repositories : undefined,
|
|
11841
12736
|
...(tagsProvided ? { tags } : {}),
|
|
11842
12737
|
...(draftProvided ? { draft: Boolean(body?.draft) } : {}),
|
|
12738
|
+
...(clearsBlockedState
|
|
12739
|
+
? { cooldownUntil: null, blockedReason: null }
|
|
12740
|
+
: (blockedReasonProvided ? { blockedReason } : {})),
|
|
12741
|
+
...(clearsBlockedState ? { replaceMeta: true } : {}),
|
|
11843
12742
|
...(baseBranchProvided ? { baseBranch } : {}),
|
|
11844
12743
|
...metadataPatch.topLevel,
|
|
11845
|
-
...(
|
|
11846
|
-
? {
|
|
11847
|
-
meta: {
|
|
11848
|
-
...(previousTask?.meta && typeof previousTask.meta === "object" ? previousTask.meta : {}),
|
|
11849
|
-
...metadataPatch.meta,
|
|
11850
|
-
},
|
|
11851
|
-
}
|
|
11852
|
-
: {}),
|
|
12744
|
+
...(nextMeta ? { meta: nextMeta } : {}),
|
|
11853
12745
|
};
|
|
11854
12746
|
if (!hasTaskPatchValues(patch) && !baseBranchProvided && !draftProvided && !tagsProvided) {
|
|
11855
12747
|
jsonResponse(res, 400, {
|
|
@@ -11960,11 +12852,20 @@ async function handleApi(req, res, url) {
|
|
|
11960
12852
|
const tagsProvided = hasOwn(body, "tags");
|
|
11961
12853
|
const tags = tagsProvided ? normalizeTagsInput(body?.tags) : undefined;
|
|
11962
12854
|
const draftProvided = hasOwn(body, "draft");
|
|
12855
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
12856
|
+
const blockedReason = blockedReasonProvided
|
|
12857
|
+
? String(body?.blockedReason || "").trim() || null
|
|
12858
|
+
: undefined;
|
|
11963
12859
|
const baseBranchProvided = hasOwn(body, "baseBranch") || hasOwn(body, "base_branch");
|
|
11964
12860
|
const baseBranch = baseBranchProvided
|
|
11965
12861
|
? normalizeBranchInput(body?.baseBranch ?? body?.base_branch)
|
|
11966
12862
|
: undefined;
|
|
11967
12863
|
const metadataPatch = buildTaskMetadataPatch(body || {});
|
|
12864
|
+
const requestedStatus = normalizeTaskStatusKey(body?.status);
|
|
12865
|
+
const clearsBlockedState = requestedStatus === "todo";
|
|
12866
|
+
const nextMeta = (Object.keys(metadataPatch.meta).length > 0 || clearsBlockedState)
|
|
12867
|
+
? buildTaskMetaPatch(previousTask?.meta, metadataPatch.meta, { clearBlockedState: clearsBlockedState })
|
|
12868
|
+
: null;
|
|
11968
12869
|
const patch = {
|
|
11969
12870
|
title: body?.title,
|
|
11970
12871
|
description: body?.description,
|
|
@@ -11975,16 +12876,13 @@ async function handleApi(req, res, url) {
|
|
|
11975
12876
|
repositories: Array.isArray(body?.repositories) ? body.repositories : undefined,
|
|
11976
12877
|
...(tagsProvided ? { tags } : {}),
|
|
11977
12878
|
...(draftProvided ? { draft: Boolean(body?.draft) } : {}),
|
|
12879
|
+
...(clearsBlockedState
|
|
12880
|
+
? { cooldownUntil: null, blockedReason: null }
|
|
12881
|
+
: (blockedReasonProvided ? { blockedReason } : {})),
|
|
12882
|
+
...(clearsBlockedState ? { replaceMeta: true } : {}),
|
|
11978
12883
|
...(baseBranchProvided ? { baseBranch } : {}),
|
|
11979
12884
|
...metadataPatch.topLevel,
|
|
11980
|
-
...(
|
|
11981
|
-
? {
|
|
11982
|
-
meta: {
|
|
11983
|
-
...(previousTask?.meta && typeof previousTask.meta === "object" ? previousTask.meta : {}),
|
|
11984
|
-
...metadataPatch.meta,
|
|
11985
|
-
},
|
|
11986
|
-
}
|
|
11987
|
-
: {}),
|
|
12885
|
+
...(nextMeta ? { meta: nextMeta } : {}),
|
|
11988
12886
|
};
|
|
11989
12887
|
if (!hasTaskPatchValues(patch) && !baseBranchProvided && !draftProvided && !tagsProvided) {
|
|
11990
12888
|
jsonResponse(res, 400, {
|
|
@@ -12157,6 +13055,10 @@ async function handleApi(req, res, url) {
|
|
|
12157
13055
|
const adapter = getKanbanAdapter();
|
|
12158
13056
|
const tags = normalizeTagsInput(body?.tags);
|
|
12159
13057
|
const wantsDraft = Boolean(body?.draft) || body?.status === "draft";
|
|
13058
|
+
const blockedReasonProvided = hasOwn(body, "blockedReason");
|
|
13059
|
+
const blockedReason = blockedReasonProvided
|
|
13060
|
+
? String(body?.blockedReason || "").trim() || null
|
|
13061
|
+
: undefined;
|
|
12160
13062
|
const baseBranch = normalizeBranchInput(body?.baseBranch ?? body?.base_branch);
|
|
12161
13063
|
const activeWorkspace = getActiveManagedWorkspace(resolveUiConfigDir());
|
|
12162
13064
|
const defaultRepository =
|
|
@@ -12175,6 +13077,7 @@ async function handleApi(req, res, url) {
|
|
|
12175
13077
|
description: body?.description || "",
|
|
12176
13078
|
status: body?.status || (wantsDraft ? "draft" : "todo"),
|
|
12177
13079
|
priority: body?.priority || undefined,
|
|
13080
|
+
...(blockedReasonProvided ? { blockedReason } : {}),
|
|
12178
13081
|
...(workspace ? { workspace } : {}),
|
|
12179
13082
|
...(repository ? { repository } : {}),
|
|
12180
13083
|
...(repositories.length ? { repositories } : {}),
|
|
@@ -13704,7 +14607,10 @@ async function handleApi(req, res, url) {
|
|
|
13704
14607
|
resolveAgentWorkLogDir(),
|
|
13705
14608
|
"shredding-stats.jsonl",
|
|
13706
14609
|
);
|
|
13707
|
-
const raw = await
|
|
14610
|
+
const [{ entries: completedSessions }, raw] = await Promise.all([
|
|
14611
|
+
readCompletedSessionEntries(100_000),
|
|
14612
|
+
readJsonlTail(shreddingPath, 10_000),
|
|
14613
|
+
]);
|
|
13708
14614
|
const inWindow = raw.filter((e) => withinDays(e, days));
|
|
13709
14615
|
let excludedSynthetic = 0;
|
|
13710
14616
|
let excludedNoop = 0;
|
|
@@ -13729,7 +14635,14 @@ async function handleApi(req, res, url) {
|
|
|
13729
14635
|
let totalOriginalChars = 0;
|
|
13730
14636
|
let totalCompressedChars = 0;
|
|
13731
14637
|
let totalSavedChars = 0;
|
|
14638
|
+
let totalOriginalTokensEstimated = 0;
|
|
14639
|
+
let totalCompressedTokensEstimated = 0;
|
|
14640
|
+
let totalSavedTokensEstimated = 0;
|
|
13732
14641
|
const dailySaved = {};
|
|
14642
|
+
const dailyOriginal = {};
|
|
14643
|
+
const dailyCompressed = {};
|
|
14644
|
+
const dailySavedTokensEstimated = {};
|
|
14645
|
+
const dailyCostSavedUsd = {};
|
|
13733
14646
|
const dailyCounts = {};
|
|
13734
14647
|
const agentCounts = {};
|
|
13735
14648
|
const stageCounts = {};
|
|
@@ -13740,17 +14653,37 @@ async function handleApi(req, res, url) {
|
|
|
13740
14653
|
let liveOriginalChars = 0;
|
|
13741
14654
|
let liveCompressedChars = 0;
|
|
13742
14655
|
let liveSavedChars = 0;
|
|
14656
|
+
let liveSavedTokensEstimated = 0;
|
|
14657
|
+
const sessionCostModel = summarizeObservedSessionCostModel(
|
|
14658
|
+
completedSessions.filter((entry) => withinDays(entry, days)),
|
|
14659
|
+
);
|
|
14660
|
+
const blendedCostPerToken = sessionCostModel.blendedCostPerToken;
|
|
13743
14661
|
|
|
13744
14662
|
for (const e of events) {
|
|
13745
14663
|
const originalChars = numberOrZero(e.originalChars);
|
|
13746
14664
|
const compressedChars = numberOrZero(e.compressedChars);
|
|
13747
14665
|
const savedChars = numberOrZero(e.savedChars);
|
|
14666
|
+
const originalTokensEstimated = estimateTokensFromChars(originalChars);
|
|
14667
|
+
const compressedTokensEstimated = estimateTokensFromChars(compressedChars);
|
|
14668
|
+
const savedTokensEstimated = estimateTokensFromChars(savedChars);
|
|
14669
|
+
const estimatedCostSavedUsd = blendedCostPerToken != null
|
|
14670
|
+
? roundMetric(savedTokensEstimated * blendedCostPerToken)
|
|
14671
|
+
: null;
|
|
13748
14672
|
totalOriginalChars += originalChars;
|
|
13749
14673
|
totalCompressedChars += compressedChars;
|
|
13750
14674
|
totalSavedChars += savedChars;
|
|
13751
|
-
|
|
14675
|
+
totalOriginalTokensEstimated += originalTokensEstimated;
|
|
14676
|
+
totalCompressedTokensEstimated += compressedTokensEstimated;
|
|
14677
|
+
totalSavedTokensEstimated += savedTokensEstimated;
|
|
14678
|
+
const day = getEntryDayKey(e);
|
|
13752
14679
|
if (day) {
|
|
14680
|
+
dailyOriginal[day] = (dailyOriginal[day] || 0) + originalChars;
|
|
14681
|
+
dailyCompressed[day] = (dailyCompressed[day] || 0) + compressedChars;
|
|
13753
14682
|
dailySaved[day] = (dailySaved[day] || 0) + savedChars;
|
|
14683
|
+
dailySavedTokensEstimated[day] = (dailySavedTokensEstimated[day] || 0) + savedTokensEstimated;
|
|
14684
|
+
if (estimatedCostSavedUsd != null) {
|
|
14685
|
+
dailyCostSavedUsd[day] = roundMetric((dailyCostSavedUsd[day] || 0) + estimatedCostSavedUsd);
|
|
14686
|
+
}
|
|
13754
14687
|
dailyCounts[day] = (dailyCounts[day] || 0) + 1;
|
|
13755
14688
|
}
|
|
13756
14689
|
const agent = normalizeShreddingAgentType(e.agentType);
|
|
@@ -13764,6 +14697,7 @@ async function handleApi(req, res, url) {
|
|
|
13764
14697
|
liveOriginalChars += originalChars;
|
|
13765
14698
|
liveCompressedChars += compressedChars;
|
|
13766
14699
|
liveSavedChars += savedChars;
|
|
14700
|
+
liveSavedTokensEstimated += savedTokensEstimated;
|
|
13767
14701
|
const compactionFamily = String(e.compactionFamily || "unknown").trim().toLowerCase() || "unknown";
|
|
13768
14702
|
const commandFamily = String(e.commandFamily || "unknown").trim().toLowerCase() || "unknown";
|
|
13769
14703
|
compactionFamilyCounts[compactionFamily] = (compactionFamilyCounts[compactionFamily] || 0) + 1;
|
|
@@ -13798,12 +14732,27 @@ async function handleApi(req, res, url) {
|
|
|
13798
14732
|
savedPct: numberOrZero(e.savedPct),
|
|
13799
14733
|
originalChars: numberOrZero(e.originalChars),
|
|
13800
14734
|
compressedChars: numberOrZero(e.compressedChars),
|
|
14735
|
+
estimatedSavedTokens: estimateTokensFromChars(numberOrZero(e.savedChars)),
|
|
14736
|
+
estimatedCostSavedUsd: blendedCostPerToken != null
|
|
14737
|
+
? roundMetric(estimateTokensFromChars(numberOrZero(e.savedChars)) * blendedCostPerToken)
|
|
14738
|
+
: null,
|
|
13801
14739
|
agentType: normalizeShreddingAgentType(e.agentType),
|
|
13802
14740
|
attemptId: e.attemptId || null,
|
|
13803
14741
|
stage: String(e.stage || "session_total").trim().toLowerCase() || "session_total",
|
|
13804
14742
|
compactionFamily: String(e.compactionFamily || "").trim().toLowerCase() || null,
|
|
13805
14743
|
commandFamily: String(e.commandFamily || "").trim().toLowerCase() || null,
|
|
13806
14744
|
}));
|
|
14745
|
+
const dailyReductionPct = {};
|
|
14746
|
+
for (const day of Object.keys(dailyOriginal)) {
|
|
14747
|
+
const originalChars = numberOrZero(dailyOriginal[day]);
|
|
14748
|
+
const savedChars = numberOrZero(dailySaved[day]);
|
|
14749
|
+
dailyReductionPct[day] = originalChars > 0
|
|
14750
|
+
? Math.round((savedChars / originalChars) * 100)
|
|
14751
|
+
: 0;
|
|
14752
|
+
}
|
|
14753
|
+
const totalEstimatedCostSavedUsd = blendedCostPerToken != null
|
|
14754
|
+
? roundMetric(totalSavedTokensEstimated * blendedCostPerToken)
|
|
14755
|
+
: null;
|
|
13807
14756
|
|
|
13808
14757
|
jsonResponse(res, 200, {
|
|
13809
14758
|
ok: true,
|
|
@@ -13814,17 +14763,35 @@ async function handleApi(req, res, url) {
|
|
|
13814
14763
|
totalSavedChars,
|
|
13815
14764
|
avgSavedPct,
|
|
13816
14765
|
sortedDates,
|
|
14766
|
+
dailyOriginal,
|
|
14767
|
+
dailyCompressed,
|
|
13817
14768
|
dailySaved,
|
|
14769
|
+
dailySavedTokensEstimated,
|
|
14770
|
+
dailyCostSavedUsd,
|
|
14771
|
+
dailyReductionPct,
|
|
13818
14772
|
dailyCounts,
|
|
13819
14773
|
topAgents,
|
|
13820
14774
|
stageCounts,
|
|
13821
14775
|
topCompactionFamilies,
|
|
13822
14776
|
topCommandFamilies,
|
|
14777
|
+
totals: {
|
|
14778
|
+
originalTokensEstimated: totalOriginalTokensEstimated,
|
|
14779
|
+
compressedTokensEstimated: totalCompressedTokensEstimated,
|
|
14780
|
+
savedTokensEstimated: totalSavedTokensEstimated,
|
|
14781
|
+
estimatedCostSavedUsd: totalEstimatedCostSavedUsd,
|
|
14782
|
+
},
|
|
14783
|
+
estimation: {
|
|
14784
|
+
charsPerToken: SHREDDING_ESTIMATED_CHARS_PER_TOKEN,
|
|
14785
|
+
costModel: blendedCostPerToken != null ? "observed_blended_session_cost" : "unavailable",
|
|
14786
|
+
blendedCostPerMillionTokensUsd: sessionCostModel.blendedCostPerMillionTokensUsd,
|
|
14787
|
+
pricedSessions: sessionCostModel.pricedSessions,
|
|
14788
|
+
},
|
|
13823
14789
|
liveCompaction: {
|
|
13824
14790
|
totalEvents: liveTotalEvents,
|
|
13825
14791
|
totalOriginalChars: liveOriginalChars,
|
|
13826
14792
|
totalCompressedChars: liveCompressedChars,
|
|
13827
14793
|
totalSavedChars: liveSavedChars,
|
|
14794
|
+
savedTokensEstimated: liveSavedTokensEstimated,
|
|
13828
14795
|
avgSavedPct: liveAvgSavedPct,
|
|
13829
14796
|
},
|
|
13830
14797
|
recentEvents,
|
|
@@ -13835,6 +14802,7 @@ async function handleApi(req, res, url) {
|
|
|
13835
14802
|
unknownAttribution,
|
|
13836
14803
|
includeSynthetic,
|
|
13837
14804
|
includeNoop,
|
|
14805
|
+
pricedSessions: sessionCostModel.pricedSessions,
|
|
13838
14806
|
},
|
|
13839
14807
|
},
|
|
13840
14808
|
});
|
|
@@ -14575,6 +15543,9 @@ async function handleApi(req, res, url) {
|
|
|
14575
15543
|
templateName: template.name,
|
|
14576
15544
|
workflowId,
|
|
14577
15545
|
variables: { ...template.variables, ...userVars },
|
|
15546
|
+
workspaceId,
|
|
15547
|
+
repository: executeInput.repository || executeInput._targetRepo || null,
|
|
15548
|
+
targetRepo: executeInput._targetRepo || executeInput.repository || null,
|
|
14578
15549
|
dispatchedAt,
|
|
14579
15550
|
});
|
|
14580
15551
|
return;
|
|
@@ -14587,6 +15558,9 @@ async function handleApi(req, res, url) {
|
|
|
14587
15558
|
templateId,
|
|
14588
15559
|
templateName: template.name,
|
|
14589
15560
|
workflowId,
|
|
15561
|
+
workspaceId,
|
|
15562
|
+
repository: executeInput.repository || executeInput._targetRepo || null,
|
|
15563
|
+
targetRepo: executeInput._targetRepo || executeInput.repository || null,
|
|
14590
15564
|
result,
|
|
14591
15565
|
});
|
|
14592
15566
|
} catch (err) {
|
|
@@ -14646,6 +15620,31 @@ async function handleApi(req, res, url) {
|
|
|
14646
15620
|
return;
|
|
14647
15621
|
}
|
|
14648
15622
|
|
|
15623
|
+
if (path === "/api/workflows/reflow-template-layouts" && req.method === "POST") {
|
|
15624
|
+
try {
|
|
15625
|
+
const wfCtx = await getWorkflowRequestContext(url);
|
|
15626
|
+
if (!wfCtx.ok) {
|
|
15627
|
+
jsonResponse(res, wfCtx.status, { ok: false, error: wfCtx.error });
|
|
15628
|
+
return;
|
|
15629
|
+
}
|
|
15630
|
+
if (typeof _wfTemplates?.relayoutInstalledTemplateWorkflows !== "function") {
|
|
15631
|
+
jsonResponse(res, 503, { ok: false, error: "Template relayout service unavailable" });
|
|
15632
|
+
return;
|
|
15633
|
+
}
|
|
15634
|
+
const body = await readJsonBody(req).catch(() => ({}));
|
|
15635
|
+
const result = _wfTemplates.relayoutInstalledTemplateWorkflows(wfCtx.engine, {
|
|
15636
|
+
workflowIds: body?.workflowIds || body?.workflowId,
|
|
15637
|
+
});
|
|
15638
|
+
const workflows = result.updatedWorkflowIds
|
|
15639
|
+
.map((workflowId) => wfCtx.engine.get(workflowId))
|
|
15640
|
+
.filter(Boolean);
|
|
15641
|
+
jsonResponse(res, 200, { ok: true, result, workflows });
|
|
15642
|
+
} catch (err) {
|
|
15643
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
15644
|
+
}
|
|
15645
|
+
return;
|
|
15646
|
+
}
|
|
15647
|
+
|
|
14649
15648
|
if (path === "/api/workflows/template-updates") {
|
|
14650
15649
|
try {
|
|
14651
15650
|
const wfCtx = await getWorkflowRequestContext(url);
|
|
@@ -14953,6 +15952,23 @@ async function handleApi(req, res, url) {
|
|
|
14953
15952
|
return;
|
|
14954
15953
|
}
|
|
14955
15954
|
|
|
15955
|
+
if (action === "reflow-layout" && req.method === "POST") {
|
|
15956
|
+
if (typeof _wfTemplates?.relayoutInstalledTemplateWorkflows !== "function") {
|
|
15957
|
+
jsonResponse(res, 503, { ok: false, error: "Template relayout service unavailable" });
|
|
15958
|
+
return;
|
|
15959
|
+
}
|
|
15960
|
+
const result = _wfTemplates.relayoutInstalledTemplateWorkflows(engine, {
|
|
15961
|
+
workflowId,
|
|
15962
|
+
});
|
|
15963
|
+
const workflow = engine.get(workflowId);
|
|
15964
|
+
if (!workflow) {
|
|
15965
|
+
jsonResponse(res, 404, { ok: false, error: "Workflow not found after relayout" });
|
|
15966
|
+
return;
|
|
15967
|
+
}
|
|
15968
|
+
jsonResponse(res, 200, { ok: true, workflow, result });
|
|
15969
|
+
return;
|
|
15970
|
+
}
|
|
15971
|
+
|
|
14956
15972
|
if (action === "runs") {
|
|
14957
15973
|
const rawOffset = Number(url.searchParams.get("offset"));
|
|
14958
15974
|
const rawLimit = Number(url.searchParams.get("limit"));
|
|
@@ -15074,7 +16090,7 @@ async function handleApi(req, res, url) {
|
|
|
15074
16090
|
if (path === "/api/manual-flows/execute" && req.method === "POST") {
|
|
15075
16091
|
try {
|
|
15076
16092
|
const body = await readJsonBody(req);
|
|
15077
|
-
const { templateId, formValues } = body || {};
|
|
16093
|
+
const { templateId, formValues, executionContext } = body || {};
|
|
15078
16094
|
if (!templateId) {
|
|
15079
16095
|
jsonResponse(res, 400, { ok: false, error: "templateId is required" });
|
|
15080
16096
|
return;
|
|
@@ -15082,7 +16098,40 @@ async function handleApi(req, res, url) {
|
|
|
15082
16098
|
const mf = await import("../workflow/manual-flows.mjs");
|
|
15083
16099
|
const ctx = resolveActiveWorkspaceExecutionContext();
|
|
15084
16100
|
const wfCtx = await getWorkflowRequestContext(url);
|
|
15085
|
-
const
|
|
16101
|
+
const repository = String(
|
|
16102
|
+
executionContext?.repository ||
|
|
16103
|
+
executionContext?.targetRepo ||
|
|
16104
|
+
formValues?._targetRepo ||
|
|
16105
|
+
resolveDefaultRepositoryForWorkspaceContext(ctx),
|
|
16106
|
+
).trim();
|
|
16107
|
+
const workspaceId = String(
|
|
16108
|
+
executionContext?.workspaceId ||
|
|
16109
|
+
executionContext?.workspace ||
|
|
16110
|
+
ctx.workspaceId ||
|
|
16111
|
+
"",
|
|
16112
|
+
).trim();
|
|
16113
|
+
const projectId = String(
|
|
16114
|
+
executionContext?.projectId ||
|
|
16115
|
+
executionContext?.project ||
|
|
16116
|
+
body?.project ||
|
|
16117
|
+
"",
|
|
16118
|
+
).trim();
|
|
16119
|
+
const flowContext = {
|
|
16120
|
+
...(wfCtx?.ok ? { engine: wfCtx.engine } : {}),
|
|
16121
|
+
taskManager: createManualFlowTaskManager(ctx, {
|
|
16122
|
+
repository,
|
|
16123
|
+
workspaceId,
|
|
16124
|
+
projectId,
|
|
16125
|
+
templateId,
|
|
16126
|
+
}),
|
|
16127
|
+
runMetadata: {
|
|
16128
|
+
repository,
|
|
16129
|
+
workspaceId,
|
|
16130
|
+
workspaceDir: ctx.workspaceDir,
|
|
16131
|
+
projectId,
|
|
16132
|
+
triggerSource: "manual-ui",
|
|
16133
|
+
},
|
|
16134
|
+
};
|
|
15086
16135
|
const run = await mf.executeFlow(templateId, formValues || {}, ctx.workspaceDir, flowContext);
|
|
15087
16136
|
jsonResponse(res, 200, { ok: true, run });
|
|
15088
16137
|
} catch (err) {
|
|
@@ -15606,12 +16655,26 @@ async function handleApi(req, res, url) {
|
|
|
15606
16655
|
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
15607
16656
|
return;
|
|
15608
16657
|
}
|
|
15609
|
-
|
|
15610
|
-
|
|
15611
|
-
|
|
15612
|
-
|
|
16658
|
+
let nextTask = unblockInternalTask(taskId, {
|
|
16659
|
+
status: "todo",
|
|
16660
|
+
source: "manual-retry",
|
|
16661
|
+
});
|
|
16662
|
+
if (!nextTask) {
|
|
16663
|
+
if (typeof adapter.updateTask === "function") {
|
|
16664
|
+
await adapter.updateTask(taskId, {
|
|
16665
|
+
status: "todo",
|
|
16666
|
+
cooldownUntil: null,
|
|
16667
|
+
blockedReason: null,
|
|
16668
|
+
meta: task?.meta && typeof task.meta === "object"
|
|
16669
|
+
? Object.fromEntries(Object.entries(task.meta).filter(([key]) => key !== "autoRecovery"))
|
|
16670
|
+
: task?.meta,
|
|
16671
|
+
});
|
|
16672
|
+
} else if (typeof adapter.updateTaskStatus === "function") {
|
|
16673
|
+
await adapter.updateTaskStatus(taskId, "todo");
|
|
16674
|
+
}
|
|
16675
|
+
nextTask = await adapter.getTask(taskId);
|
|
15613
16676
|
}
|
|
15614
|
-
executor.executeTask(task).catch((error) => {
|
|
16677
|
+
executor.executeTask(nextTask || { ...task, status: "todo" }).catch((error) => {
|
|
15615
16678
|
console.warn(
|
|
15616
16679
|
`[telegram-ui] failed to retry task ${taskId}: ${error.message}`,
|
|
15617
16680
|
);
|
|
@@ -15636,6 +16699,58 @@ async function handleApi(req, res, url) {
|
|
|
15636
16699
|
return;
|
|
15637
16700
|
}
|
|
15638
16701
|
|
|
16702
|
+
if (path === "/api/tasks/unblock") {
|
|
16703
|
+
if (req.method !== "POST") {
|
|
16704
|
+
res.setHeader("Allow", "POST");
|
|
16705
|
+
jsonResponse(res, 405, { ok: false, error: "Method Not Allowed" });
|
|
16706
|
+
return;
|
|
16707
|
+
}
|
|
16708
|
+
try {
|
|
16709
|
+
const body = await readJsonBody(req);
|
|
16710
|
+
const taskId = body?.taskId || body?.id;
|
|
16711
|
+
const targetStatus = String(body?.status || "todo").trim().toLowerCase() || "todo";
|
|
16712
|
+
if (!taskId) {
|
|
16713
|
+
jsonResponse(res, 400, { ok: false, error: "taskId is required" });
|
|
16714
|
+
return;
|
|
16715
|
+
}
|
|
16716
|
+
const adapter = getKanbanAdapter();
|
|
16717
|
+
const task = await adapter.getTask(taskId);
|
|
16718
|
+
if (!task) {
|
|
16719
|
+
jsonResponse(res, 404, { ok: false, error: "Task not found." });
|
|
16720
|
+
return;
|
|
16721
|
+
}
|
|
16722
|
+
let updatedTask = unblockInternalTask(taskId, {
|
|
16723
|
+
status: targetStatus,
|
|
16724
|
+
source: "api.tasks.unblock",
|
|
16725
|
+
});
|
|
16726
|
+
if (!updatedTask) {
|
|
16727
|
+
const nextMeta = task?.meta && typeof task.meta === "object"
|
|
16728
|
+
? Object.fromEntries(Object.entries(task.meta).filter(([key]) => key !== "autoRecovery"))
|
|
16729
|
+
: task?.meta;
|
|
16730
|
+
if (typeof adapter.updateTask === "function") {
|
|
16731
|
+
await adapter.updateTask(taskId, {
|
|
16732
|
+
status: targetStatus,
|
|
16733
|
+
cooldownUntil: null,
|
|
16734
|
+
blockedReason: null,
|
|
16735
|
+
meta: nextMeta,
|
|
16736
|
+
});
|
|
16737
|
+
} else if (typeof adapter.updateTaskStatus === "function") {
|
|
16738
|
+
await adapter.updateTaskStatus(taskId, targetStatus);
|
|
16739
|
+
}
|
|
16740
|
+
updatedTask = await adapter.getTask(taskId);
|
|
16741
|
+
}
|
|
16742
|
+
jsonResponse(res, 200, { ok: true, taskId, data: updatedTask || null });
|
|
16743
|
+
broadcastUiEvent(
|
|
16744
|
+
["tasks", "overview", "executor", "agents"],
|
|
16745
|
+
"invalidate",
|
|
16746
|
+
{ reason: "task-unblocked", taskId },
|
|
16747
|
+
);
|
|
16748
|
+
} catch (err) {
|
|
16749
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
16750
|
+
}
|
|
16751
|
+
return;
|
|
16752
|
+
}
|
|
16753
|
+
|
|
15639
16754
|
// ── GET /api/retry-queue ───────────────────────────────────────────
|
|
15640
16755
|
if (path === "/api/retry-queue" && req.method === "GET") {
|
|
15641
16756
|
try {
|