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,78 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// iriai-build — CLI entry point for AI agent orchestration.
|
|
3
|
+
// Drives the same v3 engine as bridge-v3.js, with inline terminal prompts.
|
|
4
|
+
|
|
5
|
+
import { Command } from "commander";
|
|
6
|
+
import { bareCommand } from "../cli/commands/index.js";
|
|
7
|
+
import { planCommand } from "../cli/commands/plan.js";
|
|
8
|
+
import { implementationCommand } from "../cli/commands/implementation.js";
|
|
9
|
+
import { launchCommand } from "../cli/commands/launch.js";
|
|
10
|
+
import { slackCommand } from "../cli/commands/slack.js";
|
|
11
|
+
import { setupCommand } from "../cli/commands/setup.js";
|
|
12
|
+
import { transferToSlackCommand } from "../cli/commands/transfer.js";
|
|
13
|
+
|
|
14
|
+
function handleError(err) {
|
|
15
|
+
// @inquirer/prompts throws ExitPromptError on Ctrl+C — exit silently
|
|
16
|
+
if (err?.name === "ExitPromptError") {
|
|
17
|
+
process.exit(0);
|
|
18
|
+
}
|
|
19
|
+
console.error("Fatal:", err);
|
|
20
|
+
process.exit(1);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const program = new Command();
|
|
24
|
+
|
|
25
|
+
program
|
|
26
|
+
.name("iriai-build")
|
|
27
|
+
.description("AI agent orchestration CLI — plan, implement, and ship features")
|
|
28
|
+
.version("0.1.0");
|
|
29
|
+
|
|
30
|
+
program
|
|
31
|
+
.command("plan [description]")
|
|
32
|
+
.description("Run planning pipeline. After plan approved, prompts to continue to implementation.")
|
|
33
|
+
.action(async (description) => {
|
|
34
|
+
await planCommand(description);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
program
|
|
38
|
+
.command("implementation")
|
|
39
|
+
.alias("impl")
|
|
40
|
+
.description("Select from planned/in-progress features and run implementation.")
|
|
41
|
+
.action(async () => {
|
|
42
|
+
await implementationCommand();
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
program
|
|
46
|
+
.command("launch [description]")
|
|
47
|
+
.description("Full flow: planning -> implementation -> complete. Without arg: select or create.")
|
|
48
|
+
.action(async (description) => {
|
|
49
|
+
await launchCommand(description);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
program
|
|
53
|
+
.command("slack")
|
|
54
|
+
.description("Launch the Slack bridge (replaces `node bridge-v3.js`).")
|
|
55
|
+
.action(async () => {
|
|
56
|
+
await slackCommand();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
program
|
|
60
|
+
.command("setup")
|
|
61
|
+
.description("Configure Slack tokens, tool paths, and preferences.")
|
|
62
|
+
.action(async () => {
|
|
63
|
+
await setupCommand();
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
program
|
|
67
|
+
.command("transfer-to-slack")
|
|
68
|
+
.description("Sync all CLI features to Slack and start the Slack bridge.")
|
|
69
|
+
.action(async () => {
|
|
70
|
+
await transferToSlackCommand();
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Bare invocation: interactive menu
|
|
74
|
+
if (process.argv.length <= 2) {
|
|
75
|
+
bareCommand().catch(handleError);
|
|
76
|
+
} else {
|
|
77
|
+
program.parseAsync(process.argv).catch(handleError);
|
|
78
|
+
}
|
package/bridge-v3.js
ADDED
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// bridge-v3.js — SQLite-backed agent orchestration bridge.
|
|
3
|
+
//
|
|
4
|
+
// Replaces filesystem state (bridge-v2.js) with SQLite for features, agents,
|
|
5
|
+
// events, and decisions. Filesystem stays only for agent I/O (claude CLI constraint).
|
|
6
|
+
//
|
|
7
|
+
// Usage: node bridge-v3.js (or via start-bridge.sh + launchd)
|
|
8
|
+
|
|
9
|
+
import * as db from "./v3/db.js";
|
|
10
|
+
import { SlackAdapter } from "./v3/adapters/slack-adapter.js";
|
|
11
|
+
import { Orchestrator } from "./v3/orchestrator.js";
|
|
12
|
+
import { Recovery } from "./v3/recovery.js";
|
|
13
|
+
import { ReviewSessionManager } from "./v3/review-sessions.js";
|
|
14
|
+
import { DB_PATH, PORTAL_PORT } from "./v3/constants.js";
|
|
15
|
+
import { ArtifactPortal } from "./v3/artifact-portal.js";
|
|
16
|
+
|
|
17
|
+
let _orchestrator = null;
|
|
18
|
+
let _portal = null;
|
|
19
|
+
|
|
20
|
+
// ─── Config ──────────────────────────────────────────────────────────────────
|
|
21
|
+
|
|
22
|
+
const SLACK_APP_TOKEN = process.env.SLACK_APP_TOKEN;
|
|
23
|
+
const SLACK_BOT_TOKEN = process.env.SLACK_BOT_TOKEN;
|
|
24
|
+
const PLANNING_CHANNEL = process.env.SLACK_CHANNEL_ID;
|
|
25
|
+
|
|
26
|
+
if (!SLACK_APP_TOKEN || !SLACK_BOT_TOKEN || !PLANNING_CHANNEL) {
|
|
27
|
+
console.error("Missing required env vars: SLACK_APP_TOKEN, SLACK_BOT_TOKEN, SLACK_CHANNEL_ID");
|
|
28
|
+
process.exit(1);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Startup ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
async function start() {
|
|
34
|
+
console.log("Starting Iriai Slack Bridge v3 (SQLite-backed)...");
|
|
35
|
+
|
|
36
|
+
// 1. Open database
|
|
37
|
+
db.open(DB_PATH);
|
|
38
|
+
console.log(`Database opened: ${DB_PATH}`);
|
|
39
|
+
|
|
40
|
+
// 2. Connect Slack
|
|
41
|
+
const adapter = new SlackAdapter({
|
|
42
|
+
appToken: SLACK_APP_TOKEN,
|
|
43
|
+
botToken: SLACK_BOT_TOKEN,
|
|
44
|
+
planningChannel: PLANNING_CHANNEL,
|
|
45
|
+
});
|
|
46
|
+
await adapter.connect();
|
|
47
|
+
console.log(`Slack connected. Bot user: ${adapter.botUserId}`);
|
|
48
|
+
|
|
49
|
+
// 3. Create orchestrator with review session support
|
|
50
|
+
const reviewSessions = new ReviewSessionManager();
|
|
51
|
+
const orchestrator = new Orchestrator({ adapter, reviewSessions });
|
|
52
|
+
_orchestrator = orchestrator;
|
|
53
|
+
adapter.setOrchestrator(orchestrator);
|
|
54
|
+
|
|
55
|
+
// 4. Start artifact portal
|
|
56
|
+
const portal = new ArtifactPortal({ reviewSessions });
|
|
57
|
+
_portal = portal;
|
|
58
|
+
await portal.start(PORTAL_PORT);
|
|
59
|
+
|
|
60
|
+
// 5. Run recovery (process stale signals, reattach running agents)
|
|
61
|
+
const recovery = new Recovery({ orchestrator, adapter });
|
|
62
|
+
await recovery.run();
|
|
63
|
+
|
|
64
|
+
// 6. Start stale signal safety net
|
|
65
|
+
orchestrator.startStaleScan();
|
|
66
|
+
|
|
67
|
+
console.log("Bridge v3 ready.");
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ─── Graceful Shutdown ───────────────────────────────────────────────────────
|
|
71
|
+
|
|
72
|
+
async function shutdown(signal) {
|
|
73
|
+
console.log(`\n[bridge] ${signal} received — shutting down gracefully`);
|
|
74
|
+
|
|
75
|
+
if (_portal) {
|
|
76
|
+
await _portal.stop();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (_orchestrator) {
|
|
80
|
+
await _orchestrator.shutdown();
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
db.close();
|
|
84
|
+
console.log("[bridge] Database closed. Exiting.");
|
|
85
|
+
process.exit(0);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
process.on("SIGINT", () => shutdown("SIGINT"));
|
|
89
|
+
process.on("SIGTERM", () => shutdown("SIGTERM"));
|
|
90
|
+
|
|
91
|
+
process.on("unhandledRejection", (err) => {
|
|
92
|
+
console.error("[bridge] Unhandled rejection (non-fatal):", err?.message || err);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
start().catch((err) => {
|
|
96
|
+
console.error("[bridge] Fatal:", err);
|
|
97
|
+
process.exit(1);
|
|
98
|
+
});
|
package/cli/bootstrap.js
ADDED
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
// bootstrap.js — Shared setup for all CLI commands.
|
|
2
|
+
// Opens DB, creates adapter + orchestrator + recovery, wires input handler.
|
|
3
|
+
|
|
4
|
+
import * as db from "../v3/db.js";
|
|
5
|
+
import { TerminalAdapter } from "../v3/adapters/terminal-adapter.js";
|
|
6
|
+
import { Orchestrator } from "../v3/orchestrator.js";
|
|
7
|
+
import { Recovery } from "../v3/recovery.js";
|
|
8
|
+
import { ReviewSessionManager } from "../v3/review-sessions.js";
|
|
9
|
+
import { TerminalInput } from "./terminal-input.js";
|
|
10
|
+
import * as queries from "../v3/queries.js";
|
|
11
|
+
import { DB_PATH } from "../v3/constants.js";
|
|
12
|
+
import { slugify } from "../v3/helpers.js";
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Bootstrap the engine for CLI use.
|
|
16
|
+
* Returns { orchestrator, adapter, recovery, input, shutdown }.
|
|
17
|
+
*/
|
|
18
|
+
export function bootstrap() {
|
|
19
|
+
db.open(DB_PATH);
|
|
20
|
+
|
|
21
|
+
const adapter = new TerminalAdapter();
|
|
22
|
+
const reviewSessions = new ReviewSessionManager();
|
|
23
|
+
const orchestrator = new Orchestrator({ adapter, reviewSessions });
|
|
24
|
+
const input = new TerminalInput({ orchestrator });
|
|
25
|
+
adapter.setInputHandler(input);
|
|
26
|
+
const recovery = new Recovery({ orchestrator, adapter });
|
|
27
|
+
|
|
28
|
+
let _shutdownCalled = false;
|
|
29
|
+
async function shutdown() {
|
|
30
|
+
if (_shutdownCalled) return;
|
|
31
|
+
_shutdownCalled = true;
|
|
32
|
+
input.stopInputLoop();
|
|
33
|
+
input.exitScreen();
|
|
34
|
+
await reviewSessions.stopAll();
|
|
35
|
+
await orchestrator.shutdown();
|
|
36
|
+
db.close();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Graceful shutdown on signals (guard against double registration from bare → subcommand)
|
|
40
|
+
if (!process.__iriaiShutdownRegistered) {
|
|
41
|
+
process.__iriaiShutdownRegistered = true;
|
|
42
|
+
const onSignal = async (sig) => {
|
|
43
|
+
console.log(`\n[iriai-build] ${sig} — shutting down...`);
|
|
44
|
+
await shutdown();
|
|
45
|
+
process.exit(0);
|
|
46
|
+
};
|
|
47
|
+
process.on("SIGINT", () => onSignal("SIGINT"));
|
|
48
|
+
process.on("SIGTERM", () => onSignal("SIGTERM"));
|
|
49
|
+
process.on("unhandledRejection", (err) => {
|
|
50
|
+
// @inquirer/prompts throws ExitPromptError on Ctrl+C — let SIGINT handler deal with it
|
|
51
|
+
if (err?.name === "ExitPromptError") return;
|
|
52
|
+
console.error("[iriai-build] Unhandled rejection:", err?.message || err);
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return { orchestrator, adapter, recovery, input, shutdown };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Initialize a new feature from a description string.
|
|
61
|
+
* Generates slug, creates synthetic thread_ts, starts planning.
|
|
62
|
+
*/
|
|
63
|
+
export async function initNewFeature(orchestrator, description) {
|
|
64
|
+
let slug = slugify(description);
|
|
65
|
+
|
|
66
|
+
// Dedup: if slug already exists, append a numeric suffix
|
|
67
|
+
const existing = queries.getFeatureBySlug(slug);
|
|
68
|
+
if (existing) {
|
|
69
|
+
let suffix = 2;
|
|
70
|
+
while (queries.getFeatureBySlug(`${slug}-${suffix}`)) {
|
|
71
|
+
suffix++;
|
|
72
|
+
}
|
|
73
|
+
slug = `${slug}-${suffix}`;
|
|
74
|
+
console.log(`Slug "${slugify(description)}" already exists, using: ${slug}`);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const messageTs = `terminal-${Date.now()}`;
|
|
78
|
+
const userId = "terminal-user";
|
|
79
|
+
|
|
80
|
+
console.log(`\nInitializing feature: ${slug}`);
|
|
81
|
+
const feature = await orchestrator.initializeFeature(slug, messageTs, userId);
|
|
82
|
+
return feature;
|
|
83
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
// implementation.js — `iriai-build implementation` command.
|
|
2
|
+
// Select from planned/in-progress features, run implementation.
|
|
3
|
+
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import * as queries from "../../v3/queries.js";
|
|
6
|
+
import { bootstrap } from "../bootstrap.js";
|
|
7
|
+
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
8
|
+
import { waitForCompletion } from "../wait.js";
|
|
9
|
+
|
|
10
|
+
export async function implementationCommand() {
|
|
11
|
+
banner();
|
|
12
|
+
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
13
|
+
|
|
14
|
+
const features = queries.getFeaturesForImplCommand();
|
|
15
|
+
if (features.length === 0) {
|
|
16
|
+
errorMsg("No features ready for implementation. Run `iriai-build plan` first.");
|
|
17
|
+
await shutdown();
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const choices = features.map((f) => ({
|
|
22
|
+
name: formatFeatureChoice(f),
|
|
23
|
+
value: f.id,
|
|
24
|
+
}));
|
|
25
|
+
|
|
26
|
+
const featureId = await select({
|
|
27
|
+
message: "Select a feature to implement:",
|
|
28
|
+
choices,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const feature = queries.getFeatureById(featureId);
|
|
32
|
+
input.setActiveFeature(featureId, feature.feature_channel || feature.slug);
|
|
33
|
+
|
|
34
|
+
if (feature.phase === "plan-approval") {
|
|
35
|
+
const shouldStart = await input.promptConfirm("Approve plan and start implementation?");
|
|
36
|
+
if (!shouldStart) {
|
|
37
|
+
systemMsg("Exiting.");
|
|
38
|
+
await shutdown();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
systemMsg("Launching implementation...");
|
|
42
|
+
await orchestrator.startImplementation(featureId);
|
|
43
|
+
successMsg("Implementation launched.");
|
|
44
|
+
} else if (feature.phase === "impl") {
|
|
45
|
+
const gate = feature.gate_number || 0;
|
|
46
|
+
systemMsg(`Resuming implementation — gate ${gate}`);
|
|
47
|
+
input.startInputLoop();
|
|
48
|
+
await recovery.runForFeature(featureId);
|
|
49
|
+
} else {
|
|
50
|
+
input.startInputLoop();
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
orchestrator.startStaleScan();
|
|
54
|
+
|
|
55
|
+
await waitForCompletion(featureId, adapter);
|
|
56
|
+
|
|
57
|
+
const finalFeature = queries.getFeatureById(featureId);
|
|
58
|
+
if (finalFeature?.phase === "failed") {
|
|
59
|
+
errorMsg(`Feature ${finalFeature.slug} failed.`);
|
|
60
|
+
} else {
|
|
61
|
+
successMsg("Feature complete!");
|
|
62
|
+
}
|
|
63
|
+
await shutdown();
|
|
64
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// index.js — `iriai-build` bare command (interactive menu).
|
|
2
|
+
// Delegates to plan/implementation/launch/slack subcommands.
|
|
3
|
+
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import { planCommand } from "./plan.js";
|
|
6
|
+
import { implementationCommand } from "./implementation.js";
|
|
7
|
+
import { launchCommand } from "./launch.js";
|
|
8
|
+
import { slackCommand } from "./slack.js";
|
|
9
|
+
import { setupCommand } from "./setup.js";
|
|
10
|
+
import { transferToSlackCommand } from "./transfer.js";
|
|
11
|
+
|
|
12
|
+
export async function bareCommand() {
|
|
13
|
+
// Don't print banner here — the delegated command will print it.
|
|
14
|
+
const action = await select({
|
|
15
|
+
message: "What would you like to do?",
|
|
16
|
+
choices: [
|
|
17
|
+
{ name: "Launch (full flow: plan -> implement -> complete)", value: "launch" },
|
|
18
|
+
{ name: "Plan (planning only)", value: "plan" },
|
|
19
|
+
{ name: "Implementation (run implementation for a planned feature)", value: "implementation" },
|
|
20
|
+
{ name: "Slack (launch Slack bridge)", value: "slack" },
|
|
21
|
+
{ name: "Transfer to Slack (sync CLI features to Slack)", value: "transfer" },
|
|
22
|
+
{ name: "Setup (configure Slack tokens & tools)", value: "setup" },
|
|
23
|
+
],
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
switch (action) {
|
|
27
|
+
case "launch":
|
|
28
|
+
await launchCommand();
|
|
29
|
+
break;
|
|
30
|
+
case "plan":
|
|
31
|
+
await planCommand();
|
|
32
|
+
break;
|
|
33
|
+
case "implementation":
|
|
34
|
+
await implementationCommand();
|
|
35
|
+
break;
|
|
36
|
+
case "slack":
|
|
37
|
+
await slackCommand();
|
|
38
|
+
break;
|
|
39
|
+
case "transfer":
|
|
40
|
+
await transferToSlackCommand();
|
|
41
|
+
break;
|
|
42
|
+
case "setup":
|
|
43
|
+
await setupCommand();
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
// launch.js — `iriai-build launch [description]` command.
|
|
2
|
+
// Full flow: planning -> approve -> implementation -> complete.
|
|
3
|
+
// Without arg: select from active features or start new.
|
|
4
|
+
|
|
5
|
+
import { select } from "@inquirer/prompts";
|
|
6
|
+
import * as queries from "../../v3/queries.js";
|
|
7
|
+
import { bootstrap, initNewFeature } from "../bootstrap.js";
|
|
8
|
+
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
9
|
+
import { waitForPlanDecision, waitForCompletion } from "../wait.js";
|
|
10
|
+
|
|
11
|
+
export async function launchCommand(description) {
|
|
12
|
+
banner();
|
|
13
|
+
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
14
|
+
|
|
15
|
+
let featureId;
|
|
16
|
+
let isNew = false;
|
|
17
|
+
|
|
18
|
+
if (description) {
|
|
19
|
+
const feature = await initNewFeature(orchestrator, description);
|
|
20
|
+
featureId = feature.id;
|
|
21
|
+
isNew = true;
|
|
22
|
+
} else {
|
|
23
|
+
const features = queries.getActiveFeatures();
|
|
24
|
+
const choices = [
|
|
25
|
+
{ name: "[NEW] Start a new feature", value: "__new__" },
|
|
26
|
+
...features.map((f) => ({
|
|
27
|
+
name: formatFeatureChoice(f),
|
|
28
|
+
value: f.id,
|
|
29
|
+
})),
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const selected = await select({
|
|
33
|
+
message: "Select a feature or start new:",
|
|
34
|
+
choices,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
if (selected === "__new__") {
|
|
38
|
+
const desc = await input.promptFeatureDescription();
|
|
39
|
+
if (!desc) {
|
|
40
|
+
console.log("No description provided. Exiting.");
|
|
41
|
+
await shutdown();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const feature = await initNewFeature(orchestrator, desc);
|
|
45
|
+
featureId = feature.id;
|
|
46
|
+
isNew = true;
|
|
47
|
+
} else {
|
|
48
|
+
featureId = selected;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const feature = queries.getFeatureById(featureId);
|
|
53
|
+
input.setActiveFeature(featureId, feature.feature_channel || feature.slug);
|
|
54
|
+
orchestrator.startStaleScan();
|
|
55
|
+
|
|
56
|
+
if (isNew) {
|
|
57
|
+
// New feature — agents already dispatched by initNewFeature
|
|
58
|
+
systemMsg("Planning agents starting. The Operator will update you as they progress.");
|
|
59
|
+
input.startInputLoop();
|
|
60
|
+
await runPlanningWait(featureId, adapter);
|
|
61
|
+
} else if (feature.phase === "planning") {
|
|
62
|
+
printResumeStatus(feature);
|
|
63
|
+
systemMsg("Recovering state and resuming agents...");
|
|
64
|
+
input.startInputLoop();
|
|
65
|
+
await recovery.runForFeature(featureId);
|
|
66
|
+
systemMsg("Agents resumed. The Operator will present decisions as they come.");
|
|
67
|
+
await runPlanningWait(featureId, adapter);
|
|
68
|
+
} else if (feature.phase === "plan-approval") {
|
|
69
|
+
const shouldStart = await input.promptConfirm("Plan is approved. Start implementation?");
|
|
70
|
+
if (!shouldStart) {
|
|
71
|
+
systemMsg("Exiting. Resume later with: iriai-build implementation");
|
|
72
|
+
await shutdown();
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
systemMsg("Launching implementation...");
|
|
76
|
+
await orchestrator.startImplementation(featureId);
|
|
77
|
+
successMsg("Implementation launched.");
|
|
78
|
+
input.startInputLoop();
|
|
79
|
+
await waitForCompletion(featureId, adapter);
|
|
80
|
+
printFinalStatus(featureId, shutdown);
|
|
81
|
+
return;
|
|
82
|
+
} else if (feature.phase === "impl") {
|
|
83
|
+
printResumeStatus(feature);
|
|
84
|
+
systemMsg("Recovering state and resuming agents...");
|
|
85
|
+
input.startInputLoop();
|
|
86
|
+
await recovery.runForFeature(featureId);
|
|
87
|
+
systemMsg("Agents resumed. The Operator will present decisions as they come.");
|
|
88
|
+
await waitForCompletion(featureId, adapter);
|
|
89
|
+
printFinalStatus(featureId, shutdown);
|
|
90
|
+
return;
|
|
91
|
+
} else {
|
|
92
|
+
// complete/failed/closed — nothing to do
|
|
93
|
+
systemMsg(`Feature ${feature.slug} is already ${feature.phase}.`);
|
|
94
|
+
await shutdown();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Planning wait resolved — check result
|
|
99
|
+
let current = queries.getFeatureById(featureId);
|
|
100
|
+
if (current?.phase === "failed") {
|
|
101
|
+
errorMsg(`Feature ${current.slug} failed.`);
|
|
102
|
+
await shutdown();
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Plan approved — start implementation
|
|
107
|
+
input.stopInputLoop();
|
|
108
|
+
systemMsg("Starting implementation...");
|
|
109
|
+
await orchestrator.startImplementation(featureId);
|
|
110
|
+
successMsg("Implementation launched. The Operator will keep you updated.");
|
|
111
|
+
input.startInputLoop();
|
|
112
|
+
|
|
113
|
+
await waitForCompletion(featureId, adapter);
|
|
114
|
+
await printFinalStatus(featureId, shutdown);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Wait through planning → plan-approval, handling rejection loops.
|
|
119
|
+
*/
|
|
120
|
+
async function runPlanningWait(featureId, adapter) {
|
|
121
|
+
await waitForPlanDecision(featureId, adapter);
|
|
122
|
+
|
|
123
|
+
let current = queries.getFeatureById(featureId);
|
|
124
|
+
while (current?.phase === "planning") {
|
|
125
|
+
await waitForPlanDecision(featureId, adapter);
|
|
126
|
+
current = queries.getFeatureById(featureId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function printResumeStatus(feature) {
|
|
131
|
+
const meta = JSON.parse(feature.metadata || "{}");
|
|
132
|
+
|
|
133
|
+
if (feature.phase === "planning") {
|
|
134
|
+
const role = feature.active_planning_role || "pm";
|
|
135
|
+
const review = meta.awaiting_phase_review ? ` (awaiting ${meta.phase_review_role} review)` : "";
|
|
136
|
+
systemMsg(`Resuming planning — ${role} phase${review}`);
|
|
137
|
+
} else if (feature.phase === "impl") {
|
|
138
|
+
const gate = feature.gate_number || 0;
|
|
139
|
+
systemMsg(`Resuming implementation — gate ${gate}`);
|
|
140
|
+
} else {
|
|
141
|
+
systemMsg(`Resuming — ${feature.phase}`);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
async function printFinalStatus(featureId, shutdown) {
|
|
146
|
+
const finalFeature = queries.getFeatureById(featureId);
|
|
147
|
+
if (finalFeature?.phase === "failed") {
|
|
148
|
+
errorMsg(`Feature ${finalFeature.slug} failed.`);
|
|
149
|
+
} else {
|
|
150
|
+
successMsg("Feature complete!");
|
|
151
|
+
}
|
|
152
|
+
await shutdown();
|
|
153
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
// plan.js — `iriai-build plan [description]` command.
|
|
2
|
+
// Planning only. After plan approved, prompts "Continue to implementation?"
|
|
3
|
+
|
|
4
|
+
import { select } from "@inquirer/prompts";
|
|
5
|
+
import * as queries from "../../v3/queries.js";
|
|
6
|
+
import { bootstrap, initNewFeature } from "../bootstrap.js";
|
|
7
|
+
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
8
|
+
import { waitForPlanDecision, waitForCompletion } from "../wait.js";
|
|
9
|
+
|
|
10
|
+
export async function planCommand(description) {
|
|
11
|
+
banner();
|
|
12
|
+
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
13
|
+
|
|
14
|
+
// Defer impl launch so we can prompt "Continue to implementation?" first
|
|
15
|
+
orchestrator.deferImplLaunch = true;
|
|
16
|
+
|
|
17
|
+
let featureId;
|
|
18
|
+
let isNew = false;
|
|
19
|
+
|
|
20
|
+
if (description) {
|
|
21
|
+
const feature = await initNewFeature(orchestrator, description);
|
|
22
|
+
featureId = feature.id;
|
|
23
|
+
isNew = true;
|
|
24
|
+
} else {
|
|
25
|
+
const features = queries.getFeaturesForPlanCommand();
|
|
26
|
+
const choices = [
|
|
27
|
+
{ name: "[NEW] Start a new feature", value: "__new__" },
|
|
28
|
+
...features.map((f) => ({
|
|
29
|
+
name: formatFeatureChoice(f),
|
|
30
|
+
value: f.id,
|
|
31
|
+
})),
|
|
32
|
+
];
|
|
33
|
+
|
|
34
|
+
const selected = await select({
|
|
35
|
+
message: "Select a feature or start new:",
|
|
36
|
+
choices,
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (selected === "__new__") {
|
|
40
|
+
const desc = await input.promptFeatureDescription();
|
|
41
|
+
if (!desc) {
|
|
42
|
+
console.log("No description provided. Exiting.");
|
|
43
|
+
await shutdown();
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
const feature = await initNewFeature(orchestrator, desc);
|
|
47
|
+
featureId = feature.id;
|
|
48
|
+
isNew = true;
|
|
49
|
+
} else {
|
|
50
|
+
featureId = selected;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const feature = queries.getFeatureById(featureId);
|
|
55
|
+
input.setActiveFeature(featureId, feature.feature_channel || feature.slug);
|
|
56
|
+
orchestrator.startStaleScan();
|
|
57
|
+
|
|
58
|
+
if (!isNew && feature.phase === "plan-approval") {
|
|
59
|
+
// Already approved — skip straight to impl continuation prompt
|
|
60
|
+
return await promptImplContinuation(orchestrator, adapter, input, featureId, shutdown);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!isNew && feature.phase === "planning") {
|
|
64
|
+
const role = feature.active_planning_role || "pm";
|
|
65
|
+
const meta = JSON.parse(feature.metadata || "{}");
|
|
66
|
+
const review = meta.awaiting_phase_review ? ` (awaiting ${meta.phase_review_role} review)` : "";
|
|
67
|
+
systemMsg(`Resuming planning — ${role} phase${review}`);
|
|
68
|
+
input.startInputLoop();
|
|
69
|
+
await recovery.runForFeature(featureId);
|
|
70
|
+
} else {
|
|
71
|
+
// Start input loop
|
|
72
|
+
input.startInputLoop();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Wait for plan to be approved
|
|
76
|
+
await waitForPlanDecision(featureId, adapter);
|
|
77
|
+
|
|
78
|
+
// Handle rejection loops
|
|
79
|
+
let currentFeature = queries.getFeatureById(featureId);
|
|
80
|
+
while (currentFeature?.phase === "planning") {
|
|
81
|
+
await waitForPlanDecision(featureId, adapter);
|
|
82
|
+
currentFeature = queries.getFeatureById(featureId);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
if (currentFeature?.phase === "failed") {
|
|
86
|
+
errorMsg(`Feature ${currentFeature.slug} failed.`);
|
|
87
|
+
await shutdown();
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Plan approved — prompt for continuation
|
|
92
|
+
await promptImplContinuation(orchestrator, adapter, input, featureId, shutdown);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async function promptImplContinuation(orchestrator, adapter, input, featureId, shutdown) {
|
|
96
|
+
const shouldContinue = await input.promptContinueToImpl();
|
|
97
|
+
|
|
98
|
+
if (shouldContinue) {
|
|
99
|
+
orchestrator.deferImplLaunch = false;
|
|
100
|
+
systemMsg("Starting implementation...");
|
|
101
|
+
await orchestrator.startImplementation(featureId);
|
|
102
|
+
successMsg("Implementation launched. Monitoring agents...");
|
|
103
|
+
input.startInputLoop();
|
|
104
|
+
|
|
105
|
+
await waitForCompletion(featureId, adapter);
|
|
106
|
+
const finalFeature = queries.getFeatureById(featureId);
|
|
107
|
+
if (finalFeature?.phase === "failed") {
|
|
108
|
+
errorMsg(`Feature ${finalFeature.slug} failed.`);
|
|
109
|
+
} else {
|
|
110
|
+
successMsg("Feature complete!");
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
systemMsg("Exiting. Resume later with: iriai-build implementation");
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
await shutdown();
|
|
117
|
+
}
|