rvlib-shipkit 1.3.0 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (203) hide show
  1. package/CHANGELOG.md +105 -0
  2. package/README.md +17 -17
  3. package/package.json +3 -2
  4. package/src/asks/asks-queue.ts +4 -2
  5. package/src/assets/kits/identity-kit.ts +15 -13
  6. package/src/assets/run-assets.ts +12 -2
  7. package/src/check.ts +73 -98
  8. package/src/checks/SK001-package-json-shape.check.ts +26 -4
  9. package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
  10. package/src/checks/SK003-claude-md-shape.check.ts +5 -1
  11. package/src/checks/SK004-gitignore-policy.check.ts +7 -4
  12. package/src/checks/SK005-rvlib-versions.check.ts +53 -21
  13. package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
  14. package/src/checks/SK007-github-visibility.check.ts +4 -2
  15. package/src/checks/SK008-reviewer-agent.check.ts +6 -4
  16. package/src/checks/SK009-commit-ai-tells.check.ts +94 -74
  17. package/src/checks/SK010-undeclared-unit.check.ts +4 -0
  18. package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
  19. package/src/checks/SK012-config-barrel-import.check.ts +5 -1
  20. package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
  21. package/src/checks/SK014-prose-ste.check.ts +20 -6
  22. package/src/checks/SK015-nested-repo.check.ts +4 -2
  23. package/src/checks/SK016-legacy-check-shapes.check.ts +23 -16
  24. package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
  25. package/src/checks/SK018-test-files-run.check.ts +6 -2
  26. package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
  27. package/src/checks/SK020-self-spawning-check.check.ts +4 -0
  28. package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
  29. package/src/checks/SK024-identity-colors.check.ts +5 -3
  30. package/src/checks/SK025-release-drift.check.ts +4 -5
  31. package/src/checks/SK026-asks-autoclose.check.ts +4 -4
  32. package/src/checks/SK027-agent-wiki-links.check.ts +34 -9
  33. package/src/checks/SK028-readme.check.ts +10 -2
  34. package/src/checks/SK029-explicit-check-files.check.ts +95 -0
  35. package/src/cli/cmd_Asks.ts +5 -5
  36. package/src/cli/cmd_Assets.ts +7 -7
  37. package/src/cli/cmd_Bump.ts +12 -11
  38. package/src/cli/cmd_Check.ts +8 -7
  39. package/src/cli/cmd_Checks.ts +7 -6
  40. package/src/cli/cmd_Ci.ts +12 -11
  41. package/src/cli/cmd_Commands.ts +16 -13
  42. package/src/cli/cmd_ConfigKeys.ts +7 -6
  43. package/src/cli/cmd_Contracts.ts +3 -3
  44. package/src/cli/cmd_CrossRepoActivity.ts +10 -6
  45. package/src/cli/cmd_Fleet.ts +9 -7
  46. package/src/cli/cmd_GlobalProcesses.ts +116 -4
  47. package/src/cli/cmd_Home.ts +23 -17
  48. package/src/cli/cmd_Hooks.ts +6 -5
  49. package/src/cli/cmd_Init.ts +5 -4
  50. package/src/cli/cmd_Keys.ts +8 -7
  51. package/src/cli/cmd_Mv.ts +7 -6
  52. package/src/cli/cmd_Needs.ts +4 -3
  53. package/src/cli/cmd_Overview.ts +4 -3
  54. package/src/cli/cmd_Pb.ts +2 -2
  55. package/src/cli/cmd_Planned.ts +6 -5
  56. package/src/cli/cmd_Prose.ts +5 -5
  57. package/src/cli/cmd_Reflect.ts +7 -7
  58. package/src/cli/cmd_Release.ts +6 -5
  59. package/src/cli/cmd_Releases.ts +5 -4
  60. package/src/cli/cmd_Serve.ts +4 -3
  61. package/src/cli/cmd_Smoke.ts +5 -4
  62. package/src/cli/cmd_Start.ts +7 -6
  63. package/src/cli/cmd_Status.ts +4 -3
  64. package/src/cli/cmd_Tui.ts +3 -2
  65. package/src/cli/cmd_When.ts +71 -0
  66. package/src/cli/cmd_Wiki.ts +4 -4
  67. package/src/cli.ts +7 -2
  68. package/src/collections/checks.ts +2 -0
  69. package/src/collections/collection-types.ts +24 -4
  70. package/src/collections/providers.ts +6 -2
  71. package/src/contracts/apply-contracts.ts +9 -8
  72. package/src/contracts/contract-types.ts +8 -2
  73. package/src/contracts/contracts-engine.ts +34 -13
  74. package/src/contracts/manifests/identity.contract.ts +6 -5
  75. package/src/contracts/manifests/social.contract.ts +4 -2
  76. package/src/contracts/manifests/steam.contract.ts +7 -3
  77. package/src/contracts/web-surface.ts +59 -0
  78. package/src/core/budget.ts +19 -9
  79. package/src/core/canonical-scripts.ts +1 -1
  80. package/src/core/check-runner.ts +63 -25
  81. package/src/core/config-rewrite.ts +37 -24
  82. package/src/core/discover-checks.ts +31 -7
  83. package/src/core/layout.ts +2 -0
  84. package/src/core/legacy-shapes.ts +39 -10
  85. package/src/core/motherhome.ts +88 -66
  86. package/src/core/one-way-in-policy.ts +2 -2
  87. package/src/core/outcome-words.ts +18 -2
  88. package/src/core/port-check-cli.ts +52 -0
  89. package/src/core/port-check-file.ts +295 -0
  90. package/src/core/port-check-files.ts +31 -0
  91. package/src/core/retention.ts +0 -14
  92. package/src/core/run-check.ts +14 -14
  93. package/src/core/run-ci.ts +11 -7
  94. package/src/core/run-fix.ts +30 -23
  95. package/src/core/run-fixers.ts +42 -5
  96. package/src/core/runs-matrix.ts +179 -0
  97. package/src/core/runs.ts +242 -0
  98. package/src/core/update-policy.ts +20 -0
  99. package/src/core/when-block.ts +29 -30
  100. package/src/core/when.ts +41 -103
  101. package/src/core/wire.ts +34 -9
  102. package/src/fleet/scan-fleet.ts +20 -4
  103. package/src/fleet/update-fleet.ts +6 -6
  104. package/src/home/activity-shape.ts +4 -1
  105. package/src/home/activity.ts +22 -15
  106. package/src/home/gui-app/ShipkitGui.swift +144 -25
  107. package/src/home/gui-app.ts +93 -30
  108. package/src/home/home-launchd.ts +30 -6
  109. package/src/home/home-routes.ts +14 -2
  110. package/src/home/home-start.ts +33 -5
  111. package/src/home/hubs.ts +5 -5
  112. package/src/home/machine-stores.ts +4 -2
  113. package/src/home/open-app.ts +23 -10
  114. package/src/home/pm-config.ts +15 -11
  115. package/src/home/pm-inputs.ts +76 -0
  116. package/src/home/pm.ts +11 -3
  117. package/src/home/process-rows.ts +23 -6
  118. package/src/home/resources.ts +67 -15
  119. package/src/home/run-daemon.ts +6 -1
  120. package/src/home/run-pm.ts +30 -5
  121. package/src/home/serve-web.ts +2 -2
  122. package/src/home/supervise.ts +24 -1
  123. package/src/home/widgets.ts +32 -3
  124. package/src/hooks/commit-message.ts +2 -3
  125. package/src/hooks/run-hooks.ts +60 -6
  126. package/src/init-repo.ts +6 -6
  127. package/src/ledger/gate-checks.ts +115 -61
  128. package/src/ledger/git-key.ts +5 -1
  129. package/src/ledger/ledger.ts +5 -5
  130. package/src/ledger/store.ts +11 -0
  131. package/src/ledger/toolchain-identity.ts +45 -27
  132. package/src/ledger/world-cache.ts +17 -15
  133. package/src/plan/repo-source.ts +18 -39
  134. package/src/release/run-release.ts +5 -0
  135. package/src/repo-config.ts +36 -56
  136. package/src/rules/rule.ts +2 -2
  137. package/src/scaffold/files/agent-coding.md +1 -1
  138. package/src/scaffold/files/agent-index.md +1 -1
  139. package/src/scaffold/files/verify-skill.md +2 -2
  140. package/src/serve/hub-app/activity-st.ts +5 -4
  141. package/src/serve/hub-app/activity-view.tsx +4 -4
  142. package/src/serve/hub-app/admin-view.tsx +13 -11
  143. package/src/serve/hub-app/check-view.tsx +66 -46
  144. package/src/serve/hub-app/check-words.ts +1 -11
  145. package/src/serve/hub-app/checks-svc.ts +16 -36
  146. package/src/serve/hub-app/col-tables-view.tsx +131 -0
  147. package/src/serve/hub-app/col-tables.ts +115 -0
  148. package/src/serve/hub-app/cols-svc.ts +33 -4
  149. package/src/serve/hub-app/dock-st.ts +3 -2
  150. package/src/serve/hub-app/dock-view.tsx +36 -0
  151. package/src/serve/hub-app/dock.css +7 -1
  152. package/src/serve/hub-app/float-cards.tsx +30 -0
  153. package/src/serve/hub-app/floating.tsx +41 -4
  154. package/src/serve/hub-app/git-st.ts +42 -0
  155. package/src/serve/hub-app/git-view.tsx +61 -0
  156. package/src/serve/hub-app/hash-route.ts +45 -1
  157. package/src/serve/hub-app/header-bar.tsx +62 -72
  158. package/src/serve/hub-app/health-svc.ts +3 -2
  159. package/src/serve/hub-app/home-st.ts +15 -3
  160. package/src/serve/hub-app/home-strip.css +69 -19
  161. package/src/serve/hub-app/home-strip.tsx +192 -118
  162. package/src/serve/hub-app/home.html +1 -0
  163. package/src/serve/hub-app/hub-app.tsx +21 -5
  164. package/src/serve/hub-app/hub-types.ts +11 -5
  165. package/src/serve/hub-app/hub.css +88 -44
  166. package/src/serve/hub-app/icons.tsx +15 -0
  167. package/src/serve/hub-app/index.html +1 -0
  168. package/src/serve/hub-app/keys.ts +11 -8
  169. package/src/serve/hub-app/live-svc.ts +3 -2
  170. package/src/serve/hub-app/machine-st.ts +34 -49
  171. package/src/serve/hub-app/palette-st.ts +5 -5
  172. package/src/serve/hub-app/palette-view.tsx +1 -1
  173. package/src/serve/hub-app/plan-view.tsx +51 -0
  174. package/src/serve/hub-app/planned-st.ts +9 -4
  175. package/src/serve/hub-app/processes-view.tsx +38 -0
  176. package/src/serve/hub-app/project-list.tsx +3 -3
  177. package/src/serve/hub-app/projects-svc.ts +15 -10
  178. package/src/serve/hub-app/prose-hits-st.ts +3 -2
  179. package/src/serve/hub-app/reach.ts +36 -0
  180. package/src/serve/hub-app/readme-svc.ts +3 -2
  181. package/src/serve/hub-app/repo-sections.ts +117 -0
  182. package/src/serve/hub-app/repo-st.ts +47 -10
  183. package/src/serve/hub-app/runs-tabs.ts +1 -1
  184. package/src/serve/hub-app/sidebar.tsx +12 -37
  185. package/src/serve/hub-app/timeline-svc.ts +3 -2
  186. package/src/serve/hub-app/timeline-view.tsx +14 -11
  187. package/src/serve/hub-app/tree-build.ts +149 -155
  188. package/src/serve/hub-app/tree-row.tsx +39 -9
  189. package/src/serve/hub-app/tree-svc.ts +70 -49
  190. package/src/serve/hub-app/when-view.tsx +89 -0
  191. package/src/serve/hub-app/when.css +17 -0
  192. package/src/serve/hub-runtime.ts +3 -16
  193. package/src/serve/project-gutter.ts +1 -2
  194. package/src/serve/run-serve.ts +1 -1
  195. package/src/serve/serve-hub.ts +4 -14
  196. package/src/serve/warm-cell.ts +49 -16
  197. package/src/suite/judge-suites.ts +42 -17
  198. package/src/suite/suite-cli.ts +5 -8
  199. package/src/wiki/lint-md.ts +2 -0
  200. package/src/wiki/scan-md.ts +37 -10
  201. package/src/serve/hub-app/processes-svc.ts +0 -63
  202. package/src/serve/hub-app/repo-sections.tsx +0 -61
  203. package/src/serve/repo-processes.ts +0 -89
