diff --git a/.ascli.json b/.ascli.json new file mode 100644 index 0000000..645221b --- /dev/null +++ b/.ascli.json @@ -0,0 +1,12 @@ +{ + "version": 1, + "binding": { + "apiBaseUrl": "https://agentspace.so", + "claimToken": "5Jr5_HEFEH_4Mc-7_dzUTEhYUWKFC-uOi58RrqMQ7RTGTA01", + "claimUrl": "https://agentspace.so/claim?workspaceId=ws_iTSoXqyy7Mcf&token=5Jr5_HEFEH_4Mc-7_dzUTEhYUWKFC-uOi58RrqMQ7RTGTA01", + "clientId": "ascli", + "createdAt": "2026-06-07T17:39:16.001Z", + "workspaceId": "ws_iTSoXqyy7Mcf", + "workspaceName": "fork-lifts-phases-3-11" + } +} \ No newline at end of file diff --git a/.codesight/CODESIGHT.md b/.codesight/CODESIGHT.md new file mode 100644 index 0000000..b806795 --- /dev/null +++ b/.codesight/CODESIGHT.md @@ -0,0 +1,1439 @@ +# boocode — AI Context Map + +> **Stack:** fastify, go-net-http | none | react | typescript +> **Microservices:** @boocode/contracts, @boocode/ion, @boocode/booterm, @boocode/coder, @boocode/server, @boocode/web, codecontext, @boocode/conductor + +> 131 routes (9 inferred) + 9 ws | 18 models | 69 components | 247 lib files | 39 env vars | 16 middleware +> **Token savings:** this file is ~0 tokens. Without it, AI exploration would cost ~0 tokens. **Saves ~0 tokens per conversation.** +> **Last scanned:** 2026-06-07 21:09 — re-run after significant changes + +--- + +# Routes + +## CRUD Resources + +- **`/api/battles`** GET | POST | GET/:id → Battle +- **`/api/runs`** GET | POST | GET/:id → Run +- **`/api/tasks`** GET | POST | GET/:id → Task +- **`/api/chats/:id/messages`** GET | POST | GET/:id | DELETE/:id → Message +- **`/api/projects`** GET | POST | GET/:id | PATCH/:id | DELETE/:id → Project +- **`/api/sessions`** GET/:id | PATCH/:id | DELETE/:id → Session + +## Other Routes + +### fastify + +- `GET` `/api/term/health` params() +- `POST` `/api/term/sessions/:sid/panes/:pid/start` params(sid, pid) [auth] +- `POST` `/api/term/sessions/:sid/panes/:pid/kill` params(sid, pid) [auth] +- `GET` `/ws/term/sessions/:sid/panes/:pid` params(sid, pid) [auth] +- `GET` `/api/health` params() [auth, db, queue, ai] +- `GET` `/api/sessions/:sessionId/agent-sessions` params(sessionId) [auth, db] +- `POST` `/api/battles/generate-prompt` params() [auth, db] +- `POST` `/api/battles/:id/stop` params(id) [auth, db] +- `GET` `/api/battles/:id/analysis` params(id) [auth, db] +- `POST` `/api/battles/:id/analyze` params(id) [auth, db] +- `PATCH` `/api/battles/:id/winner` params(id) [auth, db] +- `GET` `/api/battles/:id/contestants/:cid/diff` params(id, cid) [auth, db] +- `POST` `/api/battles/:id/cross-examine` params(id) [auth, db] +- `GET` `/api/sessions/:sessionId/checkpoints` params(sessionId) [auth, db] +- `POST` `/api/sessions/:sessionId/checkpoints/:checkpointId/restore` params(sessionId, checkpointId) [auth, db] +- `GET` `/api/inbox` params() [auth, db] +- `POST` `/api/inbox/:id/retry` params(id) [auth, db] +- `POST` `/api/chats/:chatId/close` params(chatId) [auth, db] +- `POST` `/api/sessions/:sessionId/close` params(sessionId) [auth, db] +- `GET` `/api/sessions/:sessionId/messages` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/messages` params(sessionId) [auth, db, queue] +- `POST` `/api/chats/:id/answer_user_input` params(id) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/stop` params(sessionId) [auth, db, queue] +- `GET` `/api/sessions/:sessionId/pending` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/pending/create` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/pending/apply` params(sessionId) [auth, db, queue] +- `POST` `/api/pending/:id/apply` params(id) [auth, db, queue] +- `POST` `/api/pending/:id/reject` params(id) [auth, db, queue] +- `POST` `/api/pending/:id/rewind` params(id) [auth, db, queue] +- `GET` `/api/providers/snapshot` params() [db, cache] +- `GET` `/api/providers/config` params() [db, cache] +- `PATCH` `/api/providers/config` params() [db, cache] +- `POST` `/api/providers/refresh` params() [db, cache] +- `GET` `/api/providers/:id/diagnostic` params(id) [db, cache] +- `POST` `/api/runs/:id/cancel` params(id) [auth, db] +- `POST` `/api/sessions/:sessionId/skill_invoke` params(sessionId) [auth, db, queue] +- `GET` `/api/stats/costs` params() [auth, db] +- `POST` `/api/tasks/:id/cancel` params(id) [auth, db, cache, ai] +- `GET` `/api/tasks/:id/permission` params(id) [auth, db, cache, ai] +- `POST` `/api/tasks/:id/permission` params(id) [auth, db, cache, ai] +- `GET` `/api/tasks/:id/commands` params(id) [auth, db, cache, ai] +- `GET` `/api/sessions/:sessionId/worktree-risk` params(sessionId) [auth, db] +- `POST` `/api/sessions/:sessionId/worktree-stash` params(sessionId) [auth, db] +- `GET` `/api/ws/sessions/:sessionId` params(sessionId) [auth, db] +- `GET` `/api/ws/user` params() [auth, db] +- `GET` `/api/projects/:id/agents` params(id) [db, cache] +- `POST` `/api/chats/:id/messages/:msg_id/artifacts/download` params(id, msg_id) [auth, db] +- `GET` `/api/chats/:id/messages/:msg_id/html_artifact` params(id, msg_id) [auth, db] +- `GET` `/api/projects/:project_id/artifacts/:filename` params(project_id, filename) [auth, db] +- `GET` `/api/sessions/:id/chats` params(id) [auth, db] +- `POST` `/api/sessions/:id/chats` params(id) [auth, db] +- `PATCH` `/api/chats/:id` params(id) [auth, db] +- `POST` `/api/sessions/:id/chats/archive-all` params(id) [auth, db] +- `GET` `/api/sessions/:id/chats/open-count` params(id) [auth, db] +- `POST` `/api/chats/:id/archive` params(id) [auth, db] +- `POST` `/api/chats/:id/unarchive` params(id) [auth, db] +- `DELETE` `/api/chats/:id` params(id) [auth, db] +- `POST` `/api/chats/:id/fork` params(id) [auth, db] +- `POST` `/api/chats/:id/discard_stale` params(id) [auth, db] +- `GET` `/api/coder/ws/sessions/:sessionId` params(sessionId) [auth] +- `ALL` `/api/coder/*` params() [auth] +- `GET` `/api/settings/inference` params() [cache] +- `PATCH` `/api/settings/inference` params() [cache] +- `GET` `/api/sessions/:id/messages` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/messages/:message_id/regenerate` params(id, message_id) [auth, db, queue] +- `POST` `/api/chats/:id/compact` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/stop` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/continue` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/force_send` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/grant_read_access` params(id) [auth, db, queue] +- `GET` `/api/models` params() +- `POST` `/api/projects/create` params() [auth, db] +- `POST` `/api/projects/:id/archive` params(id) [auth, db] +- `POST` `/api/projects/:id/unarchive` params(id) [auth, db] +- `GET` `/api/projects/available` params() [auth, db] +- `GET` `/api/projects/:id/list_dir` params(id) [auth, db] +- `GET` `/api/projects/:id/view_file` params(id) [auth, db] +- `GET` `/api/projects/:id/git` params(id) [auth, db] +- `GET` `/api/projects/:id/git/diff` params(id) [auth, db] +- `POST` `/api/projects/:id/git/stage` params(id) [auth, db] +- `POST` `/api/projects/:id/git/unstage` params(id) [auth, db] +- `POST` `/api/projects/:id/git/commit` params(id) [auth, db] +- `POST` `/api/projects/:id/git/discard` params(id) [auth, db] +- `POST` `/api/projects/:id/write_file` params(id) [auth, db] +- `GET` `/api/projects/:id/files` params(id) [auth, db] +- `GET` `/api/projects/:id/sessions` params(id) [auth, db] +- `POST` `/api/projects/:id/sessions` params(id) [auth, db] +- `PATCH` `/api/sessions/:id/workspace` params(id) [auth, db] +- `POST` `/api/projects/:id/sessions/archive-all` params(id) [auth, db] +- `GET` `/api/projects/:id/sessions/open-count` params(id) [auth, db] +- `POST` `/api/sessions/:id/archive` params(id) [auth, db] +- `POST` `/api/sessions/:id/unarchive` params(id) [auth, db] +- `GET` `/api/settings` params() [db] +- `PATCH` `/api/settings` params() [db] +- `GET` `/api/sidebar` params() [auth, db] +- `GET` `/api/skills` params() [auth, db, queue] +- `POST` `/api/chats/:id/skill_invoke` params(id) [auth, db, queue] +- `GET` `/api/tools/cost_stats` params() [auth, db] +- `GET` `/api/ws/sessions/:id` params(id) [auth, db] + +### go-net-http + +- `GET` `/health` params() [queue] +- `POST` `/v1/get_codebase_overview` params() [queue] +- `POST` `/v1/get_file_analysis` params() [queue] +- `POST` `/v1/get_symbol_info` params() [queue] +- `POST` `/v1/search_symbols` params() [queue] +- `POST` `/v1/get_dependencies` params() [queue] +- `POST` `/v1/watch_changes` params() [queue] +- `POST` `/v1/get_semantic_neighborhoods` params() [queue] +- `POST` `/v1/get_framework_analysis` params() [queue] +- `POST` `/v1/get_symbol_details` params() [queue] +- `POST` `/v1/get_call_graph` params() [queue] +- `POST` `/v1/get_blast_radius` params() [queue] + +## WebSocket Events + +- `WS` `message` — `apps/booterm/src/ws/attach.ts` +- `WS` `close` — `apps/booterm/src/ws/attach.ts` +- `WS` `message` — `apps/coder/src/cli.ts` +- `WS` `error` — `apps/coder/src/cli.ts` +- `WS` `close` — `apps/coder/src/cli.ts` +- `WS` `close` — `apps/coder/src/routes/ws.ts` +- `WS` `error` — `apps/coder/src/routes/ws.ts` +- `WS` `close` — `apps/server/src/routes/ws.ts` +- `WS` `error` — `apps/server/src/routes/ws.ts` + +--- + +# Schema + +### pending_changes +- id: uuid (pk) +- session_id: uuid (required, fk) +- task_id: uuid (fk) +- file_path: text (required) +- operation: text (required) +- diff: text (required) +- status: text (required) + +### tasks +- id: uuid (pk) +- project_id: uuid (required, fk) +- parent_task_id: uuid (fk) +- state: text (required) +- input: text (required) +- output_summary: text +- agent: text +- model: text +- execution_path: text +- cost_tokens: integer +- started_at: timestamp(tz) +- ended_at: timestamp(tz) + +### available_agents +- name: text (pk) +- install_path: text +- version: text +- supports_acp: boolean (required) +- last_probed_at: timestamp(tz) + +### agent_sessions +- session_id: uuid (required, fk) +- agent: text (required) +- backend: text (required) +- agent_session_id: text (fk) +- server_port: integer +- status: text (required) +- last_active_at: timestamp(tz) + +### worktrees +- id: uuid (pk) +- session_id: uuid (fk) +- project_id: uuid (fk) +- path: text (required) +- branch: text +- base_commit: text +- slug: text +- status: text (required) + +### checkpoints +- id: uuid (pk) +- chat_id: uuid (required, fk) +- session_id: uuid (fk) +- worktree_id: uuid (fk) +- message_id: uuid (fk) + +### claude_session_entries +- id: bigint(auto) (pk) +- project_key: text (required) +- session_id: text (required, fk) +- subpath: text (required) + +### flow_runs +- id: uuid (pk) +- project_id: uuid (required, fk) +- flow_name: text (required) +- band: text (required) +- model: text (required) +- status: text (required) +- input: jsonb (required) +- report: text +- error: text + +### flow_steps +- id: uuid (pk) +- run_id: uuid (required, fk) +- step_id: text (required, fk) +- kind: text (required) +- agent: text +- status: text (required) +- task_id: uuid (fk) +- chat_id: uuid (fk) +- input: text +- output: text +- error: text + +### battles +- id: uuid (pk) +- project_id: uuid (required, fk) +- battle_type: text (required) +- prompt: text (required) +- status: text (required) +- winner_contestant_id: uuid (fk) +- results_path: text +- error: text + +### contestants +- id: uuid (pk) +- battle_id: uuid (required, fk) +- identity: text (required) +- model: text (required) +- lane: text (required) +- task_id: uuid (fk) +- worktree_id: uuid (fk) +- status: text (required) +- duration_ms: integer +- tokens_per_sec: float8 +- cost_tokens: integer +- result_path: text +- error: text + +### cross_examinations +- id: uuid (pk) +- battle_id: uuid (required, fk) +- identity: text (required) +- model: text (required) +- verdict: text + +### projects +- id: uuid (pk) +- name: text (required) +- path: text (required) +- added_at: timestamp(tz) (required) +- last_session_id: uuid (fk) + +### sessions +- id: uuid (pk) +- project_id: uuid (required, fk) +- name: text (required) +- model: text (required) +- system_prompt: text (required) + +### messages +- id: uuid (pk) +- session_id: uuid (required, fk) +- role: text (required) +- content: text (required) +- status: text (required) +- last_seq: integer (required) + +### message_parts +- id: uuid (pk) +- message_id: uuid (required, fk) +- sequence: integer (required) +- kind: text (required) +- payload: jsonb (required) + +### settings +- value: jsonb (required) + +### chats +- id: uuid (pk) +- session_id: uuid (required, fk) +- name: text +- status: text (required) + +--- + +# Components + +- **App** — `apps/web/src/App.tsx` +- **AddProjectModal** — props: open, onOpenChange, onAdded — `apps/web/src/components/AddProjectModal.tsx` +- **AgentComposerBar** — props: projectPath, value, onChange, onProviderCommandsChange, connected, agentStatus — `apps/web/src/components/AgentComposerBar.tsx` +- **AgentPicker** — props: projectId, value, onChange — `apps/web/src/components/AgentPicker.tsx` +- **ArenaLauncherDialog** — `apps/web/src/components/ArenaLauncherDialog.tsx` +- **ArtifactPaneHeader** — props: title, defaultTitle, onDownload, downloadDisabled, onClose, onCopy, justCopied, copyDisabled — `apps/web/src/components/ArtifactPaneHeader.tsx` +- **AskUserInputCard** — props: toolCall, toolResult, chatId, apiPrefix — `apps/web/src/components/AskUserInputCard.tsx` +- **AttachmentChip** — props: attachment, onRemove, onPreview — `apps/web/src/components/AttachmentChip.tsx` +- **AttachmentPreviewModal** — props: attachment, onClose — `apps/web/src/components/AttachmentPreviewModal.tsx` +- **BottomSheet** — props: open, onClose, title — `apps/web/src/components/BottomSheet.tsx` +- **CapHitSentinel** — props: message, capHitPosition, isLatest — `apps/web/src/components/CapHitSentinel.tsx` +- **ChatInput** — props: disabled, projectId, agentId, onAgentChange, sessionId, webSearchEnabled, onSend, onForceSend, generating, onStop — `apps/web/src/components/ChatInput.tsx` +- **ChatTabBar** — props: pane, tabs, tabNumbers, onSwitchTab, onRemoveTab, onCloseOthers, onCloseToRight, onCloseAll, onNewTab, onSplitPane — `apps/web/src/components/ChatTabBar.tsx` +- **ChatThroughput** — props: chatId, className — `apps/web/src/components/ChatThroughput.tsx` +- **CodeBlock** — props: code, lang — `apps/web/src/components/CodeBlock.tsx` +- **ContextMeter** — props: messages, modelContextLimit, sessionCostUsd — `apps/web/src/components/ContextMeter.tsx` +- **CreateProjectModal** — props: open, onOpenChange — `apps/web/src/components/CreateProjectModal.tsx` +- **DoomLoopSentinel** — props: message — `apps/web/src/components/DoomLoopSentinel.tsx` +- **DropOverlay** — props: visible — `apps/web/src/components/DropOverlay.tsx` +- **FileMentionPopover** — props: query, files, anchorRect, onSelect, onClose — `apps/web/src/components/FileMentionPopover.tsx` +- **FileViewerOverlay** — props: path, content, lang, onClose — `apps/web/src/components/FileViewerOverlay.tsx` +- **FlowLauncherDialog** — `apps/web/src/components/FlowLauncherDialog.tsx` +- **GitDiffView** — props: result, loading, error, mode, onSelectMode, onRefresh, mutating, mutateError, onStage, onUnstage — `apps/web/src/components/GitDiffView.tsx` +- **HtmlArtifactPane** — props: chatId, state, onClose — `apps/web/src/components/HtmlArtifactPane.tsx` +- **InferenceSettings** — `apps/web/src/components/InferenceSettings.tsx` +- **MarkdownArtifactPane** — props: chatId, state, onClose — `apps/web/src/components/MarkdownArtifactPane.tsx` +- **MarkdownRenderer** — props: content — `apps/web/src/components/MarkdownRenderer.tsx` +- **MessageBubble** — props: message, sessionChats, capHitInfo, actions, hideActions, hasCheckpoint, restoreDisabled — `apps/web/src/components/MessageBubble.tsx` +- **MessageList** — props: messages, sessionChats — `apps/web/src/components/MessageList.tsx` +- **MobileTabSwitcher** — props: panes, activePaneIdx, chats, onSwitchPane, onRemovePane, onRenameChat — `apps/web/src/components/MobileTabSwitcher.tsx` +- **ModelPicker** — props: value, onChange — `apps/web/src/components/ModelPicker.tsx` +- **NewPaneMenu** — props: onAddPane, disabled, projectId — `apps/web/src/components/NewPaneMenu.tsx` +- **PaneHeaderActions** — props: onNewTab, onSplitPane, onNewOrchestrator, onNewArena, onReopenPane, onShowHistory, onRemovePane, historyActive, className — `apps/web/src/components/PaneHeaderActions.tsx` +- **PermissionCard** — props: prompt, onRespond, busy — `apps/web/src/components/PermissionCard.tsx` +- **ProjectSidebar** — `apps/web/src/components/ProjectSidebar.tsx` +- **RequestReadAccessCard** — props: toolCall, toolResult, chatId — `apps/web/src/components/RequestReadAccessCard.tsx` +- **RightRail** — props: projectId, sessionId — `apps/web/src/components/RightRail.tsx` +- **SessionLandingPage** — props: projectId, sessionId, agentId, onAgentChange, onSend, onSkillInvoke, createChat, chats, onOpenChat, onUnarchiveChat — `apps/web/src/components/SessionLandingPage.tsx` +- **SlashCommandPicker** — props: query, items, groups, inputRef, onSelect, onClose, emptyLabel — `apps/web/src/components/SlashCommandPicker.tsx` +- **StaleStreamBanner** — props: onRetry, onDiscard — `apps/web/src/components/StaleStreamBanner.tsx` +- **StatusDot** — props: chatId, className — `apps/web/src/components/StatusDot.tsx` +- **ThemePicker** — `apps/web/src/components/ThemePicker.tsx` +- **ToolCallGroup** — props: runs — `apps/web/src/components/ToolCallGroup.tsx` +- **ToolCallLine** — props: run, insideGroup — `apps/web/src/components/ToolCallLine.tsx` +- **Workspace** — props: sessionId, projectId, agentId, onAgentChange, panesHook, chatsHook, session, project, onAddPane — `apps/web/src/components/Workspace.tsx` +- **AddProviderModal** — props: open, onOpenChange, onAdded — `apps/web/src/components/coder/AddProviderModal.tsx` +- **ProvidersSettings** — `apps/web/src/components/coder/ProvidersSettings.tsx` +- **MatrixRain** — props: enabled, density, speed, opacity — `apps/web/src/components/fx/MatrixRain.tsx` +- **NeonField** — props: enabled, opacity, speed — `apps/web/src/components/fx/NeonField.tsx` +- **ThemeFx** — `apps/web/src/components/fx/ThemeFx.tsx` +- **ClaudeIcon** — props: size, className — `apps/web/src/components/icons/ProviderIcons.tsx` +- **OpenCodeIcon** — props: size, className — `apps/web/src/components/icons/ProviderIcons.tsx` +- **ArenaPane** — props: state, onClose — `apps/web/src/components/panes/ArenaPane.tsx` +- **ChatPane** — props: sessionId, chatId, projectId, agentId, onAgentChange, sessionChats, webSearchEnabled — `apps/web/src/components/panes/ChatPane.tsx` +- **CoderMessageList** — props: messages, chatId, footer, actions, checkpointMessageIds, restoreDisabled — `apps/web/src/components/panes/CoderMessageList.tsx` +- **CoderPane** — props: sessionId, paneId, chatId, chatPending, projectPath, onConnectedChange, onAgentLabelChange — `apps/web/src/components/panes/CoderPane.tsx` +- **OrchestratorPane** — props: state, onClose — `apps/web/src/components/panes/OrchestratorPane.tsx` +- **SettingsPane** — props: session, project, maximized, onToggleMaximize, onClose, isMobile — `apps/web/src/components/panes/SettingsPane.tsx` +- **TerminalPane** — props: sessionId, paneId, label, active — `apps/web/src/components/panes/TerminalPane.tsx` +- **FloatingMenu** — props: x, y, hasSelection, chatInputs, onCopy, onPaste, onSelectAll, onSearch, onSendToChat, onDismiss — `apps/web/src/components/panes/terminal/FloatingMenu.tsx` +- **SearchBar** — props: searchRef, theme, onClose — `apps/web/src/components/panes/terminal/SearchBar.tsx` +- **TerminalHotkeyBar** — props: ctrlArmed, onSendBytes, onArmCtrl, onFit — `apps/web/src/components/panes/terminal/TerminalHotkeyBar.tsx` +- **RightRailDrawerProvider** — `apps/web/src/hooks/useRightRailDrawer.tsx` +- **SidebarDrawerProvider** — `apps/web/src/hooks/useSidebarDrawer.tsx` +- **PATH_REGEX** — `apps/web/src/lib/linkify-paths.tsx` +- **Home** — `apps/web/src/pages/Home.tsx` +- **Project** — `apps/web/src/pages/Project.tsx` +- **Session** — `apps/web/src/pages/Session.tsx` +- **Settings** — `apps/web/src/pages/Settings.tsx` + +--- + +# Libraries + +- `apps/booterm/src/auth.ts` — function getUser: (req) => string +- `apps/booterm/src/config.ts` — function loadConfig: () => Config +- `apps/booterm/src/db.ts` + - function getPool: (databaseUrl) => pg.Pool + - function getSessionInfo: (sessionId) => Promise + - function pingDb: () => Promise + - function closeDb: () => Promise +- `apps/booterm/src/pty/manager.ts` + - function sanitizeId: (raw) => string | null + - function tmuxSessionName: (paneId) => string + - function hasSession: (tmuxConfPath, sessionName) => Promise + - function ensureSession: (tmuxConfPath, sessionName, projectRoot, log, cols?, rows?) => Promise + - function killSession: (tmuxConfPath, sessionName) => Promise + - function capturePane: (tmuxConfPath, sessionName, lines) => Promise +- `apps/booterm/src/pty/pty.ts` — function attachPty: (opts) => IPty +- `apps/booterm/src/ws/attach.ts` — function registerWsAttachRoute: (app, tmuxConfPath) => void +- `apps/coder/src/conductor/contracts.ts` + - function produceContract: (contracts) => string + - function reviewContract: (contracts) => string + - type Contract + - const EVIDENCE_PRODUCE + - const EVIDENCE_REVIEW + - const YAGNI_PRODUCE + - _...1 more_ +- `apps/coder/src/conductor/flows/_util.ts` — function q, function repoLine +- `apps/coder/src/conductor/flows/index.ts` + - function describeFlows: () => string + - function getFlow: (name) => Flow | undefined + - const FLOWS: Record + - const FLOW_NAMES: string[] +- `apps/coder/src/conductor/persona-loader.ts` — function loadPersona: (agent) => Promise, const AGENTS_DIR +- `apps/coder/src/conductor/render.ts` — function slugify: (s) => string +- `apps/coder/src/conductor/spine.ts` + - function readBand: (input) => Band + - function fastNote: (ctx) => string + - function buildSpineFlow: (spine) => Flow +- `apps/coder/src/config.ts` — function loadConfig: () => Config, type Config +- `apps/coder/src/db.ts` + - function getSql: (config) => Sql + - function applySchema: (sql) => Promise + - function pingDb: (sql) => Promise + - function closeDb: () => Promise + - type Sql +- `apps/coder/src/plugins/host.ts` + - function registerHook: (name, fn) => void + - function emitHook: (name, ctx) => Promise + - function clearHooks: () => void + - interface ToolHookContext + - interface ToolResultContext + - type HookName + - _...1 more_ +- `apps/coder/src/services/acp-client-fs.ts` — function readWorktreeTextFile: (worktreePath, filePath, line?, limit?) => Promise, function writeWorktreeTextFile: (worktreePath, filePath, content) => Promise +- `apps/coder/src/services/acp-client.ts` — function buildAcpClient: (worktreePath, resolveTurn) => void, interface AcpTurnContext +- `apps/coder/src/services/acp-derive.ts` + - function deriveModesFromACP: (fallbackModes, modeState?, configOptions?) => void + - function deriveModelDefinitionsFromACP: (models, configOptions?) => ProviderModel[] + - function findThoughtLevelConfigId: (configOptions) => string | null +- `apps/coder/src/services/acp-dispatch.ts` + - function dispatchViaAcp: (opts) => Promise + - interface AcpDispatchResult + - interface AcpDispatchOpts +- `apps/coder/src/services/acp-event-map.ts` — function mapSessionUpdate: (params, priorSnapshots, AcpToolSnapshot>) => void +- `apps/coder/src/services/acp-probe.ts` — function probeAcpProvider: (agent, installPath, cwd) => Promise, interface AcpProbeResult +- `apps/coder/src/services/acp-spawn.ts` + - function resolveAcpSpawnArgs: (agent) => string[] | null + - function resolveLaunchSpec: (resolved, installPath) => void + - function resolveAcpProbeBinaries: (agent) => string[] +- `apps/coder/src/services/acp-stream.ts` — function createAcpNdJsonStream: (child) => void +- `apps/coder/src/services/acp-tool-snapshot.ts` + - function mergeToolSnapshot: (toolCallId, update, previous?) => AcpToolSnapshot + - function mapToolLifecycleStatus: (status, rawOutput?) => AcpToolLifecycleStatus + - function snapshotToWireToolCall: (snapshot) => void + - function snapshotToPartPayload: (snapshot) => void + - function synthesizeCanceledSnapshots: (snapshots) => AcpToolSnapshot[] + - interface AcpToolSnapshot + - _...2 more_ +- `apps/coder/src/services/agent-commands-cache.ts` + - function setTaskCommands: (taskId, commands) => void + - function mergeTaskCommands: (taskId, commands) => void + - function getTaskCommands: (taskId) => AgentCommand[] | null + - function clearTaskCommands: (taskId) => void +- `apps/coder/src/services/agent-pool.ts` + - class AgentPool + - interface AgentPoolOpts + - const OPENCODE_POOL_KEY + - const agentPool +- `apps/coder/src/services/agent-probe.ts` — function probeAgents: (sql, log) => Promise +- `apps/coder/src/services/agent-status-publish.ts` — function publishAgentStatus: (publishFrame, sessionId, chatId, agent, status, reason?, at) => void +- `apps/coder/src/services/agent-turn-persist.ts` — function persistExternalAgentTurn: (sql, assistantMessageId, snapshots, reasoningText) => Promise +- `apps/coder/src/services/arena-analyzer-helpers.ts` + - function buildDigestPrompt: (input) => void + - function buildJudgePrompt: (originalPrompt, digests) => void + - function shouldNameWinner: (succeededCount) => boolean + - function extractWinner: (judgeOutput) => void + - function buildCrossExamPrompt: (opts) => void + - interface ContestantDigestInput + - _...1 more_ +- `apps/coder/src/services/arena-analyzer.ts` — function createAnalyzer: (deps) => Analyzer, interface Analyzer +- `apps/coder/src/services/arena-decisions.ts` + - function classifyLane: (battleType, _identity, model, localModels) => ContestantLane + - function nextLocalContestant: (contestants) => string | null + - function isBattleComplete: (contestants) => boolean + - function computeBenchmark: (startedAt, endedAt, costTokens, lane) => Benchmark + - function sanitizeSlug: (s) => string + - function buildBattleSlug: (battleId, battleType, createdAt) => string + - _...7 more_ +- `apps/coder/src/services/arena-model-call.ts` — function arenaModelCall: (opts, 'LLAMA_SWAP_URL'>; + model) => Promise +- `apps/coder/src/services/arena-runner.ts` + - function createBattleRunner: (deps) => BattleRunner + - interface ContestantSpec + - interface BattleStartOpts + - interface BattleRunner + - type DispatchContestantFn + - type OnBattleComplete + - _...1 more_ +- `apps/coder/src/services/audit-session.ts` + - function generateSessionId: () => string + - function getCurrentSession: (basePath?) => Promise + - function getSessionJson: (sessionId, basePath?) => Promise + - function getIndex: (basePath?) => Promise + - function startSession: (task, basePath?) => Promise + - function endSession: (basePath?) => Promise + - _...18 more_ +- `apps/coder/src/services/backends/claude-sdk-map.ts` + - function createClaudeSdkMapState: () => ClaudeSdkMapState + - function mapSdkMessage: (msg, state) => AgentEvent[] + - interface ClaudeSdkMapState +- `apps/coder/src/services/backends/claude-sdk-routing.ts` — function claudeSdkBackendEnabled: (env) => boolean, function shouldUseClaudeSdk: (task, env) => boolean +- `apps/coder/src/services/backends/claude-sdk.ts` — class ClaudeSdkBackend, interface ClaudeSdkBackendDeps +- `apps/coder/src/services/backends/claude-session-store.ts` — class PostgresSessionStore +- `apps/coder/src/services/backends/lifecycle-decisions.ts` + - function selectIdleEvictionTargets: (entries, now, ttlMs) => string[] + - function selectLruEvictionTargets: (entries, cap) => string[] + - function decideRestart: (input) => RestartDecision + - function selectOrphanWorktreeTargets: (onDisk, liveWorktreePaths, now, graceMs) => string[] + - interface PoolEntrySnapshot + - interface RestartDecisionInput + - _...7 more_ +- `apps/coder/src/services/backends/opencode-event-map.ts` + - function stripDcpTags: (s) => string + - function eventSessionId: (ev) => string | null + - function resolvePartDedupeKey: (part, type) => string | null + - function mapToolStatus: (s) => ToolCallStatus | null + - function toolPartToSnapshot: (part) => AcpToolSnapshot + - function toolCalledSnapshot: (p) => AcpToolSnapshot + - _...7 more_ +- `apps/coder/src/services/backends/opencode-server-process.ts` + - function shouldStartServer: (s) => boolean + - class OpenCodeServerSupervisor + - interface ServerDownInfo + - interface SupervisorHooks + - interface OpenCodeServerSupervisorDeps +- `apps/coder/src/services/backends/opencode-server.ts` — class OpenCodeServerBackend, interface OpenCodeServerBackendDeps +- `apps/coder/src/services/backends/opencode-sse.ts` + - function reconnectDecision: (failures, policy) => ReconnectDecision + - function startSessionEventLoop: (state, deps) => void + - function runSessionEventLoop: (state, abort, deps) => Promise + - interface TurnState + - interface SessionState + - interface ReconnectPolicy + - _...4 more_ +- `apps/coder/src/services/backends/opencode-usage.ts` + - function stepEndedToUsage: (props) => StepUsage + - interface StepEndedProps + - interface StepUsage +- `apps/coder/src/services/backends/pushable-iterable.ts` — function createPushable: () => Pushable, interface Pushable +- `apps/coder/src/services/backends/turn-guard.ts` + - function armAbortGuard: (g) => void + - function noteTurnActivity: (g) => void + - function consumeTerminal: (g) => 'swallow' | 'settle' + - interface AbortTerminalGuard +- `apps/coder/src/services/backends/warm-acp-routing.ts` — function shouldUseWarmBackend: (task) => boolean, function isTurnOkForStopReason: (stopReason) => boolean +- `apps/coder/src/services/backends/warm-acp.ts` — class WarmAcpBackend, interface WarmAcpBackendDeps +- `apps/coder/src/services/cancel-registry.ts` — function createCancelRegistry: () => CancelRegistry, interface CancelRegistry +- `apps/coder/src/services/checkpoints.ts` + - function buildShadowCommitCommand: (worktreePath, id) => string + - function createCheckpoint: (sql, args, opts?) => Promise< + - function restoreCheckpoint: (sql, checkpointId, opts?) => Promise + - class CheckpointNotFoundError + - interface CreateCheckpointArgs + - interface RestoreCheckpointResult + - _...1 more_ +- `apps/coder/src/services/claude-command-discovery.ts` — function discoverClaudeCommands: () => AgentCommand[] +- `apps/coder/src/services/command-availability.ts` — function isCommandAvailable: (binary) => Promise +- `apps/coder/src/services/correction-service.ts` + - function recordCorrection: (originalClaim, correction, principleExtracted, persistedTo, basePath?) => Promise + - function scanForCorrections: (auditPath) => Promise + - function checkContradiction: (action, corrections) => void + - function markPersisted: (correctionId, filePath, basePath?) => Promise + - function listCorrections: (basePath?) => Promise + - function appendCorrectionToTrail: (trailPath, correction) => Promise + - _...2 more_ +- `apps/coder/src/services/dcp-strip.ts` + - function stripDcpTags: (s) => string + - function makeDcpStreamStripper: () => DcpStreamStripper + - interface DcpStreamStripper +- `apps/coder/src/services/dispatcher.ts` — function createDispatcher: (deps) => void +- `apps/coder/src/services/edit-guards-imports.ts` — function checkDroppedImports: (original, updated, filePath) => ImportCheckResult, interface ImportCheckResult +- `apps/coder/src/services/edit-guards.ts` + - function validateEditResult: (original, updated, filePath) => GuardResult + - function formatGuardError: (guard, filePath) => string + - interface GuardResult +- `apps/coder/src/services/finalize-message.ts` + - function classifyTerminalStatus: (opts) => TerminalMessageStatus + - function finalizeStreamingMessage: (sql, publishFrame, frame) => void + - type TerminalMessageStatus +- `apps/coder/src/services/flow-artifacts.ts` — function getArtifactPath: (flowRunId, stepId) => string, function writeFlowArtifact: (flowRunId, stepId, content) => Promise +- `apps/coder/src/services/flow-runner-decisions.ts` + - function manifestSteps: (flow, launchCtx) => Step[] + - function readySteps: (flow, state) => Step[] + - function partitionReady: (ready, ctx) => void + - function isRunComplete: (flow, state) => boolean + - function isStuck: (flow, state) => boolean + - function reconcileResumeStep: (status, taskId, taskState) => ResumeAction + - _...5 more_ +- `apps/coder/src/services/flow-runner.ts` + - function createFlowRunner: (deps) => FlowRunner + - interface LaunchOpts + - interface FlowRunner +- `apps/coder/src/services/frame-emitter.ts` + - function makeFrameEmitter: (opts) => FrameEmitter + - interface FrameEmitterOpts + - interface FrameEmitter +- `apps/coder/src/services/fuzzy-match.ts` + - function locateMatch: (content, needle) => MatchResult + - type MatchResult + - const SIMILARITY_THRESHOLD + - const AMBIGUITY_EPSILON +- `apps/coder/src/services/guideline-service.ts` + - function createGuideline: (params, basePath?) => Promise + - function listGuidelines: (filter?, basePath?) => Promise + - function readGuideline: (id, basePath?) => Promise + - function updateGuideline: (id, params, basePath?) => Promise + - function deleteGuideline: (id, basePath?) => Promise + - function findGuideline: (content, basePath?) => Promise + - _...14 more_ +- `apps/coder/src/services/host-exec.ts` — function hostExec: (command, opts?) => Promise, interface HostExecResult +- `apps/coder/src/services/lsp/client.ts` — class LspClient +- `apps/coder/src/services/lsp/config.ts` — function getServerConfig: (filePath) => LspServerConfig | null, interface LspServerConfig +- `apps/coder/src/services/lsp/operations.ts` + - function openDocument: (client, filePath, content, version) => Promise + - function closeDocument: (client, filePath) => Promise + - function getDiagnostics: (client, filePath, content) => Promise + - function gotoDefinition: (client, filePath, content, line, character) => Promise + - function findReferences: (client, filePath, content, line, character) => Promise +- `apps/coder/src/services/lsp/server-manager.ts` — class LspServerManager, const lspManager +- `apps/coder/src/services/mcp-server.ts` — function startMcpServer: (sql) => Promise +- `apps/coder/src/services/net/port-utils.ts` + - function reclaimPort: (port) => void + - function waitForPortRelease: (port, timeoutMs) => Promise + - function freePort: () => Promise +- `apps/coder/src/services/orphan-worktree-reaper.ts` + - function reapOrphanWorktrees: (sql, log, graceMs, now) => void + - function createOrphanWorktreeReaper: (deps) => void + - interface OrphanWorktreeReaperDeps + - interface OrphanReaperResult +- `apps/coder/src/services/pending_changes.ts` + - function planEdit: (content, oldStr, newStr) => EditPlan + - function queueEdit: (sql, sessionId, taskId, filePath, oldString, newString, projectRoot, // v2.6 Phase 1-UX) => void + - function queueCreate: (sql, sessionId, taskId, filePath, content, projectRoot, // See queueEdit) => Promise + - function queueDelete: (sql, sessionId, taskId, filePath, projectRoot, // See queueEdit) => Promise + - function applyOne: (sql, changeId, projectRoot) => Promise + - function applyAll: (sql, sessionId, projectRoot) => Promise + - _...6 more_ +- `apps/coder/src/services/permission-waiter.ts` + - function setPermissionHooks: (next) => void + - function waitForPermissionResponse: (taskId, sessionId, provider, modeId, params, timeoutMs) => Promise + - function respondToPermission: (taskId, optionId, updatedInput?, unknown>) => boolean + - function getPendingPermission: (taskId) => PermissionPrompt | null + - function waitForElicitationResponse: (taskId, sessionId, provider, modeId, params, timeoutMs) => Promise + - function cancelPendingPermission: (taskId) => void + - _...3 more_ +- `apps/coder/src/services/provider-commands.ts` + - function getManifestCommands: (provider) => AgentCommand[] + - function mergeCommands: (...lists) => AgentCommand[] + - const PROVIDER_COMMANDS: Record +- `apps/coder/src/services/provider-config-registry.ts` + - function buildResolvedRegistry: (builtins, config) => Map + - function loadProviderConfig: (path) => Map + - function reloadProviderConfig: () => Map + - function getResolvedRegistry: () => Map + - interface ResolvedProviderDef +- `apps/coder/src/services/provider-config.ts` + - function mergeProviderConfigPatch: (current, patch) => CoderProvidersFile + - function load: (path) => CoderProvidersFile + - function save: (path, config) => void +- `apps/coder/src/services/provider-diagnostic.ts` — function getProviderDiagnostic: (resolved, agentRow, opts) => Promise, interface DiagnosticAgentRow +- `apps/coder/src/services/provider-manifest.ts` + - function getManifestModes: (provider) => ProviderMode[] + - function getManifestDefaultModeId: (provider) => string | null + - function isUnattendedMode: (provider, modeId) => boolean + - interface ProviderManifestEntry + - const PROVIDER_MANIFEST: Record +- `apps/coder/src/services/provider-snapshot.ts` + - function fetchLlamaSwapModels: (config) => Promise + - function prefixLlamaSwapModels: (models) => ProviderModel[] + - function mergeModels: (...lists) => ProviderModel[] + - function getProviderSnapshot: (sql, config, cwd?, force) => Promise + - function clearProviderSnapshotCache: () => void + - function peekSnapshotEntry: (name, cwd?) => ProviderSnapshotEntry | undefined + - _...1 more_ +- `apps/coder/src/services/pty-dispatch.ts` + - function dispatchViaPty: (opts) => Promise + - interface DispatchResult + - interface PtyDispatchOpts +- `apps/coder/src/services/qwen-settings.ts` — function readQwenSettingsModels: () => Promise +- `apps/coder/src/services/stream-json-parser.ts` + - function makeStreamJsonState: () => StreamJsonState + - function parseStreamJsonLine: (line, state) => AgentEvent[] + - function makeStreamJsonParser: () => StreamJsonParser + - interface StreamJsonUsage + - interface StreamJsonState + - interface StreamJsonParser + - _...1 more_ +- `apps/coder/src/services/token-analysis/analyzer.ts` — function analyzeMessages: (parts) => TokenBreakdown, interface TokenBreakdown +- `apps/coder/src/services/token-analysis/persist.ts` + - function persistTaskBreakdown: (sql, taskId, breakdown) => Promise + - function getTaskBreakdown: (sql, taskId) => Promise + - function analyzeAndPersistTaskBreakdown: (sql, taskId, parts) => Promise +- `apps/coder/src/services/tools/adapter.ts` — function adaptWriteTool: (tool) => ServerToolDef +- `apps/coder/src/services/tools/inference_context.ts` + - function runWithInferenceContext: (ctx, fn) => void + - function getInferenceContext: () => InferenceContext + - interface InferenceContext +- `apps/coder/src/services/tools/types.ts` + - function asPermissionMode: (id) => PermissionMode | undefined + - interface ToolJsonSchema + - interface ToolContext + - interface ToolDef + - type PermissionMode +- `apps/coder/src/services/tools/write-gate.ts` — function denyReadOnly: (operation) => unknown, function finalizeWrite: (context, projectRoot, change, queuedHint) => Promise +- `apps/coder/src/services/worktree-risk.ts` — function checkWorktreeWorkAtRisk: (worktreePath, opts?) => Promise, function stashWorktree: (worktreePath, opts?) => Promise< +- `apps/coder/src/services/worktrees.ts` + - function createWorktree: (projectPath, taskId, opts?) => Promise + - function diffWorktree: (worktreePath, projectPath, opts?) => Promise + - function cleanupWorktree: (projectPath, taskId) => Promise + - function ensureSessionWorktree: (sql, projectPath, sessionId, opts?) => Promise + - function removeSessionWorktree: (sql, projectPath, worktree, opts?) => Promise + - function closeChatBackendState: (sql, chatId, opts?) => Promise + - _...4 more_ +- `apps/coder/src/services/write_guard.ts` + - function isSecretPath: (filePath) => boolean + - function resolveWritePath: (projectRoot, filePath) => string + - class WriteGuardError +- `apps/server/src/config.ts` — function loadConfig: () => Config, type Config +- `apps/server/src/db.ts` + - function getSql: (config) => Sql + - function applySchema: (sql) => Promise + - function pingDb: (sql) => Promise + - function closeDb: () => Promise + - type Sql +- `apps/server/src/services/agents.ts` + - function refreshToolNames: () => void + - function matchToolGlob: (toolName, patterns) => boolean + - function slugify: (name) => string + - function parseAgentsMd: (content) => ParseResult + - function isAgentRegistryMarkdown: (content) => boolean + - function getAgentsMtimes: (projectPath) => void + - _...2 more_ +- `apps/server/src/services/artifacts.ts` + - function deriveMarkdownSlug: (messageContent) => string + - function deriveHtmlSlug: (payload) => string + - function deriveHtmlTitle: (html) => string | null + - function detectHtmlArtifact: (text) => string | null + - function decideHtmlArtifactWrite: (htmlContent) => HtmlArtifactDecision + - function writeMarkdownArtifact: (message, 'content'>, ctx) => Promise + - _...6 more_ +- `apps/server/src/services/audit/corrections.ts` + - function createCorrection: (params) => UserCorrectionRecord + - function findCorrections: (records, unknown>[]) => UserCorrectionRecord[] + - function checkCorrectionConflict: (proposedAction, corrections) => UserCorrectionRecord | null + - interface UserCorrectionRecord +- `apps/server/src/services/audit/guideline-store.ts` + - class GuidelineDocumentStore + - interface GuidelineContent + - interface Guideline + - interface GuidelineDocument + - interface GuidelineUpdateParams + - type GuidelineId + - _...3 more_ +- `apps/server/src/services/audit/journey-projection.ts` + - function projectJourneyToGuidelines: (journey, nodes, edges) => ProjectedGuideline[] + - function detectJourneyBacktrack: (journey, nodes, edges, currentNodeId, previousNodeId) => BacktrackCheck + - interface ProjectedGuideline + - interface BacktrackCheck +- `apps/server/src/services/audit/journey-store.ts` + - class JourneyStore + - interface JourneyNode + - interface JourneyEdge + - interface Journey + - type JourneyId + - type JourneyNodeId + - _...1 more_ +- `apps/server/src/services/audit/runs-dir.ts` + - function findRunsDir: (projectRoot?) => string + - function ensureRunsDir: (projectRoot?) => string + - function readCurrentSession: (projectRoot?) => string | null + - function writeCurrentSession: (sessionId, projectRoot?) => void + - function clearCurrentSession: (projectRoot?) => void + - function readIndex: (projectRoot?) => IndexFile + - _...7 more_ +- `apps/server/src/services/audit/session-manager.ts` + - function generateSessionId: () => string + - function isoNow: () => string + - function createSession: (task, sessionId?, projectRoot?) => string + - function getSessionDir: (sessionId, projectRoot?) => string + - function getActiveSession: (projectRoot?) => SessionJson | null + - function readSession: (sessionId, projectRoot?) => SessionJson | null + - _...9 more_ +- `apps/server/src/services/auto_name.ts` — function maybeAutoNameChat: (ctx, chatId, sessionId) => Promise +- `apps/server/src/services/broker.ts` + - function createBroker: (log?) => Broker + - interface Broker + - type Frame + - type Listener +- `apps/server/src/services/codecontext_client.ts` + - function callCodecontext: (req, fetcher) => Promise + - interface CodecontextRequest + - interface CodecontextResponse +- `apps/server/src/services/coder-notify.ts` — function notifyCoderClose: (kind, id, log?, 'debug'>, fetcher) => Promise, type CoderCloseKind +- `apps/server/src/services/compaction.ts` + - function usable: (contextLimit) => number + - function isOverflow: (usage, contextLimit) => boolean + - function estimate: (messages) => number + - function turns: (messages) => Turn[] + - function select: (messages, contextLimit, tailTurns) => SelectResult + - function deriveFilesRead: (head) => string[] + - _...8 more_ +- `apps/server/src/services/file_index.ts` — function getProjectFiles: (projectId, projectRoot) => Promise +- `apps/server/src/services/file_ops.ts` + - function listDir: (projectRoot, relPath, opts?) => Promise + - function viewFile: (projectRoot, relPath, opts?) => Promise + - function grep: (projectRoot, pattern, opts?) => Promise + - function findFiles: (projectRoot, pattern?, opts?) => Promise + - interface FileEntry + - interface ListDirResult + - _...4 more_ +- `apps/server/src/services/git_diff.ts` + - function parseNameStatus: (output) => void + - function parseNumStatLine: (line) => void + - function splitDiffByFile: (diffText) => Map + - function classifyDiffBody: (body, cap) => 'diff' | 'binary' | 'too_large' + - function autoSelectMode: (isDirty) => GitDiffMode + - function canCommit: (files) => boolean + - _...17 more_ +- `apps/server/src/services/git_meta.ts` — function getGitMeta: (rootPath) => Promise, interface GitMeta +- `apps/server/src/services/gitea.ts` + - function createGiteaRepo: (cfg, name, options) => Promise + - class GiteaRepoExistsError + - interface GiteaConfig + - interface GiteaRepo +- `apps/server/src/services/grant_resolver.ts` — function resolveGrantRoot: (sql, requestedPath, projectRoot, whitelistRoot) => Promise, type GrantResolution +- `apps/server/src/services/inference/budget.ts` — function resolveToolBudget: (agent) => number +- `apps/server/src/services/inference/content-flusher.ts` — function createContentFlusher: (sql, messageId, getContent) => void, interface ContentFlusher +- `apps/server/src/services/inference/dcp/messages.ts` + - function toDcpMessages: (parts) => DcpMessage[] + - function fromDcpMessages: (msgs) => any[] + - interface DcpMessage +- `apps/server/src/services/inference/dcp/state.ts` + - function getDcpState: (chatId) => ChatDcpState | undefined + - function setDcpState: (chatId, messageCount) => void + - function clearDcpState: (chatId) => void + - function shouldTransform: (chatId, messageCount) => boolean +- `apps/server/src/services/inference/dcp/strategies/deduplication.ts` — function deduplicate: (messages) => void +- `apps/server/src/services/inference/dcp/strategies/purge-errors.ts` — function purgeErrors: (messages, windowSize) => void +- `apps/server/src/services/inference/dcp/transform.ts` + - function transformMessages: (chatId, messages) => TransformResult + - interface TransformStats + - interface TransformResult +- `apps/server/src/services/inference/error-handler.ts` + - function handleAbortOrError: (ctx, args, accumulated, err) => Promise + - function finalizeStreamedRow: (ctx, opts) => void + - function finalizeEmpty: (ctx, args) => Promise + - function finalizeCompletion: (ctx, args, result, startedAt, session) => Promise +- `apps/server/src/services/inference/llama-args-validator.ts` + - function validateExtraArgs: (args?) => string[] + - function isManagedFlag: (flag) => boolean + - function stripShadowingFlags: (args, opts?) => string[] + - interface StripOptions +- `apps/server/src/services/inference/loop-detectors.ts` + - function detectContentRepeat: (messages) => LoopDetectionResult + - function detectToolLoop: (toolNames) => LoopDetectionResult + - function detectDoomLoop: (messages, toolNames) => LoopDetectionResult + - interface LoopDetectionResult +- `apps/server/src/services/inference/mistake-tracker.ts` + - function freshMistakeState: () => MistakeState + - function recordStep: (state, outcome) => void + - function detectMistakePattern: (state) => 'nudge' | 'escalate' | null + - interface MistakeState + - type FailureKind + - const MISTAKE_THRESHOLD + - _...1 more_ +- `apps/server/src/services/inference/parts.ts` + - function insertParts: (sql, parts) => Promise + - function partsFromAssistantMessage: (args) => void + - function partsFromToolMessage: (args) => Omit[] + - interface PartInsert + - type PartKind +- `apps/server/src/services/inference/payload.ts` + - function buildMessagesPayload: (session, project, history, agent, log?) => Promise + - function loadContext: (sql, sessionId, chatId) => Promise< + - function maybeFlagForCompaction: (ctx, chatId, updated) => Promise + - interface OpenAiMessage +- `apps/server/src/services/inference/provider.ts` + - function resolveRoute: (agent, config?) => RoutingInfo + - function upstreamModel: (config, modelId, agent?) => LanguageModel + - interface RoutingInfo + - type InferenceRoute +- `apps/server/src/services/inference/prune.ts` + - function selectPruneTargets: (partsNewestFirst, tailStartCreatedAt) => void + - function prune: (args) => Promise + - interface PruneResult + - interface PartForPrune + - const PROTECTED_TOKENS + - const PRUNE_TRIGGER_TOKENS +- `apps/server/src/services/inference/sentinel-summaries.ts` + - function runCapHitSummary: (ctx, args, session, project, history, agent, budget) => Promise + - function runDoomLoopSummary: (ctx, args, session, project, history, agent, loop, unknown> }) => Promise + - function runStepCapSummary: (ctx, args, session, project, history, agent, steps, cap) => Promise + - function insertMistakeRecoverySentinel: (ctx, sessionId, chatId, opts) => Promise +- `apps/server/src/services/inference/sentinels.ts` + - function detectDoomLoop: (recentToolCalls) => void + - function isCapHitSentinel: (m) => boolean + - function isDoomLoopSentinel: (m) => boolean + - function isMistakeRecoverySentinel: (m) => boolean + - function isAnySentinel: (m) => boolean + - const DOOM_LOOP_THRESHOLD + - _...1 more_ +- `apps/server/src/services/inference/step-decision.ts` + - function decideStep: (input) => PreStepDecision + - function decidePostToolAction: (action, mistakeTracker) => PostToolDecision + - type PreStepDecision + - type PostToolDecision +- `apps/server/src/services/inference/stream-error-classifier.ts` — function classifyStreamError: (err) => StreamErrorKind, type StreamErrorKind +- `apps/server/src/services/inference/stream-phase-adapter.ts` + - function samplerOptsFromAgent: (agent) => SamplerOpts + - function streamCompletion: (ctx, model, messages, opts, onDelta) => void + - interface StreamAdapterContext + - interface StreamOptions + - type SamplerOpts + - const STALL_TIMEOUT_MS +- `apps/server/src/services/inference/stream-phase.ts` — function executeStreamPhase: (ctx, args, session, messages, state, agent, // v1.11.8, web_search and web_fetch are stripped from the + // tool list sent to the LLM, so the model can't even attempt them. + webToolsEnabled) => Promise +- `apps/server/src/services/inference/tool-call-parser.ts` + - function stripToolMarkup: (text, opts?) => string + - function extractToolCallBlocks: (buffer, log?) => ToolCallExtraction + - interface ParsedCall + - interface ToolCallExtraction +- `apps/server/src/services/inference/tool-phase.ts` — function executeToolPhase: (ctx, args, result, startedAt, session, projectRoot, agent?) => Promise, interface ToolPhaseResult +- `apps/server/src/services/inference/tool-shim.ts` + - function extractToolCalls: (text) => ParsedToolCall[] + - function hasToolCallMarkup: (text) => boolean + - interface ParsedToolCall +- `apps/server/src/services/inference/tool-suggestions.ts` + - function levenshtein: (a, b) => number + - function suggestToolName: (name, available) => string | null + - function formatUnknownToolError: (name, available) => string +- `apps/server/src/services/inference/turn-config.ts` + - function resolveTurnConfig: (agent) => TurnConfig + - interface TurnConfig + - const MAX_STEPS +- `apps/server/src/services/inference/turn.ts` + - function runAssistantTurn: (ctx, args) => Promise + - function runInference: (ctx, sessionId, chatId, assistantMessageId, signal?) => Promise + - function createInferenceRunner: (ctx, 'publishUser'>, publishUserFn, frame) => void +- `apps/server/src/services/mcp-client.ts` + - function initialize: (entries, logger) => Promise + - function callTool: (prefixedName, args, unknown>) => Promise + - function getTools: () => ToolDef>[] + - function getMcpServers: () => Array< + - function shutdown: () => Promise + - function wrapMcpTool: (serverName, mcpTool) => ToolDef> + - _...2 more_ +- `apps/server/src/services/mcp-config.ts` + - function substituteEnvVars: (value, log, unsetVars?) => unknown + - function loadMcpConfig: (configPath, log) => McpServerEntry[] + - interface McpServerEntry + - type McpServerConfig +- `apps/server/src/services/memory/entries.ts` — function parseMemoryEntries: (fileName, markdown) => MemoryEntry[], interface MemoryEntry +- `apps/server/src/services/memory/paths.ts` + - function getMemoryRoot: (projectRoot) => string + - function getTopicDir: (root, topic) => string + - function ensureMemoryScaffold: (root) => Promise + - type MemoryTopic +- `apps/server/src/services/memory/prompt.ts` — function formatMemoryBlock: (entries) => string +- `apps/server/src/services/memory/recall.ts` — function rankByRelevance: (query, entries) => MemoryEntry[], function loadMemoryForSession: (projectRoot, _sessionId?, query?) => Promise +- `apps/server/src/services/memory/scan.ts` + - function scanMemoryScopes: (scope) => Promise + - function scanProjectMemory: (projectRoot) => Promise + - interface MemoryScope +- `apps/server/src/services/memory/store.ts` — function readTopicFiles: (root, topic) => Promise>, function writeEntry: (root, topic, title, content, tags) => Promise +- `apps/server/src/services/model-context.ts` + - function configureModelContext: (opts) => void + - function getModelContext: (model) => Promise + - function invalidateModelContext: (model?) => void + - interface ModelContext +- `apps/server/src/services/path_guard.ts` + - function resolveProjectRoot: (projectPath) => Promise + - function pathGuard: (projectRoot, requested, extraRoots) => Promise + - class PathScopeError +- `apps/server/src/services/project_bootstrap.ts` + - function sanitizeFolderName: (raw) => string + - function bootstrapProject: (config, log, options) => Promise + - class BootstrapNameError + - class BootstrapCollisionError + - class BootstrapPathError + - interface BootstrapResult +- `apps/server/src/services/read_tab_by_number.ts` + - function executeReadTabByNumber: (input, sql, sessionId) => Promise + - type ReadTabByNumberInputT + - const readTabByNumber: ToolDef +- `apps/server/src/services/secret_guard.ts` + - function isSecretPath: (relPath) => boolean + - function filterSecretEntries: (entries, pathOf) => void + - class SecretBlockedError + - const DEFAULT_SECURITY_IGNORE_FILETYPES: ReadonlyArray +- `apps/server/src/services/skill-invoke.ts` + - function runSkillInvokeTransaction: (sql, args) => Promise< + - function buildSkillInvokeSyntheticFrames: (chatId, result, toolCall, skillBody) => SkillInvokeSessionFrame[] + - function buildSkillInvokeUserFrames: (chatId, userMessageId, userText) => SkillInvokeSessionFrame[] + - interface SkillInvokeTransactionResult + - interface SkillInvokeToolCall + - type SkillInvokeSessionFrame + - _...1 more_ +- `apps/server/src/services/skills.ts` + - function listSkills: () => Promise + - function findSkills: (query) => Promise + - function getSkillBody: (name) => Promise + - function getSkillResource: (name, relativePath) => Promise + - interface Skill + - interface SkillSummary + - _...2 more_ +- `apps/server/src/services/synthesisPipeline.ts` + - function runSynthesisPass: (p) => Promise + - interface SynthesisParams + - const SYNTHESIS_TOOLS: ReadonlySet +- `apps/server/src/services/system-prompt.ts` + - function loadContainerGuidance: () => Promise + - function getContainerGuidance: () => Promise + - function _resetContainerGuidanceCacheForTests: () => void + - function _resetPrefixObserverForTests: () => void + - function buildSystemPromptWithFingerprint: (project, session, agent) => Promise< + - function buildSystemPrompt: (project, session, agent) => Promise + - _...2 more_ +- `apps/server/src/services/task-model.ts` — function taskModelCompletion: (opts) => Promise +- `apps/server/src/services/task-search-rewrite.ts` — function rewriteSearchQuery: (userMessage) => Promise +- `apps/server/src/services/tools/codecontext/factory.ts` — function makeCodecontextTool: (opts, unknown>; + mapArgs) => void +- `apps/server/src/services/tools/registry.ts` — function appendMcpTools: (mcpTools) => void, function toolJsonSchemas: () => ToolJsonSchema[] +- `apps/server/src/services/tools/tiers.ts` + - function resolveToolTier: (tier) => readonly string[] + - const CORE_TOOL_NAMES + - const STANDARD_TOOL_NAMES +- `apps/server/src/services/truncate.ts` + - function storeTruncation: (fullContent) => Promise + - function readTruncation: (id) => Promise + - function truncateIfNeeded: (args) => Promise< + - function cleanupTruncations: (args, msg) => void + - const TRUNCATION_DIR + - const TRUNCATION_TTL_MS + - _...1 more_ +- `apps/server/src/services/url_guard.ts` — function isPublicUrl: (input) => UrlGuardResult, interface UrlGuardResult +- `apps/server/src/services/web/html-to-md.ts` — function htmlToMarkdown: (sourceHtml) => string +- `apps/server/src/services/web_fetch.ts` + - function executeWebFetch: (input, fetcher) => Promise + - type WebFetchInputT + - type WebFetchOutput + - const webFetch: ToolDef +- `apps/server/src/services/web_search.ts` + - function executeWebSearch: (input, searxngUrl, fetcher) => Promise + - interface WebSearchOutput + - type WebSearchInputT + - const webSearch: ToolDef +- `apps/server/src/utils/string-utils.ts` — function stripQuotes: (s) => string +- `apps/web/src/api/client.ts` + - class ApiError + - interface AgentSessionInfo + - interface CoderCheckpoint + - interface CoderRestoreResult + - const api +- `apps/web/src/data/acp-provider-catalog.ts` + - function buildAcpProviderConfigPatch: (entry) => ProviderConfigPatch + - interface AcpCatalogEntry + - const ACP_PROVIDER_CATALOG: AcpCatalogEntry[] +- `apps/web/src/hooks/terminal/useTerminalFit.ts` + - function cellSize: (term, container) => void + - function useTerminalFit: ({...}, containerRef, sessionId, paneId }) => TerminalFit + - interface TerminalFit +- `apps/web/src/hooks/terminal/useTerminalSelection.ts` + - function useTerminalSelection: ({...}, containerRef, sessionId, paneId, label, send, }) => TerminalSelection + - interface TerminalSelectionActions + - interface TerminalSelection +- `apps/web/src/hooks/terminal/useTerminalSocket.ts` + - function useTerminalSocket: ({...}, sessionId, paneId, fit, getSize, setSize, }) => TerminalSocket + - interface TerminalSocket + - type ConnState +- `apps/web/src/hooks/useActivePane.ts` + - function setActivePaneInfo: (next) => void + - function clearActivePane: () => void + - function useActivePane: () => ActivePaneSnapshot + - interface ActivePaneSnapshot +- `apps/web/src/hooks/useAgentSessions.ts` — function refreshAgentSessions: (sessionId) => Promise, function useAgentSessions: (sessionId) => void +- `apps/web/src/hooks/useAgentStatus.ts` + - function useAgentStatus: () => void + - interface AgentStatusEntry + - type AgentStatus +- `apps/web/src/hooks/useArtifactDownload.ts` — function useArtifactDownload: (chatId, messageId, format) => void +- `apps/web/src/hooks/useChatStatus.ts` + - function useChatStatus: (chatId) => DerivedStatus + - type RawStatus + - type DerivedStatus +- `apps/web/src/hooks/useChatThroughput.ts` + - function recordUsage: (chatId, data) => void + - function useChatThroughput: (chatId) => ThroughputSample | null + - interface ThroughputSample +- `apps/web/src/hooks/useCoderUserEvents.ts` — function useCoderUserEvents: () => void +- `apps/web/src/hooks/useDiffPreferences.ts` — function useDiffPreferences: () => void, interface DiffPreferences +- `apps/web/src/hooks/useGitDiff.ts` — function useGitDiff: (projectId) => void +- `apps/web/src/hooks/useLongPress.ts` — function useLongPress: (callback) => void +- `apps/web/src/hooks/useProjectGit.ts` — function useProjectGit: (projectId) => GitMeta | null +- `apps/web/src/hooks/useProviderSnapshot.ts` — function refreshProviderSnapshot: (cwd?) => Promise, function useProviderSnapshot: (cwd?) => ProviderSnapshotEntry[] | null +- `apps/web/src/hooks/usePullToRefresh.ts` — function usePullToRefresh: (onRefresh) => void +- `apps/web/src/hooks/useSessionChats.ts` + - function useSessionChats: (sessionId, opts) => UseSessionChatsResult + - interface UseSessionChatsOpts + - interface UseSessionChatsResult +- `apps/web/src/hooks/useSessionStream.ts` — function useSessionStream: (sessionId) => void +- `apps/web/src/hooks/useSessions.ts` — function useSessions: (projectId) => void +- `apps/web/src/hooks/useSidebar.ts` — function useSidebar: () => void +- `apps/web/src/hooks/useSkills.ts` — function useSkills: () => void +- `apps/web/src/hooks/useUserEvents.ts` — function useUserEvents: () => void +- `apps/web/src/hooks/useViewport.ts` — function useViewport: () => ViewportSnapshot, interface ViewportSnapshot +- `apps/web/src/hooks/useWorkspacePanes.ts` + - function activePaneChatId: (pane) => string | undefined + - function useWorkspacePanes: (sessionId) => UseWorkspacePanesResult + - interface UseWorkspacePanesResult + - const MAX_PANES +- `apps/web/src/hooks/wsReconnectToast.ts` — function createWsReconnectToast: (opts) => WsReconnectToast, interface WsReconnectToast +- `apps/web/src/lib/anim.ts` + - function getAnimBg: () => boolean + - function setAnimBg: (on) => void + - function setAnimDensity: (v) => void + - function setAnimSpeed: (v) => void + - function setAnimOpacity: (v) => void + - function useAnimBg: () => boolean + - _...3 more_ +- `apps/web/src/lib/attachments.ts` + - function looksBinary: (content) => boolean + - function inferLanguage: (filename) => string | null + - function flattenToMessage: (attachments, text) => string + - type Attachment + - const MAX_FILE_SIZE_BYTES + - const PASTE_INLINE_MAX_LINES + - _...1 more_ +- `apps/web/src/lib/coder-session.ts` — function isCoderSessionName: (name) => boolean +- `apps/web/src/lib/coder-tools.ts` + - function wireToolCallToRun: (wire) => ToolRun + - function mergeWireToolCall: (existing, incoming, unknown> }) => CoderToolCallWire[] + - interface AcpWireMeta + - interface CoderToolCallWire +- `apps/web/src/lib/format.ts` + - function relTime: (iso) => string + - function formatRelative: (iso) => string + - function formatAgo: (iso) => string +- `apps/web/src/lib/model-label.ts` — function formatModelLabel: (raw) => string +- `apps/web/src/lib/modelName.ts` — function shortenModelName: (model) => string | null +- `apps/web/src/lib/permission-mode.ts` + - function nativeModeForPermission: (mode, modes, defaultModeId) => string | null + - function permissionForModeId: (modeId, modes) => PermissionMode + - function availablePermissionModes: (modes) => Array< + - type PermissionMode + - const PERMISSION_LABELS: Record +- `apps/web/src/lib/projectUrls.ts` — function giteaUrlFor: (project) => string +- `apps/web/src/lib/slash-command.ts` + - function isSlashCommandToken: (value) => boolean + - function slashQuery: (value) => string + - function parseSlashInput: (text) => void + - function mergeCommandsByName: (...lists) => T[] + - interface SlashCommandItem +- `apps/web/src/lib/terminal-protocol.ts` + - function encodeInput: (text) => Uint8Array + - function encodeResize: (cols, rows) => string + - function parseServerFrame: (data) => ServerControlFrame | null + - type ServerControlFrame +- `apps/web/src/lib/theme.ts` + - function isThemeId: (s) => s is ThemeId + - function applyTheme: (id, mode) => void + - function setTheme: (id, mode) => Promise + - function useTheme: () => ThemeState + - interface ThemeMeta + - type ThemeId + - _...5 more_ +- `apps/web/src/lib/utils.ts` — function cn: (...inputs) => void +- `apps/web/src/utils/diff-layout.ts` + - function parseDiff: (diffBody) => ParsedDiffFile[] + - function buildSplitRows: (file) => SplitRow[] + - function reconstructNewContent: (hunks) => string + - interface DiffLine + - interface DiffHunk + - interface ParsedDiffFile + - _...3 more_ +- `conductor/src/contracts.ts` + - function produceContract: (contracts) => string + - function reviewContract: (contracts) => string + - type Contract + - const EVIDENCE_PRODUCE + - const EVIDENCE_REVIEW + - const YAGNI_PRODUCE + - _...1 more_ +- `conductor/src/dispatch.ts` + - function loadPersona: (agent) => Promise + - function dispatchAgent: (agent, task, opts) => Promise + - function cleanOutput: (raw) => string +- `conductor/src/flow.ts` — function runFlow: (flow, input, opts) => Promise, interface RunOptions +- `conductor/src/flows/_util.ts` — function q, function repoLine +- `conductor/src/flows/index.ts` + - function describeFlows: () => string + - function getFlow: (name) => Flow | undefined + - const FLOWS: Record + - const FLOW_NAMES: string[] +- `conductor/src/render.ts` — function slugify: (s) => string +- `conductor/src/spine.ts` + - function readBand: (input) => Band + - function fastNote: (ctx) => string + - function buildSpineFlow: (spine) => Flow +- `data/skills/superpowers/systematic-debugging/condition-based-waiting-example.ts` + - function waitForEvent: (threadManager, threadId, eventType, timeoutMs) => Promise + - function waitForEventCount: (threadManager, threadId, eventType, count, timeoutMs) => Promise + - function waitForEventMatch: (threadManager, threadId, predicate) => void +- `packages/ion/src/cli/commands/abandon.ts` — function abandonCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/approve.ts` — function approveCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/cleanup.ts` — function cleanupCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/convert.ts` — function convertCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/list.ts` — function listCommand: (_args, options) => Promise +- `packages/ion/src/cli/commands/reject.ts` — function rejectCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/resume.ts` — function resumeCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/run.ts` — function runCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/runs.ts` — function runsCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/status.ts` — function statusCommand: (_args, options) => Promise +- `packages/ion/src/cli/commands/validate.ts` — function validateCommand: (args, options) => Promise +- `packages/ion/src/cli/index.ts` — function main: (argv) => void +- `packages/ion/src/cli/utils.ts` + - function formatDuration: (ms) => string + - function formatTimestamp: (date) => string + - function truncate: (str, max) => string + - function printTable: (rows, unknown>[], columns) => void + - function printJson: (data) => void + - function parseArgs: (argv) => void + - _...3 more_ +- `packages/ion/src/engine/command-validation.ts` — function isValidCommandName: (name) => boolean +- `packages/ion/src/engine/condition-evaluator.ts` — function evaluateCondition: (expression, nodeOutputs, Record>) => boolean, class ConditionError +- `packages/ion/src/engine/dag-executor.ts` + - function buildTopologicalLayers: (nodes) => DagNode[][] + - function checkTriggerRule: (node, nodeOutputs, NodeOutput>) => 'run' | 'skip' + - function executeNodeInternal: (node, deps, platform, conversationId, cwd, config, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - function executeScriptNode: (node, cwd, envVars, string>, artifactsDir) => Promise + - function handleApprovalNode: (node, deps, platform, conversationId, workflowRunId, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - function handleLoopNode: (node, deps, platform, conversationId, cwd, config, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - _...2 more_ +- `packages/ion/src/engine/event-emitter.ts` + - function getWorkflowEventEmitter: () => WorkflowEventEmitter + - class WorkflowEventEmitter + - interface WorkflowEventBase + - interface WorkflowStartedEvent + - interface WorkflowCompletedEvent + - interface WorkflowFailedEvent + - _...11 more_ +- `packages/ion/src/engine/executor-shared.ts` + - function substituteWorkflowVariables: (template, context) => string + - function buildPromptWithContext: (template, context, issueContext?) => string + - function classifyError: (error) => ErrorClassification + - function safeSendMessage: (platform, conversationId, message, metadata?, unknown>) => Promise + - function detectCompletionSignal: (output, until) => boolean + - function stripCompletionTags: (output, until) => string + - _...5 more_ +- `packages/ion/src/engine/executor.ts` + - function executeWorkflow: (deps, platform, conversationId, cwd, workflow, userMessage, opts) => Promise + - function hydrateResumableRun: (deps, candidate) => Promise + - function resolveProjectPaths: (_deps, cwd, workflowRunId, codebaseId?) => ProjectPaths + - interface WorkflowExecutionOptions + - interface WorkflowExecutionResult + - interface HydratedResumableRun + - _...1 more_ +- `packages/ion/src/engine/model-validation.ts` + - function isLiteralSpec: (spec) => spec is LiteralModelSpec + - function buildAiProfile: (opts) => AiProfile + - function resolveModelSpec: (profile, modelRef) => LiteralModelSpec + - interface LiteralModelSpec + - interface ModelAliasPreset + - interface AiProfileTiers + - _...2 more_ +- `packages/ion/src/engine/output-ref.ts` + - function declaredFieldsFromSchema: (outputFormat, unknown> | string | undefined) => Set + - function resolveNodeOutputField: (nodeOutput, unknown>, nodeId, field, declaredFields?) => OutputRefResult + - class OutputRefError + - interface OutputRefResult + - type OutputRefKind +- `packages/ion/src/engine/utils.ts` + - function substituteWorkflowVariables: (template, variables, unknown>) => string + - function substituteNodeOutputRefs: (prompt, nodeOutputs, NodeOutput>, escapedForBash) => string + - function resolveNodeOutputField: (output, field) => string + - function buildPromptWithContext: (prompt, variables, unknown>, nodeOutputs, NodeOutput>, escapedForBash) => string + - function evaluateCondition: (condition, variables, unknown>) => boolean + - function classifyError: (error) => ErrorCategory + - _...10 more_ +- `packages/ion/src/format/sop-discovery.ts` — function discoverSopFiles: (cwd, globFn) => Promise, type GlobFn +- `packages/ion/src/format/sop-parser.ts` + - function parseSopContent: (markdown) => SopDocument + - interface SopParameter + - interface SopStep + - interface SopDocument +- `packages/ion/src/format/sop-to-yaml.ts` — function convertSopToWorkflowYaml: (sop) => string +- `packages/ion/src/schema/dag-node.ts` + - function isBashNode: (node) => node is BashNode + - function isScriptNode: (node) => node is ScriptNode + - function isLoopNode: (node) => node is LoopNode + - function isApprovalNode: (node) => node is ApprovalNode + - function isCancelNode: (node) => node is CancelNode + - function isPromptNode: (node) => node is PromptNode + - _...27 more_ +- `packages/ion/src/store/fs-store.ts` — function createFsStore: (basePath) => IWorkflowStore +- `packages/ion/src/store/pg-store.ts` — function createPostgresStore: (connectionString) => Promise +- `packages/ion/src/store/sqlite-store.ts` — function createSqliteStore: (dbPath) => Promise + +--- + +# Config + +## Environment Variables + +- `AUDIT_DOT_DIR` **required** — apps/server/src/services/audit/runs-dir.ts +- `BOOCODE_DATA_DIR` **required** — apps/server/src/routes/inference-settings.ts +- `BOOCODE_TOOLS` **required** — apps/server/src/services/agents.ts +- `BOOCODE_TRUNCATION_DIR` **required** — apps/server/src/services/__tests__/truncate.test.ts +- `BOOCODER_DEV_URL` **required** — apps/web/vite.config.ts +- `BOOCODER_URL` **required** — apps/coder/src/cli.ts +- `BOOTERM_DEV_URL` **required** — apps/web/vite.config.ts +- `BOOTERM_SSH_HOST` **required** — apps/booterm/src/pty/manager.ts +- `BOOTERM_SSH_USER` **required** — apps/booterm/src/pty/manager.ts +- `BOOTSTRAP_ROOT` (has default) — .env.example +- `BRAINSTORM_DIR` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_HOST` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_OWNER_PID` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_PORT` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_URL_HOST` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `CODECONTEXT_CHILD` **required** — codecontext/shim.go +- `CODECONTEXT_URL` **required** — apps/server/src/services/codecontext_client.ts +- `CONDUCTOR_MODEL` **required** — conductor/src/dispatch.ts +- `CONDUCTOR_OPENCODE_BIN` **required** — conductor/src/dispatch.ts +- `CONDUCTOR_TIMEOUT_MS` **required** — conductor/src/dispatch.ts +- `CONTAINER_GUIDANCE_FILE` **required** — apps/server/src/services/__tests__/system-prompt.test.ts +- `CONTEXT7_API_KEY` (has default) — .env +- `DATABASE_URL` (has default) — .env.example +- `DEFAULT_MODEL` (has default) — .env.example +- `DEV_REMOTE_USER` **required** — apps/web/vite.config.ts +- `GITEA_BASE_URL` (has default) — .env +- `GITEA_SSH_HOST` (has default) — .env +- `GITEA_TOKEN` (has default) — .env +- `GITEA_USER` (has default) — .env +- `LLAMA_SWAP_URL` (has default) — .env.example +- `MCP_TEST_MISSING` **required** — apps/server/src/services/__tests__/mcp-config.test.ts +- `MCP_TEST_SECRET` **required** — apps/server/src/services/__tests__/mcp-config.test.ts +- `NODE_ENV` (has default) — .env.example +- `PORT` (has default) — .env.example +- `POSTGRES_PASSWORD` (has default) — .env.example +- `PROJECT_ROOT_WHITELIST` (has default) — .env.example +- `SEARXNG_URL` (has default) — .env.example +- `SKILLS_ROOT` **required** — apps/server/src/services/skills.ts +- `WEB_DIST_PATH` **required** — apps/server/src/index.ts + +## Config Files + +- `.env.example` +- `Dockerfile` +- `apps/web/vite.config.ts` +- `docker-compose.yml` + +--- + +# Middleware + +## auth +- auth — `apps/booterm/src/auth.ts` +- authoring — `apps/coder/src/conductor/flows/authoring.ts` +- turn-guard.test — `apps/coder/src/services/backends/__tests__/turn-guard.test.ts` +- turn-guard — `apps/coder/src/services/backends/turn-guard.ts` +- get_middleware — `apps/server/src/services/tools/codecontext/get_middleware.ts` +- authoring — `conductor/src/flows/authoring.ts` + +## custom +- write_guard.test — `apps/coder/src/services/__tests__/write_guard.test.ts` +- write_guard_fuzz.test — `apps/coder/src/services/__tests__/write_guard_fuzz.test.ts` +- edit-guards-imports — `apps/coder/src/services/edit-guards-imports.ts` +- write_guard — `apps/coder/src/services/write_guard.ts` +- secret_guard.test — `apps/server/src/services/__tests__/secret_guard.test.ts` +- path_guard — `apps/server/src/services/path_guard.ts` +- secret_guard — `apps/server/src/services/secret_guard.ts` +- url_guard — `apps/server/src/services/url_guard.ts` + +## validation +- edit-guards — `apps/coder/src/services/edit-guards.ts` +- path_guard.test — `apps/server/src/services/__tests__/path_guard.test.ts` + +--- + +# Dependency Graph + +## Most Imported Files (change these carefully) + +- `apps/coder/src/db.ts` — imported by **40** files +- `apps/server/src/types/api.ts` — imported by **28** files +- `apps/server/src/db.ts` — imported by **25** files +- `packages/ion/src/cli/utils.ts` — imported by **24** files +- `apps/coder/src/services/tools/types.ts` — imported by **18** files +- `apps/coder/src/conductor/types.ts` — imported by **14** files +- `apps/coder/src/services/agent-backend.ts` — imported by **14** files +- `apps/coder/src/services/acp-tool-snapshot.ts` — imported by **14** files +- `apps/server/src/services/tools/codecontext/factory.ts` — imported by **14** files +- `apps/server/src/services/tools.ts` — imported by **13** files +- `conductor/src/types.ts` — imported by **13** files +- `apps/coder/src/services/provider-config-registry.ts` — imported by **12** files +- `apps/server/src/config.ts` — imported by **12** files +- `apps/coder/src/config.ts` — imported by **11** files +- `apps/coder/src/services/provider-types.ts` — imported by **11** files +- `apps/server/src/services/agents.ts` — imported by **10** files +- `apps/coder/src/services/pending_changes.ts` — imported by **9** files +- `apps/server/src/services/broker.ts` — imported by **9** files +- `apps/server/src/services/path_guard.ts` — imported by **9** files +- `apps/server/src/services/inference/payload.ts` — imported by **9** files + +## Import Map (who imports what) + +- `apps/coder/src/db.ts` ← `apps/coder/src/index.ts`, `apps/coder/src/routes/__tests__/agent-sessions.routes.test.ts`, `apps/coder/src/routes/__tests__/chat-resolve.test.ts`, `apps/coder/src/routes/__tests__/providers.routes.test.ts`, `apps/coder/src/routes/agent-sessions.ts` +35 more +- `apps/server/src/types/api.ts` ← `apps/server/src/routes/chats.ts`, `apps/server/src/routes/messages.ts`, `apps/server/src/routes/models.ts`, `apps/server/src/routes/projects.ts`, `apps/server/src/routes/sessions.ts` +23 more +- `apps/server/src/db.ts` ← `apps/server/src/index.ts`, `apps/server/src/routes/agents.ts`, `apps/server/src/routes/artifacts.ts`, `apps/server/src/routes/chats.ts`, `apps/server/src/routes/messages.ts` +20 more +- `packages/ion/src/cli/utils.ts` ← `packages/ion/src/cli/commands/abandon.ts`, `packages/ion/src/cli/commands/abandon.ts`, `packages/ion/src/cli/commands/approve.ts`, `packages/ion/src/cli/commands/approve.ts`, `packages/ion/src/cli/commands/cleanup.ts` +19 more +- `apps/coder/src/services/tools/types.ts` ← `apps/coder/src/routes/messages.ts`, `apps/coder/src/services/dispatcher.ts`, `apps/coder/src/services/tools/adapter.ts`, `apps/coder/src/services/tools/apply_pending.ts`, `apps/coder/src/services/tools/check_task_status.ts` +13 more +- `apps/coder/src/conductor/types.ts` ← `apps/coder/src/conductor/flows/_util.ts`, `apps/coder/src/conductor/flows/architectural-analysis.ts`, `apps/coder/src/conductor/flows/authoring.ts`, `apps/coder/src/conductor/flows/code-review.ts`, `apps/coder/src/conductor/flows/discovery.ts` +9 more +- `apps/coder/src/services/agent-backend.ts` ← `apps/coder/src/routes/lifecycle.ts`, `apps/coder/src/services/__tests__/stream-json-parser.test.ts`, `apps/coder/src/services/acp-event-map.ts`, `apps/coder/src/services/agent-pool.ts`, `apps/coder/src/services/backends/__tests__/claude-sdk-map.test.ts` +9 more +- `apps/coder/src/services/acp-tool-snapshot.ts` ← `apps/coder/src/services/__tests__/acp-event-map.test.ts`, `apps/coder/src/services/__tests__/frame-emitter.test.ts`, `apps/coder/src/services/__tests__/stream-json-parser.test.ts`, `apps/coder/src/services/acp-dispatch.ts`, `apps/coder/src/services/acp-event-map.ts` +9 more +- `apps/server/src/services/tools/codecontext/factory.ts` ← `apps/server/src/services/tools/codecontext/get_blast_radius.ts`, `apps/server/src/services/tools/codecontext/get_call_graph.ts`, `apps/server/src/services/tools/codecontext/get_codebase_overview.ts`, `apps/server/src/services/tools/codecontext/get_dependencies.ts`, `apps/server/src/services/tools/codecontext/get_file_analysis.ts` +9 more +- `apps/server/src/services/tools.ts` ← `apps/server/src/index.ts`, `apps/server/src/services/__tests__/agent-allowlist.test.ts`, `apps/server/src/services/agents.ts`, `apps/server/src/services/inference/stream-phase-adapter.ts`, `apps/server/src/services/inference/stream-phase.ts` +8 more + +--- + +_Generated by [codesight](https://github.com/Houseofmvps/codesight) — see your codebase clearly_ \ No newline at end of file diff --git a/.codesight/components.md b/.codesight/components.md new file mode 100644 index 0000000..1906d05 --- /dev/null +++ b/.codesight/components.md @@ -0,0 +1,71 @@ +# Components + +- **App** — `apps/web/src/App.tsx` +- **AddProjectModal** — props: open, onOpenChange, onAdded — `apps/web/src/components/AddProjectModal.tsx` +- **AgentComposerBar** — props: projectPath, value, onChange, onProviderCommandsChange, connected, agentStatus — `apps/web/src/components/AgentComposerBar.tsx` +- **AgentPicker** — props: projectId, value, onChange — `apps/web/src/components/AgentPicker.tsx` +- **ArenaLauncherDialog** — `apps/web/src/components/ArenaLauncherDialog.tsx` +- **ArtifactPaneHeader** — props: title, defaultTitle, onDownload, downloadDisabled, onClose, onCopy, justCopied, copyDisabled — `apps/web/src/components/ArtifactPaneHeader.tsx` +- **AskUserInputCard** — props: toolCall, toolResult, chatId, apiPrefix — `apps/web/src/components/AskUserInputCard.tsx` +- **AttachmentChip** — props: attachment, onRemove, onPreview — `apps/web/src/components/AttachmentChip.tsx` +- **AttachmentPreviewModal** — props: attachment, onClose — `apps/web/src/components/AttachmentPreviewModal.tsx` +- **BottomSheet** — props: open, onClose, title — `apps/web/src/components/BottomSheet.tsx` +- **CapHitSentinel** — props: message, capHitPosition, isLatest — `apps/web/src/components/CapHitSentinel.tsx` +- **ChatInput** — props: disabled, projectId, agentId, onAgentChange, sessionId, webSearchEnabled, onSend, onForceSend, generating, onStop — `apps/web/src/components/ChatInput.tsx` +- **ChatTabBar** — props: pane, tabs, tabNumbers, onSwitchTab, onRemoveTab, onCloseOthers, onCloseToRight, onCloseAll, onNewTab, onSplitPane — `apps/web/src/components/ChatTabBar.tsx` +- **ChatThroughput** — props: chatId, className — `apps/web/src/components/ChatThroughput.tsx` +- **CodeBlock** — props: code, lang — `apps/web/src/components/CodeBlock.tsx` +- **ContextMeter** — props: messages, modelContextLimit, sessionCostUsd — `apps/web/src/components/ContextMeter.tsx` +- **CreateProjectModal** — props: open, onOpenChange — `apps/web/src/components/CreateProjectModal.tsx` +- **DoomLoopSentinel** — props: message — `apps/web/src/components/DoomLoopSentinel.tsx` +- **DropOverlay** — props: visible — `apps/web/src/components/DropOverlay.tsx` +- **FileMentionPopover** — props: query, files, anchorRect, onSelect, onClose — `apps/web/src/components/FileMentionPopover.tsx` +- **FileViewerOverlay** — props: path, content, lang, onClose — `apps/web/src/components/FileViewerOverlay.tsx` +- **FlowLauncherDialog** — `apps/web/src/components/FlowLauncherDialog.tsx` +- **GitDiffView** — props: result, loading, error, mode, onSelectMode, onRefresh, mutating, mutateError, onStage, onUnstage — `apps/web/src/components/GitDiffView.tsx` +- **HtmlArtifactPane** — props: chatId, state, onClose — `apps/web/src/components/HtmlArtifactPane.tsx` +- **InferenceSettings** — `apps/web/src/components/InferenceSettings.tsx` +- **MarkdownArtifactPane** — props: chatId, state, onClose — `apps/web/src/components/MarkdownArtifactPane.tsx` +- **MarkdownRenderer** — props: content — `apps/web/src/components/MarkdownRenderer.tsx` +- **MessageBubble** — props: message, sessionChats, capHitInfo, actions, hideActions, hasCheckpoint, restoreDisabled — `apps/web/src/components/MessageBubble.tsx` +- **MessageList** — props: messages, sessionChats — `apps/web/src/components/MessageList.tsx` +- **MobileTabSwitcher** — props: panes, activePaneIdx, chats, onSwitchPane, onRemovePane, onRenameChat — `apps/web/src/components/MobileTabSwitcher.tsx` +- **ModelPicker** — props: value, onChange — `apps/web/src/components/ModelPicker.tsx` +- **NewPaneMenu** — props: onAddPane, disabled, projectId — `apps/web/src/components/NewPaneMenu.tsx` +- **PaneHeaderActions** — props: onNewTab, onSplitPane, onNewOrchestrator, onNewArena, onReopenPane, onShowHistory, onRemovePane, historyActive, className — `apps/web/src/components/PaneHeaderActions.tsx` +- **PermissionCard** — props: prompt, onRespond, busy — `apps/web/src/components/PermissionCard.tsx` +- **ProjectSidebar** — `apps/web/src/components/ProjectSidebar.tsx` +- **RequestReadAccessCard** — props: toolCall, toolResult, chatId — `apps/web/src/components/RequestReadAccessCard.tsx` +- **RightRail** — props: projectId, sessionId — `apps/web/src/components/RightRail.tsx` +- **SessionLandingPage** — props: projectId, sessionId, agentId, onAgentChange, onSend, onSkillInvoke, createChat, chats, onOpenChat, onUnarchiveChat — `apps/web/src/components/SessionLandingPage.tsx` +- **SlashCommandPicker** — props: query, items, groups, inputRef, onSelect, onClose, emptyLabel — `apps/web/src/components/SlashCommandPicker.tsx` +- **StaleStreamBanner** — props: onRetry, onDiscard — `apps/web/src/components/StaleStreamBanner.tsx` +- **StatusDot** — props: chatId, className — `apps/web/src/components/StatusDot.tsx` +- **ThemePicker** — `apps/web/src/components/ThemePicker.tsx` +- **ToolCallGroup** — props: runs — `apps/web/src/components/ToolCallGroup.tsx` +- **ToolCallLine** — props: run, insideGroup — `apps/web/src/components/ToolCallLine.tsx` +- **Workspace** — props: sessionId, projectId, agentId, onAgentChange, panesHook, chatsHook, session, project, onAddPane — `apps/web/src/components/Workspace.tsx` +- **AddProviderModal** — props: open, onOpenChange, onAdded — `apps/web/src/components/coder/AddProviderModal.tsx` +- **ProvidersSettings** — `apps/web/src/components/coder/ProvidersSettings.tsx` +- **MatrixRain** — props: enabled, density, speed, opacity — `apps/web/src/components/fx/MatrixRain.tsx` +- **NeonField** — props: enabled, opacity, speed — `apps/web/src/components/fx/NeonField.tsx` +- **ThemeFx** — `apps/web/src/components/fx/ThemeFx.tsx` +- **ClaudeIcon** — props: size, className — `apps/web/src/components/icons/ProviderIcons.tsx` +- **OpenCodeIcon** — props: size, className — `apps/web/src/components/icons/ProviderIcons.tsx` +- **ArenaPane** — props: state, onClose — `apps/web/src/components/panes/ArenaPane.tsx` +- **ChatPane** — props: sessionId, chatId, projectId, agentId, onAgentChange, sessionChats, webSearchEnabled — `apps/web/src/components/panes/ChatPane.tsx` +- **CoderMessageList** — props: messages, chatId, footer, actions, checkpointMessageIds, restoreDisabled — `apps/web/src/components/panes/CoderMessageList.tsx` +- **CoderPane** — props: sessionId, paneId, chatId, chatPending, projectPath, onConnectedChange, onAgentLabelChange — `apps/web/src/components/panes/CoderPane.tsx` +- **OrchestratorPane** — props: state, onClose — `apps/web/src/components/panes/OrchestratorPane.tsx` +- **SettingsPane** — props: session, project, maximized, onToggleMaximize, onClose, isMobile — `apps/web/src/components/panes/SettingsPane.tsx` +- **TerminalPane** — props: sessionId, paneId, label, active — `apps/web/src/components/panes/TerminalPane.tsx` +- **FloatingMenu** — props: x, y, hasSelection, chatInputs, onCopy, onPaste, onSelectAll, onSearch, onSendToChat, onDismiss — `apps/web/src/components/panes/terminal/FloatingMenu.tsx` +- **SearchBar** — props: searchRef, theme, onClose — `apps/web/src/components/panes/terminal/SearchBar.tsx` +- **TerminalHotkeyBar** — props: ctrlArmed, onSendBytes, onArmCtrl, onFit — `apps/web/src/components/panes/terminal/TerminalHotkeyBar.tsx` +- **RightRailDrawerProvider** — `apps/web/src/hooks/useRightRailDrawer.tsx` +- **SidebarDrawerProvider** — `apps/web/src/hooks/useSidebarDrawer.tsx` +- **PATH_REGEX** — `apps/web/src/lib/linkify-paths.tsx` +- **Home** — `apps/web/src/pages/Home.tsx` +- **Project** — `apps/web/src/pages/Project.tsx` +- **Session** — `apps/web/src/pages/Session.tsx` +- **Settings** — `apps/web/src/pages/Settings.tsx` diff --git a/.codesight/config.md b/.codesight/config.md new file mode 100644 index 0000000..1c33524 --- /dev/null +++ b/.codesight/config.md @@ -0,0 +1,50 @@ +# Config + +## Environment Variables + +- `AUDIT_DOT_DIR` **required** — apps/server/src/services/audit/runs-dir.ts +- `BOOCODE_DATA_DIR` **required** — apps/server/src/routes/inference-settings.ts +- `BOOCODE_TOOLS` **required** — apps/server/src/services/agents.ts +- `BOOCODE_TRUNCATION_DIR` **required** — apps/server/src/services/__tests__/truncate.test.ts +- `BOOCODER_DEV_URL` **required** — apps/web/vite.config.ts +- `BOOCODER_URL` **required** — apps/coder/src/cli.ts +- `BOOTERM_DEV_URL` **required** — apps/web/vite.config.ts +- `BOOTERM_SSH_HOST` **required** — apps/booterm/src/pty/manager.ts +- `BOOTERM_SSH_USER` **required** — apps/booterm/src/pty/manager.ts +- `BOOTSTRAP_ROOT` (has default) — .env.example +- `BRAINSTORM_DIR` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_HOST` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_OWNER_PID` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_PORT` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `BRAINSTORM_URL_HOST` **required** — data/skills/superpowers/brainstorming/scripts/server.cjs +- `CODECONTEXT_CHILD` **required** — codecontext/shim.go +- `CODECONTEXT_URL` **required** — apps/server/src/services/codecontext_client.ts +- `CONDUCTOR_MODEL` **required** — conductor/src/dispatch.ts +- `CONDUCTOR_OPENCODE_BIN` **required** — conductor/src/dispatch.ts +- `CONDUCTOR_TIMEOUT_MS` **required** — conductor/src/dispatch.ts +- `CONTAINER_GUIDANCE_FILE` **required** — apps/server/src/services/__tests__/system-prompt.test.ts +- `CONTEXT7_API_KEY` (has default) — .env +- `DATABASE_URL` (has default) — .env.example +- `DEFAULT_MODEL` (has default) — .env.example +- `DEV_REMOTE_USER` **required** — apps/web/vite.config.ts +- `GITEA_BASE_URL` (has default) — .env +- `GITEA_SSH_HOST` (has default) — .env +- `GITEA_TOKEN` (has default) — .env +- `GITEA_USER` (has default) — .env +- `LLAMA_SWAP_URL` (has default) — .env.example +- `MCP_TEST_MISSING` **required** — apps/server/src/services/__tests__/mcp-config.test.ts +- `MCP_TEST_SECRET` **required** — apps/server/src/services/__tests__/mcp-config.test.ts +- `NODE_ENV` (has default) — .env.example +- `PORT` (has default) — .env.example +- `POSTGRES_PASSWORD` (has default) — .env.example +- `PROJECT_ROOT_WHITELIST` (has default) — .env.example +- `SEARXNG_URL` (has default) — .env.example +- `SKILLS_ROOT` **required** — apps/server/src/services/skills.ts +- `WEB_DIST_PATH` **required** — apps/server/src/index.ts + +## Config Files + +- `.env.example` +- `Dockerfile` +- `apps/web/vite.config.ts` +- `docker-compose.yml` diff --git a/.codesight/graph.md b/.codesight/graph.md new file mode 100644 index 0000000..27323ec --- /dev/null +++ b/.codesight/graph.md @@ -0,0 +1,37 @@ +# Dependency Graph + +## Most Imported Files (change these carefully) + +- `apps/coder/src/db.ts` — imported by **40** files +- `apps/server/src/types/api.ts` — imported by **28** files +- `apps/server/src/db.ts` — imported by **25** files +- `packages/ion/src/cli/utils.ts` — imported by **24** files +- `apps/coder/src/services/tools/types.ts` — imported by **18** files +- `apps/coder/src/conductor/types.ts` — imported by **14** files +- `apps/coder/src/services/agent-backend.ts` — imported by **14** files +- `apps/coder/src/services/acp-tool-snapshot.ts` — imported by **14** files +- `apps/server/src/services/tools/codecontext/factory.ts` — imported by **14** files +- `apps/server/src/services/tools.ts` — imported by **13** files +- `conductor/src/types.ts` — imported by **13** files +- `apps/coder/src/services/provider-config-registry.ts` — imported by **12** files +- `apps/server/src/config.ts` — imported by **12** files +- `apps/coder/src/config.ts` — imported by **11** files +- `apps/coder/src/services/provider-types.ts` — imported by **11** files +- `apps/server/src/services/agents.ts` — imported by **10** files +- `apps/coder/src/services/pending_changes.ts` — imported by **9** files +- `apps/server/src/services/broker.ts` — imported by **9** files +- `apps/server/src/services/path_guard.ts` — imported by **9** files +- `apps/server/src/services/inference/payload.ts` — imported by **9** files + +## Import Map (who imports what) + +- `apps/coder/src/db.ts` ← `apps/coder/src/index.ts`, `apps/coder/src/routes/__tests__/agent-sessions.routes.test.ts`, `apps/coder/src/routes/__tests__/chat-resolve.test.ts`, `apps/coder/src/routes/__tests__/providers.routes.test.ts`, `apps/coder/src/routes/agent-sessions.ts` +35 more +- `apps/server/src/types/api.ts` ← `apps/server/src/routes/chats.ts`, `apps/server/src/routes/messages.ts`, `apps/server/src/routes/models.ts`, `apps/server/src/routes/projects.ts`, `apps/server/src/routes/sessions.ts` +23 more +- `apps/server/src/db.ts` ← `apps/server/src/index.ts`, `apps/server/src/routes/agents.ts`, `apps/server/src/routes/artifacts.ts`, `apps/server/src/routes/chats.ts`, `apps/server/src/routes/messages.ts` +20 more +- `packages/ion/src/cli/utils.ts` ← `packages/ion/src/cli/commands/abandon.ts`, `packages/ion/src/cli/commands/abandon.ts`, `packages/ion/src/cli/commands/approve.ts`, `packages/ion/src/cli/commands/approve.ts`, `packages/ion/src/cli/commands/cleanup.ts` +19 more +- `apps/coder/src/services/tools/types.ts` ← `apps/coder/src/routes/messages.ts`, `apps/coder/src/services/dispatcher.ts`, `apps/coder/src/services/tools/adapter.ts`, `apps/coder/src/services/tools/apply_pending.ts`, `apps/coder/src/services/tools/check_task_status.ts` +13 more +- `apps/coder/src/conductor/types.ts` ← `apps/coder/src/conductor/flows/_util.ts`, `apps/coder/src/conductor/flows/architectural-analysis.ts`, `apps/coder/src/conductor/flows/authoring.ts`, `apps/coder/src/conductor/flows/code-review.ts`, `apps/coder/src/conductor/flows/discovery.ts` +9 more +- `apps/coder/src/services/agent-backend.ts` ← `apps/coder/src/routes/lifecycle.ts`, `apps/coder/src/services/__tests__/stream-json-parser.test.ts`, `apps/coder/src/services/acp-event-map.ts`, `apps/coder/src/services/agent-pool.ts`, `apps/coder/src/services/backends/__tests__/claude-sdk-map.test.ts` +9 more +- `apps/coder/src/services/acp-tool-snapshot.ts` ← `apps/coder/src/services/__tests__/acp-event-map.test.ts`, `apps/coder/src/services/__tests__/frame-emitter.test.ts`, `apps/coder/src/services/__tests__/stream-json-parser.test.ts`, `apps/coder/src/services/acp-dispatch.ts`, `apps/coder/src/services/acp-event-map.ts` +9 more +- `apps/server/src/services/tools/codecontext/factory.ts` ← `apps/server/src/services/tools/codecontext/get_blast_radius.ts`, `apps/server/src/services/tools/codecontext/get_call_graph.ts`, `apps/server/src/services/tools/codecontext/get_codebase_overview.ts`, `apps/server/src/services/tools/codecontext/get_dependencies.ts`, `apps/server/src/services/tools/codecontext/get_file_analysis.ts` +9 more +- `apps/server/src/services/tools.ts` ← `apps/server/src/index.ts`, `apps/server/src/services/__tests__/agent-allowlist.test.ts`, `apps/server/src/services/agents.ts`, `apps/server/src/services/inference/stream-phase-adapter.ts`, `apps/server/src/services/inference/stream-phase.ts` +8 more diff --git a/.codesight/libs.md b/.codesight/libs.md new file mode 100644 index 0000000..0cb2c83 --- /dev/null +++ b/.codesight/libs.md @@ -0,0 +1,927 @@ +# Libraries + +- `apps/booterm/src/auth.ts` — function getUser: (req) => string +- `apps/booterm/src/config.ts` — function loadConfig: () => Config +- `apps/booterm/src/db.ts` + - function getPool: (databaseUrl) => pg.Pool + - function getSessionInfo: (sessionId) => Promise + - function pingDb: () => Promise + - function closeDb: () => Promise +- `apps/booterm/src/pty/manager.ts` + - function sanitizeId: (raw) => string | null + - function tmuxSessionName: (paneId) => string + - function hasSession: (tmuxConfPath, sessionName) => Promise + - function ensureSession: (tmuxConfPath, sessionName, projectRoot, log, cols?, rows?) => Promise + - function killSession: (tmuxConfPath, sessionName) => Promise + - function capturePane: (tmuxConfPath, sessionName, lines) => Promise +- `apps/booterm/src/pty/pty.ts` — function attachPty: (opts) => IPty +- `apps/booterm/src/ws/attach.ts` — function registerWsAttachRoute: (app, tmuxConfPath) => void +- `apps/coder/src/conductor/contracts.ts` + - function produceContract: (contracts) => string + - function reviewContract: (contracts) => string + - type Contract + - const EVIDENCE_PRODUCE + - const EVIDENCE_REVIEW + - const YAGNI_PRODUCE + - _...1 more_ +- `apps/coder/src/conductor/flows/_util.ts` — function q, function repoLine +- `apps/coder/src/conductor/flows/index.ts` + - function describeFlows: () => string + - function getFlow: (name) => Flow | undefined + - const FLOWS: Record + - const FLOW_NAMES: string[] +- `apps/coder/src/conductor/persona-loader.ts` — function loadPersona: (agent) => Promise, const AGENTS_DIR +- `apps/coder/src/conductor/render.ts` — function slugify: (s) => string +- `apps/coder/src/conductor/spine.ts` + - function readBand: (input) => Band + - function fastNote: (ctx) => string + - function buildSpineFlow: (spine) => Flow +- `apps/coder/src/config.ts` — function loadConfig: () => Config, type Config +- `apps/coder/src/db.ts` + - function getSql: (config) => Sql + - function applySchema: (sql) => Promise + - function pingDb: (sql) => Promise + - function closeDb: () => Promise + - type Sql +- `apps/coder/src/plugins/host.ts` + - function registerHook: (name, fn) => void + - function emitHook: (name, ctx) => Promise + - function clearHooks: () => void + - interface ToolHookContext + - interface ToolResultContext + - type HookName + - _...1 more_ +- `apps/coder/src/services/acp-client-fs.ts` — function readWorktreeTextFile: (worktreePath, filePath, line?, limit?) => Promise, function writeWorktreeTextFile: (worktreePath, filePath, content) => Promise +- `apps/coder/src/services/acp-client.ts` — function buildAcpClient: (worktreePath, resolveTurn) => void, interface AcpTurnContext +- `apps/coder/src/services/acp-derive.ts` + - function deriveModesFromACP: (fallbackModes, modeState?, configOptions?) => void + - function deriveModelDefinitionsFromACP: (models, configOptions?) => ProviderModel[] + - function findThoughtLevelConfigId: (configOptions) => string | null +- `apps/coder/src/services/acp-dispatch.ts` + - function dispatchViaAcp: (opts) => Promise + - interface AcpDispatchResult + - interface AcpDispatchOpts +- `apps/coder/src/services/acp-event-map.ts` — function mapSessionUpdate: (params, priorSnapshots, AcpToolSnapshot>) => void +- `apps/coder/src/services/acp-probe.ts` — function probeAcpProvider: (agent, installPath, cwd) => Promise, interface AcpProbeResult +- `apps/coder/src/services/acp-spawn.ts` + - function resolveAcpSpawnArgs: (agent) => string[] | null + - function resolveLaunchSpec: (resolved, installPath) => void + - function resolveAcpProbeBinaries: (agent) => string[] +- `apps/coder/src/services/acp-stream.ts` — function createAcpNdJsonStream: (child) => void +- `apps/coder/src/services/acp-tool-snapshot.ts` + - function mergeToolSnapshot: (toolCallId, update, previous?) => AcpToolSnapshot + - function mapToolLifecycleStatus: (status, rawOutput?) => AcpToolLifecycleStatus + - function snapshotToWireToolCall: (snapshot) => void + - function snapshotToPartPayload: (snapshot) => void + - function synthesizeCanceledSnapshots: (snapshots) => AcpToolSnapshot[] + - interface AcpToolSnapshot + - _...2 more_ +- `apps/coder/src/services/agent-commands-cache.ts` + - function setTaskCommands: (taskId, commands) => void + - function mergeTaskCommands: (taskId, commands) => void + - function getTaskCommands: (taskId) => AgentCommand[] | null + - function clearTaskCommands: (taskId) => void +- `apps/coder/src/services/agent-pool.ts` + - class AgentPool + - interface AgentPoolOpts + - const OPENCODE_POOL_KEY + - const agentPool +- `apps/coder/src/services/agent-probe.ts` — function probeAgents: (sql, log) => Promise +- `apps/coder/src/services/agent-status-publish.ts` — function publishAgentStatus: (publishFrame, sessionId, chatId, agent, status, reason?, at) => void +- `apps/coder/src/services/agent-turn-persist.ts` — function persistExternalAgentTurn: (sql, assistantMessageId, snapshots, reasoningText) => Promise +- `apps/coder/src/services/arena-analyzer-helpers.ts` + - function buildDigestPrompt: (input) => void + - function buildJudgePrompt: (originalPrompt, digests) => void + - function shouldNameWinner: (succeededCount) => boolean + - function extractWinner: (judgeOutput) => void + - function buildCrossExamPrompt: (opts) => void + - interface ContestantDigestInput + - _...1 more_ +- `apps/coder/src/services/arena-analyzer.ts` — function createAnalyzer: (deps) => Analyzer, interface Analyzer +- `apps/coder/src/services/arena-decisions.ts` + - function classifyLane: (battleType, _identity, model, localModels) => ContestantLane + - function nextLocalContestant: (contestants) => string | null + - function isBattleComplete: (contestants) => boolean + - function computeBenchmark: (startedAt, endedAt, costTokens, lane) => Benchmark + - function sanitizeSlug: (s) => string + - function buildBattleSlug: (battleId, battleType, createdAt) => string + - _...7 more_ +- `apps/coder/src/services/arena-model-call.ts` — function arenaModelCall: (opts, 'LLAMA_SWAP_URL'>; + model) => Promise +- `apps/coder/src/services/arena-runner.ts` + - function createBattleRunner: (deps) => BattleRunner + - interface ContestantSpec + - interface BattleStartOpts + - interface BattleRunner + - type DispatchContestantFn + - type OnBattleComplete + - _...1 more_ +- `apps/coder/src/services/audit-session.ts` + - function generateSessionId: () => string + - function getCurrentSession: (basePath?) => Promise + - function getSessionJson: (sessionId, basePath?) => Promise + - function getIndex: (basePath?) => Promise + - function startSession: (task, basePath?) => Promise + - function endSession: (basePath?) => Promise + - _...18 more_ +- `apps/coder/src/services/backends/claude-sdk-map.ts` + - function createClaudeSdkMapState: () => ClaudeSdkMapState + - function mapSdkMessage: (msg, state) => AgentEvent[] + - interface ClaudeSdkMapState +- `apps/coder/src/services/backends/claude-sdk-routing.ts` — function claudeSdkBackendEnabled: (env) => boolean, function shouldUseClaudeSdk: (task, env) => boolean +- `apps/coder/src/services/backends/claude-sdk.ts` — class ClaudeSdkBackend, interface ClaudeSdkBackendDeps +- `apps/coder/src/services/backends/claude-session-store.ts` — class PostgresSessionStore +- `apps/coder/src/services/backends/lifecycle-decisions.ts` + - function selectIdleEvictionTargets: (entries, now, ttlMs) => string[] + - function selectLruEvictionTargets: (entries, cap) => string[] + - function decideRestart: (input) => RestartDecision + - function selectOrphanWorktreeTargets: (onDisk, liveWorktreePaths, now, graceMs) => string[] + - interface PoolEntrySnapshot + - interface RestartDecisionInput + - _...7 more_ +- `apps/coder/src/services/backends/opencode-event-map.ts` + - function stripDcpTags: (s) => string + - function eventSessionId: (ev) => string | null + - function resolvePartDedupeKey: (part, type) => string | null + - function mapToolStatus: (s) => ToolCallStatus | null + - function toolPartToSnapshot: (part) => AcpToolSnapshot + - function toolCalledSnapshot: (p) => AcpToolSnapshot + - _...7 more_ +- `apps/coder/src/services/backends/opencode-server-process.ts` + - function shouldStartServer: (s) => boolean + - class OpenCodeServerSupervisor + - interface ServerDownInfo + - interface SupervisorHooks + - interface OpenCodeServerSupervisorDeps +- `apps/coder/src/services/backends/opencode-server.ts` — class OpenCodeServerBackend, interface OpenCodeServerBackendDeps +- `apps/coder/src/services/backends/opencode-sse.ts` + - function reconnectDecision: (failures, policy) => ReconnectDecision + - function startSessionEventLoop: (state, deps) => void + - function runSessionEventLoop: (state, abort, deps) => Promise + - interface TurnState + - interface SessionState + - interface ReconnectPolicy + - _...4 more_ +- `apps/coder/src/services/backends/opencode-usage.ts` + - function stepEndedToUsage: (props) => StepUsage + - interface StepEndedProps + - interface StepUsage +- `apps/coder/src/services/backends/pushable-iterable.ts` — function createPushable: () => Pushable, interface Pushable +- `apps/coder/src/services/backends/turn-guard.ts` + - function armAbortGuard: (g) => void + - function noteTurnActivity: (g) => void + - function consumeTerminal: (g) => 'swallow' | 'settle' + - interface AbortTerminalGuard +- `apps/coder/src/services/backends/warm-acp-routing.ts` — function shouldUseWarmBackend: (task) => boolean, function isTurnOkForStopReason: (stopReason) => boolean +- `apps/coder/src/services/backends/warm-acp.ts` — class WarmAcpBackend, interface WarmAcpBackendDeps +- `apps/coder/src/services/cancel-registry.ts` — function createCancelRegistry: () => CancelRegistry, interface CancelRegistry +- `apps/coder/src/services/checkpoints.ts` + - function buildShadowCommitCommand: (worktreePath, id) => string + - function createCheckpoint: (sql, args, opts?) => Promise< + - function restoreCheckpoint: (sql, checkpointId, opts?) => Promise + - class CheckpointNotFoundError + - interface CreateCheckpointArgs + - interface RestoreCheckpointResult + - _...1 more_ +- `apps/coder/src/services/claude-command-discovery.ts` — function discoverClaudeCommands: () => AgentCommand[] +- `apps/coder/src/services/command-availability.ts` — function isCommandAvailable: (binary) => Promise +- `apps/coder/src/services/correction-service.ts` + - function recordCorrection: (originalClaim, correction, principleExtracted, persistedTo, basePath?) => Promise + - function scanForCorrections: (auditPath) => Promise + - function checkContradiction: (action, corrections) => void + - function markPersisted: (correctionId, filePath, basePath?) => Promise + - function listCorrections: (basePath?) => Promise + - function appendCorrectionToTrail: (trailPath, correction) => Promise + - _...2 more_ +- `apps/coder/src/services/dcp-strip.ts` + - function stripDcpTags: (s) => string + - function makeDcpStreamStripper: () => DcpStreamStripper + - interface DcpStreamStripper +- `apps/coder/src/services/dispatcher.ts` — function createDispatcher: (deps) => void +- `apps/coder/src/services/edit-guards-imports.ts` — function checkDroppedImports: (original, updated, filePath) => ImportCheckResult, interface ImportCheckResult +- `apps/coder/src/services/edit-guards.ts` + - function validateEditResult: (original, updated, filePath) => GuardResult + - function formatGuardError: (guard, filePath) => string + - interface GuardResult +- `apps/coder/src/services/finalize-message.ts` + - function classifyTerminalStatus: (opts) => TerminalMessageStatus + - function finalizeStreamingMessage: (sql, publishFrame, frame) => void + - type TerminalMessageStatus +- `apps/coder/src/services/flow-artifacts.ts` — function getArtifactPath: (flowRunId, stepId) => string, function writeFlowArtifact: (flowRunId, stepId, content) => Promise +- `apps/coder/src/services/flow-runner-decisions.ts` + - function manifestSteps: (flow, launchCtx) => Step[] + - function readySteps: (flow, state) => Step[] + - function partitionReady: (ready, ctx) => void + - function isRunComplete: (flow, state) => boolean + - function isStuck: (flow, state) => boolean + - function reconcileResumeStep: (status, taskId, taskState) => ResumeAction + - _...5 more_ +- `apps/coder/src/services/flow-runner.ts` + - function createFlowRunner: (deps) => FlowRunner + - interface LaunchOpts + - interface FlowRunner +- `apps/coder/src/services/frame-emitter.ts` + - function makeFrameEmitter: (opts) => FrameEmitter + - interface FrameEmitterOpts + - interface FrameEmitter +- `apps/coder/src/services/fuzzy-match.ts` + - function locateMatch: (content, needle) => MatchResult + - type MatchResult + - const SIMILARITY_THRESHOLD + - const AMBIGUITY_EPSILON +- `apps/coder/src/services/guideline-service.ts` + - function createGuideline: (params, basePath?) => Promise + - function listGuidelines: (filter?, basePath?) => Promise + - function readGuideline: (id, basePath?) => Promise + - function updateGuideline: (id, params, basePath?) => Promise + - function deleteGuideline: (id, basePath?) => Promise + - function findGuideline: (content, basePath?) => Promise + - _...14 more_ +- `apps/coder/src/services/host-exec.ts` — function hostExec: (command, opts?) => Promise, interface HostExecResult +- `apps/coder/src/services/lsp/client.ts` — class LspClient +- `apps/coder/src/services/lsp/config.ts` — function getServerConfig: (filePath) => LspServerConfig | null, interface LspServerConfig +- `apps/coder/src/services/lsp/operations.ts` + - function openDocument: (client, filePath, content, version) => Promise + - function closeDocument: (client, filePath) => Promise + - function getDiagnostics: (client, filePath, content) => Promise + - function gotoDefinition: (client, filePath, content, line, character) => Promise + - function findReferences: (client, filePath, content, line, character) => Promise +- `apps/coder/src/services/lsp/server-manager.ts` — class LspServerManager, const lspManager +- `apps/coder/src/services/mcp-server.ts` — function startMcpServer: (sql) => Promise +- `apps/coder/src/services/net/port-utils.ts` + - function reclaimPort: (port) => void + - function waitForPortRelease: (port, timeoutMs) => Promise + - function freePort: () => Promise +- `apps/coder/src/services/orphan-worktree-reaper.ts` + - function reapOrphanWorktrees: (sql, log, graceMs, now) => void + - function createOrphanWorktreeReaper: (deps) => void + - interface OrphanWorktreeReaperDeps + - interface OrphanReaperResult +- `apps/coder/src/services/pending_changes.ts` + - function planEdit: (content, oldStr, newStr) => EditPlan + - function queueEdit: (sql, sessionId, taskId, filePath, oldString, newString, projectRoot, // v2.6 Phase 1-UX) => void + - function queueCreate: (sql, sessionId, taskId, filePath, content, projectRoot, // See queueEdit) => Promise + - function queueDelete: (sql, sessionId, taskId, filePath, projectRoot, // See queueEdit) => Promise + - function applyOne: (sql, changeId, projectRoot) => Promise + - function applyAll: (sql, sessionId, projectRoot) => Promise + - _...6 more_ +- `apps/coder/src/services/permission-waiter.ts` + - function setPermissionHooks: (next) => void + - function waitForPermissionResponse: (taskId, sessionId, provider, modeId, params, timeoutMs) => Promise + - function respondToPermission: (taskId, optionId, updatedInput?, unknown>) => boolean + - function getPendingPermission: (taskId) => PermissionPrompt | null + - function waitForElicitationResponse: (taskId, sessionId, provider, modeId, params, timeoutMs) => Promise + - function cancelPendingPermission: (taskId) => void + - _...3 more_ +- `apps/coder/src/services/provider-commands.ts` + - function getManifestCommands: (provider) => AgentCommand[] + - function mergeCommands: (...lists) => AgentCommand[] + - const PROVIDER_COMMANDS: Record +- `apps/coder/src/services/provider-config-registry.ts` + - function buildResolvedRegistry: (builtins, config) => Map + - function loadProviderConfig: (path) => Map + - function reloadProviderConfig: () => Map + - function getResolvedRegistry: () => Map + - interface ResolvedProviderDef +- `apps/coder/src/services/provider-config.ts` + - function mergeProviderConfigPatch: (current, patch) => CoderProvidersFile + - function load: (path) => CoderProvidersFile + - function save: (path, config) => void +- `apps/coder/src/services/provider-diagnostic.ts` — function getProviderDiagnostic: (resolved, agentRow, opts) => Promise, interface DiagnosticAgentRow +- `apps/coder/src/services/provider-manifest.ts` + - function getManifestModes: (provider) => ProviderMode[] + - function getManifestDefaultModeId: (provider) => string | null + - function isUnattendedMode: (provider, modeId) => boolean + - interface ProviderManifestEntry + - const PROVIDER_MANIFEST: Record +- `apps/coder/src/services/provider-snapshot.ts` + - function fetchLlamaSwapModels: (config) => Promise + - function prefixLlamaSwapModels: (models) => ProviderModel[] + - function mergeModels: (...lists) => ProviderModel[] + - function getProviderSnapshot: (sql, config, cwd?, force) => Promise + - function clearProviderSnapshotCache: () => void + - function peekSnapshotEntry: (name, cwd?) => ProviderSnapshotEntry | undefined + - _...1 more_ +- `apps/coder/src/services/pty-dispatch.ts` + - function dispatchViaPty: (opts) => Promise + - interface DispatchResult + - interface PtyDispatchOpts +- `apps/coder/src/services/qwen-settings.ts` — function readQwenSettingsModels: () => Promise +- `apps/coder/src/services/stream-json-parser.ts` + - function makeStreamJsonState: () => StreamJsonState + - function parseStreamJsonLine: (line, state) => AgentEvent[] + - function makeStreamJsonParser: () => StreamJsonParser + - interface StreamJsonUsage + - interface StreamJsonState + - interface StreamJsonParser + - _...1 more_ +- `apps/coder/src/services/token-analysis/analyzer.ts` — function analyzeMessages: (parts) => TokenBreakdown, interface TokenBreakdown +- `apps/coder/src/services/token-analysis/persist.ts` + - function persistTaskBreakdown: (sql, taskId, breakdown) => Promise + - function getTaskBreakdown: (sql, taskId) => Promise + - function analyzeAndPersistTaskBreakdown: (sql, taskId, parts) => Promise +- `apps/coder/src/services/tools/adapter.ts` — function adaptWriteTool: (tool) => ServerToolDef +- `apps/coder/src/services/tools/inference_context.ts` + - function runWithInferenceContext: (ctx, fn) => void + - function getInferenceContext: () => InferenceContext + - interface InferenceContext +- `apps/coder/src/services/tools/types.ts` + - function asPermissionMode: (id) => PermissionMode | undefined + - interface ToolJsonSchema + - interface ToolContext + - interface ToolDef + - type PermissionMode +- `apps/coder/src/services/tools/write-gate.ts` — function denyReadOnly: (operation) => unknown, function finalizeWrite: (context, projectRoot, change, queuedHint) => Promise +- `apps/coder/src/services/worktree-risk.ts` — function checkWorktreeWorkAtRisk: (worktreePath, opts?) => Promise, function stashWorktree: (worktreePath, opts?) => Promise< +- `apps/coder/src/services/worktrees.ts` + - function createWorktree: (projectPath, taskId, opts?) => Promise + - function diffWorktree: (worktreePath, projectPath, opts?) => Promise + - function cleanupWorktree: (projectPath, taskId) => Promise + - function ensureSessionWorktree: (sql, projectPath, sessionId, opts?) => Promise + - function removeSessionWorktree: (sql, projectPath, worktree, opts?) => Promise + - function closeChatBackendState: (sql, chatId, opts?) => Promise + - _...4 more_ +- `apps/coder/src/services/write_guard.ts` + - function isSecretPath: (filePath) => boolean + - function resolveWritePath: (projectRoot, filePath) => string + - class WriteGuardError +- `apps/server/src/config.ts` — function loadConfig: () => Config, type Config +- `apps/server/src/db.ts` + - function getSql: (config) => Sql + - function applySchema: (sql) => Promise + - function pingDb: (sql) => Promise + - function closeDb: () => Promise + - type Sql +- `apps/server/src/services/agents.ts` + - function refreshToolNames: () => void + - function matchToolGlob: (toolName, patterns) => boolean + - function slugify: (name) => string + - function parseAgentsMd: (content) => ParseResult + - function isAgentRegistryMarkdown: (content) => boolean + - function getAgentsMtimes: (projectPath) => void + - _...2 more_ +- `apps/server/src/services/artifacts.ts` + - function deriveMarkdownSlug: (messageContent) => string + - function deriveHtmlSlug: (payload) => string + - function deriveHtmlTitle: (html) => string | null + - function detectHtmlArtifact: (text) => string | null + - function decideHtmlArtifactWrite: (htmlContent) => HtmlArtifactDecision + - function writeMarkdownArtifact: (message, 'content'>, ctx) => Promise + - _...6 more_ +- `apps/server/src/services/audit/corrections.ts` + - function createCorrection: (params) => UserCorrectionRecord + - function findCorrections: (records, unknown>[]) => UserCorrectionRecord[] + - function checkCorrectionConflict: (proposedAction, corrections) => UserCorrectionRecord | null + - interface UserCorrectionRecord +- `apps/server/src/services/audit/guideline-store.ts` + - class GuidelineDocumentStore + - interface GuidelineContent + - interface Guideline + - interface GuidelineDocument + - interface GuidelineUpdateParams + - type GuidelineId + - _...3 more_ +- `apps/server/src/services/audit/journey-projection.ts` + - function projectJourneyToGuidelines: (journey, nodes, edges) => ProjectedGuideline[] + - function detectJourneyBacktrack: (journey, nodes, edges, currentNodeId, previousNodeId) => BacktrackCheck + - interface ProjectedGuideline + - interface BacktrackCheck +- `apps/server/src/services/audit/journey-store.ts` + - class JourneyStore + - interface JourneyNode + - interface JourneyEdge + - interface Journey + - type JourneyId + - type JourneyNodeId + - _...1 more_ +- `apps/server/src/services/audit/runs-dir.ts` + - function findRunsDir: (projectRoot?) => string + - function ensureRunsDir: (projectRoot?) => string + - function readCurrentSession: (projectRoot?) => string | null + - function writeCurrentSession: (sessionId, projectRoot?) => void + - function clearCurrentSession: (projectRoot?) => void + - function readIndex: (projectRoot?) => IndexFile + - _...7 more_ +- `apps/server/src/services/audit/session-manager.ts` + - function generateSessionId: () => string + - function isoNow: () => string + - function createSession: (task, sessionId?, projectRoot?) => string + - function getSessionDir: (sessionId, projectRoot?) => string + - function getActiveSession: (projectRoot?) => SessionJson | null + - function readSession: (sessionId, projectRoot?) => SessionJson | null + - _...9 more_ +- `apps/server/src/services/auto_name.ts` — function maybeAutoNameChat: (ctx, chatId, sessionId) => Promise +- `apps/server/src/services/broker.ts` + - function createBroker: (log?) => Broker + - interface Broker + - type Frame + - type Listener +- `apps/server/src/services/codecontext_client.ts` + - function callCodecontext: (req, fetcher) => Promise + - interface CodecontextRequest + - interface CodecontextResponse +- `apps/server/src/services/coder-notify.ts` — function notifyCoderClose: (kind, id, log?, 'debug'>, fetcher) => Promise, type CoderCloseKind +- `apps/server/src/services/compaction.ts` + - function usable: (contextLimit) => number + - function isOverflow: (usage, contextLimit) => boolean + - function estimate: (messages) => number + - function turns: (messages) => Turn[] + - function select: (messages, contextLimit, tailTurns) => SelectResult + - function deriveFilesRead: (head) => string[] + - _...8 more_ +- `apps/server/src/services/file_index.ts` — function getProjectFiles: (projectId, projectRoot) => Promise +- `apps/server/src/services/file_ops.ts` + - function listDir: (projectRoot, relPath, opts?) => Promise + - function viewFile: (projectRoot, relPath, opts?) => Promise + - function grep: (projectRoot, pattern, opts?) => Promise + - function findFiles: (projectRoot, pattern?, opts?) => Promise + - interface FileEntry + - interface ListDirResult + - _...4 more_ +- `apps/server/src/services/git_diff.ts` + - function parseNameStatus: (output) => void + - function parseNumStatLine: (line) => void + - function splitDiffByFile: (diffText) => Map + - function classifyDiffBody: (body, cap) => 'diff' | 'binary' | 'too_large' + - function autoSelectMode: (isDirty) => GitDiffMode + - function canCommit: (files) => boolean + - _...17 more_ +- `apps/server/src/services/git_meta.ts` — function getGitMeta: (rootPath) => Promise, interface GitMeta +- `apps/server/src/services/gitea.ts` + - function createGiteaRepo: (cfg, name, options) => Promise + - class GiteaRepoExistsError + - interface GiteaConfig + - interface GiteaRepo +- `apps/server/src/services/grant_resolver.ts` — function resolveGrantRoot: (sql, requestedPath, projectRoot, whitelistRoot) => Promise, type GrantResolution +- `apps/server/src/services/inference/budget.ts` — function resolveToolBudget: (agent) => number +- `apps/server/src/services/inference/content-flusher.ts` — function createContentFlusher: (sql, messageId, getContent) => void, interface ContentFlusher +- `apps/server/src/services/inference/dcp/messages.ts` + - function toDcpMessages: (parts) => DcpMessage[] + - function fromDcpMessages: (msgs) => any[] + - interface DcpMessage +- `apps/server/src/services/inference/dcp/state.ts` + - function getDcpState: (chatId) => ChatDcpState | undefined + - function setDcpState: (chatId, messageCount) => void + - function clearDcpState: (chatId) => void + - function shouldTransform: (chatId, messageCount) => boolean +- `apps/server/src/services/inference/dcp/strategies/deduplication.ts` — function deduplicate: (messages) => void +- `apps/server/src/services/inference/dcp/strategies/purge-errors.ts` — function purgeErrors: (messages, windowSize) => void +- `apps/server/src/services/inference/dcp/transform.ts` + - function transformMessages: (chatId, messages) => TransformResult + - interface TransformStats + - interface TransformResult +- `apps/server/src/services/inference/error-handler.ts` + - function handleAbortOrError: (ctx, args, accumulated, err) => Promise + - function finalizeStreamedRow: (ctx, opts) => void + - function finalizeEmpty: (ctx, args) => Promise + - function finalizeCompletion: (ctx, args, result, startedAt, session) => Promise +- `apps/server/src/services/inference/llama-args-validator.ts` + - function validateExtraArgs: (args?) => string[] + - function isManagedFlag: (flag) => boolean + - function stripShadowingFlags: (args, opts?) => string[] + - interface StripOptions +- `apps/server/src/services/inference/loop-detectors.ts` + - function detectContentRepeat: (messages) => LoopDetectionResult + - function detectToolLoop: (toolNames) => LoopDetectionResult + - function detectDoomLoop: (messages, toolNames) => LoopDetectionResult + - interface LoopDetectionResult +- `apps/server/src/services/inference/mistake-tracker.ts` + - function freshMistakeState: () => MistakeState + - function recordStep: (state, outcome) => void + - function detectMistakePattern: (state) => 'nudge' | 'escalate' | null + - interface MistakeState + - type FailureKind + - const MISTAKE_THRESHOLD + - _...1 more_ +- `apps/server/src/services/inference/parts.ts` + - function insertParts: (sql, parts) => Promise + - function partsFromAssistantMessage: (args) => void + - function partsFromToolMessage: (args) => Omit[] + - interface PartInsert + - type PartKind +- `apps/server/src/services/inference/payload.ts` + - function buildMessagesPayload: (session, project, history, agent, log?) => Promise + - function loadContext: (sql, sessionId, chatId) => Promise< + - function maybeFlagForCompaction: (ctx, chatId, updated) => Promise + - interface OpenAiMessage +- `apps/server/src/services/inference/provider.ts` + - function resolveRoute: (agent, config?) => RoutingInfo + - function upstreamModel: (config, modelId, agent?) => LanguageModel + - interface RoutingInfo + - type InferenceRoute +- `apps/server/src/services/inference/prune.ts` + - function selectPruneTargets: (partsNewestFirst, tailStartCreatedAt) => void + - function prune: (args) => Promise + - interface PruneResult + - interface PartForPrune + - const PROTECTED_TOKENS + - const PRUNE_TRIGGER_TOKENS +- `apps/server/src/services/inference/sentinel-summaries.ts` + - function runCapHitSummary: (ctx, args, session, project, history, agent, budget) => Promise + - function runDoomLoopSummary: (ctx, args, session, project, history, agent, loop, unknown> }) => Promise + - function runStepCapSummary: (ctx, args, session, project, history, agent, steps, cap) => Promise + - function insertMistakeRecoverySentinel: (ctx, sessionId, chatId, opts) => Promise +- `apps/server/src/services/inference/sentinels.ts` + - function detectDoomLoop: (recentToolCalls) => void + - function isCapHitSentinel: (m) => boolean + - function isDoomLoopSentinel: (m) => boolean + - function isMistakeRecoverySentinel: (m) => boolean + - function isAnySentinel: (m) => boolean + - const DOOM_LOOP_THRESHOLD + - _...1 more_ +- `apps/server/src/services/inference/step-decision.ts` + - function decideStep: (input) => PreStepDecision + - function decidePostToolAction: (action, mistakeTracker) => PostToolDecision + - type PreStepDecision + - type PostToolDecision +- `apps/server/src/services/inference/stream-error-classifier.ts` — function classifyStreamError: (err) => StreamErrorKind, type StreamErrorKind +- `apps/server/src/services/inference/stream-phase-adapter.ts` + - function samplerOptsFromAgent: (agent) => SamplerOpts + - function streamCompletion: (ctx, model, messages, opts, onDelta) => void + - interface StreamAdapterContext + - interface StreamOptions + - type SamplerOpts + - const STALL_TIMEOUT_MS +- `apps/server/src/services/inference/stream-phase.ts` — function executeStreamPhase: (ctx, args, session, messages, state, agent, // v1.11.8, web_search and web_fetch are stripped from the + // tool list sent to the LLM, so the model can't even attempt them. + webToolsEnabled) => Promise +- `apps/server/src/services/inference/tool-call-parser.ts` + - function stripToolMarkup: (text, opts?) => string + - function extractToolCallBlocks: (buffer, log?) => ToolCallExtraction + - interface ParsedCall + - interface ToolCallExtraction +- `apps/server/src/services/inference/tool-phase.ts` — function executeToolPhase: (ctx, args, result, startedAt, session, projectRoot, agent?) => Promise, interface ToolPhaseResult +- `apps/server/src/services/inference/tool-shim.ts` + - function extractToolCalls: (text) => ParsedToolCall[] + - function hasToolCallMarkup: (text) => boolean + - interface ParsedToolCall +- `apps/server/src/services/inference/tool-suggestions.ts` + - function levenshtein: (a, b) => number + - function suggestToolName: (name, available) => string | null + - function formatUnknownToolError: (name, available) => string +- `apps/server/src/services/inference/turn-config.ts` + - function resolveTurnConfig: (agent) => TurnConfig + - interface TurnConfig + - const MAX_STEPS +- `apps/server/src/services/inference/turn.ts` + - function runAssistantTurn: (ctx, args) => Promise + - function runInference: (ctx, sessionId, chatId, assistantMessageId, signal?) => Promise + - function createInferenceRunner: (ctx, 'publishUser'>, publishUserFn, frame) => void +- `apps/server/src/services/mcp-client.ts` + - function initialize: (entries, logger) => Promise + - function callTool: (prefixedName, args, unknown>) => Promise + - function getTools: () => ToolDef>[] + - function getMcpServers: () => Array< + - function shutdown: () => Promise + - function wrapMcpTool: (serverName, mcpTool) => ToolDef> + - _...2 more_ +- `apps/server/src/services/mcp-config.ts` + - function substituteEnvVars: (value, log, unsetVars?) => unknown + - function loadMcpConfig: (configPath, log) => McpServerEntry[] + - interface McpServerEntry + - type McpServerConfig +- `apps/server/src/services/memory/entries.ts` — function parseMemoryEntries: (fileName, markdown) => MemoryEntry[], interface MemoryEntry +- `apps/server/src/services/memory/paths.ts` + - function getMemoryRoot: (projectRoot) => string + - function getTopicDir: (root, topic) => string + - function ensureMemoryScaffold: (root) => Promise + - type MemoryTopic +- `apps/server/src/services/memory/prompt.ts` — function formatMemoryBlock: (entries) => string +- `apps/server/src/services/memory/recall.ts` — function rankByRelevance: (query, entries) => MemoryEntry[], function loadMemoryForSession: (projectRoot, _sessionId?, query?) => Promise +- `apps/server/src/services/memory/scan.ts` + - function scanMemoryScopes: (scope) => Promise + - function scanProjectMemory: (projectRoot) => Promise + - interface MemoryScope +- `apps/server/src/services/memory/store.ts` — function readTopicFiles: (root, topic) => Promise>, function writeEntry: (root, topic, title, content, tags) => Promise +- `apps/server/src/services/model-context.ts` + - function configureModelContext: (opts) => void + - function getModelContext: (model) => Promise + - function invalidateModelContext: (model?) => void + - interface ModelContext +- `apps/server/src/services/path_guard.ts` + - function resolveProjectRoot: (projectPath) => Promise + - function pathGuard: (projectRoot, requested, extraRoots) => Promise + - class PathScopeError +- `apps/server/src/services/project_bootstrap.ts` + - function sanitizeFolderName: (raw) => string + - function bootstrapProject: (config, log, options) => Promise + - class BootstrapNameError + - class BootstrapCollisionError + - class BootstrapPathError + - interface BootstrapResult +- `apps/server/src/services/read_tab_by_number.ts` + - function executeReadTabByNumber: (input, sql, sessionId) => Promise + - type ReadTabByNumberInputT + - const readTabByNumber: ToolDef +- `apps/server/src/services/secret_guard.ts` + - function isSecretPath: (relPath) => boolean + - function filterSecretEntries: (entries, pathOf) => void + - class SecretBlockedError + - const DEFAULT_SECURITY_IGNORE_FILETYPES: ReadonlyArray +- `apps/server/src/services/skill-invoke.ts` + - function runSkillInvokeTransaction: (sql, args) => Promise< + - function buildSkillInvokeSyntheticFrames: (chatId, result, toolCall, skillBody) => SkillInvokeSessionFrame[] + - function buildSkillInvokeUserFrames: (chatId, userMessageId, userText) => SkillInvokeSessionFrame[] + - interface SkillInvokeTransactionResult + - interface SkillInvokeToolCall + - type SkillInvokeSessionFrame + - _...1 more_ +- `apps/server/src/services/skills.ts` + - function listSkills: () => Promise + - function findSkills: (query) => Promise + - function getSkillBody: (name) => Promise + - function getSkillResource: (name, relativePath) => Promise + - interface Skill + - interface SkillSummary + - _...2 more_ +- `apps/server/src/services/synthesisPipeline.ts` + - function runSynthesisPass: (p) => Promise + - interface SynthesisParams + - const SYNTHESIS_TOOLS: ReadonlySet +- `apps/server/src/services/system-prompt.ts` + - function loadContainerGuidance: () => Promise + - function getContainerGuidance: () => Promise + - function _resetContainerGuidanceCacheForTests: () => void + - function _resetPrefixObserverForTests: () => void + - function buildSystemPromptWithFingerprint: (project, session, agent) => Promise< + - function buildSystemPrompt: (project, session, agent) => Promise + - _...2 more_ +- `apps/server/src/services/task-model.ts` — function taskModelCompletion: (opts) => Promise +- `apps/server/src/services/task-search-rewrite.ts` — function rewriteSearchQuery: (userMessage) => Promise +- `apps/server/src/services/tools/codecontext/factory.ts` — function makeCodecontextTool: (opts, unknown>; + mapArgs) => void +- `apps/server/src/services/tools/registry.ts` — function appendMcpTools: (mcpTools) => void, function toolJsonSchemas: () => ToolJsonSchema[] +- `apps/server/src/services/tools/tiers.ts` + - function resolveToolTier: (tier) => readonly string[] + - const CORE_TOOL_NAMES + - const STANDARD_TOOL_NAMES +- `apps/server/src/services/truncate.ts` + - function storeTruncation: (fullContent) => Promise + - function readTruncation: (id) => Promise + - function truncateIfNeeded: (args) => Promise< + - function cleanupTruncations: (args, msg) => void + - const TRUNCATION_DIR + - const TRUNCATION_TTL_MS + - _...1 more_ +- `apps/server/src/services/url_guard.ts` — function isPublicUrl: (input) => UrlGuardResult, interface UrlGuardResult +- `apps/server/src/services/web/html-to-md.ts` — function htmlToMarkdown: (sourceHtml) => string +- `apps/server/src/services/web_fetch.ts` + - function executeWebFetch: (input, fetcher) => Promise + - type WebFetchInputT + - type WebFetchOutput + - const webFetch: ToolDef +- `apps/server/src/services/web_search.ts` + - function executeWebSearch: (input, searxngUrl, fetcher) => Promise + - interface WebSearchOutput + - type WebSearchInputT + - const webSearch: ToolDef +- `apps/server/src/utils/string-utils.ts` — function stripQuotes: (s) => string +- `apps/web/src/api/client.ts` + - class ApiError + - interface AgentSessionInfo + - interface CoderCheckpoint + - interface CoderRestoreResult + - const api +- `apps/web/src/data/acp-provider-catalog.ts` + - function buildAcpProviderConfigPatch: (entry) => ProviderConfigPatch + - interface AcpCatalogEntry + - const ACP_PROVIDER_CATALOG: AcpCatalogEntry[] +- `apps/web/src/hooks/terminal/useTerminalFit.ts` + - function cellSize: (term, container) => void + - function useTerminalFit: ({...}, containerRef, sessionId, paneId }) => TerminalFit + - interface TerminalFit +- `apps/web/src/hooks/terminal/useTerminalSelection.ts` + - function useTerminalSelection: ({...}, containerRef, sessionId, paneId, label, send, }) => TerminalSelection + - interface TerminalSelectionActions + - interface TerminalSelection +- `apps/web/src/hooks/terminal/useTerminalSocket.ts` + - function useTerminalSocket: ({...}, sessionId, paneId, fit, getSize, setSize, }) => TerminalSocket + - interface TerminalSocket + - type ConnState +- `apps/web/src/hooks/useActivePane.ts` + - function setActivePaneInfo: (next) => void + - function clearActivePane: () => void + - function useActivePane: () => ActivePaneSnapshot + - interface ActivePaneSnapshot +- `apps/web/src/hooks/useAgentSessions.ts` — function refreshAgentSessions: (sessionId) => Promise, function useAgentSessions: (sessionId) => void +- `apps/web/src/hooks/useAgentStatus.ts` + - function useAgentStatus: () => void + - interface AgentStatusEntry + - type AgentStatus +- `apps/web/src/hooks/useArtifactDownload.ts` — function useArtifactDownload: (chatId, messageId, format) => void +- `apps/web/src/hooks/useChatStatus.ts` + - function useChatStatus: (chatId) => DerivedStatus + - type RawStatus + - type DerivedStatus +- `apps/web/src/hooks/useChatThroughput.ts` + - function recordUsage: (chatId, data) => void + - function useChatThroughput: (chatId) => ThroughputSample | null + - interface ThroughputSample +- `apps/web/src/hooks/useCoderUserEvents.ts` — function useCoderUserEvents: () => void +- `apps/web/src/hooks/useDiffPreferences.ts` — function useDiffPreferences: () => void, interface DiffPreferences +- `apps/web/src/hooks/useGitDiff.ts` — function useGitDiff: (projectId) => void +- `apps/web/src/hooks/useLongPress.ts` — function useLongPress: (callback) => void +- `apps/web/src/hooks/useProjectGit.ts` — function useProjectGit: (projectId) => GitMeta | null +- `apps/web/src/hooks/useProviderSnapshot.ts` — function refreshProviderSnapshot: (cwd?) => Promise, function useProviderSnapshot: (cwd?) => ProviderSnapshotEntry[] | null +- `apps/web/src/hooks/usePullToRefresh.ts` — function usePullToRefresh: (onRefresh) => void +- `apps/web/src/hooks/useSessionChats.ts` + - function useSessionChats: (sessionId, opts) => UseSessionChatsResult + - interface UseSessionChatsOpts + - interface UseSessionChatsResult +- `apps/web/src/hooks/useSessionStream.ts` — function useSessionStream: (sessionId) => void +- `apps/web/src/hooks/useSessions.ts` — function useSessions: (projectId) => void +- `apps/web/src/hooks/useSidebar.ts` — function useSidebar: () => void +- `apps/web/src/hooks/useSkills.ts` — function useSkills: () => void +- `apps/web/src/hooks/useUserEvents.ts` — function useUserEvents: () => void +- `apps/web/src/hooks/useViewport.ts` — function useViewport: () => ViewportSnapshot, interface ViewportSnapshot +- `apps/web/src/hooks/useWorkspacePanes.ts` + - function activePaneChatId: (pane) => string | undefined + - function useWorkspacePanes: (sessionId) => UseWorkspacePanesResult + - interface UseWorkspacePanesResult + - const MAX_PANES +- `apps/web/src/hooks/wsReconnectToast.ts` — function createWsReconnectToast: (opts) => WsReconnectToast, interface WsReconnectToast +- `apps/web/src/lib/anim.ts` + - function getAnimBg: () => boolean + - function setAnimBg: (on) => void + - function setAnimDensity: (v) => void + - function setAnimSpeed: (v) => void + - function setAnimOpacity: (v) => void + - function useAnimBg: () => boolean + - _...3 more_ +- `apps/web/src/lib/attachments.ts` + - function looksBinary: (content) => boolean + - function inferLanguage: (filename) => string | null + - function flattenToMessage: (attachments, text) => string + - type Attachment + - const MAX_FILE_SIZE_BYTES + - const PASTE_INLINE_MAX_LINES + - _...1 more_ +- `apps/web/src/lib/coder-session.ts` — function isCoderSessionName: (name) => boolean +- `apps/web/src/lib/coder-tools.ts` + - function wireToolCallToRun: (wire) => ToolRun + - function mergeWireToolCall: (existing, incoming, unknown> }) => CoderToolCallWire[] + - interface AcpWireMeta + - interface CoderToolCallWire +- `apps/web/src/lib/format.ts` + - function relTime: (iso) => string + - function formatRelative: (iso) => string + - function formatAgo: (iso) => string +- `apps/web/src/lib/model-label.ts` — function formatModelLabel: (raw) => string +- `apps/web/src/lib/modelName.ts` — function shortenModelName: (model) => string | null +- `apps/web/src/lib/permission-mode.ts` + - function nativeModeForPermission: (mode, modes, defaultModeId) => string | null + - function permissionForModeId: (modeId, modes) => PermissionMode + - function availablePermissionModes: (modes) => Array< + - type PermissionMode + - const PERMISSION_LABELS: Record +- `apps/web/src/lib/projectUrls.ts` — function giteaUrlFor: (project) => string +- `apps/web/src/lib/slash-command.ts` + - function isSlashCommandToken: (value) => boolean + - function slashQuery: (value) => string + - function parseSlashInput: (text) => void + - function mergeCommandsByName: (...lists) => T[] + - interface SlashCommandItem +- `apps/web/src/lib/terminal-protocol.ts` + - function encodeInput: (text) => Uint8Array + - function encodeResize: (cols, rows) => string + - function parseServerFrame: (data) => ServerControlFrame | null + - type ServerControlFrame +- `apps/web/src/lib/theme.ts` + - function isThemeId: (s) => s is ThemeId + - function applyTheme: (id, mode) => void + - function setTheme: (id, mode) => Promise + - function useTheme: () => ThemeState + - interface ThemeMeta + - type ThemeId + - _...5 more_ +- `apps/web/src/lib/utils.ts` — function cn: (...inputs) => void +- `apps/web/src/utils/diff-layout.ts` + - function parseDiff: (diffBody) => ParsedDiffFile[] + - function buildSplitRows: (file) => SplitRow[] + - function reconstructNewContent: (hunks) => string + - interface DiffLine + - interface DiffHunk + - interface ParsedDiffFile + - _...3 more_ +- `conductor/src/contracts.ts` + - function produceContract: (contracts) => string + - function reviewContract: (contracts) => string + - type Contract + - const EVIDENCE_PRODUCE + - const EVIDENCE_REVIEW + - const YAGNI_PRODUCE + - _...1 more_ +- `conductor/src/dispatch.ts` + - function loadPersona: (agent) => Promise + - function dispatchAgent: (agent, task, opts) => Promise + - function cleanOutput: (raw) => string +- `conductor/src/flow.ts` — function runFlow: (flow, input, opts) => Promise, interface RunOptions +- `conductor/src/flows/_util.ts` — function q, function repoLine +- `conductor/src/flows/index.ts` + - function describeFlows: () => string + - function getFlow: (name) => Flow | undefined + - const FLOWS: Record + - const FLOW_NAMES: string[] +- `conductor/src/render.ts` — function slugify: (s) => string +- `conductor/src/spine.ts` + - function readBand: (input) => Band + - function fastNote: (ctx) => string + - function buildSpineFlow: (spine) => Flow +- `data/skills/superpowers/systematic-debugging/condition-based-waiting-example.ts` + - function waitForEvent: (threadManager, threadId, eventType, timeoutMs) => Promise + - function waitForEventCount: (threadManager, threadId, eventType, count, timeoutMs) => Promise + - function waitForEventMatch: (threadManager, threadId, predicate) => void +- `packages/ion/src/cli/commands/abandon.ts` — function abandonCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/approve.ts` — function approveCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/cleanup.ts` — function cleanupCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/convert.ts` — function convertCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/list.ts` — function listCommand: (_args, options) => Promise +- `packages/ion/src/cli/commands/reject.ts` — function rejectCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/resume.ts` — function resumeCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/run.ts` — function runCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/runs.ts` — function runsCommand: (args, options) => Promise +- `packages/ion/src/cli/commands/status.ts` — function statusCommand: (_args, options) => Promise +- `packages/ion/src/cli/commands/validate.ts` — function validateCommand: (args, options) => Promise +- `packages/ion/src/cli/index.ts` — function main: (argv) => void +- `packages/ion/src/cli/utils.ts` + - function formatDuration: (ms) => string + - function formatTimestamp: (date) => string + - function truncate: (str, max) => string + - function printTable: (rows, unknown>[], columns) => void + - function printJson: (data) => void + - function parseArgs: (argv) => void + - _...3 more_ +- `packages/ion/src/engine/command-validation.ts` — function isValidCommandName: (name) => boolean +- `packages/ion/src/engine/condition-evaluator.ts` — function evaluateCondition: (expression, nodeOutputs, Record>) => boolean, class ConditionError +- `packages/ion/src/engine/dag-executor.ts` + - function buildTopologicalLayers: (nodes) => DagNode[][] + - function checkTriggerRule: (node, nodeOutputs, NodeOutput>) => 'run' | 'skip' + - function executeNodeInternal: (node, deps, platform, conversationId, cwd, config, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - function executeScriptNode: (node, cwd, envVars, string>, artifactsDir) => Promise + - function handleApprovalNode: (node, deps, platform, conversationId, workflowRunId, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - function handleLoopNode: (node, deps, platform, conversationId, cwd, config, nodeOutputs, NodeOutput>, workflowVariables, unknown>) => Promise + - _...2 more_ +- `packages/ion/src/engine/event-emitter.ts` + - function getWorkflowEventEmitter: () => WorkflowEventEmitter + - class WorkflowEventEmitter + - interface WorkflowEventBase + - interface WorkflowStartedEvent + - interface WorkflowCompletedEvent + - interface WorkflowFailedEvent + - _...11 more_ +- `packages/ion/src/engine/executor-shared.ts` + - function substituteWorkflowVariables: (template, context) => string + - function buildPromptWithContext: (template, context, issueContext?) => string + - function classifyError: (error) => ErrorClassification + - function safeSendMessage: (platform, conversationId, message, metadata?, unknown>) => Promise + - function detectCompletionSignal: (output, until) => boolean + - function stripCompletionTags: (output, until) => string + - _...5 more_ +- `packages/ion/src/engine/executor.ts` + - function executeWorkflow: (deps, platform, conversationId, cwd, workflow, userMessage, opts) => Promise + - function hydrateResumableRun: (deps, candidate) => Promise + - function resolveProjectPaths: (_deps, cwd, workflowRunId, codebaseId?) => ProjectPaths + - interface WorkflowExecutionOptions + - interface WorkflowExecutionResult + - interface HydratedResumableRun + - _...1 more_ +- `packages/ion/src/engine/model-validation.ts` + - function isLiteralSpec: (spec) => spec is LiteralModelSpec + - function buildAiProfile: (opts) => AiProfile + - function resolveModelSpec: (profile, modelRef) => LiteralModelSpec + - interface LiteralModelSpec + - interface ModelAliasPreset + - interface AiProfileTiers + - _...2 more_ +- `packages/ion/src/engine/output-ref.ts` + - function declaredFieldsFromSchema: (outputFormat, unknown> | string | undefined) => Set + - function resolveNodeOutputField: (nodeOutput, unknown>, nodeId, field, declaredFields?) => OutputRefResult + - class OutputRefError + - interface OutputRefResult + - type OutputRefKind +- `packages/ion/src/engine/utils.ts` + - function substituteWorkflowVariables: (template, variables, unknown>) => string + - function substituteNodeOutputRefs: (prompt, nodeOutputs, NodeOutput>, escapedForBash) => string + - function resolveNodeOutputField: (output, field) => string + - function buildPromptWithContext: (prompt, variables, unknown>, nodeOutputs, NodeOutput>, escapedForBash) => string + - function evaluateCondition: (condition, variables, unknown>) => boolean + - function classifyError: (error) => ErrorCategory + - _...10 more_ +- `packages/ion/src/format/sop-discovery.ts` — function discoverSopFiles: (cwd, globFn) => Promise, type GlobFn +- `packages/ion/src/format/sop-parser.ts` + - function parseSopContent: (markdown) => SopDocument + - interface SopParameter + - interface SopStep + - interface SopDocument +- `packages/ion/src/format/sop-to-yaml.ts` — function convertSopToWorkflowYaml: (sop) => string +- `packages/ion/src/schema/dag-node.ts` + - function isBashNode: (node) => node is BashNode + - function isScriptNode: (node) => node is ScriptNode + - function isLoopNode: (node) => node is LoopNode + - function isApprovalNode: (node) => node is ApprovalNode + - function isCancelNode: (node) => node is CancelNode + - function isPromptNode: (node) => node is PromptNode + - _...27 more_ +- `packages/ion/src/store/fs-store.ts` — function createFsStore: (basePath) => IWorkflowStore +- `packages/ion/src/store/pg-store.ts` — function createPostgresStore: (connectionString) => Promise +- `packages/ion/src/store/sqlite-store.ts` — function createSqliteStore: (dbPath) => Promise diff --git a/.codesight/middleware.md b/.codesight/middleware.md new file mode 100644 index 0000000..176f1e9 --- /dev/null +++ b/.codesight/middleware.md @@ -0,0 +1,23 @@ +# Middleware + +## auth +- auth — `apps/booterm/src/auth.ts` +- authoring — `apps/coder/src/conductor/flows/authoring.ts` +- turn-guard.test — `apps/coder/src/services/backends/__tests__/turn-guard.test.ts` +- turn-guard — `apps/coder/src/services/backends/turn-guard.ts` +- get_middleware — `apps/server/src/services/tools/codecontext/get_middleware.ts` +- authoring — `conductor/src/flows/authoring.ts` + +## custom +- write_guard.test — `apps/coder/src/services/__tests__/write_guard.test.ts` +- write_guard_fuzz.test — `apps/coder/src/services/__tests__/write_guard_fuzz.test.ts` +- edit-guards-imports — `apps/coder/src/services/edit-guards-imports.ts` +- write_guard — `apps/coder/src/services/write_guard.ts` +- secret_guard.test — `apps/server/src/services/__tests__/secret_guard.test.ts` +- path_guard — `apps/server/src/services/path_guard.ts` +- secret_guard — `apps/server/src/services/secret_guard.ts` +- url_guard — `apps/server/src/services/url_guard.ts` + +## validation +- edit-guards — `apps/coder/src/services/edit-guards.ts` +- path_guard.test — `apps/server/src/services/__tests__/path_guard.test.ts` diff --git a/.codesight/routes.md b/.codesight/routes.md new file mode 100644 index 0000000..941e1fe --- /dev/null +++ b/.codesight/routes.md @@ -0,0 +1,141 @@ +# Routes + +## CRUD Resources + +- **`/api/battles`** GET | POST | GET/:id → Battle +- **`/api/runs`** GET | POST | GET/:id → Run +- **`/api/tasks`** GET | POST | GET/:id → Task +- **`/api/chats/:id/messages`** GET | POST | GET/:id | DELETE/:id → Message +- **`/api/projects`** GET | POST | GET/:id | PATCH/:id | DELETE/:id → Project +- **`/api/sessions`** GET/:id | PATCH/:id | DELETE/:id → Session + +## Other Routes + +### fastify + +- `GET` `/api/term/health` params() +- `POST` `/api/term/sessions/:sid/panes/:pid/start` params(sid, pid) [auth] +- `POST` `/api/term/sessions/:sid/panes/:pid/kill` params(sid, pid) [auth] +- `GET` `/ws/term/sessions/:sid/panes/:pid` params(sid, pid) [auth] +- `GET` `/api/health` params() [auth, db, queue, ai] +- `GET` `/api/sessions/:sessionId/agent-sessions` params(sessionId) [auth, db] +- `POST` `/api/battles/generate-prompt` params() [auth, db] +- `POST` `/api/battles/:id/stop` params(id) [auth, db] +- `GET` `/api/battles/:id/analysis` params(id) [auth, db] +- `POST` `/api/battles/:id/analyze` params(id) [auth, db] +- `PATCH` `/api/battles/:id/winner` params(id) [auth, db] +- `GET` `/api/battles/:id/contestants/:cid/diff` params(id, cid) [auth, db] +- `POST` `/api/battles/:id/cross-examine` params(id) [auth, db] +- `GET` `/api/sessions/:sessionId/checkpoints` params(sessionId) [auth, db] +- `POST` `/api/sessions/:sessionId/checkpoints/:checkpointId/restore` params(sessionId, checkpointId) [auth, db] +- `GET` `/api/inbox` params() [auth, db] +- `POST` `/api/inbox/:id/retry` params(id) [auth, db] +- `POST` `/api/chats/:chatId/close` params(chatId) [auth, db] +- `POST` `/api/sessions/:sessionId/close` params(sessionId) [auth, db] +- `GET` `/api/sessions/:sessionId/messages` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/messages` params(sessionId) [auth, db, queue] +- `POST` `/api/chats/:id/answer_user_input` params(id) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/stop` params(sessionId) [auth, db, queue] +- `GET` `/api/sessions/:sessionId/pending` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/pending/create` params(sessionId) [auth, db, queue] +- `POST` `/api/sessions/:sessionId/pending/apply` params(sessionId) [auth, db, queue] +- `POST` `/api/pending/:id/apply` params(id) [auth, db, queue] +- `POST` `/api/pending/:id/reject` params(id) [auth, db, queue] +- `POST` `/api/pending/:id/rewind` params(id) [auth, db, queue] +- `GET` `/api/providers/snapshot` params() [db, cache] +- `GET` `/api/providers/config` params() [db, cache] +- `PATCH` `/api/providers/config` params() [db, cache] +- `POST` `/api/providers/refresh` params() [db, cache] +- `GET` `/api/providers/:id/diagnostic` params(id) [db, cache] +- `POST` `/api/runs/:id/cancel` params(id) [auth, db] +- `POST` `/api/sessions/:sessionId/skill_invoke` params(sessionId) [auth, db, queue] +- `GET` `/api/stats/costs` params() [auth, db] +- `POST` `/api/tasks/:id/cancel` params(id) [auth, db, cache, ai] +- `GET` `/api/tasks/:id/permission` params(id) [auth, db, cache, ai] +- `POST` `/api/tasks/:id/permission` params(id) [auth, db, cache, ai] +- `GET` `/api/tasks/:id/commands` params(id) [auth, db, cache, ai] +- `GET` `/api/sessions/:sessionId/worktree-risk` params(sessionId) [auth, db] +- `POST` `/api/sessions/:sessionId/worktree-stash` params(sessionId) [auth, db] +- `GET` `/api/ws/sessions/:sessionId` params(sessionId) [auth, db] +- `GET` `/api/ws/user` params() [auth, db] +- `GET` `/api/projects/:id/agents` params(id) [db, cache] +- `POST` `/api/chats/:id/messages/:msg_id/artifacts/download` params(id, msg_id) [auth, db] +- `GET` `/api/chats/:id/messages/:msg_id/html_artifact` params(id, msg_id) [auth, db] +- `GET` `/api/projects/:project_id/artifacts/:filename` params(project_id, filename) [auth, db] +- `GET` `/api/sessions/:id/chats` params(id) [auth, db] +- `POST` `/api/sessions/:id/chats` params(id) [auth, db] +- `PATCH` `/api/chats/:id` params(id) [auth, db] +- `POST` `/api/sessions/:id/chats/archive-all` params(id) [auth, db] +- `GET` `/api/sessions/:id/chats/open-count` params(id) [auth, db] +- `POST` `/api/chats/:id/archive` params(id) [auth, db] +- `POST` `/api/chats/:id/unarchive` params(id) [auth, db] +- `DELETE` `/api/chats/:id` params(id) [auth, db] +- `POST` `/api/chats/:id/fork` params(id) [auth, db] +- `POST` `/api/chats/:id/discard_stale` params(id) [auth, db] +- `GET` `/api/coder/ws/sessions/:sessionId` params(sessionId) [auth] +- `ALL` `/api/coder/*` params() [auth] +- `GET` `/api/settings/inference` params() [cache] +- `PATCH` `/api/settings/inference` params() [cache] +- `GET` `/api/sessions/:id/messages` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/messages/:message_id/regenerate` params(id, message_id) [auth, db, queue] +- `POST` `/api/chats/:id/compact` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/stop` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/continue` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/force_send` params(id) [auth, db, queue] +- `POST` `/api/chats/:id/grant_read_access` params(id) [auth, db, queue] +- `GET` `/api/models` params() +- `POST` `/api/projects/create` params() [auth, db] +- `POST` `/api/projects/:id/archive` params(id) [auth, db] +- `POST` `/api/projects/:id/unarchive` params(id) [auth, db] +- `GET` `/api/projects/available` params() [auth, db] +- `GET` `/api/projects/:id/list_dir` params(id) [auth, db] +- `GET` `/api/projects/:id/view_file` params(id) [auth, db] +- `GET` `/api/projects/:id/git` params(id) [auth, db] +- `GET` `/api/projects/:id/git/diff` params(id) [auth, db] +- `POST` `/api/projects/:id/git/stage` params(id) [auth, db] +- `POST` `/api/projects/:id/git/unstage` params(id) [auth, db] +- `POST` `/api/projects/:id/git/commit` params(id) [auth, db] +- `POST` `/api/projects/:id/git/discard` params(id) [auth, db] +- `POST` `/api/projects/:id/write_file` params(id) [auth, db] +- `GET` `/api/projects/:id/files` params(id) [auth, db] +- `GET` `/api/projects/:id/sessions` params(id) [auth, db] +- `POST` `/api/projects/:id/sessions` params(id) [auth, db] +- `PATCH` `/api/sessions/:id/workspace` params(id) [auth, db] +- `POST` `/api/projects/:id/sessions/archive-all` params(id) [auth, db] +- `GET` `/api/projects/:id/sessions/open-count` params(id) [auth, db] +- `POST` `/api/sessions/:id/archive` params(id) [auth, db] +- `POST` `/api/sessions/:id/unarchive` params(id) [auth, db] +- `GET` `/api/settings` params() [db] +- `PATCH` `/api/settings` params() [db] +- `GET` `/api/sidebar` params() [auth, db] +- `GET` `/api/skills` params() [auth, db, queue] +- `POST` `/api/chats/:id/skill_invoke` params(id) [auth, db, queue] +- `GET` `/api/tools/cost_stats` params() [auth, db] +- `GET` `/api/ws/sessions/:id` params(id) [auth, db] + +### go-net-http + +- `GET` `/health` params() [queue] +- `POST` `/v1/get_codebase_overview` params() [queue] +- `POST` `/v1/get_file_analysis` params() [queue] +- `POST` `/v1/get_symbol_info` params() [queue] +- `POST` `/v1/search_symbols` params() [queue] +- `POST` `/v1/get_dependencies` params() [queue] +- `POST` `/v1/watch_changes` params() [queue] +- `POST` `/v1/get_semantic_neighborhoods` params() [queue] +- `POST` `/v1/get_framework_analysis` params() [queue] +- `POST` `/v1/get_symbol_details` params() [queue] +- `POST` `/v1/get_call_graph` params() [queue] +- `POST` `/v1/get_blast_radius` params() [queue] + +## WebSocket Events + +- `WS` `message` — `apps/booterm/src/ws/attach.ts` +- `WS` `close` — `apps/booterm/src/ws/attach.ts` +- `WS` `message` — `apps/coder/src/cli.ts` +- `WS` `error` — `apps/coder/src/cli.ts` +- `WS` `close` — `apps/coder/src/cli.ts` +- `WS` `close` — `apps/coder/src/routes/ws.ts` +- `WS` `error` — `apps/coder/src/routes/ws.ts` +- `WS` `close` — `apps/server/src/routes/ws.ts` +- `WS` `error` — `apps/server/src/routes/ws.ts` diff --git a/.codesight/schema.md b/.codesight/schema.md new file mode 100644 index 0000000..5048564 --- /dev/null +++ b/.codesight/schema.md @@ -0,0 +1,157 @@ +# Schema + +### pending_changes +- id: uuid (pk) +- session_id: uuid (required, fk) +- task_id: uuid (fk) +- file_path: text (required) +- operation: text (required) +- diff: text (required) +- status: text (required) + +### tasks +- id: uuid (pk) +- project_id: uuid (required, fk) +- parent_task_id: uuid (fk) +- state: text (required) +- input: text (required) +- output_summary: text +- agent: text +- model: text +- execution_path: text +- cost_tokens: integer +- started_at: timestamp(tz) +- ended_at: timestamp(tz) + +### available_agents +- name: text (pk) +- install_path: text +- version: text +- supports_acp: boolean (required) +- last_probed_at: timestamp(tz) + +### agent_sessions +- session_id: uuid (required, fk) +- agent: text (required) +- backend: text (required) +- agent_session_id: text (fk) +- server_port: integer +- status: text (required) +- last_active_at: timestamp(tz) + +### worktrees +- id: uuid (pk) +- session_id: uuid (fk) +- project_id: uuid (fk) +- path: text (required) +- branch: text +- base_commit: text +- slug: text +- status: text (required) + +### checkpoints +- id: uuid (pk) +- chat_id: uuid (required, fk) +- session_id: uuid (fk) +- worktree_id: uuid (fk) +- message_id: uuid (fk) + +### claude_session_entries +- id: bigint(auto) (pk) +- project_key: text (required) +- session_id: text (required, fk) +- subpath: text (required) + +### flow_runs +- id: uuid (pk) +- project_id: uuid (required, fk) +- flow_name: text (required) +- band: text (required) +- model: text (required) +- status: text (required) +- input: jsonb (required) +- report: text +- error: text + +### flow_steps +- id: uuid (pk) +- run_id: uuid (required, fk) +- step_id: text (required, fk) +- kind: text (required) +- agent: text +- status: text (required) +- task_id: uuid (fk) +- chat_id: uuid (fk) +- input: text +- output: text +- error: text + +### battles +- id: uuid (pk) +- project_id: uuid (required, fk) +- battle_type: text (required) +- prompt: text (required) +- status: text (required) +- winner_contestant_id: uuid (fk) +- results_path: text +- error: text + +### contestants +- id: uuid (pk) +- battle_id: uuid (required, fk) +- identity: text (required) +- model: text (required) +- lane: text (required) +- task_id: uuid (fk) +- worktree_id: uuid (fk) +- status: text (required) +- duration_ms: integer +- tokens_per_sec: float8 +- cost_tokens: integer +- result_path: text +- error: text + +### cross_examinations +- id: uuid (pk) +- battle_id: uuid (required, fk) +- identity: text (required) +- model: text (required) +- verdict: text + +### projects +- id: uuid (pk) +- name: text (required) +- path: text (required) +- added_at: timestamp(tz) (required) +- last_session_id: uuid (fk) + +### sessions +- id: uuid (pk) +- project_id: uuid (required, fk) +- name: text (required) +- model: text (required) +- system_prompt: text (required) + +### messages +- id: uuid (pk) +- session_id: uuid (required, fk) +- role: text (required) +- content: text (required) +- status: text (required) +- last_seq: integer (required) + +### message_parts +- id: uuid (pk) +- message_id: uuid (required, fk) +- sequence: integer (required) +- kind: text (required) +- payload: jsonb (required) + +### settings +- value: jsonb (required) + +### chats +- id: uuid (pk) +- session_id: uuid (required, fk) +- name: text +- status: text (required) diff --git a/.omo/drafts/openspec-cleanup.md b/.omo/drafts/openspec-cleanup.md new file mode 100644 index 0000000..ca376dd --- /dev/null +++ b/.omo/drafts/openspec-cleanup.md @@ -0,0 +1,89 @@ +# Draft: openspec-cleanup + +## Cross-Reference: Git Tags vs openspec Batches + +### Archived Stub Files — Tag Verification + +| Stub File | Claims Version | Actual Tag | Verdict | +|---|---|---|---| +| `v1.13.12-skills-audit.md` (57B) | v1.13.12 | `v1.13.14-skills-audit` | **WRONG** — off by 2 versions | +| `v1.13.15-codecontext-synth.md` (62B) | v1.13.15 | `v1.13.15-codecontext-synth` | ✅ correct | +| `v1.13.17-cross-repo-reads.md` (61B) | v1.13.17 | `v1.13.17-cross-repo-reads` | ✅ correct | +| `v1.13.18-codecontext-file-path.md` (66B) | v1.13.18 | `v1.13.18-codecontext-file-path` | ✅ correct | +| `v1.13.20-drop-legacy-cols.md` (61B) | v1.13.20 | `v1.13.20-drop-legacy-cols` | ✅ correct | +| `v1.14-outer-loop.md` (52B) | v1.14 | `v1.14.0-outer-loop` | ⚠️ close (1.14 → 1.14.0) | +| `v1.14.1-mcp-poc.md` (51B) | v1.14.1 | `v1.14.1-mcp-poc` | ✅ correct | +| `v1.14.x-html-artifact-panes.md` (63B) | v1.14.x | `v1.13.19-html-artifact-panes` | **WRONG** — shipped as 1.13.19 | +| `v1.15-mcp-multi.md` (51B) | v1.15 | `v1.15.0-mcp-multi` | ⚠️ close (1.15 → 1.15.0) | +| `v2.0-boocoder.md` (49B) | v2.0 | `v2.0.0` | ⚠️ close (2.0 → 2.0.0) | +| `v2.2-paseo-providers.md` (222B) | v2.2 | `v2.2-paseo-providers` | ✅ correct | + +### Archived Folder Entries — Tag Verification + +| Archived Folder | Git Tag(s) | Status | +|---|---|---| +| `agent-status-normalize/` | `v2.7.6-agent-status-normalize` | ✅ shipped | +| `claude-sdk-sessionstore/` | `v2.7.5-claude-sdk-sessionstore` | ✅ shipped | +| `contracts-ssot/` | `v2.7.13-contracts-ssot` | ✅ shipped | +| `license-debt-mit/` | `v2.7.0-mit` | ✅ shipped | +| `mistake-tracker-file-ledger/` | `v2.7.4-mistake-tracker-ledger` | ✅ shipped (slug differs slightly) | +| `orchestrator/` | `v2.7.17-orchestrator` | ✅ shipped | +| `sampling-streamjson-tokens/` | `v2.7.3-sampling-streamjson-tokens` | ✅ shipped | +| `v2-3-provider-lifecycle/` | `v2.5.4-*` through `v2.5.13-*` | ✅ shipped (diff version numbering) | +| `v2-6-persistent-agent-sessions/` | `v2.6.4-*`, `v2.6.8-*` | ✅ shipped | +| `write-edit-robustness/` | `v2.7.1-write-edit-robustness` | ✅ shipped | + +### Misplaced Proposals in Archived/ + +| 2026-06-07 Folder | Git Tag? | Actually Shipped? | Should Be | +|---|---|---|---| +| `2026-06-07-boocontext/` | **None** | No | `changes/boocontext/` (partly shipped in v2.8.0) | +| `2026-06-07-eval-sandbox-agent-runtime/` | **None** | No | Merge into `changes/import-*` | +| `2026-06-07-hybrid-workflow-engine/` | **None** | No | Merge into `changes/orchestrator-flow-advanced/` | +| `2026-06-07-memory-context-engineering/` | **None** | No | Merge into `changes/memory-context/` | +| `2026-06-07-port-audit-parlant-patterns/` | **None** | No | Merge into `changes/add-behavioral-engine/` | + +## Active Batches — All Uncommitted, All Unshipped + +All 22 active batches (changes/*/) have **zero** git tags or commits referencing them. Every batch was created locally on 2026-06-07 and exists only on the filesystem. + +## High-Value Prioritization (for Implementation Plan) + +### Tier 1: Ship in Current Batch (small scope, high value) +1. **openspec-cleanup** — Fix folder structure: delete stubs, move misplaced proposals, add .openspec.yaml, populate config.yaml +2. **llama-cache-and-spec** — KV cache quantization + ngram speculative decoding (llama-server arg changes only) +3. **results-page** — New `/results` route, uses existing API endpoints +4. **token-analyzer-ui** — New `/analytics` route, uses existing DB data + +### Tier 2: Current+ Batch (moderate scope) +5. **enhanced-file-panel** — Side-by-side diff, inline comments, in-browser editing +6. **pty-enhancements** — Exit notifications, session metadata, X-Agent-Flags + +### Tier 3: Next Batch (larger scope, foundation work) +7. **memory-v2-hybrid-search** — BM25 + local embedding hybrid search +8. **orchestrator-flow-advanced** — Trigger rules, conditional branching, HITL +9. **omo-paseo-bridge** — OMO subagent visibility in Paseo + +### Tier 4: Future Batches (speculative / big effort) +10. **add-behavioral-engine** / **audit-harness-integration** / **import-llm-evaluator** / **import-pregel-engine** — Big integration efforts +11. **code-intelligence-upgrade** / **dev-workflow** / **conductor-evolution** — Platform work +12. **plugin-platform** / **ui-overhaul** / **add-3tier-memory** / **add-type-inject-mcp** — Future + +## Scope Boundaries for This Plan + +**IN SCOPE:** +- Delete 11 stub files from archived/ +- Move 5 misplaced 2026-06-07 proposals from archived/ to changes/ (with dedup) +- Add missing .openspec.yaml to 6 active batches +- Populate openspec/config.yaml with project context +- Implement Tier 1-2 high-value batches: + - llama-cache-and-spec (llama-server args) + - results-page (new route, frontend) + - token-analyzer-ui (new route, frontend + backend) + - enhanced-file-panel (frontend changes) + - pty-enhancements (backend changes) + +**OUT OF SCOPE:** +- Tier 3-4 batches (future planning) +- Full behavioral engine or Pregel state machine integration +- Plugin platform architecture diff --git a/.omo/plans/enhanced-file-panel.md b/.omo/plans/enhanced-file-panel.md new file mode 100644 index 0000000..c7abde8 --- /dev/null +++ b/.omo/plans/enhanced-file-panel.md @@ -0,0 +1,485 @@ +# Enhanced File Panel — Implementation Plan + +## TL;DR + +> **Quick Summary**: Add side-by-side diff, hide whitespace, wrap lines, expand all files, inline diff comments, and in-browser file editing to BooCode's right-rail file panel. +> +> **Deliverables**: +> - Enhanced `GitDiffView.tsx` with toolbar (layout/whitespace/wrap/expand-all toggles) +> - Split-layout diff renderer (side-by-side) +> - `useDiffPreferences` hook (localStorage persistence) +> - Inline diff comment components + Zustand store +> - File editing mode in file tree + server write endpoint +> - Server `git diff -w` support +> +> **Estimated Effort**: Medium-Large +> **Parallel Execution**: YES — 4 waves +> **Critical Path**: Wave 1 (server) → Wave 2 (diff preferences + toolbar) → Wave 3 (split layout) → Wave 4 (comments + editing) + +--- + +## Context + +### Original Request +User wants to implement these features from Paseo into BooCode's file manager: +1. Unified diff ✅ (exists) / Side by side diff ❌ +2. Hide whitespace ❌ +3. Wrap long lines ❌ +4. Expand all files ❌ (only per-file) +5. Refresh ✅ (exists) +6. Comments on specific diffs ❌ +7. File edits (editing in the file browser) ❌ + +### Research Findings +- **Paseo** (`/opt/forks/paseo`): Best reference for all features. Key files: `diff-pane.tsx`, `diff-layout.ts`, `diff-rendering.ts`, `review/surface.tsx`, `review/store.ts`, `use-changes-preferences/` +- **Existing BooCode files**: `GitDiffView.tsx`, `RightRail.tsx`, `useGitDiff.ts`, `git_diff.ts`, `FileViewerOverlay.tsx` +- Key insight: None of the web references have true inline file editing in the browser — this is new ground + +--- + +## Work Objectives + +### Core Objective +Augment the existing file panel with side-by-side diff, whitespace/wrap/expand toggles, inline comments, and inline file editing. + +### Definition of Done +- [x] `pnpm -C apps/web build` succeeds with no errors +- [x] `pnpm -C apps/server build` succeeds with no errors +- [ ] Side-by-side diff renders correctly (two aligned columns) +- [ ] Hide whitespace toggles and re-fetches diff +- [ ] Wrap lines toggles between pre / pre-wrap +- [ ] Expand/Collapse all toggles all file diffs +- [ ] Inline comments: click gutter → type → save → display thread +- [ ] File edit: double-click tree → edit → save → file changes on disk +- [ ] All preferences persist across page refresh + +### Must Have +- Side-by-side diff view +- Hide whitespace toggle (server param) +- Wrap long lines toggle (CSS) +- Expand/Collapse all file diffs +- Inline diff comments with thread UI +- In-browser file editing with save +- Preference persistence + +### Must NOT Have (Guardrails) +- No DB migration (comments are client-side) +- No new WS frames (reuse git_diff_refresh) +- No new `@boocode/contracts` types +- No multi-user comment sharing +- No git push/pull/PR operations +- No inline hunk staging + +--- + +## Verification Strategy + +### Test Decision +- **Infrastructure exists**: YES (vitest for server) +- **Automated tests**: Tests-after for new server route + `git_diff.ts` changes +- **Agent-Executed QA**: Playwright for diff interactions, curl for API endpoints + +### QA Policy +Every task includes agent-executed scenarios. Evidence saved to `.omo/evidence/`. + +--- + +## Execution Strategy + +### Waves + +``` +Wave 1 (Server — foundation): +├── Task 1: Server: whitespace param in git_diff.ts +├── Task 2: Server: POST /api/projects/:id/write_file endpoint +├── Task 3: Server tests for whitespace + write +└── [tests + typecheck] + +Wave 2 (Frontend — preferences + toolbar): +├── Task 4: useDiffPreferences hook (localStorage) +├── Task 5: GitDiffView toolbar (layout/whitespace/wrap/expand-all toggles) +├── Task 6: Wrap lines CSS + hide whitespace re-fetch +└── [pnpm build] + +Wave 3 (Frontend — split layout): +├── Task 7: Diff layout utilities (buildSplitDiffRows etc.) +├── Task 8: Side-by-side renderer in GitDiffView +├── Task 9: Line number gutter + alignment +└── [pnpm build] + +Wave 4 (Frontend — comments + file editing): +├── Task 10: InlineComment store (Zustand + localStorage) +├── Task 11: InlineReviewGutterCell + InlineReviewEditor +├── Task 12: InlineReviewThread (comment display) +├── Task 13: File editing mode in RightRail file tree +└── [pnpm build + full smoke test] +``` + +Critical Path: T1 → T2 → T4 → T5 → T7 → T8 → T10 → T11 → T12 → T13 + +--- + +## TODOs + +- [x] 1. **Server: Add `ignoreWhitespace` param to git diff** + + **What to do**: + - In `apps/server/src/services/git_diff.ts`, add `ignoreWhitespace?: boolean` to the `getGitDiff` function signature + - When `ignoreWhitespace` is true, append `'-w'` to the git diff argv call in `getGitDiff` (the main diff command, not name-status) + - Update `GET /api/projects/:id/git/diff` route in `routes/projects.ts` to accept optional query param `whitespace=1` + - The param should be optional (backward compatible) — default false + + **Files to modify**: + - `apps/server/src/services/git_diff.ts` — update `getGitDiff()` to accept and use `ignoreWhitespace` + - `apps/server/src/routes/projects.ts` — add `whitespace` query param + + **References**: + - Paseo: `useCheckoutDiffQuery({ ignoreWhitespace })` passes to server → `git diff -w` + - Existing `git_diff.ts:36-48` `runGit` function — argv pattern to follow + + **QA Scenarios**: + ``` + Scenario: Diff with whitespace changes respects ignoreWhitespace param + Tool: Bash (curl) + Preconditions: A file exists with whitespace-only changes (extra spaces) + Steps: + 1. GET /api/projects/:id/git/diff ⇒ verify diff_body includes whitespace changes + 2. GET /api/projects/:id/git/diff?whitespace=1 ⇒ verify diff_body excludes whitespace-only changes + Expected: With whitespace=1, files that only had whitespace changes show as unchanged + Evidence: .omo/evidence/task-1-whitespace.txt + ``` + +- [x] 2. **Server: Add POST /api/projects/:id/write_file endpoint** + + **What to do**: + - Add `POST /api/projects/:id/write_file` route in `routes/projects.ts` + - Accept `{ path: string, content: string }` body + - Validate path via existing `pathGuard` helper (same as git discard) + - Write file content atomically: write to `.tmp` then `rename` the file + - Return `{ ok: boolean }` on success + - Reuse the safe file-write pattern from `services/file_ops.ts` + + **Files to modify**: + - `apps/server/src/routes/projects.ts` — add POST route + - `apps/web/src/api/client.ts` — add `writeFile` method + - `apps/web/src/api/types.ts` — add write types if needed + + **References**: + - `apps/server/src/services/file_ops.ts` — existing file operations pattern + - `apps/server/src/routes/projects.ts:544-592` — git write routes (same security pattern) + - `apps/server/src/services/path_guard.ts` — path validation + + **QA Scenarios**: + ``` + Scenario: Write file content and verify on disk + Tool: Bash (curl) + Preconditions: A project exists with a writable path + Steps: + 1. POST /api/projects/:id/write_file { path: "test.txt", content: "hello" } + 2. GET /api/projects/:id/view_file?path=test.txt + Expected: Status 200, view_file returns "hello" + Evidence: .omo/evidence/task-2-write.txt + ``` + +- [x] 3. **Frontend: useDiffPreferences hook** + + **What to do**: + - Create `apps/web/src/hooks/useDiffPreferences.ts` + - Define `DiffPreferences` interface: `{ layout: 'unified'|'split', wrapLines: boolean, hideWhitespace: boolean }` + - Default: `{ layout: 'unified', wrapLines: false, hideWhitespace: false }` + - Read/write to localStorage key `boocode.diff.preferences` + - Return `{ preferences, updatePreferences, resetPreferences }` + - Zod-validate on read for forward compatibility + + **Files to create/modify**: + - Create `apps/web/src/hooks/useDiffPreferences.ts` + + **References**: + - `/opt/forks/paseo/packages/app/src/hooks/use-changes-preferences/storage.ts` — exact pattern + - `apps/web/src/hooks/useProjectGit.ts` — hooks pattern in BooCode + + **QA Scenarios**: + ``` + Scenario: Preferences persist across page refresh + Tool: Playwright + Preconditions: Page loaded + Steps: + 1. Call updatePreferences({ layout: 'split' }) + 2. Read localStorage.getItem('boocode.diff.preferences') + 3. Reload page, read preferences again + Expected: layout is 'split' after reload + Evidence: .omo/evidence/task-3-prefs.txt + ``` + +- [x] 4. **Frontend: GitDiffView toolbar with all toggles** + + **What to do**: + - Add a toolbar row inside `GitDiffView.tsx` between the mode selector and file list + - Controls (left to right): + - **Layout toggle**: two-segment button (Unified | Split) — uses `AlignJustify` / `Columns2` icons + - **Hide whitespace**: toggle button — `Pilcrow` icon, active state highlights + - **Wrap lines**: toggle button — `WrapText` icon + - **Expand/Collapse all**: toggle button — `ListChevronsUpDown` / `ListChevronsDownUp` icons + - **Refresh**: existing button (already present) + - Wire each toggle to the `useDiffPreferences` hook + - Expand all state: compute `allExpanded = files.every(f => expandedPaths.has(f.path))` + - Pass expand state as a new prop or local state + + **Files to modify**: + - `apps/web/src/components/GitDiffView.tsx` — add toolbar section, expand-all logic + + **References**: + - Paseo `diff-pane.tsx:1114-1273` — `DiffLayoutToggleGroup`, `DiffWhitespaceToggle`, `DiffFilesToolbar` + - openchamber `DiffViewToggle.tsx` — simple toggle pattern + - happy `InlineFileDiff.tsx:196-219` — `DiffStyleToggle` segment control + + **QA Scenarios**: + ``` + Scenario: All toolbar controls render and toggle + Tool: Playwright + Preconditions: Git tab active with changed files + Steps: + 1. Verify layout toggle shows "Unified" / "Split" buttons + 2. Click "Split" — verify visual change + 3. Click "Wrap" — verify wrap toggle + 4. Click "Expand all" — verify all files expand + 5. Click "Collapse all" — verify all files collapse + Expected: Each toggle works and updates state + Evidence: .omo/evidence/task-4-toolbar.png + ``` + +- [x] 5. **Frontend: Diff layout utilities + side-by-side renderer** + + **What to do**: + - Create `apps/web/src/utils/diff-layout.ts` with pure functions: + - `buildNumberedDiffHunks(diffBody: string): NumberedDiffHunk[]` — parse diff text into hunks with old/new line numbers + - `buildUnifiedDiffLines(file): UnifiedDiffDisplayLine[]` — existing behavior + - `buildSplitDiffRows(file): SplitDiffRow[]` — pair removals/additions into left/right rows + - Create `apps/web/src/components/DiffSplitView.tsx` — the side-by-side renderer: + - Two columns (left = deletions, right = additions) with a thin divider + - Each column has its own gutter (line numbers) + code content + - Use Shiki `codeToHtml(language)` for syntax highlighting per side + - Handle empty cells (unpaired lines render as blank) + - In `GitDiffView.tsx`, when `layout === 'split'`, render `DiffSplitView` instead of the unified diff body + + **Files to create/modify**: + - Create `apps/web/src/utils/diff-layout.ts` + - Create `apps/web/src/components/DiffSplitView.tsx` + - Modify `apps/web/src/components/GitDiffView.tsx` — add layout branching + + **References**: + - `/opt/forks/paseo/packages/app/src/utils/diff-layout.ts` — full algorithm + - `/opt/forks/paseo/packages/app/src/git/diff-pane.tsx:968-989` — split layout rendering + - existing `git_diff.ts` `splitDiffByFile` — already splits unified diff per file + + **QA Scenarios**: + ``` + Scenario: Side-by-side diff renders correctly + Tool: Playwright + Preconditions: Git tab active, files with changes + Steps: + 1. Click "Split" layout toggle + 2. Verify two columns appear with a divider + 3. Verify deleted lines are on left side (red background) + 4. Verify added lines are on right side (green background) + 5. Verify context lines appear on both sides, aligned + Expected: Layout matches Paseo's split diff + Evidence: .omo/evidence/task-5-splitdiff.png + ``` + +- [x] 6. **Frontend: Inline comment store + Zustand** + + **What to do**: + - Create `apps/web/src/stores/useDiffCommentStore.ts` + - Define `DiffComment` interface: `{ id, filePath, side, lineNumber, body, createdAt, updatedAt }` + - Create Zustand store with: + - `commentsByKey: Map` keyed by `${sessionId}:${mode}:${filePath}` + - `addComment(key, comment)` / `updateComment(key, id, body)` / `deleteComment(key, id)` + - `loadComments(key)` — load from localStorage + - `persist()` — subscribe to store changes, write to localStorage key `boocode.diff.comments.[key]` + - Export `useDiffCommentStore` + + **Files to create**: + - Create `apps/web/src/stores/useDiffCommentStore.ts` + + **References**: + - `/opt/forks/paseo/packages/app/src/review/store.ts` — zustand store for comments + - `/opt/forks/paseo/packages/app/src/review/state.ts` — CRUD operations + + **QA Scenarios**: + ``` + Scenario: Comments persist across page refresh + Tool: Playwright + Preconditions: Diff panel open with changes + Steps: + 1. Add comment on a diff line + 2. Verify comment thread appears + 3. Reload page + 4. Navigate to same diff + Expected: Comment thread still visible after reload + Evidence: .omo/evidence/task-6-comment-store.txt + ``` + +- [x] 7. **Frontend: InlineReviewGutterCell + InlineReviewEditor** + + **What to do**: + - Create `apps/web/src/components/InlineReviewGutterCell.tsx`: + - Replaces the plain line-number display in diff rows + - Shows line number + "+" icon on hover (to start a comment) + - Uses `ReviewableDiffTarget { filePath, side, lineNumber }` for tracking + - Create `apps/web/src/components/InlineReviewEditor.tsx`: + - Textarea with placeholder "Add comment..." + - Save (Ctrl+Enter) / Cancel (Escape) buttons + - Animates in below the target line + - Integrate into `GitDiffView.tsx` — gutter cells render in the diff line view + - Wire to `useDiffCommentStore` + + **Files to create/modify**: + - Create `apps/web/src/components/InlineReviewGutterCell.tsx` + - Create `apps/web/src/components/InlineReviewEditor.tsx` + - Modify `apps/web/src/components/GitDiffView.tsx` — integrate gutter cells + + **References**: + - Paseo `review/surface.tsx:245-309` — `DiffGutterCell` + `InlineReviewGutterCell` + - Paseo `InlineReviewEditor` pattern + + **QA Scenarios**: + ``` + Scenario: Create inline comment on diff line + Tool: Playwright + Preconditions: Git tab, file expanded + Steps: + 1. Hover over a gutter cell + 2. Click "+" button + 3. Type comment text + 4. Click Save (or Ctrl+Enter) + Expected: Comment thread appears below the line + Evidence: .omo/evidence/task-7-comment-create.png + ``` + +- [x] 8. **Frontend: InlineReviewThread component** + + **What to do**: + - Create `apps/web/src/components/InlineReviewThread.tsx`: + - Renders below a diff line when comments exist for that target + - Each comment shown as a card: avatar placeholder, body, timestamp, edit/delete actions + - Collapsed state shows comment count badge + - Expanded state shows full thread + - Integrate into `GitDiffView.tsx` below diff line rows + + **Files to create/modify**: + - Create `apps/web/src/components/InlineReviewThread.tsx` + - Modify `apps/web/src/components/GitDiffView.tsx` — render thread below lines + + **Reference**: + - Paseo `review/surface.tsx:537-573` — `InlineReviewThreadContent` + + **QA Scenarios**: + ``` + Scenario: Comment thread displays and supports edit/delete + Tool: Playwright + Preconditions: Comments exist on a diff line + Steps: + 1. Expand comment thread + 2. Verify comment body is visible with timestamp + 3. Click edit → modify text → save + 4. Click delete → verify comment removed + Expected: Full CRUD works on comments + Evidence: .omo/evidence/task-8-thread.png + ``` + +- [x] 9. **Frontend: File editing in the file tree** + + **What to do**: + - In `RightRail.tsx`, add a file edit mode: + - Double-click a file in the tree (or context menu "Edit") enters edit mode + - The file row transforms: file name becomes a monospace textarea pre-filled with file content (fetched via existing `api.projects.viewFile`) + - The row shows Save / Cancel buttons + - Save: calls `api.projects.writeFile(projectId, path, content)` — the new endpoint from Task 2 + - Cancel: reverts to the original content and exits edit mode + - After save: re-fetch the file tree + emit `git_diff_refresh` + - Only one file editable at a time (close any existing editor before opening new) + - Visual indicator (highlighted row) when in edit mode + + **Files to modify**: + - `apps/web/src/components/RightRail.tsx` — add edit mode state, edit UI + - `apps/web/src/api/client.ts` — add `writeFile` method (from Task 2) + - `apps/web/src/components/TreeLevel.tsx` (inline in RightRail) — accept edit mode props + + **References**: + - Existing `RightRail.tsx:170-175` `openFile` function — pattern for file interaction + - Existing `FileViewerOverlay.tsx` — Shiki highlighting reference + - Paseo `file-explorer-pane.tsx` — context menu actions pattern + + **QA Scenarios**: + ``` + Scenario: Edit file in file tree and save + Tool: Playwright + Preconditions: Project with a text file + Steps: + 1. Double-click a file in the file tree + 2. Verify file enters edit mode (textarea replaces filename) + 3. Modify content + 4. Ctrl+Enter to save + 5. Verify success indicator + Expected: File content updated on disk, tree refreshes + Evidence: .omo/evidence/task-9-edit-save.png + + Scenario: Cancel file edit reverts changes + Tool: Playwright + Preconditions: File in edit mode + Steps: + 1. Modify content in textarea + 2. Click Cancel / press Escape + 3. Re-open file + Expected: Original content preserved, edit mode exited + Evidence: .omo/evidence/task-9-edit-cancel.txt + ``` + +--- + +## Final Verification + +- [ ] F1. **Plan Compliance Audit** — `oracle` + Verify all Must Have features are implemented, Must NOT Have are absent. + Output: VERDICT + +- [ ] F2. **Code Quality** — `unspecified-high` + Run `pnpm -C apps/web build`, `pnpm -C apps/server build`, check for `as any`/`@ts-ignore`/console.log. + Output: VERDICT + +- [ ] F3. **Real Manual QA** — `unspecified-high` + `playwright` + Execute all QA scenarios from every task, capture evidence. + Output: Scenarios [N/N pass] + +- [ ] F4. **Scope Fidelity** — `deep` + Verify spec matches implementation, no scope creep. + Output: Tasks [N/N compliant] + +--- + +## Commit Strategy + +- **1**: `feat(server): add whitespace param to git diff + write_file endpoint` +- **2**: `feat(web): diff preferences hook, toolbar toggles, split layout` +- **3**: `feat(web): inline diff comments with zustand store` +- **4**: `feat(web): in-browser file editing in file tree` + +--- + +## Success Criteria + +### Verification Commands +```bash +pnpm -C apps/web build # Must pass +pnpm -C apps/server build # Must pass +``` + +### Final Checklist +- [ ] Side-by-side diff renders correctly +- [ ] Hide whitespace re-fetches with `-w` +- [ ] Wrap lines toggles CSS +- [ ] Expand/Collapse all toggles +- [ ] Inline comments: create, read, update, delete +- [ ] File editing: read, modify, save, cancel +- [ ] All preferences survive page reload diff --git a/.omo/plans/openspec-cleanup.md b/.omo/plans/openspec-cleanup.md new file mode 100644 index 0000000..06654cd --- /dev/null +++ b/.omo/plans/openspec-cleanup.md @@ -0,0 +1,1015 @@ +# Openspec Cleanup & High-Value Batch Implementation + +## TL;DR + +> **Quick Summary**: Clean up the `openspec/` folder structure (delete 11 stub files, move 5 misplaced proposals, add missing `.openspec.yaml` files), then implement 5 high-value batches: llama-cache-and-spec, pty-enhancements, results-page, token-analyzer-ui, and enhanced-file-panel. +> +> **Deliverables**: +> - Clean openspec folder: stubs removed, archived/ accurate, all batches schema-compliant +> - llama-server KV cache quantization + ngram speculative decoding enabled +> - PTY exit notifications and session metadata +> - `/results` page for orchestrator runs and arena battles (new route) +> - `/analytics` page for token usage dashboard (new route) +> - Enhanced file panel: side-by-side diff, hide whitespace, wrap lines, expand/collapse all +> +> **Estimated Effort**: Medium-Large +> **Parallel Execution**: YES — 3 waves + final verification +> **Critical Path**: Cleanup → Backend impls → Frontend impls → Integration + +--- + +## Context + +### Original Request +Analyze `openspec/` folder for structural issues, cross-reference against git tags, and create a work plan for implementing the high-value openspec batch proposals. + +### Interview Summary +**Key Discussions**: +- `openspec/changes/` has 22 active batches (all uncommitted, all unshipped) plus `archived/` with 29 entries +- 11 stub files in archived/ are pure noise (49-66 bytes each, "Status: Shipped. Archived." only) +- 5 misplaced 2026-06-07 proposals were dumped in archived/ — they're active design docs, not shipped batches +- 6 active batches missing `.openspec.yaml`; `openspec/config.yaml` is empty +- Active proposals overlap: multiple batches cover evaluation, memory, and workflow engine territory + +**Research Findings**: +- Git tag cross-reference confirms all folder-based archived entries match shipped tags +- 3 stub files reference wrong tags (v1.13.12→v1.13.14, v1.14.x→v1.13.19, etc.) +- All 22 active batches have zero git references — pure filesystem artifacts +- No active batch has shipped yet — zero can be archived + +### Metis Review +Identified gaps: +1. **Deduplication needed**: 2026-06-07 proposals overlap with active changes/ — merging must happen before cleanup is complete +2. **Prioritization needed**: 22 batches can't all ship at once — need clear tiers +3. **User sign-off needed**: Which Tier 1-2 batches to include in this plan vs defer + +--- + +## Work Objectives + +### Core Objective +Restore openspec structural integrity and ship the 5 highest-value, lowest-effort batch proposals. + +### Concrete Deliverables +- Clean openspec: stubs deleted (11 files ~573 bytes), misplaced proposals moved (5 folders), `.openspec.yaml` files added (6 batches), config.yaml populated +- llama-cache-and-spec: KV cache quantization (Q4_0) + ngram speculative decoding enabled +- pty-enhancements: PTY exit notifications, session metadata, X-Agent-Flags +- results-page: `/results` route with Analysis Runs + Arena Battles tabs +- token-analyzer-ui: `/analytics` route with token usage dashboard +- enhanced-file-panel: side-by-side diff toggle, hide whitespace, wrap long lines, expand/collapse all + +### Must Have +- All 11 stub files removed from archived/ +- 5 misplaced 2026-06-07 proposals moved from archived/ into `changes/` (or merged into existing batches) +- `.openspec.yaml` added to all 6 missing batches +- `openspec/config.yaml` gets a `context:` block and `rules:` block +- llama-server restarts with new flags (verify via `ps aux | grep llama`) +- `/results` page loads without 404 and shows real data from existing API endpoints +- `/analytics` page loads and shows token aggregates +- Side-by-side diff renders correctly for files with wide lines + +### Must NOT Have (Guardrails) +- **NO** breaking changes to existing routes or API contracts +- **NO** new database tables or migrations (all data sources already exist) +- **NO** external API dependencies (no cloud embedding models) +- **NO** behavioral engine or Pregel state machine work (deferred to future batch) +- **NO** touching the conductor flow runner or orchestrator pipeline +- **NO** CSS framework changes (stay on Tailwind v4 / shadcn/ui) +- **NO** backend changes unless explicitly required by the batch scope + +### Spec Framework Integration +- **Detected Framework**: OpenSpec (folder structure only — no CLI) +- **Config File**: `openspec/config.yaml` +- **Active Specs**: 22 batch folders in `openspec/changes/` +- **Available Commands**: Manual folder/file operations (no OpenSpec CLI) + +--- + +## Verification Strategy + +> **ZERO HUMAN INTERVENTION** — ALL verification is agent-executed. + +### Test Decision +- **Infrastructure exists**: YES (vitest in apps/server, apps/coder) +- **Automated tests**: Tests-after (no TDD — these are config/frontend changes) +- **Framework**: vitest for backend, Playwright for frontend verification + +### QA Policy +Every task includes agent-executed QA scenarios. Evidence saved to `.omo/evidence/`. + +- **Frontend**: Playwright — navigate, assert DOM elements, screenshot +- **Backend**: Bash (curl) — send requests, assert status + response +- **Config/Restart**: Bash — check processes, verify new flags +- **File operations**: Bash — verify files exist/deleted with `test -f` / `test ! -f` + +--- + +## Execution Strategy + +``` +Wave 1 (Structural Cleanup — quick, MAX PARALLEL): +├── Task 1: Delete 11 stub files from archived/ [quick] +├── Task 2: Move 5 misplaced 2026-06-07 proposals → changes/ [quick] +├── Task 3: Add .openspec.yaml to 6 missing batches [quick] +├── Task 4: Populate openspec/config.yaml with project context [quick] +├── Task 5: Add shipped status metadata to archived/ entries [writing] + +Wave 2 (Backend — moderate, MAX PARALLEL): +├── Task 6: llama-cache-and-spec — KV cache + ngram flags [quick] +├── Task 7: pty-enhancements — exit notifications + session metadata [unspecified-high] +├── Task 8: token-analyzer-ui — backend API endpoints [unspecified-high] + +Wave 3 (Frontend — moderate, MAX PARALLEL): +├── Task 9: results-page — /results route [visual-engineering] +├── Task 10: token-analyzer-ui — /analytics route [visual-engineering] +├── Task 11: enhanced-file-panel — diff modes + UI [visual-engineering] + +Wave FINAL (Verification — 4 parallel reviews): +├── Task F1: Plan compliance audit [oracle] +├── Task F2: Code quality + type check [unspecified-high] +├── Task F3: Real QA — execute every scenario [unspecified-high + playwright] +└── Task F4: Scope fidelity check [deep] + +Critical Path: Cleanup → Backend → Frontend → Integration +Parallel Speedup: ~60% faster than sequential +Max Concurrent: 4 (Wave 2 & 3) +``` + +--- + +## TODOs + +- [ ] 1. Delete 11 stub files from archived/ + + **What to do**: + - Remove these 11 files from `openspec/changes/archived/`: + - `v1.13.12-skills-audit.md` (57B, wrong tag ref) + - `v1.13.15-codecontext-synth.md` (62B) + - `v1.13.17-cross-repo-reads.md` (61B) + - `v1.13.18-codecontext-file-path.md` (66B) + - `v1.13.20-drop-legacy-cols.md` (61B) + - `v1.14-outer-loop.md` (52B) + - `v1.14.1-mcp-poc.md` (51B) + - `v1.14.x-html-artifact-panes.md` (63B, wrong tag ref) + - `v1.15-mcp-multi.md` (51B) + - `v2.0-boocoder.md` (49B) + - `v2.2-paseo-providers.md` (222B) + - Each file contains ONLY "# Title\n\n**Status:** Shipped. Archived.\n" — zero documentation value + - Git history preserves the knowledge; CHANGELOG.md + tags are the authoritative record + + **Must NOT do**: + - Do NOT delete any folder-based archived entries (they have real content) + - Do NOT delete `boocode_batch10.md` or handoff files (they're valuable) + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: `[]` + - **Justification**: Trivial file deletion — no domain skills needed + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 2-5) + - **Blocks**: F1-F4 + - **Blocked By**: None + + **References**: + - `openspec/changes/archived/` — target directory + - `openspec/README.md` — schema definition + - `~/.gitconfig` — no special config needed + + **Acceptance Criteria**: + - [ ] `test ! -f openspec/changes/archived/v1.13.12-skills-audit.md` → success for all 11 files + - [ ] `ls openspec/changes/archived/*.md` shows only allowed files (boocode_batch10.md, handoff_*) + + **QA Scenarios**: + ``` + Scenario: Verify stubs deleted + Tool: Bash + Preconditions: Clean working tree + Steps: + 1. For each stub file, run: test ! -f openspec/changes/archived/{filename} + 2. Assert: all 11 commands return exit code 0 (file does not exist) + 3. List remaining .md files: ls openspec/changes/archived/*.md + 4. Assert: only boocode_batch10.md and handoff_*.md files remain + Expected Result: 11 stubs absent, 3 valuable files present + Evidence: .omo/evidence/task-1-stubs-deleted.txt + + Scenario: Valuable files preserved + Tool: Bash + Preconditions: Stubs deleted + Steps: + 1. test -f openspec/changes/archived/boocode_batch10.md + 2. test -f openspec/changes/archived/handoff_v1.13.10_per_tool_cost.md + 3. test -f openspec/changes/archived/handoff_v1.13.8_prefix_verify.md + Expected Result: All 3 return exit code 0 + Evidence: .omo/evidence/task-1-valuables-preserved.txt + ``` + + **Evidence to Capture**: + - `task-1-stubs-deleted.txt` — confirmation each stub is gone + - `task-1-valuables-preserved.txt` — confirmation valuable files remain + + **Commit**: YES + - Message: `chore(openspec): delete 11 stub archive files with zero documentation value` + - Files: openspec/changes/archived/v1.13.12-skills-audit.md, ... + +- [ ] 2. Move 5 misplaced 2026-06-07 proposals from archived/ to changes/ + + **What to do**: + - Move these 5 folders from `openspec/changes/archived/2026-06-07-*` to `openspec/changes/*`: + 1. `archived/2026-06-07-boocontext/` → `changes/boocontext/` (partially shipped in v2.8.0) + 2. `archived/2026-06-07-eval-sandbox-agent-runtime/` → merge into `changes/import-llm-evaluator/` and `changes/import-pregel-engine/` (overlapping scope) + 3. `archived/2026-06-07-hybrid-workflow-engine/` → merge into `changes/orchestrator-flow-advanced/` + 4. `archived/2026-06-07-memory-context-engineering/` → merge into `changes/memory-context/` + 5. `archived/2026-06-07-port-audit-parlant-patterns/` → merge into `changes/add-behavioral-engine/` and `changes/audit-harness-integration/` + - For merges (2-5): append relevant content from the 2026-06-07 proposal into the existing batch's proposal.md, tasks.md, design.md. The 2026-06-07 versions are "grand vision" — extract the concrete specs relevant to the narrower active batch. + - For `boocontext/` (1): move as-is since it's a new slug with no direct collision. + + **Must NOT do**: + - Do NOT delete the content of the 2026-06-07 folders — merge, don't discard + - Do NOT create duplicate batch slugs + - Do NOT overwrite existing proposal content — append/extend + + **Recommended Agent Profile**: + - **Category**: `writing` + - **Skills**: `[]` + - **Justification**: File organization + content merging — technical writing task + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 3-5) + - **Blocks**: F1-F4 + - **Blocked By**: None + + **References**: + - `openspec/changes/archived/2026-06-07-*/` — source folders + - `openspec/changes/import-llm-evaluator/` — target for eval overlap + - `openspec/changes/import-pregel-engine/` — target for graph overlap + - `openspec/changes/orchestrator-flow-advanced/` — target for workflow overlap + - `openspec/changes/memory-context/` — target for memory overlap + - `openspec/changes/add-behavioral-engine/` — target for port patterns + - `openspec/changes/audit-harness-integration/` — target for audit patterns + + **Acceptance Criteria**: + - [ ] `openspec/changes/boocontext/` exists with proposal.md + tasks.md + design.md + specs/ + - [ ] `openspec/changes/import-llm-evaluator/` proposal.md now references eval-sandbox content + - [ ] `openspec/changes/import-pregel-engine/` proposal.md now references graph engine content + - [ ] `openspec/changes/orchestrator-flow-advanced/` proposal.md now references hybrid workflow + - [ ] `openspec/changes/memory-context/` proposal.md now references context engineering + - [ ] `openspec/changes/add-behavioral-engine/` and `audit-harness-integration/` now reference port patterns + - [ ] `test ! -d openspec/changes/archived/2026-06-07-eval-sandbox-agent-runtime/` for each moved folder + + **QA Scenarios**: + ``` + Scenario: boocontext moved + Tool: Bash + Preconditions: Files moved + Steps: + 1. test -f openspec/changes/boocontext/proposal.md + 2. test -f openspec/changes/boocontext/tasks.md + 3. test ! -f openspec/changes/archived/2026-06-07-boocontext/proposal.md + Expected Result: Files exist in new location, not in old + Evidence: .omo/evidence/task-2-boocontext-moved.txt + ``` + ``` + Scenario: Merged proposals updated + Tool: Bash + Preconditions: Files merged + Steps: + 1. grep -q "eval-sandbox\|graph engine\|hybrid workflow\|context engineering\|port patterns" openspec/changes/*/proposal.md + 2. Assert: each merged batch's proposal.md references the 2026-06-07 source + Expected Result: grep finds references in the right target files + Evidence: .omo/evidence/task-2-merges-verified.txt + ``` + + **Evidence to Capture**: + - `task-2-boocontext-moved.txt` + - `task-2-merges-verified.txt` + + **Commit**: YES (groups with Task 1) + - Message: `chore(openspec): move 5 misplaced proposals from archived/ → changes/, merge overlapping content` + - Files: openspec/changes/boocontext/*, openspec/changes/*/proposal.md, openspec/changes/*/tasks.md + +- [ ] 3. Add .openspec.yaml to 6 missing batches + + **What to do**: + - Create `.openspec.yaml` in each of these 6 active batches: + - `enhanced-file-panel/` + - `llama-cache-and-spec/` + - `memory-v2-hybrid-search/` + - `omo-paseo-bridge/` + - `orchestrator-flow-advanced/` + - `results-page/` + - Each file must contain: + ```yaml + schema: spec-driven + created: 2026-06-07 + ``` + + **Must NOT do**: + - Do NOT modify existing proposal.md or tasks.md content + - Do NOT add .openspec.yaml to batches that already have one + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: `[]` + - **Justification**: Trivial boilerplate file creation + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1, 2, 4, 5) + - **Blocks**: F1-F4 + - **Blocked By**: None + + **References**: + - `openspec/changes/add-3tier-memory/.openspec.yaml` — template + + **Acceptance Criteria**: + - [ ] All 6 created files contain `schema: spec-driven` + - [ ] `find openspec/changes/ -name ".openspec.yaml" | wc -l` counts all expected files + + **QA Scenarios**: + ``` + Scenario: All .openspec.yaml files present + Tool: Bash + Preconditions: Files created + Steps: + 1. For each batch: test -f openspec/changes/{batch}/.openspec.yaml + 2. For each: grep -q "schema: spec-driven" openspec/changes/{batch}/.openspec.yaml + Expected Result: All 6 files exist with correct content + Evidence: .omo/evidence/task-3-openspec-yaml-added.txt + ``` + + **Evidence to Capture**: + - `task-3-openspec-yaml-added.txt` + + **Commit**: YES (groups with Task 1) + - Message: `chore(openspec): add .openspec.yaml to 6 missing batch folders` + - Files: openspec/changes/enhanced-file-panel/.openspec.yaml, ... + +- [ ] 4. Populate openspec/config.yaml with project context + + **What to do**: + - Replace the empty `openspec/config.yaml` with a populated version: + ```yaml + schema: spec-driven + + context: | + Tech stack: TypeScript, React 18, Vite, Tailwind v4, shadcn/ui, Fastify, PostgreSQL 16, pnpm workspaces + Apps: BooChat (read-only chat), BooCoder (write tools + agent dispatch), BooTerm (PTY terminals), Orchestrator (multi-agent conductor) + Infrastructure: Docker Compose, Tailscale (100.114.205.53), Authelia auth, llama-swap inference + Monorepo: apps/server, apps/web, apps/booterm, apps/coder, packages/contracts + Commits: conventional commits, strict TypeScript, NodeNext module resolution + Testing: vitest (server + coder), Playwright (web E2E), no root tsconfig + + rules: + proposal: + - Every proposal must have a "Why" section explaining the motivation + - Every proposal must have a "What Changes" section enumerating deliverables + - Include "Must Have" / "Must NOT Have" guardrails + - Reference shipped git tags when applicable + tasks: + - Tasks must be ordered by dependency, not priority + - Each task is one atomic change (file, config, or command) + - Parallel tasks go in the same wave + ``` + + **Must NOT do**: + - Do NOT delete the `schema: spec-driven` line + + **Recommended Agent Profile**: + - **Category**: `writing` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1-3, 5) + - **Blocks**: F1-F4 + - **Blocked By**: None + + **References**: + - `openspec/config.yaml` — current (empty) file + - `/home/samkintop/opt/boocode/CLAUDE.md` — source for context info + + **Acceptance Criteria**: + - [ ] `grep -q "context:" openspec/config.yaml` → success + - [ ] `grep -q "rules:" openspec/config.yaml` → success + - [ ] config.yaml has more than 50 bytes (was 20 bytes) + + **QA Scenarios**: + ``` + Scenario: config.yaml populated + Tool: Bash + Preconditions: File written + Steps: + 1. wc -c openspec/config.yaml → assert > 500 bytes + 2. grep -q "context:" openspec/config.yaml + 3. grep -q "rules:" openspec/config.yaml + 4. grep -q "schema: spec-driven" openspec/config.yaml + Expected Result: All assertions pass + Evidence: .omo/evidence/task-4-config-populated.txt + ``` + + **Evidence to Capture**: + - `task-4-config-populated.txt` + + **Commit**: YES (groups with Task 1) + - Message: `chore(openspec): populate config.yaml with project context and rules` + - Files: openspec/config.yaml + +- [ ] 5. Add shipped-status metadata to 10 archived folder entries + + **What to do**: + - Add frontmatter or status line to each archived folder's proposal.md documenting the shipped version: + - `agent-status-normalize/` → `v2.7.6` + - `claude-sdk-sessionstore/` → `v2.7.5` + - `contracts-ssot/` → `v2.7.13` + - `license-debt-mit/` → `v2.7.0` + - `mistake-tracker-file-ledger/` → `v2.7.4` + - `orchestrator/` → `v2.7.17` + - `sampling-streamjson-tokens/` → `v2.7.3` + - `v2-3-provider-lifecycle/` → `v2.5.4`–`v2.5.13` + - `v2-6-persistent-agent-sessions/` → `v2.6.4`–`v2.6.8` + - `write-edit-robustness/` → `v2.7.1` + - Add line after the `## Why` section heading: `**Shipped in:** \`v2.7.6-agent-status-normalize\`` (or equivalent) + + **Must NOT do**: + - Do NOT change the body of the proposal beyond the shipped annotation + - Do NOT add shipped annotations to the 2026-06-07 batches (they're not shipped) + + **Recommended Agent Profile**: + - **Category**: `quick` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 1 (with Tasks 1-4) + - **Blocks**: F1-F4 + - **Blocked By**: None + + **References**: + - Git tags: `v2.7.0-mit`, `v2.7.1-write-edit-robustness`, etc. + + **Acceptance Criteria**: + - [ ] All 10 archived batch proposals contain "Shipped in:" referencing a git tag + - [ ] `grep -r "Shipped in:" openspec/changes/archived/*/proposal.md | wc -l` = 10 + + **QA Scenarios**: + ``` + Scenario: All archived batches annotated + Tool: Bash + Preconditions: Files edited + Steps: + 1. grep -rl "Shipped in:" openspec/changes/archived/*/proposal.md | wc -l + 2. Assert: exactly 10 files contain "Shipped in:" + Expected Result: 10 files annotated + Evidence: .omo/evidence/task-5-shipped-annotations.txt + ``` + + **Evidence to Capture**: + - `task-5-shipped-annotations.txt` + + **Commit**: YES (groups with Task 1) + - Message: `chore(openspec): add shipped-in version annotations to 10 archived batch proposals` + - Files: openspec/changes/archived/*/proposal.md + +--- + +## TODOs (Wave 2) + +- [ ] 6. llama-cache-and-spec — Enable KV cache quantization + ngram speculative decoding + + **What to do**: + - Edit `apps/server/src/services/inference/providers/llama.ts` (or the llama args validator `llama-args-validator.ts`) to allow `--cache-type-k q4_0` and `--spec-type ngram-mod` through the shadowing lists + - Change the base llama-server args to include: + - `--cache-type-k q4_0` (4-bit KV cache, ~4× VRAM reduction) + - `--spec-type ngram-mod` (ngram speculative decoding, 2-3× tok/s on code) + - Verify the sidecar validator (`sidecar/validator.go`) also allows these flags through + - Read `apps/server/src/services/inference/llama-args-validator.ts` and `sidecar/validator.go` to understand the current blocklist + - Add the two flags to the allowlist instead of the shadow list + - Update the sidecar Dockerfile or config if needed + + **Must NOT do**: + - Do NOT change any other llama-server args + - Do NOT enable KV cache quantization for Q8_0 or Q3_K (only Q4_0) + - Do NOT add a separate draft model (ngram is self-contained) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: `[]` + - **Justification**: Requires understanding llama.cpp arg validation across two codebases + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 7-8) + - **Blocks**: F1-F4 + - **Blocked By**: Task 1-5 (Wave 1) + + **References**: + - `apps/server/src/services/inference/llama-args-validator.ts` — current arg blocklist/allowlist + - `sidecar/validator.go` — sidecar validation (if exists) + - `docker-compose.yml` or sidecar Dockerfile — restart config + - `openspec/changes/llama-cache-and-spec/proposal.md` — full spec + + **Acceptance Criteria**: + - [ ] `--cache-type-k q4_0` present in llama-server args after restart + - [ ] `--spec-type ngram-mod` present in llama-server args after restart + - [ ] llama-server starts without errors + - [ ] Inference still works (send test message) + + **QA Scenarios**: + ``` + Scenario: KV cache quantization enabled + Tool: Bash + Preconditions: Server restarted after changes + Steps: + 1. ps aux | grep llama-server | grep -o "cache-type-k q4_0" + 2. Assert: output matches "q4_0" + Expected Result: KV cache quantization is active + Evidence: .omo/evidence/task-6-kv-cache-enabled.txt + ``` + ``` + Scenario: Speculative decoding enabled + Tool: Bash + Preconditions: Server restarted + Steps: + 1. ps aux | grep llama-server | grep -o "spec-type ngram-mod" + 2. Assert: output matches "ngram-mod" + Expected Result: Ngram speculative decoding is active + Evidence: .omo/evidence/task-6-ngram-enabled.txt + ``` + ``` + Scenario: Inference still works + Tool: Bash (curl) + Preconditions: Server running with new flags + Steps: + 1. curl -s -o /dev/null -w "%{http_code}" http://100.114.205.53:9500/api/health + 2. Assert: HTTP 200 + Expected Result: Server is healthy and serving + Evidence: .omo/evidence/task-6-health-check.txt + ``` + + **Evidence to Capture**: + - `task-6-kv-cache-enabled.txt` — grep output showing the flag + - `task-6-ngram-enabled.txt` — grep output showing the flag + - `task-6-health-check.txt` — health check confirmation + + **Commit**: YES + - Message: `perf(llama): enable KV cache quantization (q4_0) + ngram speculative decoding` + - Files: apps/server/src/services/inference/llama-args-validator.ts, sidecar/validator.go (if needed) + +- [ ] 7. pty-enhancements — PTY exit notifications + session metadata + + **What to do**: + - Add `notifyOnExit` support to the PTY session manager (likely in `apps/booterm/`) + - When a PTY process exits AND `notifyOnExit` was set: + - Emit an event/message to the agent channel with: session ID, title, exit code, total output lines, last line of output + - Add session metadata fields: agent ID that spawned it, task ID, optional title + - Add `pty_list` endpoint that returns metadata for all sessions + - Wire `X-Agent-Flags` header support for agent identification + - Read `apps/booterm/` to understand the current PTY architecture + + **Must NOT do**: + - Do NOT change the existing pty_spawn interface (add notifyOnExit as optional param) + - Do NOT implement sandbox or circuit breaker (out of scope for this wave) + - Do NOT add new database tables (metadata lives in-memory or in existing session store) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 6, 8) + - **Blocks**: F1-F4 + - **Blocked By**: Task 1-5 (Wave 1) + + **References**: + - `apps/booterm/src/` — PTY session management code + - `apps/coder/src/services/` — agent dispatch that spawns PTYs + - `openspec/changes/pty-enhancements/proposal.md` — full spec + - `apps/server/src/services/inference/` — inference pipeline that may need to handle notifications + + **Acceptance Criteria**: + - [ ] `notifyOnExit` optional parameter on pty_spawn works + - [ ] On process exit with notifyOnExit=true, agent receives notification + - [ ] `pty_list` returns session metadata + - [ ] `X-Agent-Flags` header is recognized + + **QA Scenarios**: + ``` + Scenario: notifyOnExit triggers notification + Tool: Bash + tmux + Preconditions: booterm running + Steps: + 1. Start a short PTY with notifyOnExit=true: sleep 1 + 2. Wait 2 seconds for completion + 3. Check notification was delivered + Expected Result: Exit notification received with title, exit code, last line + Evidence: .omo/evidence/task-7-notify-on-exit.txt + ``` + ``` + Scenario: pty_list shows metadata + Tool: Bash (curl) + Preconditions: PTY sessions exist + Steps: + 1. curl http://localhost:9501/api/pty/list 2>/dev/null + 2. Assert: response contains session metadata fields + Expected Result: Metadata returned for each session + Evidence: .omo/evidence/task-7-pty-list.txt + ``` + + **Evidence to Capture**: + - `task-7-notify-on-exit.txt` — notification evidence + - `task-7-pty-list.txt` — pty_list response + + **Commit**: YES + - Message: `feat(booterm): PTY exit notifications + session metadata + X-Agent-Flags` + - Files: apps/booterm/src/*.ts, apps/coder/src/services/*.ts + +- [ ] 8. token-analyzer-ui — Backend API endpoints for token analytics + + **What to do**: + - Add read-only API endpoints to serve aggregate token data: + - `GET /api/coder/token-analytics/sessions` — per-session token usage (input, output, cost) + - `GET /api/coder/token-analytics/tools` — per-tool cost breakdown (from tool_cost_stats view) + - `GET /api/coder/token-analytics/trends` — token usage over time + - Reuse existing data sources: + - `agent_sessions.input_tokens`, `agent_sessions.output_tokens`, `agent_sessions.cost` + - `tool_cost_stats` view (per-tool 100-call rolling window) + - `tasks.token_breakdown` JSONB column + - Implement in `apps/coder/src/routes/` (follow existing route patterns) + - Add proper error handling, pagination for large result sets, and date filtering + + **Must NOT do**: + - Do NOT create new database tables or migrations + - Do NOT add token tracking logic (data is already accumulated) + - Do NOT add real-time streaming (data is historical aggregate) + + **Recommended Agent Profile**: + - **Category**: `unspecified-high` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 2 (with Tasks 6-7) + - **Blocks**: Task 10 (frontend depends on backend) + - **Blocked By**: Task 1-5 (Wave 1) + + **References**: + - `apps/coder/src/routes/` — existing route patterns + - `apps/server/src/schema.sql` — `tool_cost_stats` view definition + - `apps/coder/CLAUDE.md` — coder conventions, route registration + - `packages/contracts/` — shared types for response schemas + - `openspec/changes/token-analyzer-ui/proposal.md` — full spec + + **Acceptance Criteria**: + - [ ] `GET /api/coder/token-analytics/sessions?project_id=X` returns 200 with token data + - [ ] `GET /api/coder/token-analytics/tools?project_id=X` returns 200 with tool breakdown + - [ ] `GET /api/coder/token-analytics/trends?project_id=X` returns 200 with trend data + - [ ] All endpoints respect `project_id` filtering + - [ ] Empty data returns valid empty arrays (not errors) + + **QA Scenarios**: + ``` + Scenario: Sessions endpoint works + Tool: Bash (curl) + Preconditions: Server running, project exists + Steps: + 1. curl -s "http://localhost:3000/api/coder/token-analytics/sessions?project_id=1" + 2. Assert: HTTP 200 + 3. Assert: response is valid JSON with expected fields + Expected Result: Session token data returned + Evidence: .omo/evidence/task-8-sessions-endpoint.txt + ``` + ``` + Scenario: Empty data returns valid response + Tool: Bash (curl) + Preconditions: Server running + Steps: + 1. curl -s "http://localhost:3000/api/coder/token-analytics/sessions?project_id=999" + 2. Assert: HTTP 200 + 3. Assert: response contains empty array (not error) + Expected Result: Graceful empty state + Evidence: .omo/evidence/task-8-empty-data.txt + ``` + + **Evidence to Capture**: + - `task-8-sessions-endpoint.txt` — successful API response + - `task-8-empty-data.txt` — graceful empty handling + + **Commit**: YES + - Message: `feat(coder): add token-analytics API endpoints for session/tool/trend data` + - Files: apps/coder/src/routes/token-analytics.ts, apps/coder/src/services/token-analytics.ts + +--- + +## TODOs (Wave 3) + +- [ ] 9. results-page — /results route for orchestrator runs + arena battles + + **What to do**: + - Add sidebar nav button with `ScrollText` icon (lucide-react), **above** the Token Analytics button + - Create new `/results` route page with two tabs: + - "Analysis Runs" — list orchestrator flow runs (research, code-review, investigate, etc.) + - "Arena Battles" — list battle history + - Each tab shows: status dot, name/type, band/battle-type, model, timing, error indicator + - Completed runs show "View Report" link; completed battles show "View Analysis" + - Uses existing API endpoints (no backend changes needed): + - `GET /api/coder/runs?project_id=X` + - `GET /api/coder/battles?project_id=X` + - Requires `project_id` context — load from sidebar on mount, or show project selector + - Follow existing route patterns in web (React Router routes, lazy loading) + + **Must NOT do**: + - Do NOT create new API endpoints + - Do NOT modify existing API contracts + - Do NOT add pagination beyond what the API already provides + - Do NOT add real-time updates (static list, refreshed on mount) + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 10-11) + - **Blocks**: F1-F4 + - **Blocked By**: Task 1-5 (Wave 1) + + **References**: + - `apps/web/src/routes/` — existing route patterns (analytics, settings) + - `apps/web/src/components/sidebar/` — nav button patterns + - `apps/web/src/api/` — existing API client + - `openspec/changes/results-page/proposal.md` — full spec + - `apps/coder/src/routes/runs.ts` — runs endpoint + - `apps/coder/src/routes/battles.ts` — battles endpoint + + **Acceptance Criteria**: + - [ ] Sidebar shows "Results" button with ScrollText icon above Token Analytics + - [ ] Clicking navigates to `/results` + - [ ] "Analysis Runs" tab loads and displays orchestrator flow history + - [ ] "Arena Battles" tab loads and displays battle history + - [ ] Completed runs show "View Report" link + - [ ] Empty state shown when no data + - [ ] Error state shown on API failure + + **QA Scenarios**: + ``` + Scenario: Nav button renders + Tool: Playwright + Preconditions: Web app loaded + Steps: + 1. Navigate to / + 2. Look for sidebar nav button with text "Results" + 3. Assert: button exists and links to /results + Expected Result: Results nav button present + Evidence: .omo/evidence/task-9-nav-button.png + ``` + ``` + Scenario: Results page loads + Tool: Playwright + Preconditions: Web app loaded, project exists + Steps: + 1. Navigate to /results + 2. Wait for "Analysis Runs" tab to appear + 3. Assert: tab shows list of runs or empty state + Expected Result: Page loads with data + Evidence: .omo/evidence/task-9-results-page.png + ``` + + **Evidence to Capture**: + - `task-9-nav-button.png` — screenshot of sidebar with Results button + - `task-9-results-page.png` — screenshot of /results page with data + + **Commit**: YES + - Message: `feat(web): add /results page for orchestrator runs and arena battle history` + - Files: apps/web/src/routes/results.tsx, apps/web/src/components/sidebar/*.tsx + +- [ ] 10. token-analyzer-ui — /analytics dashboard route + + **What to do**: + - Add sidebar nav button with appropriate icon, **above Settings** button + - Create new `/analytics` route page showing token usage dashboard: + - Aggregate token usage across sessions (total input/output tokens) + - Per-tool cost breakdown (bar chart or table) + - Per-session token history (list or mini chart) + - Per-provider cost comparison + - Reuse existing data from the backend endpoints created in Task 8 + - Follow the same route/nav patterns as results-page + + **Must NOT do**: + - Do NOT add new charting libraries (use what's already available) + - Do NOT implement real-time updates + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 9, 11) + - **Blocks**: F1-F4 + - **Blocked By**: Tasks 1-5 (Wave 1), Task 8 (backend endpoints) + + **References**: + - Same as Task 9 + Task 8 endpoints + - `openspec/changes/token-analyzer-ui/proposal.md` — full spec + - `apps/web/src/components/` — existing chart/list components + + **Acceptance Criteria**: + - [ ] Sidebar shows "Token Analytics" button above Settings + - [ ] `/analytics` loads and shows token dashboard + - [ ] Per-session, per-tool, per-provider breakdowns visible + - [ ] Empty state shown when no data + + **QA Scenarios**: + ``` + Scenario: Token Analytics nav button renders + Tool: Playwright + Preconditions: Web app loaded + Steps: + 1. Navigate to / + 2. Look for "Token Analytics" button in sidebar + 3. Assert: button exists above Settings + Expected Result: Nav button present + Evidence: .omo/evidence/task-10-nav-button.png + ``` + ``` + Scenario: Analytics dashboard loads + Tool: Playwright + Preconditions: Web app loaded + Steps: + 1. Navigate to /analytics + 2. Wait for dashboard content to render + 3. Assert: token usage data is visible + Expected Result: Dashboard shows data + Evidence: .omo/evidence/task-10-analytics-dashboard.png + ``` + + **Evidence to Capture**: + - `task-10-nav-button.png` + - `task-10-analytics-dashboard.png` + + **Commit**: YES + - Message: `feat(web): add /analytics route for token usage dashboard` + - Files: apps/web/src/routes/analytics.tsx, apps/web/src/components/sidebar/*.tsx + +- [ ] 11. enhanced-file-panel — Side-by-side diff, hide whitespace, wrap lines, expand/collapse all + + **What to do**: + - Add side-by-side diff toggle to the Git diff tab in the file panel + - Add "Hide whitespace" checkbox that filters whitespace-only changes + - Add "Wrap long lines" toggle for diff display + - Add "Expand All" / "Collapse All" buttons for file-level diffs + - Implement in `apps/web/src/components/` following existing file panel patterns + - Read `apps/web/src/components/` to find the existing diff rendering components + + **Must NOT do**: + - Do NOT implement inline diff comments (deferred) + - Do NOT implement in-browser file editing (deferred) + - Do NOT change the backend diff generation logic + + **Recommended Agent Profile**: + - **Category**: `visual-engineering` + - **Skills**: `[]` + + **Parallelization**: + - **Can Run In Parallel**: YES + - **Parallel Group**: Wave 3 (with Tasks 9-10) + - **Blocks**: F1-F4 + - **Blocked By**: Task 1-5 (Wave 1) + + **References**: + - `apps/web/src/components/` — existing file panel and diff components + - `apps/web/src/hooks/` — hooks for diff state management + - `openspec/changes/enhanced-file-panel/proposal.md` — full spec + - `apps/server/src/routes/projects.ts` — git diff backend route + + **Acceptance Criteria**: + - [ ] Side-by-side diff toggles correctly + - [ ] Hide whitespace checkbox filters whitespace changes + - [ ] Wrap long lines toggle works + - [ ] Expand/Collapse All buttons toggle all files + - [ ] All changes are frontend-only (no new API calls) + + **QA Scenarios**: + ``` + Scenario: Side-by-side diff renders + Tool: Playwright + Preconditions: Repo with uncommitted changes + Steps: + 1. Open file panel + 2. Click Git tab + 3. Toggle side-by-side view + 4. Assert: diff renders in two columns + Expected Result: Side-by-side diff visible + Evidence: .omo/evidence/task-11-side-by-side.png + ``` + ``` + Scenario: Hide whitespace works + Tool: Playwright + Preconditions: Diff has whitespace changes + Steps: + 1. Open diff with whitespace changes + 2. Check "Hide whitespace" + 3. Assert: only-whitespace hunks hidden + Expected Result: Whitespace-only changes filtered + Evidence: .omo/evidence/task-11-hide-whitespace.png + ``` + ``` + Scenario: Expand/Collapse All toggles + Tool: Playwright + Preconditions: Multiple files changed + Steps: + 1. Click "Collapse All" + 2. Assert: all files collapsed to summary + 3. Click "Expand All" + 4. Assert: all files expanded + Expected Result: Bulk toggle works + Evidence: .omo/evidence/task-11-expand-collapse.png + ``` + + **Evidence to Capture**: + - `task-11-side-by-side.png` + - `task-11-hide-whitespace.png` + - `task-11-expand-collapse.png` + + **Commit**: YES + - Message: `feat(web): enhanced file panel — side-by-side diff, hide whitespace, wrap lines, expand/collapse all` + - Files: apps/web/src/components/*.tsx, apps/web/src/hooks/*.ts + +--- + +## Final Verification Wave + +- [ ] F1. **Plan Compliance Audit** — `oracle` + Read the plan end-to-end. For each "Must Have": verify implementation exists (read file, curl endpoint, run command). For each "Must NOT Have": search codebase for forbidden patterns — reject with file:line if found. Check evidence files exist in .omo/evidence/. Compare deliverables against plan. + Output: `Must Have [N/N] | Must NOT Have [N/N] | Tasks [N/N] | VERDICT: APPROVE/REJECT` + +- [ ] F2. **Code Quality Review** — `unspecified-high` + Run `tsc --noEmit` for any changed apps + `bun test`. Review all changed files for: `as any`/`@ts-ignore`, empty catches, console.log in prod, commented-out code, unused imports. + Output: `Build [PASS/FAIL] | Lint [PASS/FAIL] | Tests [N pass/N fail] | Files [N clean/N issues] | VERDICT` + +- [ ] F3. **Real Manual QA** — `unspecified-high` + Start from clean state. Execute EVERY QA scenario from EVERY task — follow exact steps, capture evidence. Test cross-task integration (features working together, not isolation). Test edge cases: empty state, invalid input, missing project_id. Save to `.omo/evidence/final-qa/`. + Output: `Scenarios [N/N pass] | Integration [N/N] | Edge Cases [N tested] | VERDICT` + +- [ ] F4. **Scope Fidelity Check** — `deep` + For each task: read "What to do", read actual diff. Verify 1:1 — everything in scope was built (no missing), nothing beyond scope was built (no creep). Check "Must NOT do" compliance. + Output: `Tasks [N/N compliant] | Contamination [CLEAN/N issues] | Unaccounted [CLEAN/N files] | VERDICT` + +--- + +## Commit Strategy + +- **1-5** (grouped): `chore(openspec): cleanup openspec folder structure — delete stubs, move proposals, add metadata, populate config` +- **6**: `perf(llama): enable KV cache quantization (q4_0) + ngram speculative decoding` +- **7**: `feat(booterm): PTY exit notifications + session metadata + X-Agent-Flags` +- **8**: `feat(coder): add token-analytics API endpoints` +- **9**: `feat(web): add /results page for orchestrator runs + arena battles` +- **10**: `feat(web): add /analytics token usage dashboard` +- **11**: `feat(web): enhanced file panel — side-by-side diff, hide whitespace, wrap lines, expand/collapse` + +--- + +## Success Criteria + +### Verification Commands +```bash +# OpenSpec cleanup +test ! -f openspec/changes/archived/v1.13.12-skills-audit.md +test -d openspec/changes/boocontext/ +test -f openspec/changes/enhanced-file-panel/.openspec.yaml +grep -q "context:" openspec/config.yaml + +# llama-cache-and-spec +ps aux | grep llama-server | grep -o "cache-type-k q4_0" +ps aux | grep llama-server | grep -o "spec-type ngram-mod" + +# PTY enhancements +curl -s http://localhost:9501/api/pty/list | jq '.' + +# Results page +curl -s "http://localhost:3000/api/coder/runs?project_id=1" | jq '.' + +# Token analytics +curl -s "http://localhost:3000/api/coder/token-analytics/sessions?project_id=1" | jq '.' + +# Enhanced file panel +# (visual verification via Playwright) +``` + +### Final Checklist +- [ ] 11 stub files deleted from archived/ +- [ ] 5 misplaced proposals moved/merged into changes/ +- [ ] 6 .openspec.yaml files added +- [ ] config.yaml populated with context + rules +- [ ] 10 archived proposals annotated with shipped versions +- [ ] llama-server running with KV cache Q4_0 + ngram +- [ ] PTY exit notifications working +- [ ] `/results` page renders and loads data +- [ ] `/analytics` page renders and loads data +- [ ] Side-by-side diff, hide whitespace, wrap lines, expand/collapse all working +- [ ] All type checks pass +- [ ] All QA scenarios pass diff --git a/packages/ion/package-lock.json b/packages/ion/package-lock.json new file mode 100644 index 0000000..ee491dd --- /dev/null +++ b/packages/ion/package-lock.json @@ -0,0 +1,1734 @@ +{ + "name": "@boocode/ion", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@boocode/ion", + "version": "0.0.1", + "license": "MIT", + "dependencies": { + "@types/node": "^25.9.2", + "js-yaml": "^4.1.0", + "nanoid": "^5.0.9", + "ulid": "^2.3.0", + "zod": "^3.25.76" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "typescript": "^5.5.0", + "vitest": "^3.2.4" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.61.1.tgz", + "integrity": "sha512-JnBB8MdXj45cajvTuO5FmPlvFVJRQgvrz1uSEl3NwqFnReAPGwb8EanbGi4z2nRaqLzjJSv5/JmycoTKlRZxHA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.61.1.tgz", + "integrity": "sha512-Jx2g7iSjw4AOT0HDPHM9RV3GNjRXwybWtSFZiZAYUTjUwjVrYIwq3kBf+LnhqJlzXFAqTAh2F7IGI+O568exPw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.61.1.tgz", + "integrity": "sha512-0F1L/Z3Eqv8mT2n3dCpeO8GcTvHvVqkP5/t6DMsn0KzhYVcg+s7Ncl5DS8qjKYEeio6Az0Gt6nyBORay5qIlCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.61.1.tgz", + "integrity": "sha512-qLttcH871ujY4YcVfUSShhOw+CsoTatYz8gRbHO7Bb92QH059/P0y5do1KMs41fY0BpD2x4AJH/gID0zFiqVKQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.61.1.tgz", + "integrity": "sha512-fUI4RapGE0Oh3mb8mgfvC1O2nU1RpDZUKnDQm3xB1Ipg7C2wTs5Kstz7G2uWK99a8S2yTMq8/P4uycwNa0nJyw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.61.1.tgz", + "integrity": "sha512-H5YrdvJaDtI/U9/emrD4b++xkvp3y/JvOe4rizHbxvkyMfRS/CiRYdji+Pl8D0brEaNFWUh1drQxgAGIl6Xudw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.61.1.tgz", + "integrity": "sha512-Q8CBCCQtDFrYtXoeUXSrnFXKOnyUhx6bz+SkL6A0E7V8kAiCJ5pamq1WtbfpVGhR5TSpXY6ak3avmDc5fHTyJA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.61.1.tgz", + "integrity": "sha512-nwnhk1581l0FBVellGcVCAT0Oi06onEA3WB53sf01VO3I0UPBkMH9sXONYME2K0ovXcNayJfNtHfm6mpJElatQ==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.61.1.tgz", + "integrity": "sha512-x5Xr49hwt3hdW75UOZm3395YwwzPyauktslv29KpWL/T+vVAzoT3azLcTWv0eMciBNrx+DYjH4paehHoLpPvpg==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.61.1.tgz", + "integrity": "sha512-unMS3H73DpaoPyyEVPjGKleM/s0mkmsauTENpw4INQY8y4+IuLNjkueQ5QCtC0D3N38Y38yhAU8OoZ20S2Tm6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.61.1.tgz", + "integrity": "sha512-zNZzGRnAhwjFEYmvphJRV5XaQGjs62cCmeYYHUT//NbvEnHauw+I85nGG+SiVg5ld4GX8D1IbKIX+ozITQnhMQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.61.1.tgz", + "integrity": "sha512-LdpWGL8X209B2SIvWjqlc8VZgM6PKfontSerGepuldQmHYrAOtnMCXeJkxXGbC+PPZVOuu5czJo7fNV6aeW8rQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.61.1.tgz", + "integrity": "sha512-EC5kTtNaNGOmbMGqar8dvJy6y/hg99GAwjfBz++pxZhQATXGcRjd6c5en5wcbru0vkRmiMGsQKdMJOOf6sza4g==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.61.1.tgz", + "integrity": "sha512-8hiwp6D4acEcNK78I4rP0/XtS1sknWIAMJBPdR4l6zUtyTm5KiTDr5bXmWt4foY7nAN7AThDHgkLIEZOWKbzWw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.61.1.tgz", + "integrity": "sha512-10dh/h/BqA7DuMPWSxkR8uks18FRwnwOEqr5zOTEl+NOwP/OMzKX8OFR/Of9xxDA7D5qef1Nzar5WDD2kCCr1g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.61.1.tgz", + "integrity": "sha512-YKJ5lg35DP17gcAOggnihe+APw9HLyj1Xn7gsmGumBJAUDa6NGXNixJzmkWLhcK9TOuuyQjdamzvJefkO7qHZQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.61.1.tgz", + "integrity": "sha512-Mlil5G2Jj6a7B3LWGctg+XPL9vdXYuzCtNXfxOQ0nPjc2m6ueUktocPGH9bnAM0bNRKb/bAWTujUU7IJQdQA+g==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.61.1.tgz", + "integrity": "sha512-bVWIOIk6pV01p4CdUbPP7CJ/434z+OooYjDuFcR+44N35YvKUC66G8MGnvcWx5mWKW3g61J+t74l3Kj15Kwn2Q==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.61.1.tgz", + "integrity": "sha512-qy5pBvZbqNFheBz61R1rzsezjm0J7O2oNGoWtGoY89SZYLUfxAJTBAqDChqAIdB4rCiIbi9nF7yZ83GnNiLwSw==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.61.1.tgz", + "integrity": "sha512-E83TXjI4zm0+5f2qO+UOudaCYIhYwpJ5jq6YCZNIZ+6CbfhKrkAGezeiASBL9ElxAxFsRS9ZhESv8mfnj6TKeg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.61.1.tgz", + "integrity": "sha512-fbWnKqVkjrJN38vNe3ahkbk6iejS/3b0Nt7EEtPpE6RBacZcGXNKbzfHN3GUUlXOPghUg0j6XUGrtjX9z1sIvA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.61.1.tgz", + "integrity": "sha512-ArMl38iVAbk0New1ogihQNY6iphLi4ZaRsa037gUzv5yeKPY8TD3Dmy4x2RNC1VztU/uqm+G+/RwFrSka3Oy2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.61.1.tgz", + "integrity": "sha512-0mYtjHS9ucAbcATycCNK9IGBk/cCe/ma7EmSLGZdsxnOA8cjRIyU04wDpVAD9NiOfLUR9KTxdiO53uOkherqjQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.61.1.tgz", + "integrity": "sha512-gK1iCEPfpoSG9wfBihXxvBMi8ZfcWffYkEsC/Eih+iFENTaewvNcrEQ69lIOWYO5pePHKLHHO7nq5AILGO/HQQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.61.1.tgz", + "integrity": "sha512-X+zaP2x+j4RXGfbp/seSoRHWnPxzApilDszisZxbYH5C/jTxFhCtDNdPGZb9lJyYPs24wGxruPF7Y+sIXt9Gzw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.9.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.9.2.tgz", + "integrity": "sha512-G05zqtJhcDLb8uslf5EjCxXg9G1KQxiV8OS0R26IC//Eoyitzqe8z37I7cqvnZlrlSfgocQRfSn/AHBZJJFyGw==", + "license": "MIT", + "dependencies": { + "undici-types": ">=7.24.0 <7.24.7" + } + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/chai": { + "version": "5.3.3", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.3.3.tgz", + "integrity": "sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/check-error": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.1.3.tgz", + "integrity": "sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 16" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-eql": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.2.tgz", + "integrity": "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/es-module-lexer": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", + "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", + "dev": true, + "license": "MIT" + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-tokens": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-9.0.1.tgz", + "integrity": "sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/loupe": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.2.1.tgz", + "integrity": "sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "5.1.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-5.1.11.tgz", + "integrity": "sha512-v+KEsUv2ps74PaSKv0gHTxTCgMXOIfBEbaqa6w6ISIGC7ZsvHN4N9oJ8d4cmf0n5oTzQz2SLmThbQWhjd/8eKg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.js" + }, + "engines": { + "node": "^18 || >=20" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/pathval": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.1.tgz", + "integrity": "sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss/node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/rollup": { + "version": "4.61.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.61.1.tgz", + "integrity": "sha512-I4KW6iuRpuu2uHBLraZ1wNZe0DP7lnRha+VJ9tNaYVaVgKhW0aI3h4RYnoRPeql0flHm/Co55b7snEDcOfOJrA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.61.1", + "@rollup/rollup-android-arm64": "4.61.1", + "@rollup/rollup-darwin-arm64": "4.61.1", + "@rollup/rollup-darwin-x64": "4.61.1", + "@rollup/rollup-freebsd-arm64": "4.61.1", + "@rollup/rollup-freebsd-x64": "4.61.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.61.1", + "@rollup/rollup-linux-arm-musleabihf": "4.61.1", + "@rollup/rollup-linux-arm64-gnu": "4.61.1", + "@rollup/rollup-linux-arm64-musl": "4.61.1", + "@rollup/rollup-linux-loong64-gnu": "4.61.1", + "@rollup/rollup-linux-loong64-musl": "4.61.1", + "@rollup/rollup-linux-ppc64-gnu": "4.61.1", + "@rollup/rollup-linux-ppc64-musl": "4.61.1", + "@rollup/rollup-linux-riscv64-gnu": "4.61.1", + "@rollup/rollup-linux-riscv64-musl": "4.61.1", + "@rollup/rollup-linux-s390x-gnu": "4.61.1", + "@rollup/rollup-linux-x64-gnu": "4.61.1", + "@rollup/rollup-linux-x64-musl": "4.61.1", + "@rollup/rollup-openbsd-x64": "4.61.1", + "@rollup/rollup-openharmony-arm64": "4.61.1", + "@rollup/rollup-win32-arm64-msvc": "4.61.1", + "@rollup/rollup-win32-ia32-msvc": "4.61.1", + "@rollup/rollup-win32-x64-gnu": "4.61.1", + "@rollup/rollup-win32-x64-msvc": "4.61.1", + "fsevents": "~2.3.2" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-literal": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/strip-literal/-/strip-literal-3.1.0.tgz", + "integrity": "sha512-8r3mkIM/2+PpjHoOtiAW8Rg3jJLHaV7xPwG+YRGrv6FP0wwk/toTpATxWYOW0BKdWwl82VT2tFYi5DlROa0Mxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinypool": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", + "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "node_modules/tinyrainbow": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-2.0.0.tgz", + "integrity": "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tinyspy": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/tinyspy/-/tinyspy-4.0.4.tgz", + "integrity": "sha512-azl+t0z7pw/z958Gy9svOTuzqIk6xq+NSheJzn5MMWtWTFywIacg2wUlzKFGtt3cthx0r2SxMK0yzJOR0IES7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/ulid": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ulid/-/ulid-2.4.0.tgz", + "integrity": "sha512-fIRiVTJNcSRmXKPZtGzFQv9WRrZ3M9eoptl/teFJvjOzmpU+/K/JH6HZ8deBfb5vMEpicJcLn7JmvdknlMq7Zg==", + "license": "MIT", + "bin": { + "ulid": "bin/cli.js" + } + }, + "node_modules/undici-types": { + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", + "license": "MIT" + }, + "node_modules/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vite-node": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/vite-node/-/vite-node-3.2.4.tgz", + "integrity": "sha512-EbKSKh+bh1E1IFxeO0pg1n4dvoOTt0UDiXMd/qn++r98+jPO1xtJilvXldeuQ8giIB5IkpjCgMleHMNEsGH6pg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/packages/ion/package.json b/packages/ion/package.json new file mode 100644 index 0000000..8816ca1 --- /dev/null +++ b/packages/ion/package.json @@ -0,0 +1,58 @@ +{ + "name": "@boocode/ion", + "version": "0.0.1", + "private": true, + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + }, + "./schema": { + "types": "./dist/schema/index.d.ts", + "default": "./dist/schema/index.js" + }, + "./engine": { + "types": "./dist/engine/index.d.ts", + "default": "./dist/engine/index.js" + }, + "./store": { + "types": "./dist/store/index.d.ts", + "default": "./dist/store/index.js" + }, + "./format": { + "types": "./dist/format/index.d.ts", + "default": "./dist/format/index.js" + }, + "./cli": { + "types": "./dist/cli/index.d.ts", + "default": "./dist/cli/index.js" + } + }, + "scripts": { + "build": "tsc", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "@types/node": "^25.9.2", + "js-yaml": "^4.1.0", + "nanoid": "^5.0.9", + "ulid": "^2.3.0", + "zod": "^3.25.76" + }, + "optionalDependencies": { + "better-sqlite3": "^11.0.0", + "postgres": "^3.4.0" + }, + "devDependencies": { + "@types/better-sqlite3": "^7.6.12", + "@types/js-yaml": "^4.0.9", + "typescript": "^5.5.0", + "vitest": "^3.2.4" + }, + "license": "MIT" +} diff --git a/packages/ion/src/cli/commands/abandon.ts b/packages/ion/src/cli/commands/abandon.ts new file mode 100644 index 0000000..ec26ecd --- /dev/null +++ b/packages/ion/src/cli/commands/abandon.ts @@ -0,0 +1,55 @@ +/** + * `workflow abandon` — Cancel a non-terminal workflow run. + * + * Marks the run as cancelled. Only works on runs that are not + * already in a terminal state (completed, failed, cancelled). + * + * @example + * workflow abandon abc123 + * workflow abandon abc123 --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface AbandonResult { + runId: string; + abandoned: boolean; + message: string; +} + +async function abandonWorkflowRun(_runId: string): Promise { + throw new Error('not implemented yet: abandonWorkflowRun'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function abandonCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow abandon [--json]'); + } + + const runId = args[0]!; + + const result = await abandonWorkflowRun(runId); + + if (options.json) { + printJson(result); + return; + } + + if (result.abandoned) { + console.log(`⊘ Run ${result.runId} abandoned (cancelled).`); + } else { + console.log(`Failed to abandon run ${result.runId}: ${result.message}`); + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/approve.ts b/packages/ion/src/cli/commands/approve.ts new file mode 100644 index 0000000..506468c --- /dev/null +++ b/packages/ion/src/cli/commands/approve.ts @@ -0,0 +1,60 @@ +/** + * `workflow approve` — Approve a paused workflow run. + * + * @example + * workflow approve abc123 + * workflow approve abc123 "Looks good" --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface ApproveResult { + runId: string; + approved: boolean; + comment?: string; + message: string; +} + +async function approveWorkflowRun( + _runId: string, + _comment?: string, +): Promise { + throw new Error('not implemented yet: approveWorkflowRun'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function approveCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow approve [comment] [--json]'); + } + + const runId = args[0]!; + const comment = args.length > 1 ? args.slice(1).join(' ') : undefined; + + const result = await approveWorkflowRun(runId, comment); + + if (options.json) { + printJson(result); + return; + } + + if (result.approved) { + console.log(`✓ Run ${result.runId} approved.`); + if (result.comment) { + console.log(` Comment: ${result.comment}`); + } + } else { + console.log(`✗ Failed to approve run ${result.runId}: ${result.message}`); + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/cleanup.ts b/packages/ion/src/cli/commands/cleanup.ts new file mode 100644 index 0000000..7dadaeb --- /dev/null +++ b/packages/ion/src/cli/commands/cleanup.ts @@ -0,0 +1,74 @@ +/** + * `workflow cleanup` — Remove old workflow run artifacts. + * + * Default retention: 7 days. Removes run data older than the specified + * number of days. + * + * @example + * workflow cleanup + * workflow cleanup 30 --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface CleanupResult { + removedRuns: number; + removedEvents: number; + freedBytes: number; + retentionDays: number; +} + +async function cleanupWorkflowRuns( + _days: number, + _cwd?: string, +): Promise { + throw new Error('not implemented yet: cleanupWorkflowRuns'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function cleanupCommand( + args: string[], + options: CliOptions, +): Promise { + // First positional arg is the number of days (default 7). + const days = args.length > 0 ? parseInt(args[0]!, 10) : 7; + + if (isNaN(days) || days < 1) { + throw new Error(`Invalid retention days: ${args[0]}. Must be a positive integer.`); + } + + const result = await cleanupWorkflowRuns(days, options.cwd); + + if (options.json) { + printJson(result); + return; + } + + console.log(`Cleanup complete (retention: ${result.retentionDays} days).`); + console.log(` Runs removed: ${result.removedRuns}`); + console.log(` Events removed: ${result.removedEvents}`); + console.log(` Space freed: ${formatBytes(result.freedBytes)}`); +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +function formatBytes(bytes: number): string { + if (bytes === 0) return '0 B'; + const units = ['B', 'KB', 'MB', 'GB']; + const i = Math.min( + Math.floor(Math.log(bytes) / Math.log(1024)), + units.length - 1, + ); + const value = bytes / Math.pow(1024, i); + return `${value.toFixed(i === 0 ? 0 : 1)} ${units[i]}`; +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/convert.ts b/packages/ion/src/cli/commands/convert.ts new file mode 100644 index 0000000..be22dd0 --- /dev/null +++ b/packages/ion/src/cli/commands/convert.ts @@ -0,0 +1,62 @@ +/** + * `workflow convert` — Convert a .sop.md file to a YAML workflow definition. + * + * Reads the SOP markdown file, parses its structure, and outputs + * a corresponding YAML workflow definition. + * + * @example + * workflow convert deploy.sop.md + * workflow convert deploy.sop.md --output workflows/deploy.yaml + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface ConvertResult { + inputFile: string; + outputFile: string; + workflowName: string; + nodeCount: number; +} + +async function convertSopToYaml( + _inputPath: string, + _outputPath?: string, +): Promise { + throw new Error('not implemented yet: convertSopToYaml'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function convertCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow convert [--output ]'); + } + + const inputPath = args[0]!; + + if (!inputPath.endsWith('.sop.md')) { + throw new Error(`Input file must end with .sop.md, got: ${inputPath}`); + } + + const result = await convertSopToYaml(inputPath, options.output); + + if (options.json) { + printJson(result); + return; + } + + console.log(`Converted: ${result.inputFile}`); + console.log(` Output: ${result.outputFile}`); + console.log(` Workflow: ${result.workflowName}`); + console.log(` Nodes: ${result.nodeCount}`); +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/list.ts b/packages/ion/src/cli/commands/list.ts new file mode 100644 index 0000000..b93846a --- /dev/null +++ b/packages/ion/src/cli/commands/list.ts @@ -0,0 +1,59 @@ +/** + * `workflow list` — List all available workflows. + * + * Discovers workflows from both bundled and project sources and displays + * them in a formatted table (or JSON with --json). + * + * @example + * workflow list + * workflow list --json + */ + +import type { CliOptions } from '../utils.js'; +import { printTable, printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface WorkflowEntry { + name: string; + description: string; + source: 'bundled' | 'project'; +} + +async function discoverWorkflows(_cwd?: string): Promise { + throw new Error('not implemented yet: discoverWorkflows'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function listCommand( + _args: string[], + options: CliOptions, +): Promise { + const workflows = await discoverWorkflows(options.cwd); + + if (options.json) { + printJson(workflows); + return; + } + + console.log('Available workflows:'); + console.log(''); + + printTable( + workflows.map((w) => ({ + name: w.name, + description: w.description, + source: w.source, + })), + [ + { header: 'Name', field: 'name', minWidth: 20 }, + { header: 'Description', field: 'description', minWidth: 30 }, + { header: 'Source', field: 'source', minWidth: 10 }, + ], + ); +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/reject.ts b/packages/ion/src/cli/commands/reject.ts new file mode 100644 index 0000000..cddbdfe --- /dev/null +++ b/packages/ion/src/cli/commands/reject.ts @@ -0,0 +1,62 @@ +/** + * `workflow reject` — Reject a paused workflow run. + * + * Sets $REJECTION_REASON with the provided reason string. + * + * @example + * workflow reject abc123 + * workflow reject abc123 "Not compliant" --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface RejectResult { + runId: string; + rejected: boolean; + reason?: string; + message: string; +} + +async function rejectWorkflowRun( + _runId: string, + _reason?: string, +): Promise { + throw new Error('not implemented yet: rejectWorkflowRun'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function rejectCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow reject [reason] [--json]'); + } + + const runId = args[0]!; + const reason = args.length > 1 ? args.slice(1).join(' ') : undefined; + + const result = await rejectWorkflowRun(runId, reason); + + if (options.json) { + printJson(result); + return; + } + + if (result.rejected) { + console.log(`✗ Run ${result.runId} rejected.`); + if (result.reason) { + console.log(` Reason: ${result.reason}`); + } + } else { + console.log(`Failed to reject run ${result.runId}: ${result.message}`); + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/resume.ts b/packages/ion/src/cli/commands/resume.ts new file mode 100644 index 0000000..3433365 --- /dev/null +++ b/packages/ion/src/cli/commands/resume.ts @@ -0,0 +1,55 @@ +/** + * `workflow resume` — Resume a failed workflow run. + * + * Skips completed nodes and re-executes from the failure point. + * + * @example + * workflow resume abc123 + * workflow resume abc123 --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface ResumeResult { + runId: string; + resumed: boolean; + message: string; +} + +async function resumeWorkflowRun(_runId: string): Promise { + throw new Error('not implemented yet: resumeWorkflowRun'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function resumeCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow resume [--json]'); + } + + const runId = args[0]!; + + const result = await resumeWorkflowRun(runId); + + if (options.json) { + printJson(result); + return; + } + + if (result.resumed) { + console.log(`↻ Run ${result.runId} resumed.`); + console.log(` ${result.message}`); + } else { + console.log(`Failed to resume run ${result.runId}: ${result.message}`); + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/run.ts b/packages/ion/src/cli/commands/run.ts new file mode 100644 index 0000000..58085e7 --- /dev/null +++ b/packages/ion/src/cli/commands/run.ts @@ -0,0 +1,94 @@ +/** + * `workflow run` — Execute a workflow by name. + * + * Resolves the workflow, passes message args, and shows real-time progress. + * With --detach, runs in background and returns the run ID immediately. + * + * @example + * workflow run deploy + * workflow run deploy --cwd /tmp/project --json + * workflow run deploy --detach + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface WorkflowRunResult { + id: string; + workflowName: string; + status: string; + output?: Record; + error?: string; + startedAt: string; + completedAt?: string; +} + +async function resolveWorkflow( + _name: string, + _cwd?: string, +): Promise { + throw new Error('not implemented yet: resolveWorkflow'); +} + +async function executeWorkflow( + _workflow: unknown, + _messageArgs: string[], + _options: { cwd?: string; detach?: boolean }, +): Promise { + throw new Error('not implemented yet: executeWorkflow'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function runCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow run [args...] [--cwd ] [--detach] [--json]'); + } + + const workflowName = args[0]!; + const messageArgs = args.slice(1); + const detach = options.json ? false : false; // --detach is a flag, not in CliOptions yet + + // Parse --detach from raw args (it's a boolean flag). + // This is handled by the arg parser in the main entry point, + // but since CliOptions doesn't have detach, we check process.argv. + const isDetach = process.argv.includes('--detach'); + + const workflow = await resolveWorkflow(workflowName, options.cwd); + const result = await executeWorkflow(workflow, messageArgs, { + cwd: options.cwd, + detach: isDetach, + }); + + if (options.json) { + printJson(result); + return; + } + + if (isDetach) { + console.log(`Workflow started in background.`); + console.log(`Run ID: ${result.id}`); + console.log(`Workflow: ${result.workflowName}`); + console.log(`Status: ${result.status}`); + } else { + console.log(`Workflow run completed.`); + console.log(` Run ID: ${result.id}`); + console.log(` Workflow: ${result.workflowName}`); + console.log(` Status: ${result.status}`); + if (result.output) { + console.log(` Output: ${JSON.stringify(result.output)}`); + } + if (result.error) { + console.log(` Error: ${result.error}`); + } + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/runs.ts b/packages/ion/src/cli/commands/runs.ts new file mode 100644 index 0000000..4adb705 --- /dev/null +++ b/packages/ion/src/cli/commands/runs.ts @@ -0,0 +1,91 @@ +/** + * `workflow runs` — List recent workflow runs with filters. + * + * @example + * workflow runs + * workflow runs --status failed --limit 10 --json + * workflow runs --all + */ + +import type { CliOptions } from '../utils.js'; +import { printTable, printJson, formatTimestamp, formatDuration } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface RunRecord { + id: string; + workflowName: string; + status: string; + startedAt: string; + duration?: number; // ms, absent if still running + currentNode?: string; +} + +async function listWorkflowRuns(_filters: { + status?: string; + limit?: number; + all?: boolean; + cwd?: string; +}): Promise { + throw new Error('not implemented yet: listWorkflowRuns'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function runsCommand( + args: string[], + options: CliOptions, +): Promise { + // Parse --status, --limit, --all from args/options. + // These are already extracted by parseArgs into options. + const status = typeof (options as Record).status === 'string' + ? (options as Record).status as string + : undefined; + const limit = typeof (options as Record).limit === 'string' + ? parseInt((options as Record).limit as string, 10) + : 50; + const all = (options as Record).all === true; + + const runs = await listWorkflowRuns({ + status, + limit, + all, + cwd: options.cwd, + }); + + if (options.json) { + printJson(runs); + return; + } + + if (runs.length === 0) { + console.log('No workflow runs found.'); + return; + } + + console.log(`Showing ${runs.length} run(s):`); + console.log(''); + + printTable( + runs.map((r) => ({ + id: r.id, + workflow: r.workflowName, + status: r.status, + started: formatTimestamp(new Date(r.startedAt)), + duration: r.duration != null ? formatDuration(r.duration) : '-', + currentNode: r.currentNode ?? '-', + })), + [ + { header: 'ID', field: 'id', minWidth: 26 }, + { header: 'Workflow', field: 'workflow', minWidth: 20 }, + { header: 'Status', field: 'status', minWidth: 10 }, + { header: 'Started', field: 'started', minWidth: 19 }, + { header: 'Duration', field: 'duration', minWidth: 10 }, + { header: 'Node', field: 'currentNode', minWidth: 15 }, + ], + ); +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/status.ts b/packages/ion/src/cli/commands/status.ts new file mode 100644 index 0000000..a8a3d58 --- /dev/null +++ b/packages/ion/src/cli/commands/status.ts @@ -0,0 +1,67 @@ +/** + * `workflow status` — Show active (running + paused) workflow runs. + * + * @example + * workflow status + * workflow status --json + */ + +import type { CliOptions } from '../utils.js'; +import { printTable, printJson, formatDuration } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface ActiveRun { + id: string; + workflowName: string; + status: string; + duration: number; // ms + currentNode?: string; +} + +async function getActiveRuns(_cwd?: string): Promise { + throw new Error('not implemented yet: getActiveRuns'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function statusCommand( + _args: string[], + options: CliOptions, +): Promise { + const runs = await getActiveRuns(options.cwd); + + if (options.json) { + printJson(runs); + return; + } + + if (runs.length === 0) { + console.log('No active workflow runs.'); + return; + } + + console.log('Active workflow runs:'); + console.log(''); + + printTable( + runs.map((r) => ({ + id: r.id, + workflow: r.workflowName, + status: r.status, + duration: formatDuration(r.duration), + currentNode: r.currentNode ?? '-', + })), + [ + { header: 'ID', field: 'id', minWidth: 26 }, + { header: 'Workflow', field: 'workflow', minWidth: 20 }, + { header: 'Status', field: 'status', minWidth: 10 }, + { header: 'Duration', field: 'duration', minWidth: 10 }, + { header: 'Current Node', field: 'currentNode', minWidth: 15 }, + ], + ); +} \ No newline at end of file diff --git a/packages/ion/src/cli/commands/validate.ts b/packages/ion/src/cli/commands/validate.ts new file mode 100644 index 0000000..81094e8 --- /dev/null +++ b/packages/ion/src/cli/commands/validate.ts @@ -0,0 +1,66 @@ +/** + * `workflow validate` — Validate a workflow definition without executing. + * + * Loads the workflow, runs schema validation, and reports any errors. + * + * @example + * workflow validate deploy + * workflow validate deploy --json + */ + +import type { CliOptions } from '../utils.js'; +import { printJson } from '../utils.js'; + +// --------------------------------------------------------------------------- +// Stub: engine integration (not implemented yet) +// --------------------------------------------------------------------------- + +interface ValidationError { + path: string; + message: string; +} + +interface ValidateResult { + valid: boolean; + errors: ValidationError[]; + workflowName: string; +} + +async function validateWorkflow( + _name: string, + _cwd?: string, +): Promise { + throw new Error('not implemented yet: validateWorkflow'); +} + +// --------------------------------------------------------------------------- +// Command handler +// --------------------------------------------------------------------------- + +export async function validateCommand( + args: string[], + options: CliOptions, +): Promise { + if (args.length === 0) { + throw new Error('Missing required argument: \n\nUsage: workflow validate [--json]'); + } + + const workflowName = args[0]!; + + const result = await validateWorkflow(workflowName, options.cwd); + + if (options.json) { + printJson(result); + return; + } + + if (result.valid) { + console.log(`✓ Workflow "${result.workflowName}" is valid.`); + } else { + console.log(`✗ Workflow "${result.workflowName}" has ${result.errors.length} error(s):`); + console.log(''); + for (const err of result.errors) { + console.log(` ${err.path}: ${err.message}`); + } + } +} \ No newline at end of file diff --git a/packages/ion/src/cli/index.ts b/packages/ion/src/cli/index.ts new file mode 100644 index 0000000..cd4cc71 --- /dev/null +++ b/packages/ion/src/cli/index.ts @@ -0,0 +1,207 @@ +/** + * Ion workflow engine CLI entry point. + * + * Pure Node.js CLI using process.argv parsing — no external argparse library. + * Routes subcommands to their respective handler modules. + * + * @example + * node dist/cli/index.js workflow list --json + * node dist/cli/index.js workflow run deploy --cwd /tmp/project + */ + +import { parseArgs, buildCliOptions, printJson } from './utils.js'; +import type { CliOptions } from './utils.js'; + +import { listCommand } from './commands/list.js'; +import { runCommand } from './commands/run.js'; +import { statusCommand } from './commands/status.js'; +import { runsCommand } from './commands/runs.js'; +import { approveCommand } from './commands/approve.js'; +import { rejectCommand } from './commands/reject.js'; +import { resumeCommand } from './commands/resume.js'; +import { abandonCommand } from './commands/abandon.js'; +import { cleanupCommand } from './commands/cleanup.js'; +import { validateCommand } from './commands/validate.js'; +import { convertCommand } from './commands/convert.js'; + +// --------------------------------------------------------------------------- +// Command registry +// --------------------------------------------------------------------------- + +interface CommandEntry { + name: string; + description: string; + usage: string; + handler: (args: string[], options: CliOptions) => Promise; +} + +const COMMANDS: CommandEntry[] = [ + { + name: 'list', + description: 'List all available workflows', + usage: 'workflow list [--json]', + handler: listCommand, + }, + { + name: 'run', + description: 'Execute a workflow by name', + usage: 'workflow run [args...] [--cwd ] [--detach] [--json]', + handler: runCommand, + }, + { + name: 'status', + description: 'Show active (running + paused) workflow runs', + usage: 'workflow status [--json]', + handler: statusCommand, + }, + { + name: 'runs', + description: 'List recent workflow runs with filters', + usage: 'workflow runs [--status ] [--limit N] [--all] [--json]', + handler: runsCommand, + }, + { + name: 'approve', + description: 'Approve a paused workflow run', + usage: 'workflow approve [comment] [--json]', + handler: approveCommand, + }, + { + name: 'reject', + description: 'Reject a paused workflow run', + usage: 'workflow reject [reason] [--json]', + handler: rejectCommand, + }, + { + name: 'resume', + description: 'Resume a failed workflow run', + usage: 'workflow resume [--json]', + handler: resumeCommand, + }, + { + name: 'abandon', + description: 'Cancel a non-terminal workflow run', + usage: 'workflow abandon [--json]', + handler: abandonCommand, + }, + { + name: 'cleanup', + description: 'Remove old workflow run artifacts', + usage: 'workflow cleanup [days] [--json]', + handler: cleanupCommand, + }, + { + name: 'validate', + description: 'Validate a workflow definition without executing', + usage: 'workflow validate [--json]', + handler: validateCommand, + }, + { + name: 'convert', + description: 'Convert a .sop.md file to a YAML workflow definition', + usage: 'workflow convert [--output ]', + handler: convertCommand, + }, +]; + +// --------------------------------------------------------------------------- +// Help output +// --------------------------------------------------------------------------- + +function printHelp(): void { + console.log(''); + console.log('Ion — Workflow Engine CLI'); + console.log(''); + console.log('Usage:'); + console.log(' workflow [options]'); + console.log(''); + console.log('Commands:'); + + const maxNameLen = Math.max(...COMMANDS.map((c) => c.name.length)); + for (const cmd of COMMANDS) { + const padded = cmd.name.padEnd(maxNameLen + 2); + console.log(` ${padded}${cmd.description}`); + } + + console.log(''); + console.log('Global options:'); + console.log(' --json Output as JSON (suppresses all other output)'); + console.log(' --cwd Set working directory'); + console.log(' --store Path to workflow store'); + console.log(' --db-path

