rvlib-shipkit 1.4.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 (161) hide show
  1. package/CHANGELOG.md +68 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/check.ts +68 -95
  5. package/src/checks/SK001-package-json-shape.check.ts +6 -2
  6. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  7. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  8. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  9. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  10. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  11. package/src/checks/SK007-github-visibility.check.ts +4 -2
  12. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  13. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  14. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  15. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  16. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  17. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  18. package/src/checks/SK014-prose-ste.check.ts +4 -0
  19. package/src/checks/SK015-nested-repo.check.ts +4 -2
  20. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  21. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  22. package/src/checks/SK018-test-files-run.check.ts +6 -2
  23. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  24. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  25. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  26. package/src/checks/SK024-identity-colors.check.ts +5 -3
  27. package/src/checks/SK025-release-drift.check.ts +4 -5
  28. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  29. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  30. package/src/checks/SK028-readme.check.ts +5 -1
  31. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  32. package/src/cli/cmd_Asks.ts +5 -5
  33. package/src/cli/cmd_Assets.ts +7 -7
  34. package/src/cli/cmd_Bump.ts +12 -11
  35. package/src/cli/cmd_Check.ts +8 -7
  36. package/src/cli/cmd_Checks.ts +7 -6
  37. package/src/cli/cmd_Ci.ts +12 -11
  38. package/src/cli/cmd_Commands.ts +16 -13
  39. package/src/cli/cmd_ConfigKeys.ts +7 -6
  40. package/src/cli/cmd_Contracts.ts +3 -3
  41. package/src/cli/cmd_CrossRepoActivity.ts +6 -5
  42. package/src/cli/cmd_Fleet.ts +9 -7
  43. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  44. package/src/cli/cmd_Home.ts +23 -17
  45. package/src/cli/cmd_Hooks.ts +6 -5
  46. package/src/cli/cmd_Init.ts +5 -4
  47. package/src/cli/cmd_Keys.ts +8 -7
  48. package/src/cli/cmd_Mv.ts +7 -6
  49. package/src/cli/cmd_Needs.ts +4 -3
  50. package/src/cli/cmd_Overview.ts +4 -3
  51. package/src/cli/cmd_Pb.ts +2 -2
  52. package/src/cli/cmd_Planned.ts +6 -5
  53. package/src/cli/cmd_Prose.ts +5 -5
  54. package/src/cli/cmd_Reflect.ts +7 -7
  55. package/src/cli/cmd_Release.ts +6 -5
  56. package/src/cli/cmd_Releases.ts +5 -4
  57. package/src/cli/cmd_Serve.ts +4 -3
  58. package/src/cli/cmd_Smoke.ts +5 -4
  59. package/src/cli/cmd_Start.ts +7 -6
  60. package/src/cli/cmd_Status.ts +4 -3
  61. package/src/cli/cmd_Tui.ts +3 -2
  62. package/src/cli/cmd_When.ts +71 -0
  63. package/src/cli/cmd_Wiki.ts +4 -4
  64. package/src/cli.ts +7 -2
  65. package/src/collections/checks.ts +2 -0
  66. package/src/collections/collection-types.ts +9 -4
  67. package/src/core/canonical-scripts.ts +1 -1
  68. package/src/core/check-runner.ts +37 -20
  69. package/src/core/config-rewrite.ts +37 -24
  70. package/src/core/discover-checks.ts +23 -2
  71. package/src/core/legacy-shapes.ts +39 -10
  72. package/src/core/one-way-in-policy.ts +2 -2
  73. package/src/core/outcome-words.ts +18 -2
  74. package/src/core/port-check-cli.ts +52 -0
  75. package/src/core/port-check-file.ts +295 -0
  76. package/src/core/port-check-files.ts +31 -0
  77. package/src/core/retention.ts +0 -14
  78. package/src/core/run-check.ts +12 -9
  79. package/src/core/run-ci.ts +8 -4
  80. package/src/core/run-fix.ts +30 -23
  81. package/src/core/run-fixers.ts +42 -5
  82. package/src/core/runs-matrix.ts +179 -0
  83. package/src/core/runs.ts +242 -0
  84. package/src/core/update-policy.ts +20 -0
  85. package/src/core/when-block.ts +29 -30
  86. package/src/core/when.ts +41 -103
  87. package/src/core/wire.ts +10 -14
  88. package/src/fleet/scan-fleet.ts +20 -4
  89. package/src/fleet/update-fleet.ts +6 -6
  90. package/src/home/home-launchd.ts +20 -4
  91. package/src/home/home-routes.ts +12 -1
  92. package/src/home/hubs.ts +5 -5
  93. package/src/home/pm.ts +8 -0
  94. package/src/home/run-pm.ts +1 -0
  95. package/src/home/serve-web.ts +2 -2
  96. package/src/home/supervise.ts +24 -1
  97. package/src/hooks/run-hooks.ts +48 -4
  98. package/src/init-repo.ts +6 -6
  99. package/src/ledger/gate-checks.ts +113 -59
  100. package/src/ledger/git-key.ts +5 -1
  101. package/src/ledger/ledger.ts +5 -5
  102. package/src/ledger/toolchain-identity.ts +45 -27
  103. package/src/plan/repo-source.ts +18 -39
  104. package/src/release/run-release.ts +5 -0
  105. package/src/repo-config.ts +30 -55
  106. package/src/rules/rule.ts +2 -2
  107. package/src/scaffold/files/agent-coding.md +1 -1
  108. package/src/scaffold/files/agent-index.md +1 -1
  109. package/src/scaffold/files/verify-skill.md +2 -2
  110. package/src/serve/hub-app/activity-st.ts +5 -4
  111. package/src/serve/hub-app/activity-view.tsx +1 -1
  112. package/src/serve/hub-app/admin-view.tsx +13 -11
  113. package/src/serve/hub-app/check-view.tsx +66 -46
  114. package/src/serve/hub-app/check-words.ts +1 -11
  115. package/src/serve/hub-app/checks-svc.ts +14 -35
  116. package/src/serve/hub-app/col-tables-view.tsx +13 -6
  117. package/src/serve/hub-app/cols-svc.ts +5 -4
  118. package/src/serve/hub-app/dock-st.ts +3 -2
  119. package/src/serve/hub-app/dock-view.tsx +36 -0
  120. package/src/serve/hub-app/dock.css +6 -0
  121. package/src/serve/hub-app/float-cards.tsx +30 -0
  122. package/src/serve/hub-app/floating.tsx +41 -4
  123. package/src/serve/hub-app/git-st.ts +42 -0
  124. package/src/serve/hub-app/git-view.tsx +61 -0
  125. package/src/serve/hub-app/hash-route.ts +45 -1
  126. package/src/serve/hub-app/header-bar.tsx +62 -72
  127. package/src/serve/hub-app/health-svc.ts +3 -2
  128. package/src/serve/hub-app/home-st.ts +14 -2
  129. package/src/serve/hub-app/home-strip.css +43 -8
  130. package/src/serve/hub-app/home-strip.tsx +111 -47
  131. package/src/serve/hub-app/home.html +1 -0
  132. package/src/serve/hub-app/hub-app.tsx +14 -4
  133. package/src/serve/hub-app/hub-types.ts +5 -2
  134. package/src/serve/hub-app/hub.css +65 -43
  135. package/src/serve/hub-app/icons.tsx +11 -0
  136. package/src/serve/hub-app/index.html +1 -0
  137. package/src/serve/hub-app/keys.ts +5 -2
  138. package/src/serve/hub-app/live-svc.ts +3 -2
  139. package/src/serve/hub-app/machine-st.ts +18 -22
  140. package/src/serve/hub-app/plan-view.tsx +51 -0
  141. package/src/serve/hub-app/planned-st.ts +9 -4
  142. package/src/serve/hub-app/processes-view.tsx +38 -0
  143. package/src/serve/hub-app/projects-svc.ts +15 -10
  144. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  145. package/src/serve/hub-app/reach.ts +36 -0
  146. package/src/serve/hub-app/readme-svc.ts +3 -2
  147. package/src/serve/hub-app/repo-sections.ts +117 -0
  148. package/src/serve/hub-app/repo-st.ts +46 -9
  149. package/src/serve/hub-app/sidebar.tsx +12 -37
  150. package/src/serve/hub-app/timeline-svc.ts +3 -2
  151. package/src/serve/hub-app/tree-build.ts +149 -155
  152. package/src/serve/hub-app/tree-row.tsx +39 -9
  153. package/src/serve/hub-app/tree-svc.ts +70 -49
  154. package/src/serve/hub-app/when-view.tsx +89 -0
  155. package/src/serve/hub-app/when.css +17 -0
  156. package/src/serve/hub-runtime.ts +3 -16
  157. package/src/serve/run-serve.ts +1 -1
  158. package/src/serve/serve-hub.ts +4 -14
  159. package/src/serve/hub-app/processes-svc.ts +0 -63
  160. package/src/serve/hub-app/repo-sections.tsx +0 -61
  161. package/src/serve/repo-processes.ts +0 -89
