fix: board name validation, TUI save races and review findings #79

Merged
jwarykowski merged 6 commits from fix/review-findings into master 2026-10-23 09:49:14 +00:00
Owner

Fixes from a review of the whole codebase. Each commit is scoped to one package and builds and tests on its own.

Security and data loss:

  • board delete|archive|rename never validated the board name, so board delete ../todo --force deleted the default board and ../../x reached any *.md file. They now validate, take the board lock, and refuse while $SHEPHERD_TODO_FILE is set (which used to make them act on the override file).
  • The TUI saved without the lock and overwrote any CLI or agent write made while it had unsaved edits. Saves now lock and, if the file changed on disk, merge by item id (todo.Merge; ours wins when both sides edited the same item).
  • edit <ref> status:done skipped the subtask cascade. The cascade now lives in todo.SetStatus/CycleStatus, and todo.ReconcileParent replaces five hand-rolled copies in cli/tui.
  • Save replaced a symlinked board with a plain 0600 file and didn't sync; a BOM lost the first item; CRLF broke dates and ids.

TUI: the note editor kept typing into the next row once a filter hid the item; renaming or archiving the open board dropped unsaved edits; undoing a sweep left items in both the board and archive.md (a sweep now saves and clears history); lane-view space on the detail page jumped cards; undo/redo didn't re-sort.

Smaller: reserved board names (default, todo, archive, *-archive), JSON errors for --board, stray-argument errors, control characters stripped from text output, (H) text and comma tags survive a save, month arithmetic clamps (Jan 31 + 1m = Feb 28), the stats trend axis uses the local day, long cursor rows truncate instead of wrapping.

Behaviour changes worth knowing:

  • Opening an id-less (legacy) board in the TUI writes it once to backfill ids, since the merge matches by id.
  • Undo history is cleared by an archive sweep.

Not addressed: narrow-width layout (key grid under 55 cols, many lanes, detail page scrolling) and the stats "open" vs first-configured-status split.

Fixes from a review of the whole codebase. Each commit is scoped to one package and builds and tests on its own. Security and data loss: - `board delete|archive|rename` never validated the board name, so `board delete ../todo --force` deleted the default board and `../../x` reached any `*.md` file. They now validate, take the board lock, and refuse while `$SHEPHERD_TODO_FILE` is set (which used to make them act on the override file). - The TUI saved without the lock and overwrote any CLI or agent write made while it had unsaved edits. Saves now lock and, if the file changed on disk, merge by item id (`todo.Merge`; ours wins when both sides edited the same item). - `edit <ref> status:done` skipped the subtask cascade. The cascade now lives in `todo.SetStatus`/`CycleStatus`, and `todo.ReconcileParent` replaces five hand-rolled copies in cli/tui. - `Save` replaced a symlinked board with a plain 0600 file and didn't sync; a BOM lost the first item; CRLF broke dates and ids. TUI: the note editor kept typing into the next row once a filter hid the item; renaming or archiving the open board dropped unsaved edits; undoing a sweep left items in both the board and `archive.md` (a sweep now saves and clears history); lane-view `space` on the detail page jumped cards; undo/redo didn't re-sort. Smaller: reserved board names (`default`, `todo`, `archive`, `*-archive`), JSON errors for `--board`, stray-argument errors, control characters stripped from text output, `(H) text` and comma tags survive a save, month arithmetic clamps (Jan 31 + 1m = Feb 28), the stats trend axis uses the local day, long cursor rows truncate instead of wrapping. Behaviour changes worth knowing: - Opening an id-less (legacy) board in the TUI writes it once to backfill ids, since the merge matches by id. - Undo history is cleared by an archive sweep. Not addressed: narrow-width layout (key grid under 55 cols, many lanes, detail page scrolling) and the stats "open" vs first-configured-status split.
SetStatus and CycleStatus now push a parent's done change onto its subtasks, so edit status:done matches done. ReconcileParent replaces the parent checks the CLI and TUI each did by hand. Merge folds another writer's board into ours by item id, for the TUI's save. Relative months clamp to the month's last day and are bounded, tags drop commas, and an unconfigured status falls into the first lane.
Delete, archive and rename (old name) never validated the name, so ../todo reached the default board and any *.md outside boards/. They now validate, take the board lock and refuse under $SHEPHERD_TODO_FILE, as does ResolveBoard for a named board. default, todo, archive and *-archive are reserved for new boards. Save writes through a symlink, keeps the mode and syncs before the rename. Load strips a BOM and CRLF. Text that looks like a priority marker is escaped. Board dirs unquote properly and AppendArchive starts on a fresh line.
Board errors under --json go to stdout as JSON. list, boards, stats and watch reject positional arguments instead of ignoring later flags. Text output drops control characters so item text can't drive the terminal. watch reads the mtime before its baseline load, an empty id matches nothing, and the stats trend axis uses the local day rather than UTC midnight.
Every TUI save now takes the board lock and, if the file changed on disk, merges the other writer's edits by id instead of overwriting them. External reloads wait for the list and keep the cursor on its item. The note editor keeps its item when a filter hides it, rename/archive of the open board save first, a sweep saves and clears undo history, lane-view space on the detail page stays on the card, undo/redo re-sort, the picker counts boards once, and opened links are reaped. The cursor row truncates instead of wrapping and tiny heights keep a row.
docs: reserved board names, the override, and sweep undo
All checks were successful
pull-request / lint-and-test (pull_request) Successful in 1m11s
033b9bbf38
Merge branch 'master' into fix/review-findings
All checks were successful
pull-request / lint-and-test (pull_request) Successful in 1m9s
5f84c3e1ef
jwarykowski deleted branch fix/review-findings 2026-10-23 09:49:15 +00:00
Sign in to join this conversation.
No description provided.