Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 18 additions & 2 deletions .ai/skills/hack-repo-verify/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,24 @@ For native setup use root `mise.toml`, the Bun pin in `package.json`, and
`bun install --frozen-lockfile`. Native gates are `bun run typecheck`, `bun run check`,
`bun run test`. Focused CLI checks: `bun run --cwd packages/cli typecheck` and
`bun run --cwd packages/cli check`; lint changed tests/scripts explicitly with
`bunx ultracite check <paths>`. Rust default gates are `bun run check:local` and
`bun run test:local`. Add `--all-features` to equivalent Cargo commands for feature
`bunx ultracite check <paths>`. These entry points first run
`bun run check:bun-version`, which fails when the running Bun differs from the
`packageManager` pin that CI installs. A Bun ahead of mise's shims on `PATH`
(for example mise's `latest` install) bypasses the checkout's `mise.toml`; use
`mise x bun@<pin> -- bun run <script>` instead. `HACK_ALLOW_BUN_VERSION_MISMATCH=1`
proceeds with a warning for a deliberate experiment; such a result is not CI evidence.
Raw `bun test <paths>` does not run the guard.

CI's "Config compiler" job also runs compiled-bundle CLI checks
(`scripts/check-config-compiler-cli.ts`, `scripts/check-native-*-plan-cli.ts`,
`scripts/check-native-config-import-cli.ts`) that `bun run test` does not. For
compiler, native planning or config-import changes, run them locally after
`bun run build` and `bun run build:config-compiler` with
`bun run check:native-plan-cli`. `tests/native-plan-cli-wiring.test.ts` fails when a
new `scripts/check-native-*-plan-cli.ts` is missing from that command or from CI.
CI also lints those scripts and runs their transport tests; lint changed ones explicitly.

Rust default gates are `bun run check:local` and `bun run test:local`. Add `--all-features` to equivalent Cargo commands for feature
changes; the toolchain Rust tasks already enable all features. Keep default coverage.

Before a full CLI suite, explicitly prepare the ignored compiler with
Expand Down
4 changes: 2 additions & 2 deletions .hack/tasks.json
Original file line number Diff line number Diff line change
Expand Up @@ -110,12 +110,12 @@
{
"path": "package.json",
"pointer": "/scripts/check",
"sha256": "ae7e24ee743b2aa230c6a492da63a0e7b6a0db925e691a24a7a8f1c97d3ee7fb"
"sha256": "c8dad6241a07d0de512741c0bc88b302f627b822b442de7d640490eaf239ce1f"
},
{
"path": "package.json",
"pointer": "/scripts/typecheck",
"sha256": "4209d96797d84dcd31f559acfde1395b9969d025ef6fc11f4684567a3009ef6f"
"sha256": "dce86c009756c167434b90e06ffa8ba3dbcfc01047abbae2a2803a4d5ecea01a"
}
],
"review": "HACK-1157"
Expand Down
8 changes: 7 additions & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,13 @@ or unresolved concern. Do not skip a required gate or weaken an assertion to get
unrelated installed version. Use `bun index.ts` for a source invocation.
- CLI typecheck/lint: `bun run --cwd packages/cli typecheck` and
`bun run --cwd packages/cli check`. Lint changed scripts/tests explicitly as well.
- Full gates: `bun run typecheck`, `bun run check`, `bun run test`.
- Full gates: `bun run typecheck`, `bun run check`, `bun run test`. They fail when
the running Bun differs from the `packageManager` pin CI uses; run the pinned Bun
(`mise x bun@<pin> -- bun run <script>`), or set
`HACK_ALLOW_BUN_VERSION_MISMATCH=1` only for a deliberate mismatch.
- Config compiler, native plan or config-import changes: after `bun run build` and
`bun run build:config-compiler`, run `bun run check:native-plan-cli`. It runs the
compiled-bundle CLI checks from CI's macOS "Config compiler" job.
- CLI interface changes: regenerate `docs/reference/cli.md` with
`bun run docs:cli-reference`.
- Consumer agent guidance changes: regenerate the affected repository examples and plugin
Expand Down
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@
"turbo:typecheck": "bunx turbo run typecheck",
"turbo:test": "bun run test:preflight && bunx --no-install turbo run test",
"turbo:check": "bunx turbo run check",
"check": "bun run privacy:check && bun run turbo:check",
"check": "bun run check:bun-version && bun run privacy:check && bun run turbo:check",
"check:bun-version": "bun scripts/check-bun-version.ts",
"check:native-plan-cli": "bun run check:bun-version && bun scripts/check-native-plan-clis.ts",
"prepare": "husky",
"release:prepare": "bunx semantic-release --no-ci",
"typecheck": "bun run turbo:typecheck",
"typecheck": "bun run check:bun-version && bun run turbo:typecheck",
"uninstall:local": "bun run --cwd packages/cli uninstall:local",
"test": "bun run turbo:test",
"test": "bun run check:bun-version && bun run turbo:test",
"test:preflight": "bun --no-env-file scripts/check-test-config-compiler.ts",
"test:models": "bun run scripts/check-tla-models.ts",
"test:e2e:local": "bun tests/e2e/run.ts",
Expand Down
6 changes: 3 additions & 3 deletions packages/cli/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,9 @@
"macos:release": "cd ../.. && ./scripts/build-macos-release.sh",
"macos:ghostty:setup": "cd ../.. && bun run scripts/macos-ghostty-setup.ts",
"macos:ghostty:bundle": "cd ../.. && bun run scripts/macos-ghostty-bundle.ts",
"typecheck": "bunx tsc -p tsconfig.json --noEmit",
"test": "cd ../.. && bun run test:preflight && bun test",
"check": "cd ../.. && bunx ultracite check src packages/cli/index.ts index.ts scripts/build-release.ts scripts/build-node-runtime-image.ts scripts/install-cli.ts scripts/install-status.ts scripts/uninstall-cli.ts scripts/macos-ghostty-bundle.ts scripts/macos-ghostty-setup.ts",
"typecheck": "bun ../../scripts/check-bun-version.ts && bunx tsc -p tsconfig.json --noEmit",
"test": "cd ../.. && bun run check:bun-version && bun run test:preflight && bun test",
"check": "cd ../.. && bun run check:bun-version && bunx ultracite check src packages/cli/index.ts index.ts scripts/build-release.ts scripts/build-node-runtime-image.ts scripts/install-cli.ts scripts/install-status.ts scripts/uninstall-cli.ts scripts/macos-ghostty-bundle.ts scripts/macos-ghostty-setup.ts",
"lint": "bun run check"
}
}
105 changes: 105 additions & 0 deletions scripts/check-bun-version.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
#!/usr/bin/env bun
/**
* Fails the local `test`, `check` and `typecheck` entry points when the running
* Bun differs from the `packageManager` pin in the root `package.json`.
*
* CI installs exactly that pin. A contributor whose PATH resolves another Bun
* (for example mise's `latest` install ahead of the checkout's `mise.toml`) would
* otherwise get results CI does not reproduce: a Bun 1.4.3 test pass hid a
* Bun 1.4.2 macOS SIGSTOP difference that failed in CI (HACK-1290).
*
* `HACK_ALLOW_BUN_VERSION_MISMATCH=1` proceeds deliberately, with a warning on
* stderr. Any other value is ignored. The check reads only the version strings.
*/
import { resolve } from "node:path";
import { isRecord } from "../src/lib/guards.ts";

