diff --git a/docs/trajectories/typescript-bun-migration/RESUME.md b/docs/trajectories/typescript-bun-migration/RESUME.md index bcad73f199..095b998d84 100644 --- a/docs/trajectories/typescript-bun-migration/RESUME.md +++ b/docs/trajectories/typescript-bun-migration/RESUME.md @@ -1,12 +1,12 @@ # Trajectory — TypeScript / Bun migration **Status**: Closed-maintained bash-retirement phase (Lane B slice 21 merged — [#908](https://github.com/Lucent-Financial-Group/Zeta/pull/908); bash-retirement inventory guard landed — [#2764](https://github.com/Lucent-Financial-Group/Zeta/pull/2764); **Bucket B is empty**; retained non-Lean shell surface is the repo-wide setup/bootstrap/service-wrapper/installer/dev-cluster allowlist) -**Milestone**: 43 ported. All clusters complete: budget (14/18/19), peer-call (15/16/17), git (13/20), pr-preservation (21), cluster-inventory capture. Bucket B is empty as of 2026-04-30T08:07:32Z. The remaining non-Lean `.sh` inventory is guarded repo-wide by `tools/hygiene/check-bash-retirement-inventory.ts` and wired through package script `hygiene:check-bash-retirement-inventory` plus the `gate.yml` bash-retirement inventory lint job. +**Milestone**: 43 ported. All clusters complete: budget (14/18/19), peer-call (15/16/17), git (13/20), pr-preservation (21), cluster-inventory capture. Bucket B is empty as of 2026-04-30T08:07:32Z. The remaining non-Lean shell-family inventory is guarded repo-wide by `tools/hygiene/check-bash-retirement-inventory.ts` and wired through package script `hygiene:check-bash-retirement-inventory` plus the `gate.yml` bash-retirement inventory lint job. **Current blocker**: None. **Next concrete action**: Maintain the bash-retirement inventory guard and treat -any newly tracked non-Lean `.sh` outside the allowlist as drift. Do not revive -the old Cluster G/H/I or budget-cluster port queues. -**Last updated**: 2026-05-26 +any newly tracked non-Lean shell-family file outside the allowlist as drift. Do +not revive the old Cluster G/H/I or budget-cluster port queues. +**Last updated**: 2026-05-27T17:48Z ## Why this trajectory exists @@ -52,7 +52,8 @@ bun run hygiene:check-bash-retirement-inventory The expected retained surface is the explicit repo-wide allowlist: setup and bootstrap scripts, host-service wrappers, NixOS installer scripts, dev-cluster wrappers, launchd-bootstrap, and the Kiro loop wrapper. Any new -non-Lean `.sh` outside the allowlist is bash-retirement drift. +non-Lean shell-family file (`.sh`, `.bash`, `.zsh`, `.ksh`, or `.command`) +outside the allowlist is bash-retirement drift. ### Bucket A — Should stay Shell (21 files) diff --git a/tools/hygiene/check-bash-retirement-inventory.test.ts b/tools/hygiene/check-bash-retirement-inventory.test.ts index 1a9ff99ad0..412d8be506 100644 --- a/tools/hygiene/check-bash-retirement-inventory.test.ts +++ b/tools/hygiene/check-bash-retirement-inventory.test.ts @@ -1,4 +1,8 @@ import { describe, expect, test } from "bun:test"; +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; import { buildInventoryReport, @@ -9,6 +13,18 @@ import { trackedNonLeanShellFilesFromGit, } from "./check-bash-retirement-inventory"; +function runGit(args: readonly string[], cwd: string): void { + // Test helper uses repo-pinned git with explicit argv; no shell expansion. + // eslint-disable-next-line sonarjs/no-os-command-from-path + const result = spawnSync("git", args, { cwd, encoding: "utf8" }); + if (result.error) { + throw new Error(`failed to start git ${args.join(" ")}: ${result.error.message}`); + } + if (result.status !== 0) { + throw new Error(result.stderr.trim() || `git ${args.join(" ")} failed`); + } +} + function splitExpectedRetained(): readonly [string, readonly string[]] { const [missing, ...rest] = EXPECTED_RETAINED_SHELL; if (missing === undefined) throw new Error("expected retained shell allowlist must be non-empty"); @@ -145,6 +161,34 @@ describe("buildInventoryReport", () => { expect(report.drift.unexpected).toEqual([]); expect(report.drift.missingRetained).toEqual([]); }); + + test("enumerates tracked shell-family files while excluding Lean vendor scripts", () => { + const repo = mkdtempSync(join(tmpdir(), "zeta-bash-retirement-")); + try { + runGit(["init"], repo); + + mkdirSync(join(repo, "scripts"), { recursive: true }); + mkdirSync(join(repo, "tools", "lean4"), { recursive: true }); + writeFileSync(join(repo, "scripts", "a.sh"), "#!/usr/bin/env bash\n"); + writeFileSync(join(repo, "scripts", "b.bash"), "#!/usr/bin/env bash\n"); + writeFileSync(join(repo, "scripts", "c.zsh"), "#!/usr/bin/env zsh\n"); + writeFileSync(join(repo, "scripts", "d.ksh"), "#!/usr/bin/env ksh\n"); + writeFileSync(join(repo, "scripts", "e.command"), "#!/usr/bin/env bash\n"); + writeFileSync(join(repo, "tools", "lean4", "vendor.sh"), "#!/usr/bin/env bash\n"); + writeFileSync(join(repo, "README.md"), "not shell\n"); + runGit(["add", "."], repo); + + expect(trackedNonLeanShellFilesFromGit(repo)).toEqual([ + "scripts/a.sh", + "scripts/b.bash", + "scripts/c.zsh", + "scripts/d.ksh", + "scripts/e.command", + ]); + } finally { + rmSync(repo, { recursive: true, force: true }); + } + }); }); describe("renderReport", () => { diff --git a/tools/hygiene/check-bash-retirement-inventory.ts b/tools/hygiene/check-bash-retirement-inventory.ts index 06d78882fc..ee16cb0ccb 100644 --- a/tools/hygiene/check-bash-retirement-inventory.ts +++ b/tools/hygiene/check-bash-retirement-inventory.ts @@ -2,7 +2,7 @@ // check-bash-retirement-inventory.ts — verify the retained shell surface. // // The TypeScript/Bun migration is in bash-retirement mode: repo-owned scripts -// should not grow new `.sh` entrypoints outside the explicit repo-wide +// should not grow new shell-family entrypoints outside the explicit repo-wide // retained-shell allowlist. Retained shell exists only where the script runs // before Bun is available, bootstraps a host service environment, or belongs // to a low-level installer/dev-cluster surface that is still shell-native. @@ -66,6 +66,7 @@ export interface InventoryReport { const SPAWN_MAX_BUFFER = 64 * 1024 * 1024; export const RETAINED_SHELL_SCOPE = "repo-wide setup/bootstrap/service-wrapper/installer/dev-cluster allowlist"; +export const TRACKED_SHELL_FILE_GLOBS: readonly string[] = ["*.sh", "*.bash", "*.zsh", "*.ksh", "*.command"]; export const EXPECTED_RETAINED_SHELL: readonly string[] = [ ".gemini/service/install-lior-service.sh", @@ -166,9 +167,9 @@ function runGit(args: readonly string[], cwd?: string): string { return result.stdout; } -export function trackedNonLeanShellFilesFromGit(): readonly string[] { - const repoRoot = runGit(["rev-parse", "--show-toplevel"]).trim(); - const raw = runGit(["ls-files", "-z", "*.sh"], repoRoot); +export function trackedNonLeanShellFilesFromGit(cwd?: string): readonly string[] { + const repoRoot = runGit(["rev-parse", "--show-toplevel"], cwd).trim(); + const raw = runGit(["ls-files", "-z", ...TRACKED_SHELL_FILE_GLOBS], repoRoot); return raw .split("\0") .filter((file): file is string => file.length > 0) @@ -361,7 +362,7 @@ function usage(): string { " bun tools/hygiene/check-bash-retirement-inventory.ts --enforce", " bun tools/hygiene/check-bash-retirement-inventory.ts --json", "", - `Checks that non-Lean tracked .sh files match ${RETAINED_SHELL_SCOPE}.`, + `Checks that non-Lean tracked shell-family files match ${RETAINED_SHELL_SCOPE}.`, ].join("\n"); }