agentleFS
lezli01lezli01/vincent/CLAUDE.md

vincent

CLAUDE.md28 starsChanged 3 months agoMITView on GitHub

Vincent is a local-first control plane for AI coding-agent workloads: a background daemon owns state and execution (SQLite + git worktrees + agent CLI subprocesses), and every client — the Bubble Tea TUI, vincent CLI subcommands, curl — is a thin consumer of its localhost REST+SSE API. One Go binary (cmd/vincent) serves all roles. Public documentation is feature-first and rooted at docs/README.md: docs/features.md presents the product surface, while getting-started/, guides/, platforms/, reference/, security-model.md, and faq.md explain how to use it.…

  • Installs packages

The file

We split this CLAUDE.md into the rules it gives an agent

6 rules. About 0 are common, 1 is distinctive, and 5 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈1

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 5

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Product-name style: write vincent with a lowercase v everywhere in prose, headings, labels, metadata, and skill names.

    line 42
  2. Very specific:

    All twenty-six of those run in ci.yml's gate-group job on all three platforms: four groups per OS, balanced by measured Windows time, each …

    line 201
  3. Very specific:

    One DB writer: Only the daemon opens SQLite (WAL, single connection), so writes serialize and SQLITE_BUSY cannot occur in-process.

    line 324
  4. Very specific:

    Crash-first: Every transition is persisted before it is acted on.

    line 339
  5. Very specific:

    Migrations are append-only: Add internal/store/migrations/000N_*.sql.

    line 475
  6. Distinctive:

    Never: add co-author to commits.

    line 490

≈ “Common” is an estimate: at least 1 in 5 of the 26,980 CLAUDE.md files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
CLAUDE.md

CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

What this is

Vincent is a local-first control plane for AI coding-agent workloads: a background daemon owns state and execution (SQLite + git worktrees + agent CLI subprocesses), and every client — the Bubble Tea TUI, vincent CLI subcommands, curl — is a thin consumer of its localhost REST+SSE API. One Go binary (cmd/vincent) serves all roles.

Documentation model

Public documentation is feature-first and rooted at docs/README.md: docs/features.md presents the product surface, while getting-started/, guides/, platforms/, reference/, security-model.md, and faq.md explain how to use it. It is derived from the source, not from planning records: the config reference tracks internal/config, the CLI page the cobra tree, the API page internal/api/server.go's route table, the TUI key tables internal/tui/bindings.go, and the block reasons the Reason* constants. A change to any of those is a change to its page. The same rule covers the pictures: every docs/assets/tui-*.png is a capture of the running TUI produced by scripts/screenshots.sh (below), never a drawing of one.

The published site is versioned (task 131): .github/workflows/pages.yml builds every release tag from v0.6.0 on, master as /dev/, and the newest tag again as /latest/, and cmd/sitever assembles them. The published root is latest, not master — /vincent/ redirects to /vincent/latest/ — so a docs change reaches readers at the next release, and is visible under /dev/ from the merge. Tags are built unedited by scripts/site/build.sh with the pinned github-pages gem; anything version-aware (the selector, banners, canonicals, redirects) belongs in cmd/sitever, never in a layout, because an old tag's layout cannot know about a newer release.

GitHub Pages SEO metadata is centralized in _data/seo_pages.json and _data/why_articles.json. Keep those entries in sync when public pages or the "Why vincent is awesome" collection changes. The collection's social cards are generated from that article data by scripts/social-cards.py; do not hand-edit the generated PNGs in docs/assets/social/.

Product-name style: write vincent with a lowercase v everywhere in prose, headings, labels, metadata, and skill names. Use an initial capital only when vincent is the first word of a complete sentence. Preserve the exact case of case-sensitive code and external identifiers, such as the $VINCENT gate-script variable and the WinGet package ID lezli01.Vincent; those are not prose.

