rvlib-shipkit 0.38.1 → 1.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (255) hide show
  1. package/CHANGELOG.md +379 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/derived.ts +1 -1
  6. package/src/assets/gen/generate-roll.ts +1 -1
  7. package/src/assets/kit-config.ts +6 -6
  8. package/src/assets/run-assets.ts +14 -10
  9. package/src/assets/server/studio-server.ts +6 -5
  10. package/src/check.ts +343 -0
  11. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  12. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  13. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  14. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  15. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +49 -32
  16. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  17. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  18. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  19. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  20. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  21. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  22. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  23. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  24. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  25. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  26. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  27. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  28. package/src/checks/SK018-test-files-run.check.ts +28 -0
  29. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  30. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  31. package/src/checks/contracts.check.ts +20 -0
  32. package/src/cli/cmd_Asks.ts +3 -2
  33. package/src/cli/cmd_Assets.ts +2 -2
  34. package/src/cli/cmd_Bump.ts +1 -1
  35. package/src/cli/cmd_Check.ts +6 -6
  36. package/src/cli/cmd_Checks.ts +95 -0
  37. package/src/cli/cmd_Ci.ts +48 -13
  38. package/src/cli/cmd_Commands.ts +2 -6
  39. package/src/cli/cmd_ConfigKeys.ts +69 -0
  40. package/src/cli/cmd_Contracts.ts +1 -1
  41. package/src/cli/cmd_Fleet.ts +16 -3
  42. package/src/cli/cmd_Hooks.ts +1 -1
  43. package/src/cli/cmd_Keys.ts +43 -18
  44. package/src/cli/cmd_Mv.ts +2 -2
  45. package/src/cli/cmd_Prose.ts +10 -1
  46. package/src/cli/cmd_Releases.ts +2 -2
  47. package/src/cli/cmd_Serve.ts +1 -1
  48. package/src/cli/cmd_Start.ts +6 -6
  49. package/src/cli/cmd_Status.ts +25 -0
  50. package/src/cli/cmd_Tui.ts +2 -2
  51. package/src/cli.ts +7 -4
  52. package/src/collections/actions.ts +4 -4
  53. package/src/collections/checks.ts +257 -0
  54. package/src/collections/collection-types.ts +254 -1
  55. package/src/collections/comfy.ts +23 -0
  56. package/src/collections/md-body.ts +14 -0
  57. package/src/collections/pings.ts +38 -12
  58. package/src/collections/posts.ts +19 -3
  59. package/src/collections/providers.ts +198 -91
  60. package/src/collections/reflections.ts +41 -0
  61. package/src/collections/state-rules.ts +73 -0
  62. package/src/collections/test-time.ts +288 -0
  63. package/src/collections/tests.ts +181 -0
  64. package/src/collections/wikis.ts +66 -0
  65. package/src/comfy/comfy-panel.ts +292 -0
  66. package/src/contracts/apply-contracts.ts +17 -12
  67. package/src/contracts/contract-types.ts +16 -4
  68. package/src/contracts/contracts-engine.ts +78 -42
  69. package/src/contracts/manifests/discord.contract.ts +6 -6
  70. package/src/contracts/manifests/identity.contract.ts +3 -3
  71. package/src/contracts/manifests/itch.contract.ts +12 -15
  72. package/src/contracts/manifests/social.contract.ts +5 -5
  73. package/src/contracts/manifests/steam.contract.ts +16 -16
  74. package/src/contracts/run-contracts.ts +4 -3
  75. package/src/core/attach.ts +96 -0
  76. package/src/core/canonical-scripts.ts +25 -0
  77. package/src/core/check-ctx.ts +204 -14
  78. package/src/core/check-file-names.ts +112 -0
  79. package/src/core/check-pass.ts +84 -0
  80. package/src/core/check-runner.ts +796 -0
  81. package/src/core/config-keys.ts +256 -0
  82. package/src/core/config-rewrite.ts +77 -0
  83. package/src/core/discover-checks.ts +166 -90
  84. package/src/core/discover-legacy-checks.ts +26 -0
  85. package/src/core/doc-block.ts +13 -0
  86. package/src/core/floor-resolve.ts +264 -0
  87. package/src/core/floor.ts +20 -137
  88. package/src/core/git-bin.ts +24 -0
  89. package/src/core/git-root.ts +108 -0
  90. package/src/core/instance.ts +195 -0
  91. package/src/core/jobs.ts +118 -0
  92. package/src/core/layout.ts +52 -0
  93. package/src/core/legacy-shapes.ts +207 -0
  94. package/src/core/lib-root.ts +5 -0
  95. package/src/core/locked-write.ts +110 -39
  96. package/src/core/machine-commit.ts +5 -4
  97. package/src/core/migrate-layout.ts +616 -0
  98. package/src/core/motherhome.ts +2 -8
  99. package/src/core/nesting.ts +4 -12
  100. package/src/core/one-way-in-policy.ts +73 -17
  101. package/src/core/outcome-words.ts +45 -0
  102. package/src/core/package-manager.ts +88 -0
  103. package/src/core/parallel.ts +21 -0
  104. package/src/core/process-tree.ts +44 -0
  105. package/src/core/raw-access.ts +93 -0
  106. package/src/core/read-set.ts +152 -0
  107. package/src/core/run-check.ts +155 -213
  108. package/src/core/run-ci.ts +152 -189
  109. package/src/core/run-fix.ts +99 -0
  110. package/src/core/run-fixers.ts +64 -33
  111. package/src/core/self-update.ts +11 -3
  112. package/src/core/slots.ts +83 -0
  113. package/src/core/spawn-timeout.ts +34 -3
  114. package/src/core/when-block.ts +401 -0
  115. package/src/core/when.ts +135 -0
  116. package/src/core/write-status.ts +5 -2
  117. package/src/fleet/scan-fleet.ts +16 -0
  118. package/src/hooks/leak-guard.ts +20 -9
  119. package/src/hooks/one-way-in-guard.ts +13 -5
  120. package/src/hooks/root-identity.ts +2 -1
  121. package/src/hooks/run-hooks.ts +38 -26
  122. package/src/init-repo.ts +39 -23
  123. package/src/ledger/check-stats.ts +155 -0
  124. package/src/ledger/check-table.ts +179 -0
  125. package/src/ledger/db.ts +255 -0
  126. package/src/ledger/gate-checks.ts +433 -0
  127. package/src/ledger/git-key.ts +116 -77
  128. package/src/ledger/git-scan.ts +96 -35
  129. package/src/ledger/ledger.ts +116 -42
  130. package/src/ledger/live-runs.ts +111 -0
  131. package/src/ledger/live-timeline.ts +97 -0
  132. package/src/ledger/repo-view.ts +73 -0
  133. package/src/ledger/run-log.ts +448 -0
  134. package/src/ledger/run-timeline.ts +173 -0
  135. package/src/ledger/store.ts +23 -12
  136. package/src/ledger/suite-log.ts +161 -0
  137. package/src/ledger/toolchain-identity.ts +7 -1
  138. package/src/ledger/world-cache.ts +111 -0
  139. package/src/mv/mv-md.ts +16 -6
  140. package/src/problems/extract.ts +182 -0
  141. package/src/problems/problem-board.ts +37 -0
  142. package/src/problems/problem-line.ts +77 -0
  143. package/src/prose/prose-view.ts +90 -0
  144. package/src/prose/run-prose.ts +60 -19
  145. package/src/prose/ste.ts +140 -24
  146. package/src/reflections/reflections-queue.ts +81 -0
  147. package/src/release/git-facts.ts +15 -9
  148. package/src/release/run-bump.ts +6 -5
  149. package/src/release/run-release.ts +25 -11
  150. package/src/release/run-releases.ts +2 -2
  151. package/src/release/unit-gate.ts +7 -54
  152. package/src/release/unit-resolve.ts +282 -0
  153. package/src/release/units.ts +33 -217
  154. package/src/repo-config.ts +157 -88
  155. package/src/rules/rule.ts +36 -96
  156. package/src/rules/untrack-generated.ts +5 -4
  157. package/src/scaffold/files/vscode-tasks.json +10 -31
  158. package/src/scaffold/jsonc-spans.ts +113 -0
  159. package/src/scaffold/project-scaffold.ts +16 -43
  160. package/src/scan.ts +98 -0
  161. package/src/serve/gate-trigger.ts +148 -0
  162. package/src/serve/health-schedule.ts +102 -0
  163. package/src/serve/hub-app/chart-view.ts +179 -0
  164. package/src/serve/hub-app/check-view.tsx +345 -0
  165. package/src/serve/hub-app/check-words.ts +164 -0
  166. package/src/serve/hub-app/checks-svc.ts +280 -0
  167. package/src/serve/hub-app/clock-svc.ts +33 -0
  168. package/src/serve/hub-app/col-card.tsx +23 -39
  169. package/src/serve/hub-app/cols-svc.ts +44 -20
  170. package/src/serve/hub-app/comfy-svc.ts +171 -0
  171. package/src/serve/hub-app/comfy-view.tsx +81 -0
  172. package/src/serve/hub-app/dur.ts +41 -0
  173. package/src/serve/hub-app/hash-route.ts +121 -0
  174. package/src/serve/hub-app/header-bar.tsx +117 -0
  175. package/src/serve/hub-app/health-svc.ts +23 -28
  176. package/src/serve/hub-app/help.tsx +29 -0
  177. package/src/serve/hub-app/hub-app.tsx +122 -119
  178. package/src/serve/hub-app/hub-st.ts +115 -42
  179. package/src/serve/hub-app/hub-types.ts +117 -2
  180. package/src/serve/hub-app/hub.css +534 -192
  181. package/src/serve/hub-app/index.html +2 -0
  182. package/src/serve/hub-app/live-svc.ts +174 -0
  183. package/src/serve/hub-app/md.ts +15 -0
  184. package/src/serve/hub-app/media-svc.ts +4 -1
  185. package/src/serve/hub-app/media-view.tsx +15 -0
  186. package/src/serve/hub-app/next-run.ts +68 -0
  187. package/src/serve/hub-app/project-gutter.css +65 -0
  188. package/src/serve/hub-app/project-gutter.tsx +128 -0
  189. package/src/serve/hub-app/projects-svc.ts +226 -0
  190. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  191. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  192. package/src/serve/hub-app/prose-hits.css +21 -0
  193. package/src/serve/hub-app/run-strip.ts +47 -0
  194. package/src/serve/hub-app/runs-tabs.ts +24 -0
  195. package/src/serve/hub-app/test-view.tsx +107 -0
  196. package/src/serve/hub-app/tests-svc.ts +62 -0
  197. package/src/serve/hub-app/tests-view.tsx +238 -0
  198. package/src/serve/hub-app/time-ago.tsx +34 -0
  199. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  200. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  201. package/src/serve/hub-app/timeline-svc.ts +62 -0
  202. package/src/serve/hub-app/timeline-view.tsx +245 -0
  203. package/src/serve/hub-app/tree-build.ts +410 -0
  204. package/src/serve/hub-app/tree-row.tsx +42 -0
  205. package/src/serve/hub-app/tree-svc.ts +174 -0
  206. package/src/serve/hub-app/wikis-view.tsx +53 -0
  207. package/src/serve/other-repos.ts +79 -0
  208. package/src/serve/project-gutter-route.ts +350 -0
  209. package/src/serve/project-gutter.ts +449 -0
  210. package/src/serve/run-serve.ts +245 -16
  211. package/src/serve/same-origin.ts +14 -0
  212. package/src/serve/serve-hub.ts +213 -18
  213. package/src/shipkit.ts +19 -12
  214. package/src/smoke/run-smoke.ts +9 -4
  215. package/src/start-repo.ts +75 -36
  216. package/src/suite/judge-suites.ts +209 -15
  217. package/src/suite/kept-timings.ts +58 -0
  218. package/src/suite/runner-shapes.ts +88 -0
  219. package/src/suite/suite-cli.ts +147 -24
  220. package/src/suite/suite-cpu-preload.ts +92 -0
  221. package/src/suite/test-units.ts +243 -0
  222. package/src/ts/config.ts +4 -69
  223. package/src/ts/jsonc.ts +69 -0
  224. package/src/ts/lex.ts +143 -0
  225. package/src/ts/rename.ts +73 -10
  226. package/src/ts/source-graph.ts +28 -8
  227. package/src/ts/tsconfig-projects.ts +6 -97
  228. package/src/ts/tsconfig-resolve.ts +108 -0
  229. package/src/tui/build-steps-panel.ts +61 -0
  230. package/src/tui/checks-panel.ts +24 -16
  231. package/src/tui/run-tui.ts +2 -2
  232. package/src/tui/tui-panel.ts +5 -4
  233. package/src/wiki/graph-colors.ts +68 -0
  234. package/src/wiki/graph-mount.ts +7 -9
  235. package/src/wiki/graph-page.ts +42 -25
  236. package/src/wiki/graph-positions.ts +105 -0
  237. package/src/wiki/run-wiki.ts +1 -1
  238. package/src/wiki/scan-md.ts +1 -1
  239. package/src/wiki/serve-graph.ts +33 -3
  240. package/src/cli/cmd_Rules.ts +0 -51
  241. package/src/cli/cmd_Units.ts +0 -82
  242. package/src/collections/graphs.ts +0 -32
  243. package/src/contracts/contracts-once.ts +0 -43
  244. package/src/core/check-suite.ts +0 -302
  245. package/src/core/repair-contract-dirs.ts +0 -136
  246. package/src/core/task-runner.ts +0 -339
  247. package/src/ledger/gate-units.ts +0 -138
  248. package/src/ledger/unit-table.ts +0 -105
  249. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  250. package/src/rules/SK003-claude-md-shape.ts +0 -70
  251. package/src/rules/SK004-gitignore-policy.ts +0 -238
  252. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  253. package/src/rules/rule-registry.ts +0 -64
  254. package/src/rules/run-fix.ts +0 -72
  255. package/src/tui/tasks-panel.ts +0 -60
