João Salavisa 68aeefa296 feat(gitlab): add GitLab integration with MR / discussions support (#861)
* feat(gitlab): add GitLab integration with MR / discussions support

Adds a GitLab integration parallel to the GitHub one: PAT and glab CLI auth,
self-managed host configuration, MR feedback (approvals + discussions +
pipelines), and discussion reply / resolve so agents can address review
comments. The pr skill detects the host from git remote and creates a real
GitLab merge request via glab or REST.

Closes #820

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* refactor(gitlab): address review feedback and tighten lint surface

- Add service_test.go covering ConfigureToken / ClearToken / ConfigureHost.
- Drop speculative MRWatch / TaskMR / ReviewWatch / IssueWatch / preset
  scaffolding that had no migrations or handlers behind it.
- Consolidate /approvals into one round-trip via fetchApprovals.
- Use http.DefaultClient in CheckHost (context already carries the deadline).
- Rename misleading id → configured in ConfigureToken.
- Introduce mrStateOpen constant so the lint goconst rule passes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): address PR review feedback (CodeQL SSRF, races, atomicity)

- connectivity: parse host with url.Parse, validate http/https scheme,
  build probe URL via &url.URL{} so user-controlled host can't smuggle
  into the request — clears the CodeQL go/request-forgery alert.
- pat_client: guard cached username with sync.RWMutex; IsAuthenticated
  now propagates non-401/403 errors instead of masking transport
  failures; GetMRStatus skips pipelines query when HeadSHA / HeadBranch
  are both empty; parseNextLink drops cross-host links instead of
  returning bare absolute URLs that get() would double-prefix.
- service: findTokenSecret returns (bool, string, error) and all
  callers propagate the secrets.List error so a backend outage isn't
  mistaken for "token absent"; ConfigureHost persists to hostStore
  before mutating in-memory host + client + authMethod under one lock.
- repoclone: SSH clone URL falls back to ssh:// form when the host has
  a port (scp-style git@host:path can't carry one).
- gitlab/client_helpers: namespace fallback uses LastIndex("/") so
  subgroup paths (acme/team/squad/repo) are preserved.
- pr skill / plan doc: drop hardcoded --target-branch main, derive host
  from git remote instead of $CI_SERVER_URL, build curl payload with
  jq -n; fix /pipelines/:id/jobs → :pipeline_id typo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): preserve host base path in CheckHost; broaden validation; test

- connectivity: extract validateHost so the parse / scheme / hostname /
  userinfo / opaque-URL checks each have a single home. Preserve the
  configured base path (TrimRight slash) so a host like
  https://gitlab.acme.corp/gitlab probes /gitlab/api/v4/version instead
  of dropping the prefix.
- connectivity_test: cover validateHost (default, https, http, trailing
  slash, subpath, missing scheme, ftp, empty host, userinfo) and
  CheckHost end-to-end (version endpoint, base-path preservation,
  scheme rejection, unreachable host).
- pr skill: drop the broken `glab repo view --json defaultBranch`
  branch — glab doesn't accept --json, so the lookup silently fell
  back to "main" every time. Fetch default_branch via the GitLab
  REST API directly using the PAT we already have.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): override filter defaults, refresh auth uncached, mock pipelines

- appendFilter: Del before re-Add so user-supplied filter keys actually
  override caller defaults (e.g. state=closed swaps the seeded state=opened
  instead of appending a second value GitLab silently ignored).
- IsAuthenticated: probe /user directly instead of routing through the
  username cache; a token revoked upstream now surfaces as not-connected
  immediately. Cache is opportunistically refreshed on success.
- MockClient.GetMRFeedback: include pipeline state in HasIssues, matching
  the real PATClient behavior so test flows can't miss a broken MR.
- pr skill: document `glab mr note create --reply <id>` for replies (bare
  `glab mr note` opens a new thread); fix host-extraction block to handle
  ssh://host:2222/team/repo.git without leaking the port into PROJECT.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): guard mock pipelines on empty MR head; preserve HTTPS port in pr skill

- MockClient.GetMRFeedback now mirrors PATClient: skip the pipelines
  fetch when HeadSHA and HeadBranch are both empty. The mock'\''s
  ListPipelines ignores its branch argument and returns every pipeline
  seeded under the project, so without this guard a brand-new MR would
  inherit a sibling MR'\''s failing pipeline and flip HasIssues to true.
  Tests cover both the empty-head and head-present cases.
