rvlib-shipkit 0.38.0 → 1.0.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 (252) hide show
  1. package/CHANGELOG.md +371 -16
  2. package/README.md +26 -14
  3. package/package.json +2 -2
  4. package/src/assets/gen/comfyts-gen.ts +27 -10
  5. package/src/assets/gen/generate-roll.ts +1 -1
  6. package/src/assets/kit-config.ts +6 -6
  7. package/src/assets/run-assets.ts +14 -10
  8. package/src/assets/server/studio-server.ts +6 -5
  9. package/src/check.ts +343 -0
  10. package/src/{rules/SK001-package-json-shape.ts → checks/SK001-package-json-shape.check.ts} +44 -46
  11. package/src/checks/SK002-contracts-fulfillness.check.ts +24 -0
  12. package/src/checks/SK003-claude-md-shape.check.ts +66 -0
  13. package/src/checks/SK004-gitignore-policy.check.ts +309 -0
  14. package/src/{rules/SK005-rvlib-versions.ts → checks/SK005-rvlib-versions.check.ts} +46 -32
  15. package/src/{rules/SK006-npm-publish-safety.ts → checks/SK006-npm-publish-safety.check.ts} +25 -24
  16. package/src/{rules/SK007-github-visibility.ts → checks/SK007-github-visibility.check.ts} +20 -14
  17. package/src/{rules/SK008-reviewer-agent.ts → checks/SK008-reviewer-agent.check.ts} +29 -14
  18. package/src/{rules/SK009-commit-ai-tells.ts → checks/SK009-commit-ai-tells.check.ts} +24 -21
  19. package/src/checks/SK010-undeclared-unit.check.ts +111 -0
  20. package/src/{rules/SK011-sibling-unit-ranges.ts → checks/SK011-sibling-unit-ranges.check.ts} +21 -20
  21. package/src/{rules/SK012-config-barrel-import.ts → checks/SK012-config-barrel-import.check.ts} +18 -23
  22. package/src/{rules/SK013-one-way-in-policy.ts → checks/SK013-one-way-in-policy.check.ts} +58 -50
  23. package/src/{rules/SK014-prose-ste.ts → checks/SK014-prose-ste.check.ts} +27 -13
  24. package/src/{rules/SK015-nested-repo.ts → checks/SK015-nested-repo.check.ts} +15 -12
  25. package/src/checks/SK016-legacy-check-shapes.check.ts +182 -0
  26. package/src/checks/SK017-vscode-hub-task.check.ts +197 -0
  27. package/src/checks/SK018-test-files-run.check.ts +28 -0
  28. package/src/checks/SK019-tests-run-in-units.check.ts +76 -0
  29. package/src/checks/SK020-self-spawning-check.check.ts +147 -0
  30. package/src/checks/contracts.check.ts +20 -0
  31. package/src/cli/cmd_Asks.ts +3 -2
  32. package/src/cli/cmd_Assets.ts +2 -2
  33. package/src/cli/cmd_Bump.ts +1 -1
  34. package/src/cli/cmd_Check.ts +6 -6
  35. package/src/cli/cmd_Checks.ts +95 -0
  36. package/src/cli/cmd_Ci.ts +48 -13
  37. package/src/cli/cmd_Commands.ts +2 -6
  38. package/src/cli/cmd_ConfigKeys.ts +69 -0
  39. package/src/cli/cmd_Contracts.ts +1 -1
  40. package/src/cli/cmd_Fleet.ts +6 -2
  41. package/src/cli/cmd_Hooks.ts +1 -1
  42. package/src/cli/cmd_Keys.ts +43 -18
  43. package/src/cli/cmd_Mv.ts +2 -2
  44. package/src/cli/cmd_Prose.ts +10 -1
  45. package/src/cli/cmd_Releases.ts +2 -2
  46. package/src/cli/cmd_Serve.ts +1 -1
  47. package/src/cli/cmd_Start.ts +6 -6
  48. package/src/cli/cmd_Status.ts +25 -0
  49. package/src/cli/cmd_Tui.ts +2 -2
  50. package/src/cli.ts +7 -4
  51. package/src/collections/actions.ts +4 -4
  52. package/src/collections/checks.ts +258 -0
  53. package/src/collections/collection-types.ts +254 -1
  54. package/src/collections/comfy.ts +23 -0
  55. package/src/collections/md-body.ts +14 -0
  56. package/src/collections/pings.ts +38 -12
  57. package/src/collections/posts.ts +19 -3
  58. package/src/collections/providers.ts +198 -91
  59. package/src/collections/reflections.ts +41 -0
  60. package/src/collections/state-rules.ts +73 -0
  61. package/src/collections/test-time.ts +288 -0
  62. package/src/collections/tests.ts +181 -0
  63. package/src/collections/wikis.ts +66 -0
  64. package/src/comfy/comfy-panel.ts +292 -0
  65. package/src/contracts/apply-contracts.ts +17 -12
  66. package/src/contracts/contract-types.ts +16 -4
  67. package/src/contracts/contracts-engine.ts +78 -42
  68. package/src/contracts/manifests/discord.contract.ts +6 -6
  69. package/src/contracts/manifests/identity.contract.ts +3 -3
  70. package/src/contracts/manifests/itch.contract.ts +12 -15
  71. package/src/contracts/manifests/social.contract.ts +5 -5
  72. package/src/contracts/manifests/steam.contract.ts +16 -16
  73. package/src/contracts/run-contracts.ts +4 -3
  74. package/src/core/attach.ts +96 -0
  75. package/src/core/canonical-scripts.ts +25 -0
  76. package/src/core/check-ctx.ts +204 -14
  77. package/src/core/check-file-names.ts +112 -0
  78. package/src/core/check-pass.ts +84 -0
  79. package/src/core/check-runner.ts +796 -0
  80. package/src/core/config-keys.ts +256 -0
  81. package/src/core/config-rewrite.ts +77 -0
  82. package/src/core/discover-checks.ts +166 -90
  83. package/src/core/discover-legacy-checks.ts +26 -0
  84. package/src/core/doc-block.ts +13 -0
  85. package/src/core/floor-resolve.ts +264 -0
  86. package/src/core/floor.ts +20 -137
  87. package/src/core/git-bin.ts +24 -0
  88. package/src/core/git-root.ts +108 -0
  89. package/src/core/instance.ts +195 -0
  90. package/src/core/jobs.ts +118 -0
  91. package/src/core/layout.ts +52 -0
  92. package/src/core/legacy-shapes.ts +207 -0
  93. package/src/core/lib-root.ts +5 -0
  94. package/src/core/locked-write.ts +110 -39
  95. package/src/core/machine-commit.ts +5 -4
  96. package/src/core/migrate-layout.ts +557 -0
  97. package/src/core/motherhome.ts +2 -8
  98. package/src/core/nesting.ts +4 -12
  99. package/src/core/one-way-in-policy.ts +73 -17
  100. package/src/core/outcome-words.ts +45 -0
  101. package/src/core/package-manager.ts +86 -0
  102. package/src/core/parallel.ts +21 -0
  103. package/src/core/process-tree.ts +44 -0
  104. package/src/core/raw-access.ts +93 -0
  105. package/src/core/read-set.ts +152 -0
  106. package/src/core/run-check.ts +155 -213
  107. package/src/core/run-ci.ts +153 -189
  108. package/src/core/run-fix.ts +96 -0
  109. package/src/core/run-fixers.ts +53 -30
  110. package/src/core/self-update.ts +9 -3
  111. package/src/core/slots.ts +83 -0
  112. package/src/core/spawn-timeout.ts +34 -3
  113. package/src/core/when-block.ts +401 -0
  114. package/src/core/when.ts +135 -0
  115. package/src/core/write-status.ts +5 -2
  116. package/src/hooks/leak-guard.ts +10 -8
  117. package/src/hooks/one-way-in-guard.ts +13 -5
  118. package/src/hooks/root-identity.ts +2 -1
  119. package/src/hooks/run-hooks.ts +35 -23
  120. package/src/init-repo.ts +39 -23
  121. package/src/ledger/check-stats.ts +155 -0
  122. package/src/ledger/check-table.ts +179 -0
  123. package/src/ledger/db.ts +255 -0
  124. package/src/ledger/gate-checks.ts +433 -0
  125. package/src/ledger/git-key.ts +116 -77
  126. package/src/ledger/git-scan.ts +96 -35
  127. package/src/ledger/ledger.ts +116 -42
  128. package/src/ledger/live-runs.ts +111 -0
  129. package/src/ledger/live-timeline.ts +97 -0
  130. package/src/ledger/repo-view.ts +73 -0
  131. package/src/ledger/run-log.ts +448 -0
  132. package/src/ledger/run-timeline.ts +173 -0
  133. package/src/ledger/store.ts +23 -12
  134. package/src/ledger/suite-log.ts +161 -0
  135. package/src/ledger/toolchain-identity.ts +7 -1
  136. package/src/ledger/world-cache.ts +111 -0
  137. package/src/mv/mv-md.ts +16 -6
  138. package/src/problems/extract.ts +182 -0
  139. package/src/problems/problem-board.ts +37 -0
  140. package/src/problems/problem-line.ts +77 -0
  141. package/src/prose/prose-view.ts +90 -0
  142. package/src/prose/run-prose.ts +60 -19
  143. package/src/prose/ste.ts +140 -24
  144. package/src/reflections/reflections-queue.ts +81 -0
  145. package/src/release/git-facts.ts +15 -9
  146. package/src/release/run-bump.ts +6 -5
  147. package/src/release/run-release.ts +25 -11
  148. package/src/release/run-releases.ts +2 -2
  149. package/src/release/unit-gate.ts +7 -54
  150. package/src/release/unit-resolve.ts +282 -0
  151. package/src/release/units.ts +33 -217
  152. package/src/repo-config.ts +155 -85
  153. package/src/rules/rule.ts +36 -96
  154. package/src/rules/untrack-generated.ts +5 -4
  155. package/src/scaffold/files/vscode-tasks.json +10 -31
  156. package/src/scaffold/jsonc-spans.ts +113 -0
  157. package/src/scaffold/project-scaffold.ts +16 -43
  158. package/src/scan.ts +98 -0
  159. package/src/serve/gate-trigger.ts +148 -0
  160. package/src/serve/health-schedule.ts +102 -0
  161. package/src/serve/hub-app/chart-view.ts +179 -0
  162. package/src/serve/hub-app/check-view.tsx +345 -0
  163. package/src/serve/hub-app/check-words.ts +164 -0
  164. package/src/serve/hub-app/checks-svc.ts +280 -0
  165. package/src/serve/hub-app/clock-svc.ts +33 -0
  166. package/src/serve/hub-app/col-card.tsx +23 -39
  167. package/src/serve/hub-app/cols-svc.ts +44 -20
  168. package/src/serve/hub-app/comfy-svc.ts +171 -0
  169. package/src/serve/hub-app/comfy-view.tsx +81 -0
  170. package/src/serve/hub-app/dur.ts +41 -0
  171. package/src/serve/hub-app/hash-route.ts +121 -0
  172. package/src/serve/hub-app/header-bar.tsx +117 -0
  173. package/src/serve/hub-app/health-svc.ts +23 -28
  174. package/src/serve/hub-app/help.tsx +29 -0
  175. package/src/serve/hub-app/hub-app.tsx +122 -119
  176. package/src/serve/hub-app/hub-st.ts +115 -42
  177. package/src/serve/hub-app/hub-types.ts +117 -2
  178. package/src/serve/hub-app/hub.css +534 -192
  179. package/src/serve/hub-app/index.html +2 -0
  180. package/src/serve/hub-app/live-svc.ts +174 -0
  181. package/src/serve/hub-app/md.ts +15 -0
  182. package/src/serve/hub-app/media-svc.ts +4 -1
  183. package/src/serve/hub-app/media-view.tsx +15 -0
  184. package/src/serve/hub-app/next-run.ts +68 -0
  185. package/src/serve/hub-app/project-gutter.css +65 -0
  186. package/src/serve/hub-app/project-gutter.tsx +128 -0
  187. package/src/serve/hub-app/projects-svc.ts +226 -0
  188. package/src/serve/hub-app/prose-hits-st.ts +124 -0
  189. package/src/serve/hub-app/prose-hits-view.tsx +176 -0
  190. package/src/serve/hub-app/prose-hits.css +21 -0
  191. package/src/serve/hub-app/run-strip.ts +47 -0
  192. package/src/serve/hub-app/runs-tabs.ts +24 -0
  193. package/src/serve/hub-app/test-view.tsx +107 -0
  194. package/src/serve/hub-app/tests-svc.ts +62 -0
  195. package/src/serve/hub-app/tests-view.tsx +238 -0
  196. package/src/serve/hub-app/time-ago.tsx +34 -0
  197. package/src/serve/hub-app/timeline-chart.tsx +274 -0
  198. package/src/serve/hub-app/timeline-geometry.ts +314 -0
  199. package/src/serve/hub-app/timeline-svc.ts +62 -0
  200. package/src/serve/hub-app/timeline-view.tsx +245 -0
  201. package/src/serve/hub-app/tree-build.ts +410 -0
  202. package/src/serve/hub-app/tree-row.tsx +42 -0
  203. package/src/serve/hub-app/tree-svc.ts +174 -0
  204. package/src/serve/hub-app/wikis-view.tsx +53 -0
  205. package/src/serve/other-repos.ts +79 -0
  206. package/src/serve/project-gutter-route.ts +350 -0
  207. package/src/serve/project-gutter.ts +449 -0
  208. package/src/serve/run-serve.ts +245 -16
  209. package/src/serve/same-origin.ts +14 -0
  210. package/src/serve/serve-hub.ts +213 -18
  211. package/src/shipkit.ts +19 -12
  212. package/src/smoke/run-smoke.ts +3 -2
  213. package/src/start-repo.ts +75 -36
  214. package/src/suite/judge-suites.ts +216 -18
  215. package/src/suite/kept-timings.ts +58 -0
  216. package/src/suite/runner-shapes.ts +88 -0
  217. package/src/suite/suite-cli.ts +147 -24
  218. package/src/suite/suite-cpu-preload.ts +92 -0
  219. package/src/suite/test-units.ts +243 -0
  220. package/src/ts/config.ts +4 -69
  221. package/src/ts/jsonc.ts +69 -0
  222. package/src/ts/lex.ts +143 -0
  223. package/src/ts/rename.ts +73 -10
  224. package/src/ts/source-graph.ts +28 -8
  225. package/src/ts/tsconfig-projects.ts +6 -97
  226. package/src/ts/tsconfig-resolve.ts +108 -0
  227. package/src/tui/build-steps-panel.ts +61 -0
  228. package/src/tui/checks-panel.ts +24 -16
  229. package/src/tui/run-tui.ts +2 -2
  230. package/src/tui/tui-panel.ts +5 -4
  231. package/src/wiki/graph-colors.ts +68 -0
  232. package/src/wiki/graph-mount.ts +7 -9
  233. package/src/wiki/graph-page.ts +42 -25
  234. package/src/wiki/graph-positions.ts +105 -0
  235. package/src/wiki/run-wiki.ts +1 -1
  236. package/src/wiki/serve-graph.ts +33 -3
  237. package/src/cli/cmd_Rules.ts +0 -51
  238. package/src/cli/cmd_Units.ts +0 -82
  239. package/src/collections/graphs.ts +0 -32
  240. package/src/contracts/contracts-once.ts +0 -43
  241. package/src/core/check-suite.ts +0 -302
  242. package/src/core/repair-contract-dirs.ts +0 -136
  243. package/src/core/task-runner.ts +0 -339
  244. package/src/ledger/gate-units.ts +0 -138
  245. package/src/ledger/unit-table.ts +0 -105
  246. package/src/rules/SK002-contracts-fulfillness.ts +0 -24
  247. package/src/rules/SK003-claude-md-shape.ts +0 -70
  248. package/src/rules/SK004-gitignore-policy.ts +0 -238
  249. package/src/rules/SK010-undeclared-release-unit.ts +0 -127
  250. package/src/rules/rule-registry.ts +0 -64
  251. package/src/rules/run-fix.ts +0 -72
  252. package/src/tui/tasks-panel.ts +0 -60