/** The opt-in for a deliberate mismatch, such as a Bun upgrade experiment. */
export const BUN_VERSION_OVERRIDE_ENV = "HACK_ALLOW_BUN_VERSION_MISMATCH";

const PIN_PATTERN = /^bun@(\d+\.\d+\.\d+)$/;

export type BunVersionVerdict =
| { readonly kind: "match"; readonly pinned: string }
| {
readonly kind: "overridden";
readonly pinned: string;
readonly message: string;
}
| {
readonly kind: "mismatch";
readonly pinned: string;
readonly message: string;
}
| { readonly kind: "invalid-pin"; readonly message: string };

/** Pure comparison of the running Bun against the `packageManager` value. */
export function checkBunVersion(opts: {
readonly running: string;
readonly packageManager: unknown;
readonly override: string | undefined;
}): BunVersionVerdict {
const match =
typeof opts.packageManager === "string"
? PIN_PATTERN.exec(opts.packageManager)
: null;
const pinned = match?.[1];
if (!pinned) {
return {
kind: "invalid-pin",
message:
'package.json "packageManager" must be "bun@<major>.<minor>.<patch>".',
};
}
if (opts.running === pinned) {
return { kind: "match", pinned };
}
const detail =
`Bun ${opts.running} is running, but package.json pins bun@${pinned} ` +
"(packageManager), which CI uses.";
if (opts.override === "1") {
return {
kind: "overridden",
pinned,
message: `Warning: ${detail} Continuing because ${BUN_VERSION_OVERRIDE_ENV}=1; results may differ from CI.`,
};
}
return {
kind: "mismatch",
pinned,
message:
`${detail} Run the pinned Bun, for example ` +
`\`mise x bun@${pinned} -- bun run <script>\`, or put mise shims ahead of ` +
"other Bun installs so the checkout's mise.toml applies. " +
`Set ${BUN_VERSION_OVERRIDE_ENV}=1 to run a different Bun deliberately.`,
};
}

