Skip to content
Merged
26 changes: 25 additions & 1 deletion .github/skills/coc-knowledge/references/dashboard-spa.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,21 @@ When `features.commitChatLens` is enabled from Admin -> Configure -> Features, r

The Notes view inherits the same `features.commitChatLens` source of truth for its AI chat surface. `NotesView` uses `useReviewChatPresentation()` with a workspace-scoped `notes` target, preserving the legacy workspace-scoped notes chat open key while Lens is disabled and using the shared target-scoped Lens open/pin/minimize keys when Lens is enabled. The notes area shows no separate Lens indicator; no notes-specific Lens setting is stored or exposed. Note-producing SPA flows that originate from notes/chat UI (notes chat edits, AI note creation, and bulk chat summaries) attach `context.lensChat = { inherited: true, source: 'features.commitChatLens' }` only while the shared Lens flag is enabled, so the process metadata records inherited Lens routing without adding persistent notes-specific state.

Chat-list hierarchy grouping is consolidated behind a shared engine:
`features/chat/task-group-grouping.ts` owns the generic matching/aggregation
logic (the `payload.context.taskGroup` tag reader, activity/end timestamp
chains, seeded grouping used by For Each and Map Reduce, shared helpers used
by Ralph), `features/chat/task-group-descriptors.ts` registers per-type
presentation/behavior descriptors (label, badge, accent, pin type,
`matchesTask`, `groupable` — Dreams is `groupable: false` so its internals
stay ungrouped), and `features/chat/TaskGroupRunRow.tsx` is the shared
parent-row chrome that `ForEachRunRow`/`MapReduceRunRow` configure as thin
wrappers. The per-feature grouping modules (`for-each-run-grouping.ts`,
`map-reduce-run-grouping.ts`, `ralph-session-grouping.ts`) are adapters over
the engine that keep their legacy matching (feature contexts,
`generationProcessId`) in addition to the generic tag, so historical chats
group without data migration.

`features/chat/ChatListPane.tsx` keeps grouped chat-history expansion state
local to the mounted view. Ralph session groups, For Each run groups, Map
Reduce run groups, and plan-file/history groups render collapsed by default on
Expand Down Expand Up @@ -122,13 +137,22 @@ leaf rows. Remote/Synced trees keep the type avatar, title, remote mirror badge,
and container rollups, but omit local work-item numbers and leaf status chips so
remote identifiers remain the primary row metadata. Compact GitHub mirror badges
render the issue number only; full detail-page badges keep the provider label and
link title.
link title. `work-item-added`, `work-item-updated`, and `work-item-removed`
WebSocket events update `WorkItemContext` for the matching workspace and advance
a workspace-scoped realtime revision used by `WorkItemHierarchyTree` to refetch
its tree data. The hierarchy toolbar exposes a Refresh control that calls the
same tree fetch path and is disabled while the tree request is in flight.