Implementation work also uses these maintainer records. They are not optional when a change touches the behavior or decision they describe:

  • docs/spec.md — the product and implementation spec, and the only one. Section numbers (§6, §12.2, §13.3 …) are used as identifiers throughout the code comments. When a comment says "spec §9.1", go read it before changing that code. It is deliberately not versioned: it describes the system as it is now, and is amended in place with dated notes, in the same PR as the code that makes an amendment true. Two specs would make every spec §9.1 citation ambiguous, which is why the original version's copy was extracted here rather than forked.
  • docs/tasks/ — planned and in-flight work, one document per piece of work (001-configurable-branch-names.md), with its decisions. docs/tasks/README.md is the index and defines the conventions: status markers ([ ]/[~]/[x]/[!]), NNN.n task IDs, updating the index row in the same edit, and never deleting a task. Mention the task ID in the PR ("closes 001.4").
  • docs/history/v0-tasks.md — the closed initial-release ledger (70/70, released as v0.1.0). Frozen: there is no open task in it to claim. Read it for the design decisions it carries, which are binding and are cited from code comments ("phase 2 decision", "T1.5/T1.6 decision", "PR L decision").
  • docs/gates/ — the manual walkthroughs behind the scripted acceptance gates, and the record of when each was last walked.

skills/vincent-workflows/SKILL.md and skills/vincent-triggers/SKILL.md are runtime text, not just documentation: skills/embed.go embeds both, and internal/workflow/builtin.go splices the first into the built-in create-workflow (task 024) and update-workflows (task 037) prompts and the second into create-trigger and update-triggers (task 098), at build time. An edit to either file changes what the daemon tells an agent to do. Each reaches its prompts escaped for text/template and re-indented into a YAML block scalar, so it may contain anything — but each built-in's standing corrections to its skill live in that built-in's own header, never in the skill: what asking costs, destination, missing references/ for create-workflow; that the deliverable is an edit to existing files, that asking is denied, missing references/ for update-workflows — and, in its global: true branch (task 123), that the deliverable is a staged proposal rather than an edit, that the global files have no git history to consult, and that the agent never writes {config_dir}/workflows itself; what asking costs, staging then vincent trigger apply, missing references/ for create-trigger; that asking is denied, that the deliverable is a staged proposal, missing references/ for update-triggers. Both trigger headers carry the never-arm rule, and vincent trigger apply enforces it (task 098 decision 3) — never loosen one without the other.

The built-ins are held to the same bar update-workflows holds a project's workflows to (task 037 decision 5): when a workflow feature lands, re-read all five sources in the same piece of work, use the feature where it applies, and add the line for it to update-workflows' checklist — that checklist is version-coupled by design, and a feature missing from it is one the built-in will never propagate. The trigger pair has its own version-coupled checklist, in update-triggers (task 098 decision 7): when a trigger feature lands, extend skills/vincent-triggers/, re-read both trigger built-ins, and add the feature's line there.

Phase decisions ("phase 2 decision", "PR G decision", "T1.5/T1.6 decision") recorded inline in comments and in tasks.md are binding — they are the outcome of design sessions, not incidental notes. Don't relitigate one without saying so explicitly.

Commands

Build targets run through mage with zero install (go run mage.go -l lists them). CI runs exactly these:

go run mage.go build     # build bin/vincent with version ldflags injected
go run mage.go test      # go test ./...
go run mage.go testrace  # go test -race ./...  (needs cgo + a C compiler)
go run mage.go lint      # go tool golangci-lint run (pinned via go.mod tool directive)
go run mage.go lintall   # the same, for GOOS=linux, darwin and windows from this host