package/CHANGELOG.md CHANGED
@@ -1,3 +1,71 @@
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
+
1
69
  ## 1.4.0
2
70
 
3
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.
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.4.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",
package/src/check.ts CHANGED
@@ -14,14 +14,14 @@
14
14
  // `cmd` argv, a subprocess, exit code is truth
15
15
  //
16
16
  // What a check declares about ITSELF is what every surface reads: `blocking`
17
- // (can it stop you), how it is remembered (`ttl` or `never`, else a `run`
18
- // check is keyed on what it read and a `cmd` check on its `inputs`), `when`
19
- // (where it runs), `deps` (what must pass first). The file's own path joins
20
- // its cache key automatically, so nothing declares its own code.
17
+ // (can it stop you), `runs` (every context it runs in), `cache` (how its
18
+ // answer is kept), `fix` (which command applies its repair), `applies` (does
19
+ // this repo give it work), `deps` (what must pass first). Every one of them is
20
+ // explicit, with no default (core/runs.ts). The file's own path joins its
21
+ // cache key automatically, so nothing declares its own code.
21
22
  import type { CheckCtx } from './core/check-ctx.ts'
22
- import { parseWhen } from './core/when.ts'
23
- import { planDeclProblem, type Every, type WhenInactiveDecl } from './plan/cadence.ts'
24
- import { FOREVER, parseDuration, type Line } from './core/wire.ts'
23
+ import { cacheProblem, combinationProblem, runsProblem, type Applies, type CacheDecl, type FixWhen, type Runs } from './core/runs.ts'
24
+ import { type Line } from './core/wire.ts'
25
25
  import type { RepoConfig } from './repo-config.ts'
