---
title: Workplans
---

```js
import {API, POLL_HEAVY, apiFetch, pollDelay, waitForVisible} from "./components/config.js";
import {WORKSTREAM_STATUSES, isClosedWorkstream, normalizeWorkstreamStatus} from "./components/workplan-status.js";
```

```js
// Fetch workplans + topics + dep edges in parallel; /state/deps replaces the
// heavier /state/summary which was only used here to extract dependency edges.
const wsState = (async function*() {
  let failures = 0;
  while (true) {
    let data = [], openWs = [], ok = false;
    try {
      const [rw, rt, rr, rd] = await Promise.all([
        apiFetch("/workplans/"),
        apiFetch("/topics/"),
        apiFetch("/repos/"),
        apiFetch("/state/deps"),
      ]);
      ok = rw.ok && rt.ok && rr.ok && rd.ok;
      if (ok) {
        const [wsList, topicList, repoList, depsList] = await Promise.all([rw.json(), rt.json(), rr.json(), rd.json()]);
        const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
        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 ?? "—",
        }));
        openWs = depsList;
      }
    } catch {}
    failures = ok ? 0 : failures + 1;
    yield {data, openWs, ok, ts: new Date()};
    await waitForVisible(pollDelay({ok, base: POLL_HEAVY, failures}));
  }
})();
```

```js
const data   = wsState.data   ?? [];
const openWs = wsState.openWs ?? [];
const _ok    = wsState.ok     ?? false;
const _ts    = wsState.ts;
```

```js
// ── Workplan Health Index (WHI) ────────────────────────────────────────────
const _idToDomain   = Object.fromEntries(data.map(w => [w.id, w.domain ?? "unknown"]));
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;

// Dependency Density
const _DD = _openCount > 0 ? _totalEdges / _openCount : 0;

// Blocked Ratio
const _BR = _openCount > 0 ? openWs.filter(w => w.status === "blocked").length / _openCount : 0;

// Single-Point Risk — max inbound edges on one incomplete workplan
const _inbound = {};
for (const e of _allEdges) {
  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 — ready/active workplans with all deps finished
const _PEP = _openCount > 0
  ? 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
const _crossEdges = _allEdges.filter(e => (_idToDomain[e.from] ?? "?") !== (_idToDomain[e.to] ?? "?")).length;
const _CDDR = _totalEdges > 0 ? _crossEdges / _totalEdges : 0;

// Cycle Presence Indicator — DFS with visited/inStack colouring
function _detectCycle(nodes, edges) {
  const adj = Object.fromEntries(nodes.map(n => [n.id, []]));
  for (const e of edges) { if (adj[e.from] !== undefined) adj[e.from].push(e.to); }
  const visited = new Set(), inStack = new Set();
  function dfs(id) {
    if (inStack.has(id)) return true;
    if (visited.has(id)) return false;
    visited.add(id); inStack.add(id);
    for (const nx of (adj[id] ?? [])) { if (dfs(nx)) return true; }
    inStack.delete(id);
    return false;
  }
  for (const n of nodes) { if (!visited.has(n.id) && dfs(n.id)) return 1; }
  return 0;
}
const _CPI = _detectCycle(openWs, _allEdges);

// WHI aggregation — DD normalised at DD_critical = 1.0, CPI halves the score
const _DDnorm = Math.min(1, _DD / 1.0);
let _WHI = 0.30*(1 - _DDnorm) + 0.25*(1 - _BR) + 0.15*(1 - _SPR) + 0.20*_PEP + 0.10*(1 - _CDDR);
if (_CPI === 1) _WHI *= 0.5;
_WHI = Math.max(0, Math.min(1, _WHI));

// Per-domain breakdown — intra-domain edges only (measures domain autonomy)
const _domainBreakdown = [...new Set(openWs.map(w => _idToDomain[w.id] ?? "unknown"))].sort().map(domain => {
  const nodes = openWs.filter(w => (_idToDomain[w.id] ?? "unknown") === domain);
  const edges = nodes.flatMap(w =>
    w.depends_on
      .filter(d => (_idToDomain[d.workstream_id] ?? "unknown") === domain)
      .map(d => ({from: w.id, to: d.workstream_id}))
  );
  const oc = nodes.length;
  if (oc === 0) return null;
  const te  = edges.length;
  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 (!["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 => _closedIds.has(d.workstream_id));
  }).length / oc : 0;
  const inb = {};
  for (const e of edges) inb[e.to] = (inb[e.to] ?? 0) + 1;
  const spr = oc > 0 ? (Object.keys(inb).length > 0 ? Math.max(...Object.values(inb)) : 0) / oc : 0;
  const cpi = _detectCycle(nodes, edges);
  const ddN = Math.min(1, dd / 1.0);
  let whi   = 0.30*(1 - ddN) + 0.25*(1 - br) + 0.15*(1 - spr) + 0.20*pep + 0.10; // CDDR=0 within domain
  if (cpi === 1) whi *= 0.5;
  return {domain, whi: Math.max(0, Math.min(1, whi)), br, pep, cpi, openCount: oc};
}).filter(Boolean);
```

