Settled arguments
Sprig is opinionated on purpose. Each rule below was argued both ways before it went in, and the arguments that lost are kept so nobody has to have them again. To reopen one, propose a change with the case for, the case against and a ruling.
Groups start with ’# ’, not a trailing colon
Section titled “Groups start with ’# ’, not a trailing colon”Context. Draft 0.1 used TaskPaper’s Name: for groups.
Options. The colon reads naturally, but any note ending in a colon, such as ‘Two options:’, silently became a heading and captured the lines under it. A mark matches every other line start.
Decision. Groups start with # : one character, then a space. Markdown viewers show it as a heading too.
Revisit when. Never. A rule that fires by accident stays a bad rule.
Recorded as cairn item 0028.
The title line is the root item; there is no header block
Section titled “The title line is the root item; there is no header block”Context. Draft 0.1 had key: value header lines under the title.
Options. Front matter is familiar and easy to scan, but it was a second syntax for fields, with different spacing rules from key:value in items.
Decision. The first line is the root item and takes the same tokens as any item. Values with spaces are quoted.
Revisit when. If files routinely need more than three file-level fields and title lines become unreadable.
Recorded as cairn item 0029.
Dates are stored as ISO; relative words are typing shortcuts
Section titled “Dates are stored as ISO; relative words are typing shortcuts”Context. due:fri is quick to type, but a week later the same file means something else.
Options. Allow relative dates (fast, but the file’s meaning changes with the day it’s read) or require ISO (unambiguous, slower to type).
Decision. Store ISO. Editors and sprig fmt expand fri, +2w, oct3 and tomorrow when you leave the line. Parsers still read them against today as a courtesy. The 0.1 special case where due:oct 3 swallowed the next word is removed.
Revisit when. If editors fail to expand shortcuts in practice and plain-text edits leave relative dates behind.
Recorded as cairn item 0030.
A question ends with an ’= ’ answer line
Section titled “A question ends with an ’= ’ answer line”Context. Ticking a ? with x discards the answer, which is often the most valuable line in a plan.
Options. Reuse x (nothing to learn) or add an answer line (one more line start).
Decision. = records an answer or decision. A ? with an answer is done and unblocks anything waiting on it. grep '^ *= ' is the decision log.
Revisit when. Never; this is the format’s decision log.
Recorded as cairn item 0031.
A parent keeps its written status
Section titled “A parent keeps its written status”Context. Computing a parent’s status from its children saves a tick.
Options. Computed status means the file can say - while the view says done, and the text stops being the truth.
Decision. The file is the source of truth. The resolved layer flags a parent whose children are all done as ready to close. Groups (#) are the construct for computed status.
Revisit when. If users consistently forget to close parents even with the hint shown.
Recorded as cairn item 0032.
Grafts can mount one branch with [[file^id]]
Section titled “Grafts can mount one branch with [[file^id]]”Context. Whole-file grafts only is simpler, but a person’s week or a release is naturally built from parts of several plans.
Options. Files only (one owner, simple) or branches too (flexible, and a branch grafted twice counts twice).
Decision. + [[file^id]] mounts that branch. Ticking a grafted item edits the original line. Grafting the same branch twice into one tree is the author’s error.
Revisit when. If double counting causes real confusion; then sprig check should warn on it.
Recorded as cairn item 0033.
Ticking a recurring item moves its due date
Section titled “Ticking a recurring item moves its due date”Context. Draft 0.1 had every: with no defined behaviour.
Options. Append a history line per completion (history, but plans grow into diaries) or roll due: forward (the file says what’s next; version control keeps the past).
Decision. Ticking an item with every: moves due: to the next occurrence and leaves it open. Recurring items are upkeep and don’t count toward progress.
Revisit when. If people need completion history inside the file rather than in git.
Recorded as cairn item 0034.
Indentation is relative
Section titled “Indentation is relative”Context. Exactly two spaces per level makes files uniform, but rejecting a file over a stray space is hostile, and pasted text arrives indented every way.
Options. Strict two spaces, or relative (deeper than the line above is a child).
Decision. Relative. Formatters write two spaces; parsers accept any consistent indentation.
Revisit when. Never.
Recorded as cairn item 0035.
Anchors are explicit, never generated from text
Section titled “Anchors are explicit, never generated from text”Context. Generated ids save typing.
Options. Rewording an item would silently break every link to it.
Decision. ^anchors are written by hand, and only items that something points at need one.
Revisit when. Never.
Recorded as cairn item 0036.
’‘x’’ means done
Section titled “’‘x’’ means done”Context. Every other mark is punctuation, and a note starting with ’x ’ becomes an item.
Options. A symbol for consistency, or x because people tick paper that way and todo.txt proved it in plain text.
Decision. x stays. A note that really starts with ’x ’ is escaped with a backslash.
Revisit when. Never.
Recorded as cairn item 0037.
No numbered steps for sequences
Section titled “No numbered steps for sequences”Context. 1. 2. 3. is familiar and could chain dependencies automatically.
Options. A step still needs a status, so the number competes with the mark for the start of the line. Real work is rarely strictly serial.
Decision. Rejected. Write after: where the order is real.
Revisit when. If after: chains of more than five siblings become common in real plans.
Recorded as cairn item 0038.
No Markdown checkboxes in the parser
Section titled “No Markdown checkboxes in the parser”Context. Millions of - [ ] lists already exist.
Options. Accepting them means two spellings for every status, in every tool, forever.
Decision. Rejected. sprig import converts them once.
Revisit when. Never; converting is a one-time job.
Recorded as cairn item 0039.
One reference implementation, compiled everywhere
Section titled “One reference implementation, compiled everywhere”Context. Markdown’s decade of dialects came from a thin spec and many parsers that disagreed. See design/ecosystem-plan.html, section ‘What to build’.
Options. Several native implementations (more contributors, and divergence) or one Rust core used by every official tool (no drift, one bottleneck).
Decision. One Rust core. The CLI, the language server, the MCP server and later the WebAssembly build all use it. Independent parsers are welcome and must pass the conformance suite.
Revisit when. When a second, independent parser passes the suite; then this is a preference, not a necessity.
Recorded as cairn item 0040.
Win developers working with agents first
Section titled “Win developers working with agents first”Context. A format can’t win everyone at once. See design/ecosystem-plan.html, section ‘Start narrow’.
Options. Start with consumers (a grocery app: huge audience, brutal distribution) or with developers who work with agents (plain text and git already, and an urgent need for a legible plan format).
Decision. Developers working with agents first. The phone app, hosted sync and tracker replacement wait until after 1.0.
Revisit when. If agent tooling standardises on a different plan format before v0.3 ships.
Recorded as cairn item 0041.
The CLI has no config file, daemon, plugins or network
Section titled “The CLI has no config file, daemon, plugins or network”Context. Every feature request for a command-line tool pulls toward a config file, a background index, a plugin API or a sync service. Each one makes Sprig something to install, configure and trust instead of a tool to pick up. The plan is design/cli-plan.html.
Options. A config file lets people change defaults, but then the same command prints different things on different machines, and bug reports stop being reproducible. A daemon or index makes large workspaces faster, but it can go stale, and parsing a hundred files well inside the performance budget (0056) is fast enough. Plugins add reach, but --plain and --json output plus meaningful exit codes already let any tool on the machine extend Sprig through a pipe.
Decision. The sprig binary reads flags and environment variables only: NO_COLOR, SPRIG_TODAY for tests, $EDITOR, $PAGER. It keeps no state between runs, starts no background process, loads no plugins and opens no network connection. It prompts only when stdin and stdout are both terminals. Output has three forms: readable on a terminal, stable tab-separated --plain with the ref first, and --json in the tree schema.
Revisit when. A real workspace exceeds the performance budget with the core already optimised, and the profile shows parsing, not rendering, is the cost.
Recorded as cairn item 0117.
One deadline field: due
Section titled “One deadline field: due”Context. Draft 0.3 gave due and target the same meaning, “when it should be finished” (§9.1). The only use of target: was a project’s title line, where due: works the same way and already flows down into ranking.
Options. Keeping both lets a plan separate a hard deadline from an aspiration, but nothing in the format or the tools treated them differently, so the difference lived only in the writer’s head, and every implementation had to parse a second date key for nothing. Dropping target leaves one word to learn and one field to compute with; anyone who wants the distinction can still write target: as an ordinary field (§4.6), shown but not computed.
Decision. due and start are the only date fields (§9.1). target: is an ordinary field.
Revisit when. Someone shows a computation that needs two kinds of deadline, not just two labels.
Recorded as cairn item 0138.
A start date after today blocks
Section titled “A start date after today blocks”Context. Draft 0.3 defined start: as “when work can begin” and then gave it no effect: nothing computed from it, so writing it changed nothing any tool showed.
Options. Dropping start is the smallest format. Giving it a meaning costs one sentence, and “not before this day” is one of the most common planning needs, from winter tyres to a permit that can’t be filed until a survey is back. Adding a new computed state (“deferred”) would give it meaning but add a concept; folding it into blocked adds none, because blocked already means “can’t be started now”, and the date simply becomes one more thing an item can wait on.
Decision. An open item whose start: is after today is blocked until that day, and its open descendants with it (§7.3, §7.5). Tools say it waits until the date.
Revisit when. Real plans show people using start: as a record of when work did begin, rather than when it may.
Recorded as cairn item 0139.
Grafted content appears once in a tree
Section titled “Grafted content appears once in a tree”Context. Decision 0033 kept branch grafts and left one case to the author: “grafting the same branch twice into one tree is the author’s error”, with double counting as the known cost. It also left open whether [[file^anchor]] can reach an anchor that file grafted in from elsewhere (Appendix B.7).
Options. Warning about duplicates keeps permissive files but lets progress lie until someone reads the warning. Counting a repeated item once hides the mistake while still showing it twice. Refusing the later graft, as a loop is refused (§8.4), keeps every tree a tree: each item has one place, every total is honest, and the rule is the same shape as one readers already know. For B.7, reaching through another file’s grafts would make an anchor’s meaning depend on files its own file doesn’t write, which is exactly what §8.7 exists to prevent.
Decision. Within one tree an item appears at most once; a graft that would repeat content is an error on the later graft line in document order, with no grafted children (§8.8). [[file^anchor]] reaches only anchors that file writes itself (§8.7).
Revisit when. A real workspace needs the same branch in two places of one tree and can’t be restructured into two trees.
Recorded as cairn item 0140.
A dropped item holds nothing up
Section titled “A dropped item holds nothing up”Context. §5.7 counted only done as finished, so an after: naming a dropped item waited forever (graph/023). Dropping something is a decision that it will not happen, and the only way to unblock the item waiting on it was to edit its after: by hand.
Options. Keep dropped unfinished: a dependency that was decided against is a reason to rethink what depended on it, and blocking forces that rethink. But it forces it silently, as a blocker that can never clear, which is how plans rot. Count dropped as finished: the waiting item becomes ready, and the reference still records what it once waited on. Taskwarrior stops a deleted task blocking its dependants, OmniFocus treats dropped actions as complete for availability in sequential projects, and Jira’s “is blocked by” no longer blocks once the blocker is resolved as Won’t Do. Later (>) is different: it is put off, not decided against, so it still holds things up.
Decision. A node is finished for references when it is done or dropped (§5.7). Later and empty nodes are not finished.
Revisit when. Real plans show people relying on a dropped prerequisite to hold its dependants, and missing that it no longer does.
Recorded as cairn item 0141.
A recurring item with children is a checklist
Section titled “A recurring item with children is a checklist”Context. §5.3 said no item with every: counts toward progress. The reference parser excluded only recurring leaves, so a recurring parent’s children still counted, and examples/bakery/groceries.sprig (a weekly supply run on the title) depends on that (Appendix B.6).
Options. Excluding the whole branch makes every recurring list read as empty, with nothing to show for a week’s shopping. Counting the children treats the recurring item as what it is: a checklist done again each time, whose progress is this round’s. A recurring leaf is still upkeep, since there is nothing in it to count but itself.
Decision. Only a recurring leaf is upkeep. A recurring item with children is a parent like any other, and its children count (§5.3).
Revisit when. A tool needs to reset a recurring checklist’s children when it is ticked; that would be a new rule, not a change to this one.
Recorded as cairn item 0142.
Empty is never finished
Section titled “Empty is never finished”Context. An empty group, graft or file has derived status empty (§5.5). Appendix B.5 asked whether an after: naming one should wait forever.
Options. Counting empty as finished, as vacuous truth would, unblocks after:[[launch]] the moment launch.sprig is created as a stub, before anything in it is done. That is a false green. Keeping empty unfinished is honest: nothing has been finished. Its risk is a silent wait on a placeholder, which a warning removes.
Decision. Empty is never finished (§5.5, §5.7). A reference to an empty node is reported as warning S0206, so the wait is visible.
Revisit when. Plans commonly reference placeholders on purpose and find the warning noise.
Recorded as cairn item 0143.
Loops are found once for the whole workspace
Section titled “Loops are found once for the whole workspace”Context. §8.4 refuses a graft that would loop, and said only that “one of the two graft lines reports a loop”. In the reference parser which line reported it depended on which file was read first (Appendix B.10).
Options. Per-tree detection reports the line that closes the loop below whichever file is opened, so a shows b and b shows a. The cost is that the same workspace gives different diagnostics depending on where a tool starts, and a file’s expansion depends on who grafts it, so it can’t be computed once. Workspace-wide detection walks files in path order and grafts in document order, and refuses the one graft that closes each loop everywhere. Every file’s expansion is then context-free and every tool reports the same line. The cost is that, in a two-file loop, the file later in path order loses its graft in its own tree too: a.sprig shows b.sprig, but b.sprig does not show a.sprig.
Decision. Loops are found once for the whole workspace, in path order, and the closing graft is refused in every tree (§8.4).
Revisit when. People find the path-order rule surprising in practice, and a per-tree rule can be made to report one stable line.
Recorded as cairn item 0144.
Dates only: no times of day
Section titled “Dates only: no times of day”Context. Spike 0068 asked whether due: and start: should take a time of day. RFC 0001 (rfcs/0001-dates-only.md) records the argument.
Options. A time would let a plan say when in a day something happens. It would also need a time zone to mean one instant, because the same file is read in different places, and that is a calendar’s job. todo.txt never added times. TaskPaper allows HH:MM but stores no zone, so its times are ambiguous between machines. None of the example plans needs one.
Decision. Dates only. A plan’s grain is the day. A value such as 2027-03-01T14:00 is unreadable (§9.2, §9.5): kept as text, warned about with S0001, and never misread.
Revisit when. Plans that people actually write need a time, in numbers that outweigh what a time zone costs every reader. A later version could give the form a meaning without changing what any valid 1.x file says.
Recorded as cairn item 0148.
A person is a handle, not an email address
Section titled “A person is a handle, not an email address”Context. Spike 0069 asked whether @dana@example.org should name a person. RFC 0002 (rfcs/0002-a-person-is-a-handle.md) records the argument.
Options. Accepting addresses would make a second @ special inside one token, put personal data in every repository a plan is copied to, and tie a plan to addresses that change. Every tool would also have to treat dana and dana@example.org as maybe the same person. Handles have none of these costs, and mapping a handle to an address can live beside the plans, as git’s .mailmap and GitHub’s CODEOWNERS do.
Decision. A person is a handle: @ followed by letters, digits, _, . or -, and nothing else (§4.2). @dana@example.org and dana@example.org are both words.
Revisit when. Never for the token. A mapping from handles to addresses, if one is wanted, is a separate file and a separate decision.
Recorded as cairn item 0149.
Files carry no version; the title’’s sprig: key is reserved
Section titled “Files carry no version; the title’’s sprig: key is reserved”Context. Spike 0070 asked whether a file should declare which version of the spec it follows. RFC 0003 (rfcs/0003-no-version-in-files.md) records the argument.
Options. A declared version lets a tool know what a file expects. It also gives every file a line that means nothing until a breaking change, and invites tools to refuse files over it. CommonMark never adopted a per-document version, and TOML, JSON and todo.txt carry none. Sprig’s grammar already degrades safely: an unknown key is an ordinary field and an unknown mark is a note.
Decision. A file needs no version. Every 1.x file means the same to every 1.x tool. The title’s sprig: key is reserved for a future major version (§4.6). sprig:1 changes nothing, and a tool that meets a version it does not read warns with S0007 and reads the file as best it can.
Revisit when. A 2.0 is needed; then the reserved key is how a 1.x tool finds out.
Recorded as cairn item 0150.
One command per question; the editor writes
Section titled “One command per question; the editor writes”Context. The sprig command grew to 22 commands, most with six or seven flags, while the format it reads stayed at nine marks. docs/stability.md promises that after 1.0 nothing is removed, so the surface 1.0 ships with is the surface forever. Before 1.0, cutting costs nothing.
Options. The rule proposed: one command per question a person asks of a plan, with filters and formats as flags; writing is done in an editor, because the file is the truth, and the command keeps only the one write that is fiddly by hand (ticking, which settles recurrence and dates). Each cut, with the case against it:
status→tree --summary. It is the tree, one row per file. Against: a short, memorable name for the most common glance.--onelinestays for prompts.agenda→find is:open 'due:<=+2w'. It is a search for dated work. Against: the agenda groups by day; the TUI keeps that view, andexport --format icsserves calendars.mark→tick --mark <M>. Setting a mark is the same act as ticking. Against:tick ~reads oddly;--marknames it.answer,add,edit→ the editor. Each is one line typed in a file. Against: agents without an editor; they have MCP, whoseadd,answerandmarktools stay, checked and diffed.graph→export --format dot|mermaid. It is another export format. Against: discoverability;export --helplists it.agent→mcp --instructions. One entry point for agents, besidemcp --config. Against:sprig agentis shorter.completions,man→generate <shell|man>. Install-time output, one command, as ripgrep has it. Against: none worth keeping two names for.
Kept: next, tree, why, check, fmt, tick, find, export, import, lsp, mcp, tui, generate. Thirteen. why stays because “why is this blocked” is its own question and its answer is a chain, not a tree.
Decision. Adopted as above. A removed command exits 2 and names its replacement, until 1.0, when the table goes. The simplicity budget (crates/sprig-cli/tests/budget.rs) pins the count, so a fourteenth command is a visible edit.
Revisit when. A question people ask of plans has no command and no filter that answers it.
Recorded as cairn item 0154.