Skip to content

Commit d566bca

Browse files
committed
feat(workflows): add autonomous mode with controller agent support
- Introduce controller agent role and configuration - Implement autonomous workflow execution with controller input - Add controller selection during onboarding - Support controller actions (NEXT, SKIP, STOP) and text input - Add new controller agent type and prompt template
1 parent d5dc926 commit d566bca

File tree

12 files changed

+454
-51
lines changed

12 files changed

+454
-51
lines changed

config/main.agents.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,15 @@ module.exports = [
7171
promptPath: path.join(promptsDir, 'codemachine', 'fallback-agents', 'planning-fallback.md'),
7272
},
7373

74+
// BMAD controller (Product Owner)
75+
{
76+
id: 'bmad-po',
77+
name: 'PO - Product Owner',
78+
description: 'BMAD product owner controller for autonomous mode',
79+
role: 'controller',
80+
promptPath: path.join(promptsDir, 'bmad', 'controller', 'system.md'),
81+
},
82+
7483
// BMAD agents
7584
{
7685
id: 'bmad-analyst',
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
# Product Owner Controller
2+
3+
You are the Product Owner (PO) driving a product discovery session. Your role is to collaborate with AI agents (Analyst, PM, etc.) to define and refine the product vision.
4+
5+
## Your Role
6+
7+
- Provide clear, actionable answers to agent questions
8+
- Make decisions about product scope, features, and priorities
9+
- Share business context and user insights
10+
- Keep the conversation focused and productive
11+
12+
## How This Works
13+
14+
1. An agent (e.g., Analyst, PM) will ask you questions or present findings
15+
2. You respond with your answer or feedback
16+
3. The conversation continues until the agent completes their work

src/cli/tui/app-shell.tsx

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,11 @@ import { MonitoringCleanup } from "../../agents/monitoring/index.js"
2323
import path from "path"
2424
import { createRequire } from "node:module"
2525
import { resolvePackageJson } from "../../shared/runtime/root.js"
26-
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName } from "../../shared/workflows/index.js"
26+
import { getSelectedTrack, setSelectedTrack, hasSelectedConditions, setSelectedConditions, getProjectName, setProjectName, getControllerAgents, initControllerAgent } from "../../shared/workflows/index.js"
2727
import { loadTemplate } from "../../workflows/templates/loader.js"
2828
import { getTemplatePathFromTracking } from "../../shared/workflows/template.js"
2929
import type { TrackConfig, ConditionConfig } from "../../workflows/templates/types"
30+
import type { AgentDefinition } from "../../shared/agents/config/types"
3031
import type { InitialToast } from "./app"
3132

