rvlib-shipkit 1.3.0 → 1.5.0

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 (203) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +12 -2
  7. package/src/check.ts +73 -98
  8. package/src/checks/SK001-package-json-shape.check.ts +26 -4
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  10. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  11. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  12. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  13. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  14. package/src/checks/SK007-github-visibility.check.ts +4 -2
  15. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  16. package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
  17. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  18. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  19. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  20. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  21. package/src/checks/SK014-prose-ste.check.ts +20 -6
  22. package/src/checks/SK015-nested-repo.check.ts +4 -2
  23. package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
  24. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  25. package/src/checks/SK018-test-files-run.check.ts +6 -2
  26. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  27. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  28. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  29. package/src/checks/SK024-identity-colors.check.ts +5 -3
  30. package/src/checks/SK025-release-drift.check.ts +4 -5
  31. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  32. package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
  33. package/src/checks/SK028-readme.check.ts +10 -2
  34. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  35. package/src/cli/cmd_Asks.ts +5 -5
  36. package/src/cli/cmd_Assets.ts +7 -7
  37. package/src/cli/cmd_Bump.ts +12 -11
  38. package/src/cli/cmd_Check.ts +8 -7
  39. package/src/cli/cmd_Checks.ts +7 -6
  40. package/src/cli/cmd_Ci.ts +12 -11
  41. package/src/cli/cmd_Commands.ts +16 -13
  42. package/src/cli/cmd_ConfigKeys.ts +7 -6
  43. package/src/cli/cmd_Contracts.ts +3 -3
  44. package/src/cli/cmd_CrossRepoActivity.ts +10 -6
  45. package/src/cli/cmd_Fleet.ts +9 -7
  46. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  47. package/src/cli/cmd_Home.ts +23 -17
  48. package/src/cli/cmd_Hooks.ts +6 -5
  49. package/src/cli/cmd_Init.ts +5 -4
  50. package/src/cli/cmd_Keys.ts +8 -7
  51. package/src/cli/cmd_Mv.ts +7 -6
  52. package/src/cli/cmd_Needs.ts +4 -3
  53. package/src/cli/cmd_Overview.ts +4 -3
  54. package/src/cli/cmd_Pb.ts +2 -2
  55. package/src/cli/cmd_Planned.ts +6 -5
  56. package/src/cli/cmd_Prose.ts +5 -5
  57. package/src/cli/cmd_Reflect.ts +7 -7
  58. package/src/cli/cmd_Release.ts +6 -5
  59. package/src/cli/cmd_Releases.ts +5 -4
  60. package/src/cli/cmd_Serve.ts +4 -3
  61. package/src/cli/cmd_Smoke.ts +5 -4
  62. package/src/cli/cmd_Start.ts +7 -6
  63. package/src/cli/cmd_Status.ts +4 -3
  64. package/src/cli/cmd_Tui.ts +3 -2
  65. package/src/cli/cmd_When.ts +71 -0
  66. package/src/cli/cmd_Wiki.ts +4 -4
  67. package/src/cli.ts +7 -2
  68. package/src/collections/checks.ts +2 -0
  69. package/src/collections/collection-types.ts +24 -4
  70. package/src/collections/providers.ts +6 -2
  71. package/src/contracts/apply-contracts.ts +9 -8
  72. package/src/contracts/contract-types.ts +8 -2
  73. package/src/contracts/contracts-engine.ts +34 -13
  74. package/src/contracts/manifests/identity.contract.ts +6 -5
  75. package/src/contracts/manifests/social.contract.ts +4 -2
  76. package/src/contracts/manifests/steam.contract.ts +7 -3
  77. package/src/contracts/web-surface.ts +59 -0
  78. package/src/core/budget.ts +19 -9
  79. package/src/core/canonical-scripts.ts +1 -1
  80. package/src/core/check-runner.ts +63 -25
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +31 -7
  83. package/src/core/layout.ts +2 -0
  84. package/src/core/legacy-shapes.ts +39 -10
  85. package/src/core/motherhome.ts +88 -66
  86. package/src/core/one-way-in-policy.ts +2 -2
  87. package/src/core/outcome-words.ts +18 -2
  88. package/src/core/port-check-cli.ts +52 -0
  89. package/src/core/port-check-file.ts +295 -0
  90. package/src/core/port-check-files.ts +31 -0
  91. package/src/core/retention.ts +0 -14
  92. package/src/core/run-check.ts +14 -14
  93. package/src/core/run-ci.ts +11 -7
  94. package/src/core/run-fix.ts +30 -23
  95. package/src/core/run-fixers.ts +42 -5
  96. package/src/core/runs-matrix.ts +179 -0
  97. package/src/core/runs.ts +242 -0
  98. package/src/core/update-policy.ts +20 -0
  99. package/src/core/when-block.ts +29 -30
  100. package/src/core/when.ts +41 -103
  101. package/src/core/wire.ts +34 -9
  102. package/src/fleet/scan-fleet.ts +20 -4
  103. package/src/fleet/update-fleet.ts +6 -6
  104. package/src/home/activity-shape.ts +4 -1
  105. package/src/home/activity.ts +22 -15
  106. package/src/home/gui-app/ShipkitGui.swift +144 -25
  107. package/src/home/gui-app.ts +93 -30
  108. package/src/home/home-launchd.ts +30 -6
  109. package/src/home/home-routes.ts +14 -2
  110. package/src/home/home-start.ts +33 -5
  111. package/src/home/hubs.ts +5 -5
  112. package/src/home/machine-stores.ts +4 -2
  113. package/src/home/open-app.ts +23 -10
  114. package/src/home/pm-config.ts +15 -11
  115. package/src/home/pm-inputs.ts +76 -0
  116. package/src/home/pm.ts +11 -3
  117. package/src/home/process-rows.ts +23 -6
  118. package/src/home/resources.ts +67 -15
  119. package/src/home/run-daemon.ts +6 -1
  120. package/src/home/run-pm.ts +30 -5
  121. package/src/home/serve-web.ts +2 -2
  122. package/src/home/supervise.ts +24 -1
  123. package/src/home/widgets.ts +32 -3
  124. package/src/hooks/commit-message.ts +2 -3
  125. package/src/hooks/run-hooks.ts +60 -6
  126. package/src/init-repo.ts +6 -6
  127. package/src/ledger/gate-checks.ts +115 -61
  128. package/src/ledger/git-key.ts +5 -1
  129. package/src/ledger/ledger.ts +5 -5
  130. package/src/ledger/store.ts +11 -0
  131. package/src/ledger/toolchain-identity.ts +45 -27
  132. package/src/ledger/world-cache.ts +17 -15
  133. package/src/plan/repo-source.ts +18 -39
  134. package/src/release/run-release.ts +5 -0
  135. package/src/repo-config.ts +36 -56
  136. package/src/rules/rule.ts +2 -2
  137. package/src/scaffold/files/agent-coding.md +1 -1
  138. package/src/scaffold/files/agent-index.md +1 -1
  139. package/src/scaffold/files/verify-skill.md +2 -2
  140. package/src/serve/hub-app/activity-st.ts +5 -4
  141. package/src/serve/hub-app/activity-view.tsx +4 -4
  142. package/src/serve/hub-app/admin-view.tsx +13 -11
  143. package/src/serve/hub-app/check-view.tsx +66 -46
  144. package/src/serve/hub-app/check-words.ts +1 -11
  145. package/src/serve/hub-app/checks-svc.ts +16 -36
  146. package/src/serve/hub-app/col-tables-view.tsx +131 -0
  147. package/src/serve/hub-app/col-tables.ts +115 -0
  148. package/src/serve/hub-app/cols-svc.ts +33 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +7 -1
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +15 -3
  160. package/src/serve/hub-app/home-strip.css +69 -19
  161. package/src/serve/hub-app/home-strip.tsx +192 -118
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +21 -5
  164. package/src/serve/hub-app/hub-types.ts +11 -5
  165. package/src/serve/hub-app/hub.css +88 -44
  166. package/src/serve/hub-app/icons.tsx +15 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +11 -8
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +34 -49
  171. package/src/serve/hub-app/palette-st.ts +5 -5
  172. package/src/serve/hub-app/palette-view.tsx +1 -1
  173. package/src/serve/hub-app/plan-view.tsx +51 -0
  174. package/src/serve/hub-app/planned-st.ts +9 -4
  175. package/src/serve/hub-app/processes-view.tsx +38 -0
  176. package/src/serve/hub-app/project-list.tsx +3 -3
  177. package/src/serve/hub-app/projects-svc.ts +15 -10
  178. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  179. package/src/serve/hub-app/reach.ts +36 -0
  180. package/src/serve/hub-app/readme-svc.ts +3 -2
  181. package/src/serve/hub-app/repo-sections.ts +117 -0
  182. package/src/serve/hub-app/repo-st.ts +47 -10
  183. package/src/serve/hub-app/runs-tabs.ts +1 -1
  184. package/src/serve/hub-app/sidebar.tsx +12 -37
  185. package/src/serve/hub-app/timeline-svc.ts +3 -2
  186. package/src/serve/hub-app/timeline-view.tsx +14 -11
  187. package/src/serve/hub-app/tree-build.ts +149 -155
  188. package/src/serve/hub-app/tree-row.tsx +39 -9
  189. package/src/serve/hub-app/tree-svc.ts +70 -49
  190. package/src/serve/hub-app/when-view.tsx +89 -0
  191. package/src/serve/hub-app/when.css +17 -0
  192. package/src/serve/hub-runtime.ts +3 -16
  193. package/src/serve/project-gutter.ts +1 -2
  194. package/src/serve/run-serve.ts +1 -1
  195. package/src/serve/serve-hub.ts +4 -14
  196. package/src/serve/warm-cell.ts +49 -16
  197. package/src/suite/judge-suites.ts +42 -17
  198. package/src/suite/suite-cli.ts +5 -8
  199. package/src/wiki/lint-md.ts +2 -0
  200. package/src/wiki/scan-md.ts +37 -10
  201. package/src/serve/hub-app/processes-svc.ts +0 -63
  202. package/src/serve/hub-app/repo-sections.tsx +0 -61
  203. package/src/serve/repo-processes.ts +0 -89