# Workplans

```js
import {injectTocTop}    from "./components/toc-sidebar.js";
import {withDocHelp}     from "./components/doc-overlay.js";
import                        "./components/help-tip.js";
import {openEntityModal, buildEntityTable} from "./components/entity-modal.js";
import {statusControl} from "./components/status-control.js";

// ── Live indicator ────────────────────────────────────────────────────────────
const _liveEl = html`<div class="live-indicator">
  <span style="color:${_ok ? 'var(--theme-foreground-focus)' : 'red'}">●</span>
  ${_ok
    ? `Live · updated ${_ts?.toLocaleTimeString()}`
    : html`<span style="color:red">Offline — run: <code>make api</code></span>`}
</div>`;
withDocHelp(_liveEl, "/docs/live-data");

// ── WHI card ──────────────────────────────────────────────────────────────────
function _whiColor(v)  { return v >= 0.75 ? "#16a34a" : v >= 0.50 ? "#d97706" : "#dc2626"; }
function _whiLabel(v)  { return v >= 0.75 ? "Healthy" : v >= 0.50 ? "Optimizable" : "Critical"; }
function _warnLevel(name, val) {
  if (name === "PEP")  return val < 0.30 ? 2 : val < 0.60 ? 1 : 0;
  if (name === "DD")   return val > 1.0  ? 2 : val > 0.50 ? 1 : 0;
  if (name === "BR")   return val > 0.40 ? 2 : val > 0.20 ? 1 : 0;
  if (name === "SPR")  return val > 0.40 ? 2 : val > 0.25 ? 1 : 0;
  if (name === "CDDR") return val > 0.40 ? 1 : 0;
  return 0;
}
function _warnColor(lv) { return lv === 2 ? "#dc2626" : lv === 1 ? "#d97706" : "var(--theme-foreground-muted, #666)"; }

const _whiMetrics = [
  {name: "DD",   val: _DD,   fmt: v => v.toFixed(2),           label: "Dependency Density",        desc: "Average number of dependencies per open workplan; high values indicate a tightly coupled graph that is hard to parallelise."},
  {name: "BR",   val: _BR,   fmt: v => (v*100).toFixed(0)+"%", label: "Blocked Ratio",             desc: "Share of open workplans currently in a blocked state; directly reduces the work that can proceed right now."},
  {name: "SPR",  val: _SPR,  fmt: v => (v*100).toFixed(0)+"%", label: "Single-Point Risk",         desc: "Share of workplans depended on by others but with no incoming dependencies themselves; losing one stalls everything downstream."},
  {name: "PEP",  val: _PEP,  fmt: v => (v*100).toFixed(0)+"%", label: "Parallel Execution Potential", desc: "Share of open workplans with zero blocking dependencies that could start or continue immediately."},
  {name: "CDDR", val: _CDDR, fmt: v => (v*100).toFixed(0)+"%", label: "Cross-Domain Dependency Ratio", desc: "Share of dependency edges that cross domain boundaries; high values mean progress in one domain is gated on another team or project."},
];

const _whiBox = html`<div class="kpi-infobox whi-box">
  <div class="kpi-infobox-title">Workplan Health</div>
  ${_openCount === 0
    ? html`<div class="kpi-row"><span class="kpi-muted">No active workplans</span></div>`
    : html`
      <div class="whi-score-row">
        <span class="whi-value" style="color:${_whiColor(_WHI)}">${(_WHI*100).toFixed(0)}<span class="whi-pct">%</span></span>
        <span class="whi-label" style="color:${_whiColor(_WHI)}">${_whiLabel(_WHI)}</span>
      </div>
      ${_CPI === 1 ? html`<div class="whi-cycle-alert">⚠ Cycle detected — deadlock</div>` : ""}
      <div class="whi-metrics">
        ${_whiMetrics.map(m => {
          const lv = _warnLevel(m.name, m.val);
          return html`<div class="whi-metric-row">
            <help-tip class="whi-metric-name" style="color:${_warnColor(lv)}" label="${m.label}" description="${m.desc}" doc="/docs/workstream-health-index">${m.name}</help-tip>
            <span class="whi-metric-val" style="color:${_warnColor(lv)}">${m.fmt(m.val)}</span>
          </div>`;
        })}
      </div>
      ${_domainBreakdown.length > 1 ? html`
        <div class="whi-domains">
          <div class="whi-domain-header">by domain</div>
          ${_domainBreakdown.map(d => html`<div class="whi-domain-row">
            <span class="whi-domain-dot" style="background:${_whiColor(d.whi)}"></span>
            <help-tip class="whi-domain-name"
              label="${d.domain.replaceAll('_', ' ')}"
              description="Domain-scoped WHI (intra-domain edges only). Open: ${d.openCount} · Blocked: ${(d.br*100).toFixed(0)}% · Runnable: ${(d.pep*100).toFixed(0)}%"
              doc="/docs/workstream-health-index">${d.domain}</help-tip>
            <span class="whi-domain-score" style="color:${_whiColor(d.whi)}">${(d.whi*100).toFixed(0)}%</span>
            ${d.cpi === 1 ? html`<help-tip style="color:#d97706;font-size:0.7rem" label="Dependency Cycle" description="A circular dependency exists within this domain — workplans are waiting on each other and cannot all proceed." doc="/docs/workstream-health-index">⚠</help-tip>` : ""}
          </div>`)}
        </div>` : ""}
    `}
