rvlib-shipkit 1.4.0 → 1.6.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 (199) hide show
  1. package/CHANGELOG.md +114 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/assets/gen/comfyts-gen.ts +12 -8
  5. package/src/assets/server/studio-server.ts +14 -2
  6. package/src/check.ts +73 -103
  7. package/src/checks/SK001-package-json-shape.check.ts +11 -6
  8. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  9. package/src/checks/SK003-claude-md-shape.check.ts +7 -3
  10. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  11. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  12. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  13. package/src/checks/SK007-github-visibility.check.ts +4 -2
  14. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  15. package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
  16. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  17. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  18. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  19. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  20. package/src/checks/SK014-prose-ste.check.ts +4 -0
  21. package/src/checks/SK015-nested-repo.check.ts +4 -2
  22. package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
  23. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  24. package/src/checks/SK018-test-files-run.check.ts +6 -2
  25. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  26. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  27. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  28. package/src/checks/SK024-identity-colors.check.ts +5 -3
  29. package/src/checks/SK025-release-drift.check.ts +4 -5
  30. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  31. package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
  32. package/src/checks/SK028-readme.check.ts +5 -1
  33. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  34. package/src/checks/SK030-cache-audit.check.ts +34 -0
  35. package/src/checks/SK031-trace-audit.check.ts +39 -0
  36. package/src/checks/SK032-ignored-inputs.check.ts +79 -0
  37. package/src/cli/cmd_Asks.ts +5 -5
  38. package/src/cli/cmd_Assets.ts +17 -13
  39. package/src/cli/cmd_Bump.ts +12 -11
  40. package/src/cli/cmd_CacheAudit.ts +43 -0
  41. package/src/cli/cmd_Check.ts +8 -7
  42. package/src/cli/cmd_Checks.ts +7 -6
  43. package/src/cli/cmd_Ci.ts +12 -11
  44. package/src/cli/cmd_Commands.ts +16 -13
  45. package/src/cli/cmd_ConfigKeys.ts +7 -6
  46. package/src/cli/cmd_Contracts.ts +3 -3
  47. package/src/cli/cmd_CrossRepoActivity.ts +8 -6
  48. package/src/cli/cmd_Fleet.ts +9 -7
  49. package/src/cli/cmd_GlobalProcesses.ts +120 -7
  50. package/src/cli/cmd_Home.ts +26 -19
  51. package/src/cli/cmd_Hooks.ts +6 -5
  52. package/src/cli/cmd_Init.ts +5 -4
  53. package/src/cli/cmd_Keys.ts +20 -8
  54. package/src/cli/cmd_Mv.ts +7 -6
  55. package/src/cli/cmd_Needs.ts +4 -3
  56. package/src/cli/cmd_Overview.ts +5 -4
  57. package/src/cli/cmd_Pb.ts +2 -2
  58. package/src/cli/cmd_Planned.ts +6 -5
  59. package/src/cli/cmd_Prose.ts +5 -5
  60. package/src/cli/cmd_Reflect.ts +7 -7
  61. package/src/cli/cmd_Release.ts +6 -5
  62. package/src/cli/cmd_Releases.ts +5 -4
  63. package/src/cli/cmd_Serve.ts +6 -4
  64. package/src/cli/cmd_Smoke.ts +5 -4
  65. package/src/cli/cmd_Start.ts +7 -6
  66. package/src/cli/cmd_Status.ts +4 -3
  67. package/src/cli/cmd_TraceAudit.ts +62 -0
  68. package/src/cli/cmd_Tui.ts +3 -2
  69. package/src/cli/cmd_When.ts +71 -0
  70. package/src/cli/cmd_Wiki.ts +4 -4
  71. package/src/cli.ts +11 -2
  72. package/src/collections/checks.ts +2 -0
  73. package/src/collections/collection-types.ts +12 -4
  74. package/src/collections/providers.ts +5 -3
  75. package/src/core/app-icon.ts +154 -0
  76. package/src/core/audit-report.ts +21 -0
  77. package/src/core/cache-audit.ts +72 -0
  78. package/src/core/canonical-scripts.ts +2 -2
  79. package/src/core/check-ctx.ts +14 -5
  80. package/src/core/check-runner.ts +43 -23
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +23 -2
  83. package/src/core/floor-resolve.ts +5 -7
  84. package/src/core/git-root.ts +13 -0
  85. package/src/core/ignored-block.ts +94 -0
  86. package/src/core/ignored-inputs.ts +47 -0
  87. package/src/core/jobs.ts +9 -2
  88. package/src/core/legacy-shapes.ts +39 -10
  89. package/src/core/one-lib.ts +59 -4
  90. package/src/core/one-way-in-policy.ts +2 -2
  91. package/src/core/outcome-words.ts +18 -2
  92. package/src/core/port-check-cli.ts +52 -0
  93. package/src/core/port-check-file.ts +295 -0
  94. package/src/core/port-check-files.ts +31 -0
  95. package/src/core/read-set.ts +97 -9
  96. package/src/core/retention.ts +0 -14
  97. package/src/core/run-check.ts +12 -9
  98. package/src/core/run-ci.ts +14 -5
  99. package/src/core/run-fix.ts +30 -23
  100. package/src/core/run-fixers.ts +48 -5
  101. package/src/core/runs-matrix.ts +179 -0
  102. package/src/core/runs.ts +242 -0
  103. package/src/core/trace-audit.ts +120 -0
  104. package/src/core/update-policy.ts +20 -0
  105. package/src/core/when-block.ts +29 -30
  106. package/src/core/when.ts +41 -103
  107. package/src/core/wire.ts +10 -14
  108. package/src/fleet/scan-fleet.ts +20 -4
  109. package/src/fleet/update-fleet.ts +6 -6
  110. package/src/home/gui-app.ts +24 -27
  111. package/src/home/home-launchd.ts +20 -4
  112. package/src/home/home-routes.ts +12 -1
  113. package/src/home/home-start.ts +11 -5
  114. package/src/home/hubs.ts +5 -5
  115. package/src/home/open-app.ts +2 -2
  116. package/src/home/pm.ts +8 -0
  117. package/src/home/run-pm.ts +1 -0
  118. package/src/home/serve-web.ts +2 -2
  119. package/src/home/supervise.ts +24 -1
  120. package/src/home/via-hub.ts +14 -0
  121. package/src/hooks/run-hooks.ts +48 -4
  122. package/src/init-repo.ts +6 -6
  123. package/src/ledger/db.ts +2 -13
  124. package/src/ledger/gate-checks.ts +133 -61
  125. package/src/ledger/git-key.ts +153 -31
  126. package/src/ledger/git-scan.ts +5 -2
  127. package/src/ledger/ledger.ts +136 -21
  128. package/src/ledger/store.ts +14 -0
  129. package/src/ledger/toolchain-identity.ts +248 -45
  130. package/src/plan/repo-source.ts +18 -39
  131. package/src/problems/extract.ts +3 -0
  132. package/src/release/run-release.ts +5 -0
  133. package/src/repo-config.ts +46 -54
  134. package/src/rules/rule.ts +2 -2
  135. package/src/scaffold/files/agent-coding.md +1 -1
  136. package/src/scaffold/files/agent-index.md +1 -1
  137. package/src/scaffold/files/verify-skill.md +2 -2
  138. package/src/serve/hub-app/activity-st.ts +5 -4
  139. package/src/serve/hub-app/activity-view.tsx +1 -1
  140. package/src/serve/hub-app/admin-svc.ts +36 -3
  141. package/src/serve/hub-app/admin-view.tsx +46 -12
  142. package/src/serve/hub-app/check-view.tsx +66 -46
  143. package/src/serve/hub-app/check-words.ts +1 -11
  144. package/src/serve/hub-app/checks-svc.ts +14 -35
  145. package/src/serve/hub-app/col-card.tsx +1 -44
  146. package/src/serve/hub-app/col-tables-view.tsx +84 -11
  147. package/src/serve/hub-app/col-tables.ts +27 -0
  148. package/src/serve/hub-app/cols-svc.ts +5 -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 +6 -0
  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 +14 -2
  160. package/src/serve/hub-app/home-strip.css +43 -12
  161. package/src/serve/hub-app/home-strip.tsx +115 -64
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +32 -8
  164. package/src/serve/hub-app/hub-types.ts +5 -2
  165. package/src/serve/hub-app/hub.css +89 -63
  166. package/src/serve/hub-app/icons.tsx +11 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +15 -4
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +18 -22
  171. package/src/serve/hub-app/plan-view.tsx +51 -0
  172. package/src/serve/hub-app/planned-st.ts +9 -4
  173. package/src/serve/hub-app/processes-view.tsx +45 -0
  174. package/src/serve/hub-app/project-list.css +4 -1
  175. package/src/serve/hub-app/project-list.tsx +6 -3
  176. package/src/serve/hub-app/projects-svc.ts +25 -10
  177. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  178. package/src/serve/hub-app/reach.ts +36 -0
  179. package/src/serve/hub-app/readme-svc.ts +3 -2
  180. package/src/serve/hub-app/repo-sections.ts +131 -0
  181. package/src/serve/hub-app/repo-st.ts +70 -14
  182. package/src/serve/hub-app/sidebar.tsx +17 -42
  183. package/src/serve/hub-app/timeline-svc.ts +3 -2
  184. package/src/serve/hub-app/tree-build.ts +151 -155
  185. package/src/serve/hub-app/tree-row.tsx +55 -12
  186. package/src/serve/hub-app/tree-svc.ts +97 -56
  187. package/src/serve/hub-app/when-view.tsx +89 -0
  188. package/src/serve/hub-app/when.css +17 -0
  189. package/src/serve/hub-runtime.ts +7 -18
  190. package/src/serve/project-gutter-route.ts +22 -1
  191. package/src/serve/run-serve.ts +1 -1
  192. package/src/serve/serve-hub.ts +4 -14
  193. package/src/suite/judge-suites.ts +6 -0
  194. package/src/trace/interpose.c +655 -0
  195. package/src/trace/read-trace.ts +485 -0
  196. package/src/trace/tracer.ts +282 -0
  197. package/src/serve/hub-app/processes-svc.ts +0 -63
  198. package/src/serve/hub-app/repo-sections.tsx +0 -61
  199. package/src/serve/repo-processes.ts +0 -89