@@ -1,31 +1,80 @@
1
1
  // shipkit serve — the repo's ONE Bun.serve, serving the ONE page (one server per repo, one surface, mobx+react). `/` is
2
2
  // the hub app (Bun HTML import — media studio + asks + creds + posts +
3
- // health + graphs as sections); the studio route table mounts as the media
3
+ // health + wikis as sections); the studio route table mounts as the media
4
4
  // API under /media/*; graph canvases stay as drill-down pages. Route logic
5
5
  // only — resolving WHAT to mount from repo.config is run-serve.ts's job.
6
6
  import { existsSync } from 'fs'
7
7
  import { join } from 'path'
8
- import { graphHtml } from 'rvlib-shipkit/src/wiki/graph-page.ts'
8
+ import { positionsFile } from 'rvlib-shipkit/src/wiki/graph-positions.ts'
9
+ import { graphRoutes } from 'rvlib-shipkit/src/wiki/serve-graph.ts'
9
10
  import type { StudioRoutes } from '../assets/server/studio-server.ts'
11
+ import { comfyBoxUp, type ComfyBox, type ComfyPanelUp } from '../comfy/comfy-panel.ts'
10
12
  import { appendAskRemark, appendAssetNote, appendPostFeedback, askDone, askProof, markPosted, pickPostVariant } from '../collections/actions.ts'