package/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,8 @@ 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 (core/budget.ts). The repo's `budgets` wins over it. */
123
+ * or a world probe (cache `ttl`, `fact` or `never`, core/budget.ts).
124
+ * The repo's `budgets` wins over it. */
164
125
  budgetMs?: number
165
126
  /** a label several checks share, so `--skip <group>` parks all of them
166
127
  * and `--only <group>` runs all of them. */
@@ -197,6 +158,19 @@ export function describeFirst(...candidates: (string | null | undefined)[]): str
197
158
  throw new Error(`no description candidate passes the shape: ${candidates.map((d) => JSON.stringify(d)).join(', ')}`)
198
159
  }
199
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
+
200
174
  /** THE way a check file declares itself. It validates the shape at import
201
175
  * time, so a malformed check dies when discovery loads it and never mid
202
176
  * gate. The id, the title and the owner are filled in by discovery, which
@@ -207,20 +181,21 @@ export function check(d: CheckDef): CheckDef {
207
181
  const bad = descriptionProblem(d.description)
208
182
  if (bad != null) throw new Error(`check${name}: ${bad}`)
209
183
  if (typeof d.blocking !== 'boolean')
210
- 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)`)
211
185
  if (has('run') === has('cmd'))
212
- throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: declare exactly one body, \`run\` (in process) or \`cmd\` (argv)`)
213
- const populations = (['inputs', 'ttl', 'never'] as const).filter((k) => has(k))
214
- if (populations.length > 1)
215
- 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`)
216
- if (d.ttl != null && d.ttl !== FOREVER)
217
- try { parseDuration(d.ttl) } catch { throw new Error(`check${name}: ttl '${d.ttl}' is neither a span ('7d') nor 'forever'`) }
218
- const planBad = planDeclProblem({ every: d.every, cron: d.cron, whenInactive: d.whenInactive })
219
- if (planBad != null) throw new Error(`check${name}: ${planBad}`)
220
- if (d.when != null) {
221
- const w = parseWhen(d.when)
222
- if (typeof w === 'string') throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: when ${w}`)
223
- }
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}`)
224
199
  return d
225
200
  }
226
201
 
@@ -232,7 +207,7 @@ export function check(d: CheckDef): CheckDef {
232
207
 
233
208
  export type TaskDef = {
234
209
  /** the name every surface uses: the gate line, `--only`, `--skip`, a
235
- * `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. */
236
211
  id: string
