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
@@ -1,23 +1,22 @@
1
- // SK009 — commit AI tells (identity/how-we-commit.md): a repo that will be
2
- // seen must not read as AI-driven. Prevention lives at the source (the
3
- // attribution setting + the standing from-commit-one ban, which binds every
4
- // repo because history follows a repo public and is never rewritten); this
5
- // rule is the declared-public tripwire: the FULL git log scanned for AI
6
- // attribution and dash punctuation, plus the tracked settings file that
7
- // silences attribution. Declared-private repos idle with an ok that RESTATES
8
- // the ban — a green line must never teach the wrong rule. The remedy for
9
- // pushed history is the owner's decision, never an automatic rewrite; only
10
- // an unpushed tip commit may be amended, and the fails say so.
1
+ // SK009 commit AI tells (identity/how-we-commit.md): the commit-msg hook is
2
+ // what refuses AI attribution, in every repo, so this check verifies the hook
3
+ // is ARMED: shipkit's own hooksPath, or a foreign one whose commit-msg chains
4
+ // the shipkit gate. It never scans history: what is pushed is accepted as it
5
+ // is, and only a new commit can still be stopped. A declared-public repo also
6
+ // needs the tracked attribution silencer, and its ref NAMES are checked (a
7
+ // FABLE-checkpoint tag outs the agent like a trailer); a branch nobody pushed
8
+ // is named "local only" and warns.
11
9
  import { createHash } from 'crypto'
12
10
  import { readFileSync } from 'fs'
13
- import { join } from 'path'
14
- import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
11
+ import { isAbsolute, join } from 'path'
15
12
  import { nestedIn } from '../core/nesting.ts'
16
13
  import { gitBin } from '../core/git-bin.ts'
17
14
  import { fileStamp } from '../core/file-stamp.ts'
18
15
  import { CONFIG_FILE } from '../core/check-pass.ts'
16
+ import { LAYOUT } from '../core/layout.ts'
19
17
  import { check, type CheckEnv } from '../check.ts'
20
18
  import { declaredVisibility } from '../core/repo-facts.ts'
19
+ import { hookBinInvocation } from '../hooks/run-hooks.ts'
21
20
 
22
21
  // the documented silencer: .claude/settings.json attribution.commit/.pr both
23
22
  // empty — the harness then never writes the trailer or the PR footer
@@ -30,81 +29,102 @@ const attributionSilenced = (root: string): boolean => {
30
29
  }
31
30
  }
32
31
 
33
- /** what the answer depends on: the declared visibility, and for a public
34
- * repo the commit HEAD names, every ref name and the silencer. So a commit,
35
- * a fetch or a new tag asks again, and nothing else does */
32
+ const readOrNull = (abs: string): string | null => {
33
+ try {
34
+ return readFileSync(abs, 'utf8')
35
+ } catch {
36
+ return null
37
+ }
38
+ }
39
+
40
+ /** a commit-msg body that runs the shipkit gate on the message: a live line
41
+ * (never a comment) whose failure is not swallowed by `|| true` */
42
+ const chainsShipkit = (body: string): boolean =>
43
+ body.split('\n').some((l) => !l.trim().startsWith('#') && /\bhooks\s+commit-msg\b/.test(l) && !/\|\|\s*(?:true|:)\b/.test(l))
44
+
45
+ /** the chain line to paste into a foreign hook: a clone with no install skips
46
+ * the gate with a note, rather than failing every commit */
47
+ export const guardedChain = (bin: string): string => {
48
+ const cli = bin.replace(/^bun\s+/, '')
49
+ return `if [ -f ${cli} ]; then ${bin} hooks commit-msg "$1" || exit 1; else echo "commit-msg: rvlib-shipkit not installed (run bun install), attribution gate skipped" >&2; fi`
50
+ }
51
+
52
+ // ref NAMES tell too. bare match only for names with no other meaning;
53
+ // vendor and codename words (cursor, openai, gemini, opus, fable, …) name
54
+ // legitimate branches and features, they only match in a checkpoint/session shape
55
+ const REF_TELL = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
56
+
57
+ const gitOut = (root: string, args: string[]): { code: number; out: string } => {
58
+ const p = Bun.spawnSync([gitBin(), ...args], { cwd: root, stdout: 'pipe', stderr: 'pipe' })
59
+ return { code: p.exitCode, out: p.stdout.toString() }
60
+ }
61
+
62
+ /** what the answer depends on: the declared visibility, the hooksPath and the
63
+ * commit-msg file it points at, and for a public repo every ref NAME and the
64
+ * silencer. A commit moves none of them */
36
65
  export function historyKey(env: CheckEnv): string {
37
- const declared = declaredVisibility(env.config)
38
66
  const nested = nestedIn(env.root)
39
67
  if (nested != null) return `nested in ${nested}`
40
- if (declared !== 'public') return `declared ${declared}`
41
- const git = (args: string[]): string => {
42
- const p = Bun.spawnSync([gitBin(), ...args], { cwd: env.root, stdout: 'pipe', stderr: 'pipe' })
43
- return p.exitCode === 0 ? p.stdout.toString() : `exit ${p.exitCode}`
44
- }
45
- const refs = git(['for-each-ref', '--format=%(refname) %(objectname)', 'refs/tags', 'refs/heads', 'refs/remotes'])
46
- const head = git(['rev-parse', 'HEAD'])
47
- const digest = createHash('sha256').update(head).update('\0').update(refs).digest('hex').slice(0, 16)
48
- return `public|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
68
+ const declared = declaredVisibility(env.config)
69
+ const hooksPath = gitOut(env.root, ['config', 'core.hooksPath'])
70
+ const dir = hooksPath.code === 0 ? hooksPath.out.trim() : ''
71
+ const hook = `hooks=${dir}|msg=${dir === '' ? 'none' : fileStamp(isAbsolute(dir) ? join(dir, 'commit-msg') : join(env.root, dir, 'commit-msg'))}`
72
+ if (declared !== 'public') return `declared ${declared}|${hook}`
73
+ const refs = gitOut(env.root, ['for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes']).out
74
+ const digest = createHash('sha256').update(refs).digest('hex').slice(0, 16)
75
+ return `public|${hook}|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
49
76
  }
