AtlatestRepositoryfolio
1# Changelog
2
3All notable changes to **folio** are documented in this file.
4
5The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
8## [0.2.11] - 2026-08-11
9
10### Fixed
12- **An unknown argument is refused instead of ignored.** `folio --typo` used to
13 start the MCP server and exit 0, so a mistyped flag looked exactly like a
14 successful run. Unknown arguments now exit 1 with a usage message naming what
15 is actually valid, and `--help` / `-h` work with no environment configured at
16 all (previously `--help` required `FOLIO_ROOT`).
18- **`--version` and `--help` no longer bypass validation.** They were scanned
19 for across the whole argument list before validation ran, so
20 `folio migrate-buried --version` printed a version and exited 0 — the same
21 silent success this release exists to remove, reachable through the fix for
22 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`", after
26 which the port finder started an HTTP server nobody asked for and the logger
27 wrote a file literally named `--web`, all at exit 0. A validator that blesses
28 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 real
32 `--web-port` invocation. Caught by a test arm that runs the actual deployment
33 command rather than a synthetic one.
35### Added
37- `--stdio` is accepted and ignored, as a documented compatibility no-op. It
38 names the transport folio already uses, and it is a common MCP client
39 convention.
41- `test/test-cli-contract.sh`, 23 arms, each verified individually falsifiable
42 against a stubbed CLI. Some arms deliberately run live deployment invocations
43 rather than defect cases; they are marked as such because that is what caught
44 the `exact?` runtime failure, and they are the opposite of redundant.
46### Known
48- `--log-level=trace` is refused with a correction pointing at
49 `--log-level trace`. The consumers match flag names exactly and never split
50 on `=`, so accepting the joined spelling would start folio and silently
51 ignore the flag. Accepting both spellings honestly requires folding the three
52 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 always
56 worked by accident. Live deployments depend on it.
58## [0.2.10] - 2026-08-06
60### Fixed
62- **A multi-line capture round-trips: an item's extent is SYNTACTIC, and the
63 burial class is closed.** A task is its checkbox header line — metadata at
64 the end of that line — plus the run of following lines indented at least
65 two columns past it (one blank line is allowed between body lines when
66 bracketed by indented ones). Extent is decided by indentation alone, never
67 by content: no heading, fence or hashtag heuristics anywhere, so a removal
68 can only ever take lines indented under the item's own header, and
69 unindented prose, headings and config fragments are untouchable by task
70 operations *by construction*.
72 This ends the mechanism that buried at least 22 items between April and
73 August 2026: the old writer appended `{id: ...}` after the whole text, a
74 newline put it on the capture's LAST line, the line-oriented parser saw
75 only the headless first line, and `backfill-ids!` then minted a phantom id
76 onto it — one capture became a phantom head plus an invisible tail, while
77 `folio/status` reported the inbox empty. Writers now put metadata on the
78 header and indent the body; every mutation (complete, reopen, update, move,
79 triage, sweep, clean) operates on whole blocks by their recorded line
80 extents. Moves also now preserve completion — a completed task used to
81 arrive reopened while still carrying its `completed:` date.
83- **Buried items are reported loudly, and id assignment freezes around
84 them.** A new independent scanner — sharing no code with the parser, by
85 design and by lesson — walks raw file text for `t-` ids and compares
86 against what the parser actually produced. Anything written on disk but
87 unreachable by every query is named, with its line, in `folio/inbox` and
88 `folio/status`. `backfill-ids!` refuses to touch a file holding a buried
89 id, so the phantom-mint path is closed from both ends. Ids inside
90 terminated column-0 HTML comments are annotation and are not flagged; ids
91 quoted inside a visible task's own text are that task's content, not a
92 burial.
94- **Rewrites that would destroy bytes now refuse instead.** A header whose
95 parse is lossy (stray braces around the metadata) loses part of the line
96 on every re-serialize — completing one such task destroyed its tail, and a
97 plain LISTING destroyed `{braces} in text}` via backfill (a defect that
98 predates this release). Every in-place rewrite, move and triage now checks
99 the header survives parse→serialize byte-identically and refuses by name
100 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 captured
104 content — their presence means the client's tool call fell apart and its
105 argument list leaked into a value (this buried two items live on
106 2026-08-06). All string arguments of `inbox-add`, `task-add` and
107 `task-update` are checked; nothing is stored on refusal.
109### Added
111- **`folio/migrate-buried`** — explicit recovery of legacy buried items.
112 Dry-run by default: prints per-id actions and a unified diff, writes
113 nothing. It converts the two understood burial shapes (headless checkbox
114 head with a trailing-metadata body; a bare prose block ending in an id
115 line), REFUSES what it cannot convert without guessing — a bare-id phantom
116 head above the tail (recovery is a human MERGE of two tasks), and
117 tool-fragment shapes whose metadata does not close on its own line — and
118 verifies every conversion with the independent scanner before accepting
119 it. A conversion deletes nothing: lines are reindented and one metadata
120 substring relocated, line count preserved exactly.
122### Compatibility
124- Files written by 0.2.10 remain readable by older binaries: headers are
125 unchanged as task lines, and indented bodies read as inert prose. Older
126 binaries cannot SEE bodies (they list the header line only) and must not
127 be used to mutate a store containing multi-line items — their line-based
128 removal would strand bodies. Upgrade the binary before writing.
130## [0.2.9] - 2026-08-05
132### Fixed
134- **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 per
137 file. The maximum size of a folio was therefore set by the interpreter's
138 stack rather than by anything folio chose, and past roughly **1011 entries in
139 a single directory the walk blew the stack outright**.
141 Because every tool that enumerates notes starts at that walk, the blast
142 radius was the whole server. A fresh `folio serve` could not finish starting
143 — it died during index reconciliation, after logging `Opened search index`
144 and before `MCP server ready`. In an already-running server, startup had
145 succeeded back when the corpus was smaller, so the process stayed healthy
146 while individual calls began to fail: `note-links`, `status` and
147 `note-rename` stopped answering, and everything that addresses a note by path
148 (`note-read`, `note-create`, `note-patch`, the task tools) kept working. That
149 partition is what the defect looks like from outside.
151 The walk is now iterative, with an explicit worklist. Stack depth is the
152 directory *nesting* depth — single digits — instead of the file count.
153 Ordering is unchanged: `directory-list` order, depth-first, subdirectories
154 expanded in place. Verified against a real corpus with all 2460 notes in one
155 flat directory, 2.4x past the old ceiling, at no measurable cost (cold
156 reindex 14.0s before, 14.2s after).
158 Measured, on a copy of a real vault: 1010 entries fine, 1012 fatal. Six empty
159 notes containing no links at all were enough to cross it, and the same
160 threshold held whichever six were added — it was the file count, not the
161 content and not the link graph.
163- **A tool that dies mid-call now says so instead of going silent.** When a
164 handler raised through the internal CPS frames of a higher-order stdlib
165 procedure, the native backend mis-unwound it: the dispatcher's guard never
166 ran, no JSON-RPC response was written at all, and the caller blocked until
167 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 indistinguishable
170 from *slow* and from *succeeded*, so the reasonable response is to retry, and
171 a retry of a mutation whose disk write already landed double-applies. This
172 was not hypothetical: a `note-rename` that never answered had already renamed
173 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 explicit
178 report that also warns the change may be partially applied. It is a
179 known-list rather than a catch-all: an unmeasured condition keeps existing
180 behaviour instead of getting a guess.
182### Known issues
184- **`note-rename` is not atomic.** It renames the file first, then rewrites
185 crosslinks across referring files with no transaction and no rollback, so a
186 failure partway through leaves the note renamed and some or all crosslinks
187 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. Tracked
189 separately.
191## [0.2.7] - 2026-08-02
193### Fixed
195- **Moving a task no longer renumbers it.** Ids are preserved in every
196 direction — inbox → project, project → inbox, project → project, and
197 `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 plan
199 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 with
202 no per-project id space, so nothing required a new one on move.
203 `project-add-task!` simply called `generate-task-id` unconditionally and
204 ignored any id handed to it, while `move-task-in-store!` stripped the id out
205 on the way in. The asymmetry gave it away as an oversight: project → inbox
206 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 from
209 a project back to the inbox prepended a fresh id without removing the
210 existing one, leaving both on the line; lookups silently resolved the first.
212### Added
214- **`folio/task-move-batch`** — move many tasks in one call, either
215 `{ids: [...], project: "x"}` for a shared target or
216 `{moves: [{id, project}, ...]}` to fan a single call out across many
217 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 than
220 swallowed, and the header counts successes rather than attempts. Because ids
221 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, which
224 is a large part of why that inbox went untriaged for four months.
226### Changed
228- 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 to
230 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-crypto
232 and sigil-web-styles each declare a dev-dependency on `sigil-test ^0.17` and
233 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 issues
238- **A capture containing newlines is still stored in a shape folio cannot read
239 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 it
241 lands on the item's last line. The parser sees only the first line, which has
242 no id, and `backfill-ids!` then mints a fresh id onto it — the real item and
243 its metadata become unreachable and a phantom one-line duplicate appears in
244 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: it
247 cured the burial but introduced worse defects, because it inferred where an
248 item ends and then let destructive operations act on that inference. The
249 redesign makes an item's extent syntactic rather than inferred.
251 Until it lands, **capture multi-line content as a note and reference it from
252 a single-line inbox item**, rather than putting the body in the item itself.