Merge branch 'main' into cw/small-change-flow-conflict-1772826399181
# Conflicts: # README.md # apps/server/execution/orchestrator.ts # apps/server/test/unit/headquarters.test.ts # apps/server/trpc/router.ts # apps/server/trpc/routers/agent.ts # apps/server/trpc/routers/headquarters.ts # apps/web/src/components/hq/HQSections.test.tsx # apps/web/src/components/hq/types.ts # apps/web/src/layouts/AppLayout.tsx # apps/web/src/routes/hq.tsx # apps/web/tsconfig.app.tsbuildinfo # docs/dispatch-events.md # docs/server-api.md # vitest.config.ts
This commit is contained in:
@@ -245,8 +245,8 @@ Index: `(phaseId)`.
|
||||
| ProjectRepository | + junction ops: setInitiativeProjects (diff-based), findProjectsByInitiativeId |
|
||||
| AccountRepository | + findNextAvailable (round-robin), markExhausted, clearExpiredExhaustion |
|
||||
| ProposalRepository | + findByAgentIdAndStatus, updateManyByAgentId, countByAgentIdAndStatus |
|
||||
| LogChunkRepository | insertChunk, findByAgentId, deleteByAgentId, getSessionCount |
|
||||
| ConversationRepository | create, findById, findPendingForAgent, answer |
|
||||
| LogChunkRepository | insertChunk, findByAgentId, findByAgentIds (batch), deleteByAgentId, getSessionCount |
|
||||
| ConversationRepository | create, findById, findPendingForAgent, answer, countByFromAgentIds (batch), findByFromAgentId |
|
||||
| ChatSessionRepository | createSession, findActiveSession, findActiveSessionByAgentId, updateSession, createMessage, findMessagesBySessionId |
|
||||
| ReviewCommentRepository | create, findByPhaseId, resolve, unresolve, delete |
|
||||
| ErrandRepository | create, findById, findAll (filter by projectId/status), update, delete |
|
||||
|
||||
@@ -117,6 +117,15 @@ InitiativeChangesRequestedEvent { initiativeId, phaseId, taskId }
|
||||
| `agent:crashed` | Auto-retry crashed task up to `MAX_TASK_RETRIES` (3). Increments `retryCount`, resets status to `pending`, re-queues. Exceeding retries leaves task `in_progress` for manual intervention. |
|
||||
| `task:completed` | Merge task branch (if branch exists), check phase completion, dispatch next queued task |
|
||||
|
||||
### Conflict Resolution → Dispatch Flow
|
||||
|
||||
When a task branch merge produces conflicts:
|
||||
1. `mergeTaskIntoPhase()` detects conflicts from `branchManager.mergeBranch()`
|
||||
2. Calls `conflictResolutionService.handleConflict()` which creates a "Resolve conflicts" task (with dedup — skips if an identical pending/in_progress resolution task already exists)
|
||||
3. The original task is **not blocked** — it was already completed by `handleAgentStopped` before the merge attempt. The pending resolution task prevents premature phase completion.
|
||||
4. Orchestrator queues the new conflict task via `dispatchManager.queue()`
|
||||
5. `scheduleDispatch()` picks it up and assigns it to an idle agent
|
||||
|
||||
### Crash Recovery
|
||||
|
||||
When an agent crashes (`agent:crashed` event), the orchestrator automatically retries the task:
|
||||
@@ -125,7 +134,10 @@ When an agent crashes (`agent:crashed` event), the orchestrator automatically re
|
||||
3. If under limit: increments `retryCount`, resets task to `pending`, re-queues for dispatch
|
||||
4. If over limit: logs warning, task stays `in_progress` for manual intervention
|
||||
|
||||
On server restart, `recoverDispatchQueues()` also recovers stuck `in_progress` tasks whose agents are dead (status is not `running` or `waiting_for_input`). These are reset to `pending` and re-queued.
|
||||
On server restart, `recoverDispatchQueues()` also recovers:
|
||||
- Stuck `in_progress` tasks whose agents are dead (status is not `running` or `waiting_for_input`) — reset to `pending` and re-queued
|
||||
- Erroneously `blocked` tasks whose agents completed successfully (status is `idle` or `stopped`) — marked `completed` so the phase can progress. This handles the legacy case where conflict resolution incorrectly blocked already-completed tasks.
|
||||
- Fully-completed `in_progress` phases — after task recovery, if all tasks in an `in_progress` phase are completed, triggers `handlePhaseAllTasksDone` to complete/review the phase
|
||||
|
||||
Manual retry via `retryBlockedTask()` resets `retryCount` to 0, giving the task a fresh set of automatic retries.
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
| Tiptap | Rich text editor (ProseMirror-based) |
|
||||
| Lucide | Icon library |
|
||||
| Geist Sans/Mono | Typography (variable fonts in `public/fonts/`) |
|
||||
| react-window 2.x | Virtualized list rendering for large file trees in ReviewSidebar |
|
||||
|
||||
## Design System (v2)
|
||||
|
||||
@@ -43,6 +44,7 @@ Use `mapEntityStatus(rawStatus)` from `StatusDot.tsx` to convert raw entity stat
|
||||
| Route | Component | Purpose |
|
||||
|-------|-----------|---------|
|
||||
| `/` | `routes/index.tsx` | Dashboard / initiative list |
|
||||
| `/hq` | `routes/hq.tsx` | Headquarters — action items requiring user attention |
|
||||
| `/initiatives/$id` | `routes/initiatives/$initiativeId.tsx` | Initiative detail (tabbed) |
|
||||
| `/agents` | `routes/agents.tsx` | Agent list with Output / Details tab panel |
|
||||
| `/settings` | `routes/settings/index.tsx` | Settings page |
|
||||
@@ -113,15 +115,26 @@ The initiative detail page has three tabs managed via local state (not URL param
|
||||
### Review Components (`src/components/review/`)
|
||||
| Component | Purpose |
|
||||
|-----------|---------|
|
||||
| `ReviewTab` | Review tab container — orchestrates header, diff, sidebar, and preview. Phase-level review has threaded inline comments (with reply support) + Request Changes; initiative-level review has Request Changes (summary prompt) + Push Branch / Merge & Push |
|
||||
| `ReviewHeader` | Consolidated toolbar: phase selector pills, branch info, stats, preview controls, approve/reject actions |
|
||||
| `ReviewSidebar` | VSCode-style icon strip (Files/Commits views) with file list, root-only comment counts, and commit navigation |
|
||||
| `DiffViewer` | Unified diff renderer with threaded inline comments (root + reply threads) |
|
||||
| `ReviewTab` | Review tab container — orchestrates header, diff, sidebar, and preview. Phase-level review has threaded inline comments (with reply support) + Request Changes; initiative-level review has Request Changes (summary prompt) + Push Branch / Merge & Push. Phase diff uses metadata-only `FileDiff[]` from `getPhaseReviewDiff`; commit diff parses `rawDiff` via `parseUnifiedDiff` → `FileDiffDetail[]`. Passes `commitMode`, `phaseId`, `expandAll` to DiffViewer |
|
||||
| `ReviewHeader` | Consolidated toolbar: phase selector pills, branch info, stats (uses `totalAdditions`/`totalDeletions` props when available, falls back to summing files), preview controls, Expand all button, approve/reject actions |
|
||||
| `ReviewSidebar` | VSCode-style icon strip (Files/Commits views) with file list, root-only comment counts, and commit navigation. FilesView uses react-window 2.x `List` for virtualized rendering when the row count exceeds 50 (dir-headers + file rows). Scroll position is preserved across Files ↔ Commits tab switches. Directories are collapsible. Clicking a file scrolls the virtual list to that row. |
|
||||
| `DiffViewer` | Unified diff renderer with threaded inline comments (root + reply threads). Accepts `FileDiff[] | FileDiffDetail[]`, `phaseId`, `commitMode`, `expandAll` props |
|
||||
| `CommentThread` | Renders root comment with resolve/reopen + nested reply threads (agent replies styled with primary border). Inline reply form |
|
||||
| `ConflictResolutionPanel` | Merge conflict detection + agent resolution in initiative review. Shows conflict files, spawns conflict agent, inline questions, re-check on completion |
|
||||
| `PreviewPanel` | Docker preview status: building/running/failed with start/stop (legacy, now integrated into ReviewHeader) |
|
||||
| `ProposalCard` | Individual proposal display |
|
||||
|
||||
#### Syntax Highlighting (`use-syntax-highlight.ts` + `highlight-worker.ts`)
|
||||
|
||||
`useHighlightedFile(filePath, allLines)` returns `LineTokenMap | null`. Tokenisation runs off the main thread:
|
||||
|
||||
- **Worker path** (default): a module-level pool of 2 ES module Web Workers (`highlight-worker.ts`) each import shiki's `codeToTokens` dynamically. Requests are round-robined by `requestCount % 2`. Responses are correlated by UUID. Late responses after unmount are silently discarded via the `pending` Map.
|
||||
- **Fallback path** (CSP / browser-compat): if `Worker` construction throws, `createHighlighter` is used on the main thread but processes 200 lines per chunk, yielding between chunks via `scheduler.yield()` or `setTimeout(0)`.
|
||||
|
||||
Callers receive `null` while highlighting is in progress and a populated `Map<lineNumber, ThemedToken[]>` once it resolves. `LineWithComments` already renders plain text when `null`, so no caller changes are needed.
|
||||
|
||||
Vite must be configured with `worker.format: 'es'` (added to `vite.config.ts`) for the worker chunk to bundle correctly alongside code-split app chunks.
|
||||
|
||||
### UI Primitives (`src/components/ui/`)
|
||||
shadcn/ui components: badge (6 status variants + xs size), button, card, dialog, dropdown-menu, input, label, select, sonner, textarea, tooltip.
|
||||
|
||||
|
||||
@@ -40,6 +40,8 @@ Worktrees stored in `.cw-worktrees/` subdirectory of the repo. Each agent gets a
|
||||
| `ensureBranch(repoPath, branch, baseBranch)` | Create branch from base if it doesn't exist (idempotent) |
|
||||
| `mergeBranch(repoPath, source, target)` | Merge via ephemeral worktree, returns conflict info |
|
||||
| `diffBranches(repoPath, base, head)` | Three-dot diff between branches |
|
||||
| `diffBranchesStat(repoPath, base, head)` | Per-file metadata (path, status, additions, deletions) — no hunk content. Binary files included with `status: 'binary'` and counts of 0. Returns `FileStatEntry[]`. |
|
||||
| `diffFileSingle(repoPath, base, head, filePath)` | Raw unified diff for a single file (three-dot diff). `filePath` must be URL-decoded. Returns empty string for binary files. |
|
||||
| `deleteBranch(repoPath, branch)` | Delete local branch (no-op if missing) |
|
||||
| `branchExists(repoPath, branch)` | Check local branches |
|
||||
| `remoteBranchExists(repoPath, branch)` | Check remote tracking branches (`origin/<branch>`) |
|
||||
|
||||
@@ -69,6 +69,10 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
||||
| getActiveRefineAgent | query | Active refine agent for initiative |
|
||||
| getActiveConflictAgent | query | Active conflict resolution agent for initiative (name starts with `conflict-`) |
|
||||
| listWaitingAgents | query | Agents waiting for input |
|
||||
| listForRadar | query | Radar page: per-agent metrics (questionsCount, messagesCount, subagentsCount, compactionsCount) with time/status/mode/initiative filters |
|
||||
| getCompactionEvents | query | Compaction events for one agent: `{agentId}` → `{timestamp, sessionNumber}[]` (cap 200) |
|
||||
| getSubagentSpawns | query | Subagent spawn events for one agent: `{agentId}` → `{timestamp, description, promptPreview, fullPrompt}[]` (cap 200) |
|
||||
| getQuestionsAsked | query | AskUserQuestion tool calls for one agent: `{agentId}` → `{timestamp, questions[]}[]` (cap 200) |
|
||||
| onAgentOutput | subscription | Live raw JSONL output stream via EventBus |
|
||||
|
||||
### Tasks
|
||||
@@ -118,7 +122,8 @@ Each procedure uses `require*Repository(ctx)` helpers that throw `TRPCError(INTE
|
||||
| listInitiativePhaseDependencies | query | All dependency edges |
|
||||
| getPhaseDependencies | query | What this phase depends on |
|
||||
| getPhaseDependents | query | What depends on this phase |
|
||||
| getPhaseReviewDiff | query | Full branch diff for pending_review phase |
|
||||
| getPhaseReviewDiff | query | File-level metadata for pending_review phase: `{phaseName, sourceBranch, targetBranch, files: FileStatEntry[], totalAdditions, totalDeletions}` — no hunk content. Results are cached in-memory by `phaseId:headHash` (TTL: `REVIEW_DIFF_CACHE_TTL_MS`, default 5 min). Cache is invalidated when a task merges into the phase branch. |
|
||||
| getFileDiff | query | Per-file unified diff on demand: `{phaseId, filePath, projectId?}` → `{binary: boolean, rawDiff: string}`; `filePath` must be URL-encoded; binary files return `{binary: true, rawDiff: ''}` |
|
||||
| getPhaseReviewCommits | query | List commits between initiative and phase branch |
|
||||
| getCommitDiff | query | Diff for a single commit (by hash) in a phase |
|
||||
| approvePhaseReview | mutation | Approve and merge phase branch |
|
||||
@@ -254,6 +259,7 @@ Inter-agent communication for parallel agents.
|
||||
| `getPendingConversations` | query | Poll for incoming questions: `{agentId}` → Conversation[] |
|
||||
| `getConversation` | query | Get conversation by ID: `{id}` → Conversation |
|
||||
| `answerConversation` | mutation | Answer a conversation: `{id, answer}` → Conversation |
|
||||
| `getByFromAgent` | query | Radar drilldown: all conversations sent by agent: `{agentId}` → `{id, timestamp, toAgentName, toAgentId, question, answer, status, taskId, phaseId}[]` (cap 200) |
|
||||
|
||||
Target resolution: `toAgentId` → direct; `taskId` → find running agent by task; `phaseId` → find running agent by any task in phase.
|
||||
|
||||
@@ -273,33 +279,13 @@ Persistent chat loop for iterative phase/task refinement via agent.
|
||||
|
||||
Context dependency: `requireChatSessionRepository(ctx)`, `requireAgentManager(ctx)`, `requireInitiativeRepository(ctx)`, `requireTaskRepository(ctx)`.
|
||||
|
||||
## Errand Procedures
|
||||
|
||||
Small isolated changes that spawn a dedicated agent in a git worktree. Errands are scoped to a project and use a branch named `cw/errand/<slug>-<8-char-id>`.
|
||||
|
||||
| Procedure | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `errand.create` | mutation | Create errand: `{description, projectId, baseBranch?}` → `{id, branch, agentId}`. Creates branch, worktree, DB record, spawns agent. |
|
||||
| `errand.list` | query | List errands: `{projectId?, status?}` → ErrandWithAlias[] (ordered newest-first) |
|
||||
| `errand.get` | query | Get errand by ID: `{id}` → ErrandWithAlias with `projectPath: string \| null` (computed from workspaceRoot) |
|
||||
| `errand.diff` | query | Get branch diff: `{id}` → `{diff: string}` |
|
||||
| `errand.complete` | mutation | Mark active errand ready for review (stops agent): `{id}` → Errand |
|
||||
| `errand.merge` | mutation | Merge errand branch: `{id, target?}` → `{status: 'merged'}` or throws conflict |
|
||||
| `errand.delete` | mutation | Delete errand and clean up worktree/branch: `{id}` → `{success: true}` |
|
||||
| `errand.sendMessage` | mutation | Send message to running errand agent: `{id, message}` → `{success: true}` |
|
||||
| `errand.abandon` | mutation | Abandon errand (stop agent, clean up, set status): `{id}` → Errand |
|
||||
|
||||
**Errand statuses**: `active` → `pending_review` (via complete) → `merged` (via merge) or `conflict` (merge failed) → retry merge. `abandoned` is terminal. Only `pending_review` and `conflict` errands can be merged.
|
||||
|
||||
Context dependencies: `requireErrandRepository(ctx)`, `requireProjectRepository(ctx)`, `requireAgentManager(ctx)`, `requireBranchManager(ctx)`, `ctx.workspaceRoot` (for `ensureProjectClone`). `SimpleGitWorktreeManager` is created on-the-fly per project clone path.
|
||||
|
||||
## Headquarters Procedures
|
||||
|
||||
Composite dashboard query aggregating all action items that require user intervention.
|
||||
|
||||
| Procedure | Type | Description |
|
||||
|-----------|------|-------------|
|
||||
| `getHeadquartersDashboard` | query | Returns 5 typed arrays of action items (no input required) |
|
||||
| `getHeadquartersDashboard` | query | Returns 6 typed arrays of action items (no input required) |
|
||||
|
||||
### Return Shape
|
||||
|
||||
@@ -309,6 +295,7 @@ Composite dashboard query aggregating all action items that require user interve
|
||||
pendingReviewInitiatives: Array<{ initiativeId, initiativeName, since }>;
|
||||
pendingReviewPhases: Array<{ initiativeId, initiativeName, phaseId, phaseName, since }>;
|
||||
planningInitiatives: Array<{ initiativeId, initiativeName, pendingPhaseCount, since }>;
|
||||
resolvingConflicts: Array<{ initiativeId, initiativeName, agentId, agentName, agentStatus, since }>;
|
||||
blockedPhases: Array<{ initiativeId, initiativeName, phaseId, phaseName, lastMessage, since }>;
|
||||
}
|
||||
```
|
||||
|
||||
Reference in New Issue
Block a user