11
- import type { Collection, Instance, NeedItem } from '../collections/collection-types.ts'
13
+ import { projectResources, type Collection, type Instance, type NeedItem } from '../collections/collection-types.ts'
14
+ import type { CheckEvent } from '../core/run-check.ts'
15
+ import { configDirty, readGateBlock, writeGateBlock, type GateEdit } from '../core/when-block.ts'
16
+ import type { InvokedBy } from '../ledger/run-log.ts'
12
17
  import type { GraphMount } from '../wiki/graph-mount.ts'
18
+ import type { ProseView } from '../prose/prose-view.ts'
19
+ import type { Timeline } from '../ledger/run-timeline.ts'
20
+ import type { TestCheckView, TestsRootView } from '../collections/test-time.ts'
21
+ import type { LiveView } from '../ledger/live-timeline.ts'
22
+
23
+ export type { LiveView }
24
+ import type { HealthSchedule } from './health-schedule.ts'
13
25
  import hubIndex from './hub-app/index.html'
26
+ import { projectGutter } from './project-gutter-route.ts'
27
+ import { sameOrigin } from './same-origin.ts'
28
+ import { LAYOUT } from '../core/layout.ts'
29
+
30
+ export { sameOrigin }
14
31
 
15
32
  export type HubTool = { path: string; label: string; desc: string }