237
212
  /** one line stating what must be true (the shape: descriptionProblem).
238
213
  * A floor slot and a unit test check get theirs from the
@@ -254,7 +229,7 @@ export type TaskDef = {
254
229
  /** a label several tasks share, so `--skip <group>` parks all of them at
255
230
  * once (a local bench, a slow integration tier). */
256
231
  group?: string
257
- /** DEPRECATED, read into `when` as `health`: out of the gate until
232
+ /** DEPRECATED, read as every gate axis false: out of the gate until
258
233
  * `--with <id>` names it. */
259
234
  optIn?: boolean
260
235
  /** wall clock ceiling in ms, default 600000 for a build step. One that
@@ -332,13 +307,12 @@ export type Rule = {
332
307
 
333
308
  // ── running a body, and what every line it prints carries ────────────────
334
309
 
335
- /** the config part of a check's cache identity. Where it RUNS is part of it:
336
- * moving one off the gate, or editing the reason it is off, has to ask the
337
- * question again rather than serve the cached answer from before the move. */
338
- export function checkCacheExtra(u: { id: string; cacheKey?: (env: CheckEnv) => string }, env: CheckEnv, at: { at: string; reason: string | null; source: string }): string | undefined {
339
- const key = u.cacheKey?.(env)
340
- if (at.source === 'default') return key
341
- 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}`
342
316
  }
343
317
 
344
318
  /** THE one way to run an in-process body (the check suite and the `--fix`
@@ -373,12 +347,13 @@ export async function runCheckBody(
373
347
  }
374
348
 
375
349
  /** tag every severity line with the check id; a fix pointer of a fixable check
376
- * advertises the autofix. Idempotent, since an already-tagged line passes
377
- * back through here on a cached suite replay. */
350
+ * advertises the autofix, unless it names its own `--fix` or says the repair
351
+ * is `by hand`. Idempotent, since an already-tagged line passes back through
352
+ * here on a cached suite replay. */
378
353
  export function tagCheckLines(u: { id: string; fix?: unknown }, lines: Line[]): Line[] {
379
354
  return lines.map((l) => {
380
355
  if (l.kind === 'fix-warn' || l.kind === 'fix-fail')
381
- return u.fix == null || l.text.includes('--fix ')
356
+ return u.fix == null || l.text.includes('--fix ') || /\bby hand\b/.test(l.text)
382
357
  ? l
383
358
  : { ...l, text: `${l.text} · autofix: shipkit check --fix ${u.id}` }
384
359
  if (l.kind === 'note') return l
@@ -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
@@ -107,8 +107,11 @@ export function evaluatePackageJson(t: CheckCtx): void {
107
107
 
108
108
  // `test` is asked for only once the repo HAS tests: the fixer deliberately
109
109
  // refuses to scaffold it before that (a fixer never manufactures a
110
- // failure), so nagging for it would be a WARN no machine may clear
111
- const baseline = BASELINE.filter((n) => n !== 'test' || hasTestFiles(testFiles(t)))
110
+ // failure), so nagging for it would be a WARN no machine may clear. For
111
+ // the same reason `lint` and `fmt` wait for a source file to read
112
+ const tests = hasTestFiles(testFiles(t))
113
+ const source = tests || hasSourceFiles(t)
114
+ const baseline = BASELINE.filter((n) => (n === 'test' ? tests : source))
112
115
  const missingBaseline = baseline.filter((n) => !has(n))
113
116
  if (missingBaseline.length > 0)
114
117
  t.warn(`package.json: missing baseline script(s): ${missingBaseline.join(', ')}`)
@@ -173,18 +176,37 @@ export function hasTestFiles(paths: readonly string[]): boolean {
173
176
  })
174
177
  }
175
178
 
179
+ const SOURCE_GLOB = '**/*.{ts,tsx,mts,cts,js,jsx,mjs,cjs}'
180
+ /** an empty module: nothing but `export {}`, semicolons and whitespace */
181
+ const EMPTY_MODULE = /^(?:\s|;|export\s*\{\s*\})*$/
182
+
183
+ /** a code file with something in it, outside installs, build output, dot dirs
184
+ * and the repo config: what lint and fmt would read. Stops at the first */
185
+ function hasSourceFiles(c: CheckCtx): boolean {
186
+ for (const f of c.list(SOURCE_GLOB)) {
187
+ const parts = f.split('/')
188
+ if (f === 'repo.config.ts' || !parts.slice(0, -1).every((d) => !TEST_SKIP.has(d) && !d.startsWith('.'))) continue
189
+ if (!EMPTY_MODULE.test(c.read(f) ?? '')) return true
190
+ }
191
+ return false
192
+ }
193
+
176
194
  export const packageJsonShape = check({
177
195
  id: 'SK001',
178
196
  blocking: true,
179
197
  title: 'package.json',
180
198
  description: 'package.json routes lint, typecheck and test through the gate',
181
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',
182
204
  run: (c) => evaluatePackageJson(c),
183
205
  // mechanical repairs only: field flips + scripts whose canonical body the
184
206
  // vocabulary page states. Bodies it does NOT state (assets, comfy:up) are
185
207
  // named in the summary, never invented. rvlib-* roots untouched — their
186
208
  // license/private are publish decisions.
187
- fix: (c): FixResult => {
209
+ repair: (c): FixResult => {
188
210
  const raw = c.read('package.json')
189
211
  if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
190
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
@@ -10,7 +10,12 @@
10
10
  // Per rvlib-* dep:
11
11
  // link: → the symlink must resolve (a broken link dies cryptically at
12
12
  // import time otherwise) — nothing to update, code is live
13
- // semver → installed vs npm latest; a BREAKING jump (a new major, or a
13
+ // semver → installed vs npm latest. A semver pin whose node_modules
14
+ // entry is a `bun link` symlink is judged by the PIN's floor
15
+ // instead: the link carries the source, always the newest, while
16
+ // a fresh clone installs the pin. Behind, the pin moves through
17
+ // the same guarded update, and `bun link <name>` restores the
18
+ // link after it (a link that does not come back FAILs). A BREAKING jump (a new major, or a
14
19
  // new 0.x minor) is never taken by itself
15
20
  // (src/core/update-policy.ts: WARN naming `fleet --update --major`,
16
21
  // package.json untouched); the rvlib-shipkit tree running this
@@ -46,7 +51,7 @@
46
51
  import { existsSync, readFileSync } from 'fs'
47
52
  import { basename, join } from 'path'
48
53
  import type { CheckCtx } from '../core/check-ctx.ts'
49
- import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
54
+ import { allowMajor, breakingAdvice, isBreaking, isLinkedInstall, pinFloor } from '../core/update-policy.ts'
50
55
  import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
51
56
  import { rootPublishesNpm } from '../core/repo-facts.ts'
52
57
  import { mutateLease, withMutateLease } from '../core/locked-write.ts'
@@ -61,6 +66,13 @@ const installedVersion = (root: string, name: string): string | null => {
61
66
  try { return (JSON.parse(readFileSync(p, 'utf8')) as Pkg).version ?? null } catch { return null }
62
67
  }
63
68
 
69
+ const readPin = (root: string, name: string): string | null => {
70
+ try {
71
+ const pkg = JSON.parse(readFileSync(join(root, 'package.json'), 'utf8')) as Pkg
72
+ return pkg.dependencies?.[name] ?? pkg.devDependencies?.[name] ?? null
73
+ } catch { return null }
74
+ }
75
+
64
76
  async function latestVersion(t: CheckCtx, name: string): Promise<string> {
65
77
  const res = await t.fetch(`https://registry.npmjs.org/${name}/latest`)
