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/bridge.db ADDED
File without changes
@@ -0,0 +1,170 @@
1
+ // constants.js — Paths, timeouts, signal file names for bridge v3.
2
+ // Ported from lib/constants.js with DB_PATH addition.
3
+
4
+ import path from "node:path";
5
+
6
+ const HOME = process.env.HOME;
7
+
8
+ // ─── Base Directories ────────────────────────────────────────────────────────
9
+
10
+ export const IMPL_BASE =
11
+ process.env.IMPL_SIGNAL_BASE ||
12
+ path.join(HOME, "src/iriai/.implementation");
13
+
14
+ export const IRIAI_TEAM_DIR =
15
+ process.env.IRIAI_TEAM_DIR ||
16
+ path.join(HOME, "src/iriai/iriai-team");
17
+
18
+ export const V3_ROLES_DIR = path.join(
19
+ path.dirname(new URL(import.meta.url).pathname),
20
+ "roles",
21
+ );
22
+
23
+ export const SCRIPTS_DIR = path.join(IRIAI_TEAM_DIR, "scripts");
24
+
25
+ export const DASHBOARD_LOG = path.join(IRIAI_TEAM_DIR, ".dashboard-log");
26
+ export const DASHBOARD_FILE = path.join(IRIAI_TEAM_DIR, "DASHBOARD.md");
27
+ export const FEATURE_STATUS_FILE = path.join(IRIAI_TEAM_DIR, "FEATURE-STATUS.md");
28
+
29
+ // ─── Database ───────────────────────────────────────────────────────────────
30
+
31
+ export const DB_PATH =
32
+ process.env.BRIDGE_DB_PATH ||
33
+ path.join(HOME, ".iriai", "bridge-v3.db");
34
+
35
+ // ─── Planning Roles ─────────────────────────────────────────────────────────
36
+
37
+ export const PLANNING_ROLES = ["pm", "designer", "architect", "plan-compiler"];
38
+
39
+ export const PLANNING_ROLE_LABELS = {
40
+ pm: "PM",
41
+ designer: "Designer",
42
+ architect: "Architect",
43
+ "plan-compiler": "Plan Compiler",
44
+ };
45
+
46
+ export const ROLE_LABELS = {
47
+ ...PLANNING_ROLE_LABELS,
48
+ lead: "Feature Lead",
49
+ };
50
+
51
+ export const PIPELINE_ORDER = ["pm", "designer", "architect", "plan-compiler"];
52
+
53
+ // ─── Timeouts (milliseconds) ────────────────────────────────────────────────
54
+
55
+ export const ROLE_HARD_TIMEOUT_MS = 75 * 60 * 1000; // 75 min
56
+ export const ROLE_SOFT_TIMEOUT_MS = 15 * 60 * 1000; // 15 min
57
+ export const ORCH_SOFT_TIMEOUT_MS = 45 * 60 * 1000; // 45 min
58
+ export const PLANNING_TIMEOUT_MS = 30 * 60 * 1000; // 30 min
59
+ export const OPERATOR_TIMEOUT_MS = 2 * 60 * 1000; // 2 min
60
+ export const FL_SOFT_TIMEOUT_MS = 45 * 60 * 1000; // 45 min
61
+ export const FL_CONTEXT_EXHAUST_MS = 5 * 60 * 1000; // 5 min
62
+
63
+ export const OPERATOR_RELAY_TIMEOUT_MS = 90_000; // 90s
64
+ export const RSS_CEILING_KB = 8_388_608; // 8 GB
65
+ export const STUCK_THRESHOLD_MS = 10 * 60 * 1000; // 10 min
66
+ export const HEALTH_CHECK_INTERVAL_MS = 30_000; // 30s
67
+ export const STALE_SCAN_INTERVAL_MS = 5 * 60 * 1000; // 5 min
68
+
69
+ // ─── Retry Limits ────────────────────────────────────────────────────────────
70
+
71
+ export const MAX_ROLE_RETRIES = 3;
72
+ export const MAX_ORCH_RETRIES = 3;
73
+ export const MAX_FL_RETRIES = 5;
74
+ export const MAX_FL_INIT_RETRIES = 3;
75
+ export const MAX_PLANNING_RETRIES = 5;
76
+ export const MAX_OPERATOR_RETRIES = 3;
77
+
78
+ // ─── History Limits ─────────────────────────────────────────────────────────
79
+
80
+ export const HISTORY_CHAR_LIMIT = 6000; // Max chars for history in operator prompt
81
+ export const HISTORY_RECENT_EVENTS = 10; // Recent events kept verbatim (not summarized)
82
+ export const HISTORY_FULL_BACKUP_EVENTS = 100; // Full backup event count
83
+
84
+ // ─── Artifact Summarizer ────────────────────────────────────────────────────
85
+
86
+ export const ARTIFACT_SUMMARY_THRESHOLD_KB = 30; // Summarize plan dir if total artifacts > 30 KB
87
+ export const ARTIFACT_SUMMARY_FILE = ".artifact-summary.md";
88
+ export const SUMMARIZER_TIMEOUT_MS = 90_000; // 90s — summarizer should be fast
89
+ export const SUMMARIZER_MODEL = "sonnet";
90
+
91
+ // ─── Backoff (seconds) ──────────────────────────────────────────────────────
92
+
93
+ export const FAST_EXIT_THRESHOLD_MS = 15_000;
94
+ export const FAST_EXIT_BACKOFF_S = 30;
95
+ export const NORMAL_BACKOFF_S = 5;
96
+ export const FL_NORMAL_BACKOFF_S = 10;
97
+
98
+ // ─── Signal File Names ──────────────────────────────────────────────────────
99
+
100
+ export const SIGNAL = {
101
+ TASK: ".task",
102
+ ACTIVE_TASK: ".active-task",
103
+ DONE: ".done",
104
+ OUTPUT: ".output",
105
+ AGENT_RESPONSE: ".agent-response",
106
+ USER_MESSAGE: ".user-message",
107
+ QUESTION: ".question",
108
+ ANSWER: ".answer",
109
+ NEEDS_RESTART: ".needs-restart",
110
+ HANDOVER: ".handover",
111
+ KILL: ".kill",
112
+ STUCK: ".stuck",
113
+ CRASHED: ".crashed",
114
+ STARTED: ".started",
115
+ RUNNING: ".running",
116
+ GATE_READY: ".gate-ready",
117
+ GATE_APPROVED: ".gate-approved",
118
+ PHASE_DONE: ".phase-done",
119
+ FEATURE_COMPLETE: ".feature-complete",
120
+ CONTEXT_REFRESH: ".context-refresh",
121
+ CONVERSATION_HISTORY: ".conversation-history",
122
+ CLAUDE_SESSION_LOG: ".claude-session.log",
123
+ RUNNER_LOG: ".runner.log",
124
+ DASHBOARD_LOG: ".dashboard-log",
125
+ BRIDGE_STATE: ".bridge-state.json",
126
+ TEAM_CONFIG: ".team-config",
127
+ NEEDS_REPOS: ".needs-repos",
128
+ GATE_EVIDENCE: ".gate-evidence.yaml",
129
+ OUTPUT_PARTIAL: ".output.partial",
130
+ };
131
+
132
+ // ─── Known Repos ─────────────────────────────────────────────────────────────
133
+
134
+ export const KNOWN_REPOS = [
135
+ "first-party-apps/directory/directory-backend",
136
+ "first-party-apps/directory/directory-frontend",
137
+ "first-party-apps/events/events-backend",
138
+ "first-party-apps/events/events-frontend",
139
+ "first-party-apps/subdomain-home/subdomain-home-frontend",
140
+ "first-party-apps/subdomain-home/subdomain-home-server",
141
+ "frontend-apps/iriai-app/iriai-app-bff",
142
+ "frontend-apps/iriai-app/iriai-app-frontend",
143
+ "packages/auth-python",
144
+ "packages/auth-react",
145
+ "platform/auth/auth-frontend",
146
+ "platform/auth/auth-service",
147
+ "platform/deploy-console/deploy-console-frontend",
148
+ "platform/deploy-console/deploy-console-service",
149
+ "platform/integration-engine/integration-engine-service",
150
+ ];
151
+
152
+ // ─── Templates ──────────────────────────────────────────────────────────────
153
+
154
+ export const TEMPLATES_DIR = path.join(HOME, "src/iriai/templates");
155
+ export const AVAILABLE_TEMPLATES = ["fastapi-postgres", "react-parcel"];
156
+
157
+ // ─── Feature Review Roles ────────────────────────────────────────────────────
158
+
159
+ // ─── Artifact Portal ────────────────────────────────────────────────────────
160
+
161
+ export const PORTAL_PORT = process.env.PORTAL_PORT ? parseInt(process.env.PORTAL_PORT) : 8900;
162
+
163
+ // ─── Feature Review Roles ────────────────────────────────────────────────────
164
+
165
+ export const FEATURE_REVIEW_ROLES = [
166
+ "integration-tester",
167
+ "code-reviewer",
168
+ "security-auditor",
169
+ "health-monitor",
170
+ ];
package/v3/db.js ADDED
@@ -0,0 +1,76 @@
1
+ // db.js — SQLite setup (WAL mode), migrations, transaction helper.
2
+ // Uses node:sqlite (Node 22+ built-in).
3
+
4
+ import { DatabaseSync } from "node:sqlite";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { DB_PATH } from "./constants.js";
8
+
9
+ let _db = null;
10
+
11
+ /**
12
+ * Open (or create) the SQLite database and apply the schema.
13
+ * Returns the DatabaseSync instance.
14
+ */
15
+ export function open(dbPath = DB_PATH) {
16
+ if (_db) return _db;
17
+
18
+ // Ensure parent directory exists
19
+ const dir = path.dirname(dbPath);
20
+ fs.mkdirSync(dir, { recursive: true });
21
+
22
+ _db = new DatabaseSync(dbPath);
23
+
24
+ // WAL mode for crash consistency and concurrent reads
25
+ _db.exec("PRAGMA journal_mode = WAL");
26
+ _db.exec("PRAGMA foreign_keys = ON");
27
+ _db.exec("PRAGMA busy_timeout = 5000");
28
+
29
+ // Apply schema
30
+ const schemaPath = path.join(import.meta.dirname, "schema.sql");
31
+ const schemaSql = fs.readFileSync(schemaPath, "utf-8");
32
+
33
+ // Split on semicolons and execute each statement (skip PRAGMAs already run)
34
+ for (const stmt of schemaSql.split(";")) {
35
+ const trimmed = stmt.trim();
36
+ if (!trimmed || trimmed.startsWith("PRAGMA")) continue;
37
+ _db.exec(trimmed);
38
+ }
39
+
40
+ return _db;
41
+ }
42
+
43
+ /**
44
+ * Get the active database instance (must call open() first).
45
+ */
46
+ export function get() {
47
+ if (!_db) throw new Error("Database not opened. Call db.open() first.");
48
+ return _db;
49
+ }
50
+
51
+ /**
52
+ * Run a function inside a transaction. Automatically commits on success,
53
+ * rolls back on error. Returns the function's return value.
54
+ */
55
+ export function transaction(fn) {
56
+ const db = get();
57
+ db.exec("BEGIN");
58
+ try {
59
+ const result = fn(db);
60
+ db.exec("COMMIT");
61
+ return result;
62
+ } catch (err) {
63
+ db.exec("ROLLBACK");
64
+ throw err;
65
+ }
66
+ }
67
+
68
+ /**
69
+ * Close the database connection.
70
+ */
71
+ export function close() {
72
+ if (_db) {
73
+ _db.close();
74
+ _db = null;
75
+ }
76
+ }
package/v3/file-io.js ADDED
@@ -0,0 +1,216 @@
1
+ // file-io.js — Agent task/response filesystem transport + chokidar.
2
+ // Watches specific signal files per agent, emits events when they appear.
3
+
4
+ import { EventEmitter } from "node:events";
5
+ import fs from "node:fs";
6
+ import path from "node:path";
7
+ import { watch } from "chokidar";
8
+ import { SIGNAL } from "./constants.js";
9
+ import { readSignal } from "./helpers.js";
10
+
11
+ export class FileIO extends EventEmitter {
12
+ constructor() {
13
+ super();
14
+ this._watchers = [];
15
+ }
16
+
17
+ /**
18
+ * Watch per-feature planning signal directories and operator dir during planning phase.
19
+ */
20
+ watchFeaturePlanningSignals(slug, planningTree, operatorDir) {
21
+ // Watch directories (not individual files) — chokidar reliably detects
22
+ // add/change in existing dirs, even for files that get deleted and recreated.
23
+ const dirsToWatch = Object.values(planningTree);
24
+ if (operatorDir) dirsToWatch.push(operatorDir);
25
+
26
+ const watcher = watch(dirsToWatch, {
27
+ ignoreInitial: true,
28
+ awaitWriteFinish: { stabilityThreshold: 500 },
29
+ disableGlobbing: true,
30
+ depth: 0,
31
+ });
32
+
33
+ const handler = (filePath) => this._handleFeaturePlanningSignal(slug, filePath, planningTree, operatorDir);
34
+ watcher.on("add", handler);
35
+ watcher.on("change", handler);
36
+
37
+ watcher._slug = slug;
38
+ this._watchers.push(watcher);
39
+ console.log(`[file-io] Watching planning signals for ${slug}`);
40
+ }
41
+
42
+ _handleFeaturePlanningSignal(slug, filePath, planningTree, operatorDir) {
43
+ const fileName = path.basename(filePath);
44
+ const dir = path.dirname(filePath);
45
+
46
+ // Operator signals
47
+ if (operatorDir && dir === operatorDir) {
48
+ if (fileName === ".agent-response") {
49
+ this.emit("impl:operatorResponse", { slug, filePath });
50
+ } else if (fileName === ".user-message") {
51
+ this.emit("impl:userMessage", { slug, agent: "operator", filePath });
52
+ } else if (fileName === ".needs-repos") {
53
+ this.emit("impl:needsRepos", { slug, filePath });
54
+ }
55
+ return;
56
+ }
57
+
58
+ // Planning role signals
59
+ const role = Object.entries(planningTree).find(([, d]) => d === dir)?.[0];
60
+ if (!role) return;
61
+
62
+ if (fileName === SIGNAL.AGENT_RESPONSE || fileName === SIGNAL.AGENT_RESPONSE.slice(1)) {
63
+ this.emit("planning:response", { slug, role, filePath });
64
+ } else if (fileName === SIGNAL.QUESTION || fileName === SIGNAL.QUESTION.slice(1)) {
65
+ this.emit("planning:question", { slug, role, filePath });
66
+ } else if (fileName === SIGNAL.DONE || fileName === SIGNAL.DONE.slice(1)) {
67
+ this.emit("planning:done", { slug, role, filePath });
68
+ } else if (fileName === SIGNAL.NEEDS_RESTART || fileName === SIGNAL.NEEDS_RESTART.slice(1)) {
69
+ this.emit("impl:needsRestart", { slug, dir, filePath });
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Watch implementation signal files for a specific feature.
75
+ */
76
+ watchFeatureSignals(slug, signalTree) {
77
+ // Watch directories (not individual files) — chokidar reliably detects
78
+ // add/change in existing dirs, even for files that get deleted and recreated.
79
+ const dirsToWatch = new Set();
80
+
81
+ if (signalTree.featureLead) dirsToWatch.add(signalTree.featureLead);
82
+ if (signalTree.operator) dirsToWatch.add(signalTree.operator);
83
+
84
+ for (const [, team] of Object.entries(signalTree.teams || {})) {
85
+ if (team.orchestrator) dirsToWatch.add(team.orchestrator);
86
+ for (const [, roleDir] of Object.entries(team.roles || {})) {
87
+ dirsToWatch.add(roleDir);
88
+ }
89
+ }
90
+
91
+ for (const [, reviewDir] of Object.entries(signalTree.featureReview || {})) {
92
+ dirsToWatch.add(reviewDir);
93
+ }
94
+
95
+ const watcher = watch([...dirsToWatch], {
96
+ ignoreInitial: true,
97
+ awaitWriteFinish: { stabilityThreshold: 500 },
98
+ disableGlobbing: true,
99
+ depth: 0,
100
+ });
101
+
102
+ const handler = (filePath) => this._handleImplSignal(slug, filePath, signalTree);
103
+ watcher.on("add", handler);
104
+ watcher.on("change", handler);
105
+
106
+ watcher._slug = slug;
107
+ this._watchers.push(watcher);
108
+ console.log(`[file-io] Watching impl signals for ${slug}`);
109
+ }
110
+
111
+ _handleImplSignal(slug, filePath, tree) {
112
+ const fileName = path.basename(filePath);
113
+ const dir = path.dirname(filePath);
114
+
115
+ // Feature Lead
116
+ if (tree.featureLead && dir === tree.featureLead) {
117
+ switch (fileName) {
118
+ case ".agent-response":
119
+ this.emit("impl:response", { slug, agent: "feature-lead", filePath });
120
+ return;
121
+ case ".feature-complete":
122
+ this.emit("impl:featureComplete", { slug, filePath });
123
+ return;
124
+ case ".phase-done":
125
+ this.emit("impl:phaseDone", { slug, filePath });
126
+ return;
127
+ case ".context-refresh":
128
+ this.emit("impl:contextRefresh", { slug, filePath });
129
+ return;
130
+ case ".needs-restart":
131
+ this.emit("impl:needsRestart", { slug, dir, filePath });
132
+ return;
133
+ }
134
+ }
135
+
136
+ // Operator
137
+ if (tree.operator && dir === tree.operator) {
138
+ if (fileName === ".agent-response") {
139
+ this.emit("impl:operatorResponse", { slug, filePath });
140
+ } else if (fileName === ".user-message") {
141
+ this.emit("impl:userMessage", { slug, agent: "operator", filePath });
142
+ }
143
+ return;
144
+ }
145
+
146
+ // Teams
147
+ for (const [teamNum, team] of Object.entries(tree.teams || {})) {
148
+ if (team.orchestrator && dir === team.orchestrator) {
149
+ switch (fileName) {
150
+ case ".task":
151
+ this.emit("impl:orchTask", { slug, teamNum, filePath });
152
+ return;
153
+ case ".gate-ready":
154
+ this.emit("impl:gateReady", { slug, teamNum, filePath });
155
+ return;
156
+ case ".question":
157
+ this.emit("impl:question", { slug, teamNum, filePath });
158
+ return;
159
+ case ".crashed":
160
+ this.emit("impl:crashed", { slug, agent: `orch-${teamNum}`, teamNum, filePath });
161
+ return;
162
+ case ".needs-restart":
163
+ this.emit("impl:needsRestart", { slug, dir, filePath });
164
+ return;
165
+ }
166
+ }
167
+ for (const [role, roleDir] of Object.entries(team.roles || {})) {
168
+ if (dir === roleDir) {
169
+ if (fileName === ".task") {
170
+ this.emit("impl:task", { slug, teamNum, role, filePath });
171
+ } else if (fileName === ".done") {
172
+ this.emit("impl:done", { slug, teamNum, role, agent: `role-${teamNum}-${role}`, filePath });
173
+ } else if (fileName === ".needs-restart") {
174
+ this.emit("impl:needsRestart", { slug, dir, filePath });
175
+ }
176
+ return;
177
+ }
178
+ }
179
+ }
180
+
181
+ // Feature review
182
+ for (const [role, reviewDir] of Object.entries(tree.featureReview || {})) {
183
+ if (dir === reviewDir) {
184
+ if (fileName === ".done") {
185
+ this.emit("impl:done", { slug, role, agent: `review-${role}`, filePath });
186
+ } else if (fileName === ".needs-restart") {
187
+ this.emit("impl:needsRestart", { slug, dir, filePath });
188
+ }
189
+ return;
190
+ }
191
+ }
192
+ }
193
+
194
+ /**
195
+ * Stop watching signals for a specific feature.
196
+ */
197
+ unwatchFeature(slug) {
198
+ this._watchers = this._watchers.filter(w => {
199
+ if (w._slug === slug) {
200
+ w.close();
201
+ return false;
202
+ }
203
+ return true;
204
+ });
205
+ }
206
+
207
+ /**
208
+ * Close all watchers.
209
+ */
210
+ async closeAll() {
211
+ for (const w of this._watchers) {
212
+ await w.close();
213
+ }
214
+ this._watchers = [];
215
+ }
216
+ }
package/v3/helpers.js ADDED
@@ -0,0 +1,174 @@
1
+ // helpers.js — Pure utility functions extracted from slack-helpers.js.
2
+ // These are interface-agnostic: signal I/O, slugify, repo detection.
3
+
4
+ import fs from "node:fs";
5
+ import path from "node:path";
6
+ import { execSync } from "node:child_process";
7
+ import { IRIAI_TEAM_DIR, KNOWN_REPOS, TEMPLATES_DIR, AVAILABLE_TEMPLATES } from "./constants.js";
8
+
9
+ export function slugify(text) {
10
+ return text
11
+ .toLowerCase()
12
+ .replace(/[^a-z0-9]+/g, "-")
13
+ .replace(/^-|-$/g, "")
14
+ .slice(0, 40);
15
+ }
16
+
17
+ export function ensureDir(dir) {
18
+ fs.mkdirSync(dir, { recursive: true });
19
+ }
20
+
21
+ /**
22
+ * Read a signal file, return content, and optionally delete it.
23
+ */
24
+ export function readSignal(filePath, { deleteAfter = false } = {}) {
25
+ try {
26
+ const content = fs.readFileSync(filePath, "utf-8").trim();
27
+ if (deleteAfter) fs.unlinkSync(filePath);
28
+ return content;
29
+ } catch {
30
+ return "";
31
+ }
32
+ }
33
+
34
+ /**
35
+ * Write content to a signal file (ensures parent dir exists).
36
+ */
37
+ export function writeSignal(filePath, content) {
38
+ ensureDir(path.dirname(filePath));
39
+ fs.writeFileSync(filePath, content);
40
+ }
41
+
42
+ /**
43
+ * Extract media markers from message text.
44
+ */
45
+ export function parseGifMarkers(text) {
46
+ const gifPaths = [];
47
+ const evidencePaths = [];
48
+ const cleaned = text.replace(/\[(gif|img|image|video|screenshot|evidence):([^\]]+)\]/gi, (match, type, filePath) => {
49
+ if (type.toLowerCase() === "evidence") {
50
+ evidencePaths.push(filePath.trim());
51
+ } else {
52
+ gifPaths.push(filePath.trim());
53
+ }
54
+ return "";
55
+ });
56
+ return { text: cleaned.trim(), gifPaths, evidencePaths };
57
+ }
58
+
59
+ export function findArtifact(filename, planDir) {
60
+ const dir = planDir || path.join(IRIAI_TEAM_DIR, "implementation-plans", "current");
61
+ try {
62
+ const files = fs.readdirSync(dir);
63
+ const match = files.find((f) => f.includes(filename) && f.endsWith(".md"));
64
+ if (match) return path.join(dir, match);
65
+ } catch {
66
+ // Directory doesn't exist yet
67
+ }
68
+ return null;
69
+ }
70
+
71
+ export function parseRole(text) {
72
+ const lower = text.toLowerCase();
73
+ if (lower.includes("@pm")) return "pm";
74
+ if (lower.includes("@designer")) return "designer";
75
+ if (lower.includes("@architect")) return "architect";
76
+ if (lower.includes("@compiler") || lower.includes("@plan-compiler"))
77
+ return "plan-compiler";
78
+ if (lower.includes("@lead")) return "lead";
79
+ return null;
80
+ }
81
+
82
+ export function detectReposFromPlan(planDir, featureSlug) {
83
+ const planPath = findArtifact("implementation-plan", planDir);
84
+ const detected = [];
85
+
86
+ // 1. Text-scan plan artifact against KNOWN_REPOS
87
+ if (planPath) {
88
+ const content = fs.readFileSync(planPath, "utf-8");
89
+ for (const repo of KNOWN_REPOS) {
90
+ if (content.includes(repo)) detected.push(repo);
91
+ }
92
+ }
93
+
94
+ // 2. Scan worktree dir for repos already pulled in (catches new repos)
95
+ if (featureSlug) {
96
+ const reposDir = path.join(process.env.HOME, "src/iriai/.features", featureSlug, "repos");
97
+ if (fs.existsSync(reposDir)) {
98
+ try {
99
+ const entries = fs.readdirSync(reposDir, { withFileTypes: true });
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory()) continue;
102
+ const wtPath = path.join(reposDir, entry.name);
103
+ if (!fs.existsSync(path.join(wtPath, ".git"))) continue;
104
+ try {
105
+ const remote = execSync(`git -C "${wtPath}" remote get-url origin`, { stdio: "pipe" }).toString().trim();
106
+ const projectRoot = path.join(process.env.HOME, "src/iriai");
107
+ if (remote.startsWith(projectRoot)) {
108
+ const relPath = remote.slice(projectRoot.length + 1);
109
+ if (!detected.includes(relPath)) detected.push(relPath);
110
+ }
111
+ } catch {
112
+ // No remote — might be a locally-scaffolded new repo; skip
113
+ }
114
+ }
115
+ } catch { /* reposDir read failed */ }
116
+ }
117
+ }
118
+
119
+ return detected;
120
+ }
121
+
122
+ /**
123
+ * Scaffold a new repo locally: create directory, apply template or bare scaffold,
124
+ * initialize git. Does NOT create GitHub repo (that happens post-approval).
125
+ * Returns true on success, false on failure.
126
+ */
127
+ export function scaffoldNewRepo(localPath, githubName, template) {
128
+ const projectRoot = path.join(process.env.HOME, "src/iriai");
129
+ const repoAbsPath = path.join(projectRoot, localPath);
130
+ const repoBasename = path.basename(localPath);
131
+
132
+ // Idempotent: if already exists with .git, skip
133
+ if (fs.existsSync(path.join(repoAbsPath, ".git"))) {
134
+ console.log(`[scaffoldNewRepo] ${localPath} already exists with .git, skipping`);
135
+ return true;
136
+ }
137
+
138
+ try {
139
+ fs.mkdirSync(repoAbsPath, { recursive: true });
140
+
141
+ if (template && AVAILABLE_TEMPLATES.includes(template)) {
142
+ const templateDir = path.join(TEMPLATES_DIR, template);
143
+ if (fs.existsSync(templateDir)) {
144
+ execSync(`rsync -a --exclude '.git' "${templateDir}/" "${repoAbsPath}/"`, { stdio: "pipe" });
145
+ console.log(`[scaffoldNewRepo] Applied template '${template}' to ${localPath}`);
146
+ } else {
147
+ console.warn(`[scaffoldNewRepo] Template dir not found: ${templateDir}, using bare scaffold`);
148
+ _writeBareScaffold(repoAbsPath, repoBasename);
149
+ }
150
+ } else {
151
+ _writeBareScaffold(repoAbsPath, repoBasename);
152
+ }
153
+
154
+ // Initialize git
155
+ execSync(`git -C "${repoAbsPath}" init -b main`, { stdio: "pipe" });
156
+ execSync(`git -C "${repoAbsPath}" add -A`, { stdio: "pipe" });
157
+ execSync(`git -C "${repoAbsPath}" commit -m "chore: scaffold ${repoBasename}"`, { stdio: "pipe" });
158
+
159
+ console.log(`[scaffoldNewRepo] Scaffolded ${localPath} (template: ${template || "bare"})`);
160
+ return true;
161
+ } catch (err) {
162
+ console.error(`[scaffoldNewRepo] Failed to scaffold ${localPath}:`, err.message);
163
+ return false;
164
+ }
165
+ }
166
+
167
+ function _writeBareScaffold(dir, name) {
168
+ fs.writeFileSync(path.join(dir, "README.md"), `# ${name}\n\nPart of the iriai platform.\n`);
169
+ fs.writeFileSync(path.join(dir, ".gitignore"), [
170
+ "node_modules/", "__pycache__/", "*.pyc", ".venv/", "venv/",
171
+ ".env", ".env.local", ".idea/", ".vscode/", "*.swp",
172
+ ".DS_Store", "dist/", "build/", "*.egg-info/",
173
+ ].join("\n") + "\n");
174
+ }