rvlib-shipkit 1.2.0 → 1.3.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 +67 -0
- package/README.md +1 -1
- package/package.json +1 -1
- package/src/assets/run-assets.ts +3 -8
- package/src/check.ts +20 -6
- package/src/checks/SK001-package-json-shape.check.ts +14 -3
- package/src/checks/SK002-contracts-fulfillness.check.ts +3 -3
- package/src/checks/SK004-gitignore-policy.check.ts +11 -3
- package/src/checks/SK005-rvlib-versions.check.ts +13 -1
- package/src/checks/SK006-npm-publish-safety.check.ts +5 -0
- package/src/checks/SK007-github-visibility.check.ts +9 -9
- package/src/checks/SK008-reviewer-agent.check.ts +14 -8
- package/src/checks/SK009-commit-ai-tells.check.ts +29 -5
- package/src/checks/SK013-one-way-in-policy.check.ts +10 -2
- package/src/checks/SK015-nested-repo.check.ts +5 -1
- package/src/checks/SK017-vscode-hub-task.check.ts +7 -0
- package/src/checks/SK022-machine-runs-the-pin.check.ts +6 -2
- package/src/checks/SK024-identity-colors.check.ts +5 -0
- package/src/checks/SK025-release-drift.check.ts +58 -0
- package/src/checks/SK026-asks-autoclose.check.ts +44 -0
- package/src/checks/SK027-agent-wiki-links.check.ts +28 -0
- package/src/checks/SK028-readme.check.ts +79 -0
- package/src/cli/cmd_Assets.ts +1 -1
- package/src/cli/cmd_Ci.ts +5 -0
- package/src/cli/cmd_CrossRepoActivity.ts +116 -0
- package/src/cli/cmd_GlobalProcesses.ts +155 -0
- package/src/cli/cmd_Keys.ts +17 -3
- package/src/cli/cmd_Overview.ts +98 -0
- package/src/cli/cmd_Planned.ts +117 -0
- package/src/cli.ts +9 -1
- package/src/collections/checks.ts +3 -2
- package/src/collections/providers.ts +1 -1
- package/src/collections/test-time.ts +10 -1
- package/src/core/agent-spawn.ts +46 -0
- package/src/core/archived.ts +82 -0
- package/src/core/canonical-scripts.ts +12 -3
- package/src/core/check-runner.ts +40 -12
- package/src/core/discover-checks.ts +2 -0
- package/src/core/file-stamp.ts +20 -0
- package/src/{checks/SK021-github-login.check.ts → core/gh-login.ts} +45 -54
- package/src/core/identity-color.ts +1 -1
- package/src/core/repo-facts.ts +14 -0
- package/src/core/retention.ts +14 -5
- package/src/core/retired-checks.ts +7 -0
- package/src/core/run-check.ts +6 -2
- package/src/core/run-ci.ts +43 -12
- package/src/core/run-fix.ts +7 -2
- package/src/core/run-fixers.ts +10 -6
- package/src/core/wire.ts +11 -0
- package/src/fleet/scan-fleet.ts +2 -1
- package/src/home/activity-shape.ts +135 -0
- package/src/home/activity.ts +249 -0
- package/src/home/global-units.ts +3 -2
- package/src/home/home-routes.ts +32 -5
- package/src/home/pm.ts +37 -4
- package/src/home/process-rows.ts +205 -0
- package/src/home/resources.ts +35 -8
- package/src/home/run-daemon.ts +6 -6
- package/src/home/run-pm.ts +1 -0
- package/src/home/serve-web.ts +6 -3
- package/src/home/supervise.ts +16 -1
- package/src/home/widget-text.ts +119 -0
- package/src/hooks/run-hooks.ts +20 -3
- package/src/ledger/db.ts +1 -1
- package/src/ledger/gate-checks.ts +17 -3
- package/src/ledger/house-runs.ts +43 -0
- package/src/ledger/live-timeline.ts +4 -19
- package/src/ledger/run-log.ts +39 -10
- package/src/ledger/store.ts +19 -2
- package/src/ledger/world-cache.ts +51 -4
- package/src/plan/cadence.ts +21 -5
- package/src/plan/catch-up.ts +37 -0
- package/src/plan/plan-timeline.ts +99 -0
- package/src/plan/planner.ts +138 -13
- package/src/plan/repo-source.ts +120 -8
- package/src/release/run-release.ts +9 -2
- package/src/release/unit-resolve.ts +8 -12
- package/src/repo-config.ts +11 -3
- package/src/serve/house-runs.ts +53 -0
- package/src/serve/hub-app/activity-st.ts +168 -0
- package/src/serve/hub-app/activity-view.tsx +218 -0
- package/src/serve/hub-app/activity.css +78 -0
- package/src/serve/hub-app/admin-svc.ts +41 -0
- package/src/serve/hub-app/admin-view.tsx +42 -0
- package/src/serve/hub-app/chart-view.ts +12 -5
- package/src/serve/hub-app/check-words.ts +2 -0
- package/src/serve/hub-app/controls.css +32 -0
- package/src/serve/hub-app/cycle-button.tsx +36 -0
- package/src/serve/hub-app/dock-anim.ts +25 -0
- package/src/serve/hub-app/dock-st.ts +329 -0
- package/src/serve/hub-app/dock-view.tsx +68 -0
- package/src/serve/hub-app/dock.css +35 -0
- package/src/serve/hub-app/hash-route.ts +7 -1
- package/src/serve/hub-app/header-bar.tsx +79 -104
- package/src/serve/hub-app/home-st.ts +49 -12
- package/src/serve/hub-app/home-strip.css +65 -22
- package/src/serve/hub-app/home-strip.tsx +228 -97
- package/src/serve/hub-app/home.html +4 -0
- package/src/serve/hub-app/hub-app.tsx +96 -160
- package/src/serve/hub-app/hub.css +77 -37
- package/src/serve/hub-app/icons.tsx +23 -3
- package/src/serve/hub-app/index.html +4 -0
- package/src/serve/hub-app/keys.ts +42 -7
- package/src/serve/hub-app/live-svc.ts +24 -64
- package/src/serve/hub-app/machine-st.ts +64 -1
- package/src/serve/hub-app/md.ts +38 -5
- package/src/serve/hub-app/next-run.ts +1 -1
- package/src/serve/hub-app/palette-st.ts +11 -7
- package/src/serve/hub-app/palette-view.tsx +47 -0
- package/src/serve/hub-app/pill-row.tsx +65 -0
- package/src/serve/hub-app/planned-st.ts +21 -85
- package/src/serve/hub-app/planned-view.tsx +12 -7
- package/src/serve/hub-app/planned.css +5 -2
- package/src/serve/hub-app/project-list.css +43 -21
- package/src/serve/hub-app/project-list.tsx +122 -23
- package/src/serve/hub-app/projects-svc.ts +120 -12
- package/src/serve/hub-app/readme-svc.ts +38 -0
- package/src/serve/hub-app/readme-view.tsx +19 -0
- package/src/serve/hub-app/readme.css +11 -0
- package/src/serve/hub-app/repo-sections.tsx +61 -0
- package/src/serve/hub-app/repo-st.ts +25 -6
- package/src/serve/hub-app/runs-tabs.ts +1 -1
- package/src/serve/hub-app/sidebar.tsx +84 -0
- package/src/serve/hub-app/tests-view.tsx +31 -4
- package/src/serve/hub-app/timeline-chart.tsx +31 -27
- package/src/serve/hub-app/timeline-geometry.ts +40 -5
- package/src/serve/hub-app/timeline-svc.ts +2 -2
- package/src/serve/hub-app/timeline-view.tsx +20 -3
- package/src/serve/hub-runtime.ts +17 -9
- package/src/serve/other-repos.ts +8 -45
- package/src/serve/project-gutter-route.ts +100 -20
- package/src/serve/project-gutter.ts +33 -11
- package/src/serve/readme-route.ts +57 -0
- package/src/serve/serve-hub.ts +13 -8
- package/src/serve/tracked-watch.ts +33 -1
- package/src/shipkit-files.ts +20 -4
- package/src/checks/SK023-stores-under-cap.check.ts +0 -49
- package/src/checks/contracts.check.ts +0 -20
package/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,70 @@
|
|
|
1
|
+
## 1.3.0
|
|
2
|
+
|
|
3
|
+
- **`shipkit ci` prints what the reader acts on, and nothing else.** A green run is one line per check that ran and one verdict line: `ok: test 920 tests (1.4s)` then `ok: 1 check green`, where it used to print nine. A test check carries its count on its own line. The whole gate folds its tier and cache counts into the verdict (`ok: 31 checks green; quick tier; 25 cached, 5 ran, 1 never cached`). A fix pass prints only when it rewrote files, and names them. A green check prints one line, a warned one only its warnings and their `fix:` pointers, a failure prints whole. `--only` says nothing about checks outside its selection (floor gaps, `off` checks, parked checks). `--verbose` restores every line the default leaves out: the clean fix pass, the machine pool, the tier, the cache declarations, the parked checks, the old summary and the cache counts. STATUS.md keeps them all, a release or bump gate still prints them all, and `--json` is unchanged.
|
|
4
|
+
- **Compact monitor cards.** A widget's leading `<header>` joins its card's title line, dim (`splitWidgetHead`), and `shipkit overview` prints it on the title line the same way. A card is as wide as its content, the owner repo moves to the hover, the `.wbar` meter is 56px, and `<table class="wmeters">` lays meters out two to a row, label, bar and value.
|
|
5
|
+
- **Fix: the planned page lists only what is actually planned.** A check waiting for its repo to be active is no longer listed as late: the page folds all of them into one line at the end (`waiting for their repo to be active: 24 checks in 9 repos`) that opens on a click, and `shipkit planned` prints the same line, listing them with `--all`. A check can declare `planOff: (env) => reason | null`, and the plan leaves it out of a repo where it has nothing to do: SK026 without an ask that carries a probe, SK025 without a unit that releases, SK005 without an rvlib dependency. A decided fact (`ttl: 'forever'`) with no answer under its key adopts, once, the newest clean answer stored under its older timed key for the same declared value, so SK007 stays known where it already answered.
|
|
6
|
+
- **Every section of a repo page has a key: ⌥ and its first letter.** ⌥C checks, ⌥R runs, ⌥A asks, ⌥T tests, ⌥M media, ⌥W wikis, ⌥D admin, read from the physical key so macOS's option characters and AZERTY keep them, printed as a badge at the end of each section line and on each section's entry in the ⌘K picker.
|
|
7
|
+
- **The open in VS Code button wears the VS Code mark, and it and open in Fork close the top bar at its far right end.**
|
|
8
|
+
- **The dock header: a cycling size button and small status pills.** The size is one button showing the current size, a dot per size and `⌘B`; a click or the key moves to the next (`CycleButton`, reusable). `runs, every repo`, the `(?)`, the in flight count and the size switch are gone. The status is a row of dim pills that never wraps: idle or running, queued, a warn pill when the plan holds, the last check with its repo icon, verdict, short id and age, the settling repos as one pill listing them on hover, and the window at slim and open; what does not fit folds into `+N`.
|
|
9
|
+
- **A repo page's sections are the top of its left pane, its runs are in the top bar, and the bar under the strip is gone.** Checks, runs, asks, tests, media, wikis and admin are one line each over the tree, in the project list's line style (icon, label, a count where it says something: red checks, open asks, red test files), the current one lit; the `resources` title is gone. The home strip carries the repo's run state (`idle`, or a spinner and what runs, cut, whole on hover) and two compact buttons, `health pass` and `gate`, each with a tooltip naming what it runs; at its right the repo's processes, its hub's trouble, `modernize` while behind, and two icons, open in VS Code and open in Fork. The bar never wraps: below a width the labels shrink to their icons, then the run state to its spinner. A hub of its own draws the same pieces in its own header, with its name.
|
|
10
|
+
- **An admin page per repo (`#page:admin`).** Reload the hub (the old `restart`: stops it in the daemon and loads it again, its config read fresh), keep active (the old `active` checkbox: the plan's hand set active flag) and archive, which now asks for a confirmation first, each with the sentence that says what it does. The three left the bar.
|
|
11
|
+
- **The tree's filter is quieter.** Its placeholder is `filter`, its key `/` sits in a small badge at its right the way the project list prints its keys, and the severity filter sits beside it.
|
|
12
|
+
- **The tests page leads with every test file, slowest first.** One list of every file of every test check: its check, its last duration against its budget, its status and its age, the never ran ones last, then the test checks and the failing files. The `tests` section line opens it. `/api/tests.json` carries it as `files`, merged from each check's own list.
|
|
13
|
+
- **The dock slides between its sizes.** `⌘B` and the size switch ease the boundary over 180ms instead of jumping; a drag, a window resize and the first paint stay instant, and `prefers-reduced-motion` turns the slide off.
|
|
14
|
+
- **The list's foot: a sort menu that names each sort, and an archived button beside it.** The sort button no longer sorts on a click: it names the order the list is in and opens a small menu of labeled sorts, `last activity`, `last commit`, `name` and `open asks`, the current one checked, and a pick sorts once and closes it. `manual order` is the state after a drag, never a pick. The `▸ archived (N)` fold became a small button with the archived count beside the sort button: a click shows the archived repos just above the foot, which never moves, and a second click hides them, kept per browser. `/api/projects.json` rows carry `commit`, the last commit time, for the new sort.
|
|
15
|
+
- **Fix: a daemon restart in place no longer loses a save burst, resets the catch up window, or retries a decided fact.** When a repo joins the plan, a file the owner wrote after its last run on record counts as a save, so the burst settles and runs. The catch up window's start lives in the machine db, and a boot within 10 minutes of the plan's last beat keeps it. A decided fact whose probe skipped retries 1 hour after that skip, doubled per skip in a row up to 24 hours, from the run record. What holds the plan (`held: load 9.2 on 8 cores`, `settling: my-app, 42s`) shows on the planned page, first in `shipkit planned`, in its `--json` as `held`, and at the end of the dock's thin line.
|
|
16
|
+
- **The dock never shows nothing, and the regions read apart.** `⌘B` cycles a one row line (`2 running · 5 queued · last: my-app typecheck ✓ 12s ago`, or `idle`), the slim chart and the open panel; a browser that kept the old hidden size gets the line. The project list, the top bars and the dock each wear a surface token a small step from the page (`--bg-side`, `--bg-top`, `--bg-dock`).
|
|
17
|
+
- **The dock: what shipkit runs in every repo, under the whole page, on every page.** One run chart docked at the bottom of the window, below the rail and the page, one group of lanes per house repo that ran something in the window, the repo's icon and name on the left. `⌘B` (Ctrl+B) cycles its sizes: slim, the last two minutes in thin lanes; open, the last ten with the run bands, the axis and the concurrency band against the cap, its height dragged and kept. The size is read from the browser before the first paint, so a reload moves nothing. It reads ONE route for every repo, `GET /api/house-runs.json`, read only from each repo's record and only when that record moved, asked every 2s while the dock shows and the tab is in front.
|
|
18
|
+
- **A repo's whole run record moved to the chart over its runs page.** The live strip under the header is gone, and so are its fold key `l` and the other repos' collapsed lanes in `/api/live.json`, which the dock replaces.
|
|
19
|
+
- **The overview's processes table reads tighter.** The version has its own column (empty when unknown), the process label column is narrow and cut with the whole label on hover, and each kind of button has its own column (declared actions, take over, restart, start or stop, open), so every start sits under every start and a line without a button keeps the space. A pid copies on a click and says `copied` for a moment (a refused clipboard is said in the strip). A url is a link, opened the way the pick says. The section titles are gone: the open pick and `new window` sit at the right of the table's header row. The widget cards are as wide as what they hold, from 260px, with tighter padding.
|
|
20
|
+
- **The overview is the monitor, then one processes table, shipkit's own group first.** pm, the daemon and the web are lines of the table in the same line UI as the global processes, under a `shipkit` group: dot, label with a `core` tag and the shipkit version, port, pid, uptime from the pid's start (a pm reload no longer resets the daemon's or the web's uptime), and the log on hover. Their stop is disabled (always on), the daemon and the web restart through pm, and pm has none: it is the LaunchAgent, its tooltip says so. The separate shipkit card is gone, the monitor widgets come first, and each group's name sits at the top of its group. pm's status carries `ports` and its own log `tail`, and every child's log tail. `shipkit gp` prints the same core group first, with versions (it asks the daemon's `/hub.json` too), and `shipkit overview` prints the monitor then the processes, as the page.
|
|
21
|
+
- **The daemon or pm down is said on a line at the bottom of the page, never over it.** The line is reserved whether or not it says anything, so an alert moves nothing, and it stays empty until a probe has actually failed.
|
|
22
|
+
- **Fix: the plan no longer keeps the machine busy.** A save burst is asked about once the repo has been quiet for 60 s (every 10 minutes at most for a repo never quiet), where it was every 250 ms pause. A check a save moved waits 20 times its last measured run after that run, so a heavy suite runs at most a few times an hour. Above a load average of 1 per core the plan starts nothing on its own (`Planner.held()` says why), and a click still runs at once. A check nothing can key that has a cadence (its `every`, its `cron`, or the default range) runs on that cadence only, never on every save. Measured on the house repo before the fix: 342 background gates in 2 hours, `test:rvlib-shipkit` 188 times, `pb-playground` 716 runs in a row under a rule that read every 20h..28h.
|
|
23
|
+
- **The home's console commands read as one family.** `shipkit activity` is the activity page's command, `shipkit cross-repo-activity` stays as its long name. `shipkit overview`, `shipkit activity`, `shipkit planned` and `shipkit global-processes` (`gp`) sit together under a `Home` group in `shipkit --help` and the command page. The activity page names its command in a dim footer line, as the overview and the plan do.
|
|
24
|
+
- **`shipkit overview`: the overview page in the console.** Its three sections from the answers the page reads: pm, the daemon and the web with pid, uptime and version (pm's status, the daemon's `/hub.json`), the global processes as `shipkit gp` prints them (one `globalProcessesAnswer`), and every monitor widget as text from the html the daemon last rendered (`/api/home/widgets.json`): tags stripped, a table's cells aligned, a `.wbar` meter drawn as `██████░░░░ 62%` (`src/home/widget-text.ts`). When pm or the daemon does not answer it says which and prints what is still known (the declared processes, their state unknown), and exits 0. `--json` gives `{ now, pm, daemon, processes, widgets }`, widgets the route's exact payload. pm's status carries `since`, so its own uptime shows. The overview names the command in a dim footer line.
|
|
25
|
+
- **The global pages are four lines on top of the project list.** Overview, jump to…, activity and planned each get a line in the repo rows' style, an icon, the label and the shortcut at the line's end, then a thin divider before the repos. The page shown is lit, and the jump to line while the picker is open. At the list's narrowest the labels truncate and the shortcuts stay.
|
|
26
|
+
- **`shipkit planned`: the planned page in the console.** It asks the daemon's `/api/home/planned.json`, the route the page polls, and draws the page's one list (now, late, next) with the page's own functions, now in `src/plan/plan-timeline.ts`: per row when (`in 4m`, `late 2h`), repo, check, its mark, its last result, then its title and the rule that planned it (`every 7d`, `on change (keyed on its files)`). The checks only a save runs are counted, `--all` lists them, `--repo <name>` keeps one repo, `--json` prints the route's exact payload. With the daemon down it says so and exits 0: the plan (its marks, the running batch) lives only there, so nothing is rebuilt from disk. A plan row carries its check's `title`. The planned page names the command in a dim footer line.
|
|
27
|
+
- **A global process's uptime survives a pm reload.** While a process is up with a pid, its uptime counts from when that pid started, as the OS says (`ps -o lstart=`), not from pm's last observed state change. The start is kept per pid: a pid pm spawned needs no `ps` at all, an adopted pid costs one `ps` at adoption, and a new pid (a restart) is read again. A process without a pid keeps `since` as before.
|
|
28
|
+
- **Fix: opening the overview, the picker, the activity or the plan no longer resets the selected repo to the house repo.** The repo list keeps marking the repo you were in, dimmer while a global page is up, and on the home no row is marked before a repo is picked. On a global page the ⌘K picker lists that repo too, since it is not on screen.
|
|
29
|
+
- **The open in Fork button leads each repo's commit list on the activity page.** It left the repo name cell for a narrow column of its own right before the commits, on the same line as the first commit (or the red uncommitted line), still dim until its row is hovered.
|
|
30
|
+
- **The ⌘K picker shows icons.** Every repo entry wears its repo icon (its letter tile when it has none), as the project list shows it, and home, planned, activity and archived wear the icons of their buttons. The order is unchanged. `bun run activity:check` checks it in headless Chrome, with the activity status dot.
|
|
31
|
+
- **The activity page names what is wrong behind each status dot.** A red or orange dot is a button: hovering it floats the failing and warning checks at once, failing first, each with its id, its title, how old that outcome is and the first line it printed about the problem. A click lists the same checks under the repo's row, so they read without hover (the macOS app shows no native tooltip), and a second click hides them. Each id links to that repo's check page. The daemon's activity answer carries them as `failing` on each row, titles from the plan (`Planner.titles`).
|
|
32
|
+
- **Fix: `bun run home:check` reported screenplay's tree as taking 20s on its first visit.** It loads in about 40ms from what was read ahead. The walk waited for a title starting with the home's key (`screenplay ·`), but the title carries the name the repo's own config declares (`screenplay-shed · hub`), so the wait ran to its timeout and the timeout was then reported as a load time. The walk now matches the shown repo by its home key, and reports a load time only for a tree that showed.
|
|
33
|
+
- **Fix: `bun run home:check` read the four buttons on top of the project list as missing.** Its probe quoted `⌘'` inside a single quoted string, the page could not parse it, and the walk's wait read the parse error as "not yet" for 20s. The expected labels are now quoted by `JSON.stringify`, and a probe that does not parse now throws instead of timing out.
|
|
34
|
+
- **A private repo commits its comfy drafts through a `.comfy-ts/` block.** The `blocked-paths` pre-commit gate lets `.comfy-ts/drafts/` through when the repo declares `visibility: 'private'`; `hosts/`, `cache/`, `outputs/` and every other `.comfy-ts/` path stay blocked. A public repo and a repo with no declared visibility keep the full block, and a prefix naming the drafts folder itself still blocks it. Before, a `.comfy-ts/` block contradicted the SK004 `!.comfy-ts/drafts/` re-include, and drafts stayed uncommitted forever. `blockedStaged` is exported from `hooks/run-hooks.ts`, and `shipkit hooks list` names the exemption.
|
|
35
|
+
- **A public repo and an npm unit keep their own files.** Two declared facts, each on its owner. The root unit's `release: { npm: true }` says a package strangers install and work on: SK001 asks only that it can publish (no `"private": true`, a `license`), never routes or renames its scripts, and its fixer writes nothing; SK005 names an rvlib bump and its command and never runs it; SK006 reads it as publishing by design; SK013 and SK017 report green and never write its `.claude/settings.json` or `.vscode/tasks.json`. The repo's `visibility: 'public'` says strangers read its tree: SK004 keeps `.comfy-ts/drafts/` ignored, and SK007 and SK009 read the declared visibility only (absent is private). A public repo with no npm target, like a site, keeps SK013 and SK017. A `shipkit ci` over a public fixture whose root unit declares npm, with its own cli, leaves package.json, `.claude/settings.json` and `.vscode/tasks.json` byte for byte (`tests/public-npm-repo.test.ts`, with a private app control that is rewritten).
|
|
36
|
+
- **The root unit's npm target is declared, never inferred.** A root whose package.json is not private and whose config says neither `release: { npm: true }` nor `npm: false` no longer publishes by default: resolution refuses and names both edits, the same rule sub units follow. So the release path and the checks read one fact.
|
|
37
|
+
- **`shipkit global-processes` (short: `shipkit gp`).** The overview's global processes table in the console, from pm's own answer: per repo, one line per process with its state (`up 2h`, `crashed 3m ago`, `stopped`), label, url, pid, note and last log line. `--json` gives the rows the overview draws. When pm is down it says so, lists the declared processes with their state unknown, and exits 0.
|
|
38
|
+
- **The global processes are one table, a line per process.** The overview's cards are gone: a row group per declaring repo (its icon and name in the first column, as on the activity page), then one line per process with its state dot, icon, label (its log on hover), url, `up 2h` or `crashed 3m ago`, and start or stop and open at the right. Restart, take over and declared actions stay small, shown only when they exist. A running line is tinted green, a crashed one red, and nothing scrolls sideways at phone width. pm's resource view carries `pid` and `since` (the last status change). The rows come from `src/home/process-rows.ts`, one model for the page and the console.
|
|
39
|
+
- **Fix: a link with no class of its own was the browser's dark blue on the dark page**, like `5 archived repos` on the overview. Every such link now takes the accent colour.
|
|
40
|
+
- **Fix: a repo's own cli is never taken for shipkit.** SK001 read any `bun <path>cli.ts` script as the in repo shipkit, so in a repo with its own cli (`bun src/cli/comfy-ts-cli.ts tui`) its fixer rerouted `test`, `lint` and `typecheck` to `bun src/cli/comfy-ts-cli.ts ci --only …`, a command that cli does not have, and SK013 pointed its guard beside it. Only a file named `cli.ts` under a directory whose name says shipkit counts now (`isShipkitCli`), and the `serve` alias WARN reads the same test.
|
|
41
|
+
- **Open a repo in Fork from the activity page.** A small button beside each repo name opens it in the Fork git client (macOS). The daemon takes a house repo name only, refuses an archived one, and says so when Fork is not installed.
|
|
42
|
+
- **`shipkit cross-repo-activity`.** The activity page in the console, from the same computation: every house repo's checks, branch, upstream, uncommitted work and commits in a window (`--max-age 30m`, `--max-commit 3`), or the route's exact payload with `--json`. Local git only, no daemon needed.
|
|
43
|
+
- **Fix: the activity page and the status dot read what is true now.** An activity answer older than 30s is never served (a page opened hours later showed hours old rows), a tab coming back no longer forces a git pass, and a repo's status dot counts only the checks it declares today, so a renamed or deleted check no longer holds it red. The hover says how long each check has been red.
|
|
44
|
+
- **The activity page reads faster.** Six repos at a time, and the git asks inside a repo start together: 0.5s to 0.22s for 19 repos on an idle machine, with a larger gain while the daemon is busy.
|
|
45
|
+
- **The activity page has a status column before the branch.** One dot per repo from its own run record: red with the number of failing checks, orange with the number of warnings, green when every check passed, grey when shipkit never ran there. The check ids show on hover. The project rows carry it as `checks`.
|
|
46
|
+
- **The activity page shows only `main` green.** A repo on `master` is orange like any other branch, so a repo still to rename stands out.
|
|
47
|
+
- **New built-in SK028 readme, non-blocking.** A repo's `README.md` exists and shows at least one screenshot: a markdown or html image whose source is a file in the repo or an https url (a status badge does not count). Each requirement reports its own line, so a new mandatory README item is one more entry. Keyed on the README and the images it names, so it runs on change only.
|
|
48
|
+
- **No built-in rides the daily default range any more.** SK008, SK009, SK015, SK022 and SK024 each declare what their answer depends on and run only when it moves: SK008 on repo.config.ts, the agent definition and the brief (by content, inside the repo or not), SK009 on HEAD, every ref and the settings file (a commit, a fetch or a tag re-asks it, since the save watch now hears a ref move inside `.git`), SK015 on the config and where the work tree root sits, SK022 on package.json, the installed copy and the running version, SK024 on the config and the installed config source. The 20h..28h range is now for a REPO check that declares nothing, and `shipkit keys --burn-down` WARNs on each one (a built-in there is a FAIL line). A guard test fails the toolkit on any built-in without a key, a `ttl` or an `every`.
|
|
49
|
+
- **SK008 is titled "reviewer definition".** It lints the agent definition file and never starts an agent, so the planned page no longer reads as a scheduled review.
|
|
50
|
+
- **A sweep guard: nothing starts an agent on its own.** `agentSpawnIn` (`src/core/agent-spawn.ts`) finds a spawn or exec of `claude`, `claude -p`, or an import of the agent sdk, comments skipped. A test runs it over every source file, beside the samples it must catch and the lookalikes it must let through (a url, `~/.claude/projects`, a help line naming the product).
|
|
51
|
+
- **A repo page with no row picked shows its README, images included.** The first of `README.md`, `readme.md`, `Readme.md`, `README.markdown` or `README` at the repo root, rendered as markdown. A relative image loads through the new `GET /api/readme-file/<path>`, which serves only an image file inside the repo. An http(s) image such as a badge loads as written. A repo without a README keeps the hint.
|
|
52
|
+
- **An archived repo is processed by nothing but its own page.** `houseRepos()` now splits `archived: true` repos out, so the rail, the daemon (hub, watch, plan rows, gates, processes, widgets, identity fill, store sweep), the activity page, pm's global processes and `shipkit fleet` never see one. The project list's folded archived group is gone. `/archived` (the picker's `archived`, and a link at the foot of the overview while one exists) lists them, each with `unarchive`. `/api/projects.json` answers `archived: [{ name, icon }]` beside `projects`, and a row no longer carries `archived`.
|
|
53
|
+
- **Archive a repo: `archived: true` in repo.config.ts.** It stays known and listed (dimmed, in a folded group at the bottom of the project list, out of the ⌘ numbering), and nothing runs there: the daemon never loads its hub, so no file watch, no plan rows, no automatic gate, no repo process, no widget or global process, no identity fill, no store sweep. The `archive` button in the repo header toggles it and moves only that key (`POST /api/projects/archived`, `src/core/archived.ts`). Archiving a loaded repo stops it on the next tick; unarchiving loads it again.
|
|
54
|
+
- **Global process cards: start and open, grouped by repo, tinted by state.** A card has two buttons, `start` (`stop` while it runs) and `open`; restart, take over and declared actions stay as small controls where they apply. Cards group under their declaring repo (its icon, its identity colour) in the project list's order, are smaller, and an up card is tinted green, a crashed one red. Where `open` opens is one icon pick in the section's title row, the default browser or a shipkit app window, kept per browser. New `globalProcess({ icon })`: an emoji or a repo-relative image path, served by pm at `/api/pm/icon/<id>`; without one the card wears its repo's icon. Breaking: `app: true` is removed, since the app window opens any url; pm's `act: 'app'` works for every process with a url.
|
|
55
|
+
- **New built-ins SK026 asks auto close and SK027 agent wiki links.** Every repo with a `.shipkit/asks/` queue now archives the asks whose own `check:` probe passes (an open queue is no warning; a frozen run archives nothing). Every repo with `agent/index.md` fails an orphan page or a dangling ref there. A repo without the queue or the wiki skips.
|
|
56
|
+
- **Retired check ids are listed** (`src/core/retired-checks.ts`), and discovery refuses a built-in that takes one again.
|
|
57
|
+
- **Breaking: the `contracts` check is gone, SK002 is the one row for the release contracts.** Both ran the same scan, so every repo showed one invariant twice. A venue deploy step declares `deps: ['SK002']` instead of `deps: ['contracts']`.
|
|
58
|
+
- **New built-in SK025 release drift.** Every unit a repo releases matches npm and its push remote, the survey `shipkit releases` prints, as warnings. A repo that releases nothing (a private root with no release config and no units) skips it. A clean answer keeps 20h, keyed on every unit's version. It replaces the drift check a repo kept per published lib.
|
|
59
|
+
- **Breaking: SK021 (github login) and SK023 (stores under cap) are no longer built-ins.** Both ask about the machine, never the repo, so every repo re-asked one fact. The gh probe and its `core` credential row stay in the lib as `src/core/gh-login.ts` (`probeGhLogin`, `GH_LOGIN_CHECK`): a house repo declares the one check that runs it. The store caps keep their measure and sweep in `src/core/retention.ts` (`capWords` joins them), for a house check that measures every repo in one pass.
|
|
60
|
+
- **`shipkit assets check` no longer probes the comfy backend.** Whether a box answers is a machine fact, never a question about a repo's kit.
|
|
61
|
+
- **An activity page (`/activity`, ⌘L).** One table of every house repo: its branch (green on main, orange elsewhere) and a red line of what it holds uncommitted (the paths on hover). Beside them, its commits in a window you pick (10m to 7d, 30m by default), newest first, each with a green ✓ once pushed or an orange ↑ until then. A number sets how many commits a repo shows (1 to 15, 3 by default). It reads again every minute while shown, and the countdown is a button that reads at once. The counts beside it are pills. A repo with nothing in the window still shows its last commit, in grey, and quiet repos sit below the busy ones. The daemon reads it with local git only while the page is open and visible, never with a fetch.
|
|
62
|
+
- **New keys, and a picker button.** ⌘J the overview, ⌘K the picker, ⌘L the activity, ⌘' the plan, four buttons on top of the project list in that order. The sort by last activity moves to the foot of the list, and above it a fold lists the archived repos by name, each a link to the archived page.
|
|
63
|
+
- **The house repo is active whenever any house repo is.** Its checks serve every repo, so a `wait` check there no longer turns late while you work elsewhere.
|
|
64
|
+
- **`ttl: 'forever'` for a decided fact.** A fact that moves only when someone decides to move it (a repo's visibility, an account that exists) keeps its clean answer for good, keyed on its declared value alone (`cacheKey`), so a new shipkit asks nothing again, and the store sweep never drops it. The plan reads it as the new `every: 'once'`: it runs only while no answer exists for its current key (first time, a declaration edit, a lost db), never on a clock.
|
|
65
|
+
- **SK007 is a decided fact and no longer blocks.** Its clean answer is kept until `visibility` changes, and a mismatch warns: a gate block does not un-leak a repo.
|
|
66
|
+
- **A check keyed on its files runs on change only.** With no `every` declared, a check with `inputs`, or one keyed on what it read through `c`, no longer runs on the daily default range: its answer replays until a file moves. Only a check nothing can key keeps that range, and a `ttl` still reads as `every`.
|
|
67
|
+
|
|
1
68
|
## 1.2.0
|
|
2
69
|
|
|
3
70
|
- **Fix: SK024 read back only an `identity` key that starts a line.** On a one line config its own fix wrote the key inline, the check kept warning, and every fix pass added the key again, so a release gate always rewrote `repo.config.ts`. The key is now found at the top level of `defineRepo({…})` wherever it sits, bare or quoted, and a nested `identity` (an SK002 exemption) or one in a string or comment is not it.
|
package/README.md
CHANGED
|
@@ -81,7 +81,7 @@ Output is a stable line protocol — `ok:` / `WARN` / `FAIL:` / `skip:` with ind
|
|
|
81
81
|
|
|
82
82
|
## Release contracts
|
|
83
83
|
|
|
84
|
-
A venue contract is data: required files, exact PNG dimensions (header-parsed, no image dependency), glob minimums, required markdown sections. Enforcement is both ways — a declared venue with an incomplete folder fails, and an undeclared venue whose signals fire (deploy scripts, folders) fails too. `exempt: { steam: "reason" }` skips visibly. Wire venue deploys as `deps: ['
|
|
84
|
+
A venue contract is data: required files, exact PNG dimensions (header-parsed, no image dependency), glob minimums, required markdown sections. Enforcement is both ways — a declared venue with an incomplete folder fails, and an undeclared venue whose signals fire (deploy scripts, folders) fails too. `exempt: { steam: "reason" }` skips visibly. Wire venue deploys as `deps: ['SK002']` and an incomplete folder physically blocks the upload.
|
|
85
85
|
|
|
86
86
|
Ship your own contracts via `contracts: { extra: [defineContract({...})] }` — see `rvlib-shipkit/contracts` for the rule vocabulary.
|
|
87
87
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "rvlib-shipkit",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.3.0",
|
|
4
4
|
"funding": "https://github.com/sponsors/rvion",
|
|
5
5
|
"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",
|
|
6
6
|
"license": "MIT",
|
package/src/assets/run-assets.ts
CHANGED
|
@@ -236,14 +236,9 @@ export const runAssetsCheck = (root: string, io: AssetsIo): Promise<number> =>
|
|
|
236
236
|
io.print(`WARN no schema cache for host '${lw.wf.host.data.id}' — bunx comfy-ts gen --id ${lw.wf.host.data.id} --host ${lw.wf.host.getServerHostHTTP()}`)
|
|
237
237
|
}
|
|
238
238
|
}
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
io.print(up ? `ok: comfy backend reachable (${url})` : `WARN comfy backend unreachable (${url})${kit.comfy.heal != null ? ` — ${kit.comfy.heal}` : ''}`)
|
|
243
|
-
}
|
|
244
|
-
} finally {
|
|
245
|
-
disconnectAll({ workflows: wfs })
|
|
246
|
-
}
|
|
239
|
+
// whether the backend answers is a machine fact the house repo's
|
|
240
|
+
// box check owns, never a question about this repo's kit
|
|
241
|
+
disconnectAll({ workflows: wfs })
|
|
247
242
|
}
|
|
248
243
|
}
|
|
249
244
|
// machine-clearable red clears itself: drifted/missing identity
|
package/src/check.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import type { CheckCtx } from './core/check-ctx.ts'
|
|
22
22
|
import { parseWhen } from './core/when.ts'
|
|
23
23
|
import { planDeclProblem, type Every, type WhenInactiveDecl } from './plan/cadence.ts'
|
|
24
|
-
import type
|
|
24
|
+
import { FOREVER, parseDuration, type Line } from './core/wire.ts'
|
|
25
25
|
import type { RepoConfig } from './repo-config.ts'
|
|
26
26
|
|
|
27
27
|
export { defineScan, mdPages, packageJson, type MdPage, type PackageJson, type Scan } from './scan.ts'
|
|
@@ -98,7 +98,10 @@ export type CheckDef = {
|
|
|
98
98
|
* correctness. At most one of the three. */
|
|
99
99
|
inputs?: string[]
|
|
100
100
|
/** WORLD population: nothing on disk describes the answer, so a clean
|
|
101
|
-
* pass keeps for this long, as in '24h'.
|
|
101
|
+
* pass keeps for this long, as in '24h'. `'forever'` for a DECIDED fact
|
|
102
|
+
* (a repo's visibility, an account that exists): a clean answer never
|
|
103
|
+
* expires, it is keyed on the declared value alone (`cacheKey`), and the
|
|
104
|
+
* plan runs it only while no answer exists for that key. */
|
|
102
105
|
ttl?: string
|
|
103
106
|
/** a DECIDED never-cacheable, with the reason. Says somebody looked and
|
|
104
107
|
* no key can describe this, which takes it off the burn-down list. */
|
|
@@ -112,13 +115,16 @@ export type CheckDef = {
|
|
|
112
115
|
/** WHEN the planner runs it on its own, beside the save that moved what
|
|
113
116
|
* it reads: a span since its last run (`'24h'`), a range (`['10h',
|
|
114
117
|
* '12h']`, each repo and check on a stable point inside it, so checks
|
|
115
|
-
* spread out), a cron expression (`'0 9 * * 1'`),
|
|
116
|
-
*
|
|
118
|
+
* spread out), a cron expression (`'0 9 * * 1'`), `'once'` (only while
|
|
119
|
+
* no answer exists: what `ttl: 'forever'` reads as), or null for on
|
|
120
|
+
* change only. Or a PURE function of its last result (`status`, `at`, `streak`
|
|
117
121
|
* of that same status) and whether the repo is active, returning any of
|
|
118
122
|
* those: `(last) => (last?.status === 'fail' ? '1h' : '7d')`. A lambda
|
|
119
123
|
* that throws is an error row on the planned page, and the check runs on
|
|
120
|
-
* change only. Absent: its `ttl`, else a build step
|
|
121
|
-
*
|
|
124
|
+
* change only. Absent: its `ttl`, else a build step or a check keyed on
|
|
125
|
+
* its files waits for a save. A repo check nothing can key runs about
|
|
126
|
+
* once a day until it declares one, and a built-in never may: `shipkit
|
|
127
|
+
* keys --burn-down` names both. */
|
|
122
128
|
every?: Every
|
|
123
129
|
/** a time of day or week for the rare check that needs one, as five cron
|
|
124
130
|
* fields in local time (`'0 9 * * 1'`, Mondays at 9). Never with `every`. */
|
|
@@ -130,6 +136,12 @@ export type CheckDef = {
|
|
|
130
136
|
* working). A pure function of the last result may choose. Default
|
|
131
137
|
* `'wait'`. */
|
|
132
138
|
whenInactive?: WhenInactiveDecl
|
|
139
|
+
/** why this repo gives the check nothing to do on a clock, or null: the
|
|
140
|
+
* plan leaves it out there (no row, no timer), and a gate or a health
|
|
141
|
+
* pass still runs it. Read at every reload of the repo's checks, so a
|
|
142
|
+
* file that gives it work (an ask with a probe, a release unit) plans it
|
|
143
|
+
* again. Cheap and pure over the repo's files and config */
|
|
144
|
+
planOff?: (env: CheckEnv) => string | null
|
|
133
145
|
/** cache-identity contribution for a `ttl` check whose verdict depends on
|
|
134
146
|
* config: a changed key busts the answer instantly instead of serving
|
|
135
147
|
* a stale cached assertion for the rest of the TTL. */
|
|
@@ -201,6 +213,8 @@ export function check(d: CheckDef): CheckDef {
|
|
|
201
213
|
const populations = (['inputs', 'ttl', 'never'] as const).filter((k) => has(k))
|
|
202
214
|
if (populations.length > 1)
|
|
203
215
|
throw new Error(`check${d.id == null ? '' : ` ${d.id}`}: ${populations.join(' and ')} contradict each other — a check reads files, or the world, or nothing can describe it`)
|
|
216
|
+
if (d.ttl != null && d.ttl !== FOREVER)
|
|
217
|
+
try { parseDuration(d.ttl) } catch { throw new Error(`check${name}: ttl '${d.ttl}' is neither a span ('7d') nor 'forever'`) }
|
|
204
218
|
const planBad = planDeclProblem({ every: d.every, cron: d.cron, whenInactive: d.whenInactive })
|
|
205
219
|
if (planBad != null) throw new Error(`check${name}: ${planBad}`)
|
|
206
220
|
if (d.when != null) {
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
import { canonicalBodies, conditionalBodies, inferBin, ROUTED_SCRIPTS, rawName, routedBody, routesToShipkit } from '../core/canonical-scripts.ts'
|
|
14
14
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
15
15
|
import { checkFloorReader, resolveFloor } from '../core/floor-resolve.ts'
|
|
16
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
16
17
|
import type { RepoConfig } from '../repo-config.ts'
|
|
17
18
|
import { check, type FixResult } from '../check.ts'
|
|
18
19
|
|
|
@@ -24,7 +25,7 @@ const OWNER = 'meta/rv-conventions/package-json.md'
|
|
|
24
25
|
// alias really is the canonical capability (a `serve` that serves a dist/
|
|
25
26
|
// folder must not be told to rename)
|
|
26
27
|
const ALIASES: Record<string, { canonical: string; bodyMatch: RegExp }> = {
|
|
27
|
-
'serve': { canonical: 'hub', bodyMatch: /(shipkit
|
|
28
|
+
'serve': { canonical: 'hub', bodyMatch: /(?:^|\s)(?:shipkit|\S*shipkit\S*\/cli\.ts) serve\b/ },
|
|
28
29
|
'pretty': { canonical: 'fmt', bodyMatch: /./ },
|
|
29
30
|
'check-types': { canonical: 'typecheck', bodyMatch: /./ },
|
|
30
31
|
}
|
|
@@ -46,6 +47,15 @@ export function evaluatePackageJson(t: CheckCtx): void {
|
|
|
46
47
|
return
|
|
47
48
|
}
|
|
48
49
|
const scripts = pkg.scripts ?? {}
|
|
50
|
+
// a package whose root unit declares npm is a public contract: it
|
|
51
|
+
// publishes, so the app field rules invert, and its script names are its
|
|
52
|
+
// own vocabulary, which no house name or route may rewrite
|
|
53
|
+
if (rootPublishesNpm(c)) {
|
|
54
|
+
if (pkg.private === true) t.fail(`package.json: the root unit declares release.npm, and "private": true makes npm refuse the publish`)
|
|
55
|
+
if (pkg.license == null) t.fail(`package.json: a package published to npm declares its license`)
|
|
56
|
+
if (!t.failed) t.ok(`package.json shape ok (published to npm: publishable, license ${pkg.license})`)
|
|
57
|
+
return
|
|
58
|
+
}
|
|
49
59
|
// rvlib-* as a probe ROOT is theoretical (libs live as sub-packages and
|
|
50
60
|
// the probe only runs where repo.config.ts loads) — field inversion is
|
|
51
61
|
// still handled so a future lib-root repo fails correctly, not absurdly
|
|
@@ -177,14 +187,15 @@ export const packageJsonShape = check({
|
|
|
177
187
|
fix: (c): FixResult => {
|
|
178
188
|
const raw = c.read('package.json')
|
|
179
189
|
if (raw == null) return { changed: false, summary: 'no package.json — nothing to fix' }
|
|
190
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: 'published to npm: its package.json is its public contract, never rewritten' }
|
|
180
191
|
let pkg: Pkg & { scripts?: Record<string, string> }
|
|
181
192
|
try { pkg = JSON.parse(raw) as Pkg } catch { return { changed: false, summary: 'package.json unparseable — repair it by hand first' } }
|
|
182
193
|
if (pkg.name?.startsWith('rvlib-') === true) return { changed: false, summary: 'rvlib-* package — license/private are publish decisions, not autofixable' }
|
|
183
194
|
const changes: string[] = []
|
|
184
195
|
const notes: string[] = []
|
|
185
196
|
// any "I am published" marker → the field flips are a publish
|
|
186
|
-
// decision, not a mechanical repair (
|
|
187
|
-
//
|
|
197
|
+
// decision, not a mechanical repair (a package that publishes says so
|
|
198
|
+
// with release.npm, which skips this fixer outright)
|
|
188
199
|
const publishMarkers: string[] = ['files', 'exports', 'publishConfig'].filter((k) => (pkg as Record<string, unknown>)[k] != null)
|
|
189
200
|
if (pkg.scripts?.['prepublishOnly'] != null) publishMarkers.push('prepublishOnly')
|
|
190
201
|
if (publishMarkers.length > 0) {
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
// registration point; the evaluation machinery stays in
|
|
3
3
|
// contracts/contracts-engine.ts (venue manifests are DATA rows, not rules —
|
|
4
4
|
// one SK id covers the invariant "declared contracts are fulfilled, folders
|
|
5
|
-
// can't rot undeclared"). Every surface reporting fulfillness (`check`,
|
|
6
|
-
// `shipkit contracts
|
|
7
|
-
//
|
|
5
|
+
// can't rot undeclared"). Every surface reporting fulfillness (`check`, `ci`,
|
|
6
|
+
// `shipkit contracts`) tags with this id via contracts/apply-contracts.ts. A
|
|
7
|
+
// venue deploy step deps on it BY ID (`deps: ['SK002']`).
|
|
8
8
|
import { applyContractsResult, contractsResult } from '../contracts/apply-contracts.ts'
|
|
9
9
|
import { check } from '../check.ts'
|
|
10
10
|
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// `c.exec` is never stored on its reads.
|
|
17
17
|
import { readdirSync } from 'fs'
|
|
18
18
|
import type { RepoConfig } from '../repo-config.ts'
|
|
19
|
+
import { declaredPublic } from '../core/repo-facts.ts'
|
|
19
20
|
import { GENERATED_IGNORE_LINES, GENERATED_TRACKED_PATHS, untrackGenerated } from '../rules/untrack-generated.ts'
|
|
20
21
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
21
22
|
import { gitBin } from '../core/git-bin.ts'
|
|
@@ -29,6 +30,8 @@ const BEGIN = '# >>> shipkit gitignore policy (SK004) — managed block, edit ou
|
|
|
29
30
|
const END = '# <<< shipkit gitignore policy'
|
|
30
31
|
const CONTRIB = '# repo-contributed lines (repo.config.ts gitignore:)'
|
|
31
32
|
|
|
33
|
+
const DRAFTS_BACK = '!.comfy-ts/drafts/'
|
|
34
|
+
|
|
32
35
|
export const POLICY_LINES = [
|
|
33
36
|
// everything shipkit keeps lives under .shipkit/ (core/layout.ts). Local
|
|
34
37
|
// by default: the private notes, drafts, venue listings and caches, and the
|
|
@@ -62,7 +65,7 @@ export const POLICY_LINES = [
|
|
|
62
65
|
// steps and cfg, so it is authored state and belongs in the repo, unlike the
|
|
63
66
|
// schema dump, the lora previews and the raw run outputs beside it
|
|
64
67
|
'.comfy-ts/*',
|
|
65
|
-
|
|
68
|
+
DRAFTS_BACK,
|
|
66
69
|
'*.loras.local.txt',
|
|
67
70
|
// shipkit's own per-run output. Ignoring is only half of it: the fixer
|
|
68
71
|
// also removes these from the index, because .gitignore does nothing to a
|
|
@@ -70,6 +73,11 @@ export const POLICY_LINES = [
|
|
|
70
73
|
...GENERATED_IGNORE_LINES,
|
|
71
74
|
]
|
|
72
75
|
|
|
76
|
+
/** the policy one repo carries. A declared public tree ships to strangers,
|
|
77
|
+
* so the drafts stay ignored there: a draft holds the prompts and values
|
|
78
|
+
* behind generated assets, authored state in a private repo */
|
|
79
|
+
const policyLines = (config: RepoConfig): string[] => declaredPublic(config) ? POLICY_LINES.filter((l) => l !== DRAFTS_BACK) : POLICY_LINES
|
|
80
|
+
|
|
73
81
|
const LEGACY = '# the old root layout, ignored until the fix pass moves it under .shipkit/'
|
|
74
82
|
|
|
75
83
|
/** `legacy`: the repo still carries contract folders at its root from before `.shipkit/`. Their names
|
|
@@ -77,11 +85,11 @@ const LEGACY = '# the old root layout, ignored until the fix pass moves it under
|
|
|
77
85
|
* that is newer than the tree it guards */
|
|
78
86
|
export function expectedBlock(config: RepoConfig, legacy = false): string {
|
|
79
87
|
const extra = config.gitignore ?? []
|
|
80
|
-
return [BEGIN, ...
|
|
88
|
+
return [BEGIN, ...policyLines(config), ...(legacy ? [LEGACY, ...LEGACY_IGNORED] : []), ...(extra.length > 0 ? [CONTRIB, ...extra] : []), END].join('\n')
|
|
81
89
|
}
|
|
82
90
|
|
|
83
91
|
export function policyPatterns(config: RepoConfig, legacy = false): string[] {
|
|
84
|
-
return [...
|
|
92
|
+
return [...policyLines(config).filter((l) => !l.startsWith('#')), ...(legacy ? LEGACY_IGNORED : []), ...(config.gitignore ?? [])]
|
|
85
93
|
}
|
|
86
94
|
|
|
87
95
|
/** does the root still hold a contract folder from before `.shipkit/` */
|
|
@@ -48,6 +48,7 @@ import { basename, join } from 'path'
|
|
|
48
48
|
import type { CheckCtx } from '../core/check-ctx.ts'
|
|
49
49
|
import { allowMajor, breakingAdvice, isBreaking } from '../core/update-policy.ts'
|
|
50
50
|
import { isRunningCopy, rememberLatest } from '../core/self-update.ts'
|
|
51
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
51
52
|
import { mutateLease, withMutateLease } from '../core/locked-write.ts'
|
|
52
53
|
import { detectPackageManager, isRefusal, LOCK_FILES } from '../core/package-manager.ts'
|
|
53
54
|
import { check, type CheckEnv } from '../check.ts'
|
|
@@ -77,6 +78,11 @@ export const rvlibVersions = check({
|
|
|
77
78
|
ttl: '24h',
|
|
78
79
|
// it updates the rvlib deps unless the run is frozen
|
|
79
80
|
writes: true,
|
|
81
|
+
planOff: (env) => {
|
|
82
|
+
let pkg: Pkg
|
|
83
|
+
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'no readable package.json' }
|
|
84
|
+
return Object.keys({ ...pkg.dependencies, ...pkg.devDependencies }).some((n) => n.startsWith('rvlib-')) ? null : 'no rvlib-* dependency'
|
|
85
|
+
},
|
|
80
86
|
cacheKey: (env) => {
|
|
81
87
|
let pkg: Pkg
|
|
82
88
|
try { pkg = JSON.parse(readFileSync(join(env.root, 'package.json'), 'utf8')) as Pkg } catch { return 'unreadable' }
|
|
@@ -84,7 +90,7 @@ export const rvlibVersions = check({
|
|
|
84
90
|
.filter((n) => n.startsWith('rvlib-'))
|
|
85
91
|
.sort()
|
|
86
92
|
.map((n) => `${n}@${installedVersion(env.root, n) ?? 'absent'}`)
|
|
87
|
-
.join(',')
|
|
93
|
+
.join(',') + (rootPublishesNpm(env.config) ? ' npm' : '')
|
|
88
94
|
},
|
|
89
95
|
run: async (c) => {
|
|
90
96
|
let pkg: Pkg
|
|
@@ -137,6 +143,12 @@ export const rvlibVersions = check({
|
|
|
137
143
|
c.warn(`newer rvlib live: ${drift} (frozen — apply with: ${cmd.join(' ')})`)
|
|
138
144
|
return
|
|
139
145
|
}
|
|
146
|
+
// a package published to npm: its package.json is its public
|
|
147
|
+
// contract, so the bump is a release decision, named and never made
|
|
148
|
+
if (rootPublishesNpm(c.config)) {
|
|
149
|
+
c.warn(`newer rvlib live: ${drift} (published to npm, never updated by a check — apply with: ${cmd.join(' ')})`)
|
|
150
|
+
return
|
|
151
|
+
}
|
|
140
152
|
const dirty = await c.exec(['git', 'status', '--porcelain', '--', ...manifest], { cwd: c.root })
|
|
141
153
|
if (dirty.code !== 0 || dirty.out.trim() !== '') {
|
|
142
154
|
c.warn(`newer rvlib live: ${drift} — self-update deferred (${dirty.code !== 0 ? `git says: ${dirty.out.trim().split('\n')[0] ?? `exit ${dirty.code}`}` : 'package.json/lockfile dirty'}); commit first, then re-run check`)
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { join, relative } from 'path'
|
|
16
16
|
import { checkUnitReader, resolveUnits } from '../release/unit-resolve.ts'
|
|
17
17
|
import { check } from '../check.ts'
|
|
18
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
18
19
|
|
|
19
20
|
type Pkg = {
|
|
20
21
|
name?: string
|
|
@@ -65,6 +66,10 @@ export const npmPublishSafety = check({
|
|
|
65
66
|
if (localHits.length === 0) c.ok('rvlib-* package — publishes by design (distribute/npm owns it)')
|
|
66
67
|
return
|
|
67
68
|
}
|
|
69
|
+
if (rootPublishesNpm(c.config)) {
|
|
70
|
+
if (localHits.length === 0) c.ok('the root unit declares release.npm: publishes by design, no local dependency protocol in any releasing unit')
|
|
71
|
+
return
|
|
72
|
+
}
|
|
68
73
|
if (pkg.private !== true) return c.note('private:true missing — SK001 reports it; publish machinery not judged until the lock is on')
|
|
69
74
|
const machinery: string[] = []
|
|
70
75
|
if (pkg.publishConfig != null) machinery.push('publishConfig')
|
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// own base-repo resolution prefers `upstream`, which in a fork-shaped
|
|
9
9
|
// checkout would confidently validate the WRONG repository. Flipping a
|
|
10
10
|
// repo public stays a human act behind the pre-public audit; this rule is
|
|
11
|
-
// the standing tripwire if it ever happens out of band.
|
|
12
|
-
//
|
|
11
|
+
// the standing tripwire if it ever happens out of band. A decided fact: a
|
|
12
|
+
// clean pass is kept forever, keyed on the declared value (a flip re-probes
|
|
13
|
+
// immediately, and the plan asks only while no answer exists); every
|
|
13
14
|
// unverifiable path (no git, no remote, non-github remote, gh missing or
|
|
14
15
|
// erroring, unreadable answer) is a loud skip — an UNKNOWN, never red,
|
|
15
16
|
// never cached.
|
|
@@ -17,20 +18,19 @@ import { carveGhJson, firstGhLine } from '../git-bootstrap.ts'
|
|
|
17
18
|
import { probeGitRoot } from '../core/git-root.ts'
|
|
18
19
|
import { nestedIn } from '../core/nesting.ts'
|
|
19
20
|
import { check } from '../check.ts'
|
|
21
|
+
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
20
22
|
|
|
21
23
|
export const githubVisibility = check({
|
|
22
24
|
id: 'SK007',
|
|
23
|
-
//
|
|
24
|
-
|
|
25
|
-
// unverifiable path above is a skip, so an offline machine never goes red
|
|
26
|
-
blocking: true,
|
|
25
|
+
// a gate block does not un-leak a repo: the red shows on the needs surface
|
|
26
|
+
blocking: false,
|
|
27
27
|
title: 'github visibility',
|
|
28
28
|
description: 'the github visibility of this repo matches the one it declares',
|
|
29
29
|
owner: 'meta/contracts.md',
|
|
30
|
-
ttl: '
|
|
31
|
-
cacheKey: (env) => env.config
|
|
30
|
+
ttl: 'forever',
|
|
31
|
+
cacheKey: (env) => declaredVisibility(env.config),
|
|
32
32
|
run: async (c) => {
|
|
33
|
-
const declared = c.config
|
|
33
|
+
const declared = declaredVisibility(c.config)
|
|
34
34
|
const skip = (why: string): void => void c.lines.push({ kind: 'skip', text: `github visibility unverified — ${why} (declared: ${declared})` })
|
|
35
35
|
const firstLine = firstGhLine
|
|
36
36
|
const repo = probeGitRoot(c.root)
|
|
@@ -8,12 +8,13 @@
|
|
|
8
8
|
// harness then inherits every tool. The brief itself must exist. No
|
|
9
9
|
// declaration = not a reviewer repo, and the rule passes silently.
|
|
10
10
|
import { existsSync, mkdirSync, readFileSync } from 'fs'
|
|
11
|
-
import {
|
|
12
|
-
import { dirname, isAbsolute, join } from 'path'
|
|
11
|
+
import { dirname, join } from 'path'
|
|
13
12
|
import type { RepoConfig } from '../repo-config.ts'
|
|
14
13
|
import { check, type FixResult, type CheckEnv } from '../check.ts'
|
|
15
14
|
import { LAYOUT } from '../core/layout.ts'
|
|
16
15
|
import { atomicWrite } from '../core/locked-write.ts'
|
|
16
|
+
import { configPath, fileStamp } from '../core/file-stamp.ts'
|
|
17
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
17
18
|
|
|
18
19
|
/** where a review lands when its caller named no output file:
|
|
19
20
|
* `YYYYMMDD-HHMMSS-<review-name>.md`, local time, gitignored */
|
|
@@ -48,8 +49,7 @@ export function reviewerAgentIssues(text: string, reviewer: NonNullable<RepoConf
|
|
|
48
49
|
/** the brief must EXIST: a reviewer whose mandated first action is reading a
|
|
49
50
|
* missing file flounders silently, which is the one thing this rule prevents */
|
|
50
51
|
export function briefMissing(root: string, brief: string): boolean {
|
|
51
|
-
|
|
52
|
-
return !existsSync(p)
|
|
52
|
+
return !existsSync(configPath(root, brief))
|
|
53
53
|
}
|
|
54
54
|
|
|
55
55
|
function canonicalAgent(reviewer: NonNullable<RepoConfig['reviewer']>): string {
|
|
@@ -83,12 +83,18 @@ That brief overrides any habit of reviewing a diff for correctness alone.
|
|
|
83
83
|
export const reviewerAgent = check({
|
|
84
84
|
id: 'SK008',
|
|
85
85
|
blocking: true,
|
|
86
|
-
title: 'reviewer
|
|
86
|
+
title: 'reviewer definition',
|
|
87
87
|
description: 'the declared reviewer agent has its brief, its definition and read only tools',
|
|
88
88
|
owner: 'identity/how-we-work.md',
|
|
89
|
-
// the
|
|
90
|
-
//
|
|
91
|
-
|
|
89
|
+
// the answer is the config, the definition file and the brief. The brief
|
|
90
|
+
// is a CONFIG path that may sit outside the repo, so both files enter the
|
|
91
|
+
// key by content, wherever they live
|
|
92
|
+
inputs: [CONFIG_FILE],
|
|
93
|
+
cacheKey: (env) => {
|
|
94
|
+
const r = env.config.reviewer
|
|
95
|
+
if (r == null) return 'no reviewer'
|
|
96
|
+
return `${r.name}|${(r.tools ?? REVIEWER_TOOLS).join(',')}|def=${fileStamp(join(env.root, reviewerAgentPath(r.name)))}|${r.brief}=${fileStamp(configPath(env.root, r.brief))}`
|
|
97
|
+
},
|
|
92
98
|
run: (c) => {
|
|
93
99
|
const r = c.config.reviewer
|
|
94
100
|
if (r == null) return c.ok('no reviewer agent declared (repo.config reviewer:) — skipped')
|
|
@@ -8,11 +8,16 @@
|
|
|
8
8
|
// the ban — a green line must never teach the wrong rule. The remedy for
|
|
9
9
|
// pushed history is the owner's decision, never an automatic rewrite; only
|
|
10
10
|
// an unpushed tip commit may be amended, and the fails say so.
|
|
11
|
+
import { createHash } from 'crypto'
|
|
11
12
|
import { readFileSync } from 'fs'
|
|
12
13
|
import { join } from 'path'
|
|
13
14
|
import { hasAiAttribution, hasDashPunctuation } from '../hooks/commit-message.ts'
|
|
14
15
|
import { nestedIn } from '../core/nesting.ts'
|
|
15
|
-
import {
|
|
16
|
+
import { gitBin } from '../core/git-bin.ts'
|
|
17
|
+
import { fileStamp } from '../core/file-stamp.ts'
|
|
18
|
+
import { CONFIG_FILE } from '../core/check-pass.ts'
|
|
19
|
+
import { check, type CheckEnv } from '../check.ts'
|
|
20
|
+
import { declaredVisibility } from '../core/repo-facts.ts'
|
|
16
21
|
|
|
17
22
|
// the documented silencer: .claude/settings.json attribution.commit/.pr both
|
|
18
23
|
// empty — the harness then never writes the trailer or the PR footer
|
|
@@ -25,20 +30,39 @@ const attributionSilenced = (root: string): boolean => {
|
|
|
25
30
|
}
|
|
26
31
|
}
|
|
27
32
|
|
|
33
|
+
/** what the answer depends on: the declared visibility, and for a public
|
|
34
|
+
* repo the commit HEAD names, every ref name and the silencer. So a commit,
|
|
35
|
+
* a fetch or a new tag asks again, and nothing else does */
|
|
36
|
+
export function historyKey(env: CheckEnv): string {
|
|
37
|
+
const declared = declaredVisibility(env.config)
|
|
38
|
+
const nested = nestedIn(env.root)
|
|
39
|
+
if (nested != null) return `nested in ${nested}`
|
|
40
|
+
if (declared !== 'public') return `declared ${declared}`
|
|
41
|
+
const git = (args: string[]): string => {
|
|
42
|
+
const p = Bun.spawnSync([gitBin(), ...args], { cwd: env.root, stdout: 'pipe', stderr: 'pipe' })
|
|
43
|
+
return p.exitCode === 0 ? p.stdout.toString() : `exit ${p.exitCode}`
|
|
44
|
+
}
|
|
45
|
+
const refs = git(['for-each-ref', '--format=%(refname) %(objectname)', 'refs/tags', 'refs/heads', 'refs/remotes'])
|
|
46
|
+
const head = git(['rev-parse', 'HEAD'])
|
|
47
|
+
const digest = createHash('sha256').update(head).update('\0').update(refs).digest('hex').slice(0, 16)
|
|
48
|
+
return `public|refs=${digest}|settings=${fileStamp(join(env.root, '.claude', 'settings.json'))}`
|
|
49
|
+
}
|
|
50
|
+
|
|
28
51
|
export const commitAiTells = check({
|
|
29
52
|
id: 'SK009',
|
|
30
53
|
blocking: true,
|
|
31
54
|
title: 'commit ai tells',
|
|
32
55
|
description: 'the history of a public repo carries no AI attribution and no dash punctuation',
|
|
33
56
|
owner: 'identity/how-we-commit.md',
|
|
34
|
-
// it scans the full `git log` and every ref name
|
|
35
|
-
//
|
|
36
|
-
|
|
57
|
+
// it scans the full `git log` and every ref name, which no tree id
|
|
58
|
+
// describes, so the refs themselves are the key
|
|
59
|
+
inputs: [CONFIG_FILE],
|
|
60
|
+
cacheKey: historyKey,
|
|
37
61
|
run: async (c) => {
|
|
38
62
|
const nested = nestedIn(c.root)
|
|
39
63
|
if (nested != null)
|
|
40
64
|
return void c.lines.push({ kind: 'skip', text: `commit history not scanned, nested in ${nested}: that history belongs to the parent repo` })
|
|
41
|
-
const declared = c.config
|
|
65
|
+
const declared = declaredVisibility(c.config)
|
|
42
66
|
if (declared !== 'public')
|
|
43
67
|
return c.ok(`declared ${declared} — the no-AI-attribution ban still applies from commit one (identity/how-we-commit.md); the full-history scan arms at visibility: 'public'`)
|
|
44
68
|
// %x1f separates hash from body, %x1e terminates the record — commit
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
import { isAbsolute, join } from 'path'
|
|
16
16
|
import { inferBin } from '../core/canonical-scripts.ts'
|
|
17
17
|
import { denyRules, RAW_RUNNERS } from '../core/one-way-in-policy.ts'
|
|
18
|
+
import { rootPublishesNpm } from '../core/repo-facts.ts'
|
|
18
19
|
import type { RepoConfig } from '../repo-config.ts'
|
|
19
20
|
import { check, type CheckCtx, type FixResult } from '../check.ts'
|
|
20
21
|
|
|
@@ -95,8 +96,8 @@ async function guardScriptMissing(d: Disk, command: string): Promise<string | nu
|
|
|
95
96
|
return null // a form this rule cannot resolve is not a form it may condemn
|
|
96
97
|
}
|
|
97
98
|
|
|
98
|
-
/** the guard the fixer writes: the cli path the scripts run when a
|
|
99
|
-
* beside it
|
|
99
|
+
/** the guard the fixer writes: the shipkit cli path the scripts run when a
|
|
100
|
+
* guard sits beside it, else the
|
|
100
101
|
* repo's own dependency, else the copy of shipkit doing the fixing, which is
|
|
101
102
|
* the one the launcher falls back to in a repo that has none */
|
|
102
103
|
export async function guardCommandFor(d: Disk): Promise<string> {
|
|
@@ -109,6 +110,11 @@ export async function guardCommandFor(d: Disk): Promise<string> {
|
|
|
109
110
|
return guardCommand(join(import.meta.dir, '..', 'cli.ts'))
|
|
110
111
|
}
|
|
111
112
|
|
|
113
|
+
/** a package published to npm is worked on by strangers: this file binds
|
|
114
|
+
* their agents, and a deny list with a hook would tie them to a toolkit they
|
|
115
|
+
* never installed */
|
|
116
|
+
const PUBLIC_SKIP = `the root unit publishes to npm: ${SETTINGS_PATH} belongs to every contributor, so it is never written`
|
|
117
|
+
|
|
112
118
|
export const oneWayInPolicy = check({
|
|
113
119
|
id: 'SK013',
|
|
114
120
|
blocking: true,
|
|
@@ -119,6 +125,7 @@ export const oneWayInPolicy = check({
|
|
|
119
125
|
// the declared guard script resolves. A guard at an absolute path lives
|
|
120
126
|
// outside the repo, so an answer that probed one is never stored
|
|
121
127
|
run: async (c) => {
|
|
128
|
+
if (rootPublishesNpm(c.config)) return c.ok(PUBLIC_SKIP)
|
|
122
129
|
const d = c
|
|
123
130
|
const want = denyRules(exemptSlots(c.config))
|
|
124
131
|
if (want.length === 0) return c.ok('every governed slot is exempt (repo.config oneWayIn/defaults) — nothing to deny')
|
|
@@ -165,6 +172,7 @@ export const oneWayInPolicy = check({
|
|
|
165
172
|
// a repo's own allow list and its own hooks, and clobbering those to fix a
|
|
166
173
|
// deny list would cost more than the finding it repairs
|
|
167
174
|
fix: async (c): Promise<FixResult> => {
|
|
175
|
+
if (rootPublishesNpm(c.config)) return { changed: false, summary: PUBLIC_SKIP }
|
|
168
176
|
const d = c
|
|
169
177
|
const want = denyRules(exemptSlots(c.config))
|
|
170
178
|
if (want.length === 0) return { changed: false, summary: 'every governed slot is exempt — nothing to write' }
|