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,179 @@
1
+ // slack-helpers.js โ€” Slack-specific helper functions.
2
+ // Markdown conversion, Block Kit builders, media upload.
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+
7
+ /**
8
+ * Convert standard Markdown to Slack mrkdwn format.
9
+ */
10
+ export function markdownToMrkdwn(text) {
11
+ if (!text) return text;
12
+
13
+ const codeBlocks = [];
14
+ let result = text.replace(/```[\s\S]*?```/g, (match) => {
15
+ codeBlocks.push(match);
16
+ return `\x00CODE${codeBlocks.length - 1}\x00`;
17
+ });
18
+
19
+ const inlineCode = [];
20
+ result = result.replace(/`[^`]+`/g, (match) => {
21
+ inlineCode.push(match);
22
+ return `\x00INLINE${inlineCode.length - 1}\x00`;
23
+ });
24
+
25
+ // Headers -> bold
26
+ result = result.replace(/^#{1,6}\s+(.+)$/gm, "*$1*");
27
+
28
+ // Bold: **text** -> *text*
29
+ result = result.replace(/\*\*(.+?)\*\*/g, "*$1*");
30
+
31
+ // Links: [text](url) -> <url|text>
32
+ result = result.replace(/\[([^\]]+)\]\(([^)]+)\)/g, "<$2|$1>");
33
+
34
+ // Image refs: ![alt](path) -> [img:path]
35
+ result = result.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, "[img:$2]");
36
+
37
+ // Restore inline code
38
+ result = result.replace(/\x00INLINE(\d+)\x00/g, (_, i) => inlineCode[i]);
39
+
40
+ // Restore code blocks
41
+ result = result.replace(/\x00CODE(\d+)\x00/g, (_, i) => codeBlocks[i]);
42
+
43
+ return result;
44
+ }
45
+
46
+ function isMediaPathAllowed(filePath) {
47
+ const resolved = path.resolve(filePath);
48
+ const ALLOWED_DIRS = [
49
+ path.resolve("/Users/danielzhang/src/iriai"),
50
+ "/tmp",
51
+ "/private/tmp",
52
+ ];
53
+ return ALLOWED_DIRS.some(dir => resolved.startsWith(dir + path.sep) || resolved === dir);
54
+ }
55
+
56
+ /**
57
+ * Upload a binary file as a Slack attachment.
58
+ */
59
+ export async function uploadGifAttachment(web, channel, thread_ts, filePath, title) {
60
+ try {
61
+ if (!isMediaPathAllowed(filePath)) {
62
+ console.warn(`[uploadGifAttachment] Path not in allowed directories: ${filePath}`);
63
+ return false;
64
+ }
65
+
66
+ if (!fs.existsSync(filePath)) {
67
+ console.warn(`[bridge] Media file not found: ${filePath}`);
68
+ return false;
69
+ }
70
+
71
+ const stats = fs.statSync(filePath);
72
+ if (stats.size > 20 * 1024 * 1024) {
73
+ console.warn(`[bridge] Media too large (${(stats.size / 1024 / 1024).toFixed(1)}MB): ${filePath}`);
74
+ return false;
75
+ }
76
+
77
+ const fileBuffer = fs.readFileSync(filePath);
78
+ await web.filesUploadV2({
79
+ channel_id: channel,
80
+ thread_ts,
81
+ file: fileBuffer,
82
+ filename: path.basename(filePath),
83
+ title: title || path.basename(filePath),
84
+ });
85
+ return true;
86
+ } catch (err) {
87
+ console.error(`[bridge] Media upload failed for ${filePath}:`, err.message);
88
+ return false;
89
+ }
90
+ }
91
+
92
+ export async function uploadArtifact(web, channel, thread_ts, filePath, title) {
93
+ try {
94
+ const content = fs.readFileSync(filePath, "utf-8");
95
+ await web.filesUploadV2({
96
+ channel_id: channel,
97
+ thread_ts,
98
+ content,
99
+ filename: path.basename(filePath),
100
+ title,
101
+ });
102
+ } catch (err) {
103
+ console.error(`Error uploading artifact ${title}:`, err.message);
104
+ try {
105
+ const content = fs.readFileSync(filePath, "utf-8");
106
+ const truncated =
107
+ content.length > 3000
108
+ ? content.slice(0, 3000) + "\n\n_(truncated -- full document in repo)_"
109
+ : content;
110
+ await postToThread(web, channel, thread_ts, `*${title}:*\n\n${truncated}`);
111
+ } catch (fallbackErr) {
112
+ console.error(`Fallback text post also failed for ${title}:`, fallbackErr.message);
113
+ }
114
+ }
115
+ }
116
+
117
+ /**
118
+ * Post a message to a Slack thread with mrkdwn conversion.
119
+ */
120
+ export async function postToThread(web, channel, thread_ts, text) {
121
+ await web.chat.postMessage({ channel, thread_ts, text: markdownToMrkdwn(text), mrkdwn: true });
122
+ }
123
+
124
+ /**
125
+ * Build Slack Block Kit blocks for a decision with action buttons.
126
+ */
127
+ export function buildDecisionBlocks(decisionId, title, context, options) {
128
+ const blocks = [];
129
+
130
+ blocks.push({
131
+ type: "section",
132
+ text: { type: "mrkdwn", text: `*${title}*${context ? `\n${context}` : ""}` },
133
+ });
134
+
135
+ blocks.push({
136
+ type: "actions",
137
+ block_id: `decision_${decisionId}`,
138
+ elements: options.map(opt => ({
139
+ type: "button",
140
+ text: { type: "plain_text", text: opt.label, emoji: true },
141
+ value: opt.id,
142
+ action_id: `decision_${decisionId}_${opt.id}`,
143
+ ...(opt.style === "primary" ? { style: "primary" } : {}),
144
+ ...(opt.style === "danger" ? { style: "danger" } : {}),
145
+ })),
146
+ });
147
+
148
+ return blocks;
149
+ }
150
+
151
+ /**
152
+ * Build resolved-state blocks to replace buttons after a decision is made.
153
+ */
154
+ export function buildResolvedBlocks(title, selectedLabel, resolvedBy, feedback = "") {
155
+ const feedbackLine = feedback ? `\n> ${feedback.replace(/\n/g, "\n> ")}` : "";
156
+ return [{
157
+ type: "section",
158
+ text: {
159
+ type: "mrkdwn",
160
+ text: `*${title}*\n~Resolved~: *${selectedLabel}*${resolvedBy ? ` by <@${resolvedBy}>` : ""}${feedbackLine}`,
161
+ },
162
+ }];
163
+ }
164
+
165
+ export async function addReaction(web, channel, timestamp, reaction) {
166
+ try {
167
+ await web.reactions.add({ channel, name: reaction, timestamp });
168
+ } catch {
169
+ // Ignore -- reaction may already exist
170
+ }
171
+ }
172
+
173
+ export async function removeReaction(web, channel, timestamp, reaction) {
174
+ try {
175
+ await web.reactions.remove({ channel, name: reaction, timestamp });
176
+ } catch {
177
+ // Ignore
178
+ }
179
+ }
@@ -0,0 +1,249 @@
1
+ // terminal-adapter.js โ€” TerminalAdapter implementing InterfaceAdapter.
2
+ // For terminal-based (iriai-build CLI) development workflows.
3
+ //
4
+ // postDecision() routes decisions through the Operator agent. The Operator
5
+ // presents the decision conversationally and resolves it via [RESOLVE_DECISION].
6
+ // This matches Slack's async model โ€” no blocking prompts on stdin.
7
+
8
+ import fs from "node:fs";
9
+ import path from "node:path";
10
+ import { EventEmitter } from "node:events";
11
+ import * as queries from "../queries.js";
12
+ import { InterfaceAdapter } from "./interface.js";
13
+ import { ensureDir, parseGifMarkers } from "../helpers.js";
14
+
15
+ export class TerminalAdapter extends InterfaceAdapter {
16
+ constructor({ logDir } = {}) {
17
+ super();
18
+ this.logDir = logDir || path.join(process.env.HOME, ".iriai", "bridge-logs");
19
+ ensureDir(this.logDir);
20
+ this._inputHandler = null;
21
+ this._events = new EventEmitter();
22
+ this._recentArtifacts = []; // tracks artifacts for inclusion in decision blocks
23
+ }
24
+
25
+ /**
26
+ * Set the input handler (TerminalInput instance) for interactive input.
27
+ */
28
+ setInputHandler(handler) {
29
+ this._inputHandler = handler;
30
+ }
31
+
32
+ /**
33
+ * Print a line, respecting active prompts (buffer if needed, stop spinner).
34
+ */
35
+ _print(text) {
36
+ if (this._inputHandler) {
37
+ this._inputHandler.printAboveLine(text);
38
+ } else {
39
+ console.log(text);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * Subscribe to adapter lifecycle events.
45
+ * Events: "plan-approved", "plan-rejected", "feature-complete"
46
+ */
47
+ on(event, listener) {
48
+ this._events.on(event, listener);
49
+ }
50
+
51
+ removeListener(event, listener) {
52
+ this._events.removeListener(event, listener);
53
+ }
54
+
55
+ async createFeatureChannel(featureId, slug) {
56
+ const featureLogDir = path.join(this.logDir, slug);
57
+ ensureDir(featureLogDir);
58
+ return slug; // opaque ref = slug
59
+ }
60
+
61
+ async postMessage(featureId, text) {
62
+ const feature = queries.getFeatureById(featureId);
63
+ const slug = feature?.slug || "unknown";
64
+ const C = { reset: "\x1b[0m", dim: "\x1b[2m" };
65
+ this._print(`${C.dim}[${slug}]${C.reset} ${text}`);
66
+ this._appendLog(slug, text);
67
+ return { ref: Date.now().toString() };
68
+ }
69
+
70
+ async postThreadMessage(featureId, text) {
71
+ await this.postMessage(featureId, text);
72
+ }
73
+
74
+ async postPipelineMessage(featureId, text) {
75
+ const feature = queries.getFeatureById(featureId);
76
+ const slug = feature?.slug || "unknown";
77
+ const C = { reset: "\x1b[0m", dim: "\x1b[2m", bold: "\x1b[1m", blue: "\x1b[34m", white: "\x1b[37m", bgBlue: "\x1b[44m" };
78
+ const msg = `${C.dim}[${slug}]${C.reset} ${C.bgBlue}${C.white} Pipeline ${C.reset} ${text}`;
79
+ this._print(msg);
80
+ this._appendLog(slug, `[Pipeline] ${text}`);
81
+ queries.insertEvent(featureId, "system", "bridge", text);
82
+ return { ref: Date.now().toString() };
83
+ }
84
+
85
+ async postAgentResponse(featureId, agentLabel, rawContent) {
86
+ const feature = queries.getFeatureById(featureId);
87
+ const slug = feature?.slug || "unknown";
88
+ const { text } = parseGifMarkers(rawContent);
89
+
90
+ const C = {
91
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
92
+ cyan: "\x1b[36m", yellow: "\x1b[33m", white: "\x1b[37m",
93
+ magenta: "\x1b[35m", blue: "\x1b[34m", green: "\x1b[32m",
94
+ };
95
+ const labelColor = agentLabel === "Operator" ? C.white : C.cyan;
96
+
97
+ // Colorize inline agent attribution tags like *[PM]* or [PM] in Operator relay text
98
+ const AGENT_TAG_COLORS = {
99
+ PM: C.cyan, Designer: C.magenta, Architect: C.blue,
100
+ "Plan Compiler": C.green, FL: C.yellow, "Feature Lead": C.yellow,
101
+ };
102
+ const colorized = text.replace(/\*?\[([^\]]+)\]\*?/g, (match, name) => {
103
+ const color = AGENT_TAG_COLORS[name];
104
+ if (color) return `${color}${C.bold}[${name}]${C.reset}`;
105
+ return match;
106
+ });
107
+
108
+ const msg = `${C.dim}[${slug}]${C.reset} ${labelColor}${C.bold}[${agentLabel}]${C.reset} ${colorized}`;
109
+ this._print(msg);
110
+ this._appendLog(slug, `[${agentLabel}] ${text}`);
111
+ queries.insertEvent(featureId, "agent-response", `agent:${agentLabel}`, rawContent);
112
+ return { ref: Date.now().toString() };
113
+ }
114
+
115
+ async uploadArtifact(featureId, filePath, title) {
116
+ const feature = queries.getFeatureById(featureId);
117
+ const slug = feature?.slug || "unknown";
118
+ // Track for inclusion in the next decision block
119
+ this._recentArtifacts.push({ title, filePath });
120
+ this._appendLog(slug, `Artifact: ${title} at ${filePath}`);
121
+ }
122
+
123
+ async postDecision(featureId, decision) {
124
+ const feature = queries.getFeatureById(featureId);
125
+ const slug = feature?.slug || "unknown";
126
+ const C = {
127
+ reset: "\x1b[0m", bold: "\x1b[1m", dim: "\x1b[2m",
128
+ yellow: "\x1b[33m", bgYellow: "\x1b[43m", black: "\x1b[30m",
129
+ white: "\x1b[37m", green: "\x1b[32m", red: "\x1b[31m",
130
+ cyan: "\x1b[36m",
131
+ };
132
+
133
+ const w = (this._inputHandler?._cols) || (process.stdout.columns || 80);
134
+ const rule = `${C.yellow}${"โ”€".repeat(w)}${C.reset}`;
135
+
136
+ this._print("");
137
+ this._print(rule);
138
+ this._print("");
139
+
140
+ // Title with badge
141
+ const badge = `${C.bgYellow}${C.black}${C.bold} ACTION REQUIRED ${C.reset}`;
142
+ this._print(` ${badge} ${C.bold}${decision.title}${C.reset}`);
143
+
144
+ // Context
145
+ if (decision.context) {
146
+ this._print(` ${C.dim}${decision.context}${C.reset}`);
147
+ }
148
+
149
+ // Review URLs
150
+ if (decision.reviewUrl || decision.qaUrl) {
151
+ this._print("");
152
+ this._print(` ${C.cyan}${C.bold}Review & Annotate:${C.reset}`);
153
+ if (decision.reviewUrl) {
154
+ this._print(` ${C.cyan}๐Ÿ“Ž Doc Review${C.reset} ${C.bold}${decision.reviewUrl}${C.reset}`);
155
+ }
156
+ if (decision.qaUrl) {
157
+ this._print(` ${C.cyan}๐Ÿงช QA Test${C.reset} ${C.bold}${decision.qaUrl}${C.reset}`);
158
+ }
159
+ this._print(` ${C.dim}Open in browser to review with annotation tools.${C.reset}`);
160
+ }
161
+
162
+ // Artifacts
163
+ const artifacts = this._recentArtifacts;
164
+ if (artifacts.length > 0) {
165
+ this._print("");
166
+ this._print(` ${C.cyan}${C.bold}Artifacts:${C.reset}`);
167
+ for (const a of artifacts) {
168
+ this._print(` ${C.cyan}๐Ÿ“Ž ${a.title}${C.reset} ${C.dim}${a.filePath}${C.reset}`);
169
+ }
170
+ }
171
+
172
+ this._print("");
173
+
174
+ // Options
175
+ for (const opt of decision.options) {
176
+ const color = opt.style === "primary" ? C.green : opt.style === "danger" ? C.red : C.white;
177
+ this._print(` ${color}${C.bold}โ–ธ ${opt.label}${C.reset} ${C.dim}type: ${C.bold}${opt.id}${C.reset}`);
178
+ }
179
+
180
+ this._print("");
181
+ this._print(` ${C.dim}Reply with an option id (e.g. "${decision.options[0]?.id}") or ask the Operator.${C.reset}`);
182
+ this._print("");
183
+ this._print(rule);
184
+ this._print("");
185
+
186
+ // Clear tracked artifacts after rendering
187
+ this._recentArtifacts = [];
188
+
189
+ this._appendLog(slug, `Decision: ${decision.title}`);
190
+ return { ref: decision.id };
191
+ }
192
+
193
+ async resolveDecisionMessage(featureId, messageRef, decisionId, selectedOption, selectedLabel, resolvedBy, feedback) {
194
+ const feature = queries.getFeatureById(featureId);
195
+ const slug = feature?.slug || "unknown";
196
+ const C = { reset: "\x1b[0m", dim: "\x1b[2m", green: "\x1b[32m", bold: "\x1b[1m" };
197
+ this._print(`${C.dim}[${slug}]${C.reset} ${C.green}${C.bold}โœ“ Decision resolved:${C.reset} ${selectedLabel}${feedback ? ` ${C.dim}(${feedback})${C.reset}` : ""}`);
198
+ this._appendLog(slug, `Decision "${decisionId}" resolved: ${selectedLabel}`);
199
+
200
+ // Clean up pending decision file
201
+ const decisionFile = path.join(this.logDir, slug, `.pending-decision-${decisionId}`);
202
+ try { fs.unlinkSync(decisionFile); } catch { /* ok */ }
203
+
204
+ // Emit lifecycle events for plan-approval decisions
205
+ if (decisionId === "plan-approval") {
206
+ const updatedFeature = queries.getFeatureById(featureId);
207
+ if (updatedFeature?.phase === "impl" || updatedFeature?.phase === "plan-approval") {
208
+ this._events.emit("plan-approved", featureId);
209
+ } else if (updatedFeature?.phase === "planning") {
210
+ this._events.emit("plan-rejected", featureId);
211
+ }
212
+ }
213
+ }
214
+
215
+ async postPlanForApproval(featureId, planDir) {
216
+ const feature = queries.getFeatureById(featureId);
217
+ const slug = feature?.slug || "unknown";
218
+ const C = { reset: "\x1b[0m", dim: "\x1b[2m", bold: "\x1b[1m", bgBlue: "\x1b[44m", white: "\x1b[37m" };
219
+ this._print(`${C.dim}[${slug}]${C.reset} ${C.bgBlue}${C.white} Pipeline ${C.reset} ${C.bold}Plan ready for approval.${C.reset} ${C.dim}Artifacts: ${planDir}${C.reset}`);
220
+ this._appendLog(slug, `Plan ready for approval. Artifacts in: ${planDir}`);
221
+
222
+ return await this.postDecision(featureId, {
223
+ id: "plan-approval",
224
+ title: "Plan ready for approval",
225
+ context: "All planning phases complete. Review the artifacts.",
226
+ options: [
227
+ { id: "approve", label: "Approve Plan", style: "primary" },
228
+ { id: "reject", label: "Reject Plan", style: "danger" },
229
+ ],
230
+ });
231
+ }
232
+
233
+ async postFeatureComplete(featureId) {
234
+ const feature = queries.getFeatureById(featureId);
235
+ const slug = feature?.slug || "unknown";
236
+ const C = { reset: "\x1b[0m", bold: "\x1b[1m", green: "\x1b[32m", bgGreen: "\x1b[42m", white: "\x1b[37m" };
237
+ this._print(`\n${C.bgGreen}${C.white}${C.bold} COMPLETE ${C.reset} ${C.green}${C.bold}Feature ${slug} โ€” all gates passed!${C.reset}\n`);
238
+ this._appendLog(slug, "Feature complete! All gates passed.");
239
+ this._events.emit("feature-complete", featureId);
240
+ }
241
+
242
+ _appendLog(slug, text) {
243
+ const logFile = path.join(this.logDir, slug, "bridge.log");
244
+ const timestamp = new Date().toISOString();
245
+ try {
246
+ fs.appendFileSync(logFile, `[${timestamp}] ${text}\n`);
247
+ } catch { /* log dir may not exist yet */ }
248
+ }
249
+ }