Repository navigation
v0.3.0
Upgrading from 0.2
0.3 is a rewrite, so there is no in-place upgrade. Start over like this.
- The binary is
mtnow.cargo install mindtape-cliinstalls it, and the old
mindtapebinary can go. - A task is one Typst file of its own (
#show: task.with(...)), not a checkbox
line with an#id(). Nothing converts 0.2 files. Refile each open task with
mt add, or as a batch withmt import. - Config lives in each project's
.mindtape/config.toml, whichmt initwrites.
The global~/.config/mindtape/config.tomlis no longer read. Settings you
share across projects go in~/.config/mindtape/defaults.toml. - The
watchdaemon, its SQLite index and the NixOS module are gone. Everymt
command reads the files directly, so stop the service and delete the index. - The Typst package is
@local/mindtape:0.2.0.mt installputs it in place.
Added
Task files
- Task files are checked field by field. A misspelled field fails with
Typst's ownunexpected argument: titel, placed on that argument so an editor
marks it in the task file. A wrong-typed value reports the position you can
count to (tags[1],estimate.days,priority.inputs.ease,status.date,
recur.on[1]), the entries that do read survive, and a wrong-typed required
field is reported once, by its type. - The DSL helpers refuse what they would drop.
depends-on(link("a.typ")),
a second note block or a named argument to an edge helper (depends-on,
parent,recurred-from,supersedes,duplicates,related,edge), a
non-content note in a status constructor (wip(2026, 6, 14, "note")) and a
positional input to a framework are eval errors naming the helper, not a
silently empty value. - A load failure on a literal
#in a body says to write\#. Prose like
Channel #Nopens Typst code, so every command that warns about load errors
adds one note pointing at\#or a code block. - Typst errors carry their place and hints.
mt checkprints
error: tasks/broken.typ:2:3: unexpected argument: titeland each Typst hint
as ahint:line, andmt lsprints the same line as awarning:. An error
inside the prelude names the task file's line that led into it. A Typst
warning on a file that loads prints as its ownwarning:, cached or not. A
missing prelude is one error namingmt installand
[[typst.local-packages]], not one per task. recurdescribes a recurring task.recur: every(weeks: 1, on: (mon, wed, fri), anchor: "due", until: on(2027, 1, 1), count: 10, materialize: "spawn")is the common shape, andstart: on(2026, 6, 1)picks which weeks
a multi-week series runs on.anchor: "completion"(the default) advances
from today, andanchor: "due"catches up from a stale due date.
rrule("FREQ=MONTHLY;BYDAY=1MO")covers the rest of RFC 5545, anchored on
due, once[tasks.recur] rrule = true. Every build expands it, and
mt checkquotes a malformed rule.mt recur Xfiles the next occurrence.spawnwrites a new file, named
by[tasks.naming]or{slug}-{date}, with arecurred-fromedge to the
one before it, andmutaterefreshes the file in place.--dry-run
previews,--forcespawns a second,--advance Nspawns ahead up to
[tasks.recur] max-advance, and--skipmoves the series on without filing
anything.mt donekeepsrecurand points atmt recur.- A parent tracks its children. Computed fields
childrenandprogress
(the finished share, shown as3/5) filter, sort and render as columns. A
status that is bothterminalandunblocked = trueneeds every child
finished, somt flipnames the open ones andmt checkwarns about a parent
already closed over them. waits-onand[[tasks.waits]]let a task wait on something outside the
repo. A[[tasks.waits]]entry names a command, and a task lists
checker:identries inwaits-on.mtruns the command with{id}filled
in, once per distinct line per call and only for open tasks. Exit 0 frees the
task, 1 keeps itblocked, and anything else blocks it as unknown.why
shows the command's first line,mt checkrefuses an undeclared checker or an
id a shell could misread, andmt check --commitsasks each checker once per
run.- A link kind can point outside the backlog.
[[tasks.links.kinds]] validate
takeslocal-task-path(the default),url,git-sha,uuid,regex(with
a siblingregex = "...") orany. An external kind skips stem resolution,
cardinality, cycles and readiness (blockingandacyclicdon't apply) but
keeps its note rules, andmt graphdraws its targets as dashed leaves. A
swh:target is a history pointer, not an edge. - Status transitions turn the legend into a state machine. A
[[tasks.status]]entry'sto = [...]lists the allowed next statuses, and
with none declared any move goes.mt fliprefuses an unlisted move and
names the allowed ones, andmt checkflags a status the initial one cannot
reach.unblocked = truemakesmt fliprefuse entering a status while a
blocking target is unfinished, andmt checkwarns about a task already
there. Theflowandgatedpresets set it onwipanddone.--force
overrides both. - An estimate unit can count the files its queries match.
estimate: (src-files: count(2, grep(parse_task, "src/**")))
resolves to 2 plus the files thatgrep(...)orglob(...)match, each file
once, among the files git lists. A unit takescount(...)once
[tasks.estimate.queries]names the path set its files must fall in, such
assrc-files = "@source", and rules readestimate.src-filesas the
resolved number.mt checkreports a regex that doesn't compile, and a
count on a unit with no entry. mt evalshows where acount(...)estimate comes from. Under the
table it prints each such unit's total, its manual part and each query with
the files it matched, so a query that matches nothing shows as0 files.
--jsoncarries the same underestimate_counts, beside the resolved
number inestimate.- A
count(...)estimate survivesmt ls --json | mt import. The
canonical JSON carries each such unit underestimate_countsin its source
form, andmt importwrites it back as thecount(...)it came from, not
the number it resolved to on the day of the export.
Naming tasks
- A task is named by its stem, its path or its stem hash. The
slug#
column prints the shortest prefix, three characters or more, of a hash of the
filename stem that no other task shares. It is the same in every listing and
changes only whenmt mvrenames the stem. An exact stem wins over a hash, an
ambiguous hash is refused naming each candidate, and a stem two folders share
names the one beside the base or is refused. A link target spelledname.typ
is a path from the linking file.
Commands
mt flipsets a status without opening the file.mt flip <status> <task>...stamps the status dated today, and every legend status is a verb of
its own, such asmt wip aormt done a b, listed bymt flip --list. A
built-in command wins a name clash, with a warning.-m/--notesets the note.
A refused flip prints the way past as ahint:and what it waits on as
note:lines, and a batch ends witherror: flipped 1 of 3 tasks; 2 failed.mt set <task> <field>=<value>writes one core field. It coverstitle,
priority(a number),difficulty,kind,assignee,tags,due,
startandwaits-on, withmt add's=,+=and@pathtokens. A field
with a command of its own (status,links,reviewed,recur, the stem)
is refused naming that command, and a framework priority andestimatestay
hand edits.mt linkwriteslinks:.mt link <task> <kind> <target>...upserts one
edge per target, andmt link --apply <file>(-for stdin) reads a YAML or
JSON map of stem to kind to targets, where a target is a task reference or
{target, note}. Re-applying writes nothing,--replacemakes the map the
whole edge set (refused over a SWHID history pointer), and--dry-runprints
the plan. A target is written relative to the linking file, one failure
doesn't stop the rest, and a write that closes a cycle is refused.mt mv <task> <new-stem>renames a task and retargets every inbound link.
The file moves throughgit mvwhen git tracks it, each edge keeps its own
relative prefix, a taken stem is refused, and--dry-runprints the plan.
The new stem is bare, since moving folders changes derived tags.mt rmretires finished tasks. It deletes each target and rewrites every
surviving link to it into a SWHID tombstone,
link("swh:1:rev:<commit>;path=<path>"), planned in memory before a byte is
written. It takes several targets, a folder (mt rm tasks/sw1) or a filter,
and with none it applies[rm] default-filter. It refuses a dirty tree,
--dry-runpreviews,--yesskips the prompt (needed without a terminal),
and it commits nothing. A gitless project uses--strategy retireor
unlink. A mistyped name readserror: no task: 'nope'with the nearest stem.mt goopens a task in your editor. It reads[go] editor, then
$VISUAL, then$EDITOR, and a{task}token places the path.mt reviewstampsreviewed:with today's date on an open task.mt eval <task>takes a stem, path or hash. It loads the project config,
so[[typst.local-packages]]applies, and a path still evaluates a lone file.mt find TERM…searches text.mt find login bugruns
mt ls 'contains("login") && contains("bug")', takes everymt lsflag and
shows the expression in theapplied:footer.mt graph [filter]draws the dependency graph. Graphviz DOT by default,
--format mermaidfor Mermaid, or--jsonwithnodes, typededgesand
externalleaves. Nodes are coloured by readiness, a filter scopes the
subgraph and--no-terminaldrops closed tasks.mt import [FILE|-]files tasks from JSON. It reads the array or envelope
mt ls --jsonprints, maps each record onto the project's legend, scales,
kindvalues, tags andx-<ns>fields, names the file from the title, and
validates the batch as one set. A bad record is skipped and the rest land.
--dry-runpreviews,--into/--topick the destination,--statusfills
a missing status,--map OLD=NEWrenames a foreign one, and
--drop-unmappeddrops a tag outside the allow-list. A dry run that skips a
record endserror: would file 1 of 2 tasks; 1 failed, notfiled.
A skipped record's report keeps a content rule's detail on an indented
note:line under the breach, asmt checkprints it.
contrib/jq/taskwarrior.jqandcontrib/jq/github.jqturntask exportand
gh issue list --jsonoutput into that JSON, with neutral statuses (todo,
done,cancelled) and the source's own data kept inx-tworx-gh.mt preset save <name>andmt preset ls.savecopies the project's
.mindtape/config.tomlandtemplates/into a user preset (--force
overwrites), andlslists built-in and user presets, the shadowed ones and
the default (--jsontoo). A preset's[overlay]table holds raw config the
mt initwizard can't ask for, merged last.mt initcomposes a config from presets and axis flags. The built-in
presets aretodo-done,flow(the default),personalandgated. Two
fragments declaring the same array section are an error naming both.
--list-presetsnames the real preset directory,--recurrence rrule
composes,--priority ice-strictadopts themindtape:icepack, and an
answer's packs fold into oneextendsarray. The scaffolded config opens
with a#:schemaline (--schema URL|PATHretargets it,--no-schema
drops it).mt installinstalls the prelude, defaults, schema and shell completions.
--no-lib,--no-defaults,--no-schemaand--no-completionsskip a part,
and--forceoverwrites. A package directory that is a symlink, such as one
pointing at a checkout'slib/, stays as it is unless--forcereplaces it.- Tab completion asks the live project. The hooks under
~/.config/mindtape/completions/callmton each press, somt flip
offers the legend,mt ls @the views,mt add -tthe templates,
mt link <task>the edge kinds,mt init --presetthe presets, every task
argument its stems, andmt set ... tags=the tags. A project that fails to
load offers nothing, quietly. mt allowapproves the config's commands before any run. No
[[tasks.waits]]checker, no[[on.flip]]runand no[go] editoror
[add] editorruns untilmt allowrecords a hash of every such line for
the repository, shared by its worktrees. Until then a wait reads
unknown: not allowed, a gate refuses withgate not allowed(--force
waives it), arunwithoutgateis skipped with a note,mt go, the
mt lspicker andmt add --edittry$VISUAL, then$EDITOR, in place of
a held editor (mt gofails with neither,mt addopensvi), andmt
warns once listing the commands. A changed line asks again. An editor in your
own<config home>/mindtape/defaults.tomlneeds no approval.mt viewslists and inspects views, with--json.mt dictslists the dictionaries, built-ins included, with each one's
entry count and case, andmt dicts <name>prints its entries one per line,
afterinclude,valuesandpathare merged. Both take--json.-w/--worktree <path>and$MINDTAPE_WORKTREEpointmtat another
project.mt @homereads every project you track in one run. A baremt @home
prints one board row per project,mt @home ls readylists their tasks in
one table grouped by project, andmt @home checkchecks them all. Each
project loads in parallel under its own config, so 28 projects take about
as long as the slowest one, and a project whose config won't load is a ⚠
row instead of a dead run. Writes still go to one project at a time.~/.config/mindtape/at.tomlnames projects and sets of projects. An
[at.<name>]entry holdsinclude(literal paths, one-level*globs or
other@names),exclude, andforks(false,trueor"only", for
linked git worktrees). Put@nameright aftermt. An entry with one
literal path is a project, somt @sn lsmeansmt -w <path> ls, and so is
a directory anincludematches by name. Twoblogdirectories go by the
shortest path suffix that tells them apart, asmt @oss/blog ls, and a bare
mt @bloglists both with the suffix that picks each. Any other entry is a set. Every
command butls,find,checkandboardrefuses a set before it runs,
mt @set atandmt @set config schemaincluded, naming a member to pick.mt at
lists the entries, andmt at <name>lists a set's projects in aligned columns, a home path as~/…, marking
forks. Both take--json.mt --helpends with a line on@name.mt @set checkchecks every project of a set. It prints each project's
problems together in set order, each naming its project, and ends with
ok: 27 projects, 1904 tasksor3 of 30 projects failed, which exits 1.
A project whose config won't parse fails that way too.--jsonprints one
object per project, the projects check in parallel, and--commit-msgand--commitsrefuse a set.mt boardshows one row per project, and a baremt @homeruns
mt @home board. Each row counts the project's open, ready and blocked
tasks and names its next ready one, under that project's own config. The
columns are[[board.columns]]inat.toml, each afiltercount or a
toptask, withsortandhidebeside them and[at.<name>.board]per
set. A failed cell prints?with a note, a project whose config won't load
is a⚠row and exits 1,--allshows the hidden rows and--jsonprints
them all.mt _cli --jsondescribes the CLI, its filter functions, the filter
fields and which of them sort, the template slot kinds, the built-in link
kinds with their rules, the{date:FMT}tokens and which commands run on a
set, for tools and docs.--jsonon every read command,mt graphandmt preset lsincluded.
mt check --jsonprints aproblemsarray (severity,file,line,
place,rule,message,hints) in theschema_versionenvelope.- Paths read the way your platform writes them. On Windows, every path
mtshows you uses\: thefilecolumn,--format path, an error's
place such astasks\a.typ:3, and the paths in a message, with no
.mindtape/templates\bug.typmix, and a command hint leaves a Windows path
unquoted. What a program reads uses/on every platform: thefilefield
of--jsonand--format csv, thefileandplaceofmt check --json,
and the paths ofmt at --jsonandmt board --json. The global
--path-separator <SEP>picks the separator of the paths shown to you, as
in ripgrep and fd.
Filing tasks with mt add
mt add <title…>files a task from a template and checks it before writing.
It names the file from the title, writes it under the first root and prints
its slug hash (wrote tasks/x.typ (ksr),slugin--format json). A task
the project would refuse is never written.--printshows the task after the
same check,--no-verifyskips the check for both, and a refused--edit
buffer (opened with[add] editor,$VISUALor$EDITOR) is parked in a
temp file named on stderr. A refusal names the line that failed to load and
hints only at templates that pin the field at fault.mt add --listlists
the templates.- One token grammar carries every value.
key=valuesets,key+=value
appends to a list andkey@pathreads a file, after--setor bare after
--, as inmt add "Fix it" -- estimate.files=2 tags+=docs.+wordadds a
tag.-freads YAML or JSON values (-f -for stdin), andMINDTAPE_ADD_*
env vars and the--editbuffer fill slots too. - Template slots.
slot(..., example: ...)shows a sample that
mt add --describeprints, in text and--describe=json, and nothing files.
Atextorrawdefault or example can be a raw block, and a slot name may
hold dots (estimate.files). Atextslot escapes Typst markup, arawslot
splices verbatim, and an unknownkind:is an error. A slot that is a whole
field with configvaluesis a choice over them,multichoicefor a
multiplefield.--describerefuses a title and suggests-t. - A code span in a
textslot files escaped, with a warning that names the
slot and suggestskind: "raw". - A YAML error in
mt add -fcarries ahint:. A value that starts with a
YAML indicator such as a backtick needs quotes, or a|-block. - A template's own
statusis the statusmt addfiles it at, dated today,
and--statusoverrides it. Thetweaktemplate files done. Filed done, a
task asks only for the fields a done task requires. - A strict framework priority gets one slot per input. With
allow-manual = false, whereice(...)is the only spelling,mt add
splicesframework("ice", impact: ..., ...)with a typed slot for each
declared input, so a strict, required[tasks.priority]can still be filed.
Listing and filtering
mt @set lsandmt @set findlist every project of a set in one
table. APROJECTcolumn comes first and rows group by project in set
order. Each project applies its own default listing,@viewand sort, and
-nand--skipcount per project. The footer readsyour filter · 9 of 61 tasks in 27 projects.--jsonprints one object per project with its
projectanderror, and CSV gains aprojectcolumn. A project that
fails to list prints its error naming it after the rest, andmtexits 1.[views.<name>]names a filter, sort and columns.mt ls @brokenapplies
a view whole,@nameinside a filter ('@summer && !@broken') or
in_view("name")uses its filter, and--sort @viewand--columns @view
borrow its sort or columns. Views are checked at load, cycles included.- Bare
mt lspicks its view from context. The first[[ls.rules]]entry
whoseconditionholds wins, usingon_branch(glob),env(name),
weekday()andnow(), withimport-viewsand its own overrides, else
[ls] default-view, else the floor's[views.default].[ls] default-sort
sets the fallback sort. A filter argument replaces only the filter, so the
rule's sort and columns stay, and theapplied:footer names each facet's
source, marking a rule that lost every facet as superseded.mt ls ''lists
everything. -n,--skipand a footer that says what was cut. A cut table ends with
truncated: 20 of 143, next: mt ls -n 20 --skip 20, and--jsoncarries
truncated(shown,total,next_skip,nullwhen complete). CSV and the
path formats stay clean.
Thenext page:command keeps how you chose the project, so pasting it
from any directory pages the same listing:mt @sn ls -n 20 --skip 20, or
mt -w ../x ls -n 20 --skip 20with a quoted path when it needs one.
$MINDTAPE_WORKTREEstays out, because the environment carries over.- The footer counts the task files that failed to load.
mt ls,mt find
and their set forms end it with· 1 broken, as indefault view · 1 of 2 tasks · 1 broken. They still list the rest and exit 0, andmt checkfails
on the file. - Output formats.
--format jsonprints{"tasks": [...], "applied": ...}
with aretiredkey whentouches()ran,--format csvgives the
root-relativefileand stableid, and--format pathorpath0prints
bare file paths forxargsandfzf(contrib/fzf-mt-go.sh). - Columns.
slug#,assignee,why(what a blocked task waits on, in
--jsontoo),progressandchildrenjoin the set. Each
[ls.columns.<name>]shows when itswhenexpression holds, using
nonempty(),contested(),named(),blocked(),tty()andformat(),
and the floor sets every default.mt evaland[[on.flip]]
lists render columns the same way. mt ls -i(--interactive) picks a task. Type a hash to highlight it,
Enteropens it in the editor,Ctrl+Enteropens every listed task,Space
reloads,Delclears andEscorCtrl+Cleaves. It needs a terminal.contains()searches text.contains("bug")sweepstitle,idand
body,contains("bug", title, body)names its fields, and
[ls] contains-fieldschanges the default, withnote(link notes) and
tagson offer.has_tag()still tests a tag.- Filters read more of a task.
status.name,status.date,status.note
andstatus.indexfilter and sort.estimate.<unit>reads one unit, names
may be kebab-case (has(x-goals)), and anx-<ns>value compares like any
field (a dict value takes onlyhas()andmiss()).has(field, value)
tests a list, scalar or dict key, as inhas(tags, "frontend").
links("<kind>")tests an edge kind,readyandblockedread the graph,
and the root-relative path ispath. - Functions compose.
word(field, n)takes the nth word,matches(field, "regex")tests a pattern, andin_dict("name", <string>)looks a word up, so
in_dict("verbs", word(title, 1))checks a title's first word. count(<expr>)counts matching tasks across the loaded set, as in
count(status == "wip") <= 1, evaluated once per command.changed("<states>", "<glob>"…)counts changed files. It readsstaged,
worktreeorstaged+worktreewith include and!exclude globs, so
changed("staged+worktree", "crates/*/src/**") <= estimate.src-filesis one
rule. A glob may be@<dict>, a path set. A comparison's right side may be a
field or a count. Git runs read-only, once per command.touches("<path>")finds the tasks that shaped a file, through theRefs:
trailers of the commits that changed it, following renames.topoandblocksrank the dependency graph.topois a task's depth
(0 when nothing blocks it), so ascending is a working order, andblocks
counts the unfinished tasks waiting on it. Both readdepends-onand any
kind withblocking = true, and both filter and sort, as in
--sort topo --sort priority:desc.[dicts.<name>]names a set of strings once. Sources arevalues, a
pathfile (one entry per line) andincludeof other dicts, compared under
case = "fold"(default) or"exact".mindtape-verbsand
mindtape-dev-verbsship built in, and themindtape-prefix is reserved.
[tasks.tags] allowedand[tasks.fields.<name>] valuestake@<dict>
entries, and an entry starting with@always names a dict.- Sorting is consistent. Every field sorts a missing value last ascending
and first descending.--sorttakes every filter field plus theready
rank,assigneeandreviewedincluded, and a direction other thanascor
descis an error. matches()readsbody,noteandtags. It reads the text-only
fieldscontains()reads, somatches(body, "(?m)^- TODO:")finds a task
that still holds a template's placeholder line. Before, it exited 2 with
unknown field: 'body'.
Rules and checks
[[tasks.rules]]holds any convention as a filter expression. An entry
hasassert, plus optionalname,when,messageandseverity(warnby
default, orerror).check = { typst = "<path>", function = "<name>", field = "<field>" }replacesassertfor a shape the filter language can't
name, returning one string per breach. A breach reports as
rules.<name>, orrules[0]unnamed, and every expression is dry-run at load.
A work-in-progress cap isassert = 'count(<when>) <= 2'.- A rule refused by
in_dict()says what missed. Under the rule's own
message, the breach names the word, the dictionary and up to three of its
nearest entries, as inhint: 'Login' is not in dict verbs; did you mean begin, join or log?, thenhint: run mt dicts verbs to see its entries.
mt check --jsoncarries both inhints. A call under!explains nothing. - Every write enforces the rules, not just
mt check.mt add,set,
flip,link,mv,import,recurandreviewcheck the candidate, body
rules included, and write nothing on anerrorbreach. A warning prints and
the write lands. A batch is checked as one set, a breach it opens on another
task (a cap, a cycle) is onenote:naming that task, and a refused rewrite
leaves the file as it was. - Body rules.
[[tasks.body.headers.rules]]hold a task's heading tree to an
actionofrequire,forbid,unique,orderorallowlist, and
[[tasks.body.content.rules]]hold a section tonon-empty,min-words,
max-words,must-match,must-not-matchordensity(pattern,max,
per = "section" | "paragraph" | "sentence", code skipped whether fenced or
inline). Both take aname, reported asbody.headers.rules.<name>. - A content rule's
messagekeeps the check's own words. They print on a
note:under the message, such assection 'TO-BE' sentence 2 ("Should it also cover the board?") must carry at most 0 matches, inmt check, in a
refused write, and in anotesarray that everymt check --jsonproblem
now carries. - Packs. A config layer's
extends = [...]pulls in a pack, named
<source>:<pack>or given as a TOML file by path, anddisable = [...]
drops rules by name.mindtapeis the one source known without a
declaration, andmindtape:tasksis the pack built in. An old
mindtape-tasksis refused as unknown, withmindtape:tasksas the
suggestion. A later rule with the samenamereplaces an earlier one, and
the rule arrays append across layers.mt config show --originnames each
rule's pack or layer. - A pack carries any config key. Its
[tasks.priority], views, statuses
and the rest deep-merge just below the layer that extends it, so the layer's
[tasks.priority] allow-manualkeeps the pack'sframeworkand the floor
fills in the rest. Two packs in oneextendsthat set one value differently
fail, naming the dotted key and both packs, unless the extending layer sets
that key itself. A pack may not declareextends,disable,
pack-sources,project.code,tasks.roots,templates.dirsor
[typst]. The library'sConfigError::Invalidcarries ahint, whichmt
prints ashint:. - A project declares pack sources.
[pack-sources.<name>]names a git
repository bygitandref, a tag, branch or commit, and
.mindtape/packs.lockpins each one to a commit and a hash. Every config
load refuses a source the lock lacks, or pins from anothergitorref,
aspack source lock is stale: 'acme'. Only the project's own
.mindtape/config.tomlmay declare a source or extend a pack from one, and
mindtapeis taken. The library'sconfig::pack_sources::read_rawreads
the sources and the lock without loading the config. - A pack can be a directory. An
extendspath naming a folder reads the
pack.tomlin it, and the folder'stemplates/joins[templates] dirs
behind the project's own, somt add --listlists both and a project
template with the same name wins. The folder may sit outside the project. A
team.tomlbeside ateam/folder makesextends = ["./team"]ambiguous,
andmtrefuses it naming both. - A pack's check and dictionary files resolve from its directory. A pack
rule'scheckTypst module and a pack dictionary'spathare read from
the folder of a directory pack, or the one holding a pack file, not the
project root. The check runs in a Typst world rooted there, so it imports
the pack's own files and@localpackages, never a file of the project. A
rule or dictionary the project declares itself resolves from the project
root as before. [[on.flip]]hooks merge by name. A hook takes an optionalname. Hooks
from every config layer and pack now add up instead of the nearest layer's
replacing the rest, a later hook with the samenamereplaces the earlier
one in place, anddisable = ["<name>"]drops it. A hook with noname
always appends.mt config show --originlabels a hookon.flip.<name>or
on.flip[<index>], andmt ruleslists its actions under that label, such
ason.flip.stale-wip.actions[0].- A pack takes parameters. It declares each with a default in
[params],
and its expressions read it asparam("name"). Anextendsentry
{ pack = "./team.toml", with = { max-files = 8 } }sets the pack's own
value, andmtwrites it into every expression of the pack at load, skipping
string literals and a rule'smessage.mtrefuses awithkey the pack
doesn't declare, suggesting the nearest, a value of another type than the
default, aparam()naming no parameter, aparam()in a layer's own
expression, and one pack listed twice in oneextends, even under two
paths to the same file. A path pack's label is its canonical path. The
config schema tags each expression key withx-expressionand its
language. mt config show --originnames the pack behind each key and parameter.
After the rule lines it printskey <path>: <origin>for each value a pack
set, naming the layer instead when the layer replaced it, andparam <name> = <value>: <set by>, for pack <pack>for each parameter, where<set by>is
the layer whosewithset it ordefault. The library'sConfigkeeps the
record:key_origins()andparam_origins().- A config warning points into the pack that holds its key. A filter in a
pack's rule that names an undeclaredx-field now warns at that pack's
file and line, asteam.toml:6: tasks.rules[0].assert: ..., not at the
project config.Config::warning_placegives the place. - The library fetches a locked pack source into a cache.
config::pack_sources::fetchrunsgit fetchfor the source'sref, with
the caller'sGIT_*repository variables dropped, fetched objects checked
and only https, ssh, git and local paths allowed. It writes the locked
commit's files from git's own objects, so nocore.autocrlf,
.gitattributesor filter changes a byte, and renames the tree to
<cache root>/<sha256>/.config::pack_sources::hashis that sha256, over
every file framed by its path and length. A symlink, a submodule, a path
that is not ASCII, has an empty,.or..part, or names something
Windows can't hold (C:x,a\b,CON, a trailing dot), two spellings of
one path in different case, or a hash other than the lock's fails, on every
platform. A SHA-256 repository fetches by its 64-digit commit, two fetches
of one tree at once both pass, and a fetch sweeps scratch directories over
an hour old. - A config error names the layer or pack that holds its key. An unknown
or wrong-typed key in a pack or an ancestordefaults.tomlfails as
team.toml:3:2: can't parse config: unknown field 'projet', and a pack from
the binary readspack mindtape:tasks. A bad value a pack sets, such as a
view's filter, is placed at the pack's line, and a pack rule's bad expression
at its own[[tasks.rules]]table, wheremtused to point at a line of
.mindtape/config.tomlthat held neither. A key no file declares in full has
no place, and the error leads with the key alone. - A pack from a declared source resolves from the cache.
acme:commits
readscommits.toml, orcommits/pack.tomlfor a folder pack, in
<cache>/mindtape/packs/<sha256>/, the tree the lock's hash names, and
mt config show --originlabels its rulespack acme:commits. Every load
hashes each source's tree once, before reading it, and refuses one that
doesn't match the lock, naming its directory. A source the cache lacks fails as
pack source not installed: 'acme', with a hint to runmt packs install,
and an unknown pack lists the ones the tree holds. A pack name takes letters,
digits,-and_, a cached pack's dictionarypathmust stay inside its
tree, and any pack'scheckmodule must stay inside the pack's directory.
The cache is$MINDTAPE_CACHE_HOME, else the platform cache directory, such
as~/.cacheon Linux. The library addsconfig::pack_sources::cache_root
andload_config_layered_in, which takes the cache, while
load_config_layered_fromreads none. mt packs installfetches the pack sources the lock pins. For each
declared source whose tree the cache lacks it fetches the locked commit,
checks its hash against the lock and prints `fetched acme 3f2c1a9 into `, then counts the rest as `1 source up to date`. Like `npm ci`, it refuses a lock that disagrees with `[pack-sources]`, with a hint to run `mt packs update`. It reads the sources without loading the config, writes only the cache, and takes `--json`. A relative `git` path is read from the project root, which the library's `config::pack_sources::locate` resolves, and a drive-relative one such as `C:packs` is refused on every platform. Every git command runs in the cache root and searches no further, so no repository around the project, nor its config, takes part.mt packs updatemoves pack sources along theirref. It resolves
each source'srefwithgit ls-remote, the peeled commit of an annotated
tag and a tag before a branch of the same name, or takes a 40- or 64-digit
refas the commit. It fetches each moved source, rewrites its lock entry,
and printsacme v1: 3f2c1a9 → 8d01e44,acme v1: locked 3f2c1a9for a
new one, oracme v1: up to date. It drops the entries no source declares,
takes source names to update only those, and writes.mindtape/packs.lock
once, atomically, after every fetch passed. It prints its report only after
that write, so a failed update prints its error alone. The library adds
config::pack_sources::resolveandwrite_lock.mt packs updateprints what a moved source changes. Under each moved
source it lists the packs added (+), changed (~) or removed (-), and
in a changed pack each key, rule and array entry, such as
~ rule tasks.rules.changelogor+ on.flip.gate, compared as parsed TOML
so a reformatted pack shows nothing. Every newrunoreditorline
prints in full under its entry, and every other file of the source as
~ file team/check.typ. When the old tree isn't cached it fetches it by
its commit from the oldgit, then from the source'sgit, since a
source may move hosts. When both fail it warns and lists the new tree as
all added, so no new command goes unlisted. The library adds
config::pack_sources::diff_trees.mt ruleslists the rules that apply, one line each. A line is the
rule's severity, its name asmt config show --originprints it, and the
message a breach prints, after packs, overrides anddisable. A rule with no
severityshows the levelmtapplies, and an[[on.flip]]action with a
messageis listed too.--for add|commit|flipkeeps what that step checks,
and--jsonadds the section,when,assertand acheckrule's
function. The library'sactive_rulesreturns the same list as
ActiveRulerecords, and the config schema giveswarnas the default
severityof[[tasks.rules]]and[[commits.rules]].- The
mindtape:taskspack ships the flagship practice. It holds the
section shape (Problem, TO-BE, Changes, Out of scope, plus Environment,
Alternatives, Source of truth or Rollback by kind), heading order, 🔴/🟢
markers, word caps, an em-dash density cap,no-open-questions(a?that
ends a sentence of an open task's TO-BE, Changes, Out of scope or Dogfood,
outside code) andimperative-title, and each message says the next step.
Its commit rules arework-names-a-task,
flip-with-work(awipordoneflip rides with its work; adecision
task, amilestoneor anmt rmof other tasks counts) andnamed-tasks-finish-at-the-tip. - The
mindtape:wip-capsandmindtape:budgetpacks ship the WIP caps and
the size budget.wip-capsholdsmilestone-wip-capandleaf-wip-cap,
withmilestones = 1andleaves = 1as parameters.budgetholds
judgment-stays-small, which skips a milestone, andsrc-budget, with
min-difficulty = 3andmax-files = 5as parameters. It also sets the
src-filesestimate unit, its@sourcequery and asourcepath set of
**, which a project's own[dicts.source]replaces. - The
mindtape:tweaksandmindtape:commitspacks ship the tweak and commit
rules.tweaksholdstweak-lands-doneandtweak-stays-one-file, with a
sourcepath set of**that equalsmindtape:budget's, so a project
adopts both.commitsholdsrefs-task-is-startedand
changelog-with-product-change, which skips afixup!,squash!oramend!
subject and aChangelog: nonetrailer, with aproductpath set of**
that a project replaces. - The
mindtape:dogfoodpack ships the Dogfood section, andmindtape:tasks
declareskind.dogfoodholdsone-dogfood-section,
dogfood-after-out-of-scope,dogfood-non-empty,dogfood-word-capand a
section-allowlistthat admits Dogfood, which replaces the tasks pack's
allowlist when listed after it.mindtape:tasksnow declares
[tasks.fields.kind](feat,fix,refactor,docs,test,dev,
milestone, required of an open task) and[tasks.comments]with
policy = "forbid", so a project that extends it alone needs neither. - The
mindtape:flowandmindtape:icepacks ship the status legend and
the ICE priority.flowrestates the floor's six statuses with theirto
edges andunblockedflags, and holds thestale-wiphook, withdays = 30
as a parameter. A project's own[[tasks.status]]replaces the legend whole.
icesetsframework = "ice",allow-manual = false,requiredand
exampleunder[tasks.priority], so the floor's scale and bands show
through. - Field rules live in one descriptor.
[tasks.fields.<name>](a project
field such as[tasks.fields.x-<ns>]included),[tasks.assignee],
[tasks.tags],[tasks.estimate]and the numeric scales takevalues,
default,required,exampleandvalues-when.requiredistrue,
falseor a filter such as"!terminal".multiple = truemakes an
x-<ns>field a list, withvalueschecked per element. Any other key in
[tasks.fields]fails the load. Tags and estimate units are checked on open
tasks only, and a filter naming an undeclaredx-field warns with the
nearest one. mt checkchecks templates too. It fills every template with each slot's
example, else itsdefault, and reports breaches at the template's file
and line, the built-in one at[templates] fallback-severity. A template
that fails to load reports like a task file, and atextslot whose example
shows a code span is refused.mt checkreports on stderr and ends with a count, such as
error: 3 problems in 2 tasks, somt check 2>/dev/nullprints onlyok:.
A rule skipped for lack of git points at its own config entry.
Commits
mt check --commit-msg <file>checks a commit from a hook. The
[commits] trailer(defaultRefs) names tasks by stem, and a path or a
stem naming no task is refused with the stem to write.git commit --amend
is judged against the replaced commit's parent.[[commits.rules]]write commit policy in the filter language. A rule
readssubject,refs,trailers, the stagedfilesandtasks_only, and
count(files, "glob"…)counts files.scope = "task"runs per task the
commit changes or names, throughhead(...),staged(...),worktree(...),
exists,named,into("s")andretires_other.scope = "tip"runs once
per named task, over the range's newest commit.mt check --commits <range>holds past commits to the rules, each against
its first parent. A rule readingworktree(...)orchanged(...)is skipped
and reported, and each problem names its commit.- The
mindtape:taskspack'swork-names-a-taskskips afixup!,squash!
oramend!commit, since autosquash drops its message. - A commit rule reads
in_range, true undermt check --commitsand false
undermt check --commit-msg. The pack'sno-unsquashed-fixupuses it to
refuse a range that still holds afixup!,squash!oramend!commit.
Flip hooks
[[on.flip]]runs actions when a task changes status.whenis a filter
over the task before the write plusinto("status").{ show = "text" }
prints,{ list = "filter or @view", title = "..." }lists tasks, and
{ run = "cmd", gate = true, message = "..." }refuses the flip when the
command fails. A gate withoutrunrefuses outright.--forcewaives a gate
and records it in the note. Commands run throughsh -c(cmd /Con
Windows) from the project root withMINDTAPE_TASK_FILE,MINDTAPE_TASK_STEM
andMINDTAPE_TASK_STATUSset, andshowandlistgo quiet under
--quietor a pipe. Read commands run nothing. Thegatedpreset lists the
open work on the way intowipand gates the way intodone.- A
runtakes an array, run with no shell. An[[on.flip]]runor a
[[tasks.waits]]checker'srunis a string forsh -c(cmd /Con
Windows), as before, or an array such as["cargo", "test"], whose program
mtstarts directly, the same on every OS. A checker's{id}fills each item
on its own, so no shell reads it.mt allowlists an array as written, and
turning a string into an array, or back, asks again.
Config and environment
- A JSON Schema for
.mindtape/config.toml.mt installwrites it,
mt config schemaprints it, and a schema-aware editor reads the#:schema
linemt initwrites. Its descriptions read as user docs. - A JSON Schema for
at.toml.mt installwritesat.schema.jsonbeside
config.schema.json, so a#:schema ./at.schema.jsonline gets your editor
completinginclude,exclude,forksand the[board]keys. - Config layers.
~/.config/mindtape/defaults.toml, ancestor
.mindtape/defaults.tomlfiles and the project config merge in order. $MINDTAPE_CONFIG_HOME,$MINDTAPE_DATA_HOMEand$MINDTAPE_CACHE_HOME
move the per-user directories on every platform, for reading,mt installand package
loading alike. Set but empty means none.$MINDTAPE_NOWpins today, as aYYYY-MM-DDdate or an RFC 3339
timestamp, for every stamp,now(),stale(N)andmt recur. A malformed
value is an error, and$SOURCE_DATE_EPOCHis ignored.- A content-hash cache under
.mindtape/cache/skips evaluating unchanged
task files. A change to the file, a module it imports, the config, the prelude
ormtre-reads it, andMINDTAPE_NO_CACHE=1bypasses it. Read commands
fill the cache but never change the authored tree. - Writes come out typstyle-formatted.
[format] enabled = falsekeeps raw
splices. - Windows binaries. Releases attach
mindtape-x86_64-pc-windows-msvc.exe
beside the Linux and macOS builds, and CI tests on Windows.mt add --edit
falls back tonotepadthere.
Docs and skill
- Every docs page has a Markdown copy, and
/llms.txtlists them all. The
skill points agents at it. - The docs open on the problems Mindtape solves, using the
examples/api-servicebacklog, with a page per problem. The sidebar groups
pages by job, and terms are defined at first use. - New pages. One per
mtcommand under/cli/,/task-files/for the
Typst in a task file and its common mistakes,/packs/, rendered
graphs of the links and the legend, and the language server's completions and
diagnostics on a task file. - The docs build their lists from the code. The filter fields, link kinds,
slot kinds,at.tomlkeys, the shipped board and the commands that run on a
set all render from whatmtitself knows, and the docs build fails when a
page names a key or valuemtdoesn't. The schema page shows the real JSON
of a set and of the board, andmt --help, the quickstart and the sidebar
lead to reading many projects withmt @name. - The agents page names the skill installers,
gh skill installand
npx skills add. - The skill files verb-first titles and leads
-fwith YAML.
Library
- The
mindtapecrate is the domain library behindmt, over seven
crates.io crates (typst-world,typst-harvest,typst-cst,typst-edit,
typst-cleanup,swhid-mint,artifact-gate). Areas live in folders
(mindtape::filter,config,commit,graph,model,loader,body,
validate,template,recur,emit,link,git), with root re-exports.
Public entry points includecheck_candidate,check_candidates,
check_body_rules,BodyRules,Checked::elsewhere,resolve_ref
(Base::CwdorBase::Linker,Resolved,RefError::Ambiguous,
RefError::NoMatch),relative_target,Severity,Violationwith its
optional severity andViolationKind,write_atomic,shell,
config_schema_json,map_canonical,stem_for_title,emit_task,
package_overrides,package_rootandsort::sort_fields.LoadError
carriesline,column,hints,place()andregion, theCallRegion
thatTaskCallSpans::region_atgives its point.LoadOutcomecarries
warnings,ConfigError::Invalidcarries aConfigPlace,
naming::disambiguatereturnsNamingError::Exhaustedpast 10,000 tries, and
recurrence catch-up returnsRecurError::CatchUpExhausted. mindtape::load_one(root, config, path)loads one task file. It reads
through the same content-hash cache asload_project, annotates waits the same
way, and returnsOk(Some(task)),Ok(None)for a file with notask.with,
or the file'sLoadError. A path outside every task root, or not a.typ
file, is an error.topo,blocks,childrenandprogressstay empty,
since they need the whole set.
Changed
- One error format. Every stderr line starts with
error:,hint:,note:
orwarning:, coloured on the prefix in a terminal, with typed values in
single quotes and near misses ashint: did you mean x?. A wrong command
line, an unknown field or a filter that doesn't parse exits 2. Rust'sError:
andCaused by:lines are gone, and each failure is reported once, with no
summary of its own. Outside a project,mtnamesmt initand-w <path>. - Config errors name their file and line, as in
error: .mindtape/config.toml:5: views.broken.sort: unknown sort field: 'nofield',
for every layer and pack. An unknown status in a filter or view fails and
suggests the nearest one. - Filter errors say what to do, as in
a '(' is never closedor
'~' isn't an operatorwith amatches()hint, and show no Rust type. - Today is the local calendar date (
$TZon Unix), not UTC, unless
$MINDTAPE_NOWpins it. --helpexplains each flag in a sentence or two, and the name is spelled
Mindtape throughout.cargo install mindtape-clibuilds faster, with thin LTO.--hyperlinksbelongs tols,findandflip, the commands that
print a task table, so write it after the command, as in
mt ls --hyperlinks always.mt boardandmt atno longer list it, and its
help and the[ls] hyperlinksschema text name theslug#andfilecolumns.- The config schema descriptions read as plain sentences, with no em dashes
and no[section]lead-in.mt config schema, the hover text in an editor
and the Configuration page of the docs site now share one voice.
Removed
search(), replaced bycontains().- The implicit
lib/package. A project whoselib/typst.tomlnamed the
packagemindtaperesolved@local/mindtapefrom there with no entry. Pin
it with[[typst.local-packages]]instead, as this repository does. - Spellings from 0.3's development. They never shipped and have no alias.
The gone ones are[handle] check-len(handles are stem hashes now),
[[tasks.limits]](usecount()in a rule),[tasks.estimate.drift]and
[[tasks.estimate.max]]ormin(use a rule withestimate.<unit>or
changed()),[ls] default-filter(usedefault-view),mt ls --all(use
mt ls ''),mt install-lib,key[]=value,required = "non-terminal",
thefilefilter field (usepath),has(depends_on), thetypstslot
kind (useraw),[ls.columns.<name>] visibilityandrules(usewhen),
therrulecargo feature,mindtape::sort::SORT_FIELDSand
LimitSeverity.
Fixed
-
The
filecolumn stays short under a short or symlinked directory. On
Windows, from a directory spelled with a short name such asRUNNER~1, the
column climbed to the drive, as in..\..\..\..\..\runneradmin\...\a.typ,
because the working directory and the project root were spelled two ways.
Both resolve to their canonical form now. -
A flip hook can't flip its own task in a loop. An
[[on.flip]]command
that ranmt flip doneon the task it ran for started the same hook again,
without end. Every command gets the flips in progress in
MINDTAPE_FLIP_CHAINnow, and a nested flip that repeats one is refused
with the chain named. A hook that flips another task still runs that
task's hooks, up to 16 flips deep. -
A checker that runs
mtanswers once. A[[tasks.waits]]checker such
asmt ls readyloaded its own project, ran itself again and never came
back. Every checker runs withMINDTAPE_WAITS_CHECKER=1now, and anmt
that sees it runs no checker, so its waits readunknown: asked inside a checker. -
A fresh
gatedproject closes a task on any OS. Itsdonegate was a
grepshell line, somt donerefused untilmt allow, andcmd /Con
Windows couldn't run it at all. It's a filter condition now,
matches(body, "(?m)^\s*- TODO:"), which runs no command. No shipped
preset,mt initanswer set ormindtape:pack declares arun. -
A hyperlink on Windows opens its task. The
slug#andfilelinks
readfile://C:\proj\tasks\a.typ, which names a hostC:, or mixed both
separators, as infile:///home/user/proj\truth/.... They're RFC 8089 URIs
now:file:///C:/proj/tasks/a.typ, with/, a percent-encoded space, no
\\?\prefix, andfile://server/share/...for a UNC path. The
#:schemalinemt init --schema localwrites is one too. -
A
checkrule gets an infinite or NaN float from anx-field.mt
wrote it into the call as Rust'sinforNaN, so the rule broke with
unknown variable: inf. It's Typst'sfloat.infandfloat.nannow. -
Task files in a folder load on Windows. Each one failed with
bad virtual path "tasks\\alpha.typ": path contains backslash, so a project
with atasks/root loaded nothing there.mtnow names every file to
Typst with/, as on Linux and macOS. -
No
\\?\paths on Windows, and pack errors stay relative.mt -w
printed task files as\\?\C:\..., and an error in a pack named its
absolute path whenever the project root was spelled another way than the
resolved pack, as through a symlink or a shortRUNNER~1name. Paths now
resolve through one helper that drops the prefix, and a pack under the
project is named relative to it. -
A tombstone written on Windows resolves everywhere.
mt rmwrote its
path=with\, as inswh:1:rev:…;path=tasks\gone.typ, and git paths
use/. It writes/now. -
mt add --torefuses\etcandC:etcon Windows. Neither is absolute
there, so both got past the check, and joining either left the destination
root. -
A failed read ends without a stray period on Windows. Windows ends its
reasons with one, socan't read: 'a.json': the system cannot find the file specified.had it mid-sentence. -
mt rm --dry-runrefuses an uncommitted task the waymt rmdoes. It
named only the first file git couldn't pin, astask file has uncommitted changes, while the real run listed every one undercannot remove. Both
list them all now. -
A bare
Refs:value is the stem as written.Refs: foo.task-261023
read asfoo, somt check --commitssaid it named no task and
touches()matched nothing. Only a value holding/or\, or ending in
.typ, reads as a path and loses its extension. -
mt installputs the prelude where Typst and tinymist look, on Windows
too. It wrote under%LOCALAPPDATA%\typst\packageswhile both read
%APPDATA%\typst\packages, so tinymist never found it.mt installand task
files now share the directory typst-world resolves for Typst, and a set
$TYPST_PACKAGE_PATHmoves it on every platform, as it does fortypst. On
Windows, runmt installonce more and delete the old copy. -
mt findnames a task file that fails to load. It used to skip the file
without a word, whilemt lswarned. Both print the same warning now, and
mt findstill lists the rest and exits 0. -
A misspelled status helper points at the statuses, not at
#.
status: finished(…)used to get the advice about a literal#in a task
body. Now an unknown name instatus:hintsrun mt flip --list to see the statuses, any other unknown name in thetask.with(...)header gets no
hint, and only one outside the header keeps the#advice. -
A hint inside a set runs in the project it came from. Under blog's
error,mt @work checkused to hintrun mt flip --list to see the statuses, which ran in whatever project you stood in. Now every problem a
member prints names itsmtcommands with the member's@name, as in
run mt @blog flip --list to see the statuses. Inmt @work ls @nope, a
project that lacks the view hintsrun mt @blog views to see the views, and
a member's warning about commands you haven't allowed ends inmt @blog allowandnote: project: blog. A command that reads no project, such as
mt install, stays bare. -
A hint under
mt @blogor-wruns on that same project.mt @blog checkused to hintrun mt flip --list to see the statuses, which fails
typed where you stand. Now a hint'smtcommand carries the@blogor
-w <dir>you gave, as inrun mt @blog flip --list to see the statuses. A
run inside the project, or one$MINDTAPE_WORKTREEpoints at, keeps its
hints bare, and so does a command that reads no project, such asmt preset ls. -
A second
mt initfails. Over an existing config it used to print a note
and exit 0, so a script never knew nothing was written. Now it exits 1 with
error: config already existsand the hint to pass--force. -
A mistyped slot kind lists every kind
mt addaccepts. The error left
outmultichoice, so a template author who wrotemulti-choicewas told
to pick from ten kinds when there are eleven. -
mt ls --format csvends with one newline. It used to print a blank
line after the last record, so a script that read the listing saw an empty
last row. An empty listing is nowid,titleand one newline. -
mt ls --format pathprints nothing when no task matches. It used to
print a lone newline, somt ls blocked --format path | wc -lsaid 1. -
A reader that stops reading no longer panics
mt.mt ls | head -3
ends cleanly, and a real write error such as a full disk still fails. -
A killed write can't leave a task file empty or torn. Writes go through
a temp file and a rename, andmt addnever overwrites a file that appeared
meanwhile. -
mt rmwrites retargeted links with/on Windows, and stops suggesting
a commit outside a git repository. -
mindtape-clibuilds from its published tarball, carrying its own copy
of the prelude. -
An injected
status:brings its own comma, so a one-line template files
a task that parses.
The rewrite (2026-06-14)
Added
- Per-file task model — one task per file via
#show: task.with(...), with
a(name, date, note)status, configurable enum/number core fields, task
links, tags, and anx-<ns>extension seam for downstream tools typst-harvestcrate — a package-agnostic, eval-only Typst World plus
labelled-metadata-marker harvesting; a reusable substrate for file-based tools.mindtape/config.toml— per-project status lifecycle, enum/number field
constraints (required-unless-terminal, bounds), tag allow-list, default filtermt check— config-driven validation incl. link resolutionmt install— installs the Typst library (once per machine)mt init— scaffolds project config (once per project)- Dev tooling: parallel
just check, sccache, cargo-machete, linecop, outdatty
Changed
- Split into a three-crate workspace:
typst-harvest,mindtape(library),
mindtape-cli(binary). The filter language now runs over live-evaluated
tasks instead of SQL - Typst library bumped to 0.2.0 (new task DSL; breaking)
Removed
- The
watchdaemon, the SQLite index, themindtape-storecrate,
eval-hash skipping, the global cross-project agenda, inline checkbox markers
and#id()identity, and the NixOS service module
The agenda era (2026-03-23)
Added
- Global agenda filter —
[agenda]now supports a top-levelfilterfield
that is AND-ed into every task section, e.g. hide tasks with a future
#start()date across all sections - Default
#start()filter in the built-in agenda: tasks whose start date
hasn't arrived are automatically hidden
Changed
- Agenda config restructured —
[[agenda]]sections are now nested under
[agenda]as[[agenda.sections]], enabling agenda-level settings alongside
per-section configuration (breaking change to config format)
Fixed
- Resolve workspace version for naersk Nix build
Full Changelog: v0.2.0...v0.3.0