package/CHANGELOG.md CHANGED
@@ -1,3 +1,117 @@
1
+ ## 1.6.0
2
+
3
+ - **lint, typecheck, test and every task that declares nothing are cached now.** Such a task is keyed on the whole work tree, plus the gitignored paths the new `ignoredInputs` lists for it (`{ test: ['dist/**'] }`, `'*'` for every task; a path by content, `dir/**` by the names under it), and the lockfile, the installed packages, the bun and node versions and the toolkit code behind it. `sk trace-audit` runs each gate task once under a file tracer and names the gitignored paths it reads that its key misses. Tracing is on wherever it can run (Apple silicon macOS): SK031 audits about daily, `sk trace-audit --disable` opts a machine out and `SHIPKIT_TRACE=0` one run, and each finding joins the key of the task that read it at once, in every worktree of the repo. SK032 and the fix pass of `sk ci` then write the findings into their own block of `repo.config.ts`, once the repo pins 1.6.0 or later. The tracer never decides a verdict. It needs Apple silicon macOS: on Linux and in CI nothing audits, and SK030's daily sample is the only net there.
4
+
5
+ - **Every check that did not replay says why**, one line per reason: `ran: src/a.ts changed (lint, typecheck)`, `ran: nothing stored for it yet`, `ran, not kept: it reached the world through …`. A check its author declared never cached says so only under `--verbose`.
6
+
7
+ - **A cache key carries the code behind the answer, not the whole toolkit.** A check is keyed on its own module graph and the runner's, and a repo task on the toolkit modules the repo's config imports, so a toolkit edit re-runs only the checks it can change.
8
+
9
+ - **A command loads the image and comfy libraries only when it runs**: `sk --help` went from 540 ms to 220 ms.
10
+
11
+ - **SK030 audits the cache.** About once a day, while somebody works in the repo, it re-runs a sample of the checks the cache would answer right now, fresh, and compares. A cached green that a fresh run disagrees with fails, naming the check whose key misses a path its body reads, and the stale answer is dropped so it is never served again. `sk cache-audit` is the same audit on demand (`--sample <n>`, `--only <id>`, `--json`).
12
+
13
+ ## Unreleased (patch)
14
+
15
+ - **The hub tree groups assets by kit category.** Under media, each category is a row that folds, and its label opens the category page. An asset row reads by its kit label (`mon/brindon/front`), never its file id.
16
+
17
+ - **SK001 no longer asks a comfy repo for a `comfy:up` script.** The box is a global process that `sk gp start comfyui` starts from any repo, so a per-repo script only copied that command. A `.shipkit/comfy/` folder now asks for nothing, and SK001 no longer probes it.
18
+
19
+ - **Every repo's tree has the same top rows.** A section the repo has nothing in (no process, no contract, no secret, no asks, no reflections, no tests, no media, no wiki) stays in the list, greyed, its title saying why. The processes row still opens its page, which says the repo declares none. The others open nothing, and the arrows step over them. Plan, git and admin still exist only on the home.
20
+
21
+ - **Every command starts faster.** A command that draws images or talks to ComfyUI loads canvas, sharp and comfy-ts when it runs, instead of every command loading them at startup (canvas alone read the system fonts). A fully cached `sk ci` drops from about 1.2s to 0.7s, `sk --help` from 540ms to 220ms.
22
+
23
+ - **Every check that did not come from the cache says why.** Above the verdict, one line per reason: `ran: src changed (lint, test)` names what moved since the last stored answer, `ran: nothing stored for it yet`, `ran, not kept: it reached the world through \`git check-ignore -q\`` for an answer that can never be stored, and `never cached: <reason>` for a check no key describes. A check that can never be kept was counted like a key that moved.
24
+
25
+ - **Every repo's media page loads its comfy workflows, not only the first one's.** comfy-ts keeps one instance per process, rooted at one repo, and the daemon serves every repo from one process: the second repo to load its workflows got "conflicts with the existing instance". The instance is now the first repo's, holding only the host caches every repo on that host shares, and each repo's drafts resolve under its own `.comfy-ts/drafts/`.
26
+
27
+ - **Editing shipkit no longer re-runs every check in every repo.** The cache key hashed the toolkit's whole source, and a repo that links it hashed the linked package again, tests included, so a css tweak of the hub or a new test re-ran every cached check everywhere. A key now carries the runner (the modules that run a check and store its answer, and the templates a check reads), the check's own module closure (modules it runs by path included), and for a check running the repo's code, the toolkit modules the repo imports. An edit no answer depends on moves no key, and `sk keys --why` lists the toolkit files behind a key.
28
+
29
+ - **⌘↑ and ⌘↓ open the repo above or below in the project list**, from any page, round both ends. In a text field they still move the caret. The list's foot hints them, and its sort button now just says sort, the order it is in moving to its title.
30
+
31
+ - **An app icon fills its tile, on the repo's colour.** The shipkit app and every process app drew the repo's mark as it was, transparent margin included, so a small mark was a dot on the dock. The icon now cuts the mark to its own box (faint alpha noise, which generated images carry over the whole canvas, counts as margin) and sets it on a rounded plate in the identity colour it stands out on most, on the macOS icon grid. A mark that already fills its box, a photo or a tile, becomes the plate. New colours rebuild the app at its next open.
32
+
33
+ - **The identity colours moved to the admin page, with a preview of the app icon.** The two pickers left the strip. The admin page opens on an identity card: the app icon drawn in the colours being picked (`/api/projects/app-icon/<repo>`, the same drawing the app build uses), and the repo name on a dark and on a light page. The card reads a repo by the name the project list knows, so a repo whose config names it otherwise (screenplay, named screenplay-shed) shows its own colours and icon.
34
+
35
+ - **A broken comfy setup no longer takes the whole media page down.** The workflows load only when a kind generates with comfy. When they fail to load, the media API still mounts: the rolls, the picks and every other kind stay served, and the reason shows in the page errors and in every comfy roll that fails.
36
+
37
+ - **`@napi-rs/canvas` and `comfy-ts` are one copy per process, like the toolkit.** A repo file that imports its own install gets the host copy. A brand that loaded an image with its own canvas copy and drew it on the toolkit's canvas failed ("Value is not one of these types"), and workflows written against an older comfy-ts failed to load.
38
+
39
+ - **A failing test no longer paints the last test file red.** Bun repeats every failure in a recap after the last file of its report, and the hub read those lines as failures of that file: the tests page and the problems pane blamed an innocent suite. The recap now closes the report.
40
+
41
+ - **The arrows walk the repo tree from anywhere on the page.** Up and down move to the next row and open its page at once, with no need to click the tree first. Left and right fold, unfold and climb as before. In the filter box, up and down walk the rows the filter keeps, and left and right stay the text caret's. With no cursor yet, they start from the section of the page shown. A branch with no page of its own only takes the cursor. A walk with the arrows is one history entry, so a held arrow never floods the browser history and back returns to where the walk started. While an asset is open, left and right stay the atelier's roll keys.
42
+
43
+ - **No repo page is a card grid.** Every collection without a table of its own (contracts, reflections, posts, workflows) is now a list, one line per item, red first. The processes page is the overview's process table for that repo, with the opened process's description and log under it. The tables sit on a panel box for contrast, and the page title and group headers are larger and in plain ink.
44
+
45
+ - **The secrets page is a list.** One line per secret, never a card: a dot, the name, missing, minor or present, and its sentence. Missing ones come first.
46
+
47
+ ## 1.5.0
48
+
49
+ - **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.
50
+
51
+ - **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.
52
+
53
+ - **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.
54
+
55
+ - **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.
56
+
57
+ - **`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.
58
+
59
+ - **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`).
60
+
61
+ - **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.
62
+
63
+ - **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.
64
+
65
+ - **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.
66
+
67
+ - **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.
68
+
69
+ - **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.
70
+
71
+ - **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.
72
+
73
+ - **`sk`, the short name.** The package ships a second bin, `sk`, the same cli as `shipkit`.
74
+
75
+ - **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.
76
+
77
+ - **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.
78
+
79
+ - **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.
80
+
81
+ - **`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.
82
+
83
+ - **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.
84
+
85
+ - **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.
86
+
87
+ - **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.
88
+
89
+ - **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>]`.
90
+
91
+ - **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.
92
+
93
+ - **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.
94
+
95
+ - **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`).
96
+
97
+ - **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.
98
+
99
+ - **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.
100
+
101
+ - **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.
102
+
103
+ - **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' }`.
104
+
105
+ - **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`.
106
+
107
+ - **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.
108
+
109
+ - **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.
110
+
111
+ - **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.
112
+
113
+ - **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.
114
+
1
115
  ## 1.4.0
2
116
 
3
117
  - **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.6.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",
@@ -12,7 +12,8 @@ import { ComfyTS, type AnyVar, type ComfyExecution, type DefinedWorkflow, type M
12
12
  export type GenProgress = { value: number; max: number; node: string | null }
13
13
  export type OnProgress = (p: GenProgress) => void
14
14
 
15
- export type LoadedWorkflow = { id: string; file: string; wf: DefinedWorkflow }
15
+ /** root: the repo the workflow belongs to, which holds its drafts */
16
+ export type LoadedWorkflow = { id: string; root: string; file: string; wf: DefinedWorkflow }
16
17
  export type ComfyTsRuntime = { workflows: Map<string, LoadedWorkflow>; heal?: string }
17
18
 
18
19
  // function declaration, not const — TS only narrows on never-calls to
@@ -47,9 +48,11 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
47
48
  // explicit COMFY_TS_QUIET=0 from the caller.
48
49
  process.env.COMFY_TS_QUIET ??= '1'
49
50
  // before the first module import: a bare ComfyTS.create() inside the
50
- // module then returns this same instance. A pre-existing instance with a
51
- // different root throws loud — a real misconfiguration.
52
- ComfyTS.create({ rootPath: root })
51
+ // module then returns this same instance. comfy-ts keeps ONE instance per
52
+ // process, and the daemon serves every repo from one process, so only the
53
+ // first repo roots it. What it roots is the host caches, shared by every
54
+ // repo on the same host; each repo's drafts resolve from its own root
55
+ if ((globalThis as { comfyts?: unknown }).comfyts == null) ComfyTS.create({ rootPath: root })
53
56
  if (!existsSync(dir)) fail(`${dir}/ missing — ship at least one <id>.cflow.ts comfy-ts workflow module (copy from the rv-comfy template)`)
54
57
  const out = new Map<string, LoadedWorkflow>()
55
58
  for (const f of readdirSync(dir).filter((x) => x.endsWith('.cflow.ts')).sort()) {
@@ -65,7 +68,7 @@ export async function loadCflowWorkflows(root: string, dir: string): Promise<Map
65
68
  const vars = varsOf(wf)
66
69
  if (vars.prompt?.kind !== 'prompt' || vars.seed?.kind !== 'seed')
67
70
  fail(`workflow '${id}' must declare vars.prompt (v.prompt) and vars.seed (v.seed) — shipkit drives both`)
68
- out.set(id, { id, file, wf })
71
+ out.set(id, { id, root, file, wf })
69
72
  }
70
73
  if (out.size === 0) fail(`${dir}/ has no *.cflow.ts workflow modules — seed one from the rv-comfy template`)
71
74
  return out
@@ -96,8 +99,9 @@ export function applyVarValues(wf: DefinedWorkflow, vars: Record<string, unknown
96
99
  }
97
100
  }
98
101
 
99
- // ── drafts — comfy-ts disk format, path rule taken from the ComfyTS instance
100
- // so it cannot drift: .comfy-ts/drafts/<wfId>/<draftId>.json.
102
+ // ── drafts — comfy-ts disk format, .comfy-ts/drafts/<wfId>/<draftId>.json
103
+ // under the workflow's own repo, never the process instance's root (which is
104
+ // the first repo's in the daemon).
101
105
  //
102
106
  // The draft id is the KIND, never the asset: ONE `creature` draft behind all
103
107
  // 107 creatures, so tuning the loras once tunes them for the whole kind. The
@@ -107,7 +111,7 @@ export function applyVarValues(wf: DefinedWorkflow, vars: Record<string, unknown
107
111
  //
108
112
  // Shipkit does its own IO here because comfy-ts exports no drafts API yet;
109
113
  // swap when it lands.
110
- const draftDir = (lw: LoadedWorkflow): string => ComfyTS.create().resolveFromDrafts(lw.id)
114
+ const draftDir = (lw: LoadedWorkflow): string => join(lw.root, '.comfy-ts', 'drafts', lw.id)
111
115
  export const draftPath = (lw: LoadedWorkflow, name: string): string => join(draftDir(lw), `${name}.json`)
112
116
 
113
117
  export function readAssetDraft(lw: LoadedWorkflow, draftId: string): Record<string, unknown> | null {
@@ -49,7 +49,18 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
49
49
  const workspace = join(root, kit.workspace)
50
50
  const dirOf = (id: string): string => assetDir(workspace, id)
51
51
  let workflows: Map<string, LoadedWorkflow> | null = null
52
- if (kit.comfy != null) workflows = await loadCflowWorkflows(root, join(root, kit.comfy.workflows))
52
+ // only a comfy kind needs them, and a broken load costs only comfy
53
+ // generation: the rolls, the picks and every other kind stay served, and
54
+ // the reason rides in /api/state errors and in every comfy roll that fails
55
+ let comfyErr: string | null = null
56
+ if (kit.comfy != null && kit.kinds.some((k) => k.generate.backend === 'comfy')) {
57
+ try {
58
+ workflows = await loadCflowWorkflows(root, join(root, kit.comfy.workflows))
59
+ } catch (e) {
60
+ comfyErr = `comfy workflows not loaded: ${(e as Error).message}`
61
+ console.log(`WARN ${comfyErr}`)
62
+ }
63
+ }
53
64
 
54
65
  // custom panel scripts fail-fast here too (tests/embedders may bypass loadKit)
55
66
  const missingPanels = missingPanelScripts(kit, root)
@@ -111,6 +122,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
111
122
  // workflows are pre-loaded above (fail-fast for a comfy studio); the
112
123
  // resolver just hands the shared roll generator the ready runtime.
113
124
  const resolveComfy = async (): Promise<ComfyTsRuntime> => {
125
+ if (comfyErr != null) throw new Error(comfyErr)
114
126
  if (kit.comfy == null || workflows == null) throw new Error('kit has no comfy config — only canvas kinds can generate')
115
127
  return { workflows, heal: kit.comfy.heal }
116
128
  }
@@ -243,7 +255,7 @@ export async function studioRoutes(deps: StudioDeps): Promise<StudioRoutes> {
243
255
  derived: parentIdsOf(f).length > 0, stale: await isStale(kit, workspace, f),
244
256
  }
245
257
  }))
246
- return json({ ...queue.state, queued: queue.queuedCount, pending: queue.pendingIds(), errors: queue.errors.slice(-5), assets })
258
+ return json({ ...queue.state, queued: queue.queuedCount, pending: queue.pendingIds(), errors: [...(comfyErr == null ? [] : [comfyErr]), ...queue.errors.slice(-5)], assets })
247
259
  }
248
260
 
249
261
  if (pathname === '/api/asset') {
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
@@ -264,14 +238,11 @@ export type TaskDef = {
264
238
  timeoutMs?: number
265
239
  /** CONTENT population: the repo-relative paths this task reads. Declaring
266
240
  * them is what makes it keyable (ledger/git-key.ts). Undeclared is the
267
- * DEFAULT and means never cached, so forgetting one costs speed and
268
- * never correctness. A list MISSING a path the command reads is the one
269
- * that costs correctness: the key cannot move when that path changes, so
270
- * a cached answer comes back over a tree it no longer describes. `['.']` is the
271
- * whole TRACKED tree, and it is NOT a catch-all: an ignored path enters a
272
- * key by name and never by content, so a task reading a gitignored
273
- * fixture, a built dir or a .env under declares with `['.']` exactly as
274
- * it would with a narrow list. */
241
+ * DEFAULT: the task is keyed on the whole work tree plus the repo's
242
+ * `ignoredInputs` (build/shipkit/cache.md). A list MISSING a path
243
+ * the command reads is the one that costs correctness: the key cannot
244
+ * move when that path changes, so a cached answer comes back over a tree
245
+ * it no longer describes. */
275
246
  inputs?: string[]
276
247
  /** WORLD population: it reads the network, a keychain, $PATH or the clock,
277
248
  * so nothing on disk describes its answer and a clean pass is remembered
@@ -333,13 +304,12 @@ export type Rule = {
333
304
 
334
305
  // ── running a body, and what every line it prints carries ────────────────
335
306
 
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}`
307
+ /** the config part of a check's cache identity: its declared key, and the
308
+ * reason it is off. Turning a check off, or editing why, asks the question
309
+ * again rather than serving the answer from before. Where it runs is not
310
+ * part of it: the answer to one question does not depend on who asks. */
311
+ export function checkCacheExtra(key: string | undefined, off: string | null): string | undefined {
312
+ return off == null ? key : `${key ?? ''}|exempt:${off}`
343
313
  }
344
314
 
345
315
  /** THE one way to run an in-process body (the check suite and the `--fix`