50
77
 
51
78
  export const commitAiTells = check({
52
79
  id: 'SK009',
53
80
  blocking: true,
54
81
  title: 'commit ai tells',
55
- description: 'the history of a public repo carries no AI attribution and no dash punctuation',
82
+ description: 'the commit-msg hook that refuses AI attribution is armed, and no ref name tells',
56
83
  owner: 'identity/how-we-commit.md',
57
- // it scans the full `git log` and every ref name, which no tree id
58
- // describes, so the refs themselves are the key
59
- inputs: [CONFIG_FILE],
60
- cacheKey: historyKey,
84
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
85
+ // git config and ref names, which no tree id describes, so they are the key
86
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: historyKey },
87
+ fix: 'none',
88
+ applies: 'always',
61
89
  run: async (c) => {
62
90
  const nested = nestedIn(c.root)
63
- if (nested != null)
64
- return void c.lines.push({ kind: 'skip', text: `commit history not scanned, nested in ${nested}: that history belongs to the parent repo` })
65
- const declared = declaredVisibility(c.config)
66
- if (declared !== 'public')
67
- return c.ok(`declared ${declared} — the no-AI-attribution ban still applies from commit one (identity/how-we-commit.md); the full-history scan arms at visibility: 'public'`)
68
- // %x1f separates hash from body, %x1e terminates the record — commit
69
- // bodies are free text, so only unprintable separators are safe
70
- const log = await c.exec(['git', 'log', '--format=%h%x1f%B%x1e'], { cwd: c.root, timeoutMs: 30_000 })
71
- if (log.code !== 0) {
72
- const first = log.out.trim().split('\n').find((l) => l.trim() !== '')?.slice(0, 120) ?? `exit ${log.code}`
73
- return void c.lines.push({ kind: 'skip', text: `commit history unverifiable — git says: ${first}` })
91
+ if (nested != null) return void c.lines.push({ kind: 'skip', text: `commit gates not checked, nested in ${nested}: the parent repo owns them` })
92
+ const top = await c.exec(['git', 'rev-parse', '--show-toplevel'], { cwd: c.root })
93
+ if (top.code !== 0) return void c.lines.push({ kind: 'skip', text: 'not a git repo: no commit to guard yet' })
94
+
95
+ const hp = await c.exec(['git', 'config', 'core.hooksPath'], { cwd: c.root })
96
+ const dir = hp.code === 0 ? hp.out.trim() : ''
97
+ const chain = guardedChain(hookBinInvocation(c.root))
98
+ if (dir === '' && process.env['CI'] != null && process.env['CI'] !== '') {
99
+ c.lines.push({ kind: 'skip', text: 'commit-msg hook not armed in this CI checkout: a CI run makes no commit, and the fix pass arms hooks on a machine only' })
100
+ } else if (dir === '') {
101
+ c.fail('the commit-msg hook is not armed, so nothing refuses an AI attribution trailer: run `shipkit hooks install` (`shipkit ci` arms it too)')
102
+ } else {
103
+ const body = isAbsolute(dir) ? readOrNull(join(dir, 'commit-msg')) : c.read(`${dir}/commit-msg`)
104
+ const own = dir === LAYOUT.hooks
105
+ if (body == null)
106
+ c.fail(`core.hooksPath=${dir} has no ${dir}/commit-msg, so nothing refuses an AI attribution trailer: ${own ? 'run `shipkit hooks install`' : `add one that runs \`${chain}\``}`)
107
+ else if (!chainsShipkit(body))
108
+ c.fail(`core.hooksPath=${dir}: its commit-msg (${body.split('\n').find((l) => l.trim() !== '' && !l.startsWith('#'))?.trim().slice(0, 80) ?? 'empty'}) does not run the shipkit gate, so nothing refuses AI attribution: add \`${chain}\` above its last line`)
109
+ else c.ok(`the commit-msg hook refuses AI attribution (${own ? dir : `${dir}, chained to the shipkit gate`})`)
74
110
  }
75
- const commits = log.out
76
- .split('\x1e')
77
- .map((r) => r.trim())
78
- .filter((r) => r !== '')
79
- .map((r) => {
80
- const cut = r.indexOf('\x1f')
81
- return { hash: r.slice(0, cut), body: r.slice(cut + 1) }
82
- })
83
- if (commits.length === 0) return void c.lines.push({ kind: 'skip', text: 'commit history unverifiable — no commits yet' })
84
- const remedy = 'an unpushed TIP commit may be amended; anything deeper is the owner\'s decision (history is never rewritten as a side effect)'
85
- const sample = (hits: { hash: string }[]): string => hits.slice(0, 5).map((h) => h.hash).join(' ') + (hits.length > 5 ? ' …' : '')
86
- const attributed = commits.filter((m) => hasAiAttribution(m.body))
87
- const dashed = commits.filter((m) => hasDashPunctuation(m.body))
88
- // ref NAMES tell too (a FABLE-checkpoint-3 tag outs the agent as
89
- // loudly as a trailer). bare match only for names with no other
90
- // meaning; vendor and codename words (cursor, openai, gemini, opus,
91
- // fable, …) name legitimate branches and features, they only match in
92
- // a checkpoint/session shape
93
- const refTell = /\b(?:claude|chatgpt|copilot|aider|devin)\b|(?:^|[-_/])(?:fable|sonnet|haiku|opus|gemini|codex)[-_](?:checkpoint|session|ckpt)/i
94
- const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname:short)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
95
- if (refs.code !== 0) c.lines.push({ kind: 'skip', text: `ref names unverifiable — git for-each-ref exit ${refs.code}` })
96
- const tellRefs = refs.code === 0 ? refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '' && refTell.test(r)) : []
97
- if (tellRefs.length > 0)
98
- c.fail(`${tellRefs.length} ref name(s) carry an AI tell in a declared-public repo (${tellRefs.slice(0, 5).join(' ')}${tellRefs.length > 5 ? ' …' : ''}) — rename or delete the refs before the flip; a pushed tag also needs the remote delete`)
99
- if (attributed.length > 0)
100
- c.fail(`${attributed.length}/${commits.length} commits carry AI attribution (a trailer naming an assistant / a session trailer or claude.ai url / "Generated with" / 🤖) in a declared-public repo — e.g. ${sample(attributed)}; ${remedy}`)
101
- if (dashed.length > 0)
102
- c.fail(`${dashed.length}/${commits.length} commit messages carry dash punctuation (em/en dash or inline " - ") in a declared-public repo — e.g. ${sample(dashed)}; ${remedy} (identity/writing-voice.md owns the punctuation rule)`)
103
- const silenced = attributionSilenced(c.root)
104
- if (!silenced)
105
- c.fail(`.claude/settings.json does not silence AI attribution — track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
106
- if (attributed.length === 0 && dashed.length === 0 && tellRefs.length === 0 && silenced)
107
- c.ok(`${commits.length} commits + ${refs.code === 0 ? refs.out.split('\n').filter((r) => r.trim() !== '').length : 0} ref names scanned, attribution silenced in .claude/settings.json — no AI tells`)
111
+
112
+ if (declaredVisibility(c.config) !== 'public') return
113
+ if (!attributionSilenced(c.root))
114
+ c.fail(`.claude/settings.json does not silence AI attribution: track it with {"attribution": {"commit": "", "pr": ""}} so no agent's harness ever writes the trailer or the PR footer`)
115
+ const refs = await c.exec(['git', 'for-each-ref', '--format=%(refname)', 'refs/tags', 'refs/heads', 'refs/remotes'], { cwd: c.root, timeoutMs: 30_000 })
116
+ if (refs.code !== 0) return void c.lines.push({ kind: 'skip', text: `ref names unverifiable: git for-each-ref exit ${refs.code}` })
117
+ const all = refs.out.split('\n').map((r) => r.trim()).filter((r) => r !== '')
118
+ const remote = new Set(all.filter((r) => r.startsWith('refs/remotes/')).map((r) => r.split('/').slice(3).join('/')))
119
+ const tells = all.filter((r) => REF_TELL.test(r.replace(/^refs\/(?:heads|tags|remotes)\//, '')))
120
+ const shown = (r: string): string => r.replace(/^refs\/(?:heads|tags|remotes)\//, '')
121
+ const local = tells.filter((r) => r.startsWith('refs/heads/') && !remote.has(shown(r)))
122
+ const out = tells.filter((r) => !local.includes(r) && !(r.startsWith('refs/heads/') && remote.has(shown(r))))
123
+ const list = (rs: string[], suffix = ''): string => rs.slice(0, 5).map((r) => `${shown(r)}${suffix}`).join(' ') + (rs.length > 5 ? ' …' : '')
124
+ if (out.length > 0)
125
+ c.fail(`${out.length} ref name(s) carry an AI tell in a declared-public repo (${list(out)}): rename or delete them, a pushed one on the remote too, then \`git fetch --prune\` so a remote ref deleted there leaves this clone`)
126
+ if (local.length > 0)
127
+ c.warn(`${local.length} branch name(s) carry an AI tell (${list(local, ' (local only)')}): never push them under this name, rename or delete them`)
108
128
  },
109
129
  })
110
130
 
@@ -40,6 +40,10 @@ export const undeclaredUnit = check({
40
40
  owner: 'build/shipkit/units.md',
41
41
  // keyed on what it reads: the listing of every package.json, each one it
42
42
  // opens, and the declared units' package.json
43
+ runs: { onPreCommit: false, onCi: true, 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 { units, errors, testOnly } = resolveUnits(c.root, c.config, checkUnitReader(c))
45
49
  // a DECLARED unit that does not resolve is a declaration bug and
@@ -61,6 +61,10 @@ export const siblingUnitRanges = check({
61
61
  owner: 'build/shipkit/built-in-checks.md',
62
62
  // keyed on what it reads: every declared unit's package.json, at the dirs
63
63
  // the config picks
64
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
65
+ cache: { by: 'reads' },
66
+ fix: 'none',
67
+ applies: 'always',
64
68
  run: (c) => {
65
69
  const { units } = resolveUnits(c.root, c.config, checkUnitReader(c))
66
70
  const version = new Map(units.map((u) => [u.name, u.version]))
@@ -30,6 +30,10 @@ export const configBarrelImport = check({
30
30
  title: 'config import path',
31
31
  description: 'repo.config.ts imports `defineRepo` from the narrow module, never the barrel',
32
32
  owner: 'build/shipkit/config-keys.md',
33
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
34
+ cache: { by: 'reads' },
35
+ fix: 'ci',
36
+ applies: 'always',
33
37
  run: (c) => {
34
38
  const src = c.read(CONFIG)
35
39
  if (src == null) return void c.ok(`${CONFIG} absent — nothing to narrow`)
@@ -43,7 +47,7 @@ export const configBarrelImport = check({
43
47
  }
44
48
  c.fail(`${CONFIG} imports from the package barrel (~400ms of module load on every shipkit run, the pre-commit hook included)`)
45
49
  },
46
- fix: (c): FixResult => {
50
+ repair: (c): FixResult => {
47
51
  const src = c.read(CONFIG)
48
52
  if (src == null) return { changed: false, summary: `${CONFIG} absent` }
49
53
  if (!BARREL.test(src)) return { changed: false, summary: 'already narrow' }
@@ -1,4 +1,4 @@
1
- // SK013 — the harness half of ONE WAY IN (build/shipkit.md). Rule SK001 routes
1
+ // SK013 — the harness half of ONE WAY IN (build/shipkit/one-way-in.md). Rule SK001 routes
2
2
  // the package.json scripts, and that alone is not enough: `bun test` is a bun
3
3
  // BUILTIN, so no script can intercept it and the reflex stays open. This rule
4
4
  // makes the repo carry the two things that CAN close it, and repairs them:
@@ -19,7 +19,7 @@ import { rootPublishesNpm } from '../core/repo-facts.ts'
19
19
  import type { RepoConfig } from '../repo-config.ts'
20
20
  import { check, type CheckCtx, type FixResult } from '../check.ts'
21
21
 
22
- const OWNER = 'build/shipkit.md'
22
+ const OWNER = 'build/shipkit/one-way-in.md'
23
23
  export const SETTINGS_PATH = '.claude/settings.json'
24
24
 
25
25
  type Settings = {
@@ -124,6 +124,10 @@ export const oneWayInPolicy = check({
124
124
  // keyed on what it reads: .claude/settings.json, package.json, and whether
125
125
  // the declared guard script resolves. A guard at an absolute path lives
126
126
  // outside the repo, so an answer that probed one is never stored
127
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
128
+ cache: { by: 'reads' },
129
+ fix: 'ci',
130
+ applies: 'always',
127
131
  run: async (c) => {
128
132
  if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
129
133
  const d = c
@@ -171,7 +175,7 @@ export const oneWayInPolicy = check({
171
175
  // MERGES, never rewrites: this file also carries the attribution silencer,
172
176
  // a repo's own allow list and its own hooks, and clobbering those to fix a
173
177
  // deny list would cost more than the finding it repairs
174
- fix: async (c): Promise<FixResult> => {
178
+ repair: async (c): Promise<FixResult> => {
175
179
  if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
176
180
  const d = c
177
181
  const want = denyRules(exemptSlots(c.config))
@@ -7,16 +7,17 @@
7
7
  //
8
8
  // The tier split is the whole design. `fix` and `hard` hits BLOCK: they are
9
9
  // mechanical, and a machine reporting them is never guessing. `soft` hits
10
- // (sentence length, passive voice) need a human read, so they WARN, and only
11
- // a declared `baseline` makes them blocking, as a ratchet that can only go
12
- // down.
10
+ // (sentence length, passive voice, modals, semicolons, …) need a human read,
11
+ // so they are a note, never an attention item, and only a declared
12
+ // `baseline` makes them blocking, as a ratchet that can only go down.
13
13
  //
14
14
  // It carries NO `fix()` on purpose. `shipkit ci` runs `--fix-rules` by
15
15
  // default, so a rule with a fixer rewrites tracked files unattended, and
16
16
  // prose is not repo shape: a wrong package.json line is a config, a wrong
17
17
  // sentence is content. The rewrite lives behind an explicit `shipkit prose
18
18
  // --fix` that a human ran and can read the diff of.
19
- import { classSummary, proseLines, runProseCheck } from '../prose/run-prose.ts'
19
+ import { classSummary, proseLines, runProseCheck, type ProseReport } from '../prose/run-prose.ts'
20
+ import { tierOf, type ProseClass } from '../prose/ste.ts'
20
21
  import { check } from '../check.ts'
21
22
 
22
23
  const OWNER = 'identity/writing-voice.md'
@@ -29,6 +30,10 @@ export const proseSte = check({
29
30
  owner: OWNER,
30
31
  // keyed on what it reads: each include glob's listing and every file in
31
32
  // scope, so an edit outside the declared prose never reruns it
33
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
34
+ cache: { by: 'reads' },
35
+ fix: 'none',
36
+ applies: 'always',
32
37
  run: (c) => {
33
38
  const cfg = c.config.prose
34
39
  if (cfg == null || (cfg.include ?? []).length === 0) {
@@ -53,12 +58,21 @@ export const proseSte = check({
53
58
  `${blocking} STE violation(s) across ${r.byFile.length}/${r.files} file(s) — ${classSummary(r)}`,
54
59
  r.fixable > 0 ? `shipkit prose --fix rewrites ${r.fixable} of them` : undefined,
55
60
  )
61
+ const soft = softSummary(r)
56
62
  if (cfg.baseline != null && r.byTier.soft > cfg.baseline)
57
- c.fail(`${r.byTier.soft} soft hits (sentence length, passive voice), above the declared baseline of ${cfg.baseline} — the ratchet only goes down`)
63
+ c.fail(`${r.byTier.soft} soft hits (${soft}), above the declared baseline of ${cfg.baseline}: the ratchet only goes down`)
58
64
  else if (r.byTier.soft > 0)
59
- c.warn(`${r.byTier.soft} soft hit(s): sentence length and passive voice need a human read${cfg.baseline != null ? ` (baseline ${cfg.baseline})` : ''}`)
65
+ c.note(`${r.byTier.soft} soft hit(s) need a human read (${soft})${cfg.baseline != null ? `, within the baseline of ${cfg.baseline}` : ''}: \`shipkit prose\` lists them`)
60
66
  if (!c.failed && blocking === 0) c.ok(`${r.files} file(s) STE-clean on the mechanical rules`)
61
67
  }
62
68
  })
63
69
 
70
+ /** every soft class with its count, most first */
71
+ const softSummary = (r: ProseReport): string =>
72
+ Object.entries(r.byClass)
73
+ .filter(([cls]) => tierOf(cls as ProseClass) === 'soft')
74
+ .sort((a, b) => b[1] - a[1])
75
+ .map(([cls, n]) => `${cls} ${n}`)
76
+ .join(', ')
77
+
64
78
  export default proseSte
@@ -12,10 +12,12 @@ export const nestedRepo = check({
12
12
  title: 'nested repo',
13
13
  description: 'a config inside a larger work tree declares `nested: true`, and only there',
14
14
  owner: 'build/shipkit/config-keys.md',
15
+ runs: { onPreCommit: { budgetMs: 20 }, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
15
16
  // the declaration, and where the git work tree root sits, which no tree
16
17
  // id describes, so its path is the rest of the key
17
- inputs: [CONFIG_FILE],
18
- cacheKey: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}`,
18
+ cache: { by: 'files', inputs: [CONFIG_FILE], key: (env) => `nested=${env.config.nested === true}|top=${nestedIn(env.root) ?? 'root'}` },
19
+ fix: 'none',
20
+ applies: 'always',
19
21
  run: (c) => {
20
22
  const top = nestedIn(c.root)
21
23
  const declared = c.config.nested === true
@@ -1,8 +1,9 @@
1
1
  // SK016 — the shapes the check file replaced, and the machine that
2
2
  // rewrites them (build/shipkit/writing-a-check.md owns the shape itself).
3
3
  //
4
- // The window: every shape below is READ through 0.40, named once per run, and
5
- // refused at the first release after `shipkit fleet` shows no repo below 0.40.
4
+ // The window: every shape below is still READ, named once per run, and
5
+ // refused at the first release after `shipkit fleet` shows no repo below 0.40
6
+ // and none still carrying a check script.
6
7
  // So a repo on an old declaration keeps working, learns what to write instead
7
8
  // on every gate, and is carried across by one command, save what only a
8
9
  // reader can port: a check script's body, a rule, a task whose body is code.
@@ -48,6 +49,10 @@ export const legacyCheckShapes = check({
48
49
  blocking: true,
49
50
  // keyed on what it reads: the listing of check files, their sources, and
50
51
  // the config. A gitignored check file is no declaration of this repo
52
+ runs: { onPreCommit: false, onCi: false, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
53
+ cache: { by: 'reads' },
54
+ fix: 'manual',
55
+ applies: 'always',
51
56
  run: (c) => {
52
57
  const s = legacyShapes(c, c.config)
53
58
  if (s.count === 0 && s.placeholders.length === 0) {
@@ -60,7 +65,7 @@ export const legacyCheckShapes = check({
60
65
  if (s.placeholders.length > 0)
61
66
  c.warn(
62
67
  `${s.placeholders.length} check file(s) still open with the TODO description the migration wrote: ${s.placeholders.join(', ')}`,
63
- 'replace each TODO with one line stating what must be true',
68
+ 'by hand: replace each TODO with one line stating what must be true',
64
69
  )
65
70
  if (s.count === 0) return
66
71
  // one WARN per shape, never a bare line: the gate promises every line
@@ -69,7 +74,7 @@ export const legacyCheckShapes = check({
69
74
  const head = s.checkFiles.slice(0, 3).map((f) => f.rel).join(', ')
70
75
  c.warn(`${s.checkFiles.length} legacy check script(s) read by the static reader: ${head}${s.checkFiles.length > 3 ? `, and ${s.checkFiles.length - 3} more` : ''}`)
71
76
  const off = s.checkFiles.filter((f) => !f.read.gate).length
72
- if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is \`when: 'health'\``)
77
+ if (off > 0) c.warn(`${off} of them carry \`export const gate = false\`, which is every gate axis false and onHealth true`)
73
78
  }
74
79
  if (s.checkDefaults) c.warn(`repo.config.ts checkDefaults lets a file forget \`blocking\` — the value belongs in each file`)
75
80
  if (s.tasks.length > 0) {
@@ -77,13 +82,18 @@ export const legacyCheckShapes = check({
77
82
  c.warn(`${s.tasks.length} \`tasks: []\` entr(ies), ${data} of them pure data — each one is a *.check.ts file with \`build: true\``)
78
83
  }
79
84
  if (s.rules.length > 0) c.warn(`${s.rules.length} \`rules: []\` entr(ies): ${s.rules.join(', ')} — each one is a *.check.ts file whose \`run\` is its \`lint\``)
80
- if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`when: { id: 'off:<why>' }\``)
85
+ if (s.ruleExempt.length > 0) c.warn(`ruleExempt names ${s.ruleExempt.join(', ')} — the spelling is \`off: { id: '<why>' }\`, and \`shipkit ci\` ports it (SK029)`)
86
+ // what the fixer rewrites without guessing: the exemptions, the tasks
87
+ // once all of them are data, checkDefaults once no script leans on it
88
+ const mechanical = (s.tasks.every((x) => x.data) ? s.tasks.length : 0) + (s.checkDefaults && s.checkFiles.length === 0 ? 1 : 0)
81
89
  c.warn(
82
- `${s.count} declaration(s) in a shape the check file replaced — read through 0.40, refused after`,
83
- 'shipkit check --fix SK016 rewrites what is mechanical and names the rest',
90
+ `${s.count} declaration(s) in a shape the check file replaced: still read, and refused by the first shipkit release after \`shipkit fleet\` shows no repo below 0.40 and none still carrying a check script`,
91
+ mechanical > 0
92
+ ? `shipkit check --fix SK016 rewrites ${mechanical} of them and names the rest`
93
+ : `nothing left is mechanical, so the port is by hand: each line above becomes a *.check.ts file (${OWNER})`,
84
94
  )
85
95
  },
86
- fix: async (c) => await fixLegacyShapes(c),
96
+ repair: async (c) => await fixLegacyShapes(c),
87
97
  })
88
98
 
89
99
  export default legacyCheckShapes
@@ -156,17 +166,14 @@ export async function fixLegacyShapes(c: CheckCtx, rewrite: ConfigRewriter = inO
156
166
  for (const r of config.rules ?? [])
157
167
  hand.push(`rule ${r.id}: move it into ${checkFileName(r.id)} beside repo.config.ts, \`lint\` becomes \`run\` and \`code\` goes (the file's own path is its closure)`)
158
168
 
159
- // 4. the exemptions that were their own key, as `when` entries (the
160
- // managed block is the one writer), then 5. the config keys, last: a
161
- // key is only dropped once every declaration that leaned on it has a
162
- // home. Both load the config, so they run in their own process
163
- const cfgPlan: ConfigPlan = { when: {}, drop: [], name: config.name }
164
- for (const [id, why] of Object.entries(config.ruleExempt ?? {})) cfgPlan.when[id] = `off:${why}`
169
+ // 4. the config keys, last: a key is only dropped once every declaration
170
+ // that leaned on it has a home. It loads the config, so it runs in its
171
+ // own process. `ruleExempt` is SK029's: it ports it into `off`
172
+ const cfgPlan: ConfigPlan = { off: {}, drop: [], name: config.name }
165
173
  if (s.checkDefaults && filesLeft === 0) cfgPlan.drop.push({ key: 'checkDefaults', why: 'every file now declares its own blocking' })
166
174
  else if (s.checkDefaults) hand.push(`repo.config.ts \`checkDefaults\` stays until the ${filesLeft} file(s) above declare their own \`blocking\``)
167
175
  if (tasksMove) cfgPlan.drop.push({ key: 'tasks', why: 'every task is a check file now' })
168
- if (s.ruleExempt.length > 0) cfgPlan.drop.push({ key: 'ruleExempt', why: 'the exemptions are `when` entries now', afterWhen: true })
169
- if (Object.keys(cfgPlan.when).length > 0 || cfgPlan.drop.length > 0) {
176
+ if (cfgPlan.drop.length > 0) {
170
177
  const out = await rewrite(cfgPlan)
171
178
  did.push(...out.did)
172
179
  hand.push(...out.hand)
@@ -12,7 +12,7 @@ import { check } from '../check.ts'
12
12
  import type { CheckCtx } from '../core/check-ctx.ts'
13
13
  import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
14
14
  import { rootPublishesNpm } from '../core/repo-facts.ts'
15
- import { whenOfTask } from '../core/when.ts'
15
+ import { repoOff } from '../core/when.ts'
16
16
  import { hubProblemMatcher } from '../problems/problem-line.ts'
17
17
  import { indentAt, parseSpans, plain, removalSpan, type Node } from '../scaffold/jsonc-spans.ts'
18
18
 
@@ -24,12 +24,15 @@ type Arr = Extract<Node, { kind: 'array' }>
24
24
 
25
25
  const prop = (o: Obj, key: string): Node | undefined => o.props.find((p) => p.key === key)?.value
26
26
 
27
+ /** the type checker binaries: tsc, and tsgo, its native port */
28
+ const TSC_BINS = new Set(['tsc', 'tsgo'])
29
+
27
30
  /** the house type checker task and nothing that only looks like it: the exact
28
- * label, a tsc binary, in watch mode */
31
+ * label, a tsc or tsgo binary, in watch mode */
29
32
  function isTscWatcher(t: Record<string, unknown>): boolean {
30
33
  const cmd = typeof t['command'] === 'string' ? t['command'] : ''
31
34
  const args = Array.isArray(t['args']) ? t['args'] : []
32
- return t['label'] === TSC_LABEL && basename(cmd) === 'tsc' && args.includes('--watch')
35
+ return t['label'] === TSC_LABEL && TSC_BINS.has(basename(cmd)) && args.includes('--watch')
33
36
  }
34
37
 
35
38
  /** the hub task: `bun run hub`, or the house label */
@@ -90,9 +93,17 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
90
93
  if (/\/\/.*(master task|subtask)/i.test(text)) changes.push('a comment still describes a master task with subtasks: rewrite it by hand')
91
94
  }
92
95
 
96
+ // a wrapper that started only the TSC task starts nothing once it goes
97
+ const emptied = !dropTsc ? [] : tasks.items.flatMap((n, i) => (n !== wrapper && n.kind === 'object' && isWrapper(record(n)) && depsOf(n).length === 0 ? [i] : []))
98
+ for (const i of emptied) {
99
+ const [start, end] = removalSpan(text, tasks, i)
100
+ edits.push({ start, end, body: '' })
101
+ changes.push(`removed the '${labelOf(tasks.items[i]!)}' wrapper, which started nothing else`)
102
+ }
103
+
93
104
  if (dropTsc) {
94
105
  for (const t of items) {
95
- if (t === wrapper) continue
106
+ if (t === wrapper || emptied.some((i) => tasks.items[i] === t)) continue
96
107
  const d = prop(t, 'dependsOn')
97
108
  if (d?.kind === 'array') {
98
109
  const idx = d.items.findIndex((x) => x.kind === 'scalar' && x.value === TSC_LABEL)
@@ -148,13 +159,13 @@ export function repairTasks(text: string, o: RepairOpts): { text: string; change
148
159
  }
149
160
 
150
161
  /** whether this repo's gate runs a typecheck: a floor slot or a task that
151
- * resolves, and no `when` turning it off */
162
+ * resolves, and no `off` entry turning it off */
152
163
  export function typecheckOn(c: CheckCtx): boolean {
153
164
  const config = c.config
154
165
  let floor: ReturnType<typeof resolveFloor>
155
166
  try { floor = resolveFloor(config, c.root, checkFloorReader(c)) } catch { return false }
156
167
  const t = [...floor.tasks, ...(config.tasks ?? [])].find((x) => x.id === 'typecheck')
157
- return t != null && whenOfTask(config, t).at !== 'off'
168
+ return t != null && repoOff(config, t.id) == null
158
169
  }
159
170
 
160
171
  /** the tasks file this check may touch: none, or left alone because .vscode/
@@ -178,6 +189,10 @@ export const vscodeHubTask = check({
178
189
  title: 'vscode hub task',
179
190
  description: 'the vscode hub task carries the problem matcher and no extra tsc watcher runs',
180
191
  owner: 'build/vscode-tasks.md',
192
+ runs: { onPreCommit: false, onCi: false, onFull: false, onRelease: false, onHealth: true, onSave: 'moved', onClock: false },
193
+ cache: { by: 'reads' },
194
+ fix: 'manual',
195
+ applies: 'always',
181
196
  run: (c) => {
182
197
  if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
183
198
  const own = ownTasks(c)
@@ -190,7 +205,7 @@ export const vscodeHubTask = check({
190
205
  if (r.changes.length === 0) return c.ok(`${TASKS}: the hub task feeds the problems pane`)
191
206
  for (const ch of r.changes) c.fail(`${TASKS}: ${ch.replace(/^removed/, 'should remove').replace(/^dropped/, 'should drop').replace(/^wrote/, 'should write').replace(/^folded/, 'should fold')}`)
192
207
  },
193
- fix: (c) => {
208
+ repair: (c) => {
194
209
  if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
195
210
  const own = ownTasks(c)
196
211
  if (typeof own === 'string') return { changed: false, summary: `no ${TASKS} to repair` }
@@ -1,4 +1,4 @@
1
- // SK018 — every test file runs in a check (build/shipkit.md, the test checks).
1
+ // SK018 — every test file runs in a check (build/shipkit/test-runner.md).
2
2
  // A `*.test.ts` that no check runs is a green nobody measured: the file says
3
3
  // something is proved, and nothing proves it. Each file belongs to the release
4
4
  // unit whose dir holds it (the root takes the rest), and the unit's own
@@ -12,7 +12,11 @@ export const testFilesRun = check({
12
12
  blocking: true,
13
13
  title: 'test files run',
14
14
  description: 'every test file in the repo runs in a test check or is ignored on purpose',
15
- owner: 'build/shipkit.md',
15
+ owner: 'build/shipkit/test-runner.md',
16
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
17
+ cache: { by: 'reads' },
18
+ fix: 'none',
19
+ applies: 'always',
16
20
  run: (c) => {
17
21
  const files = c.list(TEST_GLOB).filter(isTestFile)
18
22
  if (files.length === 0) return c.ok('no test file in this repo')
@@ -1,5 +1,5 @@
1
- // SK019 — tests run through the generated test checks only (build/shipkit.md,
2
- // the test checks). The floor `test` slot and one `test:<unit>` per declared
1
+ // SK019 — tests run through the generated test checks only (build/shipkit/test-runner.md).
2
+ // The floor `test` slot and one `test:<unit>` per declared
3
3
  // unit are the ONE way tests run: in the unit's dir with its bunfig and
4
4
  // preload, parallel, timed against the budget, and planned file by file by
5
5
  // SK018. A hand written check that runs `bun test` itself is a second runner
@@ -9,7 +9,7 @@
9
9
  // Its own id rather than a line of SK018: SK018 answers "does every test file
10
10
  // run", this one "does anything else run them", and each is read from
11
11
  // different files (test files and bunfigs there, check sources and the config
12
- // here), so each keeps its own key, row and `when` switch.
12
+ // here), so each keeps its own key, row and `off` switch.
13
13
  import { dirname, posix, relative } from 'path'
14
14
  import { check } from '../check.ts'
15
15
  import { repoCheckSources } from '../core/legacy-shapes.ts'
@@ -36,7 +36,11 @@ export const testsRunInUnits = check({
36
36
  blocking: true,
37
37
  title: 'tests run in unit checks',
38
38
  description: 'no check runs the test runner itself: tests run only in the generated test checks',
39
- owner: 'build/shipkit.md',
39
+ owner: 'build/shipkit/test-runner.md',
40
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
41
+ cache: { by: 'reads' },
42
+ fix: 'none',
43
+ applies: 'always',
40
44
  run: (c) => {
41
45
  // the toolkit's own checks (this file's dir) live inside the repo that
42
46
  // hosts it, and a built-in that WRITES a test script (SK001) is not a
@@ -132,6 +132,10 @@ export const selfSpawningCheck = check({
132
132
  description: 'no check file runs its own body by spawning itself',
133
133
  owner: OWNER,
134
134
  blocking: true,
135
+ runs: { onPreCommit: false, onCi: true, onFull: true, onRelease: true, onHealth: true, onSave: 'moved', onClock: false },
136
+ cache: { by: 'reads' },
137
+ fix: 'none',
138
+ applies: 'always',
135
139
  run: (c) => {
136
140
  const found = repoCheckSources(c).flatMap((f) => {
137
141
  if (f.src == null || isLegacyCheckSource(f.src)) return []