26 KiB
Row 53 (#1542): Console tokens and shell restyle
Dewey, 2026-10-10. Brief: docs/plans/2026-10-10_design-implementation.md,
section "Console tokens and shell restyle" (origin/refactor a81915e8), with
docs/design/IMPLEMENTING.md sections "Tokens", "Icons and the mark",
"Components", "Strings", "Boundaries" and "Acceptance". Lead decision 81
(Harbor following the system setting by default, the Relay placeholder mark
stays, no Decision Seen from the web).
Built on 923957e2; no file under packages/webui, docs/design or the
test scripts changed between it and the gate base 8a7871ff (only
scripts/mosaic-task.mjs, row 56). Candidate: 16 files, uncommitted in the
canonical checkout, listed in candidate-manifest.sha256 beside this file
(round 1 manifest sha256 75569953c1a2cd7b7266208d6e6607290335f1bfe0cf94d96ed224e480a1dda8;
rounds 2 and 3 below; round 3's is current).
Round 3 (answers #1542 comments 27163 and 27165)
Scope from Sage: two fixes, each with a test. 1: a board 403 followed by a 503 or 500 must not bring back the refused scan's rows or waiting card (Filbert C4, Darkwing P9). 2: a board 403 must clear an open conversation's history (Darkwing P10).
Round 3 candidate: the same 16 files, manifest sha256
2562c05d4a92f90096c0fe68ef8c2adfa7244fa547f3390155f5bee5cce64cba
(candidate-manifest.sha256 beside this file, replacing round 2's
78c3aef9). Three files changed from round 2: README.md,
src/public/app.js, tests/shell.test.mjs. The round 2 to round 3 delta is
round3.patch beside this file. Applied to the round 2 files, it reproduces
the manifest (16 OK, checked). Base for the gate: 03969219.
Fixes
| Item | Change | Test |
|---|---|---|
| Filbert C4, Darkwing P9: a 403 then a 500 or 503 showed the refused scan's table and waiting card again | error() now draws the empty board whenever it holds no data, not only on a refusal. It clears #waiting, #seen and #sessions, sets the three counts to –, closes the inspector, and says why in the tree, the footer and #status: "the board refused the read" or "the read failed". |
Shell browser test: after the refusal, brd.unavailable() (503) and a refresh. The banner is banner err with "No board data loaded."; #board-view shows again; no session row, open or history button, waiting card or seen card; the three counts are –; the footer and #fresh-board read "Board not read: the read failed", #status "Not read: the read failed", the tree "Not read: the read failed.". Screenshot board-refused-then-failed-400. |
Darkwing P10: a 403 with History open left #conversation and the old #conv-log messages |
On a refusal with a conversation open, error() empties #conv-log and calls closeConversation(), which hides the panel and its reply form. |
A session with registered set; History open; the fixture message "Fixture history line" in #conv-log and the reply form shown. After a 403: #conversation hidden, no has-conversation on <body>, #conv-log has no child, #conv-form not visible, the board hidden and the refusal state shown. Then recovery. |
Test fixtures in shell.test.mjs's stub board: one() adds a third
session that is seen (so the seen card is tested), registered() answers
one live session with a conversation, unavailable() answers 503, and with
status 200 the stub answers /api/conversations and /api/conversation
with one fixture page. The new assertions sit in the "shell in a browser"
test, where the board refusal state already lives.
Also answered
| Item | Done |
|---|---|
| Filbert T7: the counts after a refusal | Asserted: #waiting-count, #seen-count and #session-count are – after the 403 and after the 503 that follows it. |
| Filbert N7 (my call): the project filter on a refusal | Tested, not changed: a project is picked before the refusal (aria-pressed true), and after recovery the "all" filter is the pressed one. |
| README | The refusal line in the command bar section now names the cards, the project filter, the empty board on a later failure and the closed conversation. The shell tests line names the two new cases. |
Changes in behaviour beyond the two fixes
- A failed read before the first scan (no data yet, not a refusal) now draws
the empty board with "Not read: the read failed" in the tree, footer and
#status, where round 2 left the skeleton rows, "Loading board…" and "Loading projects…" under the error banner. This is the same code path as the C4 fix; the banner was already shown in both. error()no longer callsconvForm(). With P10, a refusal closes the conversation andconvForm()does nothing without one; a non-refusal failure with no data never had an open conversation. Theerror-noconvformmutant survived for that reason and is dropped.
Round 3 mutants
Each applied to a scratch copy of the round 3 tree, then shell.test.mjs
run (~/dewey-scratch/r53b/r3/mutants.py, outputs in r3/out/). 13 of 13
killed, every one by the "shell in a browser" test.
| Mutant | Change | Killed at |
|---|---|---|
| c4-no-clear | no-data branch leaves the cards and the table | no rows or cards after the 503 (11, expected 0) |
| c4-keep-sessions | the table kept | same (7) |
| c4-keep-waiting | the waiting cards kept | same (2) |
| c4-keep-seen | the seen cards kept | same (2) |
| c4-refused-only | the empty board only on a refusal (round 2) | footer "refused" after the 503 |
| c4-footer-refused | footer always says "refused" | footer after the 503 |
| c4-status-refused | #status always says "refused" |
#status after the 503 |
| c4-tree-refused | the tree always says "refused" | tree after the 503 |
| error-keeps-counts | the counts left as they were | #waiting-count after the 403 |
| error-noinspect | the inspector left open | #inspector hidden after the 403 |
| error-keeps-project | a 403 keeps the project filter | "all" filter pressed after recovery |
| p10-no-close | the conversation left open | #conversation hidden after the 403 |
| p10-no-clear | the panel closed but the history kept | #conv-log empty after the 403 |
Round 3 gate
Worktree at 03969219 plus the 16 round 3 files (sha256sum -c of the
manifest: 16 OK), node_modules linked from the checkout, TMPDIR in
scratch, DOCKER_HOST pointed at a missing socket, suites sequential,
2026-10-10T19:13:43Z to 19:18:19Z. core.hooksPath unset. Script
~/dewey-scratch/r53b/r3/gate.sh, outputs ~/dewey-scratch/r53b/r3/gate/out/.
| Suite | Result |
|---|---|
node --test 'packages/webui/tests/*.test.mjs' |
27 pass, 0 fail |
| node suites of business, bus, cli, control-board, conversation, discord, ledger, mosaic, queue, runs, seat, tasks | 60, 67, 66, 124, 182, 178, 78, 69, 148, 41, 19, 51 pass; 0 fail in each |
node docs/design/tools/build-tokens.mjs --check |
rc 0, "tokens.css is current" |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed |
| test-release, no Docker | 4 passed, 0 failed (state-machine cases skipped: daemon unavailable) |
| test-task, no Docker | 26 passed, 0 failed (adapter seam, workspace and live cases skipped) |
Round 2's gate had Docker for test-release and test-task (14 and 98). Round 3 changes nothing those Docker cases run; Sage's gate rerun covers them.
Not in this round
Darkwing note 3, the refresh-timer focus race in bus.js render(): it is
in HEAD, so per Sage it goes to row 54 with its own test, named there as a
fix to HEAD behaviour beside the H1 keep()/restore() fix.
Round 2 (answers #1542 comments 27143 and 27148)
Round 2 candidate: the same 16 files, manifest sha256
78c3aef963b7e35500c9f8afa6341057f1a6372ced70915d175e507b5fac8406
(candidate-manifest.sha256 beside this file, replacing round 1's
75569953). Five files changed from round 1: README.md,
src/public/app.js, src/public/bus.js, src/public/shell.css,
tests/shell.test.mjs. The round 1 to round 2 delta is
round2.patch beside this file. Base for the gate: 2d4d4e7d (nothing under
packages/webui, docs/design or scripts changed since 8a7871ff).
Blocking items
| Item | Change | Test |
|---|---|---|
Filbert C1, Darkwing 1: a board 403 left the inspector, project tree, counts, footer and #status "Scanned" line |
error() on a 403 drops data, selected and project, closes the inspector, and sets the tree to "Not read: the board refused the read.", the three counts to –, the footer to "Board not read: refused" and #status to "Not read: the board refused the read". The next good read restores them through accept() and render(). |
Shell browser test opens the inspector and checks the footer "waiting 1 · working 1" and two tree counts before the refusal; after it, the inspector is hidden, <body> has no has-inspector, the tree has no .count or [data-project], the footer and #fresh-board read "Board not read: refused", #status has no "Scanned". After recovery the footer is back. |
| Filbert C3: the freshness line called a refusal "stale" | fresh() with no data: "Board not read: refused", "Board not read: the read failed" or "Board not read yet". |
Same test: #fresh-board equals "Board not read: refused". |
| Filbert C2, Darkwing 2 and note 6: a bus refusal left the Inbox count, BLOCKING chip and palette rows | forget() clears last and the Inbox count on any refusal: in the view's read, in the board route's inbox read (was catch {}), and in pkOpen's refetch, which then redraws the section list. A refused read anywhere drops every kept bus read, so the pre-refusal rows can't come back as stale on a later failure either (Darkwing's P7). The bus freshness line reads "Bus not read: refused". |
Five-states test: #sections first reads "Inbox 1 1 blocking"; after the refusal it has no .count or .tag-block, and Ctrl+K lists only the four views. A second step refuses only pkOpen's own refetch and checks the kept tasks and the count are gone. On the board page a no-bus-host refusal of the board route's inbox read drops the count and the palette shows views only. |
| Filbert T1, Darkwing 4 (M01): only 403 was tested as a refusal | none needed | The five-states refusal step runs for human-required (403) and no-bus-host over kept rows: banner code, 0 rows, "Nothing to show.", "Bus not read: refused", no section counts, then recovery. |
| Darkwing 3: tests with the inspector open | as C1 | as C1 |
| Darkwing 5 (M08): palette labels with markup | none needed | The inbox question is <img src=x onerror="errors.push(1)" id="pk-q">Push? and a task title <b id="pk-t">Bold</b> task. The palette lists views plus three items, no img or b in the list, the labels equal the literal text, and window.errors stays empty. |
| Sage: the section list below 760px | The deviation is withdrawn. Below 760px .s1-sections is one strip, flex-wrap:nowrap; overflow-x:auto, items flex:none; it scrolls inside itself, so row 54's three sections add no rows. The command bar keeps its four controls on one row. |
At 360px and 400px: every section link has one offsetTop, the list's computed overflow-x is auto, no label wraps, and the page has no sideways scroll. |
Non-blocking items
| Item | Done |
|---|---|
Filbert T2 (accept() keeps failed) |
Test: a failed refresh, then a good one; #fresh-board no longer says "stale". |
| Filbert T3 (clipboard-denied toast stays) | Test: after 3 s the error toast's class is still toast on err. |
| Filbert T4 (empty states' second line) | Each bus empty state and the trail's is matched whole, both lines, anchored. The board's was already. |
| Filbert T5 (nav wrap untested) | Covered by Sage's strip test. |
| Filbert T6 (Ctrl+Alt+K) | Pinned: Ctrl+Alt+K opens nothing. Only Ctrl+K and Cmd+K open the palette. |
| Filbert N1 (local board time, UTC bus time) | Both are UTC now: "Board scanned 18:01:02 UTC (4s ago)". Test matches ^Board scanned \d\d:\d\d:\d\d UTC. |
| Filbert N2 (strings counted across files) | The strings test counts each string per file, with the expected count (No task ${txt(ref)} in this business. twice in bus.js; "Nothing is waiting on you." once in app.js and once in bus.js). |
| Filbert N3, round 1 pkback | Unchanged: equivalent in Chromium; the listener stays for other browsers. |
| Darkwing note 3, M09 (focus offset) | Test: #cmdk's outline-offset is 2px. Table rows keep their inset ring (-2px), unchanged and untested. |
Darkwing note 4 (toast live region display:none before the first copy) |
The toast is always rendered; while off it is empty and visually hidden (clip-path: inset(50%), 1px box), so it stays in the accessibility tree. Test: before the first copy it is not display:none, is visible, is empty and is at most 1px. |
| Darkwing note 7, M16 (tabular numbers) | Test: font-variant-numeric is tabular-nums on #fresh and #session-count. |
| Darkwing M07 (toast text escaping) | Not added. The toast text is Copied: <command>, the commands come from validated refs, and toast() passes the text through esc(). |
| Darkwing note 2 (freshness hidden below 760px) | Unchanged. The command bar has no room at 360px, and the stale and refusal banners still state it in the view. Left for row 54 or the design session. |
Darkwing note 5 (Seen 403 wording, prose in <code>) |
Unchanged. A Seen 403 now also drops the board data, the same as a read 403; since only control-board's Host/Origin check gives it, the read would be refused too. |
Round 2 mutants
Each applied to a scratch copy of the round 2 webui, then shell.test.mjs
run (mut/tools/mutants.py, run.sh). 20 of 20 killed.
| Mutant | Change | Killed by |
|---|---|---|
| C1-keep-data | a board 403 keeps data |
shell browser test |
| C1-keep-inspector | a board 403 keeps selected and the inspector |
shell browser test |
| C1-keep-footer | a board 403 leaves the footer | shell browser test |
| C3-fresh-wording | "stale" wording on a refusal | shell browser test |
| C2-render-forget | the view's refused read doesn't forget() |
five-states test |
| C2-pk-forget | pkOpen's refused refetch doesn't forget() |
five-states test |
| C2-board-forget | the board route's refused inbox read doesn't forget() |
five-states test |
| C2-forget-keeps-count | forget() clears last but not the count |
five-states test |
| M01-refusal-403-only | refusal() only for 403 |
five-states test |
| M08-pk-label-raw | palette label set as HTML | five-states test |
| nav-wraps | the round 1 wrapping section list | shell browser test (strip) |
| toast-display-none | the round 1 display:none toast |
shell browser test |
| M09-offset-0 | focus outline-offset: 0 |
shell browser test |
| M16-no-tabular | no tabular-nums |
shell browser test |
| N1-local-time | board time in local time | shell browser test |
| N2-one-copy | one copy of the duplicated task string changed | strings test |
| T6-ctrl-alt-k | Ctrl+Alt+K opens the palette | shell browser test |
| T3-err-toast-hides | the error toast closes after 2.6 s | shell browser test |
| T2-stale-sticks | accept() keeps failed |
shell browser test |
| bus-fresh-refused | bus refusal shows the round 1 freshness wording | shell browser test |
Round 2 gate
Worktree at 2d4d4e7d plus the 16 round 2 files (sha256sum -c of the
manifest: all OK), node_modules linked from the checkout, TMPDIR in scratch,
suites sequential, 2026-10-10T18:15:17Z to 18:19:16Z. Outputs:
~/dewey-scratch/r53b/gate/out/.
| Suite | Result |
|---|---|
node --test 'packages/webui/tests/*.test.mjs' |
27 pass, 0 fail |
node docs/design/tools/build-tokens.mjs --check |
rc 0, "tokens.css is current" |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed |
| test-release | 14 passed, 0 failed |
| test-task, Docker present | 98 passed, 0 failed |
The round 1 packet follows unchanged, except where rounds 2 and 3 supersede it
(the nav deviation is withdrawn; the manifest above replaces 75569953).
Round 1 packet
What changed
All in packages/webui. No new endpoint, read, write or dependency.
| File | Change |
|---|---|
src/public/tokens.css |
New. Byte copy of docs/design/tokens.css. |
src/public/icons.svg |
New. Byte copy of docs/design/icons.svg: 17 icons and the mark symbol. |
src/public/shell.css |
New. The components on top of the tokens: command bar, freshness line, section list with icons, dense table with tabular numbers, inspector, status mark (glyph plus word), class chip and BLOCKING chip, copy command, toast, Ctrl+K palette, empty, not found, banners (err, ref), skeleton rows, @font-face for JetBrains Mono, --r 6px and --r-lg 10px use, focus rings, the 400px layout. Loaded last. |
src/public/assets/fonts/jetbrains-mono-{400,500,700}.woff2, jetbrains-mono-OFL.txt, jetbrains-mono-sources.txt |
JetBrains Mono 2.304 from the official release archive (URL and archive sha256 in the sources file), each file's sha256, and the OFL text from the archive. |
src/public/index.html |
Loads tokens.css after shared/app.css and shell.css last; no data-mode on <html>; the mark from the sprite in the wordmark; the Ctrl+K button, freshness line, System option, banner error element, board refusal empty state, board skeleton, toast and palette <dialog>. |
src/public/app.js |
theme() sets no property inline: it sets data-palette, and data-mode unless the mode is System, which removes it. Default mode system. Same key mosaic-console-appearance. Status mark classes on the state badge. Empty states with "what would appear and where it comes from". Board freshness line. A 403 from the board fails closed (kept rows hidden, banner ref); any other failure keeps the rows with their scan time. |
src/public/bus.js |
Class and BLOCKING chips, section icons, empty states, the skeleton loading view, not found that names the address and points to the Board and Ctrl K, freshness for bus pages, a refusal (403, not-configured, no-bus-host) never showing kept rows, the toast on copy, the Ctrl+K palette, ↑/↓ between task links. |
src/serve.mjs |
The files map only: tokens.css, shell.css, icons.svg, the three mono fonts. |
tests/shell.test.mjs |
New: copies, font digests, strings and the write boundary; two browser tests (below). |
tests/serve.test.mjs |
The new static files are served with their types. |
tests/browser.mjs |
ArrowUp and k key codes; no text insert when a modifier is held (so Ctrl+K doesn't type k). |
README.md |
"Shell (row 53, #1542)" section; the stale/refusal rule; the verify lines. |
Acceptance against the brief
| Brief line | Where it is checked |
|---|---|
tokens.css served, theme() no inline properties |
serve.test (type); shell browser test: <html> has no style attribute; inline mutant fails |
data-palette and data-mode stay; System removes data-mode; key kept; Harbor + System default |
shell browser test: fresh load has no data-mode, palette harbor, select system; dark and light OS give dark and light pages; picking dark sets it and stores it under mosaic-console-appearance; System removes it again. Mode mutant fails. |
--r 6px, --r-lg 10px, type scale, tabular numbers |
computed --r and --r-lg asserted; type scale and font-variant-numeric: tabular-nums in shell.css |
| JetBrains Mono 400/500/700 with sha256 and OFL | static test: shipped files equal the listed set, each digest matches, OFL heading present, @font-face for each weight; document.fonts.check in the browser |
| Icon sprite; mark in one component; Relay placeholder | icons.svg copy test; the mark is drawn once, in the wordmark, from #mark |
| Components | each rendered in the browser test and its screenshots: command bar, section list (4 icons asserted), dense table, inspector (opened by keyboard), status mark (◐ glyph asserted), class chip (code action, uppercase BLOCKING asserted), copy command and toast (success and clipboard-denied), palette, not found |
| Five states on Board, Inbox, Tasks, Agents, Trail | second browser test walks each view through loading (a held read shows the skeleton), normal, error over kept rows (same row count under banner err s1-stale), refusal (banner ref, code in mono, no rows, "Nothing to show.") and back. Empty is seeded per view in the first test. |
| Browser: no script errors, no sideways scroll at 400px, keyboard ↑/↓, Enter, Esc returning focus, visible focus | window.errors empty at the end of both tests; scrollWidth <= clientWidth after every state; board ↑/↓/Enter/Esc, task-link ↑/↓, palette ↑/↓/Enter/Esc with focus back on the opener; outline ≥2px on #cmdk, a section link and Refresh |
| String grep test | static test: the kept strings from bus.js, app.js and index.html, no Resolve/Decide/Approve button, no non-GET method in bus.js, no script in the sprite |
| No writes | both browser tests: every request the page made is a GET |
Tests
Outputs in ~/dewey-scratch/r53/out/.
| Run | Result |
|---|---|
node --test 'tests/*.test.mjs' in packages/webui, with WEBUI_EVIDENCE (full-4.txt) |
27 pass, 0 fail |
shell.test.mjs alone, three repeats after the last test change (shell-rep-{1,2,3}.txt) |
5 pass each |
Five-states test alone (states-1.txt) |
pass |
Screenshots (out/evidence-4/, 400px unless named): shell-board-empty,
-board-stale, -board-refused, -bus-empty-{inbox,tasks,agents,trail},
-bus-stale, -bus-refused, -bus-inbox, -palette, -toast,
-bus-tasks-1440, -board-1440, and states-{board,inbox,tasks,agents,trail}-{loading,normal,stale,refused}
(board: loading and normal). I looked at each.
Mutations
Each applied to a scratch copy of the candidate, then shell.test.mjs run
(mutbin/run.sh, outputs out/mut-*.txt).
| Mutant | Result |
|---|---|
tokens: a comment appended to the src/public/tokens.css copy |
fails: "tokens.css differs from docs/design/tokens.css" |
inline: theme() sets one property inline again |
fails: <html> style is --canvas: #fff; |
mode: System sets data-mode="light" instead of removing it |
fails: true !== false |
refusal: drop if (refusal(err)) throw err (refusal shows kept rows) |
fails: no .banner.ref |
focus: :focus-visible{outline:none} appended to shell.css |
fails: #cmdk focus ring |
| foot: the side column's top and height rule removed (760px and up) | fails: column bottom 930 against 900 |
post: method: 'POST' in fetchRead |
fails: static check and the GET-only browser check |
| loading: no loading view on navigation | fails: no #bus-view .skel-rows[aria-busy=true] (five-states test) |
| skel: no board skeleton | fails: no #sessions .skel-rows[aria-busy=true] (five-states test) |
pkback: drop the b.focus() in the palette's close listener |
survives: Chromium's native <dialog> close puts focus back on the opener, so the listener is redundant there and the mutant is equivalent in this browser. Kept for browsers that don't. |
Gate
Worktree at 8a7871ff plus the 16 candidate files (sha256sum -c of the
manifest: all OK), node_modules linked from the checkout, TMPDIR in scratch,
suites sequential, 2026-10-10T17:24:05Z to 17:28:05Z. Outputs:
~/dewey-scratch/r53/gate/out/.
| Suite | Result |
|---|---|
node --test 'packages/webui/tests/*.test.mjs' |
27 pass, 0 fail |
node docs/design/tools/build-tokens.mjs --check |
rc 0, "tokens.css is current" |
| test-auth | 15 passed, 0 failed |
| test-conductor | 17 passed, 0 failed |
| test-config | 24 passed, 0 failed |
| test-discord | 66 passed, 0 failed |
| test-extension-package | 18 passed, 0 failed |
| test-foundation | 44 passed, 0 failed |
| test-queue | 27 passed, 0 failed (verify and render --check skip outside the canonical root) |
| test-release | 14 passed, 0 failed |
| test-task, Docker present | 98 passed, 0 failed |
An earlier gate run on 3265383f with the manifest before the five-states
test was also green (26 webui tests); it is superseded by this one.
Decisions and deviations from the design
- Bus refusal fails closed. Before this row a 403 from the bus still
showed kept rows under a stale banner. Now a refusal (403,
not-configured,no-bus-host) shows only the failure: "Console shows nothing rather than a guess". Other failures keep the rows, labelled with their read time. The board does the same for its 403. - Footer is a sticky status bar from 760px. With the side column sticky under the command bar, a page taller than the viewport slid the column under the bar at 1440×900. The footer now sits at the bottom, the column fills the space between, and the browser test checks the edges on a short page and on a long one scrolled to the end.
- Nav wraps below 760px (withdrawn in round 2: the section list is one strip that scrolls inside itself, per IMPLEMENTING.md) instead of scrolling sideways, and the command bar keeps Palette, Appearance, Refresh and Pause on one row down to 360px.
- Manrope 800 renders as 700: only 400 to 700 are shipped (row 53 adds no Manrope weight). Visible on the large headings only.
- No business pill in the command bar: there is one business and no business read yet; the Business view is row 54.
- Not found keeps the existing string "No page at this address." and adds the address, a Board link and Ctrl K.
#copy-statuslostrole="status". The toast is the live region now; both announcing the same line would read it twice. The status line still shows the text.- Class chip colours use the design's four classes; an unknown class gets the plain chip.
Not done here (follow-ups)
- Row 54 (#1543) adds Queue, Business and Settings and will reuse these
components; both rows change
app.js, so 54 starts after 53 lands. - The pkback mutant above: a test in a browser without native dialog focus restore would need another engine; none is in the repo.
Boundaries kept
Commits: none (Sage commits). No change outside packages/webui and this
directory. docs/design/ read only. The Q14 frozen paths untouched. No
request to the tracker from node. No new dependency. Scratch worktrees
removed after use; core.hooksPath unset.