CI lints with lintall, on the ubuntu-latest leg only (#727): there is no cgo, so a cross-GOOS lint sees the same files a native one would, and the macOS and Windows legs skip the step.

CI's test step runs go run mage.go testci instead of testrace: the suite under go tool gotestsum, rerunning each failed test up to twice — but never after a data race, which fails the job on its first sighting because a race that does not recur is still a bug — writing junit.xml, test.json and reruns.txt to bin/test-report/ (uploaded as the test-report-<os> artifact), and listing every rerun in the job summary. The race detector is scoped per OS (#728): Linux races the whole suite, macOS races nothing, and Windows races only internal/procx, internal/daemon, internal/service and internal/taskrun — its own process, daemon and service code — in a second pass, run alongside the plain one, whose reports carry a -race suffix (a split package's share, in shard mode below, carries -split-<pkg> and, when raced, -split-<pkg>-race — internal/taskrun's raced share is -split-taskrun-race, and the bulk of the race time). Races are overwhelmingly platform-independent, so the Linux leg catches them, and a full race run had made Windows the required check's critical path; magefile.go's TestCI records the measurements. This is deliberately weaker proof on macOS and Windows. testrace stays the local default, racing everything — locally a failure should fail. ./scripts/test-rerun-check.sh proves testci against tests that fail on purpose, in every scope (VINCENT_TEST_RACE=all, none and a package list) and in the shard mode; it is not wired into CI.

Windows' test step is sharded (#730): it runs as four test shard (windows-latest, i) jobs side by side, and ci (windows-latest) is now only an aggregator that fails unless every shard passed, kept for branch protection exactly as gates is for gate-group. Linux and macOS stay one ci job each and never wait on it. Each shard uploads test-report-windows-latest-<i>; Linux and macOS keep test-report-<os>. VINCENT_TEST_SHARD=i/n selects shard i of n (unset runs everything): the long-pole packages are split, their top-level tests spread across shards by a hash of the test name, and every other package goes to one shard whole, round-robin. VINCENT_TEST_SPLIT overrides the list of split packages, which is how the check script proves the mode on tests of its own. The shard count lives only in the job's matrix; four is what fits the free plan's 20 concurrent jobs at peak, beside the twelve gate groups.

Plain toolchain works too (go build ./..., go test ./...). Single test:

go test ./internal/taskrun -run TestRunnerStop -v
go test -race ./internal/tui -run TestLiveChangeFromRealServer

Acceptance gates — bash scripts that drive a real daemon end to end over curl against the fake agent; CI runs every one of them on Linux, macOS and Windows:

./scripts/m1-gate.sh
./scripts/m2-gate.sh
./scripts/m5-gate.sh                            # cursor adapter (§9.7)
./scripts/m6-gate.sh                            # parallel steps and fan-out (§7.5, §7.6)
./scripts/m7-gate.sh                            # conditions between steps (§7.7)
./scripts/m8-gate.sh                            # loops (§7.8)
./scripts/m9-gate.sh                            # workflow includes (§7.9)
./scripts/m10-gate.sh                           # the daemon's MCP server (§13.4)
./scripts/m11-gate.sh                           # editing config.yaml over the API (§12.3)
./scripts/m12-gate.sh                           # container step execution (§16); skips without docker
./scripts/m13-gate.sh                           # authoring a workflow over the API (§8.2, §13.2)
./scripts/m14-gate.sh                           # chats end to end (task 067)
./scripts/m15-gate.sh                           # archived boards and permanent delete (task 092)
./scripts/m16-gate.sh                           # event triggers (task 096)
./scripts/123-gate.sh                           # global update-workflows (task 123)
./scripts/125-gate.sh                           # a task or chat on an existing branch (task 125)
./scripts/064-gate.sh                           # a task from a pull request (task 064)
./scripts/052-gate.sh                           # GitHub pull requests (task 052)
./scripts/069-gate.sh                           # opening a pull request from vincent (task 069)
./scripts/068-gate.sh                           # the pull request write surface (task 068)
./scripts/130-gate.sh                           # issues end to end (task 130)
./scripts/134-gate.sh                           # issue main worktree and merge-back (task 134)
./scripts/137-gate.sh                           # handoffs end to end (task 137)
./scripts/138-gate.sh                           # the copilot adapter (task 138)
./scripts/140-gate.sh                           # a chat on an issue (task 140)
./scripts/142-gate.sh                           # Jira issue sync (task 142)
./scripts/143-gate.sh                           # auto-archive on issue close (task 143)
VINCENT_GATE_SCENARIO=2 ./scripts/m2-gate.sh    # single scenario, for debugging
VINCENT_GATE_AGENT=claude ./scripts/m2-gate.sh  # manual run against the real CLI
VINCENT_GATE_AGENT=cursor ./scripts/m5-gate.sh  # ditto, for cursor-agent
VINCENT_GATE_BIN=/some/dir ./scripts/m1-gate.sh # copy prebuilt vincent/fakeagent/fakegh/fakejira from there instead of building

All twenty-six of those run in ci.yml's gate-group job on all three platforms: four groups per OS, balanced by measured Windows time, each gate one named step that runs in exactly one group (if: matrix.group == N). The gates job is now only an aggregator that fails unless every group passed, kept because branch protection requires gates (<os>) by name. A new gate is a step in gate-group with a group number — the lightest group's — and every gate script sources scripts/lib/gate.sh right after setting ROOT: it builds through gate_build / gate_build_as (which copy from VINCENT_GATE_BIN, built once per CI job, when it is set), and its wait loops sleep $GATE_POLL for $(gate_ticks SECS) iterations so the budget stays written in seconds. Deliberate delays and workflow run: bodies keep their plain sleep N. m12 is the twenty-seventh and the exception: it needs a real container runtime, so it runs its assertions on the Linux leg only and skips itself (exit 0, one line saying why) on the other two — but for two different reasons, and only one of them is "no docker". The macOS runner has no daemon. The Windows runner does, in Windows-container mode, where FROM alpine:3 fails with "no matching manifest for windows/amd64"; probing for a daemon is therefore not enough to skip there, and the gate refuses on the host platform first. m6, m7 and m8 spent a while unwired because a cloud session's token has no workflow scope and so cannot write .github/workflows/ by any route — push or API (#120, #122, #125). A gate that has never run on a platform is not known to pass there: wiring these in turned up two Windows-only faults in a script that had passed on Linux for weeks, both recorded below.

scripts/m3-gate.sh seeds a human walkthrough instead of asserting — M3's acceptance is a judgement about a TUI. The manual legs of M5 are walked through in docs/gates/m5-gate.md, which also records the runs. Task 017's workflow graph has no script at all for the same reason M3's has none — what is judged is whether a picture is legible — so docs/gates/017-workflow-graph.md carries the corpus (docs/gates/corpus/*.yaml, kept parseable by a test) and the run record.

New gate scripts must be committed executable (git update-index --chmod=+x): chmod on Windows never reaches the index, Git Bash ignores the bit, so a non-executable gate passes on Windows and fails both POSIX legs with exit 126 before running an assertion.

scripts/screenshots.sh is the other seeding script, and the only source of the images under docs/assets/tui-*.png. It seeds a throwaway installation — its own config and data dirs, its own $HOME (so the skills and status-line rows are the seed's, not yours), seven git repos (one a GitHub project, answered by cmd/fakegh), a daemon, twenty-six tasks (a two-round fan-out with three lanes, a loop and a handoff's task among them, three archived, eight started from an issue), eleven chats (four live, three archived — one of them on an issue — one closed on an issue and three closed on a task) and sixteen issues (thirteen local, three of them closed, and three imported from the fake GitHub with a mirrored thread, platform-infra's filling every lane) covering every state — and photographs the running TUI with VHS (brew install vhs). Documentation never draws a screen: no ASCII mock-ups of panels, no hand-written "example" frames. If a panel changed, re-run the script:

./scripts/screenshots.sh            # seed, capture every shot, clean up
./scripts/screenshots.sh seed       # leave the daemon up to iterate on tapes
VINCENT_SHOTS_ONLY=tui-diff ./scripts/screenshots.sh capture

It is macOS/Linux-only and CI does not run it (VHS needs ttyd and ffmpeg, and its workflows use a POSIX shell rather than the sh∩pwsh intersection the gates are held to). Three VHS traps are already worked around in it and will bite again in any new tape: a Screenshot is written on the next captured frame, so a tape that ends on one records nothing; keys pressed inside a Hide block never reach a screenshot at all — only the launch is hidden; and xterm.js's canvas renderer, which VHS photographs, can leave a cell of the command palette's border painted after it closes (#743), so a screen reached through the palette with empty rows under the box opens and scrolls the help overlay before its Screenshot to get those cells repainted.

A gate's workflow run: bodies run under the daemon's shell — /bin/sh on POSIX, pwsh on Windows (§8.3) — not under the gate's bash, so they must be spelled in the intersection of the two: exit N, sleep N and git .... That excludes touch, seq, [ -f ] and for/if, and it is why a lane that needs a file on disk writes it with git config -f x k v and one that only needs a commit uses git commit --allow-empty. exit N has to be the whole body: pwsh's &&/|| take pipelines, so ... && exit 0 is parsed as a command named exit and fails. A body that must pass or fail on a condition is one command whose own exit code says so — git config --get exits 1 on a missing key. Assert concurrency and timing from the API instead of from inside a step body.

In the gate's own bash, a capture spanning several lines of jq output needs | tr -d '\r': jq writes CRLF on Windows, $(...) drops only the trailing one, and the interior CRs then fail an exact comparison against a $'a\nb' literal. Single-line captures are unaffected, which is how this hid until m8 compared a list.

Never pipe into grep -q. Capture first and match a here-string: out="$(git log --format=%s "$b")"; grep -qx thing <<<"$out". grep -q exits at its first match and closes the pipe; the producer then dies of SIGPIPE writing whatever came next, and set -o pipefail reports 141 — so the assertion fails because it matched. It is a race, not a certainty: it fires only when the producer still had output to write, which made m7 scenario 4 fail ~20% of runs on a three-line git log whose match was the second line, red on CI and green on a rerun. m2's follow-up assertion had carried the here-string fix and its explanation since it was written; eight other sites across m2, m5, m6 and m7 had not. The same applies to any early-exiting consumer — head, read, a jq that exits mid-stream.

Requires bash, go, git, curl, jq.

Architecture

Dependency direction is one-way: cli → tui/daemon; daemon wires store + workflow + scheduler + taskrun + events + notify + api; leaf packages (taskstate, issuestate, issueprovider, jira/jiramap, keymap, reasons, gitx, procx, pathx, version, config) depend on nothing internal — with one deliberate exception: config imports taskstate and keymap, and only those, to validate notify.on against §6's state vocabulary and tui.keys against §15's key vocabulary rather than keeping a second copy of either (task 046 decision 4, widened by task 118 decision 3). taskstate and keymap are themselves leaves, so the direction stays one-way. internal/jira/jiramap is a leaf in the same sense: it imports the standard library and issuestate only — never issues, which imports the store — so the Jira client and outbox can both consult it (#898; a test walks its go list -deps). internal/jira itself is the Jira HTTP client. internal/daemon/daemon.go:Run is the single wiring point — read it first to see how the pieces connect.

Ownership invariants. These are what make the concurrency safe; violating one is a correctness bug, not a style issue:

  • The daemon owns everything. Clients never touch git, the DB, or agent processes — only the API. Killing every client changes nothing about running work.
  • One DB writer. Only the daemon opens SQLite (WAL, single connection), so writes serialize and SQLITE_BUSY cannot occur in-process.
  • internal/scheduler is the only place queued → running happens. Single goroutine; both concurrency caps (global + per-project) are safe only because admission is unraced. Ordering and caps come from SQL (store.ListAdmissible); the walk is in Go because admission changes the tallies mid-statement.
  • internal/taskrun actors are the sole writer of a task's state and its step_run rows. One goroutine per admitted task, living for exactly one admission — a gate, a block, or a pause releases the slot and ends the goroutine.
  • internal/taskstate is the pure FSM (no I/O). Both the API (409 on invalid action) and the engine consult it, so "what may happen next" has one definition.
  • internal/events fan-out is post-commit only. The store's event hook publishes after the DB records the event. Durable state events disconnect slow subscribers (they resume from the events table via Last-Event-ID); live output chunks are dropped for slow subscribers (the transcript file is the durable copy).
  • Crash-first. Every transition is persisted before it is acted on. Recovery (internal/taskrun/recover.go, spec §12.4) finalizes running step runs as interrupted, kills verified orphans (the PID must still hold the process the row journaled, proved by an exact platform-native identity — procx.Identity, with the older start-time tolerance kept only for rows carrying none — the PID reuse guard), and re-runs the step as an attempt that does not consume a retry.

Package map (each has a doc.go stating its role and spec sections):

PackageRole
internal/configPlatform-native config/data dirs, config.yaml load + hot reload
internal/storeSQLite: embedded up-only migrations, typed CRUD, event hook
internal/daemonLifecycle: foreground/detached start, lock file, token, daemon.json, log rotation
internal/apiLocalhost REST + SSE, bearer auth, snake_case error envelope
internal/apiclientTyped HTTP+SSE client — the one client for TUI and CLI; owns its wire types (server DTOs stay unexported)
internal/workflowYAML registry (builtin < global < project shadowing), live reload, text/template step engine, creation-time expansion of include steps (§7.9)
internal/taskrunStep executors — agent, command and manual are the ones that run something; parallel, fan_out, condition, loop and break are structure, check is a field agent and command steps may carry, never a type, and include never arrives at all — it is spliced away at task creation (§7.9) — plus guards (if:, §7.7), loop iteration (§7.8), retries, timeouts, human actions, recovery, transcripts
internal/agentAgentAdapter interface + option catalog; agent/claude, agent/codex, agent/cursor, agent/copilot implement it
internal/worktreePer-task git worktrees, vincent/{id}-{slug} branches, dirty detection
internal/notifyThe outward signal (§12.3, task 046): a broker subscriber that spawns notify.command when a task enters a state in notify.on, with an enriched JSON envelope on the child's stdin. Bounded queue, four workers, fixed 10 s per child, no replay
internal/triggerThe inward signal (task 096): {config_dir}/triggers/*.yaml definitions, their validator and the schema descriptor the form renders from, and the 0600 writer that edits them with workflow.Edit ops. Replays POST /v1/tasks through an http.Handler the way internal/mcp does, and never imports internal/api (decision 30)
internal/keymapThe TUI's rebindable operations (task 118, §12.3, §15): ids, default keys and surfaces, the fixed keys and recorded exceptions, key-string parsing, and the one clause checker the registry tests run over the defaults and config runs over tui.keys. A leaf
internal/pathxThe one directory comparison (task 132.3): SameDir and by-component Contains, lexical after Clean, case-folded on Windows and macOS, then symlink-resolved. worktree and api delegate to it, and the TUI matches its working directory with it without importing a git-running package. A leaf
internal/reasonsThe plain-language catalogue of task and step reasons (task 127, §18): a title, a meaning, the §6 actions and a docs anchor for each block_reason/failure_reason/skip_reason/queued_reason. A leaf that spells the strings out; tests hold it to every Reason* constant and to docs/reference/task-lifecycle.md's tables
internal/issuestateThe issue lifecycle of §5.6 (task 130): open/closed, close reasons, and which actor (human, agent, sync) may take which action. Pure and stdlib-only, a third vocabulary beside taskstate and chatstate; the store consults it inside the transaction that changes an issue's state
internal/issueproviderThe issue-provider registry (task 142.5, task 142 decision 2, §5.6): the issue_remotes.provider ids (github, jira), Known, WritesBack and All. The store's outbox enqueue, issues.WritesBack, the API's source vocabulary and issuesource ask it rather than compare a "github" literal. Stdlib only, so the store can import it; a test walks its go list -deps
internal/jira/jiramapThe Jira-to-§5.6 mapping (task 142 decision 9, 142.2): a status is closed iff its category is done, the close reason comes from the resolution (with a null-resolution status-name fallback), kind and label normalization, the remote_json record, and the transition chooser for a vincent close or reopen with typed no_transition/ambiguous_transition/transition_needs_fields failures. Pure; imports stdlib and issuestate only, and repeats issues' kind and label bounds
internal/jiraThe native Jira Cloud REST v3 client, reads (issue #908, task 142.12) and writes (issue #909, task 142.13): Basic auth from credentials read per request and never cached, search/jql only (never the retired search) paged by nextPageToken within one call, single and bulk (bulkfetch, 100 ids) issue reads with an explicit field list and rendered HTML, comment threads with their visibility, project, myself and serverInfo; the initial and incremental JQL builders; the write half — Transitions (with screen fields), DoTransition and AddComment (ADF), a write's 403 being no_write_scope; and a typed *Error whose reason equals the seam's strings and whose text never carries the token. Imports jira/jiramap only
internal/issuesourceThe issue-provider seam (task 142 decision 2, 142.4; supersedes task 130 decision 15.1): the Source interface the importer and the write-back drain call instead of *github.Client, its neutral types and reason vocabulary (equal to GitHub's strings), the GitHub source, and the Jira source (#908, writing back since #909). Optional capabilities — GitHub's repository-wide comment listing and placeholder adoption, Jira's bulk read, binding resolution, ProjectLocator (the drain binds a row through its project) and LaggingLister (a listing that may trail its own writes is confirmed by id) — are small interfaces found by type assertion. Imports store, github, gitx, issuestate, issueprovider, jira, jira/jiramap and jira/render; neither store, github nor jira imports it back
internal/issuesThe one validated write path for issues (§5.6, task 130): title/label/kind/priority rules and actor attribution, shared by the API, sync and triggers. Imports store, issuestate and issueprovider only; store errors pass through for errors.Is
internal/tuiBubble Tea client: thirteen views routed by viewID — the board, the task workspace, new task, projects, workflows, daemon, pull requests, the chats board and a chat, the two archived boards, triggers, and the issues list and an issue (§15)

Adapters differ in what they can do, and the differences are documented, never faked: codex ships no model catalog and no mid-run input; cursor has no effort concept at all (it lives in the model id), probes its models over an authenticated network call rather than from --help, and cannot honor restricted on Windows — where it returns agent.ErrRestrictedUnsupported from Start so the engine fails the step rather than silently running it full-auto; copilot enumerates no models (ids are free-form, auto is the CLI's default), takes no mid-run input, reports no cost (it spends premium requests, never dollars), always reports logged_in as null, supports restricted on every OS as a tool set with no shell, and is not a SkillInvoker (spec §9.9). A capability an adapter lacks is stated in spec §9.x and ignored at run time; it is never emulated.

internal/worktree and internal/taskrun/engine.go share one snake_case vocabulary for failure/block reasons (ReasonTimeout, ReasonWorktreeDirty, …) — a block_reason means the same thing wherever it originated. Reuse those constants rather than inventing strings.

The TUI holds no state the daemon doesn't have. Views are sub-models behind the view interface; the root routes non-global messages to the active view and checks inputCapturing before applying single-key global bindings.

Testing conventions

Tests are self-contained and hermetic — no real agent CLI, no network, no running daemon, no shared fixtures:

  • internal/testrepo — throwaway git repos (skips when git is absent).
  • internal/store/storetest — storetest.Open(path) is store.Open on a copy of a once-per-process migrated template. Use it for every test store that is not testing the migrations themselves: under -race the pure-Go SQLite makes migrating a new file ~40× slower than opening a copy (#726).
  • internal/testutil/wait — the one poll-until-true helper (wait.Until, wait.UntilWithin, wait.Poll) and wait.Timeout, which scales a budget by VINCENT_TEST_TIMEOUT_SCALE (set to 3 on CI's Windows leg). Never hand-roll deadline := time.Now().Add(...) in a test: a fixed budget is sized for a quiet machine and that leg overruns it (#731).
  • testing/synctest — a test whose only waiting is on timers, tickers or goroutines talking over channels (no subprocess, no socket, no fsnotify watcher) runs inside synctest.Test and waits with synctest.Wait() instead of a sleep or a poll: virtual time makes a real interval free and its boundary exact, and "nothing more will happen" becomes provable rather than "nothing within 200 ms". Build the harness inside the bubble so its goroutines belong to it. Anything that spawns a process, dials a socket or watches files keeps wait.Until. The first adopters (#737): the notify filter and pool tests over the spawn hook, the §13.3 status throttle, and the scheduler's tick loop (internal/scheduler/loop_test.go).
  • internal/agent/agenttest — compiles cmd/fakeagent once per test process.
  • cmd/fakeagent — scenario-driven stand-in for an agent CLI. Dialect comes from argv shape (exec first arg ⇒ codex-shaped; --trust anywhere ⇒ cursor-shaped; --output-format json ⇒ copilot-shaped; otherwise claude-shaped — cursor's run argv is claude's plus flags, and --trust is the one flag only cursor passes, in both permission modes; claude and cursor pass --output-format stream-json, so json is copilot's alone, whose prompt is read from stdin only); models and status as argv[1] answer cursor's option and login probes. Scenarios come from env (FAKEAGENT_SCENARIO, FAKEAGENT_VERSION, FAKEAGENT_EDIT_FILE, FAKEAGENT_CURSOR_LOGGED_OUT, FAKEAGENT_SCENARIO_COPILOT, …) so argv stays faithful to the real CLIs. Add scenarios here rather than special-casing adapters for tests.
  • cmd/fakegh — the same for gh; its mutable issue corpus and gh api answers live in internal/github/fakeissues, so a net/http leg can serve the same fake.
  • cmd/fakejira — a stand-in Jira Cloud site (#907): fakejira serve binds a loopback port, writes it to --port-file atomically, and stops cleanly on POST /_fake/shutdown on every OS (SIGTERM/interrupt too, on POSIX only — Windows has no graceful signal for a console-less child). Its corpus, scenarios and REST answers live in internal/jira/fakejira, whose *Store is the http.Handler an httptest leg mounts too, so both legs serve the same fake; internal/jira/jiratest builds and starts the binary.
  • *live_test.go — the TUI/apiclient wired to the real API handlers over httptest, which is what keeps client and server wire types from drifting.
  • e2e_test.go in internal/cli and internal/tui build the real binary in TestMain; detached self-exec and daemon auto-start can't be proven in-process.
  • Adapter parsing is table-driven against captured real-CLI output in testdata/*.jsonl / help_*.txt, named with the CLI version they came from.

Tests isolate state via VINCENT_CONFIG_DIR / VINCENT_DATA_DIR (see internal/config/dirs.go) — use those, never the user's real dirs.

Conventions

  • Cross-platform is a hard requirement. Windows, macOS, and Linux all run the full suite plus every gate in CI. Platform-specific code goes in _unix.go/_windows.go/_darwin.go/_linux.go files (see internal/procx, internal/daemon/spawn*.go, internal/service). Never assume a POSIX shell, /-separated paths, or signal semantics.
  • Lint the other platforms before pushing, not just build them. go build cross-compiles with GOOS=…, but go tool golangci-lint cross-builds the linter and then cannot run it. Build it for the host once and run that — go run mage.go lintall does exactly this, and is what CI runs:

    LINT=$(go tool -n golangci-lint)
    for os in windows darwin linux; do GOOS=$os "$LINT" run ./...; done

    A host-only lint hides findings in every build-tagged file — unparam fired on macOS alone for a helper whose argument is constant there and varying on Linux, and Windows does not compile that file at all.

  • Package docs carry the design. New packages get a doc.go explaining their role and citing spec sections. Non-obvious choices get a comment saying why, the way the existing code does.
  • Lint is strict — golangci-lint v2 standard set plus errorlint, gocritic, revive, copyloopvar, intrange, misspell, unconvert, unparam, nolintlint, sqlclosecheck, with gofumpt formatting. Run go run mage.go lint before pushing.
  • Migrations are append-only. Add internal/store/migrations/000N_*.sql; never edit an applied one.
  • Time in SQLite is RFC3339 UTC with fixed-width nanoseconds (store.TimeFormat) so lexicographic TEXT ordering matches chronological order.
  • Git flow: everything lands via PR to master, merged with merge commits (no squash — branch history becomes master's history). Conventional Commits (feat, fix, docs, ci, chore, refactor, test), branches named type/short-description. Conventional prefixes belong on commits, not human PR titles: use a plain-language title such as Add workflow fields, never feat: add workflow fields. GitHub copies the PR title into the merge commit body, so a conventional title makes Release Please record both the merge and the matching inner commit in CHANGELOG.md. The PR title workflow enforces this; Release Please and Dependabot PRs are narrowly exempt because those tools couple their PR titles to their generated commit messages. CI green on all three platforms is required.
  • Never add co-author to commits.

Security posture

Agents run full-auto by default — a documented design decision (spec §16), not a bug. An agent can run arbitrary commands as the invoking user; the git worktree isolates collisions between tasks, not privileges. The TUI shows this warning once on first run (internal/tui/firstrun.go). Keep that property in mind when changing permission modes, workflow defaults, or the first-run acknowledgment.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.