@thatix.io/context-first-agents-cli 0.2.1 → 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);
@@ -93,15 +93,64 @@ See `agents/CONTEXT-CONTRACT.md` for the exact shape. In short:
93
93
  - **limits**: `contextPolicy` (default `select-do-not-dump`), `maxFilesPerWorker`
94
94
  - **return**: summary, changes, evidence, tests, unresolved questions, confidence
95
95
 
96
- ## Step 6Spawn ephemeral agents (Task tool)
96
+ ## Step 5bPrepare the session worktrees (via git, not Node)
97
+
98
+ Before spawning any agent, create an **isolated git worktree per impacted repository**
99
+ (only the ones in the graph), so each implementer has a place to write without touching
100
+ the main repo. Use `base_path` (from `ai.properties.md`) and the `<ISSUE-ID>`.
101
+
102
+ For each impacted repository `<repo>`:
103
+
104
+ 1. If `.sessions/<ISSUE-ID>/<repo>/` already exists, **skip** (worktree ready).
105
+ 2. Check whether branch `feature/<ISSUE-ID>` already exists in the repo:
106
+ ```bash
107
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
108
+ ```
109
+ 3. Create the worktree from the main repo:
110
+ - if the branch does **not** exist (create it in the worktree):
111
+ ```bash
112
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
113
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
114
+ ```
115
+ - if the branch **already** exists (reuse it):
116
+ ```bash
117
+ git -C "{base_path}/<repo>" worktree add \
118
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
119
+ ```
120
+
121
+ Rules:
122
+ - **Never** `checkout` in the main repo (`{base_path}/<repo>`) — the worktree isolates everything.
123
+ - If `git worktree add` fails with "already exists", treat it as ready and continue.
124
+ - Only prepare worktrees for the **impacted** repos in the graph, not every repo in the manifest.
125
+ - Record in `execution-plan.md` which worktrees were created (path + branch).
126
+
127
+ After this, each agent's `writeBoundary` (`.sessions/<ISSUE-ID>/<repo>/`) actually exists.
128
+
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 }`.
138
+
139
+ Keep writes small and frequent — the dashboard polls these files.
97
140
 
98
- Execute the DAG respecting `dependsOn`:
141
+ ## Step 6 Spawn ephemeral agents (Task tool)
99
142
 
100
- 1. **Parallel wave**: spawn all nodes whose dependencies are satisfied **in a single
101
- message with multiple Task calls** so they run concurrently. Give each subagent ONLY
102
- its compiled contract + objective never the whole conversation.
103
- 2. Wait for a wave to finish. Collect each subagent's structured return.
104
- 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).
105
154
 
106
155
  Use the archetype prompt templates in `agents/` (implementer, reviewer, integrator,
107
156
  tester, …) as the system framing for each subagent, filled with the node's objective,
@@ -94,15 +94,64 @@ Ver `agents/CONTEXT-CONTRACT.md` para el formato exacto. En resumen:
94
94
  - **limits**: `contextPolicy` (por defecto `select-do-not-dump`), `maxFilesPerWorker`
95
95
  - **return**: summary, changes, evidence, tests, unresolved, confidence
96
96
 
97
- ## Paso 6Spawnear los agentes efímeros (Task tool)
97
+ ## Paso 5bPreparar los worktrees de la sesión (vía git, no Node)
98
+
99
+ Antes de spawnear cualquier agente, crea un **git worktree aislado por repositorio
100
+ impactado** (sólo los del grafo), para que cada implementer tenga dónde escribir sin tocar
101
+ el repo principal. Usa `base_path` (de `ai.properties.md`) y el `<ISSUE-ID>`.
102
+
103
+ Para cada repositorio impactado `<repo>`:
104
+
105
+ 1. Si `.sessions/<ISSUE-ID>/<repo>/` ya existe, **sáltalo** (worktree listo).
106
+ 2. Chequea si la branch `feature/<ISSUE-ID>` ya existe en el repo:
107
+ ```bash
108
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
109
+ ```
110
+ 3. Crea el worktree desde el repo principal:
111
+ - si la branch **no** existe (créala en el worktree):
112
+ ```bash
113
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
114
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
115
+ ```
116
+ - si la branch **ya** existe (reutilízala):
117
+ ```bash
118
+ git -C "{base_path}/<repo>" worktree add \
119
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
120
+ ```
121
+
122
+ Reglas:
123
+ - **Nunca** hagas `checkout` en el repo principal (`{base_path}/<repo>`) — el worktree aísla todo.
124
+ - Si `git worktree add` falla con "already exists", trátalo como listo y continúa.
125
+ - Sólo prepara worktrees de los repos **impactados** del grafo, no de todos los del manifiesto.
126
+ - Registra en `execution-plan.md` qué worktrees se crearon (path + branch).
127
+
128
+ Tras esto, el `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de verdad.
129
+
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 }`.
139
+
140
+ Mantén escrituras pequeñas y frecuentes — el dashboard hace polling de estos archivos.
98
141
 
99
- Ejecuta el DAG respetando `dependsOn`:
142
+ ## Paso 6 Spawnear los agentes efímeros (Task tool)
100
143
 
101
- 1. **Ola paralela**: spawnea todos los nodos con dependencias satisfechas **en un único
102
- mensaje con múltiples llamadas Task**, para que corran concurrentemente. Dale a cada
103
- subagente SÓLO su contrato compilado + objetivo nunca la conversación entera.
104
- 2. Espera a que la ola termine. Recolecta el retorno estructurado de cada subagente.
105
- 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ó).
106
155
 
107
156
  Usa las plantillas de arquetipo en `agents/` (implementer, reviewer, integrator, tester…)
108
157
  como marco de cada subagente, rellenadas con objetivo, repositorio y contrato.
@@ -94,15 +94,64 @@ Veja `agents/CONTEXT-CONTRACT.md` para o formato exato. Em resumo:
94
94
  - **limits**: `contextPolicy` (padrão `select-do-not-dump`), `maxFilesPerWorker`
95
95
  - **return**: summary, changes, evidence, tests, unresolved, confidence
96
96
 
97
- ## Passo 6Spawnar os agentes efêmeros (Task tool)
97
+ ## Passo 5bPreparar os worktrees da sessão (via git, não Node)
98
+
99
+ Antes de spawnar qualquer agente, crie um **git worktree isolado por repositório
100
+ impactado** (só os do grafo), para que cada implementer tenha onde escrever sem tocar no
101
+ repo principal. Use `base_path` (de `ai.properties.md`) e o `<ISSUE-ID>`.
102
+
103
+ Para cada repositório impactado `<repo>`:
104
+
105
+ 1. Se `.sessions/<ISSUE-ID>/<repo>/` já existir, **pule** (worktree já preparado).
106
+ 2. Descubra se a branch `feature/<ISSUE-ID>` já existe no repo:
107
+ ```bash
108
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
109
+ ```
110
+ 3. Crie o worktree a partir do repo principal:
111
+ - se a branch **não** existe (cria a branch no worktree):
112
+ ```bash
113
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
114
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
115
+ ```
116
+ - se a branch **já** existe (reaproveita):
117
+ ```bash
118
+ git -C "{base_path}/<repo>" worktree add \
119
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
120
+ ```
121
+
122
+ Regras:
123
+ - **Nunca** faça `checkout` no repo principal (`{base_path}/<repo>`) — o worktree isola tudo.
124
+ - Se `git worktree add` falhar por "already exists", trate como já preparado e siga.
125
+ - Só prepare worktrees dos repos **impactados** pelo grafo, não de todos do manifesto.
126
+ - Registre no `execution-plan.md` quais worktrees foram criados (path + branch).
127
+
128
+ Depois disso, o `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de fato.
129
+
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 }`.
139
+
140
+ Mantenha escritas pequenas e frequentes — o dashboard faz polling desses arquivos.
98
141
 
99
- Execute o DAG respeitando `dependsOn`:
142
+ ## Passo 6 Spawnar os agentes efêmeros (Task tool)
100
143
 
101
- 1. **Onda paralela**: spawne todos os nós com dependências satisfeitas **numa única
102
- mensagem com múltiplas chamadas Task**, para rodarem concorrentemente. Dê a cada
103
- subagente APENAS o contrato compilado + objetivo nunca a conversa inteira.
104
- 2. Aguarde a onda terminar. Colete o retorno estruturado de cada subagente.
105
- 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).
106
155
 
107
156
  Use os templates de arquétipo em `agents/` (implementer, reviewer, integrator, tester…)
108
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.1",
3
+ "version": "0.3.0",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
@@ -93,15 +93,64 @@ See `agents/CONTEXT-CONTRACT.md` for the exact shape. In short:
93
93
  - **limits**: `contextPolicy` (default `select-do-not-dump`), `maxFilesPerWorker`
94
94
  - **return**: summary, changes, evidence, tests, unresolved questions, confidence
95
95
 
96
- ## Step 6Spawn ephemeral agents (Task tool)
96
+ ## Step 5bPrepare the session worktrees (via git, not Node)
97
+
98
+ Before spawning any agent, create an **isolated git worktree per impacted repository**
99
+ (only the ones in the graph), so each implementer has a place to write without touching
100
+ the main repo. Use `base_path` (from `ai.properties.md`) and the `<ISSUE-ID>`.
101
+
102
+ For each impacted repository `<repo>`:
103
+
104
+ 1. If `.sessions/<ISSUE-ID>/<repo>/` already exists, **skip** (worktree ready).
105
+ 2. Check whether branch `feature/<ISSUE-ID>` already exists in the repo:
106
+ ```bash
107
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
108
+ ```
109
+ 3. Create the worktree from the main repo:
110
+ - if the branch does **not** exist (create it in the worktree):
111
+ ```bash
112
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
113
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
114
+ ```
115
+ - if the branch **already** exists (reuse it):
116
+ ```bash
117
+ git -C "{base_path}/<repo>" worktree add \
118
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
119
+ ```
120
+
121
+ Rules:
122
+ - **Never** `checkout` in the main repo (`{base_path}/<repo>`) — the worktree isolates everything.
123
+ - If `git worktree add` fails with "already exists", treat it as ready and continue.
124
+ - Only prepare worktrees for the **impacted** repos in the graph, not every repo in the manifest.
125
+ - Record in `execution-plan.md` which worktrees were created (path + branch).
126
+
127
+ After this, each agent's `writeBoundary` (`.sessions/<ISSUE-ID>/<repo>/`) actually exists.
128
+
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 }`.
138
+
139
+ Keep writes small and frequent — the dashboard polls these files.
97
140
 
