bosun 0.40.3 → 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/ui/tabs/tasks.js CHANGED
@@ -690,6 +690,177 @@ function buildEpicDagGraph(tasks = [], epicDependencies = []) {
690
690
  edges,
691
691
  };
692
692
  }
693
+ function slugifyPlanningId(value, fallback = "item") {
694
+ const normalized = String(value || "")
695
+ .trim()
696
+ .toLowerCase()
697
+ .replace(/[^a-z0-9]+/g, "-")
698
+ .replace(/^-+|-+$/g, "");
699
+ return normalized || fallback;
700
+ }
701
+
702
+ function normalizeTaskTypeValue(value, fallback = "task") {
703
+ const normalized = toText(value, fallback).toLowerCase();
704
+ return ["epic", "task", "subtask"].includes(normalized) ? normalized : fallback;
705
+ }
706
+
707
+ function normalizeTaskStatusValue(value) {
708
+ return toText(value).toLowerCase();
709
+ }
710
+
711
+ function isTerminalTaskStatus(value) {
712
+ return ["done", "completed", "closed", "merged", "cancelled"].includes(normalizeTaskStatusValue(value));
713
+ }
714
+
715
+ function isQueuedTask(task) {
716
+ const runtime = getTaskRuntimeSnapshot(task);
717
+ return runtime?.state === "queued" || normalizeTaskStatusValue(task?.status) === "queued";
718
+ }
719
+
720
+ function isBacklogDraftTask(task) {
721
+ const status = normalizeTaskStatusValue(task?.status);
722
+ return status === "draft" || status === "todo" || status === "backlog" || status === "planned" || status === "";
723
+ }
724
+
725
+ function isExecutionTask(task) {
726
+ return isActiveStatus(task?.status) || isReviewStatus(task?.status) || isQueuedTask(task);
727
+ }
728
+
729
+ function filterDagGraphByIds(graph = EMPTY_DAG_GRAPH, allowedIds = null, resolver = (node) => toText(node?.id || node?.taskId)) {
730
+ if (!allowedIds || allowedIds === "all") return graph;
731
+ const allowed = allowedIds instanceof Set ? allowedIds : new Set(Array.isArray(allowedIds) ? allowedIds : []);
732
+ const nodes = (graph?.nodes || []).filter((node) => allowed.has(resolver(node)) || allowed.has(toText(node?.id || node?.taskId)));
733
+ const nodeIds = new Set(nodes.map((node) => toText(node?.id || node?.taskId)).filter(Boolean));
734
+ const edges = (graph?.edges || []).filter((edge) => nodeIds.has(toText(edge?.source || edge?.from)) && nodeIds.has(toText(edge?.target || edge?.to)));
735
+ return { ...graph, nodes, edges };
736
+ }
737
+
738
+ function buildEpicCatalog(tasks = [], epicDependencies = []) {
739
+ const catalog = new Map();
740
+ const ensureEpic = (epicId) => {
741
+ const id = toText(epicId);
742
+ if (!id) return null;
743
+ if (!catalog.has(id)) {
744
+ catalog.set(id, {
745
+ id,
746
+ label: id,
747
+ taskIds: [],
748
+ dependencies: [],
749
+ completedCount: 0,
750
+ activeCount: 0,
751
+ });
752
+ }
753
+ return catalog.get(id);
754
+ };
755
+ for (const task of tasks || []) {
756
+ const epicId = toText(task?.epicId || task?.meta?.epicId);
757
+ if (!epicId) continue;
758
+ const entry = ensureEpic(epicId);
759
+ if (!entry) continue;
760
+ entry.taskIds.push(task.id);
761
+ if (normalizeTaskTypeValue(task?.type) === "epic") {
762
+ entry.label = toText(task?.title, epicId);
763
+ entry.anchorTaskId = task.id;
764
+ }
765
+ if (isTerminalTaskStatus(task?.status)) entry.completedCount += 1;
766
+ if (isExecutionTask(task)) entry.activeCount += 1;
767
+ }
768
+ for (const row of epicDependencies || []) {
769
+ const entry = ensureEpic(row?.epicId);
770
+ if (!entry) continue;
771
+ entry.dependencies = normalizeDependencyInput(row?.dependencies || []);
772
+ }
773
+ return [...catalog.values()]
774
+ .map((entry) => ({
775
+ ...entry,
776
+ taskIds: normalizeDependencyInput(entry.taskIds),
777
+ taskCount: normalizeDependencyInput(entry.taskIds).length,
778
+ }))
779
+ .sort((a, b) => String(a.label || a.id).localeCompare(String(b.label || b.id)));
780
+ }
781
+
782
+ function buildDagPlanningState({ tasks = [], sprintId = "all", sprintOrderMode = "parallel", sprintOptions = [], epicDependencies = [] }) {
783
+ const scopedTasks = (tasks || []).filter((task) => sprintId === "all" ? true : getTaskSprintId(task) === sprintId);
784
+ const allTaskMap = new Map((tasks || []).map((task) => [toText(task?.id), task]));
785
+ const scopedTaskIds = new Set(scopedTasks.map((task) => toText(task?.id)).filter(Boolean));
786
+ const sprintModeMap = new Map((sprintOptions || []).map((sprint) => [sprint.id, toText(sprint.executionMode || sprint.taskOrderMode || 'parallel', 'parallel')]));
787
+ const epicDependencyMap = new Map((epicDependencies || []).map((row) => [toText(row?.epicId), normalizeDependencyInput(row?.dependencies || [])]));
788
+ const tasksByEpic = new Map();
789
+ for (const task of tasks || []) {
790
+ const epicId = toText(task?.epicId || task?.meta?.epicId);
791
+ if (!epicId) continue;
792
+ const list = tasksByEpic.get(epicId) || [];
793
+ list.push(task);
794
+ tasksByEpic.set(epicId, list);
795
+ }
796
+ const isSequentialBlocked = (task) => {
797
+ const taskSprintId = getTaskSprintId(task);
798
+ const mode = sprintModeMap.get(taskSprintId) || sprintOrderMode;
799
+ if (mode !== 'sequential') return false;
800
+ const currentOrder = Number(getTaskSprintOrder(task));
801
+ if (!Number.isFinite(currentOrder) || currentOrder <= 1) return false;
802
+ return scopedTasks.some((candidate) => getTaskSprintId(candidate) === taskSprintId && Number(getTaskSprintOrder(candidate)) < currentOrder && !isTerminalTaskStatus(candidate?.status));
803
+ };
804
+ const isEpicBlocked = (task) => {
805
+ const epicId = toText(task?.epicId || task?.meta?.epicId);
806
+ if (!epicId) return false;
807
+ const requiredEpics = epicDependencyMap.get(epicId) || [];
808
+ if (!requiredEpics.length) return false;
809
+ return requiredEpics.some((requiredEpicId) => {
810
+ const epicTasks = tasksByEpic.get(requiredEpicId) || [];
811
+ if (!epicTasks.length) return true;
812
+ return epicTasks.some((candidate) => !isTerminalTaskStatus(candidate?.status));
813
+ });
814
+ };
815
+ const isDependencyBlocked = (task) => getTaskDependencyIds(task).some((depId) => {
816
+ const dependencyTask = allTaskMap.get(depId);
817
+ return !dependencyTask || !isTerminalTaskStatus(dependencyTask?.status);
818
+ });
819
+ const backlogTaskIds = new Set();
820
+ const executionTaskIds = new Set();
821
+ const readyTaskIds = new Set();
822
+ const sprintIdsByFocus = { all: new Set(), backlog: new Set(), execution: new Set(), ready: new Set() };
823
+ const epicIdsByFocus = { all: new Set(), backlog: new Set(), execution: new Set(), ready: new Set() };
824
+ for (const task of scopedTasks) {
825
+ const taskId = toText(task?.id);
826
+ const taskSprintId = getTaskSprintId(task);
827
+ const epicId = toText(task?.epicId || task?.meta?.epicId);
828
+ sprintIdsByFocus.all.add(taskSprintId || 'unassigned');
829
+ if (epicId) epicIdsByFocus.all.add(epicId);
830
+ if (isBacklogDraftTask(task)) {
831
+ backlogTaskIds.add(taskId);
832
+ sprintIdsByFocus.backlog.add(taskSprintId || 'unassigned');
833
+ if (epicId) epicIdsByFocus.backlog.add(epicId);
834
+ }
835
+ if (isExecutionTask(task)) {
836
+ executionTaskIds.add(taskId);
837
+ sprintIdsByFocus.execution.add(taskSprintId || 'unassigned');
838
+ if (epicId) epicIdsByFocus.execution.add(epicId);
839
+ }
840
+ const ready = !isBacklogDraftTask(task) && !isExecutionTask(task) && !isTerminalTaskStatus(task?.status) && !isDependencyBlocked(task) && !isEpicBlocked(task) && !isSequentialBlocked(task);
841
+ if (ready) {
842
+ readyTaskIds.add(taskId);
843
+ sprintIdsByFocus.ready.add(taskSprintId || 'unassigned');
844
+ if (epicId) epicIdsByFocus.ready.add(epicId);
845
+ }
846
+ }
847
+ return {
848
+ scopedTaskIds,
849
+ backlogTaskIds,
850
+ executionTaskIds,
851
+ readyTaskIds,
852
+ sprintIdsByFocus,
853
+ epicIdsByFocus,
854
+ epicCatalog: buildEpicCatalog(tasks, epicDependencies),
855
+ counts: {
856
+ all: scopedTaskIds.size,
857
+ backlog: backlogTaskIds.size,
858
+ execution: executionTaskIds.size,
859
+ ready: readyTaskIds.size,
860
+ },
861
+ };
862
+ }
863
+
693
864
  function extractGlobalDagPayload(...sources) {
694
865
  for (const source of sources) {
695
866
  const payload = extractDagPayload(source);
@@ -930,7 +1101,9 @@ async function applyTaskLifecycleTransition(task, requestedStatus) {
930
1101
 
931
1102
  const wantsDraft = decision.nextStatus === "draft";
932
1103
  const prevTasks = cloneValue(tasksData.value);
933
- const optimisticStatus = decision.action === "start" ? "inprogress" : decision.nextStatus;
1104
+ const optimisticStatus = decision.action === "start"
1105
+ ? String(task?.status || "todo")
1106
+ : decision.nextStatus;
934
1107
  let apiResult = null;
935
1108
 
936
1109
  await runOptimistic(
@@ -1563,6 +1736,18 @@ function isReviewStatus(s) {
1563
1736
  return ["inreview", "review", "pr-open", "pr-review"].includes(String(s || ""));
1564
1737
  }
1565
1738
 
1739
+ function getTaskRuntimeSnapshot(task) {
1740
+ return task?.runtimeSnapshot || task?.meta?.runtimeSnapshot || null;
1741
+ }
1742
+
1743
+ function hasLiveExecutionEvidence(task) {
1744
+ const runtime = getTaskRuntimeSnapshot(task);
1745
+ if (runtime?.isLive === true) return true;
1746
+ if (runtime?.state === "running") return true;
1747
+ if (runtime?.slot?.taskId) return true;
1748
+ return false;
1749
+ }
1750
+
1566
1751
  async function reactivateTaskSession(taskId, options = {}) {
1567
1752
  const normalizedTaskId = String(taskId || "").trim();
1568
1753
  if (!normalizedTaskId) return false;
@@ -1590,41 +1775,20 @@ async function reactivateTaskSession(taskId, options = {}) {
1590
1775
  showToast("Agent session reactivated", "success");
1591
1776
  }
1592
1777
 
1778
+ const nextStatus = String(res?.data?.status || (res?.queued ? "queued" : "inprogress")).trim() || "todo";
1593
1779
  tasksData.value = (tasksData.value || []).map((t) =>
1594
1780
  String(t?.id || "").trim() === normalizedTaskId
1595
- ? { ...t, status: "inprogress" }
1781
+ ? {
1782
+ ...t,
1783
+ ...(res?.data || {}),
1784
+ status: nextStatus,
1785
+ }
1596
1786
  : t,
1597
1787
  );
1598
1788
  scheduleRefresh(150);
1599
1789
  return true;
1600
1790
  }
1601
1791
 
1602
- /* ─── Derive agent steps from task title/description ─── */
1603
- function deriveSteps(task) {
1604
- const t = String(task?.title || "").toLowerCase();
1605
- const steps = [];
1606
- if (/feat|add|implement|build|create/.test(t)) steps.push({ label: "Understand requirements & read context" });
1607
- if (/fix|bug|patch|resolve|repair/.test(t)) steps.push({ label: "Reproduce and diagnose issue" });
1608
- if (/refactor|restructure|cleanup|reorganize/.test(t)) steps.push({ label: "Map current code structure" });
1609
- if (/test|spec|coverage/.test(t)) steps.push({ label: "Write test cases" });
1610
- if (/docs|document|readme/.test(t)) steps.push({ label: "Draft documentation content" });
1611
- steps.push({ label: "Write implementation" });
1612
- if (!/docs|readme/.test(t)) steps.push({ label: "Run tests & fix issues" });
1613
- steps.push({ label: "Commit changes" });
1614
- steps.push({ label: "Handoff PR lifecycle to Bosun" });
1615
- return steps;
1616
- }
1617
-
1618
- /* ─── Estimate step progress from timing ─── */
1619
- function estimateStep(task, steps) {
1620
- const elapsed = task?.updated ? (Date.now() - new Date(task.updated).getTime()) : 0;
1621
- const totalDur = task?.created ? (Date.now() - new Date(task.created).getTime()) : 60000;
1622
- const pct = Math.min(0.85, totalDur > 0 ? (elapsed / totalDur) : 0);
1623
- // Bias: show 40-75% through to look realistic
1624
- const biasedPct = 0.35 + pct * 0.4;
1625
- return Math.max(0, Math.min(steps.length - 1, Math.floor(biasedPct * steps.length)));
1626
- }
1627
-
1628
1792
  /* ─── TaskProgressModal — live view for in-progress tasks ─── */
1629
1793
  export function TaskProgressModal({ task, onClose }) {
1630
1794
  const [liveTask, setLiveTask] = useState(task);
@@ -1672,8 +1836,7 @@ export function TaskProgressModal({ task, onClose }) {
1672
1836
  logEndRef.current?.scrollIntoView({ behavior: "smooth" });
1673
1837
  }, [logs]);
1674
1838
 
1675
- const steps = useMemo(() => deriveSteps(liveTask || task), [liveTask?.id]);
1676
- const currentStep = useMemo(() => estimateStep(liveTask || task, steps), [liveTask?.updated]);
1839
+ const runtime = getTaskRuntimeSnapshot(liveTask || task);
1677
1840
 
1678
1841
  const healthScore = health?.currentHealthScore ?? health?.averageHealthScore ?? null;
1679
1842
  const healthColor =
@@ -1685,6 +1848,10 @@ export function TaskProgressModal({ task, onClose }) {
1685
1848
  const startedRelative = liveTask?.created ? formatRelative(liveTask.created) : "—";
1686
1849
  const agentLabel = liveTask?.assignee || task.assignee || "Agent";
1687
1850
  const branchLabel = liveTask?.branch || task.branch || "—";
1851
+ const runtimeState = runtime?.state || "pending";
1852
+ const runtimeLabel = runtime?.statusLabel || "Live execution";
1853
+ const activeSlots = Number(runtime?.executor?.activeSlots || 0);
1854
+ const maxParallel = Number(runtime?.executor?.maxParallel || 0);
1688
1855
 
1689
1856
  const handleCancel = async () => {
1690
1857
  const ok = await showConfirm("Cancel this task?");
@@ -1736,9 +1903,9 @@ export function TaskProgressModal({ task, onClose }) {
1736
1903
  <div class="tp-hero">
1737
1904
  <div class="tp-pulse-dot"></div>
1738
1905
  <div class="tp-hero-title">
1739
- <div class="tp-hero-status-label">${iconText(":zap: Active — Agent Working")}</div>
1906
+ <div class="tp-hero-status-label">${iconText(`:zap: ${runtimeLabel}`)}</div>
1740
1907
  </div>
1741
- <${Badge} status="inprogress" text="running" />
1908
+ <${Badge} status="inprogress" text=${runtimeState} />
1742
1909
  </div>
1743
1910
 
1744
1911
 
@@ -3361,6 +3528,7 @@ function DagGraphSection({
3361
3528
  allowWiring = false,
3362
3529
  graphKey = "dag",
3363
3530
  emptyMessage = "No DAG nodes available for this view yet.",
3531
+ highlightNodeIds = null,
3364
3532
  }) {
3365
3533
  const stageRef = useRef(null);
3366
3534
  const [zoom, setZoom] = useState(1);
@@ -3505,6 +3673,13 @@ function DagGraphSection({
3505
3673
  return map;
3506
3674
  }, [sortedNodes]);
3507
3675
 
3676
+ const highlightedIds = useMemo(() => {
3677
+ if (!highlightNodeIds || highlightNodeIds === "all") return new Set();
3678
+ return highlightNodeIds instanceof Set
3679
+ ? highlightNodeIds
3680
+ : new Set(Array.isArray(highlightNodeIds) ? highlightNodeIds : []);
3681
+ }, [highlightNodeIds]);
3682
+
3508
3683
  const handleNodeClick = useCallback(async (node, event) => {
3509
3684
  event?.stopPropagation?.();
3510
3685
  if (allowWiring && typeof onCreateEdge === "function") {
@@ -3549,7 +3724,7 @@ function DagGraphSection({
3549
3724
  <div class="task-dag-header-row">
3550
3725
  <div>
3551
3726
  <div style=${{ fontWeight: "700" }}>${title || "Task DAG"}</div>
3552
- ${description && html`<div class="meta-text">${description}</div>`}
3727
+ ${description ? html`<div class="meta-text">${description}</div>` : null}
3553
3728
  <div class="meta-text">Drag to pan · wheel to zoom · click node to ${allowWiring ? "wire edges" : "open task"}.</div>
3554
3729
  </div>
3555
3730
  <div class="task-dag-controls">
@@ -3558,7 +3733,9 @@ function DagGraphSection({
3558
3733
  <${Button} size="small" variant="outlined" onClick=${fitToView}>Fit</${Button}>
3559
3734
  <${Button} size="small" variant="text" onClick=${() => { setZoom(1); setPan({ x: 24, y: 24 }); }}>Reset</${Button}>
3560
3735
  <span class="task-dag-zoom-pill">${Math.round(zoom * 100)}%</span>
3561
- ${allowWiring && html`<span class="task-dag-wire-pill">${wireSourceId ? `Source: ${wireSourceId}` : wiringBusy ? "Saving edge…" : "Wiring: click source then target"}</span>`}
3736
+ ${allowWiring
3737
+ ? html`<span class="task-dag-wire-pill">${wireSourceId ? `Source: ${wireSourceId}` : wiringBusy ? "Saving edge…" : "Wiring: click source then target"}</span>`
3738
+ : null}
3562
3739
  </div>
3563
3740
  </div>
3564
3741
  <div class="task-dag-legend">
@@ -3600,10 +3777,11 @@ function DagGraphSection({
3600
3777
  const pos = layout.positions.get(String(node.id));
3601
3778
  if (!pos) return null;
3602
3779
  const selected = wireSourceId && String(node.id) === wireSourceId;
3780
+ const highlighted = highlightedIds.has(String(node.id)) || highlightedIds.has(String(node.taskId || ""));
3603
3781
  return html`
3604
3782
  <g
3605
3783
  key=${node.id}
3606
- class=${`dag-node ${selected ? "dag-node-selected" : ""}`}
3784
+ class=${`dag-node ${selected ? "dag-node-selected" : ""} ${highlighted ? "dag-node-highlighted" : ""}`}
3607
3785
  onPointerDown=${(event) => event.stopPropagation()}
3608
3786
  onClick=${(event) => handleNodeClick(node, event)}
3609
3787
  style=${{ cursor: allowWiring || node.taskId ? "pointer" : "default" }}
@@ -3616,8 +3794,8 @@ function DagGraphSection({
3616
3794
  rx="14"
3617
3795
  ry="14"
3618
3796
  fill="var(--bg-surface)"
3619
- stroke=${selected ? "var(--accent)" : "var(--border)"}
3620
- stroke-width=${selected ? "2.2" : "1.5"}
3797
+ stroke=${selected ? "var(--accent)" : highlighted ? "var(--color-done)" : "var(--border)"}
3798
+ stroke-width=${selected || highlighted ? "2.2" : "1.5"}
3621
3799
  />
3622
3800
  <text x=${pos.x + 12} y=${pos.y + 24} fill="var(--text-primary)" font-size="13" font-weight="700">
3623
3801
  ${truncate(node.title || "(untitled)", 34)}
@@ -3629,6 +3807,7 @@ function DagGraphSection({
3629
3807
  ${String(node.status || "todo")}
3630
3808
  </text>
3631
3809
  ${Number.isFinite(node.order) && html`<text x=${pos.x + pos.width - 16} y=${pos.y + 22} text-anchor="end" fill="var(--text-muted)" font-size="11">#${node.order}</text>`}
3810
+ ${highlighted && html`<text x=${pos.x + pos.width - 12} y=${pos.y + pos.height - 12} text-anchor="end" fill="var(--color-done)" font-size="11" font-weight="700">Ready</text>`}
3632
3811
  </g>
3633
3812
  `;
3634
3813
  })}
@@ -3663,6 +3842,11 @@ export function TasksTab() {
3663
3842
  const [dagEpicGraph, setDagEpicGraph] = useState(EMPTY_DAG_GRAPH);
3664
3843
  const [dagSources, setDagSources] = useState({ sprints: "", sprintGraph: "", globalGraph: "", epicDeps: "", tasks: "" });
3665
3844
  const [dagSprintOrderMode, setDagSprintOrderMode] = useState("parallel");
3845
+ const [dagAllTasks, setDagAllTasks] = useState([]);
3846
+ const [dagEpicDependencies, setDagEpicDependencies] = useState([]);
3847
+ const [dagFocusMode, setDagFocusMode] = useState("all");
3848
+ const [showCreateSprint, setShowCreateSprint] = useState(false);
3849
+ const [createSeed, setCreateSeed] = useState(null);
3666
3850
  const [isCompact, setIsCompact] = useState(() => {
3667
3851
  try { return globalThis.matchMedia?.("(max-width: 768px)")?.matches ?? false; }
3668
3852
  catch { return false; }
@@ -3730,6 +3914,21 @@ export function TasksTab() {
3730
3914
  const hasMoreKanbanPages = isKanban && page + 1 < totalPages;
3731
3915
  const boardColumnTotals = tasksStatusCounts?.value || { draft: 0, backlog: 0, inProgress: 0, inReview: 0, done: 0 };
3732
3916
  const boardTotalTasks = Number(tasksTotal?.value || 0);
3917
+ const dagTaskCatalog = dagAllTasks.length ? dagAllTasks : tasks;
3918
+ const dagPlanningState = useMemo(() => buildDagPlanningState({
3919
+ tasks: dagTaskCatalog,
3920
+ sprintId: dagSelectedSprint,
3921
+ sprintOrderMode: dagSprintOrderMode,
3922
+ sprintOptions: dagSprints,
3923
+ epicDependencies: dagEpicDependencies,
3924
+ }), [dagAllTasks, dagEpicDependencies, dagSelectedSprint, dagSprintOrderMode, dagSprints, tasks]);
3925
+ const dagEpicCatalog = dagPlanningState.epicCatalog;
3926
+ const dagFocusOptions = [
3927
+ { id: "all", label: "All structure", count: dagPlanningState.counts.all },
3928
+ { id: "backlog", label: "Backlog & draft", count: dagPlanningState.counts.backlog },
3929
+ { id: "execution", label: "Running & review", count: dagPlanningState.counts.execution },
3930
+ { id: "ready", label: "Ready next", count: dagPlanningState.counts.ready },
3931
+ ];
3733
3932
 
3734
3933
  const loadMoreKanbanTasks = useCallback(async () => {
3735
3934
  if (!isKanban || kanbanLoadingMore || isSearching) return;
@@ -3786,6 +3985,8 @@ export function TasksTab() {
3786
3985
  : tasks;
3787
3986
  const epicDeps = normalizeEpicDependenciesPayload(epicDepsMeta?.payload);
3788
3987
  const nextEpicGraph = buildEpicDagGraph(allTasks, epicDeps);
3988
+ setDagAllTasks(allTasks);
3989
+ setDagEpicDependencies(epicDeps);
3789
3990
 
3790
3991
  const sprintMetaEntry = sprintOptions.find((entry) => entry.id === resolvedSprint) || null;
3791
3992
  setDagSprintOrderMode(toText(sprintMetaEntry?.executionMode || sprintMetaEntry?.taskOrderMode || sprintMetaEntry?.sprintOrderMode || "parallel", "parallel"));
@@ -4028,6 +4229,32 @@ export function TasksTab() {
4028
4229
  });
4029
4230
  }, [visible, listSortCol, listSortDir]);
4030
4231
 
4232
+ const dagTaskFocusIds = useMemo(() => {
4233
+ if (dagFocusMode === "backlog") return dagPlanningState.backlogTaskIds;
4234
+ if (dagFocusMode === "ready") return dagPlanningState.readyTaskIds;
4235
+ if (dagFocusMode === "execution") return new Set([...dagPlanningState.executionTaskIds, ...dagPlanningState.readyTaskIds]);
4236
+ return "all";
4237
+ }, [dagFocusMode, dagPlanningState]);
4238
+
4239
+ const dagSprintFocusIds = useMemo(() => {
4240
+ if (dagFocusMode === "backlog") return dagPlanningState.sprintIdsByFocus.backlog;
4241
+ if (dagFocusMode === "ready") return dagPlanningState.sprintIdsByFocus.ready;
4242
+ if (dagFocusMode === "execution") return new Set([...dagPlanningState.sprintIdsByFocus.execution, ...dagPlanningState.sprintIdsByFocus.ready]);
4243
+ return "all";
4244
+ }, [dagFocusMode, dagPlanningState]);
4245
+
4246
+ const dagEpicFocusIds = useMemo(() => {
4247
+ if (dagFocusMode === "backlog") return dagPlanningState.epicIdsByFocus.backlog;
4248
+ if (dagFocusMode === "ready") return dagPlanningState.epicIdsByFocus.ready;
4249
+ if (dagFocusMode === "execution") return new Set([...dagPlanningState.epicIdsByFocus.execution, ...dagPlanningState.epicIdsByFocus.ready]);
4250
+ return "all";
4251
+ }, [dagFocusMode, dagPlanningState]);
4252
+
4253
+ const dagSprintGraphView = useMemo(() => filterDagGraphByIds(dagSprintGraph, dagTaskFocusIds, (node) => toText(node?.taskId || node?.id)), [dagSprintGraph, dagTaskFocusIds]);
4254
+ const dagGlobalGraphView = useMemo(() => filterDagGraphByIds(dagGlobalGraph, dagSprintFocusIds, (node) => toText(node?.sprintId || node?.id)), [dagGlobalGraph, dagSprintFocusIds]);
4255
+ const dagEpicGraphView = useMemo(() => filterDagGraphByIds(dagEpicGraph, dagEpicFocusIds, (node) => toText(node?.epicId || node?.id)), [dagEpicGraph, dagEpicFocusIds]);
4256
+ const dagReadyHighlightIds = dagFocusMode === "execution" || dagFocusMode === "ready" ? dagPlanningState.readyTaskIds : null;
4257
+
4031
4258
  /* ── Handlers ── */
4032
4259
  const handleFilter = async (s) => {
4033
4260
  haptic();
@@ -4114,29 +4341,31 @@ export function TasksTab() {
4114
4341
  }
4115
4342
  }, [loadDagViews]);
4116
4343
 
4117
- const handleCreateSprint = useCallback(async () => {
4118
- const rawName = globalThis.prompt?.("Sprint name", "Sprint " + new Date().toISOString().slice(0, 10));
4119
- const name = toText(rawName);
4120
- if (!name) return;
4121
- const rawId = globalThis.prompt?.("Sprint ID (optional)", name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, ""));
4122
- const id = toText(rawId);
4344
+ const handleCreateSprint = useCallback(() => {
4345
+ haptic("medium");
4346
+ setShowCreateSprint(true);
4347
+ }, []);
4348
+
4349
+ const handleMoveSprint = useCallback(async (sprintId, direction) => {
4350
+ const currentIndex = dagSprints.findIndex((entry) => entry.id === sprintId);
4351
+ const targetIndex = currentIndex + direction;
4352
+ if (currentIndex < 0 || targetIndex < 0 || targetIndex >= dagSprints.length) return;
4353
+ const reordered = [...dagSprints];
4354
+ const [moved] = reordered.splice(currentIndex, 1);
4355
+ reordered.splice(targetIndex, 0, moved);
4123
4356
  haptic("medium");
4124
4357
  try {
4125
- await apiFetch("/api/tasks/sprints", {
4126
- method: "POST",
4127
- body: JSON.stringify({
4128
- ...(id ? { id } : {}),
4129
- name,
4130
- status: "active",
4131
- }),
4132
- });
4133
- showToast("Sprint created", "success");
4358
+ await Promise.all(reordered.map((entry, index) => apiFetch(`/api/tasks/sprints/${encodeURIComponent(entry.id)}`, {
4359
+ method: "PATCH",
4360
+ body: JSON.stringify({ order: index + 1 }),
4361
+ })));
4362
+ showToast("Sprint order updated", "success");
4134
4363
  await loadDagViews();
4135
- if (id) setDagSelectedSprint(id);
4136
4364
  } catch {
4137
- /* toast via apiFetch */
4365
+ setDagError("Failed to reorder sprints.");
4138
4366
  }
4139
- }, [loadDagViews]);
4367
+ }, [dagSprints, loadDagViews]);
4368
+
4140
4369
  const handleDagSprintModeChange = useCallback(async (mode) => {
4141
4370
  const nextMode = toText(mode, "parallel").toLowerCase();
4142
4371
  if (!dagSelectedSprint || dagSelectedSprint === "all") {
@@ -4856,8 +5085,8 @@ export function TasksTab() {
4856
5085
 
4857
5086
  ${isDag && html`
4858
5087
  <div class="task-dag-wrap" style=${{ display: "grid", gap: "10px", marginTop: "8px" }}>
4859
- ${dagError && html`<${Alert} severity="warning">${dagError}</${Alert}>`}
4860
- ${dagLoading && html`<${Alert} severity="info">Loading DAG data…</${Alert}>`}
5088
+ ${dagError ? html`<${Alert} severity="warning">${dagError}</${Alert}>` : null}
5089
+ ${dagLoading ? html`<${Alert} severity="info">Loading DAG data…</${Alert}>` : null}
4861
5090
  <${DagGraphSection}
4862
5091
  title=${dagSprintGraph.title || (dagSelectedSprint === "all" ? "All Sprint DAG" : `Sprint ${dagSelectedSprint} DAG`)}
4863
5092
  description=${dagSprintGraph.description || "Task dependency order within the selected sprint."}
@@ -4867,7 +5096,7 @@ export function TasksTab() {
4867
5096
  onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "task" })}
4868
5097
  allowWiring=${true}
4869
5098
  emptyMessage="No sprint DAG data available yet."
4870
- />
5099
+ ><//>
4871
5100
  <${DagGraphSection}
4872
5101
  title=${dagGlobalGraph.title || "Global DAG of DAGs"}
4873
5102
  description=${dagGlobalGraph.description || "Cross-sprint dependency overview."}
@@ -4877,7 +5106,7 @@ export function TasksTab() {
4877
5106
  onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "task" })}
4878
5107
  allowWiring=${true}
4879
5108
  emptyMessage="No global DAG data available yet."
4880
- />
5109
+ ><//>
4881
5110
  <${DagGraphSection}
4882
5111
  title=${dagEpicGraph.title || "Epic Dependency DAG"}
4883
5112
  description=${dagEpicGraph.description || "Epics and their run prerequisites."}
@@ -4886,7 +5115,7 @@ export function TasksTab() {
4886
5115
  onCreateEdge={({ sourceNode, targetNode }) => handleCreateDagEdge({ sourceNode, targetNode, graphKind: "epic" })}
4887
5116
  allowWiring=${true}
4888
5117
  emptyMessage="No epic DAG data available yet."
4889
- />
5118
+ ><//>
4890
5119
  </div>
4891
5120
  `}
4892
5121
 
@@ -5013,22 +5242,14 @@ export function TasksTab() {
5013
5242
  html`
5014
5243
  <${CreateTaskModalInline} onClose=${() => setShowCreate(false)} />
5015
5244
  `}
5016
- ${detailTask && isActiveStatus(detailTask.status) &&
5245
+ ${detailTask && isActiveStatus(detailTask.status) && hasLiveExecutionEvidence(detailTask) &&
5017
5246
  html`
5018
5247
  <${TaskProgressModal}
5019
5248
  task=${detailTask}
5020
5249
  onClose=${() => setDetailTask(null)}
5021
5250
  />
5022
5251
  `}
5023
- ${detailTask && isReviewStatus(detailTask.status) &&
5024
- html`
5025
- <${TaskReviewModal}
5026
- task=${detailTask}
5027
- onClose=${() => setDetailTask(null)}
5028
- onStart=${(task) => openStartModal(task)}
5029
- />
5030
- `}
5031
- ${detailTask && !isActiveStatus(detailTask.status) && !isReviewStatus(detailTask.status) &&
5252
+ ${detailTask && (!isActiveStatus(detailTask.status) || !hasLiveExecutionEvidence(detailTask)) &&
5032
5253
  html`
5033
5254
  <${TaskDetailModal}
5034
5255
  task=${detailTask}
@@ -2466,18 +2466,6 @@ export class WorkflowEngine extends EventEmitter {
2466
2466
 
2467
2467
  if (!runs.length) {
2468
2468
  this._resumingRuns = false;
2469
- this._taskTraceHooks = new Set();
2470
- if (typeof opts.onTaskWorkflowEvent === "function") {
2471
- this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
2472
- }
2473
- if (typeof opts.taskTraceHook === "function") {
2474
- this._taskTraceHooks.add(opts.taskTraceHook);
2475
- }
2476
- if (Array.isArray(opts.taskTraceHooks)) {
2477
- for (const hook of opts.taskTraceHooks) {
2478
- if (typeof hook === "function") this._taskTraceHooks.add(hook);
2479
- }
2480
- }
2481
2469
  return;
2482
2470
  }
2483
2471
 
@@ -2536,18 +2524,6 @@ export class WorkflowEngine extends EventEmitter {
2536
2524
  }
2537
2525
  } finally {
2538
2526
  this._resumingRuns = false;
2539
- this._taskTraceHooks = new Set();
2540
- if (typeof opts.onTaskWorkflowEvent === "function") {
2541
- this._taskTraceHooks.add(opts.onTaskWorkflowEvent);
2542
- }
2543
- if (typeof opts.taskTraceHook === "function") {
2544
- this._taskTraceHooks.add(opts.taskTraceHook);
2545
- }
2546
- if (Array.isArray(opts.taskTraceHooks)) {
2547
- for (const hook of opts.taskTraceHooks) {
2548
- if (typeof hook === "function") this._taskTraceHooks.add(hook);
2549
- }
2550
- }
2551
2527
  }
2552
2528
  }
2553
2529