iriai-build 0.1.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/bin/iriai-build.js +78 -0
- package/bridge-v3.js +98 -0
- package/cli/bootstrap.js +83 -0
- package/cli/commands/implementation.js +64 -0
- package/cli/commands/index.js +46 -0
- package/cli/commands/launch.js +153 -0
- package/cli/commands/plan.js +117 -0
- package/cli/commands/setup.js +80 -0
- package/cli/commands/slack.js +97 -0
- package/cli/commands/transfer.js +111 -0
- package/cli/config.js +92 -0
- package/cli/display.js +121 -0
- package/cli/terminal-input.js +666 -0
- package/cli/wait.js +82 -0
- package/index.js +1488 -0
- package/lib/agent-process.js +170 -0
- package/lib/bridge-state.js +126 -0
- package/lib/constants.js +137 -0
- package/lib/health-monitor.js +113 -0
- package/lib/prompt-builder.js +565 -0
- package/lib/signal-watcher.js +215 -0
- package/lib/slack-helpers.js +224 -0
- package/lib/state-machines/feature-lead.js +408 -0
- package/lib/state-machines/operator-agent.js +173 -0
- package/lib/state-machines/planning-role.js +161 -0
- package/lib/state-machines/role-agent.js +186 -0
- package/lib/state-machines/team-orchestrator.js +160 -0
- package/package.json +31 -0
- package/v3/.handover-html-evidence.md +35 -0
- package/v3/KICKOFF-HTML-EVIDENCE.md +98 -0
- package/v3/PLAN-HTML-EVIDENCE-HARDENING.md +603 -0
- package/v3/adapters/desktop-adapter.js +78 -0
- package/v3/adapters/interface.js +146 -0
- package/v3/adapters/slack-adapter.js +608 -0
- package/v3/adapters/slack-helpers.js +179 -0
- package/v3/adapters/terminal-adapter.js +249 -0
- package/v3/agent-supervisor.js +320 -0
- package/v3/artifact-portal.js +1184 -0
- package/v3/bridge.db +0 -0
- package/v3/constants.js +170 -0
- package/v3/db.js +76 -0
- package/v3/file-io.js +216 -0
- package/v3/helpers.js +174 -0
- package/v3/operator.js +364 -0
- package/v3/orchestrator.js +2886 -0
- package/v3/plan-compiler.js +440 -0
- package/v3/prompt-builder.js +849 -0
- package/v3/queries.js +461 -0
- package/v3/recovery.js +508 -0
- package/v3/review-sessions.js +360 -0
- package/v3/roles/accessibility-auditor/CLAUDE.md +50 -0
- package/v3/roles/analytics-engineer/CLAUDE.md +40 -0
- package/v3/roles/architect/CLAUDE.md +809 -0
- package/v3/roles/backend-implementer/CLAUDE.md +97 -0
- package/v3/roles/code-reviewer/CLAUDE.md +89 -0
- package/v3/roles/database-implementer/CLAUDE.md +97 -0
- package/v3/roles/deployer/CLAUDE.md +42 -0
- package/v3/roles/designer/CLAUDE.md +386 -0
- package/v3/roles/documentation/CLAUDE.md +40 -0
- package/v3/roles/feature-lead/CLAUDE.md +233 -0
- package/v3/roles/frontend-implementer/CLAUDE.md +97 -0
- package/v3/roles/implementer/CLAUDE.md +97 -0
- package/v3/roles/integration-tester/CLAUDE.md +174 -0
- package/v3/roles/observability-engineer/CLAUDE.md +40 -0
- package/v3/roles/operator/CLAUDE.md +322 -0
- package/v3/roles/orchestrator/CLAUDE.md +288 -0
- package/v3/roles/package-implementer/CLAUDE.md +47 -0
- package/v3/roles/performance-analyst/CLAUDE.md +49 -0
- package/v3/roles/plan-compiler/CLAUDE.md +163 -0
- package/v3/roles/planning-lead/CLAUDE.md +41 -0
- package/v3/roles/pm/CLAUDE.md +806 -0
- package/v3/roles/regression-tester/CLAUDE.md +135 -0
- package/v3/roles/release-manager/CLAUDE.md +43 -0
- package/v3/roles/security-auditor/CLAUDE.md +90 -0
- package/v3/roles/smoke-tester/CLAUDE.md +97 -0
- package/v3/roles/test-author/CLAUDE.md +42 -0
- package/v3/roles/verifier/CLAUDE.md +90 -0
- package/v3/schema.sql +134 -0
- package/v3/slack-adapter.js +510 -0
- package/v3/slack-helpers.js +346 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
// planning-role.js — Planning pipeline role (PM, Designer, Architect, Plan-Compiler).
|
|
2
|
+
// Replaces run-planning-role.sh in Slack mode. Sequential dispatch via bridge.
|
|
3
|
+
//
|
|
4
|
+
// IDLE ──[dispatch(task)]──→ RUNNING ──[signal:done]──→ COMPLETE
|
|
5
|
+
// ├──[exit + no .done]──→ RETRYING → RUNNING
|
|
6
|
+
// └──[signal:agentResponse]──→ post to Slack, stay RUNNING
|
|
7
|
+
|
|
8
|
+
import { EventEmitter } from "node:events";
|
|
9
|
+
import fs from "node:fs";
|
|
10
|
+
import path from "node:path";
|
|
11
|
+
import AgentProcess from "../agent-process.js";
|
|
12
|
+
import { buildPlanningRolePrompt } from "../prompt-builder.js";
|
|
13
|
+
import {
|
|
14
|
+
PLANNING_BASE, IRIAI_TEAM_DIR, MAX_PLANNING_RETRIES,
|
|
15
|
+
FAST_EXIT_THRESHOLD_MS, FAST_EXIT_BACKOFF_S, NORMAL_BACKOFF_S,
|
|
16
|
+
SIGNAL, ROLE_DIRS,
|
|
17
|
+
} from "../constants.js";
|
|
18
|
+
|
|
19
|
+
export default class PlanningRole extends EventEmitter {
|
|
20
|
+
/**
|
|
21
|
+
* @param {object} opts
|
|
22
|
+
* @param {string} opts.role - Role name (pm, designer, architect, plan-compiler)
|
|
23
|
+
*/
|
|
24
|
+
constructor({ role }) {
|
|
25
|
+
super();
|
|
26
|
+
this.role = role;
|
|
27
|
+
this.signalDir = ROLE_DIRS[role];
|
|
28
|
+
this.key = `planning-${role}`;
|
|
29
|
+
|
|
30
|
+
this._agent = null;
|
|
31
|
+
this._state = "idle";
|
|
32
|
+
this._crashCount = 0;
|
|
33
|
+
this._taskContent = "";
|
|
34
|
+
this._featureSlug = "";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
get state() { return this._state; }
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Dispatch this role with a task.
|
|
41
|
+
* @param {string} featureSlug
|
|
42
|
+
* @param {string} thread_ts
|
|
43
|
+
*/
|
|
44
|
+
dispatch(featureSlug, thread_ts) {
|
|
45
|
+
if (this._state === "running") {
|
|
46
|
+
this.kill();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
this._featureSlug = featureSlug;
|
|
50
|
+
|
|
51
|
+
// Build task header (same as index.js dispatchToRole)
|
|
52
|
+
const taskHeader = [
|
|
53
|
+
"SLACK_MODE=true",
|
|
54
|
+
`FEATURE_SLUG=${featureSlug}`,
|
|
55
|
+
`SIGNAL_DIR=${this.signalDir}`,
|
|
56
|
+
`THREAD_TS=${thread_ts}`,
|
|
57
|
+
"---",
|
|
58
|
+
].join("\n");
|
|
59
|
+
|
|
60
|
+
// Check if there's an existing .task from planning lead
|
|
61
|
+
const taskPath = path.join(this.signalDir, SIGNAL.TASK);
|
|
62
|
+
let existingTask = "";
|
|
63
|
+
try {
|
|
64
|
+
if (fs.existsSync(taskPath)) {
|
|
65
|
+
existingTask = fs.readFileSync(taskPath, "utf-8");
|
|
66
|
+
}
|
|
67
|
+
} catch { /* ok */ }
|
|
68
|
+
|
|
69
|
+
this._taskContent = existingTask
|
|
70
|
+
? `${taskHeader}\n${existingTask}`
|
|
71
|
+
: `${taskHeader}\nStart the ${this.role} phase for feature: ${featureSlug}`;
|
|
72
|
+
|
|
73
|
+
// Write task file
|
|
74
|
+
fs.writeFileSync(taskPath, this._taskContent);
|
|
75
|
+
|
|
76
|
+
// Parse Slack metadata from task content
|
|
77
|
+
const bodyMatch = this._taskContent.split("---\n").slice(1).join("---\n");
|
|
78
|
+
const taskBody = bodyMatch || this._taskContent;
|
|
79
|
+
|
|
80
|
+
this._crashCount = 0;
|
|
81
|
+
this._spawn(taskBody);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
kill() {
|
|
85
|
+
if (this._agent) {
|
|
86
|
+
this._agent.kill();
|
|
87
|
+
this._agent = null;
|
|
88
|
+
}
|
|
89
|
+
this._state = "idle";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
_spawn(taskBody) {
|
|
93
|
+
// Clean done/output signals
|
|
94
|
+
for (const sig of [SIGNAL.DONE, SIGNAL.OUTPUT]) {
|
|
95
|
+
try { fs.unlinkSync(path.join(this.signalDir, sig)); } catch { /* ok */ }
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const prompt = buildPlanningRolePrompt({
|
|
99
|
+
task: taskBody || this._taskContent,
|
|
100
|
+
signalDir: this.signalDir,
|
|
101
|
+
featureSlug: this._featureSlug,
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this._state = "running";
|
|
105
|
+
this._agent = new AgentProcess({
|
|
106
|
+
key: this.key,
|
|
107
|
+
cwd: this.signalDir,
|
|
108
|
+
extraEnv: { PLANNING_SIGNAL_BASE: PLANNING_BASE },
|
|
109
|
+
signalDir: this.signalDir,
|
|
110
|
+
});
|
|
111
|
+
this._agent.spawnClaude(prompt);
|
|
112
|
+
|
|
113
|
+
this._agent.on("exit", ({ exitCode, elapsed }) => {
|
|
114
|
+
this._agent = null;
|
|
115
|
+
|
|
116
|
+
// Check for .done (success)
|
|
117
|
+
if (fs.existsSync(path.join(this.signalDir, SIGNAL.DONE))) {
|
|
118
|
+
this._state = "complete";
|
|
119
|
+
this.emit("done", { key: this.key, role: this.role });
|
|
120
|
+
this.emit("lifecycle", { key: this.key, event: "done" });
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Crash — retry
|
|
125
|
+
this._crashCount++;
|
|
126
|
+
if (this._crashCount <= MAX_PLANNING_RETRIES) {
|
|
127
|
+
const isFast = elapsed < FAST_EXIT_THRESHOLD_MS;
|
|
128
|
+
const backoffS = isFast
|
|
129
|
+
? this._crashCount * FAST_EXIT_BACKOFF_S
|
|
130
|
+
: this._crashCount * NORMAL_BACKOFF_S;
|
|
131
|
+
|
|
132
|
+
this._state = "retrying";
|
|
133
|
+
this.emit("lifecycle", {
|
|
134
|
+
key: this.key,
|
|
135
|
+
event: "crash-retry",
|
|
136
|
+
retryCount: this._crashCount,
|
|
137
|
+
backoffS,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Re-create .task for retry
|
|
141
|
+
try {
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
path.join(this.signalDir, SIGNAL.TASK),
|
|
144
|
+
this._taskContent
|
|
145
|
+
);
|
|
146
|
+
} catch { /* ok */ }
|
|
147
|
+
|
|
148
|
+
setTimeout(() => {
|
|
149
|
+
const bodyMatch = this._taskContent.split("---\n").slice(1).join("---\n");
|
|
150
|
+
this._spawn(bodyMatch || this._taskContent);
|
|
151
|
+
}, backoffS * 1000);
|
|
152
|
+
} else {
|
|
153
|
+
this._state = "crashed";
|
|
154
|
+
this.emit("crashed", { key: this.key, role: this.role });
|
|
155
|
+
this.emit("lifecycle", { key: this.key, event: "crashed" });
|
|
156
|
+
}
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
this.emit("lifecycle", { key: this.key, event: "started" });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
@@ -0,0 +1,186 @@
|
|
|
1
|
+
// role-agent.js — Task lifecycle with retry for implementation roles.
|
|
2
|
+
// Replaces run-role.sh. Direct PTY spawn, event-driven exit, crash retry.
|
|
3
|
+
//
|
|
4
|
+
// IDLE ──[signal:task]──→ RUNNING ──[signal:done]──→ IDLE
|
|
5
|
+
// ├──[exit + no .done + retries left]──→ RETRYING → RUNNING
|
|
6
|
+
// ├──[exit + no .done + no retries]──→ CRASHED
|
|
7
|
+
// └──[signal:needsRestart]──→ RUNNING (handover)
|
|
8
|
+
|
|
9
|
+
import { EventEmitter } from "node:events";
|
|
10
|
+
import fs from "node:fs";
|
|
11
|
+
import path from "node:path";
|
|
12
|
+
import AgentProcess from "../agent-process.js";
|
|
13
|
+
import { buildRolePrompt } from "../prompt-builder.js";
|
|
14
|
+
import {
|
|
15
|
+
IMPL_BASE, MAX_ROLE_RETRIES,
|
|
16
|
+
FAST_EXIT_THRESHOLD_MS, FAST_EXIT_BACKOFF_S, NORMAL_BACKOFF_S,
|
|
17
|
+
SIGNAL,
|
|
18
|
+
} from "../constants.js";
|
|
19
|
+
|
|
20
|
+
export default class RoleAgent extends EventEmitter {
|
|
21
|
+
/**
|
|
22
|
+
* @param {object} opts
|
|
23
|
+
* @param {string} opts.slug - Feature slug
|
|
24
|
+
* @param {string} opts.role - Role name
|
|
25
|
+
* @param {string} opts.signalDir - Role's signal directory
|
|
26
|
+
* @param {string} opts.cwd - Working directory (worktree)
|
|
27
|
+
* @param {string} [opts.teamNum] - Team number (for key generation)
|
|
28
|
+
* @param {string} [opts.model] - Claude model (default: "opus")
|
|
29
|
+
*/
|
|
30
|
+
constructor({ slug, role, signalDir, cwd, teamNum, model }) {
|
|
31
|
+
super();
|
|
32
|
+
this.slug = slug;
|
|
33
|
+
this.role = role;
|
|
34
|
+
this.signalDir = signalDir;
|
|
35
|
+
this.cwd = cwd;
|
|
36
|
+
this.model = model || "opus";
|
|
37
|
+
this.key = teamNum
|
|
38
|
+
? `role-${slug}-${teamNum}-${role}`
|
|
39
|
+
: `review-${slug}-${role}`;
|
|
40
|
+
|
|
41
|
+
this._agent = null;
|
|
42
|
+
this._state = "idle";
|
|
43
|
+
this._retryCount = 0;
|
|
44
|
+
this._handoverContent = "";
|
|
45
|
+
this._task = "";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
get state() { return this._state; }
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Handle a new .task file (triggered by SignalWatcher or orchestrator).
|
|
52
|
+
*/
|
|
53
|
+
handleTask() {
|
|
54
|
+
if (this._state === "running") {
|
|
55
|
+
console.log(`[role] ${this.key}: already running, ignoring new task`);
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const taskPath = path.join(this.signalDir, SIGNAL.TASK);
|
|
60
|
+
try {
|
|
61
|
+
this._task = fs.readFileSync(taskPath, "utf-8").trim();
|
|
62
|
+
// Atomic rename: .task → .active-task
|
|
63
|
+
fs.renameSync(taskPath, path.join(this.signalDir, SIGNAL.ACTIVE_TASK));
|
|
64
|
+
} catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (!this._task) return;
|
|
68
|
+
|
|
69
|
+
this._retryCount = 0;
|
|
70
|
+
this._handoverContent = "";
|
|
71
|
+
this._spawn();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Handle .needs-restart signal (context handover).
|
|
76
|
+
*/
|
|
77
|
+
handleNeedsRestart() {
|
|
78
|
+
// Read handover content before killing
|
|
79
|
+
const handoverPath = path.join(this.signalDir, SIGNAL.HANDOVER);
|
|
80
|
+
if (fs.existsSync(handoverPath)) {
|
|
81
|
+
this._handoverContent = fs.readFileSync(handoverPath, "utf-8");
|
|
82
|
+
fs.unlinkSync(handoverPath);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Clean signal
|
|
86
|
+
const restartPath = path.join(this.signalDir, SIGNAL.NEEDS_RESTART);
|
|
87
|
+
try { fs.unlinkSync(restartPath); } catch { /* ignore */ }
|
|
88
|
+
|
|
89
|
+
// Kill current and respawn (NOT counted as crash)
|
|
90
|
+
if (this._agent) {
|
|
91
|
+
this._agent.kill();
|
|
92
|
+
this._agent = null;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
this.emit("lifecycle", { key: this.key, event: "handover-restart" });
|
|
96
|
+
this._spawn();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
kill() {
|
|
100
|
+
if (this._agent) {
|
|
101
|
+
this._agent.kill();
|
|
102
|
+
this._agent = null;
|
|
103
|
+
}
|
|
104
|
+
this._state = "idle";
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
_spawn() {
|
|
108
|
+
// Clean signals
|
|
109
|
+
for (const sig of [SIGNAL.DONE, SIGNAL.OUTPUT, SIGNAL.NEEDS_RESTART]) {
|
|
110
|
+
try { fs.unlinkSync(path.join(this.signalDir, sig)); } catch { /* ok */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
let recoveryContext = null;
|
|
114
|
+
if (this._handoverContent) {
|
|
115
|
+
recoveryContext = { type: "handover", content: this._handoverContent };
|
|
116
|
+
this._handoverContent = "";
|
|
117
|
+
} else if (this._retryCount > 0) {
|
|
118
|
+
recoveryContext = { type: "crash", retryCount: this._retryCount };
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
const prompt = buildRolePrompt({
|
|
122
|
+
role: this.role,
|
|
123
|
+
signalDir: this.signalDir,
|
|
124
|
+
task: this._task,
|
|
125
|
+
recoveryContext,
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
this._state = "running";
|
|
129
|
+
this._agent = new AgentProcess({
|
|
130
|
+
key: this.key,
|
|
131
|
+
cwd: this.cwd,
|
|
132
|
+
extraEnv: { IMPL_SIGNAL_BASE: IMPL_BASE },
|
|
133
|
+
signalDir: this.signalDir,
|
|
134
|
+
});
|
|
135
|
+
this._agent.spawnClaude(prompt, { model: this.model });
|
|
136
|
+
|
|
137
|
+
// Write PID to .running so recovery can kill orphans
|
|
138
|
+
try { fs.writeFileSync(path.join(this.signalDir, SIGNAL.RUNNING), String(this._agent.pid)); } catch { /* ok */ }
|
|
139
|
+
|
|
140
|
+
this._agent.on("exit", ({ exitCode, elapsed }) => {
|
|
141
|
+
this._agent = null;
|
|
142
|
+
try { fs.unlinkSync(path.join(this.signalDir, SIGNAL.RUNNING)); } catch { /* ok */ }
|
|
143
|
+
|
|
144
|
+
// Check for .done
|
|
145
|
+
const donePath = path.join(this.signalDir, SIGNAL.DONE);
|
|
146
|
+
if (fs.existsSync(donePath)) {
|
|
147
|
+
this._state = "idle";
|
|
148
|
+
this.emit("done", { key: this.key, role: this.role });
|
|
149
|
+
this.emit("lifecycle", { key: this.key, event: "done" });
|
|
150
|
+
return;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// No .done — crash or unexpected exit
|
|
154
|
+
this._retryCount++;
|
|
155
|
+
if (this._retryCount <= MAX_ROLE_RETRIES) {
|
|
156
|
+
const isFast = elapsed < FAST_EXIT_THRESHOLD_MS;
|
|
157
|
+
const backoffS = isFast
|
|
158
|
+
? this._retryCount * FAST_EXIT_BACKOFF_S
|
|
159
|
+
: this._retryCount * NORMAL_BACKOFF_S;
|
|
160
|
+
|
|
161
|
+
this._state = "retrying";
|
|
162
|
+
this.emit("lifecycle", {
|
|
163
|
+
key: this.key,
|
|
164
|
+
event: "crash-retry",
|
|
165
|
+
retryCount: this._retryCount,
|
|
166
|
+
backoffS,
|
|
167
|
+
fastExit: isFast,
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
setTimeout(() => this._spawn(), backoffS * 1000);
|
|
171
|
+
} else {
|
|
172
|
+
// Max retries exceeded
|
|
173
|
+
this._state = "crashed";
|
|
174
|
+
try {
|
|
175
|
+
fs.writeFileSync(path.join(this.signalDir, SIGNAL.DONE), "CRASHED");
|
|
176
|
+
fs.writeFileSync(path.join(this.signalDir, SIGNAL.CRASHED), "CRASHED");
|
|
177
|
+
} catch { /* ignore */ }
|
|
178
|
+
|
|
179
|
+
this.emit("crashed", { key: this.key, role: this.role, retries: MAX_ROLE_RETRIES });
|
|
180
|
+
this.emit("lifecycle", { key: this.key, event: "crashed" });
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
this.emit("lifecycle", { key: this.key, event: "started" });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// team-orchestrator.js — Team orchestrator state machine.
|
|
2
|
+
// Replaces run-team.sh orchestrator logic. Direct PTY spawn.
|
|
3
|
+
//
|
|
4
|
+
// IDLE ──[signal:task]──→ RUNNING ──[signal:gateReady]──→ GATE_READY
|
|
5
|
+
// └──[exit + no .gate-ready + retry ≤1]──→ RETRYING → RUNNING
|
|
6
|
+
|
|
7
|
+
import { EventEmitter } from "node:events";
|
|
8
|
+
import fs from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import AgentProcess from "../agent-process.js";
|
|
11
|
+
import { buildOrchestratorPrompt } from "../prompt-builder.js";
|
|
12
|
+
import {
|
|
13
|
+
IMPL_BASE, IRIAI_TEAM_DIR, MAX_ORCH_RETRIES,
|
|
14
|
+
FAST_EXIT_THRESHOLD_MS, FAST_EXIT_BACKOFF_S, NORMAL_BACKOFF_S,
|
|
15
|
+
SIGNAL,
|
|
16
|
+
} from "../constants.js";
|
|
17
|
+
|
|
18
|
+
export default class TeamOrchestrator extends EventEmitter {
|
|
19
|
+
/**
|
|
20
|
+
* @param {object} opts
|
|
21
|
+
* @param {string} opts.slug - Feature slug
|
|
22
|
+
* @param {string} opts.teamNum - Team number
|
|
23
|
+
* @param {string} opts.teamDir - Team directory (signal base for team)
|
|
24
|
+
* @param {string} opts.orchDir - Orchestrator signal directory
|
|
25
|
+
* @param {string} [opts.model] - Claude model (default: "opus")
|
|
26
|
+
*/
|
|
27
|
+
constructor({ slug, teamNum, teamDir, orchDir, model }) {
|
|
28
|
+
super();
|
|
29
|
+
this.slug = slug;
|
|
30
|
+
this.teamNum = teamNum;
|
|
31
|
+
this.teamDir = teamDir;
|
|
32
|
+
this.orchDir = orchDir;
|
|
33
|
+
this.model = model || "opus";
|
|
34
|
+
this.key = `team-${slug}-${teamNum}`;
|
|
35
|
+
|
|
36
|
+
this._agent = null;
|
|
37
|
+
this._state = "idle";
|
|
38
|
+
this._crashCount = 0;
|
|
39
|
+
this._task = "";
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
get state() { return this._state; }
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Handle a new .task file from Feature Lead.
|
|
46
|
+
*/
|
|
47
|
+
handleTask() {
|
|
48
|
+
if (this._state === "running") {
|
|
49
|
+
console.log(`[orch] ${this.key}: already running, ignoring task`);
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
const taskPath = path.join(this.orchDir, SIGNAL.TASK);
|
|
54
|
+
try {
|
|
55
|
+
this._task = fs.readFileSync(taskPath, "utf-8").trim();
|
|
56
|
+
fs.unlinkSync(taskPath);
|
|
57
|
+
} catch {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!this._task) return;
|
|
61
|
+
|
|
62
|
+
// Clean previous gate signals
|
|
63
|
+
for (const sig of [SIGNAL.DONE, SIGNAL.GATE_READY, SIGNAL.CRASHED,
|
|
64
|
+
SIGNAL.GATE_APPROVED, SIGNAL.QUESTION, SIGNAL.ANSWER]) {
|
|
65
|
+
try { fs.unlinkSync(path.join(this.orchDir, sig)); } catch { /* ok */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
this._crashCount = 0;
|
|
69
|
+
this._spawn();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
kill() {
|
|
73
|
+
if (this._agent) {
|
|
74
|
+
this._agent.kill();
|
|
75
|
+
this._agent = null;
|
|
76
|
+
}
|
|
77
|
+
this._state = "idle";
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
_spawn() {
|
|
81
|
+
let recoveryContext = null;
|
|
82
|
+
if (this._crashCount > 0) {
|
|
83
|
+
recoveryContext = { retryCount: this._crashCount };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const prompt = buildOrchestratorPrompt({
|
|
87
|
+
teamDir: this.teamDir,
|
|
88
|
+
orchDir: this.orchDir,
|
|
89
|
+
task: this._task,
|
|
90
|
+
recoveryContext,
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// Determine CWD — prefer worktree if available
|
|
94
|
+
const teamsRoot = path.join(path.dirname(IRIAI_TEAM_DIR), ".teams", `team-${this.teamNum}`);
|
|
95
|
+
const cwd = fs.existsSync(teamsRoot) ? teamsRoot : this.teamDir;
|
|
96
|
+
|
|
97
|
+
this._state = "running";
|
|
98
|
+
|
|
99
|
+
this._agent = new AgentProcess({
|
|
100
|
+
key: this.key,
|
|
101
|
+
cwd,
|
|
102
|
+
extraEnv: { IMPL_SIGNAL_BASE: IMPL_BASE },
|
|
103
|
+
signalDir: this.orchDir,
|
|
104
|
+
});
|
|
105
|
+
this._agent.spawnClaude(prompt, { model: this.model });
|
|
106
|
+
|
|
107
|
+
// Write PID to .running so recovery can kill orphans
|
|
108
|
+
fs.writeFileSync(path.join(this.orchDir, SIGNAL.RUNNING), String(this._agent.pid));
|
|
109
|
+
|
|
110
|
+
this._agent.on("exit", ({ exitCode, elapsed }) => {
|
|
111
|
+
this._agent = null;
|
|
112
|
+
try { fs.unlinkSync(path.join(this.orchDir, SIGNAL.RUNNING)); } catch { /* ok */ }
|
|
113
|
+
|
|
114
|
+
// Check for gate-ready (success)
|
|
115
|
+
if (fs.existsSync(path.join(this.orchDir, SIGNAL.GATE_READY))) {
|
|
116
|
+
this._state = "gate-ready";
|
|
117
|
+
this.emit("gateReady", { key: this.key, teamNum: this.teamNum });
|
|
118
|
+
this.emit("lifecycle", { key: this.key, event: "gate-ready" });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check for .done (single-team fallback)
|
|
123
|
+
if (fs.existsSync(path.join(this.orchDir, SIGNAL.DONE))) {
|
|
124
|
+
this._state = "idle";
|
|
125
|
+
this.emit("done", { key: this.key, teamNum: this.teamNum });
|
|
126
|
+
this.emit("lifecycle", { key: this.key, event: "done" });
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Crash — retry with backoff
|
|
131
|
+
this._crashCount++;
|
|
132
|
+
if (this._crashCount <= MAX_ORCH_RETRIES) {
|
|
133
|
+
const isFast = elapsed < FAST_EXIT_THRESHOLD_MS;
|
|
134
|
+
const backoffS = isFast
|
|
135
|
+
? this._crashCount * FAST_EXIT_BACKOFF_S
|
|
136
|
+
: this._crashCount * NORMAL_BACKOFF_S;
|
|
137
|
+
|
|
138
|
+
this._state = "retrying";
|
|
139
|
+
this.emit("lifecycle", {
|
|
140
|
+
key: this.key,
|
|
141
|
+
event: "crash-retry",
|
|
142
|
+
retryCount: this._crashCount,
|
|
143
|
+
backoffS,
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
setTimeout(() => this._spawn(), backoffS * 1000);
|
|
147
|
+
} else {
|
|
148
|
+
this._state = "crashed";
|
|
149
|
+
try {
|
|
150
|
+
fs.writeFileSync(path.join(this.orchDir, SIGNAL.CRASHED), "CRASHED");
|
|
151
|
+
} catch { /* ignore */ }
|
|
152
|
+
|
|
153
|
+
this.emit("crashed", { key: this.key, teamNum: this.teamNum });
|
|
154
|
+
this.emit("lifecycle", { key: this.key, event: "crashed" });
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
this.emit("lifecycle", { key: this.key, event: "started" });
|
|
159
|
+
}
|
|
160
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "iriai-build",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Iriai Build tool — AI agent orchestration CLI",
|
|
5
|
+
"main": "index.js",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"iriai-build": "./bin/iriai-build.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"bin/",
|
|
12
|
+
"cli/",
|
|
13
|
+
"lib/",
|
|
14
|
+
"v3/",
|
|
15
|
+
"index.js",
|
|
16
|
+
"bridge-v3.js"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "node index.js",
|
|
20
|
+
"start:v2": "node bridge-v2.js",
|
|
21
|
+
"start:v3": "node bridge-v3.js"
|
|
22
|
+
},
|
|
23
|
+
"dependencies": {
|
|
24
|
+
"@inquirer/prompts": "^8.3.0",
|
|
25
|
+
"@slack/socket-mode": "^2.0.1",
|
|
26
|
+
"@slack/web-api": "^7.8.0",
|
|
27
|
+
"chokidar": "^4.0.3",
|
|
28
|
+
"commander": "^14.0.3",
|
|
29
|
+
"iriai-feedback": "^0.1.0"
|
|
30
|
+
}
|
|
31
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Handover: HTML Evidence Hardening
|
|
2
|
+
|
|
3
|
+
## Session 1 (2026-03-05)
|
|
4
|
+
|
|
5
|
+
### Completed Files
|
|
6
|
+
- [x] File 1: `v3/roles/implementer/CLAUDE.md` — Added deviations, self_reported_risks to Output, steps 6-7 to Process, replaced Context Management with .output.partial protocol (file_complete, deviation, risk entry types)
|
|
7
|
+
- [x] File 2: `v3/roles/backend-implementer/CLAUDE.md` — Same as #1 (steps 5-6 added as "Process continued")
|
|
8
|
+
- [x] File 3: `v3/roles/frontend-implementer/CLAUDE.md` — Same as #1
|
|
9
|
+
- [x] File 4: `v3/roles/database-implementer/CLAUDE.md` — Same as #1
|
|
10
|
+
- [x] File 5: `v3/roles/code-reviewer/CLAUDE.md` — Added gaps field (categories: error-handling, input-validation, pattern-compliance, edge-cases, test-coverage), .output.partial protocol (file_review, gap entry types)
|
|
11
|
+
- [x] File 6: `v3/roles/security-auditor/CLAUDE.md` — Added gaps field (categories: auth, injection, rate-limiting, secrets, cors, csrf, data-exposure), .output.partial protocol (endpoint_review, gap entry types)
|
|
12
|
+
- [x] File 7: `v3/roles/integration-tester/CLAUDE.md` — Added comprehensive E2E mandate section (happy path + error cases per journey + gap reporting), gaps field (categories: untested-journey, missing-error-case, missing-edge-case, visual-gap), .output.partial protocol (journey, gap entry types)
|
|
13
|
+
- [x] File 8: `v3/roles/regression-tester/CLAUDE.md` — Added gaps field (categories: untested-regression, missing-backward-compat, skipped-test-suite), .output.partial protocol (test_suite, gap entry types)
|
|
14
|
+
- [x] File 9: `v3/roles/verifier/CLAUDE.md` — Added gaps field (categories: unverified-criterion, insufficient-evidence, missing-acceptance-check), .output.partial protocol (criterion_check, gap entry types)
|
|
15
|
+
- [x] File 10: `v3/roles/orchestrator/CLAUDE.md` — Added steps 4b (review gaps), 4c (aggregate deviations/risks), 4d (build coverage matrix). Updated gate evidence YAML example with coverage_matrix, deviations, self_reported_risks, reviewer_comments, error-case journeys, gaps in qa_verdicts. Added note about team HTML compilation (no buttons, doc_type: "team")
|
|
16
|
+
- [x] File 11: `v3/roles/feature-lead/CLAUDE.md` — Added steps 4b (review gaps across levels), 4c (cross-team integration surface), 4d (feature-level coverage matrix), 4e (FL comments). Updated step 6 (merge evidence with new fields), step 7 (doc_type: "feature", team_html_paths), step 8 (HTML IS the message, no text summary)
|
|
17
|
+
- [x] File 12: `tools/visual-verification-mcp/evidence-compiler.js` — Major overhaul: validateEvidence() warns on missing gaps/deviations/coverage_matrix, errors on missing reviewer_comments for feature docs. buildHtml() now renders: coverage matrix table, deviation cards, self-reported risk cards, happy path / error case journey grouping, QA gaps subsections, pending QA sections, reviewer comments, cross-team surface (feature only), team evidence links (feature only). New CSS: .section-pending, .gap-item, .coverage-* classes. handleCompileGateEvidence() accepts doc_type and team_html_paths params.
|
|
18
|
+
- [x] File 13: `iriai-build/v3/prompt-builder.js` — Updated buildGateReviewInstructions() step 6 (include coverage_matrix, deviations, self_reported_risks, reviewer_comments, cross_team_surface), step 7 (doc_type: "feature", team_html_paths), step 8 (HTML IS the message, no text summary)
|
|
19
|
+
- [x] File 14: `iriai-build/v3/constants.js` — Added OUTPUT_PARTIAL: ".output.partial" to SIGNAL object
|
|
20
|
+
- [x] File 15: `iriai-build/v3/orchestrator.js` — Removed text status messages: _requestPhaseReview (summary post), handlePhaseReviewApproval ("Phase approved..."), handlePhaseReviewRejection ("phase rejected..."), handlePlanApproval ("Plan approved!", "Creating branches...", "Feature branches created...", "Implementation launched..."), handleGateApproval ("Gate approved!"), handleGateRejection ("Gate rejected...")
|
|
21
|
+
|
|
22
|
+
### In Progress
|
|
23
|
+
(none — all complete)
|
|
24
|
+
|
|
25
|
+
### Not Started
|
|
26
|
+
(none — all complete)
|
|
27
|
+
|
|
28
|
+
### Decisions Made
|
|
29
|
+
- Used `_doc_type` and `_team_html_links` as internal properties on the evidence object (prefixed with underscore) to pass doc_type/team_html_paths from handler to buildHtml/validateEvidence without polluting the YAML schema
|
|
30
|
+
- Kept `nextLabel` variable in handlePhaseReviewApproval even though channel post was removed — it may be used elsewhere or for logging
|
|
31
|
+
- Removed ALL text status posts from planning thread in handlePlanApproval including "Implementation launched" (not explicitly listed in plan but follows the principle of "NO text-based status messages in the planning thread")
|
|
32
|
+
- For pending QA sections, check against EXPECTED_QA_ROLES constant defined in buildHtml
|
|
33
|
+
|
|
34
|
+
### Notes for Next Session
|
|
35
|
+
All 15 files are complete. No further work needed.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
# Kickoff: HTML Evidence Document Hardening
|
|
2
|
+
|
|
3
|
+
## Your Mission
|
|
4
|
+
|
|
5
|
+
Implement the plan at `iriai-build/v3/PLAN-HTML-EVIDENCE-HARDENING.md`. Read it fully before starting.
|
|
6
|
+
|
|
7
|
+
## Key Files (absolute paths)
|
|
8
|
+
|
|
9
|
+
### Plan & Handover
|
|
10
|
+
- Plan: `/Users/danielzhang/src/iriai/iriai-build/v3/PLAN-HTML-EVIDENCE-HARDENING.md`
|
|
11
|
+
- Handover: `/Users/danielzhang/src/iriai/iriai-build/v3/.handover-html-evidence.md`
|
|
12
|
+
|
|
13
|
+
### Files to Modify (15 total)
|
|
14
|
+
|
|
15
|
+
**Phase 1 — Implementer CLAUDE.md (add deviations, self_reported_risks, .output.partial):**
|
|
16
|
+
1. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/implementer/CLAUDE.md`
|
|
17
|
+
2. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/backend-implementer/CLAUDE.md`
|
|
18
|
+
3. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/frontend-implementer/CLAUDE.md`
|
|
19
|
+
4. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/database-implementer/CLAUDE.md`
|
|
20
|
+
|
|
21
|
+
**Phase 1 — Review agent CLAUDE.md (add gaps, .output.partial):**
|
|
22
|
+
5. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/code-reviewer/CLAUDE.md`
|
|
23
|
+
6. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/security-auditor/CLAUDE.md`
|
|
24
|
+
7. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/integration-tester/CLAUDE.md` (also: comprehensive E2E mandate)
|
|
25
|
+
8. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/regression-tester/CLAUDE.md`
|
|
26
|
+
9. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/verifier/CLAUDE.md`
|
|
27
|
+
|
|
28
|
+
**Phase 1 — Constants:**
|
|
29
|
+
14. `/Users/danielzhang/src/iriai/iriai-build/v3/constants.js` (add OUTPUT_PARTIAL signal)
|
|
30
|
+
|
|
31
|
+
**Phase 2 — Orchestrator & Feature Lead CLAUDE.md:**
|
|
32
|
+
10. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/orchestrator/CLAUDE.md` (coverage matrix, deviation aggregation, reviewer comments, team HTML compilation — no buttons)
|
|
33
|
+
11. `/Users/danielzhang/src/iriai/iriai-build/v3/roles/feature-lead/CLAUDE.md` (cross-team surface, FL comments, feature-level coverage matrix, sole poster of approve/reject with HTML)
|
|
34
|
+
|
|
35
|
+
**Phase 3 — Code changes:**
|
|
36
|
+
12. `/Users/danielzhang/src/iriai/tools/visual-verification-mcp/evidence-compiler.js` (HTML template overhaul, new sections, pending styling, doc_type param)
|
|
37
|
+
13. `/Users/danielzhang/src/iriai/iriai-build/v3/prompt-builder.js` (gate review instructions update)
|
|
38
|
+
15. `/Users/danielzhang/src/iriai/iriai-build/v3/orchestrator.js` (remove text status messages from planning thread)
|
|
39
|
+
|
|
40
|
+
## Execution Rules
|
|
41
|
+
|
|
42
|
+
1. Read the plan fully before making any changes
|
|
43
|
+
2. Read each file before editing it
|
|
44
|
+
3. Work through phases in order (Phase 1 files can be done in parallel, Phase 2 depends on Phase 1, Phase 3 parallel with Phase 2)
|
|
45
|
+
4. For CLAUDE.md files: ADD new sections/fields to existing content — do not rewrite entire files
|
|
46
|
+
5. For evidence-compiler.js: this is a major overhaul of the buildHtml() function and validateEvidence() — read the full file first
|
|
47
|
+
6. For orchestrator.js: surgical removal of specific `postToChannel`/`postToThread` calls in planning thread — do not change dispatch logic
|
|
48
|
+
|
|
49
|
+
## Recursive Handover Protocol
|
|
50
|
+
|
|
51
|
+
You will likely not finish all 15 files in one context window. Before your context runs low:
|
|
52
|
+
|
|
53
|
+
1. Write your progress to `/Users/danielzhang/src/iriai/iriai-build/v3/.handover-html-evidence.md` using this format:
|
|
54
|
+
|
|
55
|
+
```markdown
|
|
56
|
+
# Handover: HTML Evidence Hardening
|
|
57
|
+
|
|
58
|
+
## Session N (date)
|
|
59
|
+
|
|
60
|
+
### Completed Files
|
|
61
|
+
- [x] File 1: `/path/to/file` — description of changes made
|
|
62
|
+
- [x] File 2: ...
|
|
63
|
+
|
|
64
|
+
### In Progress
|
|
65
|
+
- [ ] File X: `/path/to/file` — what was started, what remains
|
|
66
|
+
|
|
67
|
+
### Not Started
|
|
68
|
+
- [ ] File Y: `/path/to/file`
|
|
69
|
+
- [ ] File Z: ...
|
|
70
|
+
|
|
71
|
+
### Decisions Made
|
|
72
|
+
- [any judgment calls or deviations from the plan]
|
|
73
|
+
|
|
74
|
+
### Notes for Next Session
|
|
75
|
+
- [anything the next session needs to know]
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
2. Tell the user: "Context getting low. Handover written. Start a new session with this prompt."
|
|
79
|
+
|
|
80
|
+
## Resuming From Handover
|
|
81
|
+
|
|
82
|
+
If `.handover-html-evidence.md` exists, read it FIRST. It contains:
|
|
83
|
+
- Which files are done (do NOT re-edit them)
|
|
84
|
+
- Which file was in progress (finish it)
|
|
85
|
+
- Which files remain (continue in order)
|
|
86
|
+
|
|
87
|
+
Then continue executing the plan from where the previous session left off. Update the handover file before your own context runs low, incrementing the session number.
|
|
88
|
+
|
|
89
|
+
## Summary of What We're Building
|
|
90
|
+
|
|
91
|
+
- **Incremental output (.output.partial)**: Append-only multi-doc YAML so agents don't lose work on context exhaustion
|
|
92
|
+
- **Enriched schemas**: Implementers report deviations + risks. Review agents report gaps. Orchestrator/FL add reviewer comments.
|
|
93
|
+
- **Integration-tester mandate**: Comprehensive GIFs for every golden path AND every error case
|
|
94
|
+
- **Coverage matrix**: Plan items mapped to implemented/verified/missing status
|
|
95
|
+
- **HTML overhaul**: New sections (coverage matrix, deviations, risks, gaps per agent, reviewer comments, cross-team surface), pending section styling, feature-level doc links to team docs
|
|
96
|
+
- **Approve/reject buttons**: ONLY on feature gate HTML in impl channel (not per-team)
|
|
97
|
+
- **Planning thread**: HTML evidence docs only, no text status messages
|
|
98
|
+
- **Impl channel**: Gate approvals must now include the feature gate HTML attachment. Everything else unchanged.
|