generated from coulomb/repo-seed
Implement State Hub v0.2: dependency graph, next-steps suggestions, design boundary
S0 — Design boundary formalised across all integration surfaces:
- TOOLS.md restructured with Design Boundary section, Sanctioned Write Tools,
and Bootstrap-Only Tools (create_workstream, create_task) with explicit note
- project_claude_md.template and railiance CLAUDE.md updated with boundary note
and get_next_steps() in session start protocol
- Global ~/.claude/CLAUDE.md updated accordingly
S1 — Workstream dependency graph:
- WorkstreamDependency model (directed edge, CASCADE on delete, unique pair constraint)
- Alembic migration 0b547c153153; script.py.mako added (was missing)
- REST API: POST/GET /workstreams/{id}/dependencies/, DELETE …/{dep_id} (hard delete)
- StateSummary open_workstreams enriched with depends_on/blocks lists
- MCP tools: create_dependency(), list_dependencies()
- Dashboard workstreams page: Dependencies section with relationship cards
- Seeded: custodian-agent-runtime → llm-shared-library + phase-0-operational-baseline
S2 — Suggesting Next Steps (sanctioned write use case #2):
- GET /state/next_steps derives suggestions from recently resolved decisions
(→ first open task in same workstream) and cleared dependencies
(→ first todo task in now-unblocked workstream)
- StateSummary.next_steps included on every summary call
- MCP tool: get_next_steps()
- Dashboard: "What's next?" card grid above Registered Projects
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -8,36 +8,40 @@ const POLL = 15_000;
|
||||
```
|
||||
|
||||
```js
|
||||
// Fetch workstreams + topics in parallel, join on topic_id → domain/title
|
||||
// Fetch workstreams + topics + summary (for dep graph) in parallel
|
||||
const wsState = (async function*() {
|
||||
while (true) {
|
||||
let data = [], ok = false;
|
||||
let data = [], openWs = [], ok = false;
|
||||
try {
|
||||
const [rw, rt] = await Promise.all([
|
||||
const [rw, rt, rs] = await Promise.all([
|
||||
fetch(`${API}/workstreams/`),
|
||||
fetch(`${API}/topics/`),
|
||||
fetch(`${API}/state/summary`),
|
||||
]);
|
||||
ok = rw.ok && rt.ok;
|
||||
ok = rw.ok && rt.ok && rs.ok;
|
||||
if (ok) {
|
||||
const [wsList, topicList] = await Promise.all([rw.json(), rt.json()]);
|
||||
const [wsList, topicList, summary] = await Promise.all([rw.json(), rt.json(), rs.json()]);
|
||||
const topicMap = Object.fromEntries(topicList.map(t => [t.id, t]));
|
||||
data = wsList.map(w => ({
|
||||
...w,
|
||||
domain: topicMap[w.topic_id]?.domain ?? "unknown",
|
||||
topic_title: topicMap[w.topic_id]?.title ?? "—",
|
||||
}));
|
||||
// open_workstreams from summary carry depends_on / blocks lists
|
||||
openWs = summary.open_workstreams ?? [];
|
||||
}
|
||||
} catch {}
|
||||
yield {data, ok, ts: new Date()};
|
||||
yield {data, openWs, ok, ts: new Date()};
|
||||
await new Promise(res => setTimeout(res, POLL));
|
||||
}
|
||||
})();
|
||||
```
|
||||
|
||||
```js
|
||||
const data = wsState.data ?? [];
|
||||
const _ok = wsState.ok ?? false;
|
||||
const _ts = wsState.ts;
|
||||
const data = wsState.data ?? [];
|
||||
const openWs = wsState.openWs ?? [];
|
||||
const _ok = wsState.ok ?? false;
|
||||
const _ts = wsState.ts;
|
||||
```
|
||||
|
||||
# Workstreams
|
||||
@@ -96,6 +100,47 @@ display(Plot.plot({
|
||||
}));
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
```js
|
||||
// Build dep cards from the enriched open_workstreams in the summary
|
||||
const wsWithDeps = openWs.filter(w =>
|
||||
(domainFilter === "(all)" || (data.find(d => d.id === w.id)?.domain ?? "unknown") === domainFilter) &&
|
||||
(statusFilter === "(all)" || w.status === statusFilter) &&
|
||||
(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 workstreams.</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-bar { font-size: 0.8rem; color: gray; text-align: right; margin-bottom: 0.5rem; }
|
||||
.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-completed { background: #cce5ff; color: #004085; }
|
||||
.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>
|
||||
|
||||
Reference in New Issue
Block a user