66
78
  if (!res.ok) throw new Error(`registry ${res.status}`)
@@ -74,23 +86,24 @@ export const rvlibVersions = check({
74
86
  blocking: true,
75
87
  title: 'rvlib versions',
76
88
  description: 'the rvlib packages installed here are as new as the published ones',
77
- owner: 'build/shipkit.md',
78
- ttl: '24h',
89
+ owner: 'build/shipkit/consuming.md',
79
90
  // it updates the rvlib deps unless the run is frozen
80
91
  writes: true,
81
- planOff: (env) => {
82
- let pkg: Pkg
83
- try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
84
- return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
85
- },
86
- cacheKey: (env) => {
92
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: false, onClock: { every: '24h', inactive: 'wait' } },
93
+ cache: { by: 'ttl', span: '24h', key: (env) => {
87
94
  let pkg: Pkg
88
95
  try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
89
96
  return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies })
90
97
  .filter((n) => n.startsWith('rvlib-'))
91
98
  .sort()
92
- .map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
99
+ .map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}${isLinkedInstall(env.root, n) ? ` pin ${readPin(env.root, n)}` : ''}`)
93
100
  .join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
101
+ } },
102
+ fix: 'none',
103
+ applies: (env) => {
104
+ let pkg: Pkg
105
+ try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
106
+ return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
94
107
  },
95
108
  run: async (c) => {
96
109
  let pkg: Pkg
@@ -100,39 +113,47 @@ export const rvlibVersions = check({
100
113
  const pm = detectPackageManager(c.root)
101
114
  const manifest = ['package.json', ...(isRefusal(pm) ? LOCK_FILES : pm.locks)]
102
115
 
103
- const stale: Array<{ name: string; installed: string; latest: string }> = []
116
+ const stale: Array<{ name: string; installed: string; latest: string; linked: boolean }> = []
104
117
  for (const [name, spec] of deps) {
105
118
  if (spec.startsWith('link:')) {
106
119
  if (installedVersion(c.root, name) == null) c.fail(`${name} is link: but the symlink does not resolve — re-run \`bun link ${name}\` (a broken link dies cryptically at import time)`)
