rvlib-shipkit 0.22.1 → 0.22.2

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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,10 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.22.2
4
+
5
+ - **Frozen gates never write the motherhome row.** The bump preflight ate the machine dirt, then the gate INSIDE the bump re-wrote the local-projects row and the post-gate cleanliness check refused, the same wart's second head. A frozen run is release-shaped: it reports nothing to the motherhome, so the tree it proves clean stays clean. Interactive ci/check keep reporting.
6
+ - **The hub banner says only what exists.** It printed the hub URL twice (the banner plus the '/' tool row) and advertised a wiki graph in repos with no wiki: the default wiki graph now mounts only when a `wiki/` dir exists (a config-declared one always mounts), the '/' tool stays in /hub.json but never re-prints, and a game repo's startup reads one hub line plus the routes it really serves.
7
+
3
8
  ## 0.22.1
4
9
 
5
10
  - **Fix: the cache stamp is baked in at RUN time, replays print it verbatim.** 0.22.0 annotated only the REPLAY, so a fresh run and a cache hit rendered different strings and STATUS.md still diffed on every alternation (his catch, the second one on this line). A TTL clean pass now writes "(cached at <stamp> until <stamp>)" into the line once, live output and every replay are byte-identical, and the line changes only when a re-validation happens. Proven: fresh-then-cached runs produce a zero-diff STATUS body.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "rvlib-shipkit",
3
- "version": "0.22.1",
3
+ "version": "0.22.2",
4
4
  "description": "All-in-one repo companion: ci gate (task DAG), auto-discovered health checks, release-contract fulfillness, asks queue, md-wiki tools, asset atelier, live tui",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -123,7 +123,9 @@ export async function collectChecks(root: string, o: CheckOpts = {}): Promise<Ch
123
123
  const r = await suite.execute()
124
124
  if (prevFresh == null) delete process.env.SHIPKIT_FRESH; else process.env.SHIPKIT_FRESH = prevFresh
125
125
  if (prevFrozen == null) delete process.env.SHIPKIT_FROZEN; else process.env.SHIPKIT_FROZEN = prevFrozen
126
- reportToMotherhome(root, c)
126
+ // frozen = a release-shaped run: reporting would re-dirty the tree the
127
+ // caller is about to demand clean (field wart 2026-08-05, second head)
128
+ if (o.frozen !== true) reportToMotherhome(root, c)
127
129
  return { lines: r.lines, blocked: r.exitCode !== 0, advisoryFailures, undeclared }
128
130
  }
129
131
 
@@ -120,9 +120,13 @@ export async function runCi(root: string, o: CiOpts = {}): Promise<number> {
120
120
  } else sink(`WARN ${checks.advisoryFailures.length} non-blocking check(s) failing, about the world rather than this repo: ${checks.advisoryFailures.join(', ')}`)
121
121
  }
122
122
  const code = summary.ok && !fixFailed && !checks.blocked && !advisoryFatal && checks.undeclared.length === 0 ? 0 : 1
