Complete workplan state model cleanup

This commit is contained in:
2026-05-18 01:31:36 +02:00
parent 98b2cb6484
commit d6522a9a40
42 changed files with 789 additions and 310 deletions

View File

@@ -131,8 +131,12 @@ function _ensureStyles() {
/* ── Style maps ──────────────────────────────────────────────────────────── */
const _STATUS_STYLE = {
proposed: "background:#fef3c7;color:#92400e",
ready: "background:#e0f2fe;color:#075985",
active: "background:#d4edda;color:#155724",
blocked: "background:#f8d7da;color:#721c24",
backlog: "background:#f1f5f9;color:#64748b",
finished: "background:#cce5ff;color:#004085",
completed: "background:#cce5ff;color:#004085",
archived: "background:#e2e3e5;color:#383d41",
open: "background:#dbeafe;color:#1e40af",

View File

@@ -0,0 +1,45 @@
export const WORKSTREAM_STATUSES = [
"proposed",
"ready",
"active",
"blocked",
"backlog",
"finished",
"archived",
];
export const OPEN_WORKSTREAM_STATUSES = ["ready", "active", "blocked"];
export const CLOSED_WORKSTREAM_STATUSES = ["finished", "archived"];
export const LEGACY_STATUS_ALIASES = {
todo: "ready",
done: "finished",
completed: "finished",
accepted: "finished",
};
export function normalizeWorkstreamStatus(status) {
const value = String(status ?? "").trim().toLowerCase();
return LEGACY_STATUS_ALIASES[value] ?? value;
}
export function isClosedWorkstream(status) {
return CLOSED_WORKSTREAM_STATUSES.includes(normalizeWorkstreamStatus(status));
}
export function isOpenWorkstream(status) {
return OPEN_WORKSTREAM_STATUSES.includes(normalizeWorkstreamStatus(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);
return ["active", "blocked"].includes(normalizeWorkstreamStatus(w.status))
&& new Date(w.updated_at) < staleAt
&& (w.done ?? 0) > 0
&& openTasks > 0;
}
export function needsReviewWorkstream(w) {
return Array.isArray(w.health_labels) && w.health_labels.includes("needs_review");
}

View File

@@ -19,7 +19,16 @@ except urllib.error.URLError as e:
"generated_at": None,
"totals": {
"topics": {"active": 0, "paused": 0, "archived": 0, "total": 0},
"workstreams": {"active": 0, "blocked": 0, "completed": 0, "archived": 0, "total": 0},
"workstreams": {
"proposed": 0,
"ready": 0,
"active": 0,
"blocked": 0,
"backlog": 0,
"finished": 0,
"archived": 0,
"total": 0,
},
"tasks": {"todo": 0, "in_progress": 0, "blocked": 0, "done": 0, "cancelled": 0, "total": 0},
"decisions": {"open": 0, "resolved": 0, "escalated": 0, "superseded": 0, "total": 0},
},

View File

@@ -4,6 +4,7 @@ title: Dependencies
```js
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {normalizeWorkstreamStatus} from "./components/workplan-status.js";
```
```js
@@ -29,6 +30,7 @@ const depState = (async function*() {
const repoMap = Object.fromEntries(repoList.map(r => [r.id, r]));
wsMap = Object.fromEntries(wsList.map(w => [w.id, {
...w,
status: normalizeWorkstreamStatus(w.status),
// Prefer repo→domain (GEMS primary); fall back to topic→domain
domain: repoMap[w.repo_id]?.domain_slug ?? topicMap[w.topic_id]?.domain_slug ?? "unknown",
}]));
@@ -87,7 +89,7 @@ injectTocTop("dep-kpi-box", _kpiBox);
injectTocTop("live-indicator", _liveEl);
```
Directed edges between active workstreams. An edge **A → B** means A cannot
Directed edges between open workstreams. An edge **A → B** means A cannot
fully proceed until B reaches a satisfactory state.
```js
@@ -152,9 +154,12 @@ if (edges.length === 0) {
.dep-title { font-weight: 500; max-width: 22rem; }
.dep-arrow { text-align: center; color: var(--theme-foreground-faint, #bbb); font-size: 1rem; }
.dep-status { display: inline-block; padding: 0.1rem 0.45rem; border-radius: 10px; font-size: 0.7rem; font-weight: 500; }
.dep-status-proposed { background: #fef3c7; color: #92400e; }
.dep-status-ready { background: #e0f2fe; color: #075985; }
.dep-status-active { background: #dcfce7; color: #166534; }
.dep-status-completed { background: #f1f5f9; color: #475569; }
.dep-status-blocked { background: #fee2e2; color: #991b1b; }
.dep-status-backlog { background: #f1f5f9; color: #64748b; }
.dep-status-finished { background: #f1f5f9; color: #475569; }
.dep-status-archived { background: #f1f5f9; color: #9ca3af; }
.dim { color: gray; font-style: italic; }
</style>

View File

@@ -244,7 +244,8 @@ The Overview page renders a horizontal stacked bar chart using `@observablehq/pl
showing task counts (done / in progress / blocked / todo) per workstream.
A `<select>` dropdown switches between:
- **Status modes**: active, accepted, finished, blocked, stalled, oldies
- **Lifecycle modes**: proposed, ready, active, blocked, backlog, finished, archived
- **Health modes**: needs review, stalled
- **Time modes**: last 1h, 24h, 7d, 30d, today, this week, this month
Domains are sorted by most recent workstream activity (most active domain at

View File

@@ -4,7 +4,7 @@ title: Dependencies — Reference
# Dependencies — Reference
The Dependencies page shows the directed dependency graph between active
The Dependencies page shows the directed dependency graph between open
workstreams — which workstreams are waiting on others to reach a satisfactory
state before they can fully proceed.
@@ -13,7 +13,7 @@ state before they can fully proceed.
## What is a dependency edge?
A dependency edge **A → B** means workstream A cannot fully proceed until
workstream B is in a satisfactory state (typically `completed` or `archived`).
workstream B is in a satisfactory state (typically `finished` or `archived`).
Edges are used to model real sequencing constraints: for example, a shared
library must reach a stable release before downstream domains can build on it.
@@ -36,7 +36,7 @@ Each row shows:
| **→** | Direction arrow |
| **Blocked-by domain** | Domain of the prerequisite workstream |
| **Blocked-by workstream** | Title of the workstream that must complete first |
| **Status** | Current status of the prerequisite (green = active, grey = completed) |
| **Status** | Current status of the prerequisite (green = active, grey = finished/archived) |
---

View File

@@ -48,7 +48,7 @@ Four metric cards:
| Card | Meaning |
|------|---------|
| **Active Workstreams** | Count of non-completed, non-archived workstreams |
| **Active Workstreams** | Count of active/blocked execution workstreams |
| **Blocking Decisions** | Pending decisions with status `open` or `escalated` — orange border if > 0 |
| **Blocked Tasks** | Click to expand the list with blocking reasons |
| **Events Today** | Progress events created on today's date |

View File

@@ -39,7 +39,7 @@ These types are used by the State Hub's built-in write operations:
| Type | When emitted |
|---|---|
| `workstream_created` | A new workstream was registered |
| `workstream_status_changed` | Workstream moved to active / blocked / completed / archived |
| `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 |

View File

@@ -92,7 +92,7 @@ cd ~/ralph-workplan && ./install.sh --uninstall
---
id: WP-0001
title: "Build a thing"
status: active
status: ready
---
Optional description.
@@ -120,7 +120,7 @@ priority: medium
|-------|--------|-------------|
| `id` | string | Unique workplan identifier |
| `title` | string | Human-readable name |
| `status` | `active` \| `done` \| `paused` | Workplan lifecycle state |
| `status` | `proposed` \| `ready` \| `active` \| `blocked` \| `backlog` \| `finished` \| `archived` | Workplan lifecycle state |
**Task block fields:**
@@ -144,7 +144,7 @@ status: in_progress → status: done (when verified complete)
When every task is `done`, Claude also updates the frontmatter:
```
status: active status: done
status: active -> status: finished
```
The loop detects this on the next iteration and stops.

View File

@@ -72,7 +72,7 @@ autonomously. No human interaction is needed unless the agent has a question.
The [Repos](/repos) page shows each repo's integration status. An **integrating**
badge appears on repos with an active Repo Integration workstream. The badge
clears when the workstream is marked completed.
clears when the workstream is marked finished.
---
@@ -120,8 +120,8 @@ repo agent should:
primary near-term work; register the workstream in the hub via MCP
4. Execute T3 — ingest the SBOM so the repo appears green on the Repos page
5. Execute T4 — a quick scan for obvious EPs/TDs; defer if nothing obvious
6. Mark each task `done` in the hub as completed
7. Mark the Repo Integration workstream `completed`
6. Mark each task `done` in the hub
7. Mark the Repo Integration workstream `finished`
8. Log a progress event summarising the integration
The agent should resolve each task independently and in order. It does not

View File

@@ -73,13 +73,13 @@ Detects concentration of blocking power. High SPR means one delay propagates wid
### PEP — Parallel Execution Potential
```
PEP = active workstreams with all deps completed / (active + blocked)
PEP = ready or active workstreams with all deps finished / (ready + active + blocked)
```
Estimates how much work can proceed right now. A workstream is eligible if its
stored workstation label is `active` and the flow/dependency checks report no
stored workstation label is `ready` or `active` and the flow/dependency checks report no
unmet dependency assertion; practically, every workstream it depends on has
reached `completed` or `archived`.
reached `finished` or `archived`.
| PEP | Warning |
|---|---|
@@ -147,7 +147,7 @@ The domain breakdown is shown when at least two domains have active workstreams.
| Symptom | Action |
|---|---|
| High DD | Decompose tightly coupled workstreams; remove unnecessary dependencies |
| High BR | Unblock workstreams — resolve the blocking condition, or mark dependency as completed if done |
| High BR | Unblock workstreams — resolve the blocking condition, or mark dependency as finished if done |
| High SPR | Split the bottleneck workstream into independent deliverables |
| Low PEP | Complete prerequisite workstreams or re-sequence work |
| High CDDR | Refactor cross-domain dependencies into shared contracts or invert the dependency |

View File

@@ -143,11 +143,11 @@ High SPR indicates fragile structure where one delay propagates widely.
A workstream is eligible if:
* Status = active
* All dependencies are completed
* Status = ready or active
* All dependencies are finished or archived
[
PEP = \frac{\text{Eligible active workstreams}}{\text{Active + Blocked}}
PEP = \frac{\text{Eligible ready or active workstreams}}{\text{Ready + Active + Blocked}}
]
---
@@ -378,4 +378,3 @@ It captures both:
* Operational flow conditions
By combining graph properties with status information, WHI enables proactive management of coordination complexity.

View File

@@ -1,111 +1,86 @@
---
title: Workstream Lifecycle Reference
title: Workstream Lifecycle - Reference
---
# Workstream Lifecycle Reference
# Workstream Lifecycle - Reference
A workstream is an information object that occupies a named workstation. The
stored `status` field keeps the current workstation label, while the
task-flow engine derives which other workstations are reachable and which exit
assertions are blocking movement. The dashboard "Workstreams by Domain" chart
exposes stored and derived states as selectable filters so attention can be
directed to the right workstreams at the right time.
A workstream is an information object that occupies a named lifecycle state.
The stored `status` field keeps that state, while the task-flow engine derives
which other states are reachable and which exit assertions are blocking
movement. Dashboard health filters such as `needs_review` and `stalled` are
derived labels, not stored lifecycle values.
---
## Core workstations
## Stored Lifecycle States
These are the primary workstations used by State Hub workstreams:
| Workstation | Source | Meaning |
| State | Source | Meaning |
|---|---|---|
| **active** | DB `status = active` | Work is in progress or ready to start |
| **finished** | Derived — no open tasks | All tasks are done, but no explicit review has taken place yet |
| **accepted** | DB `status = completed` | Custodian and human have reviewed the workstream, quality checks passed, and it is formally signed off |
| **proposed** | DB `status = proposed` | Plan exists, but must be reviewed against current repo state |
| **ready** | DB `status = ready` | Plan has been reviewed and is ready to execute |
| **active** | DB `status = active` | Work is in progress |
| **blocked** | DB `status = blocked` | Work cannot proceed until a dependency, decision, or input clears |
| **backlog** | DB `status = backlog` | Intentionally parked so it stays out of current work views |
| **finished** | DB `status = finished` | Implementation is complete |
| **archived** | DB `status = archived` | Historical record outside normal planning and execution |
The normal human-facing path is: **active → finished → accepted**.
`accepted` is the only state that requires an explicit action. It is reached by
advancing the workstream to the `completed` workstation after deliberate
review, not by task counts alone. This makes it a reliable anchor: anything in
`finished` but not yet in `accepted` is work that still needs a quality pass.
---
## Attention signals
These signals are orthogonal to the core workstation — a workstream can be
`active` and `stalled` at the same time. They serve as health indicators
rather than stored lifecycle stages.
| Signal | Source | Meaning |
|---|---|---|
| **blocked** | Derived — unmet exit assertion or blocked task | Work cannot currently leave its workstation; inspect `blocked_reasons` |
| **stalled** | Derived — `updated_at` > 7 days ago, has both done and open tasks | Work started but activity has stopped; needs a nudge |
| **oldies** | Derived — `created_at` > 7 days ago, zero done tasks | Workstream is old and nothing has been completed yet; may need re-evaluation |
---
## The acceptance quality gate
When a workstream reaches **finished** (all tasks done), the custodian's role is to:
1. Review the deliverables against the workstream's stated purpose and scope
2. Check for missing tests, documentation, or follow-up issues
3. Create tasks for any gaps found — this moves the workstream back to **active**
4. Once satisfied, advance to the `completed` workstation — this marks it as **accepted**
This pattern ensures that "done" and "accepted" are distinct signals.
`finished` is a fact about task counts; `accepted` is a statement of quality.
Normal progression:
```text
backlog -> proposed -> ready -> active -> finished -> archived
\ \
\ -> blocked -> active
-> backlog
```
# Inspect and accept a workstream via MCP
get_flow_state(entity_type="workstream", entity_id="<uuid>")
advance_workstation(entity_type="workstream", entity_id="<uuid>", target_workstation="completed")
# Or via REST
---
## Health Labels
| Label | Source | Meaning |
|---|---|---|
| **needs_review** | Ready-review metadata + git diff | A `ready` workplan may be stale because relevant files changed since review |
| **stalled** | Task counts + timestamp | Work started, but there has been no meaningful progress after the threshold |
`needs_review` and `stalled` can appear beside lifecycle states. They should
not be written into workplan frontmatter or directly into the workstream
`status` field.
---
## Ready Review Metadata
Ready workplans may include optional frontmatter:
```yaml
reviewed_at: "YYYY-MM-DD"
reviewed_by: "human-or-agent"
reviewed_against_commit: "<git-sha>"
context_paths:
- "path/or/glob"
```
If `reviewed_against_commit` differs from `HEAD`, State Hub checks
`context_paths` when present. Relevant changes produce the derived
`needs_review` label. Automatic demotion from `ready` to `proposed` is guarded
behind explicit tooling, not done silently.
---
## Flow Operations
```text
get_flow_state(entity_type="workstream", entity_id="<uuid>")
advance_workstation(entity_type="workstream", entity_id="<uuid>", target_workstation="finished")
```
Direct status patching still exists for bootstrap and compatibility work:
```bash
curl -X PATCH http://127.0.0.1:8000/workstreams/<uuid>/ \
-H "Content-Type: application/json" \
-d '{"status": "completed"}'
-d '{"status": "finished"}'
```
---
## Time-based filters
The chart also supports time-window filters that cut across all lifecycle states:
| Filter | Shows workstreams where… |
|---|---|
| **last 1 hour** | `updated_at` or `created_at` within the last 60 minutes |
| **last 24 hours** | … within the last 24 hours |
| **last 7 days** | … within the last 7 days |
| **last 30 days** | … within the last 30 days |
| **today** | … since midnight today |
| **this week** | … since Monday of the current week |
| **this month** | … since the 1st of the current month |
In time-based views, workstream labels are **bold** for accepted and blocked
workstreams to distinguish notable states at a glance.
---
## Stored Label Vs. Dashboard State
The DB stores a single `status` field on each workstream. Treat it as the
current workstation label. The dashboard maps this alongside flow-engine
results, dependency assertions, and task-count data to produce the richer set
of filter states:
| Dashboard state | Stored label / derived source | Condition |
|---|---|---|
| active | `status = active` | — |
| accepted | `status = completed` | — |
| finished | task counts | `todo + in_progress + blocked = 0` |
| blocked | flow result / task counts | `exit_blocked = true` or `blocked ≥ 1` |
| stalled | task counts + timestamp | `done ≥ 1` and `open ≥ 1` and `updated_at > 7d ago` |
| oldies | task counts + timestamp | `done = 0` and `open ≥ 1` and `created_at > 7d ago` |
*Workstreams are never hard-deleted — use `advance_workstation(...,
"completed")` or advance/patch to `"archived"` to close them without losing
history.*
Workstreams are never hard-deleted. Use `finished` for completed
implementation and `archived` for historical records outside normal planning.

View File

@@ -20,14 +20,17 @@ as filters change.
| Workstation | Meaning |
|---|---|
| **active** | Work in progress or ready to start |
| **proposed** | Plan exists, but needs review against current repo state |
| **ready** | Reviewed and ready to execute |
| **active** | Work is in progress |
| **blocked** | Stored blocker label; the State Hub can also derive blocked state from unmet exit assertions |
| **completed** | Formally accepted after custodian review (shown as **accepted** in the overview chart) |
| **archived** | Closed without completion; no longer relevant |
| **backlog** | Intentionally parked for later |
| **finished** | Implementation is complete |
| **archived** | Closed historical record |
See [Workstream Lifecycle](/docs/workstream-lifecycle) for the full task-flow
model including derived states (finished, stalled, oldies) and assertion-based
blocking.
model including derived health labels (`needs_review`, `stalled`) and
assertion-based blocking.
---
@@ -91,7 +94,7 @@ create_workstream(
topic_id = "<uuid>",
title = "Build user authentication",
description = "JWT-based auth, refresh tokens, middleware",
status = "active",
status = "ready",
owner = "human",
due_date = "2026-04-01"
)
@@ -102,7 +105,7 @@ Via REST:
```bash
curl -X POST http://127.0.0.1:8000/workstreams/ \
-H "Content-Type: application/json" \
-d '{"topic_id": "<uuid>", "title": "…", "status": "active"}'
-d '{"topic_id": "<uuid>", "title": "…", "status": "ready"}'
```
---
@@ -111,7 +114,7 @@ curl -X POST http://127.0.0.1:8000/workstreams/ \
```
get_flow_state(entity_type="workstream", entity_id="<uuid>")
advance_workstation(entity_type="workstream", entity_id="<uuid>", target_workstation="completed")
advance_workstation(entity_type="workstream", entity_id="<uuid>", target_workstation="finished")
```
Movement is flow-aware: the task-flow engine evaluates the target

View File

@@ -4,6 +4,13 @@ title: Overview
```js
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {
WORKSTREAM_STATUSES,
isClosedWorkstream,
isStalledWorkstream,
needsReviewWorkstream,
normalizeWorkstreamStatus,
} from "./components/workplan-status.js";
```
```js
@@ -52,11 +59,13 @@ const pageState = (async function*() {
const workplan = workplanMap[w.id] ?? {};
return {
...w,
status: normalizeWorkstreamStatus(w.status),
domain: repo?.domain_slug ?? topic?.domain_slug ?? "unknown",
repo_label: repo?.slug ?? workplan.repo_slug ?? "unassigned",
workplan_filename: workplan.filename ?? null,
workplan_relative_path: workplan.relative_path ?? null,
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}),
};
@@ -126,13 +135,18 @@ display(html`<div class="warning" style="display:${summary.error ? '' : 'none'}"
// view() is the idiomatic Observable Framework reactive input:
// it displays the element AND returns a reactive value that re-runs dependent blocks.
const _chartMode = view(html`<select class="ws-mode-select">
<optgroup label="By Status">
<option value="active" selected>active</option>
<option value="accepted">accepted</option>
<option value="finished">finished</option>
<optgroup label="Lifecycle">
<option value="ready" selected>ready</option>
<option value="active">active</option>
<option value="blocked">blocked</option>
<option value="proposed">proposed</option>
<option value="backlog">backlog</option>
<option value="finished">finished</option>
<option value="archived">archived</option>
</optgroup>
<optgroup label="Health">
<option value="needs_review">needs review</option>
<option value="stalled">stalled</option>
<option value="oldies">oldies</option>
</optgroup>
<optgroup label="Recently Changed">
<option value="1h">last 1 hour</option>
@@ -150,12 +164,11 @@ const _chartMode = view(html`<select class="ws-mode-select">
import * as Plot from "npm:@observablehq/plot";
// ── Filter workstreams by selected mode ───────────────────────────────────────
// "active" matches the DB status field directly.
// "accepted" = DB status "completed" (explicitly reviewed and signed off).
// "finished" = no open tasks remaining (derived from task counts).
// "blocked" = has ≥1 blocked task; "stalled" / "oldies" = activity-based.
// Lifecycle modes match stored canonical status values.
// Health modes are derived labels; they are not stored lifecycle states.
// Time modes filter by updated_at / created_at.
const _STATUS_MODES = new Set(["active"]);
const _STATUS_MODES = new Set(WORKSTREAM_STATUSES);
const _HEALTH_MODES = new Set(["needs_review", "stalled"]);
function _timeCutoff(mode) {
const now = new Date();
@@ -175,27 +188,11 @@ function _timeCutoff(mode) {
const _chartWsFiltered = (
_STATUS_MODES.has(_chartMode)
? wsAll.filter(w => w.status === _chartMode)
: _chartMode === "accepted"
? wsAll.filter(w => w.status === "completed")
: _chartMode === "finished"
? wsAll.filter(w => (w.todo ?? 0) + (w.in_progress ?? 0) + (w.blocked ?? 0) === 0)
: _chartMode === "blocked"
? wsAll.filter(w => (w.blocked ?? 0) > 0)
? wsAll.filter(w => normalizeWorkstreamStatus(w.status) === _chartMode)
: _chartMode === "needs_review"
? wsAll.filter(needsReviewWorkstream)
: _chartMode === "stalled"
? wsAll.filter(w => {
const staleAt = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
return new Date(w.updated_at) < staleAt
&& (w.done ?? 0) > 0
&& (w.todo ?? 0) + (w.in_progress ?? 0) + (w.blocked ?? 0) > 0;
})
: _chartMode === "oldies"
? wsAll.filter(w => {
const oldAt = new Date(Date.now() - 7 * 24 * 60 * 60 * 1000);
return new Date(w.created_at) < oldAt
&& (w.done ?? 0) === 0
&& (w.todo ?? 0) + (w.in_progress ?? 0) + (w.blocked ?? 0) > 0;
})
? wsAll.filter(isStalledWorkstream)
: (() => {
const since = _timeCutoff(_chartMode);
return wsAll.filter(w =>
@@ -215,9 +212,9 @@ const chartWs = [..._chartWsFiltered].sort((a, b) => {
});
// ── Status weight: bold for notable statuses in mixed-status modes ─────────────
// Color is NOT used for status — avoids green-on-green when completed bars fill the row.
const _isTimeBased = !_STATUS_MODES.has(_chartMode);
function _wsWeight(s) { return (s === "accepted" || s === "blocked" || s === "stalled") ? "bold" : "normal"; }
// Color is NOT used for status — avoids green-on-green when finished bars fill the row.
const _isTimeBased = !_STATUS_MODES.has(_chartMode) && !_HEALTH_MODES.has(_chartMode);
function _wsWeight(s) { return (isClosedWorkstream(s) || normalizeWorkstreamStatus(s) === "blocked") ? "bold" : "normal"; }
// ── y-axis: domain/repo label for first workstream per repository only ────────
const _yLabels = {};
@@ -251,10 +248,15 @@ function _wsTitle(d) {
// ── Render ────────────────────────────────────────────────────────────────────
if (chartWs.length === 0) {
const _emptyMsg = {
active: "No active workstreams.", accepted: "No accepted workstreams.",
finished: "No finished workstreams.", blocked: "No blocked workstreams.",
proposed: "No proposed workstreams.",
ready: "No ready workstreams.",
active: "No active workstreams.",
blocked: "No blocked workstreams.",
backlog: "No backlog workstreams.",
finished: "No finished workstreams.",
archived: "No archived workstreams.",
needs_review: "No ready workstreams need review.",
stalled: "No stalled workstreams — everything is moving.",
oldies: "No oldies — all older workstreams have at least one task done.",
"1h": "No workstreams changed in the last hour.",
"1d": "No workstreams changed in the last 24 hours.",
"7d": "No workstreams changed in the last 7 days.",

View File

@@ -4,6 +4,7 @@ title: Workstreams
```js
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {WORKSTREAM_STATUSES, isClosedWorkstream, normalizeWorkstreamStatus} from "./components/workplan-status.js";
```
```js
@@ -27,6 +28,7 @@ const wsState = (async function*() {
const repoMap = Object.fromEntries(repoList.map(r => [r.id, r]));
data = wsList.map(w => ({
...w,
status: normalizeWorkstreamStatus(w.status),
domain: repoMap[w.repo_id]?.domain_slug ?? topicMap[w.topic_id]?.domain_slug ?? "unknown",
topic_title: topicMap[w.topic_id]?.title ?? "—",
}));
@@ -50,7 +52,7 @@ const _ts = wsState.ts;
```js
// ── Workstream Health Index (WHI) ────────────────────────────────────────────
const _idToDomain = Object.fromEntries(data.map(w => [w.id, w.domain ?? "unknown"]));
const _completedIds = new Set(data.filter(w => w.status === "completed" || w.status === "archived").map(w => w.id));
const _closedIds = new Set(data.filter(w => isClosedWorkstream(w.status)).map(w => w.id));
const _openCount = openWs.length;
const _allEdges = openWs.flatMap(w => w.depends_on.map(d => ({from: w.id, to: d.workstream_id})));
const _totalEdges = _allEdges.length;
@@ -64,15 +66,15 @@ const _BR = _openCount > 0 ? openWs.filter(w => w.status === "blocked").length /
// Single-Point Risk — max inbound edges on one incomplete workstream
const _inbound = {};
for (const e of _allEdges) {
if (!_completedIds.has(e.to)) _inbound[e.to] = (_inbound[e.to] ?? 0) + 1;
if (!_closedIds.has(e.to)) _inbound[e.to] = (_inbound[e.to] ?? 0) + 1;
}
const _SPR = _openCount > 0
? (Object.keys(_inbound).length > 0 ? Math.max(...Object.values(_inbound)) : 0) / _openCount
: 0;
// Parallel Execution Potential — active workstreams with all deps completed
// Parallel Execution Potential — ready/active workstreams with all deps finished
const _PEP = _openCount > 0
? openWs.filter(w => w.status === "active" && w.depends_on.every(d => _completedIds.has(d.workstream_id))).length / _openCount
? openWs.filter(w => ["ready", "active"].includes(normalizeWorkstreamStatus(w.status)) && w.depends_on.every(d => _closedIds.has(d.workstream_id))).length / _openCount
: 0;
// Cross-Domain Dependency Ratio
@@ -117,9 +119,9 @@ const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unkno
const dd = oc > 0 ? te / oc : 0;
const br = oc > 0 ? nodes.filter(w => w.status === "blocked").length / oc : 0;
const pep = oc > 0 ? nodes.filter(w => {
if (w.status !== "active") return false;
if (!["ready", "active"].includes(normalizeWorkstreamStatus(w.status))) return false;
const intraDeps = w.depends_on.filter(d => (_idToDomain[d.workstream_id] ?? "unknown") === domain);
return intraDeps.every(d => _completedIds.has(d.workstream_id));
return intraDeps.every(d => _closedIds.has(d.workstream_id));
}).length / oc : 0;
const inb = {};
for (const e of edges) inb[e.to] = (inb[e.to] ?? 0) + 1;
@@ -222,7 +224,7 @@ const _domainsResp = await fetch(`${API}/domains/?status=active`).catch(() => nu
const DOMAINS = _domainsResp?.ok
? (await _domainsResp.json()).map(d => d.slug)
: ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
const STATUSES = ["active", "blocked", "completed", "archived"];
const STATUSES = WORKSTREAM_STATUSES;
// Create filter form without displaying — shown below the chart
const _filtersForm = Inputs.form(
@@ -357,7 +359,10 @@ if (wsWithDeps.length === 0) {
.dep-status { display: inline-block; font-size: 0.7rem; padding: 1px 6px; border-radius: 10px; margin-bottom: 0.5rem; text-transform: uppercase; }
.dep-status-active { background: #d4edda; color: #155724; }
.dep-status-blocked { background: #f8d7da; color: #721c24; }
.dep-status-completed { background: #cce5ff; color: #004085; }
.dep-status-proposed { background: #fef3c7; color: #92400e; }
.dep-status-ready { background: #e0f2fe; color: #075985; }
.dep-status-finished { background: #cce5ff; color: #004085; }
.dep-status-backlog { background: #f1f5f9; color: #64748b; }
.dep-row { font-size: 0.85rem; margin: 0.2rem 0 0 0.5rem; color: #444; }
.dep-on { color: #1a5276; }
.dep-block { color: #6e2f00; }