- pr skill: derive HOST scheme-aware so https://host:8443 keeps the port
  (HTTPS API endpoint), ssh://host:2222 strips it (different transport),
  and scp-style git@host stays portless. Avoids silently re-routing
  self-managed API calls to the default 443.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): export SeedPipelines; mock GetMRStatus parity; drain bodies; test cache hit

- MockClient: add exported SeedPipelines(project, iid, []) alongside the
  other Seed* methods; mirror PATClient.GetMRStatus by populating
  PipelineState / jobs counts via summarizePipelines, guarded on a
  non-empty head ref. Brings mock parity with the real client so any
  future test seeding pipelines sees the same MRStatus shape.
- PATClient HTTP plumbing: drain resp.Body via io.Copy(io.Discard, ...)
  when result == nil in both getWithTotal and doWrite, so HTTP/1.1 can
  reuse the connection instead of opening a new TCP socket per call.
- TestPATClient_GetAuthenticatedUser: assert calls == 1 after the second
  GetAuthenticatedUser hit so the cache contract is actually verified
  (a non-caching implementation would have passed the prior version).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): surface connection errors, align MR pipeline guards, fix mock seed key

- service.GetStatus now captures the IsAuthenticated error into a new
  Status.ConnectionError field (empty on healthy / 401-403, populated on
  network / 5xx / parse failures). Frontend GitLabStatus + settings page
  render a distinct "Unreachable" badge + alert so users don't delete a
  still-valid token during a transient GitLab outage.
- PATClient.GetMRFeedback now guards on HeadSHA || HeadBranch, matching
  GetMRStatus and MockClient.GetMRFeedback (which had been updated in an
  earlier round). The three call sites were inconsistent — a mock test
  with HeadBranch-but-no-SHA would pass while the real client silently
  dropped pipelines.
- MockClient.pipelines is now keyed by project alone (was
  map[mockMRKey][]Pipeline). SeedPipelines takes (project, []) so
  successive seeds for the same project deterministically overwrite,
  instead of leaving multiple entries that ListPipelines would resolve
  in Go's unspecified map-iteration order.
- Tests: TestService_GetStatus_{PopulatesConnectionErrorOn5xx,
  ClearsConnectionErrorOn401} cover the new error surface;
  TestMockClient_SeedPipelines_OverwritesByProject locks the key change.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): tighten validateHost; init Status scopes; close token-host race

- validateHost: drop the parsed.Opaque != "" branch — Go only sets
  Opaque for opaque URIs, which have an empty Host the prior check
  already rejects. Narrow the userinfo error to mention the actual
  user:password@host shape so the message matches what users see.
- service.GetStatus: initialize RequiredScopes: []string{} on the
  nil-client defensive branch so the JSON payload keeps the
  always-an-array contract declared on the TypeScript side. The branch
  is effectively unreachable (every Provide path returns at least a
  NoopClient) but the empty-slice fallback costs nothing.
- service.ConfigureToken: build the installed client inside the write
  lock using the up-to-date s.host instead of the pre-probe snapshot.
  A concurrent ConfigureHost between RUnlock and Lock used to leave
  s.client pointing at the prior host while s.host advertised the new
  one — every subsequent API call would silently target the wrong
  instance until the next reconfigure.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): drop MR-commit stats fields; rename AuthorName; log post-lock host

- MRCommitInfo: drop Additions / Deletions / FilesChanged. GitLab's
  /merge_requests/:iid/commits endpoint doesn't return a stats object,
  so those fields were always 0 — better to omit them than feed callers
  silently-wrong numbers. Rename AuthorUsername to AuthorName since the
  API returns a display name, not a login handle. Doc-comment records
  the per-commit endpoint to use if a future revision wants real stats.
- ListMRCommits: drop the Stats anonymous struct from the JSON decode
  target now that the destination fields are gone.
- service.ConfigureToken: capture installedHost inside the write lock
  and log that value. The previous log used the pre-validation host
  snapshot, so a concurrent ConfigureHost between snapshot and lock
  would leave the log entry referring to a stale value while the
  client itself was correctly built against the new host.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): encode subgroup paths; sentinel-route errors; mock approval parity

- pat_client.SearchGroupProjects now encodes the group path through a new
  groupRef helper that %2F-substitutes slashes (mirrors projectRef). Subgroup
  paths like "acme/team" used to land at /groups/acme/team/projects, which
  GitLab routed to the parent group only and returned 404 for. Subgroups now
  work end-to-end.
