fix(list): include scheduled projects in today, upcoming and anytime - #205
Merged
Merged
Conversation
Things schedules a project exactly as it schedules a to-do: start, startBucket, startDate and todayIndex all live on the project row, and the app lists the project itself in Today, Upcoming and Anytime. The CLI's view filters were pinned to t.type = 0, so a project put in Today was invisible to `things today` and an agent driving the CLI had no way to know it was there. The three scheduling views now select t.type IN (0, 1). Everything else about them is unchanged: repeating project templates and the to-dos inside them stay out, trashed projects stay out, headings stay out, and --include-completed keeps its Things-parity meaning for project rows too. inbox, someday, logbook, trash, deadlines and the bare filter view stay to-do only. A project row is already marked `(project)` in plain output and already carries `"type": 1` in JSON, so nothing new was needed to tell the two kinds apart. One output fix was: the today ordering sorts a project's own to-dos straight after the project row, and their group header restated the title on the line above, so that header is now folded into the row. --project can never match a project row, since a project has no parent project; --area still finds it, because a project carries its own area. Closes #201
ryanlewis
force-pushed
the
fix/issue-201-today-projects
branch
from
September 10, 2026 10:05
bd061cd to
16a1345
Compare
This was referenced Sep 10, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #203 ## What changed Docs only. `things projects -j` has reported `taskCount` and `openCount` since the counts were added to drive the progress icon, but nothing said what they count, so the only documented way to find a project whose work had all landed was one `things list` per project. This documents both fields and adds a jq recipe for the daily reconcile. - `internal/skill/SKILL.md` — a bullet in "Output and `--json`", two comment lines on the `things projects` reference line, and a "projects with no open to-dos" recipe in "Common flows". - `docs/content/commands.md` — a block in "Listing", after the paragraph about projects carrying `start` / `startBucket` / `startDate` / `deadline`. - `docs/content/agents.md` — a bullet in "Script it with `--json`" and a line in the recipe block. No new flags, no new JSON fields, no change to plain-text output, no Go code touched. The user decided to keep the existing field names rather than the `openTodoCount` / `completedTodoCount` the issue proposed, and to leave the `●` progress icon as the human-output answer. ## Why these semantics, and how they were verified The fields come straight from Things' own columns `untrashedLeafActionsCount` and `openUntrashedLeafActionsCount`. Rather than infer the meaning from the column names, I checked them against real data with `sqlite3 -readonly` on the live database and on a backup snapshot, recounting each project's children and comparing: | Database | Projects compared | Match | Diverge | | --- | --- | --- | --- | | Live | 82 | 82 | 0 | | Backup 2026-09-01 | 63 | 63 | 0 | A deliberately wrong value matched zero rows, confirming the comparison detects divergence rather than passing vacuously. What that established, and what the docs now say: - **To-dos under a heading are included.** A to-do filed under a heading has `project` NULL and points only at `heading`, so a correct recount has to union direct children with the children of the project's headings. Ignoring headings gives the wrong total for exactly the 7 projects here that have them. - **Heading rows themselves are never counted**, nor are trashed to-dos or checklist items. The trashed exclusion is real rather than theoretical: counting trashed to-dos would change the total for 13 projects. ## Three caveats, each documented Each one can mislead someone acting on the recipe: - `taskCount - openCount` counts to-dos that are no longer open, which means **completed or cancelled**. 23 projects here contain at least one cancelled to-do, so a project whose to-dos were all cancelled matches the same filter. - Under `--completed` the `●` icon marks **any** completed project, including one with no to-dos at all, so the icon and the filter are not identical there. Verified against a fixture database. - A repeating to-do makes a project **unreachable** by the filter. Things counts the hidden template row as an open to-do and a template never completes. Measured on the backup snapshot: the project holding two templates reports 68/21, which matches the recount only when the templates are included; excluding them gives 66/19. All open contributions came from the template rows themselves, with no open generated instance. `things list -p` hides that template, so the two disagree, and that is now called out. ## How it was verified - `make test` and `make lint` green (lint reports 0 issues; the warning names a stale sibling worktree unrelated to this change). - All three jq recipes were run as written. - Against a fixture database, the filter selects the all-done project and correctly excludes both the half-done and the empty one. - `hugo` builds the site and renders the new sections. - The new text ships in the binary, confirmed via `things skill show`. Rebased on `main` after #201 (#205) landed. It also appended a bullet to the same SKILL.md list, so both bullets are kept, that one first.
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #206 ## What changed `someday` and `logbook` now list projects alongside to-dos, the same rule #205 applied to `today`, `upcoming` and `anytime`. A project is deferred exactly as a to-do is deferred, and completed exactly as a to-do is completed, so the app shows the project itself in Someday and in the Logbook. The CLI's two view filters were pinned to `t.type = 0`, so those project rows were excluded by construction, not by data. Both views now select `t.type IN (0, 1)` through the existing `todoOrProject` constant. Nothing else about them moved: repeating templates and the to-dos inside them stay out of `someday`, trashed rows stay out of both, and headings (type 2) stay out. `logbook` keeps its literal reading of the database — it is still one of the two views that report trashed-project children and templates as they are stored. `inbox`, `trash`, `deadlines` and the bare-filter view stay to-do only. ## Telling a project row from a to-do No new marker was needed, and no output change either. `printTasks` already appends a dim `(project)` for `type = 1` rows, and the header-folding fix from #205 is view-agnostic, so a completed project sitting directly above its own completed to-dos in the Logbook prints its title once. ## Verified against the real database Read from a checkpointed copy of the live Things database, compared against the app's own list membership via AppleScript (`id of to dos of list "Logbook"` / `list "Someday"`), uuids not titles. - `logbook`: 36 project rows appear that the previous build did not return, and all 36 are in the app's Logbook list. No false positives. Total rows go from 1154 to 1190. The newest is `L16wNJD3D84AEJwr14QWni` "OQ-11 — Salesforce to AWS threat model", which is also the first row of the app's Logbook. - `someday`: no project currently sits in Someday, so this was checked by inserting one row into a copy of the real database. `PROBE-someday-project` is absent before the change and present after, with `(project)` in plain output. The six rows the app does show in Someday all have `start = 2`, `startDate IS NULL`, `status = 0` and no parent project, which is the filter minus the type pin. ## How tested `make test` (race) and `make lint` (0 issues) both pass. New tests, all confirmed to fail with the filters reverted: - `internal/db`: a deferred project appears in `someday` and a completed project in `logbook`, both with `type = model.TypeProject`; a Someday-start repeating project template, the to-dos inside it, trashed projects and headings stay out; the Logbook order still keys on `stopDate` with a project in it; `--project` never matches a project row while `--area` does. - `cmd/things`: `things someday` and `things logbook` end to end — JSON carries the project row, plain text marks it `(project)` and does not mark to-dos. No new `internal/output` test. The header-folding behaviour these views can hit is already covered by `TestPrintTasksNoRepeatedProjectHeader`, which is written against `Print` rather than a view. ## Docs `internal/skill/SKILL.md` (the "Output and `--json`" list and the `things list` block), `docs/content/commands.md` (Listing) and `docs/content/agents.md` — extending the wording #205 added rather than adding a parallel paragraph. Two knock-on corrections in both SKILL.md and commands.md. They said `trash` and `logbook` are to-do lists, so a project template never shows in either; `logbook` now carries projects, so that is only true of `trash`. And "the other views stay to-do only" now names which ones, since `repeating` has carried project templates since #165. The comment on the header fold in `internal/output/output.go` named only the three views from #205 and said a project's to-dos sort straight after it. It now names all five and says the fold applies where the view's order puts them there, which is what the code actually tests for. ## Not addressed here Two Someday/Logbook parity gaps found while verifying, both pre-existing and unrelated to project rows: - The `logbook` filter is `t.status = 3`, so cancelled items never appear, but the app's Logbook shows them. 138 cancelled to-dos and 6 cancelled projects are missing. - `things someday` returns 8 to-dos that the app's Someday list does not show. Each is a Someday to-do inside an Anytime project; the app keeps those inside the project rather than in the global Someday list. `trash` and `deadlines` are the two views left pinned to `t.type = 0`. Trashing a project in the app leaves no CLI route to see it, since `things projects` filters trashed rows and `trash` excludes projects. And `things deadlines` skips a project with a deadline, though `things projects` reports `deadline` since #204. Both are behaviour changes outside this issue and want their own tests.
This was referenced Sep 10, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #212 ## What changed `trash` now lists projects alongside to-dos, the same rule #205 and #209 applied to the other views. Trashing a project in Things puts the project row itself in the app's Trash, and `things projects` filters trashed rows, so a trashed project was visible nowhere in the CLI — excluded by construction, not by data. The view now selects `t.type IN (0, 1)` through the existing `todoOrProject` constant. Nothing else about it moved: headings (type 2) stay out, and `trash` keeps the literal reading of the database it shares with `logbook`, so the children of a trashed project and any repeating template that ends up in the bin still list as they are stored. `inbox` and `deadlines` stay to-do only. #213 covers `deadlines`. ## Telling a project row from a to-do No output change was needed. `printTasks` already appends a dim `(project)` for project rows, and since #214 the JSON carries `"type": "project"`. ## Verified against the real database Read the live database with `sqlite3 -readonly`, and compared against the app's own list membership via AppleScript (`id of to dos of list "Trash"`), by uuid rather than title. | | count | |---|---| | App's Trash list | 162 | | CLI `list trash` before | 148 | | CLI `list trash` after | 165 | | Trashed project rows gained | 17 | All 17 project rows the CLI now returns are in the app's Trash, and nothing the app shows is missing from the CLI. No false positives: the database holds no trashed headings and no trashed repeating templates, so neither could leak in. The 3 rows the CLI returns that the app does not are unchanged by this PR. All three are to-dos whose parent project is itself trashed; the app shows the trashed project as one row and folds its children into it. They were already returned before this change, under the `viewsIncludingTrashedProjects` decision from #155, and narrowing that is a separate question from this issue. ## Tests Three tests in `internal/db/tasks_test.go` over a new `seedTrashedProjects` helper: trashed projects of both statuses are listed and carry `model.TypeProject`; headings and live rows of either kind stay out; `--area` matches a trashed project through its own area while `--project` cannot. `TestTrashAndLogbookKeepTrashedProjectChildren` seeds a trashed project as the parent of its fixture, so its trash expectation gained the project row it was already seeding. Its subject, that the child is kept, is unchanged. Docs updated in `internal/skill/SKILL.md`, `docs/content/commands.md` and `docs/content/agents.md`: the sentence naming which views carry projects, and the repeating-template note that said a project template never reaches `trash`.
This was referenced Sep 10, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #213 ## What changed `deadlines` now lists projects alongside to-dos. A project takes a deadline exactly as a to-do does, `things projects -j` has reported it since #204, and agents.md advertises this view as the way to sweep what is due — so an agent following the docs missed every project deadline. The view was pinned to `t.type = 0`, so those rows were excluded by construction, not by data. The view now selects `t.type IN (0, 1)` through the existing `todoOrProject` constant, the same rule #205, #209 and #216 applied to the other views. Its `ORDER BY t.deadline ASC` is unchanged, so project rows fall in among the to-dos by date rather than forming a block of their own. `--on`, `--from` and `--to` already filter `t.deadline` on this view and now filter project rows by their own deadline. `inbox` is now the only named view that stays to-do only, which is a property of the Inbox rather than a limitation: an inbox item has not been filed anywhere yet, so it is never a project. A bare `-p`/`-a`/`-t` filter with no view named is the other exception, and the docs now say so explicitly — it routes to the internal catch-all view, which is still pinned to to-dos. ## Verification: negative only, and worth being plain about There is nothing on this database that exercises the change, and I would rather say so than imply a check I did not make. The app has no Deadlines list to compare against, so the fallback is `things projects -j`, which reports no project deadlines at all. Reading the live database with `sqlite3 -readonly` shows why. Seven projects carry a deadline, and every one is already excluded by the `status = 0 AND trashed = 0` clause this PR does not touch: | why excluded | count | |---|---| | completed or cancelled | 4 | | trashed | 3 | | would list | 0 | So `things deadlines` returns the same 13 to-do rows before and after this change, and the SQL the new filter generates selects those same 13. What I have verified is that no project is wrongly admitted and none is wrongly held back for a reason this PR introduces. What I have not verified is a project deadline appearing in the app and then in the CLI, because no such project exists here. The tests below cover what the live data cannot. ## Tests Six tests in `internal/db/tasks_test.go` over a new `seedDeadlines` helper. The fixture interleaves a project between two to-dos by deadline while running `"index"` against that order, so an ordering that fell back to `"index"` or grouped by type would fail: - projects with a deadline are listed and carry `model.TypeProject` - the project sorts between the two to-dos, by deadline - a project with no deadline, a completed one, a trashed one and a heading all stay out - `--on` matches a project row by its own deadline - `--area` finds a project through its own area; `--project` cannot match one - a repeating project template with a deadline, and the to-do inside it, stay out — the one exclusion path that comes from `viewsIncludingTemplates` rather than the view filter ## Docs `internal/skill/SKILL.md`, `docs/content/commands.md`, `docs/content/agents.md` and `README.md`: the view lists now say every named view except `inbox` carries projects, and name the bare-filter exception so an agent knows to name a view when project rows matter. The README gains the general rule it never stated, so its `deadlines` table row no longer reads as though that view is the only one carrying projects. `internal/output/output.go` had a comment enumerating the old set of views.
This was referenced Sep 10, 2026
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #222 ## What changed The internal catch-all view — the one a bare `--project`/`--area`/`--tag` filter routes to when no view is named — was still pinned to `t.type = 0` while every named view except `inbox` had been widened to `todoOrProject` (#205, #209, #216, #220). It now uses the same set. `--project` is unaffected in practice: a project has no parent project of its own, so `p.uuid` never matches a project row and the filter still returns a project's contents. The visible change is for `--area` and `--tag`. ## Why An agent sweeping an area with `things --area Work -j` got none of that area's projects, which is exactly the miss #222 describes. Parity with what Things.app shows in the matching list is the project's stated goal. ## How verified Measured by uuid against Things.app on 10 Sep 2026. For one area the app reports four projects; the CLI returned zero project rows before this change and returns the same four uuids after. `things --project <uuid> -j` still returns zero project rows. Tests added in `internal/db/tasks_test.go` cover the widened set, the `--area`/`--tag`/`--project` split, and the exclusions that must not widen with it — headings, trashed projects, repeating project templates and the to-dos inside them. Four existing tests asserted the old to-do-only behaviour and now assert the project rows: two area filters, the catch-all grouping order, and the CLI-level default-view test. `make test` and `make lint` are clean. ## Docs The paragraph naming the bare-filter exception was rewritten once, in `internal/skill/SKILL.md`, `docs/content/commands.md`, `docs/content/agents.md` and the README. It now states the `--project` case rather than a whole-view exception.
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
Closes #217 ## What changed Three changes to the two views the app arranges differently from the CLI, all measured against it on 10 Sep 2026 by uuid. **`anytime` no longer lists project rows.** This is a revision of what #205 and #216 widened, for this view alone. Every active project is trivially "anytime", so listing them all buries the to-dos; the app uses the project as the group header above its own to-dos instead. Measured: the app's Anytime held **none** of the 23 active projects as a row, and all 107 of their to-dos, which the CLI already matched exactly. `today` keeps its project rows, and so do `upcoming` and `someday`, where a project has actually been put somewhere. **`anytime` is ordered to match that arrangement:** items filed nowhere lead, then areas in area order, and inside an area its own loose to-dos before those of its projects. It listed in bare `t."index"` order before, which scattered each project's to-dos and made the rendered group header repeat. **`upcoming` reads by date**, then by the within-day `todayIndex` the app also keys Today on. Bare `t."index"` order interleaved the dates. ## Getting the keys right took two corrections Worth recording, because both were wrong in ways that still produced plausible-looking output: 1. Sorting on the area index alone put the unfiled items **last**. Things writes its indexes negative, so the COALESCE default of 0 that stands for "not filed here" outranks every real area. 2. Sorting on the project index alone put each area's project to-dos **before** its own loose ones, for the same reason. The app does the opposite. Each key is therefore a CASE rather than a plain index. ## How verified By uuid against the app on live data: | view | rows | order vs the app | | --- | --- | --- | | anytime | 105 shared | identical for all 105 | | upcoming | 30 | identical for all 30, no membership difference either way | Plain output now renders the app's shape: unfiled items, then each area, then each project name once as a group header above its to-dos. ## Deliberately left alone `today` and the catch-all `--project`/`--area`/`--tag` ordering are byte-identical to before. An earlier revision of this branch had factored their ORDER BY into shared constants; I backed that out, because neither was asked to change and `today` is already at parity. Two ordering gaps are recorded in the comments rather than guessed at, both raised by `/code-review`: - A to-do inside a project that carries **no area** falls to the COALESCE default and sorts after every area. There is no such project in this data, so the app's answer could not be measured. - `upcoming` orders on a nullable `todayIndex`, and SQLite sorts NULL first. No upcoming row carries a NULL, and the `today` ordering has the same shape, so changing one and not the other would invent a rule rather than match one. One consequence worth a decision, not fixed here: because `upcoming` now orders by date, a project with to-dos on several days gets its group header re-printed per day, since the CLI has no day header to group under the way the app does. ## Docs The "which views carry projects" paragraph was rewritten once in `internal/skill/SKILL.md`, `docs/content/commands.md`, `docs/content/agents.md` and the README: every named view except `inbox` and `anytime`. Each says why `anytime` is the exception and points at `things projects` for sweeping projects themselves. `make test` and `make lint` are clean.
ryanlewis
added a commit
that referenced
this pull request
Sep 10, 2026
…#264) Closes #243 ## What changed `printTasks`, `printProjects`, `printAreas` and `printTags` each declared a local row struct, measured every column with `lipgloss.Width` and padded with `padCol` before joining with a gap. Four copies of the same twenty lines, and the copy inside `printTasks` also carried the terminal-width column dropping. There is now one `table` in `internal/output/table.go`. Rows of cells go in, measured and padded lines come out. The column dropping is a `dropOrder` field naming the columns that may be given up, in the order they go, rather than a pair of booleans and a hand-written width sum. The rule the four copies shared is written down once: every column is padded to its widest cell except the last one declared, which goes out as it is. Dropping a column does not move that rule, which is why a narrow listing still pads its title column. That is the behaviour the copies had; it is now stated rather than implied. `printTasks` keeps its group-header logic, which no table can own, because it has to write a header between two rows. `table.lines()` hands back one line per row for exactly that, so the header fold from #205 is untouched. ## One place each for the kind word and the status text `t.Type == model.TypeProject` was branched on six times across `output.go` and `agent.go`. It is now `isProject(t)`, and the word that reaches the output is `kindWord(t)`, which takes "task" and "project" from model's type codec, the same source the JSON `type` field renders from. `kindWord` is deliberately not `TaskType.String()`. A heading, or a code Things has yet to write, reads as a task on these surfaces, which is what the CLI has always shown. The doc comment says so. `statusText` no longer keeps its own switch over the three statuses. It capitalises `model.Status.String()`, so the detail block, the JSON and the agent brief cannot drift apart, and the unrecognised code still renders "Unknown" because the codec already falls back to "unknown". `checklistLine` takes its "(cancelled)" from the same source. `statusIcon` stays as it is. `[ ]`, `[~]` and `[x]` are glyphs rather than the status words, and Markdown's two-state checkbox in a brief is a third thing again. Folding those together would have meant inventing a rendering, which this change is not for. ## Proof the output did not change The golden test went in first, in its own commit, before anything was refactored. `TestRenderGolden` renders a fixed fixture through every public entry point: `Print` for tasks, projects, areas and tags and for a `*model.Task`, `PrintTaskList` with and without a view label, `PrintTaskWithChecklist` for a to-do, a project and a repeating item, `PrintHint`, and `PrintAgentBrief` for a task, an open project, an empty project, a closed project, a repeating item and a note carrying its own code fence. Empty listings are cases too. Each case renders at both ends of the colour profile, so the ANSI is pinned as well as the text, and the task listings render at three terminal widths so both drop steps are covered. **The refactor commit does not touch `testdata/render_golden.txt`.** That is the claim, and the file's absence from that commit's diff is the evidence. The golden values were captured from unmodified rendering code. The one production change in that first commit is the seam the test needs: `termWidth` becomes a var, like `nowFn` beside it, so a test can pin a width instead of depending on how the test binary's stdout happens to be attached. The body is unchanged. `origin/main` moved to 61542c3 while this was in review, but #260 does not touch `internal/output`, so the captured bytes are still the bytes of the base this merges onto. Against the live database, 226 invocations of the built binary are byte-identical to `origin/main`, 1.8 MB of output in all: nine views plain, with `--include-completed`, as JSON, with `--color always` and with `--no-hints`; `projects`, `areas` and `tags` in the same three forms; and 43 real items through `show`, `show --agent`, `show --json` and `show --color always`. Exit codes are compared too. The database was read only through the binary. Every line in the golden file ends in `|`, because a padded column emits trailing spaces an editor or a hook would otherwise eat, and ANSI escapes are written `\e` so the file stays readable and greppable. ## Review `/code-review --fix` at high effort found one defect, and it was mine, in the golden test rather than in the refactor. `TestRenderGolden` returned early when the document matched and otherwise reported from inside its per-case loop. A difference the case splitter does not see, such as trailing whitespace or text before the first header, both of which it drops, left the loop with nothing to report and the test passed with `got != want`. Appending two newlines to the golden file reproduced it. For a test whose whole job is pinning bytes that is the one outcome it must not have, so it now fails loudly and names the regeneration command. The review separately re-derived the extraction rather than trusting the tests: that `table.fits` reproduces the old arithmetic including the two-step drop and the absent re-check after the last drop, that the last declared column is the one left unpadded whether or not it survived, that `kindWord` and `statusText` are equivalent to the switches they replace down to the unknown-code arm, and that the golden file is the same blob in both commits. It also confirmed the golden output is timezone-stable, checked under `TZ=Pacific/Kiritimati` and `TZ=UTC`, and that making `termWidth` a var introduces no race. ## Noted, not fixed `TestListQueryGoldenSQL` in `internal/db` has the same gap the review found here: it reports only from inside its per-case loop, so a difference outside a `### view=` block would pass. It is a separate package and a separate change, so it is left alone. ## Not touched No command surface moves, so `internal/skill/SKILL.md` and the pages under `docs/content/` have nothing to say about this. The `(project)` marker, the group header fold, the project icons and the agent brief's wording are all documented contracts and all unchanged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #201
What changed
today,upcomingandanytimenow list projects alongside to-dos. Things schedules a project exactly as it schedules a to-do —start,startBucket,startDateandtodayIndexall live on the project row — and the app shows the project itself in those three lists. The CLI's view filters were pinned tot.type = 0, so a project put in Today was excluded by construction, not by data.The three views now select
t.type IN (0, 1). Nothing else about them moved: repeating project templates and the to-dos inside them stay out, trashed projects stay out, headings (type 2) stay out, and--include-completedkeeps its Things-parity meaning for project rows too.inbox,someday,logbook,trash,deadlinesand the bare-filter view stay to-do only.Telling a project row from a to-do
No new marker was needed.
printTasksalready appends a dim(project)after the title fortype = 1rows (added forthings repeatingandthings search), and JSON already carried"type". Atodaylisting now reads:One output fix went with it: the
todayordering sorts a project's own to-dos straight after the project row, so their project group header restated the title on the line immediately above. That header is now folded into the row. Unrelated project groups still get their header, and the group state still advances so later groups break as before.Filters
A project has no parent project, so
--project Xnever matches a project row — it narrows to the project's contents, which is what it means.--areastill finds it, because a project carries its own area. Both are covered by tests.Why the ordering still holds
The
todayORDER BY keys onCOALESCE(p."index", 0), which a project row leaves at 0, so it lands in its area's group next to that area's unparented to-dos and ahead of the to-dos of any project with a non-zero index. Its owntodayIndexthen places it, the same signal the app orders Today by. Checked against the live database: Things setstodayIndexReferenceDateon scheduled project rows exactly as it does on to-dos, so theDESCleg of the sort has no NULLs to sink.How tested
make test(race) andmake lint(0 issues) both pass. New tests:internal/db: projects appear in all three views withtype = 1; templates, template children, trashed projects and headings stay excluded;--projectdrops project rows while--areakeeps them; the today order is deterministic with a project in it;--include-completedcarries a completed project.cmd/things:things todayend to end — JSON carries the project with"type": 1, plain text marks it(project)and does not mark to-dos.internal/output: a project row followed by its own to-dos prints the title once.Also verified against the real Things database from the issue:
things todaynow returnsX28bim63xxvGvWLncbfd7f"Runbook audit", which it did not before.Docs
internal/skill/SKILL.md(the "Output and--json" list and thethings listblock),docs/content/commands.md(Listing) anddocs/content/agents.md— including the bulk-reschedule example, which now filtersselect(.type==0)so a scheduled project is not handed tothings edit.Known gap, not addressed here
somedayandlogbookremain to-do only, though the app's Someday and Logbook lists both show projects. Out of scope for #201; worth a follow-up issue.