123
- // motherhome row (docsBase wiki on this disk) — never affects the gate
124
- const mh = reportToMotherhome(root, c)
125
- if (mh.note != null) print(mh.note)
123
+ // motherhome row (docsBase wiki on this disk) — never affects the gate,
124
+ // and never fires under frozen: a bump/release gate must not re-dirty
125
+ // the tree it just proved clean (field wart 2026-08-05, second head)
126
+ if (o.frozen !== true) {
127
+ const mh = reportToMotherhome(root, c)
128
+ if (mh.note != null) print(mh.note)
129
+ }
126
130
  if (o.status !== false) {
127
131
  // scope is named in the file: ci proves the gate, not repo health, and
128
132
  // a bare GREEN here would claim clear-to-ship on a repo with red checks
@@ -27,9 +27,14 @@ export function defaultHubPort(name: string): number {
27
27
  }
28
28
 
29
29
  export async function resolveHubDeps(root: string, c: RepoConfig, io: ServeIo, o?: { port?: number }): Promise<HubDeps> {
30
+ // the default wiki graph exists only when the repo HAS a wiki dir: a
31
+ // game repo's hub advertising a wiki route was startup noise (his catch
32
+ // 2026-08-05). A config-declared wikiGraph always mounts.
30
33
  const wikiGraph = c.serve?.wikiGraph != null
31
34
  ? { title: c.serve.wikiGraph.title ?? `${c.name} wiki`, load: c.serve.wikiGraph.load }
32
- : { title: `${c.name} wiki`, load: () => scanMdGraph({ root }) }
35
+ : existsSync(join(root, 'wiki'))
36
+ ? { title: `${c.name} wiki`, load: () => scanMdGraph({ root }) }
37
+ : null
33
38
  const agentRoot = join(root, 'agent')
34
39
  const agentGraph = existsSync(agentRoot)
35
40
  ? { title: `${c.name} agent/`, load: () => scanMdGraph({ root: agentRoot, index: c.serve?.agentIndex }) }
@@ -105,7 +110,8 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
105
110
  const hub = serveHub(deps)
106
111
  const base = `http://localhost:${hub.server.port}`
107
112
  io.print(`ok: ${c.name} hub → ${base}`)
108
- for (const t of hub.tools) io.print(`ok: ${t.label} → ${base}${t.path}`)
113
+ // the '/' tool IS the banner line above — printing it again was noise
114
+ for (const t of hub.tools) if (t.path !== '/') io.print(`ok: ${t.label} → ${base}${t.path}`)
109
115
  // the off-machine door (phone, other machines) — publicHost is config
110
116
  if (c.serve?.publicHost != null) io.print(`ok: public → http://${c.serve.publicHost}:${hub.server.port}/`)
111
117
  await new Promise(() => {}) // the hub owns the process — never resolve
@@ -121,7 +127,7 @@ export async function runServe(root: string, io: ServeIo, o?: { port?: number })
121
127
  // a re-run terminal stays as clickable as a first run — the
122
128
  // owner's /hub.json tool list is the same source of truth
123
129
  io.print(`ok: hub already served → http://localhost:${deps.port} (another window owns it)`)
124
- for (const t of owner.tools ?? []) io.print(`ok: ${t.label} → http://localhost:${deps.port}${t.path}`)
130
+ for (const t of owner.tools ?? []) if (t.path !== '/') io.print(`ok: ${t.label} → http://localhost:${deps.port}${t.path}`)
125
131
  return 0
126
132
  }
127
133
  io.printErr(`FAIL: port ${deps.port} is taken by ${owner?.name != null ? `repo '${owner.name}'` : 'another process'} — set serve: { port } in repo.config.ts to a free one`)
@@ -19,7 +19,8 @@ export type HubDeps = {
19
19
  name: string
20
20
  /** 0 = random (tests); real launches pass repo.config/default */
21
21
  port: number
22
- wikiGraph: HubGraph
22
+ /** null = this repo has no wiki to graph — the route stays unmounted */
23
+ wikiGraph: HubGraph | null
23
24
  /** null = no agent/ dir */
24
25
  agentGraph: HubGraph | null
25
26
  /** the media API (mounted under /media/*); null only if the route
@@ -44,7 +45,7 @@ export type Hub = { server: ReturnType<typeof Bun.serve>; tools: HubTool[]; stop
44
45
  // so they can never disagree
45
46
  export function hubTools(d: Pick<HubDeps, 'wikiGraph' | 'agentGraph'>): HubTool[] {
46
47
  const tools: HubTool[] = [{ path: '/', label: 'hub', desc: 'THE page — media · asks · creds · posts · health · graphs' }]
47
- tools.push({ path: '/graph/wiki', label: 'wiki graph', desc: d.wikiGraph.title })
48
+ if (d.wikiGraph != null) tools.push({ path: '/graph/wiki', label: 'wiki graph', desc: d.wikiGraph.title })
48
49
  if (d.agentGraph != null) tools.push({ path: '/graph/agent', label: 'agent graph', desc: d.agentGraph.title })
49
50
  return tools
50
51
  }
@@ -124,8 +125,8 @@ export function serveHub(d: HubDeps): Hub {
124
125
  return new Response(proc.stdout, { headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': 'no-store' } })
125
126
  }
126
127
  const page = (s: string): Response => new Response(withHome(s), { headers: { 'content-type': 'text/html; charset=utf-8' } })
127
- if (path === '/graph/wiki') return page(graphHtml(d.wikiGraph.load(), { title: d.wikiGraph.title }))
128
- if (path === '/graph/wiki/graph.json') return Response.json(d.wikiGraph.load())
128
+ if (d.wikiGraph != null && path === '/graph/wiki') return page(graphHtml(d.wikiGraph.load(), { title: d.wikiGraph.title }))
129
+ if (d.wikiGraph != null && path === '/graph/wiki/graph.json') return Response.json(d.wikiGraph.load())
129
130
  if (d.agentGraph != null && path === '/graph/agent') return page(graphHtml(d.agentGraph.load(), { title: d.agentGraph.title }))
130
131
  if (d.agentGraph != null && path === '/graph/agent/graph.json') return Response.json(d.agentGraph.load())
131
132
  // the media API namespace — the studio route table, ONE prefix strip