3233
// Module-level view state for post-processing effects
@@ -127,6 +128,7 @@ export function App(props: { initialToast?: InitialToast }) {
127128
const [templateTracks, setTemplateTracks] = createSignal<Record<string, TrackConfig> | null>(null)
128129
const [templateConditions, setTemplateConditions] = createSignal<Record<string, ConditionConfig> | null>(null)
129130
const [initialProjectName, setInitialProjectName] = createSignal<string | null>(null)
131+
const [controllerAgents, setControllerAgents] = createSignal<AgentDefinition[] | null>(null)
130132

131133
let pendingWorkflowStart: (() => void) | null = null
132134

@@ -155,10 +157,18 @@ export function App(props: { initialToast?: InitialToast }) {
155157
const needsConditionsSelection = hasConditions && !conditionsSelected
156158
const needsProjectName = !existingProjectName
157159

158-
// If project name, tracks or conditions need selection, show onboard view
159-
if (needsProjectName || needsTrackSelection || needsConditionsSelection) {
160+
// Check if workflow requires controller selection
161+
let controllers: AgentDefinition[] = []
162+
if (template.controller === true) {
163+
controllers = await getControllerAgents(cwd)
164+
}
165+
const needsControllerSelection = controllers.length > 0
166+
167+
// If project name, tracks, conditions, or controller need selection, show onboard view
168+
if (needsProjectName || needsTrackSelection || needsConditionsSelection || needsControllerSelection) {
160169
if (hasTracks) setTemplateTracks(template.tracks!)
161170
if (hasConditions) setTemplateConditions(template.conditions!)
171+
if (needsControllerSelection) setControllerAgents(controllers)
162172
setInitialProjectName(existingProjectName) // Pass existing name if any (to skip that step)
163173
currentView = "onboard"
164174
setView("onboard")
@@ -196,7 +206,7 @@ export function App(props: { initialToast?: InitialToast }) {
196206
setView("workflow")
197207
}
198208

199-
const handleOnboardComplete = async (result: { projectName?: string; trackId?: string; conditions?: string[] }) => {
209+
const handleOnboardComplete = async (result: { projectName?: string; trackId?: string; conditions?: string[]; controllerAgentId?: string }) => {
200210
const cwd = process.env.CODEMACHINE_CWD || process.cwd()
201211
const cmRoot = path.join(cwd, '.codemachine')
202212

@@ -215,6 +225,21 @@ export function App(props: { initialToast?: InitialToast }) {
215225
await setSelectedConditions(cmRoot, result.conditions)
216226
}
217227

228+
// Initialize controller agent if selected
229+
if (result.controllerAgentId) {
230+
const agent = controllerAgents()?.find(a => a.id === result.controllerAgentId)
231+
if (agent) {
232+
// Get prompt path from agent config (or use default)
233+
const promptPath = (agent.promptPath as string) || `prompts/agents/${result.controllerAgentId}/system.md`
234+
try {
235+
await initControllerAgent(result.controllerAgentId, promptPath, cwd, cmRoot)
236+
} catch (error) {
237+
console.error("Failed to initialize controller agent:", error)
238+
// Continue anyway - workflow will run without autonomous mode
239+
}
240+
}
241+
}
242+
218243
// Start workflow
219244
startWorkflowExecution()
220245
}
@@ -316,6 +341,7 @@ export function App(props: { initialToast?: InitialToast }) {
316341
<Onboard
317342
tracks={templateTracks() ?? undefined}
318343
conditions={templateConditions() ?? undefined}
344+
controllerAgents={controllerAgents() ?? undefined}
319345
initialProjectName={initialProjectName()}
320346
onComplete={handleOnboardComplete}
321347
onCancel={handleOnboardCancel}

src/cli/tui/routes/onboard/index.tsx

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,18 @@ import { createSignal, For, onMount, Show, createEffect } from "solid-js"
99
import { useKeyboard, useTerminalDimensions } from "@opentui/solid"
1010
import { useTheme } from "@tui/shared/context/theme"
1111
import type { TrackConfig, ConditionConfig } from "../../../../workflows/templates/types"
12+
import type { AgentDefinition } from "../../../../shared/agents/config/types"
1213

1314
export interface OnboardProps {
1415
tracks?: Record<string, TrackConfig>
1516
conditions?: Record<string, ConditionConfig>
17+
controllerAgents?: AgentDefinition[] // Available controller agents
1618
initialProjectName?: string | null // If set, skip project name input
17-
onComplete: (result: { projectName?: string; trackId?: string; conditions?: string[] }) => void
19+
onComplete: (result: { projectName?: string; trackId?: string; conditions?: string[]; controllerAgentId?: string }) => void
1820
onCancel?: () => void
1921
}
2022

21-
type OnboardStep = 'project_name' | 'tracks' | 'conditions'
23+
type OnboardStep = 'project_name' | 'tracks' | 'conditions' | 'controller'
2224

2325
export function Onboard(props: OnboardProps) {
2426
const themeCtx = useTheme()
@@ -32,9 +34,11 @@ export function Onboard(props: OnboardProps) {
3234
const [projectName, setProjectName] = createSignal("")
3335
const [selectedTrackId, setSelectedTrackId] = createSignal<string | undefined>()
3436
const [selectedConditions, setSelectedConditions] = createSignal<Set<string>>(new Set())
37+
const [selectedControllerId, setSelectedControllerId] = createSignal<string | undefined>()
3538

3639
const hasTracks = () => props.tracks && Object.keys(props.tracks).length > 0
3740
const hasConditions = () => props.conditions && Object.keys(props.conditions).length > 0
41+
const hasControllers = () => props.controllerAgents && props.controllerAgents.length > 0
3842

3943
// Determine initial step - skip project_name if already set
4044
onMount(() => {
@@ -44,6 +48,8 @@ export function Onboard(props: OnboardProps) {
4448
setCurrentStep('tracks')
4549
} else if (hasConditions()) {
4650
setCurrentStep('conditions')
51+
} else if (hasControllers()) {
52+
setCurrentStep('controller')
4753
} else {
4854
props.onComplete({ projectName: props.initialProjectName })
4955
}
@@ -53,18 +59,28 @@ export function Onboard(props: OnboardProps) {
5359
const projectNameQuestion = "What is your project name?"
5460
const trackQuestion = "What is your project size?"
5561
const conditionsQuestion = "What features does your project have?"
62+
const controllerQuestion = "Select a controller agent for autonomous mode:"
5663

5764
const trackEntries = () => props.tracks ? Object.entries(props.tracks) : []
5865
const conditionEntries = () => props.conditions ? Object.entries(props.conditions) : []
66+
const controllerEntries = () => props.controllerAgents ? props.controllerAgents.map(a => [a.id, a] as const) : []
5967

6068
const currentQuestion = () => {
6169
switch (currentStep()) {
6270
case 'project_name': return projectNameQuestion
6371
case 'tracks': return trackQuestion
6472
case 'conditions': return conditionsQuestion
73+
case 'controller': return controllerQuestion
74+
}
75+
}
76+
const currentEntries = () => {
77+
switch (currentStep()) {
78+
case 'tracks': return trackEntries()
79+
case 'conditions': return conditionEntries()
80+
case 'controller': return controllerEntries()
81+
default: return []
6582
}
6683
}
67-
const currentEntries = () => currentStep() === 'tracks' ? trackEntries() : conditionEntries()
6884

6985
// Typing effect - reset when step changes
7086
createEffect(() => {
@@ -96,6 +112,8 @@ export function Onboard(props: OnboardProps) {
96112
setCurrentStep('tracks')
97113
} else if (hasConditions()) {
98114
setCurrentStep('conditions')
115+
} else if (hasControllers()) {
116+
setCurrentStep('controller')
99117
} else {
100118
props.onComplete({ projectName: name })
101119
}
@@ -105,16 +123,32 @@ export function Onboard(props: OnboardProps) {
105123
setSelectedTrackId(trackId)
106124
if (hasConditions()) {
107125
setCurrentStep('conditions')
126+
} else if (hasControllers()) {
127+
setCurrentStep('controller')
108128
} else {
109129
props.onComplete({ projectName: projectName(), trackId })
110130
}
111131
}
112132

113133
const handleConditionsComplete = () => {
134+
if (hasControllers()) {
135+
setCurrentStep('controller')
136+
} else {
137+
props.onComplete({
138+
projectName: projectName(),
139+
trackId: selectedTrackId(),
140+
conditions: Array.from(selectedConditions())
141+
})
142+
}
143+
}
144+
145+
const handleControllerSelect = (controllerId: string) => {
146+
setSelectedControllerId(controllerId)
114147
props.onComplete({
115148
projectName: projectName(),
116149
trackId: selectedTrackId(),
117-
conditions: Array.from(selectedConditions())
150+
conditions: Array.from(selectedConditions()),
151+
controllerAgentId: controllerId
118152
})
119153
}
120154

@@ -164,11 +198,14 @@ export function Onboard(props: OnboardProps) {
164198
evt.preventDefault()
165199
if (step === 'tracks') {
166200
const [trackId] = entries[selectedIndex()]
167-
handleTrackSelect(trackId)
201+
handleTrackSelect(trackId as string)
202+
} else if (step === 'controller') {
203+
const [controllerId] = entries[selectedIndex()]
204+
handleControllerSelect(controllerId as string)
168205
} else {
169206
// In conditions step, Enter toggles the checkbox
170207
const [conditionId] = entries[selectedIndex()]
171-
toggleCondition(conditionId)
208+
toggleCondition(conditionId as string)
172209
}
173210
} else if (evt.name === "tab" && step === 'conditions') {
174211
evt.preventDefault()
@@ -182,10 +219,13 @@ export function Onboard(props: OnboardProps) {
182219
evt.preventDefault()
183220
if (step === 'tracks') {
184221
const [trackId] = entries[num - 1]
185-
handleTrackSelect(trackId)
222+
handleTrackSelect(trackId as string)
223+
} else if (step === 'controller') {
224+
const [controllerId] = entries[num - 1]
225+
handleControllerSelect(controllerId as string)
186226
} else {
187227
const [conditionId] = entries[num - 1]
188-
toggleCondition(conditionId)
228+
toggleCondition(conditionId as string)
189229
}
190230
}
191231
}
@@ -309,6 +349,29 @@ export function Onboard(props: OnboardProps) {
309349
}}
310350
</For>
311351
</Show>
352+
353+
<Show when={currentStep() === 'controller'}>
354+
<For each={controllerEntries()}>
355+
{([controllerId, agent], index) => {
356+
const isSelected = () => index() === selectedIndex()
357+
return (
358+
<box flexDirection="column">
359+
<box flexDirection="row" gap={1}>
360+
<text fg={isSelected() ? themeCtx.theme.primary : themeCtx.theme.textMuted}>
361+
{isSelected() ? ">" : " "}
362+
</text>
363+
<text fg={isSelected() ? themeCtx.theme.primary : themeCtx.theme.textMuted}>
364+
{isSelected() ? "(*)" : "( )"}
365+
</text>
366+
<text fg={isSelected() ? themeCtx.theme.primary : themeCtx.theme.text}>
367+
{controllerId}
368+
</text>
369+
</box>
370+
</box>
371+
)
372+
}}
373+
</For>
374+
</Show>
312375
</box>
313376

314377
{/* Footer */}
@@ -328,6 +391,11 @@ export function Onboard(props: OnboardProps) {
328391
[Up/Down] Navigate [Enter] Toggle [Tab] Confirm [Esc] Cancel
329392
</text>
330393
</Show>
394+
<Show when={currentStep() === 'controller'}>
395+
<text fg={themeCtx.theme.textMuted}>
396+
[Up/Down] Navigate [Enter] Select [Esc] Cancel
397+
</text>
398+
</Show>
331399
</box>
332400
</box>
333401
</box>

src/shared/agents/config/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ export type AgentDefinition = {
1818
model_reasoning_effort?: unknown;
1919
engine?: string; // Engine to use for this agent (dynamically determined from registry)
2020
chainedPromptsPath?: ChainedPathEntry | ChainedPathEntry[]; // Path(s) to folder(s) containing chained prompt .md files
21+
role?: 'controller'; // Agent role - 'controller' agents can drive autonomous mode
2122
[key: string]: unknown;
2223
};
2324

0 commit comments

Comments
 (0)