package/CHANGELOG.md CHANGED
@@ -1,3 +1,366 @@
1
+ ## 1.0.1
2
+
3
+ - **A layout move lands once, and its commit says what it moved.** A tracked root `STATUS.md` that named an old folder was dropped and then written back by the text sweep in the same pass, so the move commit kept it, and the next run landed a second commit, titled as a move, that moved nothing and reported `moved under .shipkit/`. A legacy generated file is now only dropped, never swept. The commit title is built from what the pass did (`chore(machine): move asks/ and .rv-identity/ under .shipkit/, and drop the root STATUS.md, which shipkit writes under .shipkit/`), so a pass that moves no folder never carries the move title, and the fix pass and SK004 print the same words (`layoutSummary` in `core/migrate-layout.ts`, which replaces `MIGRATION_MESSAGE`).
4
+
5
+ - **An update never guesses the package manager.** `detectPackageManager` returns the manager the tracked lock files name, or a refusal (`{ refused }`, `isRefusal`) when they name none or two: no lock at all used to mean bun, and two managers' locks used to mean bun with a warning, so an npm repo could be updated with bun, which moves package.json and leaves `package-lock.json` on the old version. SK005 and the self-update WARN with the refusal and run nothing. `shipkit fleet --update` refuses a repo it cannot tell, and a non bun repo whose installed shipkit is older than 1.0.0, whose own update ran bun whatever the lock said; it prints the command in the repo's manager instead (`updateRefusal`, `PM_AWARE_SINCE` in `fleet/scan-fleet.ts`). The `warn` field of `PackageManager` is gone.
6
+
7
+ - **The layout move rewrites the paths `repo.config.ts` holds.** Every quoted value that names a moved folder as a path (`hooks.leakAllow` paths, `hooks.blockedPaths`, `gitignore` lines, a prose include, any other string starting with `asks/`, `tmp/`, `hooks/` or a `.rv-<x>/` folder) now names its new place, in the same machine commit. The ignore block is rendered from those rewritten values, and the rule fixers that run after the move in the same fix pass read the rewritten config (`runFix` takes `config`, and the fix pass reports the config as reshaped). Before, `leakAllow: [{ paths: ['asks/'] }]` kept naming a folder that was gone, and a repo line in `gitignore` came back under its old name, which took a second move commit to clear. `sweepConfigSource` and `rebaseConfigPaths` are exported from `core/migrate-layout.ts`, and `renderGitignore` receives the moved config as a third argument.
8
+
9
+ - **The seeded `sk-` key shape starts where a token starts.** `re:sk-[A-Za-z0-9_-]{20,}` matched inside a word ending in `sk`, so an ask file named `ask-the-town-hall-...` was refused as an API key. The starter row is now `re:(?<![A-Za-z0-9_-])sk-[A-Za-z0-9_-]{20,}` (`SK_KEY_ROW`), and a keyword file that still carries the old seeded row is read with the fixed shape under its old id, so a waiver written against its fingerprint keeps working.
10
+
11
+ - **Every file a check file or `repo.config.ts` reaches compiles under a strict, node only consumer tsconfig.** The package ships its source, so a consumer's compiler judges it. Unused imports in `core/run-ci.ts`, `repo-config.ts`, `core/floor-resolve.ts`, `collections/checks.ts`, `wiki/scan-md.ts` and `assets/gen/derived.ts` failed a consumer with `noUnusedLocals` or `noUnusedParameters`, and `smoke/run-smoke.ts` named the dom `document`, which fails a consumer whose `lib` has no dom. `tsconfig.consumer.json` compiles `check.ts`, `repo-config.ts` and the barrel with those flags, `lib: ["ESNext"]` and bun types only, and `tests/consumer-typecheck.test.ts` (full tier) runs it, with samples that prove each flag bites.
12
+
13
+ ## 1.0.0
14
+
15
+ - **The PreToolUse guard refuses a piped gate.** `shipkit ci …|`, `bun <dir>/cli.ts ci …|`, `bunx shipkit ci …|`, `bun run test …|` and `bun run ci …|` exit 2 with the reason and the fix, whatever the deny list says: a pipeline exits with its last command's code, so a red gate reads green. A `>` redirect, `bun run test:raw <file> | tail` and a pipeline under `set -o pipefail` pass. `pipedGateIn`, `pipedGateRefusal` and `shellParts` (each segment with the operator that ends it) are exported from `core/one-way-in-policy.ts`, and a redirection's `&` (`2>&1`, `&>log`) no longer splits a segment.
16
+
17
+ - **A check file never spawns itself.** A `cmd` that names the check file's own path (`['bun', import.meta.path]` with the body behind `if (import.meta.main)`) is a second way to write a body, in a process of its own, with none of what `c` gives. Check SK020 (blocking) FAILs one: a `cmd` element that IS the file (`import.meta.path`, `import.meta.filename`, `fileURLToPath(import.meta.url)`, `__filename`, `Bun.main`, or a string naming its own path), or an `import.meta.main` guard in a check file. A `cmd` that runs a tool or another file passes, and so does a comment that shows the old shape. It reads each check file's source through `c`, and a legacy check script is SK016's. A probe is `run: (c) => …`, its secrets, tools and independent parts small helpers over `c` in the repo that owns them.
18
+
19
+ - **BREAKING: `CheckSuite` is gone.** It was what a probe declared inside a process of its own, a check file run a second time by its own `cmd`. A probe is a `run` body, so `core/check-suite.ts` is deleted with its export, and so are `CheckSuiteOpts`, `CheckOutcome`, `ToolUnresolved`, `GetSecret` and `SecretSpec`.
20
+
21
+ - **BREAKING: `c.tool(name, candidates?)` resolves a binary.** It returns where the binary lives, an existing candidate path first and then PATH, or null when it is not installed, and marks the answer untracked. It used to look up a path a `CheckSuite` had registered, which a `run` body never has. `new CheckCtx(pass?)` takes no tool map.
22
+
23
+ - **`c.exec` hands the child the environment as it stands now.** Left without `env`, Bun gives a child the environment the process started with, so a variable set since (a test seam, a flag the gate set) never reached it. It defaults to a copy of `process.env` at call time.
24
+
25
+ - **A 429 inside a `run` body warns, never fails.** `runCheckBody` reports a `RateLimited` from `c.fetch` as unreachable: no answer, so nothing remembers it.
26
+
27
+ - **The fix pointer a body did not give names a repo file as it stands.** A repo check's owner defaults to its own path, so `c.ownerPointer(owner)` keeps an owner that is a file of this repo as it stands, and only a docs page takes `docsBase`.
28
+
29
+ - **BREAKING: everything shipkit keeps in a repo lives under `.shipkit/`.** The repo root held a dozen toolkit folders; it now holds one. `.rv-<x>/` became `.shipkit/<x>/` (identity, assets, comfy, private, reflections, social, journal, domain, itch, steam, discord, and any other `.rv-` name), `asks/` became `.shipkit/asks/`, `hooks/` became `.shipkit/hooks/` (with `core.hooksPath`), `STATUS.md` and `STATUS.full.md` moved to `.shipkit/`, and the non git cache moved from `tmp/.shipkit` to `.shipkit/cache/`. Every path comes from one table, `core/layout.ts`. The machine-global `~/.rv-private/` keeps its name.
30
+
31
+ - **A full tier gate says so, and a check's average is its quick runs.** The invocation label carries `--full` whenever the run asked for the full tier (a release always does), `RunRow.full` reads it back through the invocation, and the per check average and slowest leave full tier runs out while the check has quick ones. A check that only runs at the full tier keeps its numbers.
32
+
33
+ - **A suite run takes at least half the cores.** The worker count is fixed for the whole run, and a gate starts the longest suite while its first burst of short checks holds the machine pool, so the slot share alone left it on two workers long after that burst ended (20 to 46s where four workers take 7.6s). The share still applies above that floor.
34
+
35
+ - **BREAKING: everything shipkit keeps in a repo lives under `.shipkit/`.** The repo root holds the project and one toolkit folder. `asks`, `hooks` (with `core.hooksPath`), `identity`, `assets` and `comfy` are tracked; `private`, `reflections`, `social`, `journal`, `domain`, the venue folders and `cache` stay local; `STATUS.md` and `STATUS.full.md` are generated there. The machine global private folder is `~/.shipkit/private/`. Every path comes from one table, `core/layout.ts`. Contract manifests declare `dir: '.shipkit/<venue>'`.
36
+
37
+ - **Upgrading moves a repo by itself.** The first fix pass of `shipkit ci` after the upgrade moves the toolkit folders into `.shipkit/`, rewrites the tracked files and `.gitignore` lines that name them, and lands one `chore(machine)` commit of exactly those paths. It never pushes, and a move never changes what git tracks. It waits, and says why, when a path it must touch has uncommitted edits, during a merge or rebase, and in a nested repo; it refuses only when the same file name exists in both places. SK004 fails until the move is done. `SHIPKIT_NO_AUTOCOMMIT=1` keeps the move and skips the commit.
38
+
39
+ - **The SK004 block ignores `.shipkit/*` and re-includes the tracked folders**, so a folder nobody listed stays local by default. `.shipkit/private/` is spelled out so the check asks git about it. A live venue re-includes its folder below the block, `!.shipkit/itch/`.
40
+
41
+ - **The SK004 block ignores `.shipkit/*` and re-includes the authored folders** (`asks`, `hooks`, `identity`, `assets`, `comfy`), so a folder no one listed stays local by default. `.shipkit/private/` is spelled out so the check asks git about it. While a repo still holds a `.rv-*` folder, the block keeps the old names ignored too, so nothing private is exposed before it moves. SK004 fails on an old layout with the command that moves it. A live venue re-includes its folder below the block, `!.shipkit/itch/`. Contract manifests declare `dir: '.shipkit/<venue>'`.
42
+
43
+ - **BREAKING: the `tmp/` scratch is `.shipkit/tmp/`.** Every scratch write shipkit makes lands there: the per run logs (`.shipkit/tmp/shipkit-ci/`, the new default of `logDir`), the mutate leases, the detached gate records (`.shipkit/tmp/shipkit-jobs/`) and the smoke screenshots (`.shipkit/tmp/smoke/`, the default of `smoke.shots`). `LAYOUT.tmp` and `CI_LOG_DIR` (`core/layout.ts`) carry the paths. The layout move carries a repo's `tmp/` along, the repo's own files in it included: the same one commit, the same refusals (a tracked file with uncommitted edits defers the move, a file name on both sides refuses it, and a lease a killed run left behind is dropped rather than counted as a clash). A `tmp/` holding a git checkout (a clone, a linked worktree) is refused rather than moved, and a second move of a recreated `tmp/` lands without an empty commit or a false WARN. The SK004 block ignores `.shipkit/tmp/`, and still ignores a root `tmp/`, so a plan written there stays private until the fix pass moves it. The release dirt check skips `.shipkit/tmp/` and lists untracked files one by one (`-uall`), so no folded row hides real dirt. A test guard fails on any `tmp/` path spelled in lib source.
44
+
45
+ - **A review with no output file named lands in `.shipkit/reviews/`.** The reviewer agent file SK008 writes tells the reviewer to save its review as `.shipkit/reviews/YYYYMMDD-HHMMSS-<review-name>.md` (through Bash, so its tools stay read only) and to return the same findings to the caller. The SK004 block spells out `.shipkit/reviews/`, so SK004 asks git that no line re-includes it. An existing reviewer file is never rewritten.
46
+
47
+ - **Readers of other repos accept the old layout for now.** The hub's project gutter reads a repo's `asks/` when it has no `.shipkit/asks/`, and the reflections queue reads `.rv-reflections/` when it has no `.shipkit/reflections/`. Both reads go once `shipkit fleet` shows no repo on the old layout.
48
+
49
+
50
+ - **BREAKING: the concept is a unit, and `ReleaseUnitDecl` is `UnitDecl`.** A `release: false` unit releases nothing, so "release unit" named the wrong thing. The exported type is renamed, SK010 is titled `units declared` (file `SK010-undeclared-unit.check.ts`, owner page `build/shipkit/units.md`), and the words a person reads say unit: `bump` bumps a unit, `shipkit releases` lists every unit that releases, SK010 counts `releasing unit(s) declared`, an unknown id says `no unit '<id>' — releasing units: …`, SK011 and SK006 name sibling and releasing units, the suite runner says `no test file outside the units`, and the hub test page help says unit.
51
+
52
+ - **No user facing string says `replayed` or `measured`.** A test reads every string, template chunk and JSX text in `src/` and fails on either word; keys, kinds, SQL columns, identifiers and comments pass. Three lines were reworded: SK001 says `would run nothing`, a secret probe line says `did not check it`, and the repo.config scaffold says `are cached on the tracked tree` and `runs every time`. `ts/lex.ts` is the source lexer both this test and SK019 read with. Only a whole quoted literal can be a key: a template chunk, an operand of `+` and a label value are prose. `lexSource` resolves escapes and reports which strings are template chunks (`template`).
53
+
54
+ - **Rule SK019: tests run only in the generated test checks.** A check file (a legacy script included), a `tasks: []` entry, a `rules: []` body or a non-test `defaults` slot that runs `bun test`, `bun run test` / `bun run test:raw`, or the suite runner itself FAILs, naming the file, with `declare a unit instead (units in repo.config.ts)`. The floor `test` slot and the `test:<unit>` checks are the allowed runners and are never scanned. The reader, `sourceRunsTests` and `argvRunsTests` in `suite/runner-shapes.ts`, skips comments and flags only shapes that execute: an argv literal, a command string handed to a shell, a reference to the suite runner. It also reads `TZ=UTC bun test` style env prefixes, a `bun run <script>` through its package.json body, a template's interpolated command (`cd ${dir} && bun test`), and the `lint`, `typecheck`, `imports:check` and `cycles` slots filled from package.json. An argv counts only in a run position (`cmd: [`, `exec([`, `spawn([`), so words held as data pass.
55
+
56
+ - **A hub started in a linked worktree never notifies.** `pingTick` returns at once where `.git` is a file: no `notifyCommand`, no macOS notification, no `pinged:` marker written into an ask, no state write. The boot banner prints `pings: off` there. `isLinkedWorktree` is exported from `core/git-root.ts`.
57
+
58
+ - **A test group of the tree has a page: where its time goes.** The `tests` group and each `test` / `test:<unit>` group open a page (`#page:tests`, `#page:tests:<check id>`) while their chevron still folds: on a folder row that carries a page, the chevron folds and the label opens. A test check page lists the failing files first with their failure line, then every file slowest first (duration against its budget, CPU when known, share of the last run, budget, trend, tier), a summary line with the sum of file times over the check's wall, the workers and a verdict (outside the files, long tail, volume), and the suite runner's phases. The `tests` page lists every test check and every failing file. Data: `/api/tests.json[?check=<id>]`, shaped by `collections/test-time.ts`.
59
+
60
+ - **The suite runner records where its time goes.** Schema v9 of `shipkit.sqlite`: `suites` gains `cpu_ms`, `phase`, `phase_ms` and `fail_line`, and a `suite_runs` table keeps one row per runner run (wall, setup, workers, files, parallel and serial phase walls and CPU, alone reruns, load, cores). Every change is a column with a default or a new table, so an older build still reads and writes the file. `failLines` (`suite/judge-suites.ts`) reads the first failure of each failing file off bun's report, and the runs page `test files to fix` row carries it as its tooltip.
61
+
62
+ - **Every test file is a resource of the hub tree.** A `tests` collection (`collections/tests.ts`) lists every file bun would discover as `test:<repo relative path>`, filed under the check that runs it (`tests/test/…`, `tests/test:<unit>/…` by path, `tests` then one group per check by type). Which check runs a file is `testPlanOf`, the plan SK018 judges. Files a bunfig ignore keeps out are quiet `ignored` rows carrying the pattern, and files no check reaches are red rows under `in no check` carrying the reason SK018 prints. A row shows the file name, its last duration against the per file budget, its verdict colour and its tier. Its page (`hub-app/test-view.tsx`) shows the path with an editor link, the check, the unit, the tier, the budget, the last ten runs from the `suites` table and the copyable command that runs it alone from its unit dir. `testPlanOf` now also returns `unread`, the files of a check whose script is not a plain `bun test`, and `TEST_GLOB` and `declaredSlowFiles(config)` are exported from `suite/test-units.ts`.
63
+
64
+ - **One definition of a test file to fix.** `testFilesToFix` decides it for the runs page tab and for the tree row colour alike. A tab row opens the test file page, its check tag opens the check.
65
+
66
+ - **A declared slow suite rides on its test file.** The checks collection no longer emits a `suite` resource: the declaration (`catches`, `why not fast`, the way back to the quick tier) is on the test file page, and the kind `suite` is gone. A declaration naming a file that is not on disk is a `missing` row in warn.
67
+
68
+ - **The runs page is no longer a tree resource.** The `runs` collection and the `runs` kind are gone. The page stays at `#item:runs:runs` with its tabs, the header always links it, and the cmd+j palette lists it.
69
+
70
+ - **A by type group row wears the mark of the kind it gathers.** Every folder row showed the folder glyph, so the by type view hid the kind it groups by. A tree node carries `kind` for a by type group, and `markOf(res, dir, kind)` returns that kind's mark; a real folder keeps the folder mark. The tree row is its own module, `hub-app/tree-row.tsx`, so a test renders it headless.
71
+
72
+ - **Every row of the gate inventory carries a description, a generated row included.** A floor slot states what its slot asserts (`tsc finds no type error in the repo`, `no import cycle between modules`), a release unit's test check names its dir (`every test file under libs/lib-a passes`), and a slot turned off keeps its slot line. `TaskDef.description` is new: a `defaults` override or a `tasks: []` entry can declare one, and a task that declares none reads as its command exiting 0. A legacy rule shows its title, a legacy check script `` `bun <file>` exits 0``. `CheckRow.description` is a `string` now, so `shipkit checks --json`, the hub check page and the TUI detail always have a line to show. `slotDescription`, `unitTestDescription` (`core/floor-resolve.ts`) and `describeFirst` (`check.ts`) are exported. `shipkit check --fix SK016` keeps a task's own description when it rewrites the task into a check file.
73
+
74
+ - **A repo keeps its own prose word lists, and the hub edits them.** `prose.words` in `repo.config.ts` takes `allow: [{ word, reason? }]`, which drops every SK014 hit on a word or phrase (a dictionary entry covers its inflected forms), and `ban: [{ word, to?, reason? }]`, which refuses an extra word as a hard hit, with `to` as the fix `shipkit prose --fix` writes. The house dictionary stays the default and a list binds its repo only. `defineRepo` refuses an empty word, a word listed twice, and a word both allowed and banned. Every hit now carries `word`, the dictionary entry that fired. The hub writes the lists through the managed block writer the run when switch uses, as a second block inside `prose: {` (`core/when-block.ts`, `WORDS_BEGIN`, `nextWords`), on `POST /api/checks/when` with `{ words: { allow, ban, remove } }`.
75
+
76
+ - **The SK014 page lists its hits.** `GET /api/prose.json` groups every hit by word or rule, noisiest first, each with its file, line and the marked source line (`src/prose/prose-view.ts`). The check page shows the groups with word, file and tier filters, a `file:line` link into the editor, an `allow it here` button per word, and the repo's lists with a form to add or drop an entry. An edit reruns SK014.
77
+
78
+ - **Every wiki the hub judges is green or red.** A repo whose wiki uses a custom graph provider (`serve.wikiGraph`) drew it white, as "repo-owned lint". `serve.wikiGraph.lint` names the check ids whose last verdicts are the wiki's verdict, red when one failed. Without it, the built-in md lint judges the wiki as it does for a generic scan. md lint issues now read red on the tree instead of orange, with the same yellow feed line that never pings. The wikis page colours each tab label like its tree row. `GraphMount.lint` takes `{ checks }` beside `{ root, index }`, and `GraphMount.note` is gone. `wikisCollection(mounts, root, { stats })` takes the check stats for tests.
79
+
80
+ - **The check page id tag is the bare `tid` tag.** It was its own `.cid` class with a border and padding, a boxed tag the tree and the runs page had already lost. The page uses the one `.tid` class, and `.cid` is gone. The css invariants test fails on any `.cid` rule or class and on a `.tid` rule that gains padding, a border or a background.
81
+
82
+ - **A stale runs tab address is rewritten.** `#item:runs:runs:timeline`, or any tab this build does not name, opened the runs tab and left the unknown address in the url. `applyHash` now rewrites it in place to the address of what it opened (`canonicalSel` in `hash-route.ts`), as a replace, so back never lands on it. The runs tab list moves to `runs-tabs.ts`, which has no view code, so the router can read it.
83
+
84
+ - **`slowest checks` lists only checks that exist.** A check deleted from the repo kept leading the list from its old rows, and no edit could move it. `shapeTimeline` and `slowestChecks` take an optional `inventory` (the check ids declared today), and the hub passes the gate inventory. The rows stay in the run history. The longest runs keep their labels, since a label is the command a run was started with.
85
+
86
+ - **A runs page check line draws no verdict dot.** Its label takes the verdict colour instead, the `tl` tones the tree uses, so the whole hub says red or green one way. The `.rdot` rules are gone.
87
+
88
+ - **No hub page draws a back link.** The item cards of asks, secrets, posts, reflections and media, and the comfy view, lost the `← section` link at their top, as the check page already had: every page has an address, the tree navigates and the browser back button walks the history. `ColsSvc.colTitle`, which only that link read, is gone.
89
+
90
+ - **A secret wears the verdict of the check that probes it, read from the run record.** The secrets collection read STATUS.md, which only `shipkit ci` writes, and a probe's ok line never names the secret it proved, so every present secret read orange "nothing has probed it" while the health pass had just run its probes green. A cred now declares `probedBy: '<check id>'` in `repo.config.ts`, and its row reads that check's last recorded run from the checks collection, which gate runs and health passes both write: green "checked 2h ago by npm" when it passed, red with the failure line when it failed, plain when the pass is older than the check's ttl or the check never measured it, dim "no check probes this secret" when nothing probes it. A secret some check reported missing is read from the same record, a FAIL demoted to a WARN included. No collection reads STATUS.md for a verdict any more.
91
+
92
+ - **Feed lines use the tree's words, with no dash punctuation.** A check reads "typecheck passed, took 15.3s, average 3.4s over 275 runs" and "test passed, cached", from one `checkVerdictWords` shared with the hub tree's tooltip. A secret reads "acme/npm/token (npm) present, checked 1m ago by npm". A declared slow suite reads "<file> is a test suite the quick gate skips".
93
+
94
+ - **The release gate keeps a non blocking check that a selected check waits on.** It runs only blocking checks, and a dropped dep read as satisfied, so a suite whose prerequisite is a world check (`blocking: false`, as a world check should be) ran without it and went red on a machine that never ran the setup. The dep now runs, and its WARN skips the dependent there too.
95
+
96
+ - **The unit gate no longer compiles a sub-unit.** `typecheck:<unit id>` does it in the release gate, so a release compiled every sub-unit twice through two code paths. The unit gate keeps what no check says: the unit's tree is committed, and an npm target has a tsconfig. `resolveTsc` is gone.
97
+
98
+ - **`typecheck:<unit id>` is keyed on its tsconfig and its `extends` chain** when they live outside the unit dir, so an edit to a shared base config reruns it.
99
+
100
+ - **A unit can be `release: false`: it cuts the repo for checks and never ships.** `units: [{ dir, release: false }]` gets `test:<unit id>` and `typecheck:<unit id>` like any unit and claims its dir for rule SK010, while `shipkit releases`, `bump`, `release`, the npm checks (SK006, SK011) and the per unit gate never list it. `resolveUnits` returns it in `testOnly`, apart from `units`. Beside it only `dir`, `id`, `tests`, `tsconfig` and `deps` mean anything, and any release key is refused rather than ignored. `bump` or `release` naming it says it never releases. A sub-unit's release still runs its test check, because it ships nothing of its own, so what its suites prove is somebody else. Naming its dir in `notReleasable` too fails SK010 as a second statement of one fact.
101
+
102
+ - **Every unit with a tsconfig gets `typecheck:<unit id>`.** The root tsconfig excludes a unit's dir, so a green root typecheck hid every error only the unit's tsconfig sees unless the repo scripted a per unit pass itself. The check runs the root slot's fallback runner over `units[].tsconfig` (else `<dir>/tsconfig.json`), project by project, with tsc from the unit's node_modules, then the repo's, then PATH. A declared tsconfig that is not on disk is named, never skipped quietly.
103
+
104
+ - **`units[].deps`: the checks a unit's generated checks wait for.** A dep that warns or fails SKIPS `test:<unit id>` and `typecheck:<unit id>` with its name, so a suite that needs the world (a local server binary) is neither a green that proved nothing nor a red that only means nobody ran the setup.
105
+
106
+ - **A unit's checks are keyed on the siblings its tsconfig `paths` point into.** Beside the ones its package.json names, a unit that resolves sibling libs at their source folders through `paths` (a bench) now reruns when one of them changes, where it replayed a stale green before.
107
+
108
+ - **A test check never sits at one of its test files in the hub.** The root `test` carries its test files as code for its key, and the check entry read the first one as the module the check IS, so the hub linked `test` to whichever test file sorted first. A check whose owner is its command has no source file.
109
+
110
+ - **The floor source of a generated unit check reads `unit`**, not `release unit`, since a `release: false` unit has one too.
111
+
112
+ - **A gate rereads less.** The import and raw access scans of a source text are remembered per text, a composition loads each root's config once for all its checks, a repo listing is remembered by tree id, and the ledger's second listing of a run reuses the first one's ignore answer. Measured on a small repo: 97 to 104ms per gate before, 72 to 78ms after. `rawAccessInClosure` takes an optional `configs` map for the shared config, and `RepoView.open(root, prior?)` an earlier view.
113
+
114
+ - **A suspect the parallel phase already cleared skips its alone run.** The suite runner preloads `src/suite/suite-cpu-preload.ts` into the parallel phase, which records each suite's CPU (the worker's plus the children it reaped, through `getrusage`) and all its worker burned before it began, earlier suites' tests included, and a suspect whose sum is inside `tests.budgetMs` is cleared without running again alone: a lone run can only need what that worker computed by then, and sharing the machine only raises CPU. A suite that ends with a child alive or not yet reaped gets no figure and runs alone. One past twice the budget in wall time still runs alone on an idle machine, since only that run can see a wait. A runtime where the figure cannot be read runs every suspect alone, as before.
115
+
116
+ - **The quick tier keeps bun's durations between runs.** `--timings` now starts from `<shipkit root>/suite-timings/<unit>.json`, so bun starts the slowest files first, and the file is replaced whole after each run. A file not in bun's own shape seeds nothing, and only suites this run ran, or still on disk, are judged and kept, so a deleted suite is never judged again.
117
+
118
+ - **A holder is judged by its run, not by its pid.** `bun --watch` restarts a program in the same pid, so a machine slot, a mutate lease or an attach answer left by the previous image named a pid that still ran. The pool then read "8 of 8 held" for up to two minutes, and a waiter printed a steal from a pid it then said it had attached to. Every lock now records the image that wrote it (pid plus a nonce drawn once per program start, `core/instance.ts`), and one registry file per pid under `~/.cache/shipkit/instances/` (`SHIPKIT_INSTANCES_DIR`) says which image owns that pid. A holder whose pid is gone or runs another image is free at once. A live image stays the holder however long its event loop is busy, because a long synchronous step (a spawnSync, a laptop asleep) must never hand its phase to a second process; a wedged one is freed by the lock age rule. The registry file leaves with its process at exit, and a long lived process sweeps the folder every hour. A lock with no nonce, from an older shipkit, keeps the pid rule. The steal line names the reason (`pid 4242 restarted in place`).
119
+
120
+ - **An asker that stole a lock measures itself.** It just called that run gone, so it never adopts an answer and never prints "attached" after a steal.
121
+
122
+ - **The hub reaps what its previous image left running.** Every child a check spawns is listed in its image's registry file while it runs. `shipkit serve` reads the previous image's list at start and kills each listed process that is still its child, with its whole tree, and prints how many. A child the hub spawns for itself (the comfy panel) is not listed and is still adopted. `descendantPids` moves to `core/process-tree.ts`.
123
+
124
+ - **`fixLegacyShapes(c, rewrite?)` takes the config half as a parameter.** The default still runs `core/config-rewrite.ts` in its own process through `c.exec`, which is what a gate applies. A caller allowed to load the config in process (a test) hands `rewriteConfig` itself and spawns nothing. `ConfigRewriter` is the type.
125
+
126
+ - **SK001 and SK017 read through `c` and are cached on what they read.** They were the last built-ins keyed on the whole tree. SK001 reads package.json, probes `.shipkit/comfy` and `deploy.sh`, and lists the test files git sees (`**/*.{test,spec}.*`), so an edit anywhere else stays cached and a new test file reruns it. SK017 reads `.vscode/tasks.json` and writes it through `c`. Both ask how the floor resolves through the context, so the package.json scripts, the tsconfig chain and the local tsc they consulted are in their read set. `hasTestFiles(paths)` takes a listing now rather than walking the disk, so a gitignored test file no longer counts and the old depth limit is gone. `evaluatePackageJson(c)` and `typecheckOn(c)` take the context.
127
+
128
+ - **`c.isLink(path)` joins the check context.** It says whether a symlink stands at the path itself, which `exists` cannot, since it follows the link. The answer is recorded, so a dir turning into a link reruns the check. `shipkit keys --why` lists it as a link probe.
129
+
130
+ - **`resolveFloor` and `resolveTypecheckProjects` read through a reader.** `core/floor-resolve.ts` and `ts/tsconfig-resolve.ts` hold the resolution with no disk access, and take a reader (`FloorReader`, the `UnitReader` shape plus a PATH lookup). `core/floor.ts` and `ts/tsconfig-projects.ts` keep the old signatures with a disk reader as the default, so callers outside a check are unchanged. Inside a check, `checkFloorReader(c)` reads through the context, and looking `tsc` up on PATH (a repo with no local tsc and no typecheck script) marks the answer untracked. `parseJsonc` moved to `ts/jsonc.ts`, and `ts/config.ts` still exports it.
131
+
132
+ - **A non blocking check that passes is cached.** The ledger stored only answers from blocking checks, so a non blocking repo fact such as SK017 ran every time while `shipkit checks` reported it cached. A pass is stored whatever the check blocks. A WARN from a non blocking check is still never stored.
133
+
134
+ - **The config's closure follows what its load runs.** What `c.config` records, and the raw access scan of it, now follow static imports only: a literal `import()` inside a function the config exports does not run at load, so a config that loads a disk reading module lazily keeps its readers cached. A check file's closure still follows `import()` too, since its body can run it. `importClosure`, `extractSpecs`, `rawAccessInSource` and `rawAccessInClosure` take `{ dynamic: false }` for this.
135
+
136
+ - **`shipkit ci --only <x>` that leaves nothing to run FAILS.** A value naming no check and no group already failed. A value naming only checks that are off (a floor slot the config turned off) printed `all 0 check(s) green` and exited 0, a green over nothing that reads like a clean repo. It now prints `FAIL: --only <x> leaves nothing to run: <id> is off` and exits 1.
137
+
138
+ - **SK004 reads and writes `.gitignore` through `c` and asks git through `c.exec`.** It keeps the whole tree as its declared input, on purpose: what git ignores and tracks depends on the index, `.git/info/exclude` and the user's global excludes file, which no read set records exactly. `notIgnored(c, paths)` is async and takes the context.
139
+
140
+ - **SK016 reads the repo's check files through `c` and is cached on them.** It lists `**/*.check.ts` through `c.list` with discovery's skip rules, so an edit to a file that is not a check file no longer reruns it, and a gitignored check file is not reported as this repo's declaration. Its fixer writes through `c.write`, and the config half of the rewrite (the `when` entries, the dropped keys, the read back through the loader) runs as its own process, `core/config-rewrite.ts`, through `c.exec`, because loading `repo.config.ts` as a module is not a read the context can stand for. `legacyShapes(c, config)` takes the context. `core/check-file-names.ts` holds the path and source rules discovery and SK016 share (`isLegacyCheckSource`, `fallbackNames`, `readLegacyCheck`, `isCheckFilePath`), and `discover-checks.ts` still exports the first two.
141
+
142
+ - **SK014 and `shipkit prose` read their scope through `c`, and SK014 is cached on it.** The scope is each include glob listed through `c.list`, so it is what git sees: a gitignored file is never house prose. The dot rule of the disk scan it replaces holds (a hidden segment matches only a pattern segment that names one, `hiddenAllowed`). SK014 reruns when a file in scope changes or a file joins or leaves a glob, and stays cached on an edit anywhere else. `runProseCheck({ c, config, paths, fix })` and `proseFiles(c, config)` take the context, and `--fix` writes through `c.write`.
143
+
144
+ - **SK002 and `contracts` read through `c`, share one evaluation per run, and are cached on what the contracts looked at.** The contract engine reads every artifact, detector and venue folder through the check context, a gitignored venue folder included (`c.entries`), so an edit to an unrelated file no longer reruns them and an edit to a file a contract reads does. `evaluateContracts(manifests, cfg, c)` and `contractsConfigFrom(config, c)` take the context they read through, `pngDims` takes the bytes, and a custom rule's `env` gains `read(path)`: a probe that reads through `env.read` and `env.exists` keeps its check cached, and one that reaches the disk around them keeps it uncached. `withContractsScope` and `evaluateContractsOnce` are gone, since the shared scan `contractsResult` is the one evaluation per run.
145
+
146
+ - **`c.bytes(path)` and `c.entries(dir)` join the check context.** `bytes` reads a file raw (an image) and is recorded like `read`. `entries` returns what stands directly inside one dir on the disk, ignored entries included, with each entry's kind, so a gitignored folder a check must still inspect is readable through `c`. Its names and kinds are recorded, and a change reruns the check. Both are on a shared scan's context too.
147
+
148
+ - **`shipkit ci --only <group>` runs every check declaring that `group`.** A value no check answers to by id, title or file is read as a group name, and each check in it runs with its dep closure. A check name wins over a group of the same spelling. `--with` still takes names only.
149
+
150
+ - **Every test file is discovered, and each release unit gets its own test check.** A repo that declares `units[]` gets `test:<unit id>` for each one, beside the root's `test`: the unit's suites run in its own dir through the suite runner, with its own bunfig, preload and `units[].tests` budget, keyed on its dir plus the sibling units its package.json depends on. The root `test` leaves each unit dir to its own check. A repo with no units gets the one `test` it always had.
151
+
152
+ - **Rule SK018: a test file that no check runs fails the gate.** Each `*.test.ts` belongs to the unit whose dir holds it, and that unit's `bun test` must reach it. A file kept out on purpose goes under bunfig `pathIgnorePatterns`, with the reason in a comment.
153
+
154
+ - **A release runs the shipped unit's tests, in the full tier, and skips the others.** The release gate leaves out `test:<unit>` for every unit it does not ship, and the unit gate no longer runs a bare `bun test` of its own. `release.skipTasks` is for other repo-wide tasks now.
155
+
156
+ - **The root `test` key follows what its test files import.** When a repo declares `defaults.test.inputs`, the sub-unit dirs are cut out of those paths and the root's own test files join the key as code, so an edit in a lib reruns the root only when the root imports it.
157
+
158
+ - **The suite runner keeps the repo's own bunfig ignores.** bun replaces the bunfig `pathIgnorePatterns` with any given on the command line, so the moment the runner left a declared serial or slow suite out of the parallel phase, every file the repo's bunfig skipped ran again. The runner now passes the bunfig patterns along with its own.
159
+
160
+ - **A test file that cannot load is recorded red.** A failed import prints no `(fail)` line, only an unhandled error under the file's header, and the suite record now reads that as a red file too.
161
+
162
+ - **A `run` body gets ONE parameter, `c`, and it is the body's only door to the repo.** `run: (c) => ...` replaces `run: (t, env) => ...`, which is gone. `c` reports (`c.ok`, `c.warn`, `c.fail`), says where (`c.root`, `c.config`, `c.frozen`), reads (`c.read`, `c.json`, `c.exists`, `c.list`), shares work (`c.use(scan)`) and reaches the world (`c.exec`, `c.fetch`, `c.tool`). `fix` gets the same `c`. The legacy `rules: []` and `tasks: []` shapes read as before.
163
+
164
+ - **A `run` check that declares no `inputs`, `ttl` or `never` is cached on what it read.** Every read through `c` is recorded with the hash of what it saw, stored beside the answer, and re-hashed before any replay: an edited file it read runs it again, an edited file it never read does not. A listing is what git sees, so a file matching the glob, or leaving it, runs it again. Reading `c.config` records `repo.config.ts`. A body that used `c.exec`, `c.fetch` or `c.tool`, or called `c.untrack`, is never stored. A check whose import closure touches the disk or a process around `c` (an `fs`, `fs/promises` or `child_process` import, `Bun.file`, `Bun.spawn`, `Bun.$`, `Bun.write`) is not keyed on its reads: it runs every time, and `shipkit checks` and `shipkit keys --why` name the module and line. Reading `c.config` records `repo.config.ts` and every repo module it imports, and a config whose closure touches the disk keeps those answers uncached. `c.write` and `c.remove` let a `fix` write without an `fs` import. `shipkit checks` shows the population as `reads`, and `shipkit keys --why <id>` prints the recorded set.
165
+
166
+ - **`defineScan(name, (c) => ...)` declares work several checks share.** `c.use(scan)` runs it once per run, however many checks ask, and records its reads into every check that used it. Two scans ship: `mdPages(dir)` and `packageJson()`.
167
+
168
+ - **SK003, SK006, SK010, SK011, SK012 and SK013 read and fix through `c`** and are cached on what they read rather than on the whole tree.
169
+
170
+ - **An unfolded run's checks scroll in a box of their own, named by their titles.** A full gate unfolds into fifty lines, so they now sit under the run line in a box of 16rem at most that scrolls inside a thin edge, with the reds first, then what ran, slowest first, then what was cached. Each line names its check the way the tree does: the title, with the id as a small dim tag before it, the id once when the title is the id, and the id alone for a check that no longer exists. The description is the line's tooltip, and a click opens the check's page.
171
+
172
+ - **Every hub page and tab is in the url, and back and forward walk them.** A header link used to swap the page and leave the url on the page before it. Every selection now has an address: `#item:<collection>:<id>` for an entry, `#col:<collection>` for a section's grid, `#page:status` for the raw text of the last run, and the media forms unchanged. Each page change is a history entry, so reload, back, forward and a copied link all land on what was on screen. `hash-route.ts` exports `selOf`, the inverse of `hashFor`, and `HubSt.go(sel)` opens any selection through the same calls a click makes.
173
+
174
+ - **The graphs section is now the wikis page, one tab per mounted wiki.** The header link reads `wikis` and opens the first wiki. A tab per mount, labelled `this repo's wiki`, the house wiki named after its repo, and `agent/`, shows one link graph at a time, and the open tab is in the url (`#item:wikis:wiki:agent`). The collection id is `wikis`, entries are `wiki:<mount id>` filed under `wikis/<id>`, and the resource type `graph` is now `wiki`. Old `#item:graphs:…` links open nothing. The `/graph/<id>` routes keep their names.
175
+
176
+ - **A mutate lease whose lock cannot be created refuses at once instead of spinning.** When the lock's directory was removed under a waiter, or was not writable, `withMutateLease` retried in a loop with no await: the process burned a full core forever, past `maxWaitMs`, with its event loop blocked. Only an existing lock is now waited on. Any other failure to create it returns `{ ok: false }` with a warn naming the lock path and the error, and `tryLease` answers null.
177
+
178
+ - **The quick tier's parallel workers no longer isolate each file, so a worker cannot hang in spawnSync.** `--parallel` makes bun isolate every file, and under that isolation a worker's `Bun.spawnSync` can spin forever at full CPU on a child that already exited (bun issue 34069), which the 120s phase ceiling then reported as a stuck worker. When the bun in use knows `--no-isolate`, the suite runner passes it, so each worker keeps one global across its files, as a plain `bun test` and the serial phase always did. Measured: rvlib-shipkit's suite hung 3 runs in 24 before and 0 in 25 after. A test file that stubs a global must restore it now, and a repo whose `bun test` body names `--isolate` keeps isolation. `planPhases` takes `noIsolate`.
179
+
180
+ - **A `cmd` check's log is whole on disk when the run returns.** The runner wrote it through a stream that opened after the child exited, so a run could resolve before its log existed: the `full log:` path pointed at nothing, and a caller that removed the dir right after got an ENOENT thrown from nowhere, which a test runner blames on the next test. The log is now opened and written synchronously, and a log that cannot be written adds one WARN line naming the path and the reason instead of throwing.
181
+
182
+ - **A run says `cached` or `ran`, never replayed or measured.** The run line is `ok: cache — 41 cached, 8 ran, 9 never cached`, a cached check ends `(cached, 5.6s saved)`, an attached one `(attached to pid N, ran there in Xs)`. `shipkit keys`, STATUS.md, `shipkit checks`, the needs feed, the tui and the help texts use the same words, and `src/core/outcome-words.ts` exports them for any other surface. The `replayed` field of the run record and of `--json` output keeps its name.
183
+
184
+ - **A check description states what must be true, in one short line.** `description` is ONE line of at most 90 characters, in the present tense, like `package.json routes lint, typecheck and test through the gate`. The `this checks ` opener is gone, and a clause saying what red means is refused, because red is always the negation of the line. `check()` refuses a description that breaks this at import and names the file. `descriptionProblem()` is exported for a repo's own sweep test. Every built-in description is rewritten. A check file the SK016 migration writes carries its task's own description when it fits, and a TODO placeholder that loads otherwise.
185
+
186
+ - **The runs page is `runs` in the address, with one tab per list.** The hash reads `#item:runs:runs`, never `timeline`. Four tabs sit at the top: `runs`, `test files to fix`, `slowest checks`, `longest runs`, one list visible at a time, and the chosen tab rides in the hash (`#item:runs:runs:files`), so a reload or a copied link restores it. A tab shows a count where it means something: the red runs, the test files to fix (red when any, a calm zero when none), the checks flagged near their ceiling. A run line has no separator glyph any more: the ran count and the cached count are two right aligned columns, ran in the accent colour, cached dim, a zero dimmed further, and a check line under an unfolded run puts its `ran` or `cached` in the matching column. The slowest checks name their `mean` and `worst` columns once, above them. The resource kind `timeline` is now `runs`.
187
+
188
+ - **The runs page is a tree of one line runs.** Each run is one 24px line on fixed columns: age, who started it (`hub`, `terminal`, `daily`, `save`), the command, its wall time, `8 ran · 41 cached`, and `1 red` when any, with a red edge on a red run. Unfolding a run shows one line per check on the same columns: a verdict dot, the check id, its time, and `ran` or `cached`. The three lists below it use the same compact grid, and no run is a card any more.
189
+
190
+ - **"Tests to fix" is now "test files to fix", and each row names the check that ran the file** (`test:libs`, the file, its time, over its budget or red). The check runner sets `SHIPKIT_CHECK_ID` on every process it spawns, and the suite runner files every test file it times under it. The run record goes to schema v6: `suites` gains a `check_id` column with a default, so older rows stay (shown as unnamed) and older builds still write.
191
+
192
+ - **Idle time on the run chart is a visible slice that says how long nothing ran.** Every idle stretch over a minute folds into a tinted slice of fixed width, the full height of the chart, with its duration inside (`idle 4m 12s`, seconds dropped past an hour). The slice from the last run to now counts up every second, so a chart with nothing running still shows time passing, and the older idle time stays compact.
193
+
194
+ - **A check's page is one compact block.** The four boxes are gone. The id sits as a small tag before the title, the description under it, then the exact command the check runs as copyable monospace (repo relative tokens, full paths in the tooltips), with the package.json script a build step resolved to and the body it expands to, or "in the gate process" with a link to the source for a check whose body is a function. One grid of facts follows, two columns on a wide screen: `from`, `kind`, `on failure`, `run when` (options `always`, `before a release`, `health pass only`, `never`; the config words are unchanged), `remembered` (paths folded under it, `for 24h`, or `never`), `next run`, `last run` (`ran 3m ago in 412ms` or `cached 16m ago`), the owner page shown short, and the timings with their bar row. Check entries carry a new optional `script` field (`{ name, body }`) on the wire.
195
+
196
+ - **The resource tree has no run button.** A check row shows its id as a small dim tag before its title, and a check runs from its own page. The row grid lost its run column, and a folder no longer runs the checks under it.
197
+
198
+ - **`src/core/outcome-words.ts`** holds the two words for how a check got its answer, `ran` and `cached`, and the hub reads them.
199
+
200
+ - **Hovering the live strip shows its tooltip again.** It sat inside the strip's boxes, which clip their overflow, so the folded strip showed a sliver of it or nothing. It is fixed to the window at the pointer now, to the left of it past the middle of the window, and names the check, its verdict with its duration or elapsed time, when it started, the run and who started it. The strip's `(?)` opens below the strip. A drag hides the tooltip while it pans.
201
+
202
+ - **A run nothing drives anymore leaves the live strip within 20 seconds.** A runner stamps its live row every 3 seconds while it runs, changed or not, from an unref'd timer stopped when the run ends or flushes. A row whose stamp is older than 20 seconds is dead whatever its pid says, is dropped on read and swept, which covers a hub under `bun --watch` that restarted in place with a run in flight. The hub also sweeps, at start, every row its own pid holds. `sweepLive(path, now, { ownPid })` is exported, and `RunLog.open` takes an `every` timer for tests.
203
+
204
+ - **Every git process shipkit starts costs a third of what it did on macOS.** `/usr/bin/git` there is a shim that looks up the developer directory on each call: 15ms a spawn against 5ms for the git it forwards to, and a gate spawns git a dozen times. `gitBin()` asks `git --exec-path` once per process and spawns the real binary; anything unexpected keeps plain `git`. Every direct git spawn and every `spawnWithTimeout(['git', …])` goes through it, except the merge probe, which keeps resolving `git` on PATH at the call.
205
+
206
+ - **A key scan whose staged index is byte for byte one this process already derived runs no `write-tree` and no `cat-file`.** Both are pure functions of the index bytes, so the answer is reused. The post run re-hash of an untouched tree is the usual hit, and any edit, including one a check makes to its own input, changes the bytes and measures again.
207
+
208
+ - **The rail's hover card is structured.** A header (icon, bold name, version at the right, branch with its uncommitted count, a two line description), a NEEDS YOU part only when something waits on a human (the asks with their titles, a red gate with its failed checks), then label and value rows: `release`, `live`, `activity` (`2 agents · VS Code open · 3h ago`). 300px wide, and an unknown fact leaves its row out.
209
+
210
+ - **The quick tier budget judges the CPU a suite burns, not the wall time it took.** A busy machine made correct suites fail the gate: one took 1.3s on a quiet machine and 5.6s under another gate. A suite over the budget in the parallel run now runs again alone in its own `bun test` process, and fails when that process (with every child it reaped) burns more CPU than `tests.budgetMs`, or when its wall time passes twice the budget while the 1 minute load average is under the core count, so a suite that sleeps or polls still fails on an idle machine. The FAIL line gives CPU, wall time and load. A suspect that clears prints a `note:` line with the same figures. `confirmOver` takes the alone runs and the machine load and returns `{ over, cleared }`.
211
+
212
+ - **The project rail is quieter, and a hover card says the rest.** An icon carries at most one badge, its open asks. It dims while VS Code has the folder closed, and a thin edge pulses while Claude Code sessions run in it (static under reduced motion). Hovering shows a card at once beside the rail, one line per known fact: name, description, branch with its uncommitted count, version, last release, last gate (green or the failed checks), open asks with their titles, sessions, VS Code, last activity, live url. The native tooltip is gone. The new facts ride in the same cached poll: the branch from `.git/HEAD`, the uncommitted count only when the refs or the index moved, the gate only when the run record moved.
213
+
214
+ - **An md graph starts where its nodes sat last time.** About 3 seconds into a display the page sends its node positions once, and the server keeps them in `<git-common-dir>/shipkit/graph-positions/<graph id>.json`, never tracked. The next display starts each node at its saved spot, or near its saved neighbours, and starts calm when most nodes have one. The layout still runs from there. Nodes that no longer exist drop on the next save. It works on the hub's `/graph/<id>` pages, in the graph a hub item view embeds, and in `shipkit wiki serve` when its root is in a git checkout.
215
+
216
+ - **The run chart is a live trail you can stop.** It follows now by default. Dragging it into the past, or zooming away from the right edge, stops it where it is so a bar can be read, and a `● live` pill, the `k` key or dragging back to the right edge follows now again. An open tooltip holds the chart still, and a drag never clicks the bar it started on.
217
+
218
+ - **The hub has one run chart: the live strip, and the run timeline page is now runs.** The page keeps each run as a block and the three lists, and draws no chart. Unfolded, the strip covers the whole record, opening on the last ten minutes of runs; `fit` shows every run on record with the idle stretches folded, the wheel zooms, a drag pans, and scrolling back to the right edge follows now. The unfolded strip has no title row, padding or scrollbar: its zoom control floats over the chart while the pointer is on it, its explanation is the (?) beside the chevron, and its height follows its lanes, growing at once and shrinking only after ten quiet seconds.
219
+
220
+ - **The run timeline shows how many checks ran at once.** A band over the lanes draws the count at every moment, with the runner's cap as a dashed `cap N` line and the stretches past it in orange, on the timeline page and in the unfolded live strip alike. Run labels no longer overlap: a label is cut before the next one, or left out, with the whole label on hover.
221
+
222
+ - **The project rail reorders by drag and drop, and sorts once by last activity.** A drag shows a line where the icon will land and moves nothing else. The button above the icons sorts once, newest activity first, and that becomes the manual order. Last activity is the newest of the last commit, the last write to the repo's run record and its newest Claude Code transcript, and the tooltip says it. The order is one text file for the whole machine, `~/.config/shipkit/projects-order.txt`, one repo name per line: every hub reads it on each poll, new repos join at the end, vanished ones drop out, and `POST /api/projects/order` writes it.
223
+
224
+ - **No TSC task anymore, and no wrapper around the hub; SK017 wires the hub task instead.** `shipkit init` and `shipkit start` write ONE task: the hub, opening with the folder itself and carrying the background problem matcher. Rule SK017 (non-blocking, fixable) repairs an existing `.vscode/tasks.json` by editing only the spans it changes: it writes the matcher on the hub task, removes a `👁 TSC` watcher task and its `dependsOn` entry when the gate runs an enabled `typecheck` (otherwise it keeps it and says why), and folds a folder-open wrapper whose only `dependsOn` is the hub into the hub. A wrapper that starts other processes too stays, and so does a tsc watcher under another label. `init` on an adopted repo applies the same fixer.
225
+
226
+ - **A run the hub starts on its own never writes into the tree.** No repair pass, `frozen`, no status files, quick tier, and the save watch ignores every path the gate writes, so an automatic run never schedules the next one. The hub prints a problem frame after each check whose verdict moved the board, so a type error reaches the pane when typecheck ends.
227
+
228
+ - **Every check's findings reach the vscode problems pane through the hub task.** The hub runs the quick gate at start and again once the tracked files have been quiet for 2s, one run at a time, and after every run it makes it prints a frame (`shipkit problems: begin`, one `<file>:<line>:<col>: <error|warning> <check id>: <message>` line per finding, `shipkit problems: end, N error(s), M warning(s)`) that the hub task's problem matcher reads. Findings are read from tsc, oxlint, eslint stylish, bun test failures and wire lines naming a file of the repo. A replayed check prints the findings stored with its green. A check declaring `ttl` never feeds the pane: its answer is about the world. The watch only starts ordinary gate runs, which measure or replay like any other run. `CheckResult` and `CheckEvent` carry `problems`.
229
+
230
+ - **The run timeline opens on a horizontal time axis.** One axis across every run on record, now on the right: each invocation a labelled band, each check a bar at its true start and length, checks that ran at once stacked in lanes. Idle stretches over a minute fold into a break that says how long they were. The wheel zooms around the pointer, a small control zooms in, out and to fit, a hover names the check, its time and its verdict, and a click opens its page. The per run blocks and the three lists stay below it.
231
+
232
+ - **The hub has a header over every page, and a live strip under it.** The header carries the repo name, one link per core page the repo has (checks, run timeline, asks, media, graphs), the run state and the two run buttons, which leave the bar over the tree. The live strip is the run timeline's own chart over a rolling window: folded, a thin row of the last two minutes where each check is a bar growing at the right while it runs and sliding left once it ends, with `in flight N / cap M` at its end; unfolded (the chevron or `l`, remembered per repo), the full chart over ten minutes, zoomable. It shows a run started from a terminal the same as one started from the hub, and the runs in flight in the other house repos as one lane each, whose click opens that repo's hub. The floating run strip is gone.
233
+
234
+ - **The run record knows which runs are in flight.** A new `live` table holds one row per runner going right now, whoever started it: the checks running with their start, the ones finished with their verdict, the checks waiting with what they wait on, the cap, and the counts so far. The runner rewrites it as it goes, with the checks it has finished, marks it ended when it returns, and the write of the run's rows deletes it. A row left by a killed process, or ended and never written, is ignored and swept. `GET /api/live.json` on the hub serves it. The cap comes from one function, `runnerCap`, the one the runner uses.
235
+
236
+ - **The hub's `run the gate` button runs the gate.** It posted the same body as the health pass, so it ran the health checks and left the build steps out. It now asks for the gate, build steps included.
237
+
238
+ - **The hub has a project gutter.** A column on the far left holds one icon per sibling repo that includes the same house wiki index as the hub's own CLAUDE.md (its favicon, or its initial). A green dot says VS Code has the folder open, an accent count gives the Claude Code sessions whose working directory sits in it, an orange count gives its open asks, and a bar marks the hub's own repo. The tooltip adds the last release (tag, age, commits since, or never released) and the live url `smoke.base` declares. A click opens the folder in VS Code. `GET /api/projects.json` answers all of it, measured at most once per 5s for every tab: about 55ms per poll on 20 repos, since git is asked only when a commit or a tag moved.
239
+
240
+ - **A frozen run and a normal one share every check that does not write.** `frozen` was in every check's cache key, so a frozen background run and an agent's gate never replayed nor attached to each other and measured the same tsc twice. A check now declares `writes: true` when it rewrites the repo unless the run is frozen (SK004 untracks generated files, SK005 updates the rvlib deps), and only such a check carries `frozen` in its key, content and TTL alike. Every other key ignores the mode, so each moves once on upgrade.
241
+
242
+ - **The run record never shows more checks at once than ran.** The runner timed checks on the wall clock and let the record stamp each row when it arrived, so a clock step back between two checks stamped a later check before an earlier one ended: one gate read as 9 at once against a cap of 8. Checks are now timed on a clock that never goes back inside a process, and each row's `at` is the moment its `ms` was measured. A check that attached to another run is filed as a replay (it measured nothing here), and the check event says `attached: true`.
243
+
244
+ - **`shipkit ci --detach` and `shipkit status [id]`.** `--detach` starts the same gate, with the same flags, in the background and returns a run id at once (about 0.3s). `shipkit status <id>` prints the wire lines so far, then the verdict, and exits with the gate's own code, 3 while it still runs, 1 when it died without a verdict, 2 for an id nobody started in this checkout. No id reads the latest run. The detached gate is an ordinary gate, whose answers land in the same ledger and run record; only its output and its exit code are kept, under `tmp/shipkit-jobs/<id>/`, for a week.
245
+
246
+ - **One machine pool caps the checks measured at once, across every shipkit process.** Every repo, every agent and the hub share it: a check takes a slot before its body starts and gives it back when the body ends, and a replay or an attached wait takes none. One lock file per slot under `~/.cache/shipkit/slots/`, held like the mutate lease, so a slot whose holder died is free at once. The size is `parallelCap()` (`src/core/parallel.ts`): the machine's cores, never below 2 nor above 8, or `SHIPKIT_PARALLEL` for the machine; a value that is not a whole number of at least 1 is named in a WARN and the default holds. A process a check spawns (a suite that runs gates) takes no slot of its own, since it runs on its parent's. `shipkit ci` prints the pool size and how many slots are held, a gate that waits for one says who holds them, and `poolOccupancy()` (`src/core/slots.ts`) returns pid, repo and check per busy slot. A row in the run record now spans the body alone, never the queue.
247
+
248
+ - **Two processes asking one check at once run it once.** A check whose cache key another run is measuring right now attaches to that run: it waits for the answer and prints the same verdict and the same lines, its headline stamped `attached to pid N`, and the ledger line counts it apart (`1 attached to another run`). The lock is per check key, so two `--only typecheck` runs, or a full gate and a `--only typecheck`, share the one tsc, in one checkout or across worktrees holding the same content. A holder that died is stolen at once and the taker measures. An answer is adopted only when it finished after the asker asked, so a red never replays to a later run. A check with no key never attaches, nor does a run that must measure for itself (`--fresh`, `SHIPKIT_LEDGER=0`, `bump`, `release`). Five `shipkit ci --only typecheck` at once on a cold key: one tsc run in 4.8s, where it was five in 7.2s.
249
+
250
+ - **`shipkit mv` on code rewrites relative imports.** It rewrote only repo-relative and package-name paths, so moving a module imported as `./mod.ts` or `../core/mod.ts` left every such importer pointing at a file that no longer existed, and said it succeeded. Every relative specifier in a code file is now resolved from its importer (explicit extension, `.js` for a `.ts` file, no extension, or a folder index), and when either end moves it is written again from where both land, in the same style: the importers of a moved file, the moved file's own imports of what stays behind, and for a folder, the importers outside it while the imports inside keep their text.
251
+
252
+ - **A `/*` inside a line comment no longer hides the imports below it.** The import scan stripped block comments before line comments, so a header line naming a glob like `"src/*"` opened a fake block comment that ran to the next `*/` and swallowed every import in between. `src/ts/config.ts` showed NO imports to the cycle scan, the cache key closure and `shipkit mv`. Both comment shapes now go in one left to right pass, and the type text `shipkit config-keys` prints is read the same way.
253
+
254
+ - **A stuck test worker fails the suite runner at a wall ceiling instead of hanging it.** bun's per test timeout cannot interrupt a synchronous call, so each `bun test` phase now has a ceiling (quick 120s, full 30 minutes, `--ceiling-ms` to set it). Past it the whole process tree is killed and the FAIL line names the phase and the suites still running. The case in the field: under a dense parallel run, bun 1.4.3 canary's `spawnSync` can spin on a child that already exited.
255
+
256
+ - **A comfy panel is seen the moment it answers.** The boot, kill and quiet waits poll every 50ms, 25ms and 50ms instead of 250ms, 100ms and 250ms, under the same ceilings (30s to boot, 2s to die, 3s to go quiet), so a panel that is up in 80ms opens then rather than at the end of a 250ms step.
257
+
258
+ - **SK007 outside a git repository skips without spawning git.** It asks the git root probe first and skips with git's own words, where it used to spawn `git remote` to hear the same refusal.
259
+
260
+ - **A directory with no `.git` in reach is answered without git.** The git root probe walks up for a `.git` first, and with none at or above and no `GIT_DIR`, `GIT_WORK_TREE` or `GIT_COMMON_DIR` set, it returns git's own `not a git repository` words without spawning. The suite record reads the tree prefix from the same probe instead of its own `rev-parse`. In rvlib-shipkit's own quick tier this removed 112 of 128 `rev-parse` spawns.
261
+
262
+ - **The cache key scan spawns fewer git processes.** The declared paths go to git in one `git add` instead of one per path, and the force-added ones in one more, so a repo that declares ten inputs no longer pays ten spawns per scan. The post run re-hash reuses the first scan's ignored answer and, when neither root tree moved, the first scan's per path ids: two git processes instead of four when nothing changed (about 95ms to 60ms on a 13 input repo). A path git refuses still fails the scan by name, and a force-added input edited mid run still drifts.
263
+
264
+ - **The one word is CHECK: a `<name>.check.ts` file default-exporting `check({...})`.** The shape is `CheckDef` in `src/check.ts`, the built-ins live in `src/checks/SK###-<slug>.check.ts`, the inventory is `shipkit checks`, the hub files each one at `#item:checks:check:<id>`, and the TUI panels are `build steps` and `checks`. The row, the file and the command now say the same word. `shipkit units`, the 0.38 name of the inventory, still answers, hidden from the help, with one WARN on stderr naming `shipkit checks`, and goes at 0.40 with the rest of the compatibility window. The barrel's `discoverChecks` returns the check files (async, imported), and the legacy script reader is `discoverLegacyChecks`. Release units (`units:` in `repo.config.ts`, `shipkit releases`, `shipkit bump <unit>`) keep their word: they are a different thing.
265
+
266
+ - **A check file and a legacy check script share the suffix, and the SOURCE says which is which.** A legacy script (0.38 and before) carries named `export const blocking | inputs | ttl | never | gate` declarations and runs its whole body on import, so discovery reads each `*.check.ts` before importing it: a top level legacy declaration, or no default export that could be a check object, makes it a script, read by the static reader and spawned, never imported. A comment that mentions the old shape changes nothing. SK016 lists each script with the check object its declarations become. A basename shared across the two shapes names both files by their path.
267
+
268
+ - **The hub has a run timeline.** One tree node, `health/run-timeline`: every invocation on record, newest first, labelled with what ran it and its wall time, one lane per check with its bar at its real start and end so parallel checks stack, and beside it the tests to fix, the slowest checks (flagged near their ceiling) and the longest runs. Each bar and row opens the check page. The suite runner now records every suite it times, with whether a test in it failed, in the run record's `suites` table, which is where tests to fix reads from.
269
+
270
+ - **The check page's cache widget is facts, and runs in is one select.** Cache shows `next run`, `last run` (measured or replayed) and `remembered` as labelled rows, `remembered` as a rhythm (until a file it reads changes, re-measured every ~3d, never measured every daily pass), with every explaining sentence and the `never` reason in the row's help. Runs in is one select named by effect (every run, before a release, health pass only, never) over the unchanged config words; never asks for its reason before it writes.
271
+
272
+ - **A check page says when the check runs next.** `next run` opens the cache widget: next shipkit ci, before a release, never with the reason it is off, or for a health check the time of the next daily pass, and for a world probe whose remembered answer outlives that pass, the pass that actually measures it again. The exact moment is in the tooltip.
273
+
274
+ - **A comfy panel refused for serving no web ui has exited when the refusal returns.** The spawned child is killed and awaited before the error, so the next open never adopts the dying process.
275
+
276
+ - **A gate asks git where the repo is once, not a dozen times.** The store, the run record, the scan, the nesting guard and three rules each spawned their own `git rev-parse`. One call now answers all of them and is remembered per directory, re-validated on every read so a `git init` or a removed `.git` is seen at once. A gate on a small repo spawns half the processes it did and runs about 40% faster.
277
+
278
+ - **The hub runs the health pass once a day.** While `shipkit serve` runs, the whole health pass runs every 24h, and at startup when the last whole pass on record is older than that, through the same in process runner as the button and never on top of a run already going. A check's `ttl` is still its own cadence: a fresh answer replays inside the pass. The run record (sqlite schema v3) files every invocation, with what ran it and when it started and ended, which is how the timer knows when the last pass was.
279
+
280
+ - **A check file a fix pass just wrote is discovered in a repo reached through a linked path.** Discovery imports the real path of each file, where the path the walk found missed a file created after the resolver first listed its directory. A check's `import.meta.path` is that real path too. SK016 prints one WARN per legacy shape instead of bare lines, so every line `shipkit ci` prints carries a wire prefix.
281
+
282
+ - **A repo that runs a check script as a task reads as one check, and the gate reads what the fix pass rewrote.** A task whose argv runs a legacy check script (`bun <file>` or `bun run <file>`, nothing after it) was that file run twice under one name, so the pair reads as ONE check, and a repo on the old shapes no longer dies on `duplicate check ids`. The task goes once the script is ported, since a check file is its own check. Tasks move all together or stay together, so a task is never left beside the check file it became. `shipkit check --fix` and `shipkit ci` re-read `repo.config.ts` after a fixer rewrote it. A check file whose `cwd` is absolute runs there. A check file still opening with the TODO description the migration wrote keeps running, and SK016 warns with its path until a sentence replaces it.
283
+
284
+ - **The check page answers at a glance, and a check says what it is in one required sentence.** `description` is REQUIRED on every check: ONE line of at most 90 characters stating what must be true, which the shape enforces at import and refuses by naming the file. The page renders it above every widget, and the tree row's tooltip carries it. The WHAT widget stopped being three sentences: it is FACTS now (from, build step, body, on failure, file, owner), each a short value with its explanation in that row's own `(?)`. Every wire line of the last run renders as INLINE markdown, so a `fix:` pointer's url is clickable and its backtick spans read as code while the line keeps the shape the protocol fixes. `shipkit config-keys` generates the `CheckDef` block beside the config keys, so a check field cannot land undocumented.
285
+
286
+ - **The TUI panels are `build steps` and `units`.** They split on `build`, the one axis that says which question a check answers, and the checks panel draws its rows from the same inventory every other surface reads, so a check cannot exist for the gate and not for the dashboard. Each row carries the check's own sentence. `tasks-panel.ts` is `build-steps-panel.ts`, `checks-panel.ts` is `checks-panel.ts`, and the `shipkit tui` description says the same.
287
+
288
+ - **A check says in ONE line what it holds.** `description` on the check shape states in one line what must be true. The hub shows it first on the check page's WHAT widget and on the tree row's tooltip, so a row says what it is about without being opened. Every built-in carries one. `shipkit config-keys` now generates a SECOND block, the `CheckDef` shape, onto the same page beside the config keys, so a check field cannot land with no doc line any more than a config key can.
289
+
290
+ - **Check SK016 carries the migration, and it writes.** Every declaration in a shape the check file replaced is READ through the window and named once per run by one check rather than by a warning per surface: a legacy check script (named `export const` declarations, no check object), `rules: []`, `tasks: []`, `checkDefaults`, `ruleExempt`, and `export const gate = false`. It WARNs for the whole window, because a repo carrying them is behind rather than broken, and they are refused at the first release after `shipkit fleet` shows no repo carrying one. `shipkit check --fix SK016` rewrites what is mechanical: a task that is pure data becomes a check file beside `repo.config.ts` with `build: true`, `ruleExempt` is written into the managed block, and `checkDefaults` is dropped once no script leans on it. What it cannot port is listed BY NAME and left byte identical: a check script, whose body prints and exits in a process of its own (`scriptPort` names the check object its declarations become), and what holds a FUNCTION, a `rules: []` entry or a task whose body is `logic` (with the file to create). The config rewrite is read back through the loader, and the previous bytes go back when it does not survive.
291
+
292
+ - **The hub says what a check IS in the one vocabulary.** The check page's WHAT widget answers three questions rather than one: whose check it is (the toolkit's or this repo's), whether it builds or reports, and whether its body runs in the gate process or in its own, with the argv named when it has one. `whatWords()` in `check-words.ts` owns the sentences, so they are pinned by tests instead of read off a screenshot. The tree's by type tab is two groups, `from shipkit` and `from this repo`, and every check opens the same page with the same widgets and the same switch. The run buttons say what they run (`run the health pass`, `run the gate`), and no surface of the page files a check under a word that no longer names a kind.
293
+
294
+ - **`shipkit rules` is deleted, and `shipkit checks --fixable` is the catalog it was.** It listed a third of the inventory under a word that names no kind of anything any more, and it answered one question `shipkit checks` could not: which checks repair themselves. That flag answers it now, with the page that owns each invariant, where the check runs (so an exempted one cannot look like a repair waiting to happen) and the command that applies one. A check row carries `fixable` for every surface that asks. `cmd_Rules.ts` and `ruleRows` are gone, and the two `defineRepo` errors and the init hint that named the command now name `shipkit checks`. One loss, stated: the catalog needs a wired repo, where `shipkit rules` would answer from any directory.
295
+
296
+ - **A check file can say it is a BUILD STEP.** `build: true` in a `*.check.ts` puts it where `shipkit ci` runs it and `shipkit check` never does, which a floor slot and a `tasks: []` entry were the only way to be: the merge left the plan's own `test-libs.check.ts` example joining the health pass, because `build` was derived from where a declaration came from and a file had no way to answer. It is still DERIVED for a floor slot, which has no file to say it in. `when` keeps answering the other question, which is about time, and the two cannot be spelled into a contradiction: `build: true` with `when: 'health'` is a check nothing runs, and `check()` refuses it at import with the sentence that names both. A declared build step takes the 600s ceiling rather than the 120s one, and it keeps its file and its owning page on the hub, where a step derived from a script has a command where a path would be.
297
+
298
+ - **`shipkit checks` and `STATUS.full.md` render from ONE set of cells.** `checkCells()` answers every column once (check, runs in, from, build, on failure, cache, ttl, reads, owner) and both renders read it, so a column cannot exist on the terminal table and not in the file, or say two different things in the two places. `build` is the new column on both, and the file's legend says what it means.
299
+
300
+ - **ONE runner runs every check, and the two engines are deleted.** A check used to route to the task DAG or to the check suite by whether it declared `deps`, so one word meant two things and a behaviour either engine had stopped at its own border. `src/core/check-runner.ts` is a DAG over every check, a check with no `deps` being a root, with both halves folded in: the `blocking: false` demotion at the source (the exit code means exactly "something that blocks you failed", while the REAL outcome rides beside the demoted lines for a page to show), the ledger replay and the TTL replay in front of every body, a subprocess killed with its whole process tree at its ceiling, an in-process body whose throw is that check's failure rather than the gate's crash, the per check log file, and ONE tap, `onCheck`, replacing `onTaskFinish` and `onResult`. `core/task-runner.ts` is gone, `runTasks` and `selectTasks` are `runChecks` and `selectChecks`, and `TaskDef` survives only as the legacy shape `defaults` and `tasks: []` still speak.
301
+
302
+ - **What may be remembered about the world, and for how long, lives in `ledger/world-cache.ts`.** One module the runner calls, with the measured-negative cap and the unmeasured eviction rule it has always had.
303
+
304
+ - **`kind` is gone from the declaration, the key, the record, the timings and the page.** It said which of three engines a check routed to, and there is one. Two derived facts replace it where a real distinction existed: `from` (`shipkit` or `repo`) carries the hub's by type tree, which is two groups now, and `build` says the one thing the health pass needs, that a step derived from a floor slot or `tasks: []` belongs to `shipkit ci` and not to `shipkit check`. A check row also carries the `timeoutMs` it declared, a resource id is `check:<id>`, and `STATUS.full.md` drops its `kind` column.
305
+
306
+ - **The run history is re-keyed, and the pre-cut rows are dropped.** A check is filed under its bare id rather than `<kind>:<id>`: the sqlite schema is at v2 without its `kind` column, and the rows written under the old scheme are deleted rather than migrated, because they are per checkout timings a fortnight deep at most and re-keying them would invent a mapping nobody can verify. The ledger's CONTENT key drops the same prefix, so `KEY_VERSION` is 3 and every keyed check measures once more before it replays again.
307
+
308
+ - **One walk per run.** The floor, the repo's `tasks: []`, every `*.check.ts` and both legacy forms compose ONCE (`ledger/gate-checks.ts composeChecks`) into a list the runner runs, the key layer keys and every surface renders, where a gate used to walk the registries three times. `gateInventory` and `gateChecks` take `{ tier }` instead of a pre-resolved task list, and `tieredTasks` is gone with the concept.
309
+
310
+ - **ONE shape, one file, one word: a `<name>.check.ts` reaches the gate with no config edit.** `check({...})` is the whole declaration and the package root exports it, so `import { check } from 'rvlib-shipkit'` plus a default export is the complete registration. `blocking` is required with no default, exactly one of `run` (in process, reports on the wire context) and `cmd` (argv, exit code is truth), at most one of `inputs` / `ttl` / `never`, plus `deps`, `when`, `cacheKey`, `fix`, `timeoutMs`, `group`, `env`, `cwd`, `id`, `title` and `owner`. What a build step, a repo guard and a world probe differ in is what the FIELDS say, never which registry they were put in. The id defaults to the file basename and the owner to the file's own path, so `code` is gone: a check file IS its own closure. `check()` refuses a missing `blocking`, two bodies, two cache populations and a `when` nobody defined, at import, so a broken declaration dies when discovery loads it rather than half way through a gate.
311
+
312
+ - **Discovery walks for `*.check.ts` and IMPORTS each file, the toolkit's own included.** `src/checks/` is a second discovery root, walked by the same code as the repo's, so a built-in and a repo check differ by `from` and by nothing else. The 15 SK modules and the contracts pass moved there as check files and the registry array is deleted: an SK id now lives in its FILE NAME, and discovery refuses a name that disagrees with the declared id, a built-in with no owner page, a duplicate id and a repo check squatting the reserved `SK` prefix. The walk skips what the check walk skips, a repo that HOSTS the toolkit never discovers the same file twice, and a module is re-imported when its mtime moves so a rewritten check never runs its old body. Measured on a repo carrying 16 built-ins and 27 legacy check files: 23ms cold for the whole walk plus every import, 4ms warm, against the 300ms budget the suite pins. The cost of importing rather than scanning is that a check file keeps its metadata imports at the top level and its BODY's imports inside `run`.
313
+
314
+ - **Two engines for one shape, this release only.** A check that declares `deps` runs in the task DAG, everything else runs in the check suite: `run` in process with id tagging and the fix-pointer floor, `cmd` spawned with the exit code as truth and the non-blocking demotion at the source. The next release merges the two runners and `deps` goes back to meaning only what it says. **The legacy check script still works**, read by the source scanner and run as a `cmd` check, and a run names the whole set ONCE with the form that replaces it rather than printing a warning per file (one line instead of twenty seven, in the repo that has that many). **`rules: []` still works**, read as a check whose body is its `lint`, and `Rule` keeps its import path. `checkDefaults`, `tasks: []` and the three legacy `when` spellings are untouched this release. Everything in this paragraph is refused at the first release after `shipkit fleet` shows no repo below 0.40.
315
+
316
+ - **`shipkit check --fix` and `shipkit rules` read the discovered set.** The fixer takes any check id, not only an `SK###` one, and its unknown-id line points at `shipkit checks`. `allTasks` moved out of `repo-config.ts` into the gate inventory, because the DAG half of a repo's task list is now discovered rather than configured, and one walk feeds both. `contractsTask` is gone from the barrel: `src/checks/contracts.check.ts` replaces it, keeping its id, its DAG position and its deliberate absence from the ledger.
317
+
318
+ - **`always` is the word for the place a check runs by default, and every row says who it comes from.** `continual` named the default value of `when` and nobody says it out loud, so the word is `always` everywhere the binary prints: the switch, the hub legend, the count line above the tree, `shipkit checks`, the markdown inventory and the config page. `always`, `release`, `health` and `off:<reason>` are the only words a `when` value takes. Beside `kind`, a check row carries `from`, either `shipkit` or `repo`, which replaces the nullable `builtin` bit: a built-in SK check, the contracts pass and a floor slot nothing in the repo filled come from shipkit, and a package.json script behind a floor slot, a task, a registered rule and a `*.check.ts` file come from the repo. Every check answers, where the bit it replaces had no answer for a task. `shipkit checks` shows it as a column, and the terminal table and `STATUS.full.md` are rendered from one function now, so the two can never disagree about a column.
319
+
320
+ - **The run action on a tree row is always there, dim, and never appears under the pointer.** The play button was `opacity:0` at rest, revealed by the ROW hover, so it materialised under the cursor. A click meant to open a folder ran every check beneath it instead. It is painted at all times now, at 35 percent, in the fixed slot it already had. Only hovering the glyph itself brightens it to full and to the accent colour. A run in flight dims it further rather than erasing it, so nothing on the row moves while a gate runs. Its hit target is the slot, never the row, so the label and the duration stay a plain row click, and its title names what it runs, with a plural that agrees with the count. A guard in `tests/hub-css-invariants.test.ts` reads the sheet as data and holds three things: no rule hides the run slot at rest in any state, nothing reveals it on an ancestor hover, and no hover rule on the row changes a box property. It ships with the shape that shipped as its bad sample, watched red against the real sheet first.
321
+
322
+ - **The by type tab files a check by WHOSE it is, and the archive stops padding the counts.** The tab had grown one level under each check type for where it runs, which the row already answers in its `runs in` slot and the line above the tree already counts, so the level is gone and a kind holds its rows directly. What it groups by instead is OWNERSHIP, the distinction the bare words `rule` and `check` never carried: `built-in rules` (the SK modules that ship with shipkit), `repo rules` (the ones a repo registers through `rules: []`), `custom checks` (the `*.check.ts` files the repo wrote) and `tasks`. The gate inventory carries that answer now, one `builtin` field read straight through to the check row: true for a shipped rule, false for one a repo registers and for a check file it owns, null on a task, whose set resolves from the floor and the repo config together. The `(?)` legend says what each one IS in one sentence, since "rule versus check" was readable as a name and not as a difference: a rule lints one repo shape concern on every run it is placed in and often fixes it too, a check is a probe the repo owns, and the two are gate checks with the same page and the same switch. **A done ask leaves the open list**: the `asks/done/` archive is a `done` sub group in BOTH views, folded by default and left folded while a live search yields every other fold, so an ask folder counts the OPEN asks (9 rather than 25) while the archive counts its own, and a done row reads dim under its own mark. A done ask reaches `shipkit needs` no more than before: it carries no severity, which is the one thing the feed reads.
323
+
324
+ - **A run can be handed the gate binary its answers are keyed on.** Toolchain identity content-hashes the directory the cli entry lives in, and three call sites each built that path from the module running them, so every replay a run makes is a bet that nobody writes a byte under `src/` while it runs. That is fine for a real gate, where an edit to shipkit SHOULD move every key, and wrong for the ledger suite, whose whole subject is a count of how many times a task really ran: a second agent editing this checkout mid suite moved the key between two gates of one test, the second gate re-measured, and the suite read as flaky. Reproduced: `--only narrowed to a replayed task` re-measured in a run whose window carried three writes under `src/` from another session. `runCi` and `collectChecks` take a `cliEntry` now, defaulting to `gateBinary()`, which is the one owner of that path; the ledger suite hands in a stand-in created once per run, and a test pins the contract by moving that stand-in and watching the next gate re-measure, with the untouched case beside it replaying.
325
+
326
+ - **The full tier ceiling reaches every file, because a preload could only reach the first one.** A full tier run gives each test 120s: its suites build real repositories and spawn whole gates, and the same file has finished in 6.8s and in 21.3s on identical code depending on machine load, so bun's 5s default turns load into a red release gate. That ceiling was set by a `setDefaultTimeout` call in the test preload, and a preload reaches the FIRST file of a run and nothing after it, so 106 of this repo's 107 suites ran at 5s while the preload said otherwise. Measured: a 6s test passes as file one and times out at 5000ms as file two. The runner passes `--timeout` instead, which covers every file, in the full tier only, and never when the repo's own `bun test` arguments already name one. The quick tier is untouched: a suite there is under a second and a hang must fail fast. What the missing ceiling cost: a release fixture test timed out mid-run, its `afterEach` deleted the repository under the work still running, and the orphaned assertions then failed against a fixture that no longer existed, which reads as a mismatch in a test that was merely slow.
327
+
328
+ - **SK014 stops blocking a semicolon.** The class moves from the hard tier to the soft one, so it WARNs like passive voice and sentence length and never fails a gate. Blocking it was the standard taken to an extreme: the join carries meaning, and a gate that refuses it forces a rewrite of prose nobody found unclear. `vague` is the only hard class left, which is the one repair a machine can demand without changing what a sentence says. The preference stays on the page and in the report, so a writer still reads "write two sentences". The table row exemption is untouched.
329
+
330
+ - **A rule's fix pointer follows its documentation.** SK010 now points at `build/shipkit/units.md`, SK011 at `build/shipkit/built-in-checks.md`, SK012 and SK015 at `build/shipkit/config-keys.md`, because the page that states an invariant is the page a reader lands on. The scaffolded `repo.config.ts` closes with the line that answers "what else can this file carry", `shipkit config-keys`, and its `blockedPaths` comment says what the key is for instead of teaching one project's folders. The paths stay: a scaffold shipping an empty list teaches nothing about the shape.
331
+
332
+ - **A declared slow suite sits at its file, says where it runs, and has a page.** Its row was filed under an invented `health/release-unit/<dir>/` folder, so a suite with a real file on disk appeared in a directory that does not exist; it sits at that file now, with `file` set and `virtual` gone, and the only virtual case left is a declaration naming a file nobody wrote. Its note read `slow`, the config keyword rather than an answer, and reads `release only`. And its page said "this row is not a gate unit" and stopped, which made every suite row a dead end: a suite has its own page now, with what it is and where it was declared, `catches` and `why not fast` as the two answers `defineRepo` demands, and release only with the button back to the quick tier, offered for a ROOT declaration only, since the managed block owns the root `tests` key and a release unit's own entry must be edited where it lives. Honest gap: no per suite timing is persisted anywhere, so the page states the quick tier BUDGET it broke rather than inventing a duration.
333
+ - **`shipkit config-keys`: the repo config surface documents itself.** Every `repo.config.ts` key and every key nested under one, rendered from the JSDoc above the field into markdown, json, or a marked block inside a docs page (`--out`, `--check`, the shape `shipkit commands` already uses). `--undocumented` names the keys that carry no doc comment and exits non zero, because a key nobody documented is a key nobody uses. `src/core/config-keys.ts` is the scanner: it follows an import to the type a field names and expands an inline object, and it follows only a bare identifier, since a scanner guessing its way into a type documents the wrong shape. Every field of `RepoConfig` and of the types under it now carries a doc comment, the ones that never had one included: `logDir`, `untrackGenerated`, `contracts.extra`, `creds`, and a task's `env`, `cwd` and `group`.
334
+
335
+ - **A secret and a graph say what they are, in colour.** Both kinds carried a glyph and a hue already and no state at all, so a present secret and a clean graph read the same plain white as a row nobody measured. A SECRET is green when it is present AND the last run carries a wire line naming it as resolved, orange when present with that probe failed or absent, red when a page requires it and it is not there. Only the failed probe carries a `severity`: "nobody probed this" is a colour on the tree, never an item asking for hands, and a feed full of silent yellows is the progress spam the feed exists to avoid. The row says which case it is in words, so the colour is not the only clue. The probe result is CARRIED now, off the `STATUS.md` wire lines that name the secret as a WHOLE token, so a line about `rv/npm/token` says nothing about `rv/npm/token-2`. A GRAPH is green when its lint is clean, orange with its orphan and dangling counts, and plain when it carries no lint, which is a base wiki another repo owns and must never be double judged here. The five colour rules live in one file, `src/collections/state-rules.ts`, one pure function each, with the asset rule moved in beside them.
336
+ - **A repo's pending reflections are a resource kind.** The `.shipkit/reflections/` queue was readable only from the repo that OWNS the wiki, whose harvest check scans every repo beside it, so the repo that logged the friction had no surface showing what it was still owed. Every pending `*.md` is a `reflection` row now, at its own file path, named by its first heading, with `request` or `note` in the note slot and its own glyph and hue in the one kind mapping. The feed line names the file and the shape, and a change request missing sections lists them the way the harvest check does. Always YELLOW: the file waits on a human process and nothing is broken, so it reaches `shipkit needs`, reads orange on the tree, and never reaches the pings, which fire on red only. `README.md` is the scaffold's own seed and never a row, so a repo with only the seed shows nothing. The six section headings that tell a change request from a plain note live here now (`src/reflections/reflections-queue.ts`) rather than in one consumer, and the resource KIND list is ONE array (`RESOURCE_TYPES`) that the wire types re-export, since a kind known on one side of the wire only is how a new row arrives with a hole where its glyph should be.
337
+ - **The hub design pass, round two: the glyph on the right, one duration unit, a check page of widgets.** The state glyph sat between the run action and the duration, so the widest column on the row pushed the one character that says what a row IS. The slots read name, run, duration, note, then the glyph on the right edge, and a guard reads the page source as data and fails an order whose last slot is not the glyph. Every row also carries its KIND in its own slot after the caret, one glyph and one hue per kind, where the hue is the kind's identity and never a state, with a `(?)` legend in the tree header naming each glyph. The fold marker is an 11px chevron drawn inline, turning rather than swapping glyph, with the whole first column as its hit target. **ONE duration unit**: milliseconds everywhere, grouped by a thin space, so `6 600ms`, `129ms` and `80ms` compare by eye, coloured by one table (green under 500ms, plain under 2000ms, orange under 10000ms, red above it or past half the check's kill time) that the tree row and the check page both read. **The check page is widgets**: a header with the name, the kind and a run button, then WHAT, RUNS IN, TIMINGS, CACHE and LAST OUTPUT, each answering one question, with no back link since the tree is the navigation. TIMINGS draws the last sixteen outcomes as a bar row coloured by verdict, reds and replays included, which an average hides. CACHE speaks the key instead of quoting it, so `cache content keyed on .` reads `keyed on the whole repo tree, so any change re-runs it`. **A reusable `(?)`**, one component and one css block, instant on hover and on focus, carries every term nobody was born knowing. **Clicking run moves nothing**: the live strip floats over the stage instead of mounting into the flow, the run button holds its width and wears a spinner, and the output widget streams the wire lines as they arrive and scrolls itself. **What each colour may mean is one rule set**: a folded folder wears the worst state under it with ok outranking idle (a folder of passing checks read white), an open ask is orange with its severity untouched so it still reaches needs and still pings once, and an asset is green once it has a pick. **One clock**: a second tick on the root store, read only by a small TimeAgo component, so an age moves without repainting the page.
338
+ - **The third `when` value is `health`, not `check`.** `check` was the name of a command rather than a meaning, so the switch offered a word nobody could read. `health` is the health pass (`shipkit check`), never a gate and never a commit gate, which is what that value always did. There is no alias: the key landed the same day, so `defineRepo` refuses a config still saying `check` with a message naming `health`. The switch also stops narrating itself: the current value is the pressed button, a chip beside it names the declaration that put it there, and each of the four words carries its own `(?)`.
339
+ - **A comfy panel that cannot answer a browser is refused, never mounted.** comfy-ts serves its web bundle on `GET /` from 2.5 on, and an older one answers the json index to every request, browsers included. The identity probe reads exactly that json, so the hub adopted an old panel as live and embedded it, and the frame showed the raw index (server, usage, workflows, vars) with the chip stuck on starting, while nothing on screen said the installed comfy-ts was too old. `ensure()` probes `GET /` with `accept: text/html` after the identity check now, and requires a `text/html` content type; `spawn()` runs the same check once the port answers and kills the process it just started, rather than leaving it on the port for the next adoption. A json answer is one loud error naming the version in the repo's own `node_modules` and the cure: `comfy-ts 2.4.2 serves no web panel (needs 2.5 or later), run: bun add comfy-ts@latest`. The fake panel the tests use answers html or json on demand, so a json only server is proved refused on both paths, with a control that an html answering server still mounts.
340
+ - **A hub row is SLOTS, the theme is graphite, and a run is something you watch.** A tree row said what it was and what it last did in one line of prose, with an em dash and the words "passes" and "fails, does not block" strung after the name and a duration that lined up with nothing. Every row is a fixed grid now: the indent rides in the first column and the label eats the slack, so the state glyph, the duration in tabular figures and one short note line up down the whole column at any depth. One glyph per state, the same everywhere, and a failure that cannot block is that fail glyph in a muted tone rather than a phrase. The words move to the row tooltip and the check page. The note slot is filled for every kind that has something to say: an ask its kind, a secret present or missing, a post its venue, an asset its roll count, a graph its issue count, a suite the word slow, a check where it runs, a folder how many rows it holds and how many are red. Two optional instance fields carry it, `label` (the tree label when the feed line is not one) and `slot`, so `text` is untouched and every terminal surface keeps the sentence it prints. **The theme drops the brown and the yellow**: near black surfaces, light grey ink, muted grey secondary text, one desaturated blue for selection, focus and the primary action, and red, orange and green reserved for state. Every colour in the sheet comes from a `:root` token, scrims and shadows included, guarded by a test that fails a colour literal outside that block and a chrome token inside the yellow and brown band, samples shipped. **Buttons are small and flat**: one pixel border on the panel colour, a 12px lowercase label, the accent as a border and an ink colour and never a fill, with the severity filter and the tree tabs reading as underlines rather than filled pills. **Running is visible**: `run checks` and `run the gate` sit over the tree, every row covering gate checks carries a run action that appears under the pointer (on a folder, everything beneath it), a running check pulses and each row recolours as its result lands, and a strip at the top of the stage reads `running 12 of 70 · 3 red so far` and clears itself when the run is over. No second mechanism: `POST /api/check/run` gained `gate: true`, the check engine tap now reaches `runCi`, so one narrowed run can cover tasks and checks together, and the strip state is a pure reducer over the same stream.
341
+ - **One `when` key says where every check runs, and the hub writes it.** Whether a check runs, and where, used to be answered in three unrelated places with three vocabularies: `ruleExempt` for a rule, `optIn` for a task, `export const gate` inside a check file. Nothing could read the three together, so no surface could show them on one row and nothing could write one of them. `repo.config.ts when: { '<check id>': '<value>' }` is that one declaration, keyed by check id, with the four words the page shows: `always` (every gate and every `shipkit check`), `release` (out of the quick gate, in at `--full` and on the release path), `health` (the health pass `shipkit check` only, never a gate), and `off:<reason>` with the reason mandatory. `defineRepo` refuses a value nobody defined, an `off` with a blank reason, and a rule id no rule carries; the other two namespaces are not knowable at config load, since a task is resolved from the floor and a check is discovered on disk, so the gate reports a `when` naming no check as a FAIL and exits non zero, because a switch wired to nothing must never be quiet. A check set `off` is skipped BY NAME with its reason, reports as a SKIP rather than as a green nobody measured, and never enters the store. Checks parked out of one gate run share ONE line naming how many and which, because twenty four skip lines is the wall of text the hub exists to replace. A task named in `--with` or `--only` survives the TIER filter, which is how an opt in task has always been asked for, and never an `off`: one word means never on all three surfaces. A parked or off task reports through the per task tap and the runs log the way a parked check does, so its row goes quiet rather than keeping the verdict it had before it left the gate. A floor slot turned off with `defaults: { <id>: false }` reads as off too, with `defaults.<id>: false` as its reason, so it keeps a row instead of disappearing before it is a check. Where a check runs is part of a rule's cache identity, so moving one, or editing the reason it is off, asks the question again instead of replaying the answer from before the move. **The two older spellings still load and are READ into `when`, and they are DEPRECATED: they die when the fleet is past 0.40.** `ruleExempt` reads as `off` with its reason, a task `optIn` reads as `health`, and a check file `gate = false` reads as `health`; an explicit `when` wins over all three. `release.skipTasks` and `tests.slow` stay as they are, since one is the opposite axis and the other is suite granularity inside one task.
342
+ - **The switch writes `repo.config.ts` itself, through a managed block.** There is no shadow store beside the file: the config is the one owner of what runs in a repo, and a second copy would rot the first time somebody edited the file by hand. The block sits between `// >>> shipkit gate` and `// <<< shipkit gate`, the pattern rule SK004 already uses inside `.gitignore` and `shipkit commands --out` uses inside a docs page, and it owns `when` and `tests`. The writer refuses a file that moved since the page read it (every read and every write carries the file's hash), a block that stopped matching what the config loads, and a `when` or `tests` declared outside the markers, since a second declaration is a duplicate key rather than a merge. It validates the value the way `defineRepo` validates it before anything reaches disk, then reads the result back through the SAME loader the cli uses, BOTH keys it owns and not just `when`, and restores the previous bytes when either does not round trip, which needed a cache busting import: an ES module is cached by path, so the process that wrote the file was reading the version from before its own write. No external formatter runs over the file, because a repo scopes its formatter to its source tree and a pass here would land a whole file diff for a one line toggle. The file is left DIRTY on purpose: a toggle is a change its owner reviews in a git client, never a commit an agent made on their behalf, and the page says so with the path. `GET` and `POST /api/checks/when` are one endpoint over one writer. The hub re-reads `repo.config.ts` whenever its bytes move, so a toggle reaches the check rows without a restart, while the kit, the graphs, the studio and the comfy panel stay bound to the process that mounted them.
343
+ - **Always versus release, visible without opening a row.** One line above the tree counts the checks that are always, release, health and off. The by type tab adds ONE level under each check type, the value itself, because answering that question by opening rows one at a time was not answering it. Declared slow suites are rows of their own, AT THE TEST FILE. A suite is not a check: it is suite granularity inside one task, and its declaration demands a written case that a four value switch cannot invent, so the tree SHOWS it rather than folding it into `when`. Its note reads `release only`, and it has its own page carrying `catches`, `whyNotFast`, the declaration site and the way back to the quick tier. Under the `test` check a form adds one, demanding both answers exactly as `defineRepo` does, and a button drops one back to the quick tier.
344
+
345
+ - **Every check, rule and task is its own entry, with its own verdict and its own timings.** Repo health was the text of `STATUS.md` parsed back into one row per wire line, so seventy checks shared one destination, an instance id was a whole sentence, and nothing could be opened, timed or addressed on its own. The gate inventory is a collection now: one resource per check, `check:<kind>:<id>`, typed `task`, `rule` or `check`, filed at the check's own file (a check IS a file; a rule sits under its wiki page when that page is inside the repo) and under `health/<kind>/<id>` when it has none, which is every task, since a task is a command. Each entry carries what it declared (gate or proactive, blocks or warns, where it runs today, its cache population, its timeout ceiling, what it waits on) and what it costs (last verdict with a stamp, last duration, average over N measured runs, slowest, how many went red). `GET /api/checks.json` serves the same rows for a terminal. A check's page adds the lines it printed last time with their fix pointers and one button that runs THAT check now, through the gate for a task and through the check engine for a rule or a check. **A failure that cannot block is now told apart from a warning**: the engine demotes a non blocking FAIL to a warn in the LINES, so every surface built on the wire showed a failed advisory check as a warning; the entry reads the real outcome, says "fails, does not block", and stays yellow in the needs feed so it never pings a phone at night. The check run happens IN THE SERVER's process: `POST /api/check/run` streams NDJSON, one event per wire line and one per check as it finishes, so a row recolours the moment its check is done, and the shelled check command is deleted. `STATUS.md` keeps exactly one node, the raw text of the last run. A selection reaches the url as `#item:<collection>:<id>`, so a reload or a copied link lands on what is on screen, and the media forms (`#<assetId>?tools=`) are untouched.
346
+ - **Honest timings, in one sqlite db: what every run COST, beside the store that holds what it ANSWERED.** The ledger stores a GREEN only, by design, so every average it could offer was an average of the runs that passed, and a discovered check never enters it at all (its answer lives in the check TTL cache), so nothing could time a check however often it ran. `shipkit.sqlite` sits beside the store under the git common dir, outside the work tree, opened with `bun:sqlite` in WAL mode with a busy timeout, its schema created by one versioned migration on first open. It holds one row per check per run (`{ runId, id, kind, at, ms, status, replayed }`) written by the ci gate and by `shipkit check`, plus each check's last wire lines so a check page can say why it is red without re-running it, upserted per check so a `--only` run never erases the other sixty nine. A whole run is ONE transaction, so several gates in one checkout interleave runs and never rows: six processes writing at once land every row whole, where the same storm against a db opened without WAL and without a busy timeout keeps 250 of 1500. The age sweep is a DELETE inside that transaction rather than a rewrite of the whole file, which is the defect it retires: the jsonl this replaces swept itself by rewriting, and every row appended during the rewrite was lost. It is NEVER read by the gate, so a corrupt, locked or unwritable db costs a run its history and one WARN line, never its verdict, and a location that cannot be resolved at all collects rows and writes nothing rather than throwing. One run id per gate makes the wall time reconstructable as a breakdown, which nothing in the store could do. A replay is the last verdict and never a sample, because it measured nothing. Stored greens count as history only while they predate that check's own first row, so one measurement never lands in the average twice. Run history from before the db is not carried over: the files an earlier checkout wrote were never released, and a leftover one under the git common dir is ignored.
347
+ - **The ping state file survives an id scheme change.** Ping dedup marks are keyed by the instance id, so moving repo health from wire line text to check ids would have re fired every red already acknowledged, at once, on his phone. `pinged.json` carries a version: a bare array (the pre check shape), a version this build does not know, or an unreadable file all put the tick in ADOPT mode, where every machine owned red is marked and nothing is notified. One silent tick, then normal service, and a red that appears after it still pings. An ask is deliberately outside it, since its once ever marker lives in the ask file and no id move can invalidate it.
348
+ - **`a`, `n` and `r` work on an asset page too.** The atelier consumed `a` for the alt verb while an asset was open, which is most of the time, so the all filter was bound in both places and worked in neither. Alt is `shift+A` now, the binding the help line already printed.
349
+
350
+ - **The hub is ONE tree of every repo resource, by path or by type.** Every provider now projects into one shape carrying a `type` (the resource KIND, never the collection: ask, secret, asset, post, workflow, graph, contract, plus health until each gate check is its own resource) and a `path` (the repo-relative truth file where one exists, else a virtual path under a root named for the kind, with `virtual: true` saying so rather than sending a reader hunting for a file). `/api/resources.json` serves that flat array plus the collection metadata, and the page groups the SAME array twice, by path as the default and by type beside it, so the two tabs can never disagree. `title` and `state` are derived server side in one place instead of by seven providers. A folder holding several kinds shows them on sibling rows, which is what pathing buys over listing per collection: assets moved off the single kit file they all collided on and onto their own roll workspace dir, and a secret's already slash shaped name groups it by vendor for free. Colour rides on the LABEL text and the severity dots are deleted, class and call site: red broken, orange taste pending or a default in use, green MEASURED passing which nothing unmeasured may claim, white the neutral default, dim archived or out of play. A directory wears the worst state under it, so a collapsed branch cannot hide a red. The tree takes the arrows while it has the focus, right and left expand and collapse, enter opens, and the atelier keeps its own roll keys. The tab and the expansion set persist to localStorage as newline separated paths, every access guarded so a private window cannot take the tree down. The filter and the search apply BEFORE the grouping, so a filtered tree prunes to the branches that still hold something. `/api/collections.json` still answers, for a page opened before the change and still on screen. The icon rail is deleted with its column: its one job, scroll to a section and unfold it, is what the tree does by existing.
351
+ - **One spacing scale in `hub.css`, and the shortcuts are printed on the control.** About forty hand picked padding values became `--s0..--s4` (2, 4, 8, 12, 16px) declared once on `:root`, with every padding, margin and gap in the sheet a value from it or 0, so the same screen holds noticeably more rows. A guard reads the sheet as data beside the legs stacking invariant and ships with the two samples it was watched against, a bad block whose off scale values it must name and a good block of sizes, radii, offsets and a `scroll-margin` it must leave alone, because a guard that inspects nothing is green too. `a`, `n` and `r` set the all, needs and red filter from anywhere outside an input, with the letter dim inside its own button so the binding needs no legend, `/` focuses the search box, and `?f=needs` and `?f=red` keep working as the shareable addresses.
352
+
353
+ - **The hub says when its page cannot refresh itself.** The tab keeps up with an edit through bun's hot reload client, which rides in the served page: it swaps a hub-app or css edit in within a second, and reconnects across the process restart a source edit triggers under `bun --watch`, reloading the tab and keeping its query string. Measured against a real headless chromium on `/?f=red`: a `serve-hub.ts` edit, a `repo.config.ts` edit and a `hub.css` edit each reached the tab with no reload key, and a data change on disk arrived through the ten second collections poll. Bun puts that client on the page only in development mode, which it derives from NODE_ENV at process start, so an ambient `NODE_ENV=production` serves a hub that renders perfectly and can never refresh itself: every change then needs a reload key, and no line anywhere says why. `serveHub` returns `liveReload` now, and `shipkit serve` prints a WARN naming NODE_ENV and the one thing that fixes it. Forcing `development: { hmr: true }` was tried and rejected: under that same env bun serves the dev JSX runtime against a production react resolution and the page dies on `jsxDEV is not a function`, trading a stale page for a white one. The flag is pinned against the real served bytes by two child processes, one per NODE_ENV, so it cannot drift from what bun actually puts on the page.
354
+ - **`restart panel` now restarts the panel it ADOPTED, which is the only panel there usually is.** The hub runs under `bun --watch`, so every lib edit restarts the hub while the comfy-ts child outlives it and the next hub adopts that process. `restart()` killed only a child it had spawned itself, so on that path it killed nothing, `ensure()` re-adopted the same old process, and an edited `.cflow.ts` module never reached the panel: the exact no-op the port wait exists to prevent, reported as a success. An adopted panel is killed by pid now (`lsof -ti tcp:<port> -sTCP:LISTEN`, listeners only and never this process), after its identity is verified a second time, and a port no other process listens on is a loud error naming the port and the command that clears it. Two more from the same reading. A live panel serving this repo's own files that simply booted BEFORE a module was written was refused as `port N is taken by another server`, which sends the reader to change `comfyPort` and moves a working panel for nothing: that refusal names the missing modules now and points at `restart panel`, while a foreign server (another repo, another program) keeps the port sentence. And SK004's verify pass probed only the patterns the block claims IGNORED, so the `!.comfy-ts/drafts/` re-inclusion was measured by nothing: a repo-owned `.comfy-ts/` line below the block, the shape every repo wired before the drafts policy carries, re-ignores the drafts folder and the rule stayed green over a repo whose drafts can never be committed. Every `!` pattern is probed too now, expecting NOT ignored, with the same `check-ignore -v` fix pointer.
355
+ - **The STE checker stops reporting markdown STRUCTURE as prose, and SK014 stops reading a scope that matches nothing as green.** Three precision defects from the field, all of them noise a writer cannot act on. A semicolon inside a table ROW is no longer a `semicolon` hit: a cell is not a sentence, so "write two sentences" asks for the table to be broken, while the cell's own items are already comma separated and the semicolon is the only separator left. On one repo 216 of 671 blocking hits were cells, burying the 345 real ones. The `paragraph` limit now takes a LIST ITEM as a paragraph boundary, both numbered forms (`1.` and `1)`) at any indentation, so a run of items with no blank line between them is measured per item instead of as one block: a five item list reported 38 sentences, a number no rewrite can bring down because every item is already short. Prose above a list is closed by it too. A table's rows were already a boundary and stay one. And rule SK014 WARNs, naming the globs, when `prose.include` is declared and resolves to zero files, where it printed the same green a clean repo gets: a repo that asked for the gate and inspects nothing is the one case silence cannot be right for. `shipkit prose` printed `ok: 0 file(s) STE-clean` on that same repo, so the command a human runs carries the same WARN and the same fix line. A semicolon in a bullet, a semicolon in a prose line that merely contains a pipe, and a real seven sentence paragraph with no list marker ship as controls, so a later green can never mean the classes stopped inspecting. Measured over this wiki's own prose scope: semicolon 5 to 3, every other class unchanged.
356
+ - **A task's `timeoutMs` ceiling kills the whole process TREE, and the fail line says where the ceiling lives.** In the field a gate sat on its test task until somebody pressed ctrl+c: one suite hung inside a sync loop, so bun's per test timeout never fired, and the ceiling that did fire signalled the wrapper alone, leaving `bun test` orphaned at 100 percent CPU. `Bun.spawn` offers no process group to signal, so the timeout takes one `ps -Ao pid=,ppid=` snapshot and walks the child's descendants BEFORE it signals anything: a survivor is reparented the moment its parent dies, which is also why the SIGKILL pass reuses the list the SIGTERM pass captured instead of walking again. Deepest first, then the child itself through its own handle, SIGKILL after the same 2s grace. The bail and SIGINT paths orphaned a grandchild the same way and now take the same walk. The fail line reads `timeout: test killed at its 600.0s ceiling, raise timeoutMs on the task in repo.config.ts`, where it used to say `timeout after 600000ms` and name neither the task nor the key that moves it. The suite runner deliberately grows no ceiling of its own: it is spawned BY a task, so the task ceiling now reaches the workers it spawns.
357
+ - **SK005 self-updates with the package manager the repo's own lock files name.** The rule ran `bun update --latest` in every repo. On a consumer whose deploy installs with `npm ci`, that moved `package.json` onto the new range and wrote a `bun.lock`, while `package-lock.json` stayed pinned to the version before the bump: `npm ci` refuses a lock that disagrees with the manifest, so the next push died at install with every local gate green. The manager is now read off the lock files at the root (`src/core/package-manager.ts`): `package-lock.json` is npm, where `npm install <name>@latest` moves the manifest and the lock in one call, `bun.lock` or `bun.lockb` is bun, `pnpm-lock.yaml` is pnpm, and `yarn.lock` is yarn (classic `upgrade`, or berry `up` when a `.yarnrc.yml` sits beside it). The locks git TRACKS decide, because an untracked `bun.lock` is precisely what a wrong update leaves behind, and reading it as the repo's choice would make that first mistake permanent. Nothing tracked, or no git at all, falls back to what sits on disk, and no lock anywhere stays bun, the house default. Several locks owned at one root is the ambiguous case: bun wins, and the run WARNs, naming the ones that will drift. The git dirty probe guarding the write, the frozen hint and the `not installed` line all carry the detected manager's own commands, so the guard watches the lock the repo actually writes. The OTHER self-update layer, the one that upgrades `rvlib-shipkit` itself and re-execs before an interactive `ci` or `check` dispatches, goes through the same detection: it runs first, so leaving it on bun would have kept the same broken pair landing in an npm repo. `shipkit fleet --update` builds its hand-run fallback line the same way, so the command it prints for a repo it could not update through that repo's own gate does not recreate the bug by hand.
358
+ - **A bare `shipkit start` writes the two-line CLAUDE.md on its own.** The scaffold wrote `agent/index.md` on every start, and only a caller passing `--include` got a CLAUDE.md routing it, so a repo started with the bare verb was born failing SK003 (`agent/index.md exists but there is no CLAUDE.md routing it`) against an autofix that refuses to create the file. `start <name>` is the one command anyone types, and a repo without the house wiki in its context is not a started repo, so the house template now lives in the lib (`HOUSE_INCLUDE`, `HOUSE_ALSO_OK` and `HOUSE_SKIP` in `src/start-repo.ts`): the base wiki include then `@./agent/index.md`, with `@./CLAUDE.local.md` standing in for line 1 on a public repo, and the one day-job checkout never wired. `--include`, `--also-ok` and `--skip` replace those defaults for another house. Independently of the house, `@./agent/index.md` is routed, last, whenever the repo has that file after the scaffold. A base line that arrives after a one-line CLAUDE.md is inserted BEFORE the agent route, where it used to land after it and invert the order SK003 checks. CLAUDE.md is written after the scaffold and before the publish, so it still rides inside the first commit. The `.claude/` policy files land with the house lines, so on every bare start. The house applies only where its index is actually on disk, since this lib is published: a bare start on a machine that has no house wiki writes the agent route alone, no policy files, and one line naming the path it looked for.
359
+ - **An asset page tunes its own workflow, in a popup, on its KIND's draft.** The asset's tools row carries a `tune` button: it starts the panel if nothing started it, opens it over the asset instead of navigating away, and asks it for the draft of that asset's kind (`.comfy-ts/drafts/<workflowId>/<kindId>.json`, one file shared by every asset of that kind) rather than `default`, which is the draft no asset roll ever reads. The asset's own prompt and seed are applied on top of that draft at run time. The asset, its rolls and its prompt stay on screen behind the popup while a value is tried. The panel's own selection message is followed everywhere EXCEPT here, because following it over an asset page would move him off the asset he opened it from.
360
+ - **The hub carries a comfy section: one click opens a workflow's own control panel and runs a test generation.** A repo holding `.shipkit/comfy/workflows/*.cflow.ts` modules gets a row per module in the sidebar, and selecting one embeds comfy-ts's own web panel in the stage: every `v.*` var as a form control, a generate button, the gallery. The panel is a CHILD PROCESS (`comfy-ts serve`, from the repo's own `node_modules/.bin/comfy-ts`, under bun because a workflow module is TypeScript) started on the first click and killed when the hub stops. Its port is `serve: { comfyPort }`, defaulting to the hub port plus 1000. A server already on that port is adopted only when it serves this repo's own module FILES, because two repos both serving a `txt2img-cutout` is the normal case and tuning the wrong one writes another repo's draft. Nothing mounts, and no process starts, in a repo with no module.
361
+ - **The header states what the frame cannot be asked.** The panel is cross-origin, so the chip reads `live` only on the panel's own `ready` over the documented postMessage host protocol, `failed` on its boot error, and `no handshake` when a panel serves a working ui and speaks no protocol. A switch made inside the panel moves the hub's selected row, and a switch made in the hub moves the panel by message instead of re-pointing the frame, which would throw away its gallery. The box those workflows generate on is probed separately (`/system_stats` on the `.shipkit/comfy/config.json` port), and an unreachable one is stated beside the panel rather than discovered when a run fails.
362
+ - **`restart panel` sits in that header because `comfy-ts serve` imports every module at boot.** An edited `.cflow.ts` reaches a panel that has been up since before the edit through nothing else. The restart waits for the killed port to go quiet before starting, since a dying server still answers for a beat and adopting it would turn the restart into a no-op.
363
+
1
364
  ## 0.38.1
2
365
 
3
366
  - **The suite runner no longer refuses the test arguments of a repo with no `tests` block.** The floor spawns `bun suite-cli.ts [flags] -- <bun test args>`, and bun swallows a LEADING `--`. With no declaration there are no flags before it, so the runner saw `src/ tests/` bare and answered `unknown flag src/`, and a `bun test <args>` script with no `tests` block had a dead test slot on 0.38.0. The runner's own flags now end at `--` or at the first token that is not one of them. Pinned by a test fed both shapes, with a `bun test` flag and a separated declaration as controls.
@@ -20,13 +383,13 @@
20
383
  - **What this release deliberately does NOT do is auto-declare `['.']` for those slots**, and the reason is worth more than the speed. `['.']` is not the work tree, it is the TRACKED tree: `git-scan` force-adds only a path a declaration NAMES, so an ignored path enters a key by NAME and never by content. Every floor task reads ignored content in an ordinary repo, a suite through its fixtures and its local database, tsc and the two graph walks through generated types and `node_modules`. Measured on a scratch repo: warm the ledger with two green runs, break a gitignored file the suite reads, and the third gate prints `ok: test (replayed, 0.0s saved)` over a suite that exits 1. `tests/floor-cache-safety.test.ts` holds that repro, watched fail, with the declaring lookalike beside it still replaying. Speed here is bought by a REPO declaring what its own tasks read, which is the only declaration with a real hit rate anyway.
21
384
  - Two doc sentences that argued the other way are corrected where they live: `TaskDef.inputs` no longer says `['.']` "cannot under declare", and the `git-key` header no longer promises the never-cached default is the whole safety story without naming who can declare on a unit's behalf.
22
385
 
23
- - **SK004 asks git what is ignored, instead of trusting the text it wrote.** The rule compared the managed block to the expected string and stopped there, so it reported `current` over a repo where git ignored none of what the block claims. Two ways that happens, both from the field. A bare `printf 'work/\n' >> .gitignore` onto a file whose last line carried no terminator fuses the two into `sourcework/`, which ignores neither path, and the file ends with a newline afterwards so nothing textual looks wrong (cost in that repo: 1.1GB and 700 screenshots pushed across four commits, a rejected push, and a `git filter-repo` rewrite). And a negation below the block is the DOCUMENTED override, so nothing stopped one landing on `.rv-private/`, the directory holding the keyword list the leak guard itself reads. Every policy pattern is now probed through one `git check-ignore --stdin` call and a contradiction FAILs, naming the pattern and the `check-ignore -v` that reports the winning line. The three venue folders stay exempt, because unignoring one is how a live listing ships. A `.gitignore` that does not end with a newline is now a FAIL of its own, since that missing byte is the trap rather than a cosmetic defect, and `--fix` terminates the file even when the block is current. Outside a git repo the rule states the block went unverified rather than reporting a green nothing measured.
386
+ - **SK004 asks git what is ignored, instead of trusting the text it wrote.** The rule compared the managed block to the expected string and stopped there, so it reported `current` over a repo where git ignored none of what the block claims. Two ways that happens, both from the field. A bare `printf 'work/\n' >> .gitignore` onto a file whose last line carried no terminator fuses the two into `sourcework/`, which ignores neither path, and the file ends with a newline afterwards so nothing textual looks wrong (cost in that repo: 1.1GB and 700 screenshots pushed across four commits, a rejected push, and a `git filter-repo` rewrite). And a negation below the block is the DOCUMENTED override, so nothing stopped one landing on `.shipkit/private/`, the directory holding the keyword list the leak guard itself reads. Every policy pattern is now probed through one `git check-ignore --stdin` call and a contradiction FAILs, naming the pattern and the `check-ignore -v` that reports the winning line. The three venue folders stay exempt, because unignoring one is how a live listing ships. A `.gitignore` that does not end with a newline is now a FAIL of its own, since that missing byte is the trap rather than a cosmetic defect, and `--fix` terminates the file even when the block is current. Outside a git repo the rule states the block went unverified rather than reporting a green nothing measured.
24
387
 
25
388
  - **A check declaration the gate's own formatter wrote was unreadable to the gate.** `BLOCKING_RE` and `GATE_RE` both ended in `\s*$`, and oxfmt (which the fix pass RUNS) terminates every statement with a semicolon. So `shipkit ci` reformatted `export const blocking = false` into `export const blocking = false;`, then failed the file with `does not declare whether it blocks` and printed as the fix the exact line it had just written. Two repos in the field shipped `// oxfmt-ignore` workarounds to get a green gate. The worse half was silent: a `export const gate = false;` was not read either, so a world probe that had opted out of the gate quietly re-entered it, and a repo could declare fifteen off-gate probes and watch all fifteen run. Both readers now accept an optional `as const` and an optional semicolon through one shared tail. `INPUTS_RE`, `TTL_RE` and `NEVER_RE` never anchored the line end, so they were never affected. Pinned by three tests: the formatted shapes, the gate opt-out in both spellings, and a CONTROL that the unterminated shapes and a silent file still read as before.
26
389
 
27
390
  ## 0.37.0
28
391
 
29
- - **The leak guard grew an allowlist, because it had no way to be wrong.** Every keyword matched as a case-insensitive substring over the staged added lines, so a four letter row landed inside a real npm dependency name and inside a base64 integrity hash in `bun.lock`, and that lockfile became uncommittable forever with no per repo override of any kind. The only way past was `--no-verify`, which disarms every other gate too, so the guard was training people to switch it off. Two answers ship, in this order. A `w:<word>` keyword row now matches on word boundaries spelled in unicode rather than with `\b`, which is ascii only and would let an accented name match inside a longer accented word: that makes the PATTERN smarter rather than the guard looser, and it is opt in, so a keyword file that carries no `w:` row behaves exactly as before. Then `repo.config.ts hooks.leakAllow` (with the machine-global twin `~/.rv-private/leak-allow.json`) declares a known-good match: `paths` names the files, `patterns` names the keywords, and `inHashes` narrows to a match that falls inside a content hash, where the bytes are an address rather than prose. `paths` plus `patterns` is the shape to reach for, because it waives one keyword in named files while the same keyword still blocks everywhere else, and a single occurrence outside a hash refuses an `inHashes` entry outright.
392
+ - **The leak guard grew an allowlist, because it had no way to be wrong.** Every keyword matched as a case-insensitive substring over the staged added lines, so a four letter row landed inside a real npm dependency name and inside a base64 integrity hash in `bun.lock`, and that lockfile became uncommittable forever with no per repo override of any kind. The only way past was `--no-verify`, which disarms every other gate too, so the guard was training people to switch it off. Two answers ship, in this order. A `w:<word>` keyword row now matches on word boundaries spelled in unicode rather than with `\b`, which is ascii only and would let an accented name match inside a longer accented word: that makes the PATTERN smarter rather than the guard looser, and it is opt in, so a keyword file that carries no `w:` row behaves exactly as before. Then `repo.config.ts hooks.leakAllow` (with the machine-global twin `~/.shipkit/private/leak-allow.json`) declares a known-good match: `paths` names the files, `patterns` names the keywords, and `inHashes` narrows to a match that falls inside a content hash, where the bytes are an address rather than prose. `paths` plus `patterns` is the shape to reach for, because it waives one keyword in named files while the same keyword still blocks everywhere else, and a single occurrence outside a hash refuses an `inHashes` entry outright.
30
393
  - **A waiver is never silent.** Each entry carries a required `reason`, every waiver that fires PRINTS with that reason and the entry that owns it (on the FAIL path too, so a partial waive is visible beside what still blocks), and `hooks list` counts the declared waivers. An entry with no reason, with neither `paths` nor `patterns`, or with an unknown key dies at config load through `defineRepo`, and an allowlist file that will not parse waives nothing and WARNs, so a broken waiver leaves the guard stricter rather than looser.
31
394
  - **A tracked allowlist never becomes a copy of the private keyword list.** An entry names a keyword by FINGERPRINT (`fp:` plus 12 hex of its sha256), printed beside every hit, which is how a waiver gets written without anyone typing the private word into git. `repo.config.ts` takes the fingerprint form alone and refuses a literal by name. The private machine-global file may use either. The honest limit is stated in the source: a fingerprint stops the list being read off a tracked file and is no defence against a targeted guess.
32
395
  - The staged diff is now parsed per FILE (`addedLinesByFile`), so a hit says which file it came from and a waiver can name one. Which file a line belongs to is decided by COUNTING each hunk body from the size its header declares, never by trusting a `+++ …` line wherever it appears: an added line whose own text starts with `++` reaches the parser looking exactly like a file header, and believing one inside a hunk would let ordinary looking text drop every added line after it (`++ /dev/null`) or file the rest under a name a waiver covers (`++ b/bun.lock`). Substring rows match through a case-insensitive regex instead of `indexOf` over a lowercased copy, because one unicode character lowercases into two and slides every offset after it, and those offsets are what decide whether a match sits inside a content hash. A content-hash span is consulted ONLY by an entry that asked for `inHashes`: an api token is also a long base64 run, so skipping those spans by default would silence the guard on the exact shape it exists to catch.
@@ -65,7 +428,7 @@
65
428
 
66
429
  ## 0.35.6
67
430
 
68
- - **`shipkit init` furnishes an adopted repo with the folder-open half of the scaffold.** The day-one shape was written only for a dir with no commit history, so every repo wired AFTER it already existed got `repo.config.ts` and the scripts and no `.vscode/` at all: opening the folder started no watcher, and the hub the config advertises had no way in. `init` now writes `.vscode/settings.json` and `.vscode/tasks.json` on both its paths, the wiring run and the re-run, through the same writers and the same templates the start scaffold uses. The split it holds: a file that changes no output of any command lands on adopt, and anything saying what the repo IS (`tsconfig.json`, `src/index.ts`) or what an agent may DO in it (the verify skill, the `.claude/` policy files, the `agent/` wiki, the `.rv-reflections/` seed) still needs an explicit caller, because a repo with a history of its own already answered those. Create only, so a `.vscode/` a repo was forked with survives byte for byte, and a second run writes nothing and says so. The advertised port is resolved from `repo.config.ts` (`serve.port`, else derived from the config name), which is where the hub reads it, rather than from the folder name or a stale `package.json`. A `.vscode` path that cannot hold the two files (a read only tree, a path that is a file) costs one WARN naming the errno: this write runs after `repo.config.ts` is on disk and before the publish chain, and it is the least important thing the verb does, so it never kills the run.
431
+ - **`shipkit init` furnishes an adopted repo with the folder-open half of the scaffold.** The day-one shape was written only for a dir with no commit history, so every repo wired AFTER it already existed got `repo.config.ts` and the scripts and no `.vscode/` at all: opening the folder started no watcher, and the hub the config advertises had no way in. `init` now writes `.vscode/settings.json` and `.vscode/tasks.json` on both its paths, the wiring run and the re-run, through the same writers and the same templates the start scaffold uses. The split it holds: a file that changes no output of any command lands on adopt, and anything saying what the repo IS (`tsconfig.json`, `src/index.ts`) or what an agent may DO in it (the verify skill, the `.claude/` policy files, the `agent/` wiki, the `.shipkit/reflections/` seed) still needs an explicit caller, because a repo with a history of its own already answered those. Create only, so a `.vscode/` a repo was forked with survives byte for byte, and a second run writes nothing and says so. The advertised port is resolved from `repo.config.ts` (`serve.port`, else derived from the config name), which is where the hub reads it, rather than from the folder name or a stale `package.json`. A `.vscode` path that cannot hold the two files (a read only tree, a path that is a file) costs one WARN naming the errno: this write runs after `repo.config.ts` is on disk and before the publish chain, and it is the least important thing the verb does, so it never kills the run.
69
432
 
70
433
  ## 0.35.5
71
434
 
@@ -81,7 +444,7 @@
81
444
  - The `mv` header comment no longer claims the token sweep has nothing left to corrupt. A path that merely ENDS with the old path is still rewritten by that sweep, the relative pass declines it on purpose, and the comment now says so instead of telling the next reader to stop looking.
82
445
  - The STATUS.md RED verdict no longer claims `1 FAIL line` when the body has none. A red exit with no FAIL line of its own is its own state and says so.
83
446
 
84
- - **The contract-folder repair asks the REPO whether the dotted name is ignored, instead of trusting a regex over a comment.** The candidate names come from the SK004 policy block, and three of them (`.rv-itch`, `.rv-steam`, `.rv-discord`) carry "ignored until the listing is live, override below the block". On a repo with a live listing those folders are TRACKED on purpose, so renaming an undotted one into them would have committed the removal of real files from the index, silently, inside somebody else's gate run. Now: untracked folders rename freely, since nothing can be lost, and a TRACKED one only renames when `git check-ignore` says this repo treats the dotted name as local.
447
+ - **The contract-folder repair asks the REPO whether the dotted name is ignored, instead of trusting a regex over a comment.** The candidate names come from the SK004 policy block, and three of them (`.shipkit/itch`, `.shipkit/steam`, `.shipkit/discord`) carry "ignored until the listing is live, override below the block". On a repo with a live listing those folders are TRACKED on purpose, so renaming an undotted one into them would have committed the removal of real files from the index, silently, inside somebody else's gate run. Now: untracked folders rename freely, since nothing can be lost, and a TRACKED one only renames when `git check-ignore` says this repo treats the dotted name as local.
85
448
  - **A repair that renamed and could not commit is REPORTED.** Silence is for a repair that completed. The hooks can refuse, `SHIPKIT_NO_AUTOCOMMIT=1` can be set, a merge can be in progress: the rename is still the right end state, and the dirt it leaves has to be traceable to the run that made it rather than surfacing as an unexplained refusal in the next bump.
86
449
 
87
450
  ## 0.35.3
@@ -90,7 +453,7 @@
90
453
 
91
454
  ## 0.35.2
92
455
 
93
- - **A contract folder typed without its dot is repaired, not reported.** `rv-reflections/` instead of `.rv-reflections/` is invisible in every direction at once: the harvest scanner reads the dotted name, the SK004 ignore block covers the dotted name, and the repo filing notes into it gets silence rather than an error. Three notes sat unread for a day that way with nothing red anywhere. It is one character, a machine can be certain, and nobody gets a say in the answer, so the fix pass renames it and lands it through the machine commit path, and no agent is told: a WARN a machine can repair is a design bug. An EMPTY misnamed folder is removed instead, since there is nothing to carry. A rename takes the files OUT of the index, which is the contract rather than a side effect: those folders are gitignored under their real name, so tracking them was the missing dot's doing. The names come from the SK004 policy block itself, so a new always-ignored `.rv-*` folder is covered by declaring it there and nowhere else; the TRACKED contract dirs stay out, because their own contract check already reports them missing. BOTH names present is the one case that reaches a human, and it refuses out loud rather than merging: two folders can hold the same filename, and picking a winner is a decision.
456
+ - **A contract folder typed without its dot is repaired, not reported.** `rv-reflections/` instead of `.shipkit/reflections/` is invisible in every direction at once: the harvest scanner reads the dotted name, the SK004 ignore block covers the dotted name, and the repo filing notes into it gets silence rather than an error. Three notes sat unread for a day that way with nothing red anywhere. It is one character, a machine can be certain, and nobody gets a say in the answer, so the fix pass renames it and lands it through the machine commit path, and no agent is told: a WARN a machine can repair is a design bug. An EMPTY misnamed folder is removed instead, since there is nothing to carry. A rename takes the files OUT of the index, which is the contract rather than a side effect: those folders are gitignored under their real name, so tracking them was the missing dot's doing. The names come from the SK004 policy block itself, so a new always-ignored `.shipkit/*` folder is covered by declaring it there and nowhere else; the TRACKED contract dirs stay out, because their own contract check already reports them missing. BOTH names present is the one case that reaches a human, and it refuses out loud rather than merging: two folders can hold the same filename, and picking a winner is a decision.
94
457
  - The machine commit path is one function, `core/machine-commit`, rather than a copy per caller. Same scoping as before: the working tree version of the named paths only, never the index, so a neighbour's staged work cannot ride along, no commit mid merge or mid rebase or on a detached HEAD, never a push, and `SHIPKIT_NO_AUTOCOMMIT=1` off.
95
458
  - **The imports fixer rewrote toward a shape the repo could not resolve, then reported green over it.** It validated its own output by asking the DISK whether the target file existed, which it always did, having just resolved the path from it. Nothing asked whether the REPO could follow the specifier being written. In two repos with no `paths`, no `baseUrl` and no bundler alias, the fix pass turned a building tree into 150 `TS2307`s and a `Rollup failed to resolve import "src/setup.tsx"`, and `imports:check` printed `ok` straight afterwards. A prefix nothing maps is now never a rewrite TARGET: resolution is read from every `tsconfig*.json` at the root (the standard vite shape keeps `paths` in a REFERENCED config), and a prefix counts as mapped by a `paths` key, by a `baseUrl` it sits under, or, for a package name, by a real `node_modules` entry. A refused rewrite is an ERROR naming the mapping to add, so the run is red in every mode. Only the write is refused: a specifier already in canonical form is left alone, because this tool did not put it there and the typechecker owns that verdict.
96
459
 
@@ -267,12 +630,12 @@
267
630
 
268
631
  - **The attribution tell list covers session trailers and assistant share urls.** `<assistant>-Session:` trailers (claude, chatgpt, copilot, cursor, aider, devin, gemini, codex) and claude.ai/claude.com/chatgpt.com/chat.openai.com urls now trip the shared predicate (commit-msg hook + SK009 history scan). Field case: 162 session links sat in a history both gates called clean.
269
632
  - **SK009 scans ref names too**, local and remote-tracking. A checkpoint tag named after an assistant (`FABLE-checkpoint-3`) outs the agent as loudly as a trailer. Bare match only for names with no other meaning (claude, chatgpt, copilot, aider, devin); vendor and codename words (cursor, openai, gemini, fable, sonnet, haiku, opus) name legitimate branches, so they only match in a `-checkpoint`/`-session` shape.
270
- - **The leak guard reads a machine-global list.** Every run also scans `~/.rv-private/banned-keywords.txt` when it exists: one private list per machine, armed in every repo at once, never copied into a repo (a copy rots and a copy can leak). `hooks install` seeds the repo's own configured keyword file with generic secret-shape starters, never touching an existing one. Both files absent stays a loud skip.
633
+ - **The leak guard reads a machine-global list.** Every run also scans `~/.shipkit/private/banned-keywords.txt` when it exists: one private list per machine, armed in every repo at once, never copied into a repo (a copy rots and a copy can leak). `hooks install` seeds the repo's own configured keyword file with generic secret-shape starters, never touching an existing one. Both files absent stays a loud skip.
271
634
  - **The itch listing answered-check catches empty fields.** A `## field` heading with no body line under it is an unanswered field wearing no `<placeholder>`; the contract names it now. Prose-level open decisions stay a human read.
272
635
 
273
636
  ## 0.18.0
274
637
 
275
- - **The comfy backend runs on comfy-ts.** `.rv-comfy/workflows/` holds `*.cflow.ts` workflow modules (one `host.defineWorkflow` per graph family, plus a `_host.ts` the modules import). Exported json graphs and `.ts` sidekick pairs are gone, and the raw HTTP engine (`comfy-gen.ts`) and pair loader (`workflow-loader.ts`) are deleted with them. Every module must declare `vars.prompt` (`v.prompt`) and `vars.seed` (`v.seed`), shipkit drives both.
638
+ - **The comfy backend runs on comfy-ts.** `.shipkit/comfy/workflows/` holds `*.cflow.ts` workflow modules (one `host.defineWorkflow` per graph family, plus a `_host.ts` the modules import). Exported json graphs and `.ts` sidekick pairs are gone, and the raw HTTP engine (`comfy-gen.ts`) and pair loader (`workflow-loader.ts`) are deleted with them. Every module must declare `vars.prompt` (`v.prompt`) and `vars.seed` (`v.seed`), shipkit drives both.
276
639
  - **One comfy-ts draft per asset.** Shipkit seeds `.comfy-ts/drafts/<workflowId>/<assetId>.json` once and never overwrites it, so `bunx comfy-ts tui` lists every asset as a tweakable row under its workflow. At generation: reset vars → kit `vars` → asset draft → prompt and seed pinned programmatically. The kit + studio overlay stay the prompt source of truth.
277
640
  - **BREAKING: `comfy({ workflow, vars })` replaces `bg`.** `vars` is a record of workflow var values applied before the draft, an unknown var name fails loud.
278
641
  - **BREAKING: `KitConfig.comfy.host` is removed.** The workflow's `_host.ts` module owns the box connection; `comfy:` keeps `{ workflows, heal }`.
@@ -303,7 +666,7 @@
303
666
  - **A release gates fresh.** SK005 caches a clean pass for a day to rate-limit its registry call, but its blocking branches are local facts about whether the tree can import its own dependencies. Those are measured at release time now, not replayed.
304
667
  - **The bump rehearsal reads `git fetch`.** Offline or with an expired credential it could compare against stale remote refs and still report a clean merge. It warns and closes inconclusive instead.
305
668
  - New pre-commit gate **root-identity**: the name in the root `package.json` and `repo.config.ts`, index against HEAD, plus staged `.bak`/`.orig`/`.rej`. A fixture builder that escapes its temp dir overwrites those files and every other gate still passes, because a gutted repo lints fine. A deliberate rename passes with `SHIPKIT_ALLOW_IDENTITY_CHANGE=1`.
306
- - `.rv-private/` joins the SK004 gitignore policy. It holds the leak guard's own keyword file, which was committable in every repo until now.
669
+ - `.shipkit/private/` joins the SK004 gitignore policy. It holds the leak guard's own keyword file, which was committable in every repo until now.
307
670
  - `ci --frozen` exists as a flag, and no TUI panel writes STATUS.md any more, so opening the dashboard cannot overwrite a full gate's verdict with a tasks-only one.
308
671
 
309
672
  ## 0.16.0
@@ -464,7 +827,7 @@ Dep: `rvlib-ts-tools` ^0.2.0 → ^0.3.0, so the zero-config `imports:check`
464
827
  floor task inherits its honest reporting (a run that scanned zero files now
465
828
  fails instead of printing green).
466
829
 
467
- MIGRATION: the itch venue contract gains a required `.rv-itch/listing.md`
830
+ MIGRATION: the itch venue contract gains a required `.shipkit/itch/listing.md`
468
831
  (every store-form field answered, paste-ready). A repo already shipping on itch
469
832
  will see one new FAIL until the file exists; the docs that own the itch
470
833
  contract carry a fill-in template for it.
@@ -476,7 +839,7 @@ an SK rule — one module per rule (`src/rules/SK###-<slug>.ts`), one
476
839
  registry (`rule-registry.ts`, throws at import on a duplicate or malformed
477
840
  id), ids never reused or renumbered. The two existing built-ins migrated
478
841
  whole: SK001 package.json shape (was `core/package-json-check.ts`, deleted),
479
- SK002 `.rv-*` contracts fulfillness. Line text changed on every consumer:
842
+ SK002 `.shipkit/*` contracts fulfillness. Line text changed on every consumer:
480
843
  rule-emitted `ok:`/`WARN`/`FAIL:`/`skip:` lines now carry a `[SK###]` tag
481
844
  after the wire prefix — on `check`, `shipkit contracts`, AND the ci
482
845
  `contracts` task (one tagged rendering path,
@@ -601,7 +964,7 @@ New:
601
964
  API).
602
965
  - Remarks: cards AND the asset view have a remark box; `/api/act/remark`
603
966
  routes the text into the item's own file (post `## Feedback`, the ask
604
- file, or `.rv-assets/notes.md` for media) and it returns as `remarks`
967
+ file, or `.shipkit/assets/notes.md` for media) and it returns as `remarks`
605
968
  on the instance — `shipkit needs --json` briefs agents on them at boot.