98
- Execute the DAG respecting `dependsOn`:
141
+ ## Step 6 Spawn ephemeral agents (Task tool)
99
142
 
100
- 1. **Parallel wave**: spawn all nodes whose dependencies are satisfied **in a single
101
- message with multiple Task calls** so they run concurrently. Give each subagent ONLY
102
- its compiled contract + objective never the whole conversation.
103
- 2. Wait for a wave to finish. Collect each subagent's structured return.
104
- 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).
105
154
 
106
155
  Use the archetype prompt templates in `agents/` (implementer, reviewer, integrator,
107
156
  tester, …) as the system framing for each subagent, filled with the node's objective,
@@ -94,15 +94,64 @@ Ver `agents/CONTEXT-CONTRACT.md` para el formato exacto. En resumen:
94
94
  - **limits**: `contextPolicy` (por defecto `select-do-not-dump`), `maxFilesPerWorker`
95
95
  - **return**: summary, changes, evidence, tests, unresolved, confidence
96
96
 
97
- ## Paso 6Spawnear los agentes efímeros (Task tool)
97
+ ## Paso 5bPreparar los worktrees de la sesión (vía git, no Node)
98
+
99
+ Antes de spawnear cualquier agente, crea un **git worktree aislado por repositorio
100
+ impactado** (sólo los del grafo), para que cada implementer tenga dónde escribir sin tocar
101
+ el repo principal. Usa `base_path` (de `ai.properties.md`) y el `<ISSUE-ID>`.
102
+
103
+ Para cada repositorio impactado `<repo>`:
104
+
105
+ 1. Si `.sessions/<ISSUE-ID>/<repo>/` ya existe, **sáltalo** (worktree listo).
106
+ 2. Chequea si la branch `feature/<ISSUE-ID>` ya existe en el repo:
107
+ ```bash
108
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
109
+ ```
110
+ 3. Crea el worktree desde el repo principal:
111
+ - si la branch **no** existe (créala en el worktree):
112
+ ```bash
113
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
114
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
115
+ ```
116
+ - si la branch **ya** existe (reutilízala):
117
+ ```bash
118
+ git -C "{base_path}/<repo>" worktree add \
119
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
120
+ ```
121
+
122
+ Reglas:
123
+ - **Nunca** hagas `checkout` en el repo principal (`{base_path}/<repo>`) — el worktree aísla todo.
124
+ - Si `git worktree add` falla con "already exists", trátalo como listo y continúa.
125
+ - Sólo prepara worktrees de los repos **impactados** del grafo, no de todos los del manifiesto.
126
+ - Registra en `execution-plan.md` qué worktrees se crearon (path + branch).
127
+
128
+ Tras esto, el `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de verdad.
129
+
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 }`.
139
+
140
+ Mantén escrituras pequeñas y frecuentes — el dashboard hace polling de estos archivos.
98
141
 
