generated from coulomb/repo-seed
feat(tasks): adopt canonical task statuses
This commit is contained in:
@@ -141,11 +141,14 @@ const _STATUS_STYLE = {
|
||||
archived: "background:#e2e3e5;color:#383d41",
|
||||
open: "background:#dbeafe;color:#1e40af",
|
||||
in_progress: "background:#fef3c7;color:#92400e",
|
||||
wait: "background:#fef3c7;color:#92400e",
|
||||
progress: "background:#ede9fe;color:#5b21b6",
|
||||
addressed: "background:#dcfce7;color:#166534",
|
||||
deferred: "background:#f1f5f9;color:#64748b",
|
||||
wont_fix: "background:#f3f4f6;color:#9ca3af",
|
||||
todo: "background:#f1f5f9;color:#475569",
|
||||
done: "background:#dcfce7;color:#166534",
|
||||
cancel: "background:#f3f4f6;color:#9ca3af",
|
||||
cancelled: "background:#f3f4f6;color:#9ca3af",
|
||||
resolved: "background:#dcfce7;color:#166534",
|
||||
superseded: "background:#e2e3e5;color:#383d41",
|
||||
@@ -226,8 +229,8 @@ function _buildBody(entity, type) {
|
||||
if (entity.tasks_total !== undefined) {
|
||||
els.push(_divider(),
|
||||
tf("Tasks", `${entity.tasks_done ?? 0} done / ${entity.tasks_total} total` +
|
||||
(entity.tasks_in_progress > 0 ? ` · ${entity.tasks_in_progress} in progress` : "") +
|
||||
(entity.tasks_blocked > 0 ? ` · ${entity.tasks_blocked} blocked` : ""))
|
||||
(entity.tasks_progress > 0 ? ` · ${entity.tasks_progress} progress` : "") +
|
||||
(entity.tasks_wait > 0 ? ` · ${entity.tasks_wait} wait` : ""))
|
||||
);
|
||||
}
|
||||
if (entity.depends_on?.length) {
|
||||
|
||||
@@ -153,7 +153,7 @@ export const FIELD_HELP = {
|
||||
},
|
||||
status: {
|
||||
label: "Status",
|
||||
description: "Current lifecycle state: todo, in_progress, blocked, done, or cancelled.",
|
||||
description: "Current lifecycle state. Tasks use wait, todo, progress, done, or cancel.",
|
||||
doc: "/docs/workstream-lifecycle",
|
||||
},
|
||||
topic_id: {
|
||||
@@ -182,7 +182,7 @@ export const FIELD_HELP = {
|
||||
},
|
||||
needs_human: {
|
||||
label: "Needs Human",
|
||||
description: "True if the task is blocked waiting for human input or approval.",
|
||||
description: "True if the task is waiting for human input or approval.",
|
||||
doc: "/interventions",
|
||||
},
|
||||
intervention_note: {
|
||||
|
||||
@@ -3,7 +3,7 @@ import {WORKSTREAM_STATUSES} from "./workplan-status.js";
|
||||
|
||||
const STYLE_ID = "status-control-styles";
|
||||
|
||||
export const TASK_STATUSES = ["todo", "in_progress", "blocked", "done", "cancelled"];
|
||||
export const TASK_STATUSES = ["wait", "todo", "progress", "done", "cancel"];
|
||||
export {WORKSTREAM_STATUSES};
|
||||
|
||||
function ensureStyles() {
|
||||
@@ -138,9 +138,9 @@ export function statusControl({
|
||||
if (nextStatus === currentStatus) return;
|
||||
|
||||
let blockingReason = null;
|
||||
if (type === "task" && nextStatus === "blocked") {
|
||||
if (type === "task" && nextStatus === "wait") {
|
||||
const existingReason = entity?.blocking_reason ?? "";
|
||||
const reason = existingReason || window.prompt("Blocking reason required for blocked tasks:");
|
||||
const reason = existingReason || window.prompt("Reason this task is waiting:");
|
||||
if (!reason) {
|
||||
select.value = currentStatus;
|
||||
setMessage("unchanged");
|
||||
|
||||
@@ -33,7 +33,7 @@ export function isOpenWorkstream(status) {
|
||||
|
||||
export function isStalledWorkstream(w, staleDays = 7) {
|
||||
const staleAt = new Date(Date.now() - staleDays * 24 * 60 * 60 * 1000);
|
||||
const openTasks = (w.todo ?? 0) + (w.in_progress ?? 0) + (w.blocked ?? 0);
|
||||
const openTasks = (w.todo ?? 0) + (w.progress ?? 0) + (w.wait ?? 0);
|
||||
return ["active", "blocked"].includes(normalizeWorkstreamStatus(w.status))
|
||||
&& new Date(w.updated_at) < staleAt
|
||||
&& (w.done ?? 0) > 0
|
||||
|
||||
@@ -29,11 +29,12 @@ except urllib.error.URLError as e:
|
||||
"archived": 0,
|
||||
"total": 0,
|
||||
},
|
||||
"tasks": {"todo": 0, "in_progress": 0, "blocked": 0, "done": 0, "cancelled": 0, "total": 0},
|
||||
"tasks": {"wait": 0, "todo": 0, "progress": 0, "done": 0, "cancel": 0, "total": 0},
|
||||
"decisions": {"open": 0, "resolved": 0, "escalated": 0, "superseded": 0, "total": 0},
|
||||
},
|
||||
"topics": [],
|
||||
"blocking_decisions": [],
|
||||
"waiting_tasks": [],
|
||||
"blocked_tasks": [],
|
||||
"recent_progress": [],
|
||||
"open_workstreams": [],
|
||||
|
||||
@@ -145,8 +145,8 @@ Notifications appear in the [Inbox](/inbox) page and are queryable via
|
||||
|
||||
A request can optionally link to a **blocking task** via `blocking_task_id`.
|
||||
When the request reaches `completed`, the system automatically patches that
|
||||
task from `blocked` → `todo` and clears its `blocking_reason`. This means
|
||||
blocked work resumes without manual intervention.
|
||||
task from `wait` → `todo` and clears its `blocking_reason`. This means
|
||||
waiting work resumes without manual intervention.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -241,7 +241,7 @@ TOC sidebar as a persistent KPI card.
|
||||
### Multi-mode workstream chart
|
||||
|
||||
The Overview page renders a horizontal stacked bar chart using `@observablehq/plot`
|
||||
showing task counts (done / in progress / blocked / todo) per workstream.
|
||||
showing task counts (done / progress / wait / todo) per workstream.
|
||||
A `<select>` dropdown switches between:
|
||||
|
||||
- **Lifecycle modes**: proposed, ready, active, blocked, backlog, finished, archived
|
||||
|
||||
@@ -129,8 +129,8 @@ The session orientation protocol (every repo's CLAUDE.md) surfaces todos from
|
||||
two sources:
|
||||
|
||||
**Internal todos** (Step 2 of orientation) — workplan files in `workplans/`
|
||||
whose stored workstation/status label is `active`, with tasks in `todo` or
|
||||
`in_progress`.
|
||||
whose stored workstation/status label is `active`, with tasks in `wait`,
|
||||
`todo`, or `progress`.
|
||||
|
||||
**Ecosystem todos targeting this repo** (Step 1 of orientation) —
|
||||
`get_state_summary()` returns all open tasks across all workstreams. The session
|
||||
|
||||
@@ -10,7 +10,7 @@ The Interventions page lists every task that has been flagged `needs_human=true`
|
||||
|
||||
## What is a human intervention?
|
||||
|
||||
A human intervention is a task that an agent has determined cannot proceed without direct human action — for example, approving a financial decision, confirming a legal commitment, or resolving a sensitive ambiguity. Flagging a task does not change its work status; the task continues to be `todo`, `in_progress`, or `blocked` while awaiting attention.
|
||||
A human intervention is a task that an agent has determined cannot proceed without direct human action — for example, approving a financial decision, confirming a legal commitment, or resolving a sensitive ambiguity. Flagging a task does not change its work status; the task continues to be `wait`, `todo`, or `progress` while awaiting attention.
|
||||
|
||||
---
|
||||
|
||||
@@ -26,7 +26,7 @@ A human intervention is a task that an agent has determined cannot proceed witho
|
||||
|
||||
## Open section
|
||||
|
||||
Tasks are sorted by priority (critical → high → medium → low), then by status (blocked → in_progress → todo). Each card shows:
|
||||
Tasks are sorted by priority (critical → high → medium → low), then by status (wait → progress → todo). Each card shows:
|
||||
|
||||
| Element | Meaning |
|
||||
|---|---|
|
||||
@@ -43,7 +43,7 @@ Tasks are sorted by priority (critical → high → medium → low), then by sta
|
||||
|
||||
Click **Mark done** on any open card. An inline form appears requiring a **resolution comment** — a short note describing what was done. The comment is mandatory; clicking **Confirm** without entering text highlights the field in red and does nothing.
|
||||
|
||||
Once confirmed, the API call sets `status = done`, `needs_human = false`, and replaces the action note with your resolution comment. The card moves to the **Completed / Cancelled** section on the next poll.
|
||||
Once confirmed, the API call sets `status = done`, `needs_human = false`, and replaces the action note with your resolution comment. The card moves to the **Completed / Canceled** section on the next poll.
|
||||
|
||||
Click **Cancel** to dismiss the form without making changes.
|
||||
|
||||
|
||||
@@ -20,8 +20,8 @@ by repository. Each bar is broken into four task-status segments:
|
||||
| Colour | Segment |
|
||||
|--------|---------|
|
||||
| green | done |
|
||||
| blue | in progress |
|
||||
| orange-red | blocked |
|
||||
| purple | progress |
|
||||
| orange | wait |
|
||||
| light grey | todo |
|
||||
|
||||
The left axis shows the `domain / repository` label once per repository group.
|
||||
|
||||
@@ -42,7 +42,7 @@ These types are used by the State Hub's built-in write operations:
|
||||
| `workstream_status_changed` | Workstream moved between canonical lifecycle states |
|
||||
| `workstation_advanced` | Flow-aware movement via `advance_workstation()` succeeded |
|
||||
| `task_created` | A new task was added to a workstream |
|
||||
| `task_status_changed` | Task moved to todo / in_progress / blocked / done / cancelled |
|
||||
| `task_status_changed` | Task moved to wait / todo / progress / done / cancel |
|
||||
| `decision_recorded` | A decision (pending or made) was recorded |
|
||||
| `decision_resolved` | A pending decision was resolved |
|
||||
|
||||
|
||||
@@ -127,7 +127,7 @@ priority: medium
|
||||
| Field | Values | Description |
|
||||
|-------|--------|-------------|
|
||||
| `id` | string | Unique task identifier |
|
||||
| `status` | `todo` \| `in_progress` \| `done` | Task state |
|
||||
| `status` | `wait` \| `todo` \| `progress` \| `done` \| `cancel` | Task state |
|
||||
| `priority` | `high` \| `medium` \| `low` | Execution order hint |
|
||||
|
||||
---
|
||||
@@ -137,8 +137,8 @@ priority: medium
|
||||
As Claude completes tasks it edits the workplan file directly:
|
||||
|
||||
```
|
||||
status: todo → status: in_progress (when starting)
|
||||
status: in_progress → status: done (when verified complete)
|
||||
status: todo → status: progress (when starting)
|
||||
status: progress → status: done (when verified complete)
|
||||
```
|
||||
|
||||
When every task is `done`, Claude also updates the frontmatter:
|
||||
|
||||
@@ -5,7 +5,7 @@ title: Tasks — Reference
|
||||
# Tasks — Reference
|
||||
|
||||
The Tasks page shows all tasks across every workstream and domain, with live
|
||||
filtering, a workstation distribution chart, and a blocked-tasks highlight
|
||||
filtering, a workstation distribution chart, and a waiting-tasks highlight
|
||||
section.
|
||||
|
||||
---
|
||||
@@ -17,11 +17,11 @@ compatibility.
|
||||
|
||||
| Workstation | Meaning |
|
||||
|--------|---------|
|
||||
| **wait** | Waiting on another actor, event, decision, input, or condition |
|
||||
| **todo** | Not yet started |
|
||||
| **in_progress** | Actively being worked on |
|
||||
| **blocked** | Cannot proceed — has a blocking reason |
|
||||
| **progress** | Actively being worked on |
|
||||
| **done** | Completed |
|
||||
| **cancelled** | Dropped; not counted toward totals |
|
||||
| **cancel** | Stopped; not counted toward totals |
|
||||
|
||||
---
|
||||
|
||||
@@ -56,37 +56,37 @@ per stored workstation/status label, colour-coded:
|
||||
|
||||
| Colour | Status |
|
||||
|--------|--------|
|
||||
| orange | wait |
|
||||
| grey-blue | todo |
|
||||
| blue | in_progress |
|
||||
| red | blocked |
|
||||
| purple | progress |
|
||||
| green | done |
|
||||
| light grey | cancelled |
|
||||
| light grey | cancel |
|
||||
|
||||
---
|
||||
|
||||
## Blocked Tasks section
|
||||
## Waiting Tasks section
|
||||
|
||||
Shows cards for every task currently in the `blocked` workstation within the
|
||||
Shows cards for every task currently in the `wait` workstation within the
|
||||
active filter. Each card displays:
|
||||
|
||||
- Priority badge and status
|
||||
- Domain and workstream context
|
||||
- Task title
|
||||
- Blocking reason (amber background)
|
||||
- Wait reason (amber background)
|
||||
|
||||
---
|
||||
|
||||
## KPI sidebar card
|
||||
|
||||
Shows four counts for the unfiltered dataset: open (todo + in_progress +
|
||||
blocked), blocked, in progress, done, and a done-% of total.
|
||||
Shows counts for the unfiltered dataset: open (`wait` + `todo` + `progress`),
|
||||
waiting, progress, done, and a done-% of total.
|
||||
|
||||
---
|
||||
|
||||
## Sorting
|
||||
|
||||
Tasks are sorted by status (blocked first, then in_progress, todo, done,
|
||||
cancelled) then by priority (critical → high → medium → low) within each
|
||||
Tasks are sorted by status (wait first, then progress, todo, done, cancel) then
|
||||
by priority (critical → high → medium → low) within each
|
||||
status group.
|
||||
|
||||
---
|
||||
|
||||
@@ -18,7 +18,7 @@ boundary rule and routing workflows.
|
||||
|
||||
### Internal
|
||||
|
||||
Open tasks (`todo`, `in_progress`, `blocked`) in **custodian domain workstreams**
|
||||
Open tasks (`wait`, `todo`, `progress`) in **custodian domain workstreams**
|
||||
whose title does not contain a `[repo:]` routing prefix.
|
||||
|
||||
These are tasks this agent is directly responsible for and can address within
|
||||
|
||||
@@ -57,12 +57,12 @@ const pageState = (async function*() {
|
||||
const counts = {};
|
||||
for (const t of taskList) {
|
||||
const wid = t.workstream_id;
|
||||
if (!counts[wid]) counts[wid] = {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0};
|
||||
if (!counts[wid]) counts[wid] = {done: 0, progress: 0, wait: 0, todo: 0, total: 0};
|
||||
counts[wid].total++;
|
||||
if (t.status === "done") counts[wid].done++;
|
||||
else if (t.status === "in_progress") counts[wid].in_progress++;
|
||||
else if (t.status === "blocked") counts[wid].blocked++;
|
||||
else if (t.status === "todo") counts[wid].todo++;
|
||||
if (t.status === "done") counts[wid].done++;
|
||||
else if (t.status === "progress") counts[wid].progress++;
|
||||
else if (t.status === "wait") counts[wid].wait++;
|
||||
else if (t.status === "todo") counts[wid].todo++;
|
||||
}
|
||||
wsAll = wsList.map(w => {
|
||||
const repo = repoMap[w.repo_id];
|
||||
@@ -78,7 +78,7 @@ const pageState = (async function*() {
|
||||
workplan_archived: workplan.archived ?? false,
|
||||
health_labels: workplan.health_labels ?? [],
|
||||
href: `./workstreams/${w.id}`,
|
||||
...(counts[w.id] ?? {done: 0, in_progress: 0, blocked: 0, todo: 0, total: 0}),
|
||||
...(counts[w.id] ?? {done: 0, progress: 0, wait: 0, todo: 0, total: 0}),
|
||||
};
|
||||
});
|
||||
} catch (e) {
|
||||
@@ -233,13 +233,13 @@ for (const w of chartWs) {
|
||||
_seen.add(group);
|
||||
}
|
||||
|
||||
const statusOrder = ["done", "in progress", "blocked", "todo"];
|
||||
const statusColors = ["#4caf50", "steelblue", "#ff7043", "#e0e0e0"];
|
||||
const statusOrder = ["done", "progress", "wait", "todo"];
|
||||
const statusColors = ["#4caf50", "#8b5cf6", "#f59e0b", "#e0e0e0"];
|
||||
|
||||
const _taskRows = chartWs.flatMap(w => [
|
||||
{id: w.id, title: w.title, status: "done", count: w.done ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
{id: w.id, title: w.title, status: "in progress", count: w.in_progress ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
{id: w.id, title: w.title, status: "blocked", count: w.blocked ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
{id: w.id, title: w.title, status: "progress", count: w.progress ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
{id: w.id, title: w.title, status: "wait", count: w.wait ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
{id: w.id, title: w.title, status: "todo", count: w.todo ?? 0, domain: w.domain, repo: w.repo_label, workplan: w.workplan_filename, href: w.href},
|
||||
]).filter(d => d.count > 0);
|
||||
|
||||
@@ -370,7 +370,7 @@ display(html`<div class="grid grid-cols-3" style="gap:1rem;margin-bottom:1.5rem"
|
||||
## Status
|
||||
|
||||
```js
|
||||
const blockedTasks = summary.blocked_tasks ?? [];
|
||||
const waitingTasks = summary.waiting_tasks ?? summary.blocked_tasks ?? [];
|
||||
const wsById = Object.fromEntries((summary.open_workstreams ?? []).map(w => [w.id, w]));
|
||||
const todayCount = (summary.recent_progress ?? []).filter(e =>
|
||||
e.created_at?.startsWith(new Date().toISOString().slice(0, 10))).length;
|
||||
@@ -388,9 +388,9 @@ const statusEl = html`<div>
|
||||
<p class="big-num">${decCount}</p>
|
||||
<small>${decisions.escalated ?? 0} escalated</small>
|
||||
</a>
|
||||
<div class="card card-link ${blockedTasks.length > 0 ? 'warn' : ''}" data-toggle="blocked-panel">
|
||||
<h3>Blocked Tasks</h3>
|
||||
<p class="big-num">${blockedTasks.length}</p>
|
||||
<div class="card card-link ${waitingTasks.length > 0 ? 'warn' : ''}" data-toggle="waiting-panel">
|
||||
<h3>Waiting Tasks</h3>
|
||||
<p class="big-num">${waitingTasks.length}</p>
|
||||
<small>of ${tasks.total ?? 0} total · click to expand</small>
|
||||
</div>
|
||||
<a class="card card-link" href="#recent-activity">
|
||||
@@ -400,10 +400,10 @@ const statusEl = html`<div>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div id="blocked-panel" style="display:none;margin-bottom:1rem">
|
||||
${blockedTasks.length === 0
|
||||
? html`<p class="dim" style="padding:0.5rem 0">No tasks currently blocked.</p>`
|
||||
: html`<div class="bt-list">${blockedTasks.map(t => {
|
||||
<div id="waiting-panel" style="display:none;margin-bottom:1rem">
|
||||
${waitingTasks.length === 0
|
||||
? html`<p class="dim" style="padding:0.5rem 0">No tasks currently waiting.</p>`
|
||||
: html`<div class="bt-list">${waitingTasks.map(t => {
|
||||
const wsName = wsById[t.workstream_id]?.title ?? t.workstream_id?.slice(0,8) ?? "—";
|
||||
return html`<div class="bt-row">
|
||||
<div class="bt-meta">${wsName}</div>
|
||||
@@ -415,11 +415,11 @@ const statusEl = html`<div>
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
statusEl.querySelector('[data-toggle="blocked-panel"]').addEventListener('click', () => {
|
||||
const panel = statusEl.querySelector('#blocked-panel');
|
||||
statusEl.querySelector('[data-toggle="waiting-panel"]').addEventListener('click', () => {
|
||||
const panel = statusEl.querySelector('#waiting-panel');
|
||||
const isOpen = panel.style.display !== 'none';
|
||||
panel.style.display = isOpen ? 'none' : 'block';
|
||||
statusEl.querySelector('[data-toggle="blocked-panel"] small').textContent =
|
||||
statusEl.querySelector('[data-toggle="waiting-panel"] small').textContent =
|
||||
isOpen ? `of ${tasks.total ?? 0} total · click to expand` : `of ${tasks.total ?? 0} total · click to collapse`;
|
||||
});
|
||||
|
||||
|
||||
@@ -51,7 +51,7 @@ const _ts = interventionState.ts;
|
||||
```
|
||||
|
||||
```js
|
||||
const OPEN_STATUSES = new Set(["todo", "in_progress", "blocked"]);
|
||||
const OPEN_STATUSES = new Set(["wait", "todo", "progress"]);
|
||||
// open = currently flagged for human action
|
||||
// closed = previously flagged (intervention_note records the resolution comment)
|
||||
const open = tasks.filter(t => t.needs_human === true);
|
||||
@@ -125,7 +125,7 @@ Tasks flagged `needs_human=true` — actions only a human can take.
|
||||
import {openActionConfirm} from "./components/action-confirm.js";
|
||||
|
||||
const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3};
|
||||
const STATUS_ORDER = {blocked: 0, in_progress: 1, todo: 2};
|
||||
const STATUS_ORDER = {wait: 0, progress: 1, todo: 2};
|
||||
|
||||
function sortTasks(arr) {
|
||||
return [...arr].sort((a, b) => {
|
||||
@@ -217,11 +217,11 @@ if (closed.length === 0) {
|
||||
.task-priority-medium { background: #dbeafe; color: #1e40af; }
|
||||
.task-priority-low { background: #f1f5f9; color: #475569; }
|
||||
.task-status-chip { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; }
|
||||
.status-chip-blocked { background: #fee2e2; color: #991b1b; }
|
||||
.status-chip-in_progress { background: #dbeafe; color: #1e40af; }
|
||||
.status-chip-wait { background: #fef3c7; color: #92400e; }
|
||||
.status-chip-progress { background: #ede9fe; color: #5b21b6; }
|
||||
.status-chip-todo { background: #f1f5f9; color: #475569; }
|
||||
.status-chip-done { background: #dcfce7; color: #166534; }
|
||||
.status-chip-cancelled { background: #f1f5f9; color: #64748b; }
|
||||
.status-chip-cancel { background: #f1f5f9; color: #64748b; }
|
||||
.task-context { color: var(--theme-foreground-muted, #666); }
|
||||
.task-ws-name { font-style: italic; }
|
||||
.dim { color: gray; font-style: italic; }
|
||||
|
||||
@@ -50,7 +50,7 @@ const _ts = taskState.ts;
|
||||
```js
|
||||
import {MultiSelect} from "./components/multiselect.js";
|
||||
|
||||
const STATUSES = ["todo", "in_progress", "blocked", "done", "cancelled"];
|
||||
const STATUSES = ["wait", "todo", "progress", "done", "cancel"];
|
||||
const PRIORITIES = ["critical", "high", "medium", "low"];
|
||||
const _domainsResp = await fetch(`${API}/domains/?status=active`).catch(() => null);
|
||||
const DOMAINS = _domainsResp?.ok
|
||||
@@ -95,11 +95,11 @@ import {openEntityModal, buildEntityTable} from "./components/entity-modal.js";
|
||||
import {statusControl, TASK_STATUSES} from "./components/status-control.js";
|
||||
|
||||
// ── KPI sidebar card ─────────────────────────────────────────────────────────
|
||||
const _open = data.filter(t => ["todo", "in_progress", "blocked"].includes(t.status));
|
||||
const _blocked = data.filter(t => t.status === "blocked");
|
||||
const _inProg = data.filter(t => t.status === "in_progress");
|
||||
const _open = data.filter(t => ["wait", "todo", "progress"].includes(t.status));
|
||||
const _waiting = data.filter(t => t.status === "wait");
|
||||
const _inProg = data.filter(t => t.status === "progress");
|
||||
const _done = data.filter(t => t.status === "done");
|
||||
const _total = data.filter(t => t.status !== "cancelled").length;
|
||||
const _total = data.filter(t => t.status !== "cancel").length;
|
||||
const _donePct = _total > 0 ? Math.round(_done.length / _total * 100) : 0;
|
||||
|
||||
const _kpiBox = html`<div class="kpi-infobox">
|
||||
@@ -111,13 +111,13 @@ const _kpiBox = html`<div class="kpi-infobox">
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-row">
|
||||
<span class="kpi-row-label">blocked</span>
|
||||
<span class="kpi-row-label">waiting</span>
|
||||
<div class="kpi-row-right">
|
||||
<div class="kpi-row-value" style="color:${_blocked.length > 0 ? '#dc2626' : 'inherit'}">${_blocked.length}</div>
|
||||
<div class="kpi-row-value" style="color:${_waiting.length > 0 ? '#d97706' : 'inherit'}">${_waiting.length}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="kpi-row">
|
||||
<span class="kpi-row-label">in progress</span>
|
||||
<span class="kpi-row-label">progress</span>
|
||||
<div class="kpi-row-right">
|
||||
<div class="kpi-row-value">${_inProg.length}</div>
|
||||
</div>
|
||||
@@ -154,11 +154,11 @@ injectTocTop("live-indicator", _liveEl);
|
||||
import * as Plot from "npm:@observablehq/plot";
|
||||
|
||||
const STATUS_COLOR = {
|
||||
wait: "#f59e0b",
|
||||
todo: "#94a3b8",
|
||||
in_progress: "#3b82f6",
|
||||
blocked: "#ef4444",
|
||||
progress: "#8b5cf6",
|
||||
done: "#22c55e",
|
||||
cancelled: "#cbd5e1",
|
||||
cancel: "#cbd5e1",
|
||||
};
|
||||
|
||||
const byStatus = STATUSES
|
||||
@@ -178,16 +178,16 @@ display(byStatus.length === 0
|
||||
);
|
||||
```
|
||||
|
||||
## Blocked Tasks
|
||||
## Waiting Tasks
|
||||
|
||||
```js
|
||||
const _blockedInFilter = filtered.filter(t => t.status === "blocked");
|
||||
const _waitingInFilter = filtered.filter(t => t.status === "wait");
|
||||
|
||||
if (_blockedInFilter.length === 0) {
|
||||
display(html`<p class="dim">No blocked tasks in current filter. ✓</p>`);
|
||||
if (_waitingInFilter.length === 0) {
|
||||
display(html`<p class="dim">No waiting tasks in current filter. ✓</p>`);
|
||||
} else {
|
||||
display(html`<div class="task-blocked-list">${_blockedInFilter.map(t => html`
|
||||
<div class="task-blocked-item entity-row" onclick=${() => openEntityModal(t, "task")}>
|
||||
display(html`<div class="task-waiting-list">${_waitingInFilter.map(t => html`
|
||||
<div class="task-waiting-item entity-row" onclick=${() => openEntityModal(t, "task")}>
|
||||
<div class="task-item-header">
|
||||
<span class="task-badge task-priority-${t.priority}">${t.priority}</span>
|
||||
<span class="task-context">${t.domain}</span>
|
||||
@@ -196,7 +196,7 @@ if (_blockedInFilter.length === 0) {
|
||||
${t.assignee ? html`<span class="task-assignee">@${t.assignee}</span>` : ""}
|
||||
</div>
|
||||
<div class="task-title">${t.title}</div>
|
||||
${t.blocking_reason ? html`<div class="task-blocking-reason">⊘ ${t.blocking_reason}</div>` : ""}
|
||||
${t.blocking_reason ? html`<div class="task-wait-reason">⊘ ${t.blocking_reason}</div>` : ""}
|
||||
</div>
|
||||
`)}</div>`);
|
||||
}
|
||||
@@ -209,7 +209,7 @@ display(_filtersForm);
|
||||
display(html`<p><strong>${filtered.length}</strong> tasks shown.</p>`);
|
||||
|
||||
const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3};
|
||||
const STATUS_ORDER = {blocked: 0, in_progress: 1, todo: 2, done: 3, cancelled: 4};
|
||||
const STATUS_ORDER = {wait: 0, progress: 1, todo: 2, done: 3, cancel: 4};
|
||||
|
||||
const sorted = [...filtered].sort((a, b) => {
|
||||
const sd = (STATUS_ORDER[a.status] ?? 9) - (STATUS_ORDER[b.status] ?? 9);
|
||||
@@ -246,9 +246,9 @@ display(buildEntityTable(
|
||||
|
||||
/* ── Filters ──────────────────────────────────────────────────────────────── */
|
||||
|
||||
/* ── Blocked task cards ───────────────────────────────────────────────────── */
|
||||
.task-blocked-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.task-blocked-item { border-left: 3px solid #ef4444; border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.65rem 0.9rem; }
|
||||
/* ── Waiting task cards ───────────────────────────────────────────────────── */
|
||||
.task-waiting-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.task-waiting-item { border-left: 3px solid #f59e0b; border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.65rem 0.9rem; }
|
||||
.task-item-header { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
|
||||
.task-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
.task-priority-critical { background: #fee2e2; color: #991b1b; }
|
||||
@@ -260,7 +260,7 @@ display(buildEntityTable(
|
||||
.task-due { color: #dc2626; font-weight: 600; }
|
||||
.task-assignee { color: var(--theme-foreground-muted, #888); }
|
||||
.task-title { font-weight: 600; font-size: 0.95rem; margin-bottom: 0.15rem; }
|
||||
.task-blocking-reason { font-size: 0.8rem; color: #b45309; background: #fef3c7; border-radius: 4px; padding: 0.2rem 0.5rem; margin-top: 0.25rem; }
|
||||
.task-wait-reason { font-size: 0.8rem; color: #b45309; background: #fef3c7; border-radius: 4px; padding: 0.2rem 0.5rem; margin-top: 0.25rem; }
|
||||
|
||||
/* ── Utility ──────────────────────────────────────────────────────────────── */
|
||||
.dim { color: gray; font-style: italic; }
|
||||
|
||||
@@ -60,7 +60,7 @@ const _ts = todoState.ts;
|
||||
|
||||
```js
|
||||
// ── Classify tasks ────────────────────────────────────────────────────────────
|
||||
const OPEN_STATUSES = new Set(["todo", "in_progress", "blocked"]);
|
||||
const OPEN_STATUSES = new Set(["wait", "todo", "progress"]);
|
||||
|
||||
// Internal: custodian domain, open, no [repo:] routing prefix
|
||||
const internal = tasks.filter(t =>
|
||||
@@ -141,7 +141,7 @@ without a cross-repo routing prefix.
|
||||
|
||||
```js
|
||||
const PRIORITY_ORDER = {critical: 0, high: 1, medium: 2, low: 3};
|
||||
const STATUS_ORDER = {blocked: 0, in_progress: 1, todo: 2};
|
||||
const STATUS_ORDER = {wait: 0, progress: 1, todo: 2};
|
||||
|
||||
function sortTasks(arr) {
|
||||
return [...arr].sort((a, b) => {
|
||||
@@ -249,8 +249,8 @@ if (improvements.length === 0) {
|
||||
/* ── Task list ────────────────────────────────────────────────────────────── */
|
||||
.task-list { display: flex; flex-direction: column; gap: 0.5rem; }
|
||||
.task-item { border-left: 3px solid var(--theme-foreground-faint, #ccc); border-radius: 0 6px 6px 0; background: var(--theme-background-alt); padding: 0.65rem 0.9rem; }
|
||||
.task-item.status-blocked { border-left-color: #ef4444; }
|
||||
.task-item.status-in_progress { border-left-color: #3b82f6; }
|
||||
.task-item.status-wait { border-left-color: #f59e0b; }
|
||||
.task-item.status-progress { border-left-color: #8b5cf6; }
|
||||
.task-item.status-todo { border-left-color: #94a3b8; }
|
||||
.task-item-header { display: flex; flex-wrap: wrap; align-items: center; gap: 0.4rem; margin-bottom: 0.3rem; font-size: 0.75rem; }
|
||||
.task-badge { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.04em; }
|
||||
@@ -259,8 +259,8 @@ if (improvements.length === 0) {
|
||||
.task-priority-medium { background: #dbeafe; color: #1e40af; }
|
||||
.task-priority-low { background: #f1f5f9; color: #475569; }
|
||||
.task-status-chip { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; }
|
||||
.status-chip-blocked { background: #fee2e2; color: #991b1b; }
|
||||
.status-chip-in_progress { background: #dbeafe; color: #1e40af; }
|
||||
.status-chip-wait { background: #fef3c7; color: #92400e; }
|
||||
.status-chip-progress { background: #ede9fe; color: #5b21b6; }
|
||||
.status-chip-todo { background: #f1f5f9; color: #475569; }
|
||||
.task-context { color: var(--theme-foreground-muted, #666); }
|
||||
.task-ws-name { font-style: italic; }
|
||||
|
||||
@@ -44,7 +44,7 @@ if (raw.error) {
|
||||
<div><span>Tasks</span><strong>${taskRows.length}</strong></div>
|
||||
</div>`);
|
||||
|
||||
const statusOrder = {blocked: 0, in_progress: 1, todo: 2, done: 3, cancelled: 4};
|
||||
const statusOrder = {wait: 0, progress: 1, todo: 2, done: 3, cancel: 4};
|
||||
const sortedTasks = [...taskRows].sort((a, b) => {
|
||||
const statusCompare = (statusOrder[a.status] ?? 9) - (statusOrder[b.status] ?? 9);
|
||||
if (statusCompare !== 0) return statusCompare;
|
||||
@@ -131,8 +131,8 @@ if (raw.error) {
|
||||
white-space: nowrap;
|
||||
}
|
||||
.task-status-done { background: #e8f5e9; color: #1b5e20; }
|
||||
.task-status-in_progress { background: #e3f2fd; color: #0d47a1; }
|
||||
.task-status-blocked { background: #fff3e0; color: #bf360c; }
|
||||
.task-status-progress { background: #ede9fe; color: #5b21b6; }
|
||||
.task-status-wait { background: #fff3e0; color: #bf360c; }
|
||||
.task-status-todo { background: #f1f5f9; color: #334155; }
|
||||
.task-status-cancelled { background: #f3f4f6; color: #6b7280; }
|
||||
.task-status-cancel { background: #f3f4f6; color: #6b7280; }
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user