AtlatestRepositoryfolio
1
# Changelog3
All notable changes to **folio** are documented in this file.5
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),6
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).8
## [0.2.11] - 2026-08-1110
### Fixed12
- **An unknown argument is refused instead of ignored.** `folio --typo` used to13
start the MCP server and exit 0, so a mistyped flag looked exactly like a14
successful run. Unknown arguments now exit 1 with a usage message naming what15
is actually valid, and `--help` / `-h` work with no environment configured at16
all (previously `--help` required `FOLIO_ROOT`).18
- **`--version` and `--help` no longer bypass validation.** They were scanned19
for across the whole argument list before validation ran, so20
`folio migrate-buried --version` printed a version and exited 0 — the same21
silent success this release exists to remove, reachable through the fix for22
it. The check is a property of what can reach it, not of the check itself.24
- **The validator no longer disagrees with the code that reads the arguments.**25
`folio --log --web` was accepted as "`--log` with the value `--web`", after26
which the port finder started an HTTP server nobody asked for and the logger27
wrote a file literally named `--web`, all at exit 0. A validator that blesses28
an invocation the program then reads differently is worse than no validator.30
- **`--web-port` no longer fails at runtime.** The port check called `exact?`,31
which is unbound in sigil: it compiled cleanly and raised on every real32
`--web-port` invocation. Caught by a test arm that runs the actual deployment33
command rather than a synthetic one.35
### Added37
- `--stdio` is accepted and ignored, as a documented compatibility no-op. It38
names the transport folio already uses, and it is a common MCP client39
convention.41
- `test/test-cli-contract.sh`, 23 arms, each verified individually falsifiable42
against a stubbed CLI. Some arms deliberately run live deployment invocations43
rather than defect cases; they are marked as such because that is what caught44
the `exact?` runtime failure, and they are the opposite of redundant.46
### Known48
- `--log-level=trace` is refused with a correction pointing at49
`--log-level trace`. The consumers match flag names exactly and never split50
on `=`, so accepting the joined spelling would start folio and silently51
ignore the flag. Accepting both spellings honestly requires folding the three52
argument parsers into one pass, which is tracked separately.54
- `serve` remains accepted. It was never a subcommand: there was no dispatch,55
and the old loop ignored unrecognised arguments, so `folio serve` has always56
worked by accident. Live deployments depend on it.58
## [0.2.10] - 2026-08-0660
### Fixed62
- **A multi-line capture round-trips: an item's extent is SYNTACTIC, and the63
burial class is closed.** A task is its checkbox header line — metadata at64
the end of that line — plus the run of following lines indented at least65
two columns past it (one blank line is allowed between body lines when66
bracketed by indented ones). Extent is decided by indentation alone, never67
by content: no heading, fence or hashtag heuristics anywhere, so a removal68
can only ever take lines indented under the item's own header, and69
unindented prose, headings and config fragments are untouchable by task70
operations *by construction*.72
This ends the mechanism that buried at least 22 items between April and73
August 2026: the old writer appended `{id: ...}` after the whole text, a74
newline put it on the capture's LAST line, the line-oriented parser saw75
only the headless first line, and `backfill-ids!` then minted a phantom id76
onto it — one capture became a phantom head plus an invisible tail, while77
`folio/status` reported the inbox empty. Writers now put metadata on the78
header and indent the body; every mutation (complete, reopen, update, move,79
triage, sweep, clean) operates on whole blocks by their recorded line80
extents. Moves also now preserve completion — a completed task used to81
arrive reopened while still carrying its `completed:` date.83
- **Buried items are reported loudly, and id assignment freezes around84
them.** A new independent scanner — sharing no code with the parser, by85
design and by lesson — walks raw file text for `t-` ids and compares86
against what the parser actually produced. Anything written on disk but87
unreachable by every query is named, with its line, in `folio/inbox` and88
`folio/status`. `backfill-ids!` refuses to touch a file holding a buried89
id, so the phantom-mint path is closed from both ends. Ids inside90
terminated column-0 HTML comments are annotation and are not flagged; ids91
quoted inside a visible task's own text are that task's content, not a92
burial.94
- **Rewrites that would destroy bytes now refuse instead.** A header whose95
parse is lossy (stray braces around the metadata) loses part of the line96
on every re-serialize — completing one such task destroyed its tail, and a97
plain LISTING destroyed `{braces} in text}` via backfill (a defect that98
predates this release). Every in-place rewrite, move and triage now checks99
the header survives parse→serialize byte-identically and refuses by name100
when it does not; backfill skips such lines.102
- **Text containing tool-call markup is refused at the door.** `</text>`,103
`<parameter name=` and `</parameter>` can never be legitimate captured104
content — their presence means the client's tool call fell apart and its105
argument list leaked into a value (this buried two items live on106
2026-08-06). All string arguments of `inbox-add`, `task-add` and107
`task-update` are checked; nothing is stored on refusal.109
### Added111
- **`folio/migrate-buried`** — explicit recovery of legacy buried items.112
Dry-run by default: prints per-id actions and a unified diff, writes113
nothing. It converts the two understood burial shapes (headless checkbox114
head with a trailing-metadata body; a bare prose block ending in an id115
line), REFUSES what it cannot convert without guessing — a bare-id phantom116
head above the tail (recovery is a human MERGE of two tasks), and117
tool-fragment shapes whose metadata does not close on its own line — and118
verifies every conversion with the independent scanner before accepting119
it. A conversion deletes nothing: lines are reindented and one metadata120
substring relocated, line count preserved exactly.122
### Compatibility124
- Files written by 0.2.10 remain readable by older binaries: headers are125
unchanged as task lines, and indented bodies read as inert prose. Older126
binaries cannot SEE bodies (they list the header line only) and must not127
be used to mutate a store containing multi-line items — their line-based128
removal would strand bodies. Upgrade the binary before writing.130
## [0.2.9] - 2026-08-05132
### Fixed134
- **The note walk no longer has a size ceiling, and folio can start again.**135
`find-md-files-recursive` folded each directory's entries with `fold-right`,136
which is not tail recursive — it burned one interpreter stack frame per137
file. The maximum size of a folio was therefore set by the interpreter's138
stack rather than by anything folio chose, and past roughly **1011 entries in139
a single directory the walk blew the stack outright**.141
Because every tool that enumerates notes starts at that walk, the blast142
radius was the whole server. A fresh `folio serve` could not finish starting143
— it died during index reconciliation, after logging `Opened search index`144
and before `MCP server ready`. In an already-running server, startup had145
succeeded back when the corpus was smaller, so the process stayed healthy146
while individual calls began to fail: `note-links`, `status` and147
`note-rename` stopped answering, and everything that addresses a note by path148
(`note-read`, `note-create`, `note-patch`, the task tools) kept working. That149
partition is what the defect looks like from outside.151
The walk is now iterative, with an explicit worklist. Stack depth is the152
directory *nesting* depth — single digits — instead of the file count.153
Ordering is unchanged: `directory-list` order, depth-first, subdirectories154
expanded in place. Verified against a real corpus with all 2460 notes in one155
flat directory, 2.4x past the old ceiling, at no measurable cost (cold156
reindex 14.0s before, 14.2s after).158
Measured, on a copy of a real vault: 1010 entries fine, 1012 fatal. Six empty159
notes containing no links at all were enough to cross it, and the same160
threshold held whichever six were added — it was the file count, not the161
content and not the link graph.163
- **A tool that dies mid-call now says so instead of going silent.** When a164
handler raised through the internal CPS frames of a higher-order stdlib165
procedure, the native backend mis-unwound it: the dispatcher's guard never166
ran, no JSON-RPC response was written at all, and the caller blocked until167
its idle timeout while the server stayed up and answered later calls.169
Silence is the worst failure a caller can be handed — it is indistinguishable170
from *slow* and from *succeeded*, so the reasonable response is to retry, and171
a retry of a mutation whose disk write already landed double-applies. This172
was not hypothetical: a `note-rename` that never answered had already renamed173
the file and updated none of its crosslinks.175
Every tool handler is now wrapped. Ordinary failures are re-raised unchanged,176
so they still arrive as a proper JSON-RPC error envelope; stack exhaustion —177
the one condition measured to be unserialisable — is returned as an explicit178
report that also warns the change may be partially applied. It is a179
known-list rather than a catch-all: an unmeasured condition keeps existing180
behaviour instead of getting a guess.182
### Known issues184
- **`note-rename` is not atomic.** It renames the file first, then rewrites185
crosslinks across referring files with no transaction and no rollback, so a186
failure partway through leaves the note renamed and some or all crosslinks187
stale — with no error if the failure was the silent kind described above.188
The walk fix removes the cause seen so far, but not the exposure. Tracked189
separately.191
## [0.2.7] - 2026-08-02193
### Fixed195
- **Moving a task no longer renumbers it.** Ids are preserved in every196
direction — inbox → project, project → inbox, project → project, and197
`folio/inbox-triage`. An id you hold before a move is still valid after it,198
so `task-move` → `task-update <same-id>` chains correctly and a triage plan199
built from an inbox dump keeps working once the items are routed.201
This was never a design constraint. Ids are globally unique random hex with202
no per-project id space, so nothing required a new one on move.203
`project-add-task!` simply called `generate-task-id` unconditionally and204
ignored any id handed to it, while `move-task-in-store!` stripped the id out205
on the way in. The asymmetry gave it away as an oversight: project → inbox206
passed metadata through untouched, and only inbox → project destroyed the id.208
- **A task can no longer end up carrying two `id` entries.** Moving a task from209
a project back to the inbox prepended a fresh id without removing the210
existing one, leaving both on the line; lookups silently resolved the first.212
### Added214
- **`folio/task-move-batch`** — move many tasks in one call, either215
`{ids: [...], project: "x"}` for a shared target or216
`{moves: [{id, project}, ...]}` to fan a single call out across many217
projects. An entry with no `project` moves that task to the inbox.219
Returns a result line per id so partial failures are visible rather than220
swallowed, and the header counts successes rather than attempts. Because ids221
are now stable, the reported id is the one the task landed under.223
Triaging 227 inbox items on 2026-08-02 cost 226 individual round-trips, which224
is a large part of why that inbox went untriaged for four months.226
### Changed228
- Toolchain moved to sigil 0.18 (`sigil ^0.18`, `sigil-run ^0.18.0`,229
`sigil-http ^0.18.0`, `sigil-sxml ^0.15.0`), with runtime core resolving to230
0.18.1. `sigil-test` / `sigil-test-runner` remain pinned at `^0.17` (0.17.16):231
bumping them aborts the install because sigil-http, sigil-tls, sigil-crypto232
and sigil-web-styles each declare a dev-dependency on `sigil-test ^0.17` and233
the resolver unifies across the graph. That affects the test harness only,234
not the shipped runtime, and clears once those four repos move their dev-dep.236
### Known issues238
- **A capture containing newlines is still stored in a shape folio cannot read239
back.** `folio/inbox-add` accepts multi-line text, but the `{id: ...}`240
metadata is appended to the END of the text, so on a multi-line capture it241
lands on the item's last line. The parser sees only the first line, which has242
no id, and `backfill-ids!` then mints a fresh id onto it — the real item and243
its metadata become unreachable and a phantom one-line duplicate appears in244
its place. Six items were hidden this way between April and August 2026.246
This is not fixed in 0.2.7. A block-parsing fix was written and withdrawn: it247
cured the burial but introduced worse defects, because it inferred where an248
item ends and then let destructive operations act on that inference. The249
redesign makes an item's extent syntactic rather than inferred.251
Until it lands, **capture multi-line content as a note and reference it from252
a single-line inbox item**, rather than putting the body in the item itself.