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.
Files changed (80) hide show
  1. package/bin/iriai-build.js +78 -0
  2. package/bridge-v3.js +98 -0
  3. package/cli/bootstrap.js +83 -0
  4. package/cli/commands/implementation.js +64 -0
  5. package/cli/commands/index.js +46 -0
  6. package/cli/commands/launch.js +153 -0
  7. package/cli/commands/plan.js +117 -0
  8. package/cli/commands/setup.js +80 -0
  9. package/cli/commands/slack.js +97 -0
  10. package/cli/commands/transfer.js +111 -0
  11. package/cli/config.js +92 -0
  12. package/cli/display.js +121 -0
  13. package/cli/terminal-input.js +666 -0
  14. package/cli/wait.js +82 -0
  15. package/index.js +1488 -0
  16. package/lib/agent-process.js +170 -0
  17. package/lib/bridge-state.js +126 -0
  18. package/lib/constants.js +137 -0
  19. package/lib/health-monitor.js +113 -0
  20. package/lib/prompt-builder.js +565 -0
  21. package/lib/signal-watcher.js +215 -0
  22. package/lib/slack-helpers.js +224 -0
  23. package/lib/state-machines/feature-lead.js +408 -0
  24. package/lib/state-machines/operator-agent.js +173 -0
  25. package/lib/state-machines/planning-role.js +161 -0
  26. package/lib/state-machines/role-agent.js +186 -0
  27. package/lib/state-machines/team-orchestrator.js +160 -0
  28. package/package.json +31 -0
  29. package/v3/.handover-html-evidence.md +35 -0
  30. package/v3/KICKOFF-HTML-EVIDENCE.md +98 -0
  31. package/v3/PLAN-HTML-EVIDENCE-HARDENING.md +603 -0
  32. package/v3/adapters/desktop-adapter.js +78 -0
  33. package/v3/adapters/interface.js +146 -0
  34. package/v3/adapters/slack-adapter.js +608 -0
  35. package/v3/adapters/slack-helpers.js +179 -0
  36. package/v3/adapters/terminal-adapter.js +249 -0
  37. package/v3/agent-supervisor.js +320 -0
  38. package/v3/artifact-portal.js +1184 -0
  39. package/v3/bridge.db +0 -0
  40. package/v3/constants.js +170 -0
  41. package/v3/db.js +76 -0
  42. package/v3/file-io.js +216 -0
  43. package/v3/helpers.js +174 -0
  44. package/v3/operator.js +364 -0
  45. package/v3/orchestrator.js +2886 -0
  46. package/v3/plan-compiler.js +440 -0
  47. package/v3/prompt-builder.js +849 -0
  48. package/v3/queries.js +461 -0
  49. package/v3/recovery.js +508 -0
  50. package/v3/review-sessions.js +360 -0
  51. package/v3/roles/accessibility-auditor/CLAUDE.md +50 -0
  52. package/v3/roles/analytics-engineer/CLAUDE.md +40 -0
  53. package/v3/roles/architect/CLAUDE.md +809 -0
  54. package/v3/roles/backend-implementer/CLAUDE.md +97 -0
  55. package/v3/roles/code-reviewer/CLAUDE.md +89 -0
  56. package/v3/roles/database-implementer/CLAUDE.md +97 -0
  57. package/v3/roles/deployer/CLAUDE.md +42 -0
  58. package/v3/roles/designer/CLAUDE.md +386 -0
  59. package/v3/roles/documentation/CLAUDE.md +40 -0
  60. package/v3/roles/feature-lead/CLAUDE.md +233 -0
  61. package/v3/roles/frontend-implementer/CLAUDE.md +97 -0
  62. package/v3/roles/implementer/CLAUDE.md +97 -0
  63. package/v3/roles/integration-tester/CLAUDE.md +174 -0
  64. package/v3/roles/observability-engineer/CLAUDE.md +40 -0
  65. package/v3/roles/operator/CLAUDE.md +322 -0
  66. package/v3/roles/orchestrator/CLAUDE.md +288 -0
  67. package/v3/roles/package-implementer/CLAUDE.md +47 -0
  68. package/v3/roles/performance-analyst/CLAUDE.md +49 -0
  69. package/v3/roles/plan-compiler/CLAUDE.md +163 -0
  70. package/v3/roles/planning-lead/CLAUDE.md +41 -0
  71. package/v3/roles/pm/CLAUDE.md +806 -0
  72. package/v3/roles/regression-tester/CLAUDE.md +135 -0
  73. package/v3/roles/release-manager/CLAUDE.md +43 -0
  74. package/v3/roles/security-auditor/CLAUDE.md +90 -0
  75. package/v3/roles/smoke-tester/CLAUDE.md +97 -0
  76. package/v3/roles/test-author/CLAUDE.md +42 -0
  77. package/v3/roles/verifier/CLAUDE.md +90 -0
  78. package/v3/schema.sql +134 -0
  79. package/v3/slack-adapter.js +510 -0
  80. package/v3/slack-helpers.js +346 -0
