iriai-build 0.3.0 → 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 +168 -122
- 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("");
|
|
@@ -469,67 +476,43 @@ export class Orchestrator {
|
|
|
469
476
|
this._notifyOperatorOfDecision(feature, planDecision);
|
|
470
477
|
}
|
|
471
478
|
} else if (role === "ux-designer") {
|
|
472
|
-
//
|
|
473
|
-
await this.adapter.postPipelineMessage(feature.id,
|
|
474
|
-
`UX Designer complete. Starting UI Designer...`);
|
|
475
|
-
queries.updateFeaturePlanningRole(feature.id, "ui-designer");
|
|
476
|
-
this.dispatchPlanningRole(feature.id, "ui-designer");
|
|
477
|
-
} else if (role === "ui-designer") {
|
|
478
|
-
// Compound design step: ui-designer done → surface as "designer" phase-review
|
|
479
|
-
// Both sub-roles are complete; present combined output for user approval
|
|
480
|
-
await this._requestPhaseReview(feature, "designer", output);
|
|
481
|
-
} else if (role === "architect") {
|
|
482
|
-
// Architect validation: ensure structured plan directory before review gate.
|
|
483
|
-
// The architect often drifts to writing a monolithic architecture doc
|
|
484
|
-
// instead of the structured plan directory (plan.yaml + phases/ + task files).
|
|
485
|
-
// Validate required artifacts and redispatch if incomplete.
|
|
479
|
+
// Validate UX designer output before proceeding to UI designer
|
|
486
480
|
const planDir = path.join(feature.signal_dir, "plans");
|
|
487
|
-
const validation = this.
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
481
|
+
const validation = this._validatePlanningOutput(role, planDir);
|
|
482
|
+
if (!validation.pass) {
|
|
483
|
+
await this.adapter.postPipelineMessage(feature.id,
|
|
484
|
+
`UX Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
|
|
485
|
+
this._redispatchForMissingArtifacts(feature, role, validation);
|
|
491
486
|
} else {
|
|
492
|
-
//
|
|
493
|
-
console.log(`[orchestrator] Architect output incomplete for ${slug}: missing ${validation.missing.join(", ")}`);
|
|
487
|
+
// Compound design step: ux-designer done → dispatch ui-designer (no phase-review yet)
|
|
494
488
|
await this.adapter.postPipelineMessage(feature.id,
|
|
495
|
-
`
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
`
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
`WHAT IS MISSING (you must create these):`,
|
|
511
|
-
validation.missing.map(f => ` ✗ ${f}`).join("\n"),
|
|
512
|
-
``,
|
|
513
|
-
`INSTRUCTIONS:`,
|
|
514
|
-
`1. Read $PLAN_DIR/context.md for your investigation notes — do NOT re-explore the entire codebase`,
|
|
515
|
-
`2. Do targeted source code reads ONLY where you need specific file paths, function signatures, or line numbers for task files`,
|
|
516
|
-
`3. Produce plan.yaml with phase DAG, role assignments, and journey refs`,
|
|
517
|
-
`4. Create phases/ directory with structured task files (YAML frontmatter + detailed instructions)`,
|
|
518
|
-
`5. Create journeys/ directory with test journeys (happy-path, failure, regression)`,
|
|
519
|
-
`6. Every task file must cite real file paths and code evidence from the codebase`,
|
|
520
|
-
`7. Signal .done + .output when complete`,
|
|
521
|
-
``,
|
|
522
|
-
`Your context.md is the foundation. Build the structured plan ON TOP of it.`,
|
|
523
|
-
].join("\n");
|
|
524
|
-
|
|
525
|
-
writeSignal(path.join(signalDir, SIGNAL.USER_MESSAGE), redispatchMsg);
|
|
526
|
-
queries.updateFeaturePlanningRole(feature.id, "architect");
|
|
527
|
-
// Fresh context — investigation session is done, architect needs full budget for structured output
|
|
528
|
-
this.dispatchPlanningRole(feature.id, "architect", { continue: false });
|
|
489
|
+
`UX Designer complete. Starting UI Designer...`);
|
|
490
|
+
queries.updateFeaturePlanningRole(feature.id, "ui-designer");
|
|
491
|
+
this.dispatchPlanningRole(feature.id, "ui-designer");
|
|
492
|
+
}
|
|
493
|
+
} else if (role === "ui-designer") {
|
|
494
|
+
// Validate UI designer output before surfacing combined design review
|
|
495
|
+
const planDir = path.join(feature.signal_dir, "plans");
|
|
496
|
+
const validation = this._validatePlanningOutput(role, planDir);
|
|
497
|
+
if (!validation.pass) {
|
|
498
|
+
await this.adapter.postPipelineMessage(feature.id,
|
|
499
|
+
`UI Designer output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
|
|
500
|
+
this._redispatchForMissingArtifacts(feature, role, validation);
|
|
501
|
+
} else {
|
|
502
|
+
// Both sub-roles complete; present combined output for user approval
|
|
503
|
+
await this._requestPhaseReview(feature, "designer", output);
|
|
529
504
|
}
|
|
530
505
|
} else {
|
|
531
|
-
//
|
|
532
|
-
|
|
506
|
+
// All other roles (pm, architect, designer): validate then review gate
|
|
507
|
+
const planDir = path.join(feature.signal_dir, "plans");
|
|
508
|
+
const validation = this._validatePlanningOutput(role, planDir);
|
|
509
|
+
if (!validation.pass) {
|
|
510
|
+
await this.adapter.postPipelineMessage(feature.id,
|
|
511
|
+
`${PLANNING_ROLE_LABELS[role] || role} output incomplete (missing: ${validation.missing.join(", ")}). Redispatching...`);
|
|
512
|
+
this._redispatchForMissingArtifacts(feature, role, validation);
|
|
513
|
+
} else {
|
|
514
|
+
await this._requestPhaseReview(feature, role, output);
|
|
515
|
+
}
|
|
533
516
|
}
|
|
534
517
|
|
|
535
518
|
} finally {
|
|
@@ -539,69 +522,124 @@ export class Orchestrator {
|
|
|
539
522
|
}
|
|
540
523
|
|
|
541
524
|
/**
|
|
542
|
-
*
|
|
543
|
-
*
|
|
544
|
-
* Returns { pass: boolean, found: string[], missing: string[] }
|
|
525
|
+
* Artifact requirements per planning role.
|
|
526
|
+
* Each entry: { label, check(planDir) → boolean }
|
|
545
527
|
*/
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
528
|
+
static ROLE_ARTIFACTS = {
|
|
529
|
+
pm: [
|
|
530
|
+
{ label: "PRD document (*-prd.md)", check: (d) => !!findArtifact("prd", d) },
|
|
531
|
+
],
|
|
532
|
+
"ux-designer": [
|
|
533
|
+
{ label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
|
|
534
|
+
],
|
|
535
|
+
"ui-designer": [
|
|
536
|
+
{ label: "design-decisions.md (with Visual Design Language)", check: (d) => {
|
|
537
|
+
const content = findArtifact("design-decisions", d);
|
|
538
|
+
if (!content) return false;
|
|
539
|
+
try {
|
|
540
|
+
const text = fs.readFileSync(content, "utf-8");
|
|
541
|
+
return text.includes("Visual Design Language") || text.includes("Color Palette");
|
|
542
|
+
} catch { return false; }
|
|
543
|
+
}},
|
|
544
|
+
{ label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
|
|
545
|
+
],
|
|
546
|
+
designer: [
|
|
547
|
+
{ label: "design-decisions.md", check: (d) => !!findArtifact("design-decisions", d) },
|
|
548
|
+
{ label: "mockup.html", check: (d) => fs.existsSync(path.join(d, "mockup.html")) },
|
|
549
|
+
],
|
|
550
|
+
architect: [
|
|
551
|
+
{ label: "context.md (investigation notes)", check: (d) => !!findArtifact("context", d) || !!findArtifact("architecture", d) },
|
|
552
|
+
{ label: "plan.yaml (phase DAG)", check: (d) => fs.existsSync(path.join(d, "plan.yaml")) },
|
|
553
|
+
{ label: "phases/ (task files)", check: (d) => {
|
|
554
|
+
try {
|
|
555
|
+
return fs.readdirSync(path.join(d, "phases"), { withFileTypes: true }).some(e => e.isDirectory());
|
|
556
|
+
} catch { return false; }
|
|
557
|
+
}},
|
|
558
|
+
{ label: "journeys/ (test journeys)", check: (d) => {
|
|
559
|
+
try {
|
|
560
|
+
return fs.readdirSync(path.join(d, "journeys")).some(e => e.endsWith(".md"));
|
|
561
|
+
} catch { return false; }
|
|
562
|
+
}},
|
|
563
|
+
],
|
|
564
|
+
};
|
|
549
565
|
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
566
|
+
/**
|
|
567
|
+
* Validate that a planning role produced its required artifacts.
|
|
568
|
+
* Runs auto-normalization (e.g. architecture.md → context.md) before checking.
|
|
569
|
+
* Returns { pass, found, missing }
|
|
570
|
+
*/
|
|
571
|
+
_validatePlanningOutput(role, planDir) {
|
|
572
|
+
// Auto-normalize known filename drift
|
|
573
|
+
if (role === "architect") {
|
|
574
|
+
const archFile = path.join(planDir, "architecture.md");
|
|
575
|
+
const contextFile = path.join(planDir, "context.md");
|
|
576
|
+
if (fs.existsSync(archFile) && !fs.existsSync(contextFile)) {
|
|
577
|
+
try {
|
|
578
|
+
fs.renameSync(archFile, contextFile);
|
|
579
|
+
console.log(`[orchestrator] Renamed architecture.md → context.md in ${planDir}`);
|
|
580
|
+
} catch { /* ok */ }
|
|
581
|
+
}
|
|
558
582
|
}
|
|
559
583
|
|
|
560
|
-
|
|
561
|
-
if (
|
|
562
|
-
found.push("context.md (investigation notes)");
|
|
563
|
-
} else {
|
|
564
|
-
missing.push("context.md (investigation notes)");
|
|
565
|
-
}
|
|
584
|
+
const artifacts = Orchestrator.ROLE_ARTIFACTS[role];
|
|
585
|
+
if (!artifacts) return { pass: true, found: [], missing: [] };
|
|
566
586
|
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
587
|
+
const found = [];
|
|
588
|
+
const missing = [];
|
|
589
|
+
for (const art of artifacts) {
|
|
590
|
+
if (art.check(planDir)) {
|
|
591
|
+
found.push(art.label);
|
|
592
|
+
} else {
|
|
593
|
+
missing.push(art.label);
|
|
594
|
+
}
|
|
572
595
|
}
|
|
573
596
|
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
let hasPhases = false;
|
|
577
|
-
try {
|
|
578
|
-
const entries = fs.readdirSync(phasesDir, { withFileTypes: true });
|
|
579
|
-
hasPhases = entries.some(e => e.isDirectory());
|
|
580
|
-
} catch { /* doesn't exist */ }
|
|
581
|
-
if (hasPhases) {
|
|
582
|
-
found.push("phases/ (task files)");
|
|
583
|
-
} else {
|
|
584
|
-
missing.push("phases/ directory with task files");
|
|
585
|
-
}
|
|
597
|
+
return { pass: missing.length === 0, found, missing };
|
|
598
|
+
}
|
|
586
599
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
}
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
600
|
+
/**
|
|
601
|
+
* Redispatch a planning role that produced incomplete output.
|
|
602
|
+
* Writes focused instructions to .user-message and re-spawns with fresh context.
|
|
603
|
+
*/
|
|
604
|
+
_redispatchForMissingArtifacts(feature, role, validation) {
|
|
605
|
+
const slug = feature.slug;
|
|
606
|
+
console.log(`[orchestrator] ${role} output incomplete for ${slug}: missing ${validation.missing.join(", ")}`);
|
|
607
|
+
|
|
608
|
+
const signalDir = path.join(feature.signal_dir, "planning", role);
|
|
609
|
+
ensureDir(signalDir);
|
|
610
|
+
|
|
611
|
+
const roleName = PLANNING_ROLE_LABELS[role] || role;
|
|
612
|
+
const lines = [
|
|
613
|
+
`INCOMPLETE OUTPUT — REDISPATCH`,
|
|
614
|
+
``,
|
|
615
|
+
`You signaled .done but required artifacts are missing.`,
|
|
616
|
+
`Read your CLAUDE.md output format section and produce the missing items.`,
|
|
617
|
+
``,
|
|
618
|
+
`WHAT EXISTS (do NOT redo):`,
|
|
619
|
+
...validation.found.map(f => ` ✓ ${f}`),
|
|
620
|
+
``,
|
|
621
|
+
`WHAT IS MISSING (you MUST create these):`,
|
|
622
|
+
...validation.missing.map(f => ` ✗ ${f}`),
|
|
623
|
+
``,
|
|
624
|
+
`Write the missing artifacts to $PLAN_DIR/, then signal .done + .output again.`,
|
|
625
|
+
];
|
|
626
|
+
|
|
627
|
+
// Role-specific guidance
|
|
628
|
+
if (role === "architect") {
|
|
629
|
+
lines.push(
|
|
630
|
+
``,
|
|
631
|
+
`ARCHITECT-SPECIFIC:`,
|
|
632
|
+
`1. Read $PLAN_DIR/context.md for your investigation notes — do NOT re-explore the entire codebase`,
|
|
633
|
+
`2. Do targeted source code reads ONLY where needed for specific file paths or function signatures`,
|
|
634
|
+
`3. Every task file must cite real file paths and code evidence from the codebase`,
|
|
635
|
+
);
|
|
598
636
|
}
|
|
599
637
|
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
};
|
|
638
|
+
writeSignal(path.join(signalDir, SIGNAL.USER_MESSAGE), lines.join("\n"));
|
|
639
|
+
queries.updateFeaturePlanningRole(feature.id, role);
|
|
640
|
+
|
|
641
|
+
// Fresh context for the structured output pass
|
|
642
|
+
this.dispatchPlanningRole(feature.id, role, { continue: false });
|
|
605
643
|
}
|
|
606
644
|
|
|
607
645
|
/**
|
|
@@ -1065,6 +1103,7 @@ export class Orchestrator {
|
|
|
1065
1103
|
rolesDir: V3_ROLES_DIR,
|
|
1066
1104
|
implBase: IMPL_BASE,
|
|
1067
1105
|
planDir,
|
|
1106
|
+
budget: this.budget,
|
|
1068
1107
|
});
|
|
1069
1108
|
|
|
1070
1109
|
// Launch implementation agents
|
|
@@ -1074,7 +1113,7 @@ export class Orchestrator {
|
|
|
1074
1113
|
queries.updateFeaturePhase(featureId, "impl");
|
|
1075
1114
|
});
|
|
1076
1115
|
|
|
1077
|
-
queries.insertEvent(featureId, "phase-transition", "bridge",
|
|
1116
|
+
queries.insertEvent(featureId, "phase-transition", "bridge", `Implementation launched (budget: ${this.budget.label})`);
|
|
1078
1117
|
} catch (err) {
|
|
1079
1118
|
console.error("[orchestrator] Implementation launch error:", err.message);
|
|
1080
1119
|
const stderr = err.stderr ? err.stderr.toString().slice(-500) : err.message;
|
|
@@ -1146,6 +1185,7 @@ export class Orchestrator {
|
|
|
1146
1185
|
rolesDir: V3_ROLES_DIR,
|
|
1147
1186
|
implBase: IMPL_BASE,
|
|
1148
1187
|
planDir,
|
|
1188
|
+
budget: this.budget,
|
|
1149
1189
|
});
|
|
1150
1190
|
} catch (launchErr) {
|
|
1151
1191
|
console.error("[orchestrator] launchImpl failed:", launchErr.message);
|
|
@@ -1318,6 +1358,7 @@ export class Orchestrator {
|
|
|
1318
1358
|
const feature = queries.getFeatureById(featureId);
|
|
1319
1359
|
if (!feature) return;
|
|
1320
1360
|
|
|
1361
|
+
const budget = this.budget;
|
|
1321
1362
|
const tree = this.discoverSignalTree(feature.slug);
|
|
1322
1363
|
const numTeams = Object.keys(tree.teams).length || feature.num_teams;
|
|
1323
1364
|
const worktreeDir = path.join(PROJECT_ROOT, ".features", feature.slug);
|
|
@@ -1337,10 +1378,11 @@ export class Orchestrator {
|
|
|
1337
1378
|
roleName: "feature-lead",
|
|
1338
1379
|
signalDir: tree.featureLead,
|
|
1339
1380
|
cwd: featureCwd,
|
|
1340
|
-
|
|
1381
|
+
model: getModel(budget, "feature-lead"),
|
|
1382
|
+
maxRetries: budget.retries.featureLead,
|
|
1341
1383
|
});
|
|
1342
1384
|
} else {
|
|
1343
|
-
this._syncAgentRetries(flAgent,
|
|
1385
|
+
this._syncAgentRetries(flAgent, budget.retries.featureLead);
|
|
1344
1386
|
}
|
|
1345
1387
|
|
|
1346
1388
|
// Use "refresh" mode if FEATURE-STATUS.md exists (recovery/restart scenario)
|
|
@@ -1364,10 +1406,11 @@ export class Orchestrator {
|
|
|
1364
1406
|
roleName: "operator",
|
|
1365
1407
|
signalDir: tree.operator,
|
|
1366
1408
|
cwd: featureCwd,
|
|
1367
|
-
|
|
1409
|
+
model: getModel(budget, "operator"),
|
|
1410
|
+
maxRetries: budget.retries.operator,
|
|
1368
1411
|
});
|
|
1369
1412
|
} else {
|
|
1370
|
-
this._syncAgentRetries(opAgent,
|
|
1413
|
+
this._syncAgentRetries(opAgent, budget.retries.operator);
|
|
1371
1414
|
}
|
|
1372
1415
|
// Operator is reactive — spawned by routeUserMessage
|
|
1373
1416
|
queries.updateAgentStatus(opAgent.id, "idle");
|
|
@@ -1390,10 +1433,11 @@ export class Orchestrator {
|
|
|
1390
1433
|
teamNum,
|
|
1391
1434
|
signalDir: team.orchestrator,
|
|
1392
1435
|
cwd: teamCwd,
|
|
1393
|
-
|
|
1436
|
+
model: getModel(budget, "team-orchestrator"),
|
|
1437
|
+
maxRetries: budget.retries.orchestrator,
|
|
1394
1438
|
});
|
|
1395
1439
|
} else {
|
|
1396
|
-
this._syncAgentRetries(orchAgent,
|
|
1440
|
+
this._syncAgentRetries(orchAgent, budget.retries.orchestrator);
|
|
1397
1441
|
}
|
|
1398
1442
|
// Orchestrator waits for .task — no auto-start
|
|
1399
1443
|
}
|
|
@@ -1411,10 +1455,11 @@ export class Orchestrator {
|
|
|
1411
1455
|
teamNum,
|
|
1412
1456
|
signalDir: roleDir,
|
|
1413
1457
|
cwd: teamCwd,
|
|
1414
|
-
|
|
1458
|
+
model: getModel(budget, "role-agent"),
|
|
1459
|
+
maxRetries: budget.retries.role,
|
|
1415
1460
|
});
|
|
1416
1461
|
} else {
|
|
1417
|
-
this._syncAgentRetries(roleAgent,
|
|
1462
|
+
this._syncAgentRetries(roleAgent, budget.retries.role);
|
|
1418
1463
|
}
|
|
1419
1464
|
// Role waits for .task — no auto-start
|
|
1420
1465
|
}
|
|
@@ -1432,10 +1477,11 @@ export class Orchestrator {
|
|
|
1432
1477
|
roleName: role,
|
|
1433
1478
|
signalDir: reviewDir,
|
|
1434
1479
|
cwd: featureCwd,
|
|
1435
|
-
|
|
1480
|
+
model: getModel(budget, "review-agent"),
|
|
1481
|
+
maxRetries: budget.retries.role,
|
|
1436
1482
|
});
|
|
1437
1483
|
} else {
|
|
1438
|
-
this._syncAgentRetries(reviewAgent,
|
|
1484
|
+
this._syncAgentRetries(reviewAgent, budget.retries.role);
|
|
1439
1485
|
}
|
|
1440
1486
|
}
|
|
1441
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:
|