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
package/v3/bridge.db
ADDED
|
File without changes
|
package/v3/constants.js
ADDED
|
@@ -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
|
+
}
|