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
In this repository28 other files it gives its agents.
- AGENTS.mdAGENTS.md
- ask-mattSkill
- codebase-designSkill
- code-reviewSkill
- diagnosing-bugsSkill
- domain-modelingSkill
- grillingSkill
- grill-meSkill
- grill-with-docsSkill
- handoffSkill
- implementSkill
- improve-codebase-architectureSkill
- prototypeSkill
- researchSkill
- resolving-merge-conflictsSkill
- setup-matt-pocock-skillsSkill
- tddSkill
- teachSkill
- to-questionnaireSkill
- to-specSkill
- to-ticketsSkill
- triageSkill
- wait-whatSkill
- wayfinderSkill
- wizardSkill
- writing-for-agentsSkill
- vincent-triggersSkill
- vincent-workflowsSkill
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
- Very specific: line 42
Product-name style: write vincent with a lowercase v everywhere in prose, headings, labels, metadata, and skill names.
- Very specific: line 201
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 …
- Very specific: line 324
One DB writer: Only the daemon opens SQLite (WAL, single connection), so writes serialize and SQLITE_BUSY cannot occur in-process.
- Very specific: line 339
Crash-first: Every transition is persisted before it is acted on.
- Very specific: line 475
Migrations are append-only: Add internal/store/migrations/000N_*.sql.
- Distinctive: line 490
Never: add co-author to commits.
≈ “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.
Files
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 everyspec §9.1citation 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.mdis the index and defines the conventions: status markers ([ ]/[~]/[x]/[!]),NNN.ntask 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 asv0.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 hostCI 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 TestLiveChangeFromRealServerAcceptance 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 buildingAll 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 captureIt 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_BUSYcannot occur in-process. internal/scheduleris the only placequeued → runninghappens. 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/taskrunactors 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/taskstateis 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/eventsfan-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 viaLast-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) finalizesrunningstep runs asinterrupted, 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):
| Package | Role |
internal/config | Platform-native config/data dirs, config.yaml load + hot reload |
internal/store | SQLite: embedded up-only migrations, typed CRUD, event hook |
internal/daemon | Lifecycle: foreground/detached start, lock file, token, daemon.json, log rotation |
internal/api | Localhost REST + SSE, bearer auth, snake_case error envelope |
internal/apiclient | Typed HTTP+SSE client — the one client for TUI and CLI; owns its wire types (server DTOs stay unexported) |
internal/workflow | YAML registry (builtin < global < project shadowing), live reload, text/template step engine, creation-time expansion of include steps (§7.9) |
internal/taskrun | Step 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/agent | AgentAdapter interface + option catalog; agent/claude, agent/codex, agent/cursor, agent/copilot implement it |
internal/worktree | Per-task git worktrees, vincent/{id}-{slug} branches, dirty detection |
internal/notify | The 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/trigger | The 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/keymap | The 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/pathx | The 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/reasons | The 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/issuestate | The 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/issueprovider | The 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/jiramap | The 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/jira | The 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/issuesource | The 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/issues | The 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/tui | Bubble 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)isstore.Openon a copy of a once-per-process migrated template. Use it for every test store that is not testing the migrations themselves: under-racethe 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) andwait.Timeout, which scales a budget byVINCENT_TEST_TIMEOUT_SCALE(set to 3 on CI's Windows leg). Never hand-rolldeadline := 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 insidesynctest.Testand waits withsynctest.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 keepswait.Until. The first adopters (#737): the notify filter and pool tests over thespawnhook, the §13.3 status throttle, and the scheduler's tick loop (internal/scheduler/loop_test.go).internal/agent/agenttest— compilescmd/fakeagentonce per test process.cmd/fakeagent— scenario-driven stand-in for an agent CLI. Dialect comes from argv shape (execfirst arg ⇒ codex-shaped;--trustanywhere ⇒ cursor-shaped;--output-format json⇒ copilot-shaped; otherwise claude-shaped — cursor's run argv is claude's plus flags, and--trustis the one flag only cursor passes, in both permission modes; claude and cursor pass--output-format stream-json, sojsonis copilot's alone, whose prompt is read from stdin only);modelsandstatusas 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 forgh; its mutable issue corpus andgh apianswers live ininternal/github/fakeissues, so anet/httpleg can serve the same fake.cmd/fakejira— a stand-in Jira Cloud site (#907):fakejira servebinds a loopback port, writes it to--port-fileatomically, and stops cleanly onPOST /_fake/shutdownon 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 ininternal/jira/fakejira, whose*Storeis thehttp.Handleranhttptestleg mounts too, so both legs serve the same fake;internal/jira/jiratestbuilds and starts the binary.*live_test.go— the TUI/apiclient wired to the real API handlers overhttptest, which is what keeps client and server wire types from drifting.e2e_test.goininternal/cliandinternal/tuibuild the real binary inTestMain; 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.gofiles (seeinternal/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 buildcross-compiles withGOOS=…, butgo tool golangci-lintcross-builds the linter and then cannot run it. Build it for the host once and run that —go run mage.go lintalldoes exactly this, and is what CI runs:LINT=$(go tool -n golangci-lint) for os in windows darwin linux; do GOOS=$os "$LINT" run ./...; doneA host-only lint hides findings in every build-tagged file —
unparamfired 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.goexplaining 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 lintbefore 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 becomesmaster's history). Conventional Commits (feat,fix,docs,ci,chore,refactor,test), branches namedtype/short-description. Conventional prefixes belong on commits, not human PR titles: use a plain-language title such asAdd workflow fields, neverfeat: 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 inCHANGELOG.md. ThePR titleworkflow 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.
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
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.

