You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[roadmap] Track the verified missing-capability inventory #168
A third-party inventory of TinyCode's missing capabilities (dated 2026-10-09, baseline 7e6b017) lists 39 items. It was written by reading the source, and its baseline numbers,
its quoted excerpts and the large majority of its claims hold up under re-check — but
several entries describe a capability as wholly absent where a narrower mechanism already
exists, and one entry is wrong.
Filing 39 issues would put 36 items nobody is about to touch into the tracker, which costs
the backlog its signal. Instead the inventory is recorded once as a document, and this issue
is the durable tracker anchor for it. docs/roadmap.md is the record; an entry becomes work
only when it is picked up.
What to build
docs/roadmap.md: every verified gap, grouped by area, each entry carrying the code the
source inventory used (A1 … H3), the gap itself, the evidence as a file and line, the
mechanism that already exists and must not be re-implemented, and the source report's
effort estimate explicitly marked as unverified.
A verification deltas section recording the entries the re-check corrected, so the
overstated version cannot be re-derived from the source report.
The entry the re-check refuted is recorded as refuted, not as work.
Acceptance
docs/roadmap.md exists, is linked from this issue, and carries one entry per verified gap.
Every entry's evidence names a file and line that exists at the commit that lands the doc.
The document states its provenance (source report, inventory date, baseline commit) and
restates the repository's rule that a number written into a document must be computable by
one command.
No work items are created here: an entry becomes work when it is picked up, as its own issue
with numbered increments (S1, S2, …), so that the tracker stays a set of commitments
rather than an inventory.
docs/roadmap.md (new, 518 lines): 38 entries across the eight areas of the source inventory,
each carrying its code (A1 … H3), the gap, the evidence as file:line, the mechanism that
already exists so it is not built twice, and the source report's effort estimate marked as
unverified. Plus a verification-deltas section, the one refuted entry, and a recommended reading
order.
README.md: a link to it beside the existing docs/ references.
Corrections the re-check recorded (the source report's own versions are in the deltas table, so
they cannot be re-derived): A2, A3, A5, C2, C3, C4, C5 each described a capability as wholly absent
where a narrower mechanism already exists. E5 is refuted outright — project instruction files are
loaded (main.go:151 with loadProjectContext, four tests), and it is recorded as refuted rather
than as work. The source's arithmetic was corrected too: 39 items rather than 38, and its P0 table's
rows sum to 31.5–51.5 man-days rather than the "~30–35" it is introduced with.
Measurements: all 83 file:line references resolve at 7e6b017 → checked=83 failures=0. The
sweep was mutation-tested so it is known to be able to fail: a renamed file → failures=1, exit 1;
a line past end of file → failures=1, exit 1. Both suites green on the tree before branching
(go test ./... -count=1 and -race, 13 packages). The PR's nine checks all passed with annotations=0 new.
What this issue is for now: an entry becomes work when it is picked up, as its own issue with
numbered increments (S1, S2, …), and that new issue gets linked here. No entry has been picked up
yet, and the roadmap's line numbers are pinned to 7e6b017, so read them as a starting point rather
than as a live index — the document says this itself, and it is why the reference sweep was not
committed as a test.
Picked up: A2 — provider-reported usage is discarded (docs/roadmap.md:60-71) → #174, with increments S1 (value + both providers), S2 (agent accumulation and one OnUsage event per call) and S3 (status bar shows the real number).
Out of scope there, and still A3's: cost/pricing and a cumulative budget.
The A2 entry will carry the issue link in the PR that lands it.
A2 is done and merged; the entry now carries its issue link in the document.
Picked up → delivered: #174, PR #175, rebase-merged to master as 5371cc1 + d5a625a.
What A2's gap actually was. The roadmap's evidence said the usage-only chunk "carries empty choices and is skipped". True, but weaker than the real defect: the SSE loop also breaks on finish_reason, and the usage chunk arrives after it — so the chunk was never read off the socket
at all. Both halves are now pinned by a fixture that places usage after finish_reason, and by two
mutations that reintroduce each half and fail it.
Measurements
go test ./... -count=1 and -race: 13 packages ok, 0 failed, before and after the merge.
CI: nine checks green, 11 annotations = the 3e7c23a baseline, per job. No new annotations.
Main-module test functions: 749 → 767 (+18, find … | xargs grep -h '^func Test' | wc -l).
This moves the number again, so #170 gets a note
rather than a silent drift.
Roadmap's own reference sweep: the A2 entry's line numbers are pinned to 7e6b017, and the entry
grew by six lines in this change — every line reference after A2 in the document is therefore
shifted. That is the drift the document already warns about; the durable part (file and symbol) is
untouched.
What this does not close: roadmap A3's cumulative token/cost budget and circuit breaker. A2's
title also mentions "no cost accounting", and cost is deliberately still absent — no price table
exists in the tree, and per-model pricing belongs with the budget work, not with reporting a number
the provider already sends.
Still open in the tracker, unchanged by this: #166, #167, #169, #170, and this anchor. No entry
has been picked up beyond A2.
Handoff recorded for the A2 pickup: #176 — a defect PR #175 did not fix and did not otherwise
track.
The read-to-[DONE] change that made the usage chunk reachable also removed the early return at finish_reason, with no bound in its place. Measured at d5a625a against an endpoint that completes
its answer and then holds the connection open: the call takes the provider's whole 120 s request
timeout instead of a clean return (with a 1.5 s caller deadline: 1.502 s, versus 2 ms when the stream
ends with data: [DONE]), and chatStream returns the truncated content with err == nil, so the
loop cannot tell it was cut. A2's scope was reporting usage; the bound is its own issue.
The handoff from the A2 pickup is closed: #176 → PR #178, rebase-merged to master as 49964e2.
The OpenAI-compatible provider now has the same two-bounds shape as the Ollama one — a 120 s
whole-request bound for a batch call and a 2 min per-line idle bound for a stream — and a read that
is cut returns an error naming the bound instead of the partial answer with a nil error. Measured
against an endpoint that completes its answer and then goes quiet: err=nil and the partial text
before, openai stream: context deadline exceeded and a nil response after. Nine CI checks green,
11 annotations = baseline.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched. On
the roadmap, A2 is delivered and carries its issue link; A3 (cumulative token/cost budget and circuit
breaker) is the next item in A2's own area and is not started.
A3 is delivered; the entry now carries its issue link in the document.
Picked up → delivered: #179, PR #180, rebase-merged to master as 116301b.
What landed: budget.max_tokens_per_run and budget.max_tokens_per_session, both off by default;
a check at the top of the ReAct loop, before the call that would spend, so a run cannot overshoot by
one call; a message naming the bound, the spend and the knob; and tokens: spent/limit in the status
bar when a session budget is configured (byte-identical to before when none is, which is why the 27
frame goldens are untouched).
Design decisions recorded in the issue before the work started, and honoured: tokens rather than
money (no price table exists, and none can be produced by a command in this repository); off by
default; stop before the call; a silent provider counted as an estimate rather than allowed to switch
the budget off; sub-agents bounded against their own spend.
Measurements
go test ./... -count=1 and -race: 13 packages ok, 0 failed, before and after the merge.
CI: nine checks green, 11 annotations = the 3e7c23a baseline, per job.
Six mutations, six assertion failures (the check removed → 11 calls instead of 2; session budget
ignored; estimate fallback removed; estimate firing the usage event; run total not reset; status bar
always showing a fraction).
A3's remaining half, stated plainly: cost and pricing. The entry's title is "no cumulative token or cost budget"; tokens are now bounded, money is not, because a price table is a data-maintenance
surface (which models, which currency, refreshed how) rather than a code gap. If that is worth doing,
it is its own issue.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched.
On the roadmap, A2 and A3 are delivered; the other certainties in the recommended order (A1 retry and
backoff, D5 notification, F3 tool-call validation) are not started.
Recorded while answering a design question about where model prices would come from, because the
answer turned out to be two different kinds of work rather than one:
[usage] Decide whether to vendor a model price table (and whose data it would be) #182 (question) — the decision: whether this repository vendors a price table at all, whose data
it would be, under which licence, refreshed how, with which staleness policy. Its deliverable is a
written verdict, and "do not vendor a table" is a first-class outcome.
The separation is the point: #181 must not wait on #182, because OpenRouter reports the cost of a
request outright and everything else accepts a user-declared price. Nothing about a table blocks the
pipeline.
The doc correction that came with it (PR #183, aea5dc3): A3's entry in docs/roadmap.md used to
say cost needs "a per-model price table, which no command in this repository can produce". That was
written from the baseline and is too strong on two counts, both checked against the providers' own
documentation:
A provider may report the cost itself — OpenRouter returns usage.cost and cost_details.upstream_inference_cost, so no table is involved at all.
A table with one input price and one output price is wrong for the provider this repository ships
by default: DeepSeek prices four input lanes (cache hit vs cache miss) times a peak/off-peak rule —
a 50× spread on deepseek-flash input alone. Its API does identify the lane
(prompt_cache_hit_tokens / prompt_cache_miss_tokens), but types.Usage cannot hold it yet.
The entry now carries both issue links and keeps the narrowing visible, the same way the
verification-deltas section keeps the source report's overstated versions: a wrong reason in a
document is what makes the next reader re-derive the question.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched; #181 and #182 are new. On the roadmap, A2 and A3's token half are delivered, and A3's cost half is now
tracked outside the entry.
A3's cost half is delivered; the roadmap entry says so now, and this anchor has one fewer open
thread.
Picked up → delivered: #181, PR #184, rebase-merged to master as 1e3d103 + 0496da2 + 9e32eb5.
What landed: a route's own charge carried verbatim (OpenRouter's cost, with the unit named by cost_currency); types.Usage given one field per billing lane, because DeepSeek bills four input
lanes times a peak/off-peak rule and a single input price cannot express it; user-declared pricing
rates keyed by <route>/<model> with wildcards, applied only to reported usage; and unknown costs
counted as unknown rather than as zero.
Measurements
go test ./... -count=1 and -race: 13 packages ok, 0 failed, before and after the merge.
CI: nine checks green, 11 annotations = the 3e7c23a baseline, per job.
A CI failure worth recording as a lesson, not just a fix. The first run of PR #184 failed TUI visual (screenshots + PTY): the new cost segment appeared on every run, because a route that
reports neither usage nor a charge makes every call unpriced — the CI stubs are exactly that. The bar
grew by 26 columns and pushed Press Ctrl+C again to quit past the wrap, so permission-allow-always
timed out waiting for text that had been split across two lines. A pure Go test could not see it (the
goldens have no agent run); the gated PTY job is what caught it, and the same runner verified the fix
locally afterwards (31 scenarios, 27 ok, 4 gated). The rule that came out of it: a status bar may
only grow for information the user opted into.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 and #182 remain open and
untouched. On the roadmap, A2, A3's token half and A3's cost half are delivered; the open item that
came out of this work is the price-table decision in #182, which is deliberately independent.
A1 is delivered, and the entry needed correcting before it could be.
Picked up → delivered: #192, PR #194, rebase-merged to master as c36d2ff + b1f8d1d + 70d7564.
What landed: a shared retry policy for both providers — 408/429/5xx and a response-less transport error are retried with exponential backoff and equal jitter, floored at Retry-After and capped; a context error, a body we cannot parse and every other 4xx are not. The design decision is the safe-replay rule: a streaming attempt is repeated only while nothing has reached the caller, because text already drawn may not be drawn twice. A retry announces itself, and a final failure names how many attempts it took.
The correction: A1's "Already present" line pointed at tool/web_browser.go:297 and tool/web_extract.go:44 as an existing retry/backoff idiom. Neither is one — the first is the cooldown that expires a timed-out browser-probe verdict, the second a sentence inside the web_extract description — and the only retry-shaped loop in the repository picks a different temp-file name after a collision. The entry now says that plainly. This is the second overstated "already present" line found in the inventory (the first was A2's, corrected in #181); both are recorded rather than deleted, so the next reader does not re-derive the question.
Measurements
go test ./... -count=1, -race and make test-repeat (-count=3): 13 packages ok, 0 failed, on the PR head and on the merged tree.
CI: nine checks green, 11 annotations = the 3e7c23a baseline, per job.
Eight mutations, eight assertion failures.
Main-module test functions: 802 → 821.
Handed off: #193 — a flaky assertion in one of the new tests, which CI caught and which is now attributed, fixed and closed. Filed rather than fixed silently because it was an unattributed failure in a gate this change relied on.
Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182. On the roadmap, A2, A3 (both halves) and A1 are delivered; the remaining certainties are D5 (notification) and F3 (tool-call validation), and A1 was the last of the three the recommended order put first.
The coverage gap I opened one message ago is closed, and it paid for itself immediately.
Picked up → delivered: #195, PR #197, rebase-merged to master as d3cd53a + 31a34e0.
What it closed. The three status-bar surfaces that A2/A3 added had only string-level assertions on renderStatusBar. They now have a committed frame at 80x24 and 120x40, and two PTY scenarios: one asserting the reported charge and the budget fraction on a real terminal (cost: 0.0042 credits, tokens: 1500/100000), and one counting that a retried call draws its answer exactly once. make test-tui-scenarios runs 33 files, 29 ok, 4 gated; the frames went 27 → 29 and the scenario files 31 → 33, with CODEBASE.md, README.md and AGENTS.md updated — including two counts that were already wrong before this change.
What it found. Writing the scenario reproduced the failure mode this repository has now hit three times, from three directions: a long status bar clips its own trailing status message. At 120 columns the quit confirmation reads Press Ctrl+C again with quit off the line, measured in the scenario dump. Filed as #196 with the reproduction and three candidate fixes; the scenario runs at 200 columns as an explicitly recorded workaround, and should come back down when that is fixed. This is the second time a PTY job has caught something no string assertion could — which is the argument for having written #195 in the first place.
Measurements: 13 packages ok plain and -race; make staticcheck exit 0; nine CI checks green with 11 annotations = the baseline on both the PR head and the merged commit; two mutations verified (the frame golden fails on a changed bar, the scenario fails on a stub that stops reporting usage).
Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, and the two new ones from this round — #196 (the clipping) and #193's closed attribution. On the roadmap, A1, A2 and A3 (both halves) are delivered; the remaining certainties are D5 (notification) and F3 (tool-call validation), and the verification debt for what was delivered is now paid down.
D5 is delivered — the cheapest item in the inventory, and the one whose cost was never in the plumbing.
Picked up → delivered: #198, PR #199, rebase-merged to master as 14b5ddd + 9ff62fc.
What landed: a finished background task announces itself, and so does a foreground run that outlived a threshold. The terminal channel writes OSC 9 and OSC 777 (the bet the CellGrid already makes with OSC 8 links: a terminal ignores an escape it does not know); the desktop channel runs osascript/notify-send with the text as arguments rather than interpolated into a script; one min_duration_seconds (default 30) gates both; the escape text is sanitized so a task's own goal cannot end the escape and write to the terminal. channel: off turns it off.
Where the work actually was. The issue's evidence — osascript, notify-send, terminal-notifier → 0 — says the mechanism was absent; it says nothing about the policy, and every decision worth reviewing was there: on or off by default, which channel, a threshold instead of a focus check, no sound, no Windows path, and treating a missing notifier as silence rather than an error. Those went into the issue before the code, which is why the PR body could cite them rather than defend them.
Measurements: 14 packages ok plain and -race; make staticcheck exit 0; nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; six mutations, six assertion failures.
One of those mutations earned its keep: the "announce exactly once" test was passing for the wrong reason — the duplicate's zero elapsed time was being swallowed by the threshold, so the once-guard was unpinned. That is now fixed and verified by the mutation. A second gate in the same PR (the announcement must happen outside the manager's lock, or a Collect waits on a notification) is pinned by a probe that takes the lock and fails on a bound rather than hanging.
Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, #196. On the roadmap, A1, A2, A3 (both halves) and D5 are delivered out of 38 verified gaps; of the four items the document called certainties (A1, A2, D5, F3), only F3 (tool-call validation) is left, and A3's cost half came along with A2's area.
F3 is delivered, and with it every item this document called a certainty.
Picked up → delivered: #200, PR #201, rebase-merged to master as a111efd + a4c4cd4.
What landed: a tool call's arguments are checked against the schema the model was shown before the tool runs, with every problem reported at once, and a call that keeps failing the same way is refused as a loop instead of being retried until the step budget is gone.
The census that scoped it. F3's evidence said nothing checked required fields, types or enums — it did not say which keywords the schemas actually use, which is the difference between adding a JSON Schema library and adding five checks. The count (tool/, product code only): type 65, properties 24, required 15, enum 5, items 3, additionalProperties 1 — and that one is true, the default, so there is nothing to enforce. A first pass of that census also mistook a property namedpattern for the keyword; the issue's numbers come from a re-run that excludes test files.
Measurements: 14 packages ok plain and -race; make staticcheck exit 0; nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; seven mutations, seven assertion failures.
Roadmap progress, four rounds in: 5 of 38 verified gaps delivered — A1 (retry and backoff), A2 (reported usage), A3 (token budget, and its cost half in a second issue), D5 (notification), F3 (tool-call validation). The document's own "certainties, days not weeks" tier was A1, A2, D5, F3: all four are now done, with A3 picked up alongside them.
What the pick-ups changed in the document itself, which is worth keeping visible: A2's and A1's entries each overstated what already existed (a counter that was an estimate; a "retry/backoff idiom" that was a probe cooldown and a sentence in a tool description), and F3's entry was silent on the keyword census that decides the implementation's size. Each correction is recorded in the entry rather than deleted, so the next reader does not re-derive the question.
Tracker state: #168 (this anchor) stays open — it is the anchor, not a work item. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, #196. The next cheapest certainty-shaped work is gone; what remains is the larger half of the inventory (B, C, D, E, G, H areas) and the open decisions like #182.
[docs] CODEBASE.md links a deleted harness document #169 — the dead link was real, and so were two more dead references in the same sentence (tui/frame_shot_test.go, tui/pty_screen_test.go). The sentence now points at tuiprobe/README.md + tuiprobe/docs/parity.md and docs/tui-verification.md. Sweep after: 0 broken relative links of 28 checked, against 1 before.
[docs] Five documented test counts, three different values, none correct #170 — the test-count badge is removed on the user's decision, and the four prose figures corrected to the same measurement (857 test functions in the main module, 10 fuzz targets). The command in "How these numbers are measured" needed a decision rather than a number swap: it counted the nested tuiprobe module (956) while the prose counted the main module, so it now excludes the nested module and the command, its comment and the prose all agree. Six figures are deliberately left as history — the two in CHANGELOG.md scoped to their commit, docs/roadmap.md's 7e6b017 baseline, and three in tuiprobe's own released records — because a measurement that was true when taken is not a defect.
The finding the request was worth: verifying #169 meant checking whether the files it names exist, and that check does not stop at the one link. CODEBASE.md's whole "Harness rules" section — the design record an agent or reviewer is told to read before touching the gated harness — names runStage, capturePNG, cleanupLauncher, shotTimeout, TestRunStageReportsTimeout, TestCleanupLauncherDoesNotWaitForever and tui/program_driver_test.go, and none of them exist in either module. Filed as #212 with the measured table and the acceptance that every named path and identifier must resolve by command. It is not absorbed into #169's PR: a stale identifier is worse than a broken link, because the link fails visibly and the name reads as a fact.
Tracker: open issues 6, read from the tracker at the time of writing rather than from memory — 167, 168 (this anchor), 182, 188, 196, 212.
#212 is fixed — PR #215, merged as 796e6a4. It is the finding that came out of the #169/#170 review, so this closes the loop that request opened.
What it was: CODEBASE.md's "Harness rules" section — the design record an agent or reviewer is told to read before touching a test that drives an external process — named seven identifiers and three files that were deleted with the harness (runStage, capturePNG, cleanupLauncher, shotTimeout, two test names, tui/program_driver_test.go, tui/frame_shot_test.go, tui/pty_screen_test.go). The rules survived the move into tuiprobe; the names did not.
What the fix turned up that the issue did not know:
The TUI bullet above the section named three more deleted files (same defect, same document).
A real test gap: nothing resizes a running program. The deleted in-process driver was the only thing that did; the scenario runner has no resize step, and tui/geometry_test.go covers only the model's response to a tea.WindowSizeMsg. Filed as #214, with both routes (an in-process driver, or a resize step in tuiprobe — a tool change per the repository's route 3) and an acceptance that the assertion must fail if the program ignores a resize. The section records the gap and links there instead of naming a file that would pretend to cover it.
Verification: every path and identifier in the rewritten text resolved by command (grep -rl for names, git ls-files/glob for paths — fourteen, all resolving), a whole-file sweep of 82 repo-path-like references with one non-resolving prose mention, and the five tokens my first checker flagged examined by hand (all its own false positives).
Tracker: open issues 6, read from the tracker just now — 167, 168 (this anchor), 182, 188, 196, 214.
#214 is closed — the coverage gap that #212's fix produced, now covered by a real step rather than a file name. Two PRs and a release: #216 (merged 64d7d5f, tagged tuiprobe/v0.1.5) added the resize step to the scenario runner; #217 (merged c4d2fb5) bumps the pin and adds resize-running.scenario, which resizes the real binary from 80x24 to 40x12 while it runs and asserts the reflow three ways (--since after a mark, fit, and a screenshot whose pixel size is checked against the new geometry).
Three things about this one are worth keeping:
The mechanism existed; only the step layer did not.pty.Setsize, Session.Resize, the daemon's resize command and the tuiprobe resize CLI were all there. The gap was that a scenario file could not reach them, which is exactly the kind of gap a documentation fix would have papered over.
The tool's own documentation guards refused the change three times (its Verbs(), then docs/tui-verification.md, then AGENTS.md). Adding a step is a four-place change and the tool enforces it — recorded because it is a guard working as designed, not friction.
The control was run, not assumed: deleting the resize line makes the new scenario fail. An assertion that survives the feature's removal is not an assertion.
Counts that moved: tui/testdata/scenarios/*.scenario 33 → 34, and the step lists in AGENTS.md / docs/tui-verification.md gained resize. CODEBASE.md's sentence claiming nothing resizes a running program was replaced with what covers it.
Tracker: open issues 5, read from the tracker just now — 167, 168 (this anchor), 182, 188, 196.
#196 is fixed — PR #219, merged as 372b9fd. It is the last defect that was sitting inside already-delivered work: the status bar built by the A2/A3 cost-and-budget work could not fit its own trailing status message, and #195's verification work is what found it.
The fix (S1 of the three options the issue listed): the bar is budgeted before it renders — the message is measured first and kept whole, the other segments are added left to right while they fit, a segment that does not fit is dropped whole, and a message that no width can fit is clipped with a visible marker. The committed frames now show the screen instead of the model's unclipped output, which is what the acceptance meant by "the frames at both widths show what the terminal would actually display".
Measurements: 14 packages ok plain and -race; make test-tui-scenarios 34 files / 30 ok / 4 gated with status-cost back at 120x30 (it ran at 200x50 only because of this bug — that workaround is gone); nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; the regenerated PNG read by eye rather than inferred from text.
Three things worth keeping from this one:
A guard lost its fixture, and the honest answer was not to fabricate one.TestShotScenariosFitTheirGeometry required one of its own shots to be over-wide so the clip was exercised; fixing the bar removed the last such case. Rather than add a PNG to keep the guard's shape alive, the guard is gone and its comment names where the clip is still exercised — same function, same input string, so the coverage never moved.
My own measurement was wrong and I caught it before it became a five-times-larger issue. An awk 'length($0)' pass reported five over-wide frames; all five were byte counts, and ⚡/■ are one cell and three bytes. In cells, exactly one frame is genuinely over-wide: the permission dialog's hint row at 40 columns, filed as #218.
Tracker: open issues 5, read from the tracker at the time of writing — 167, 168 (this anchor), 182, 188, 218. Of the defects found inside delivered work, none remain open: #185, #186 and #196 are closed.
#218 is closed — PR #220, merged as 6b34e58. It was the finding #196's own measurement work produced: the permission dialog is drawn below the cell grid, so nothing trims it against the terminal, and its hint row was 54 cells at 40 columns — the user read a sentence with no end, Esc cancel being the part that went missing.
What landed: the three key hints became lists of alternatives, and the input area budgets itself against m.width — fitHintRow packs whole alternatives into lines (the trailing · marks a wrapped line so it still reads as one list), wrapInputText wraps the dialog's heading and answers at word boundaries, and a word that cannot fit a line even alone is split across lines rather than clipped. Nothing is dropped to make room at 40 columns.
Four things worth keeping from this one:
The issue's measurement was right about one line and short by two. Reading the code rather than the frame found the heading (47 cells) and the always-allow answer (60 before its [3] marker) over-wide as well. The committed frame could not show the third one because the fixture was wrong: it carried three answers with a stale label (Allow for this session — the sandbox offers Allow session), which is how a fixture silently stops covering the thing it names. It now carries the real four.
A design choice that is not obvious got its own gate. A wrapped answer continues under its marker's own indent (2 cells, not the full 6); with 6, the real config path splits into config.js / on). The mutation that narrows it turns TestPermissionDialogTakesTheRealAnswersAtFortyColumns red, so the choice is held by an assertion rather than by a comment.
The clip's fixture moved off the screens entirely.[tui] A long status bar clips its own trailing status message #196's fix had already cost one guard its fixture, and this fix removed the last over-wide frame. Rather than keep a screen over-wide to exercise golden.Clip, the golden suite now asserts the opposite — every committed frame fits the geometry it claims, 0 over-wide out of 29 — and the clip keeps a synthetic over-wide fixture whose own width is asserted first. Keeping a defect to test the mechanism is not coverage.
A PTY scenario cannot gate this class of bug. tuiprobe's own fit note says the emulator wraps at its own width, so the tail of an over-wide line is still on screen and a wait --text for it passes either way. The gates are the committed frame and the model tests; writing that down is what stopped a scenario that would have looked like evidence.
Filed from this work: #221 — the command palette's and provider picker's rows are still drawn as-is (widest palette row 36 cells, so they fit the narrowest geometry the suite covers, 40, and would be cut below it).
Measurements: 14 packages ok plain and -race; make staticcheck clean; make test-tui-scenarios 34 files / 30 ran / 4 gated; dialog_40x12 regenerated and dialog-narrow-40x12.png read by eye; 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit; six mutations run, each named by the gate it turns red.
Docs corrected in the same PR (found while editing): the frame counts in docs/tui-verification.md, tui/scenarios_test.go and tui/usage_test.go still said 27 where there are 29, and the statusbar entry in frameScenarios still described the model as not clipping the bar, which #196 changed.
Tracker: open issues 5, read from the tracker at the time of writing — 167, 168 (this anchor), 182, 188, 221.
#221 is closed — PR #222, merged as b287cdf. It was filed from #218's work: the input area is drawn below the cell grid, so nothing trims its rows, and #218 had only budgeted the permission dialog. The palette's widest row is 36 cells and the provider picker's labels are built from the user's own provider names (openrouter-anthropic-claude-sonnet (mock) is 41), so at 30 columns a description or a provider name lost its tail.
What landed: palette rows and picker rows are budgeted against m.width; one shared renderHeading budgets the Commands: / Select provider: headings and the dialog's title; splitAtCells prefers a break point that fits. Two new frame sets (provider at 80x24 and 30x12, palette at 30x12) with narrow screenshots.
Four things worth keeping from this one:
Looking at the result changed the design twice. The first implementation wrapped a long description into the narrow column beside its name, which at 30 columns produced /diagnostics LSP / diagnostics — so a description that does not fit beside its name in full now takes a line of its own and gets the whole width. And the picker's label split as openrouter-anthropic-claud / e-sonnet, so a long token now breaks after a hyphen, slash or dot when one fits. Neither was visible in the code; both were visible in the regenerated frame a minute after it was written.
The tests found a defect in a line the issue had excused.[tui] The command palette and provider picker rows are not budgeted below 40 columns #221 said the Commands: and Select provider: headings "are short and need no change". The picker's heading is 16 cells before its style padding, and the 16-column assertion caught it at 18 cells. It is budgeted here rather than written down as an exception.
My own first version of the picker loop had the bug the golden then showed: the row marker printed on every wrapped line, so the label could not line up under itself. The marker now lives outside the header style, which also puts the selected row's > in column 0 where the palette's already is — and the new 80x24 golden is what pins that, because the picker had no frame coverage at all before this.
The fix is inert above the widest row. Every frame at 40 columns and above is byte-identical; only 30 and below change. That is what makes it safe to land the layout change against an existing golden set without re-reading every frame.
Measurements: 14 packages ok plain and -race (also on master after the merge); make staticcheck clean; make test-tui-scenarios 34 files / 30 ran / 4 gated (unchanged baseline); 32 committed frames, 0 over-wide; 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit; six mutations run, each named by the gate it turns red; both new images read by eye.
Docs corrected in the same PR: the frame count 29 → 32 in docs/tui-verification.md and tui/usage_test.go, and the judgments list gained the palette's layout rule.
Tracker: open issues 4, read from the tracker at the time of writing — 167 (a chore), 168 (this anchor), 182 (a usage decision), 188 (a docs diagram). No defect that was found inside delivered work is open.
#167 is closed — PR #223, merged as 31ba725. It was the chore the C3 note in docs/roadmap.md pointed at: the agent workflow keeps a session's scratch worktree inside the workspace root (.worktrees/<scope>), and nothing ignored that directory, so git worktree add .worktrees/probe … left the outer tree reporting ?? .worktrees/ and a plain git add -A staged the worktree as an embedded repository — a gitlink in place of the files, which git warns about and records anyway.
What landed: .worktrees/ in .gitignore with its one-line reason, plus the two documents that would otherwise have been wrong — the roadmap C3 note (which said "#167 tracks ignoring it") and a changelog entry.
Two things worth keeping from this one:
An acceptance clause that passes for the wrong reason is worse than no clause. Clause 1 was mkdir -p .worktrees/x && git status --short — an empty directory, which git never reports whether it is ignored or not. Control: an unignored empty directory also prints nothing, while a file inside it prints ?? …. The clause was measured as vacuous at b287cdf, kept, and strengthened with a file inside the ignored directory; the strengthened form and clause 2 both fail before and pass after. A criterion that cannot fail is not evidence, and the fix was to make the criterion observable rather than to declare it met.
The defect is a staging one, and the symptom is not the defect.git status --short printing ?? .worktrees/ is what a reader notices; git add -A turning the worktree into a gitlink is what actually reaches a commit — git only warns (adding embedded git repository), and a clone of the outer repository cannot obtain the files. The acceptance therefore asserts git add -A --dry-run prints nothing, not merely that status is clean.
Measurements: git check-ignore -v .worktrees/ → .gitignore:41; 14 packages ok plain and -race (also on master after the merge); make staticcheck clean; gofmt clean; 9/9 CI checks with 11 annotations = the baseline on both the PR head and the merged commit; the merged diff is three files and contains no .go file.
Honest scope: this property has no automated gate — the acceptance is two shell properties and no tracked .go file reads .gitignore. The before/after table is the evidence, and the failure is self-announcing.
One observation handed back, not filed: the branch commit was GPG-signed (%G? = G), but GitHub's rebase-merge produced an unsigned 31ba725, and every commit on master is unsigned. The repository states no signing policy, so this is a merge-strategy property rather than a defect, and it is the maintainer's call whether to change it (branch protection or a different merge strategy).
Tracker: open issues 3, read from the tracker at the time of writing — 168 (this anchor), 182 (a usage decision), 188 (a docs diagram). No defect found inside delivered work is open.
#188 is closed — PR #224, merged as 9058599. It was filed against the README's System Overview box, which listed memory among the tools; #187's figure had already replaced that diagram, so what remained was the decision the issue asked for — plus an audit that turned up two things the issue did not have.
What landed: CODEBASE.md's "Complete Tool Name Registry" gains the missing web_extract_browser row; memory.go, Agent.Memory/MemoryMode and the MemoryStore entry are marked internal and unwired; the TUI housekeeping whitelist drops the phantom "memory" key; docs/roadmap.md B1 records the decision; and tool/documented_tools_test.go now reads the table and the code and fails when they disagree.
Three things worth keeping from this one:
A criterion the code satisfies can still be hiding the defect. Criterion 1 was "every name in a Tool Layer list is registered in main.go" — true of every name in the table, while the table was missing a registered tool. A list and a registry have to be compared in both directions, and only one of them was written down.
A row count that matches the expected number hides a wrong row. The table had 24 rows because the dynamic mcp_* row stood in for one tool while web_extract_browser was absent — so counting rows reproduced the documented "24 tools" exactly, and the arithmetic a reviewer reaches for agreed with the prose while a real tool was missing.
"Never a tool" needs a place to live, or the claim comes back.memory is internal and unwired, but the capability gap is real; the resolution was to point at the roadmap entry that already tracks it (B1/B2, behind a privacy decision) and mark the code internal — not to file a duplicate issue, and not to give the client a config switch it has no caller for.
Measurements: 24 code-defined tool names against 24 static ag.AddTool registrations; 14/14 packages ok plain and -race, also on master after the merge; make staticcheck clean; gofmt clean; make test-tui-scenarios 34 files / 30 ran / 4 gated with no golden moved; 9/9 CI checks with 11 annotations = the baseline on the PR head and on the merged commit; four mutations each named by the gate assertion it turns red.
A number in the issue's own acceptance was wrong: grep -c 'ag.AddTool' main.go prints 25, not 24, because the MCP registration loop is itself an ag.AddTool call site. The 24 static registrations are what the documents mean and they are correct.
Honest scope: the gate covers CODEBASE.md's table only — README.md:52 groups web and LSP under category words rather than enumerating them, so there is nothing to compare row for row. The TUI housekeeping whitelist stays ungated, and the mute path it lives in has no test at all (pre-existing), which is how a phantom tool name survived there.
Tracker: open issues 2, read from the tracker at the time of writing — 168 (this anchor) and 182 (a usage decision). Both are decisions rather than defects; no defect found inside delivered work is open.
#182 is closed — PR #227, merged as 0a1b5fb. It was the data-governance question the cost work (#181) left open: whether to ship a model price table, and whose data. Its deliverable was a written verdict rather than code, and the verdict is no table, recorded in docs/pricing-decision.md.
What landed: the decision document with the seven sections the issue's acceptance names (source, licence/provenance, update mechanism, cadence and staleness policy, coverage and key, tiering, rejected options), docs/roadmap.md A3 repointed from the open question to the verdict, the docs index in README.md, and a changelog entry. No application code.
Three things worth keeping from this one:
A decision can come from a disagreement rather than a preference. The verdict did not turn on "tables are maintenance" but on a measurement: for this repository's own default route, LiteLLM's base entry says $0.30/M input and models.dev says $0.15/M — and both are correct, because one carries the peak rate with off_peak_pricing beside it and the other carries the off-peak rate as its only number. DeepSeek's peak is 20.8% of the week. A vendored single value is therefore ambiguous, not merely stale, which is a stronger and checkable reason to refuse than staleness.
Auditing the decision found a live defect in our own example.agent.Price has no time dimension, so the shipped config.example.json — which declares the peak rates — estimates 2× too high for about 79% of the week with nothing saying so. The issue framed tiering as an argument against vendoring; it applies equally to the user-declared path that already shipped, and that turned out to be the more useful half of the answer.
"No" to the table was not "nothing to do". The recorded no is to vendoring, and the piece it hands over is the schema (#225); a second finding, that our default model name is a retired alias and so a price key may not even match what we send, is #226.
Measurements: LiteLLM 4,084 entries / 2.8 MB with off_peak_pricing on 10 and tiered_pricing on 51, MIT inside a split licence GitHub reports as NOASSERTION; models.dev 229 providers / 8,832 models / 5.6 MB, MIT cleanly, with no time dimension anywhere; DeepSeek's rates and peak window quoted from its own pricing page (peak 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri); 14/14 packages ok plain (a regression check — no Go file changed); 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit.
Honest scope: aggregator feeds were recorded as "not fetched" rather than described second-hand; the licences were read, not legally reviewed; the tiering evidence covers DeepSeek Flash's lanes and both datasets' schema shape rather than all 4,084 entries; and nothing is implemented — #225 and #226 are filed, not started.
Tracker: open issues 3, read from the tracker at the time of writing — 168 (this anchor), 225 (the pricing schema's time dimension), 226 (the retired default model name). No defect found inside delivered work is open.
#226 is closed — PR #230, merged as bbce8e0. It was the naming debt the pricing verdict (#182) noticed and filed rather than fixed: config.DefaultConfig() sent deepseek-v4-flash, which DeepSeek footnotes as a retired name — still accepted, served by DeepSeek-V4.1-Flash, billed at the Flash price. What made it worth a change is not the spelling but the key: the model name is what a declared rate is looked up by (<route>/<model>), so an alias is a second name for one route, and a price table naming only one of the two silently fails to price the other.
What landed: the default, the shipped example and the example's pricing.prices key all name deepseek-flash, with README.md and CODEBASE.md in prose; docs/pricing-decision.md §5 records the rename as done while keeping the alias-resolution argument; the example's pricing.$comment now says the DeepSeek rates below are its peak rates, 2× too high for about 79% of the week until #225 can express the off-peak half; and TestDefaultConfigSendsTheCanonicalDeepSeekModel asserts the name in the request body that DefaultConfig → buildProviderRegistry → the wire actually produces. CI on bd8b62a: 9/9 jobs, 11 annotations, the baseline. go test ./... -count=1 → 28 packages ok, 0 FAIL; -race the same; make test-tui-scenarios → 34 files, 30 ran, 4 gated.
Three things worth keeping from this one:
Assert the wire, not the field. The acceptance says "a request built from DefaultConfig() carries model: deepseek-flash", and the difference is real: the config record's Model is what a provider may still override, while the body is what the server bills on. The test builds the real registry, points it at an httptest server and reads the captured body — so it would also catch the rename being lost in SetRouteInfo or in the request builder, which a struct-field assertion cannot. Mutating config.go back to the alias fails it.
A grep in an acceptance list is a classification, not a count. The criterion is that deepseek-v4-flash survives only where it is about the legacy name. The tree ends with 11 occurrences, and each one had to be argued rather than aimed at zero: a fixture that keys a table on the alias is the evidence that the alias still works, and the test's own failure message has to name the string it rejects, or the next reader cannot tell what failed.
The gate you cannot write is itself a finding. The obvious companion assertion — that the example declares a rate for the default route it ships — cannot be honest on a file no program applies, and trying it turned up config.example.json never loading: a $comment inside the agents map is decoded as data, loadFile reports it, and both callers then merge nothing, so a user who copies the template loses every section. That is pre-existing and wider than a rename, so it is filed with two candidate fix shapes instead of being smuggled into this PR; the example-level gate waits for it.
#229 is closed — PR #232, merged as 0186074. It was the defect last round's gate could not be written around: config.example.json opens with "Copy to config.json" and could not survive that copy, because its agents block carries a $comment and agents decodes into a map, so the note was read as an entry. loadFile reported the error and both callers read it as "no file", so the whole configuration — five providers, pricing, sandbox, notify, budget — was silently replaced by the defaults.
What landed: loadFile drops every object key whose name begins with $ before decoding, at any depth, so the convention the file teaches holds where it is written rather than only where the container happens to be a struct; config.example.json states the rule and keeps every $comment where it was; four tests in config/shipped_example_test.go read the template itself, so it cannot rot unseen again; CODEBASE.md gains the loadFile entry it never had. CI on a19c8d5: 9/9 jobs, 11 annotations, the baseline. go test ./... -count=1 → 28 packages ok, 0 FAIL; -race the same; make test-tui-scenarios → 34 files, 30 ran, 4 gated.
Three things worth keeping from this one:
A convention that works by accident is not a convention.$comment looked safe because every instance sat inside a struct, where encoding/json ignores unknown fields. It was never safe: the schema has four map positions (agents, pricing.prices, an MCP server's env and headers), and a note in any of them costs the file. The fix belongs in the loader because that is the only place that knows the rule is about documentation, not about a section.
A control run is what turns a demonstration into evidence. The example loaded fine after the fix; running the same command against printf '{"providers": ' showed the diagnostic still appears, which is what makes "stderr was empty" mean something — and that control is what turned up #231.
An assertion that cannot be honest is a finding, not a gap. This issue exists because the obvious companion gate for [config] The default DeepSeek model is a retired alias #226 — that the example declares a rate for the default route it ships — could not be meaningful on a file no program applied. Refusing to write it was the useful move; writing it anyway would have hidden this.
One correction to the issue text itself: AddAllowedPath did not need the "same treatment" its loader-side option describes. It round-trips raw JSON and preserves$ keys, which is the behaviour we want, so it was left alone and TestAllowedPathRoundTripKeepsDollarComments pins that instead.
#231 is fixed — PR #233, merged as f21e7a8 + 7ac3282 + 596c679.
It is the finding #229's control run produced, and it was the last thing this tracker owed a record. main.go parsed the user layer twice at startup — UserSandboxBoundary() at main.go:556 and LoadConfig() at main.go:74 — so a malformed ~/.tinycode/config.json printed the same line twice and the same path was read twice. config.Layers now reads each file once per process start and every startup reader takes that value: the merged configuration, the sandbox boundary, the confine-commands resolver (both applySandboxCapabilityPolicy call sites, which keep their positions), the persistent-grant loader and --list-grants. loadFile is split into the read and decodeConfig, so the typed Config and the raw object come from the same bytes and a malformed file is reported once through reportIgnoredConfig. The #162 rule is untouched: the project-local layer still cannot set or extend sandbox.project_root, sandbox.allowed_paths or the confinement booleans.
Measurements. Reads were counted with a FIFO at the user config path, served one write at a time, so a process that wants a second read blocks in open(): --list-sessions3 → 1, --list-grants4 → 1, interactive start 5 → 1. With printf '{"providers": ' > ~/.tinycode/config.json, tinycode --list-sessions reports grep -c ignoring = 1 (was 2), the same path and message, exit code 0; a malformed ./.tinycode/config.json likewise 2 → 1 with exit 0, while --list-grants on a malformed file still exits 1 with Error: parse ….
Independently re-measured on the merged default branch (57a4de2, from a different session): go test ./... -count=1 → 14 test packages ok, 0 FAIL; go vet ./... and gofmt -l . both clean; the 9 CI checks all success with annotations at the documented baseline (1 per job, 2 in browser and tui-visual) — no new annotation.
Tracker arithmetic, read from the tracker at the moment of writing — the lesson comments 14 and 16 record, applied rather than restated. Live open set after this merge: gh issue list --state open → #168, #225 — 2 issues. It was 3 before the merge (168, 225, 231), so the count moved by exactly the one issue this PR closes.
Not verified here. staticcheck did not run locally (unavailable on this machine; make staticcheck pins v0.8.1, whose x/tools does not support Go 1.27) — the PR's staticcheck job is the gate and it passed. The gated suites (TUI visual/scenario, browser, LSP, live provider) ran only in CI. The three commits were signed when pushed; the rebase merge rewrote them, so the commits on the default branch carry no GPG signature — a property of this repository's merge strategy, not of this change.
No follow-up issue, nothing handed off, and no roadmap entry changes: this is not one of the 39 inventory items — it is a defect found while landing #229.
Roadmap entry C1 — No checkpoint and no undo (docs/roadmap.md:234-248) is picked up: #234, with increments S1 (the store, the pre-images and a restore), S2 (the /undo surface) and S3 (documents, retention, the boundary). S1 is landed — PR #235, merged as 1a6c1d7 + 9795cb0 + a21d9a0 + 845b35e. The entry will carry its Picked up line in S3, when the document is updated in the same PR as the retention rules.
Why this entry and not the ones above it: the document calls C1 the single hardest gap in the inventory, and the one a user must trust before letting the agent write. Tier 2 (B1/B2, memory) needs a privacy decision before an implementation, so it is not work that can start without the user; tier 4's C7 (macOS command confinement) is real daily cost but its own spike; the tier-5 bets (B4, F1) need a spike that answers "is this worth it" first. C1 is the one whose value is felt on the first real mistake.
What S1 found that the entry did not know.apply_patch's delete operation wrote through os.Remove (tool/apply_patch.go:198) rather than the write path, so a deletion was the one tool-mediated content change with nothing to restore. The new census test pins the five sites that change content (four writes, one delete) so a sixth cannot appear silently — that is a correction to the entry's own framing, which counted four call sites.
Measurements.go test ./... -count=1 → 14 packages ok, 0 FAIL on merged master (845b35e), and 14 ok / 0 FAIL under -race; go vet clean; gofmt -l . → 0 files; PR checks 9/9, annotations 11 (the documented baseline, so none new). Three mutations, each producing the failure the gate exists for: recording disabled → "recorded 0 calls, want 3"; the delete put back on os.Remove → "recorded 2 calls, want 3" plus the census failing; a call site handed context.Background() → the census names the line. Issue #231's acceptance was re-checked on this branch and on merged master (diagnostic printed once, exit 0), because the new wiring sits beside that fix's startup path.
Not verified here. The real-binary PTY path arrives with S2, which adds /undo; the gated suites (TUI visual/scenario, browser, LSP, live provider) ran only in CI. Files changed by a bash command or any other subprocess are not captured — that producer is its own issue, to be filed when S2 lands.
Tracker arithmetic, read from the tracker at the moment of writing — the lesson comments 14 and 16 record, applied rather than restated. gh issue list --state open → #168, #225, #234 — 3 issues. It was 2 before this round (168, 225); the one that joined is the C1 issue this comment is on. Each increment's comment will re-read this rather than write it from memory.
Roadmap entry C1 (#234) has its second increment: S2 is landed — PR #236, merged as 2d5fe7e + 80921f0. S1 was PR #235 (1a6c1d7 + 9795cb0 + a21d9a0 + 845b35e). S3 — documents, retention, and the written bash boundary — is what remains, and the entry gets its Picked up line there, in the same PR as the document update.
What S2 added, in one line: the store S1 filled is now reachable the way the entry asks for it — /undo in the TUI and --undo on the CLI, each restoring the newest tool call that has not been undone, with the files listed before anything changes.
The measurement that makes it a real gate, not a message. The new undo-command.scenario writes a file through the real binary, then undoes it, and the assertion is on the file's bytes: the wrapper shell seeds the file before the run and prints its content after it. The mutation that removes the undone mark fails both gates — the Go test with "last call is 2, want 1", and the scenario, where the second /undo re-presents the same dialog instead of reporting that there is nothing left.
Measurements.go test ./... -count=1 → 14 packages ok, 0 FAIL on merged master (80921f0), and the same under -race; go vet clean; gofmt -l . → 0 files. The scenario suite ran 31 of 34 files (the three gated families skip as designed) — all pass. PR checks 9/9, annotations 11, which is the documented baseline, so none new. Issue #231's acceptance was re-checked on merged master (one diagnostic, exit 0).
A process correction worth recording, because this tracker has been correcting numbers rather than rewriting them: the two S2 commits were first made on master — the branch-before-committing step was skipped. Nothing had been pushed, so a branch was created at the commit and master was rewound to origin/master before the push; the merged history carries the work from the branch. The published record is unaffected, and the slip is the kind that is invisible until something is pushed.
Tracker arithmetic, read from the tracker at the moment of writing.gh issue list --state open → #168, #225, #234 — 3 issues, unchanged from the S1 comment, because S2 was an increment of the issue already open rather than a new one.
Next: S3 (documents, config.example.json's checkpoint_dir, retention and object collection, and the statement that files changed by a bash command or any other subprocess are not captured), then the issue closes and the entry gets its link and its Picked up line together.
Roadmap entry C1 — No checkpoint and no undo is delivered, and its third increment is the one that closes it: #234 → PR #237, merged as dbf5bb3 + ab8167b + e202f67. The full set:
Increment
PR
Commits
S1 — the store, the pre-images, a restore, the call-site ratchet
The entry now carries its Picked up line and the delivered record, including the boundary as part of the entry rather than as a footnote to it, and the reading-order section marks the tier delivered. The document that owns the feature is docs/checkpoints.md.
What the entry did not know, and the increment that found it. The entry counted four call sites that reach a file's bytes; apply_patch's delete operation called os.Remove directly, so a deletion was the one tool-mediated content change with nothing to restore — invisible until a restore was attempted. The census test now pins five (four writes, one delete), and it fails loudly when a sixth appears rather than trusting the next author to remember. That correction is in the entry itself.
Measurements. On merged master (e202f67): go test ./... -count=1 → 14 packages ok, 0 FAIL; -race → 14 ok / 0 FAIL; go vet clean; gofmt -l . → 0 files. make test-tui-scenarios → 31 scenarios ran, all pass. All three PRs: 9/9 checks, annotations 11 — the documented baseline, so none new. Nine mutation checks across the three increments, each failing the gate it guards (the S3 set: pruning disabled → "5 calls kept, want 3"; every call droppable → "2 calls kept, want 4"; an inverted collector → "a kept call lost the pre-image its undo still needs"; the /sandbox report losing the boundary line → the report test).
What is deliberately still not covered, and where it went. Files changed by a bash command or any other subprocess are not captured — nothing tells this layer what such a command will touch. That boundary is documented rather than implied, and the producer that would close it is #238, whose first increment is a measurement (hard-link snapshot latency and disk cost on this repository) before any code, so the question "is this affordable" is answered before it is built.
Two corrections to the record, both kept rather than tidied away. S3's acceptance named a per-write tool-result note; none was added, because a line repeated on every write result is noise on the common path and it is the user who decides whether to undo — the boundary lives in the page, the entry and /sandbox instead, and the issue's closing comment says so. And the S2 commits were first made on master: nothing was pushed, so a branch was created at the commit and master rewound to origin/master, which is recorded here because the tracker is the durable record of how the work went, not only of what landed.
Tracker arithmetic, read from the tracker at the moment of writing.gh issue list --state open → #168, #225, #238 — 3 issues. #234 is closed and #238 is the handoff that replaced it, so the count is unchanged by those two moves; what changed is that the roadmap's C1 is no longer counted among the open work.
Activity
docs(roadmap): register the verified missing-capability inventory
docs(roadmap): register the verified missing-capability inventory
yusiwen commentedon Oct 9, 2026
The inventory landed; this issue stays open as its tracker anchor.
Landed: PR #171, merge commit
3e7c23a—docs/roadmap.md(new, 518 lines): 38 entries across the eight areas of the source inventory,each carrying its code (
A1…H3), the gap, the evidence asfile:line, the mechanism thatalready exists so it is not built twice, and the source report's effort estimate marked as
unverified. Plus a verification-deltas section, the one refuted entry, and a recommended reading
order.
README.md: a link to it beside the existingdocs/references.Corrections the re-check recorded (the source report's own versions are in the deltas table, so
they cannot be re-derived): A2, A3, A5, C2, C3, C4, C5 each described a capability as wholly absent
where a narrower mechanism already exists. E5 is refuted outright — project instruction files are
loaded (
main.go:151withloadProjectContext, four tests), and it is recorded as refuted ratherthan as work. The source's arithmetic was corrected too: 39 items rather than 38, and its P0 table's
rows sum to 31.5–51.5 man-days rather than the "~30–35" it is introduced with.
Measurements: all 83
file:linereferences resolve at7e6b017→checked=83 failures=0. Thesweep was mutation-tested so it is known to be able to fail: a renamed file →
failures=1, exit 1;a line past end of file →
failures=1, exit 1. Both suites green on the tree before branching(
go test ./... -count=1and-race, 13 packages). The PR's nine checks all passed withannotations=0new.Handed off:
CODEBASE.md:568links a document deleted in289d355. Found while sweeping references.staticcheckwent red on go1.27.2 on an unrelated docs-only run; fixed onmasterbypinning that job's toolchain (PR ci(staticcheck): pin the job Go to 1.27.1 until staticcheck reads v5 #173,
f4c3043), which is why docs(roadmap): register the verified missing-capability inventory #171 could go green at all.What this issue is for now: an entry becomes work when it is picked up, as its own issue with
numbered increments (
S1,S2, …), and that new issue gets linked here. No entry has been picked upyet, and the roadmap's line numbers are pinned to
7e6b017, so read them as a starting point ratherthan as a live index — the document says this itself, and it is why the reference sweep was not
committed as a test.
yusiwen commentedon Oct 9, 2026
Picked up: A2 — provider-reported usage is discarded (
docs/roadmap.md:60-71) → #174, with increments S1 (value + both providers), S2 (agent accumulation and oneOnUsageevent per call) and S3 (status bar shows the real number).Out of scope there, and still A3's: cost/pricing and a cumulative budget.
The A2 entry will carry the issue link in the PR that lands it.
yusiwen commentedon Oct 10, 2026
A2 is done and merged; the entry now carries its issue link in the document.
Picked up → delivered: #174, PR
#175, rebase-merged to
masteras5371cc1+d5a625a.What A2's gap actually was. The roadmap's evidence said the usage-only chunk "carries empty
choicesand is skipped". True, but weaker than the real defect: the SSE loop alsobreaks onfinish_reason, and the usage chunk arrives after it — so the chunk was never read off the socketat all. Both halves are now pinned by a fixture that places usage after
finish_reason, and by twomutations that reintroduce each half and fail it.
Measurements
go test ./... -count=1and-race: 13 packages ok, 0 failed, before and after the merge.3e7c23abaseline, per job. No new annotations.find … | xargs grep -h '^func Test' | wc -l).This moves the number again, so #170 gets a note
rather than a silent drift.
7e6b017, and the entrygrew by six lines in this change — every line reference after A2 in the document is therefore
shifted. That is the drift the document already warns about; the durable part (file and symbol) is
untouched.
What this does not close: roadmap A3's cumulative token/cost budget and circuit breaker. A2's
title also mentions "no cost accounting", and cost is deliberately still absent — no price table
exists in the tree, and per-model pricing belongs with the budget work, not with reporting a number
the provider already sends.
Still open in the tracker, unchanged by this: #166, #167, #169, #170, and this anchor. No entry
has been picked up beyond A2.
yusiwen commentedon Oct 10, 2026
Handoff recorded for the A2 pickup: #176 — a defect PR #175 did not fix and did not otherwise
track.
The read-to-
[DONE]change that made the usage chunk reachable also removed the early return atfinish_reason, with no bound in its place. Measured atd5a625aagainst an endpoint that completesits answer and then holds the connection open: the call takes the provider's whole 120 s request
timeout instead of a clean return (with a 1.5 s caller deadline: 1.502 s, versus 2 ms when the stream
ends with
data: [DONE]), andchatStreamreturns the truncated content witherr == nil, so theloop cannot tell it was cut. A2's scope was reporting usage; the bound is its own issue.
yusiwen commentedon Oct 10, 2026
The handoff from the A2 pickup is closed: #176 → PR
#178, rebase-merged to
masteras49964e2.The OpenAI-compatible provider now has the same two-bounds shape as the Ollama one — a 120 s
whole-request bound for a batch call and a 2 min per-line idle bound for a stream — and a read that
is cut returns an error naming the bound instead of the partial answer with a nil error. Measured
against an endpoint that completes its answer and then goes quiet:
err=niland the partial textbefore,
openai stream: context deadline exceededand a nil response after. Nine CI checks green,11 annotations = baseline.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched. On
the roadmap, A2 is delivered and carries its issue link; A3 (cumulative token/cost budget and circuit
breaker) is the next item in A2's own area and is not started.
yusiwen commentedon Oct 10, 2026
A3 is delivered; the entry now carries its issue link in the document.
Picked up → delivered: #179, PR
#180, rebase-merged to
masteras116301b.What landed:
budget.max_tokens_per_runandbudget.max_tokens_per_session, both off by default;a check at the top of the ReAct loop, before the call that would spend, so a run cannot overshoot by
one call; a message naming the bound, the spend and the knob; and
tokens: spent/limitin the statusbar when a session budget is configured (byte-identical to before when none is, which is why the 27
frame goldens are untouched).
Design decisions recorded in the issue before the work started, and honoured: tokens rather than
money (no price table exists, and none can be produced by a command in this repository); off by
default; stop before the call; a silent provider counted as an estimate rather than allowed to switch
the budget off; sub-agents bounded against their own spend.
Measurements
go test ./... -count=1and-race: 13 packages ok, 0 failed, before and after the merge.3e7c23abaseline, per job.ignored; estimate fallback removed; estimate firing the usage event; run total not reset; status bar
always showing a fraction).
find . -name '*_test.go' -not -path './tuiprobe/*' -print0 | xargs -0 grep -h '^func Test' | wc -l.[docs] Five documented test counts, three different values, none correct #170 already tracks this moving number.
A3's remaining half, stated plainly: cost and pricing. The entry's title is "no cumulative token
or cost budget"; tokens are now bounded, money is not, because a price table is a data-maintenance
surface (which models, which currency, refreshed how) rather than a code gap. If that is worth doing,
it is its own issue.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched.
On the roadmap, A2 and A3 are delivered; the other certainties in the recommended order (A1 retry and
backoff, D5 notification, F3 tool-call validation) are not started.
yusiwen commentedon Oct 10, 2026
Recorded while answering a design question about where model prices would come from, because the
answer turned out to be two different kinds of work rather than one:
usage detail that money has to be computed from.
it would be, under which licence, refreshed how, with which staleness policy. Its deliverable is a
written verdict, and "do not vendor a table" is a first-class outcome.
The separation is the point: #181 must not wait on #182, because OpenRouter reports the cost of a
request outright and everything else accepts a user-declared price. Nothing about a table blocks the
pipeline.
The doc correction that came with it (PR #183,
aea5dc3): A3's entry indocs/roadmap.mdused tosay cost needs "a per-model price table, which no command in this repository can produce". That was
written from the baseline and is too strong on two counts, both checked against the providers' own
documentation:
usage.costandcost_details.upstream_inference_cost, so no table is involved at all.by default: DeepSeek prices four input lanes (cache hit vs cache miss) times a peak/off-peak rule —
a 50× spread on
deepseek-flashinput alone. Its API does identify the lane(
prompt_cache_hit_tokens/prompt_cache_miss_tokens), buttypes.Usagecannot hold it yet.The entry now carries both issue links and keeps the narrowing visible, the same way the
verification-deltas section keeps the source report's overstated versions: a wrong reason in a
document is what makes the next reader re-derive the question.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 remain open and untouched;
#181 and #182 are new. On the roadmap, A2 and A3's token half are delivered, and A3's cost half is now
tracked outside the entry.
yusiwen commentedon Oct 11, 2026
A3's cost half is delivered; the roadmap entry says so now, and this anchor has one fewer open
thread.
Picked up → delivered: #181, PR
#184, rebase-merged to
masteras1e3d103+0496da2+9e32eb5.What landed: a route's own charge carried verbatim (
OpenRouter'scost, with the unit named bycost_currency);types.Usagegiven one field per billing lane, because DeepSeek bills four inputlanes times a peak/off-peak rule and a single input price cannot express it; user-declared
pricingrates keyed by
<route>/<model>with wildcards, applied only to reported usage; and unknown costscounted as unknown rather than as zero.
Measurements
go test ./... -count=1and-race: 13 packages ok, 0 failed, before and after the merge.3e7c23abaseline, per job.A CI failure worth recording as a lesson, not just a fix. The first run of PR #184 failed
TUI visual (screenshots + PTY): the new cost segment appeared on every run, because a route thatreports neither usage nor a charge makes every call unpriced — the CI stubs are exactly that. The bar
grew by 26 columns and pushed
Press Ctrl+C again to quitpast the wrap, sopermission-allow-alwaystimed out waiting for text that had been split across two lines. A pure Go test could not see it (the
goldens have no agent run); the gated PTY job is what caught it, and the same runner verified the fix
locally afterwards (31 scenarios, 27 ok, 4 gated). The rule that came out of it: a status bar may
only grow for information the user opted into.
Tracker state: #168 (this anchor) stays open. #166, #167, #169, #170 and #182 remain open and
untouched. On the roadmap, A2, A3's token half and A3's cost half are delivered; the open item that
came out of this work is the price-table decision in #182, which is deliberately independent.
yusiwen commentedon Oct 11, 2026
A1 is delivered, and the entry needed correcting before it could be.
Picked up → delivered: #192, PR #194, rebase-merged to
masterasc36d2ff+b1f8d1d+70d7564.What landed: a shared retry policy for both providers — 408/429/5xx and a response-less transport error are retried with exponential backoff and equal jitter, floored at
Retry-Afterand capped; a context error, a body we cannot parse and every other 4xx are not. The design decision is the safe-replay rule: a streaming attempt is repeated only while nothing has reached the caller, because text already drawn may not be drawn twice. A retry announces itself, and a final failure names how many attempts it took.The correction: A1's "Already present" line pointed at
tool/web_browser.go:297andtool/web_extract.go:44as an existing retry/backoff idiom. Neither is one — the first is the cooldown that expires a timed-out browser-probe verdict, the second a sentence inside theweb_extractdescription — and the only retry-shaped loop in the repository picks a different temp-file name after a collision. The entry now says that plainly. This is the second overstated "already present" line found in the inventory (the first was A2's, corrected in #181); both are recorded rather than deleted, so the next reader does not re-derive the question.Measurements
go test ./... -count=1,-raceandmake test-repeat(-count=3): 13 packages ok, 0 failed, on the PR head and on the merged tree.3e7c23abaseline, per job.Handed off: #193 — a flaky assertion in one of the new tests, which CI caught and which is now attributed, fixed and closed. Filed rather than fixed silently because it was an unattributed failure in a gate this change relied on.
Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182. On the roadmap, A2, A3 (both halves) and A1 are delivered; the remaining certainties are D5 (notification) and F3 (tool-call validation), and A1 was the last of the three the recommended order put first.
yusiwen commentedon Oct 11, 2026
The coverage gap I opened one message ago is closed, and it paid for itself immediately.
Picked up → delivered: #195, PR #197, rebase-merged to
masterasd3cd53a+31a34e0.What it closed. The three status-bar surfaces that A2/A3 added had only string-level assertions on
renderStatusBar. They now have a committed frame at 80x24 and 120x40, and two PTY scenarios: one asserting the reported charge and the budget fraction on a real terminal (cost: 0.0042 credits,tokens: 1500/100000), and one counting that a retried call draws its answer exactly once.make test-tui-scenariosruns 33 files, 29 ok, 4 gated; the frames went 27 → 29 and the scenario files 31 → 33, withCODEBASE.md,README.mdandAGENTS.mdupdated — including two counts that were already wrong before this change.What it found. Writing the scenario reproduced the failure mode this repository has now hit three times, from three directions: a long status bar clips its own trailing status message. At 120 columns the quit confirmation reads
Press Ctrl+C againwithquitoff the line, measured in the scenario dump. Filed as #196 with the reproduction and three candidate fixes; the scenario runs at 200 columns as an explicitly recorded workaround, and should come back down when that is fixed. This is the second time a PTY job has caught something no string assertion could — which is the argument for having written #195 in the first place.Measurements: 13 packages ok plain and
-race;make staticcheckexit 0; nine CI checks green with 11 annotations = the baseline on both the PR head and the merged commit; two mutations verified (the frame golden fails on a changed bar, the scenario fails on a stub that stops reporting usage).Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, and the two new ones from this round — #196 (the clipping) and #193's closed attribution. On the roadmap, A1, A2 and A3 (both halves) are delivered; the remaining certainties are D5 (notification) and F3 (tool-call validation), and the verification debt for what was delivered is now paid down.
yusiwen commentedon Oct 11, 2026
D5 is delivered — the cheapest item in the inventory, and the one whose cost was never in the plumbing.
Picked up → delivered: #198, PR #199, rebase-merged to
masteras14b5ddd+9ff62fc.What landed: a finished background task announces itself, and so does a foreground run that outlived a threshold. The terminal channel writes
OSC 9andOSC 777(the bet the CellGrid already makes with OSC 8 links: a terminal ignores an escape it does not know); the desktop channel runsosascript/notify-sendwith the text as arguments rather than interpolated into a script; onemin_duration_seconds(default 30) gates both; the escape text is sanitized so a task's own goal cannot end the escape and write to the terminal.channel: offturns it off.Where the work actually was. The issue's evidence —
osascript,notify-send,terminal-notifier→ 0 — says the mechanism was absent; it says nothing about the policy, and every decision worth reviewing was there: on or off by default, which channel, a threshold instead of a focus check, no sound, no Windows path, and treating a missing notifier as silence rather than an error. Those went into the issue before the code, which is why the PR body could cite them rather than defend them.Measurements: 14 packages ok plain and
-race;make staticcheckexit 0; nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; six mutations, six assertion failures.One of those mutations earned its keep: the "announce exactly once" test was passing for the wrong reason — the duplicate's zero elapsed time was being swallowed by the threshold, so the once-guard was unpinned. That is now fixed and verified by the mutation. A second gate in the same PR (the announcement must happen outside the manager's lock, or a
Collectwaits on a notification) is pinned by a probe that takes the lock and fails on a bound rather than hanging.Tracker state: #168 (this anchor) stays open. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, #196. On the roadmap, A1, A2, A3 (both halves) and D5 are delivered out of 38 verified gaps; of the four items the document called certainties (A1, A2, D5, F3), only F3 (tool-call validation) is left, and A3's cost half came along with A2's area.
yusiwen commentedon Oct 11, 2026
F3 is delivered, and with it every item this document called a certainty.
Picked up → delivered: #200, PR #201, rebase-merged to
masterasa111efd+a4c4cd4.What landed: a tool call's arguments are checked against the schema the model was shown before the tool runs, with every problem reported at once, and a call that keeps failing the same way is refused as a loop instead of being retried until the step budget is gone.
The census that scoped it. F3's evidence said nothing checked required fields, types or enums — it did not say which keywords the schemas actually use, which is the difference between adding a JSON Schema library and adding five checks. The count (
tool/, product code only):type65,properties24,required15,enum5,items3,additionalProperties1 — and that one istrue, the default, so there is nothing to enforce. A first pass of that census also mistook a property namedpatternfor the keyword; the issue's numbers come from a re-run that excludes test files.Measurements: 14 packages ok plain and
-race;make staticcheckexit 0; nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; seven mutations, seven assertion failures.Roadmap progress, four rounds in: 5 of 38 verified gaps delivered —
A1(retry and backoff),A2(reported usage),A3(token budget, and its cost half in a second issue),D5(notification),F3(tool-call validation). The document's own "certainties, days not weeks" tier wasA1,A2,D5,F3: all four are now done, withA3picked up alongside them.What the pick-ups changed in the document itself, which is worth keeping visible: A2's and A1's entries each overstated what already existed (a counter that was an estimate; a "retry/backoff idiom" that was a probe cooldown and a sentence in a tool description), and F3's entry was silent on the keyword census that decides the implementation's size. Each correction is recorded in the entry rather than deleted, so the next reader does not re-derive the question.
Tracker state: #168 (this anchor) stays open — it is the anchor, not a work item. Open and untouched: #166, #167, #169, #170, #182, #185, #186, #188, #196. The next cheapest certainty-shaped work is gone; what remains is the larger half of the inventory (B, C, D, E, G, H areas) and the open decisions like #182.
5 remaining items
yusiwen commentedon Oct 13, 2026
Two documentation issues closed, and one new one opened — the "look at these" request produced more than the two filed items.
#169 and #170: PR #213, merged as
3c2b64f.tui/frame_shot_test.go,tui/pty_screen_test.go). The sentence now points attuiprobe/README.md+tuiprobe/docs/parity.mdanddocs/tui-verification.md. Sweep after: 0 broken relative links of 28 checked, against 1 before.tuiprobemodule (956) while the prose counted the main module, so it now excludes the nested module and the command, its comment and the prose all agree. Six figures are deliberately left as history — the two inCHANGELOG.mdscoped to their commit,docs/roadmap.md's7e6b017baseline, and three intuiprobe's own released records — because a measurement that was true when taken is not a defect.The finding the request was worth: verifying #169 meant checking whether the files it names exist, and that check does not stop at the one link.
CODEBASE.md's whole "Harness rules" section — the design record an agent or reviewer is told to read before touching the gated harness — namesrunStage,capturePNG,cleanupLauncher,shotTimeout,TestRunStageReportsTimeout,TestCleanupLauncherDoesNotWaitForeverandtui/program_driver_test.go, and none of them exist in either module. Filed as #212 with the measured table and the acceptance that every named path and identifier must resolve by command. It is not absorbed into #169's PR: a stale identifier is worse than a broken link, because the link fails visibly and the name reads as a fact.Tracker: open issues 6, read from the tracker at the time of writing rather than from memory — 167, 168 (this anchor), 182, 188, 196, 212.
yusiwen commentedon Oct 13, 2026
#212 is fixed — PR #215, merged as
796e6a4. It is the finding that came out of the #169/#170 review, so this closes the loop that request opened.What it was:
CODEBASE.md's "Harness rules" section — the design record an agent or reviewer is told to read before touching a test that drives an external process — named seven identifiers and three files that were deleted with the harness (runStage,capturePNG,cleanupLauncher,shotTimeout, two test names,tui/program_driver_test.go,tui/frame_shot_test.go,tui/pty_screen_test.go). The rules survived the move intotuiprobe; the names did not.What the fix turned up that the issue did not know:
resizestep, andtui/geometry_test.gocovers only the model's response to atea.WindowSizeMsg. Filed as #214, with both routes (an in-process driver, or aresizestep intuiprobe— a tool change per the repository's route 3) and an acceptance that the assertion must fail if the program ignores a resize. The section records the gap and links there instead of naming a file that would pretend to cover it.Verification: every path and identifier in the rewritten text resolved by command (
grep -rlfor names,git ls-files/glob for paths — fourteen, all resolving), a whole-file sweep of 82 repo-path-like references with one non-resolving prose mention, and the five tokens my first checker flagged examined by hand (all its own false positives).Tracker: open issues 6, read from the tracker just now — 167, 168 (this anchor), 182, 188, 196, 214.
yusiwen commentedon Oct 13, 2026
#214 is closed — the coverage gap that #212's fix produced, now covered by a real step rather than a file name. Two PRs and a release: #216 (merged
64d7d5f, taggedtuiprobe/v0.1.5) added theresizestep to the scenario runner; #217 (mergedc4d2fb5) bumps the pin and addsresize-running.scenario, which resizes the real binary from 80x24 to 40x12 while it runs and asserts the reflow three ways (--sinceafter amark,fit, and a screenshot whose pixel size is checked against the new geometry).Three things about this one are worth keeping:
pty.Setsize,Session.Resize, the daemon'sresizecommand and thetuiprobe resizeCLI were all there. The gap was that a scenario file could not reach them, which is exactly the kind of gap a documentation fix would have papered over.Verbs(), thendocs/tui-verification.md, thenAGENTS.md). Adding a step is a four-place change and the tool enforces it — recorded because it is a guard working as designed, not friction.resizeline makes the new scenario fail. An assertion that survives the feature's removal is not an assertion.Counts that moved:
tui/testdata/scenarios/*.scenario33 → 34, and the step lists inAGENTS.md/docs/tui-verification.mdgainedresize.CODEBASE.md's sentence claiming nothing resizes a running program was replaced with what covers it.Tracker: open issues 5, read from the tracker just now — 167, 168 (this anchor), 182, 188, 196.
yusiwen commentedon Oct 13, 2026
#196 is fixed — PR #219, merged as
372b9fd. It is the last defect that was sitting inside already-delivered work: the status bar built by the A2/A3 cost-and-budget work could not fit its own trailing status message, and#195's verification work is what found it.The fix (S1 of the three options the issue listed): the bar is budgeted before it renders — the message is measured first and kept whole, the other segments are added left to right while they fit, a segment that does not fit is dropped whole, and a message that no width can fit is clipped with a visible marker. The committed frames now show the screen instead of the model's unclipped output, which is what the acceptance meant by "the frames at both widths show what the terminal would actually display".
Measurements: 14 packages ok plain and
-race;make test-tui-scenarios34 files / 30 ok / 4 gated withstatus-costback at 120x30 (it ran at 200x50 only because of this bug — that workaround is gone); nine CI checks green with 11 annotations = the baseline on the PR head and the merged commit; the regenerated PNG read by eye rather than inferred from text.Three things worth keeping from this one:
TestShotScenariosFitTheirGeometryrequired one of its own shots to be over-wide so the clip was exercised; fixing the bar removed the last such case. Rather than add a PNG to keep the guard's shape alive, the guard is gone and its comment names where the clip is still exercised — same function, same input string, so the coverage never moved.awk 'length($0)'pass reported five over-wide frames; all five were byte counts, and⚡/■are one cell and three bytes. In cells, exactly one frame is genuinely over-wide: the permission dialog's hint row at 40 columns, filed as #218.creditsand would drop the exact fraction that [usage] Stop a run at a cumulative token budget (per run and per session) #179's and [tests] Give the new status-bar surfaces a frame and a PTY scenario #195's assertions pin. A real motivation does not make a formatting change part of a bug fix.Tracker: open issues 5, read from the tracker at the time of writing — 167, 168 (this anchor), 182, 188, 218. Of the defects found inside delivered work, none remain open: #185, #186 and #196 are closed.
yusiwen commentedon Oct 14, 2026
#218 is closed — PR #220, merged as
6b34e58. It was the finding #196's own measurement work produced: the permission dialog is drawn below the cell grid, so nothing trims it against the terminal, and its hint row was 54 cells at 40 columns — the user read a sentence with no end,Esc cancelbeing the part that went missing.What landed: the three key hints became lists of alternatives, and the input area budgets itself against
m.width—fitHintRowpacks whole alternatives into lines (the trailing·marks a wrapped line so it still reads as one list),wrapInputTextwraps the dialog's heading and answers at word boundaries, and a word that cannot fit a line even alone is split across lines rather than clipped. Nothing is dropped to make room at 40 columns.Four things worth keeping from this one:
[3]marker) over-wide as well. The committed frame could not show the third one because the fixture was wrong: it carried three answers with a stale label (Allow for this session— the sandbox offersAllow session), which is how a fixture silently stops covering the thing it names. It now carries the real four.config.js/on). The mutation that narrows it turnsTestPermissionDialogTakesTheRealAnswersAtFortyColumnsred, so the choice is held by an assertion rather than by a comment.golden.Clip, the golden suite now asserts the opposite — every committed frame fits the geometry it claims, 0 over-wide out of 29 — and the clip keeps a synthetic over-wide fixture whose own width is asserted first. Keeping a defect to test the mechanism is not coverage.fitnote says the emulator wraps at its own width, so the tail of an over-wide line is still on screen and await --textfor it passes either way. The gates are the committed frame and the model tests; writing that down is what stopped a scenario that would have looked like evidence.Filed from this work: #221 — the command palette's and provider picker's rows are still drawn as-is (widest palette row 36 cells, so they fit the narrowest geometry the suite covers, 40, and would be cut below it).
Measurements: 14 packages ok plain and
-race;make staticcheckclean;make test-tui-scenarios34 files / 30 ran / 4 gated;dialog_40x12regenerated anddialog-narrow-40x12.pngread by eye; 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit; six mutations run, each named by the gate it turns red.Docs corrected in the same PR (found while editing): the frame counts in
docs/tui-verification.md,tui/scenarios_test.goandtui/usage_test.gostill said 27 where there are 29, and thestatusbarentry inframeScenariosstill described the model as not clipping the bar, which #196 changed.Tracker: open issues 5, read from the tracker at the time of writing — 167, 168 (this anchor), 182, 188, 221.
yusiwen commentedon Oct 14, 2026
#221 is closed — PR #222, merged as
b287cdf. It was filed from #218's work: the input area is drawn below the cell grid, so nothing trims its rows, and #218 had only budgeted the permission dialog. The palette's widest row is 36 cells and the provider picker's labels are built from the user's own provider names (openrouter-anthropic-claude-sonnet (mock)is 41), so at 30 columns a description or a provider name lost its tail.What landed: palette rows and picker rows are budgeted against
m.width; one sharedrenderHeadingbudgets theCommands:/Select provider:headings and the dialog's title;splitAtCellsprefers a break point that fits. Two new frame sets (providerat 80x24 and 30x12,paletteat 30x12) with narrow screenshots.Four things worth keeping from this one:
/diagnostics LSP/diagnostics— so a description that does not fit beside its name in full now takes a line of its own and gets the whole width. And the picker's label split asopenrouter-anthropic-claud/e-sonnet, so a long token now breaks after a hyphen, slash or dot when one fits. Neither was visible in the code; both were visible in the regenerated frame a minute after it was written.Commands:andSelect provider:headings "are short and need no change". The picker's heading is 16 cells before its style padding, and the 16-column assertion caught it at 18 cells. It is budgeted here rather than written down as an exception.>in column 0 where the palette's already is — and the new 80x24 golden is what pins that, because the picker had no frame coverage at all before this.Measurements: 14 packages ok plain and
-race(also onmasterafter the merge);make staticcheckclean;make test-tui-scenarios34 files / 30 ran / 4 gated (unchanged baseline); 32 committed frames, 0 over-wide; 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit; six mutations run, each named by the gate it turns red; both new images read by eye.Docs corrected in the same PR: the frame count 29 → 32 in
docs/tui-verification.mdandtui/usage_test.go, and the judgments list gained the palette's layout rule.Tracker: open issues 4, read from the tracker at the time of writing — 167 (a chore), 168 (this anchor), 182 (a usage decision), 188 (a docs diagram). No defect that was found inside delivered work is open.
yusiwen commentedon Oct 14, 2026
#167 is closed — PR #223, merged as
31ba725. It was the chore the C3 note indocs/roadmap.mdpointed at: the agent workflow keeps a session's scratch worktree inside the workspace root (.worktrees/<scope>), and nothing ignored that directory, sogit worktree add .worktrees/probe …left the outer tree reporting?? .worktrees/and a plaingit add -Astaged the worktree as an embedded repository — a gitlink in place of the files, which git warns about and records anyway.What landed:
.worktrees/in.gitignorewith its one-line reason, plus the two documents that would otherwise have been wrong — the roadmap C3 note (which said "#167 tracks ignoring it") and a changelog entry.Two things worth keeping from this one:
mkdir -p .worktrees/x && git status --short— an empty directory, which git never reports whether it is ignored or not. Control: an unignored empty directory also prints nothing, while a file inside it prints?? …. The clause was measured as vacuous atb287cdf, kept, and strengthened with a file inside the ignored directory; the strengthened form and clause 2 both fail before and pass after. A criterion that cannot fail is not evidence, and the fix was to make the criterion observable rather than to declare it met.git status --shortprinting?? .worktrees/is what a reader notices;git add -Aturning the worktree into a gitlink is what actually reaches a commit — git only warns (adding embedded git repository), and a clone of the outer repository cannot obtain the files. The acceptance therefore assertsgit add -A --dry-runprints nothing, not merely that status is clean.Measurements:
git check-ignore -v .worktrees/→.gitignore:41; 14 packages ok plain and-race(also onmasterafter the merge);make staticcheckclean;gofmtclean; 9/9 CI checks with 11 annotations = the baseline on both the PR head and the merged commit; the merged diff is three files and contains no.gofile.Honest scope: this property has no automated gate — the acceptance is two shell properties and no tracked
.gofile reads.gitignore. The before/after table is the evidence, and the failure is self-announcing.One observation handed back, not filed: the branch commit was GPG-signed (
%G?=G), but GitHub's rebase-merge produced an unsigned31ba725, and every commit onmasteris unsigned. The repository states no signing policy, so this is a merge-strategy property rather than a defect, and it is the maintainer's call whether to change it (branch protection or a different merge strategy).Tracker: open issues 3, read from the tracker at the time of writing — 168 (this anchor), 182 (a usage decision), 188 (a docs diagram). No defect found inside delivered work is open.
yusiwen commentedon Oct 14, 2026
#188 is closed — PR #224, merged as
9058599. It was filed against the README's System Overview box, which listedmemoryamong the tools; #187's figure had already replaced that diagram, so what remained was the decision the issue asked for — plus an audit that turned up two things the issue did not have.What landed:
CODEBASE.md's "Complete Tool Name Registry" gains the missingweb_extract_browserrow;memory.go,Agent.Memory/MemoryModeand theMemoryStoreentry are marked internal and unwired; the TUI housekeeping whitelist drops the phantom"memory"key;docs/roadmap.mdB1 records the decision; andtool/documented_tools_test.gonow reads the table and the code and fails when they disagree.Three things worth keeping from this one:
main.go" — true of every name in the table, while the table was missing a registered tool. A list and a registry have to be compared in both directions, and only one of them was written down.mcp_*row stood in for one tool whileweb_extract_browserwas absent — so counting rows reproduced the documented "24 tools" exactly, and the arithmetic a reviewer reaches for agreed with the prose while a real tool was missing.memoryis internal and unwired, but the capability gap is real; the resolution was to point at the roadmap entry that already tracks it (B1/B2, behind a privacy decision) and mark the code internal — not to file a duplicate issue, and not to give the client a config switch it has no caller for.Measurements: 24 code-defined tool names against 24 static
ag.AddToolregistrations; 14/14 packages ok plain and-race, also onmasterafter the merge;make staticcheckclean;gofmtclean;make test-tui-scenarios34 files / 30 ran / 4 gated with no golden moved; 9/9 CI checks with 11 annotations = the baseline on the PR head and on the merged commit; four mutations each named by the gate assertion it turns red.A number in the issue's own acceptance was wrong:
grep -c 'ag.AddTool' main.goprints 25, not 24, because the MCP registration loop is itself anag.AddToolcall site. The 24 static registrations are what the documents mean and they are correct.Honest scope: the gate covers
CODEBASE.md's table only —README.md:52groups web and LSP under category words rather than enumerating them, so there is nothing to compare row for row. The TUI housekeeping whitelist stays ungated, and the mute path it lives in has no test at all (pre-existing), which is how a phantom tool name survived there.Tracker: open issues 2, read from the tracker at the time of writing — 168 (this anchor) and 182 (a usage decision). Both are decisions rather than defects; no defect found inside delivered work is open.
yusiwen commentedon Oct 14, 2026
#182 is closed — PR #227, merged as
0a1b5fb. It was the data-governance question the cost work (#181) left open: whether to ship a model price table, and whose data. Its deliverable was a written verdict rather than code, and the verdict is no table, recorded in docs/pricing-decision.md.What landed: the decision document with the seven sections the issue's acceptance names (source, licence/provenance, update mechanism, cadence and staleness policy, coverage and key, tiering, rejected options),
docs/roadmap.mdA3 repointed from the open question to the verdict, the docs index inREADME.md, and a changelog entry. No application code.Three things worth keeping from this one:
off_peak_pricingbeside it and the other carries the off-peak rate as its only number. DeepSeek's peak is 20.8% of the week. A vendored single value is therefore ambiguous, not merely stale, which is a stronger and checkable reason to refuse than staleness.agent.Pricehas no time dimension, so the shippedconfig.example.json— which declares the peak rates — estimates 2× too high for about 79% of the week with nothing saying so. The issue framed tiering as an argument against vendoring; it applies equally to the user-declared path that already shipped, and that turned out to be the more useful half of the answer.Measurements: LiteLLM 4,084 entries / 2.8 MB with
off_peak_pricingon 10 andtiered_pricingon 51, MIT inside a split licence GitHub reports asNOASSERTION; models.dev 229 providers / 8,832 models / 5.6 MB, MIT cleanly, with no time dimension anywhere; DeepSeek's rates and peak window quoted from its own pricing page (peak 01:00–04:00 and 06:00–10:00 UTC, Mon–Fri); 14/14 packages ok plain (a regression check — no Go file changed); 9/9 CI checks with 11 annotations = the baseline on the PR head and the merged commit.Honest scope: aggregator feeds were recorded as "not fetched" rather than described second-hand; the licences were read, not legally reviewed; the tiering evidence covers DeepSeek Flash's lanes and both datasets' schema shape rather than all 4,084 entries; and nothing is implemented — #225 and #226 are filed, not started.
Tracker: open issues 3, read from the tracker at the time of writing — 168 (this anchor), 225 (the pricing schema's time dimension), 226 (the retired default model name). No defect found inside delivered work is open.
yusiwen commentedon Oct 14, 2026
#226 is closed — PR #230, merged as
bbce8e0. It was the naming debt the pricing verdict (#182) noticed and filed rather than fixed:config.DefaultConfig()sentdeepseek-v4-flash, which DeepSeek footnotes as a retired name — still accepted, served by DeepSeek-V4.1-Flash, billed at the Flash price. What made it worth a change is not the spelling but the key: the model name is what a declared rate is looked up by (<route>/<model>), so an alias is a second name for one route, and a price table naming only one of the two silently fails to price the other.What landed: the default, the shipped example and the example's
pricing.priceskey all namedeepseek-flash, withREADME.mdandCODEBASE.mdin prose;docs/pricing-decision.md§5 records the rename as done while keeping the alias-resolution argument; the example'spricing.$commentnow says the DeepSeek rates below are its peak rates, 2× too high for about 79% of the week until #225 can express the off-peak half; andTestDefaultConfigSendsTheCanonicalDeepSeekModelasserts the name in the request body thatDefaultConfig→buildProviderRegistry→ the wire actually produces. CI onbd8b62a: 9/9 jobs, 11 annotations, the baseline.go test ./... -count=1→ 28 packages ok, 0 FAIL;-racethe same;make test-tui-scenarios→ 34 files, 30 ran, 4 gated.Three things worth keeping from this one:
DefaultConfig()carriesmodel: deepseek-flash", and the difference is real: the config record'sModelis what a provider may still override, while the body is what the server bills on. The test builds the real registry, points it at anhttptestserver and reads the captured body — so it would also catch the rename being lost inSetRouteInfoor in the request builder, which a struct-field assertion cannot. Mutatingconfig.goback to the alias fails it.deepseek-v4-flashsurvives only where it is about the legacy name. The tree ends with 11 occurrences, and each one had to be argued rather than aimed at zero: a fixture that keys a table on the alias is the evidence that the alias still works, and the test's own failure message has to name the string it rejects, or the next reader cannot tell what failed.config.example.jsonnever loading: a$commentinside theagentsmap is decoded as data,loadFilereports it, and both callers then merge nothing, so a user who copies the template loses every section. That is pre-existing and wider than a rename, so it is filed with two candidate fix shapes instead of being smuggled into this PR; the example-level gate waits for it.yusiwen commentedon Oct 14, 2026
#229 is closed — PR #232, merged as
0186074. It was the defect last round's gate could not be written around:config.example.jsonopens with "Copy to config.json" and could not survive that copy, because itsagentsblock carries a$commentandagentsdecodes into a map, so the note was read as an entry.loadFilereported the error and both callers read it as "no file", so the whole configuration — five providers,pricing,sandbox,notify,budget— was silently replaced by the defaults.What landed:
loadFiledrops every object key whose name begins with$before decoding, at any depth, so the convention the file teaches holds where it is written rather than only where the container happens to be a struct;config.example.jsonstates the rule and keeps every$commentwhere it was; four tests inconfig/shipped_example_test.goread the template itself, so it cannot rot unseen again;CODEBASE.mdgains theloadFileentry it never had. CI ona19c8d5: 9/9 jobs, 11 annotations, the baseline.go test ./... -count=1→ 28 packages ok, 0 FAIL;-racethe same;make test-tui-scenarios→ 34 files, 30 ran, 4 gated.Three things worth keeping from this one:
$commentlooked safe because every instance sat inside a struct, whereencoding/jsonignores unknown fields. It was never safe: the schema has four map positions (agents,pricing.prices, an MCP server'senvandheaders), and a note in any of them costs the file. The fix belongs in the loader because that is the only place that knows the rule is about documentation, not about a section.printf '{"providers": 'showed the diagnostic still appears, which is what makes "stderr was empty" mean something — and that control is what turned up #231.One correction to the issue text itself:
AddAllowedPathdid not need the "same treatment" its loader-side option describes. It round-trips raw JSON and preserves$keys, which is the behaviour we want, so it was left alone andTestAllowedPathRoundTripKeepsDollarCommentspins that instead.yusiwen commentedon Oct 16, 2026
#231 is fixed — PR #233, merged as
f21e7a8+7ac3282+596c679.It is the finding #229's control run produced, and it was the last thing this tracker owed a record.
main.goparsed the user layer twice at startup —UserSandboxBoundary()atmain.go:556andLoadConfig()atmain.go:74— so a malformed~/.tinycode/config.jsonprinted the same line twice and the same path was read twice.config.Layersnow reads each file once per process start and every startup reader takes that value: the merged configuration, the sandbox boundary, the confine-commands resolver (bothapplySandboxCapabilityPolicycall sites, which keep their positions), the persistent-grant loader and--list-grants.loadFileis split into the read anddecodeConfig, so the typedConfigand the raw object come from the same bytes and a malformed file is reported once throughreportIgnoredConfig. The #162 rule is untouched: the project-local layer still cannot set or extendsandbox.project_root,sandbox.allowed_pathsor the confinement booleans.Measurements. Reads were counted with a FIFO at the user config path, served one write at a time, so a process that wants a second read blocks in
open():--list-sessions3 → 1,--list-grants4 → 1, interactive start 5 → 1. Withprintf '{"providers": ' > ~/.tinycode/config.json,tinycode --list-sessionsreportsgrep -c ignoring= 1 (was 2), the same path and message, exit code 0; a malformed./.tinycode/config.jsonlikewise 2 → 1 with exit 0, while--list-grantson a malformed file still exits 1 withError: parse ….Independently re-measured on the merged default branch (
57a4de2, from a different session):go test ./... -count=1→ 14 test packages ok, 0 FAIL;go vet ./...andgofmt -l .both clean; the 9 CI checks allsuccesswith annotations at the documented baseline (1 per job, 2 inbrowserandtui-visual) — no new annotation.Tracker arithmetic, read from the tracker at the moment of writing — the lesson comments 14 and 16 record, applied rather than restated. Live open set after this merge:
gh issue list --state open→ #168, #225 — 2 issues. It was 3 before the merge (168, 225, 231), so the count moved by exactly the one issue this PR closes.Not verified here. staticcheck did not run locally (unavailable on this machine;
make staticcheckpins v0.8.1, whosex/toolsdoes not support Go 1.27) — the PR'sstaticcheckjob is the gate and it passed. The gated suites (TUI visual/scenario, browser, LSP, live provider) ran only in CI. The three commits were signed when pushed; the rebase merge rewrote them, so the commits on the default branch carry no GPG signature — a property of this repository's merge strategy, not of this change.No follow-up issue, nothing handed off, and no roadmap entry changes: this is not one of the 39 inventory items — it is a defect found while landing #229.
yusiwen commentedon Oct 16, 2026
Roadmap entry C1 — No checkpoint and no undo (
docs/roadmap.md:234-248) is picked up: #234, with increments S1 (the store, the pre-images and a restore), S2 (the/undosurface) and S3 (documents, retention, the boundary). S1 is landed — PR #235, merged as1a6c1d7+9795cb0+a21d9a0+845b35e. The entry will carry its Picked up line in S3, when the document is updated in the same PR as the retention rules.Why this entry and not the ones above it: the document calls C1 the single hardest gap in the inventory, and the one a user must trust before letting the agent write. Tier 2 (B1/B2, memory) needs a privacy decision before an implementation, so it is not work that can start without the user; tier 4's C7 (macOS command confinement) is real daily cost but its own spike; the tier-5 bets (B4, F1) need a spike that answers "is this worth it" first. C1 is the one whose value is felt on the first real mistake.
What S1 found that the entry did not know.
apply_patch's delete operation wrote throughos.Remove(tool/apply_patch.go:198) rather than the write path, so a deletion was the one tool-mediated content change with nothing to restore. The new census test pins the five sites that change content (four writes, one delete) so a sixth cannot appear silently — that is a correction to the entry's own framing, which counted four call sites.Measurements.
go test ./... -count=1→ 14 packages ok, 0 FAIL on mergedmaster(845b35e), and 14 ok / 0 FAIL under-race;go vetclean;gofmt -l .→ 0 files; PR checks 9/9, annotations 11 (the documented baseline, so none new). Three mutations, each producing the failure the gate exists for: recording disabled → "recorded 0 calls, want 3"; the delete put back onos.Remove→ "recorded 2 calls, want 3" plus the census failing; a call site handedcontext.Background()→ the census names the line. Issue #231's acceptance was re-checked on this branch and on merged master (diagnostic printed once, exit 0), because the new wiring sits beside that fix's startup path.Not verified here. The real-binary PTY path arrives with S2, which adds
/undo; the gated suites (TUI visual/scenario, browser, LSP, live provider) ran only in CI. Files changed by abashcommand or any other subprocess are not captured — that producer is its own issue, to be filed when S2 lands.Tracker arithmetic, read from the tracker at the moment of writing — the lesson comments 14 and 16 record, applied rather than restated.
gh issue list --state open→ #168, #225, #234 — 3 issues. It was 2 before this round (168, 225); the one that joined is the C1 issue this comment is on. Each increment's comment will re-read this rather than write it from memory.yusiwen commentedon Oct 16, 2026
Roadmap entry C1 (#234) has its second increment: S2 is landed — PR #236, merged as
2d5fe7e+80921f0. S1 was PR #235 (1a6c1d7+9795cb0+a21d9a0+845b35e). S3 — documents, retention, and the writtenbashboundary — is what remains, and the entry gets its Picked up line there, in the same PR as the document update.What S2 added, in one line: the store S1 filled is now reachable the way the entry asks for it —
/undoin the TUI and--undoon the CLI, each restoring the newest tool call that has not been undone, with the files listed before anything changes.The measurement that makes it a real gate, not a message. The new
undo-command.scenariowrites a file through the real binary, then undoes it, and the assertion is on the file's bytes: the wrapper shell seeds the file before the run and prints its content after it. The mutation that removes the undone mark fails both gates — the Go test with "last call is 2, want 1", and the scenario, where the second/undore-presents the same dialog instead of reporting that there is nothing left.Measurements.
go test ./... -count=1→ 14 packages ok, 0 FAIL on mergedmaster(80921f0), and the same under-race;go vetclean;gofmt -l .→ 0 files. The scenario suite ran 31 of 34 files (the three gated families skip as designed) — all pass. PR checks 9/9, annotations 11, which is the documented baseline, so none new. Issue #231's acceptance was re-checked on merged master (one diagnostic, exit 0).A process correction worth recording, because this tracker has been correcting numbers rather than rewriting them: the two S2 commits were first made on
master— the branch-before-committing step was skipped. Nothing had been pushed, so a branch was created at the commit andmasterwas rewound toorigin/masterbefore the push; the merged history carries the work from the branch. The published record is unaffected, and the slip is the kind that is invisible until something is pushed.Tracker arithmetic, read from the tracker at the moment of writing.
gh issue list --state open→ #168, #225, #234 — 3 issues, unchanged from the S1 comment, because S2 was an increment of the issue already open rather than a new one.Next: S3 (documents,
config.example.json'scheckpoint_dir, retention and object collection, and the statement that files changed by abashcommand or any other subprocess are not captured), then the issue closes and the entry gets its link and its Picked up line together.yusiwen commentedon Oct 16, 2026
Roadmap entry C1 — No checkpoint and no undo is delivered, and its third increment is the one that closes it: #234 → PR #237, merged as
dbf5bb3+ab8167b+e202f67. The full set:1a6c1d79795cb0a21d9a0845b35e/undoand--undo2d5fe7e80921f0dbf5bb3ab8167be202f67The entry now carries its Picked up line and the delivered record, including the boundary as part of the entry rather than as a footnote to it, and the reading-order section marks the tier delivered. The document that owns the feature is docs/checkpoints.md.
What the entry did not know, and the increment that found it. The entry counted four call sites that reach a file's bytes;
apply_patch's delete operation calledos.Removedirectly, so a deletion was the one tool-mediated content change with nothing to restore — invisible until a restore was attempted. The census test now pins five (four writes, one delete), and it fails loudly when a sixth appears rather than trusting the next author to remember. That correction is in the entry itself.Measurements. On merged
master(e202f67):go test ./... -count=1→ 14 packages ok, 0 FAIL;-race→ 14 ok / 0 FAIL;go vetclean;gofmt -l .→ 0 files.make test-tui-scenarios→ 31 scenarios ran, all pass. All three PRs: 9/9 checks, annotations 11 — the documented baseline, so none new. Nine mutation checks across the three increments, each failing the gate it guards (the S3 set: pruning disabled → "5 calls kept, want 3"; every call droppable → "2 calls kept, want 4"; an inverted collector → "a kept call lost the pre-image its undo still needs"; the/sandboxreport losing the boundary line → the report test).What is deliberately still not covered, and where it went. Files changed by a
bashcommand or any other subprocess are not captured — nothing tells this layer what such a command will touch. That boundary is documented rather than implied, and the producer that would close it is #238, whose first increment is a measurement (hard-link snapshot latency and disk cost on this repository) before any code, so the question "is this affordable" is answered before it is built.Two corrections to the record, both kept rather than tidied away. S3's acceptance named a per-write tool-result note; none was added, because a line repeated on every write result is noise on the common path and it is the user who decides whether to undo — the boundary lives in the page, the entry and
/sandboxinstead, and the issue's closing comment says so. And the S2 commits were first made onmaster: nothing was pushed, so a branch was created at the commit andmasterrewound toorigin/master, which is recorded here because the tracker is the durable record of how the work went, not only of what landed.Tracker arithmetic, read from the tracker at the moment of writing.
gh issue list --state open→ #168, #225, #238 — 3 issues. #234 is closed and #238 is the handoff that replaced it, so the count is unchanged by those two moves; what changed is that the roadmap's C1 is no longer counted among the open work.