107
120
  else c.ok(`${name} (linked, live)`)
108
121
  continue
109
122
  }
110
- const installed = installedVersion(c.root, name)
111
- if (installed == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
123
+ const onDisk = installedVersion(c.root, name)
124
+ if (onDisk == null) { c.fail(`${name} declared (${spec}) but not installed — ${isRefusal(pm) ? pm.refused : `run: ${pm.install.join(' ')}`}`); continue }
125
+ const linked = isLinkedInstall(c.root, name)
126
+ const floor = linked ? pinFloor(spec) : null
127
+ if (linked && floor == null) { c.ok(`${name} (linked to its source, pin ${spec} names no version to judge)`); continue }
128
+ const installed = floor ?? onDisk
129
+ // how the lines below name it: a linked dep is its pin, never the source it links
130
+ const label = linked ? `${name} pin ${installed}` : `${name} ${installed}`
112
131
  let latest: string
113
132
  try { latest = await latestVersion(c, name) } catch (e) {
114
- c.lines.push({ kind: 'skip', text: `${name} ${installed} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
133
+ c.lines.push({ kind: 'skip', text: `${label} — registry unreachable (${String(e).slice(0, 80)}), version drift unknown` })
115
134
  continue
116
135
  }
117
136
  // real semver order, not string difference — a dist-tag pointing
118
137
  // BEHIND the installed version (prerelease, unpublish window)
119
138
  // must never trigger a silent downgrade
120
139
  const cmp = Bun.semver.order(latest, installed)
121
- if (cmp === 0) c.ok(`${name} ${installed} (latest)`)
122
- else if (cmp < 0) c.ok(`${name} ${installed} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
140
+ if (cmp === 0) c.ok(`${label} (latest${linked ? ', linked to its source' : ''})`)
141
+ else if (cmp < 0) c.ok(`${label} ahead of npm latest ${latest} (prerelease or dist-tag lag) — not touching`)
123
142
  else if (isBreaking(installed, latest) && !allowMajor()) c.warn(breakingAdvice(name, latest, basename(c.root)))
124
- else if (name === 'rvlib-shipkit' && isRunningCopy(c.root)) {
143
+ // a linked toolkit moves only its pin: the source tree this process
144
+ // runs from is never written, so the swap below cannot happen
145
+ else if (name === 'rvlib-shipkit' && !linked && isRunningCopy(c.root)) {
125
146
  // swapping this tree under the running process is how a run
126
147
  // meets modules of two versions. The startup self-update owns
127
148
  // it: handing it the answer makes the next run update and
128
149
  // restart before any check loads
129
150
  rememberLatest(latest)
130
151
  c.warn(`${name} ${installed} → ${latest} is the toolkit running this check, so it is not swapped mid run: the next shipkit run updates it and restarts before any check`)
131
- } else stale.push({ name, installed, latest })
152
+ } else stale.push({ name, installed, latest, linked })
132
153
  }
133
154
  if (stale.length === 0) return
134
155
 
135
- const drift = stale.map((s) => `${s.name} ${s.installed} → ${s.latest}`).join(', ')
156
+ const drift = stale.map((s) => `${s.name}${s.linked ? ' pin' : ''} ${s.installed} → ${s.latest}`).join(', ')
136
157
  const names = stale.map((s) => s.name)
137
158
  if (isRefusal(pm)) {
138
159
  c.warn(`newer rvlib live: ${drift}, not updating: ${pm.refused}`)
@@ -164,7 +185,11 @@ export const rvlibVersions = check({
164
185
  const leased = await withMutateLease(mutateLease(c.root, 'fix'), { label: 'self-update', print: () => {}, escape: 'SHIPKIT_FROZEN=1' }, async () => {
165
186
  const again = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
166
187
  if (again.code === 0 && again.out.trim() !== '') return null
167
- return await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
188
+ const up = await c.exec(cmd, { cwd: c.root, timeoutMs: 120_000 })
189
+ // the update may swap a link for the tarball: put every link back,
190
+ // whatever the update did, inside the same lease
191
+ for (const s of stale) if (s.linked) await c.exec(['bun', 'link', s.name], { cwd: c.root, timeoutMs: 60_000 })
192
+ return up
168
193
  })
169
194
  if (!leased.ok) {
170
195
  c.warn(`newer rvlib live: ${drift} — self-update deferred (${leased.warn}); re-run check once that run finishes`)
@@ -188,6 +213,13 @@ export const rvlibVersions = check({
188
213
  return
189
214
  }
190
215
  for (const s of stale) {
216
+ if (s.linked) {
217
+ const now = pinFloor(readPin(c.root, s.name) ?? '')
218
+ if (!isLinkedInstall(c.root, s.name)) c.fail(`${s.name} pin ${s.installed} → ${now ?? 'unreadable'} but node_modules/${s.name} is no longer linked to its source — run: bun link ${s.name}`)
219
+ else if (now === s.latest) c.warn(`${s.name} pin ${s.installed} → ${now} self-updated, link to the source restored — commit the bump`)
220
+ else c.warn(`${s.name} updated but its pin reads ${now ?? 'unreadable'} (wanted ${s.latest}) — check package.json`)
221
+ continue
222
+ }
191
223
  const now = installedVersion(c.root, s.name)
192
224
  // WARN on purpose: the run MUTATED the repo and a follow-up
193
225
  // (restart watchers, commit) is owed — an ok here would be a
@@ -40,6 +40,10 @@ export const npmPublishSafety = check({
40
40
  owner: 'meta/rv-conventions/package-json.md',
41
41
  // keyed on what it reads: the root package.json and every declared release
42
42
  // unit's, at the dirs the config picks
43
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
44
+ cache: { by: 'reads' },
45
+ fix: 'none',
46
+ applies: 'always',
43
47
  run: (c) => {
44
48
  const json = (rel: string): Pkg | null | 'unparseable' => {
45
49
  try { return c.json<Pkg>(rel) } catch { return 'unparseable' }
@@ -27,8 +27,10 @@ export const githubVisibility = check({
27
27
  title: 'github visibility',
28
28
  description: 'the github visibility of this repo matches the one it declares',
29
29
  owner: 'meta/contracts.md',
30
- ttl: 'forever',
31
- cacheKey: (env) => declaredVisibility(env.config),
30
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: 'once' },
31
+ cache: { by: 'fact', key: (env) => declaredVisibility(env.config) },
32
+ fix: 'none',
33
+ applies: 'always',
32
34
  run: async (c) => {
33
35
  const declared = declaredVisibility(c.config)
34
36
  const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
@@ -86,15 +86,17 @@ export const reviewerAgent = check({
86
86
  title: 'reviewer definition',
87
87
  description: 'the declared reviewer agent has its brief, its definition and read only tools',
88
88
  owner: 'identity/how-we-work.md',
89
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
89
90
  // the answer is the config, the definition file and the brief. The brief
90
91
  // is a CONFIG path that may sit outside the repo, so both files enter the
91
92
  // key by content, wherever they live
92
- inputs: [CONFIG_FILE],
93
- cacheKey: (env) => {
93
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => {
94
94
  const r = env.config.reviewer
95
95
  if (r == null) return 'no reviewer'
96
96
  return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
97
- },
97
+ } },
98
+ fix: 'ci',
99
+ applies: 'always',
98
100
  run: (c) => {
99
101
  const r = c.config.reviewer
100
102
  if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
@@ -109,7 +111,7 @@ export const reviewerAgent = check({
109
111
  // create-if-missing only: an existing definition may carry a repo's own
110
112
  // tuned prompt, and silently rewriting that would cost more than the
111
113
  // finding it repairs. Violations in a present file stay a loud FAIL.
112
- fix: (c): FixResult => {
114
+ repair: (c): FixResult => {
113
115
  const r = c.config.reviewer
114
116
  if (r == null) return { changed: false, summary: 'no reviewer declared — nothing to write' }
115
117
  const rel = reviewerAgentPath(r.name)