diff --git a/README.md b/README.md
index 62259df8..96a81cb5 100644
--- a/README.md
+++ b/README.md
@@ -26,6 +26,17 @@ https://github.com/user-attachments/assets/9cae73cd-7f42-46e5-a8fb-ad6d41737ff8
+## Table of contents
+
+- [What is this](#what-is-this)
+- [Installation](#installation)
+- [Quick start](#quick-start)
+- [FAQ](#faq)
+- [Comparison](#comparison)
+- [Roadmap](#roadmap)
+- [Development](#development)
+- [Contributing](#contributing)
+
## What is this
A new approach to task management with AI agent teams.
@@ -78,24 +89,6 @@ A new approach to task management with AI agent teams.
-## Comparison
-
-How we compare to other multi-agent orchestration tools:
-
-| Feature | Claude Agent Teams UI | Vibe Kanban | Dorothy | Cursor | Claude Code CLI |
-|---|---|---|---|---|---|
-| **Agent-to-agent messaging** | ✅ Native real-time mailbox | ❌ Agents are independent | ⚠️ Only via Super Agent | ❌ | ✅ Built-in (no UI) |
-| **Cross-team communication** | ✅ | ❌ | ❌ | ❌ | ✅ (no UI) |
-| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ Auto-assignment | ❌ | ❌ |
-| **Per-task code review** | ✅ Accept / reject / comment | ⚠️ PR-level only | ❌ | ✅ BugBot on PRs | ❌ |
-| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
-| **Review workflow** | ✅ Agents review each other | ❌ | ❌ | ❌ | ✅ (no UI) |
-| **Session analysis** | ✅ 6-category token tracking | ❌ | ⚠️ Basic stats | ❌ | ❌ |
-| **Execution log viewer** | ✅ Tool calls, reasoning, timeline | ❌ | ❌ | ✅ | ❌ |
-| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ✅ |
-| **Multi-agent backend** | 🗓️ Planned | ✅ 6+ agents | ✅ 3 agents | ✅ Own models | — |
-| **Git worktree isolation** | ✅ Optional | ✅ Built-in | ❌ | ✅ | ✅ |
-| **Price** | **Free** | Free / $30 user/mo | Free | $0–$200/mo | Claude subscription |
## Installation
@@ -206,6 +199,33 @@ Yes. Run multiple teams in one project or across different projects, even simult
---
+## Comparison
+
+How we compare to other multi-agent orchestration tools:
+
+| Feature | Claude Agent Teams UI | Vibe Kanban | Aperant | Cursor | Claude Code CLI |
+|---|---|---|---|---|---|
+| **Agent-to-agent messaging** | ✅ Native real-time mailbox | ❌ Agents are independent | ❌ Fixed pipeline | ❌ | ✅⚠️ Built-in (no UI) |
+| **Cross-team communication** | ✅ | ❌ | ❌ | ❌ | ✅⚠️ (no UI) |
+| **Kanban board** | ✅ 5 columns, real-time | ✅ | ✅ 6 columns (pipeline) | ❌ | ❌ |
+| **Per-task code review** | ✅ Accept / reject / comment | ⚠️ PR-level only | ⚠️ File-level only | ✅ BugBot on PRs | ❌ |
+| **Hunk-level review** | ✅ Accept / reject individual hunks | ❌ | ❌ | ✅ | ❌ |
+| **Review workflow** | ✅ Agents review each other | ❌ | ⚠️ Auto QA pipeline | ❌ | ✅⚠️ (no UI) |
+| **Session analysis** | ✅ 6-category token tracking | ❌ | ⚠️ Execution logs | ❌ | ❌ |
+| **Execution log viewer** | ✅ Tool calls, reasoning, timeline | ❌ | ✅ Phase-based logs | ✅ | ❌ |
+| **Zero setup** | ✅ | ❌ Config required | ❌ Config required | ✅ | ✅ |
+| **Live processes** | ✅ View, stop, open URLs in browser | ❌ | ✅ 12 agent terminals | ✅ | ❌ |
+| **Flexible autonomy** | ✅ Granular settings, per-action approval, notifications | ❌ | ⚠️ Plan approval only | ✅ | ✅ |
+| **Full autonomy** | ✅ Agents create, assign, review tasks end-to-end | ❌ Human manages tasks | ❌ Fixed pipeline | ⚠️ Isolated tasks only | ✅⚠️ (no UI) |
+| **Task dependencies (blocked by)** | ✅ Guaranteed ordering | ❌ | ⚠️ Within plan only | ❌ | ❌ |
+| **Linked tasks** | ✅ Cross-references in messages | ⚠️ Subtasks only | ❌ | ❌ | ❌ |
+| **Task attachments** | ✅ Auto-attach, agents read & attach files | ❌ | ✅ Images + files | ❌ | ❌ |
+| **Multi-agent backend** | 🗓️ In development | ✅ 6+ agents | ✅ 11 providers | ✅ Own models | — |
+| **Git worktree isolation** | ✅ Optional | ⚠️ Mandatory | ⚠️ Mandatory | ✅ | ✅ |
+| **Price** | **Free** | Free / $30 user/mo | Free | $0–$200/mo | Claude subscription |
+
+---
+
## Development
diff --git a/src/main/services/team/TeamProvisioningService.ts b/src/main/services/team/TeamProvisioningService.ts
index 23df85b8..de0203f9 100644
--- a/src/main/services/team/TeamProvisioningService.ts
+++ b/src/main/services/team/TeamProvisioningService.ts
@@ -2657,9 +2657,11 @@ export class TeamProvisioningService {
// Restart filesystem monitor for createTeam (launch skips it)
if (!run.isLaunch) {
+ updateProgress(run, 'configuring', 'Waiting for team configuration...');
+ run.onProgress(run.progress);
this.startFilesystemMonitor(run, run.request);
} else {
- updateProgress(run, 'monitoring', 'CLI running — reconnecting with teammates');
+ updateProgress(run, 'configuring', 'CLI running — reconnecting with teammates');
run.onProgress(run.progress);
}
@@ -3018,6 +3020,8 @@ export class TeamProvisioningService {
// of relying on stdout (which only arrives at the end in text mode).
// When config + members + tasks are all present, kill the process early
// rather than waiting for it to deadlock on system-reminder shutdown.
+ updateProgress(run, 'configuring', 'Waiting for team configuration...');
+ run.onProgress(run.progress);
this.startFilesystemMonitor(run, request);
run.timeoutHandle = setTimeout(() => {
@@ -3442,7 +3446,7 @@ export class TeamProvisioningService {
// For launch, skip the filesystem monitor — files (config, inboxes, tasks)
// already exist from the previous run and would trigger immediate false
// completion on the first poll. Rely on stream-json result.success instead.
- updateProgress(run, 'monitoring', 'CLI running — reconnecting with teammates');
+ updateProgress(run, 'configuring', 'CLI running — reconnecting with teammates');
run.onProgress(run.progress);
run.timeoutHandle = setTimeout(() => {
@@ -3502,7 +3506,11 @@ export class TeamProvisioningService {
if (!run) {
throw new Error('Unknown runId');
}
- if (!['spawning', 'monitoring', 'verifying'].includes(run.progress.state)) {
+ if (
+ !['spawning', 'configuring', 'assembling', 'finalizing', 'verifying'].includes(
+ run.progress.state
+ )
+ ) {
throw new Error('Provisioning cannot be cancelled in current state');
}
@@ -6325,7 +6333,7 @@ export class TeamProvisioningService {
run.fsPhase = 'waiting_members';
const progress = updateProgress(
run,
- 'monitoring',
+ 'assembling',
'Team config created, waiting for members',
{ configReady: true }
);
@@ -6336,11 +6344,7 @@ export class TeamProvisioningService {
if (run.fsPhase === 'waiting_members') {
if (request.members.length === 0) {
run.fsPhase = 'waiting_tasks';
- const progress = updateProgress(
- run,
- 'monitoring',
- 'Solo team, skipping member inbox wait'
- );
+ const progress = updateProgress(run, 'finalizing', 'Solo team, preparing workspace');
run.onProgress(progress);
} else {
const teamDir = (await resolveTeamDir()) ?? configuredTeamDir;
@@ -6350,14 +6354,14 @@ export class TeamProvisioningService {
run.fsPhase = 'waiting_tasks';
const progress = updateProgress(
run,
- 'monitoring',
- `All ${inboxCount} member inboxes created, waiting for tasks`
+ 'finalizing',
+ `All ${inboxCount} member inboxes created, preparing workspace`
);
run.onProgress(progress);
} else if (inboxCount > 0) {
const progress = updateProgress(
run,
- 'monitoring',
+ 'assembling',
`${inboxCount}/${request.members.length} member inboxes created`
);
run.onProgress(progress);
diff --git a/src/renderer/components/team/ProvisioningProgressBlock.tsx b/src/renderer/components/team/ProvisioningProgressBlock.tsx
index 329a81db..621b1c43 100644
--- a/src/renderer/components/team/ProvisioningProgressBlock.tsx
+++ b/src/renderer/components/team/ProvisioningProgressBlock.tsx
@@ -7,15 +7,16 @@ import { ChevronDown, ChevronRight, Loader2 } from 'lucide-react';
import { MarkdownViewer } from '../chat/viewers/MarkdownViewer';
import { CliLogsRichView } from './CliLogsRichView';
-import { STEP_LABELS, STEP_ORDER } from './provisioningSteps';
+import { DISPLAY_STEPS } from './provisioningSteps';
import { StepProgressBar } from './StepProgressBar';
import type { StepProgressBarStep } from './StepProgressBar';
-/** Pre-built step definitions for the provisioning stepper (excludes 'ready') */
-const PROVISIONING_STEPS: StepProgressBarStep[] = STEP_ORDER.filter((s) => s !== 'ready').map(
- (s) => ({ key: s, label: STEP_LABELS[s] })
-);
+/** Pre-built step definitions for the provisioning stepper. */
+const PROVISIONING_STEPS: StepProgressBarStep[] = DISPLAY_STEPS.map((s) => ({
+ key: s.key,
+ label: s.label,
+}));
export interface ProvisioningProgressBlockProps {
/** Title above the steps, e.g. "Launching team" */
@@ -26,7 +27,7 @@ export interface ProvisioningProgressBlockProps {
tone?: 'default' | 'error';
/** Whether Live output is expanded by default */
defaultLiveOutputOpen?: boolean;
- /** Index of the current step in STEP_ORDER (0-based), or -1 if unknown */
+ /** Display step index (0-3 for active steps, 4 for ready/all done, -1 for terminal) */
currentStepIndex: number;
/** Show spinner next to title */
loading?: boolean;
@@ -155,15 +156,23 @@ export const ProvisioningProgressBlock = ({
// Open CLI logs while loading, collapse when done (unless error).
const prevLoadingRef = useRef(loading);
+ const hadLogsRef = useRef(Boolean(cliLogsTail));
useEffect(() => {
- if (!isError && cliLogsTail) {
- if (loading && !prevLoadingRef.current) {
- // Started loading → open
+ if (!isError) {
+ const hasLogs = Boolean(cliLogsTail);
+
+ if (loading && hasLogs && !hadLogsRef.current) {
+ // Logs just appeared while loading → open
+ setLogsOpen(true);
+ } else if (loading && !prevLoadingRef.current && hasLogs) {
+ // Started loading with logs already present → open
setLogsOpen(true);
} else if (!loading && prevLoadingRef.current) {
// Finished loading → collapse
setLogsOpen(false);
}
+
+ hadLogsRef.current = hasLogs;
}
prevLoadingRef.current = loading;
}, [loading, cliLogsTail, isError]);
diff --git a/src/renderer/components/team/TeamListView.tsx b/src/renderer/components/team/TeamListView.tsx
index 71b52441..e096b325 100644
--- a/src/renderer/components/team/TeamListView.tsx
+++ b/src/renderer/components/team/TeamListView.tsx
@@ -144,7 +144,9 @@ function resolveTeamStatus(
}
if (
currentProgress &&
- ['validating', 'spawning', 'monitoring', 'verifying'].includes(currentProgress.state)
+ ['validating', 'spawning', 'configuring', 'assembling', 'finalizing', 'verifying'].includes(
+ currentProgress.state
+ )
) {
return 'provisioning';
}
diff --git a/src/renderer/components/team/TeamProvisioningBanner.tsx b/src/renderer/components/team/TeamProvisioningBanner.tsx
index 266e0d30..5f4b45c3 100644
--- a/src/renderer/components/team/TeamProvisioningBanner.tsx
+++ b/src/renderer/components/team/TeamProvisioningBanner.tsx
@@ -7,9 +7,8 @@ import { CheckCircle2, X } from 'lucide-react';
import { useShallow } from 'zustand/react/shallow';
import { ProvisioningProgressBlock } from './ProvisioningProgressBlock';
-import { STEP_ORDER } from './provisioningSteps';
+import { getDisplayStepIndex } from './provisioningSteps';
-import type { ProvisioningStep } from './provisioningSteps';
interface TeamProvisioningBannerProps {
teamName: string;
}
@@ -51,15 +50,19 @@ export const TeamProvisioningBanner = ({
const isActive =
progress.state === 'validating' ||
progress.state === 'spawning' ||
- progress.state === 'monitoring' ||
+ progress.state === 'configuring' ||
+ progress.state === 'assembling' ||
+ progress.state === 'finalizing' ||
progress.state === 'verifying';
const canCancel =
progress.state === 'spawning' ||
- progress.state === 'monitoring' ||
+ progress.state === 'configuring' ||
+ progress.state === 'assembling' ||
+ progress.state === 'finalizing' ||
progress.state === 'verifying';
- const progressStepIndex = STEP_ORDER.indexOf(progress.state as ProvisioningStep);
+ const progressStepIndex = getDisplayStepIndex(progress.state);
if (isFailed) {
return (
diff --git a/src/renderer/components/team/provisioningSteps.ts b/src/renderer/components/team/provisioningSteps.ts
index 6446431e..1127cd38 100644
--- a/src/renderer/components/team/provisioningSteps.ts
+++ b/src/renderer/components/team/provisioningSteps.ts
@@ -1,9 +1,32 @@
-export const STEP_ORDER = ['validating', 'spawning', 'monitoring', 'verifying', 'ready'] as const;
-export type ProvisioningStep = (typeof STEP_ORDER)[number];
-export const STEP_LABELS: Record = {
- validating: 'Validate',
- spawning: 'Start CLI',
- monitoring: 'Provisioning',
- verifying: 'Verify',
- ready: 'Ready',
-};
+import type { TeamProvisioningState } from '@shared/types/team';
+
+/** Display steps for the provisioning stepper (0-indexed). */
+export const DISPLAY_STEPS = [
+ { key: 'starting', label: 'Starting' },
+ { key: 'configuring', label: 'Team setup' },
+ { key: 'assembling', label: 'Members joining' },
+ { key: 'finalizing', label: 'Finalizing' },
+] as const;
+
+/**
+ * Maps a backend provisioning state to a 0-based display step index.
+ * Returns DISPLAY_STEPS.length for 'ready' (all steps complete), -1 for terminal/unknown.
+ */
+export function getDisplayStepIndex(state: Exclude): number {
+ switch (state) {
+ case 'validating':
+ case 'spawning':
+ return 0;
+ case 'configuring':
+ return 1;
+ case 'assembling':
+ return 2;
+ case 'finalizing':
+ case 'verifying':
+ return 3;
+ case 'ready':
+ return DISPLAY_STEPS.length;
+ default:
+ return -1;
+ }
+}
diff --git a/src/renderer/store/slices/teamSlice.ts b/src/renderer/store/slices/teamSlice.ts
index 5bf77a87..d33f2a4e 100644
--- a/src/renderer/store/slices/teamSlice.ts
+++ b/src/renderer/store/slices/teamSlice.ts
@@ -26,7 +26,14 @@ function sleep(ms: number): Promise {
return new Promise((resolve) => setTimeout(resolve, ms));
}
-const ACTIVE_PROVISIONING_STATES = new Set(['validating', 'spawning', 'monitoring', 'verifying']);
+const ACTIVE_PROVISIONING_STATES = new Set([
+ 'validating',
+ 'spawning',
+ 'configuring',
+ 'assembling',
+ 'finalizing',
+ 'verifying',
+]);
const TERMINAL_PROVISIONING_STATES = new Set(['ready', 'failed', 'disconnected', 'cancelled']);
function isPendingProvisioningRunId(runId: string): boolean {
@@ -1689,7 +1696,7 @@ export const createTeamSlice: StateCreator = (set,
if (pendingRun) {
delete nextRuns[pendingRunId];
// Only use pending data as fallback if real progress events haven't arrived yet.
- // This prevents overwriting real progress (e.g. 'monitoring') with stale pending data ('spawning')
+ // This prevents overwriting real progress (e.g. 'assembling') with stale pending data ('spawning')
// when the invoke response arrives before IPC progress events.
if (!realProgressAlreadyExists) {
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
@@ -1826,7 +1833,7 @@ export const createTeamSlice: StateCreator = (set,
if (pendingRun) {
delete nextRuns[pendingRunId];
// Only use pending data as fallback if real progress events haven't arrived yet.
- // This prevents overwriting real progress (e.g. 'monitoring') with stale pending data ('spawning')
+ // This prevents overwriting real progress (e.g. 'assembling') with stale pending data ('spawning')
// when the invoke response arrives before IPC progress events.
if (!realProgressAlreadyExists) {
nextRuns[response.runId] = { ...pendingRun, runId: response.runId };
diff --git a/src/shared/types/team.ts b/src/shared/types/team.ts
index d36c5460..01218e08 100644
--- a/src/shared/types/team.ts
+++ b/src/shared/types/team.ts
@@ -544,7 +544,9 @@ export type TeamProvisioningState =
| 'idle'
| 'validating'
| 'spawning'
- | 'monitoring'
+ | 'configuring'
+ | 'assembling'
+ | 'finalizing'
| 'verifying'
| 'ready'
| 'disconnected'
diff --git a/test/main/services/team/TeamProvisioningServicePrepare.test.ts b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
index b521d28f..6b30db2d 100644
--- a/test/main/services/team/TeamProvisioningServicePrepare.test.ts
+++ b/test/main/services/team/TeamProvisioningServicePrepare.test.ts
@@ -125,8 +125,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
progress: {
runId: 'run-1',
teamName: 'team-alpha',
- state: 'monitoring',
- message: 'Monitoring',
+ state: 'assembling',
+ message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},
@@ -183,8 +183,8 @@ describe('TeamProvisioningService prepare/auth behavior', () => {
progress: {
runId: 'run-2',
teamName: 'team-alpha',
- state: 'monitoring',
- message: 'Monitoring',
+ state: 'assembling',
+ message: 'Assembling',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
},
diff --git a/test/renderer/store/teamSlice.test.ts b/test/renderer/store/teamSlice.test.ts
index 72fb7157..98a48107 100644
--- a/test/renderer/store/teamSlice.test.ts
+++ b/test/renderer/store/teamSlice.test.ts
@@ -442,7 +442,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'run-real',
teamName: 'my-team',
- state: 'monitoring',
+ state: 'assembling',
message: 'Real run',
startedAt: '2026-03-12T10:00:01.000Z',
updatedAt: '2026-03-12T10:00:01.000Z',
@@ -454,7 +454,7 @@ describe('teamSlice actions', () => {
expect(store.getState().provisioningRuns['run-real']).toEqual(
expect.objectContaining({
runId: 'run-real',
- state: 'monitoring',
+ state: 'assembling',
})
);
});
@@ -513,7 +513,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'pending:my-team:1',
teamName: 'my-team',
- state: 'monitoring',
+ state: 'assembling',
message: 'Late zombie progress',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:02.000Z',
@@ -576,7 +576,7 @@ describe('teamSlice actions', () => {
'run-current': {
runId: 'run-current',
teamName: 'my-team',
- state: 'monitoring',
+ state: 'assembling',
message: 'Current run',
startedAt: '2026-03-12T10:00:00.000Z',
updatedAt: '2026-03-12T10:00:00.000Z',
@@ -615,7 +615,7 @@ describe('teamSlice actions', () => {
store.getState().onProvisioningProgress({
runId: 'run-current',
teamName: 'my-team',
- state: 'monitoring',
+ state: 'assembling',
message: 'Current run',
startedAt,
updatedAt: startedAt,