* refactor: extract shared shapes between jira and linear integrations Factors out the duplicated logic between internal/jira + internal/linear (backend) and components/jira + components/linear (frontend) into reusable shared shapes. Follow-up to PR #736. Backend: - New internal/integrations/secretadapter wraps secrets.SecretStore with the upsert API both jira.SecretStore and linear.SecretStore expect; drops both adapter copies from cmd/kandev/services.go. - New internal/integrations/healthpoll owns the 90s auth-health loop via a Prober interface; linear/poller.go is a thin wrapper, jira/poller.go keeps only its (jira-specific) issue-watch loop. - provideServices split into initGitHubService / initJiraService / initLinearService — now 75 lines (was ~125, exceeded the 80-line limit). Frontend: - New hooks/domains/integrations/use-integration-availability.ts and use-integration-enabled.ts; jira/linear hooks reduce to one-line delegates and move from components/<x>/ to hooks/domains/<x>/ per the project convention Greptile flagged on PR #736. - New components/integrations/{auth-status-banner,auth-error-message, validated-popover}.tsx; the four link/import-bar wrappers drop from ~110 lines to ~35 each, and JiraLinkButton picks up the clear-error-on-close behaviour LinearLinkButton already had. Docs: - New 'Adding a new third-party integration' section in CLAUDE.md/AGENTS.md documenting the shared shapes and intentional Jira/Linear divergence (transition vs state, JQL vs filter object, eventBus only on jira). * fix: address PR #759 review feedback - validated-popover: outline-with-label trigger now sets type="button" and honours triggerDisabled, matching ghost-icon (Greptile, Claude, CodeRabbit). - jira-link-button / linear-link-button: move updateTask() into the awaited fetch path so a failed title update surfaces as the popover's inline error instead of an unhandled rejection (CodeRabbit). - use-integration-availability: monotonic requestId race guard prevents a slow earlier probe from overwriting a newer auth result (CodeRabbit). - use-integration-enabled: drop unnecessary async on synchronous init (Claude). - jira/linear poller smoke tests: wrap in testing/synctest instead of time.After(2s) per backend testing convention (CodeRabbit nitpick). Skipped with reasoning on the threads: - secretadapter Set TOCTOU — single-process SQLite + single-writer per workspace; constraint violation on conflict surfaces as an error to the caller, not silent corruption. - healthpoll SetInterval validation — the function isn't called from production code today; a panic on misuse would be a footgun and silent ignore would just hide bugs differently. * fix: address PR #759 round-2 review feedback - jira-settings / linear-settings: drop the local STATUS_REFRESH_MS = 90_000 copies and import the shared INTEGRATION_STATUS_REFRESH_MS so changing the cadence only touches one file (Claude). - healthpoll: rename Poller.ProbeAll → probeAll. The only callers are the loop and the in-package tests, so widening the public API wasn't buying anything (Claude). * fix: invalidate ValidatedPopover in-flight submits on close If the user clicked Submit and dismissed the popover before the request settled, the next time they re-opened it would inherit `loading=true` (the disabled-while-loading submit button was stuck) and a late rejection from the previous session could repopulate `error` in the new one. Track the submit via a `useRef` token, bump it on close, and ignore any result/rejection whose token no longer matches. Also reset `loading` and `error` on close so the next open starts clean. Adds a regression test covering both the late-resolution and late-rejection paths. Addresses CodeRabbit review on PR #759. * ci: trigger workflow re-run GitHub Actions did not dispatch the pull_request workflows on the previous two pushes (1d869f0e,0ab6c034); only pull_request_target and CodeQL fired. Empty commit to nudge synchronize event. * fix: address PR #759 round-3 review feedback - jira.Poller.Stop: add nil-receiver guard so callers using the defer-Stop-after-NewPoller pattern don't panic when initJiraService returned nil (Greptile + Claude). Matches linear.Poller's guard. - healthpoll TestStart_RunsImmediateProbe + TestStart_IsIdempotent: rewrap in testing/synctest with synctest.Wait() instead of time.After(2s) wall-clock waits. Drops the now-unused probedAll signal channel from fakeProber (Claude). --------- Co-authored-by: Kandev Agent <agent@kandev.dev>
21 KiB
Kandev Engineering Guide
Purpose: Architecture notes, key patterns, and conventions for LLM agents working on Kandev.
Repo Layout
apps/
├── backend/ # Go backend (orchestrator, lifecycle, agentctl, WS gateway)
├── web/ # Next.js frontend (SSR + WS + Zustand)
├── cli/ # CLI tool (TypeScript)
├── landing/ # Landing page
└── packages/ # Shared packages/types
Tooling
- Package manager:
pnpmworkspace (run fromapps/, not repo root) - Backend: Go with Make (
make -C apps/backend test|lint|build) - Frontend: Next.js (
cd apps && pnpm --filter @kandev/web dev|lint|typecheck) - UI: Shadcn components via
@kandev/ui - GitHub repo:
https://github.com/kdlbs/kandev - Container image:
ghcr.io/kdlbs/kandev(GitHub Container Registry)
Backend Architecture
Package Structure
apps/backend/
├── cmd/
│ ├── kandev/ # Main backend binary entry point
│ ├── agentctl/ # Agentctl binary (runs inside containers or standalone)
│ └── mock-agent/ # Mock agent for testing
├── internal/
│ ├── agent/
│ │ ├── lifecycle/ # Agent instance management (see below)
│ │ ├── agents/ # Agent type implementations
│ │ ├── controller/ # Agent control operations
│ │ ├── credentials/ # Agent credential management
│ │ ├── discovery/ # Agent discovery
│ │ ├── docker/ # Docker-specific agent logic
│ │ ├── dto/ # Agent data transfer objects
│ │ ├── executor/ # Executor types, checks, and service
│ │ ├── handlers/ # Agent event handlers
│ │ ├── registry/ # Agent type registry and defaults
│ │ ├── settings/ # Agent settings
│ │ ├── mcpconfig/ # MCP server configuration
│ │ └── remoteauth/ # Remote auth catalog and method IDs for remote executors/UI
│ ├── agentctl/
│ │ ├── client/ # HTTP client for talking to agentctl
│ │ └── server/ # agentctl HTTP server
│ │ ├── acp/ # ACP protocol implementation
│ │ ├── adapter/ # Protocol adapters + transport/ (ACP, Codex, OpenCode, Copilot, Amp)
│ │ ├── api/ # HTTP endpoints
│ │ ├── config/ # agentctl configuration
│ │ ├── instance/ # Multi-instance management
│ │ ├── mcp/ # MCP server integration
│ │ ├── process/ # Agent subprocess management
│ │ ├── shell/ # Shell session management
│ │ └── utility/ # agentctl utilities
│ ├── orchestrator/ # Task execution coordination
│ │ ├── dto/ # Orchestrator data transfer objects
│ │ ├── executor/ # Launches agents via lifecycle manager
│ │ ├── handlers/ # Orchestrator event handlers
│ │ ├── messagequeue/ # Message queue for agent prompts
│ │ ├── queue/ # Task queue
│ │ ├── scheduler/ # Task scheduling
│ │ └── watcher/ # Event handlers
│ ├── task/
│ │ ├── controller/ # Task HTTP/WS controllers
│ │ ├── dto/ # Task data transfer objects
│ │ ├── events/ # Task event types
│ │ ├── handlers/ # Task event handlers
│ │ ├── models/ # Task, Session, Executor, Message models
│ │ ├── repository/ # Database access (SQLite)
│ │ └── service/ # Task business logic
│ ├── analytics/ # Usage analytics
│ ├── clarification/ # Agent clarification handling
│ ├── common/ # Shared utilities
│ ├── db/ # Database initialization
│ ├── debug/ # Debug tooling
│ ├── editors/ # Editor integration
│ ├── events/ # Event bus for internal pub/sub
│ ├── gateway/ # WebSocket gateway
│ ├── github/ # GitHub API integration (PRs, reviews, webhooks)
│ ├── integration/ # External integrations
│ ├── integrations/ # Shared shapes for third-party integrations
│ │ ├── healthpoll/ # Reusable 90s auth-health Poller (used by jira, linear)
│ │ └── secretadapter/ # Upsert-style adapter over secrets.SecretStore
│ ├── jira/ # Jira/Atlassian Cloud integration (config, REST client, poller)
│ ├── linear/ # Linear integration (config, GraphQL client, poller)
│ ├── lsp/ # LSP server
│ ├── mcp/ # MCP protocol support
│ ├── health/ # Health check endpoints
│ ├── notifications/ # Notification system
│ ├── persistence/ # Persistence layer
│ ├── prompts/ # Prompt management
│ ├── repoclone/ # Repository cloning for remote executors
│ ├── scriptengine/ # Script placeholder resolution and interpolation
│ ├── secrets/ # Secret management
│ ├── sprites/ # Sprites AI integration
│ ├── sysprompt/ # System prompt injection
│ ├── task/
│ │ └── ...
│ ├── tools/ # Tool integrations
│ ├── user/ # User management
│ ├── utility/ # Shared utility functions
│ ├── workflow/ # Workflow engine
│ │ ├── engine/ # Typed state-machine engine (trigger evaluation, action callbacks, transition store)
│ │ ├── models/ # Workflow step, template, and history models
│ │ ├── repository/ # Workflow persistence (SQLite)
│ │ └── service/ # Workflow CRUD and step resolution
│ └── worktree/ # Git worktree management for workspace isolation
Key Concepts
Orchestrator coordinates task execution:
- Receives task start/stop/resume requests via WebSocket
- Delegates to lifecycle manager for agent operations
- Handles event-driven state transitions via workflow engine
- Located in
internal/orchestrator/
Workflow Engine (internal/workflow/engine/) provides typed state-machine evaluation:
Engine.HandleTrigger()evaluates step actions for triggers (on_enter, on_turn_start, on_turn_complete, on_exit)TransitionStoreinterface abstracts persistence (implemented byorchestrator.workflowStore)CallbackRegistrymaps action kinds to callbacks (plan mode, auto-start, context reset)- First-transition-wins: multiple transition actions in one trigger, first eligible wins
EvaluateOnlymode: engine evaluates without persisting, caller orchestrates on_exit → DB → on_enterRequiresApprovalon actions: transitions requiring review gating are skipped- Idempotent by
OperationID; session-scoped data bag viaMachineState.Data
Lifecycle Manager (internal/agent/lifecycle/) manages agent instances:
Manager(manager.go,manager_*.go) - central coordinator for agent lifecycleExecutorBackendinterface (executor_backend.go) - abstracts execution environment (Docker, Standalone, Sprites, Remote Docker)ExecutionStore(execution_store.go) - thread-safe in-memory execution trackingsession.go- ACP session initialization and resumestreams.go- WebSocket stream connections to agentctlprocess_runner.go- agent process launch and managementprofile_resolver.go- resolves agent profiles/settings
agentctl is an HTTP server that:
- Runs inside Docker containers or as standalone process
- Manages agent subprocess via stdin/stdout (ACP protocol)
- Exposes workspace operations (shell, git, files)
- Supports multiple concurrent instances on different ports
Executor Types (database model):
local_pc- Standalone process on host ✅local_docker- Docker container on host ✅sprites- Sprites cloud environment ✅remote_docker,remote_vps,k8s- Planned
Execution Flow
Client (WS) → Orchestrator → Lifecycle Manager → ExecutorBackend (container/process) → agentctl
↓
Client (WS) ← Orchestrator ← Lifecycle Manager ←──── stream updates (WS) ──────── agent subprocess
- Orchestrator receives
task.startvia WS - Lifecycle Manager creates executor instance (container or process)
- agentctl starts inside the instance, agent subprocess is configured and started
- Agent events stream back via WS through the chain
Session Resume: TaskSession.ACPSessionID stored for resume; ExecutorRunning tracks active state; on restart RecoverInstances() reconnects.
Provider Pattern: Packages expose Provide(cfg, log) (*impl, cleanup, error) for DI. Returns implementation, cleanup function, and error. Cleanup called during graceful shutdown.
Worktrees: internal/worktree/Manager provides workspace isolation. Each session can have its own worktree (branch) to prevent conflicts between concurrent agents.
Executor default scripts: Default prepare scripts are in internal/agent/lifecycle/default_scripts.go; internal/scriptengine/ handles placeholder resolution.
agentctl Server
API Groups
agentctl exposes these route groups (see internal/agentctl/server/api/):
/health,/info,/status- Health and status/instances/*- Multi-instance management/processes/*- Agent subprocess management (start/stop)/agent/configure,/agent/stream- Agent configuration and event streaming/git/*- Git operations (status, commit, push, pull, rebase, stage, etc.)/shell/*- Shell session management/workspace/*- File operations, search, tree/vscode/*- VS Code integration proxy
Adapter Model
Protocol adapters in adapter/transport/ normalize different agent CLIs:
AgentAdapterinterface definesStart(),Stop(),Prompt(),Cancel()- Transports:
acp(Claude Code),codex(OpenAI Codex),opencode,shared,streamjson - Top-level adapters:
CopilotAdapter(GitHub Copilot SDK),AmpAdapter(Sourcegraph Amp) process.Managerowns subprocess, wires stdio to adapter- Factory pattern in
adapter/factory.goselects adapter by agent type
ACP Protocol
JSON-RPC 2.0 over stdin/stdout between agentctl and agent process. Requests: initialize, session/new, session/load, session/prompt, session/cancel. Notifications: session/update with types message_chunk, tool_call, tool_update, complete, error, permission_request, context_window.
Frontend Architecture
UI Components
Shadcn Components: Import from @kandev/ui package:
import { Badge } from '@kandev/ui/badge';
import { Button } from '@kandev/ui/button';
import { Dialog } from '@kandev/ui/dialog';
// etc...
Do NOT import from @/components/ui/* - always use @kandev/ui package.
Data Flow Pattern (Critical)
SSR Fetch -> Hydrate Store -> Components Read Store -> Hooks Subscribe
Never fetch data directly in components.
Store Structure (Domain Slices)
lib/state/
├── store.ts # Root composition
├── default-state.ts # Default state + initial state merge
├── slices/ # Domain slices
│ ├── kanban/ # boards, tasks, columns
│ ├── session/ # sessions, messages, turns, worktrees
│ ├── session-runtime/ # shell, processes, git, context
│ ├── workspace/ # workspaces, repos, branches
│ ├── settings/ # executors, agents, editors, prompts (incl. userSettings)
│ ├── comments/ # code review diff comments
│ ├── github/ # GitHub PRs, reviews
│ └── ui/ # preview, connection, active state, sidebar views
├── hydration/ # SSR merge strategies
hooks/domains/{kanban,session,workspace,settings,comments,github}/ # Domain-organized hooks
lib/api/domains/ # API clients
├── kanban-api, session-api, workspace-api, settings-api, process-api
├── plan-api, queue-api, workflow-api, stats-api, github-api
├── user-shell-api, debug-api, secrets-api, sprites-api, vscode-api
├── health-api, utility-api
Key State Paths:
messages.bySession[sessionId],shell.outputs[sessionId],gitStatus.bySessionId[sessionId]tasks.activeTaskId,tasks.activeSessionId,workspaces.activeIdrepositories.byWorkspace,repositoryBranches.byRepository
Hydration: lib/state/hydration/merge-strategies.ts has deepMerge(), mergeSessionMap(), mergeLoadingState() to avoid overwriting live client state. Pass activeSessionId to protect active sessions.
Hooks Pattern: Hooks in hooks/domains/ encapsulate WS subscription + store selection. WS client deduplicates subscriptions automatically.
WS
Format: {id, type, action, payload, timestamp}.
Best Practices
Commit Conventions (enforced by CI)
Commits to main must follow Conventional Commits (type: description). PRs are squash-merged — the PR title becomes the commit, validated by CI. Changelog is auto-generated from these via git-cliff (cliff.toml). See .agents/skills/commit/SKILL.md for allowed types and examples.
Code Quality (enforced by linters)
Static analysis runs in CI and pre-commit. New code must stay within these limits:
Go (apps/backend/.golangci.yml - errors on new code only):
- Functions: ≤80 lines, ≤50 statements
- Cyclomatic complexity: ≤15 · Cognitive complexity: ≤30
- Nesting depth: ≤5 · Naked returns only in functions ≤30 lines
- No duplicated blocks (≥150 tokens) · Repeated strings → constants (≥3 occurrences)
TypeScript (apps/web/eslint.config.mjs - warnings, will become errors):
- Files: ≤600 lines · Functions: ≤100 lines
- Cyclomatic complexity: ≤15 · Cognitive complexity: ≤20
- Nesting depth: ≤4 · Parameters: ≤5
- No duplicated strings (≥4 occurrences) · No identical functions · No unused imports
- No nested ternaries
When you hit a limit: extract a helper function, custom hook, or sub-component. Prefer composition over growing a single function.
Testing
Every code change must include tests for new or changed logic. Backend: *_test.go files alongside the source. Frontend: *.test.ts files for utility functions, hooks, API clients, and store slices. Exceptions: config files, generated code, React component markup. Use /tdd for test-driven development.
Knowledge
- Specs: Feature specs live in
docs/specs/<slug>/spec.md— the "what & why" of a feature, written before coding. Optional siblings:plan.md(how),notes.md(post-ship). Use/specto write or update a spec. Seedocs/specs/INDEX.md. - Decisions: Architecture decisions are recorded in
docs/decisions/. Readdocs/decisions/INDEX.mdfor an overview. When making significant architectural choices, create a new ADR via/record decision. - Plans: Retrospective implementation plans live in
docs/plans/(created via/record plan). A forward-looking plan tied to a spec goes atdocs/specs/<slug>/plan.md.
Backend
- Provider pattern for DI; stderr for logs, stdout for ACP only
- Pass context through chains; event bus for cross-component comm
- Execution access: Workspace-oriented handlers (files, shell, inference, ports, vscode, LSP) MUST use
GetOrEnsureExecution(ctx, sessionID)— it recovers from backend restarts by creating executions on-demand. Only useGetExecutionBySessionIDfor operations that require a running agent process (prompt, cancel, mode). - Testing: Prefer
testing/synctest(Go 1.24+) overtime.Sleepfor time-dependent tests. Usesynctest.Testto wrap tests with tickers or timeouts — it advances fake time instantly when all goroutines are idle. Whensynctestis not feasible (e.g., tests spawning external processes likegit), use channel-based synchronization (<-started, non-blockingselect) instead of sleep-based waits. Reservetime.Sleeponly for integration tests that need real subprocess execution time.
Frontend
- Data: SSR fetch → hydrate → read store. Never fetch in components
- UI Components:
- Import shadcn components from
@kandev/ui, NOT@/components/ui/* - Always prefer native shadcn components over custom implementations
- Check
apps/packages/ui/src/for available components (pagination, table, dialog, etc.) - For data tables, use
@kandev/ui/tablewith TanStack Table; use shadcn Pagination components - Only create custom components when shadcn doesn't provide what's needed
- Import shadcn components from
- Components: <200 lines, extract to domain components, composition over props
- Hooks: Domain-organized in
hooks/domains/, encapsulate subscription + selection - WS: Use subscription hooks only; client auto-deduplicates
- Interactivity: All buttons and links with actions must have
cursor-pointerclass
Plan Implementation
- After implementing a plan, run
make fmtfirst to format code, then runmake typecheck test lintto verify the changes. Formatting must come first because formatters may split lines, which can trigger complexity linter warnings.
GitHub Operations
Skills use gh CLI by default. If a gh command fails (not installed, not authenticated, etc.), use whatever GitHub tools are available in the environment (MCP GitHub tools, API tools, etc.) to accomplish the same operation. The goal is the same — the tool may differ.
Adding a new third-party integration
Jira and Linear are the model: per-workspace credentials, a 90s auth-health poller, a settings page with status banner + reconnect CTA, link/import buttons that gate on availability. New integrations should reuse the shared shapes rather than copying either.
Backend (apps/backend/internal/<name>/):
- Mirror the package layout:
service.go,store.go,client.go,provider.go,handlers.go,models.go,poller.go. ExposeProvide(writer, reader *sqlx.DB, secrets SecretStore, log *logger.Logger) (*Service, func() error, error)(addeventBus bus.EventBusonly if the integration actually publishes events — Linear does not). - Use
internal/integrations/secretadapterinstead of writing your own upsert wrapper aroundsecrets.SecretStore. The adapter satisfies any per-integrationSecretStoreinterface shaped as{Reveal, Set, Delete, Exists}. - Use
internal/integrations/healthpollfor the auth-health loop. Implement theProberinterface (ListConfiguredWorkspaces+RecordAuthHealth) on a small adapter and lethealthpoll.New("name", prober, log)own Start/Stop/ticker. Keep integration-specific loops (JQL polling, webhook reconciliation, etc.) separate, like jira's issue-watch loop. - Wire the service via a per-domain
init<Name>Service(...)helper incmd/kandev/services.go, not inline inprovideServices.
Frontend:
- Hooks live under
hooks/domains/<name>/, notcomponents/<name>/. - Use
hooks/domains/integrations/use-integration-availability.tsanduse-integration-enabled.ts— each integration'suseXAvailable/useXEnabledshould be a one-line wrapper passing the storage key + sync event + config-fetch function. - Settings page reuses
<IntegrationAuthStatusBanner>(components/integrations/auth-status-banner.tsx). - "Auth required / reconnect" UI reuses
<IntegrationAuthErrorMessage>(components/integrations/auth-error-message.tsx) — supply the integration's display name, regex check, and reconnect href. - Link / import popovers reuse
<ValidatedPopover>(components/integrations/validated-popover.tsx) — supply the icon, label, key regex, fetch function, and success callback.
Where Jira and Linear deliberately diverge:
- Issue model: Jira uses transitions + JQL; Linear uses state IDs + structured filters. Don't merge these schemas — the upstream APIs are genuinely different.
- Event publishing: only Jira accepts
eventBusand emitsNewJiraIssueEventfrom its issue-watch loop. Linear has no equivalent feature today. - Health column extras: Linear's
linear_configsrow carries anorg_slugcaptured from successful probes; Jira's row does not.
Maintaining This File
This file is read by AI coding agents (Claude Code via CLAUDE.md symlink, Codex via AGENTS.md). If your changes make any section of this file outdated or inaccurate - e.g., you add/remove/rename packages, change architectural patterns, add new adapters, modify store slices, or change conventions - update the relevant sections of this file as part of the same PR. Keep descriptions concise and factual. Do not add speculative or aspirational content.
Last Updated: 2026-05-01