Keep the TODO comments in your codebase in sync with your issue tracker.
tudu scans source files for structured TODO(...) comments, validates the
referenced issues against your tracker (Notion, GitHub, GitLab, Jira, or
Linear), and optionally reconciles them bidirectionally, creating issues from
code, updating tracker state, and producing safe, reviewable patches.
- Validate by default.
tudu scanis read-only and exits non-zero on closed, unknown, or malformed tracked TODOs, so it drops straight into CI. - Opt-in two-way sync. Flip to
tudu syncand nothing is written without--apply; by default you get unified diffs under.tudu/patches/. - 50+ languages. The language registry covers
//,#,--,;,%,(* *),{- -},""" """,<!-- -->,=begin/=end, and more. - Stable anchors. Each TODO is anchored by an xxhash of its surrounding context, so it survives line shifts and renames across runs.
- No surprises. Legacy
TODO TASK-123:and plainTODO:comments are recognized but never rewritten unless you ask.
Status: work in progress / learning project. The scanner, parser, providers, sync engine, and CLI are functional; the tool is not yet packaged for Homebrew or containers.
- Install
- Quickstart
- Comment syntax
- Configuration
- Providers
- Commands
- CI examples
- Performance
- Testing
- Architecture
- License
From source (a Rust toolchain is required):
git clone https://github.com/michaelfromorg/tudu
cd tudu/tudu
cargo install --path .tudu is a single static binary. Put it on your PATH and you're done.
Run from the checkout without installing:
cargo run -- scan .# 1. From a repository root, list every TODO/FIXME comment.
tudu scan
# 2. Add a .tudu.yaml (see Configuration) and validate tracked TODOs.
tudu scan --verbose
# 3. Plan a sync without touching anything.
tudu sync --dry-run
# 4. Apply the planned edits and tracker updates.
tudu sync --applyscan exits 0 when everything is clean and non-zero when it finds malformed
TODOs or tracked references that are closed/unknown, so it works as a CI gate.
TODO(<ref>[, attributes...]): human-friendly comment
| Form | Meaning | Example |
|---|---|---|
gh:org/repo#1234 | GitHub qualified | TODO(gh:org/repo#1234): |
gl:group/proj#77 | GitLab qualified | TODO(gl:group/proj#77): |
jira:PROJ-456 | Jira qualified | TODO(jira:PROJ-456): |
nt:PAGE_ID | Notion page qualified | TODO(nt:abc123): |
lin:LIN-123 | Linear qualified | TODO(lin:LIN-123): |
TASK-123 | Shorthand (routed by prefix/config) | TODO(TASK-123): |
#1234 | Project-local shorthand (via origin) | TODO(#1234): |
new / new="title" | File a new issue | TODO(new="Add retry"): ... |
| (none) | Untracked (reported, never synced) | TODO: fix this |
(person) | Untracked (not a valid ID) | TODO(alice): review |
Attributes are comma-separated inside the parentheses.
| Attribute | Form | Example |
|---|---|---|
bidir / one_way | flag | TODO(TASK-1, bidir): |
labels | comma list | labels=perf,cleanup |
assignee | text | assignee=@alice |
due | date (YYYY-MM-DD) | due=2025-09-01 |
close_on_delete | flag or true/false | close_on_delete=true |
status | text (quoted ok) | status="In Progress" |
section | text (quoted ok) | section="parser" |
db | Notion database alias | db=tasks |
prop.<Name> | provider property override | prop.priority=high |
Quoted values may contain commas and spaces: section="a,b,c" is a single text
value, not a list.
TODO TASK-123:(no parens, space-separated) is recognized as tracked.FIXMEis treated identically toTODOfor every form above.- Plain
TODO:andTODO(person):are untracked, reported but never synced. - Markers match case-insensitively (
todo,Todo,TODOall work); ID prefixes are uppercase by default (scan.match_case_insensitiverelaxes this).
// Rust// TODO(TASK-1234, bidir, labels=parser): rewrite tokenizer for streaming// TODO: optimize this loop // Untracked — won't sync# Python# TODO(TASK-9, due=2025-10-01): account for leap seconds in scheduler<!-- TODO(TASK-333): HTML style comment -->tudu looks for .tudu.yaml at the base path (the scanned directory, or
--basepath). It is optional: without it, scan reports TODOs without
validating them. A commented example lives at tudu/.tudu.yaml.
origin: github.com/org/repoissue_tracker: notion # notion | github | gitlab | jira | linearmode: validate # validate | syncscan:
ignore:
- target/
- vendor/**include:
- "**/*"match_case_insensitive: falseproviders:
notion:
type: notiondatabases:
tasks:
database_id: "25dc4188fa0e806c9dd1f4e7327751c5"prefix: TASKstatus_property: Statuslocation_property: Locationdone_statuses: ["Done", "Resolved"]default_database: taskssync:
default_direction: one_way # one_way | bidircreate_on_missing: falseclose_on_todo_delete: ask # ask | never | alwaysupdate_from: none # none | issue | commentclosed_issue_action: annotate # annotate | removepatch_dir: .tudu/patchesstate_file: .tudu/state.jsonanchor_context_lines: 3auth:
type: apitokentokens_cache: ~/.tudu/authtokens.yamloutput:
format: standard # standard | jsonverbose: falseTokens are resolved in this order for each provider:
- The
token:field in.tudu.yaml(avoid committing secrets). - Provider-specific environment variables (recommended).
TUDU_AUTH_TOKEN(a single override for CI).
| Provider | Environment variable(s) |
|---|---|
| Notion | NOTION_TOKEN |
| GitHub | GITHUB_TOKEN |
| GitLab | GITLAB_TOKEN |
| Jira | JIRA_API_TOKEN (+ JIRA_EMAIL, JIRA_SERVER) |
| Linear | LINEAR_API_KEY |
The token cache at ~/.tudu/authtokens.yaml is created with 0600 permissions.
.gitignoreis respected via theignorecrate..tuduignoreadds project-local ignore rules.scan.ignoreglobs in.tudu.yamlapply on top of both.scan.includeis an optional allowlist; when non-empty, only matching files are scanned (ignored files stay excluded even if they match an include).
Notion supports any number of databases, each with its own prefix and property mapping. TODOs are routed to the correct database by prefix match.
providers:
notion:
type: notiondatabases:
tasks:
database_id: "aaa..."prefix: TASKid_property: IDtitle_property: Namestatus_property: Statuslabels_property: Labelsassignee_property: Assigneeassignee_property_type: people # people | rich_textdue_property: Duedone_statuses: ["Done", "Resolved"]bugs:
database_id: "bbb..."prefix: BUGstatus_property: Statedone_statuses: ["Fixed", "Won't Fix"]default_database: taskstudu file --db tasksfiles untracked TODOs into thetasksdatabase and rewrites them with the new ID.prop.<Name>=valueattributes map to arbitrary Notion properties.
providers:
github:
type: githubowner: orgrepo: repoReference forms: gh:org/repo#1234, #1234 (via origin).
providers:
gitlab:
type: gitlabproject: group/proj # numeric ID or "group/project"Reference forms: gl:group/proj#77, #77 (via origin).
providers:
jira:
type: jiraserver: "https://example.atlassian.net"project: PROJAuth uses basic auth with JIRA_EMAIL + JIRA_API_TOKEN. Reference forms:
jira:PROJ-456, PROJ-456 (shorthand via issue_tracker: jira).
providers:
linear:
type: linearteam: ENGReference forms: lin:LIN-123, LIN-123 (shorthand via issue_tracker: linear).
tudu scan [PATH] [--format json|standard] [--verbose] [--config FILE] [--basepath DIR]
tudu sync [PATH] [--dry-run] [--apply] [--update-from SOURCE] [--close-on-todo-delete POLICY] [flags]
tudu file [PATH] [--db NAME] [--interactive] [--apply] [flags]
tudu link --id REF --file PATH --line N -- "comment text"
tudu status [PATH] [--since DATE] [flags]
Common flags (available on most subcommands): --config FILE,
--basepath DIR, --format json|standard, --verbose. Availability is
noted per command below; for example --format applies to scan, sync,
file, and status, while --verbose is offered by scan and sync.
Lists every TODO/FIXME comment (tracked and untracked) and validates tracked references against configured providers. Exits non-zero on malformed TODOs or closed/unknown tracked references.
tudu scan src/
tudu scan --format json | jq .
tudu scan --verboseReconciles tracked TODOs with the tracker. --dry-run (the default) plans
changes and writes unified diffs to .tudu/patches/; --apply edits files in
place and posts tracker updates.
tudu sync --dry-run
tudu sync --apply
tudu sync --update-from comment --close-on-todo-delete alwaysFiles untracked TODO: comments as issues and rewrites them with the new ID.
Batch mode is the default; --interactive prompts per TODO. --apply is
required to create issues and edit files.
tudu file --db tasks --apply
tudu file --interactiveAdd or update a tracked TODO at a specific file/line location.
tudu link --id TASK-321 --file src/lib.rs --line 120 -- "Refactor allocator"Report drift between TODOs and the tracker since a date (read-only).
tudu status --since 2025-08-01
tudu status --format jsonStandard (human-readable):
ERROR: Malformed TODO
src/main.ts:17: // TODO(): missing ref; expected TODO(<ref>):
ERROR: Issue doesn't exist
src/parser.rs:42: // TODO(gh:org/repo#999999): …
JSON (machine-readable, stdout only):
[
{"type":"Malformed TODO","filename":"src/main.ts","line":17,"message":"Expected TODO(<ref>):"},
{"type":"Issue doesn't exist","filename":"src/parser.rs","line":42,"metadata":{"ref":"gh:org/repo#999999"}}
]In JSON mode, stdout carries only the JSON array; diagnostics go to stderr.
name: tuduon: [push, pull_request]jobs:
validate:
runs-on: ubuntu-lateststeps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@stable
- run: cargo install --path tudu
- name: Validate tracked TODOsenv:
NOTION_TOKEN: ${{ secrets.NOTION_TOKEN }}GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}run: tudu scan --format json | tee tudu-report.json
- name: Plan sync (no writes)run: tudu sync --dry-run || true
- name: Upload patches and reportif: always()uses: actions/upload-artifact@v4with:
name: tudu-artifactspath: | tudu-report.json .tudu/patches/*.difftudu:validate:
image: rust:latestscript:
- cargo install --path tudu
- tudu scan --format json > tudu-report.json
- tudu sync --dry-run || trueartifacts:
when: alwayspaths:
- tudu-report.json
- .tudu/patches/0— clean scan, no malformed TODOs and no invalid tracked references.- non-zero — malformed TODOs, closed/unknown tracked references, or a runtime error (missing path, bad config, provider failure).
The scanner walks files in parallel with the ignore crate and reads each file
once. Targets from the PRD, verified by tests/benchmarks.rs:
| Scenario | Target | Measured |
|---|---|---|
| Cold scan, ~100k LOC | < 60s | well under 1s on commodity hardware |
| Repeated/incremental scan | < 10s | well under 1s |
Run the benchmarks yourself:
cargo test --test benchmarks -- --nocapturecargo test# full suite (unit, integration, property, snapshot)
cargo clippy --all-targets -- -D warnings
cargo fmt -- --checkThe suite is fully deterministic and environment-independent:
- Provider tests use
wiremockHTTP mocks matching real API shapes; no real tokens or network calls are required. - CLI tests use
assert_cmdandinstasnapshots. - Parser edge cases are fuzzed with
proptest. - No test reads or writes outside its temp directory.
tudu/src/
├── main.rs # Entry point, tracing setup, delegates to cli
├── cli.rs # clap subcommands (scan, sync, file, link, status)
├── config.rs # .tudu.yaml parsing
├── error.rs # TuduError (thiserror)
├── output.rs # Output formatting (standard + JSON)
├── scanner/ # File walker, 50+ language registry, comment extraction
├── parser/ # TODO grammar, references, attributes
├── anchor.rs # Context hashing (xxhash), relocation heuristics
├── providers/ # IssueProvider trait + Notion, GitHub, GitLab, Jira, Linear
├── reconciler.rs # Drift detection, conflict resolution, action plan
├── writer.rs # Unified diff generation, in-place editing
├── state.rs # .tudu/state.json management
├── sync.rs # sync command orchestration
├── link.rs # link command orchestration
└── status.rs # status command orchestration
Data flow:
Scan files → Extract comments → Parse TODOs → Validate against providers
→ Reconcile (compute plan) → Write (patches or in-place) → Persist state
See docs/PRD.md and docs/RFC.md for the full
design and tudu/src/lib.rs for the public API surface.
MIT — see LICENSE.