bosun 0.40.2 → 0.40.4
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/README.md +4 -0
- package/cli.mjs +127 -69
- package/config/config.mjs +35 -15
- package/desktop/package.json +2 -2
- package/infra/monitor.mjs +52 -16
- package/infra/session-tracker.mjs +1 -0
- package/infra/sync-engine.mjs +6 -1
- package/infra/update-check.mjs +16 -10
- package/kanban/kanban-adapter.mjs +19 -4
- package/kanban/ve-orchestrator.ps1 +25 -0
- package/package.json +1 -1
- package/server/ui-server.mjs +502 -39
- package/task/task-executor.mjs +690 -6
- package/task/task-store.mjs +116 -1
- package/ui/components/kanban-board.js +137 -9
- package/ui/components/shared.js +107 -45
- package/ui/demo-defaults.js +20 -20
- package/ui/demo.html +26 -1
- package/ui/modules/mui.js +600 -397
- package/ui/styles/components.css +43 -3
- package/ui/styles/kanban.css +66 -11
- package/ui/styles.monolith.css +89 -0
- package/ui/tabs/agents.js +194 -20
- package/ui/tabs/tasks.js +673 -162
- package/workflow/workflow-engine.mjs +30 -29
- package/workflow/workflow-nodes.mjs +321 -22
- package/workflow/workflow-templates.mjs +1 -1
- package/workflow-templates/task-batch.mjs +10 -10
- package/workspace/workspace-manager.mjs +25 -0
- package/workspace/worktree-manager.mjs +8 -2
package/server/ui-server.mjs
CHANGED
|
@@ -188,6 +188,12 @@ const TASK_STORE_DEPENDENCY_EXPORTS = {
|
|
|
188
188
|
update: ["updateTask"],
|
|
189
189
|
};
|
|
190
190
|
const TASK_STORE_ASSIGN_SPRINT_EXPORTS = ["assignTaskToSprint", "setTaskSprint"];
|
|
191
|
+
const TASK_STORE_EPIC_DEPENDENCY_EXPORTS = Object.freeze({
|
|
192
|
+
list: ["getEpicDependencies", "listEpicDependencies"],
|
|
193
|
+
set: ["setEpicDependencies", "updateEpicDependencies"],
|
|
194
|
+
add: ["addEpicDependency"],
|
|
195
|
+
remove: ["removeEpicDependency"],
|
|
196
|
+
});
|
|
191
197
|
let taskStoreApi = null;
|
|
192
198
|
let taskStoreApiPromise = null;
|
|
193
199
|
let didLogTaskStoreLoadFailure = false;
|
|
@@ -6154,6 +6160,68 @@ async function setTaskDependenciesForApi({
|
|
|
6154
6160
|
};
|
|
6155
6161
|
}
|
|
6156
6162
|
|
|
6163
|
+
async function listEpicDependenciesForApi() {
|
|
6164
|
+
const listed = await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.list, []);
|
|
6165
|
+
if (listed.found && Array.isArray(listed.value)) {
|
|
6166
|
+
return {
|
|
6167
|
+
ok: true,
|
|
6168
|
+
source: `task-store.${listed.found}`,
|
|
6169
|
+
data: listed.value
|
|
6170
|
+
.map((entry) => ({
|
|
6171
|
+
epicId: String(entry?.epicId || entry?.id || "").trim(),
|
|
6172
|
+
dependencies: normalizeTaskIdList(entry?.dependencies || entry?.dependsOn || []),
|
|
6173
|
+
}))
|
|
6174
|
+
.filter((entry) => entry.epicId),
|
|
6175
|
+
};
|
|
6176
|
+
}
|
|
6177
|
+
return { ok: false, source: null, data: [] };
|
|
6178
|
+
}
|
|
6179
|
+
|
|
6180
|
+
async function setEpicDependenciesForApi({ epicId, dependencies }) {
|
|
6181
|
+
const normalizedEpicId = String(epicId || "").trim();
|
|
6182
|
+
if (!normalizedEpicId) return { ok: false, status: 400, error: "epicId required" };
|
|
6183
|
+
const normalizedDependencies = normalizeTaskIdList(dependencies, { exclude: normalizedEpicId });
|
|
6184
|
+
|
|
6185
|
+
const setResult = await callTaskStoreFunction(
|
|
6186
|
+
TASK_STORE_EPIC_DEPENDENCY_EXPORTS.set,
|
|
6187
|
+
[normalizedEpicId, normalizedDependencies],
|
|
6188
|
+
);
|
|
6189
|
+
if (setResult.found) {
|
|
6190
|
+
return {
|
|
6191
|
+
ok: true,
|
|
6192
|
+
source: `task-store.${setResult.found}`,
|
|
6193
|
+
data: {
|
|
6194
|
+
epicId: normalizedEpicId,
|
|
6195
|
+
dependencies: normalizeTaskIdList(setResult.value?.dependencies || normalizedDependencies),
|
|
6196
|
+
},
|
|
6197
|
+
};
|
|
6198
|
+
}
|
|
6199
|
+
|
|
6200
|
+
const listed = await listEpicDependenciesForApi();
|
|
6201
|
+
const current = listed.ok
|
|
6202
|
+
? normalizeTaskIdList((listed.data.find((entry) => entry.epicId === normalizedEpicId) || {}).dependencies || [])
|
|
6203
|
+
: [];
|
|
6204
|
+
|
|
6205
|
+
const toRemove = current.filter((entry) => !normalizedDependencies.includes(entry));
|
|
6206
|
+
const toAdd = normalizedDependencies.filter((entry) => !current.includes(entry));
|
|
6207
|
+
|
|
6208
|
+
for (const dep of toRemove) {
|
|
6209
|
+
await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.remove, [normalizedEpicId, dep]);
|
|
6210
|
+
}
|
|
6211
|
+
for (const dep of toAdd) {
|
|
6212
|
+
await callTaskStoreFunction(TASK_STORE_EPIC_DEPENDENCY_EXPORTS.add, [normalizedEpicId, dep]);
|
|
6213
|
+
}
|
|
6214
|
+
|
|
6215
|
+
const refreshed = await listEpicDependenciesForApi();
|
|
6216
|
+
const row = refreshed.ok
|
|
6217
|
+
? refreshed.data.find((entry) => entry.epicId === normalizedEpicId)
|
|
6218
|
+
: null;
|
|
6219
|
+
return {
|
|
6220
|
+
ok: true,
|
|
6221
|
+
source: refreshed.source || "task-store.fallback",
|
|
6222
|
+
data: { epicId: normalizedEpicId, dependencies: normalizeTaskIdList(row?.dependencies || []) },
|
|
6223
|
+
};
|
|
6224
|
+
}
|
|
6157
6225
|
async function getTaskCommentsForApi(taskId, adapter = null) {
|
|
6158
6226
|
const storeComments = await callTaskStoreFunction(TASK_STORE_COMMENT_EXPORTS, [taskId]);
|
|
6159
6227
|
if (storeComments.found && Array.isArray(storeComments.value)) {
|
|
@@ -6354,6 +6422,150 @@ function applyInternalLifecycleTransition(taskId, action, options = {}) {
|
|
|
6354
6422
|
return null;
|
|
6355
6423
|
}
|
|
6356
6424
|
|
|
6425
|
+
async function persistTaskStatusForExecution(adapter, taskId, nextStatus, source) {
|
|
6426
|
+
if (!taskId || !nextStatus || !adapter) return null;
|
|
6427
|
+
const normalized = String(nextStatus || "").trim();
|
|
6428
|
+
if (!normalized) return null;
|
|
6429
|
+
let updated = null;
|
|
6430
|
+
if (typeof adapter.updateTaskStatus === "function") {
|
|
6431
|
+
updated = await adapter.updateTaskStatus(taskId, normalized, { source });
|
|
6432
|
+
} else if (typeof adapter.updateTask === "function") {
|
|
6433
|
+
updated = await adapter.updateTask(taskId, { status: normalized });
|
|
6434
|
+
}
|
|
6435
|
+
return withTaskMetadataTopLevel(updated);
|
|
6436
|
+
}
|
|
6437
|
+
|
|
6438
|
+
async function persistTaskExecutionMeta(adapter, taskId, executionPatch = {}) {
|
|
6439
|
+
if (!taskId || !adapter || typeof adapter.updateTask !== "function") return null;
|
|
6440
|
+
const current = typeof adapter.getTask === "function"
|
|
6441
|
+
? await adapter.getTask(taskId).catch(() => null)
|
|
6442
|
+
: null;
|
|
6443
|
+
const currentMeta = current?.meta && typeof current.meta === "object" ? current.meta : {};
|
|
6444
|
+
const currentExecution = currentMeta.execution && typeof currentMeta.execution === "object"
|
|
6445
|
+
? currentMeta.execution
|
|
6446
|
+
: {};
|
|
6447
|
+
const nextExecution = {
|
|
6448
|
+
...currentExecution,
|
|
6449
|
+
...executionPatch,
|
|
6450
|
+
};
|
|
6451
|
+
if (nextExecution.queueState == null) delete nextExecution.queueState;
|
|
6452
|
+
return withTaskMetadataTopLevel(await adapter.updateTask(taskId, {
|
|
6453
|
+
meta: {
|
|
6454
|
+
...currentMeta,
|
|
6455
|
+
execution: nextExecution,
|
|
6456
|
+
},
|
|
6457
|
+
}));
|
|
6458
|
+
}
|
|
6459
|
+
|
|
6460
|
+
function resolveFallbackStatusAfterFailedDispatch(previousStatus, startDispatch) {
|
|
6461
|
+
if (startDispatch?.reason === "no_free_slots") return "queued";
|
|
6462
|
+
const previous = String(previousStatus || "").trim();
|
|
6463
|
+
return previous || "todo";
|
|
6464
|
+
}
|
|
6465
|
+
|
|
6466
|
+
async function reconcileTaskAfterDispatchAttempt({
|
|
6467
|
+
adapter,
|
|
6468
|
+
taskId,
|
|
6469
|
+
previousStatus,
|
|
6470
|
+
requestedStatus,
|
|
6471
|
+
lifecycleAction,
|
|
6472
|
+
startDispatch,
|
|
6473
|
+
source,
|
|
6474
|
+
}) {
|
|
6475
|
+
const action = normalizeLifecycleAction(lifecycleAction);
|
|
6476
|
+
if (action !== "start" && action !== "resume") return null;
|
|
6477
|
+
const requested = normalizeTaskStatusKey(requestedStatus);
|
|
6478
|
+
if (requested !== "inprogress") return null;
|
|
6479
|
+
const targetStatus = startDispatch?.started
|
|
6480
|
+
? "inprogress"
|
|
6481
|
+
: resolveFallbackStatusAfterFailedDispatch(previousStatus, startDispatch);
|
|
6482
|
+
return persistTaskStatusForExecution(adapter, taskId, targetStatus, source);
|
|
6483
|
+
}
|
|
6484
|
+
|
|
6485
|
+
function buildTaskRuntimeSnapshot(task) {
|
|
6486
|
+
const runtimeExecutor = uiDeps.getInternalExecutor?.() || null;
|
|
6487
|
+
const status = runtimeExecutor?.getStatus?.() || {};
|
|
6488
|
+
const activeSlots = Array.isArray(status?.slots) ? status.slots : [];
|
|
6489
|
+
const taskId = String(task?.id || task?.taskId || "").trim();
|
|
6490
|
+
const slot = taskId
|
|
6491
|
+
? activeSlots.find((entry) => String(entry?.taskId || entry?.task_id || "").trim() === taskId)
|
|
6492
|
+
: null;
|
|
6493
|
+
const normalizedStatus = normalizeTaskStatusKey(task?.status);
|
|
6494
|
+
const queuedFlag = task?.meta?.execution?.queued === true
|
|
6495
|
+
|| normalizeTaskStatusKey(task?.meta?.execution?.queueState) === "queued";
|
|
6496
|
+
if (slot) {
|
|
6497
|
+
return {
|
|
6498
|
+
state: "running",
|
|
6499
|
+
isLive: true,
|
|
6500
|
+
taskId,
|
|
6501
|
+
taskStatus: task?.status || null,
|
|
6502
|
+
statusLabel: "Live execution",
|
|
6503
|
+
slot: {
|
|
6504
|
+
taskId,
|
|
6505
|
+
branch: slot?.branch || slot?.branchName || null,
|
|
6506
|
+
sdk: slot?.sdk || slot?.executor || null,
|
|
6507
|
+
model: slot?.model || null,
|
|
6508
|
+
startedAt: slot?.startedAt || slot?.started_at || null,
|
|
6509
|
+
completedCount: slot?.completedCount || 0,
|
|
6510
|
+
},
|
|
6511
|
+
executor: {
|
|
6512
|
+
activeSlots: Number(status?.activeSlots || activeSlots.length || 0),
|
|
6513
|
+
maxParallel: Number(status?.maxParallel || 0),
|
|
6514
|
+
paused: runtimeExecutor?.isPaused?.() === true,
|
|
6515
|
+
},
|
|
6516
|
+
};
|
|
6517
|
+
}
|
|
6518
|
+
if (queuedFlag || normalizedStatus === "queued") {
|
|
6519
|
+
return {
|
|
6520
|
+
state: "queued",
|
|
6521
|
+
isLive: false,
|
|
6522
|
+
taskId,
|
|
6523
|
+
taskStatus: task?.status || null,
|
|
6524
|
+
statusLabel: "Queued for execution",
|
|
6525
|
+
reason: "no_free_slots",
|
|
6526
|
+
};
|
|
6527
|
+
}
|
|
6528
|
+
if (normalizedStatus === "inprogress") {
|
|
6529
|
+
return {
|
|
6530
|
+
state: "pending",
|
|
6531
|
+
isLive: false,
|
|
6532
|
+
taskId,
|
|
6533
|
+
taskStatus: task?.status || null,
|
|
6534
|
+
statusLabel: "No live execution detected",
|
|
6535
|
+
reason: "no_active_executor_slot",
|
|
6536
|
+
};
|
|
6537
|
+
}
|
|
6538
|
+
if (normalizedStatus === "inreview") {
|
|
6539
|
+
return {
|
|
6540
|
+
state: "review",
|
|
6541
|
+
isLive: false,
|
|
6542
|
+
taskId,
|
|
6543
|
+
taskStatus: task?.status || null,
|
|
6544
|
+
statusLabel: "Awaiting review",
|
|
6545
|
+
};
|
|
6546
|
+
}
|
|
6547
|
+
return {
|
|
6548
|
+
state: "idle",
|
|
6549
|
+
isLive: false,
|
|
6550
|
+
taskId,
|
|
6551
|
+
taskStatus: task?.status || null,
|
|
6552
|
+
statusLabel: "No active execution",
|
|
6553
|
+
};
|
|
6554
|
+
}
|
|
6555
|
+
|
|
6556
|
+
function withTaskRuntimeSnapshot(task) {
|
|
6557
|
+
if (!task || typeof task !== "object") return task;
|
|
6558
|
+
const runtimeSnapshot = buildTaskRuntimeSnapshot(task);
|
|
6559
|
+
return {
|
|
6560
|
+
...task,
|
|
6561
|
+
runtimeSnapshot,
|
|
6562
|
+
meta: {
|
|
6563
|
+
...(task.meta || {}),
|
|
6564
|
+
runtimeSnapshot,
|
|
6565
|
+
},
|
|
6566
|
+
};
|
|
6567
|
+
}
|
|
6568
|
+
|
|
6357
6569
|
async function maybeStartTaskFromLifecycleAction({
|
|
6358
6570
|
taskId,
|
|
6359
6571
|
updatedTask,
|
|
@@ -6848,14 +7060,13 @@ async function resolveLogPath(logType, query) {
|
|
|
6848
7060
|
return resolvePreferredSystemLogPath();
|
|
6849
7061
|
}
|
|
6850
7062
|
if (logType === "agent") {
|
|
7063
|
+
const matches = await listAgentLogFiles(query, 1);
|
|
7064
|
+
if (matches.length > 0) {
|
|
7065
|
+
return resolve(matches[0].source, matches[0].name);
|
|
7066
|
+
}
|
|
6851
7067
|
const agentLogsDir = await resolveAgentLogsDir();
|
|
6852
7068
|
const files = await readdir(agentLogsDir).catch(() => []);
|
|
6853
|
-
|
|
6854
|
-
if (query) {
|
|
6855
|
-
const q = query.toLowerCase();
|
|
6856
|
-
const filtered = candidates.filter((f) => f.toLowerCase().includes(q));
|
|
6857
|
-
if (filtered.length) candidates = filtered;
|
|
6858
|
-
}
|
|
7069
|
+
const candidates = files.filter((f) => f.endsWith(".log")).sort().reverse();
|
|
6859
7070
|
return candidates.length ? resolve(agentLogsDir, candidates[0]) : null;
|
|
6860
7071
|
}
|
|
6861
7072
|
return null;
|
|
@@ -7884,6 +8095,13 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
7884
8095
|
}
|
|
7885
8096
|
}
|
|
7886
8097
|
|
|
8098
|
+
if (hasOwn(input, "type")) {
|
|
8099
|
+
const type = normalizeTaskTypeInput(input?.type);
|
|
8100
|
+
if (type) {
|
|
8101
|
+
topLevel.type = type;
|
|
8102
|
+
}
|
|
8103
|
+
}
|
|
8104
|
+
|
|
7887
8105
|
if (hasOwn(input, "epicId")) {
|
|
7888
8106
|
const epicId = normalizeOptionalStringInput(input?.epicId);
|
|
7889
8107
|
if (epicId) {
|
|
@@ -7919,6 +8137,13 @@ function buildTaskMetadataPatch(input = {}) {
|
|
|
7919
8137
|
return { topLevel, meta };
|
|
7920
8138
|
}
|
|
7921
8139
|
|
|
8140
|
+
const TASK_TYPE_VALUES = new Set(["epic", "task", "subtask"]);
|
|
8141
|
+
|
|
8142
|
+
function normalizeTaskTypeInput(input) {
|
|
8143
|
+
const normalized = String(input ?? "").trim().toLowerCase();
|
|
8144
|
+
return TASK_TYPE_VALUES.has(normalized) ? normalized : null;
|
|
8145
|
+
}
|
|
8146
|
+
|
|
7922
8147
|
const SPRINT_EXECUTION_MODES = new Set(["sequential", "parallel"]);
|
|
7923
8148
|
|
|
7924
8149
|
function normalizeSprintExecutionMode(input) {
|
|
@@ -8259,11 +8484,40 @@ async function listAgentLogFiles(query = "", limit = 60) {
|
|
|
8259
8484
|
const entries = [];
|
|
8260
8485
|
const agentLogsDir = await resolveAgentLogsDir();
|
|
8261
8486
|
const files = await readdir(agentLogsDir).catch(() => []);
|
|
8487
|
+
const normalizedQuery = String(query || "").trim().toLowerCase();
|
|
8488
|
+
const queryTerms = Array.from(new Set([
|
|
8489
|
+
normalizedQuery,
|
|
8490
|
+
...normalizedQuery
|
|
8491
|
+
.split(/[^a-z0-9]+/i)
|
|
8492
|
+
.map((part) => part.trim())
|
|
8493
|
+
.filter((part) => part.length >= 3),
|
|
8494
|
+
].filter(Boolean)));
|
|
8495
|
+
|
|
8496
|
+
const scoreAgentLogMatch = (name, lines = []) => {
|
|
8497
|
+
if (!queryTerms.length) return 0;
|
|
8498
|
+
const fileName = String(name || "").toLowerCase();
|
|
8499
|
+
const joined = lines.join("\n").toLowerCase();
|
|
8500
|
+
let score = 0;
|
|
8501
|
+
for (const term of queryTerms) {
|
|
8502
|
+
if (fileName.includes(term)) score += 120;
|
|
8503
|
+
if (joined.includes(term)) score += 80;
|
|
8504
|
+
}
|
|
8505
|
+
if (joined.includes("task id:")) score += 8;
|
|
8506
|
+
if (/(error|warn|failed|exception|timeout|anomal)/i.test(joined)) score += 6;
|
|
8507
|
+
return score;
|
|
8508
|
+
};
|
|
8509
|
+
|
|
8262
8510
|
for (const name of files) {
|
|
8263
8511
|
if (!name.endsWith(".log")) continue;
|
|
8264
|
-
if (query && !name.toLowerCase().includes(query.toLowerCase())) continue;
|
|
8265
8512
|
try {
|
|
8266
|
-
const
|
|
8513
|
+
const filePath = resolve(agentLogsDir, name);
|
|
8514
|
+
const info = await stat(filePath);
|
|
8515
|
+
let score = 0;
|
|
8516
|
+
if (queryTerms.length) {
|
|
8517
|
+
const sample = await tailFile(filePath, 160, 250_000).catch(() => ({ lines: [] }));
|
|
8518
|
+
score = scoreAgentLogMatch(name, sample?.lines || []);
|
|
8519
|
+
if (score <= 0) continue;
|
|
8520
|
+
}
|
|
8267
8521
|
entries.push({
|
|
8268
8522
|
name,
|
|
8269
8523
|
source: agentLogsDir,
|
|
@@ -8271,15 +8525,115 @@ async function listAgentLogFiles(query = "", limit = 60) {
|
|
|
8271
8525
|
mtime:
|
|
8272
8526
|
info.mtime?.toISOString?.() || new Date(info.mtime).toISOString(),
|
|
8273
8527
|
mtimeMs: info.mtimeMs,
|
|
8528
|
+
score,
|
|
8274
8529
|
});
|
|
8275
8530
|
} catch {
|
|
8276
8531
|
// ignore
|
|
8277
8532
|
}
|
|
8278
8533
|
}
|
|
8279
|
-
entries.sort((a, b) => b.mtimeMs - a.mtimeMs);
|
|
8534
|
+
entries.sort((a, b) => (b.score || 0) - (a.score || 0) || b.mtimeMs - a.mtimeMs);
|
|
8280
8535
|
return entries.slice(0, limit);
|
|
8281
8536
|
}
|
|
8282
8537
|
|
|
8538
|
+
function buildLogQueryTerms(query = "") {
|
|
8539
|
+
const normalized = String(query || "").trim().toLowerCase();
|
|
8540
|
+
if (!normalized) return [];
|
|
8541
|
+
return Array.from(new Set([
|
|
8542
|
+
normalized,
|
|
8543
|
+
...normalized
|
|
8544
|
+
.split(/[^a-z0-9]+/i)
|
|
8545
|
+
.map((part) => part.trim())
|
|
8546
|
+
.filter((part) => part.length >= 3),
|
|
8547
|
+
].filter(Boolean)));
|
|
8548
|
+
}
|
|
8549
|
+
|
|
8550
|
+
function isHighSignalLogLine(line = "") {
|
|
8551
|
+
return /(error|warn|failed|exception|timeout|anomal|retry|blocked|fatal)/i.test(String(line || ""));
|
|
8552
|
+
}
|
|
8553
|
+
|
|
8554
|
+
function filterRelevantLogLines(lines = [], query = "", limit = 200) {
|
|
8555
|
+
const sourceLines = Array.isArray(lines)
|
|
8556
|
+
? lines.map((line) => String(line || "")).filter(Boolean)
|
|
8557
|
+
: [];
|
|
8558
|
+
if (!sourceLines.length) return [];
|
|
8559
|
+
|
|
8560
|
+
const terms = buildLogQueryTerms(query);
|
|
8561
|
+
if (!terms.length) return sourceLines.slice(-limit);
|
|
8562
|
+
|
|
8563
|
+
const picked = new Set();
|
|
8564
|
+
const addWithContext = (index, radius = 1) => {
|
|
8565
|
+
for (let cursor = Math.max(0, index - radius); cursor <= Math.min(sourceLines.length - 1, index + radius); cursor += 1) {
|
|
8566
|
+
picked.add(cursor);
|
|
8567
|
+
}
|
|
8568
|
+
};
|
|
8569
|
+
|
|
8570
|
+
sourceLines.forEach((line, index) => {
|
|
8571
|
+
const lower = line.toLowerCase();
|
|
8572
|
+
const termHit = terms.some((term) => lower.includes(term));
|
|
8573
|
+
if (termHit) {
|
|
8574
|
+
addWithContext(index, isHighSignalLogLine(line) ? 2 : 1);
|
|
8575
|
+
return;
|
|
8576
|
+
}
|
|
8577
|
+
if (isHighSignalLogLine(line)) {
|
|
8578
|
+
addWithContext(index, 1);
|
|
8579
|
+
}
|
|
8580
|
+
});
|
|
8581
|
+
|
|
8582
|
+
if (!picked.size) {
|
|
8583
|
+
return sourceLines.slice(-limit);
|
|
8584
|
+
}
|
|
8585
|
+
|
|
8586
|
+
const filtered = [...picked]
|
|
8587
|
+
.sort((a, b) => a - b)
|
|
8588
|
+
.map((index) => sourceLines[index]);
|
|
8589
|
+
return filtered.slice(-limit);
|
|
8590
|
+
}
|
|
8591
|
+
|
|
8592
|
+
async function resolveSessionWorktreePath(session) {
|
|
8593
|
+
if (!session || typeof session !== "object") return null;
|
|
8594
|
+
const directCandidates = [
|
|
8595
|
+
session?.metadata?.worktreePath,
|
|
8596
|
+
session?.metadata?.workspaceDir,
|
|
8597
|
+
session?.metadata?.workspacePath,
|
|
8598
|
+
session?.metadata?.cwd,
|
|
8599
|
+
]
|
|
8600
|
+
.map((value) => String(value || "").trim())
|
|
8601
|
+
.filter(Boolean);
|
|
8602
|
+
for (const candidate of directCandidates) {
|
|
8603
|
+
if (existsSync(candidate)) return candidate;
|
|
8604
|
+
}
|
|
8605
|
+
|
|
8606
|
+
const branchHints = [
|
|
8607
|
+
session?.metadata?.branch,
|
|
8608
|
+
session?.metadata?.branchName,
|
|
8609
|
+
session?.branch,
|
|
8610
|
+
]
|
|
8611
|
+
.map((value) => String(value || "").trim().replace(/^refs\/heads\//, ""))
|
|
8612
|
+
.filter(Boolean);
|
|
8613
|
+
const taskHints = [session?.taskId, session?.id]
|
|
8614
|
+
.map((value) => String(value || "").trim().toLowerCase())
|
|
8615
|
+
.filter(Boolean);
|
|
8616
|
+
|
|
8617
|
+
try {
|
|
8618
|
+
const active = await listActiveWorktrees(repoRoot);
|
|
8619
|
+
const matched = (active || []).find((worktree) => {
|
|
8620
|
+
const worktreePath = String(worktree?.path || "").trim();
|
|
8621
|
+
const worktreeTaskKey = String(worktree?.taskKey || "").trim().toLowerCase();
|
|
8622
|
+
const worktreeBranch = String(worktree?.branch || "")
|
|
8623
|
+
.trim()
|
|
8624
|
+
.replace(/^refs\/heads\//, "");
|
|
8625
|
+
if (worktreePath && directCandidates.includes(worktreePath)) return true;
|
|
8626
|
+
if (worktreeTaskKey && taskHints.includes(worktreeTaskKey)) return true;
|
|
8627
|
+
return branchHints.some((hint) =>
|
|
8628
|
+
hint && (worktreeBranch === hint || worktreeBranch.endsWith(`/${hint}`)),
|
|
8629
|
+
);
|
|
8630
|
+
});
|
|
8631
|
+
return matched?.path || null;
|
|
8632
|
+
} catch {
|
|
8633
|
+
return null;
|
|
8634
|
+
}
|
|
8635
|
+
}
|
|
8636
|
+
|
|
8283
8637
|
async function ensurePresenceLoaded() {
|
|
8284
8638
|
const loaded = await loadWorkspaceRegistry().catch(() => null);
|
|
8285
8639
|
const registry = loaded?.registry || loaded || null;
|
|
@@ -8608,6 +8962,12 @@ async function handleApi(req, res, url) {
|
|
|
8608
8962
|
task.repository || task.meta?.repository || "",
|
|
8609
8963
|
).trim().toLowerCase();
|
|
8610
8964
|
if (workspaceFilter && taskWorkspace !== workspaceFilter) {
|
|
8965
|
+
// Backward compatibility: many legacy internal-store tasks predate
|
|
8966
|
+
// workspace stamping and should remain visible in the active
|
|
8967
|
+
// workspace board instead of being filtered out.
|
|
8968
|
+
if (!taskWorkspaceRaw) {
|
|
8969
|
+
return true;
|
|
8970
|
+
}
|
|
8611
8971
|
const taskWorkspacePath = normalizeCandidatePath(taskWorkspaceRaw);
|
|
8612
8972
|
const workspaceMatchByPath =
|
|
8613
8973
|
Boolean(taskWorkspacePath) &&
|
|
@@ -8647,9 +9007,10 @@ async function handleApi(req, res, url) {
|
|
|
8647
9007
|
const start = page * pageSize;
|
|
8648
9008
|
const slice = filtered.slice(start, start + pageSize);
|
|
8649
9009
|
const enriched = await applySharedStateToTasks(slice);
|
|
9010
|
+
const withRuntime = enriched.map((task) => withTaskRuntimeSnapshot(task));
|
|
8650
9011
|
jsonResponse(res, 200, {
|
|
8651
9012
|
ok: true,
|
|
8652
|
-
data:
|
|
9013
|
+
data: withRuntime,
|
|
8653
9014
|
page,
|
|
8654
9015
|
pageSize,
|
|
8655
9016
|
total,
|
|
@@ -8675,7 +9036,7 @@ async function handleApi(req, res, url) {
|
|
|
8675
9036
|
const adapter = getKanbanAdapter();
|
|
8676
9037
|
const task = await adapter.getTask(taskId);
|
|
8677
9038
|
const enriched = await applySharedStateToTasks(task ? [task] : []);
|
|
8678
|
-
|
|
9039
|
+
let detailTask = enriched[0] || null;
|
|
8679
9040
|
if (detailTask) {
|
|
8680
9041
|
const workflowRuns = await collectWorkflowRunsForTask(detailTask.id, url, 40);
|
|
8681
9042
|
const mergedWorkflowRuns = mergeTaskWorkflowRuns(detailTask.workflowRuns, workflowRuns, 80);
|
|
@@ -8696,6 +9057,7 @@ async function handleApi(req, res, url) {
|
|
|
8696
9057
|
};
|
|
8697
9058
|
if (sprintDag) detailTask.sprintDag = sprintDag.data;
|
|
8698
9059
|
if (globalDag) detailTask.dagOfDags = globalDag.data;
|
|
9060
|
+
detailTask = withTaskRuntimeSnapshot(detailTask);
|
|
8699
9061
|
}
|
|
8700
9062
|
jsonResponse(res, 200, { ok: true, data: detailTask });
|
|
8701
9063
|
} catch (err) {
|
|
@@ -9053,6 +9415,55 @@ async function handleApi(req, res, url) {
|
|
|
9053
9415
|
}
|
|
9054
9416
|
return;
|
|
9055
9417
|
}
|
|
9418
|
+
if (path === "/api/tasks/epic-dependencies" && req.method === "GET") {
|
|
9419
|
+
try {
|
|
9420
|
+
const listed = await listEpicDependenciesForApi();
|
|
9421
|
+
if (!listed.ok) {
|
|
9422
|
+
jsonResponse(res, 501, { ok: false, error: "Epic dependency APIs are unavailable." });
|
|
9423
|
+
return;
|
|
9424
|
+
}
|
|
9425
|
+
jsonResponse(res, 200, { ok: true, source: listed.source, data: listed.data });
|
|
9426
|
+
} catch (err) {
|
|
9427
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
9428
|
+
}
|
|
9429
|
+
return;
|
|
9430
|
+
}
|
|
9431
|
+
|
|
9432
|
+
if (path === "/api/tasks/epic-dependencies" && req.method === "PUT") {
|
|
9433
|
+
try {
|
|
9434
|
+
const body = await readJsonBody(req);
|
|
9435
|
+
const epicId = String(body?.epicId || body?.id || "").trim();
|
|
9436
|
+
const dependencies = Array.isArray(body?.dependencies)
|
|
9437
|
+
? body.dependencies
|
|
9438
|
+
: Array.isArray(body?.dependsOn)
|
|
9439
|
+
? body.dependsOn
|
|
9440
|
+
: [];
|
|
9441
|
+
if (!epicId) {
|
|
9442
|
+
jsonResponse(res, 400, { ok: false, error: "epicId required" });
|
|
9443
|
+
return;
|
|
9444
|
+
}
|
|
9445
|
+
const updated = await setEpicDependenciesForApi({ epicId, dependencies });
|
|
9446
|
+
if (!updated.ok) {
|
|
9447
|
+
jsonResponse(res, updated.status || 400, { ok: false, error: updated.error || "Failed to update epic dependencies" });
|
|
9448
|
+
return;
|
|
9449
|
+
}
|
|
9450
|
+
const globalDag = await getGlobalDagData();
|
|
9451
|
+
jsonResponse(res, 200, {
|
|
9452
|
+
ok: true,
|
|
9453
|
+
source: updated.source,
|
|
9454
|
+
data: updated.data,
|
|
9455
|
+
dag: globalDag?.data || null,
|
|
9456
|
+
});
|
|
9457
|
+
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9458
|
+
reason: "epic-dependencies-updated",
|
|
9459
|
+
epicId,
|
|
9460
|
+
});
|
|
9461
|
+
} catch (err) {
|
|
9462
|
+
jsonResponse(res, 500, { ok: false, error: err.message });
|
|
9463
|
+
}
|
|
9464
|
+
return;
|
|
9465
|
+
}
|
|
9466
|
+
|
|
9056
9467
|
if (path === "/api/tasks/start") {
|
|
9057
9468
|
try {
|
|
9058
9469
|
const body = await readJsonBody(req);
|
|
@@ -9102,25 +9513,12 @@ async function handleApi(req, res, url) {
|
|
|
9102
9513
|
const freeSlots =
|
|
9103
9514
|
(status.maxParallel || 0) - (status.activeSlots || 0);
|
|
9104
9515
|
|
|
9105
|
-
try {
|
|
9106
|
-
if (typeof adapter.updateTaskStatus === "function") {
|
|
9107
|
-
await adapter.updateTaskStatus(taskId, "inprogress", { source: "api.tasks.start" });
|
|
9108
|
-
} else if (typeof adapter.updateTask === "function") {
|
|
9109
|
-
await adapter.updateTask(taskId, { status: "inprogress" });
|
|
9110
|
-
}
|
|
9111
|
-
applyInternalLifecycleTransition(taskId, "start", {
|
|
9112
|
-
source: "api.tasks.start",
|
|
9113
|
-
actor: "ui",
|
|
9114
|
-
force: forceStart || manualOverride,
|
|
9115
|
-
reason: "manual start",
|
|
9116
|
-
});
|
|
9117
|
-
} catch (err) {
|
|
9118
|
-
console.warn(
|
|
9119
|
-
`[telegram-ui] failed to mark task ${taskId} inprogress: ${err.message}`,
|
|
9120
|
-
);
|
|
9121
|
-
}
|
|
9122
|
-
|
|
9123
9516
|
if (freeSlots <= 0) {
|
|
9517
|
+
const queuedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
9518
|
+
queued: true,
|
|
9519
|
+
queueState: "queued",
|
|
9520
|
+
requestedAt: new Date().toISOString(),
|
|
9521
|
+
});
|
|
9124
9522
|
jsonResponse(res, 202, {
|
|
9125
9523
|
ok: true,
|
|
9126
9524
|
taskId,
|
|
@@ -9128,6 +9526,7 @@ async function handleApi(req, res, url) {
|
|
|
9128
9526
|
started: false,
|
|
9129
9527
|
reason: "No free slots",
|
|
9130
9528
|
canStart,
|
|
9529
|
+
data: withTaskRuntimeSnapshot(queuedTask || task),
|
|
9131
9530
|
});
|
|
9132
9531
|
broadcastUiEvent(
|
|
9133
9532
|
["tasks", "overview", "executor", "agents"],
|
|
@@ -9139,8 +9538,27 @@ async function handleApi(req, res, url) {
|
|
|
9139
9538
|
);
|
|
9140
9539
|
return;
|
|
9141
9540
|
}
|
|
9541
|
+
let startedTask = task;
|
|
9542
|
+
try {
|
|
9543
|
+
startedTask = await persistTaskStatusForExecution(adapter, taskId, "inprogress", "api.tasks.start") || task;
|
|
9544
|
+
startedTask = await persistTaskExecutionMeta(adapter, taskId, {
|
|
9545
|
+
queued: false,
|
|
9546
|
+
queueState: null,
|
|
9547
|
+
}) || startedTask;
|
|
9548
|
+
applyInternalLifecycleTransition(taskId, "start", {
|
|
9549
|
+
source: "api.tasks.start",
|
|
9550
|
+
actor: "ui",
|
|
9551
|
+
force: forceStart || manualOverride,
|
|
9552
|
+
reason: "manual start",
|
|
9553
|
+
});
|
|
9554
|
+
} catch (err) {
|
|
9555
|
+
console.warn(
|
|
9556
|
+
`[telegram-ui] failed to mark task ${taskId} inprogress: ${err.message}`,
|
|
9557
|
+
);
|
|
9558
|
+
}
|
|
9559
|
+
|
|
9142
9560
|
const wasPaused = executor.isPaused?.();
|
|
9143
|
-
executor.executeTask(
|
|
9561
|
+
executor.executeTask(startedTask, {
|
|
9144
9562
|
...(sdk ? { sdk } : {}),
|
|
9145
9563
|
...(model ? { model } : {}),
|
|
9146
9564
|
force: forceStart || manualOverride,
|
|
@@ -9148,6 +9566,16 @@ async function handleApi(req, res, url) {
|
|
|
9148
9566
|
console.warn(
|
|
9149
9567
|
`[telegram-ui] failed to execute task ${taskId}: ${error.message}`,
|
|
9150
9568
|
);
|
|
9569
|
+
void persistTaskStatusForExecution(
|
|
9570
|
+
adapter,
|
|
9571
|
+
taskId,
|
|
9572
|
+
resolveFallbackStatusAfterFailedDispatch(task?.status, { started: false }),
|
|
9573
|
+
"api.tasks.start.failed",
|
|
9574
|
+
);
|
|
9575
|
+
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
9576
|
+
reason: "task-start-failed",
|
|
9577
|
+
taskId,
|
|
9578
|
+
});
|
|
9151
9579
|
});
|
|
9152
9580
|
jsonResponse(res, 200, {
|
|
9153
9581
|
ok: true,
|
|
@@ -9156,6 +9584,7 @@ async function handleApi(req, res, url) {
|
|
|
9156
9584
|
started: true,
|
|
9157
9585
|
wasPaused,
|
|
9158
9586
|
canStart,
|
|
9587
|
+
data: withTaskRuntimeSnapshot(startedTask),
|
|
9159
9588
|
});
|
|
9160
9589
|
broadcastUiEvent(
|
|
9161
9590
|
["tasks", "overview", "executor", "agents"],
|
|
@@ -9253,6 +9682,17 @@ async function handleApi(req, res, url) {
|
|
|
9253
9682
|
forceStart,
|
|
9254
9683
|
manualOverride,
|
|
9255
9684
|
});
|
|
9685
|
+
const reconciled = await reconcileTaskAfterDispatchAttempt({
|
|
9686
|
+
adapter,
|
|
9687
|
+
taskId,
|
|
9688
|
+
previousStatus: previousTask?.status || null,
|
|
9689
|
+
requestedStatus: nextStatus,
|
|
9690
|
+
lifecycleAction,
|
|
9691
|
+
startDispatch,
|
|
9692
|
+
source: "api.tasks.update",
|
|
9693
|
+
});
|
|
9694
|
+
const responseTask = withTaskRuntimeSnapshot(reconciled || updated);
|
|
9695
|
+
const responseStatus = responseTask?.status || nextStatus;
|
|
9256
9696
|
if (body?.pauseExecution === true && lifecycleAction === "pause" && executor && typeof executor.abortTask === "function") {
|
|
9257
9697
|
executor.abortTask(taskId, "task_lifecycle_pause");
|
|
9258
9698
|
}
|
|
@@ -9269,19 +9709,19 @@ async function handleApi(req, res, url) {
|
|
|
9269
9709
|
});
|
|
9270
9710
|
jsonResponse(res, 200, {
|
|
9271
9711
|
ok: true,
|
|
9272
|
-
data:
|
|
9712
|
+
data: responseTask,
|
|
9273
9713
|
restart,
|
|
9274
9714
|
lifecycle: {
|
|
9275
9715
|
action: lifecycleAction,
|
|
9276
9716
|
previousStatus: previousTask?.status || null,
|
|
9277
|
-
nextStatus,
|
|
9717
|
+
nextStatus: responseStatus,
|
|
9278
9718
|
startDispatch,
|
|
9279
9719
|
},
|
|
9280
9720
|
});
|
|
9281
9721
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9282
9722
|
reason: "task-updated",
|
|
9283
9723
|
taskId,
|
|
9284
|
-
status:
|
|
9724
|
+
status: responseStatus,
|
|
9285
9725
|
});
|
|
9286
9726
|
if (restart?.started || startDispatch?.started) {
|
|
9287
9727
|
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
@@ -9377,6 +9817,17 @@ async function handleApi(req, res, url) {
|
|
|
9377
9817
|
forceStart,
|
|
9378
9818
|
manualOverride,
|
|
9379
9819
|
});
|
|
9820
|
+
const reconciled = await reconcileTaskAfterDispatchAttempt({
|
|
9821
|
+
adapter,
|
|
9822
|
+
taskId,
|
|
9823
|
+
previousStatus: previousTask?.status || null,
|
|
9824
|
+
requestedStatus: nextStatus,
|
|
9825
|
+
lifecycleAction,
|
|
9826
|
+
startDispatch,
|
|
9827
|
+
source: "api.tasks.edit",
|
|
9828
|
+
});
|
|
9829
|
+
const responseTask = withTaskRuntimeSnapshot(reconciled || updated);
|
|
9830
|
+
const responseStatus = responseTask?.status || nextStatus;
|
|
9380
9831
|
if (body?.pauseExecution === true && lifecycleAction === "pause" && executor && typeof executor.abortTask === "function") {
|
|
9381
9832
|
executor.abortTask(taskId, "task_lifecycle_pause");
|
|
9382
9833
|
}
|
|
@@ -9393,19 +9844,19 @@ async function handleApi(req, res, url) {
|
|
|
9393
9844
|
});
|
|
9394
9845
|
jsonResponse(res, 200, {
|
|
9395
9846
|
ok: true,
|
|
9396
|
-
data:
|
|
9847
|
+
data: responseTask,
|
|
9397
9848
|
restart,
|
|
9398
9849
|
lifecycle: {
|
|
9399
9850
|
action: lifecycleAction,
|
|
9400
9851
|
previousStatus: previousTask?.status || null,
|
|
9401
|
-
nextStatus,
|
|
9852
|
+
nextStatus: responseStatus,
|
|
9402
9853
|
startDispatch,
|
|
9403
9854
|
},
|
|
9404
9855
|
});
|
|
9405
9856
|
broadcastUiEvent(["tasks", "overview"], "invalidate", {
|
|
9406
9857
|
reason: "task-edited",
|
|
9407
9858
|
taskId,
|
|
9408
|
-
status:
|
|
9859
|
+
status: responseStatus,
|
|
9409
9860
|
});
|
|
9410
9861
|
if (restart?.started || startDispatch?.started) {
|
|
9411
9862
|
broadcastUiEvent(["tasks", "overview", "executor", "agents"], "invalidate", {
|
|
@@ -11034,8 +11485,20 @@ async function handleApi(req, res, url) {
|
|
|
11034
11485
|
jsonResponse(res, 200, { ok: true, data: null });
|
|
11035
11486
|
return;
|
|
11036
11487
|
}
|
|
11037
|
-
const tail = await tailFile(filePath, lines);
|
|
11038
|
-
|
|
11488
|
+
const tail = await tailFile(filePath, Math.max(lines * 4, 240));
|
|
11489
|
+
const filteredLines = filterRelevantLogLines(tail?.lines || [], query || fileName, lines);
|
|
11490
|
+
const contentLines = filteredLines.length ? filteredLines : (tail?.lines || []).slice(-lines);
|
|
11491
|
+
jsonResponse(res, 200, {
|
|
11492
|
+
ok: true,
|
|
11493
|
+
data: {
|
|
11494
|
+
file: fileName,
|
|
11495
|
+
content: contentLines.join("\n"),
|
|
11496
|
+
lines: contentLines,
|
|
11497
|
+
mode: filteredLines.length ? "focused" : "tail",
|
|
11498
|
+
totalLines: Array.isArray(tail?.lines) ? tail.lines.length : 0,
|
|
11499
|
+
truncated: tail?.truncated === true,
|
|
11500
|
+
},
|
|
11501
|
+
});
|
|
11039
11502
|
} catch (err) {
|
|
11040
11503
|
jsonResponse(res, 200, { ok: true, data: null });
|
|
11041
11504
|
}
|
|
@@ -13421,7 +13884,7 @@ async function handleApi(req, res, url) {
|
|
|
13421
13884
|
});
|
|
13422
13885
|
return;
|
|
13423
13886
|
}
|
|
13424
|
-
const worktreePath = session
|
|
13887
|
+
const worktreePath = await resolveSessionWorktreePath(session);
|
|
13425
13888
|
if (!worktreePath || !existsSync(worktreePath)) {
|
|
13426
13889
|
jsonResponse(res, 200, { ok: true, diff: { files: [], totalFiles: 0, totalAdditions: 0, totalDeletions: 0, formatted: "(no worktree)" }, summary: "(no worktree)", commits: [] });
|
|
13427
13890
|
return;
|