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
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
// setup.js — `iriai-build setup` command.
|
|
2
|
+
// Interactive config for Slack tokens and tool paths.
|
|
3
|
+
|
|
4
|
+
import { input, confirm } from "@inquirer/prompts";
|
|
5
|
+
import * as config from "../config.js";
|
|
6
|
+
import { banner, systemMsg, successMsg, errorMsg } from "../display.js";
|
|
7
|
+
|
|
8
|
+
export async function setupCommand() {
|
|
9
|
+
banner();
|
|
10
|
+
systemMsg("Configure iriai-build. Values are saved to " + config.configPath());
|
|
11
|
+
console.log("");
|
|
12
|
+
|
|
13
|
+
const current = config.load();
|
|
14
|
+
|
|
15
|
+
// Slack tokens
|
|
16
|
+
console.log("\x1b[1mSlack Configuration\x1b[0m");
|
|
17
|
+
console.log("\x1b[2mRequired for `iriai-build slack` and `iriai-build transfer-to-slack`.\x1b[0m");
|
|
18
|
+
console.log("\x1b[2mEnv vars (SLACK_APP_TOKEN, etc.) always override these values.\x1b[0m\n");
|
|
19
|
+
|
|
20
|
+
const appToken = await input({
|
|
21
|
+
message: "Slack App Token (xapp-...):",
|
|
22
|
+
default: current.slack_app_token || "",
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const botToken = await input({
|
|
26
|
+
message: "Slack Bot Token (xoxb-...):",
|
|
27
|
+
default: current.slack_bot_token || "",
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
const channelId = await input({
|
|
31
|
+
message: "Slack Planning Channel ID (C...):",
|
|
32
|
+
default: current.slack_channel_id || "",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
// QA Feedback tool path
|
|
36
|
+
console.log("\n\x1b[1mTool Paths\x1b[0m");
|
|
37
|
+
console.log("\x1b[2mOptional — leave blank to use defaults (tool on PATH).\x1b[0m\n");
|
|
38
|
+
|
|
39
|
+
const claudeBin = await input({
|
|
40
|
+
message: "claude CLI path:",
|
|
41
|
+
default: current.claude_bin || "claude",
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const qaFeedbackBin = await input({
|
|
45
|
+
message: "iriai-feedback CLI path:",
|
|
46
|
+
default: current.qa_feedback_bin || "iriai-feedback",
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
// Save
|
|
50
|
+
const updates = {};
|
|
51
|
+
if (appToken) updates.slack_app_token = appToken;
|
|
52
|
+
if (botToken) updates.slack_bot_token = botToken;
|
|
53
|
+
if (channelId) updates.slack_channel_id = channelId;
|
|
54
|
+
if (claudeBin && claudeBin !== "claude") updates.claude_bin = claudeBin;
|
|
55
|
+
if (qaFeedbackBin && qaFeedbackBin !== "iriai-feedback") updates.qa_feedback_bin = qaFeedbackBin;
|
|
56
|
+
|
|
57
|
+
config.update(updates);
|
|
58
|
+
|
|
59
|
+
console.log("");
|
|
60
|
+
successMsg(`Config saved to ${config.configPath()}`);
|
|
61
|
+
|
|
62
|
+
// Validate
|
|
63
|
+
if (appToken && botToken && channelId) {
|
|
64
|
+
const shouldTest = await confirm({
|
|
65
|
+
message: "Test Slack connection?",
|
|
66
|
+
default: true,
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
if (shouldTest) {
|
|
70
|
+
try {
|
|
71
|
+
const { WebClient } = await import("@slack/web-api");
|
|
72
|
+
const web = new WebClient(botToken);
|
|
73
|
+
const auth = await web.auth.test();
|
|
74
|
+
successMsg(`Connected as ${auth.user} (${auth.user_id}) in workspace ${auth.team}`);
|
|
75
|
+
} catch (err) {
|
|
76
|
+
errorMsg(`Slack connection failed: ${err.message}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
// slack.js — `iriai-build slack` command.
|
|
2
|
+
// Wraps the existing bridge-v3.js Slack bridge startup.
|
|
3
|
+
// Also syncs terminal-created features to Slack channels.
|
|
4
|
+
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
6
|
+
import * as db from "../../v3/db.js";
|
|
7
|
+
import { SlackAdapter } from "../../v3/adapters/slack-adapter.js";
|
|
8
|
+
import { Orchestrator } from "../../v3/orchestrator.js";
|
|
9
|
+
import { Recovery } from "../../v3/recovery.js";
|
|
10
|
+
import { ReviewSessionManager } from "../../v3/review-sessions.js";
|
|
11
|
+
import * as queries from "../../v3/queries.js";
|
|
12
|
+
import { DB_PATH } from "../../v3/constants.js";
|
|
13
|
+
import * as config from "../config.js";
|
|
14
|
+
import { systemMsg, successMsg, errorMsg } from "../display.js";
|
|
15
|
+
|
|
16
|
+
export async function slackCommand() {
|
|
17
|
+
const slack = config.getSlackConfig();
|
|
18
|
+
|
|
19
|
+
if (!slack.appToken || !slack.botToken || !slack.channelId) {
|
|
20
|
+
errorMsg("Slack not configured. Run `iriai-build setup` first, or set SLACK_APP_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID env vars.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
console.log("Starting Iriai Slack Bridge v3...");
|
|
25
|
+
|
|
26
|
+
// Prevent macOS from sleeping while the bridge is running
|
|
27
|
+
const caffeinate = spawn("caffeinate", ["-dims"], {
|
|
28
|
+
stdio: "ignore",
|
|
29
|
+
detached: true,
|
|
30
|
+
});
|
|
31
|
+
caffeinate.unref();
|
|
32
|
+
|
|
33
|
+
db.open(DB_PATH);
|
|
34
|
+
|
|
35
|
+
const adapter = new SlackAdapter({
|
|
36
|
+
appToken: slack.appToken,
|
|
37
|
+
botToken: slack.botToken,
|
|
38
|
+
planningChannel: slack.channelId,
|
|
39
|
+
});
|
|
40
|
+
await adapter.connect();
|
|
41
|
+
console.log(`Slack connected. Bot user: ${adapter.botUserId}`);
|
|
42
|
+
|
|
43
|
+
const reviewSessions = new ReviewSessionManager();
|
|
44
|
+
const orchestrator = new Orchestrator({ adapter, reviewSessions });
|
|
45
|
+
adapter.setOrchestrator(orchestrator);
|
|
46
|
+
|
|
47
|
+
const recovery = new Recovery({ orchestrator, adapter });
|
|
48
|
+
await recovery.run();
|
|
49
|
+
|
|
50
|
+
// Sync terminal-created features to Slack
|
|
51
|
+
await syncTerminalFeatures(adapter);
|
|
52
|
+
|
|
53
|
+
orchestrator.startStaleScan();
|
|
54
|
+
console.log("Bridge v3 ready.");
|
|
55
|
+
|
|
56
|
+
// Graceful shutdown
|
|
57
|
+
const shutdown = async (signal) => {
|
|
58
|
+
console.log(`\n[bridge] ${signal} — shutting down...`);
|
|
59
|
+
caffeinate.kill();
|
|
60
|
+
await orchestrator.shutdown();
|
|
61
|
+
db.close();
|
|
62
|
+
process.exit(0);
|
|
63
|
+
};
|
|
64
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
65
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
66
|
+
process.on("unhandledRejection", (err) => {
|
|
67
|
+
console.error("[bridge] Unhandled rejection:", err?.message || err);
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function syncTerminalFeatures(adapter) {
|
|
72
|
+
const terminalFeatures = queries.getTerminalFeatures();
|
|
73
|
+
if (terminalFeatures.length === 0) return;
|
|
74
|
+
|
|
75
|
+
systemMsg(`Syncing ${terminalFeatures.length} terminal-created feature(s) to Slack...`);
|
|
76
|
+
|
|
77
|
+
for (const feature of terminalFeatures) {
|
|
78
|
+
try {
|
|
79
|
+
const channelId = await adapter.createFeatureChannel(feature.id, feature.slug);
|
|
80
|
+
if (channelId) {
|
|
81
|
+
queries.updateFeatureChannel(feature.id, channelId);
|
|
82
|
+
await adapter.postMessage(feature.id,
|
|
83
|
+
`Feature *${feature.slug}* was created via terminal. Phase: ${feature.phase}.`);
|
|
84
|
+
|
|
85
|
+
// Post plan artifacts if plan-approval or later
|
|
86
|
+
if (feature.phase !== "planning") {
|
|
87
|
+
const planDir = `${feature.signal_dir}/plans`;
|
|
88
|
+
await adapter.postMessage(feature.id, `Plan artifacts: ${planDir}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
successMsg(`Synced: ${feature.slug} -> ${channelId}`);
|
|
92
|
+
}
|
|
93
|
+
} catch (err) {
|
|
94
|
+
console.error(`Failed to sync ${feature.slug}:`, err.message);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// transfer.js — `iriai-build transfer-to-slack` command.
|
|
2
|
+
// Connects to Slack, syncs all active CLI features, and stays running as the Slack bridge.
|
|
3
|
+
|
|
4
|
+
import { spawn } from "node:child_process";
|
|
5
|
+
import * as db from "../../v3/db.js";
|
|
6
|
+
import { SlackAdapter } from "../../v3/adapters/slack-adapter.js";
|
|
7
|
+
import { Orchestrator } from "../../v3/orchestrator.js";
|
|
8
|
+
import { Recovery } from "../../v3/recovery.js";
|
|
9
|
+
import { ReviewSessionManager } from "../../v3/review-sessions.js";
|
|
10
|
+
import * as queries from "../../v3/queries.js";
|
|
11
|
+
import { DB_PATH } from "../../v3/constants.js";
|
|
12
|
+
import * as config from "../config.js";
|
|
13
|
+
import { banner, systemMsg, successMsg, errorMsg } from "../display.js";
|
|
14
|
+
|
|
15
|
+
export async function transferToSlackCommand() {
|
|
16
|
+
banner();
|
|
17
|
+
|
|
18
|
+
const slack = config.getSlackConfig();
|
|
19
|
+
if (!slack.appToken || !slack.botToken || !slack.channelId) {
|
|
20
|
+
errorMsg("Slack not configured. Run `iriai-build setup` first, or set SLACK_APP_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID env vars.");
|
|
21
|
+
process.exit(1);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// Open DB (may already be open if coming from a CLI session)
|
|
25
|
+
try { db.open(DB_PATH); } catch { /* already open */ }
|
|
26
|
+
|
|
27
|
+
const activeFeatures = queries.getActiveFeatures();
|
|
28
|
+
const cliFeatures = activeFeatures.filter(f => {
|
|
29
|
+
const ch = f.feature_channel;
|
|
30
|
+
return ch && !/^[CGD][A-Z0-9]+$/.test(ch);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
systemMsg(`Found ${activeFeatures.length} active feature(s), ${cliFeatures.length} need Slack sync.`);
|
|
34
|
+
|
|
35
|
+
// Prevent macOS from sleeping while the bridge is running
|
|
36
|
+
const caffeinate = spawn("caffeinate", ["-dims"], {
|
|
37
|
+
stdio: "ignore",
|
|
38
|
+
detached: true,
|
|
39
|
+
});
|
|
40
|
+
caffeinate.unref();
|
|
41
|
+
|
|
42
|
+
// Connect Slack
|
|
43
|
+
systemMsg("Connecting to Slack...");
|
|
44
|
+
const adapter = new SlackAdapter({
|
|
45
|
+
appToken: slack.appToken,
|
|
46
|
+
botToken: slack.botToken,
|
|
47
|
+
planningChannel: slack.channelId,
|
|
48
|
+
});
|
|
49
|
+
await adapter.connect();
|
|
50
|
+
successMsg(`Slack connected. Bot user: ${adapter.botUserId}`);
|
|
51
|
+
|
|
52
|
+
// Create orchestrator
|
|
53
|
+
const reviewSessions = new ReviewSessionManager();
|
|
54
|
+
const orchestrator = new Orchestrator({ adapter, reviewSessions });
|
|
55
|
+
adapter.setOrchestrator(orchestrator);
|
|
56
|
+
|
|
57
|
+
// Sync CLI features to Slack channels
|
|
58
|
+
if (cliFeatures.length > 0) {
|
|
59
|
+
systemMsg("Syncing CLI features to Slack...");
|
|
60
|
+
for (const feature of cliFeatures) {
|
|
61
|
+
try {
|
|
62
|
+
const channelId = await adapter.createFeatureChannel(feature.id, feature.slug);
|
|
63
|
+
if (channelId) {
|
|
64
|
+
queries.updateFeatureChannel(feature.id, channelId);
|
|
65
|
+
|
|
66
|
+
// Announce in #planning
|
|
67
|
+
await adapter.web.chat.postMessage({
|
|
68
|
+
channel: slack.channelId,
|
|
69
|
+
text: `[FEATURE][CLI SYNC] ${feature.slug} → <#${channelId}>`,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Post status in feature channel
|
|
73
|
+
const phase = feature.phase || "planning";
|
|
74
|
+
const meta = JSON.parse(feature.metadata || "{}");
|
|
75
|
+
const lines = [`*Feature:* \`${feature.slug}\``, `*Phase:* ${phase}`];
|
|
76
|
+
if (meta.awaiting_phase_review) lines.push(`*Status:* Awaiting ${meta.phase_review_role} phase review`);
|
|
77
|
+
lines.push("_Transferred from CLI to Slack._");
|
|
78
|
+
await adapter.postMessage(feature.id, lines.join("\n"));
|
|
79
|
+
|
|
80
|
+
successMsg(` ${feature.slug} → <#${channelId}>`);
|
|
81
|
+
}
|
|
82
|
+
} catch (err) {
|
|
83
|
+
errorMsg(` Failed to sync ${feature.slug}: ${err.message}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Run recovery (re-post pending decisions, resume agents)
|
|
89
|
+
systemMsg("Running recovery...");
|
|
90
|
+
const recovery = new Recovery({ orchestrator, adapter });
|
|
91
|
+
await recovery.run();
|
|
92
|
+
|
|
93
|
+
orchestrator.startStaleScan();
|
|
94
|
+
successMsg("Transfer complete. Slack bridge is now running.");
|
|
95
|
+
systemMsg("Press Ctrl+C to stop.\n");
|
|
96
|
+
|
|
97
|
+
// Graceful shutdown
|
|
98
|
+
const shutdown = async (signal) => {
|
|
99
|
+
console.log(`\n[bridge] ${signal} — shutting down...`);
|
|
100
|
+
caffeinate.kill();
|
|
101
|
+
if (reviewSessions) await reviewSessions.stopAll();
|
|
102
|
+
await orchestrator.shutdown();
|
|
103
|
+
db.close();
|
|
104
|
+
process.exit(0);
|
|
105
|
+
};
|
|
106
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
107
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
108
|
+
process.on("unhandledRejection", (err) => {
|
|
109
|
+
console.error("[bridge] Unhandled rejection:", err?.message || err);
|
|
110
|
+
});
|
|
111
|
+
}
|
package/cli/config.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// config.js — Persistent config for iriai-build.
|
|
2
|
+
// Stores Slack tokens, tool paths, and preferences in ~/.iriai/config.json.
|
|
3
|
+
// Env vars always override config file values.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs";
|
|
6
|
+
import path from "node:path";
|
|
7
|
+
|
|
8
|
+
const CONFIG_DIR = path.join(process.env.HOME, ".iriai");
|
|
9
|
+
const CONFIG_PATH = path.join(CONFIG_DIR, "config.json");
|
|
10
|
+
|
|
11
|
+
let _cache = null;
|
|
12
|
+
|
|
13
|
+
function ensureDir() {
|
|
14
|
+
if (!fs.existsSync(CONFIG_DIR)) {
|
|
15
|
+
fs.mkdirSync(CONFIG_DIR, { recursive: true });
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Load config from disk (cached after first read).
|
|
21
|
+
*/
|
|
22
|
+
export function load() {
|
|
23
|
+
if (_cache) return _cache;
|
|
24
|
+
try {
|
|
25
|
+
const raw = fs.readFileSync(CONFIG_PATH, "utf-8");
|
|
26
|
+
_cache = JSON.parse(raw);
|
|
27
|
+
} catch {
|
|
28
|
+
_cache = {};
|
|
29
|
+
}
|
|
30
|
+
return _cache;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Save config to disk.
|
|
35
|
+
*/
|
|
36
|
+
export function save(config) {
|
|
37
|
+
ensureDir();
|
|
38
|
+
_cache = config;
|
|
39
|
+
fs.writeFileSync(CONFIG_PATH, JSON.stringify(config, null, 2) + "\n", "utf-8");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Update specific keys (shallow merge).
|
|
44
|
+
*/
|
|
45
|
+
export function update(partial) {
|
|
46
|
+
const config = load();
|
|
47
|
+
Object.assign(config, partial);
|
|
48
|
+
save(config);
|
|
49
|
+
return config;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get a config value. Env var overrides config file.
|
|
54
|
+
*/
|
|
55
|
+
export function get(key, envVar) {
|
|
56
|
+
if (envVar && process.env[envVar]) return process.env[envVar];
|
|
57
|
+
return load()[key] || null;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Get Slack config, resolved from env vars or config file.
|
|
62
|
+
*/
|
|
63
|
+
export function getSlackConfig() {
|
|
64
|
+
return {
|
|
65
|
+
appToken: get("slack_app_token", "SLACK_APP_TOKEN"),
|
|
66
|
+
botToken: get("slack_bot_token", "SLACK_BOT_TOKEN"),
|
|
67
|
+
channelId: get("slack_channel_id", "SLACK_CHANNEL_ID"),
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Check if Slack is configured (either env vars or config file).
|
|
73
|
+
*/
|
|
74
|
+
export function isSlackConfigured() {
|
|
75
|
+
const { appToken, botToken, channelId } = getSlackConfig();
|
|
76
|
+
return !!(appToken && botToken && channelId);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Get the claude CLI binary path. Env var CLAUDE_BIN overrides config file.
|
|
81
|
+
* Defaults to "claude" (assumes it's on PATH).
|
|
82
|
+
*/
|
|
83
|
+
export function getClaudeBin() {
|
|
84
|
+
return get("claude_bin", "CLAUDE_BIN") || "claude";
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the config file path (for display purposes).
|
|
89
|
+
*/
|
|
90
|
+
export function configPath() {
|
|
91
|
+
return CONFIG_PATH;
|
|
92
|
+
}
|
package/cli/display.js
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
// display.js — Terminal output formatting for iriai-build CLI.
|
|
2
|
+
// Colored agent labels, decision prompts, pipeline status.
|
|
3
|
+
// When the TUI is active, output is routed through setOutputFn().
|
|
4
|
+
|
|
5
|
+
let _outputFn = null;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Set the output function for TUI mode. Pass null to reset to console.log.
|
|
9
|
+
*/
|
|
10
|
+
export function setOutputFn(fn) {
|
|
11
|
+
_outputFn = fn;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function _print(text) {
|
|
15
|
+
if (_outputFn) {
|
|
16
|
+
_outputFn(text);
|
|
17
|
+
} else {
|
|
18
|
+
console.log(text);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const COLORS = {
|
|
23
|
+
reset: "\x1b[0m",
|
|
24
|
+
bold: "\x1b[1m",
|
|
25
|
+
dim: "\x1b[2m",
|
|
26
|
+
cyan: "\x1b[36m",
|
|
27
|
+
green: "\x1b[32m",
|
|
28
|
+
yellow: "\x1b[33m",
|
|
29
|
+
red: "\x1b[31m",
|
|
30
|
+
magenta: "\x1b[35m",
|
|
31
|
+
blue: "\x1b[34m",
|
|
32
|
+
white: "\x1b[37m",
|
|
33
|
+
bgBlue: "\x1b[44m",
|
|
34
|
+
bgGreen: "\x1b[42m",
|
|
35
|
+
bgYellow: "\x1b[43m",
|
|
36
|
+
bgRed: "\x1b[41m",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const AGENT_COLORS = {
|
|
40
|
+
PM: COLORS.cyan,
|
|
41
|
+
Designer: COLORS.magenta,
|
|
42
|
+
Architect: COLORS.blue,
|
|
43
|
+
"Plan Compiler": COLORS.green,
|
|
44
|
+
"Feature Lead": COLORS.yellow,
|
|
45
|
+
Operator: COLORS.white,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
export function agentMsg(slug, agentLabel, text) {
|
|
49
|
+
const color = AGENT_COLORS[agentLabel] || COLORS.white;
|
|
50
|
+
const prefix = `${COLORS.dim}[${slug}]${COLORS.reset} ${color}${COLORS.bold}${agentLabel}${COLORS.reset}`;
|
|
51
|
+
_print(`${prefix} ${text}`);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export function pipelineMsg(slug, text) {
|
|
55
|
+
_print(`${COLORS.dim}[${slug}]${COLORS.reset} ${COLORS.bgBlue}${COLORS.white} Pipeline ${COLORS.reset} ${text}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function systemMsg(text) {
|
|
59
|
+
_print(`${COLORS.dim}[system]${COLORS.reset} ${text}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export function errorMsg(text) {
|
|
63
|
+
_print(`${COLORS.red}${COLORS.bold}Error:${COLORS.reset} ${text}`);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function successMsg(text) {
|
|
67
|
+
_print(`${COLORS.green}${COLORS.bold}${text}${COLORS.reset}`);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function decisionHeader(title) {
|
|
71
|
+
_print(`\n${COLORS.bgYellow}${COLORS.bold} DECISION ${COLORS.reset} ${COLORS.bold}${title}${COLORS.reset}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function featureComplete(slug) {
|
|
75
|
+
_print(`\n${COLORS.bgGreen}${COLORS.bold} COMPLETE ${COLORS.reset} Feature ${COLORS.bold}${slug}${COLORS.reset} — all gates passed!`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function separator() {
|
|
79
|
+
_print(`${COLORS.dim}${"─".repeat(60)}${COLORS.reset}`);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function banner() {
|
|
83
|
+
_print(`${COLORS.bold}${COLORS.cyan}iriai-build${COLORS.reset} — AI agent orchestration CLI`);
|
|
84
|
+
separator();
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatFeatureChoice(feature) {
|
|
88
|
+
const age = timeAgo(feature.updated_at || feature.created_at);
|
|
89
|
+
const phase = feature.phase;
|
|
90
|
+
let detail = phase;
|
|
91
|
+
|
|
92
|
+
if (phase === "planning" && feature.active_planning_role) {
|
|
93
|
+
detail = `planning: ${feature.active_planning_role} phase`;
|
|
94
|
+
} else if (phase === "plan-approval") {
|
|
95
|
+
detail = "plan-approval: awaiting approval";
|
|
96
|
+
} else if (phase === "impl") {
|
|
97
|
+
const gate = feature.gate_number || 0;
|
|
98
|
+
detail = `impl: gate ${gate}`;
|
|
99
|
+
} else if (phase === "launching") {
|
|
100
|
+
detail = "launching: setting up implementation";
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return `${feature.slug} (${detail}, ${age})`;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function timeAgo(dateStr) {
|
|
107
|
+
if (!dateStr) return "unknown";
|
|
108
|
+
// SQLite datetime('now') returns "YYYY-MM-DD HH:MM:SS" without T or Z.
|
|
109
|
+
// Normalize to ISO format for reliable parsing.
|
|
110
|
+
const normalized = dateStr.includes("T") ? dateStr : dateStr.replace(" ", "T") + "Z";
|
|
111
|
+
const then = new Date(normalized).getTime();
|
|
112
|
+
if (Number.isNaN(then)) return "unknown";
|
|
113
|
+
const diffMs = Date.now() - then;
|
|
114
|
+
if (diffMs < 0) return "just now";
|
|
115
|
+
const mins = Math.floor(diffMs / 60000);
|
|
116
|
+
if (mins < 60) return `${mins}m ago`;
|
|
117
|
+
const hours = Math.floor(mins / 60);
|
|
118
|
+
if (hours < 24) return `${hours}h ago`;
|
|
119
|
+
const days = Math.floor(hours / 24);
|
|
120
|
+
return `${days}d ago`;
|
|
121
|
+
}
|