@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 +15 -0
- package/dist/commands/dashboard.d.ts +5 -0
- package/dist/commands/dashboard.js +55 -0
- package/dist/core/sessions.d.ts +29 -0
- package/dist/core/sessions.js +105 -0
- package/dist/index.js +6 -0
- package/dist/templates/commands/en/orchestrate.md +23 -7
- package/dist/templates/commands/es/orchestrate.md +23 -7
- package/dist/templates/commands/pt-BR/orchestrate.md +23 -7
- package/dist/templates/dashboard/index.html +123 -0
- package/dist/templates/orchestrator/SESSION-STATE.md +65 -0
- package/package.json +1 -1
- package/templates/commands/en/orchestrate.md +23 -7
- package/templates/commands/es/orchestrate.md +23 -7
- package/templates/commands/pt-BR/orchestrate.md +23 -7
- package/templates/dashboard/index.html +123 -0
- package/templates/orchestrator/SESSION-STATE.md +65 -0
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,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
|
|
129
|
+
## Step 5c — Write 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
|
-
|
|
139
|
+
Keep writes small and frequent — the dashboard polls these files.
|
|
140
|
+
|
|
141
|
+
## Step 6 — Spawn ephemeral agents (Task tool)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
130
|
+
## Paso 5c — Escribir 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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
130
|
+
## Passo 5c — Gravar 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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
Execute o DAG respeitando `dependsOn`. **A cada transição, atualize os arquivos de estado**:
|
|
145
|
+
|
|
146
|
+
1. **Ao iniciar uma onda**: para cada nó 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 => ({ '&':'&','<':'<','>':'>' }[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 <ISSUE-ID></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
|
@@ -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
|
|
129
|
+
## Step 5c — Write 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
|
-
|
|
139
|
+
Keep writes small and frequent — the dashboard polls these files.
|
|
140
|
+
|
|
141
|
+
## Step 6 — Spawn ephemeral agents (Task tool)
|
|
132
142
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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
|
|
130
|
+
## Paso 5c — Escribir 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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
|
130
|
+
## Passo 5c — Gravar 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
|
-
|
|
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
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
144
|
+
Execute o DAG respeitando `dependsOn`. **A cada transição, atualize os arquivos de estado**:
|
|
145
|
+
|
|
146
|
+
1. **Ao iniciar uma onda**: para cada nó 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 => ({ '&':'&','<':'<','>':'>' }[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 <ISSUE-ID></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.
|