Path to database file'); + console.log(''); + console.log('Run "workflow --help" for command-specific usage.'); + console.log(''); +} + +function printCommandHelp(cmd: CommandEntry): void { + console.log(''); + console.log(`workflow ${cmd.name}`); + console.log(''); + console.log(` ${cmd.description}`); + console.log(''); + console.log('Usage:'); + console.log(` ${cmd.usage}`); + console.log(''); +} + +// --------------------------------------------------------------------------- +// Main entry +// --------------------------------------------------------------------------- + +export async function main(argv: string[] = process.argv.slice(2)): Promise { + const { args, options } = parseArgs(argv); + const cliOptions = buildCliOptions(options); + + // --help with no command → general help + if (args.length === 0 || options.help === true) { + printHelp(); + process.exit(0); + } + + const commandName = args[0]; + const commandArgs = args.slice(1); + + // --help after a command name → command-specific help + if (options.help) { + const cmd = COMMANDS.find((c) => c.name === commandName); + if (cmd) { + printCommandHelp(cmd); + } else { + console.error(`Unknown command: ${commandName}`); + printHelp(); + } + process.exit(0); + } + + const command = COMMANDS.find((c) => c.name === commandName); + + if (!command) { + console.error(`Unknown command: ${commandName}`); + printHelp(); + process.exit(1); + return; // unreachable, but satisfies TS control flow + } + + try { + await command.handler(commandArgs, cliOptions); + } catch (err: unknown) { + const message = err instanceof Error ? err.message : String(err); + if (cliOptions.json) { + printJson({ error: message }); + } else { + console.error(`Error: ${message}`); + } + process.exit(1); + } +} + +// Run when executed directly (not imported). +// In ESM, check import.meta.url to detect direct execution. +const _directRun = typeof import.meta !== 'undefined' && import.meta.url; +if (_directRun) { + main().catch((err: unknown) => { + console.error(err); + process.exit(1); + }); +} \ No newline at end of file diff --git a/packages/ion/src/cli/utils.ts b/packages/ion/src/cli/utils.ts new file mode 100644 index 0000000..bb5a3ab --- /dev/null +++ b/packages/ion/src/cli/utils.ts @@ -0,0 +1,239 @@ +/** + * CLI utility functions for the Ion workflow engine. + * + * Provides formatting, table rendering, and JSON output helpers + * used across all CLI commands. + */ + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +export interface CliOptions { + /** Working directory override. */ + cwd?: string; + /** Output as JSON (suppresses all other output). */ + json?: boolean; + /** Path to the workflow store database. */ + store?: string; + /** Path to the database file. */ + dbPath?: string; + /** Output file path (for convert command). */ + output?: string; +} + +// --------------------------------------------------------------------------- +// Duration formatting +// --------------------------------------------------------------------------- + +/** + * Format a duration in milliseconds into a human-readable string. + * + * @example + * formatDuration(90500) // "1m 30s" + * formatDuration(3661000) // "1h 1m" + * formatDuration(500) // "0s" + */ +export function formatDuration(ms: number): string { + if (ms < 0) ms = 0; + + const seconds = Math.floor(ms / 1000) % 60; + const minutes = Math.floor(ms / 60000) % 60; + const hours = Math.floor(ms / 3600000); + + if (hours > 0) { + return minutes > 0 ? `${hours}h ${minutes}m` : `${hours}h`; + } + if (minutes > 0) { + return seconds > 0 ? `${minutes}m ${seconds}s` : `${minutes}m`; + } + return `${seconds}s`; +} + +// --------------------------------------------------------------------------- +// Timestamp formatting +// --------------------------------------------------------------------------- + +/** + * Format a Date into an ISO-like timestamp suitable for CLI display. + * + * @example + * formatTimestamp(new Date('2025-06-07T14:30:00Z')) + * // "2025-06-07 14:30:00" + */ +export function formatTimestamp(date: Date): string { + const y = date.getFullYear(); + const mo = String(date.getMonth() + 1).padStart(2, '0'); + const d = String(date.getDate()).padStart(2, '0'); + const h = String(date.getHours()).padStart(2, '0'); + const mi = String(date.getMinutes()).padStart(2, '0'); + const s = String(date.getSeconds()).padStart(2, '0'); + return `${y}-${mo}-${d} ${h}:${mi}:${s}`; +} + +// --------------------------------------------------------------------------- +// String truncation +// --------------------------------------------------------------------------- + +/** + * Truncate a string to `max` characters, appending an ellipsis if truncated. + * + * @example + * truncate('hello world', 8) // "hello..." + * truncate('hi', 8) // "hi" + */ +export function truncate(str: string, max: number): string { + if (str.length <= max) return str; + if (max <= 3) return str.slice(0, max); + return str.slice(0, max - 3) + '...'; +} + +// --------------------------------------------------------------------------- +// Table rendering +// --------------------------------------------------------------------------- + +export interface TableColumn { + /** Column header label. */ + header: string; + /** Minimum column width. */ + minWidth?: number; + /** Field name to extract from each row object. */ + field: string; +} + +/** + * Print a formatted table to stdout. + * + * @param rows - Array of row objects. + * @param columns - Column definitions with header labels and field names. + * + * @example + * printTable( + * [{ name: 'deploy', desc: 'Deploy app' }], + * [{ header: 'Name', field: 'name' }, { header: 'Description', field: 'desc' }], + * ) + */ +export function printTable( + rows: Record[], + columns: TableColumn[], +): void { + if (rows.length === 0) { + console.log('(no results)'); + return; + } + + // Compute column widths. + const widths: number[] = columns.map((col) => { + const headerLen = col.header.length; + const dataLen = Math.max( + ...rows.map((row) => { + const val = row[col.field]; + const str = val === undefined || val === null ? '' : String(val); + return str.length; + }), + 0, + ); + const min = col.minWidth ?? 0; + return Math.max(headerLen, dataLen, min); + }); + + // Header row. + const headerLine = columns + .map((col, i) => col.header.padEnd(widths[i]!)) + .join(' '); + console.log(headerLine); + + // Separator. + const sepLine = widths.map((w) => '-'.repeat(w)).join(' '); + console.log(sepLine); + + // Data rows. + for (const row of rows) { + const line = columns + .map((col, i) => { + const val = row[col.field]; + const str = val === undefined || val === null ? '' : String(val); + return str.padEnd(widths[i]!); + }) + .join(' '); + console.log(line); + } +} + +// --------------------------------------------------------------------------- +// JSON output +// --------------------------------------------------------------------------- + +/** + * Print a data structure as formatted JSON to stdout. + * Uses 2-space indentation. + */ +export function printJson(data: unknown): void { + console.log(JSON.stringify(data, null, 2)); +} + +// --------------------------------------------------------------------------- +// Argument parsing +// --------------------------------------------------------------------------- + +/** + * Parse CLI arguments into positional args and named options. + * + * Supports `--flag` (boolean) and `--key value` (string) formats. + * Everything after `--` is treated as positional. + * + * @example + * parseArgs(['run', 'deploy', '--json', '--cwd', '/tmp']) + * // { args: ['run', 'deploy'], options: { json: true, cwd: '/tmp' } } + */ +export function parseArgs(argv: string[]): { + args: string[]; + options: Record; +} { + const args: string[] = []; + const options: Record = {}; + let i = 0; + + while (i < argv.length) { + const token = argv[i]!; + + if (token === '--') { + args.push(...argv.slice(i + 1)); + break; + } + + if (token.startsWith('--')) { + const key = token.slice(2); + const next = argv[i + 1]; + + if (next && !next.startsWith('--')) { + options[key] = next; + i += 2; + } else { + options[key] = true; + i += 1; + } + } else { + args.push(token); + i += 1; + } + } + + return { args, options }; +} + +/** + * Build a CliOptions object from parsed options. + * Extracts known CLI flags into their typed fields. + */ +export function buildCliOptions( + options: Record, +): CliOptions { + return { + cwd: typeof options.cwd === 'string' ? options.cwd : undefined, + json: options.json === true, + store: typeof options.store === 'string' ? options.store : undefined, + dbPath: typeof options['db-path'] === 'string' ? options['db-path'] : undefined, + output: typeof options.output === 'string' ? options.output : undefined, + }; +} \ No newline at end of file diff --git a/packages/ion/src/engine/__tests__/command-validation.test.ts b/packages/ion/src/engine/__tests__/command-validation.test.ts new file mode 100644 index 0000000..334a059 --- /dev/null +++ b/packages/ion/src/engine/__tests__/command-validation.test.ts @@ -0,0 +1,70 @@ +import { describe, it, expect } from 'vitest'; +import { isValidCommandName } from '../command-validation.js'; + +describe('isValidCommandName', () => { + describe('valid command names', () => { + it('accepts simple lowercase names', () => { + expect(isValidCommandName('assist')).toBe(true); + }); + + it('accepts kebab-case names', () => { + expect(isValidCommandName('code-review')).toBe(true); + }); + + it('accepts names with numbers', () => { + expect(isValidCommandName('deploy-v2')).toBe(true); + }); + + it('accepts single character names', () => { + expect(isValidCommandName('a')).toBe(true); + }); + + it('accepts names with only numbers', () => { + expect(isValidCommandName('123')).toBe(true); + }); + + it('accepts names with mixed alphanumeric and hyphens', () => { + expect(isValidCommandName('a1-b2-c3')).toBe(true); + }); + + it('accepts names starting with numbers', () => { + expect(isValidCommandName('2fa-verify')).toBe(true); + }); + }); + + describe('invalid command names', () => { + it('rejects uppercase letters', () => { + expect(isValidCommandName('Assist')).toBe(false); + expect(isValidCommandName('CODE-REVIEW')).toBe(false); + }); + + it('rejects leading hyphens', () => { + expect(isValidCommandName('-assist')).toBe(false); + }); + + it('rejects trailing hyphens', () => { + expect(isValidCommandName('assist-')).toBe(false); + }); + + it('rejects double hyphens', () => { + expect(isValidCommandName('code--review')).toBe(false); + }); + + it('rejects empty strings', () => { + expect(isValidCommandName('')).toBe(false); + }); + + it('rejects underscores', () => { + expect(isValidCommandName('code_review')).toBe(false); + }); + + it('rejects spaces', () => { + expect(isValidCommandName('code review')).toBe(false); + }); + + it('rejects special characters', () => { + expect(isValidCommandName('code.review')).toBe(false); + expect(isValidCommandName('code@review')).toBe(false); + }); + }); +}); \ No newline at end of file diff --git a/packages/ion/src/engine/__tests__/condition-evaluator.test.ts b/packages/ion/src/engine/__tests__/condition-evaluator.test.ts new file mode 100644 index 0000000..a7accb4 --- /dev/null +++ b/packages/ion/src/engine/__tests__/condition-evaluator.test.ts @@ -0,0 +1,149 @@ +import { describe, it, expect } from 'vitest'; +import { evaluateCondition, ConditionError } from '../condition-evaluator.js'; + +describe('evaluateCondition', () => { + describe('simple boolean conditions', () => { + it('evaluates boolean true in a comparison', () => { + expect(evaluateCondition('true == true', {})).toBe(true); + }); + + it('evaluates boolean false in a comparison', () => { + expect(evaluateCondition('false == false', {})).toBe(true); + }); + + it('evaluates true != false', () => { + expect(evaluateCondition('true != false', {})).toBe(true); + }); + + it('evaluates boolean via node reference', () => { + const outputs = { flag: { output: true } }; + expect(evaluateCondition('$flag.output == true', outputs)).toBe(true); + }); + }); + + describe('string equality with node references', () => { + it('evaluates $nodeId.output == "value" as true when matching', () => { + const outputs = { analysis: { output: 'done' } }; + expect(evaluateCondition('$analysis.output == "done"', outputs)).toBe(true); + }); + + it('evaluates $nodeId.output == "value" as false when not matching', () => { + const outputs = { analysis: { output: 'pending' } }; + expect(evaluateCondition('$analysis.output == "done"', outputs)).toBe(false); + }); + + it('evaluates $nodeId.output != "value" correctly', () => { + const outputs = { analysis: { output: 'pending' } }; + expect(evaluateCondition('$analysis.output != "done"', outputs)).toBe(true); + }); + }); + + describe('numeric comparisons', () => { + it('evaluates $score.output > 5 as true', () => { + const outputs = { score: { output: 10 } }; + expect(evaluateCondition('$score.output > 5', outputs)).toBe(true); + }); + + it('evaluates $score.output > 5 as false when score is lower', () => { + const outputs = { score: { output: 3 } }; + expect(evaluateCondition('$score.output > 5', outputs)).toBe(false); + }); + + it('evaluates >= comparison', () => { + const outputs = { score: { output: 5 } }; + expect(evaluateCondition('$score.output >= 5', outputs)).toBe(true); + }); + + it('evaluates < comparison', () => { + const outputs = { score: { output: 3 } }; + expect(evaluateCondition('$score.output < 5', outputs)).toBe(true); + }); + + it('evaluates <= comparison', () => { + const outputs = { score: { output: 5 } }; + expect(evaluateCondition('$score.output <= 5', outputs)).toBe(true); + }); + }); + + describe('AND/OR compounds', () => { + it('evaluates AND compound: both true', () => { + const outputs = { a: { output: 'x' }, b: { output: 'y' } }; + expect(evaluateCondition('$a.output == "x" AND $b.output == "y"', outputs)).toBe(true); + }); + + it('evaluates AND compound: one false', () => { + const outputs = { a: { output: 'x' }, b: { output: 'z' } }; + expect(evaluateCondition('$a.output == "x" AND $b.output == "y"', outputs)).toBe(false); + }); + + it('evaluates OR compound: one true', () => { + const outputs = { a: { output: 'x' }, b: { output: 'z' } }; + expect(evaluateCondition('$a.output == "x" OR $b.output == "y"', outputs)).toBe(true); + }); + + it('evaluates OR compound: both false', () => { + const outputs = { a: { output: 'z' }, b: { output: 'z' } }; + expect(evaluateCondition('$a.output == "x" OR $b.output == "y"', outputs)).toBe(false); + }); + + it('evaluates mixed AND/OR with correct precedence', () => { + const outputs = { a: { output: 'x' }, b: { output: 'y' }, c: { output: 'z' } }; + // false AND true OR true => false OR true => true (AND binds tighter) + expect(evaluateCondition('$a.output == "wrong" AND $b.output == "y" OR $c.output == "z"', outputs)).toBe(true); + }); + }); + + describe('parenthesized expressions', () => { + it('evaluates parenthesized expressions', () => { + const outputs = { a: { output: 'x' }, b: { output: 'y' } }; + expect(evaluateCondition('($a.output == "x" OR $a.output == "z") AND $b.output == "y"', outputs)).toBe(true); + }); + }); + + describe('error handling', () => { + it('throws ConditionError on invalid expressions', () => { + expect(() => evaluateCondition('!!!invalid', {})).toThrow(ConditionError); + }); + + it('throws ConditionError on missing node reference', () => { + expect(() => evaluateCondition('$missing.output == "x"', {})).toThrow(ConditionError); + }); + + it('throws ConditionError on node reference without field', () => { + expect(() => evaluateCondition('$analysis == "x"', {})).toThrow(ConditionError); + }); + + it('throws ConditionError on unterminated string', () => { + expect(() => evaluateCondition('"unterminated', {})).toThrow(ConditionError); + }); + }); + + describe('whitespace handling', () => { + it('handles extra whitespace around operators', () => { + const outputs = { a: { output: 'x' } }; + expect(evaluateCondition(' $a.output == "x" ', outputs)).toBe(true); + }); + }); + + describe('quoted strings with special characters', () => { + it('handles double-quoted strings with spaces', () => { + const outputs = { msg: { output: 'hello world' } }; + expect(evaluateCondition('$msg.output == "hello world"', outputs)).toBe(true); + }); + + it('handles single-quoted strings', () => { + const outputs = { msg: { output: 'hello' } }; + expect(evaluateCondition("$msg.output == 'hello'", outputs)).toBe(true); + }); + }); + + describe('empty condition', () => { + it('returns true for empty string', () => { + expect(evaluateCondition('', {})).toBe(true); + }); + + it('returns true for whitespace-only string', () => { + expect(evaluateCondition(' ', {})).toBe(true); + }); + }); +}); \ No newline at end of file diff --git a/packages/ion/src/engine/__tests__/output-ref.test.ts b/packages/ion/src/engine/__tests__/output-ref.test.ts new file mode 100644 index 0000000..7927d50 --- /dev/null +++ b/packages/ion/src/engine/__tests__/output-ref.test.ts @@ -0,0 +1,132 @@ +import { describe, it, expect } from 'vitest'; +import { + resolveNodeOutputField, + declaredFieldsFromSchema, + OutputRefError, +} from '../output-ref.js'; + +describe('resolveNodeOutputField', () => { + describe('with declared schema match', () => { + it('returns value when field exists in output and schema', () => { + const declaredFields = new Set(['name', 'status']); + const output = { name: 'test-result', status: 'completed' }; + const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields); + expect(result).toEqual({ kind: 'value', value: 'test-result' }); + }); + + it('returns JSON-serialized value for non-string fields', () => { + const declaredFields = new Set(['count']); + const output = { count: 42 }; + const result = resolveNodeOutputField(output, 'node-1', 'count', declaredFields); + expect(result).toEqual({ kind: 'value', value: '42' }); + }); + + it('returns JSON-serialized value for object fields', () => { + const declaredFields = new Set(['data']); + const output = { data: { key: 'val' } }; + const result = resolveNodeOutputField(output, 'node-1', 'data', declaredFields); + expect(result).toEqual({ kind: 'value', value: '{"key":"val"}' }); + }); + }); + + describe('with schemaless JSON output', () => { + it('returns value when field exists in output without schema', () => { + const output = { dynamic_field: 'hello' }; + const result = resolveNodeOutputField(output, 'node-1', 'dynamic_field'); + expect(result).toEqual({ kind: 'value', value: 'hello' }); + }); + + it('returns JSON-serialized number without schema', () => { + const output = { score: 99 }; + const result = resolveNodeOutputField(output, 'node-1', 'score'); + expect(result).toEqual({ kind: 'value', value: '99' }); + }); + }); + + describe('missing optional field', () => { + it('returns empty when field is declared in schema but missing from output', () => { + const declaredFields = new Set(['name', 'optional_field']); + const output = { name: 'test' }; + const result = resolveNodeOutputField(output, 'node-1', 'optional_field', declaredFields); + expect(result).toEqual({ kind: 'empty', value: '' }); + }); + + it('returns empty when field exists but value is null', () => { + const declaredFields = new Set(['name']); + const output = { name: null }; + const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields); + expect(result).toEqual({ kind: 'empty', value: '' }); + }); + + it('returns empty when field exists but value is undefined', () => { + const declaredFields = new Set(['name']); + const output = { name: undefined }; + const result = resolveNodeOutputField(output, 'node-1', 'name', declaredFields); + expect(result).toEqual({ kind: 'empty', value: '' }); + }); + }); + + describe('missing required field', () => { + it('throws OutputRefError when field is not declared and not in output', () => { + const output = { name: 'test' }; + expect(() => resolveNodeOutputField(output, 'node-1', 'nonexistent')).toThrow(OutputRefError); + }); + + it('throws OutputRefError with nodeId and field info', () => { + const output = { name: 'test' }; + try { + resolveNodeOutputField(output, 'my-node', 'missing_field'); + expect.unreachable('Should have thrown'); + } catch (err) { + expect(err).toBeInstanceOf(OutputRefError); + if (err instanceof OutputRefError) { + expect(err.nodeId).toBe('my-node'); + expect(err.field).toBe('missing_field'); + } + } + }); + + it('includes available fields in error message', () => { + const output = { name: 'test', status: 'ok' }; + try { + resolveNodeOutputField(output, 'node-1', 'nonexistent'); + expect.unreachable('Should have thrown'); + } catch (err) { + expect(err).toBeInstanceOf(OutputRefError); + if (err instanceof OutputRefError) { + expect(err.message).toContain('name'); + expect(err.message).toContain('status'); + } + } + }); + }); +}); + +describe('declaredFieldsFromSchema', () => { + it('extracts fields from a valid JSON Schema object', () => { + const schema = { + type: 'object', + properties: { + name: { type: 'string' }, + count: { type: 'number' }, + }, + }; + const fields = declaredFieldsFromSchema(schema); + expect(fields).toEqual(new Set(['name', 'count'])); + }); + + it('returns empty set for undefined schema', () => { + const fields = declaredFieldsFromSchema(undefined); + expect(fields).toEqual(new Set()); + }); + + it('returns empty set for string schema', () => { + const fields = declaredFieldsFromSchema('just a string description'); + expect(fields).toEqual(new Set()); + }); + + it('returns empty set for schema without properties', () => { + const fields = declaredFieldsFromSchema({ type: 'object' }); + expect(fields).toEqual(new Set()); + }); +}); \ No newline at end of file diff --git a/packages/ion/src/engine/command-validation.ts b/packages/ion/src/engine/command-validation.ts new file mode 100644 index 0000000..245f54b --- /dev/null +++ b/packages/ion/src/engine/command-validation.ts @@ -0,0 +1,27 @@ +/** + * Command name validation for the Ion workflow engine. + * + * Command names must be lowercase kebab-case: lowercase alphanumeric + * segments separated by single hyphens. + */ + +/** Pattern for valid command names: lowercase kebab-case. */ +const COMMAND_NAME_PATTERN = /^[a-z0-9]+(-[a-z0-9]+)*$/; + +/** + * Validate a command name. + * + * Valid names match the pattern: `^[a-z0-9]+(-[a-z0-9]+)*$` + * - Lowercase alphanumeric segments + * - Segments separated by single hyphens + * - No leading or trailing hyphens + * - No consecutive hyphens + * + * @returns `true` if the name is valid, `false` otherwise. + */ +export function isValidCommandName(name: string): boolean { + if (name.length === 0) { + return false; + } + return COMMAND_NAME_PATTERN.test(name); +} \ No newline at end of file diff --git a/packages/ion/src/engine/condition-evaluator.ts b/packages/ion/src/engine/condition-evaluator.ts new file mode 100644 index 0000000..85cd119 --- /dev/null +++ b/packages/ion/src/engine/condition-evaluator.ts @@ -0,0 +1,427 @@ +/** + * Condition evaluator for the Ion workflow engine. + * + * Parses and evaluates `when:` conditions that reference node outputs. + * Supports comparison operators, AND/OR compounds, and literal values. + * + * Grammar (informal): + * condition = orExpr + * orExpr = andExpr ( "OR" andExpr )* + * andExpr = comparison ( "AND" comparison )* + * comparison = value operator value + * value = nodeRef | literal + * nodeRef = "$" nodeId "." field + * literal = number | boolean | quotedString + * operator = "==" | "!=" | "<" | ">" | "<=" | ">=" + */ + +import { resolveNodeOutputField, OutputRefError } from './output-ref.js'; + +// --------------------------------------------------------------------------- +// Error type +// --------------------------------------------------------------------------- + +export class ConditionError extends Error { + public readonly expression: string; + + constructor(expression: string, message: string) { + super(`Condition evaluation error in "${expression}": ${message}`); + this.name = 'ConditionError'; + this.expression = expression; + } +} + +// --------------------------------------------------------------------------- +// Token types +// --------------------------------------------------------------------------- + +type TokenType = + | 'NODE_REF' // $nodeId.field + | 'NUMBER' // 42, 3.14 + | 'BOOLEAN' // true, false + | 'STRING' // "hello" or 'hello' + | 'OPERATOR' // ==, !=, <, >, <=, >= + | 'AND' // AND keyword + | 'OR' // OR keyword + | 'LPAREN' // ( + | 'RPAREN' // ) + | 'EOF'; + +interface Token { + type: TokenType; + value: string; +} + +// --------------------------------------------------------------------------- +// Tokenizer +// --------------------------------------------------------------------------- + +const OPERATORS = new Set(['==', '!=', '<=', '>=', '<', '>']); + +function tokenize(expression: string): Token[] { + const tokens: Token[] = []; + let pos = 0; + + while (pos < expression.length) { + // Skip whitespace. + if (/\s/.test(expression[pos]!)) { + pos++; + continue; + } + + // Parentheses. + if (expression[pos] === '(') { + tokens.push({ type: 'LPAREN', value: '(' }); + pos++; + continue; + } + if (expression[pos] === ')') { + tokens.push({ type: 'RPAREN', value: ')' }); + pos++; + continue; + } + + // Node reference: $nodeId.field + if (expression[pos] === '$') { + const start = pos; + pos++; // skip $ + let field = ''; + // Read the nodeId (alphanumeric, underscores, hyphens). + while (pos < expression.length && /[\w-]/.test(expression[pos]!)) { + pos++; + } + const nodeId = expression.slice(start + 1, pos); + if (nodeId.length === 0) { + throw new ConditionError(expression, `Expected node identifier after $ at position ${start}`); + } + // Expect a dot then field name. + if (expression[pos] !== '.') { + throw new ConditionError(expression, `Expected "." after node reference $${nodeId} at position ${pos}`); + } + pos++; // skip dot + const fieldStart = pos; + while (pos < expression.length && /[\w-]/.test(expression[pos]!)) { + pos++; + } + field = expression.slice(fieldStart, pos); + if (field.length === 0) { + throw new ConditionError(expression, `Expected field name after $${nodeId}. at position ${fieldStart}`); + } + tokens.push({ type: 'NODE_REF', value: `${nodeId}.${field}` }); + continue; + } + + // Quoted string. + if (expression[pos] === '"' || expression[pos] === "'") { + const quote = expression[pos]!; + const start = pos; + pos++; + let str = ''; + while (pos < expression.length && expression[pos] !== quote) { + if (expression[pos] === '\\' && pos + 1 < expression.length) { + pos++; // skip escape + str += expression[pos]!; + } else { + str += expression[pos]!; + } + pos++; + } + if (pos >= expression.length) { + throw new ConditionError(expression, `Unterminated string starting at position ${start}`); + } + pos++; // skip closing quote + tokens.push({ type: 'STRING', value: str }); + continue; + } + + // Two-character operators. + if (pos + 1 < expression.length) { + const twoChar = expression.slice(pos, pos + 2); + if (OPERATORS.has(twoChar)) { + tokens.push({ type: 'OPERATOR', value: twoChar }); + pos += 2; + continue; + } + } + + // Single-character operators. + const oneChar = expression[pos]!; + if (OPERATORS.has(oneChar)) { + tokens.push({ type: 'OPERATOR', value: oneChar }); + pos++; + continue; + } + + // AND / OR keywords. + const remaining = expression.slice(pos); + const andMatch = remaining.match(/^AND(?=\s|\(|$)/i); + if (andMatch) { + tokens.push({ type: 'AND', value: 'AND' }); + pos += 3; + continue; + } + const orMatch = remaining.match(/^OR(?=\s|\(|$)/i); + if (orMatch) { + tokens.push({ type: 'OR', value: 'OR' }); + pos += 2; + continue; + } + + // Boolean literals. + const trueMatch = remaining.match(/^true(?=\s|\)|$)/i); + if (trueMatch) { + tokens.push({ type: 'BOOLEAN', value: 'true' }); + pos += 4; + continue; + } + const falseMatch = remaining.match(/^false(?=\s|\)|$)/i); + if (falseMatch) { + tokens.push({ type: 'BOOLEAN', value: 'false' }); + pos += 5; + continue; + } + + // Number literal. + const numMatch = remaining.match(/^(-?\d+\.?\d*)/); + if (numMatch && numMatch[1] !== undefined) { + tokens.push({ type: 'NUMBER', value: numMatch[1] }); + pos += numMatch[1].length; + continue; + } + + throw new ConditionError( + expression, + `Unexpected character "${expression[pos]}" at position ${pos}`, + ); + } + + tokens.push({ type: 'EOF', value: '' }); + return tokens; +} + +// --------------------------------------------------------------------------- +// Parser (recursive descent) +// --------------------------------------------------------------------------- + +class ConditionParser { + private pos = 0; + + constructor( + private tokens: Token[], + private expression: string, + private nodeOutputs: Record>, + ) {} + + parse(): boolean { + const result = this.parseOr(); + if (this.tokens[this.pos]!.type !== 'EOF') { + throw new ConditionError( + this.expression, + `Unexpected token "${this.tokens[this.pos]!.value}" after expression`, + ); + } + return result; + } + + // orExpr = andExpr ( "OR" andExpr )* + private parseOr(): boolean { + let result = this.parseAnd(); + while (this.tokens[this.pos]!.type === 'OR') { + this.pos++; // consume OR + const right = this.parseAnd(); + result = result || right; + } + return result; + } + + // andExpr = comparison ( "AND" comparison )* + private parseAnd(): boolean { + let result = this.parseComparison(); + while (this.tokens[this.pos]!.type === 'AND') { + this.pos++; // consume AND + const right = this.parseComparison(); + result = result && right; + } + return result; + } + + // comparison = value operator value | "(" orExpr ")" + private parseComparison(): boolean { + // Parenthesized expression. + if (this.tokens[this.pos]!.type === 'LPAREN') { + this.pos++; // consume ( + const result = this.parseOr(); + if (this.tokens[this.pos]!.type !== 'RPAREN') { + throw new ConditionError(this.expression, 'Expected closing ")"'); + } + this.pos++; // consume ) + return result; + } + + // value operator value + const left = this.resolveValue(); + const opToken = this.tokens[this.pos]!; + + if (opToken.type !== 'OPERATOR') { + throw new ConditionError( + this.expression, + `Expected comparison operator, got "${opToken.value}" (${opToken.type})`, + ); + } + + this.pos++; // consume operator + const right = this.resolveValue(); + + return this.compare(left, opToken.value, right); + } + + private resolveValue(): string | number | boolean { + const token = this.tokens[this.pos]!; + + switch (token.type) { + case 'NODE_REF': { + this.pos++; + const dotIndex = token.value.indexOf('.'); + if (dotIndex === -1) { + throw new ConditionError( + this.expression, + `Invalid node reference: ${token.value}`, + ); + } + const nodeId = token.value.slice(0, dotIndex); + const field = token.value.slice(dotIndex + 1); + + const output = this.nodeOutputs[nodeId]; + if (!output) { + throw new ConditionError( + this.expression, + `Node "${nodeId}" has no output available. Available nodes: ${Object.keys(this.nodeOutputs).join(', ') || '(none)'}`, + ); + } + + try { + const result = resolveNodeOutputField(output, nodeId, field); + // For comparison, we need the raw value, not the stringified version. + const rawValue = output[field]; + if (typeof rawValue === 'number') return rawValue; + if (typeof rawValue === 'boolean') return rawValue; + return result.value; + } catch (err) { + if (err instanceof OutputRefError) { + throw new ConditionError(this.expression, err.message); + } + throw err; + } + } + + case 'NUMBER': { + this.pos++; + const num = Number(token.value); + if (Number.isNaN(num)) { + throw new ConditionError( + this.expression, + `Invalid number literal: ${token.value}`, + ); + } + return num; + } + + case 'BOOLEAN': { + this.pos++; + return token.value.toLowerCase() === 'true'; + } + + case 'STRING': { + this.pos++; + return token.value; + } + + default: + throw new ConditionError( + this.expression, + `Expected value (node reference, number, boolean, or string), got "${token.value}" (${token.type})`, + ); + } + } + + private compare( + left: string | number | boolean, + op: string, + right: string | number | boolean, + ): boolean { + // Coerce types for comparison. + const leftNum = typeof left === 'number' ? left : Number(left); + const rightNum = typeof right === 'number' ? right : Number(right); + + switch (op) { + case '==': + return left === right; + case '!=': + return left !== right; + case '<': + if (!Number.isNaN(leftNum) && !Number.isNaN(rightNum)) { + return leftNum < rightNum; + } + return String(left) < String(right); + case '>': + if (!Number.isNaN(leftNum) && !Number.isNaN(rightNum)) { + return leftNum > rightNum; + } + return String(left) > String(right); + case '<=': + if (!Number.isNaN(leftNum) && !Number.isNaN(rightNum)) { + return leftNum <= rightNum; + } + return String(left) <= String(right); + case '>=': + if (!Number.isNaN(leftNum) && !Number.isNaN(rightNum)) { + return leftNum >= rightNum; + } + return String(left) >= String(right); + default: + throw new ConditionError(this.expression, `Unknown operator: ${op}`); + } + } +} + +// --------------------------------------------------------------------------- +// Public API +// --------------------------------------------------------------------------- + +/** + * Evaluate a `when:` condition expression against node outputs. + * + * Supports: + * - Node output references: `$nodeId.field` + * - Comparison operators: `==`, `!=`, `<`, `>`, `<=`, `>=` + * - Logical compounds: `AND`, `OR` + * - Parenthesized sub-expressions + * - Literal values: numbers, booleans, quoted strings + * + * Returns `true` or `false`. Throws `ConditionError` on parse failure (fail-closed). + */ +export function evaluateCondition( + expression: string, + nodeOutputs: Record>, +): boolean { + if (!expression || expression.trim().length === 0) { + // Empty condition is always true (no guard = proceed). + return true; + } + + const trimmed = expression.trim(); + const tokens = tokenize(trimmed); + const parser = new ConditionParser(tokens, trimmed, nodeOutputs); + + try { + return parser.parse(); + } catch (err) { + if (err instanceof ConditionError) { + throw err; + } + throw new ConditionError( + trimmed, + `Unexpected error: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} \ No newline at end of file diff --git a/packages/ion/src/engine/dag-executor.ts b/packages/ion/src/engine/dag-executor.ts new file mode 100644 index 0000000..dcb8a40 --- /dev/null +++ b/packages/ion/src/engine/dag-executor.ts @@ -0,0 +1,1149 @@ +/** + * DAG Executor — the core execution engine for Ion workflows. + * + * Traverses a DAG of nodes in topological layers, executing nodes within + * each layer concurrently via Promise.allSettled. Supports prompt nodes, + * command/bash/script nodes, approval gates, loop nodes, and cancel nodes. + * + * Architecture mirrors Archon's dag-executor: Kahn's algorithm for layer + * building, trigger rules for dependency evaluation, and Promise.allSettled + * for concurrent execution within each layer. + */ + +import { execFile } from 'node:child_process'; +import { promisify } from 'node:util'; +import { readFile } from 'node:fs/promises'; +import { join } from 'node:path'; + +import type { + DagNode, + PromptNode, + BashNode, + ScriptNode, + ApprovalNode, + LoopNode, + CancelNode, + NodeOutput, + NodeExecutionResult, + TriggerRule, + WorkflowDefinition, +} from '../schema/index.js'; +import { DEFAULT_TRIGGER_RULE } from '../schema/index.js'; +import { + isBashNode, + isScriptNode, + isLoopNode, + isApprovalNode, + isCancelNode, + isPromptNode, + isCommandNode, +} from '../schema/index.js'; + +import type { IWorkflowPlatform, WorkflowDeps, WorkflowConfig } from './deps.js'; +import { + evaluateCondition, + substituteWorkflowVariables, + substituteNodeOutputRefs, + buildPromptWithContext, + classifyError, + safeSendMessage, + formatSubprocessFailure, + sleep, + retryWithBackoff, + OutputRefError, + DagCycleError, + NodeTimeoutError, + ApprovalRejectedError, + LoopMaxIterationsError, +} from './utils.js'; + +const execFileAsync = promisify(execFile); + +// --------------------------------------------------------------------------- +// Topological layer building (Kahn's algorithm) +// --------------------------------------------------------------------------- + +/** + * Build topological layers from a flat list of DAG nodes using Kahn's algorithm. + * + * Each layer contains nodes that can execute concurrently. Nodes in layer N+1 + * depend only on nodes in layers 0..N. + * + * @param nodes - Flat list of DAG nodes with `depends_on` references. + * @returns Array of layers, each layer being an array of nodes. + * @throws DagCycleError if a cycle is detected. + */ +export function buildTopologicalLayers(nodes: DagNode[]): DagNode[][] { + const nodeMap = new Map(); + const inDegree = new Map(); + const adjacency = new Map>(); // dep → nodes that depend on it + + // Initialize + for (const node of nodes) { + nodeMap.set(node.id, node); + inDegree.set(node.id, node.depends_on.length); + for (const dep of node.depends_on) { + if (!adjacency.has(dep)) adjacency.set(dep, new Set()); + adjacency.get(dep)!.add(node.id); + } + } + + // Start with zero-in-degree nodes + let currentLayer: string[] = []; + for (const [id, degree] of inDegree) { + if (degree === 0) currentLayer.push(id); + } + + const layers: DagNode[][] = []; + let totalProcessed = 0; + + while (currentLayer.length > 0) { + // Build the layer from current zero-in-degree nodes + const layerNodes = currentLayer + .map((id) => nodeMap.get(id)) + .filter((n): n is DagNode => n !== undefined); + layers.push(layerNodes); + totalProcessed += layerNodes.length; + + // Reduce in-degree for dependents + const nextLayer: string[] = []; + for (const id of currentLayer) { + const dependents = adjacency.get(id); + if (!dependents) continue; + for (const depId of dependents) { + const currentDeg = inDegree.get(depId)! - 1; + inDegree.set(depId, currentDeg); + if (currentDeg === 0) nextLayer.push(depId); + } + } + + currentLayer = nextLayer; + } + + // Cycle detection + if (totalProcessed < nodes.length) { + throw new DagCycleError(nodes.length, totalProcessed); + } + + return layers; +} + +// --------------------------------------------------------------------------- +// Trigger rule evaluation +// --------------------------------------------------------------------------- + +/** + * Check whether a node should run or be skipped based on its trigger rule + * and the completion states of its dependencies. + * + * @param node - The DAG node to evaluate. + * @param nodeOutputs - Map of completed node outputs. + * @returns `'run'` if the node should execute, `'skip'` if it should be skipped. + */ +export function checkTriggerRule( + node: DagNode, + nodeOutputs: Map, +): 'run' | 'skip' { + const rule: TriggerRule = node.trigger_rule ?? DEFAULT_TRIGGER_RULE; + + if (node.depends_on.length === 0) return 'run'; + + const depOutputs = node.depends_on.map((depId) => nodeOutputs.get(depId)); + + switch (rule) { + case 'all_success': + // All dependencies must have completed successfully + return depOutputs.every((o) => o?.state === 'completed') ? 'run' : 'skip'; + + case 'one_success': + // At least one dependency must have completed successfully + return depOutputs.some((o) => o?.state === 'completed') ? 'run' : 'skip'; + + case 'all_done': + // All dependencies must have finished (any terminal status) + return depOutputs.every((o) => o !== undefined) ? 'run' : 'skip'; + + case 'none_failed_min_one_success': + // No dependency failed AND at least one succeeded + const hasFailure = depOutputs.some((o) => o?.state === 'failed'); + const hasSuccess = depOutputs.some((o) => o?.state === 'completed'); + return !hasFailure && hasSuccess ? 'run' : 'skip'; + + default: + return 'run'; + } +} + +// --------------------------------------------------------------------------- +// Node output reference substitution +// --------------------------------------------------------------------------- + +/** + * Substitute node output references in a prompt string. + * + * Resolves `$nodeId.output` → full text, `$nodeId.output.field` → structured field. + * + * @param prompt - Template string with `$nodeId.output` references. + * @param nodeOutputs - Map of node id → NodeOutput. + * @param escapedForBash - If true, escape special bash characters in output values. + */ +export { substituteNodeOutputRefs } from './utils.js'; + +// --------------------------------------------------------------------------- +// Prompt / command node execution +// --------------------------------------------------------------------------- + +/** + * Execute a single PromptNode or CommandNode by sending a prompt to an AI provider. + * + * Handles: + * - Loading prompt from command file or inline prompt + * - Variable substitution (workflow vars + node output refs) + * - Streaming: accumulates output, forwards messages to platform + * - Idle timeout: aborts after configurable period of inactivity + * - Structured output: validates against output_format schema, reask loop + * - Retry on transient errors + */ +export async function executeNodeInternal( + node: PromptNode, + deps: WorkflowDeps, + platform: IWorkflowPlatform, + conversationId: string, + cwd: string, + config: WorkflowConfig, + nodeOutputs: Map, + workflowVariables: Record, +): Promise { + const providerId = node.provider ?? config.assistant; + const provider = deps.getAgentProvider(providerId); + + // Resolve prompt text + let promptText: string; + if (node.command_file) { + try { + const filePath = join(cwd, node.command_file); + promptText = await readFile(filePath, 'utf-8'); + } catch (err) { + return { + state: 'failed', + error: `Failed to read command file "${node.command_file}": ${err instanceof Error ? err.message : String(err)}`, + }; + } + } else if (node.prompt) { + promptText = node.prompt; + } else { + return { state: 'failed', error: `Prompt node "${node.id}" has neither prompt nor command_file` }; + } + + // Apply variable substitution + try { + promptText = buildPromptWithContext(promptText, workflowVariables, nodeOutputs); + } catch (err) { + if (err instanceof OutputRefError) { + return { state: 'failed', error: err.message }; + } + throw err; + } + + // Merge node-level env vars + const mergedVars = { ...workflowVariables, ...(node.env ?? {}) }; + + // Retry configuration + const maxAttempts = node.retry?.max_attempts ?? 1; + const onError = node.retry?.on_error ?? 'transient'; + const delayMs = node.retry?.delay_ms ?? 1000; + + // Idle timeout + const idleTimeoutMs = node.idle_timeout_ms ?? 300_000; // 5 minutes default + + // Structured output reask loop + const maxReaskAttempts = node.output_format ? 3 : 1; + let lastOutput = ''; + let lastFields: Record | undefined; + let lastError: string | undefined; + let costUsd: number | undefined; + + for (let reaskAttempt = 0; reaskAttempt < maxReaskAttempts; reaskAttempt++) { + const currentPrompt = reaskAttempt === 0 + ? promptText + : `${promptText}\n\nPrevious response did not match the expected format. Please try again, ensuring your response matches: ${JSON.stringify(node.output_format)}`; + + // Execute with retry + let responseText: string | undefined; + let retryError: unknown; + + const retryPredicate = onError === 'all' + ? undefined + : (err: unknown) => classifyError(err) === 'transient' || classifyError(err) === 'rate_limit'; + + try { + responseText = await retryWithBackoff( + () => executeWithIdleTimeout(provider, currentPrompt, idleTimeoutMs, node.id), + maxAttempts, + delayMs, + retryPredicate, + ); + } catch (err) { + retryError = err; + } + + if (retryError) { + const category = classifyError(retryError); + if (category === 'timeout') { + return { + state: 'failed', + error: `Node "${node.id}" timed out after ${idleTimeoutMs}ms of inactivity`, + }; + } + return { + state: 'failed', + error: retryError instanceof Error ? retryError.message : String(retryError), + }; + } + + lastOutput = responseText ?? ''; + + // Validate structured output if schema provided + if (node.output_format && lastOutput) { + try { + const parsed = tryParseStructuredOutput(lastOutput); + if (parsed) { + const validation = validateStructuredOutput(parsed, node.output_format); + if (validation.valid) { + lastFields = parsed; + break; // Valid structured output + } + // If not valid and we have reask attempts left, continue loop + if (reaskAttempt < maxReaskAttempts - 1) continue; + } + } catch { + // If parsing fails and we have reask attempts left, continue + if (reaskAttempt < maxReaskAttempts - 1) continue; + } + } + + // No structured output required, or best-effort on last attempt + break; + } + + // Notify platform + await safeSendMessage( + platform, + conversationId, + `✅ Node "${node.name ?? node.id}" completed`, + ); + + return { + state: 'completed', + output: lastOutput, + fields: lastFields, + costUsd, + }; +} + +/** + * Execute a provider call with an idle timeout. + * + * If no response is received within `idleTimeoutMs`, the request is aborted. + */ +async function executeWithIdleTimeout( + provider: { sendPrompt: (prompt: string, options?: Record) => Promise }, + prompt: string, + idleTimeoutMs: number, + nodeId: string, +): Promise { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), idleTimeoutMs); + + try { + const result = await provider.sendPrompt(prompt, { signal: controller.signal }); + clearTimeout(timeoutId); + return result; + } catch (err) { + clearTimeout(timeoutId); + if (controller.signal.aborted) { + throw new NodeTimeoutError(nodeId, idleTimeoutMs); + } + throw err; + } +} + +/** + * Attempt to parse structured output from a model response. + * + * Tries JSON parse first, then looks for JSON within markdown code blocks. + */ +function tryParseStructuredOutput(text: string): Record | undefined { + // Try direct JSON parse + try { + const parsed = JSON.parse(text); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + // Not direct JSON + } + + // Try extracting JSON from markdown code block + const jsonBlockMatch = text.match(/```(?:json)?\s*\n?([\s\S]*?)\n?```/); + if (jsonBlockMatch) { + try { + const parsed = JSON.parse(jsonBlockMatch[1]!); + if (typeof parsed === 'object' && parsed !== null && !Array.isArray(parsed)) { + return parsed as Record; + } + } catch { + // Not valid JSON in code block + } + } + + return undefined; +} + +/** + * Validate parsed output against a schema definition. + * + * Simple validation: checks that all required keys are present. + * Full JSON Schema validation would require a library; this is best-effort. + */ +function validateStructuredOutput( + parsed: Record, + schema: Record, +): { valid: boolean; missingKeys?: string[] } { + const required = schema['required']; + if (Array.isArray(required)) { + const missing = required.filter((key) => !(key in parsed)); + if (missing.length > 0) { + return { valid: false, missingKeys: missing as string[] }; + } + } + return { valid: true }; +} + +// --------------------------------------------------------------------------- +// Script / Bash node execution +// --------------------------------------------------------------------------- + +/** + * Execute a BashNode or ScriptNode. + * + * For bash nodes: runs `bash -c