`workItems.workflow.enabled` is the disabled-by-default durable workflow gate for
turning local Work Items and Goals into the command-center planning/execution
surface. The SPA receives it as `workItemsWorkflowEnabled` from bootstrap config
and `GET /api/config/runtime`; use `isWorkItemsWorkflowEnabled()` for UI gates so
legacy Work Items and Chat behavior remains unchanged while the flag is off.
Work Item detail renders the editable title in the top header row and keeps
type, status, mirror, plan version, priority, updated time, parent, tags,
auto-execute, source, and primary actions in the compact properties row directly
below it; the scrollable body starts with description/plan content rather than a
separate metadata card.
When the flag is on, the local create dialog exposes a Work Item vs Goal type
selector for title-first shell creation even when hierarchy mode is off; existing
bug and hierarchy-type creation paths keep their prior behavior. Saved local-only
Expand Down
8 changes: 7 additions & 1 deletion .github/skills/coc-knowledge/references/process-store.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ Location: `packages/forge/src/` (`process-store.ts`, `sqlite-process-store.ts`,

## SqliteProcessStore

Default backend. Single `processes.db` file at `~/.coc/processes.db`. Schema version 20.
Default backend. Single `processes.db` file at `~/.coc/processes.db`. Schema version 22.

### Tables

Expand All @@ -20,6 +20,8 @@ Default backend. Single `processes.db` file at `~/.coc/processes.db`. Schema ver
| `commit_chat_bindings` | commitHash → taskId mappings |
| `pull_request_chat_bindings` | prId → taskId mappings (one persistent chat per PR per workspace) |
| `work_item_chat_bindings` | workItemId → taskId mappings (one persistent chat per Work Item per workspace) |
| `task_groups` | Generic parent/child task-group registry: one row per hierarchical run/session (type, title, normalized status, hidden flag, origin process, extra JSON) |
| `task_group_members` | Child links per task group: role ('generation'/'item'/'reduce'/'iteration'/'final-check'/'analyzer'/'critic'), task/process IDs, itemKey, memberIndex |

Commit, Pull Request, and Work Item binding routes also expose a workspace-scoped fresh-chat operation that archives the currently bound process and deletes only that target's binding. It does not fork the process, copy conversation turns, or create a new process record; the next lens send uses the normal target-specific creation flow to bind a fresh chat.

Expand Down Expand Up @@ -48,6 +50,10 @@ unarchiveProcesses(ids)
getPinnedProcesses()
```

### Task Group Registry

`SqliteTaskGroupStore` (forge) owns the `task_groups`/`task_group_members` tables over the shared database handle. The CoC server wraps it in `TaskGroupService` (`packages/coc/src/server/task-groups/`): feature stores fire change hooks (`onRunChanged` on the For Each/Map Reduce/Dream stores, a dataDir-keyed module listener on `RalphSessionStore`) that project run/session records into the registry via `feature-sync.ts`. Group statuses are normalized to `draft | running | completed | failed | cancelled`; feature states like `reducing`, `approved`, or `grilling` ride in `extra.detailStatus`. Child tasks additionally carry a generic `payload.context.taskGroup = { groupId, groupType, role, itemKey?, workspaceId }` tag mirrored into `AIProcess.metadata.taskGroup` and forwarded on history items. Dream groups are `hidden` (linkage-only). `backfillTaskGroups` idempotently projects pre-framework runs/sessions on server start. Registry writes are best-effort — failures log and never break feature orchestration. With the legacy file process-store backend the registry is in-memory only.

## FileProcessStore (Legacy)

Per-repo directory layout under `~/.coc/repos/<workspaceId>/processes/`. Used only when `store.backend: file` in config. 500-process cap.
Expand Down
12 changes: 12 additions & 0 deletions .github/skills/coc-knowledge/references/ralph.md
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,18 @@ The Ralph executor is the only writer. It must:
`currentIteration`, append to `iterations[]`, and update `phase` for
terminal signals.

After every successful `session.json` write (`initSession` and
`updateSessionRecord`), the store notifies a module-level, dataDir-keyed
session-change listener (`registerRalphSessionChangeListener`). The server
registers one listener at startup that projects the record into the generic
task-group registry (`syncRalphSessionToTaskGroup`): groupId = sessionId,
type `ralph`, children = iterations (role `iteration`) and final checks (role
`final-check`). Iteration and final-check queue tasks also carry the generic
`payload.context.taskGroup` tag alongside `context.ralph`.
`listSessionIds(workspaceId)` enumerates persisted session directories (used
by the registry backfill). Listener errors are swallowed — registry sync never
breaks session persistence.

Readers, including REST handlers and the SPA `useRalphSessionView` hook, treat
`session.json` and `progress.md` as source of truth and never mutate them. The
session read route also returns raw text for every direct file in the session
Expand Down
11 changes: 10 additions & 1 deletion .github/skills/coc-knowledge/references/rest-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,16 @@ CoC server exposes HTTP endpoints organized by domain. All routes are registered
| PATCH | `/api/processes/:id/turns/:idx/pin` | Pin a turn |
| PATCH | `/api/processes/:id/turns/:idx/archive` | Archive a turn |
| GET | `/api/workspaces/:id/group-pins` | List workspace-scoped parent-row group pins for Ralph session groups, For Each run groups, and Map Reduce run groups, sorted newest pin first |
| PATCH | `/api/workspaces/:id/group-pins/:type/:groupId` | Pin/unpin a parent group row. `type` is `ralph-session`, `for-each-run`, or `map-reduce-run`; body `{ pinned: boolean }`. This updates only the group pin record and does not mutate child process pin/archive metadata |
| PATCH | `/api/workspaces/:id/group-pins/:type/:groupId` | Pin/unpin a parent group row. `type` is an open string: legacy names `ralph-session`, `for-each-run`, `map-reduce-run` plus any registered task-group type; body `{ pinned: boolean }`. This updates only the group pin record and does not mutate child process pin/archive metadata |

## Task Groups

Generic parent/child task relationship registry shared by For Each, Map Reduce, Ralph, Dreams, and future hierarchical features. Always registered (no feature flag).

| Method | Path | Description |
|--------|------|-------------|
| GET | `/api/workspaces/:id/task-groups` | List visible task-group summaries (group record + child links with roles). Query: `type=` filters by group type; `includeHidden=true` includes linkage-only groups (Dream runs) |
| GET | `/api/workspaces/:id/task-groups/:groupId` | Get one task-group summary; 404 when unknown |

## Queue

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,7 @@ The `executors/` directory contains the AI chat execution layer:
| `chat-tool-builder.ts` | Common chat tool bundle assembly |
| `bounded-memory-addon.ts` | Wires bounded MEMORY.md into chat executors |

CoC chat tasks use Ask, Autopilot, or Ralph modes. Diff classification jobs are queued as first-class `pr-classification` tasks or legacy classify-diff chat tasks, but `ClassificationExecutor` runs them with interactive Ask-mode semantics while preserving the `saveClassification` tool side effect for result persistence. Pull Requests Team auto-classification reuses the generic classify-diff enqueue path as low-priority background work, building PR identifiers as `prNumber:headSha`, capping each trigger at 10 new enqueues, and skipping ready/running entries through the existing classification store and pending-marker self-healing; the Pull Requests tab's manual "Classify now" action uses the same bounded server helper. Legacy stored or incoming chat payloads with `mode='plan'` are normalized to Ask before dispatch, metadata persistence, schedule execution, and follow-up execution; the server does not route CoC chat work through a dedicated Plan executor. Follow-up execution resolves the final provider/session/default model before applying per-turn reasoning effort; unsupported per-turn efforts are omitted with a warning so stale UI tier selections do not fail an existing chat, while persisted/default effort validation remains strict.
CoC chat tasks use Ask, Autopilot, or Ralph modes. Diff classification jobs are queued as first-class `pr-classification` tasks or legacy classify-diff chat tasks, but `ClassificationExecutor` runs them with interactive Ask-mode semantics while preserving the `saveClassification` tool side effect for result persistence. The process-lifecycle runner persists `mode: 'ask'` in `pr-classification` process metadata (chat payloads still record their normalized payload mode) so mode-less classification records are not mislabelled Autopilot by UI fallbacks. Pull Requests Team auto-classification reuses the generic classify-diff enqueue path as low-priority background work, building PR identifiers as `prNumber:headSha`, capping each trigger at 10 new enqueues, and skipping ready/running entries through the existing classification store and pending-marker self-healing; the Pull Requests tab's manual "Classify now" action uses the same bounded server helper. Legacy stored or incoming chat payloads with `mode='plan'` are normalized to Ask before dispatch, metadata persistence, schedule execution, and follow-up execution; the server does not route CoC chat work through a dedicated Plan executor. Follow-up execution resolves the final provider/session/default model before applying per-turn reasoning effort; unsupported per-turn efforts are omitted with a warning so stale UI tier selections do not fail an existing chat, while persisted/default effort validation remains strict.

## Configuration

Expand Down
4 changes: 3 additions & 1 deletion packages/coc-client/src/client.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { AdminClient, AgentProvidersClient, DbBrowserClient, DreamsClient, ExplorerClient, ForEachClient, GitClient, HealthClient, LoopsClient, MapReduceClient, MemoryClient, MemoryV2Client, NotesClient, PreferencesClient, ProcessesClient, PromptHistoryClient, PullRequestsClient, QueueClient, SchedulesClient, SeenStateClient, ServersClient, SkillsClient, StatsClient, SuggestionsClient, SyncClient, TasksClient, TemplatesClient, WikiClient, WorkflowClient, WorkItemsClient, WorkspacesClient } from './domains';
import { AdminClient, AgentProvidersClient, DbBrowserClient, DreamsClient, ExplorerClient, ForEachClient, GitClient, HealthClient, LoopsClient, MapReduceClient, MemoryClient, MemoryV2Client, NotesClient, PreferencesClient, ProcessesClient, PromptHistoryClient, PullRequestsClient, QueueClient, SchedulesClient, SeenStateClient, ServersClient, SkillsClient, StatsClient, SuggestionsClient, SyncClient, TaskGroupsClient, TasksClient, TemplatesClient, WikiClient, WorkflowClient, WorkItemsClient, WorkspacesClient } from './domains';
import { HttpTransport, normalizeOptions } from './http';
import { EventsClient } from './realtime';
import type { CocClientOptions, CocRequestOptions, NormalizedCocClientOptions } from './types';
Expand Down Expand Up @@ -27,6 +27,7 @@ export class CocClient {
readonly skills: SkillsClient;
readonly stats: StatsClient;
readonly suggestions: SuggestionsClient;
readonly taskGroups: TaskGroupsClient;
readonly tasks: TasksClient;
readonly templates: TemplatesClient;
readonly wiki: WikiClient;
Expand Down Expand Up @@ -66,6 +67,7 @@ export class CocClient {
this.skills = new SkillsClient(this.transport);
this.stats = new StatsClient(this.transport);
this.suggestions = new SuggestionsClient(this.transport);
this.taskGroups = new TaskGroupsClient(this.transport);
this.tasks = new TasksClient(this.transport);
this.templates = new TemplatesClient(this.transport);
this.wiki = new WikiClient(this.transport, this.options);
Expand Down
1 change: 1 addition & 0 deletions packages/coc-client/src/contracts/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export * from './schedules';
export * from './seen-state';
export * from './skills';
export * from './stats';
export * from './task-groups';
export * from './tasks';
export * from './templates';
export * from './servers';
Expand Down
6 changes: 5 additions & 1 deletion packages/coc-client/src/contracts/processes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,11 @@ export interface PinnedTurnsResponse {
turns: ConversationTurn[];
}

export type ProcessGroupPinType = 'ralph-session' | 'for-each-run' | 'map-reduce-run';
/**
* Group-pin type. Open union: the known literals are the legacy pin type
* names; any registered task-group type pins under its own type string.
*/
export type ProcessGroupPinType = 'ralph-session' | 'for-each-run' | 'map-reduce-run' | (string & {});

export interface ProcessGroupPin {
type: ProcessGroupPinType;
Expand Down
70 changes: 70 additions & 0 deletions packages/coc-client/src/contracts/task-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
/**
* Task Groups — generic parent/child task relationship registry.
*
* One summary shape covers every hierarchical feature (For Each runs,
* Map Reduce runs, Ralph sessions, Dream runs, and future group types).
* Feature-specific summary fields ride in `extra`.
*/

/** Normalized group lifecycle. Feature-specific states ride in `extra.detailStatus`. */
export type TaskGroupStatus = 'draft' | 'running' | 'completed' | 'failed' | 'cancelled';

/** Known group types. The registry is open — future types are plain strings. */
export type TaskGroupType = 'for-each' | 'map-reduce' | 'ralph' | 'dream' | (string & {});

export interface TaskGroupChildLink {
/** Child role within the group ('generation' | 'item' | 'reduce' | 'iteration' | 'grilling' | 'analyzer' | 'critic' | ...). */
role: string;
taskId?: string;
processId?: string;
/** Stable per-item key (For Each/Map Reduce item ID, Ralph iteration index, ...). */
itemKey?: string;
/** Ordering hint within the group (e.g. iteration number). */
memberIndex?: number;
linkedAt: string;
}

export interface TaskGroupSummary {
groupId: string;
workspaceId: string;
type: TaskGroupType;
title?: string;
status: TaskGroupStatus;
/** Hidden groups are linkage-only (e.g. Dream internals) — not rendered as chat-list groups. */
hidden?: boolean;
/** Process ID of the visible origin chat (generation chat, grilling chat). */
originProcessId?: string;
createdAt: string;
updatedAt: string;
completedAt?: string;
childCount: number;
children: TaskGroupChildLink[];
/** Feature summary extras (itemCount, reduceStatus, detailStatus, loopCount, ...). */
extra?: Record<string, unknown>;
}

export interface ListTaskGroupsQuery {
type?: string;
includeHidden?: boolean;
}

export interface ListTaskGroupsResponse {
groups: TaskGroupSummary[];
}

export interface TaskGroupResponse {
group: TaskGroupSummary;
}

/**
* The `payload.context.taskGroup` tag carried by every child task of a group.
* Mirrored into `AIProcess.metadata.taskGroup` when the process is created.
*/
export interface TaskGroupRef {
groupId: string;
groupType: TaskGroupType;
/** Child role within the group. */
role: string;
itemKey?: string;
workspaceId: string;
}
1 change: 1 addition & 0 deletions packages/coc-client/src/domains/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export { ServersClient } from './servers';
export { SkillsClient } from './skills';
export { StatsClient } from './stats';
export { SuggestionsClient } from './suggestions';
export { TaskGroupsClient } from './task-groups';
export { TasksClient } from './tasks';
export { TemplatesClient } from './templates';
export { WorkItemsClient } from './work-items';
Expand Down
34 changes: 34 additions & 0 deletions packages/coc-client/src/domains/task-groups.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
import type {
ListTaskGroupsQuery,
ListTaskGroupsResponse,
TaskGroupResponse,
TaskGroupSummary,
} from '../contracts';
import type { RequestAdapter } from '../types';
import { encodePathSegment } from '../url';

function groupsPath(workspaceId: string, suffix = ''): string {
return `/workspaces/${encodePathSegment(workspaceId)}/task-groups${suffix}`;
}

export class TaskGroupsClient {
constructor(private readonly transport: RequestAdapter) {}

async list(workspaceId: string, query?: ListTaskGroupsQuery): Promise<TaskGroupSummary[]> {
const params = new URLSearchParams();
if (query?.type) params.set('type', query.type);
if (query?.includeHidden) params.set('includeHidden', 'true');
const queryString = params.toString();
const response = await this.transport.request<ListTaskGroupsResponse>(
groupsPath(workspaceId, queryString ? `?${queryString}` : ''),
);
return response.groups ?? [];
}

async get(workspaceId: string, groupId: string): Promise<TaskGroupSummary> {
const response = await this.transport.request<TaskGroupResponse>(
groupsPath(workspaceId, `/${encodePathSegment(groupId)}`),
);
return response.group;
}
}
Loading
Loading