iriai-build 0.3.1 → 0.4.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/cli.js +9 -6
- package/cli/bootstrap.js +8 -4
- package/cli/commands/implementation.js +3 -2
- package/cli/commands/launch.js +3 -2
- package/cli/commands/plan.js +3 -2
- package/cli/commands/setup.js +17 -1
- package/cli/config.js +8 -0
- package/cli/first-run.js +19 -1
- package/package.json +1 -1
- package/v3/agent-supervisor.js +8 -5
- package/v3/budget.js +110 -0
- package/v3/constants.js +3 -1
- package/v3/launch-impl.js +30 -9
- package/v3/orchestrator.js +28 -13
- package/v3/prompt-builder.js +24 -19
- package/v3/roles/architect/CLAUDE.md +10 -0
- package/v3/roles/feature-lead/CLAUDE.md +2 -2
- package/v3/roles/plan-compiler/CLAUDE.md +25 -0
package/bin/cli.js
CHANGED
|
@@ -38,23 +38,26 @@ program
|
|
|
38
38
|
program
|
|
39
39
|
.command("plan [description]")
|
|
40
40
|
.description("Run planning pipeline. After plan approved, prompts to continue to implementation.")
|
|
41
|
-
.
|
|
42
|
-
|
|
41
|
+
.option("--budget <tier>", "Usage budget tier: unrestricted, econ, or budget")
|
|
42
|
+
.action(async (description, opts) => {
|
|
43
|
+
await withReadyCheck(() => planCommand(description, opts));
|
|
43
44
|
});
|
|
44
45
|
|
|
45
46
|
program
|
|
46
47
|
.command("implementation")
|
|
47
48
|
.alias("impl")
|
|
48
49
|
.description("Select from planned/in-progress features and run implementation.")
|
|
49
|
-
.
|
|
50
|
-
|
|
50
|
+
.option("--budget <tier>", "Usage budget tier: unrestricted, econ, or budget")
|
|
51
|
+
.action(async (opts) => {
|
|
52
|
+
await withReadyCheck(() => implementationCommand(opts));
|
|
51
53
|
});
|
|
52
54
|
|
|
53
55
|
program
|
|
54
56
|
.command("launch [description]")
|
|
55
57
|
.description("Full flow: planning -> implementation -> complete. Without arg: select or create.")
|
|
56
|
-
.
|
|
57
|
-
|
|
58
|
+
.option("--budget <tier>", "Usage budget tier: unrestricted, econ, or budget")
|
|
59
|
+
.action(async (description, opts) => {
|
|
60
|
+
await withReadyCheck(() => launchCommand(description, opts));
|
|
58
61
|
});
|
|
59
62
|
|
|
60
63
|
program
|
package/cli/bootstrap.js
CHANGED
|
@@ -11,17 +11,21 @@ import * as queries from "../v3/queries.js";
|
|
|
11
11
|
import { DB_PATH, PORTAL_PORT } from "../v3/constants.js";
|
|
12
12
|
import { ArtifactPortal } from "../v3/artifact-portal.js";
|
|
13
13
|
import { slugify } from "../v3/helpers.js";
|
|
14
|
+
import { resolveBudget } from "../v3/budget.js";
|
|
14
15
|
|
|
15
16
|
/**
|
|
16
17
|
* Bootstrap the engine for CLI use.
|
|
17
|
-
*
|
|
18
|
+
* @param {object} [opts]
|
|
19
|
+
* @param {string} [opts.budgetOverride] - CLI flag override for budget tier
|
|
20
|
+
* Returns { orchestrator, adapter, recovery, input, shutdown, budget }.
|
|
18
21
|
*/
|
|
19
|
-
export function bootstrap() {
|
|
22
|
+
export function bootstrap({ budgetOverride } = {}) {
|
|
20
23
|
db.open(DB_PATH);
|
|
21
24
|
|
|
25
|
+
const budget = resolveBudget(budgetOverride);
|
|
22
26
|
const adapter = new TerminalAdapter();
|
|
23
27
|
const reviewSessions = new ReviewSessionManager();
|
|
24
|
-
const orchestrator = new Orchestrator({ adapter, reviewSessions });
|
|
28
|
+
const orchestrator = new Orchestrator({ adapter, reviewSessions, budget });
|
|
25
29
|
const input = new TerminalInput({ orchestrator });
|
|
26
30
|
adapter.setInputHandler(input);
|
|
27
31
|
const recovery = new Recovery({ orchestrator, adapter });
|
|
@@ -61,7 +65,7 @@ export function bootstrap() {
|
|
|
61
65
|
});
|
|
62
66
|
}
|
|
63
67
|
|
|
64
|
-
return { orchestrator, adapter, recovery, input, shutdown };
|
|
68
|
+
return { orchestrator, adapter, recovery, input, shutdown, budget };
|
|
65
69
|
}
|
|
66
70
|
|
|
67
71
|
/**
|
|
@@ -7,9 +7,10 @@ import { bootstrap } from "../bootstrap.js";
|
|
|
7
7
|
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
8
8
|
import { waitForCompletion } from "../wait.js";
|
|
9
9
|
|
|
10
|
-
export async function implementationCommand() {
|
|
10
|
+
export async function implementationCommand(opts = {}) {
|
|
11
11
|
banner();
|
|
12
|
-
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
12
|
+
const { orchestrator, adapter, recovery, input, shutdown, budget } = bootstrap({ budgetOverride: opts.budget });
|
|
13
|
+
systemMsg(`Budget: ${budget.label} — ${budget.description}`);
|
|
13
14
|
|
|
14
15
|
const features = queries.getFeaturesForImplCommand();
|
|
15
16
|
if (features.length === 0) {
|
package/cli/commands/launch.js
CHANGED
|
@@ -8,9 +8,10 @@ import { bootstrap, initNewFeature } from "../bootstrap.js";
|
|
|
8
8
|
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
9
9
|
import { waitForPlanDecision, waitForCompletion } from "../wait.js";
|
|
10
10
|
|
|
11
|
-
export async function launchCommand(description) {
|
|
11
|
+
export async function launchCommand(description, opts = {}) {
|
|
12
12
|
banner();
|
|
13
|
-
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
13
|
+
const { orchestrator, adapter, recovery, input, shutdown, budget } = bootstrap({ budgetOverride: opts.budget });
|
|
14
|
+
systemMsg(`Budget: ${budget.label} — ${budget.description}`);
|
|
14
15
|
|
|
15
16
|
let featureId;
|
|
16
17
|
let isNew = false;
|
package/cli/commands/plan.js
CHANGED
|
@@ -7,9 +7,10 @@ import { bootstrap, initNewFeature } from "../bootstrap.js";
|
|
|
7
7
|
import { banner, formatFeatureChoice, successMsg, systemMsg, errorMsg } from "../display.js";
|
|
8
8
|
import { waitForPlanDecision, waitForCompletion } from "../wait.js";
|
|
9
9
|
|
|
10
|
-
export async function planCommand(description) {
|
|
10
|
+
export async function planCommand(description, opts = {}) {
|
|
11
11
|
banner();
|
|
12
|
-
const { orchestrator, adapter, recovery, input, shutdown } = bootstrap();
|
|
12
|
+
const { orchestrator, adapter, recovery, input, shutdown, budget } = bootstrap({ budgetOverride: opts.budget });
|
|
13
|
+
systemMsg(`Budget: ${budget.label} — ${budget.description}`);
|
|
13
14
|
|
|
14
15
|
// Defer impl launch so we can prompt "Continue to implementation?" first
|
|
15
16
|
orchestrator.deferImplLaunch = true;
|
package/cli/commands/setup.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// setup.js — `iriai-build setup` command.
|
|
2
2
|
// Interactive config for Slack tokens and tool paths.
|
|
3
3
|
|
|
4
|
-
import { input, confirm } from "@inquirer/prompts";
|
|
4
|
+
import { input, confirm, select } from "@inquirer/prompts";
|
|
5
5
|
import * as config from "../config.js";
|
|
6
6
|
import { banner, systemMsg, successMsg, errorMsg } from "../display.js";
|
|
7
|
+
import { BUDGET_TIERS } from "../../v3/budget.js";
|
|
7
8
|
|
|
8
9
|
function mask(value) {
|
|
9
10
|
if (!value) return null;
|
|
@@ -47,6 +48,20 @@ export async function setupCommand() {
|
|
|
47
48
|
message: `Slack Planning Channel ID (C...):${hint(current.slack_channel_id)}`,
|
|
48
49
|
});
|
|
49
50
|
|
|
51
|
+
// Budget tier
|
|
52
|
+
console.log("\n\x1b[1mUsage Budget\x1b[0m");
|
|
53
|
+
console.log("\x1b[2mControls model selection, team count, and review depth during implementation.\x1b[0m");
|
|
54
|
+
console.log("\x1b[2mPlanning is always full-quality regardless of tier.\x1b[0m\n");
|
|
55
|
+
|
|
56
|
+
const budgetTier = await select({
|
|
57
|
+
message: `Usage budget:${hint(current.budget_tier || "unrestricted")}`,
|
|
58
|
+
choices: Object.entries(BUDGET_TIERS).map(([key, tier]) => ({
|
|
59
|
+
name: `${tier.label} — ${tier.description}`,
|
|
60
|
+
value: key,
|
|
61
|
+
})),
|
|
62
|
+
default: current.budget_tier || "unrestricted",
|
|
63
|
+
});
|
|
64
|
+
|
|
50
65
|
// Tool paths
|
|
51
66
|
console.log("\n\x1b[1mTool Paths\x1b[0m");
|
|
52
67
|
console.log("\x1b[2mOptional — Enter to keep defaults.\x1b[0m\n");
|
|
@@ -67,6 +82,7 @@ export async function setupCommand() {
|
|
|
67
82
|
if (channelId) updates.slack_channel_id = channelId;
|
|
68
83
|
if (claudeBin) updates.claude_bin = claudeBin;
|
|
69
84
|
if (qaFeedbackBin) updates.qa_feedback_bin = qaFeedbackBin;
|
|
85
|
+
updates.budget_tier = budgetTier;
|
|
70
86
|
|
|
71
87
|
config.save(updates);
|
|
72
88
|
|
package/cli/config.js
CHANGED
|
@@ -92,6 +92,14 @@ export function getProjectRoot() {
|
|
|
92
92
|
return get("project_root", "PROJECT_ROOT") || process.cwd();
|
|
93
93
|
}
|
|
94
94
|
|
|
95
|
+
/**
|
|
96
|
+
* Get the budget tier. Env var IRIAI_BUDGET overrides config file.
|
|
97
|
+
* Defaults to null (will be prompted on first run).
|
|
98
|
+
*/
|
|
99
|
+
export function getBudgetTier() {
|
|
100
|
+
return get("budget_tier", "IRIAI_BUDGET") || null;
|
|
101
|
+
}
|
|
102
|
+
|
|
95
103
|
/**
|
|
96
104
|
* Get the config file path (for display purposes).
|
|
97
105
|
*/
|
package/cli/first-run.js
CHANGED
|
@@ -5,9 +5,10 @@
|
|
|
5
5
|
import fs from "node:fs";
|
|
6
6
|
import path from "node:path";
|
|
7
7
|
import { execSync } from "node:child_process";
|
|
8
|
-
import { input } from "@inquirer/prompts";
|
|
8
|
+
import { input, select } from "@inquirer/prompts";
|
|
9
9
|
import * as config from "./config.js";
|
|
10
10
|
import { banner, systemMsg, successMsg, errorMsg, separator } from "./display.js";
|
|
11
|
+
import { BUDGET_TIERS } from "../v3/budget.js";
|
|
11
12
|
|
|
12
13
|
const C = {
|
|
13
14
|
reset: "\x1b[0m",
|
|
@@ -65,10 +66,27 @@ export async function onboard() {
|
|
|
65
66
|
default: detectedClaude || "claude",
|
|
66
67
|
});
|
|
67
68
|
|
|
69
|
+
// 3. Budget tier
|
|
70
|
+
console.log("");
|
|
71
|
+
console.log(`${C.bold}Usage Budget${C.reset}`);
|
|
72
|
+
console.log(`${C.dim}Controls model selection, team count, and review depth during implementation.${C.reset}`);
|
|
73
|
+
console.log(`${C.dim}Planning is always full-quality regardless of tier. Change anytime via ${C.bold}iriai-build setup${C.reset}${C.dim}.${C.reset}`);
|
|
74
|
+
console.log("");
|
|
75
|
+
|
|
76
|
+
const budgetTier = await select({
|
|
77
|
+
message: "Select your usage budget:",
|
|
78
|
+
choices: Object.entries(BUDGET_TIERS).map(([key, tier]) => ({
|
|
79
|
+
name: `${tier.label} — ${tier.description}`,
|
|
80
|
+
value: key,
|
|
81
|
+
})),
|
|
82
|
+
default: "unrestricted",
|
|
83
|
+
});
|
|
84
|
+
|
|
68
85
|
// Save minimal config
|
|
69
86
|
config.save({
|
|
70
87
|
project_root: resolvedRoot,
|
|
71
88
|
claude_bin: claudeBin || detectedClaude || "claude",
|
|
89
|
+
budget_tier: budgetTier,
|
|
72
90
|
});
|
|
73
91
|
|
|
74
92
|
console.log("");
|
package/package.json
CHANGED
package/v3/agent-supervisor.js
CHANGED
|
@@ -13,11 +13,13 @@ import {
|
|
|
13
13
|
ROLE_HARD_TIMEOUT_MS, ORCH_SOFT_TIMEOUT_MS, FL_SOFT_TIMEOUT_MS,
|
|
14
14
|
RSS_CEILING_KB, STUCK_THRESHOLD_MS, HEALTH_CHECK_INTERVAL_MS,
|
|
15
15
|
} from "./constants.js";
|
|
16
|
+
import { resolveBudget } from "./budget.js";
|
|
16
17
|
import { readSignal, ensureDir } from "./helpers.js";
|
|
17
18
|
|
|
18
19
|
export class AgentSupervisor extends EventEmitter {
|
|
19
|
-
constructor() {
|
|
20
|
+
constructor(budget) {
|
|
20
21
|
super();
|
|
22
|
+
this.budget = budget || resolveBudget();
|
|
21
23
|
// agentKey -> { process: AgentProcess, retryTimer, healthRegistered }
|
|
22
24
|
this._processes = {};
|
|
23
25
|
this._healthTimer = null;
|
|
@@ -254,7 +256,8 @@ export class AgentSupervisor extends EventEmitter {
|
|
|
254
256
|
const agent = queries.getAgentByKey(key);
|
|
255
257
|
if (!agent) continue;
|
|
256
258
|
|
|
257
|
-
// Hard timeout based on agent type
|
|
259
|
+
// Hard timeout based on agent type (budget-aware for role and FL)
|
|
260
|
+
const budgetTimeouts = this.budget.timeouts;
|
|
258
261
|
let hardTimeout;
|
|
259
262
|
switch (agent.agent_type) {
|
|
260
263
|
case "planning-role":
|
|
@@ -262,16 +265,16 @@ export class AgentSupervisor extends EventEmitter {
|
|
|
262
265
|
break;
|
|
263
266
|
case "role-agent":
|
|
264
267
|
case "review-agent":
|
|
265
|
-
hardTimeout =
|
|
268
|
+
hardTimeout = budgetTimeouts.roleHardMs;
|
|
266
269
|
break;
|
|
267
270
|
case "team-orchestrator":
|
|
268
271
|
hardTimeout = ORCH_SOFT_TIMEOUT_MS;
|
|
269
272
|
break;
|
|
270
273
|
case "feature-lead":
|
|
271
|
-
hardTimeout =
|
|
274
|
+
hardTimeout = budgetTimeouts.flSoftMs;
|
|
272
275
|
break;
|
|
273
276
|
default:
|
|
274
|
-
hardTimeout =
|
|
277
|
+
hardTimeout = budgetTimeouts.roleHardMs;
|
|
275
278
|
}
|
|
276
279
|
|
|
277
280
|
if (hardTimeout && elapsed > hardTimeout) {
|
package/v3/budget.js
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// budget.js — Budget tier presets for iriai-build.
|
|
2
|
+
// Controls model selection, team limits, role sets, review agents, timeouts, and retries.
|
|
3
|
+
// Planning phase is identical across all tiers.
|
|
4
|
+
|
|
5
|
+
import * as config from "../cli/config.js";
|
|
6
|
+
|
|
7
|
+
export const BUDGET_TIERS = {
|
|
8
|
+
unrestricted: {
|
|
9
|
+
label: "Unrestricted",
|
|
10
|
+
description: "Full power — Opus everywhere, up to 5 teams, all review agents",
|
|
11
|
+
models: {
|
|
12
|
+
"feature-lead": "opus",
|
|
13
|
+
"team-orchestrator": "opus",
|
|
14
|
+
"role-agent": "opus",
|
|
15
|
+
"review-agent": "opus",
|
|
16
|
+
operator: "haiku",
|
|
17
|
+
},
|
|
18
|
+
maxTeams: 5,
|
|
19
|
+
reviewRoles: ["integration-tester", "code-reviewer", "security-auditor", "regression-tester", "verifier"],
|
|
20
|
+
timeouts: {
|
|
21
|
+
roleHardMs: 75 * 60 * 1000,
|
|
22
|
+
flSoftMs: 45 * 60 * 1000,
|
|
23
|
+
},
|
|
24
|
+
retries: {
|
|
25
|
+
role: 3,
|
|
26
|
+
orchestrator: 3,
|
|
27
|
+
featureLead: 5,
|
|
28
|
+
featureLeadInit: 3,
|
|
29
|
+
planning: 5,
|
|
30
|
+
operator: 3,
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
|
|
34
|
+
econ: {
|
|
35
|
+
label: "Econ",
|
|
36
|
+
description: "Balanced — Sonnet for most agents, 1 team, code reviewer + verifier",
|
|
37
|
+
models: {
|
|
38
|
+
"feature-lead": "opus",
|
|
39
|
+
"team-orchestrator": "sonnet",
|
|
40
|
+
"role-agent": "sonnet",
|
|
41
|
+
"review-agent": "sonnet",
|
|
42
|
+
operator: "haiku",
|
|
43
|
+
},
|
|
44
|
+
maxTeams: 1,
|
|
45
|
+
reviewRoles: ["code-reviewer", "verifier"],
|
|
46
|
+
timeouts: {
|
|
47
|
+
roleHardMs: 30 * 60 * 1000,
|
|
48
|
+
flSoftMs: 20 * 60 * 1000,
|
|
49
|
+
},
|
|
50
|
+
retries: {
|
|
51
|
+
role: 2,
|
|
52
|
+
orchestrator: 2,
|
|
53
|
+
featureLead: 3,
|
|
54
|
+
featureLeadInit: 2,
|
|
55
|
+
planning: 5,
|
|
56
|
+
operator: 3,
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
|
|
60
|
+
budget: {
|
|
61
|
+
label: "Budget",
|
|
62
|
+
description: "Minimal spend — Haiku/Sonnet, 1 team, verifier only",
|
|
63
|
+
models: {
|
|
64
|
+
"feature-lead": "sonnet",
|
|
65
|
+
"team-orchestrator": "sonnet",
|
|
66
|
+
"role-agent": "haiku",
|
|
67
|
+
"review-agent": "haiku",
|
|
68
|
+
operator: "haiku",
|
|
69
|
+
},
|
|
70
|
+
maxTeams: 1,
|
|
71
|
+
reviewRoles: ["verifier"],
|
|
72
|
+
timeouts: {
|
|
73
|
+
roleHardMs: 15 * 60 * 1000,
|
|
74
|
+
flSoftMs: 10 * 60 * 1000,
|
|
75
|
+
},
|
|
76
|
+
retries: {
|
|
77
|
+
role: 1,
|
|
78
|
+
orchestrator: 1,
|
|
79
|
+
featureLead: 2,
|
|
80
|
+
featureLeadInit: 1,
|
|
81
|
+
planning: 5,
|
|
82
|
+
operator: 3,
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Resolve the active budget tier. Priority: explicit override > config > default (unrestricted).
|
|
89
|
+
* @param {string} [override] - CLI flag override (e.g., --budget econ)
|
|
90
|
+
* @returns {object} The budget preset object
|
|
91
|
+
*/
|
|
92
|
+
export function resolveBudget(override) {
|
|
93
|
+
const tierName = override || config.get("budget_tier") || "unrestricted";
|
|
94
|
+
const tier = BUDGET_TIERS[tierName];
|
|
95
|
+
if (!tier) {
|
|
96
|
+
console.warn(`[budget] Unknown tier "${tierName}", falling back to unrestricted`);
|
|
97
|
+
return { ...BUDGET_TIERS.unrestricted, name: "unrestricted" };
|
|
98
|
+
}
|
|
99
|
+
return { ...tier, name: tierName };
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Get the model for a given agent type from the budget.
|
|
104
|
+
* @param {object} budget - Resolved budget preset
|
|
105
|
+
* @param {string} agentType - One of: feature-lead, team-orchestrator, role-agent, review-agent, operator
|
|
106
|
+
* @returns {string} Model name (opus, sonnet, haiku)
|
|
107
|
+
*/
|
|
108
|
+
export function getModel(budget, agentType) {
|
|
109
|
+
return budget.models[agentType] || "opus";
|
|
110
|
+
}
|
package/v3/constants.js
CHANGED
|
@@ -163,10 +163,12 @@ export const AVAILABLE_TEMPLATES = [];
|
|
|
163
163
|
export const PORTAL_PORT = process.env.PORTAL_PORT ? parseInt(process.env.PORTAL_PORT) : 8900;
|
|
164
164
|
|
|
165
165
|
// ─── Feature Review Roles ────────────────────────────────────────────────────
|
|
166
|
+
// Default (unrestricted) review roles. Budget-aware code should use budget.reviewRoles instead.
|
|
166
167
|
|
|
167
168
|
export const FEATURE_REVIEW_ROLES = [
|
|
168
169
|
"integration-tester",
|
|
169
170
|
"code-reviewer",
|
|
170
171
|
"security-auditor",
|
|
171
|
-
"
|
|
172
|
+
"regression-tester",
|
|
173
|
+
"verifier",
|
|
172
174
|
];
|
package/v3/launch-impl.js
CHANGED
|
@@ -5,6 +5,7 @@ import fs from "node:fs";
|
|
|
5
5
|
import path from "node:path";
|
|
6
6
|
import { execSync } from "node:child_process";
|
|
7
7
|
import { IMPL_BASE, PROJECT_ROOT, V3_ROLES_DIR } from "./constants.js";
|
|
8
|
+
import { resolveBudget } from "./budget.js";
|
|
8
9
|
|
|
9
10
|
const LEADERSHIP_ROLES = new Set([
|
|
10
11
|
"orchestrator", "feature-lead", "planning-lead", "pm", "designer", "architect", "operator",
|
|
@@ -76,25 +77,43 @@ function discoverDispatchRoles(rolesDir) {
|
|
|
76
77
|
*
|
|
77
78
|
* @param {string} featureSlug - Kebab-case feature name
|
|
78
79
|
* @param {object} opts
|
|
79
|
-
* @param {number} [opts.numTeams
|
|
80
|
+
* @param {number} [opts.numTeams] - Number of teams (capped by budget.maxTeams)
|
|
80
81
|
* @param {string[]} [opts.roles] - Explicit roles (auto-discovered if not set)
|
|
81
82
|
* @param {string} [opts.rolesDir] - Roles directory (defaults to V3_ROLES_DIR)
|
|
82
83
|
* @param {string} [opts.implBase] - Implementation signal base directory
|
|
83
84
|
* @param {string} [opts.planDir] - Plan directory to check for plan.yaml
|
|
85
|
+
* @param {object} [opts.budget] - Resolved budget preset (from resolveBudget())
|
|
84
86
|
*/
|
|
85
87
|
export function launchImpl(featureSlug, {
|
|
86
|
-
numTeams
|
|
88
|
+
numTeams,
|
|
87
89
|
roles,
|
|
88
90
|
rolesDir = V3_ROLES_DIR,
|
|
89
91
|
implBase = IMPL_BASE,
|
|
90
92
|
planDir,
|
|
93
|
+
budget: budgetOverride,
|
|
91
94
|
} = {}) {
|
|
95
|
+
const budget = budgetOverride || resolveBudget();
|
|
92
96
|
const dispatchRoles = roles || discoverDispatchRoles(rolesDir);
|
|
93
97
|
const featureDir = path.join(implBase, "features", featureSlug);
|
|
94
98
|
|
|
99
|
+
// Determine team count: explicit param > plan.yaml num_teams > budget max.
|
|
100
|
+
// Plan compiler should set num_teams based on actual plan structure (not to fill a max).
|
|
101
|
+
// Budget maxTeams is always the hard cap.
|
|
102
|
+
let planTeams = numTeams;
|
|
103
|
+
if (!planTeams && planDir) {
|
|
104
|
+
try {
|
|
105
|
+
const planYaml = fs.readFileSync(path.join(planDir, "plan.yaml"), "utf-8");
|
|
106
|
+
const match = planYaml.match(/^num_teams:\s*(\d+)/m);
|
|
107
|
+
if (match) planTeams = parseInt(match[1], 10);
|
|
108
|
+
} catch { /* no plan.yaml or no num_teams field */ }
|
|
109
|
+
}
|
|
110
|
+
const effectiveTeams = Math.min(planTeams || budget.maxTeams, budget.maxTeams);
|
|
111
|
+
|
|
95
112
|
console.log(`\nLaunching feature: ${featureSlug}`);
|
|
96
|
-
console.log(`
|
|
113
|
+
console.log(` Budget: ${budget.label}`);
|
|
114
|
+
console.log(` Teams: ${effectiveTeams} (max: ${budget.maxTeams})`);
|
|
97
115
|
console.log(` Roles per team: ${dispatchRoles.length}`);
|
|
116
|
+
console.log(` Review roles: ${budget.reviewRoles.join(", ")}`);
|
|
98
117
|
console.log(` Signal base: ${featureDir}`);
|
|
99
118
|
|
|
100
119
|
// Warn if no plan.yaml
|
|
@@ -117,15 +136,15 @@ export function launchImpl(featureSlug, {
|
|
|
117
136
|
symlinkRole(rolesDir, "operator", opDir);
|
|
118
137
|
}
|
|
119
138
|
|
|
120
|
-
// Feature-level review roles
|
|
121
|
-
for (const reviewRole of
|
|
139
|
+
// Feature-level review roles (budget-aware)
|
|
140
|
+
for (const reviewRole of budget.reviewRoles) {
|
|
122
141
|
const reviewDir = path.join(featureDir, "feature-review", reviewRole);
|
|
123
142
|
fs.mkdirSync(reviewDir, { recursive: true });
|
|
124
143
|
symlinkRole(rolesDir, reviewRole, reviewDir);
|
|
125
144
|
}
|
|
126
145
|
|
|
127
146
|
// Per-team directories
|
|
128
|
-
for (let i = 1; i <=
|
|
147
|
+
for (let i = 1; i <= effectiveTeams; i++) {
|
|
129
148
|
const teamDir = path.join(featureDir, "teams", `team-${i}`);
|
|
130
149
|
const orchDir = path.join(teamDir, "orchestrator");
|
|
131
150
|
|
|
@@ -190,11 +209,13 @@ export function launchImpl(featureSlug, {
|
|
|
190
209
|
}
|
|
191
210
|
fs.writeFileSync(path.join(flMcpDir, "settings.local.json"), JSON.stringify({ mcpServers: flServers }, null, 2) + "\n");
|
|
192
211
|
|
|
193
|
-
// Feature-level review roles
|
|
194
|
-
|
|
212
|
+
// Feature-level review roles (only configure MCP for roles in budget)
|
|
213
|
+
if (budget.reviewRoles.includes("integration-tester")) {
|
|
214
|
+
configureMcp("integration-tester", path.join(featureDir, "feature-review", "integration-tester"));
|
|
215
|
+
}
|
|
195
216
|
|
|
196
217
|
// Per-team roles
|
|
197
|
-
for (let i = 1; i <=
|
|
218
|
+
for (let i = 1; i <= effectiveTeams; i++) {
|
|
198
219
|
const teamDir = path.join(featureDir, "teams", `team-${i}`);
|
|
199
220
|
configureMcp("orchestrator", path.join(teamDir, "orchestrator"));
|
|
200
221
|
for (const role of dispatchRoles) {
|
package/v3/orchestrator.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
} from "./prompt-builder.js";
|
|
19
19
|
import { planningComplete } from "./planning-complete.js";
|
|
20
20
|
import { launchImpl } from "./launch-impl.js";
|
|
21
|
+
import { resolveBudget, getModel } from "./budget.js";
|
|
21
22
|
import {
|
|
22
23
|
IMPL_BASE, IRIAI_TEAM_DIR, PROJECT_ROOT,
|
|
23
24
|
V3_ROLES_DIR, PLANNING_ROLES, PLANNING_ROLE_LABELS,
|
|
@@ -38,10 +39,11 @@ import { formatAnnotationsAsFeedback } from "./review-sessions.js";
|
|
|
38
39
|
import { compilePlanReviewHtml } from "./plan-compiler.js";
|
|
39
40
|
|
|
40
41
|
export class Orchestrator {
|
|
41
|
-
constructor({ adapter, reviewSessions = null }) {
|
|
42
|
+
constructor({ adapter, reviewSessions = null, budget = null }) {
|
|
42
43
|
this.adapter = adapter;
|
|
43
44
|
this.reviewSessions = reviewSessions;
|
|
44
|
-
this.
|
|
45
|
+
this.budget = budget || resolveBudget();
|
|
46
|
+
this.supervisor = new AgentSupervisor(this.budget);
|
|
45
47
|
this.fileIO = new FileIO();
|
|
46
48
|
|
|
47
49
|
// Per-feature signal trees (discovered on launch)
|
|
@@ -348,6 +350,11 @@ export class Orchestrator {
|
|
|
348
350
|
`THREAD_TS=${feature.thread_ts}`,
|
|
349
351
|
];
|
|
350
352
|
|
|
353
|
+
// Inject available review roles for architect and plan-compiler
|
|
354
|
+
if (role === "architect" || role === "plan-compiler") {
|
|
355
|
+
taskHeaderLines.push(`AVAILABLE_REVIEW_ROLES=${this.budget.reviewRoles.join(",")}`);
|
|
356
|
+
}
|
|
357
|
+
|
|
351
358
|
// Role-specific dispatch instructions (interview enforcement)
|
|
352
359
|
if ((role === "architect" || role === "pm" || role === "designer" || role === "ux-designer") && !revisionContext) {
|
|
353
360
|
taskHeaderLines.push("");
|
|
@@ -1096,6 +1103,7 @@ export class Orchestrator {
|
|
|
1096
1103
|
rolesDir: V3_ROLES_DIR,
|
|
1097
1104
|
implBase: IMPL_BASE,
|
|
1098
1105
|
planDir,
|
|
1106
|
+
budget: this.budget,
|
|
1099
1107
|
});
|
|
1100
1108
|
|
|
1101
1109
|
// Launch implementation agents
|
|
@@ -1105,7 +1113,7 @@ export class Orchestrator {
|
|
|
1105
1113
|
queries.updateFeaturePhase(featureId, "impl");
|
|
1106
1114
|
});
|
|
1107
1115
|
|
|
1108
|
-
queries.insertEvent(featureId, "phase-transition", "bridge",
|
|
1116
|
+
queries.insertEvent(featureId, "phase-transition", "bridge", `Implementation launched (budget: ${this.budget.label})`);
|
|
1109
1117
|
} catch (err) {
|
|
1110
1118
|
console.error("[orchestrator] Implementation launch error:", err.message);
|
|
1111
1119
|
const stderr = err.stderr ? err.stderr.toString().slice(-500) : err.message;
|
|
@@ -1177,6 +1185,7 @@ export class Orchestrator {
|
|
|
1177
1185
|
rolesDir: V3_ROLES_DIR,
|
|
1178
1186
|
implBase: IMPL_BASE,
|
|
1179
1187
|
planDir,
|
|
1188
|
+
budget: this.budget,
|
|
1180
1189
|
});
|
|
1181
1190
|
} catch (launchErr) {
|
|
1182
1191
|
console.error("[orchestrator] launchImpl failed:", launchErr.message);
|
|
@@ -1349,6 +1358,7 @@ export class Orchestrator {
|
|
|
1349
1358
|
const feature = queries.getFeatureById(featureId);
|
|
1350
1359
|
if (!feature) return;
|
|
1351
1360
|
|
|
1361
|
+
const budget = this.budget;
|
|
1352
1362
|
const tree = this.discoverSignalTree(feature.slug);
|
|
1353
1363
|
const numTeams = Object.keys(tree.teams).length || feature.num_teams;
|
|
1354
1364
|
const worktreeDir = path.join(PROJECT_ROOT, ".features", feature.slug);
|
|
@@ -1368,10 +1378,11 @@ export class Orchestrator {
|
|
|
1368
1378
|
roleName: "feature-lead",
|
|
1369
1379
|
signalDir: tree.featureLead,
|
|
1370
1380
|
cwd: featureCwd,
|
|
1371
|
-
|
|
1381
|
+
model: getModel(budget, "feature-lead"),
|
|
1382
|
+
maxRetries: budget.retries.featureLead,
|
|
1372
1383
|
});
|
|
1373
1384
|
} else {
|
|
1374
|
-
this._syncAgentRetries(flAgent,
|
|
1385
|
+
this._syncAgentRetries(flAgent, budget.retries.featureLead);
|
|
1375
1386
|
}
|
|
1376
1387
|
|
|
1377
1388
|
// Use "refresh" mode if FEATURE-STATUS.md exists (recovery/restart scenario)
|
|
@@ -1395,10 +1406,11 @@ export class Orchestrator {
|
|
|
1395
1406
|
roleName: "operator",
|
|
1396
1407
|
signalDir: tree.operator,
|
|
1397
1408
|
cwd: featureCwd,
|
|
1398
|
-
|
|
1409
|
+
model: getModel(budget, "operator"),
|
|
1410
|
+
maxRetries: budget.retries.operator,
|
|
1399
1411
|
});
|
|
1400
1412
|
} else {
|
|
1401
|
-
this._syncAgentRetries(opAgent,
|
|
1413
|
+
this._syncAgentRetries(opAgent, budget.retries.operator);
|
|
1402
1414
|
}
|
|
1403
1415
|
// Operator is reactive — spawned by routeUserMessage
|
|
1404
1416
|
queries.updateAgentStatus(opAgent.id, "idle");
|
|
@@ -1421,10 +1433,11 @@ export class Orchestrator {
|
|
|
1421
1433
|
teamNum,
|
|
1422
1434
|
signalDir: team.orchestrator,
|
|
1423
1435
|
cwd: teamCwd,
|
|
1424
|
-
|
|
1436
|
+
model: getModel(budget, "team-orchestrator"),
|
|
1437
|
+
maxRetries: budget.retries.orchestrator,
|
|
1425
1438
|
});
|
|
1426
1439
|
} else {
|
|
1427
|
-
this._syncAgentRetries(orchAgent,
|
|
1440
|
+
this._syncAgentRetries(orchAgent, budget.retries.orchestrator);
|
|
1428
1441
|
}
|
|
1429
1442
|
// Orchestrator waits for .task — no auto-start
|
|
1430
1443
|
}
|
|
@@ -1442,10 +1455,11 @@ export class Orchestrator {
|
|
|
1442
1455
|
teamNum,
|
|
1443
1456
|
signalDir: roleDir,
|
|
1444
1457
|
cwd: teamCwd,
|
|
1445
|
-
|
|
1458
|
+
model: getModel(budget, "role-agent"),
|
|
1459
|
+
maxRetries: budget.retries.role,
|
|
1446
1460
|
});
|
|
1447
1461
|
} else {
|
|
1448
|
-
this._syncAgentRetries(roleAgent,
|
|
1462
|
+
this._syncAgentRetries(roleAgent, budget.retries.role);
|
|
1449
1463
|
}
|
|
1450
1464
|
// Role waits for .task — no auto-start
|
|
1451
1465
|
}
|
|
@@ -1463,10 +1477,11 @@ export class Orchestrator {
|
|
|
1463
1477
|
roleName: role,
|
|
1464
1478
|
signalDir: reviewDir,
|
|
1465
1479
|
cwd: featureCwd,
|
|
1466
|
-
|
|
1480
|
+
model: getModel(budget, "review-agent"),
|
|
1481
|
+
maxRetries: budget.retries.role,
|
|
1467
1482
|
});
|
|
1468
1483
|
} else {
|
|
1469
|
-
this._syncAgentRetries(reviewAgent,
|
|
1484
|
+
this._syncAgentRetries(reviewAgent, budget.retries.role);
|
|
1470
1485
|
}
|
|
1471
1486
|
}
|
|
1472
1487
|
|
package/v3/prompt-builder.js
CHANGED
|
@@ -667,37 +667,42 @@ adversarial cross-check happens LAST (after all review agents have completed):
|
|
|
667
667
|
1. **Read team evidence** — Read \`.gate-evidence.yaml\` from each team's orchestrator signal dir.
|
|
668
668
|
If any team lacks \`.gate-evidence.yaml\`, REJECT the gate immediately.
|
|
669
669
|
2. **Push team branches** and create PRs (team branch → integration branch) per repo using gh CLI
|
|
670
|
-
3. **
|
|
670
|
+
3. **Determine which review agents to dispatch** for this gate:
|
|
671
|
+
a. Read the current phase's \`phase.yaml\` and look for the \`review_roles\` field.
|
|
672
|
+
b. If \`review_roles\` is present, dispatch ONLY the listed roles.
|
|
673
|
+
c. If \`review_roles\` is absent, discover available review roles by listing the directories under \`${featureReviewDir}/\`:
|
|
674
|
+
\`\`\`bash
|
|
675
|
+
ls ${featureReviewDir}/
|
|
676
|
+
\`\`\`
|
|
677
|
+
Dispatch all discovered review roles.
|
|
678
|
+
d. Only dispatch to roles that have a directory — skip any that don't exist.
|
|
679
|
+
4. **Dispatch review agents** by writing .task files to each role:
|
|
671
680
|
\`\`\`bash
|
|
672
681
|
PREVIEW_ENV=""
|
|
673
682
|
if [ -f "${featureLeadDir}/preview-env.json" ]; then
|
|
674
683
|
PREVIEW_ENV="Preview environment: ${featureLeadDir}/preview-env.json"
|
|
675
684
|
fi
|
|
676
|
-
|
|
677
|
-
|
|
685
|
+
# For each review role from step 3:
|
|
686
|
+
cat > ${featureReviewDir}/<role-name>/.task << TASK_EOF
|
|
687
|
+
<your review task here — include gate context, PRs, journeys to verify>
|
|
678
688
|
\$PREVIEW_ENV
|
|
679
689
|
TASK_EOF
|
|
680
|
-
cat > ${featureReviewDir}/code-reviewer/.task << 'TASK_EOF'
|
|
681
|
-
<your review task here>
|
|
682
|
-
TASK_EOF
|
|
683
|
-
cat > ${featureReviewDir}/security-auditor/.task << 'TASK_EOF'
|
|
684
|
-
<your review task here>
|
|
685
|
-
TASK_EOF
|
|
686
690
|
\`\`\`
|
|
687
|
-
|
|
691
|
+
5. **Wait for all dispatched review agents** to complete (.done files) — poll every 75s:
|
|
688
692
|
\`\`\`bash
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
+
# Check .done for each role you dispatched in step 4
|
|
694
|
+
ALL_DONE=true
|
|
695
|
+
for role in <list of dispatched roles>; do
|
|
696
|
+
[ ! -f "${featureReviewDir}/$role/.done" ] && ALL_DONE=false
|
|
693
697
|
done
|
|
698
|
+
# Repeat until ALL_DONE is true
|
|
694
699
|
\`\`\`
|
|
695
|
-
|
|
700
|
+
6. **Adversarial cross-check** (FINAL step before user sees it):
|
|
696
701
|
- Cross-check evidence across ALL teams for inconsistencies
|
|
697
702
|
- Call \`get_screenshots\` for critical journeys and independently verify orchestrator claims
|
|
698
703
|
- Review feature-level review agent outputs
|
|
699
704
|
- If discrepancies found → REJECT gate, do NOT escalate to user
|
|
700
|
-
|
|
705
|
+
7. **Merge evidence** — Combine all team YAMLs + feature-level review outputs into:
|
|
701
706
|
\`\`\`bash
|
|
702
707
|
cat > ${featureLeadDir}/.gate-evidence.yaml << 'EVIDENCE_EOF'
|
|
703
708
|
<merged evidence YAML>
|
|
@@ -706,14 +711,14 @@ adversarial cross-check happens LAST (after all review agents have completed):
|
|
|
706
711
|
Include: \`coverage_matrix\`, \`deviations\`, \`self_reported_risks\` (aggregated from all teams)
|
|
707
712
|
Include: \`reviewer_comments\` with your FL assessment
|
|
708
713
|
Include: \`cross_team_surface\` (APIs, contracts, shared state)
|
|
709
|
-
|
|
714
|
+
8. **Compile feature gate HTML** — Call \`compile_gate_evidence\` MCP tool:
|
|
710
715
|
- \`evidence_yaml_path\`: ${featureLeadDir}/.gate-evidence.yaml
|
|
711
716
|
- \`output_html_path\`: ${featureLeadDir}/.gate-evidence.html
|
|
712
717
|
- \`doc_type\`: \`"feature"\`
|
|
713
718
|
- \`team_html_paths\`: list of \`{ team_num, html_path }\` for each team's gate HTML
|
|
714
|
-
- If tool returns ERROR → re-dispatch affected verification role → retry from step
|
|
719
|
+
- If tool returns ERROR → re-dispatch affected verification role → retry from step 5
|
|
715
720
|
- Do NOT proceed until \`compile_gate_evidence\` succeeds
|
|
716
|
-
|
|
721
|
+
9. **Post feature gate HTML** to .agent-response — the HTML IS the message:
|
|
717
722
|
- \`[evidence:${featureLeadDir}/.gate-evidence.html]\` marker
|
|
718
723
|
- \`[DECISION]\` block with approve/reject buttons
|
|
719
724
|
- No text summary needed — the HTML contains everything
|
|
@@ -272,6 +272,7 @@ Conforms to `phase.schema.md`. Contains:
|
|
|
272
272
|
- Phase identity (id, title, objective, risk)
|
|
273
273
|
- Task DAG — ordered list of tasks with dependency references
|
|
274
274
|
- Role assignments — which role executes which tasks
|
|
275
|
+
- Review roles — which feature-level review agents run at this phase's gate
|
|
275
276
|
- Phase-level acceptance criteria (user_criteria with action/observe pairs)
|
|
276
277
|
- Phase-scoped journey references
|
|
277
278
|
- Regression test references
|
|
@@ -311,6 +312,15 @@ role_assignments:
|
|
|
311
312
|
verifier: ["1.v"]
|
|
312
313
|
test-author: ["1.t"]
|
|
313
314
|
|
|
315
|
+
# Which feature-level review agents run at this phase's gate.
|
|
316
|
+
# ONLY use roles from $AVAILABLE_REVIEW_ROLES (set in your task header).
|
|
317
|
+
# Choose based on what changed: security-auditor for auth/data changes,
|
|
318
|
+
# integration-tester for API/UI flows, code-reviewer for structural quality, etc.
|
|
319
|
+
# If omitted, all available review roles are dispatched (wasteful — always specify).
|
|
320
|
+
review_roles:
|
|
321
|
+
- code-reviewer
|
|
322
|
+
- integration-tester
|
|
323
|
+
|
|
314
324
|
acceptance:
|
|
315
325
|
user_criteria:
|
|
316
326
|
- action: "POST /api/bot-collaborator/invite with body {repo_owner, repo_name}"
|
|
@@ -57,8 +57,8 @@ You post to the feature's channel via the bridge. Rules:
|
|
|
57
57
|
|
|
58
58
|
1. **Read team evidence** — Read `.gate-evidence.yaml` from each team's orchestrator signal dir
|
|
59
59
|
2. **Validate evidence exists** — If any team lacks `.gate-evidence.yaml`, REJECT the gate immediately (write feedback to team orchestrator, do not escalate to user)
|
|
60
|
-
3. **Dispatch feature-level review agents**
|
|
61
|
-
4. **Wait for review agents to complete** — read their `.output` files
|
|
60
|
+
3. **Dispatch feature-level review agents** — read the current phase's `phase.yaml` for a `review_roles` field. If present, dispatch ONLY those roles. If absent, dispatch all review roles that have directories under `feature-review/`. These run against the merged codebase and produce their own `.output` files.
|
|
61
|
+
4. **Wait for dispatched review agents to complete** — read their `.output` files
|
|
62
62
|
4b. **Review gaps across all levels.** Read `gaps` from:
|
|
63
63
|
- Each team orchestrator's `.gate-evidence.yaml` (team-level QA gaps)
|
|
64
64
|
- Each team's compiled `.gate-evidence.html` (review visually)
|
|
@@ -58,6 +58,16 @@ You are a fresh-context validator. The Architect just produced a structured plan
|
|
|
58
58
|
- [ ] No orphaned tasks (every task is reachable from the root of the DAG)
|
|
59
59
|
- [ ] Parallelizable tasks have no false dependencies (tasks that could run in parallel shouldn't depend on each other unless truly necessary)
|
|
60
60
|
|
|
61
|
+
### 2b. Review Roles Validation
|
|
62
|
+
|
|
63
|
+
- [ ] Every `phase.yaml` has a `review_roles` list (warn if missing — FL will fall back to dispatching all available review roles, which is wasteful)
|
|
64
|
+
- [ ] Every role in `review_roles` is from `$AVAILABLE_REVIEW_ROLES` (set in your task header) — reject any role not in that list
|
|
65
|
+
- [ ] Review role selection makes sense for the phase:
|
|
66
|
+
- Phases touching auth, secrets, or data access should include `security-auditor`
|
|
67
|
+
- Phases with API or UI changes should include `integration-tester`
|
|
68
|
+
- `code-reviewer` and `verifier` are reasonable defaults for any phase
|
|
69
|
+
- `regression-tester` should be included when the phase modifies existing behavior
|
|
70
|
+
|
|
61
71
|
### 3. Codebase Cross-Check
|
|
62
72
|
|
|
63
73
|
For every task's `scope.modify` and `scope.read` paths:
|
|
@@ -204,6 +214,21 @@ budget:
|
|
|
204
214
|
|
|
205
215
|
If the user delegates, use sensible defaults and document them.
|
|
206
216
|
|
|
217
|
+
### Step 6c: Team Count (`num_teams` in plan.yaml)
|
|
218
|
+
|
|
219
|
+
Add a `num_teams` field to `plan.yaml` that reflects the **actual parallelism needed by the plan** — NOT just filling to a maximum. Consider:
|
|
220
|
+
|
|
221
|
+
- How many independent domain boundaries exist (e.g., backend vs frontend vs infrastructure)
|
|
222
|
+
- Whether phases can actually run in parallel or are sequential
|
|
223
|
+
- A single-service feature with 2 phases probably needs 1 team, not 5
|
|
224
|
+
|
|
225
|
+
The orchestrator will cap `num_teams` to the user's budget tier maximum, so err toward what the plan actually needs. If only 2 independent workstreams exist, set `num_teams: 2` even if the budget allows 5.
|
|
226
|
+
|
|
227
|
+
```yaml
|
|
228
|
+
# In plan.yaml:
|
|
229
|
+
num_teams: 2 # Based on 2 independent workstreams: backend API + frontend UI
|
|
230
|
+
```
|
|
231
|
+
|
|
207
232
|
### Step 7: Write Final Report
|
|
208
233
|
|
|
209
234
|
Write your findings to `.output` with this structure:
|