rvlib-shipkit 1.4.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.
- package/CHANGELOG.md +68 -0
- package/README.md +17 -17
- package/package.json +3 -2
- package/src/check.ts +68 -95
- package/src/checks/SK001-package-json-shape.check.ts +6 -2
- package/src/checks/SK002-contracts-fulfillness.check.ts +4 -0
- package/src/checks/SK003-claude-md-shape.check.ts +5 -1
- package/src/checks/SK004-gitignore-policy.check.ts +7 -4
- package/src/checks/SK005-rvlib-versions.check.ts +53 -21
- package/src/checks/SK006-npm-publish-safety.check.ts +4 -0
- package/src/checks/SK007-github-visibility.check.ts +4 -2
- package/src/checks/SK008-reviewer-agent.check.ts +6 -4
- package/src/checks/SK009-commit-ai-tells.check.ts +4 -2
- package/src/checks/SK010-undeclared-unit.check.ts +4 -0
- package/src/checks/SK011-sibling-unit-ranges.check.ts +4 -0
- package/src/checks/SK012-config-barrel-import.check.ts +5 -1
- package/src/checks/SK013-one-way-in-policy.check.ts +7 -3
- package/src/checks/SK014-prose-ste.check.ts +4 -0
- package/src/checks/SK015-nested-repo.check.ts +4 -2
- package/src/checks/SK016-legacy-check-shapes.check.ts +13 -12
- package/src/checks/SK017-vscode-hub-task.check.ts +22 -7
- package/src/checks/SK018-test-files-run.check.ts +6 -2
- package/src/checks/SK019-tests-run-in-units.check.ts +8 -4
- package/src/checks/SK020-self-spawning-check.check.ts +4 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -4
- package/src/checks/SK024-identity-colors.check.ts +5 -3
- package/src/checks/SK025-release-drift.check.ts +4 -5
- package/src/checks/SK026-asks-autoclose.check.ts +4 -4
- package/src/checks/SK027-agent-wiki-links.check.ts +4 -1
- package/src/checks/SK028-readme.check.ts +5 -1
- package/src/checks/SK029-explicit-check-files.check.ts +95 -0
- package/src/cli/cmd_Asks.ts +5 -5
- package/src/cli/cmd_Assets.ts +7 -7
- package/src/cli/cmd_Bump.ts +12 -11
- package/src/cli/cmd_Check.ts +8 -7
- package/src/cli/cmd_Checks.ts +7 -6
- package/src/cli/cmd_Ci.ts +12 -11
- package/src/cli/cmd_Commands.ts +16 -13
- package/src/cli/cmd_ConfigKeys.ts +7 -6
- package/src/cli/cmd_Contracts.ts +3 -3
- package/src/cli/cmd_CrossRepoActivity.ts +6 -5
- package/src/cli/cmd_Fleet.ts +9 -7
- package/src/cli/cmd_GlobalProcesses.ts +116 -4
- package/src/cli/cmd_Home.ts +23 -17
- package/src/cli/cmd_Hooks.ts +6 -5
- package/src/cli/cmd_Init.ts +5 -4
- package/src/cli/cmd_Keys.ts +8 -7
- package/src/cli/cmd_Mv.ts +7 -6
- package/src/cli/cmd_Needs.ts +4 -3
- package/src/cli/cmd_Overview.ts +4 -3
- package/src/cli/cmd_Pb.ts +2 -2
- package/src/cli/cmd_Planned.ts +6 -5
- package/src/cli/cmd_Prose.ts +5 -5
- package/src/cli/cmd_Reflect.ts +7 -7
- package/src/cli/cmd_Release.ts +6 -5
- package/src/cli/cmd_Releases.ts +5 -4
- package/src/cli/cmd_Serve.ts +4 -3
- package/src/cli/cmd_Smoke.ts +5 -4
- package/src/cli/cmd_Start.ts +7 -6
- package/src/cli/cmd_Status.ts +4 -3
- package/src/cli/cmd_Tui.ts +3 -2
- package/src/cli/cmd_When.ts +71 -0
- package/src/cli/cmd_Wiki.ts +4 -4
- package/src/cli.ts +7 -2
- package/src/collections/checks.ts +2 -0
- package/src/collections/collection-types.ts +9 -4
- package/src/core/canonical-scripts.ts +1 -1
- package/src/core/check-runner.ts +37 -20
- package/src/core/config-rewrite.ts +37 -24
- package/src/core/discover-checks.ts +23 -2
- package/src/core/legacy-shapes.ts +39 -10
- package/src/core/one-way-in-policy.ts +2 -2
- package/src/core/outcome-words.ts +18 -2
- package/src/core/port-check-cli.ts +52 -0
- package/src/core/port-check-file.ts +295 -0
- package/src/core/port-check-files.ts +31 -0
- package/src/core/retention.ts +0 -14
- package/src/core/run-check.ts +12 -9
- package/src/core/run-ci.ts +8 -4
- package/src/core/run-fix.ts +30 -23
- package/src/core/run-fixers.ts +42 -5
- package/src/core/runs-matrix.ts +179 -0
- package/src/core/runs.ts +242 -0
- package/src/core/update-policy.ts +20 -0
- package/src/core/when-block.ts +29 -30
- package/src/core/when.ts +41 -103
- package/src/core/wire.ts +10 -14
- package/src/fleet/scan-fleet.ts +20 -4
- package/src/fleet/update-fleet.ts +6 -6
- package/src/home/home-launchd.ts +20 -4
- package/src/home/home-routes.ts +12 -1
- package/src/home/hubs.ts +5 -5
- package/src/home/pm.ts +8 -0
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +2 -2
- package/src/home/supervise.ts +24 -1
- package/src/hooks/run-hooks.ts +48 -4
- package/src/init-repo.ts +6 -6
- package/src/ledger/gate-checks.ts +113 -59
- package/src/ledger/git-key.ts +5 -1
- package/src/ledger/ledger.ts +5 -5
- package/src/ledger/toolchain-identity.ts +45 -27
- package/src/plan/repo-source.ts +18 -39
- package/src/release/run-release.ts +5 -0
- package/src/repo-config.ts +30 -55
- package/src/rules/rule.ts +2 -2
- package/src/scaffold/files/agent-coding.md +1 -1
- package/src/scaffold/files/agent-index.md +1 -1
- package/src/scaffold/files/verify-skill.md +2 -2
- package/src/serve/hub-app/activity-st.ts +5 -4
- package/src/serve/hub-app/activity-view.tsx +1 -1
- package/src/serve/hub-app/admin-view.tsx +13 -11
- package/src/serve/hub-app/check-view.tsx +66 -46
- package/src/serve/hub-app/check-words.ts +1 -11
- package/src/serve/hub-app/checks-svc.ts +14 -35
- package/src/serve/hub-app/col-tables-view.tsx +13 -6
- package/src/serve/hub-app/cols-svc.ts +5 -4
- package/src/serve/hub-app/dock-st.ts +3 -2
- package/src/serve/hub-app/dock-view.tsx +36 -0
- package/src/serve/hub-app/dock.css +6 -0
- package/src/serve/hub-app/float-cards.tsx +30 -0
- package/src/serve/hub-app/floating.tsx +41 -4
- package/src/serve/hub-app/git-st.ts +42 -0
- package/src/serve/hub-app/git-view.tsx +61 -0
- package/src/serve/hub-app/hash-route.ts +45 -1
- package/src/serve/hub-app/header-bar.tsx +62 -72
- package/src/serve/hub-app/health-svc.ts +3 -2
- package/src/serve/hub-app/home-st.ts +14 -2
- package/src/serve/hub-app/home-strip.css +43 -8
- package/src/serve/hub-app/home-strip.tsx +111 -47
- package/src/serve/hub-app/home.html +1 -0
- package/src/serve/hub-app/hub-app.tsx +14 -4
- package/src/serve/hub-app/hub-types.ts +5 -2
- package/src/serve/hub-app/hub.css +65 -43
- package/src/serve/hub-app/icons.tsx +11 -0
- package/src/serve/hub-app/index.html +1 -0
- package/src/serve/hub-app/keys.ts +5 -2
- package/src/serve/hub-app/live-svc.ts +3 -2
- package/src/serve/hub-app/machine-st.ts +18 -22
- package/src/serve/hub-app/plan-view.tsx +51 -0
- package/src/serve/hub-app/planned-st.ts +9 -4
- package/src/serve/hub-app/processes-view.tsx +38 -0
- package/src/serve/hub-app/projects-svc.ts +15 -10
- package/src/serve/hub-app/prose-hits-st.ts +3 -2
- package/src/serve/hub-app/reach.ts +36 -0
- package/src/serve/hub-app/readme-svc.ts +3 -2
- package/src/serve/hub-app/repo-sections.ts +117 -0
- package/src/serve/hub-app/repo-st.ts +46 -9
- package/src/serve/hub-app/sidebar.tsx +12 -37
- package/src/serve/hub-app/timeline-svc.ts +3 -2
- package/src/serve/hub-app/tree-build.ts +149 -155
- package/src/serve/hub-app/tree-row.tsx +39 -9
- package/src/serve/hub-app/tree-svc.ts +70 -49
- package/src/serve/hub-app/when-view.tsx +89 -0
- package/src/serve/hub-app/when.css +17 -0
- package/src/serve/hub-runtime.ts +3 -16
- package/src/serve/run-serve.ts +1 -1
- package/src/serve/serve-hub.ts +4 -14
- package/src/serve/hub-app/processes-svc.ts +0 -63
- package/src/serve/hub-app/repo-sections.tsx +0 -61
- package/src/serve/repo-processes.ts +0 -89
|
@@ -26,16 +26,12 @@
|
|
|
26
26
|
// cache slower than the gate it caches. No tool produces that state; a human
|
|
27
27
|
// debugging on purpose can delete the ledger.
|
|
28
28
|
import { createHash } from 'crypto'
|
|
29
|
-
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync } from 'fs'
|
|
29
|
+
import { existsSync, lstatSync, readdirSync, readFileSync, realpathSync, statSync } from 'fs'
|
|
30
30
|
import { isAbsolute, join, relative, resolve } from 'path'
|
|
31
31
|
|
|
32
32
|
export type Toolchain = {
|
|
33
33
|
/** the identity, 16 hex chars, stable for identical programs */
|
|
34
34
|
id: string
|
|
35
|
-
/** non-empty means DO NOT CACHE this run: something in the toolchain is
|
|
36
|
-
* neither in the repo's git tree nor pinned by a version string, so no
|
|
37
|
-
* key can describe it. Each entry says which package and why. */
|
|
38
|
-
uncacheable: string[]
|
|
39
35
|
}
|
|
40
36
|
|
|
41
37
|
/** the gate binary driving THIS process, which is what the key hashes when a
|
|
@@ -62,25 +58,40 @@ function installedNames(nm: string): string[] {
|
|
|
62
58
|
return names.sort()
|
|
63
59
|
}
|
|
64
60
|
|
|
65
|
-
/**
|
|
66
|
-
*
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
61
|
+
/** every file under `dir`, hashed as its path, its size and its bytes in
|
|
62
|
+
* path order, so a rename or a line moved between files moves the hash too.
|
|
63
|
+
* Its own installs and dot entries are left out. A symlink is followed (a
|
|
64
|
+
* cycle is walked once), and one that cannot be read THROWS: a tree only
|
|
65
|
+
* partly read must never be keyed as if it were whole */
|
|
66
|
+
function hashTree(h: ReturnType<typeof createHash>, dir: string, tag: string): void {
|
|
67
|
+
const files: { rel: string; abs: string }[] = []
|
|
68
|
+
const seen = new Set<string>()
|
|
69
|
+
const walk = (d: string, rel: string): void => {
|
|
70
|
+
const real = realpathSync(d)
|
|
71
|
+
if (seen.has(real)) return
|
|
72
|
+
seen.add(real)
|
|
73
|
+
for (const name of readdirSync(d)) {
|
|
74
|
+
if (name === 'node_modules' || name.startsWith('.')) continue
|
|
75
|
+
const abs = join(d, name)
|
|
76
|
+
const r = rel === '' ? name : `${rel}/${name}`
|
|
77
|
+
if (statSync(abs).isDirectory()) walk(abs, r)
|
|
78
|
+
else files.push({ rel: r, abs })
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
walk(
|
|
79
|
-
files.sort()
|
|
80
|
-
for (const f of files)
|
|
81
|
-
|
|
81
|
+
walk(dir, '')
|
|
82
|
+
files.sort((a, b) => (a.rel < b.rel ? -1 : a.rel > b.rel ? 1 : 0))
|
|
83
|
+
for (const f of files) {
|
|
84
|
+
const bytes = readFileSync(f.abs)
|
|
85
|
+
h.update(`${f.rel}\0${bytes.length}\0`)
|
|
86
|
+
h.update(bytes)
|
|
87
|
+
}
|
|
88
|
+
h.update(`${tag}=${files.length}\n`)
|
|
82
89
|
}
|
|
83
90
|
|
|
91
|
+
/** the gate binary's own source, content-hashed. This is what makes editing
|
|
92
|
+
* shipkit invalidate every key in every repo, in-repo copy or dependency. */
|
|
93
|
+
const hashCliTree = (h: ReturnType<typeof createHash>, cli: string): void => hashTree(h, resolve(cli, '..'), 'clifiles')
|
|
94
|
+
|
|
84
95
|
/**
|
|
85
96
|
* what ONE install holds, as a hash: every top-level package's name, version
|
|
86
97
|
* and link target.
|
|
@@ -119,7 +130,6 @@ export function installIdentity(nodeModules: string): string {
|
|
|
119
130
|
*/
|
|
120
131
|
export function toolchainIdentity(repoRoot: string, cliEntry: string): Toolchain {
|
|
121
132
|
const h = createHash('sha256')
|
|
122
|
-
const uncacheable: string[] = []
|
|
123
133
|
// BOTH sides resolved, or the comparison is wrong on macOS: /var is a
|
|
124
134
|
// symlink to /private/var, so an in-repo cli under a resolved path reads
|
|
125
135
|
// as outside an unresolved root. That mistake is silent and expensive: it
|
|
@@ -151,27 +161,35 @@ export function toolchainIdentity(repoRoot: string, cliEntry: string): Toolchain
|
|
|
151
161
|
if (!existsSync(nm)) {
|
|
152
162
|
// a worktree nobody installed is a REAL difference, not a missing input
|
|
153
163
|
h.update('nm=absent\n')
|
|
154
|
-
return { id: h.digest('hex').slice(0, 16)
|
|
164
|
+
return { id: h.digest('hex').slice(0, 16) }
|
|
155
165
|
}
|
|
156
166
|
const names = installedNames(nm)
|
|
157
167
|
for (const n of names) {
|
|
158
168
|
const dir = join(nm, n)
|
|
159
169
|
let link = ''
|
|
170
|
+
let outside: string | null = null
|
|
160
171
|
try {
|
|
161
172
|
if (lstatSync(dir).isSymbolicLink()) {
|
|
162
173
|
const target = realpathSync(dir)
|
|
163
174
|
link = `->${target}`
|
|
164
|
-
|
|
165
|
-
// edit: its content is in neither this repo's git tree nor its
|
|
166
|
-
// version string, so nothing here can describe it
|
|
167
|
-
if (!inRepo(root, target)) uncacheable.push(`${n} links outside the repo (${target})`)
|
|
175
|
+
if (!inRepo(root, target)) outside = target
|
|
168
176
|
}
|
|
169
177
|
} catch {}
|
|
178
|
+
// a `bun link`ed package is live source under someone else's edit:
|
|
179
|
+
// neither this repo's git tree nor its version string describes it, so
|
|
180
|
+
// its files do, and so does what it has installed itself, since its
|
|
181
|
+
// own imports resolve there. Outside the try on purpose: a linked tree
|
|
182
|
+
// that cannot be read throws, and the key layer then keys nothing.
|
|
183
|
+
// ~20ms for the whole rvlib-shipkit tree
|
|
184
|
+
if (outside != null) {
|
|
185
|
+
hashTree(h, outside, `linked:${n}`)
|
|
186
|
+
h.update(`${installIdentity(join(outside, 'node_modules'))}\n`)
|
|
187
|
+
}
|
|
170
188
|
let version = 'MISSING'
|
|
171
189
|
try { version = (JSON.parse(readFileSync(join(dir, 'package.json'), 'utf8')) as { version?: string }).version ?? '?' } catch {}
|
|
172
190
|
h.update(`${n}@${version}${link}\n`)
|
|
173
191
|
}
|
|
174
192
|
// the count catches a removal that leaves every survivor identical
|
|
175
193
|
h.update(`n=${names.length}\n`)
|
|
176
|
-
return { id: h.digest('hex').slice(0, 16)
|
|
194
|
+
return { id: h.digest('hex').slice(0, 16) }
|
|
177
195
|
}
|
package/src/plan/repo-source.ts
CHANGED
|
@@ -12,6 +12,7 @@ import { Ledger } from '../ledger/ledger.ts'
|
|
|
12
12
|
import { answersDb } from '../ledger/store.ts'
|
|
13
13
|
import { hasWorld, worldKeyOf, type WorldCache } from '../ledger/world-cache.ts'
|
|
14
14
|
import { FOREVER, FOREVER_MS } from '../core/wire.ts'
|
|
15
|
+
import { planned } from '../core/runs.ts'
|
|
15
16
|
import { ONCE, type LastResult } from './cadence.ts'
|
|
16
17
|
import { openDb } from '../ledger/db.ts'
|
|
17
18
|
import { readLastResults } from '../ledger/run-log.ts'
|
|
@@ -21,57 +22,33 @@ import { movedSince } from '../serve/tracked-watch.ts'
|
|
|
21
22
|
import type { RepoConfig } from '../repo-config.ts'
|
|
22
23
|
import type { PlanCheck, PlanReason, PlanSource } from './planner.ts'
|
|
23
24
|
|
|
24
|
-
/** the rows the plan covers:
|
|
25
|
-
*
|
|
25
|
+
/** the rows the plan covers: every live row declaring `onSave` or
|
|
26
|
+
* `onClock` (core/runs.ts). Nothing else is ever planned */
|
|
26
27
|
export function plannable(row: CheckRow): boolean {
|
|
27
|
-
|
|
28
|
-
// nothing to do in this repo on a clock (check.ts planOff)
|
|
29
|
-
if (row.planOff != null) return false
|
|
30
|
-
if (row.optIn) return false
|
|
31
|
-
return !(row.build && row.when.at === 'health')
|
|
28
|
+
return planned(row.runs)
|
|
32
29
|
}
|
|
33
30
|
|
|
34
|
-
/** a REPO check nothing can key and that declares no cadence runs about
|
|
35
|
-
* once a day, never in step, until it declares what it depends on. No
|
|
36
|
-
* built-in may land here (tests/plan-default-range.test.ts) */
|
|
37
|
-
const DEFAULT_EVERY: [string, string] = ['20h', '28h']
|
|
38
|
-
|
|
39
31
|
/** why the plan cannot tell when this check's answer moves, or null when it
|
|
40
|
-
* can
|
|
32
|
+
* can. With every axis explicit, a planned row always says: kept as the
|
|
33
|
+
* burn-down reader for rows declaring a save trigger on nothing keyed */
|
|
41
34
|
export function planGap(row: CheckRow): string | null {
|
|
35
|
+
if (row.runs.onSave !== 'moved') return null
|
|
42
36
|
const pop = populationOf(row.decl)
|
|
43
|
-
if (pop === 'content' || pop === 'reads' ||
|
|
44
|
-
|
|
45
|
-
const why = row.decl.never != null && row.decl.never !== '' ? `never cached (${row.decl.never})` : (row.decl.uncacheable ?? 'declares neither inputs nor ttl')
|
|
46
|
-
return `${why}, and no every: nothing says when its answer moves`
|
|
37
|
+
if (pop === 'content' || pop === 'reads' || isForever(row)) return null
|
|
38
|
+
return `onSave 'moved' but nothing keys it (${row.decl.uncacheable ?? row.decl.declError ?? 'no key'}), so every save moves it`
|
|
47
39
|
}
|
|
48
40
|
|
|
49
|
-
/** one inventory row as a plan check
|
|
50
|
-
*
|
|
51
|
-
* answer replays until a file moves, so a clock re-asks nothing); a repo
|
|
52
|
-
* check with no cadence runs at its own stable point of the default range,
|
|
53
|
-
* and a built-in with none waits for a save and shows on the burn-down */
|
|
41
|
+
/** one inventory row as a plan check, straight from its declared axes:
|
|
42
|
+
* `onClock` is the cadence, `onSave` whether a save can make it due */
|
|
54
43
|
export function planCheckOf(row: CheckRow): PlanCheck {
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const ttl = row.decl.ttl != null && row.decl.ttl !== '' ? row.decl.ttl : null
|
|
58
|
-
const fallback =
|
|
59
|
-
ttl === FOREVER ? { value: ONCE, why: 'a decided fact: ttl forever' }
|
|
60
|
-
: ttl != null ? { value: ttl, why: 'its ttl' }
|
|
61
|
-
: row.build ? { value: null, why: 'a build step' }
|
|
62
|
-
: keyed ? { value: null, why: 'keyed on its files' }
|
|
63
|
-
: row.from === 'repo' ? { value: DEFAULT_EVERY, why: 'the default range' }
|
|
64
|
-
: { value: null, why: 'a built-in declaring nothing, see shipkit keys --burn-down' }
|
|
65
|
-
// a gate check nothing can key waits for a save only when no cadence
|
|
66
|
-
// says when to run it: a range or an every it declares is the rule
|
|
67
|
-
const clocked = fallback.value != null || row.plan?.every !== undefined || row.plan?.cron != null
|
|
44
|
+
const clock = row.runs.onClock
|
|
45
|
+
const plan = clock === false ? {} : clock === ONCE ? { every: ONCE } : { every: clock.every, whenInactive: clock.inactive }
|
|
68
46
|
return {
|
|
69
47
|
id: row.decl.id,
|
|
70
48
|
title: row.title,
|
|
71
|
-
plan
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
fallback,
|
|
49
|
+
plan,
|
|
50
|
+
onChange: row.runs.onSave !== false,
|
|
51
|
+
fallback: { value: null, why: 'onClock false: only a save runs it' },
|
|
75
52
|
}
|
|
76
53
|
}
|
|
77
54
|
|
|
@@ -182,6 +159,8 @@ export function repoSource(d: RepoSourceDeps): PlanSource {
|
|
|
182
159
|
moved.delete(c.id)
|
|
183
160
|
continue
|
|
184
161
|
}
|
|
162
|
+
// 'burst': every quiet save burst, whatever moved
|
|
163
|
+
if (r.runs.onSave === 'burst') { out.push(c.id); continue }
|
|
185
164
|
const k = ledger.layer.keyFor(c.id)
|
|
186
165
|
if (k.key == null) {
|
|
187
166
|
out.push(c.id)
|
|
@@ -112,6 +112,11 @@ export type ReleaseOpts = {
|
|
|
112
112
|
export async function runReleaseGate(root: string, config: RepoConfig, print: (l: string) => void, o: { fix?: boolean; units?: ReleaseUnit[] } = {}): Promise<number> {
|
|
113
113
|
return await runCi(root, {
|
|
114
114
|
fix: o.fix ?? false,
|
|
115
|
+
// a check's repair never runs on the release path (runs.onRelease):
|
|
116
|
+
// only the mechanical slots (the formatter) may, when bump asks
|
|
117
|
+
fixRules: false,
|
|
118
|
+
// the rows declaring onRelease, whatever the tier
|
|
119
|
+
release: true,
|
|
115
120
|
frozen: true,
|
|
116
121
|
status: false,
|
|
117
122
|
config,
|
package/src/repo-config.ts
CHANGED
|
@@ -59,29 +59,11 @@ export type ServeConfig = {
|
|
|
59
59
|
* prints beside localhost and rides on every ping message, because a
|
|
60
60
|
* phone cannot resolve localhost */
|
|
61
61
|
publicHost?: string
|
|
62
|
-
/** the long running processes this repo needs (a dev server, a watcher).
|
|
63
|
-
* The hub supervises them: a switch each on its page, a crash restarts,
|
|
64
|
-
* and they outlive a hub restart */
|
|
65
|
-
processes?: ProcessDecl[]
|
|
66
62
|
/** the ids of the global processes this repo needs (any house repo's
|
|
67
63
|
* `globalProcess` unit files). The home rail marks the repo while one is off */
|
|
68
64
|
uses?: string[]
|
|
69
65
|
}
|
|
70
66
|
|
|
71
|
-
/** one long running process a repo needs, supervised by its hub */
|
|
72
|
-
export type ProcessDecl = {
|
|
73
|
-
/** kebab-case, unique in this repo */
|
|
74
|
-
id: string
|
|
75
|
-
/** the command as a list of words, run from the repo root */
|
|
76
|
-
cmd: string[]
|
|
77
|
-
/** where it runs, repo-relative (default the repo root) */
|
|
78
|
-
cwd?: string
|
|
79
|
-
/** the url it serves, probed for up and shown as a link */
|
|
80
|
-
url?: string
|
|
81
|
-
/** starts with the hub. Default false: it starts from its switch */
|
|
82
|
-
auto?: boolean
|
|
83
|
-
}
|
|
84
|
-
|
|
85
67
|
export type RepoConfig = {
|
|
86
68
|
/** the repo name, as every surface prints it: the gate header, the hub
|
|
87
69
|
* title, the motherhome row, the default hub port hash. The pre-commit
|
|
@@ -171,21 +153,25 @@ export type RepoConfig = {
|
|
|
171
153
|
* folder only: it never commits, untracks, tags or pushes on the parent's
|
|
172
154
|
* history. Rule SK015 fails when this and the disk disagree (core/nesting.ts) */
|
|
173
155
|
nested?: boolean
|
|
174
|
-
/**
|
|
175
|
-
*
|
|
176
|
-
*
|
|
177
|
-
* `
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
|
|
156
|
+
/** THE CHECKS THIS REPO TURNS OFF, check id → why. The one thing a repo
|
|
157
|
+
* says about a check it does not own: where a check runs is its own
|
|
158
|
+
* declaration (build/shipkit/runs.md), and an off check runs in no
|
|
159
|
+
* context. `defineRepo` throws on a blank reason and on a rule id no
|
|
160
|
+
* check carries. The hub writes this map through the managed block in
|
|
161
|
+
* this file (core/when-block.ts). */
|
|
162
|
+
off?: Record<string, string>
|
|
163
|
+
/** RETIRED, read only while SK029 ports it into `off`: an `'off:<why>'`
|
|
164
|
+
* value reads as `off`, and any other word is refused, because a repo
|
|
165
|
+
* no longer places a check. Dies once `shipkit fleet` shows no repo
|
|
166
|
+
* carrying it */
|
|
181
167
|
when?: Record<string, string>
|
|
182
168
|
/** WHAT A HEALTHY RUN OF EACH CHECK TAKES, in ms, keyed by check id. A
|
|
183
169
|
* check that passes over its budget prints a WARN and reads yellow, and
|
|
184
170
|
* never blocks. It wins over the check's own `budgetMs` and the default
|
|
185
171
|
* for its kind (core/budget.ts). An id no check carries fails the run,
|
|
186
|
-
* the same as
|
|
172
|
+
* the same as an `off` entry. */
|
|
187
173
|
budgets?: Record<string, number>
|
|
188
|
-
/**
|
|
174
|
+
/** RETIRED, read as `off` while SK029 ports it there: rule id → the reason
|
|
189
175
|
* this repo breaks that invariant on purpose. An exempted rule does not
|
|
190
176
|
* lint and prints a skip line carrying the reason. `defineRepo` throws on
|
|
191
177
|
* an unknown id or a blank reason */
|
|
@@ -215,7 +201,7 @@ export type RepoConfig = {
|
|
|
215
201
|
/** widens the read-only tool allowlist SK008 enforces (default Read, Grep, Glob, Bash) */
|
|
216
202
|
tools?: string[]
|
|
217
203
|
}
|
|
218
|
-
/** ONE WAY IN escape hatch (build/shipkit.md): governed script names this
|
|
204
|
+
/** ONE WAY IN escape hatch (build/shipkit/one-way-in.md): governed script names this
|
|
219
205
|
* repo keeps as a direct tool call. Declared per NAME, never as a list of
|
|
220
206
|
* banned binaries: the invariant is about which VERBS an agent reaches
|
|
221
207
|
* for, and a binary blocklist goes stale the day a repo changes runner.
|
|
@@ -300,18 +286,7 @@ export type RepoConfig = {
|
|
|
300
286
|
logDir?: string
|
|
301
287
|
}
|
|
302
288
|
|
|
303
|
-
const KNOWN_KEYS = new Set(['name', 'venues', 'social', 'docsBase', 'defaults', 'tasks', 'asks', 'creds', 'credsCommand', 'credsReadCommand', 'notifyCommand', 'contracts', 'gitignore', 'untrackGenerated', 'nested', 'when', 'budgets', 'ruleExempt', 'visibility', 'archived', 'reviewer', 'oneWayIn', 'rules', 'release', 'units', 'notReleasable', 'hooks', 'checkDefaults', 'tui', 'serve', 'identity', 'logDir', 'prose', 'smoke', 'tests'])
|
|
304
|
-
|
|
305
|
-
const KEBAB = /^[a-z0-9][a-z0-9-]*$/
|
|
306
|
-
const words = (v: unknown): boolean => Array.isArray(v) && v.length > 0 && v.every((w) => typeof w === 'string')
|
|
307
|
-
|
|
308
|
-
/** why a process declaration cannot run, or null */
|
|
309
|
-
export function processProblem(d: ProcessDecl): string | null {
|
|
310
|
-
if (!KEBAB.test(d.id ?? '')) return `serve.processes: id '${String(d.id)}' must be kebab-case`
|
|
311
|
-
if (!words(d.cmd)) return `serve.processes['${d.id}'] needs cmd: a list of words`
|
|
312
|
-
if (d.url != null && !/^https?:\/\//.test(d.url)) return `serve.processes['${d.id}'].url must be an http url`
|
|
313
|
-
return null
|
|
314
|
-
}
|
|
289
|
+
const KNOWN_KEYS = new Set(['name', 'venues', 'social', 'docsBase', 'defaults', 'tasks', 'asks', 'creds', 'credsCommand', 'credsReadCommand', 'notifyCommand', 'contracts', 'gitignore', 'untrackGenerated', 'nested', 'off', 'when', 'budgets', 'ruleExempt', 'visibility', 'archived', 'reviewer', 'oneWayIn', 'rules', 'release', 'units', 'notReleasable', 'hooks', 'checkDefaults', 'tui', 'serve', 'identity', 'logDir', 'prose', 'smoke', 'tests'])
|
|
315
290
|
|
|
316
291
|
export function defineRepo(c: RepoConfig): RepoConfig {
|
|
317
292
|
if (typeof c.name !== 'string' || c.name === '') throw new Error('defineRepo: name is required')
|
|
@@ -321,13 +296,8 @@ export function defineRepo(c: RepoConfig): RepoConfig {
|
|
|
321
296
|
// a half-declared reviewer would lint a path like `.claude/agents/.md`
|
|
322
297
|
if (c.reviewer != null && (!/^[a-z0-9][a-z0-9-]*$/.test(c.reviewer.name ?? '') || (c.reviewer.brief ?? '') === ''))
|
|
323
298
|
throw new Error(`defineRepo: reviewer needs { name: kebab-case, brief: path to the standing brief }`)
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
const why = processProblem(d)
|
|
327
|
-
if (why != null) throw new Error(`defineRepo: ${why}`)
|
|
328
|
-
if (pids.has(d.id)) throw new Error(`defineRepo: serve.processes declares '${d.id}' twice`)
|
|
329
|
-
pids.add(d.id)
|
|
330
|
-
}
|
|
299
|
+
if (c.serve != null && 'processes' in c.serve)
|
|
300
|
+
throw new Error(`defineRepo: serve.processes was removed; declare each process in a <name>.shipkit.ts file as \`export default globalProcess({...})\``)
|
|
331
301
|
// unknown keys die loud — a typo'd or removed key silently changing the
|
|
332
302
|
// gate is exactly the never-silence failure mode (A23)
|
|
333
303
|
for (const k of Object.keys(c))
|
|
@@ -384,17 +354,22 @@ export function defineRepo(c: RepoConfig): RepoConfig {
|
|
|
384
354
|
// repo rules are validated against the built-ins, so a bad id, a duplicate,
|
|
385
355
|
// or an SK squatter dies at config load, not silently mid-check
|
|
386
356
|
if (c.rules != null) validateRules(c.rules, { label: 'defineRepo: rules', taken: builtinCheckIds() })
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
//
|
|
390
|
-
//
|
|
391
|
-
|
|
357
|
+
// `off` silences a check, so it names one that exists and says why. The
|
|
358
|
+
// RULE namespace is the one this file can check: a task is resolved from
|
|
359
|
+
// the floor and a check is discovered on disk, so those two are caught by
|
|
360
|
+
// the gate against the real inventory (core/when.ts unknownOffIds)
|
|
361
|
+
for (const [id, reason] of Object.entries(c.off ?? {})) {
|
|
362
|
+
if (id.trim() === '') throw new Error(`defineRepo: off has an entry with no check id`)
|
|
363
|
+
if (typeof reason !== 'string' || reason.trim() === '') throw new Error(`defineRepo: off.${id} needs a reason: a check nobody can explain is a silenced gate`)
|
|
364
|
+
if (RULE_ID_RE.test(id) && !builtinCheckIds().includes(id) && !(c.rules ?? []).some((r) => r.id === id))
|
|
365
|
+
throw new Error(`defineRepo: off.${id} names no rule, the ids are in \`shipkit checks\``)
|
|
366
|
+
}
|
|
367
|
+
// the retired `when`: only its `off:<why>` word still means something
|
|
392
368
|
for (const [id, raw] of Object.entries(c.when ?? {})) {
|
|
393
|
-
if (id.trim() === '') throw new Error(`defineRepo: when has an entry with no check id`)
|
|
394
369
|
const w = parseWhen(raw)
|
|
395
370
|
if (typeof w === 'string') throw new Error(`defineRepo: when.${id} ${w}`)
|
|
396
|
-
if (
|
|
397
|
-
throw new Error(`defineRepo: when.${id}
|
|
371
|
+
if (w.at !== 'off')
|
|
372
|
+
throw new Error(`defineRepo: when.${id} '${raw}' no longer places a check: every check declares where it runs (build/shipkit/runs.md). A repo can only turn one off: off: { ${id}: '<why>' }`)
|
|
398
373
|
}
|
|
399
374
|
// a budget turns a slow pass yellow, so a value it cannot compare with dies here
|
|
400
375
|
for (const [id, v] of Object.entries(c.budgets ?? {})) {
|
package/src/rules/rule.ts
CHANGED
|
@@ -6,14 +6,14 @@
|
|
|
6
6
|
//
|
|
7
7
|
// A new invariant is a `<name>.check.ts` file: `check({ blocking, run })`,
|
|
8
8
|
// dropped anywhere in the repo, discovered with no config edit.
|
|
9
|
-
import {
|
|
9
|
+
import { repoOff } from '../core/when.ts'
|
|
10
10
|
import { tagCheckLines, checkCacheExtra, type Rule, type RuleEnv } from '../check.ts'
|
|
11
11
|
|
|
12
12
|
export type { FixResult, Rule, RuleEnv } from '../check.ts'
|
|
13
13
|
|
|
14
14
|
/** the config part of a rule's cache identity, including where it runs */
|
|
15
15
|
export function ruleCacheExtra(rule: Rule, env: RuleEnv): string | undefined {
|
|
16
|
-
return checkCacheExtra(rule
|
|
16
|
+
return checkCacheExtra(rule.cacheKey?.(env), repoOff(env.config, rule.id)?.reason ?? null)
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
/** tag every severity line with the rule id (check.ts owns the rendering) */
|
|
@@ -7,7 +7,7 @@ always-loaded one, and route the rest by surface.
|
|
|
7
7
|
## Gates
|
|
8
8
|
|
|
9
9
|
- **Spec-first**: the `agent/` page changes BEFORE the code it describes.
|
|
10
|
-
- **`
|
|
10
|
+
- **`sk ci` is the verification.** Run it, not the linter, the
|
|
11
11
|
typechecker and the tests one at a time.
|
|
12
12
|
- **Repro before fix**: write the failing test, then fix, then re-run it.
|
|
13
13
|
- One code path per concern. Delete the redundant mechanism rather than
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
## Quick start
|
|
6
6
|
|
|
7
7
|
- `bun run hub` → the repo toolbox at http://localhost:{{PORT}}
|
|
8
|
-
- `
|
|
8
|
+
- `sk ci` → THE gate: run it to check your work, never the parts
|
|
9
9
|
|
|
10
10
|
This folder is the project's internal wiki and its SPEC. Update the page
|
|
11
11
|
BEFORE the change it describes.
|
|
@@ -11,12 +11,12 @@ description: Prove a change in THIS project actually works — drive the real fl
|
|
|
11
11
|
|
|
12
12
|
## The rules (house-wide)
|
|
13
13
|
- Failing repro test BEFORE the fix. Green checks ≠ verification.
|
|
14
|
-
- Logic → fast headless bun tests, run through `
|
|
14
|
+
- Logic → fast headless bun tests, run through `sk ci --only test` (tiny ~10-line test files reusing fixtures). While you fix one bug, `bun run test:raw <test file>` is the narrow way in, and one test by name is `bun run test:raw -- -t "<name>"`.
|
|
15
15
|
- Look & feel → NEVER screenshot-self-verify; the repo owner playtests it.
|
|
16
16
|
- Verify as the end user would: drive the real flow, observe the real signal.
|
|
17
17
|
|
|
18
18
|
## This project
|
|
19
|
-
- Full gate: `
|
|
19
|
+
- Full gate: `sk ci` — THE command, never the linter, the typechecker
|
|
20
20
|
and the tests one at a time.
|
|
21
21
|
- TODO, fill these in once the stack exists: the dev server command and its
|
|
22
22
|
port (check it is not already running: `curl -s localhost:PORT >/dev/null
|
|
@@ -9,6 +9,7 @@ import { clampCap, DEFAULT_CAP, DEFAULT_MINUTES, idleRow, WINDOW_MINUTES, type A
|
|
|
9
9
|
// the words the page and `shipkit cross-repo-activity` share live beside the shapes
|
|
10
10
|
export { ageShort, CAP_MAX, CAP_MIN, clampCap, DEFAULT_CAP, dirtWords, filesWords, upstreamWords, windowWords } from '../../home/activity-shape.ts'
|
|
11
11
|
import type { HomeSt } from './home-st.ts'
|
|
12
|
+
import { ownTrouble, reach } from './reach.ts'
|
|
12
13
|
|
|
13
14
|
export const ACTIVITY_POLL_MS = 60_000
|
|
14
15
|
const MINUTES_KEY = 'home:activity:minutes'
|
|
@@ -105,11 +106,11 @@ export class ActivitySt {
|
|
|
105
106
|
/** the repo in Fork, the git client: the daemon opens it by name */
|
|
106
107
|
async openFork(name: string): Promise<void> {
|
|
107
108
|
try {
|
|
108
|
-
const r = await
|
|
109
|
+
const r = await reach('/api/projects/fork', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ name }) })
|
|
109
110
|
if (!r.ok) throw new Error(await r.text())
|
|
110
111
|
runInAction(() => { this.error = '' })
|
|
111
112
|
} catch (e) {
|
|
112
|
-
runInAction(() => { this.error = `open ${name} in Fork: ${
|
|
113
|
+
runInAction(() => { this.error = ownTrouble(e, this.st.machine, (m) => `open ${name} in Fork: ${m}`) })
|
|
113
114
|
}
|
|
114
115
|
}
|
|
115
116
|
|
|
@@ -128,12 +129,12 @@ export class ActivitySt {
|
|
|
128
129
|
const id = ++this.gen
|
|
129
130
|
runInAction(() => { this.loading = true; this.nextAt = Date.now() + ACTIVITY_POLL_MS })
|
|
130
131
|
try {
|
|
131
|
-
const r = await
|
|
132
|
+
const r = await reach(`/api/home/activity.json?m=${this.minutes}${fresh ? '&fresh=1' : ''}`, { signal: AbortSignal.timeout(30_000) })
|
|
132
133
|
if (!r.ok) throw new Error(`${r.status} ${await r.text()}`)
|
|
133
134
|
const j = (await r.json()) as ActivityAnswer
|
|
134
135
|
runInAction(() => { if (id === this.gen) { this.data = j; this.error = '' } })
|
|
135
136
|
} catch (e) {
|
|
136
|
-
runInAction(() => { if (id === this.gen) this.error = (e
|
|
137
|
+
runInAction(() => { if (id === this.gen) this.error = ownTrouble(e, this.st.machine) })
|
|
137
138
|
} finally {
|
|
138
139
|
// the countdown starts when the answer lands, so reads never overlap
|
|
139
140
|
runInAction(() => { if (id === this.gen) { this.loading = false; this.nextAt = Date.now() + ACTIVITY_POLL_MS } })
|
|
@@ -52,7 +52,7 @@ const Next = observer(({ app }: { app: HomeSt }) => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
/** one commit's cells; `old` is the last one, from before the window */
|
|
55
|
-
const Commit = ({ c, now, old }: { c: ActivityCommit; now: number; old: boolean }) => (
|
|
55
|
+
export const Commit = ({ c, now, old }: { c: ActivityCommit; now: number; old: boolean }) => (
|
|
56
56
|
<>
|
|
57
57
|
<td className="acsha">{c.sha}</td>
|
|
58
58
|
<td className="acwhen" title={stampWords(c.at)}>{ageShort(c.at, now)}</td>
|
|
@@ -26,17 +26,19 @@ export const AdminPage = observer(({ app }: { app: RepoSt }) => {
|
|
|
26
26
|
<span className="tid">admin</span>
|
|
27
27
|
<h3 className="ctitle">{name}</h3>
|
|
28
28
|
</div>
|
|
29
|
-
<
|
|
30
|
-
<
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
<
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
29
|
+
<div className="adbox">
|
|
30
|
+
<Row title="reload the hub" says={`stops ${name}'s hub in the daemon and loads it again, its repo.config.ts read fresh and its processes started again. The daemon already does it on its own when repo.config.ts changes: this is for a hub that is stuck.${w == null ? '' : ` Its hub is ${w.status} now.`}`}>
|
|
31
|
+
<button className="ghost" onClick={() => void a.reload()}>reload</button>
|
|
32
|
+
</Row>
|
|
33
|
+
<Row title="keep active" says="an active repo runs the checks the plan holds for it and gets its red pings. It is active on its own while an agent works in it or VS Code has it open: this keeps it active without either.">
|
|
34
|
+
<label className="adflag"><input type="checkbox" checked={a.active} onChange={(e) => void a.setActive(e.currentTarget.checked)} />active</label>
|
|
35
|
+
</Row>
|
|
36
|
+
<Row title="archive" says="it leaves the project list for the archived page, and nothing runs there: no hub, no watch, no plan rows, no processes, no widgets. Written to repo.config.ts as archived: true, and undone from the archived page.">
|
|
37
|
+
{a.confirming
|
|
38
|
+
? <span className="adconfirm"><span>archive {name}?</span><button className="primary" onClick={() => a.archive()}>archive it</button><button className="ghost" onClick={() => a.cancel()}>cancel</button></span>
|
|
39
|
+
: <button className="ghost" onClick={() => a.askArchive()}>archive</button>}
|
|
40
|
+
</Row>
|
|
41
|
+
</div>
|
|
40
42
|
</div>
|
|
41
43
|
)
|
|
42
44
|
})
|