- service.ErrInvalidToken sentinel + controller errors.Is routing: token
  validation failures now wrap a package-level sentinel and the HTTP layer
  switches on it via errors.Is, replacing the fragile strings.Contains
  prefix check that would silently fall through to 500 if the message ever
  changed. Coverage: TestService_ConfigureToken_WrapsErrInvalidToken.
- MockClient gains seedable approvals: SeedApprovals(project, iid, list,
  required) backs a dedicated approvals + requiredApprovals map.
  ListMRApprovals, GetMRFeedback, and GetMRStatus all consume them so
  ApprovalState / ApprovalCount / RequiredApprovals match what the real
  PATClient.GetMRStatus computes. Coverage:
  TestMockClient_GetMRStatus_UsesSeededApprovals.
- ClearToken: same host-snapshot race ConfigureToken was fixed for last
  round — NewClient now runs under the write lock against s.host rather
  than a pre-acquired snapshot.
- CheckHost: drain resp.Body before close so http.DefaultClient's keep-alive
  pool can reuse the connection for the next probe.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): full-path projectRef round-trip; unlock during glab probe

- splitFullReference now returns (namespace, fullPath) where fullPath is
  the namespace-qualified project path ("group/sub/project") instead of
  the last segment alone. Callers can pass MR.ProjectPath straight to
  projectRef for round-trip API calls (GetMR, GetMRFeedback, etc.) —
  matches GitLab's own path_with_namespace convention. Existing test
  cases updated.
- ClearToken: build the fallback client OUTSIDE the write lock so
  NewClient's glab subprocess (bounded by glabAuthTimeout = 10s)
  doesn't freeze every concurrent GetStatus / GetMRFeedback /
  Client() RLock reader for the duration. Brief WLock now only wraps
  the field swap. Tradeoff with the concurrent-ConfigureHost race is
  documented in-code: narrow stale-host window vs. 10s of blocked
  readers, and the next reconfigure resolves it.
- New regression test TestPATClient_SearchGroupProjects_EncodesSubgroupSlash
  asserts the request URL is /groups/acme%2Fteam/projects so a future
  refactor that swapped groupRef back to a bare url.PathEscape would
  fail visibly instead of silently 404ing subgroup lookups.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): equality-guard ClearToken's client swap against ConfigureHost race

ClearToken builds the fallback client outside the write lock to avoid
blocking RLock readers for up to 10s while glab auth-probes. The
swap-only critical section now compares s.host against the pre-probe
snapshot before assigning — if a concurrent ConfigureHost moved the
host underneath us, we leave its (newer, correctly-targeted) client
alone instead of overwriting with our stale-host build.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(web): add GitLab to the settings integrations sidebar

The /settings/integrations/gitlab page existed and the index card listed
GitLab alongside the other integrations, but the settings sidebar nav
(IntegrationsNavGroup) only listed GitHub / Jira / Linear / Slack. The
page was unreachable through the standard sidebar.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(gitlab): task-MR topbar surface + DB-backed task↔MR store

Phase 1 of the GitLab UI build-out: brings the MR side of the task topbar
to parity with the existing GitHub PR button.

Backend
- gitlab_task_mrs table + Store wiring (UpsertTaskMR,
  ListTaskMRsByTask, ListTaskMRsByWorkspaceID, DeleteTaskMR), uniqued
  on (task_id, repository_id, project_path, mr_iid) for multi-repo.
- service.SyncTaskMR folds a live GetMRStatus probe into an upsert;
  ListTaskMRsByTask / ListTaskMRsByWorkspace expose the rows.
- Three HTTP endpoints: GET /workspaces/:id/task-mrs, GET
  /tasks/:taskID/mrs, POST /tasks/:taskID/mrs/sync.
- initGitLabService now takes a *db.Pool and constructs gitlab.Store.
  Failures non-fatal: integration keeps working without the table.

Frontend
- TaskMR type + listWorkspaceTaskMRs / listTaskMRs / syncTaskMR API.
- lib/state/slices/gitlab slice (taskMRs.byTaskId) wired through
  default-state, store.ts AppState, and slices/index exports.
- hooks/domains/gitlab/use-task-mr.ts: useWorkspaceMRs (fetch-on-
  workspace-switch) and useTaskMRs (per-task selector).
