diff --git a/.github/workflows/gate.yml b/.github/workflows/gate.yml index 0d40cead94..9b9989e5bb 100644 --- a/.github/workflows/gate.yml +++ b/.github/workflows/gate.yml @@ -569,6 +569,64 @@ jobs: printf ' %s\n' "${files[@]}" shellcheck --format=gcc --severity=style "${files[@]}" + lint-bash-retirement-inventory: + # Bash-retirement guard for the TypeScript/Bun migration trajectory: + # Zeta-owned post-install repo scripts should not grow new `.sh` + # entrypoints after Bucket B reached zero. The retained shell surface is + # setup/bootstrap, launchd-bootstrap, and the Kiro loop wrapper only. + name: lint (bash retirement inventory) + timeout-minutes: 5 + runs-on: ubuntu-24.04 + + steps: + - name: Checkout + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Cache install.sh outputs (mise runtimes + dotnet tools + verifier jars) + # Comprehensive cache — see lint-shell job above for the + # rationale (maintainer dev-CI parity input + + # transient 502 prevention). Same cache key shape so all + # lint jobs share the cache when toolchain unchanged. + uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5 + with: + path: | + ~/.local/bin/mise + ~/.local/share/mise + ~/.cache/mise + ~/.dotnet/tools + ~/.elan + ~/.config/zeta + key: install-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('.mise.toml', 'tools/setup/**', 'global.json') }} + + - name: Install toolchain via three-way-parity script (GOVERNANCE §24) + # Provides bun via mise's pin in .mise.toml. The inventory guard is + # intentionally separate from shellcheck: shellcheck validates the + # retained setup scripts, while this job fails any new non-allowlisted + # tracked `.sh` surface before it reaches main. + # See lint-shell job above for retry rationale. + run: | + set -euo pipefail + for attempt in 1 2 3 4 5; do + if ./tools/setup/install.sh; then exit 0; fi + [ "$attempt" = "5" ] && { echo "install.sh failed after 5 attempts"; exit 1; } + # Backoff: 10s, 30s, 60s, 120s — covers transient CDN + # blips from short-burst-and-recover up to multi-minute + # outages. The retry budget is five attempts because prior + # bun CDN 502s exhausted mise-internal retries plus a + # three-attempt wrapper. + case "$attempt" in + 1) backoff=10 ;; + 2) backoff=30 ;; + 3) backoff=60 ;; + 4) backoff=120 ;; + esac + echo "install.sh attempt $attempt failed; retrying in ${backoff}s..." >&2 + sleep "$backoff" + done + + - name: Run bash-retirement inventory guard + run: bun run hygiene:check-bash-retirement-inventory + lint-workflows: # actionlint catches .github/workflows/*.yml bugs: unknown # context refs, invalid runner labels, shellcheck-style warnings diff --git a/docs/trajectories/typescript-bun-migration/RESUME.md b/docs/trajectories/typescript-bun-migration/RESUME.md index 4427676ba4..cfa167aa77 100644 --- a/docs/trajectories/typescript-bun-migration/RESUME.md +++ b/docs/trajectories/typescript-bun-migration/RESUME.md @@ -1,12 +1,13 @@ # Trajectory — TypeScript / Bun migration -**Status**: Soak + bash-retirement phase (Lane B slice 21 merged — [#908](https://github.com/Lucent-Financial-Group/Zeta/pull/908); **Bucket B is empty**; retained non-Lean bash surface is setup/bootstrap only) -**Milestone**: 42 ported. All clusters complete: budget (14/18/19), peer-call (15/16/17), git (13/20), pr-preservation (21). Bucket B is empty as of 2026-04-30T08:07:32Z. The remaining non-Lean `.sh` inventory is guarded by `tools/hygiene/check-bash-retirement-inventory.ts`. +**Status**: Soak + 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 bash surface is setup/bootstrap, launchd-bootstrap, and the Kiro loop wrapper only) +**Milestone**: 42 ported. All clusters complete: budget (14/18/19), peer-call (15/16/17), git (13/20), pr-preservation (21). Bucket B is empty as of 2026-04-30T08:07:32Z. The remaining non-Lean `.sh` inventory is guarded 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**: Land the bash-retirement inventory check, then wire it -into the appropriate hygiene/CI surface after one clean soak pass. Do not revive -the old Cluster G/H/I or budget-cluster port queues. -**Last updated**: 2026-05-12 +**Next concrete action**: Shepherd the bash-retirement inventory wire-in PR +through review and CI; after merge, decide whether the bash-retirement phase can +move from soak to closed-maintained. Do not revive the old Cluster G/H/I or +budget-cluster port queues. +**Last updated**: 2026-05-26 ## Why this trajectory exists @@ -32,6 +33,7 @@ Per the maintainer-channel correction via the multi-AI review surface (2026-04-2 | [#882](https://github.com/Lucent-Financial-Group/Zeta/pull/882) | 2026-04-30 (commit `02266a7`) | `tools/hygiene/validate-agencysignature-pr-body.{sh→ts}`, `tools/hygiene/audit-agencysignature-main-tip.{sh→ts}`, `tools/hygiene/capture-tick-snapshot.{sh→ts}` | Merged | | [#883](https://github.com/Lucent-Financial-Group/Zeta/pull/883) | 2026-04-30 (commit `271bc38`) | `tools/hygiene/counterweight-audit.{sh→ts}`, `tools/hygiene/append-tick-history-row.{sh→ts}` | Merged | | [#884](https://github.com/Lucent-Financial-Group/Zeta/pull/884) | 2026-04-30 (commit `9237756`) | `tools/skill-catalog/backfill_dv2_frontmatter.{sh→ts}`, `tools/audit-packages.{sh→ts}` | Merged | +| [#2764](https://github.com/Lucent-Financial-Group/Zeta/pull/2764) | 2026-05-12 (commit `b563ba0`) | `tools/hygiene/check-bash-retirement-inventory.ts`, `tools/hygiene/check-bash-retirement-inventory.test.ts` | Merged | ## Inventory — Python (tools/, Zeta-authored) @@ -39,22 +41,30 @@ Per the maintainer-channel correction via the multi-AI review surface (2026-04-2 After PR #849, Zeta has zero Python files in `tools/` (Zeta-authored — the 22 `.py` files under `tools/lean4/.lake/packages/mathlib/scripts/` are mathlib upstream, not in scope). Python→TS in `tools/` is **100% complete**. -## Inventory — Bash (tools/, Zeta-authored, 13 retained files) +## Inventory — Bash (tools/, Zeta-authored, 15 retained files) Current count is repo-derived and guarded by: ```bash bun tools/hygiene/check-bash-retirement-inventory.ts --enforce +bun run hygiene:check-bash-retirement-inventory ``` -The expected retained surface is setup/bootstrap only. Any new non-Lean `.sh` -outside the allowlist is bash-retirement drift. +The expected retained surface is setup/bootstrap, launchd-bootstrap, and the +Kiro loop wrapper only. Any +new non-Lean `.sh` outside the allowlist is bash-retirement drift. -### Bucket A — Should stay Bash (13 files) +### Bucket A — Should stay Bash (15 files) -These run **before** Bun is installed (post-install scripts can use Bun; pre-install scripts cannot). Per Otto-235 4-shell portability target (macOS bash 3.2 / Ubuntu / git-bash / WSL), these are the bootstrap layer. +These either run **before** Bun is installed (post-install scripts can use Bun; +pre-install scripts cannot) or bootstrap macOS launchd into the pinned Bun +environment before handing off to TypeScript. Per Otto-235 4-shell portability +target (macOS bash 3.2 / Ubuntu / git-bash / WSL), these are the bootstrap +layer. ```text +tools/kiro/kiro-loop-wrapper.sh +tools/kiro/launchd/install.sh tools/setup/install.sh tools/setup/doctor.sh tools/setup/linux.sh @@ -70,7 +80,9 @@ tools/setup/common/sync-upstreams.sh tools/setup/common/verifiers.sh ``` -Rationale: TS/Bun is itself one of the things `install.sh` installs. These scripts cannot depend on Bun. +Rationale: TS/Bun is itself one of the things `install.sh` installs, and +launchd needs a small shell bootstrap to establish PATH/state before `exec` +hands control to Bun. ### Bucket B — Should become TypeScript (0 files remaining — empty as of 2026-04-30T08:07:32Z) @@ -100,7 +112,7 @@ tools/hygiene/snapshot-github-settings.ts # was .sh ### Bucket D — Ported, bash retained (0 tracked files; historical list) -The TS ports landed in #866 + #868 + #870 + #872 + #874 + #876 + #878 + #880 + #882 + #883 + #884 + #885 + #892 + #894 + #896 + #898 + #900 + #901 + #902. The bash originals listed below are now historical references, not tracked live files; the bash-retirement inventory check fails if any equivalent post-install `.sh` surface reappears outside setup/bootstrap. +The TS ports landed in #866 + #868 + #870 + #872 + #874 + #876 + #878 + #880 + #882 + #883 + #884 + #885 + #892 + #894 + #896 + #898 + #900 + #901 + #902. The bash originals listed below are now historical references, not tracked live files; the bash-retirement inventory check fails if any equivalent post-install `.sh` surface reappears outside setup/bootstrap, launchd-bootstrap, and the Kiro loop wrapper. **Removed 2026-05-03 (CI-workflow .sh→.ts conversion completed):** the 5 files listed in #1376's risk-stratification (audit-memory-index-duplicates, diff --git a/package.json b/package.json index ba55a49cad..ea3bbb1916 100644 --- a/package.json +++ b/package.json @@ -15,6 +15,7 @@ "test:typescript": "bun test", "hygiene:sort-tick-history": "bun ./tools/hygiene/sort-tick-history-canonical.ts", "hygiene:fix-markdown": "bun ./tools/hygiene/fix-markdown-md032-md026.ts", + "hygiene:check-bash-retirement-inventory": "bun ./tools/hygiene/check-bash-retirement-inventory.ts --enforce", "shadow": "bun tools/shadow/zeta-shadow.ts" }, "bin": { diff --git a/tools/hygiene/check-bash-retirement-inventory.test.ts b/tools/hygiene/check-bash-retirement-inventory.test.ts index 05d3d66ab1..43829930d6 100644 --- a/tools/hygiene/check-bash-retirement-inventory.test.ts +++ b/tools/hygiene/check-bash-retirement-inventory.test.ts @@ -5,6 +5,7 @@ import { EXPECTED_RETAINED_BASH, hasDrift, renderReport, + RETAINED_BASH_SCOPE, } from "./check-bash-retirement-inventory"; function splitExpectedRetained(): readonly [string, readonly string[]] { @@ -14,7 +15,7 @@ function splitExpectedRetained(): readonly [string, readonly string[]] { } describe("buildInventoryReport", () => { - test("accepts the retained setup/bootstrap allowlist", () => { + test("accepts the retained bash allowlist", () => { const report = buildInventoryReport(EXPECTED_RETAINED_BASH); expect(hasDrift(report)).toBe(false); @@ -31,7 +32,7 @@ describe("buildInventoryReport", () => { expect(report.drift.missingRetained).toEqual([]); }); - test("flags missing retained setup scripts", () => { + test("flags missing retained bash scripts", () => { const [missing, rest] = splitExpectedRetained(); const report = buildInventoryReport(rest); @@ -45,7 +46,9 @@ describe("renderReport", () => { test("renders an OK summary for a matching inventory", () => { const report = buildInventoryReport(EXPECTED_RETAINED_BASH); - expect(renderReport(report)).toContain("OK: retained non-Lean bash surface matches setup/bootstrap allowlist."); + expect(renderReport(report)).toContain( + `OK: retained non-Lean bash surface matches ${RETAINED_BASH_SCOPE} allowlist.`, + ); }); test("renders drift sections", () => { @@ -55,7 +58,7 @@ describe("renderReport", () => { expect(rendered).toContain("## Unexpected non-Lean bash files"); expect(rendered).toContain("tools/hygiene/new-post-install-wrapper.sh"); - expect(rendered).toContain("## Missing retained setup/bootstrap files"); + expect(rendered).toContain(`## Missing retained ${RETAINED_BASH_SCOPE} files`); expect(rendered).toContain(missing); }); }); diff --git a/tools/hygiene/check-bash-retirement-inventory.ts b/tools/hygiene/check-bash-retirement-inventory.ts index 5a03fa6458..1bd1e39c47 100644 --- a/tools/hygiene/check-bash-retirement-inventory.ts +++ b/tools/hygiene/check-bash-retirement-inventory.ts @@ -3,7 +3,9 @@ // // The TypeScript/Bun migration is in bash-retirement mode: repo tools should // not grow new post-install `.sh` entrypoints. The only non-Lean shell scripts -// still allowed are setup/bootstrap scripts that run before Bun is available. +// still allowed are setup/bootstrap scripts that run before Bun is available, +// launchd bootstrap scripts that establish the pinned Bun environment, and +// the Kiro loop wrapper that is itself launched by launchd. // // Usage: // bun tools/hygiene/check-bash-retirement-inventory.ts @@ -33,8 +35,11 @@ export interface InventoryReport { } const SPAWN_MAX_BUFFER = 64 * 1024 * 1024; +export const RETAINED_BASH_SCOPE = "setup/bootstrap/launchd-bootstrap/Kiro-wrapper"; export const EXPECTED_RETAINED_BASH: readonly string[] = [ + "tools/kiro/kiro-loop-wrapper.sh", + "tools/kiro/launchd/install.sh", "tools/setup/common/curl-fetch.sh", "tools/setup/common/dotnet-tools.sh", "tools/setup/common/elan.sh", @@ -129,7 +134,7 @@ export function renderReport(report: InventoryReport): string { lines.push(`missing_retained: ${String(report.drift.missingRetained.length)}`); lines.push(""); if (!hasDrift(report)) { - lines.push("OK: retained non-Lean bash surface matches setup/bootstrap allowlist."); + lines.push(`OK: retained non-Lean bash surface matches ${RETAINED_BASH_SCOPE} allowlist.`); return `${lines.join("\n")}\n`; } if (report.drift.unexpected.length > 0) { @@ -139,7 +144,7 @@ export function renderReport(report: InventoryReport): string { lines.push(""); } if (report.drift.missingRetained.length > 0) { - lines.push("## Missing retained setup/bootstrap files"); + lines.push(`## Missing retained ${RETAINED_BASH_SCOPE} files`); lines.push(""); for (const file of report.drift.missingRetained) lines.push(`- ${file}`); lines.push(""); @@ -154,7 +159,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 are limited to setup/bootstrap scripts.", + `Checks that non-Lean tracked .sh files are limited to ${RETAINED_BASH_SCOPE} scripts.`, ].join("\n"); }