package/CHANGELOG.md CHANGED
@@ -1,3 +1,108 @@
1
+ ## 1.5.0
2
+
3
+ - **Every check declares where it runs and how it caches, with no default.** A check file carries `runs` (`onPreCommit`, `onCi`, `onFull`, `onRelease`, `onHealth`, `onSave`, `onClock`), `cache` (`files`, `reads`, `ttl`, `fact` or `never`, each with its one parameter), `fix` (`none`, `ci` or `manual`, the function itself is now `repair`) and `applies`. `check()` refuses a missing key, a retired key (`when`, `inputs`, `ttl`, `never`, `every`, `cron`, `whenInactive`, `planOff`, `cacheKey`) and the combinations that contradict each other. The gate, the fix pass, the release preflight and the planner read the declared axes; a release never applies a check repair. Every built-in carries its own placement: SK024, SK017 and SK028 run in the health pass only, and SK005, SK006, SK011, SK014, SK016 and SK022 are out of the quick gate. A check file that does not load is one failing row instead of a dead inventory.
4
+
5
+ - **SK029 ports a check file still in the retired shape.** Its repair writes `runs`, `cache`, `fix` and `applies` with the values the old keys resolved to, so the check runs where it ran, and the fix pass of `sk ci` applies it: a repo is carried across by its first gate.
6
+
7
+ - **A repo only turns a check off.** `off: { <id>: '<why>' }` in `repo.config.ts` replaces the `when` map; `defineRepo` refuses every `when` word but `off:<why>`, and SK029 ports that and `ruleExempt` into `off`. The hub's switch on a check page writes it.
8
+
9
+ - **The pre-commit hook runs the checks declaring `onPreCommit`**, in process on the working tree, under a budget of at most 100ms each; a failure refuses the commit. `sk hooks list` names them. The whole hook warns past 1s.
10
+
11
+ - **`sk when` and the when page show what runs when**: the legend of every context and value, then every check on every context. `--only onCi` keeps what the quick gate runs.
12
+
13
+ - **The hub tree has four sections where it had checks.** contracts (⌥K), shipkit checks (⌥S), repo checks (⌥C) and secrets (⌥X) are top rows, each with its own red count, and their rows sit straight under them, one indent less. shipkit checks and repo checks open the checks table showing only their own checks (`#page:checks:shipkit`, `#page:checks:repo`).
14
+
15
+ - **A check page lists where the check runs, one line per context.** Commit, ci, full, release, health, save, clock, fix and cache each show the declared value and what it means, in the words of the `sk when` legend. A context that does not run the check stays dim. The switch that turns the check off in this repo, with its reason, sits under that list.
16
+
17
+ - **The tree drops the gray ci, release, health word on a check row.** A check declares seven contexts now, which one word could not say. The row's tooltip lists every context that runs it (`runs on ci · full · release · health · save moved`), and the label gets the width back. A check that no context runs reads off.
18
+
19
+ - **A link button copies the page's url.** At the bottom right of the dock bar, it shows the url on hover and copies it on click. The app window has no url bar.
20
+
21
+ - **Fix: the repo tree's scrollbar is a thin bar.** In the macOS app it drew a wide dark band beside the tree; it is now a 6px thumb on a transparent track, and the rows sit closer to it.
22
+
23
+ - **The tree gives its width back to the labels.** A row's duration is a small clock now, tinted by the same tone table, with the figure in its tooltip. The indent per level and the space around the chevrons are a little smaller.
24
+
25
+ - **The command index is lean.** Every command's `description` is one short line (at most 100 characters, a test holds it), and the long text moved to `details`, which `--help` still prints. `shipkit commands` renders each signature with its flags inline, but no longer a line per flag, and every example uses `sk`, which is also the name `--help` shows. The generated index went from 33 KB to 17 KB.
26
+
27
+ - **`sk`, the short name.** The package ships a second bin, `sk`, the same cli as `shipkit`.
28
+
29
+ - **The fix pass is skipped over a tree it already left clean.** When HEAD, every uncommitted file, the toolchain and the fixer commands are the same as at the last pass that rewrote nothing, the formatter and import fixers do not run again. That was the whole wait before the first line of `shipkit ci`. CI never skips.
30
+
31
+ - **Every gate line says where its answer came from.** A check that ran ends its first line with `(ran 6.4s)`, one replayed from the cache with `(cached, 3h old, 6.4s saved)`, and a world check with `(ran 40ms, kept 1h more)` or `(cached, 2m old, kept 23h more)`. Before, a rule check that ran said nothing and a cached one never said how old its answer was.
32
+
33
+ - **Fix: a repo that `bun link`s a package replays from the cache.** A package linked outside the repo made every check "never cached", so a repo linking its toolchain re-ran everything on every gate. The link target is now keyed by its content (each file's path, size and bytes, symlinks followed, its own installs), so an edit there still moves the key and an untouched one replays. A linked tree that cannot be read keys nothing.
34
+
35
+ - **`shipkit gp start|stop|restart <id>`.** The overview's switches for a global process, from a terminal: pm takes the switch, and the command follows pm's status until the probe settles it. It exits 0 once the process is up (down for stop), and 1 with its note and its last log lines when it crashed or stayed down.
36
+
37
+ - **Widgets are asked for only by a visible overview.** The page polled `/api/home/widgets.json` from every page, hidden tabs and app windows included, so the daemon rendered every widget around the clock. A widget that samples another machine over ssh kept sampling it all day. A repo page or a hidden tab now asks for none, and the daemon stops rendering 30 s after the last read.
38
+
39
+ - **Removed: `serve.processes`.** A hub no longer supervises processes of its own: declare each one as a global process in a `<name>.shipkit.ts` file (`export default globalProcess({...})`), which pm runs. `defineRepo` refuses the old key and says so. The hub routes `/api/processes.json` and `/api/processes/act`, the header switches and the `process logs` retention store go with it.
40
+
41
+ - **A repo's plan, git and reflections are sections of its tree.** `plan` (⌥N) lists what the machine runs there and when, with run now. `git` (⌥G) shows the branch against its upstream, what is not committed and the commits of the week, with VS Code and Fork, from a new daemon route `/api/home/git.json?repo=`. `reflections` (⌥F) left the checks section for one of their own, one row each.
42
+
43
+ - **A repo's global processes are a section of its tree.** `processes` (⌥P) sits after `runs`, one row per process the repo declares, coloured by its state. The section opens a page with one card per process (its actions, its description as markdown, its log), and a row opens its own card, lit. A new resource kind, `process`, and a new address, `#page:processes[:<id>]`.
44
+
45
+ - **A repo page is not a box, each group is one.** A check, test, admin, runs or item page sits on the page background, and each group it holds (the facts, a titled section, the command, the admin rows, the runs list) is one lighter box.
46
+
47
+ - **A process menu shows its description as markdown, on the right.** The menu a process chip opens is two panels: its actions on the left, its `description` rendered as markdown on the right with its note and last log line, selectable, so a dev login written there is one click away. A process with nothing to say draws the actions alone.
48
+
49
+ - **Fix: a search reads every row, and a filtered tree says what it hides.** With the severity group on `needs` or `red`, a repo with few of them showed empty sections with no chevron and a search that found nothing. A search now ignores the group (`TreeSvc.visible`), and while the group hides rows a line under the tree counts them and shows all on a click (`TreeSvc.hidden`).
50
+
51
+ - **A process chip opens a menu instead of starting or stopping.** A click on a global process chip in the top bar opens its menu: start or stop, restart, open in a browser tab, in Chrome or as an app, its declared actions and take over. The chips sit right after the repo's name, and the spinner and `check…` sit at the right end before the editor icons.
52
+
53
+ - **The machine not answering is said once.** When the daemon, pm or the web does not answer, one card under the strip says so, with what hangs, what runs it and that the page comes back by itself; the daemon and pm down together share that card. The project list, the dock, a repo's trouble card and every repo pane keep what they last read and add no error line of their own. The bottom right toast is gone. `reach` and `ownTrouble` in `reach.ts` are the one reading of the web's tagged 502 and of a fetch with no answer. A route that fails while the daemon answers still shows its own error. The page stays drawn meanwhile: a repo keeps its last screen, since its hub's state is unknown rather than down, and the overview draws what it last had; a wait panel shows only when nothing was ever read.
54
+
55
+ - **Fix: pm kills a child that stopped answering, and its agent is never disk throttled.** A child whose pid lives but has not answered its probe for 90 s is hung: pm writes a WARN line to `pm.log`, sends SIGTERM then SIGKILL, and starts it again through the crash backoff (`hangMs` and `say` on a `ProcSpec`, `CHILD_HANG_MS`). A daemon had sat `starting` for an hour with its port open. The LaunchAgent now carries `ProcessType` Interactive: with none, launchd throttles the disk reads of pm and of every child it starts, and under a busy disk the daemon's boot reads of its sqlite store took minutes. Run `shipkit pm install` once to rewrite an installed agent. That install no longer strands the machine without pm: `launchctl bootout` returns before launchd lets go of the label, and the bootstrap right after it could fail with exit 5 once the old agent was gone. It is retried for 5 s (`BOOTSTRAP_TRIES`), and a final failure says pm is not loaded.
56
+
57
+ - **A readme section, and switching repo keeps the section open.** `readme` is the first row of a repo's left pane (⌥E, `#page:readme`). Selecting another repo opens the section the one you left showed; a repo already on a page of that section keeps it, and one without that section opens its README. `sectionOfSel` and `openSection` in `repo-sections.ts` light the rows and drive the switch, and `HashSel` gains `{ t: 'readme' }`.
58
+
59
+ - **A repo page's left pane is one tree whose top rows are its sections.** checks, runs, asks, tests, media, wikis and admin keep their icon, count, ⌥ key and lit state; a click opens the section's page, and only the chevron, the right arrow or a search unfolds it. Under them: the check origins with reflections, secrets and contracts under checks, one row per ask with the `done` archive, one row per test file, the assets with the workflow and post groups under media, one row per wiki. `KIND_SECTION` gives every resource kind one home, and a section the repo does not draw still gets a row when a resource lives in it (`buildTree` replaces `buildPathTree` and `buildTypeTree`). The by path and by type tabs, the `(?)` legend and the total are gone: each kind glyph names its kind in its tooltip, and the filter matches paths. The severity group sits beside the filter and narrows the section counts too. A search unfolds every section with a match and hides the rest; clearing it restores the folds. The planning line moved to the top of the checks page. `repo-sections.tsx` is `repo-sections.ts`.
60
+
61
+ - **The top bar carries no trouble text and no run buttons.** The hub's trouble and the machine's (a crashed child of pm, a crashed global process, a failed poll) are cards floating at the top right, just under the bar, full text, nothing drawn while all is well (`FloatCards`). The running command is a spinner with its words in a tooltip that opens at once, and nothing shows while idle. The health pass and the gate sit behind one small `check…` button, in a popover (`Pop` in floating.tsx) that Escape or a click outside closes. The standalone hub header gets the same.
62
+
63
+ - **The strip over a repo starts its global processes.** One chip per process the shown repo declares in its `.shipkit.ts` files: its dot and label, a click starts or stops it, and `↗` opens its url while it runs. pm runs them, so the chips show whether the repo's hub answers or not.
64
+
65
+ - **Fix: SK017 removes a `👁 TSC` watcher that runs `tsgo`.** The rule matched only a `tsc` binary, so a repo whose watcher had moved to tsgo kept it beside the gate's typecheck. A tsgo task under another label is still left alone. A wrapper that started only the TSC task goes with it, since it would open with the folder and start nothing.
66
+
67
+ - **Fix: a semver pin under a `bun link` moves when a release ships.** SK005 and `shipkit fleet` read the linked source, always the newest, so a repo pinned at 1.3.0 read current and a fresh clone kept installing 1.3.0. A pinned dep whose node_modules entry is a symlink is now judged by the pin's floor (`pinFloor`, `isLinkedInstall`). Behind, SK005 moves the pin through its guarded update, keeps its style, runs `bun link <name>` after it and FAILs when the link does not come back. The fleet row names it `pin 1.3.0 → 1.4.0 (linked, stale pin)`, `FleetDep` gains `linkedInstall` and `at`, and `fleet --update` reads the move off the pin (`judgedVersion`). A `link:` spec is judged as before.
68
+
69
+ ## 1.4.0
70
+
71
+ - **Fix: `shipkit gui` wears the house mark from any directory.** The app took its icon from cwd, so a run outside every house repo rebuilt `Shipkit.app` with no icon. The house now comes from cwd, else from the installed pm agent's `WorkingDirectory` (`installedHouseRoot`, `guiRoot`), and never from cwd itself.
72
+
73
+ - **Fix: a busy machine never fails a suite as a wait.** The wait verdict judges the wall against `waitAllowance`, twice the budget divided by `1 - share²`, which has no end once the load reaches the core count; `fairWall` is gone. The fair share model failed four suites at 11 to 19s wall with about 1s of CPU at load 20.5 on 8 cores. The FAIL line names the load a wait was judged at instead of "on an idle machine", and `overWords` moved to `judge-suites.ts`.
74
+ - `Answers.busyTimeout()` names the busy timeout sqlite holds on the open handle, so a test asserts a read waits only `READ_BUSY_MS` on a writer instead of timing it.
75
+ - **Fix: a save in a repo nobody looks at costs the daemon nothing.** A hub's warm answers (rows, runs page, wiki graphs) recompute in the background only while a page read them in the last 30s (`WARM_WATCH_MS`), at most once per ten times their last compute (`WARM_SPACING`); the first reader after that waits on one fresh compute. Measured on a live daemon, one file of a large repo touched every 2s: 83.5% cpu before, 5.0% after.
76
+ - **Fix: widgets scan and render only while someone reads them.** No read of `/api/home/widgets.json` for 30s (`WIDGET_WATCH_MS`), and the daemon walks no repo and renders no widget; the first read after that waits on a scan and a render. `Widgets.look(repos)` is the reader's call.
77
+ - **Fix: pm reads the house side again only when it moved.** The `pm-config.ts` child answers with the paths it read and their stamp, and pm stats them every 15s (about 4ms) instead of spawning a child that imports every unit file (about 0.6s of cpu). A dir counts by its subdirs and unit files, so a temp file coming and going in a repo root moves nothing. `repoUnitFiles(root, dirs)` collects the dirs its walk read.
78
+ - **Fix: an ask probe runs with the env the process holds now.** `shipkit asks check` and SK026 spawn each probe with `process.env` named, since bun's default is the env the process started with, blind to a later write such as a caller pointing `HOME` elsewhere.
79
+ - **Fix: the line SK009 asks a foreign commit-msg hook to add works on a clone without `node_modules`.** It reads `if [ -f <cli> ]; then <shipkit> hooks commit-msg "$1" || exit 1; else echo "… attribution gate skipped" >&2; fi`, so a fresh clone skips the gate with a note instead of failing every commit. The bare form and the multi line guarded form both still count as chained.
80
+ - **Fix: the runs page list never moves in.** The `reading the run record…` line sits inside the list box, where the list will be, so the list no longer drops 8px when it arrives (a refresh of the runs tab measured CLS 0.0205, now 0.0019).
81
+ - **The checks page and the asks page are tables.** One line per item on the activity table style instead of a card grid, under group headers that count their lines. A check line: status dot, id, title, age of its last result, duration against its budget, the first finding line (cut, every line on hover), and how it is planned (`on change`, `every 7d`, `once`, `every run`, `health pass`, `every release`, `off`); groups failing, warning, never ran, passing, off, most recent first. An ask line: kind, title, age, what it blocks, `auto close` or `manual`; open asks by what they block then oldest first, done ones after. Passing, off and done fold by default, a header click folds or opens, and the choice is kept per viewer (`hub:table-folds`). The filter input and the `a`, `n`, `r` keys narrow the lines. An ask resource now carries `ask: AskFacts` (kind, created, closed, blocks, probe), and `Ask` gains `closed`.
82
+ - **Every url on the overview opens three ways.** Before the url: open in Chrome, and open as the process's own app (a spinner while it builds). The url itself opens the default browser. The open pick in the url header is gone, and `MachineSt.openProcess` takes the way as its second argument.
83
+ - **The shipkit app finds and inspects.** Cmd-F opens the page's find bar (cmd-G, shift-cmd-G, Esc), alt-cmd-I the Web Inspector (Edit › Find, View › Developer Tools). The zoom keys stay.
84
+ - **Every global process with a url can open as its own app.** One Swift source serves the shipkit window and each process's app: the bundle's Info.plist names it and its start page. The app is built on first use into the declaring repo's `.shipkit/wrapper/<id>.app` (bundle id `dev.shipkit.app.<id>`, its image icon, else the repo's mark), rebuilt only when the source, the icon or the url changed. pm's resource route takes `chrome` (Chrome, else the default browser) beside `open` and `app`, and reports `appBusy` while an app builds.
85
+ - **Fix: no empty band at the bottom of every page.** The daemon or pm not answering is a toast floating over the bottom right corner of the page column, above the dock (`MachineToasts`, replacing `MachineBar`), drawn only while it lasts and taking no room.
86
+ - **The overview's process switches are small icons beside the label.** Play (stop while it runs) and restart sit in their own narrow columns right after the process name, every play under every play, each with a tooltip; open is an open external icon before the url, which stays a link. The new window button and the open choice sit in the url's header. New icons `play`, `stop`, `restart`, `external`.
87
+ - **The runs page opens on its tabs.** The record chart no longer stands over the tabs: it shows only in the `runs` tab, under its list. `test files to fix`, `slowest checks` and `longest runs` draw no chart.
88
+ - **Smaller monitor cards.** The `.wbar` meter is 40px, a meter's value cell takes the class `wv` (11px, tabular figures), and a widget card's padding and line height are tighter.
89
+ - **SK028 names both ways out.** Each finding asks for the screenshot, or for the rule turned off with its reason by the exact key: `when: { SK028: 'off:<why this repo shows no screenshot>' }` in repo.config.ts, which refuses an `off` with no reason.
90
+ - **SK016 says the real refusal condition and offers `--fix` only when it can fix.** The line reads `still read, and refused by the first shipkit release after \`shipkit fleet\` shows no repo below 0.40 and none still carrying a check script`, instead of `read through 0.40, refused after`. Its pointer says `shipkit check --fix SK016 rewrites N of them` while something is mechanical, and that the port is by hand when nothing is. A fix pointer that says `by hand` no longer gets the `· autofix:` suffix, the TODO placeholder warning included.
91
+ - **Fix: the overview never moves when the daemon or pm stops answering.** A process's note or a refused take over is a warn mark after its state words, the words on hover, instead of a line of its own. The daemon, the web or pm not answering reads `no answer` in its own row (row status `silent`, `CoreOpts.silent`), and while pm is silent every global process keeps its line with its state `unknown`. A widget error is a mark on its card's title. The table waits for pm's first answer and the list's order, so a cold load adds no row after its first paint. `bun run home:shift` now also refuses the daemon's and pm's answers in the browser and requires the table's height unchanged.
92
+ - **SK001 asks for `lint` and `fmt` only once the repo has a source file**, like `test` already waits for a test file. A source file is a code file outside installs, build output, dot dirs and `repo.config.ts`, with more in it than an empty `export {}`, or any test file.
93
+ - **Fix: the suite budget verdict is stable.** One continuous rule replaces the step at load == cores: the load is read before the run (never after this run's own parallel phase) and enters as a share of the cores; a suite is charged the smaller of its CPU and its wall, so bun's GC and JIT threads and side by side children are never billed as the suite's time, against a CPU allowance that grows with the load share up to 1.5 times the budget; a wait fails when the wall at a fair share of the machine passes twice the budget. A check's own budget warning (`passed in 12.0s, over its 10s budget`) is no longer printed while the load is at or past the core count, where the wall is the machine's. `SHIPKIT_MACHINE_LOAD=<load1>/<cores>` pins the reading for tests.
94
+ - **Fix: a `never:` check gets the subprocess budget.** A check that declares `never` reads the world like a `ttl` one, so its default budget is 10s instead of the 2s of an in process body: SK026, which runs each ask's own shell probe, no longer warns at 7s.
95
+ - **SK014 soft hits are a note.** With no `prose.baseline`, the soft classes print as one note, `N soft hit(s) need a human read (sentence 900, passive 400, modal 120, …)`, naming every soft class with its count, instead of a warning that named only two of them. Within a declared baseline it stays a note, above it the check fails with the same breakdown.
96
+ - **⌘J opens the picker and ⌘K the overview.** The lines on top of the project list read jump to… ⌘J, overview ⌘K, activity ⌘L, planned ⌘', and the picker lists home, activity and planned in that order.
97
+ - **SK009 checks the hook, not the history.** The commit-msg hook is what refuses AI attribution, so SK009 now checks in every repo that it is armed: `core.hooksPath` at `.shipkit/hooks`, or a foreign dir whose commit-msg chains the shipkit gate, and the finding prints the exact line to add (`<shipkit> hooks commit-msg "$1" || exit 1`). It no longer scans `git log`: what is pushed is accepted as it is. A declared-public repo still needs the `.claude/settings.json` silencer and a clean ref name; a telling branch nobody pushed warns as `local only` instead of failing. `hooks.disable: ['commit-message']` no longer lifts the attribution rule: it runs as its own `ai-attribution` gate, never disabled.
98
+ - **Fix: a gate never writes or commits in another repo.** `shipkit ci` and `check` used to rewrite the docsBase wiki's `local-projects.md` and commit it there ("chore(machine): refresh local-projects row"), so a narrowed gate in one repo landed commits in the house repo. They now file the row in the machine db only. The daemon's hourly sweep (`startSweeps` takes an `also`) runs `landMotherhome`, which writes every filed row into its page and lands it as one scoped commit, at most once a day per page (`LAND_GAP_MS`), with the same churn rule (data change or 7 days) and the same dead path pruning.
99
+ - **SK002 asks each contract only where it means something.** A public surface is a declared venue, `deploy.sh`, a GitHub Pages workflow, a wrangler config, or `visibility: 'public'` (unless an npm package is the repo's only surface). Before one exists the identity contract is silent instead of a warning, and after, it wants favicon, OG image and logo at the dir the deploy serves (a Pages artifact path, a wrangler output dir), overridden by the new `contracts.webRoot`, `public` otherwise; `shipkit assets identity` writes there too. An undeclared `.shipkit/social/` ships only once a post says `status: posted`: a folder of drafts is a note. A `discord` social channel no longer requires the discord server contract. Steam asks for `vdf/` only once `.shipkit/steam/steam_appid.txt` records the appid, and names that blocker before. Contract lines read `steam: …` instead of a double space. New detector `fileMatches(glob, pattern)`.
100
+ - **Fix: a remembered world answer says how old it is, never a clock time frozen into its text.** A `ttl` check's first line carried `(ran at 21:47:51, kept until 22:47:51)` in its stored text, which the next day still read as an expiry to come. The times now ride as data on the line (`Line.stamp`: `at`, `until`, `cached`), and every surface renders them against its own clock: `(ran just now, kept 1h more)`, `(cached, ran 3h ago, kept 57m more)`, `(ran 1d ago, expired 1d ago)`. `formatLine` takes an optional `now`, and `lineText`, `stampWords` and `spanShort` are exported from the wire module. An answer stored by an older build drops its baked times on replay.
101
+ - **Fix: every status surface shows every finding of a check, not the first one.** A failed build step that speaks the wire (the suite runner) keeps the FAIL lines its report closes on, and its headline counts them: `test — FAIL: 3 test(s) failed (+1 more below)`, the budget failure listed under it instead of hidden. The Activity page's hover list and `shipkit activity` list every fail and warn line of each check, the worst weight first (`StatusCheck.lines`), so SK009 shows its 17 attributed commits beside its ref name, and `tts` both of its warnings.
102
+ - **Fix: a non-blocking failure reads orange everywhere, as the gate prints it.** The run record filed a failure of a `blocking: false` check as `fail`, so Activity, the rail's dot and the check feed painted it red while the gate printed WARN and exited 0. The record now files it as `warn`, the status the gate reported.
103
+ - **Fix: SK027 no longer flags a working link.** A link that leaves `agent/` but stays in the repo (`../tools/readme.md`) is checked on disk and fails only when the file is really missing. A link to a folder (`[docs](external-docs/)`, or a backticked `agent/external-docs/`) reaches every page under it. An orphan page still fails. The dangling hint no longer tells anyone to backtick a working link. `scanMd` takes a `boundary`, the dir a ref may leave the scan root for.
104
+ - **SK027 says how to fix an orphan.** The finding asks for a markdown link from the page that should mention it, and names that page when one already names the file (its path, its file name, or a glob like `*_Compte_rendu_*`): `add a markdown link to it from agent/brief.md, which already names it`. Otherwise it points at `agent/index.md or the page of its topic`, and deleting comes second, for a dead page only. An `@./page.md` include now counts as a link.
105
+
1
106
  ## 1.3.0
2
107
 
3
108
  - **`shipkit ci` prints what the reader acts on, and nothing else.** A green run is one line per check that ran and one verdict line: `ok: test 920 tests (1.4s)` then `ok: 1 check green`, where it used to print nine. A test check carries its count on its own line. The whole gate folds its tier and cache counts into the verdict (`ok: 31 checks green; quick tier; 25 cached, 5 ran, 1 never cached`). A fix pass prints only when it rewrote files, and names them. A green check prints one line, a warned one only its warnings and their `fix:` pointers, a failure prints whole. `--only` says nothing about checks outside its selection (floor gaps, `off` checks, parked checks). `--verbose` restores every line the default leaves out: the clean fix pass, the machine pool, the tier, the cache declarations, the parked checks, the old summary and the cache counts. STATUS.md keeps them all, a release or bump gate still prints them all, and `--json` is unchanged.
package/README.md CHANGED
@@ -18,7 +18,7 @@ export default defineRepo({
18
18
  })
19
19
  ```
20
20
 
21
- Everything the gate runs beyond the floor is a CHECK, and a check needs no config at all: drop a `<name>.check.ts` file anywhere and `shipkit ci`, `shipkit check`, `shipkit checks` and the hub discover it.
21
+ Everything the gate runs beyond the floor is a CHECK, and a check needs no config at all: drop a `<name>.check.ts` file anywhere and `sk ci`, `sk check`, `sk checks` and the hub discover it.
22
22
 
23
23
  ```ts
24
24
  // e2e.check.ts
@@ -35,46 +35,46 @@ export default check({
35
35
  ## CLI
36
36
 
37
37
  ```
38
- shipkit ci commit gate: floor (lint/typecheck/test/imports/cycles —
38
+ sk ci commit gate: floor (lint/typecheck/test/imports/cycles —
39
39
  scripts win, imports/cycles/typecheck fill from bundled
40
40
  tools with zero config, gaps print visibly) + contracts
41
41
  + your checks, parallel DAG; refreshes STATUS.md
42
42
  [--bail --skip g --only id --json]
43
- shipkit contracts release-contract fulfillness alone [--venue v --json]
44
- shipkit check periodic health: the built-in checks + every *.check.ts in
43
+ sk contracts release-contract fulfillness alone [--venue v --json]
44
+ sk check periodic health: the built-in checks + every *.check.ts in
45
45
  the repo, auto-discovered + TTL-cached
46
46
  [--json --fresh --frozen --fix [SK### …]]
47
47
  --fix applies check autofixes then re-runs them; --frozen =
48
48
  self-maintaining checks (rvlib self-update) report only
49
- shipkit checks the inventory: every check, where it runs, blocks or warns,
49
+ sk checks the inventory: every check, where it runs, blocks or warns,
50
50
  and how it caches. --fixable = what repairs itself,
51
51
  with its owning doc [--json --md --out]
52
- shipkit tui live dashboard: build steps + every other check + panels
52
+ sk tui live dashboard: build steps + every other check + panels
53
53
  the repo injects via repo.config tui.panels
54
- shipkit asks human action queue: list · ask "<title>" · asks check (auto-close)
55
- shipkit needs everything blocked on a human, red/yellow by collection
54
+ sk asks human action queue: list · ask "<title>" · asks check (auto-close)
55
+ sk needs everything blocked on a human, red/yellow by collection
56
56
  report-only, exit 0 always [--json]
57
- shipkit wiki serve|lint — live md link-graph + orphan/dangling-ref lint
58
- shipkit mv rename an md page + its sibling dir, sweeping every ref
59
- shipkit assets serve|fill|add|gen|identity|check — the asset atelier
57
+ sk wiki serve|lint — live md link-graph + orphan/dangling-ref lint
58
+ sk mv rename an md page + its sibling dir, sweeping every ref
59
+ sk assets serve|fill|add|gen|identity|check — the asset atelier
60
60
  (declare kinds in .shipkit/assets/kit.ts, roll, curate, ship)
61
- shipkit pb collection <name> — scaffold a PocketBase collection's UI
61
+ sk pb collection <name> — scaffold a PocketBase collection's UI
62
62
  folder (columns/card/grid/form/index) from the app's
63
63
  generated pb types, create-only [--dry-run]
64
- shipkit smoke browser pass over a DEPLOYED web app, read only: login
64
+ sk smoke browser pass over a DEPLOYED web app, read only: login
65
65
  through the app's own form, every route in repo.config
66
66
  `smoke`, screenshots, console/network problems, the host
67
67
  quota headers, a detail page per `open` link. Exit code =
68
68
  failed routes. Needs playwright in the repo
69
69
  [--base <url> --routes /a,/b]
70
- shipkit serve the repo's ONE server: hub page, wikis, studio, status
71
- shipkit reflect your prompts across every Claude Code session of a repo:
70
+ sk serve the repo's ONE server: hub page, wikis, studio, status
71
+ sk reflect your prompts across every Claude Code session of a repo:
72
72
  all config roots, queued-mid-run ones included, harness
73
73
  noise out, chronological md corpus for a retrospective
74
74
  [repo…] [--short --out <dir> --json]
75
- shipkit init scaffold config + scripts, print the resolved floor
75
+ sk init scaffold config + scripts, print the resolved floor
76
76
  [--bin <cli.ts path> for in-repo-source consumers]
77
- shipkit start ensure <root>/<name> + its CLAUDE.md include; prints the dir
77
+ sk start ensure <root>/<name> + its CLAUDE.md include; prints the dir
78
78
  ```
79
79
 
80
80
  Output is a stable line protocol — `ok:` / `WARN` / `FAIL:` / `skip:` with indented `fix:` pointers — exit 0/1. Greppable by humans, parseable by agents, `--json` where structure helps.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "1.3.0",
3
+ "version": "1.5.0",
4
4
  "funding": "https://github.com/sponsors/rvion",
5
5
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
6
6
  "license": "MIT",
@@ -11,7 +11,8 @@
11
11
  "CHANGELOG.md"
12
12
  ],
13
13
  "bin": {
14
- "shipkit": "./src/cli.ts"
14
+ "shipkit": "./src/cli.ts",
15
+ "sk": "./src/cli.ts"
15
16
  },
16
17
  "exports": {
17
18
  ".": "./src/shipkit.ts",
@@ -9,7 +9,7 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
9
9
  import { atomicWrite, mutateLease, withMutateLease } from '../core/locked-write.ts'
10
10
  import { join } from 'path'
11
11
 
12
- export type Ask = { file: string; status: string; kind: string; created: string; blocks: string; check: string | null; title: string }
12
+ export type Ask = { file: string; status: string; kind: string; created: string; closed: string | null; blocks: string; check: string | null; title: string }
13
13
  export type CreateOpts = { kind?: string; blocks?: string; check?: string; why?: string }
14
14
 
15
15
  /** filename slug: kebab, truncated at a WORD boundary (never mid-word — a
@@ -31,6 +31,7 @@ const parse = (dir: string, file: string): Ask => {
31
31
  status: fm('status') ?? 'open',
32
32
  kind: fm('kind') ?? 'action',
33
33
  created: fm('created') ?? '?',
34
+ closed: fm('closed'),
34
35
  blocks: fm('blocks') ?? '-',
35
36
  check: fm('check'),
36
37
  title: md.match(/^# (.+)$/m)?.[1] ?? file,
@@ -73,7 +74,8 @@ function checkAsksBody(dir: string, print: (line: string) => void): { closed: nu
73
74
  for (const a of listAsks(dir)) {
74
75
  if (a.status === 'done') { renameSync(join(dir, a.file), join(done, a.file)); print(`archived ${a.file} → done/`); continue }
75
76
  if (a.check == null) { print(`still ${a.status.padEnd(8)} ${a.file} (manual — no probe)`); continue }
76
- const p = Bun.spawnSync(['bash', '-lc', a.check])
77
+ // env named: bun's default is the env the process started with, blind to a later process.env write
78
+ const p = Bun.spawnSync(['bash', '-lc', a.check], { env: process.env })
77
79
  if (p.exitCode === 0) {
78
80
  const md = readFileSync(join(dir, a.file), 'utf8').replace(/^status:.*$/m, `status: done\nclosed: ${new Date().toISOString().slice(0, 10)}`)
79
81
  atomicWrite(join(dir, a.file), md)
@@ -7,9 +7,9 @@
7
7
  // hook, so picking a logo roll re-renders the whole identity set from the
8
8
  // picked png. Headless `identity` uses the current pick — no pick yet is a
9
9
  // loud FAIL, never a placeholder.
10
- // Outputs are COMMITTED: public/favicon.png (512² master), public/og.png
11
- // (1200×630), public/logo.png. Brand data is per-repo and never defaults
12
- // across repos.
10
+ // Outputs are COMMITTED at the web root (public/ unless the deploy says
11
+ // otherwise): favicon.png (512² master), og.png (1200×630), logo.png. Brand
12
+ // data is per-repo and never defaults across repos.
13
13
  import { createCanvas, loadImage, type Image, type SKRSContext2D } from '@napi-rs/canvas'
14
14
  import { mkdirSync } from 'fs'
15
15
  import { dirname, join } from 'path'
@@ -81,11 +81,13 @@ const defaultOg = (ctx: SKRSContext2D, brand: Brand, size: { w: number; h: numbe
81
81
  }
82
82
 
83
83
  /** THE identity writer — any mark png (canvas draw or picked comfy roll) →
84
- * public/logo.png (1024² master) + public/favicon.png (512² resize) +
85
- * public/og.png (default card: bg + mark left + name/tagline right). */
86
- export async function renderIdentityFromMark(brand: Brand, root: string, markPng: Buffer): Promise<string[]> {
84
+ * <web>/logo.png (1024² master) + <web>/favicon.png (512² resize) +
85
+ * <web>/og.png (default card: bg + mark left + name/tagline right), where
86
+ * <web> is the dir the site serves (contracts/web-surface.ts, default public) */
87
+ export async function renderIdentityFromMark(brand: Brand, root: string, markPng: Buffer, webRoot = 'public'): Promise<string[]> {
87
88
  const out: string[] = []
88
- const write = async (rel: string, buf: Buffer): Promise<void> => {
89
+ const write = async (name: string, buf: Buffer): Promise<void> => {
90
+ const rel = webRoot === '.' ? name : `${webRoot}/${name}`
89
91
  const path = join(root, rel)
90
92
  mkdirSync(dirname(path), { recursive: true })
91
93
  await Bun.write(path, buf)
@@ -94,26 +96,26 @@ export async function renderIdentityFromMark(brand: Brand, root: string, markPng
94
96
  const logoPng = await sharp(markPng)
95
97
  .resize(LOGO_SIZE, LOGO_SIZE, { fit: 'contain', background: { r: 0, g: 0, b: 0, alpha: 0 } })
96
98
  .png().toBuffer()
97
- await write('public/logo.png', logoPng)
99
+ await write('logo.png', logoPng)
98
100
  // favicon = logo master resized to 512² (browsers derive the rest)
99
- await write('public/favicon.png', await sharp(logoPng).resize(FAVICON_SIZE, FAVICON_SIZE).png().toBuffer())
101
+ await write('favicon.png', await sharp(logoPng).resize(FAVICON_SIZE, FAVICON_SIZE).png().toBuffer())
100
102
  const og = createCanvas(OG_W, OG_H)
101
103
  const ctx = og.getContext('2d')
102
104
  if (brand.drawOg != null) brand.drawOg(ctx, brand, { w: OG_W, h: OG_H })
103
105
  else defaultOg(ctx, brand, { w: OG_W, h: OG_H }, await loadImage(logoPng))
104
- await write('public/og.png', og.toBuffer('image/png'))
106
+ await write('og.png', og.toBuffer('image/png'))
105
107
  return out
106
108
  }
107
109
 
108
110
  /** Headless render of the full identity set. Canvas marks draw themselves;
109
111
  * comfy marks need `markFile` = the PICKED roll (the caller resolves the
110
112
  * pick — run-assets does) and FAIL loud without one. */
111
- export async function renderIdentity(brand: Brand, root: string, opts?: { markFile?: string }): Promise<string[]> {
113
+ export async function renderIdentity(brand: Brand, root: string, opts?: { markFile?: string; webRoot?: string }): Promise<string[]> {
112
114
  if (brand.mark.backend === 'canvas') {
113
115
  const logo = createCanvas(LOGO_SIZE, LOGO_SIZE)
114
116
  await brand.mark.draw(logo.getContext('2d'), { id: 'identity-logo', prompt: '' })
115
- return renderIdentityFromMark(brand, root, logo.toBuffer('image/png'))
117
+ return renderIdentityFromMark(brand, root, logo.toBuffer('image/png'), opts?.webRoot)
116
118
  }
117
119
  if (opts?.markFile == null) throw new Error(`brand '${brand.name}' uses a comfy mark and no roll is picked — ${NO_PICK_FIX}`)
118
- return renderIdentityFromMark(brand, root, await sharp(opts.markFile).png().toBuffer())
120
+ return renderIdentityFromMark(brand, root, await sharp(opts.markFile).png().toBuffer(), opts.webRoot)
119
121
  }
@@ -8,6 +8,9 @@ import { comfyResolver, generateRoll, type ComfyResolver } from './gen/generate-
8
8
  import { parentIdsOf, resolveParentContext, topoSort, type ParentContext } from './gen/derived.ts'
9
9
  import { allAssets, findAsset, loadKit, type ComfyGen, type ResolvedKit } from './kit-config.ts'
10
10
  import { NO_PICK_FIX, renderIdentity, type Brand } from './kits/identity-kit.ts'
11
+ import { webDeploys, webRootOf } from '../contracts/web-surface.ts'
12
+ import { CheckCtx } from '../core/check-ctx.ts'
13
+ import { defineRepo, loadRepoConfig } from '../repo-config.ts'
11
14
  import { assetDir, listRolls, readPick, readPromptOverlay } from './roll-workspace.ts'
12
15
 
13
16
  export type Print = (line: string) => void
@@ -111,6 +114,12 @@ const brandHashPath = (root: string): string => join(root, '.shipkit/identity/br
111
114
  const brandHash = async (root: string): Promise<string> =>
112
115
  new Bun.CryptoHasher('sha256').update(await Bun.file(join(root, '.shipkit/identity/brand.ts')).text()).digest('hex').slice(0, 16)
113
116
 
117
+ /** the dir the identity outputs go to: the same answer SK002 reads */
118
+ async function repoWebRoot(root: string): Promise<string> {
119
+ const config = existsSync(join(root, 'repo.config.ts')) ? await loadRepoConfig(root) : defineRepo({ name: 'repo' })
120
+ return webRootOf(config.contracts?.webRoot, webDeploys(CheckCtx.open({ root, config })))
121
+ }
122
+
114
123
  export type IdentityRender = { written: string[] } | { blocked: string }
115
124
 
116
125
  export async function renderIdentityNow(root: string): Promise<IdentityRender> {
@@ -133,7 +142,7 @@ export async function renderIdentityNow(root: string): Promise<IdentityRender> {
133
142
  markFile = join(dir, pick.pick)
134
143
  if (!existsSync(markFile)) return { blocked: `picked roll ${pick.pick} vanished from ${dir} — re-pick in the studio` }
135
144
  }
136
- const written = await renderIdentity(brand, root, { markFile })
145
+ const written = await renderIdentity(brand, root, { markFile, webRoot: await repoWebRoot(root) })
137
146
  await Bun.write(brandHashPath(root), (await brandHash(root)) + '\n')
138
147
  return { written }
139
148
  }
@@ -142,7 +151,8 @@ export async function renderIdentityNow(root: string): Promise<IdentityRender> {
142
151
  * Returns what happened; 'blocked' = only a human pick can clear it. */
143
152
  export async function healIdentity(root: string): Promise<{ state: 'fresh' | 'rendered' | 'blocked' | 'absent'; detail?: string }> {
144
153
  if (!existsSync(join(root, '.shipkit/identity/brand.ts'))) return { state: 'absent' }
145
- const outputsMissing = ['public/favicon.png', 'public/og.png', 'public/logo.png'].some((p) => !existsSync(join(root, p)))
154
+ const web = await repoWebRoot(root)
155
+ const outputsMissing = ['favicon.png', 'og.png', 'logo.png'].some((p) => !existsSync(join(root, web, p)))
146
156
  const stored = existsSync(brandHashPath(root)) ? (await Bun.file(brandHashPath(root)).text()).trim() : null
147
157
  if (!outputsMissing && stored === (await brandHash(root))) return { state: 'fresh' }
148
158
  const r = await renderIdentityNow(root)