99
- Ejecuta el DAG respetando `dependsOn`:
142
+ ## Paso 6 Spawnear los agentes efímeros (Task tool)
100
143
 
101
- 1. **Ola paralela**: spawnea todos los nodos con dependencias satisfechas **en un único
102
- mensaje con múltiples llamadas Task**, para que corran concurrentemente. Dale a cada
103
- subagente SÓLO su contrato compilado + objetivo nunca la conversación entera.
104
- 2. Espera a que la ola termine. Recolecta el retorno estructurado de cada subagente.
105
- 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ó).
106
155
 
107
156
  Usa las plantillas de arquetipo en `agents/` (implementer, reviewer, integrator, tester…)
108
157
  como marco de cada subagente, rellenadas con objetivo, repositorio y contrato.
@@ -94,15 +94,64 @@ Veja `agents/CONTEXT-CONTRACT.md` para o formato exato. Em resumo:
94
94
  - **limits**: `contextPolicy` (padrão `select-do-not-dump`), `maxFilesPerWorker`
95
95
  - **return**: summary, changes, evidence, tests, unresolved, confidence
96
96
 
97
- ## Passo 6Spawnar os agentes efêmeros (Task tool)
97
+ ## Passo 5bPreparar os worktrees da sessão (via git, não Node)
98
+
99
+ Antes de spawnar qualquer agente, crie um **git worktree isolado por repositório
100
+ impactado** (só os do grafo), para que cada implementer tenha onde escrever sem tocar no
101
+ repo principal. Use `base_path` (de `ai.properties.md`) e o `<ISSUE-ID>`.
102
+
103
+ Para cada repositório impactado `<repo>`:
104
+
105
+ 1. Se `.sessions/<ISSUE-ID>/<repo>/` já existir, **pule** (worktree já preparado).
106
+ 2. Descubra se a branch `feature/<ISSUE-ID>` já existe no repo:
107
+ ```bash
108
+ git -C "{base_path}/<repo>" rev-parse --verify --quiet "feature/<ISSUE-ID>"
109
+ ```
110
+ 3. Crie o worktree a partir do repo principal:
111
+ - se a branch **não** existe (cria a branch no worktree):
112
+ ```bash
113
+ git -C "{base_path}/<repo>" worktree add -b "feature/<ISSUE-ID>" \
114
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>"
115
+ ```
116
+ - se a branch **já** existe (reaproveita):
117
+ ```bash
118
+ git -C "{base_path}/<repo>" worktree add \
119
+ "$(pwd)/.sessions/<ISSUE-ID>/<repo>" "feature/<ISSUE-ID>"
120
+ ```
121
+
122
+ Regras:
123
+ - **Nunca** faça `checkout` no repo principal (`{base_path}/<repo>`) — o worktree isola tudo.
124
+ - Se `git worktree add` falhar por "already exists", trate como já preparado e siga.
125
+ - Só prepare worktrees dos repos **impactados** pelo grafo, não de todos do manifesto.
126
+ - Registre no `execution-plan.md` quais worktrees foram criados (path + branch).
127
+
128
+ Depois disso, o `writeBoundary` de cada agente (`.sessions/<ISSUE-ID>/<repo>/`) existe de fato.
129
+
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 }`.
139
+
140
+ Mantenha escritas pequenas e frequentes — o dashboard faz polling desses arquivos.
98
141
 
99
- Execute o DAG respeitando `dependsOn`:
142
+ ## Passo 6 Spawnar os agentes efêmeros (Task tool)
100
143
 
101
- 1. **Onda paralela**: spawne todos os nós com dependências satisfeitas **numa única
102
- mensagem com múltiplas chamadas Task**, para rodarem concorrentemente. Dê a cada
103
- subagente APENAS o contrato compilado + objetivo nunca a conversa inteira.
104
- 2. Aguarde a onda terminar. Colete o retorno estruturado de cada subagente.
105
- 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).
106
155
 
107
156
  Use os templates de arquétipo em `agents/` (implementer, reviewer, integrator, tester…)
108
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.