- components/gitlab/mr-topbar-button.tsx — single/multi MR variants,
  status icons mirroring pr-task-icon priority order.
- task-top-bar.tsx renders PRTopbarButton and MRTopbarButton side-by-
  side; each returns null when empty, so the union handles GitHub-only,
  GitLab-only, and multi-repo tasks without an explicit switch.
- kanban-board.tsx hydrates useWorkspaceMRs alongside useWorkspacePRs.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* feat(gitlab): add /gitlab page for browsing user MRs and issues

Adds the GitLab counterpart to /github so users can browse their own
merge requests and issues from inside kandev, parallel to the existing
GitHub "My GitHub" page.

Backend
- /api/v1/gitlab/user/mrs and /user/issues — paginated wrappers around
  the existing SearchMRsPaged / ListIssuesPaged client methods.
  paginationFromQuery clamps page/per_page the same way as the client.

Frontend
- /gitlab SSR page + client with MR/Issue tabs, scope filters
  (assigned / created / review-requested), and a free-text search box
  that falls back to raw GitLab query-string for power users.
- searchUserMRs / searchUserIssues API helpers + MR / Issue / *SearchPage
  types in lib/types/gitlab.
- useGitLabAvailable hook (probes /status + refreshes on window focus)
  feeds the integrations menu so the GitLab link only shows when a
  token is configured or auth is green.

Tests
- task-top-bar.test mocks the new MRTopbarButton so renders no longer
  pick up useAppStore without a StateProvider.
- integrations-menu.test wires the new useGitLabAvailable mock and
  gitlabReady prop into mockAvailability / getAvailableIntegrationLinks
  fixtures.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): stable empty array in useTaskMRs to avoid infinite re-render

useTaskMRs returned `state.taskMRs.byTaskId[taskId] ?? []` — that fresh
literal made the zustand selector produce a new reference on every
render when the task had no MRs, triggering an infinite re-render loop
in MRTopbarButton that crashed the task page with a Next.js client-side
exception.

The page never mounted session-chat, so Docker E2E tests that drive
`/t/:id` (slow-bootstrap progress, external-stop resume, environment
reset) all timed out waiting for the chat panel.

Mirror the GitHub side's `EMPTY_PRS` pattern: hoist a module-level
`EMPTY_MRS = []` and return it for the empty case.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): address PR review — cache header, in-flight cancel, tests

- Top-level `cache: "no-store"` in 5 sites (use-task-mr.ts, gitlab-api.ts,
  gitlab-page-client.tsx). The earlier `init: { cache: "no-store" }`
  placement was silently overwritten by fetchJson with undefined, so probes
  hit the browser cache instead.
- useWorkspaceMRs now invalidates in-flight responses and calls
  resetTaskMRs when the workspace becomes null, so the previous workspace's
  MRs don't linger after sign-out / workspace switch.
- Drop unpopulated UnresolvedThreads/DiscussionCount from TaskMR (model,
  schema, TS type). SyncTaskMR's source MRStatus never populated them and
  no surface reads them; the dropped columns are skipped via an explicit
  projection in store.go so existing dev DBs keep binding.
- Extract MRList/IssueList inline fetches in /gitlab into useGitLabUserMRs
  / useGitLabUserIssues — matches CLAUDE.md "never fetch in components".
- Tests for store (UpsertTaskMR, composite key, workspace-JOIN isolation,
  ordering, Delete), service (SyncTaskMR mock round-trip + field mapping +
  store-unset error, ListTaskMRsByWorkspace JOIN isolation, ListTaskMRsByTask),
  pat_client (ListIssuesPaged X-Total), gitlab-api (5 new fns), gitlab-slice
  (setTaskMR composite key), and the use-task-mr / use-user-search hooks.
- pr SKILL.md: skip note for steps 5–6 on GitLab remotes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(gitlab): only wrap 401/403 probe errors as ErrInvalidToken

Network failures, 5xx, and DNS errors during `ConfigureToken` previously
surfaced to the user as HTTP 400 "invalid token: …" — during a GitLab
outage a user with a valid token might delete it assuming it was rejected.
Now only `*APIError` with 401/403 is mapped to `ErrInvalidToken`; other
errors propagate unwrapped so the controller routes them to "GitLab
unreachable" instead.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

---------