606
969
  - Posted/rejected post history is VISIBLE (quiet), no longer hidden;
607
970
  media collections list ALL kit assets, not only needing ones.
@@ -670,7 +1033,7 @@ Breaking (0.10.x consumers):
670
1033
  exports removed (use `redCount`/`NeedItem`/`Instance`). Unpicked-roll
671
1034
  and stale assets are now YELLOW (taste pending — a default ships), no
672
1035
  longer red.
673
- - `.rv-social` post files: `status:` frontmatter is required (missing =
1036
+ - `.shipkit/social` post files: `status:` frontmatter is required (missing =
674
1037
  a red feed item); v1 vocab maps idea→draft, ready→picked, link→posted.
675
1038
 
676
1039
  ## 0.10.0 — 2026-07-22
@@ -742,7 +1105,7 @@ New (the hub becomes THE dashboard; autonomy over nagging):
742
1105
  - **autonomy contract — machine-clearable red clears itself**: re-picking a
743
1106
  parent AUTO-enqueues every now-stale child (no button, no nag; spend
744
1107
  gated on the intentful pick). Identity self-heals the same way: brand.ts
745
- content hash in `.rv-identity/brand.hash`; `assets check` and the
1108
+ content hash in `.shipkit/identity/brand.hash`; `assets check` and the
746
1109
  attention collector detect drift and RE-RENDER public/* themselves —
747
1110
  only a missing pick still reaches a human. The old
748
1111
  "WARN outputs missing — run identity" line is gone.
@@ -802,8 +1165,8 @@ New (wave 1 of the shipkit expansion — uniform repo setup):
802
1165
  - built-in `package.json` probe in `shipkit check`
803
1166
  (`core/package-json-check.ts`): FAILs on `license` present / `private` not
804
1167
  true / missing capability-required scripts (repo.config → ci check
805
- contracts tui hub; `.rv-assets/kit.ts` → assets; `.rv-identity/brand.ts`
806
- → assets:identity; `.rv-comfy/` → comfy:up; `deploy.sh` → up; ask dir →
1168
+ contracts tui hub; `.shipkit/assets/kit.ts` → assets; `.shipkit/identity/brand.ts`
1169
+ → assets:identity; `.shipkit/comfy/` → comfy:up; `deploy.sh` → up; ask dir →
807
1170
  asks); WARNs on banned aliases (`serve`→`hub`, `identity`→
808
1171
  `assets:identity`, `pretty`→`fmt`, `check-types`→`typecheck`; body-matched
809
1172
  so an unrelated `serve` is left alone) and a missing `test`/`lint`/`fmt`
@@ -866,7 +1229,7 @@ New:
866
1229
  (`ServeConfig`).
867
1230
  - studio per-asset info surface (W2-2): the detail view shows where every
868
1231
  roll comes from and where a pick goes — backend, resolved workflow id +
869
- its `.rv-comfy/` path (or WHY it's unresolved), ship target (built-in /
1232
+ its `.shipkit/comfy/` path (or WHY it's unresolved), ship target (built-in /
870
1233
  hook / record), meta write-through file, kit source. `/api/asset` gains
871
1234
  an additive `info` block. Deterministic canvas kinds (draw takes no seed
872
1235
  → identical rolls) are labelled next to Generate and capped to one roll