@thatix.io/context-first-agents-cli 0.2.2 → 0.3.0

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/README.md CHANGED
@@ -150,6 +150,7 @@ commands `/observe` and `/metrics` are also included.
150
150
  | `update:commands` | Overwrite the command templates |
151
151
  | `doctor` | Validate manifest, hints, indexes, and installed flow commands |
152
152
  | `status` | Show repos, risk signals, and active sessions |
153
+ | `dashboard` | Serve a local web dashboard of sessions and running agents |
153
154
 
154
155
  `--lang en|es|pt-BR` selects the language of the installed `.md` commands.
155
156
 
@@ -157,6 +158,20 @@ commands `/observe` and `/metrics` are also included.
157
158
  `products/{collect,refine,spec,check}`, **`orchestrate`** (+ `agents/`),
158
159
  `engineer/{start,plan,work,pre-pr,pr}` (escape hatches), `quality/{observe,metrics}`.
159
160
 
161
+ ## Dashboard
162
+
163
+ `/orchestrate` writes machine-readable state per session (`state.json` +
164
+ `workers/<id>.json`, format in `SESSION-STATE.md`). Serve a live view of it:
165
+
166
+ ```bash
167
+ context-agents dashboard # → http://localhost:4517
168
+ ```
169
+
170
+ The dashboard lists every session (issue), its complexity, the agent graph by wave, and
171
+ each agent's status (`pending → running → done/blocked`) with its current step and
172
+ verdict — auto-refreshing every 2s. Sessions created before state-writing still render via
173
+ an `execution-plan.md` fallback (shown without live status).
174
+
160
175
  ## The `.md` orchestration layer (the engine)
161
176
 
162
177
  Installed into `.claude/commands/`:
@@ -0,0 +1,5 @@
1
+ interface DashboardOpts {
2
+ port?: string;
3
+ }
4
+ export declare function dashboardCommand(opts: DashboardOpts): Promise<void>;
5
+ export {};
@@ -0,0 +1,55 @@
1
+ import http from 'node:http';
2
+ import path from 'node:path';
3
+ import fs from 'node:fs/promises';
4
+ import chalk from 'chalk';
5
+ import { loadManifest, pathExists } from '../utils/config.js';
6
+ import { listSessions, readSession } from '../core/sessions.js';
7
+ import { templatesDir } from '../utils/paths.js';
8
+ export async function dashboardCommand(opts) {
9
+ const cwd = process.cwd();
10
+ const port = Number(opts.port ?? 4517);
11
+ if (!(await pathExists(path.join(cwd, 'context-manifest.json')))) {
12
+ console.log(chalk.yellow('\nNo context-manifest.json here — run inside an orchestrator.\n'));
13
+ process.exitCode = 1;
14
+ return;
15
+ }
16
+ const htmlPath = path.join(templatesDir(), 'dashboard', 'index.html');
17
+ const html = (await pathExists(htmlPath)) ? await fs.readFile(htmlPath, 'utf-8') : '<h1>dashboard asset missing</h1>';
18
+ const server = http.createServer(async (req, res) => {
19
+ try {
20
+ const url = new URL(req.url ?? '/', `http://localhost:${port}`);
21
+ if (url.pathname === '/' || url.pathname === '/index.html') {
22
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
23
+ res.end(html);
24
+ return;
25
+ }
26
+ if (url.pathname === '/api/sessions') {
27
+ const manifest = await loadManifest(cwd);
28
+ const ids = await listSessions(cwd);
29
+ const sessions = await Promise.all(ids.map((id) => readSession(cwd, id)));
30
+ res.writeHead(200, { 'Content-Type': 'application/json' });
31
+ res.end(JSON.stringify({ project: manifest?.project ?? 'orchestrator', sessions }));
32
+ return;
33
+ }
34
+ if (url.pathname === '/api/session') {
35
+ const id = url.searchParams.get('id') ?? '';
36
+ const session = await readSession(cwd, id);
37
+ res.writeHead(200, { 'Content-Type': 'application/json' });
38
+ res.end(JSON.stringify(session));
39
+ return;
40
+ }
41
+ res.writeHead(404, { 'Content-Type': 'application/json' });
42
+ res.end(JSON.stringify({ error: 'not found' }));
43
+ }
44
+ catch (err) {
45
+ res.writeHead(500, { 'Content-Type': 'application/json' });
46
+ res.end(JSON.stringify({ error: String(err) }));
47
+ }
48
+ });
49
+ server.listen(port, () => {
50
+ console.log(chalk.bold('\nContext-First Agents — dashboard\n'));
51
+ console.log(chalk.green(` ▶ http://localhost:${port}`));
52
+ console.log(chalk.gray(` Reading sessions from ${path.join(cwd, '.sessions')}`));
53
+ console.log(chalk.gray(' Press Ctrl+C to stop.\n'));
54
+ });
55
+ }
@@ -0,0 +1,29 @@
1
+ export interface WorkerState {
2
+ id: string;
3
+ archetype?: string;
4
+ repository?: string | null;
5
+ objective?: string;
6
+ dependsOn?: string[];
7
+ status: 'pending' | 'running' | 'done' | 'blocked' | 'unknown';
8
+ currentStep?: string | null;
9
+ startedAt?: string | null;
10
+ finishedAt?: string | null;
11
+ verdict?: string | null;
12
+ }
13
+ export interface SessionState {
14
+ issueId: string;
15
+ title?: string;
16
+ complexity?: string;
17
+ status: 'planned' | 'running' | 'blocked' | 'done' | 'unknown';
18
+ createdAt?: string;
19
+ updatedAt?: string;
20
+ repos?: string[];
21
+ waves?: string[][];
22
+ workers: WorkerState[];
23
+ /** true when built from execution-plan.md fallback (no state.json yet). */
24
+ fromPlanFallback?: boolean;
25
+ }
26
+ /** List session directories (skips _backlog, dotfiles). */
27
+ export declare function listSessions(orchestratorDir: string): Promise<string[]>;
28
+ /** Read one session's full state, falling back to execution-plan.md parsing. */
29
+ export declare function readSession(orchestratorDir: string, issueId: string): Promise<SessionState>;
@@ -0,0 +1,105 @@
1
+ import fs from 'node:fs/promises';
2
+ import path from 'node:path';
3
+ import { pathExists } from '../utils/config.js';
4
+ async function readJson(file) {
5
+ try {
6
+ return JSON.parse(await fs.readFile(file, 'utf-8'));
7
+ }
8
+ catch {
9
+ return null;
10
+ }
11
+ }
12
+ /** List session directories (skips _backlog, dotfiles). */
13
+ export async function listSessions(orchestratorDir) {
14
+ const dir = path.join(orchestratorDir, '.sessions');
15
+ if (!(await pathExists(dir)))
16
+ return [];
17
+ const entries = await fs.readdir(dir, { withFileTypes: true });
18
+ return entries
19
+ .filter((e) => e.isDirectory() && !e.name.startsWith('_') && !e.name.startsWith('.'))
20
+ .map((e) => e.name)
21
+ .sort();
22
+ }
23
+ /** Read one session's full state, falling back to execution-plan.md parsing. */
24
+ export async function readSession(orchestratorDir, issueId) {
25
+ const sdir = path.join(orchestratorDir, '.sessions', issueId);
26
+ const state = await readJson(path.join(sdir, 'state.json'));
27
+ const workers = [];
28
+ const workersDir = path.join(sdir, 'workers');
29
+ if (await pathExists(workersDir)) {
30
+ const files = (await fs.readdir(workersDir)).filter((f) => f.endsWith('.json'));
31
+ for (const f of files.sort()) {
32
+ const w = await readJson(path.join(workersDir, f));
33
+ if (w && w.id)
34
+ workers.push({ ...w, status: w.status ?? 'unknown' });
35
+ }
36
+ }
37
+ if (state || workers.length) {
38
+ return {
39
+ issueId,
40
+ title: state?.title,
41
+ complexity: state?.complexity,
42
+ status: state?.status ?? deriveStatus(workers),
43
+ createdAt: state?.createdAt,
44
+ updatedAt: state?.updatedAt,
45
+ repos: state?.repos,
46
+ waves: state?.waves,
47
+ workers: workers.length ? workers : wavesToWorkers(state?.waves),
48
+ };
49
+ }
50
+ // Fallback: parse execution-plan.md so old sessions still render.
51
+ return parsePlanFallback(sdir, issueId);
52
+ }
53
+ function deriveStatus(workers) {
54
+ if (!workers.length)
55
+ return 'unknown';
56
+ if (workers.some((w) => w.status === 'blocked'))
57
+ return 'blocked';
58
+ if (workers.every((w) => w.status === 'done'))
59
+ return 'done';
60
+ if (workers.some((w) => w.status === 'running'))
61
+ return 'running';
62
+ return 'planned';
63
+ }
64
+ function wavesToWorkers(waves) {
65
+ if (!waves)
66
+ return [];
67
+ return waves.flat().map((id) => ({ id, status: 'unknown' }));
68
+ }
69
+ /** Best-effort extraction of the DAG table from execution-plan.md. */
70
+ async function parsePlanFallback(sdir, issueId) {
71
+ const planPath = path.join(sdir, 'execution-plan.md');
72
+ const empty = { issueId, status: 'unknown', workers: [], fromPlanFallback: true };
73
+ if (!(await pathExists(planPath)))
74
+ return empty;
75
+ const text = await fs.readFile(planPath, 'utf-8');
76
+ const title = /Execution Plan[^(]*\(([^)]+)\)/.exec(text)?.[1];
77
+ const complexity = /\*\*(simple|medium|complex)\*\*/i.exec(text)?.[1]?.toLowerCase();
78
+ // Parse DAG table rows: | id | archetype | repo | objective | dependsOn |
79
+ const workers = [];
80
+ for (const line of text.split('\n')) {
81
+ const m = /^\|([^|]*)\|([^|]*)\|([^|]*)\|([^|]*)\|([^|]*)\|/.exec(line);
82
+ if (!m)
83
+ continue;
84
+ const id = m[1].trim();
85
+ if (!/^W\d+$/.test(id))
86
+ continue;
87
+ const [archetype, repo, objective, deps] = [m[2].trim(), m[3].trim(), m[4].trim(), m[5].trim()];
88
+ workers.push({
89
+ id,
90
+ archetype,
91
+ repository: repo === '—' ? null : repo,
92
+ objective,
93
+ dependsOn: deps && deps !== '—' ? deps.split(',').map((d) => d.trim()) : [],
94
+ status: 'unknown',
95
+ });
96
+ }
97
+ return {
98
+ issueId,
99
+ title,
100
+ complexity,
101
+ status: 'unknown',
102
+ workers,
103
+ fromPlanFallback: true,
104
+ };
105
+ }
package/dist/index.js CHANGED
@@ -6,6 +6,7 @@ import { addRepoCommand } from './commands/add-repo.js';
6
6
  import { updateCommandsCommand } from './commands/update-commands.js';
7
7
  import { doctorCommand } from './commands/doctor.js';
8
8
  import { statusCommand } from './commands/status.js';
9
+ import { dashboardCommand } from './commands/dashboard.js';
9
10
  const program = new Command();
10
11
  program
11
12
  .name('context-agents')
@@ -43,4 +44,9 @@ program
43
44
  .command('status')
44
45
  .description('Show orchestrator repos, risk signals, and active sessions')
45
46
  .action(statusCommand);
47
+ program
48
+ .command('dashboard')
49
+ .description('Serve a local web dashboard of sessions and running agents')
50
+ .option('-p, --port <port>', 'Port to listen on', '4517')
51
+ .action(dashboardCommand);
46
52
  program.parseAsync(process.argv);
@@ -126,15 +126,31 @@ Rules:
126
126
 
127
127
  After this, each agent's `writeBoundary` (`.sessions/<ISSUE-ID>/<repo>/`) actually exists.
128
128
 
