bosun 0.41.2 → 0.41.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/.env.example +1 -1
  2. package/agent/agent-prompt-catalog.mjs +971 -0
  3. package/agent/agent-prompts.mjs +2 -970
  4. package/agent/agent-supervisor.mjs +6 -3
  5. package/agent/autofix-git.mjs +33 -0
  6. package/agent/autofix-prompts.mjs +151 -0
  7. package/agent/autofix.mjs +11 -175
  8. package/agent/bosun-skills.mjs +3 -2
  9. package/bosun.config.example.json +17 -0
  10. package/bosun.schema.json +87 -188
  11. package/cli.mjs +34 -1
  12. package/config/config-doctor.mjs +5 -250
  13. package/config/config-file-names.mjs +5 -0
  14. package/config/config.mjs +89 -493
  15. package/config/executor-config.mjs +493 -0
  16. package/config/repo-root.mjs +1 -2
  17. package/config/workspace-health.mjs +242 -0
  18. package/git/git-safety.mjs +15 -0
  19. package/github/github-oauth-portal.mjs +46 -0
  20. package/infra/library-manager-utils.mjs +22 -0
  21. package/infra/library-manager-well-known-sources.mjs +578 -0
  22. package/infra/library-manager.mjs +512 -1030
  23. package/infra/monitor.mjs +28 -9
  24. package/infra/session-tracker.mjs +10 -7
  25. package/kanban/kanban-adapter.mjs +17 -1
  26. package/lib/codebase-audit-manifests.mjs +117 -0
  27. package/lib/codebase-audit.mjs +18 -115
  28. package/package.json +18 -3
  29. package/server/ui-server.mjs +1194 -79
  30. package/shell/codex-config-file.mjs +178 -0
  31. package/shell/codex-config.mjs +538 -575
  32. package/task/task-cli.mjs +54 -3
  33. package/task/task-executor.mjs +143 -13
  34. package/task/task-store.mjs +409 -1
  35. package/telegram/telegram-bot.mjs +127 -0
  36. package/tools/apply-pr-suggestions.mjs +401 -0
  37. package/tools/syntax-check.mjs +21 -9
  38. package/ui/app.js +3 -14
  39. package/ui/components/kanban-board.js +227 -4
  40. package/ui/components/session-list.js +85 -5
  41. package/ui/demo-defaults.js +334 -80
  42. package/ui/demo.html +155 -0
  43. package/ui/modules/session-api.js +96 -0
  44. package/ui/modules/settings-schema.js +1 -2
  45. package/ui/modules/state.js +21 -3
  46. package/ui/setup.html +4 -5
  47. package/ui/styles/components.css +58 -4
  48. package/ui/tabs/agents.js +12 -15
  49. package/ui/tabs/control.js +1 -0
  50. package/ui/tabs/library.js +484 -22
  51. package/ui/tabs/manual-flows.js +105 -29
  52. package/ui/tabs/tasks.js +785 -140
  53. package/ui/tabs/telemetry.js +129 -11
  54. package/ui/tabs/workflow-canvas-utils.mjs +130 -0
  55. package/ui/tabs/workflows.js +293 -23
  56. package/voice/voice-tool-definitions.mjs +757 -0
  57. package/voice/voice-tools.mjs +34 -778
  58. package/workflow/manual-flow-audit.mjs +165 -0
  59. package/workflow/manual-flows.mjs +164 -259
  60. package/workflow/workflow-engine.mjs +147 -58
  61. package/workflow/workflow-nodes/definitions.mjs +1207 -0
  62. package/workflow/workflow-nodes/transforms.mjs +612 -0
  63. package/workflow/workflow-nodes.mjs +304 -52
  64. package/workflow/workflow-templates.mjs +313 -191
  65. package/workflow-templates/_helpers.mjs +154 -0
  66. package/workflow-templates/agents.mjs +61 -4
  67. package/workflow-templates/code-quality.mjs +7 -7
  68. package/workflow-templates/github.mjs +20 -10
  69. package/workflow-templates/task-batch.mjs +20 -9
  70. package/workflow-templates/task-lifecycle.mjs +31 -6
  71. package/workspace/worktree-manager.mjs +277 -3
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: 10000, min: 1000, max: 60000, unit: "ms", description: "Delay before restarting after a crash." },
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
-
@@ -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) {
@@ -94,12 +105,18 @@ function synthesizeTaskDescription(task) {
94
105
  function normalizeTaskForUi(task) {
95
106
  if (!task || typeof task !== "object") return task;
96
107
  const title = sanitizeTaskText(task.title || "");
97
- const description = sanitizeTaskText(task.description || "");
108
+ const rawDescription = sanitizeTaskText(task.description || "");
109
+ const description = isPlaceholderTaskDescription(rawDescription) ? "" : rawDescription;
98
110
  const meta = task.meta && typeof task.meta === "object"
99
111
  ? {
100
112
  ...task.meta,
101
113
  title: task.meta.title != null ? sanitizeTaskText(task.meta.title) : task.meta.title,
102
- description: task.meta.description != null ? sanitizeTaskText(task.meta.description) : task.meta.description,
114
+ description:
115
+ task.meta.description != null
116
+ ? (isPlaceholderTaskDescription(task.meta.description)
117
+ ? ""
118
+ : sanitizeTaskText(task.meta.description))
119
+ : task.meta.description,
103
120
  }
104
121
  : task.meta;
105
122
  return {
@@ -153,7 +170,7 @@ export const tasksSearch = signal("");
153
170
  export const tasksSort = signal("updated");
154
171
  export const tasksTotalPages = signal(1);
155
172
  export const tasksTotal = signal(0);
156
- export const tasksStatusCounts = signal({ draft: 0, backlog: 0, inProgress: 0, inReview: 0, done: 0 });
173
+ export const tasksStatusCounts = signal({ draft: 0, backlog: 0, blocked: 0, inProgress: 0, inReview: 0, done: 0 });
157
174
 
158
175
  // ── Retry Queue
159
176
  export const retryQueueData = signal({ count: 0, items: [] });
@@ -556,6 +573,7 @@ export async function loadTasks(options = {}) {
556
573
  tasksStatusCounts.value = {
557
574
  draft: Number(res?.statusCounts?.draft || 0),
558
575
  backlog: Number(res?.statusCounts?.backlog || 0),
576
+ blocked: Number(res?.statusCounts?.blocked || 0),
559
577
  inProgress: Number(res?.statusCounts?.inProgress || 0),
560
578
  inReview: Number(res?.statusCounts?.inReview || 0),
561
579
  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(10000);
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) || 10000); envLoaded = true; }
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) || 10000)} />
4849
- <div class="hint">Delay before restarting after a crash. Default: 10 000 ms.</div>
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
-
@@ -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: String(task?.id || task?.taskId || "").trim(),
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 streamSessionId =
2113
- selectedEntry?.session?.id
2114
- || selectedEntry?.slot?.sessionId
2115
- || selectedEntry?.slot?.taskId
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
-
@@ -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 = [