Co-authored-by: Kandev Agent <agent@kandev.dev>
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-23 23:17:16 +01:00
2026-01-16 01:59:19 +00:00
2026-05-22 20:11:28 +00:00
2026-02-17 09:19:42 +00:00
2026-05-01 21:02:41 +01:00

Kandev

Manage and run tasks in parallel. Orchestrate agents. Review changes. Ship value.

Workflows | Run as a Service | Roadmap | Contributing | Architecture | Discord

Kandev Demo

See all screenshots

What

Organize work across kanban and pipeline views with opinionated workflows and execute multiple tasks in parallel. Assign agents from any provider, and review their output in an integrated workspace - file editor, file tree, terminal, browser preview, and git changes in one place. Terminal agent TUIs are great for running agents, but reviewing and iterating on changes there doesn't scale.

Run it locally or self-host it on your own infrastructure and access it from anywhere via Tailscale or any VPN.

Open source, multi-provider, no telemetry, not tied to any cloud.

Vision

Humans stay in control. Define tasks, build agentic workflows with gates, review every change, decide what ships.

  • Review-first - Humans support production systems. We need to understand (yet) and trust the code that gets deployed.
  • Your workflow - Every team is different, and not every developer uses AI the same way. Define workflows once, share them across the team, and give everyone a consistent process for working with agents - regardless of experience level.
  • Remote agents - Running multiple agents on a large codebase can quickly saturate a local machine. The goal is a single control plane: offload execution to servers, orchestrate from anywhere, including your phone.

Features

  • Multi-agent support - Claude Code, Codex, GitHub Copilot, Gemini CLI, Amp, Auggie, OpenCode, Cursor, Qwen, Factory Droid, iFlow, Kilocode, Pi, Kimi, AWS Kiro, Qoder, Trae
  • Parallel task execution – start and manage multiple tasks from different sources simultaneously, boosting productivity with AI agents
  • Integrated workspace - Built-in terminal, code editor with LSP, git changes panel, embedded vscode and chat in one IDE-like view
  • Kanban task management - Drag-and-drop boards, columns, and workflow automation
  • Agentic workflows - Multi-step pipelines that mix-and-match agents per step - for example, Claude Code Opus to design a plan, GitHub Copilot Sonnet to implement it, and Codex GPT 5.4 to review the changes. See docs/workflows.md
  • Sub-tasks - Agents can spawn sub-tasks that resume from the parent task's session. Useful for splitting a task that has grown too big, or producing several PRs from the same starting point.
  • CLI passthrough - Drop into raw agent CLI mode for direct terminal interaction with any supported agent, leverage the full power of their TUIs
  • Workspace isolation - Git worktrees prevent concurrent agents from conflicting
  • Multi-repository tasks - Span a single task across multiple repositories, with one worktree per repo, per-repo branches, per-repo PRs, and per-repo grouping in the Changes panel and review dialog
  • Flexible runtimes - Run agents as local processes, in isolated Docker containers or in remote executors like sprites.dev
  • Session management - Resume and review agent conversations
  • Stats - Track your productivity with stats on the completed tasks, agent turns, etc

Integrations

GitHub Jira Linear

Connect Kandev to the tools your team already uses — pull issues into the kanban, link tasks to PRs, and surface review activity inline.

Supported ACP Agents

Agent Launch
Claude Code npx -y @agentclientprotocol/claude-agent-acp
Codex npx -y @zed-industries/codex-acp
GitHub Copilot npx -y @github/copilot --acp
Gemini CLI npx -y @google/gemini-cli --acp
Amp npx -y amp-acp
Auggie npx -y @augmentcode/auggie --acp
OpenCode opencode acp
Cursor cursor-agent acp (requires Cursor Pro)
Qwen npx -y @qwen-code/qwen-code --acp
Factory Droid npx -y droid exec --output-format acp
iFlow (beta) npx -y @iflow-ai/iflow-cli --experimental-acp
Kilocode npx -y @kilocode/cli acp
Pi npx -y pi-acp
Kimi kimi acp (install Kimi CLI from Moonshot AI)
Kiro kiro-cli-chat acp (install Kiro CLI from AWS)
Qoder qodercli --acp (install Qoder CLI)
Trae traecli acp serve (install Trae IDE CLI)

All agents communicate via ACP (Agent Client Protocol). Some agents support ACP natively, while others use ACP adapter packages that bridge their native protocols. CLI Passthrough mode is also available for direct terminal interaction with any agent CLI. If your agent isn't supported yet, open an issue or submit a PR with the integration. See Adding a New Agent CLI for a step-by-step guide.