129
- ## Step 6Spawn ephemeral agents (Task tool)
129
+ ## Step 5cWrite the initial state (for the dashboard)
130
+
131
+ Write machine-readable state into `.sessions/<ISSUE-ID>/` (format in the orchestrator's
132
+ `SESSION-STATE.md`). This feeds `context-agents dashboard`.
133
+
134
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
135
+ (`waves` = the waves from Step 4).
136
+ 2. `workers/<id>.json` for each node: `{ id, archetype, repository, objective, dependsOn,
137
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
130
138
 
131
- Execute the DAG respecting `dependsOn`:
139
+ Keep writes small and frequent — the dashboard polls these files.
140
+
141
+ ## Step 6 — Spawn ephemeral agents (Task tool)
132
142
 
133
- 1. **Parallel wave**: spawn all nodes whose dependencies are satisfied **in a single
134
- message with multiple Task calls** so they run concurrently. Give each subagent ONLY
135
- its compiled contract + objective never the whole conversation.
136
- 2. Wait for a wave to finish. Collect each subagent's structured return.
137
- 3. **Next wave**: spawn nodes whose dependencies are now satisfied. Repeat until done.
143
+ Execute the DAG respecting `dependsOn`. **On each transition, update the state files**:
144
+
145
+ 1. **When a wave starts**: for each node in the wave, set `workers/<id>.json` to
146
+ `status:"running"`, `startedAt`, and a short `currentStep`; set `state.json.status="running"`.
147
+ 2. **Parallel wave**: spawn all nodes in the wave **in a single message with multiple Task
148
+ calls** so they run concurrently. Give each subagent ONLY its compiled contract +
149
+ objective — never the whole conversation.
150
+ 3. **On return**: set each `workers/<id>.json` to `status:"done"` (or `"blocked"`),
151
+ `finishedAt`, and `verdict` if any (reviewer/tester/integrator).
152
+ 4. **Next wave**: spawn nodes whose dependencies are now satisfied. Repeat until done.
153
+ 5. **At the end**: `state.json.status="done"` (or `"blocked"` if any blocked).
138
154
 
139
155
  Use the archetype prompt templates in `agents/` (implementer, reviewer, integrator,
140
156
  tester, …) as the system framing for each subagent, filled with the node's objective,
@@ -127,15 +127,31 @@ Reglas:
127
127
 
128
128
  Tras esto, el `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de verdad.
129
129
 
130
- ## Paso 6Spawnear los agentes efímeros (Task tool)
130
+ ## Paso 5cEscribir el estado inicial (para el dashboard)
131
+
132
+ Escribe el estado legible por máquina en `.sessions/<ISSUE-ID>/` (formato en el
133
+ `SESSION-STATE.md` del orquestador). Esto alimenta `context-agents dashboard`.
134
+
135
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
136
+ (`waves` = las olas del Paso 4).
137
+ 2. `workers/<id>.json` para cada nodo: `{ id, archetype, repository, objective, dependsOn,
138
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
131
139
 
132
- Ejecuta el DAG respetando `dependsOn`:
140
+ Mantén escrituras pequeñas y frecuentes — el dashboard hace polling de estos archivos.
141
+
142
+ ## Paso 6 — Spawnear los agentes efímeros (Task tool)
133
143
 
134
- 1. **Ola paralela**: spawnea todos los nodos con dependencias satisfechas **en un único
135
- mensaje con múltiples llamadas Task**, para que corran concurrentemente. Dale a cada
136
- subagente SÓLO su contrato compilado + objetivo nunca la conversación entera.
137
- 2. Espera a que la ola termine. Recolecta el retorno estructurado de cada subagente.
138
- 3. **Siguiente ola**: spawnea los nodos cuyas dependencias ya están satisfechas. Repite.
144
+ Ejecuta el DAG respetando `dependsOn`. **En cada transición, actualiza los archivos de estado**:
145
+
146
+ 1. **Al iniciar una ola**: para cada nodo de la ola, marca `workers/<id>.json` con
147
+ `status:"running"`, `startedAt`, y un `currentStep` corto; marca `state.json.status="running"`.
148
+ 2. **Ola paralela**: spawnea todos los nodos de la ola **en un único mensaje con múltiples
149
+ llamadas Task**, para que corran concurrentemente. Dale a cada subagente SÓLO su
150
+ contrato compilado + objetivo — nunca la conversación entera.
151
+ 3. **Al retornar**: marca cada `workers/<id>.json` con `status:"done"` (o `"blocked"`),
152
+ `finishedAt`, y `verdict` si hay (reviewer/tester/integrator).
153
+ 4. **Siguiente ola**: spawnea los nodos cuyas dependencias ya están satisfechas. Repite.
154
+ 5. **Al final**: `state.json.status="done"` (o `"blocked"` si alguno bloqueó).
139
155
 
140
156
  Usa las plantillas de arquetipo en `agents/` (implementer, reviewer, integrator, tester…)
141
157
  como marco de cada subagente, rellenadas con objetivo, repositorio y contrato.
@@ -127,15 +127,31 @@ Regras:
127
127
 
128
128
  Depois disso, o `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de fato.
129
129
 