</div>`;
withDocHelp(_whiBox, "/docs/workstream-health-index");

// ── Inject into TOC sidebar: WHI first (lower), live last (top) ───────────────
injectTocTop("whi-kpi-box", _whiBox);
injectTocTop("live-indicator", _liveEl);

const _h1 = document.querySelector("#observablehq-main h1");
if (_h1) { _h1.style.position = "relative"; withDocHelp(_h1, "/docs/workstreams"); }
display(html`<p class="dim" style="margin-top:-0.25rem"><a href="./workplan-queue">Workplan queue</a></p>`);
```

```js
import {MultiSelect} from "./components/multiselect.js";

// Load domain slugs from API (dynamic — works with new domains after v0.5)
const _domainsResp = await fetch(`${API}/domains/?status=active`).catch(() => null);
const DOMAINS = _domainsResp?.ok
  ? (await _domainsResp.json()).map(d => d.slug)
  : ["custodian", "railiance", "markitect", "coulomb_social", "personhood", "foerster_capabilities"];
const STATUSES = WORKSTREAM_STATUSES;

// Create filter form without displaying — shown below the chart
const _filtersForm = Inputs.form(
  {
    domain: MultiSelect(DOMAINS,  {label: "Domain",  placeholder: "All domains"}),
    status: MultiSelect(STATUSES, {label: "Status",  placeholder: "All statuses"}),
    owner:  Inputs.text({placeholder: "Owner…", style: "width:120px"}),
  },
  {
    template: ({domain, status, owner}) => html`<div class="filter-bar">
      ${domain}${status}
      <div class="filter-text-input">${owner}</div>
    </div>`,
  }
);
```

```js
const filters = Generators.input(_filtersForm);
```

```js
// Empty array = no filter applied (show all)
const filtered = data.filter(w =>
  (filters.domain.length === 0 || filters.domain.includes(w.domain)) &&
  (filters.status.length === 0 || filters.status.includes(w.status)) &&
  (!filters.owner || (w.owner ?? "").toLowerCase().includes(filters.owner.toLowerCase()))
);
```

## Status Distribution

```js
import * as Plot from "npm:@observablehq/plot";

const byStatus = Object.entries(
  filtered.reduce((acc, w) => { acc[w.status] = (acc[w.status] ?? 0) + 1; return acc; }, {})
).map(([status, count]) => ({status, count}));

display(Plot.plot({
  marks: [
    Plot.barX(byStatus, {y: "status", x: "count", fill: "status", tip: true}),
    Plot.ruleX([0]),
  ],
  marginLeft: 80,
  width: 500,
}));
```

## All Workplans

```js
display(_filtersForm);

{
  // Enrich each workplan with tasks/deps data from open_workplans summary
  const _openWsMap = Object.fromEntries(openWs.map(w => [w.id, w]));
  const _wsTable = buildEntityTable(
    filtered,
    [
      {label: "Title",   key: "title",   cls: "et-title-col et-title-cell",
       render: w => w.title},
      {label: "Domain",  key: "domain"},
      {label: "Status",  render: w => statusControl({entity: w, type: "workplan", statuses: WORKSTREAM_STATUSES})},
      {label: "Owner",   render: w => w.owner ?? "—"},
      {label: "Due",     render: w => w.due_date ?? "—"},
      {label: "Updated", render: w => new Date(w.updated_at).toLocaleDateString()},
    ],
    w => openEntityModal({...w, ..._openWsMap[w.id]}, "workplan"),
  );
  display(_wsTable);
}
```