26
26
 
27
27
  export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
@@ -77,7 +77,7 @@ export type CheckDef = {
77
77
  * gate asks "is my change good" and builds, `shipkit check` asks "is
78
78
  * this repo healthy" and reports, and a tool chain has no business in
79
79
  * the second question. A floor slot derives it, a file declares it, and
80
- * it takes the 600s ceiling rather than the 120s one. `when` stays about
80
+ * it takes the 600s ceiling rather than the 120s one. its `runs` say
81
81
  * TIME (always, release, health, off). A build step on `health` is an
82
82
  * opt in one: the gate parks it and `shipkit ci --with <id>` runs it. */
83
83
  build?: boolean
@@ -85,74 +85,34 @@ export type CheckDef = {
85
85
  * so a check naming none is a root of it, and a dep doubles as the race
86
86
  * guard between two checks that touch one file. */
87
87
  deps?: string[]
88
- /** WHERE it runs: `always`, `release`, `health`, `off:<reason>`. The
89
- * repo's `when: {}` map wins over it, so a config can move a check the
90
- * repo does not own (core/when.ts owns the vocabulary). */
91
- when?: string
92
- /** CONTENT population: the repo-relative paths this check reads, so a
93
- * cached answer is honest while they do not move. For a `cmd` check,
94
- * whose subprocess reads what it likes. A `run` check that declares none
95
- * of `inputs`, `ttl` and `never` is keyed on what it READ through `c`
96
- * instead, the `reads` population (ledger/git-key.ts). A `cmd` check
97
- * that declares none runs every time, which costs speed and never
98
- * correctness. At most one of the three. */
99
- inputs?: string[]
100
- /** WORLD population: nothing on disk describes the answer, so a clean
101
- * pass keeps for this long, as in '24h'. `'forever'` for a DECIDED fact
102
- * (a repo's visibility, an account that exists): a clean answer never
103
- * expires, it is keyed on the declared value alone (`cacheKey`), and the
104
- * plan runs it only while no answer exists for that key. */
105
- ttl?: string
106
- /** a DECIDED never-cacheable, with the reason. Says somebody looked and
107
- * no key can describe this, which takes it off the burn-down list. */
108
- never?: string
88
+ /** WHERE IT RUNS, every context explicit, no default (core/runs.ts owns
89
+ * the meaning of each key and each value). A repo moves a check it does
90
+ * not own OFF with `off` in repo.config.ts, never elsewhere. */
91
+ runs: Runs
92
+ /** HOW ITS ANSWER IS KEPT, one variant, every parameter explicit:
93
+ * `files` (keyed on these paths), `reads` (keyed on what the body read),
94
+ * `ttl` (a world answer that keeps a span), `fact` (a decided value, once
95
+ * per key), `never` (with the reason). The file's own path joins every
96
+ * key automatically, so nothing declares its own code. */
97
+ cache: CacheDecl
98
+ /** which command applies `repair`: `'none'` (no repair), `'ci'` (the
99
+ * gate's fix pass and `shipkit check --fix`), `'manual'` (only
100
+ * `shipkit check --fix <id>`) */
101
+ fix: FixWhen
102
+ /** whether this repo gives the check anything to do: `'always'`, or a
103
+ * pure function of { root, config } returning the reason it does not
104
+ * (the row is then n/a in every context), or null */
105
+ applies: Applies
109
106
  /** it REWRITES the repo while it runs (updates a dependency, untracks a
110
107
  * file) unless the run is frozen, so a frozen run asks it a different
111
108
  * question: only a check declaring this carries `frozen` in its cache
112
- * key, and every other check shares one answer between a frozen run and
113
- * a normal one. Declared, never guessed from the body. */
109
+ * key. Declared, never guessed from the body. */
114
110
  writes?: boolean
115
- /** WHEN the planner runs it on its own, beside the save that moved what
116
- * it reads: a span since its last run (`'24h'`), a range (`['10h',
117
- * '12h']`, each repo and check on a stable point inside it, so checks
118
- * spread out), a cron expression (`'0 9 * * 1'`), `'once'` (only while
119
- * no answer exists: what `ttl: 'forever'` reads as), or null for on
120
- * change only. Or a PURE function of its last result (`status`, `at`, `streak`
121
- * of that same status) and whether the repo is active, returning any of
122
- * those: `(last) => (last?.status === 'fail' ? '1h' : '7d')`. A lambda
123
- * that throws is an error row on the planned page, and the check runs on
124
- * change only. Absent: its `ttl`, else a build step or a check keyed on
125
- * its files waits for a save. A repo check nothing can key runs about
126
- * once a day until it declares one, and a built-in never may: `shipkit
127
- * keys --burn-down` names both. */
128
- every?: Every
129
- /** a time of day or week for the rare check that needs one, as five cron
130
- * fields in local time (`'0 9 * * 1'`, Mondays at 9). Never with `every`. */
131
- cron?: string
132
- /** what a due check does while its repo is inactive (no agent session in
133
- * it, VS Code does not have it open, it is not flagged active): `'wait'`
134
- * turns it late and runs it once the repo is active again, `'run'` runs
135
- * it on time anyway (a credential probe, an api key that must keep
136
- * working). A pure function of the last result may choose. Default
137
- * `'wait'`. */
138
- whenInactive?: WhenInactiveDecl
139
- /** why this repo gives the check nothing to do on a clock, or null: the
140
- * plan leaves it out there (no row, no timer), and a gate or a health
141
- * pass still runs it. Read at every reload of the repo's checks, so a
142
- * file that gives it work (an ask with a probe, a release unit) plans it
143
- * again. Cheap and pure over the repo's files and config */
144
- planOff?: (env: CheckEnv) => string | null
145
- /** cache-identity contribution for a `ttl` check whose verdict depends on
146
- * config: a changed key busts the answer instantly instead of serving
147
- * a stale cached assertion for the rest of the TTL. */
148
- cacheKey?: (env: CheckEnv) => string
149
- /** idempotent auto-repair, never interactive. `shipkit check --fix <id>`
150
- * applies it, then re-runs the check. It gets the same `c` as the body,
151
- * writes with `c.write` and `c.remove`, and is never cached. An `fs`
152
- * import in the check file would keep an undeclared body off its reads
153
- * (core/raw-access.ts). A repair that needs
154
- * judgement stays a FAIL with a pointer. */
155
- fix?: (c: CheckCtx) => FixResult | Promise<FixResult>
111
+ /** idempotent auto-repair, never interactive, required when `fix` is not
112
+ * `'none'`. It gets the same `c` as the body, writes with `c.write` and
113
+ * `c.remove`, and is never cached. A repair that needs judgement stays a
114
+ * FAIL with a pointer. */
115
+ repair?: (c: CheckCtx) => FixResult | Promise<FixResult>
156
116
  /** wall clock ceiling in ms. A `cmd` check is killed with its whole
157
117
  * process tree at the ceiling. */
158
118
  timeoutMs?: number
@@ -160,7 +120,7 @@ export type CheckDef = {
160
120
  * passes over it prints a WARN and reads yellow, and never blocks.
161
121
  * Judged on the wall time of a run that measured, at the quick tier.
162
122
  * Default 2000 for an in process body, 10000 for a build step, a `cmd`
163
- * or a world probe (one that declares `ttl` or `never`, core/budget.ts).
123
+ * or a world probe (cache `ttl`, `fact` or `never`, core/budget.ts).
164
124
  * The repo's `budgets` wins over it. */
165
125
  budgetMs?: number
166
126
  /** a label several checks share, so `--skip <group>` parks all of them
@@ -198,6 +158,19 @@ export function describeFirst(...candidates: (string | null | undefined)[]): str
198
158
  throw new Error(`no description candidate passes the shape: ${candidates.map((d) => JSON.stringify(d)).join(', ')}`)
199
159
  }
200
160
 
161
+ /** the keys the explicit declaration replaced, and what replaces each */
162
+ const RETIRED_KEYS: Record<string, string> = {
163
+ when: 'runs (every context explicit)',
164
+ every: 'runs.onClock { every, inactive }',
165
+ cron: 'runs.onClock { every: <cron>, inactive }',
166
+ whenInactive: 'runs.onClock.inactive',
167
+ planOff: 'applies',
168
+ inputs: "cache { by: 'files', inputs, key }",
169
+ ttl: "cache { by: 'ttl', span, key } or { by: 'fact', key }",
170
+ never: "cache { by: 'never', why }",
171
+ cacheKey: 'cache.key',
172
+ }
173
+
201
174
  /** THE way a check file declares itself. It validates the shape at import
202
175
  * time, so a malformed check dies when discovery loads it and never mid
203
176
  * gate. The id, the title and the owner are filled in by discovery, which
@@ -208,20 +181,21 @@ export function check(d: CheckDef): CheckDef {
208
181
  const bad = descriptionProblem(d.description)
209
182
  if (bad != null) throw new Error(`check${name}: ${bad}`)
210
183
  if (typeof d.blocking !== 'boolean')
211
- throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: blocking is required — true (about this repo, it stops you) or false (about the world, it warns)`)
184
+ throw new Error(`check${name}: blocking is required — true (about this repo, it stops you) or false (about the world, it warns)`)
212
185
  if (has('run') === has('cmd'))
213
- throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: declare exactly one body, \`run\` (in process) or \`cmd\` (argv)`)
214
- const populations = (['inputs', 'ttl', 'never'] as const).filter((k) => has(k))
215
- if (populations.length > 1)
216
- throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: ${populations.join(' and ')} contradict each other — a check reads files, or the world, or nothing can describe it`)
217
- if (d.ttl != null && d.ttl !== FOREVER)
218
- try { parseDuration(d.ttl) } catch { throw new Error(`check${name}: ttl '${d.ttl}' is neither a span ('7d') nor 'forever'`) }
219
- const planBad = planDeclProblem({ every: d.every, cron: d.cron, whenInactive: d.whenInactive })
220
- if (planBad != null) throw new Error(`check${name}: ${planBad}`)
221
- if (d.when != null) {
222
- const w = parseWhen(d.when)
223
- if (typeof w === 'string') throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: when ${w}`)
224
- }
186
+ throw new Error(`check${name}: declare exactly one body, \`run\` (in process) or \`cmd\` (argv)`)
187
+ const raw = d as unknown as Record<string, unknown>
188
+ const retired = Object.keys(RETIRED_KEYS).filter((k) => raw[k] !== undefined)
189
+ if (retired.length > 0) throw new Error(`check${name}: ${retired.map((k) => `\`${k}\` is retired, declare ${RETIRED_KEYS[k]}`).join('; ')} (build/shipkit/runs.md)`)
190
+ if (typeof raw.fix === 'function') throw new Error(`check${name}: \`fix\` is now the word 'none' | 'ci' | 'manual', and the function is \`repair\` (build/shipkit/runs.md)`)
191
+ const runs = runsProblem(d.runs)
192
+ if (runs != null) throw new Error(`check${name}: ${runs}`)
193
+ const cache = cacheProblem(d.cache)
194
+ if (cache != null) throw new Error(`check${name}: ${cache}`)
195
+ if (d.applies !== 'always' && typeof d.applies !== 'function')
196
+ throw new Error(`check${name}: applies is required: 'always', or a function of { root, config } returning why this repo gives it nothing to do, or null`)
197
+ const whole = combinationProblem({ runs: d.runs, cache: d.cache, fix: d.fix, repair: d.repair, build: d.build === true, body: has('run') ? 'run' : 'cmd', blocking: d.blocking })
198
+ if (whole != null) throw new Error(`check${name}: ${whole}`)
225
199
  return d
226
200
  }
227
201
 
@@ -233,7 +207,7 @@ export function check(d: CheckDef): CheckDef {
233
207
 
234
208
  export type TaskDef = {
235
209
  /** the name every surface uses: the gate line, `--only`, `--skip`, a
236
- * `deps` entry, a `when` key and the ledger row. Unique in the repo. */
210
+ * `deps` entry, an `off` key and the ledger row. Unique in the repo. */
237
211
  id: string
238
212
  /** one line stating what must be true (the shape: descriptionProblem).
239
213
  * A floor slot and a unit test check get theirs from the
@@ -255,7 +229,7 @@ export type TaskDef = {
255
229
  /** a label several tasks share, so `--skip <group>` parks all of them at
256
230
  * once (a local bench, a slow integration tier). */
257
231
  group?: string
258
- /** DEPRECATED, read into `when` as `health`: out of the gate until
232
+ /** DEPRECATED, read as every gate axis false: out of the gate until
259
233
  * `--with <id>` names it. */
260
234
  optIn?: boolean
261
235
  /** wall clock ceiling in ms, default 600000 for a build step. One that
@@ -333,13 +307,12 @@ export type Rule = {
333
307
 
334
308
  // ── running a body, and what every line it prints carries ────────────────
335
309
 
336
- /** the config part of a check's cache identity. Where it RUNS is part of it:
337
- * moving one off the gate, or editing the reason it is off, has to ask the
338
- * question again rather than serve the cached answer from before the move. */
339
- export function checkCacheExtra(u: { id: string; cacheKey?: (env: CheckEnv) => string }, env: CheckEnv, at: { at: string; reason: string | null; source: string }): string | undefined {
340
- const key = u.cacheKey?.(env)
341
- if (at.source === 'default') return key
342
- return at.at === 'off' ? `${key ?? ''}|exempt:${at.reason ?? ''}` : `${key ?? ''}|when:${at.at}`
310
+ /** the config part of a check's cache identity: its declared key, and the
311
+ * reason it is off. Turning a check off, or editing why, asks the question
312
+ * again rather than serving the answer from before. Where it runs is not
313
+ * part of it: the answer to one question does not depend on who asks. */
314
+ export function checkCacheExtra(key: string | undefined, off: string | null): string | undefined {
315
+ return off == null ? key : `${key ?? ''}|exempt:${off}`
343
316
  }
344
317
 
345
318
  /** THE one way to run an in-process body (the check suite and the `--fix`
@@ -5,7 +5,7 @@
5
5
  // what it read: package.json, the probes below, the test file listing, and
6
6
  // whatever the floor resolution read.
7
7
  //
8
- // It also enforces the package.json half of ONE WAY IN (build/shipkit.md):
8
+ // It also enforces the package.json half of ONE WAY IN (build/shipkit/one-way-in.md):
9
9
  // `test`, `lint` and `typecheck` ROUTE to `shipkit ci --only <id>` and the
10
10
  // tool body they used to hold sits at `<id>:raw`. That is what makes a cache
11
11
  // worth having: `bun run test` is the reflex, and the reflex has to land in
@@ -197,12 +197,16 @@ export const packageJsonShape = check({
197
197
  title: 'package.json',
198
198
  description: 'package.json routes lint, typecheck and test through the gate',
199
199
  owner: OWNER,
200
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
201
+ cache: { by: 'reads' },
202
+ fix: 'ci',
203
+ applies: 'always',
200
204
  run: (c) => evaluatePackageJson(c),
201
205
  // mechanical repairs only: field flips + scripts whose canonical body the
202
206
  // vocabulary page states. Bodies it does NOT state (assets, comfy:up) are
203
207
  // named in the summary, never invented. rvlib-* roots untouched — their
204
208
  // license/private are publish decisions.
205
- fix: (c): FixResult => {
209
+ repair: (c): FixResult => {
206
210
  const raw = c.read('package.json')
207
211
  if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
208
212
  if (rootPublishesNpm(c.config)) return { changed: false, summary: 'published to npm: its package.json is its public contract, never rewritten' }
@@ -16,6 +16,10 @@ export const contractsFulfillness = check({
16
16
  owner: 'meta/contracts.md',
17
17
  // keyed on what the contracts read: each artifact, each detector, and each
18
18
  // folder a glob walked through `c.entries`, a gitignored one included
19
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
20
+ cache: { by: 'reads' },
21
+ fix: 'none',
22
+ applies: 'always',
19
23
  run: async (c) => {
20
24
  applyContractsResult(c, await c.use(contractsResult), contractsFulfillness)
21
25
  },
@@ -20,6 +20,10 @@ export const claudeMdShape = check({
20
20
  owner: 'meta/agent-folder.md',
21
21
  // keyed on what it reads: CLAUDE.md and whether agent/index.md exists. The
22
22
  // gitignore probe runs git, so an answer that took it is never stored
23
+ runs: { onPreCommit: { budgetMs: 40 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
24
+ cache: { by: 'reads' },
25
+ fix: 'ci',
26
+ applies: 'always',
23
27
  run: async (c) => {
24
28
  const hasAgent = c.exists('agent/index.md')
25
29
  const claude = c.read('CLAUDE.md')
@@ -53,7 +57,7 @@ export const claudeMdShape = check({
53
57
  // CLAUDE.md would lint green with the base include forever missing,
54
58
  // trading a loud FAIL for a permanent silent gap. Creation and prose
55
59
  // relocation stay human/machine-local (`shipkit start --include`).
56
- fix: (c): FixResult => {
60
+ repair: (c): FixResult => {
57
61
  if (!c.exists('agent/index.md')) return { changed: false, summary: 'no agent/index.md — nothing mechanical to fix' }
58
62
  const cur = c.read('CLAUDE.md')
59
63
  if (cur == null) return { changed: false, summary: 'CLAUDE.md missing — creation needs the base include (shipkit start <name> writes it), not autofixable' }
@@ -205,11 +205,14 @@ export const gitignorePolicy = check({
205
205
  title: '.gitignore',
206
206
  description: 'the managed .gitignore block keeps private contract folders out of git',
207
207
  owner: 'meta/contracts.md',
208
- // .gitignore plus what git currently TRACKS and IGNORES, which the tree
209
- // carries and no read set records (the header says why)
210
- inputs: ['.'],
211
208
  // it untracks generated files (a commit) unless the run is frozen
212
209
  writes: true,
210
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
211
+ // .gitignore plus what git currently TRACKS and IGNORES, which the tree
212
+ // carries and no read set records (the header says why)
213
+ cache: { by: 'files', inputs: ['.'], key: null },
214
+ fix: 'ci',
215
+ applies: 'always',
213
216
  run: async (c) => {
214
217
  // a generated path still in the index is the half-applied state the
215
218
  // managed block cannot fix on its own
@@ -278,7 +281,7 @@ export const gitignorePolicy = check({
278
281
  }
279
282
  c.ok(`.gitignore policy block current, ${claimed.length + backProbes.size} patterns verified against git`)
280
283
  },
281
- fix: async (c): Promise<FixResult> => {
284
+ repair: async (c): Promise<FixResult> => {
282
285
  // the move first: it writes the new block itself, before any folder moves
283
286
  const moved = process.env.SHIPKIT_FROZEN === '1' ? null : migrateLayout(c.root, { config: c.config, renderGitignore: (t, legacy, cfg) => renderPolicy(t, cfg ?? c.config, legacy) })
284
287
  const layout = moved == null ? null