package/v3/operator.js ADDED
@@ -0,0 +1,364 @@
1
+ // operator.js — Ephemeral operator: context assembly from SQLite, spawn, parse response.
2
+ // Operator is a stateless function: (event, context) → formatted_response.
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { execFile } from "node:child_process";
7
+ import { getClaudeBin } from "../cli/config.js";
8
+ import * as queries from "./queries.js";
9
+ import { buildOperatorPrompt, buildOperatorRelayPrompt } from "./prompt-builder.js";
10
+ import {
11
+ HISTORY_CHAR_LIMIT, HISTORY_RECENT_EVENTS, HISTORY_FULL_BACKUP_EVENTS,
12
+ } from "./constants.js";
13
+
14
+ /**
15
+ * Invoke the Operator for a given event. Assembles context from SQLite,
16
+ * spawns a one-shot claude CLI, and lets the response flow through the
17
+ * normal file-based signal path (.agent-response → fileIO → slack).
18
+ *
19
+ * @param {object} opts
20
+ * @param {object} opts.feature - Feature row from SQLite
21
+ * @param {string} opts.operatorDir - Operator signal directory
22
+ * @param {string} opts.flDir - Feature Lead signal directory
23
+ * @param {string} opts.featureDir - Feature signal tree root
24
+ * @param {string} opts.userMessage - The triggering event/message
25
+ * @param {AgentSupervisor} opts.supervisor - Agent supervisor instance
26
+ * @param {number} opts.agentId - Operator agent DB id
27
+ */
28
+ export async function invokeOperator({ feature, operatorDir, flDir, featureDir, userMessage, supervisor, agentId, planDir, activePlanningRole }) {
29
+ // 1. Assemble context from SQLite (async — Haiku summarization is non-blocking)
30
+ const history = await assembleHistory(feature.id);
31
+ const activeAgents = assembleActiveAgents(feature.id);
32
+ const pendingDecision = assemblePendingDecision(feature.id);
33
+
34
+ const HOME = process.env.HOME;
35
+ const directoryMap = `${HOME}/src/iriai/DIRECTORY_MAP.MD`;
36
+
37
+ // 2. Build prompt
38
+ const prompt = buildOperatorPrompt({
39
+ featureName: feature.slug,
40
+ operatorDir,
41
+ flDir,
42
+ featureDir,
43
+ history,
44
+ userMessage,
45
+ activeAgents,
46
+ pendingDecision,
47
+ planDir,
48
+ activePlanningRole,
49
+ directoryMap,
50
+ });
51
+
52
+ // 3. Spawn claude via supervisor with --continue for session context
53
+ // The supervisor handles PID tracking, exit detection, .runner.log
54
+ // Response flows through .agent-response → fileIO → slack adapter
55
+ supervisor.spawn(agentId, prompt, { continue: true });
56
+
57
+ // Record the event
58
+ queries.insertEvent(feature.id, "user-message", "bridge", `Operator invoked for: ${userMessage.slice(0, 100)}`);
59
+ }
60
+
61
+ /**
62
+ * Invoke the Operator in relay mode — format another agent's output for Slack.
63
+ * Returns a promise that the caller manages (wait for operatorResponse or timeout).
64
+ *
65
+ * @param {object} opts
66
+ * @param {object} opts.feature - Feature row from SQLite
67
+ * @param {object} opts.queueEntry - Relay queue entry { id, source_agent, event_hint, raw_content }
68
+ * @param {AgentSupervisor} opts.supervisor - Agent supervisor instance
69
+ * @param {number} opts.agentId - Operator agent DB id
70
+ * @param {string} opts.operatorDir - Operator signal directory
71
+ * @param {string} opts.featureDir - Feature signal tree root
72
+ */
73
+ export async function invokeOperatorRelay({ feature, queueEntry, supervisor, agentId, operatorDir, featureDir, continue: cont = false }) {
74
+ const history = await assembleHistory(feature.id);
75
+ const activeAgents = assembleActiveAgents(feature.id);
76
+ const pendingDecision = assemblePendingDecision(feature.id);
77
+
78
+ const prompt = buildOperatorRelayPrompt({
79
+ featureName: feature.slug,
80
+ operatorDir,
81
+ featureDir,
82
+ history,
83
+ activeAgents,
84
+ pendingDecision,
85
+ sourceAgent: queueEntry.source_agent,
86
+ eventHint: queueEntry.event_hint,
87
+ rawContent: queueEntry.raw_content,
88
+ });
89
+
90
+ supervisor.spawn(agentId, prompt, { continue: cont });
91
+ queries.insertEvent(feature.id, "system", "bridge",
92
+ `Operator relay invoked for ${queueEntry.source_agent} (${queueEntry.event_hint})`);
93
+ }
94
+
95
+ /**
96
+ * Format a single event into a log line.
97
+ */
98
+ function formatEvent(e) {
99
+ const time = e.created_at ? e.created_at.split(" ")[1] || "" : "";
100
+ const source = e.source || "unknown";
101
+ const type = e.event_type;
102
+ const content = (e.content || "").slice(0, 500);
103
+
104
+ switch (type) {
105
+ case "user-message":
106
+ return `[${time}] ${source}: ${content}`;
107
+ case "agent-response":
108
+ return `[${time}] ${source}: ${content}`;
109
+ case "operator-response":
110
+ return `[${time}] operator: ${content}`;
111
+ case "phase-transition":
112
+ return `[${time}] SYSTEM: Phase transition — ${content}`;
113
+ case "gate-approved":
114
+ return `[${time}] SYSTEM: Gate approved`;
115
+ case "gate-rejected":
116
+ return `[${time}] SYSTEM: Gate rejected — ${content}`;
117
+ case "agent-crashed":
118
+ return `[${time}] SYSTEM: Agent crashed — ${content}`;
119
+ case "question":
120
+ return `[${time}] ${source} QUESTION: ${content}`;
121
+ case "system":
122
+ return `[${time}] SYSTEM: ${content}`;
123
+ default:
124
+ return `[${time}] [${type}] ${source}: ${content}`;
125
+ }
126
+ }
127
+
128
+ /**
129
+ * Assemble conversation history from SQLite events.
130
+ *
131
+ * Strategy: keep recent events verbatim, summarize older events via Haiku
132
+ * if total history exceeds HISTORY_CHAR_LIMIT. Full history is always
133
+ * saved to disk as a backup.
134
+ */
135
+ export async function assembleHistory(featureId) {
136
+ const allEvents = queries.getRecentEvents(featureId, HISTORY_FULL_BACKUP_EVENTS);
137
+ if (!allEvents.length) return "(no prior conversation)";
138
+
139
+ // Reverse to chronological order (DB returns DESC)
140
+ const chronological = allEvents.reverse();
141
+ const allLines = chronological.map(formatEvent);
142
+ const fullHistory = allLines.join("\n");
143
+
144
+ // Save full backup to disk
145
+ saveFullHistoryBackup(featureId, fullHistory);
146
+
147
+ // If within limit, return as-is
148
+ if (fullHistory.length <= HISTORY_CHAR_LIMIT) {
149
+ return fullHistory;
150
+ }
151
+
152
+ // Split: recent events stay verbatim, older ones get summarized
153
+ const recentCount = Math.min(HISTORY_RECENT_EVENTS, chronological.length);
154
+ const olderLines = allLines.slice(0, -recentCount);
155
+ const recentLines = allLines.slice(-recentCount);
156
+
157
+ const recentText = recentLines.join("\n");
158
+ const olderText = olderLines.join("\n");
159
+
160
+ // Summarize older events via Haiku (async — doesn't block event loop)
161
+ const summary = await summarizeViaHaiku(olderText);
162
+
163
+ return `### Earlier Context (summarized)\n${summary}\n\n### Recent Events\n${recentText}`;
164
+ }
165
+
166
+ /**
167
+ * Save full history to disk as backup.
168
+ */
169
+ function saveFullHistoryBackup(featureId, fullHistory) {
170
+ try {
171
+ const feature = queries.getFeatureById(featureId);
172
+ if (!feature) return;
173
+ const backupDir = path.join(
174
+ process.env.HOME, "src/iriai/.implementation/features", feature.slug, "operator"
175
+ );
176
+ fs.mkdirSync(backupDir, { recursive: true });
177
+ fs.writeFileSync(path.join(backupDir, ".history-full.log"), fullHistory);
178
+ } catch { /* non-critical */ }
179
+ }
180
+
181
+ /**
182
+ * Summarize older history via a one-shot Haiku call (async).
183
+ * Falls back to simple truncation if Haiku fails.
184
+ */
185
+ function summarizeViaHaiku(olderText) {
186
+ const prompt = `Summarize the following conversation history between a user and AI agents working on a software feature. Preserve:
187
+ - All decisions made (user choices, architectural decisions)
188
+ - Current phase and what's been completed
189
+ - Any pending questions or blockers
190
+ - Key context needed for continuity
191
+
192
+ Be concise — under 800 words. Use bullet points.
193
+
194
+ --- HISTORY ---
195
+ ${olderText}
196
+ --- END HISTORY ---`;
197
+
198
+ return new Promise((resolve) => {
199
+ const child = execFile(getClaudeBin(), [
200
+ "--dangerously-skip-permissions",
201
+ "--model", "haiku",
202
+ "-p", prompt,
203
+ ], {
204
+ timeout: 30_000,
205
+ encoding: "utf-8",
206
+ env: { ...process.env, CLAUDECODE: undefined },
207
+ }, (err, stdout) => {
208
+ if (!err && stdout.trim().length > 0) {
209
+ resolve(stdout.trim());
210
+ return;
211
+ }
212
+ if (err) console.error("[operator] Haiku summarization failed:", err.message);
213
+ // Fallback: truncate older history to fit
214
+ const budget = HISTORY_CHAR_LIMIT - 2000;
215
+ resolve(olderText.slice(-budget) + "\n[...earlier history truncated]");
216
+ });
217
+ });
218
+ }
219
+
220
+ /**
221
+ * Assemble a summary of active agents.
222
+ */
223
+ export function assembleActiveAgents(featureId) {
224
+ const agents = queries.getRunningAgents(featureId);
225
+ if (!agents.length) return "(no agents running)";
226
+
227
+ return agents.map(a => {
228
+ const elapsed = a.started_at
229
+ ? Math.round((Date.now() - new Date(a.started_at + "Z").getTime()) / 60000)
230
+ : "?";
231
+ return `- ${a.agent_key} (${a.agent_type}, ${a.status}, ${elapsed}min)`;
232
+ }).join("\n");
233
+ }
234
+
235
+ /**
236
+ * Assemble pending decision info.
237
+ */
238
+ export function assemblePendingDecision(featureId) {
239
+ const decision = queries.getPendingDecision(featureId);
240
+ if (!decision) return null;
241
+
242
+ const options = JSON.parse(decision.options || "[]");
243
+ const optionsList = options.map(o => ` - ${o.label}${o.description ? `: ${o.description}` : ""}`).join("\n");
244
+
245
+ return `PENDING DECISION: ${decision.title}
246
+ Type: ${decision.decision_type}
247
+ Context: ${decision.context_text || "(none)"}
248
+ Options:
249
+ ${optionsList}
250
+ Permalink: ${decision.permalink || "(not yet posted)"}`;
251
+ }
252
+
253
+ /**
254
+ * Parse Operator response for structured blocks.
255
+ * Extracts [DECISION], [ROUTE:agent_key], and [RESOLVE_DECISION] blocks.
256
+ *
257
+ * @param {string} responseText - Raw operator response
258
+ * @returns {{ plainText: string, decisions: Array, routes: Array, resolutions: Array }}
259
+ */
260
+ export function parseOperatorResponse(responseText) {
261
+ const decisions = [];
262
+ const routes = [];
263
+ const resolutions = [];
264
+
265
+ // Extract [RESOLVE_DECISION] blocks
266
+ let text = responseText.replace(
267
+ /\[RESOLVE_DECISION\]([\s\S]*?)\[\/RESOLVE_DECISION\]/gi,
268
+ (match, content) => {
269
+ const resolution = parseResolutionBlock(content.trim());
270
+ if (resolution) resolutions.push(resolution);
271
+ return "";
272
+ }
273
+ );
274
+
275
+ // Extract [DECISION] blocks (also accepts legacy [SLACK:decision] for backward compatibility)
276
+ text = text.replace(
277
+ /\[(?:DECISION|SLACK:decision)\]([\s\S]*?)\[\/(?:DECISION|SLACK:decision)\]/gi,
278
+ (match, content) => {
279
+ const decision = parseDecisionBlock(content.trim());
280
+ if (decision) decisions.push(decision);
281
+ return "";
282
+ }
283
+ );
284
+
285
+ // Extract [ROUTE:agent_key] blocks
286
+ text = text.replace(
287
+ /\[ROUTE:([^\]]+)\]\s*([\s\S]*?)(?=\[ROUTE:|$)/gi,
288
+ (match, agentKey, content) => {
289
+ routes.push({ agentKey: agentKey.trim(), content: content.trim() });
290
+ return "";
291
+ }
292
+ );
293
+
294
+ return {
295
+ plainText: text.trim(),
296
+ decisions,
297
+ routes,
298
+ resolutions,
299
+ };
300
+ }
301
+
302
+ /**
303
+ * Parse a [RESOLVE_DECISION] block.
304
+ * Format:
305
+ * id: <decision-id>
306
+ * option: <selected-option-id>
307
+ * feedback: <optional feedback text>
308
+ */
309
+ function parseResolutionBlock(content) {
310
+ const resolution = {};
311
+ for (const line of content.split("\n")) {
312
+ const trimmed = line.trim();
313
+ if (!trimmed) continue;
314
+ if (trimmed.startsWith("id:")) {
315
+ resolution.id = trimmed.replace("id:", "").trim();
316
+ } else if (trimmed.startsWith("option:")) {
317
+ resolution.option = trimmed.replace("option:", "").trim();
318
+ } else if (trimmed.startsWith("feedback:")) {
319
+ resolution.feedback = trimmed.replace("feedback:", "").trim();
320
+ }
321
+ }
322
+ return resolution.id && resolution.option ? resolution : null;
323
+ }
324
+
325
+ /**
326
+ * Parse a decision block's YAML-like content.
327
+ */
328
+ function parseDecisionBlock(content) {
329
+ const lines = content.split("\n");
330
+ const decision = { options: [] };
331
+
332
+ let inOptions = false;
333
+
334
+ for (const line of lines) {
335
+ const trimmed = line.trim();
336
+ if (!trimmed) continue;
337
+
338
+ if (trimmed.startsWith("id:")) {
339
+ decision.id = trimmed.replace("id:", "").trim();
340
+ } else if (trimmed.startsWith("type:")) {
341
+ decision.type = trimmed.replace("type:", "").trim();
342
+ } else if (trimmed.startsWith("title:")) {
343
+ decision.title = trimmed.replace("title:", "").trim();
344
+ } else if (trimmed.startsWith("context:")) {
345
+ decision.context = trimmed.replace("context:", "").trim();
346
+ } else if (trimmed.startsWith("options:")) {
347
+ inOptions = true;
348
+ } else if (inOptions && trimmed.startsWith("-")) {
349
+ const optLine = trimmed.replace(/^-\s*/, "");
350
+ const opt = {};
351
+ for (const part of optLine.split(",")) {
352
+ const [key, ...val] = part.split(":");
353
+ if (key && val.length) {
354
+ opt[key.trim()] = val.join(":").trim();
355
+ }
356
+ }
357
+ if (opt.id && opt.label) {
358
+ decision.options.push(opt);
359
+ }
360
+ }
361
+ }
362
+
363
+ return decision.id && decision.title ? decision : null;
364
+ }