todoist-cli
AGENTS.md331 starsChanged 7 days agoMITView on GitHub
This repository is set up so coding agents prove their work before they call it done. This page explains the parts, what each one guarantees, and where it stops. It grows as each part lands.
- Commits and pushes
Where files are headingNew AGENTS.md files have more rules about keeping secrets out of the code, and fewer about code style.
Of the AGENTS.md files we read, the 31,774 first seen in the last six months, against the older ones: the kind of rule that gained most share, and the kind that lost most. About files like this in general, not this one, and an estimate, since a kind is a pattern in the words.
In this repository11 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- add-commandSkill
- build-featureSkill
- fix-bugSkill
- learnSkill
- refactorSkill
- review-changesSkill
- verifySkill
- write-testsSkill
- todoist-cliSkill
The file
We split this agents.md into the rules it gives an agent
10 rules. About 0 are common, 3 are distinctive, and 7 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈3
Distinctive · estimate
This author's own choices. Worth a look.
7
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 7
Git hooks: run for everyone, people and agents alike, on every commit and push.
- Very specific: line 44
If node is not on PATH (some GUI git clients), both hooks still apply the branch rule, skip the checks, and say so.
- Very specific: line 59
The record: after a full pass it writes a content hash of every file local checks read (not docs, not CI-only files) to …
- Very specific: line 63
If a pull-request workflow gains a step, .agents/scripts/lib/steps.test.ts fails until verify gains it too, or says why it can't.
- Very specific: line 67
Every agent's hooks call one script, .agents/scripts/guard.ts, so the agents can't drift apart.
- Distinctive: line 83
If the branch's code is back to what main has (an undo, a stash, a switch), the turn passes: CI already checked that code.
- Very specific: line 108
When you say a change is ready, the agent runs review-changes:
- Very specific: line 111
Blast-radius review: only when shared code changed (src/lib/, registration and global flags in src/index.ts, the cli-core or SDK version, …
- Distinctive: line 129
Leave out the method and your theory of the cause.
- Very specific: line 154
No node on PATH in a GUI client: the hooks skip lint, the type check and the tests, with a message.
≈ “Common” is an estimate: at least 1 in 5 of the 40,246 AGENTS.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
Working with coding agents
This repository is set up so coding agents prove their work before they call it done. This page explains the parts, what each one guarantees, and where it stops. It grows as each part lands.
Layers
- Git hooks run for everyone, people and agents alike, on every commit and push.
npm run verifyruns every CI check locally and records a pass.AGENTS.mdtells agents what done means. Claude Code reads it throughCLAUDE.md; Codex and other agents read it directly.- Skills in
.agents/skills/give step-by-step procedures for recurring work. They load only when a task needs them. - Review: fresh reviewers and Doistbot check a finished change against the checks in
.agents/checks/. - The end-of-turn gate sends an agent back once when its turn changed code that
npm run verifyhas not passed.
Rules and what holds them
| Rule | Held by | Certainty |
No commits on main | pre-commit and commit-msg → .agents/scripts/branch-guard.sh | Enforced locally; --no-verify skips it |
No pushes to main | pre-push → branch-guard.sh; the "Protect default branch" ruleset | Enforced on GitHub |
| Committed files are linted and formatted | pre-commit (lefthook, staged files only); CI | Enforced in CI |
| Type check passes before a commit | pre-commit; CI | Enforced in CI |
| Tests pass before a push | pre-push; CI | Enforced in CI |
| Agent tooling never ships in the npm package | .agents/scripts/package-contents.test.ts | Enforced in CI |
npm run verify runs what CI runs | .agents/scripts/lib/steps.test.ts compares it with the pull-request workflows | Enforced in CI |
Run npm run verify before calling work done | AGENTS.md | Enforced at turn end (once); AGENTS.md |
| Never weaken a check to pass it | AGENTS.md | Asked |
| Instructions name only files, links and scripts that exist | .agents/scripts/instructions.test.ts | Enforced in CI |
| Every skill says when to use it and is linked for Claude Code | .agents/scripts/instructions.test.ts | Enforced in CI |
| Use the matching skill for each task | AGENTS.md | Asked |
| Tests meet the quality bar | .agents/skills/write-tests/references/test-rules.md | Asked |
| Review checks say what to report and what is fine, and AGENTS.md links them | .agents/scripts/instructions.test.ts | Enforced in CI |
| A finished change gets a task review, a blast-radius review when shared code changed, and a code review | review-changes skill | Asked |
| Reviewers never edit the shared checkout | the reviewer brief; npm run review:worktree gives them their own | Asked |
"Enforced in CI" means a pull request cannot merge without it. A local hook alone is a speed bump: git commit --no-verify skips it.
Git hooks
Lefthook runs the hooks, configured in lefthook.yml. npm install installs them into .git/hooks through the prepare script (lefthook install --reset-hooks-path) on every install, so a hook added to lefthook.yml reaches existing clones. Lefthook's own postinstall runs only when lefthook itself is installed, and lefthook refuses to install or sync hooks while core.hooksPath is set locally; the reset removes that setting.
pre-commitrefuses a commit onmain, then lints and formats the staged files (re-staging what the tools fix) and runs the type check. Unstaged changes in a partly staged file are left alone.commit-msgrefuses a commit onmainagain. Lefthook runs no pre-commit job when nothing is staged, so without it an empty commit would get past.pre-pushrefuses a push tomain, then runs the tests. It checksgit push --dry-runtoo.- If node is not on
PATH(some GUI git clients), both hooks still apply the branch rule, skip the checks, and say so. CI runs the same checks. - The default branch is read from
origin/HEAD, falling back tomain. Pushes tonext, the pre-release branch, are allowed: syncing it withmainis a direct push (seeCONTRIBUTING.md).
The release workflow commits and pushes to main from GitHub Actions, and npm ci runs prepare there, which installs the hooks. The release step sets LEFTHOOK=0, so semantic-release's commit and push run no hooks, as before: every check already ran on the pull request. The guard also skips itself when GITHUB_ACTIONS=true, in case another workflow commits with the hooks on. That is the one exemption. Branch protection on GitHub is the real enforcement; the hooks only catch mistakes early.
npm run verify
One command, every check the pull-request workflows run (lint.yml, test.yml, check-skill-sync.yml): lint, format, both type checks, all tests, the build and the skill sync check. It takes about fifteen seconds, so it always checks the whole tree. Picking only the affected tests would save a few seconds and add a way to miss something.
- Before the checks it stops if Node is older than
package.jsonrequires, or ifnode_modulesdoes not match the lockfile, transitive dependencies included (npm's own record of what it installed,node_modules/.package-lock.json, againstpackage-lock.json). Both make checks fail, or pass, for reasons unrelated to your change. - Only docs changed since the branch left
main: it says there is nothing to check. The agent instructions (AGENTS.md,CODEBASE.md,docs/agents.md,.agents/) count as code, since a test checks every reference in them, and so does the generatedskills/todoist-cli/SKILL.md, whichnpm run check:skill-syncreads. Deleting or renaming a doc counts as code too, since the agent docs may name it; adding one after a pass means running verify again. - Only CI files changed (Renovate,
.github/config other than workflows): it says no local check reads them. Every workflow counts as code, since a local test compares verify with the three above and checks no other workflow runs commands on pull requests. - Order: lint, format and both type checks start together. The tests wait for the build, because
src/index.integration.test.tsrebuildsdist/and runs it; the skill sync check imports fromdist/, so it waits for the tests. CI=trueis set for the build, the tests and the skill sync check, as GitHub Actions sets it. Variables that change what the CLI does (TD_ACCESSIBLE,TD_SPINNER,TD_USER,TD_VERBOSE,TODOIST_API_TOKEN,FORCE_COLOR) are removed, with a note, since CI never sets them. A real token never reaches the tests.- Output: one line per step. A failing step prints the last 30 lines of its log and the log's path under
.tmp/agents/logs/. - The record: after a full pass it writes a content hash of every file local checks read (not docs, not CI-only files) to
.tmp/agents/verified.json. A run that fails, or sees files change while it runs, deletes it. The end-of-turn gate reads it. - Platforms: macOS, Linux and WSL. Like the git hooks, it assumes a POSIX shell and runs npm's POSIX launchers in
node_modules/.bin; on native Windows, run it in WSL. - What it cannot see: the real Todoist API, output in a real terminal, the other Node version CI tests on, and the
gh skill publish --dry-runspec check. It prints these after every pass.
If a pull-request workflow gains a step, .agents/scripts/lib/steps.test.ts fails until verify gains it too, or says why it can't.
The end-of-turn gate
Every agent's hooks call one script, .agents/scripts/guard.ts, so the agents can't drift apart.
- Turn start (you send a message): it hashes every file in the checkout, untracked ones included, into
.tmp/agents/turns/. - Turn end (the agent stops): it compares the tree with the turn's start and with the last
npm run verifypass. If the turn changed code and verify has not passed on exactly this tree, it sends the agent back once, naming the files. The second time, it lets the agent stop and shows you a note. - Passes without a word: turns that changed nothing, changed only docs, or undid their edits.
- "Once" is deliberate: an agent can stop on purpose and say what is unfinished.
| Agent | Wiring | Known limits |
| Claude Code | .claude/settings.json: UserPromptSubmit and Stop | hook-wiring.test.ts runs the exact commands. Not yet seen sending a real session back. |
| Codex | .codex/hooks.json: UserPromptSubmit and Stop | Runs only in a trusted project, after each person reviews the hooks in /hooks. Some versions run no project hooks in a linked worktree. Not yet tried here. |
| Cursor | Runs the Claude Code hooks itself; the command falls back to the git root if Cursor does not set CLAUDE_PROJECT_DIR | No Cursor hooks file: a second one would run every hook twice. A test keeps it out. Not yet tried here. |
Other limits:
- Two sessions in one checkout share its files, so each may be told about the other's edits. Use a worktree per session.
- If the branch's code is back to what
mainhas (an undo, a stash, a switch), the turn passes: CI already checked that code.
Fails open, visibly: a broken guard, bad input or a checkout without the script lets the turn end and says why in a message. DISABLE_AGENT_GUARD=1 in the agent's environment turns the gate off for you.
Skills
Each skill is a short procedure with a check at the end of every step. Agents load one when the task matches its description.
| Skill | For | Ends with |
verify | Any change about to be called done | npm run verify, proof matched to the change, an evidence report |
fix-bug | Something broken | A test that failed on the symptom, the root cause, undo/restore |
build-feature | A new command, flag or behaviour | Decisions settled up front, test-first slices, a "touches X" map |
add-command | A new command or subcommand | Every file a command touches: mocks, spinner text, permissions, skill text |
refactor | Structure changes, behaviour unchanged | Pinned tests, no changed test inputs or assertions |
write-tests | Writing or changing tests | Tests that failed once, meeting the test rules |
learn | A correction from you | A type, lint rule, test or script check that catches the mistake |
review-changes | A change you say is ready | Task, blast-radius and code reviews; each finding fixed, dismissed with evidence or left open |
The evidence report rates each claim: said, cited (a line), traced (followed the code), ran (a test or command executed it), seen (in the running CLI). "Fixed" or "safe" needs ran. Anything less is listed under "Not verified".
The test rules were measured against this repository's tests first, so they accept the patterns that are fine here: .not.toHaveBeenCalled() for dry runs and confirmation gates, toBeDefined() as a guard, and the vi.mock of src/lib/api/core.ts every command test uses. There are no snapshot tests, and the rules keep it that way.
Review
When you say a change is ready, the agent runs review-changes:
- Task review: a fresh subagent gets your request in your words, what done means and the decisions made, never the author's reasoning. It looks for what is missing or unrequested. Doistbot can't do this, since it never sees the task.
- Blast-radius review: only when shared code changed (
src/lib/, registration and global flags insrc/index.ts, the cli-core or SDK version, dependencies, build config). It names the one fact that makes the change safe and proves it by running code. - Code review: Doistbot locally (
doistbot review --json, or--base <branch below>in a stack) when version 1.0.12+ is installed anddoistbot doctorpasses. Otherwise one subagent per check in.agents/checks/. Doistbot readsAGENTS.mdand its links, so the checks apply to its pull request reviews too.
Findings are fixed, dismissed with evidence (a line or a test run, never an argument), or left open for you. Reviewers that need to run code get their own worktree from npm run review:worktree -- <dir>; the shared one stays read-only.
The checks cover tests, architecture, security, privacy, the command surface and evidence. The command surface check holds the rules people and agents rely on: command, flag and --json names, SKILL_CONTENT, and output that reads in --accessible mode. Two kinds Doist uses elsewhere don't apply here: there are no translations and no database migrations.
A review costs several agent runs, so it runs when you ask, not on every change. A Doistbot run with a failed provider (providerFailures) is partial; the agent should say so.
How to ask an agent
Give it:
- the goal, in a sentence;
- a done check that can pass or fail ("
td task list --label x --jsonreturns only tasks labelled x"); - the proof you want (a failing test first, a real
node dist/index.jsrun on a test account); - facts you know and real constraints (a deadline, a flag that must not change).
Leave out the method and your theory of the cause. For a vague report, ask the agent to restate it first. Start a new session for unrelated work.
| To start… | Say |
| a bug fix | "Fix: <symptom>. Command: <td …>. Expected: <x>." (fix-bug) |
| a feature | "Add <behaviour>. Done when <check>." (build-feature) |
| a refactor | "Refactor <area> so <what gets easier>." (refactor) |
| a review | "Ready for a PR." (review-changes) |
| a release | "Ready to release." Check the PR title type: feat:, fix:, perf: and breaking changes publish; others do not. |
Your part: answer the decision questions, and read the evidence report, especially "Not verified".
Where the tooling lives
.agents/scripts/holds scripts and their tests, next to the skills and review checks. It is not underscripts/becausescripts/ships in the npm package.- Skills live in
.agents/skills/, where Codex reads them. Claude Code reads them through the symlinks in.claude/skills/; a test keeps the two in step. - The tooling runs straight from TypeScript with Node's type stripping, so it only uses erasable syntax (
.agents/scripts/tsconfig.jsonenforces this).npm run type-checkcovers it, andnpm run checklints it. - Tests run in
npm testand CI. They copy the tools into throwaway git repositories and never touch your checkout. A test helper clears the variables git exports to hooks, sincenpm testitself runs insidepre-push.
Releases
semantic-release publishes from main on every merge, and a pre-release from next. The squash-merged PR title decides whether anything ships: feat:, fix:, perf: and any breaking change (!) release; chore:, ci:, docs:, test: and refactor: do not. Changes to the agent tooling use chore: or ci:, so they never publish a new version. No path filter is needed.
What fails open
- No node on
PATHin a GUI client: the hooks skip lint, the type check and the tests, with a message. The branch rule still applies. git commit --no-verify: every local hook is skipped. CI and branch protection still apply.- The end-of-turn gate: any error lets the turn end, with a message saying why.
DISABLE_AGENT_GUARD=1turns it off. - Worktrees share the hooks in
.git/hooks, so the branch rule works in a new worktree at once. Lint, format, types and tests neednpm ciin that worktree first. Without the lefthook binary anywhere the hooks look, they print "Can't find lefthook in PATH" and let the commit through.
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.

