@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 +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 +56 -7
- package/dist/templates/commands/es/orchestrate.md +56 -7
- package/dist/templates/commands/pt-BR/orchestrate.md +56 -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 +56 -7
- package/templates/commands/es/orchestrate.md +56 -7
- package/templates/commands/pt-BR/orchestrate.md +56 -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);
|
|
@@ -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
|
|
96
|
+
## Step 5b — Prepare 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
|
-
|
|
141
|
+
## Step 6 — Spawn ephemeral agents (Task tool)
|
|
99
142
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
97
|
+
## Paso 5b — Preparar 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
|
-
|
|
142
|
+
## Paso 6 — Spawnear los agentes efímeros (Task tool)
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
97
|
+
## Passo 5b — Preparar 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
|
-
|
|
142
|
+
## Passo 6 — Spawnar os agentes efêmeros (Task tool)
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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).
|
|
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 => ({ '&':'&','<':'<','>':'>' }[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
|
@@ -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
|
|
96
|
+
## Step 5b — Prepare 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
|
-
|
|
141
|
+
## Step 6 — Spawn ephemeral agents (Task tool)
|
|
99
142
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
|
97
|
+
## Paso 5b — Preparar 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
|
-
|
|
142
|
+
## Paso 6 — Spawnear los agentes efímeros (Task tool)
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
|
97
|
+
## Passo 5b — Preparar 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
|
-
|
|
142
|
+
## Passo 6 — Spawnar os agentes efêmeros (Task tool)
|
|
100
143
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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).
|
|
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 => ({ '&':'&','<':'<','>':'>' }[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.
|