bosun 0.41.2 → 0.41.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/.env.example +1 -1
- package/agent/agent-pool.mjs +9 -2
- package/agent/agent-prompt-catalog.mjs +971 -0
- package/agent/agent-prompts.mjs +2 -970
- package/agent/agent-supervisor.mjs +119 -6
- 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 +35 -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/setup-web-server.mjs +58 -5
- package/server/ui-server.mjs +1394 -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 +28 -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 +338 -84
- 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 +43 -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 +848 -141
- 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 +358 -63
- 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 +44 -11
- package/workflow-templates/task-lifecycle.mjs +31 -6
- package/workspace/worktree-manager.mjs +277 -3
package/ui/demo.html
CHANGED
|
@@ -3311,6 +3311,67 @@
|
|
|
3311
3311
|
const t = findTask(params.get('taskId'));
|
|
3312
3312
|
return { data: t || null };
|
|
3313
3313
|
}
|
|
3314
|
+
if (route === '/api/tasks/export') {
|
|
3315
|
+
return {
|
|
3316
|
+
ok: true,
|
|
3317
|
+
data: {
|
|
3318
|
+
schemaVersion: 1,
|
|
3319
|
+
kind: 'bosun-task-state-export',
|
|
3320
|
+
exportedAt: new Date().toISOString(),
|
|
3321
|
+
backend: 'demo',
|
|
3322
|
+
tasks: Array.isArray(STATE.tasks) ? [...STATE.tasks] : [],
|
|
3323
|
+
},
|
|
3324
|
+
};
|
|
3325
|
+
}
|
|
3326
|
+
if (route === '/api/tasks/import' && method === 'POST') {
|
|
3327
|
+
const importedTasks = Array.isArray(body)
|
|
3328
|
+
? body
|
|
3329
|
+
: Array.isArray(body?.tasks)
|
|
3330
|
+
? body.tasks
|
|
3331
|
+
: Array.isArray(body?.backlog)
|
|
3332
|
+
? body.backlog
|
|
3333
|
+
: Array.isArray(body?.data?.tasks)
|
|
3334
|
+
? body.data.tasks
|
|
3335
|
+
: [];
|
|
3336
|
+
let created = 0;
|
|
3337
|
+
let updated = 0;
|
|
3338
|
+
const results = [];
|
|
3339
|
+
for (const candidate of importedTasks) {
|
|
3340
|
+
if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate)) continue;
|
|
3341
|
+
const id = String(candidate.id || '').trim();
|
|
3342
|
+
const title = String(candidate.title || '').trim();
|
|
3343
|
+
if (!id || !title) {
|
|
3344
|
+
results.push({ id: id || null, status: 'failed', error: 'task.id and task.title are required' });
|
|
3345
|
+
continue;
|
|
3346
|
+
}
|
|
3347
|
+
const existing = findTask(id);
|
|
3348
|
+
if (existing) {
|
|
3349
|
+
Object.assign(existing, candidate, { updated: Date.now() });
|
|
3350
|
+
updated += 1;
|
|
3351
|
+
results.push({ id, status: 'updated' });
|
|
3352
|
+
continue;
|
|
3353
|
+
}
|
|
3354
|
+
const createdTask = { ...candidate, created: candidate.created || Date.now(), updated: Date.now() };
|
|
3355
|
+
STATE.tasks.unshift(createdTask);
|
|
3356
|
+
created += 1;
|
|
3357
|
+
results.push({ id, status: 'created' });
|
|
3358
|
+
}
|
|
3359
|
+
addLog('info', 'tasks', `Imported task state snapshot: ${created} created, ${updated} updated`);
|
|
3360
|
+
return {
|
|
3361
|
+
ok: true,
|
|
3362
|
+
data: {
|
|
3363
|
+
backend: 'demo',
|
|
3364
|
+
mode: String(body?.mode || 'merge'),
|
|
3365
|
+
summary: {
|
|
3366
|
+
total: importedTasks.length,
|
|
3367
|
+
created,
|
|
3368
|
+
updated,
|
|
3369
|
+
failed: results.filter((entry) => entry.status === 'failed').length,
|
|
3370
|
+
},
|
|
3371
|
+
results,
|
|
3372
|
+
},
|
|
3373
|
+
};
|
|
3374
|
+
}
|
|
3314
3375
|
if (route === '/api/tasks/sprints') {
|
|
3315
3376
|
if (method === 'GET') {
|
|
3316
3377
|
const sprints = Array.isArray(STATE.taskSprints) ? STATE.taskSprints : [];
|
|
@@ -3389,6 +3450,41 @@
|
|
|
3389
3450
|
},
|
|
3390
3451
|
};
|
|
3391
3452
|
}
|
|
3453
|
+
if (route === '/api/tasks/dag/organize' && method === 'POST') {
|
|
3454
|
+
const sprintId = body?.sprintId || body?.sprint || null;
|
|
3455
|
+
const tasks = Array.isArray(STATE.tasks) ? STATE.tasks : [];
|
|
3456
|
+
const scoped = sprintId
|
|
3457
|
+
? tasks.filter((task) => String(task.sprintId || '') === String(sprintId))
|
|
3458
|
+
: tasks;
|
|
3459
|
+
const edges = [];
|
|
3460
|
+
for (const task of scoped) {
|
|
3461
|
+
const deps = Array.isArray(task.dependencyTaskIds) ? task.dependencyTaskIds : [];
|
|
3462
|
+
for (const dep of deps) {
|
|
3463
|
+
if (scoped.some((candidate) => candidate.id === dep)) {
|
|
3464
|
+
edges.push({ from: dep, to: task.id });
|
|
3465
|
+
}
|
|
3466
|
+
}
|
|
3467
|
+
}
|
|
3468
|
+
return {
|
|
3469
|
+
ok: true,
|
|
3470
|
+
sprintId,
|
|
3471
|
+
source: 'demo',
|
|
3472
|
+
suggestions: [],
|
|
3473
|
+
data: {
|
|
3474
|
+
sprintId,
|
|
3475
|
+
updatedSprintCount: sprintId ? 1 : 0,
|
|
3476
|
+
nodes: scoped.map((task) => ({
|
|
3477
|
+
id: task.id,
|
|
3478
|
+
title: task.title,
|
|
3479
|
+
status: task.status,
|
|
3480
|
+
sprintId: task.sprintId || null,
|
|
3481
|
+
sprintOrder: task.sprintOrder ?? null,
|
|
3482
|
+
})),
|
|
3483
|
+
edges,
|
|
3484
|
+
suggestions: [],
|
|
3485
|
+
},
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3392
3488
|
if (route === '/api/tasks/dag-of-dags') {
|
|
3393
3489
|
const sprints = Array.isArray(STATE.taskSprints) ? STATE.taskSprints : [];
|
|
3394
3490
|
const tasks = Array.isArray(STATE.tasks) ? STATE.tasks : [];
|
|
@@ -3600,6 +3696,17 @@
|
|
|
3600
3696
|
if (t) { t.status = 'todo'; t.updated = Date.now(); addLog('info', 'task-executor', `Task retried: ${t.title}`); }
|
|
3601
3697
|
return { ok: true, data: t || null };
|
|
3602
3698
|
}
|
|
3699
|
+
if (route === '/api/tasks/unblock') {
|
|
3700
|
+
const t = findTask(body?.taskId || body?.id);
|
|
3701
|
+
if (t) {
|
|
3702
|
+
t.status = 'todo';
|
|
3703
|
+
t.blockedReason = null;
|
|
3704
|
+
if (t.meta && typeof t.meta === 'object') t.meta.blockedReason = null;
|
|
3705
|
+
t.updated = Date.now();
|
|
3706
|
+
addLog('info', 'task-executor', `Task unblocked: ${t.title}`);
|
|
3707
|
+
}
|
|
3708
|
+
return { ok: true, data: t || null };
|
|
3709
|
+
}
|
|
3603
3710
|
if (route === '/api/tasks/ignore') {
|
|
3604
3711
|
const t = findTask(body?.taskId || body?.id);
|
|
3605
3712
|
if (t) { t.status = 'ignored'; t.updated = Date.now(); addLog('info', 'kanban', `Task ignored: ${t.title}`); }
|
|
@@ -4231,6 +4338,54 @@
|
|
|
4231
4338
|
.filter(Boolean);
|
|
4232
4339
|
return { ok: true, updates };
|
|
4233
4340
|
}
|
|
4341
|
+
if (route === '/api/workflows/reflow-template-layouts') {
|
|
4342
|
+
const requestedIds = Array.isArray(body?.workflowIds)
|
|
4343
|
+
? body.workflowIds.map((value) => String(value || '').trim()).filter(Boolean)
|
|
4344
|
+
: (body?.workflowId ? [String(body.workflowId).trim()].filter(Boolean) : []);
|
|
4345
|
+
const targetIds = new Set(requestedIds);
|
|
4346
|
+
const updatedWorkflows = [];
|
|
4347
|
+
|
|
4348
|
+
STATE.workflows = STATE.workflows.map((wf, index) => {
|
|
4349
|
+
const workflowId = String(wf?.id || '').trim();
|
|
4350
|
+
const isTemplateBacked = Boolean(wf?.metadata?.installedFrom);
|
|
4351
|
+
const isTargeted = targetIds.size === 0 || targetIds.has(workflowId);
|
|
4352
|
+
if (!workflowId || !isTemplateBacked || !isTargeted) return wf;
|
|
4353
|
+
|
|
4354
|
+
const next = JSON.parse(JSON.stringify(wf));
|
|
4355
|
+
next.nodes = (next.nodes || []).map((node, nodeIndex) => ({
|
|
4356
|
+
...node,
|
|
4357
|
+
position: {
|
|
4358
|
+
x: 160 + (nodeIndex % 3) * 280,
|
|
4359
|
+
y: 96 + Math.floor(nodeIndex / 3) * 180,
|
|
4360
|
+
},
|
|
4361
|
+
}));
|
|
4362
|
+
next.metadata = {
|
|
4363
|
+
...(next.metadata || {}),
|
|
4364
|
+
updatedAt: new Date().toISOString(),
|
|
4365
|
+
templateState: {
|
|
4366
|
+
...(next.metadata?.templateState || {}),
|
|
4367
|
+
isCustomized: false,
|
|
4368
|
+
updateAvailable: false,
|
|
4369
|
+
},
|
|
4370
|
+
};
|
|
4371
|
+
next.nodeCount = (next.nodes || []).length;
|
|
4372
|
+
updatedWorkflows.push(next);
|
|
4373
|
+
return next;
|
|
4374
|
+
});
|
|
4375
|
+
|
|
4376
|
+
return {
|
|
4377
|
+
ok: true,
|
|
4378
|
+
result: {
|
|
4379
|
+
scanned: targetIds.size > 0 ? targetIds.size : STATE.workflows.length,
|
|
4380
|
+
updated: updatedWorkflows.length,
|
|
4381
|
+
skipped: Math.max(0, (targetIds.size > 0 ? targetIds.size : STATE.workflows.length) - updatedWorkflows.length),
|
|
4382
|
+
updatedWorkflowIds: updatedWorkflows.map((wf) => wf.id),
|
|
4383
|
+
skippedWorkflowIds: [],
|
|
4384
|
+
errors: [],
|
|
4385
|
+
},
|
|
4386
|
+
workflows: updatedWorkflows,
|
|
4387
|
+
};
|
|
4388
|
+
}
|
|
4234
4389
|
if (route.startsWith('/api/workflows/runs/')) {
|
|
4235
4390
|
const runId = decodeURIComponent(route.replace('/api/workflows/runs/', '')).trim();
|
|
4236
4391
|
const run = STATE.workflowRuns.find((item) => item.runId === runId);
|
|
@@ -3,6 +3,102 @@
|
|
|
3
3
|
* Keeps workspace scoping explicit for /api/sessions/:id routes.
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
|
+
export const SESSION_RETRY_DEFAULTS = Object.freeze({
|
|
7
|
+
maxAttempts: 5,
|
|
8
|
+
baseDelayMs: 1500,
|
|
9
|
+
maxDelayMs: 20000,
|
|
10
|
+
backoffMultiplier: 2,
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
function normalizeRetryNumber(value, fallback, min = 0) {
|
|
14
|
+
const n = Number(value);
|
|
15
|
+
if (!Number.isFinite(n)) return fallback;
|
|
16
|
+
return Math.max(min, Math.floor(n));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function resolveRetryConfig(meta = {}) {
|
|
20
|
+
const maxAttempts = normalizeRetryNumber(
|
|
21
|
+
meta.maxAttempts,
|
|
22
|
+
SESSION_RETRY_DEFAULTS.maxAttempts,
|
|
23
|
+
1,
|
|
24
|
+
);
|
|
25
|
+
const baseDelayMs = normalizeRetryNumber(
|
|
26
|
+
meta.baseDelayMs,
|
|
27
|
+
SESSION_RETRY_DEFAULTS.baseDelayMs,
|
|
28
|
+
1,
|
|
29
|
+
);
|
|
30
|
+
const maxDelayMs = Math.max(
|
|
31
|
+
baseDelayMs,
|
|
32
|
+
normalizeRetryNumber(meta.maxDelayMs, SESSION_RETRY_DEFAULTS.maxDelayMs, 1),
|
|
33
|
+
);
|
|
34
|
+
const backoffMultiplier = Math.max(
|
|
35
|
+
1,
|
|
36
|
+
Number(meta.backoffMultiplier || SESSION_RETRY_DEFAULTS.backoffMultiplier),
|
|
37
|
+
);
|
|
38
|
+
return { maxAttempts, baseDelayMs, maxDelayMs, backoffMultiplier };
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export function createSessionLoadMeta(overrides = {}) {
|
|
42
|
+
const config = resolveRetryConfig(overrides);
|
|
43
|
+
return {
|
|
44
|
+
...config,
|
|
45
|
+
stale: Boolean(overrides?.stale),
|
|
46
|
+
lastSuccessAt: overrides?.lastSuccessAt ? String(overrides.lastSuccessAt) : null,
|
|
47
|
+
retryAttempt: normalizeRetryNumber(overrides?.retryAttempt, 0, 0),
|
|
48
|
+
retryDelayMs: normalizeRetryNumber(overrides?.retryDelayMs, 0, 0),
|
|
49
|
+
nextRetryAt: overrides?.nextRetryAt ? String(overrides.nextRetryAt) : null,
|
|
50
|
+
retriesExhausted: Boolean(overrides?.retriesExhausted),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function getSessionRetryDelayMs(attemptNumber, meta = {}) {
|
|
55
|
+
const { baseDelayMs, maxDelayMs, backoffMultiplier } = resolveRetryConfig(meta);
|
|
56
|
+
const attempt = Math.max(1, normalizeRetryNumber(attemptNumber, 1, 1));
|
|
57
|
+
const delay = Math.round(baseDelayMs * Math.pow(backoffMultiplier, attempt - 1));
|
|
58
|
+
return Math.min(maxDelayMs, Math.max(baseDelayMs, delay));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function markSessionLoadSuccess(previousMeta, now = Date.now()) {
|
|
62
|
+
const meta = createSessionLoadMeta(previousMeta || {});
|
|
63
|
+
return {
|
|
64
|
+
...meta,
|
|
65
|
+
stale: false,
|
|
66
|
+
lastSuccessAt: new Date(now).toISOString(),
|
|
67
|
+
retryAttempt: 0,
|
|
68
|
+
retryDelayMs: 0,
|
|
69
|
+
nextRetryAt: null,
|
|
70
|
+
retriesExhausted: false,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function markSessionLoadFailure(previousMeta, now = Date.now()) {
|
|
75
|
+
const meta = createSessionLoadMeta(previousMeta || {});
|
|
76
|
+
const retryAttempt = normalizeRetryNumber(meta.retryAttempt, 0, 0) + 1;
|
|
77
|
+
const retriesExhausted = retryAttempt > meta.maxAttempts;
|
|
78
|
+
const retryDelayMs = retriesExhausted
|
|
79
|
+
? 0
|
|
80
|
+
: getSessionRetryDelayMs(retryAttempt, meta);
|
|
81
|
+
return {
|
|
82
|
+
...meta,
|
|
83
|
+
stale: true,
|
|
84
|
+
retryAttempt,
|
|
85
|
+
retryDelayMs,
|
|
86
|
+
nextRetryAt: retriesExhausted ? null : new Date(now + retryDelayMs).toISOString(),
|
|
87
|
+
retriesExhausted,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export function resetSessionRetryMeta(previousMeta) {
|
|
92
|
+
const meta = createSessionLoadMeta(previousMeta || {});
|
|
93
|
+
return {
|
|
94
|
+
...meta,
|
|
95
|
+
retryAttempt: 0,
|
|
96
|
+
retryDelayMs: 0,
|
|
97
|
+
nextRetryAt: null,
|
|
98
|
+
retriesExhausted: false,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
6
102
|
function normalizeWorkspaceHint(value) {
|
|
7
103
|
const raw = String(value || "").trim();
|
|
8
104
|
if (!raw) return "";
|
|
@@ -248,7 +248,7 @@ export const SETTINGS_SCHEMA = [
|
|
|
248
248
|
{ key: "DEVMODE", label: "Dev Mode", category: "advanced", type: "boolean", defaultVal: false, description: "Enable development mode with extra logging, self-restart watcher, and debug endpoints." },
|
|
249
249
|
{ key: "SELF_RESTART_WATCH_ENABLED", label: "Self-Restart Watcher", category: "advanced", type: "boolean", description: "Auto-restart when source files change. Defaults to true in devmode." },
|
|
250
250
|
{ key: "MAX_PARALLEL", label: "Global Max Parallel", category: "advanced", type: "number", defaultVal: 6, min: 1, max: 50, description: "Global maximum parallel task slots across all executors." },
|
|
251
|
-
{ key: "RESTART_DELAY_MS", label: "Restart Delay", category: "advanced", type: "number", defaultVal:
|
|
251
|
+
{ key: "RESTART_DELAY_MS", label: "Restart Delay", category: "advanced", type: "number", defaultVal: 180000, min: 1000, max: 1800000, unit: "ms", description: "Delay before restarting after a crash." },
|
|
252
252
|
{ key: "ENV_RELOAD_DELAY_MS", label: "Settings Reload Delay", category: "advanced", type: "number", defaultVal: 5000, min: 500, max: 60000, unit: "ms", description: "Quiet period before Bosun reloads runtime config after .env or settings changes. Higher values make restart-sensitive saves less abrupt." },
|
|
253
253
|
{ key: "SHARED_STATE_ENABLED", label: "Shared State", category: "advanced", type: "boolean", defaultVal: true, description: "Enable distributed task coordination for multi-instance setups." },
|
|
254
254
|
{ key: "WORKFLOW_AUTOMATION_ENABLED", label: "Workflow Automation", category: "advanced", type: "boolean", defaultVal: true, description: "Enable event-driven workflow auto-triggers from monitor events." },
|
|
@@ -358,4 +358,3 @@ export function validateSetting(def, value) {
|
|
|
358
358
|
export const SENSITIVE_KEYS = new Set(
|
|
359
359
|
SETTINGS_SCHEMA.filter((s) => s.sensitive).map((s) => s.key),
|
|
360
360
|
);
|
|
361
|
-
|
package/ui/modules/state.js
CHANGED
|
@@ -83,6 +83,17 @@ export function sanitizeTaskText(value) {
|
|
|
83
83
|
return text.replace(/\s{2,}/g, " ").trim();
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
+
export function isPlaceholderTaskDescription(value) {
|
|
87
|
+
const text = sanitizeTaskText(value || "");
|
|
88
|
+
if (!text) return false;
|
|
89
|
+
const normalized = text.toLowerCase();
|
|
90
|
+
return (
|
|
91
|
+
normalized === "internal server error" ||
|
|
92
|
+
normalized === "{\"ok\":false,\"error\":\"internal server error\"}" ||
|
|
93
|
+
normalized === "{\"error\":\"internal server error\"}"
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
86
97
|
function synthesizeTaskDescription(task) {
|
|
87
98
|
const title = sanitizeTaskText(task?.title || "");
|
|
88
99
|
if (!title) {
|
|
@@ -91,21 +102,49 @@ function synthesizeTaskDescription(task) {
|
|
|
91
102
|
return `Implementation notes for "${title}". Include scope, key files, risks, and acceptance checks before dispatch.`;
|
|
92
103
|
}
|
|
93
104
|
|
|
105
|
+
function normalizeTaskDiagnosticsForUi(diagnostics) {
|
|
106
|
+
if (!diagnostics || typeof diagnostics !== "object") return diagnostics;
|
|
107
|
+
const stableCause = diagnostics.stableCause && typeof diagnostics.stableCause === "object"
|
|
108
|
+
? {
|
|
109
|
+
...diagnostics.stableCause,
|
|
110
|
+
code: sanitizeTaskText(diagnostics.stableCause.code || ""),
|
|
111
|
+
title: sanitizeTaskText(diagnostics.stableCause.title || ""),
|
|
112
|
+
summary: sanitizeTaskText(diagnostics.stableCause.summary || ""),
|
|
113
|
+
}
|
|
114
|
+
: diagnostics.stableCause;
|
|
115
|
+
return {
|
|
116
|
+
...diagnostics,
|
|
117
|
+
stableCause,
|
|
118
|
+
lastError: sanitizeTaskText(diagnostics.lastError || "") || null,
|
|
119
|
+
errorPattern: sanitizeTaskText(diagnostics.errorPattern || "") || null,
|
|
120
|
+
blockedReason: sanitizeTaskText(diagnostics.blockedReason || "") || null,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
|
|
94
124
|
function normalizeTaskForUi(task) {
|
|
95
125
|
if (!task || typeof task !== "object") return task;
|
|
96
126
|
const title = sanitizeTaskText(task.title || "");
|
|
97
|
-
const
|
|
127
|
+
const rawDescription = sanitizeTaskText(task.description || "");
|
|
128
|
+
const description = isPlaceholderTaskDescription(rawDescription) ? "" : rawDescription;
|
|
129
|
+
const diagnostics = normalizeTaskDiagnosticsForUi(task.diagnostics);
|
|
98
130
|
const meta = task.meta && typeof task.meta === "object"
|
|
99
131
|
? {
|
|
100
132
|
...task.meta,
|
|
101
133
|
title: task.meta.title != null ? sanitizeTaskText(task.meta.title) : task.meta.title,
|
|
102
|
-
description:
|
|
134
|
+
description:
|
|
135
|
+
task.meta.description != null
|
|
136
|
+
? (isPlaceholderTaskDescription(task.meta.description)
|
|
137
|
+
? ""
|
|
138
|
+
: sanitizeTaskText(task.meta.description))
|
|
139
|
+
: task.meta.description,
|
|
140
|
+
diagnostics: normalizeTaskDiagnosticsForUi(task.meta.diagnostics),
|
|
103
141
|
}
|
|
104
142
|
: task.meta;
|
|
105
143
|
return {
|
|
106
144
|
...task,
|
|
107
145
|
title,
|
|
108
146
|
description: description || synthesizeTaskDescription({ ...task, title }),
|
|
147
|
+
diagnostics,
|
|
109
148
|
meta,
|
|
110
149
|
};
|
|
111
150
|
}
|
|
@@ -153,7 +192,7 @@ export const tasksSearch = signal("");
|
|
|
153
192
|
export const tasksSort = signal("updated");
|
|
154
193
|
export const tasksTotalPages = signal(1);
|
|
155
194
|
export const tasksTotal = signal(0);
|
|
156
|
-
export const tasksStatusCounts = signal({ draft: 0, backlog: 0, inProgress: 0, inReview: 0, done: 0 });
|
|
195
|
+
export const tasksStatusCounts = signal({ draft: 0, backlog: 0, blocked: 0, inProgress: 0, inReview: 0, done: 0 });
|
|
157
196
|
|
|
158
197
|
// ── Retry Queue
|
|
159
198
|
export const retryQueueData = signal({ count: 0, items: [] });
|
|
@@ -556,6 +595,7 @@ export async function loadTasks(options = {}) {
|
|
|
556
595
|
tasksStatusCounts.value = {
|
|
557
596
|
draft: Number(res?.statusCounts?.draft || 0),
|
|
558
597
|
backlog: Number(res?.statusCounts?.backlog || 0),
|
|
598
|
+
blocked: Number(res?.statusCounts?.blocked || 0),
|
|
559
599
|
inProgress: Number(res?.statusCounts?.inProgress || 0),
|
|
560
600
|
inReview: Number(res?.statusCounts?.inReview || 0),
|
|
561
601
|
done: Number(res?.statusCounts?.done || 0),
|
package/ui/setup.html
CHANGED
|
@@ -1019,7 +1019,7 @@ function App() {
|
|
|
1019
1019
|
const [sentinelRepairAgentEnabled, setSentinelRepairAgentEnabled] = useState(false);
|
|
1020
1020
|
const [sentinelRepairTimeoutMin, setSentinelRepairTimeoutMin] = useState(15);
|
|
1021
1021
|
// Daemon restart policy
|
|
1022
|
-
const [daemonRestartDelayMs, setDaemonRestartDelayMs] = useState(
|
|
1022
|
+
const [daemonRestartDelayMs, setDaemonRestartDelayMs] = useState(180000);
|
|
1023
1023
|
const [daemonMaxRestarts, setDaemonMaxRestarts] = useState(0);
|
|
1024
1024
|
const [daemonMaxInstantRestarts, setDaemonMaxInstantRestarts] = useState(3);
|
|
1025
1025
|
const [daemonInstantCrashWindowMs, setDaemonInstantCrashWindowMs] = useState(15000);
|
|
@@ -2023,7 +2023,7 @@ function App() {
|
|
|
2023
2023
|
if (env.SENTINEL_REPAIR_AGENT_ENABLED) { setSentinelRepairAgentEnabled(env.SENTINEL_REPAIR_AGENT_ENABLED === "true"); envLoaded = true; }
|
|
2024
2024
|
if (env.SENTINEL_REPAIR_TIMEOUT_MIN) { setSentinelRepairTimeoutMin(Number(env.SENTINEL_REPAIR_TIMEOUT_MIN) || 15); envLoaded = true; }
|
|
2025
2025
|
// Daemon restart policy
|
|
2026
|
-
if (env.RESTART_DELAY_MS) { setDaemonRestartDelayMs(Number(env.RESTART_DELAY_MS) ||
|
|
2026
|
+
if (env.RESTART_DELAY_MS) { setDaemonRestartDelayMs(Number(env.RESTART_DELAY_MS) || 180000); envLoaded = true; }
|
|
2027
2027
|
if (env.MAX_RESTARTS) { setDaemonMaxRestarts(Number(env.MAX_RESTARTS) || 0); envLoaded = true; }
|
|
2028
2028
|
if (env.BOSUN_DAEMON_MAX_INSTANT_RESTARTS) { setDaemonMaxInstantRestarts(Number(env.BOSUN_DAEMON_MAX_INSTANT_RESTARTS) || 3); envLoaded = true; }
|
|
2029
2029
|
if (env.BOSUN_DAEMON_INSTANT_CRASH_WINDOW_MS) { setDaemonInstantCrashWindowMs(Number(env.BOSUN_DAEMON_INSTANT_CRASH_WINDOW_MS) || 15000); envLoaded = true; }
|
|
@@ -4845,8 +4845,8 @@ function App() {
|
|
|
4845
4845
|
<div class="form-group">
|
|
4846
4846
|
<label>Restart Delay (ms)</label>
|
|
4847
4847
|
<input type="number" value=${daemonRestartDelayMs} min="0"
|
|
4848
|
-
oninput=${(e) => setDaemonRestartDelayMs(Number(e.target.value) ||
|
|
4849
|
-
<div class="hint">Delay before restarting after a crash. Default:
|
|
4848
|
+
oninput=${(e) => setDaemonRestartDelayMs(Number(e.target.value) || 180000)} />
|
|
4849
|
+
<div class="hint">Delay before restarting after a crash. Default: 180 000 ms (180 s).</div>
|
|
4850
4850
|
</div>
|
|
4851
4851
|
<div class="form-group">
|
|
4852
4852
|
<label>Max Restarts (0 = unlimited)</label>
|
|
@@ -5094,4 +5094,3 @@ render(html`<${App} />`, document.getElementById("app"));
|
|
|
5094
5094
|
<script defer src="https://cloud.umami.is/script.js" data-website-id="24c5d605-7f25-4be5-875e-25c8f3cb4059"></script>
|
|
5095
5095
|
</html>
|
|
5096
5096
|
|
|
5097
|
-
|
package/ui/styles/components.css
CHANGED
|
@@ -6242,16 +6242,12 @@ select.input {
|
|
|
6242
6242
|
}
|
|
6243
6243
|
.task-detail-main {
|
|
6244
6244
|
padding: 20px 24px;
|
|
6245
|
-
overflow-y: auto;
|
|
6246
|
-
scrollbar-width: thin;
|
|
6247
6245
|
display: flex;
|
|
6248
6246
|
flex-direction: column;
|
|
6249
6247
|
gap: 20px;
|
|
6250
6248
|
}
|
|
6251
6249
|
.task-detail-sidebar {
|
|
6252
6250
|
padding: 16px 20px;
|
|
6253
|
-
overflow-y: auto;
|
|
6254
|
-
scrollbar-width: thin;
|
|
6255
6251
|
border-left: 1px solid var(--border, #30363d);
|
|
6256
6252
|
background: var(--bg-surface, #0d1117);
|
|
6257
6253
|
display: flex;
|
|
@@ -6306,6 +6302,64 @@ select.input {
|
|
|
6306
6302
|
padding: 14px;
|
|
6307
6303
|
}
|
|
6308
6304
|
|
|
6305
|
+
.task-blocked-banner {
|
|
6306
|
+
display: grid;
|
|
6307
|
+
gap: 8px;
|
|
6308
|
+
padding: 12px 14px;
|
|
6309
|
+
border: 1px solid rgba(248, 113, 113, 0.28);
|
|
6310
|
+
border-radius: 10px;
|
|
6311
|
+
background:
|
|
6312
|
+
linear-gradient(135deg, rgba(248, 113, 113, 0.14), rgba(245, 158, 11, 0.08)),
|
|
6313
|
+
var(--bg-card, #161b22);
|
|
6314
|
+
}
|
|
6315
|
+
|
|
6316
|
+
.task-blocked-banner[data-category="dependency_blocked"],
|
|
6317
|
+
.task-blocked-banner[data-category="start_guard_blocked"] {
|
|
6318
|
+
border-color: rgba(251, 191, 36, 0.28);
|
|
6319
|
+
background:
|
|
6320
|
+
linear-gradient(135deg, rgba(251, 191, 36, 0.14), rgba(245, 158, 11, 0.08)),
|
|
6321
|
+
var(--bg-card, #161b22);
|
|
6322
|
+
}
|
|
6323
|
+
|
|
6324
|
+
.task-blocked-banner-title {
|
|
6325
|
+
font-size: 14px;
|
|
6326
|
+
font-weight: 700;
|
|
6327
|
+
color: var(--text-primary, #f8fafc);
|
|
6328
|
+
}
|
|
6329
|
+
|
|
6330
|
+
.task-blocked-banner-copy {
|
|
6331
|
+
font-size: 13px;
|
|
6332
|
+
line-height: 1.55;
|
|
6333
|
+
color: var(--text-secondary, #c9d1d9);
|
|
6334
|
+
}
|
|
6335
|
+
|
|
6336
|
+
.task-workflow-run-card[data-clickable="true"] {
|
|
6337
|
+
cursor: pointer;
|
|
6338
|
+
transition: border-color 120ms ease, background 120ms ease, transform 120ms ease;
|
|
6339
|
+
}
|
|
6340
|
+
|
|
6341
|
+
.task-workflow-run-card[data-clickable="true"]:hover,
|
|
6342
|
+
.task-workflow-run-card[data-clickable="true"]:focus-visible {
|
|
6343
|
+
border-color: var(--accent, #3b82f6);
|
|
6344
|
+
background: rgba(59, 130, 246, 0.08);
|
|
6345
|
+
transform: translateY(-1px);
|
|
6346
|
+
outline: none;
|
|
6347
|
+
}
|
|
6348
|
+
|
|
6349
|
+
.task-workflow-run-head {
|
|
6350
|
+
display: flex;
|
|
6351
|
+
gap: 10px;
|
|
6352
|
+
justify-content: space-between;
|
|
6353
|
+
align-items: flex-start;
|
|
6354
|
+
}
|
|
6355
|
+
|
|
6356
|
+
.task-workflow-run-actions {
|
|
6357
|
+
display: flex;
|
|
6358
|
+
gap: 6px;
|
|
6359
|
+
flex-wrap: wrap;
|
|
6360
|
+
justify-content: flex-end;
|
|
6361
|
+
}
|
|
6362
|
+
|
|
6309
6363
|
/* Priority dot */
|
|
6310
6364
|
.task-priority-dot {
|
|
6311
6365
|
display: inline-block;
|
package/ui/tabs/agents.js
CHANGED
|
@@ -128,6 +128,13 @@ function fleetThreadKey(thread, index) {
|
|
|
128
128
|
return `thread-${index}:${taskKey}:${id}`;
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
function resolveFleetEntrySessionId(entry) {
|
|
132
|
+
const sessionId = String(entry?.session?.id || "").trim();
|
|
133
|
+
if (sessionId) return sessionId;
|
|
134
|
+
if (entry?.isTaskFallback || entry?.slot?.synthetic) return "";
|
|
135
|
+
return String(entry?.slot?.sessionId || "").trim();
|
|
136
|
+
}
|
|
137
|
+
|
|
131
138
|
function buildSessionLogQueryParts(values = []) {
|
|
132
139
|
return Array.from(
|
|
133
140
|
new Set(
|
|
@@ -2016,7 +2023,7 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace,
|
|
|
2016
2023
|
branch: task?.branchName || task?.branch || task?.meta?.branchName || "",
|
|
2017
2024
|
baseBranch: task?.baseBranch || task?.meta?.baseBranch || null,
|
|
2018
2025
|
status: task?.status || task?.runtimeSnapshot?.state || "idle",
|
|
2019
|
-
sessionId:
|
|
2026
|
+
sessionId: "",
|
|
2020
2027
|
startedAt:
|
|
2021
2028
|
task?.lastActivityAt ||
|
|
2022
2029
|
task?.updatedAt ||
|
|
@@ -2109,19 +2116,10 @@ function FleetSessionsPanel({ slots, taskFallbackEntries = [], onOpenWorkspace,
|
|
|
2109
2116
|
visibleEntries.find((entry) => entry.key === selectedEntryKey)
|
|
2110
2117
|
|| visibleEntries[0]
|
|
2111
2118
|
|| null;
|
|
2112
|
-
const
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|| null;
|
|
2117
|
-
const diffSessionId =
|
|
2118
|
-
selectedEntry?.session?.id
|
|
2119
|
-
|| selectedEntry?.slot?.sessionId
|
|
2120
|
-
|| null;
|
|
2121
|
-
const contextSessionId =
|
|
2122
|
-
selectedEntry?.session?.id
|
|
2123
|
-
|| selectedEntry?.slot?.sessionId
|
|
2124
|
-
|| "";
|
|
2119
|
+
const resolvedSessionId = resolveFleetEntrySessionId(selectedEntry);
|
|
2120
|
+
const streamSessionId = resolvedSessionId || null;
|
|
2121
|
+
const diffSessionId = resolvedSessionId || null;
|
|
2122
|
+
const contextSessionId = resolvedSessionId || "";
|
|
2125
2123
|
const contextTaskId =
|
|
2126
2124
|
selectedEntry?.slot?.taskId
|
|
2127
2125
|
|| selectedEntry?.session?.taskId
|
|
@@ -2489,4 +2487,3 @@ export function FleetSessionsTab() {
|
|
|
2489
2487
|
`}
|
|
2490
2488
|
`;
|
|
2491
2489
|
}
|
|
2492
|
-
|
package/ui/tabs/control.js
CHANGED
|
@@ -23,6 +23,7 @@ import { ICONS } from "../modules/icons.js";
|
|
|
23
23
|
import { iconText as iconTextUtil } from "../modules/icon-utils.js";
|
|
24
24
|
import { cloneValue, truncate } from "../modules/utils.js";
|
|
25
25
|
import { SegmentedControl, Collapsible } from "../components/forms.js";
|
|
26
|
+
import { SkeletonCard } from "../components/shared.js";
|
|
26
27
|
|
|
27
28
|
/* ─── Command registry for autocomplete ─── */
|
|
28
29
|
const CMD_REGISTRY = [
|