33
+
34
+ /** what a running gate streams back to the page */
35
+ export type CheckTap = { line: (l: string) => void; check: (e: CheckEvent) => void }
36
+ /** the comfy panel, as the hub needs it: the child process it embeds and the
37
+ * box those workflows generate on (src/comfy/comfy-panel.ts owns both) */
38
+ export type ComfyDeps = {
39
+ ensure: () => Promise<ComfyPanelUp>
40
+ restart: () => Promise<ComfyPanelUp>
41
+ box: () => ComfyBox
42
+ stop: () => void
43
+ }
16
44
  export type HubDeps = {
17
45
  root: string
18
46
  name: string
19
47
  /** 0 = random (tests); real launches pass repo.config/default */
20
48
  port: number
21
49
  /** every md graph this hub serves, in card order: /graph/<id>. run-serve
22
- * resolves the list, the routes AND the graphs section read it */
50
+ * resolves the list, the routes AND the wikis page read it */
23
51
  graphs: GraphMount[]
24
52
  /** the media API (mounted under /media/*); null only if the route
25
53
  * table failed to build — the page shows the WARN, never a second page */
26
54
  studio: StudioRoutes | null
27
- /** argv shelling the check run streamed by /api/check/run (test seam) */
28
- checkCmd: string[]
55
+ /** the check run, IN PROCESS: wire lines and per CHECK results stream out
56
+ * as they finish, which is what lets a tree row recolour the moment its
57
+ * check is done. `only` narrows it to a named set (the run button on a
58
+ * check page, or on a folder header); `gate` asks for the whole gate,
59
+ * tasks included. `auto` marks a run the hub started on its own (at
60
+ * start, after a save): quick tier, no repair pass, so it never writes
61
+ * into a tree someone is editing. run-serve owns the routing between the
62
+ * two engines. */
63
+ runChecks: (tap: CheckTap, o?: { only?: string[]; gate?: boolean; by?: InvokedBy; auto?: boolean }) => Promise<number>
64
+ /** the daily health pass timer (health-schedule.ts). The page reads its
65
+ * state to say when a health check runs next; absent = no timer */
66
+ schedule?: HealthSchedule | null
67
+ /** every run on record, shaped for the timeline page. Absent = the page
68
+ * says there is no record rather than drawing an empty one */
69
+ timeline?: () => Promise<Timeline>
70
+ /** where the test checks' time goes: the `tests` page (check null) or one
71
+ * test check's page (collections/test-time.ts). Absent = 404 */
72
+ tests?: (check: string | null) => Promise<TestsRootView | TestCheckView>
73
+ /** the live strip's view: the recent runs and the ones in flight, here
74
+ * and in the other house repos, shaped on the server clock */
75
+ live?: () => LiveView
76
+ /** another house repo's hub url when it serves, null when it does not */
77
+ repoHub?: (repo: string) => Promise<string | null>
29
78
  /** every human-action item, red/yellow, gathered per request from the
30
79
  * collection providers (run-serve builds it) */
31
80
  needs: () => Promise<NeedItem[]>
@@ -34,27 +83,48 @@ export type HubDeps = {
34
83
  collections: () => Collection[]
35
84
  /** asks dir (repo-root-relative) for the act-in-place ask verbs */
36
85
  asksDir: string
86
+ /** the comfy panel; null when the repo declares no workflow module, which
87
+ * is also when the comfy section is absent from the page */
88
+ comfy: ComfyDeps | null
89
+ /** rule SK014's hits, grouped by word, over the config on disk: the data
90
+ * of the SK014 page. Absent = the endpoint answers 404 */
91
+ prose?: () => Promise<ProseView>
92
+ /** re-read repo.config.ts when its bytes moved. The switch WRITES that
93
+ * file, so every surface built on the config asks for this first, or a
94
+ * toggle reads as a toggle that did nothing until the next restart */
95
+ refresh?: () => Promise<void>
37
96
  /** RED-only ping tick, called on an interval while serving (null = off) */
38
97
  pingTick?: ((needs: NeedItem[]) => void) | null
39
98
  }
40
- export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; stop: () => void }
99
+ export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; liveReload: boolean; stop: () => void }
100
+
101
+ /** the tab refreshes ITSELF through bun's hot reload client, which rides in
102
+ * the served page: it swaps a hub-app edit in, and reconnects across the
103
+ * process restart a source edit triggers under `bun --watch`. Bun puts that
104
+ * client on the page only in development mode, which it derives from
105
+ * NODE_ENV at process start, so a production shell serves a page that can
106
+ * never refresh itself. Pinned against the real served bytes in
107
+ * tests/hub-server.test.ts, and run-serve turns a false into a WARN line:
108
+ * silent, this reads exactly like a hub that is simply broken */
109
+ export const canLiveReload = (): boolean => process.env.NODE_ENV !== 'production'
41
110
 
42
111
  // the toolbox as data — stdout ok: lines and /hub.json render THIS list,
43
112
  // so they can never disagree