Bring your own TUI agents

There is support for running any agent as TUI inside a terminal. Just add the cli command in the agent profile settings and the task will start the agent inside a PTY terminal instead of using ACP.

Supported Executors

Executor Description
Local Process Runs the agent as a local process on the host machine
Docker Runs the agent in an isolated Docker container
Sprites Runs the agent in a remote cloud environment via sprites.dev

Each executor uses git worktrees for workspace isolation, preventing concurrent agents from conflicting.

Quick Start

Homebrew (macOS, Linux)

brew install kdlbs/kandev/kandev
kandev

NPX

npx kandev@latest

Requires npm 7+. Installs the CLI plus a platform-matched runtime bundle in one step.

NPM (global)

npm install -g kandev@latest
kandev

Updates

  • brew upgrade kandev
  • npx kandev@latest (always uses the latest published version)
  • npm install -g kandev@latest

The worktrees and sqlite db live in ~/.kandev by default. Works on macOS, Linux, and Windows (WSL or native).

From Source

# Clone the repository
git clone git@github.com:kdlbs/kandev.git
cd kandev

# Start in production mode
make start

Prerequisites: Go 1.26+, Node.js 18+, pnpm, Docker (optional - needed for container runtimes)

High level architecture

graph LR
    UI[Web UI] --> Backend


        Backend["Backend (Orchestrator)"]


    Backend --> W1 & W2

    subgraph W1[Local Process]
        Agent1[Agent CLI] --- WT1[Worktree]
    end

    subgraph W2[Docker Container]
        Agent2[Agent CLI] --- WT2[Worktree]
    end

We also want to add support for these remote runtimes:

  • Remote SSH - run agents on remote servers over SSH, using docker or local processes with workspace isolation
  • K8s operator - run agents in a Kubernetes cluster, with auto-scaling and resource management.
Development

Project Structure

apps/
├── backend/    # Go backend (orchestrator, lifecycle, agentctl, WS gateway)
├── web/        # Next.js frontend (SSR, Zustand, real-time subscriptions)
├── cli/        # CLI tool (npx kandev launcher)
└── packages/   # Shared UI components & types

Prerequisites

  • Go 1.21+
  • Node.js 18+
  • pnpm
  • Docker (optional)

Running Dev Servers

# Start everything (backend + frontend with auto ports)
make dev

# Or run separately
make dev-backend    # Backend on :38429
make dev-web        # Frontend on :37429

Testing & Linting

make test           # Run all tests (backend + web)
make lint           # Run all linters
make typecheck      # TypeScript type checking
make fmt            # Format all code

Pre-commit Hooks

# Install pre-commit (https://pre-commit.com/#install)
pipx install pre-commit

# Install git hooks
pre-commit install

Comparison to Other Tools

There are a few similar tools in this space, and new ones appearing everyday. Here's what sets this one apart:

  • Server-first architecture - Not a desktop app. Runs as a server you can access from any device, including your phone. Start a task away from your computer and check in on it later.
  • Remote runtimes - Run agents on remote servers and Docker hosts, not just your local machine.
  • Multi-provider - Use Claude Code, Codex, Copilot, Gemini, Amp, Auggie, OpenCode, Cursor, Qwen, Droid, iFlow, Kilocode, Pi, Kimi, Kiro, Qoder, and Trae side by side. Not locked to one vendor.
  • CLI passthrough and chat - Interact with agents through structured chat messages or drop into raw CLI mode for full agent TUI capabilities.
  • Open source and self-hostable - No vendor lock-in, no telemetry, runs on your infrastructure.

Contributing

Contributions are welcome! Please read CONTRIBUTING.md before opening a PR.

See the issue tracker for open tasks, or join our Discord to chat with maintainers and other contributors.

Acknowledgments

Built with these excellent open-source projects:

Monaco Editor · Tiptap · xterm.js · dockview · CodeMirror · dnd-kit · Mermaid · Recharts · TanStack Table · Zustand · Shadcn/UI · Radix UI · Tailwind CSS

License

AGPL-3.0

Star History

Star History Chart
S
Description
Upstream mirror of https://github.com/kdlbs/kandev (main). Internal development and testing.
Readme 371 MiB
Languages
Go 58.3%
TypeScript 37.6%
HTML 1.9%
JavaScript 0.8%
Python 0.7%
Other 0.5%