async function main(): Promise<void> {
const root = resolve(import.meta.dir, "..");
let packageManager: unknown;
try {
const manifest: unknown = await Bun.file(
resolve(root, "package.json")
).json();
packageManager = isRecord(manifest) ? manifest.packageManager : undefined;
} catch {
packageManager = undefined;
}
const verdict = checkBunVersion({
running: Bun.version,
packageManager,
override: process.env[BUN_VERSION_OVERRIDE_ENV],
});
if (verdict.kind === "match") {
return;
}
process.stderr.write(`${verdict.message}\n`);
if (verdict.kind !== "overridden") {
process.exitCode = 1;
}
}

if (import.meta.main) {
await main();
}
70 changes: 70 additions & 0 deletions scripts/check-native-plan-clis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
#!/usr/bin/env bun
/**
* Local entry point for the compiled-bundle CLI checks that CI runs in the
* "Config compiler" job (HACK-1294). Each script copies `dist/hack` and
* `dist/hack-config-compiler` into an isolated temporary bundle with a private
* HOME, so the current branch must be built first:
*
* bun run build && bun run build:config-compiler && bun run check:native-plan-cli
*
* The scripts run one at a time, in the CI order, and the first failure stops
* the run. `tests/native-plan-cli-wiring.test.ts` fails when a
* `scripts/check-native-*-plan-cli.ts` is missing from this list or from CI.
*/
import { stat } from "node:fs/promises";
import { resolve } from "node:path";

/** Repository-relative scripts, in the order CI runs them. */
export const NATIVE_PLAN_CLI_CHECKS = [
"scripts/check-config-compiler-cli.ts",
"scripts/check-native-env-plan-cli.ts",
"scripts/check-native-host-plan-cli.ts",
"scripts/check-native-routing-plan-cli.ts",
"scripts/check-native-endpoint-plan-cli.ts",
"scripts/check-native-process-plan-cli.ts",
"scripts/check-native-acquisition-plan-cli.ts",
"scripts/check-native-config-import-cli.ts",
] as const;

const PREREQUISITES = ["dist/hack", "dist/hack-config-compiler"] as const;

async function isFile(path: string): Promise<boolean> {
try {
return (await stat(path)).isFile();
} catch {
return false;
}
}

async function main(): Promise<void> {
const root = resolve(import.meta.dir, "..");
for (const prerequisite of PREREQUISITES) {
if (!(await isFile(resolve(root, prerequisite)))) {
process.stderr.write(
`${prerequisite} is missing. Build the current branch first: ` +
"bun run build && bun run build:config-compiler\n"
);
process.exitCode = 1;
return;
}
}
for (const script of NATIVE_PLAN_CLI_CHECKS) {
process.stdout.write(`check:native-plan-cli: ${script}\n`);
const child = Bun.spawn([process.execPath, script], {
cwd: root,
stdio: ["ignore", "inherit", "inherit"],
});
const exit = await child.exited;
if (exit !== 0) {
process.stderr.write(
`check:native-plan-cli: ${script} failed (exit ${exit}).\n`
);
process.exitCode = exit > 0 ? exit : 1;
return;
}
}
}

if (import.meta.main) {
await main();
}
Loading
Loading