44
113
  export function hubTools(d: Pick<HubDeps, 'graphs'>): HubTool[] {
45
114
  return [
46
- { path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · graphs' },
115
+ { path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · wikis' },
47
116
  ...d.graphs.map((g) => ({ path: `/graph/${g.id}`, label: g.label, desc: g.title })),
48
117
  ]
49
118
  }
50
119
 
51
120
  // drill-down pages (graph canvases) get one fixed home link back to THE page
52
121
  const HOME_LINK = `<style>.skhome{position:fixed;left:10px;top:10px;z-index:9999;font-size:17px;text-decoration:none;
53
- background:#17140f;border:1px solid #332b22;border-radius:9px;padding:5px 9px}</style><a class=skhome href="/" title="hub — THE page">⌂</a>`
122
+ background:#141619;border:1px solid #282c31;border-radius:3px;padding:5px 9px}</style><a class=skhome href="/" title="hub — THE page">⌂</a>`
54
123
  const withHome = (html: string): string => html.replace(/<body([^>]*)>/, `<body$1>${HOME_LINK}`)
55
124
 
56
125
  export function serveHub(d: HubDeps): Hub {
57
126
  const tools = hubTools(d)
127
+ const projects = projectGutter({ root: d.root })
58
128
  const server: ReturnType<typeof Bun.serve> = Bun.serve({
59
129
  hostname: '127.0.0.1',
60
130
  port: d.port,
@@ -62,13 +132,32 @@ export function serveHub(d: HubDeps): Hub {
62
132
  routes: { '/': hubIndex },
63
133
  async fetch(req): Promise<Response> {
64
134
  const path = new URL(req.url).pathname
135
+ if (!sameOrigin(req)) return new Response('refused: a request that changes something must come from the hub page', { status: 403 })
65
136
  // identity probe: lets a second launch tell "same repo, another
66
137
  // window" from "a DIFFERENT repo hashed onto my port"
67
- if (path === '/hub.json') return Response.json({ hub: 'shipkit', name: d.name, tools })
138
+ // `root` rides with it so a page can put a file link into his
139
+ // editor: only the server knows where this repo sits on disk
140
+ if (path === '/hub.json') return Response.json({ hub: 'shipkit', name: d.name, root: d.root, tools })
141
+ if (path.startsWith('/api/projects')) return (await projects.handle(req, path)) ?? new Response('not found', { status: 404 })
142
+ // the three surfaces built on the config read the config on disk
143
+ if (path === '/api/needs.json' || path === '/api/resources.json' || path === '/api/checks.json' || path === '/api/collections.json') await d.refresh?.()
68
144
  if (path === '/api/needs.json') {
69
145
  const needs = await d.needs().catch((e: Error) => [{ collection: 'hub', id: 'hub:broken', text: `needs projection broke: ${e.message}`, severity: 'red' as const }])
70
146
  return Response.json({ needs })
71
147
  }
148
+ if (path === '/api/resources.json') {
149
+ // ONE flat list for the whole repo: the page groups it by
150
+ // path and by type, and a second endpoint would be a second
151
+ // answer to drift from this one
152
+ const cols = d.collections()
153
+ return Response.json({
154
+ collections: cols.map((c) => ({ id: c.id, title: c.title, media: c.media })),
155
+ resources: await projectResources(cols),
156
+ schedule: d.schedule?.state() ?? null,
157
+ })
158
+ }
159
+ // the previous address, answered while pages opened before the
160
+ // tree are still on screen
72
161
  if (path === '/api/collections.json') {
73
162
  // browse-all: every instance of every collection, healthy
74
163
  // included — failure-isolated per collection
@@ -77,7 +166,7 @@ export function serveHub(d: HubDeps): Hub {
77
166
  try {
78
167
  out.push({ id: col.id, title: col.title, media: col.media, instances: await col.list() })
79
168
  } catch (e) {
80
- out.push({ id: col.id, title: col.title, media: col.media, instances: [{ id: `${col.id}:broken`, text: `collector broke: ${(e as Error).message}`, severity: 'red' }] })
169
+ out.push({ id: col.id, title: col.title, media: col.media, instances: [{ id: `${col.id}:broken`, type: 'health', path: `health/${col.id}-collector`, virtual: true, text: `collector broke: ${(e as Error).message}`, severity: 'red' }] })
81
170
  }
82
171
  }
83
172
  return Response.json({ collections: out })
@@ -112,20 +201,124 @@ export function serveHub(d: HubDeps): Hub {
112
201
  return new Response((e as Error).message, { status: 400 })
113
202
  }
114
203
  }
204
+ if (path.startsWith('/api/comfy/')) {
205
+ // the panel is comfy-ts's, on its own port: the page embeds it,
206
+ // and these two answers are everything the hub knows about it
207
+ if (d.comfy == null) return new Response('this repo has no .shipkit/comfy/workflows module', { status: 400 })
208
+ if (path === '/api/comfy/panel' && req.method === 'POST') {
209
+ try {
210
+ // comfy-ts serve imports the modules at BOOT, so an
211
+ // edited workflow needs a new process, never a reload
212
+ const b = (await req.json().catch(() => ({}))) as { restart?: boolean }
213
+ return Response.json(b.restart === true ? await d.comfy.restart() : await d.comfy.ensure())
214
+ } catch (e) {
215
+ return new Response((e as Error).message, { status: 400 })
216
+ }
217
+ }
218
+ if (path === '/api/comfy/box') {
219
+ const box = d.comfy.box()
220
+ return Response.json({ ...box, up: await comfyBoxUp(box.url) })
221
+ }
222
+ }
223
+ if (path === '/api/live.json') {
224
+ if (d.live == null) return Response.json({ now: Date.now(), historyMs: 0, cap: 0, busy: 0, invocations: [], repos: [], runs: [], queued: 0, blocked: [] } satisfies LiveView)
225
+ return Response.json(d.live(), { headers: { 'cache-control': 'no-store' } })
226
+ }
227
+ if (path === '/api/repo-hub') {
228
+ const repo = new URL(req.url).searchParams.get('repo') ?? ''
229
+ return Response.json({ url: d.repoHub == null ? null : await d.repoHub(repo) })
230
+ }
231
+ if (path === '/api/timeline.json') {
232
+ if (d.timeline == null) return new Response('this hub keeps no run record', { status: 404 })
233
+ try {
234
+ return Response.json(await d.timeline())
235
+ } catch (e) {
236
+ return new Response(`the run record could not be shaped: ${(e as Error).message}`, { status: 500 })
237
+ }
238
+ }
239
+ if (path === '/api/tests.json') {
240
+ if (d.tests == null) return new Response('this hub keeps no run record', { status: 404 })
241
+ await d.refresh?.()
242
+ try {
243
+ return Response.json(await d.tests(new URL(req.url).searchParams.get('check')), { headers: { 'cache-control': 'no-store' } })
244
+ } catch (e) {
245
+ return new Response(`the test times could not be shaped: ${(e as Error).message}`, { status: 500 })
246
+ }
247
+ }
115
248
  if (path === '/api/status.json') {
116
- const p = join(d.root, 'STATUS.md')
249
+ const p = join(d.root, LAYOUT.status)
117
250
  return Response.json({ statusMd: existsSync(p) ? await Bun.file(p).text() : null })
118
251
  }
252
+ if (path === '/api/checks.json') {
253
+ // the SAME rows /api/resources.json carries, addressable on
254
+ // their own so an agent can curl them. One provider, two
255
+ // addresses, so the two can never disagree
256
+ const checks = d.collections().find((c) => c.id === 'checks')
257
+ if (checks == null) return Response.json({ checks: [] })
258
+ return Response.json({ checks: await checks.list() })
259
+ }
260
+ if (path === '/api/checks/when') {
261
+ // THE SWITCH. One endpoint for one writer: where a check runs
262
+ // and which suites are slow are the same managed block in
263
+ // repo.config.ts, and `expect` is the hash of the file the
264
+ // page last read, so a write can never land on top of an edit
265
+ // it never saw. The file is left DIRTY: committing it is his
266
+ if (req.method === 'GET') {
267
+ const b = await readGateBlock(d.root)
268
+ return Response.json({ ...b, dirty: configDirty(d.root) })
269
+ }
270
+ if (req.method === 'POST') {
271
+ try {
272
+ const body = (await req.json()) as GateEdit & { expect?: string }
273
+ const b = await writeGateBlock(d.root, { when: body.when, slow: body.slow, slowRemove: body.slowRemove, words: body.words }, { expect: body.expect })
274
+ await d.refresh?.()
275
+ return Response.json({ ...b, dirty: configDirty(d.root) })
276
+ } catch (e) {
277
+ return new Response((e as Error).message, { status: 400 })
278
+ }
279
+ }
280
+ }
281
+ if (path === '/api/prose.json' && d.prose != null) {
282
+ // every SK014 hit, grouped by word, noisiest first. The word
283
+ // lists are written through /api/checks/when like every other
284
+ // edit of repo.config.ts
285
+ await d.refresh?.()
286
+ try {
287
+ return Response.json(await d.prose())
288
+ } catch (e) {
289
+ return new Response(`the prose check could not run: ${(e as Error).message}`, { status: 500 })
290
+ }
291
+ }
119
292
  if (path === '/api/check/run' && req.method === 'POST') {
120
- // stream wire lines as the check emits them; sh merges stderr
121
- // so FAIL lines reach the page, not just the terminal
122
- const proc = Bun.spawn(['sh', '-c', 'exec "$0" "$@" 2>&1', ...d.checkCmd], { cwd: d.root, stdout: 'pipe', stderr: 'ignore' })
123
- return new Response(proc.stdout, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
293
+ const body = (await req.json().catch(() => ({}))) as { only?: string[]; gate?: boolean }
294
+ // NDJSON: one line per event, so the page colours a row the
295
+ // moment its check finishes instead of waiting for the wall of
296
+ // text a shelled run could only deliver whole
297
+ const enc = new TextEncoder()
298
+ const stream = new ReadableStream({
299
+ async start(ctrl) {
300
+ const send = (e: Record<string, unknown>): void => {
301
+ try { ctrl.enqueue(enc.encode(`${JSON.stringify(e)}\n`)) } catch { /* the tab went away mid-run */ }
302
+ }
303
+ let code = 1
304
+ try {
305
+ code = await d.runChecks({ line: (text) => send({ t: 'line', text }), check: (u) => send({ t: 'check', ...u }) }, { only: body.only, gate: body.gate })
306
+ } catch (e) {
307
+ send({ t: 'line', text: `FAIL: the run broke: ${(e as Error).message}` })
308
+ }
309
+ send({ t: 'done', code })
310
+ ctrl.close()
311
+ },
312
+ })
313
+ return new Response(stream, { headers: { 'content-type': 'application/x-ndjson; charset=utf-8', 'cache-control': 'no-store' } })
124
314
  }
125
315
  const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
126
316
  for (const g of d.graphs) {
127
- if (path === `/graph/${g.id}`) return page(graphHtml(g.load(), { title: g.title }))
317
+ if (!path.startsWith(`/graph/${g.id}`)) continue
318
+ const routes = graphRoutes({ load: g.load, title: g.title, file: positionsFile(d.root, g.id), saveUrl: `/graph/${g.id}/positions` })
319
+ if (path === `/graph/${g.id}`) return page(routes.page())
128
320
  if (path === `/graph/${g.id}/graph.json`) return Response.json(g.load())
321
+ if (path === `/graph/${g.id}/positions`) return routes.save(req)
129
322
  }
130
323
  // the media API namespace — the studio route table, ONE prefix strip
131
324
  if (d.studio != null && path.startsWith('/media/')) return d.studio.handle(req, path.slice('/media'.length))
@@ -142,5 +335,7 @@ export function serveHub(d: HubDeps): Hub {
142
335
  tick()
143
336
  pingTimer = setInterval(tick, 60_000)
144
337
  }
145
- return { server, tools, stop: () => { if (pingTimer != null) clearInterval(pingTimer); server.stop(true) } }
338
+ // the panel process belongs to this hub: leaving it behind means the next
339
+ // launch adopts a server holding the previous run's modules
340
+ return { server, tools, liveReload: canLiveReload(), stop: () => { if (pingTimer != null) clearInterval(pingTimer); d.schedule?.stop(); d.comfy?.stop(); server.stop(true) } }
146
341
  }
package/src/shipkit.ts CHANGED
@@ -1,6 +1,14 @@
1
1
  // rvlib-shipkit — barrel. One import per repo: defineRepo in repo.config.ts;
2
2
  // everything else is the machinery behind the shipkit CLI.
3
- export { defineRepo, loadRepoConfig, resolveFloor, allTasks, contractsTask } from './repo-config.ts'
3
+ export { defineRepo, loadRepoConfig, resolveFloor } from './repo-config.ts'
4
+ export { allTasks, composeChecks, gateInventory, gateRunnables, gateChecks } from './ledger/gate-checks.ts'
5
+ export type { GateCheck, CheckRow } from './ledger/gate-checks.ts'
6
+ // THE one shape every gate check has, and the file suffix that registers one
7
+ export { check } from './check.ts'
8
+ export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
9
+ export type { CheckDef, CheckEnv, CheckFrom } from './check.ts'
10
+ export { discoverChecks, builtinCheckIds } from './core/discover-checks.ts'
11
+ export type { DiscoveredCheck } from './core/discover-checks.ts'
4
12
  export type { RepoConfig, ServeConfig, FloorResolution, FloorSource } from './repo-config.ts'
5
13
  export { contractsConfigFrom, activeContracts, applyContractsResult } from './contracts/apply-contracts.ts'
6
14
  // the hub (`shipkit serve`): one server, one port, every tool a click away
@@ -12,24 +20,24 @@ export { projectNeeds, redCount } from './collections/collection-types.ts'
12
20
  export type { Collection, Instance, NeedItem, NeedSeverity, CurationVerb } from './collections/collection-types.ts'
13
21
  export { buildCollections } from './collections/providers.ts'
14
22
  export { socialPostsCollection, listPosts } from './collections/posts.ts'
15
- // release units — the ONE discovery every release surface reads (drift
23
+ // units — the ONE discovery every release surface reads (drift
16
24
  // probes, per-unit tests, per-unit typecheck, the stranded-tag probe)
17
25
  export { resolveUnits, releaseOrder, unitById, renderTag, tagGlobs, prepublishScripts, unitPrepublishScripts } from './release/units.ts'
18
- export type { ReleaseUnit, ReleaseUnitDecl, UnitResolution } from './release/units.ts'
26
+ export type { ReleaseUnit, UnitDecl, UnitResolution } from './release/units.ts'
19
27
  export { deriveRemote, dirtyPaths, remoteTags, strandedTags, tagExistsLocally } from './release/git-facts.ts'
20
28
  export { collectReports, renderReports, attentionLines } from './release/run-releases.ts'
21
29
  export { extractChangelogSection, promoteUnreleased, hasUnreleasedNotes, hasVersionSection } from './release/changelog.ts'
22
30
  export type { UnitReport } from './release/run-releases.ts'
23
- export { discoverChecks } from './core/discover-checks.ts'
24
- export type { DiscoveredCheck } from './core/discover-checks.ts'
25
- export { runTasks, selectTasks } from './core/task-runner.ts'
26
- export type { TaskDef, TaskResult, RunOpts, RunSummary } from './core/task-runner.ts'
27
- export { CheckSuite } from './core/check-suite.ts'
28
- export type { CheckSuiteOpts, CheckOutcome, GetSecret, SecretSpec } from './core/check-suite.ts'
31
+ export { discoverLegacyChecks } from './core/discover-legacy-checks.ts'
32
+ export type { LegacyCheck } from './core/discover-legacy-checks.ts'
33
+ // THE runner: one DAG over every check, whatever declared it
34
+ export { runChecks, selectChecks } from './core/check-runner.ts'
35
+ export type { RunnableCheck, CheckResult, CheckEvent, RunChecksOpts, RunChecksSummary } from './core/check-runner.ts'
36
+ export type { TaskDef } from './check.ts'
29
37
  // tui: type-only here — the store/view/panels import ink+mobx, which must
30
38
  // not load on every `shipkit ci`; runtime pieces live at rvlib-shipkit/src/tui/*
31
39
  export type { TuiPanelDef, TuiRow, TuiRowStatus, MarkedLine } from './tui/tui-panel.ts'
32
- export { CheckCtx, RateLimited, ToolUnresolved } from './core/check-ctx.ts'
40
+ export { CheckCtx, RateLimited, type ScanCtx } from './core/check-ctx.ts'
33
41
  export { spawnWithTimeout } from './core/spawn-timeout.ts'
34
42
  export type { SpawnOutcome, SpawnTimeoutOpts } from './core/spawn-timeout.ts'
35
43
  export { writeStatus } from './core/write-status.ts'
@@ -38,7 +46,6 @@ export { formatLine, parseDuration, fmtStamp, parseWireOutput } from './core/wir
38
46
  export type { Line } from './core/wire.ts'
39
47
  // the SK rule registry — stable-id invariants `shipkit check` enforces;
40
48
  // consumers (e.g. a wiki lint verifying mentioned ids exist) read RULES
41
- export { RULES, ruleById } from './rules/rule-registry.ts'
42
49
  export { tagRuleLines } from './rules/rule.ts'
43
50
  export type { Rule, RuleEnv, FixResult } from './rules/rule.ts'
44
51
  export { evaluateContracts, pngDims } from './contracts/contracts-engine.ts'
@@ -49,7 +56,7 @@ export { allContracts } from './contracts/contract-manifests.ts'
49
56
  export { listAsks, openAsks, checkAsks, createAsk, renderAsksTable } from './asks/asks-queue.ts'
50
57
  export type { Ask, CreateOpts } from './asks/asks-queue.ts'
51
58
  // assets surface (folded in from the standalone atelier lib; that npm
52
- // package is frozen). A repo's .rv-assets/kit.ts imports defineKit and
59
+ // package is frozen). A repo's .shipkit/assets/kit.ts imports defineKit and
53
60
  // declares its kinds; `shipkit assets …` derives the rest.
54
61
  export { defineKit, defineKind, comfy, canvas, manual, gemini, mediaOf, MEDIA_EXTS, geometryPanel, gridPanel, customPanel, validateKit, missingPanelScripts, loadKit, findAsset, allAssets, categoriesOf } from './assets/kit-config.ts'
55
62
  export type { KitConfig, ResolvedKit, Kind, KindMeta, Media, AssetSpec, GeneratorSpec, ComfyGen, CanvasGen, ManualGen, GeminiGen, PanelSpec, GeometryPanelSpec, GridPanelSpec, CustomPanelSpec, FoundAsset, ShipContext, ShipFn } from './assets/kit-config.ts'
@@ -15,6 +15,7 @@
15
15
  // exercised in the field, by the run itself.
16
16
  import { mkdirSync } from 'fs'
17
17
  import { join } from 'path'
18
+ import { LAYOUT } from '../core/layout.ts'
18
19
 
19
20
  export type SmokeRoute = {
20
21
  /** the path to visit, always leading `/` */
@@ -50,7 +51,7 @@ export type SmokeConfig = {
50
51
  routes: (string | SmokeRoute)[]
51
52
  /** absent: the run visits the routes anonymously */
52
53
  login?: SmokeLogin
53
- /** pause between routes, ms. A route change fires a burst of requests, and
54
+ /** pause between routes, ms, default 3000. A route change fires a burst of requests, and
54
55
  * a host with a CONCURRENT cap sheds some of them without CORS headers,
55
56
  * which then reads as a broken app. 3000 is measured against PocketHost's
56
57
  * 15 concurrent per IP: 0 reported 8 of 12 routes, 3000 reports 12 of 12. */
@@ -66,7 +67,7 @@ const ROUTE_KEYS = new Set(['path', 'waitFor', 'open'])
66
67
  export const DEFAULT_LOGIN_PATH = '/login'
67
68
 
68
69
  export const DEFAULT_PACE_MS = 3000
69
- export const DEFAULT_SHOTS = 'tmp/smoke'
70
+ export const DEFAULT_SHOTS = `${LAYOUT.tmp}/smoke`
70
71
 
71
72
  /** the whole shape, refused loud on the first thing that is not so. A typo
72
73
  * here would otherwise land as a run that visits nothing and exits 0. */
package/src/start-repo.ts CHANGED
@@ -1,7 +1,10 @@
1
- // `shipkit start` — ensure <root>/<name> exists and, when --include lines
2
- // are given, that CLAUDE.md carries them (in order, at the top). Plain
3
- // module behind cmd_Start. Status lines go to STDERR — the ONLY stdout
4
- // output is the resolved dir, so a sourced shell alias can
1
+ // `shipkit start` — ensure <root>/<name> exists and that CLAUDE.md carries
2
+ // its include lines (in order, at the top): the HOUSE lines below, unless
3
+ // --include overrides them or the house index is not on this disk, then
4
+ // `@./agent/index.md` whenever the repo has that file, because the scaffold
5
+ // writes it and SK003 fails a repo that has it unrouted. Plain module behind
6
+ // cmd_Start. Status lines go to STDERR —
7
+ // the ONLY stdout output is the resolved dir, so a sourced shell alias can
5
8
  // `cd "$(shipkit start <name>)"` (the one thing a child process can't do
6
9
  // for its parent shell).
7
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'fs'
@@ -9,18 +12,37 @@ import { homedir } from 'os'
9
12
  import { join } from 'path'
10
13
  import { ensureGitRepo, ensureIgnoreBaseline, firstGhLine, isScratch, publishRepo, runner, type Run } from './git-bootstrap.ts'
11
14
  import { initRepo } from './init-repo.ts'
15
+ import { AGENT_LINE } from './checks/SK003-claude-md-shape.check.ts'
12
16
  import { scaffoldProject, TOOLCHAIN } from './scaffold/project-scaffold.ts'
13
17
  import { defaultHubPort } from './serve/hub-port.ts'
14
18
 
19
+ /** the one template: `shipkit start <name>` is the only thing anyone types,
20
+ * and a repo without the corvion wiki in its context is not a started repo.
21
+ * Base context first, the repo's own router last, so repo docs override.
22
+ * A public repo swaps line 1 for the gitignored `@./CLAUDE.local.md`,
23
+ * which is why that line satisfies the first include (SK003) */
24
+ export const HOUSE_INCLUDE = ['@~/dev/corvion/wiki/index.md', '@./agent/index.md']
25
+ export const HOUSE_ALSO_OK = ['@./CLAUDE.local.md']
26
+ /** the day-job checkout: never wired, never given an include */
27
+ export const HOUSE_SKIP = ['monoloco']
28
+
29
+ /** where the first house line points, `~` resolved against `home` */
30
+ export const housePath = (home: string): string => {
31
+ const p = HOUSE_INCLUDE[0]!.replace(/^@/, '')
32
+ return p.startsWith('~/') ? join(home, p.slice(2)) : p
33
+ }
34
+
15
35
  export type StartOpts = {
16
36
  /** parent dir for repos — default ~/dev */
17
37
  root?: string
18
- /** lines CLAUDE.md must carry, in order; omitted = only ensure the dir */
38
+ /** lines CLAUDE.md must carry, in order; omitted = HOUSE_INCLUDE. An empty list means no house: only the agent route lands */
19
39
  include?: string[]
20
- /** alternative lines that satisfy the FIRST include — never double-add */
40
+ /** alternative lines that satisfy the FIRST include — never double-add; omitted = HOUSE_ALSO_OK */
21
41
  alsoOk?: string[]
22
- /** repo names that never get includes */
42
+ /** repo names shipkit never touches; omitted = HOUSE_SKIP */
23
43
  skip?: string[]
44
+ /** the home dir the first house line resolves against; omitted = os.homedir() */
45
+ home?: string
24
46
  /** true forces the github half, false refuses it. undefined = the default:
25
47
  * a dir this run created, or an existing project, gets its private repo */
26
48
  github?: boolean
@@ -36,38 +58,22 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
36
58
  console.error(`FAIL: bad repo name '${name}' — letters/digits/._- only, no leading dot, no slashes`)
37
59
  return 2
38
60
  }
39
- const dir = join(opts.root ?? join(homedir(), 'dev'), name)
61
+ const home = opts.home ?? homedir()
62
+ const dir = join(opts.root ?? join(home, 'dev'), name)
40
63
  const justCreated = !existsSync(dir)
41
64
  if (justCreated) {
42
65
  mkdirSync(dir, { recursive: true })
43
66
  console.error(`ok: created ${dir}`)
44
67
  }
45
- const want = opts.include ?? []
46
- if (want.length === 0 || (opts.skip ?? []).includes(name)) {
47
- if (want.length > 0) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
48
- } else {
49
- const claudeMd = join(dir, 'CLAUDE.md')
50
- if (!existsSync(claudeMd)) {
51
- writeFileSync(claudeMd, `${want.join('\n')}\n`)
52
- console.error(`ok: CLAUDE.md created with ${want.length} include line(s)`)
53
- } else {
54
- const cur = readFileSync(claudeMd, 'utf8')
55
- const lineSet = new Set(cur.split('\n'))
56
- const alt = opts.alsoOk ?? []
57
- const satisfied = (w: string): boolean => lineSet.has(w) || (w === want[0] && alt.some((a) => lineSet.has(a)))
58
- const missing = want.filter((w) => !satisfied(w))
59
- if (missing.length > 0) {
60
- // insert after any leading include/alt lines so order survives
61
- const lines = cur.split('\n')
62
- const known = new Set([...want, ...alt])
63
- let at = 0
64
- while (at < lines.length && known.has(lines[at]!)) at++
65
- lines.splice(at, 0, ...missing)
66
- writeFileSync(claudeMd, lines.join('\n'))
67
- console.error(`ok: ${missing.length} missing include line(s) inserted into CLAUDE.md`)
68
- }
69
- }
70
- }
68
+ // flags win, else the house, and only on a machine that HAS the house:
69
+ // this lib is on npm, so elsewhere the template would be an include
70
+ // pointing at nothing plus the widened agent permissions riding on it
71
+ const houseOnDisk = existsSync(housePath(home))
72
+ const house = opts.include ?? (houseOnDisk ? HOUSE_INCLUDE : [])
73
+ if (opts.include == null && !houseOnDisk) console.error(`skip: no house wiki at ${housePath(home)} — CLAUDE.md gets the agent route only, and no .claude/ policy files`)
74
+ const alt = opts.alsoOk ?? HOUSE_ALSO_OK
75
+ const skipped = (opts.skip ?? HOUSE_SKIP).includes(name)
76
+ if (skipped && house.length > 0) console.error(`skip: ${name} is include-exempt — CLAUDE.md untouched`)
71
77
  // start is the from-anywhere verb, and a STARTED repo comes out READY:
72
78
  // any dir with no commit history gets a minimal package.json, a first
73
79
  // src/index.ts, the rvlib-shipkit dev dep, repo.config.ts via init, git,
@@ -78,7 +84,7 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
78
84
  // --skip means shipkit NEVER TOUCHES a listed repo (a day-job checkout
79
85
  // belongs on that list), not just no CLAUDE.md. And the cd contract survives a
80
86
  // failed adoption: the dir always prints, wiring trouble is a WARN.
81
- if (!(opts.skip ?? []).includes(name)) {
87
+ if (!skipped) {
82
88
  // git BEFORE the shipkit scaffold, and whether or not the dir is a
83
89
  // project yet: an empty new dir is still work that must not live on
84
90
  // one disk. The PUBLISH (first commit → private repo → push) is the
@@ -120,7 +126,7 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
120
126
  // command line: init takes the caller's name over the manifest's,
121
127
  // and a dir found holding a forked package.json carries a name
122
128
  // nothing will listen on
123
- try { scaffoldProject(dir, { name, port: defaultHubPort(name), print, claudePolicy: (opts.include ?? []).length > 0 }) }
129
+ try { scaffoldProject(dir, { name, port: defaultHubPort(name), print, claudePolicy: house.length > 0 }) }
124
130
  catch (e) { console.error(`WARN scaffold incomplete in ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
125
131
  // the wired scripts call node_modules binaries, so a scaffold
126
132
  // whose scripts exit 127 is not a started repo. Skips mirror the
@@ -150,6 +156,12 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
150
156
  catch (e) { console.error(`WARN could not wire ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
151
157
  } else unscaffolded = true
152
158
  }
159
+ // CLAUDE.md after the scaffold and before the publish: the agent/
160
+ // router the scaffold just wrote gets routed, LAST (include order is
161
+ // semantic, SK003), and the file rides inside the first commit
162
+ const want = [...house]
163
+ if (existsSync(join(dir, 'agent/index.md')) && !want.includes(AGENT_LINE)) want.push(AGENT_LINE)
164
+ if (want.length > 0) ensureIncludes(join(dir, 'CLAUDE.md'), want, alt)
153
165
  if (wired) {
154
166
  try { await publishRepo(dir, { name, github: opts.github, justCreated, gh: opts.gh, print }) }
155
167
  catch (e) { console.error(`WARN could not publish ${dir}: ${e instanceof Error ? e.message : String(e)} — the dir is still yours`) }
@@ -167,6 +179,33 @@ export async function startRepo(name: string, opts: StartOpts = {}): Promise<num
167
179
  return 0
168
180
  }
169
181
 
182
+ /** CLAUDE.md carries every wanted line, in order, at the top. Idempotent:
183
+ * a line already there, or an `alt` line standing in for the first one, is
184
+ * never added twice */
185
+ function ensureIncludes(claudeMd: string, want: string[], alt: string[]): void {
186
+ if (!existsSync(claudeMd)) {
187
+ writeFileSync(claudeMd, `${want.join('\n')}\n`)
188
+ console.error(`ok: CLAUDE.md created with ${want.length} include line(s)`)
189
+ return
190
+ }
191
+ const lines = readFileSync(claudeMd, 'utf8').split('\n')
192
+ const lineSet = new Set(lines)
193
+ const satisfied = (w: string): boolean => lineSet.has(w) || (w === want[0] && alt.some((a) => lineSet.has(a)))
194
+ const missing = want.filter((w) => !satisfied(w))
195
+ if (missing.length === 0) return
196
+ // insert after any leading include/alt lines so order survives, and
197
+ // BEFORE an agent route already there: a base line arriving later must
198
+ // not land under it and invert the order
199
+ const known = new Set([...want, ...alt])
200
+ let at = 0
201
+ while (at < lines.length && known.has(lines[at]!)) at++
202
+ const agentAt = lines.slice(0, at).indexOf(AGENT_LINE)
203
+ if (agentAt !== -1) at = agentAt
204
+ lines.splice(at, 0, ...missing)
205
+ writeFileSync(claudeMd, lines.join('\n'))
206
+ console.error(`ok: ${missing.length} missing include line(s) inserted into CLAUDE.md`)
207
+ }
208
+
170
209
  /** what the closing line is allowed to say, all of it measured */
171
210
  export type RepoFacts = { tracked: number; uncommitted: number; hasHead: boolean; origin: string | null; pushed: boolean }
172
211