130
- ## Passo 6Spawnar os agentes efêmeros (Task tool)
130
+ ## Passo 5cGravar o estado inicial (para o dashboard)
131
+
132
+ Grave o estado legível por máquina em `.sessions/<ISSUE-ID>/` (formato em
133
+ `SESSION-STATE.md` do orquestrador). Isto alimenta o `context-agents dashboard`.
134
+
135
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
136
+ (`waves` = as ondas do Passo 4).
137
+ 2. `workers/<id>.json` para cada nó: `{ id, archetype, repository, objective, dependsOn,
138
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
131
139
 
132
- Execute o DAG respeitando `dependsOn`:
140
+ Mantenha escritas pequenas e frequentes — o dashboard faz polling desses arquivos.
141
+
142
+ ## Passo 6 — Spawnar os agentes efêmeros (Task tool)
133
143
 
134
- 1. **Onda paralela**: spawne todos os nós com dependências satisfeitas **numa única
135
- mensagem com múltiplas chamadas Task**, para rodarem concorrentemente. Dê a cada
136
- subagente APENAS o contrato compilado + objetivo nunca a conversa inteira.
137
- 2. Aguarde a onda terminar. Colete o retorno estruturado de cada subagente.
138
- 3. **Próxima onda**: spawne os nós cujas dependências agora estão satisfeitas. Repita.
144
+ Execute o DAG respeitando `dependsOn`. **A cada transição, atualize os arquivos de estado**:
145
+
146
+ 1. **Ao iniciar uma onda**: para cada da onda, marque `workers/<id>.json` com
147
+ `status:"running"`, `startedAt`, e um `currentStep` curto; marque `state.json.status="running"`.
148
+ 2. **Onda paralela**: spawne todos os nós da onda **numa única mensagem com múltiplas
149
+ chamadas Task**, para rodarem concorrentemente. Dê a cada subagente APENAS o contrato
150
+ compilado + objetivo — nunca a conversa inteira.
151
+ 3. **Ao retornar**: marque cada `workers/<id>.json` com `status:"done"` (ou `"blocked"`),
152
+ `finishedAt`, e `verdict` se houver (reviewer/tester/integrator).
153
+ 4. **Próxima onda**: spawne os nós cujas dependências agora estão satisfeitas. Repita.
154
+ 5. **Ao final**: `state.json.status="done"` (ou `"blocked"` se algum bloqueou).
139
155
 
140
156
  Use os templates de arquétipo em `agents/` (implementer, reviewer, integrator, tester…)
141
157
  como enquadramento de cada subagente, preenchidos com objetivo, repositório e contrato.
@@ -0,0 +1,123 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Context-First Agents — Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #e6edf3;
10
+ --muted: #8b949e; --accent: #58a6ff;
11
+ --pending: #6e7681; --running: #d29922; --done: #3fb950; --blocked: #f85149;
12
+ }
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; background: var(--bg); color: var(--text);
15
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
16
+ header { padding: 16px 24px; border-bottom: 1px solid var(--border);
17
+ display: flex; align-items: center; gap: 16px; position: sticky; top: 0; background: var(--bg); z-index: 5; }
18
+ header h1 { font-size: 16px; margin: 0; font-weight: 600; }
19
+ header .project { color: var(--accent); }
20
+ header .meta { margin-left: auto; color: var(--muted); font-size: 12px; }
21
+ .wrap { padding: 24px; max-width: 1100px; margin: 0 auto; display: grid; gap: 20px; }
22
+ .session { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
23
+ .session > .head { padding: 14px 18px; display: flex; align-items: center; gap: 12px; cursor: pointer; }
24
+ .session .id { font-weight: 700; }
25
+ .session .title { color: var(--muted); }
26
+ .chip { font-size: 11px; padding: 2px 8px; border-radius: 20px; border: 1px solid var(--border); text-transform: uppercase; letter-spacing: .04em; }
27
+ .chip.simple { color: #3fb950; } .chip.medium { color: #d29922; } .chip.complex { color: #f85149; }
28
+ .status-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
29
+ .s-planned { background: var(--pending); } .s-pending { background: var(--pending); }
30
+ .s-running { background: var(--running); box-shadow: 0 0 0 3px rgba(210,153,34,.2); animation: pulse 1.4s infinite; }
31
+ .s-done { background: var(--done); } .s-blocked { background: var(--blocked); } .s-unknown { background: #484f58; }
32
+ @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .4 } }
33
+ .agents { padding: 6px 18px 18px; display: grid; gap: 10px; }
34
+ .wave-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; margin-top: 8px; }
35
+ .agent { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; display: grid;
36
+ grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; }
37
+ .agent .arch { font-size: 12px; color: var(--accent); font-weight: 600; }
38
+ .agent .obj { color: var(--muted); font-size: 13px; }
39
+ .agent .step { color: var(--running); font-size: 12px; margin-top: 2px; }
40
+ .agent .repo { font-size: 11px; color: var(--muted); }
41
+ .agent .verdict { font-size: 11px; font-weight: 700; padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border); }
42
+ .deps { font-size: 11px; color: var(--muted); }
43
+ .empty { color: var(--muted); padding: 40px; text-align: center; }
44
+ .fallback { font-size: 11px; color: var(--running); margin-left: 8px; }
45
+ a { color: var(--accent); }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <header>
50
+ <h1>Context-First Agents · <span class="project" id="project">…</span></h1>
51
+ <div class="meta"><span id="count">0</span> sessions · auto-refresh <span id="tick">2s</span></div>
52
+ </header>
53
+ <div class="wrap" id="root"><div class="empty">Loading…</div></div>
54
+
55
+ <script>
56
+ const ARCH_ORDER = { implementer: 0, integrator: 1, reviewer: 2, tester: 3 };
57
+ const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; };
58
+ const esc = (s) => (s ?? '').replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
59
+
60
+ function agentCard(w) {
61
+ const card = el('div', 'agent');
62
+ const dot = el('div', 'status-dot s-' + (w.status || 'unknown'));
63
+ dot.title = w.status || 'unknown';
64
+ const mid = el('div');
65
+ mid.appendChild(el('div', 'arch', esc((w.id ? w.id + ' · ' : '') + (w.archetype || ''))));
66
+ if (w.objective) mid.appendChild(el('div', 'obj', esc(w.objective)));
67
+ if (w.currentStep && w.status === 'running') mid.appendChild(el('div', 'step', '▸ ' + esc(w.currentStep)));
68
+ if (w.dependsOn && w.dependsOn.length) mid.appendChild(el('div', 'deps', 'depends on ' + esc(w.dependsOn.join(', '))));
69
+ const right = el('div');
70
+ if (w.repository) right.appendChild(el('div', 'repo', esc(w.repository)));
71
+ if (w.verdict) { const v = el('div', 'verdict', esc(w.verdict)); v.style.color = /pass|green|consistent|done/i.test(w.verdict) ? 'var(--done)' : 'var(--blocked)'; right.appendChild(v); }
72
+ card.append(dot, mid, right);
73
+ return card;
74
+ }
75
+
76
+ function sessionCard(s) {
77
+ const box = el('div', 'session');
78
+ const head = el('div', 'head');
79
+ head.appendChild(el('span', 'status-dot s-' + (s.status || 'unknown')));
80
+ head.appendChild(el('span', 'id', esc(s.issueId)));
81
+ if (s.title) head.appendChild(el('span', 'title', esc(s.title)));
82
+ if (s.complexity) head.appendChild(el('span', 'chip ' + s.complexity, esc(s.complexity)));
83
+ if (s.fromPlanFallback) head.appendChild(el('span', 'fallback', '(no live state — from plan)'));
84
+ box.appendChild(head);
85
+
86
+ const agents = el('div', 'agents');
87
+ const byId = Object.fromEntries((s.workers || []).map(w => [w.id, w]));
88
+ const waves = s.waves && s.waves.length ? s.waves : null;
89
+ if (waves) {
90
+ waves.forEach((wave, i) => {
91
+ agents.appendChild(el('div', 'wave-label', 'Wave ' + (i + 1)));
92
+ wave.forEach(id => agents.appendChild(agentCard(byId[id] || { id, status: 'unknown' })));
93
+ });
94
+ } else {
95
+ const sorted = (s.workers || []).slice().sort((a, b) => (ARCH_ORDER[a.archetype] ?? 9) - (ARCH_ORDER[b.archetype] ?? 9));
96
+ if (!sorted.length) agents.appendChild(el('div', 'empty', 'No workers recorded yet.'));
97
+ sorted.forEach(w => agents.appendChild(agentCard(w)));
98
+ }
99
+ box.appendChild(agents);
100
+ return box;
101
+ }
102
+
103
+ async function refresh() {
104
+ try {
105
+ const res = await fetch('/api/sessions');
106
+ const data = await res.json();
107
+ document.getElementById('project').textContent = data.project || 'orchestrator';
108
+ document.getElementById('count').textContent = (data.sessions || []).length;
109
+ const root = document.getElementById('root');
110
+ root.innerHTML = '';
111
+ if (!data.sessions || !data.sessions.length) { root.appendChild(el('div', 'empty', 'No sessions yet. Run <code>/orchestrate &lt;ISSUE-ID&gt;</code>.')); return; }
112
+ // Most recently updated first
113
+ data.sessions.sort((a, b) => (b.updatedAt || b.createdAt || '').localeCompare(a.updatedAt || a.createdAt || ''));
114
+ data.sessions.forEach(s => root.appendChild(sessionCard(s)));
115
+ } catch (e) {
116
+ document.getElementById('root').innerHTML = '<div class="empty">Server unreachable. Is <code>context-agents dashboard</code> running?</div>';
117
+ }
118
+ }
119
+ refresh();
120
+ setInterval(refresh, 2000);
121
+ </script>
122
+ </body>
123
+ </html>
@@ -0,0 +1,65 @@
1
+ # Session State Schema
2
+
3
+ The `/orchestrate` command writes machine-readable state into each session directory so
4
+ tools (like `context-agents dashboard`) can render live progress. Human-readable
5
+ `execution-plan.md` stays alongside.
6
+
7
+ ```
8
+ .sessions/<ISSUE-ID>/
9
+ ├── execution-plan.md # human-readable (prose)
10
+ ├── state.json # session-level state
11
+ └── workers/
12
+ ├── W1.json # one file per agent/worker
13
+ ├── W2.json
14
+ └── ...
15
+ ```
16
+
17
+ ## `state.json`
18
+
19
+ ```json
20
+ {
21
+ "issueId": "ISSUE-42",
22
+ "title": "Example feature",
23
+ "complexity": "medium",
24
+ "status": "running",
25
+ "createdAt": "2026-09-13T12:10:00Z",
26
+ "updatedAt": "2026-09-13T12:15:00Z",
27
+ "repos": ["client-b"],
28
+ "waves": [["W1", "W2"], ["W3"], ["W4"], ["W5"]]
29
+ }
30
+ ```
31
+
32
+ - `status`: `planned` | `running` | `blocked` | `done`.
33
+ - `waves`: array of arrays of worker ids, in execution order.
34
+
35
+ ## `workers/<id>.json`
36
+
37
+ ```json
38
+ {
39
+ "id": "W1",
40
+ "archetype": "implementer",
41
+ "repository": "client-b",
42
+ "objective": "Audio pipeline: mic+tab capture, FFT bands/RMS/onset",
43
+ "dependsOn": [],
44
+ "status": "running",
45
+ "currentStep": "implementing FFT analyser",
46
+ "startedAt": "2026-09-13T12:11:00Z",
47
+ "finishedAt": null,
48
+ "verdict": null
49
+ }
50
+ ```
51
+
52
+ - `status`: `pending` | `running` | `done` | `blocked`.
53
+ - `verdict` (optional): for reviewer/tester/integrator, e.g. `PASS` | `BLOCKED` |
54
+ `GREEN` | `RED` | `CONSISTENT` | `MISMATCH`.
55
+
56
+ ## Update protocol (for `/orchestrate`)
57
+
58
+ 1. Right after the graph is approved: write `state.json` (`status: "planned"`) and one
59
+ `workers/<id>.json` per node (`status: "pending"`).
60
+ 2. When a wave starts: set each of its workers to `running` + a short `currentStep`;
61
+ set `state.json.status = "running"`.
62
+ 3. When a worker returns: set `done` (or `blocked`), `finishedAt`, and `verdict` if any.
63
+ 4. When all workers finish: set `state.json.status = "done"` (or `blocked` if any blocked).
64
+
65
+ Keep writes small and frequent — the dashboard polls these files.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thatix.io/context-first-agents-cli",
3
- "version": "0.2.2",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -126,15 +126,31 @@ Rules:
126
126
 
127
127
  After this, each agent's `writeBoundary` (`.sessions/<ISSUE-ID>/<repo>/`) actually exists.
128
128
 
129
- ## Step 6Spawn ephemeral agents (Task tool)
129
+ ## Step 5cWrite the initial state (for the dashboard)
130
+
131
+ Write machine-readable state into `.sessions/<ISSUE-ID>/` (format in the orchestrator's
132
+ `SESSION-STATE.md`). This feeds `context-agents dashboard`.
133
+
134
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
135
+ (`waves` = the waves from Step 4).
136
+ 2. `workers/<id>.json` for each node: `{ id, archetype, repository, objective, dependsOn,
137
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
130
138
 
131
- Execute the DAG respecting `dependsOn`:
139
+ Keep writes small and frequent — the dashboard polls these files.
140
+
141
+ ## Step 6 — Spawn ephemeral agents (Task tool)
132
142
 
133
- 1. **Parallel wave**: spawn all nodes whose dependencies are satisfied **in a single
134
- message with multiple Task calls** so they run concurrently. Give each subagent ONLY
135
- its compiled contract + objective never the whole conversation.
136
- 2. Wait for a wave to finish. Collect each subagent's structured return.
137
- 3. **Next wave**: spawn nodes whose dependencies are now satisfied. Repeat until done.
143
+ Execute the DAG respecting `dependsOn`. **On each transition, update the state files**:
144
+
145
+ 1. **When a wave starts**: for each node in the wave, set `workers/<id>.json` to
146
+ `status:"running"`, `startedAt`, and a short `currentStep`; set `state.json.status="running"`.
147
+ 2. **Parallel wave**: spawn all nodes in the wave **in a single message with multiple Task
148
+ calls** so they run concurrently. Give each subagent ONLY its compiled contract +
149
+ objective — never the whole conversation.
150
+ 3. **On return**: set each `workers/<id>.json` to `status:"done"` (or `"blocked"`),
151
+ `finishedAt`, and `verdict` if any (reviewer/tester/integrator).
152
+ 4. **Next wave**: spawn nodes whose dependencies are now satisfied. Repeat until done.
153
+ 5. **At the end**: `state.json.status="done"` (or `"blocked"` if any blocked).
138
154
 
139
155
  Use the archetype prompt templates in `agents/` (implementer, reviewer, integrator,
140
156
  tester, …) as the system framing for each subagent, filled with the node's objective,
@@ -127,15 +127,31 @@ Reglas:
127
127
 
128
128
  Tras esto, el `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de verdad.
129
129
 
130
- ## Paso 6Spawnear los agentes efímeros (Task tool)
130
+ ## Paso 5cEscribir el estado inicial (para el dashboard)
131
+
132
+ Escribe el estado legible por máquina en `.sessions/<ISSUE-ID>/` (formato en el
133
+ `SESSION-STATE.md` del orquestador). Esto alimenta `context-agents dashboard`.
134
+
135
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
136
+ (`waves` = las olas del Paso 4).
137
+ 2. `workers/<id>.json` para cada nodo: `{ id, archetype, repository, objective, dependsOn,
138
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
131
139
 
132
- Ejecuta el DAG respetando `dependsOn`:
140
+ Mantén escrituras pequeñas y frecuentes — el dashboard hace polling de estos archivos.
141
+
142
+ ## Paso 6 — Spawnear los agentes efímeros (Task tool)
133
143
 
134
- 1. **Ola paralela**: spawnea todos los nodos con dependencias satisfechas **en un único
135
- mensaje con múltiples llamadas Task**, para que corran concurrentemente. Dale a cada
136
- subagente SÓLO su contrato compilado + objetivo nunca la conversación entera.
137
- 2. Espera a que la ola termine. Recolecta el retorno estructurado de cada subagente.
138
- 3. **Siguiente ola**: spawnea los nodos cuyas dependencias ya están satisfechas. Repite.
144
+ Ejecuta el DAG respetando `dependsOn`. **En cada transición, actualiza los archivos de estado**:
145
+
146
+ 1. **Al iniciar una ola**: para cada nodo de la ola, marca `workers/<id>.json` con
147
+ `status:"running"`, `startedAt`, y un `currentStep` corto; marca `state.json.status="running"`.
148
+ 2. **Ola paralela**: spawnea todos los nodos de la ola **en un único mensaje con múltiples
149
+ llamadas Task**, para que corran concurrentemente. Dale a cada subagente SÓLO su
150
+ contrato compilado + objetivo — nunca la conversación entera.
151
+ 3. **Al retornar**: marca cada `workers/<id>.json` con `status:"done"` (o `"blocked"`),
152
+ `finishedAt`, y `verdict` si hay (reviewer/tester/integrator).
153
+ 4. **Siguiente ola**: spawnea los nodos cuyas dependencias ya están satisfechas. Repite.
154
+ 5. **Al final**: `state.json.status="done"` (o `"blocked"` si alguno bloqueó).
139
155
 
140
156
  Usa las plantillas de arquetipo en `agents/` (implementer, reviewer, integrator, tester…)
141
157
  como marco de cada subagente, rellenadas con objetivo, repositorio y contrato.
@@ -127,15 +127,31 @@ Regras:
127
127
 
128
128
  Depois disso, o `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de fato.
129
129
 
130
- ## Passo 6Spawnar os agentes efêmeros (Task tool)
130
+ ## Passo 5cGravar o estado inicial (para o dashboard)
131
+
132
+ Grave o estado legível por máquina em `.sessions/<ISSUE-ID>/` (formato em
133
+ `SESSION-STATE.md` do orquestrador). Isto alimenta o `context-agents dashboard`.
134
+
135
+ 1. `state.json`: `{ issueId, title, complexity, status:"planned", createdAt, repos, waves }`
136
+ (`waves` = as ondas do Passo 4).
137
+ 2. `workers/<id>.json` para cada nó: `{ id, archetype, repository, objective, dependsOn,
138
+ status:"pending", currentStep:null, startedAt:null, finishedAt:null, verdict:null }`.
131
139
 
132
- Execute o DAG respeitando `dependsOn`:
140
+ Mantenha escritas pequenas e frequentes — o dashboard faz polling desses arquivos.
141
+
142
+ ## Passo 6 — Spawnar os agentes efêmeros (Task tool)
133
143
 
134
- 1. **Onda paralela**: spawne todos os nós com dependências satisfeitas **numa única
135
- mensagem com múltiplas chamadas Task**, para rodarem concorrentemente. Dê a cada
136
- subagente APENAS o contrato compilado + objetivo nunca a conversa inteira.
137
- 2. Aguarde a onda terminar. Colete o retorno estruturado de cada subagente.
138
- 3. **Próxima onda**: spawne os nós cujas dependências agora estão satisfeitas. Repita.
144
+ Execute o DAG respeitando `dependsOn`. **A cada transição, atualize os arquivos de estado**:
145
+
146
+ 1. **Ao iniciar uma onda**: para cada da onda, marque `workers/<id>.json` com
147
+ `status:"running"`, `startedAt`, e um `currentStep` curto; marque `state.json.status="running"`.
148
+ 2. **Onda paralela**: spawne todos os nós da onda **numa única mensagem com múltiplas
149
+ chamadas Task**, para rodarem concorrentemente. Dê a cada subagente APENAS o contrato
150
+ compilado + objetivo — nunca a conversa inteira.
151
+ 3. **Ao retornar**: marque cada `workers/<id>.json` com `status:"done"` (ou `"blocked"`),
152
+ `finishedAt`, e `verdict` se houver (reviewer/tester/integrator).
153
+ 4. **Próxima onda**: spawne os nós cujas dependências agora estão satisfeitas. Repita.
154
+ 5. **Ao final**: `state.json.status="done"` (ou `"blocked"` se algum bloqueou).
139
155
 
140
156
  Use os templates de arquétipo em `agents/` (implementer, reviewer, integrator, tester…)
141
157
  como enquadramento de cada subagente, preenchidos com objetivo, repositório e contrato.
@@ -0,0 +1,123 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
6
+ <title>Context-First Agents — Dashboard</title>
7
+ <style>
8
+ :root {
9
+ --bg: #0d1117; --panel: #161b22; --border: #30363d; --text: #e6edf3;
10
+ --muted: #8b949e; --accent: #58a6ff;
11
+ --pending: #6e7681; --running: #d29922; --done: #3fb950; --blocked: #f85149;
12
+ }
13
+ * { box-sizing: border-box; }
14
+ body { margin: 0; background: var(--bg); color: var(--text);
15
+ font: 14px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif; }
16
+ header { padding: 16px 24px; border-bottom: 1px solid var(--border);
17
+ display: flex; align-items: center; gap: 16px; position: sticky; top: 0; background: var(--bg); z-index: 5; }
18
+ header h1 { font-size: 16px; margin: 0; font-weight: 600; }
19
+ header .project { color: var(--accent); }
20
+ header .meta { margin-left: auto; color: var(--muted); font-size: 12px; }
21
+ .wrap { padding: 24px; max-width: 1100px; margin: 0 auto; display: grid; gap: 20px; }
22
+ .session { background: var(--panel); border: 1px solid var(--border); border-radius: 10px; overflow: hidden; }
23
+ .session > .head { padding: 14px 18px; display: flex; align-items: center; gap: 12px; cursor: pointer; }
24
+ .session .id { font-weight: 700; }
25
+ .session .title { color: var(--muted); }
26
+ .chip { font-size: 11px; padding: 2px 8px; border-radius: 20px; border: 1px solid var(--border); text-transform: uppercase; letter-spacing: .04em; }
27
+ .chip.simple { color: #3fb950; } .chip.medium { color: #d29922; } .chip.complex { color: #f85149; }
28
+ .status-dot { width: 9px; height: 9px; border-radius: 50%; display: inline-block; }
29
+ .s-planned { background: var(--pending); } .s-pending { background: var(--pending); }
30
+ .s-running { background: var(--running); box-shadow: 0 0 0 3px rgba(210,153,34,.2); animation: pulse 1.4s infinite; }
31
+ .s-done { background: var(--done); } .s-blocked { background: var(--blocked); } .s-unknown { background: #484f58; }
32
+ @keyframes pulse { 0%,100% { opacity: 1 } 50% { opacity: .4 } }
33
+ .agents { padding: 6px 18px 18px; display: grid; gap: 10px; }
34
+ .wave-label { font-size: 11px; color: var(--muted); text-transform: uppercase; letter-spacing: .06em; margin-top: 8px; }
35
+ .agent { border: 1px solid var(--border); border-radius: 8px; padding: 10px 12px; display: grid;
36
+ grid-template-columns: auto 1fr auto; gap: 10px; align-items: start; }
37
+ .agent .arch { font-size: 12px; color: var(--accent); font-weight: 600; }
38
+ .agent .obj { color: var(--muted); font-size: 13px; }
39
+ .agent .step { color: var(--running); font-size: 12px; margin-top: 2px; }
40
+ .agent .repo { font-size: 11px; color: var(--muted); }
41
+ .agent .verdict { font-size: 11px; font-weight: 700; padding: 2px 6px; border-radius: 4px; border: 1px solid var(--border); }
42
+ .deps { font-size: 11px; color: var(--muted); }
43
+ .empty { color: var(--muted); padding: 40px; text-align: center; }
44
+ .fallback { font-size: 11px; color: var(--running); margin-left: 8px; }
45
+ a { color: var(--accent); }
46
+ </style>
47
+ </head>
48
+ <body>
49
+ <header>
50
+ <h1>Context-First Agents · <span class="project" id="project">…</span></h1>
51
+ <div class="meta"><span id="count">0</span> sessions · auto-refresh <span id="tick">2s</span></div>
52
+ </header>
53
+ <div class="wrap" id="root"><div class="empty">Loading…</div></div>
54
+
55
+ <script>
56
+ const ARCH_ORDER = { implementer: 0, integrator: 1, reviewer: 2, tester: 3 };
57
+ const el = (tag, cls, html) => { const e = document.createElement(tag); if (cls) e.className = cls; if (html != null) e.innerHTML = html; return e; };
58
+ const esc = (s) => (s ?? '').replace(/[&<>]/g, c => ({ '&':'&amp;','<':'&lt;','>':'&gt;' }[c]));
59
+
60
+ function agentCard(w) {
61
+ const card = el('div', 'agent');
62
+ const dot = el('div', 'status-dot s-' + (w.status || 'unknown'));
63
+ dot.title = w.status || 'unknown';
64
+ const mid = el('div');
65
+ mid.appendChild(el('div', 'arch', esc((w.id ? w.id + ' · ' : '') + (w.archetype || ''))));
66
+ if (w.objective) mid.appendChild(el('div', 'obj', esc(w.objective)));
67
+ if (w.currentStep && w.status === 'running') mid.appendChild(el('div', 'step', '▸ ' + esc(w.currentStep)));
68
+ if (w.dependsOn && w.dependsOn.length) mid.appendChild(el('div', 'deps', 'depends on ' + esc(w.dependsOn.join(', '))));
69
+ const right = el('div');
70
+ if (w.repository) right.appendChild(el('div', 'repo', esc(w.repository)));
71
+ if (w.verdict) { const v = el('div', 'verdict', esc(w.verdict)); v.style.color = /pass|green|consistent|done/i.test(w.verdict) ? 'var(--done)' : 'var(--blocked)'; right.appendChild(v); }
72
+ card.append(dot, mid, right);
73
+ return card;
74
+ }
75
+
76
+ function sessionCard(s) {
77
+ const box = el('div', 'session');
78
+ const head = el('div', 'head');
79
+ head.appendChild(el('span', 'status-dot s-' + (s.status || 'unknown')));
80
+ head.appendChild(el('span', 'id', esc(s.issueId)));
81
+ if (s.title) head.appendChild(el('span', 'title', esc(s.title)));
82
+ if (s.complexity) head.appendChild(el('span', 'chip ' + s.complexity, esc(s.complexity)));
83
+ if (s.fromPlanFallback) head.appendChild(el('span', 'fallback', '(no live state — from plan)'));
84
+ box.appendChild(head);
85
+
86
+ const agents = el('div', 'agents');
87
+ const byId = Object.fromEntries((s.workers || []).map(w => [w.id, w]));
88
+ const waves = s.waves && s.waves.length ? s.waves : null;
89
+ if (waves) {
90
+ waves.forEach((wave, i) => {
91
+ agents.appendChild(el('div', 'wave-label', 'Wave ' + (i + 1)));
92
+ wave.forEach(id => agents.appendChild(agentCard(byId[id] || { id, status: 'unknown' })));
93
+ });
94
+ } else {
95
+ const sorted = (s.workers || []).slice().sort((a, b) => (ARCH_ORDER[a.archetype] ?? 9) - (ARCH_ORDER[b.archetype] ?? 9));
96
+ if (!sorted.length) agents.appendChild(el('div', 'empty', 'No workers recorded yet.'));
97
+ sorted.forEach(w => agents.appendChild(agentCard(w)));
98
+ }
99
+ box.appendChild(agents);
100
+ return box;
101
+ }
102
+
103
+ async function refresh() {
104
+ try {
105
+ const res = await fetch('/api/sessions');
106
+ const data = await res.json();
107
+ document.getElementById('project').textContent = data.project || 'orchestrator';
108
+ document.getElementById('count').textContent = (data.sessions || []).length;
109
+ const root = document.getElementById('root');
110
+ root.innerHTML = '';
111
+ if (!data.sessions || !data.sessions.length) { root.appendChild(el('div', 'empty', 'No sessions yet. Run <code>/orchestrate &lt;ISSUE-ID&gt;</code>.')); return; }
112
+ // Most recently updated first
113
+ data.sessions.sort((a, b) => (b.updatedAt || b.createdAt || '').localeCompare(a.updatedAt || a.createdAt || ''));
114
+ data.sessions.forEach(s => root.appendChild(sessionCard(s)));
115
+ } catch (e) {
116
+ document.getElementById('root').innerHTML = '<div class="empty">Server unreachable. Is <code>context-agents dashboard</code> running?</div>';
117
+ }
118
+ }
119
+ refresh();
120
+ setInterval(refresh, 2000);
121
+ </script>
122
+ </body>
123
+ </html>
@@ -0,0 +1,65 @@
1
+ # Session State Schema
2
+
3
+ The `/orchestrate` command writes machine-readable state into each session directory so
4
+ tools (like `context-agents dashboard`) can render live progress. Human-readable
5
+ `execution-plan.md` stays alongside.
6
+
7
+ ```
8
+ .sessions/<ISSUE-ID>/
9
+ ├── execution-plan.md # human-readable (prose)
10
+ ├── state.json # session-level state
11
+ └── workers/
12
+ ├── W1.json # one file per agent/worker
13
+ ├── W2.json
14
+ └── ...
15
+ ```
16
+
17
+ ## `state.json`
18
+
19
+ ```json
20
+ {
21
+ "issueId": "ISSUE-42",
22
+ "title": "Example feature",
23
+ "complexity": "medium",
24
+ "status": "running",
25
+ "createdAt": "2026-09-13T12:10:00Z",
26
+ "updatedAt": "2026-09-13T12:15:00Z",
27
+ "repos": ["client-b"],
28
+ "waves": [["W1", "W2"], ["W3"], ["W4"], ["W5"]]
29
+ }
30
+ ```
31
+
32
+ - `status`: `planned` | `running` | `blocked` | `done`.
33
+ - `waves`: array of arrays of worker ids, in execution order.
34
+
35
+ ## `workers/<id>.json`
36
+
37
+ ```json
38
+ {
39
+ "id": "W1",
40
+ "archetype": "implementer",
41
+ "repository": "client-b",
42
+ "objective": "Audio pipeline: mic+tab capture, FFT bands/RMS/onset",
43
+ "dependsOn": [],
44
+ "status": "running",
45
+ "currentStep": "implementing FFT analyser",
46
+ "startedAt": "2026-09-13T12:11:00Z",
47
+ "finishedAt": null,
48
+ "verdict": null
49
+ }
50
+ ```
51
+
52
+ - `status`: `pending` | `running` | `done` | `blocked`.
53
+ - `verdict` (optional): for reviewer/tester/integrator, e.g. `PASS` | `BLOCKED` |
54
+ `GREEN` | `RED` | `CONSISTENT` | `MISMATCH`.
55
+
56
+ ## Update protocol (for `/orchestrate`)
57
+
58
+ 1. Right after the graph is approved: write `state.json` (`status: "planned"`) and one
59
+ `workers/<id>.json` per node (`status: "pending"`).
60
+ 2. When a wave starts: set each of its workers to `running` + a short `currentStep`;
61
+ set `state.json.status = "running"`.
62
+ 3. When a worker returns: set `done` (or `blocked`), `finishedAt`, and `verdict` if any.
63
+ 4. When all workers finish: set `state.json.status = "done"` (or `blocked` if any blocked).
64
+
65
+ Keep writes small and frequent — the dashboard polls these files.