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
@@ -0,0 +1,360 @@
1
+ // review-sessions.js — Manages interactive review tool sessions for decisions.
2
+ // Loosely coupled: uses iriai-feedback CLI (child_process.spawn) + HTTP API.
3
+ // No direct imports from iriai-feedback.
4
+
5
+ import { spawn } from "node:child_process";
6
+ import { createServer } from "node:http";
7
+ import { get as httpGet } from "node:http";
8
+ import { readFile } from "node:fs/promises";
9
+ import * as queries from "./queries.js";
10
+ import { get as configGet } from "../cli/config.js";
11
+
12
+ const QA_FEEDBACK_BIN = configGet("qa_feedback_bin", "QA_FEEDBACK_BIN") || "iriai-feedback";
13
+ const PORT_MIN = 9001;
14
+ const PORT_MAX = 9020;
15
+
16
+ export class ReviewSessionManager {
17
+ constructor() {
18
+ this._sessions = new Map(); // decisionId -> { sessionId, url, port, process, type, qa? }
19
+ this._nextPort = PORT_MIN;
20
+ }
21
+
22
+ _allocPort() {
23
+ const usedPorts = new Set(
24
+ [...this._sessions.values()].flatMap(s =>
25
+ [s.port, s.qa?.port].filter(Boolean)
26
+ )
27
+ );
28
+ let port = this._nextPort;
29
+ while (usedPorts.has(port)) {
30
+ port = port >= PORT_MAX ? PORT_MIN : port + 1;
31
+ }
32
+ this._nextPort = port >= PORT_MAX ? PORT_MIN : port + 1;
33
+ return port;
34
+ }
35
+
36
+ /**
37
+ * Start a doc review session for a decision.
38
+ * Spawns `iriai-feedback review <docPath> --port <port>`.
39
+ * Returns the review URL.
40
+ */
41
+ async startDocReview(decisionId, docPath, title, { featureId, port: preferredPort } = {}) {
42
+ const port = preferredPort || this._allocPort();
43
+ const args = ["review", docPath, "--port", String(port)];
44
+ if (title) args.push("--title", title);
45
+
46
+ try {
47
+ const { proc, sessionId } = await this._spawnAndParse(args);
48
+ const url = `http://localhost:${port}/doc-review`;
49
+ this._sessions.set(decisionId, { sessionId, url, port, process: proc, type: "doc", docPath });
50
+
51
+ // Persist to SQLite for restart recovery
52
+ if (featureId) {
53
+ queries.insertReviewSession({
54
+ decisionId, featureId, sessionId, port, docPath, type: "doc",
55
+ });
56
+ }
57
+
58
+ return url;
59
+ } catch (err) {
60
+ console.error(`[review-sessions] Failed to start doc review for ${decisionId}:`, err.message);
61
+ return null;
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Start a mockup review session — serves an HTML file via a static server,
67
+ * then wraps it with the QA feedback proxy (screenshot + annotation overlay).
68
+ * Returns the QA proxy URL.
69
+ */
70
+ async startMockupReview(decisionId, htmlPath, { featureId } = {}) {
71
+ // 1. Spin up a tiny static file server for the HTML mockup
72
+ const staticPort = this._allocPort();
73
+ const staticServer = await this._startStaticServer(htmlPath, staticPort);
74
+
75
+ // 2. Wrap it with QA proxy
76
+ const qaUrl = await this.startQaSession(decisionId, `http://localhost:${staticPort}`, {
77
+ featureId,
78
+ });
79
+
80
+ if (!qaUrl) {
81
+ staticServer.close();
82
+ return null;
83
+ }
84
+
85
+ // Track the static server so we can kill it on stop()
86
+ const session = this._sessions.get(decisionId);
87
+ if (session) {
88
+ session._staticServer = staticServer;
89
+ session._staticPort = staticPort;
90
+ session.docPath = htmlPath;
91
+ session.type = "mockup";
92
+ }
93
+
94
+ // Persist as mockup type for restore
95
+ if (featureId) {
96
+ queries.insertReviewSession({
97
+ decisionId, featureId, sessionId: session?.sessionId || `mockup-${Date.now()}`,
98
+ port: session?.qa?.port || staticPort, docPath: htmlPath, type: "mockup",
99
+ });
100
+ }
101
+
102
+ return qaUrl;
103
+ }
104
+
105
+ /**
106
+ * Start a minimal static file server that serves a single HTML file at /.
107
+ */
108
+ _startStaticServer(htmlPath, port) {
109
+ return new Promise((resolve, reject) => {
110
+ const server = createServer(async (req, res) => {
111
+ try {
112
+ const content = await readFile(htmlPath, "utf-8");
113
+ res.writeHead(200, { "Content-Type": "text/html" });
114
+ res.end(content);
115
+ } catch {
116
+ res.writeHead(404);
117
+ res.end("Not found");
118
+ }
119
+ });
120
+ server.listen(port, () => resolve(server));
121
+ server.on("error", reject);
122
+ });
123
+ }
124
+
125
+ /**
126
+ * Start a QA session (reverse proxy) for a running app.
127
+ * Spawns `iriai-feedback start <targetUrl> --port <port>`.
128
+ * Attaches to an existing decision entry if one exists.
129
+ */
130
+ async startQaSession(decisionId, targetUrl, { featureId, port: preferredPort } = {}) {
131
+ const port = preferredPort || this._allocPort();
132
+ const args = ["start", targetUrl, "--port", String(port)];
133
+
134
+ try {
135
+ const { proc, sessionId } = await this._spawnAndParse(args);
136
+ const qaUrl = `http://localhost:${port}`;
137
+ const existing = this._sessions.get(decisionId);
138
+
139
+ if (existing) {
140
+ existing.qa = { sessionId, url: qaUrl, port, process: proc };
141
+ } else {
142
+ this._sessions.set(decisionId, {
143
+ sessionId, url: qaUrl, port, process: proc, type: "qa",
144
+ });
145
+ }
146
+
147
+ // Persist QA session info
148
+ if (featureId) {
149
+ queries.updateReviewSessionQa(decisionId, { qaSessionId: sessionId, qaPort: port, qaTargetUrl: targetUrl });
150
+ }
151
+
152
+ return qaUrl;
153
+ } catch (err) {
154
+ console.error(`[review-sessions] Failed to start QA session for ${decisionId}:`, err.message);
155
+ return null;
156
+ }
157
+ }
158
+
159
+ /**
160
+ * Collect annotations from all active review servers for a decision.
161
+ * Returns array of annotations or null if none.
162
+ */
163
+ async collectFeedback(decisionId) {
164
+ const session = this._sessions.get(decisionId);
165
+ if (!session) return null;
166
+
167
+ const annotations = [];
168
+ if (session.port) {
169
+ const anns = await this._httpGetJson(session.port, "/__qa__/api/annotations");
170
+ if (anns && Array.isArray(anns)) annotations.push(...anns);
171
+ }
172
+ if (session.qa?.port) {
173
+ const anns = await this._httpGetJson(session.qa.port, "/__qa__/api/annotations");
174
+ if (anns && Array.isArray(anns)) annotations.push(...anns);
175
+ }
176
+
177
+ return annotations.length > 0 ? annotations : null;
178
+ }
179
+
180
+ /**
181
+ * Stop a review session: kill processes, remove from map and SQLite.
182
+ */
183
+ async stop(decisionId) {
184
+ const session = this._sessions.get(decisionId);
185
+ if (!session) return;
186
+
187
+ this._killProc(session.process);
188
+ if (session.qa) this._killProc(session.qa.process);
189
+ if (session._staticServer) {
190
+ try { session._staticServer.close(); } catch { /* ok */ }
191
+ }
192
+ this._sessions.delete(decisionId);
193
+
194
+ // Clean up SQLite
195
+ queries.deleteReviewSession(decisionId);
196
+ }
197
+
198
+ /**
199
+ * Stop all active review sessions. Called on shutdown.
200
+ */
201
+ async stopAll() {
202
+ for (const [id] of this._sessions) {
203
+ await this.stop(id);
204
+ }
205
+ }
206
+
207
+ /**
208
+ * Re-spawn review sessions from SQLite on restart.
209
+ * Called during recovery for features with pending decisions.
210
+ */
211
+ async restoreSession(decisionId) {
212
+ const row = queries.getReviewSession(decisionId);
213
+ if (!row) return null;
214
+
215
+ // Mockup sessions: restore via QA proxy (not doc-review)
216
+ if (row.type === "mockup" && row.doc_path) {
217
+ return this.startMockupReview(decisionId, row.doc_path, {
218
+ featureId: row.feature_id,
219
+ });
220
+ }
221
+
222
+ // Re-spawn the doc review server with the same port and doc path
223
+ const url = await this.startDocReview(decisionId, row.doc_path, null, {
224
+ featureId: row.feature_id,
225
+ port: row.port,
226
+ });
227
+
228
+ // Re-spawn QA session if one was active
229
+ if (row.qa_target_url && row.qa_port) {
230
+ await this.startQaSession(decisionId, row.qa_target_url, {
231
+ featureId: row.feature_id,
232
+ port: row.qa_port,
233
+ });
234
+ }
235
+
236
+ return url;
237
+ }
238
+
239
+ /**
240
+ * Check if a decision has an active review session.
241
+ */
242
+ hasSession(decisionId) {
243
+ return this._sessions.has(decisionId);
244
+ }
245
+
246
+ // ─── Internal helpers ────────────────────────────────────────────────
247
+
248
+ /**
249
+ * Spawn the iriai-feedback CLI and parse stdout for session info.
250
+ * Expects the CLI to print a line like: "Session: <id>" and "URL: <url>"
251
+ * within the first few seconds of output.
252
+ */
253
+ _spawnAndParse(args) {
254
+ return new Promise((resolve, reject) => {
255
+ const proc = spawn(QA_FEEDBACK_BIN, args, {
256
+ stdio: ["ignore", "pipe", "pipe"],
257
+ detached: false,
258
+ });
259
+
260
+ let stdout = "";
261
+ let resolved = false;
262
+
263
+ const timer = setTimeout(() => {
264
+ if (!resolved) {
265
+ resolved = true;
266
+ // Even if we didn't parse a session ID, the server may be running
267
+ resolve({ proc, sessionId: `session-${Date.now()}` });
268
+ }
269
+ }, 5000);
270
+
271
+ proc.stdout.on("data", (chunk) => {
272
+ stdout += chunk.toString();
273
+ // Wait for both session ID and URL confirmation (server is listening)
274
+ // CLI outputs: "Doc review session <id> started\nReview: http://..."
275
+ // or: "QA session <id> started\nLocal: http://..."
276
+ const sessionMatch = stdout.match(/[Ss]ession\s+(\S+)\s+started/);
277
+ const urlReady = /(?:Review|Local):\s+http/i.test(stdout);
278
+ if (sessionMatch && urlReady && !resolved) {
279
+ resolved = true;
280
+ clearTimeout(timer);
281
+ resolve({ proc, sessionId: sessionMatch[1] });
282
+ }
283
+ });
284
+
285
+ proc.on("error", (err) => {
286
+ if (!resolved) {
287
+ resolved = true;
288
+ clearTimeout(timer);
289
+ reject(err);
290
+ }
291
+ });
292
+
293
+ proc.on("exit", (code) => {
294
+ if (!resolved) {
295
+ resolved = true;
296
+ clearTimeout(timer);
297
+ reject(new Error(`iriai-feedback exited with code ${code} before producing session ID`));
298
+ }
299
+ });
300
+ });
301
+ }
302
+
303
+ /**
304
+ * Simple HTTP GET returning parsed JSON.
305
+ */
306
+ _httpGetJson(port, urlPath) {
307
+ return new Promise((resolve) => {
308
+ const req = httpGet(`http://localhost:${port}${urlPath}`, (res) => {
309
+ let data = "";
310
+ res.on("data", (chunk) => { data += chunk; });
311
+ res.on("end", () => {
312
+ try {
313
+ resolve(JSON.parse(data));
314
+ } catch {
315
+ resolve(null);
316
+ }
317
+ });
318
+ });
319
+ req.on("error", () => resolve(null));
320
+ req.setTimeout(3000, () => { req.destroy(); resolve(null); });
321
+ });
322
+ }
323
+
324
+ _killProc(proc) {
325
+ if (!proc) return;
326
+ try {
327
+ proc.kill("SIGINT");
328
+ } catch {
329
+ // Process may already be dead
330
+ }
331
+ }
332
+ }
333
+
334
+ /**
335
+ * Format structured annotations into readable feedback text for agents.
336
+ */
337
+ export function formatAnnotationsAsFeedback(annotations, userComment) {
338
+ const parts = [];
339
+
340
+ if (annotations && annotations.length > 0) {
341
+ parts.push("## User Annotations (from interactive review)");
342
+ for (let i = 0; i < annotations.length; i++) {
343
+ const ann = annotations[i];
344
+ const location = ann.line
345
+ ? `[Line ${ann.line}]`
346
+ : ann.section
347
+ ? `[Section: ${ann.section}]`
348
+ : "[General]";
349
+ parts.push(`${i + 1}. ${location} "${ann.text || ann.comment || ann.content || ""}"`);
350
+ }
351
+ }
352
+
353
+ if (userComment) {
354
+ parts.push("");
355
+ parts.push("## User Comment");
356
+ parts.push(userComment);
357
+ }
358
+
359
+ return parts.join("\n");
360
+ }
@@ -0,0 +1,50 @@
1
+ # Accessibility Auditor
2
+
3
+ You are the Accessibility Auditor. You verify WCAG compliance and usability for assistive technologies. You assume the UI is inaccessible until proven otherwise.
4
+
5
+ ## Constraints
6
+ - NEVER modify source code — report findings only
7
+ - Check keyboard navigation, screen reader behavior, color contrast, focus management
8
+ - Every interactive element needs an accessible name
9
+ - Dynamic content changes need ARIA live regions
10
+ - Modals/dialogs need proper focus trapping
11
+
12
+ ## Adversarial Stance
13
+ Assume the UI is inaccessible. Check: missing alt text, unlabeled inputs, broken tab order, missing ARIA attributes, insufficient contrast, missing focus indicators. If you can't verify accessibility, it fails.
14
+
15
+ ## Input
16
+ Your task arrives as a `.task` file with YAML frontmatter:
17
+ - `review_focus` — accessibility-critical areas
18
+ - `scope.read` — component files to audit
19
+
20
+ ## Output
21
+ Write a structured verdict to `.output` with YAML frontmatter:
22
+ ```yaml
23
+ task_id: [id]
24
+ role: accessibility-auditor
25
+ verdict: PASS|FAIL|CONDITIONAL
26
+ summary_oneliner: "[counts]"
27
+ checks:
28
+ - criterion: "[WCAG criterion]"
29
+ result: PASS|FAIL
30
+ detail: "[evidence]"
31
+ issues:
32
+ - severity: blocker|major|minor|nit
33
+ description: "[accessibility issue]"
34
+ file: "[path]"
35
+ line: [number]
36
+ duration_seconds: [elapsed]
37
+ ```
38
+ Then signal completion: `echo DONE > .done`
39
+
40
+
41
+ ## Context Management — MANDATORY
42
+
43
+ **Read:** `reference/context-management.md` for the full protocol.
44
+
45
+ Monitor your context usage. **At 40% context remaining, you MUST:**
46
+ 1. Stop all current work — do not start new operations
47
+ 2. Write a structured `.handover` file to your signal directory with: completed work, current state, remaining work, files modified, and key decisions
48
+ 3. Signal: `echo "context_threshold" > $SIGNAL_DIR/.needs-restart`
49
+
50
+ Do NOT try to finish "one more thing." Do NOT signal `.done` — the task is not done. The wrapper script will restart you with your handover context preserved. A premature handover costs 30 seconds. A late handover costs all your work.
@@ -0,0 +1,40 @@
1
+ # Analytics Engineer
2
+
3
+ You are the Analytics Engineer. You add instrumentation and metrics tracking to new features.
4
+
5
+ ## Constraints
6
+ - ONLY modify files listed in `scope.modify`
7
+ - Track business events (user actions), not implementation details
8
+ - Event names: `snake_case`, namespaced by feature (e.g., `bot_collaborator.invited`)
9
+ - Include relevant context in event properties (user_id, app_id) but NEVER PII
10
+ - Use existing analytics patterns in the codebase — don't introduce new libraries
11
+
12
+ ## Input
13
+ Your task arrives as a `.task` file with YAML frontmatter:
14
+ - `scope.modify` — only touch these files
15
+ - `acceptance.user_criteria` — what events to track
16
+ - `context_files` — existing analytics patterns to follow
17
+
18
+ ## Output
19
+ Write a structured summary to `.output` with YAML frontmatter:
20
+ ```yaml
21
+ task_id: [id]
22
+ role: analytics-engineer
23
+ summary_oneliner: "[one line]"
24
+ files_created: [list]
25
+ files_modified: [list]
26
+ duration_seconds: [elapsed]
27
+ ```
28
+ Then signal completion: `echo DONE > .done`
29
+
30
+
31
+ ## Context Management — MANDATORY
32
+
33
+ **Read:** `reference/context-management.md` for the full protocol.
34
+
35
+ Monitor your context usage. **At 40% context remaining, you MUST:**
36
+ 1. Stop all current work — do not start new operations
37
+ 2. Write a structured `.handover` file to your signal directory with: completed work, current state, remaining work, files modified, and key decisions
38
+ 3. Signal: `echo "context_threshold" > $SIGNAL_DIR/.needs-restart`
39
+
40
+ Do NOT try to finish "one more thing." Do NOT signal `.done` — the task is not done. The wrapper script will restart you with your handover context preserved. A premature handover costs 30 seconds. A late handover costs all your work.