## Dependencies

```js
// Build dep cards from the enriched open_workplans in the summary
const wsWithDeps = openWs.filter(w => {
  const domain = data.find(d => d.id === w.id)?.domain ?? "unknown";
  return (filters.domain.length === 0 || filters.domain.includes(domain)) &&
    (filters.status.length === 0 || filters.status.includes(w.status)) &&
    (w.depends_on.length > 0 || w.blocks.length > 0);
});

if (wsWithDeps.length === 0) {
  display(html`<p class="dim">No dependency edges recorded for the current filter. Use <code>create_dependency()</code> via the MCP server to link workplans.</p>`);
} else {
  display(html`<div class="dep-grid">${wsWithDeps.map(w => {
    const depRows = w.depends_on.map(d =>
      html`<div class="dep-row dep-on">↳ depends on <strong>${d.workstream_title}</strong>${d.description ? html` <span class="dep-desc">— ${d.description}</span>` : ""}</div>`
    );
    const blockRows = w.blocks.map(d =>
      html`<div class="dep-row dep-block">⊳ blocks <strong>${d.workstream_title}</strong>${d.description ? html` <span class="dep-desc">— ${d.description}</span>` : ""}</div>`
    );
    return html`<div class="dep-card">
      <div class="dep-title">${w.title}</div>
      <div class="dep-status dep-status-${w.status}">${w.status}</div>
      ${depRows}${blockRows}
    </div>`;
  })}</div>`);
}
```

<style>
.live-indicator { font-size: 0.8rem; color: gray; position: relative; padding: 0.55rem 1.8rem 0.55rem 0.7rem; margin-bottom: 0.75rem; }

/* ── KPI infobox base (shared) ───────────────────────────────────────────── */
.kpi-row { display: flex; justify-content: space-between; align-items: center; gap: 1rem; padding: 0.3rem 0; }
.kpi-muted { color: var(--theme-foreground-faint, #aaa); font-style: italic; font-size: 0.8rem; }

/* ── WHI card ────────────────────────────────────────────────────────────── */
.whi-score-row { display: flex; align-items: baseline; gap: 0.4rem; margin: 0.35rem 0 0.5rem; }
.whi-value { font-size: 1.5rem; font-weight: 700; font-variant-numeric: tabular-nums; line-height: 1; }
.whi-pct { font-size: 1rem; font-weight: 600; }
.whi-label { font-size: 0.72rem; font-weight: 600; text-transform: uppercase; letter-spacing: 0.06em; align-self: center; }
.whi-cycle-alert { background: #fef2f2; color: #dc2626; border-radius: 4px; padding: 0.2rem 0.45rem; font-size: 0.72rem; font-weight: 600; margin-bottom: 0.4rem; }
.whi-metrics { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; margin-bottom: 0.35rem; }
.whi-metric-row { display: flex; justify-content: space-between; padding: 0.16rem 0; }
.whi-metric-name { font-family: monospace; font-size: 0.72rem; }
.whi-metric-val { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.78rem; }
.whi-domains { border-top: 1px solid var(--theme-foreground-faint, #eee); padding-top: 0.35rem; }
.whi-domain-header { font-size: 0.65rem; font-weight: 700; text-transform: uppercase; letter-spacing: 0.08em; color: var(--theme-foreground-faint, #aaa); margin-bottom: 0.2rem; }
.whi-domain-row { display: flex; align-items: center; gap: 0.3rem; padding: 0.1rem 0; }
.whi-domain-dot { display: inline-block; width: 7px; height: 7px; border-radius: 50%; flex-shrink: 0; }
.whi-domain-name { flex: 1; font-size: 0.75rem; color: var(--theme-foreground-muted, #666); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.whi-domain-score { font-variant-numeric: tabular-nums; font-weight: 600; font-size: 0.75rem; }
.dim { color: gray; font-style: italic; }
.dep-grid { display: flex; flex-direction: column; gap: 0.75rem; }
.dep-card { border: 1px solid #e0e0e0; border-radius: 6px; padding: 0.75rem 1rem; background: var(--theme-background-alt, #fafafa); }
.dep-title { font-weight: 600; margin-bottom: 0.25rem; }
.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-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; }
.dep-desc  { color: #888; font-size: 0.8rem; }
</style>
