diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml index 311ca6623..e55ad31a2 100644 --- a/.github/actionlint.yaml +++ b/.github/actionlint.yaml @@ -4,3 +4,7 @@ self-hosted-runner: - evalops-internal - evalops-maestro-internal-rbe - evalops-maestro-rbe + - blacksmith-4vcpu-ubuntu-2404 + - blacksmith-4vcpu-ubuntu-2404-arm + - blacksmith-6vcpu-macos-15 + - blacksmith-8vcpu-ubuntu-2404 diff --git a/CHANGELOG.md b/CHANGELOG.md index dbc491327..6d7bb371e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -21,6 +21,42 @@ versioning when releases are cut. and keep scheduled public runs inert so public publishing stays downstream of the internal source-of-truth release. +## [0.10.54] - 2026-07-20 + +### Added + +- Add Grok-inspired turn status row (#2933). +- Move agents command to Rust (#2924). +- Adopt Grok-inspired session chrome (#2926). +- Migrate modes command to Rust (#2921). + +### Changed + +- Fix eval Guardian failure and unblock 0.10.53 (#2935). +- Move OpenAI CLI authentication to Rust (#2934). +- Move shared-memory CLI fully to Rust (#2932). +- Migrate Anthropic CLI stub to Rust (#2930). +- Delete removed TypeScript exec runtime stub (#2931). +- Move painter CLI fully to Rust (#2927). +- Finish portable session transfer in Rust (#2929). +- Move skill CLI and package runtime to Rust (#2919). +- [maestro] Move update command to Rust and delete TypeScript handler (#2918). +- [maestro] Delete legacy TypeScript hosted runner command (#2917). +- [maestro] Delete migrated TypeScript utility commands (#2916). +- [maestro] Move hosted runner lifecycle to Rust (#2914). + +### Fixed + +- Package stripped Rust TUI binaries for every supported npm platform and build + them on Blacksmith runners before publishing. +- Make packed and registry install smokes launch a native Rust command so a + missing `vendor/maestro-tui` payload blocks publication. +- Hand off utilities before replay setup (#2925). +- Align native utility global flags (#2923). +- Consume named worktree before native utilities (#2922). +- Enforce native skill package validation (#2920). +- Accept public runner failover variable (#2915). + ## [0.10.53] - 2026-07-20 ### Added diff --git a/openapi.json b/openapi.json index 323b783bc..6beb91c1e 100644 --- a/openapi.json +++ b/openapi.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Maestro Web API", - "version": "0.10.53", + "version": "0.10.54", "description": "Auto-generated from src/web-server.ts routes. Components seeded from runtime schemas." }, "servers": [ diff --git a/package.json b/package.json index 31b95d5e4..34ee9e442 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@evalops/maestro", "description": "Maestro by EvalOps - Deterministic coding agent with TUI/CLI and Web UI for AI-assisted development", - "version": "0.10.53", + "version": "0.10.54", "private": false, "type": "module", "workspaces": [ diff --git a/packages/ai/package.json b/packages/ai/package.json index e65c88695..dd6cda27c 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/ai", - "version": "0.10.53", + "version": "0.10.54", "description": "Shared Maestro AI SDK (models, transport, and agent event stream facade).", "type": "module", "main": "./dist/packages/ai/src/index.js", @@ -131,7 +131,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@evalops/contracts": "^0.10.53", + "@evalops/contracts": "^0.10.54", "@google/genai": "^1.29.1", "@sinclair/typebox": "^0.34.0", "nats": "^2.29.3" diff --git a/packages/consumer-sdk/package.json b/packages/consumer-sdk/package.json index 6a94d6ef5..31ab3da8b 100644 --- a/packages/consumer-sdk/package.json +++ b/packages/consumer-sdk/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/consumer", - "version": "0.10.53", + "version": "0.10.54", "description": "Unified EvalOps consumer SDK for Maestro service integrations", "type": "module", "main": "./dist/index.js", @@ -28,7 +28,7 @@ "directory": "packages/consumer-sdk" }, "dependencies": { - "@evalops/contracts": "^0.10.53" + "@evalops/contracts": "^0.10.54" }, "devDependencies": { "@types/node": "^24.10.7", diff --git a/packages/contracts/package.json b/packages/contracts/package.json index d5153577f..38c7e7ad1 100644 --- a/packages/contracts/package.json +++ b/packages/contracts/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/contracts", - "version": "0.10.53", + "version": "0.10.54", "description": "Shared Maestro contracts for frontend/backend integration", "type": "module", "main": "./dist/index.js", diff --git a/packages/core/package.json b/packages/core/package.json index c06ab0158..07e48abc0 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/maestro-core", - "version": "0.10.53", + "version": "0.10.54", "description": "Maestro agent loop, transport, types, and sandbox primitives — the engine behind all Maestro interfaces", "type": "module", "main": "./dist/packages/core/src/index.js", diff --git a/packages/desktop/package.json b/packages/desktop/package.json index f1ce79e26..8d764d315 100644 --- a/packages/desktop/package.json +++ b/packages/desktop/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/maestro-desktop", - "version": "0.10.53", + "version": "0.10.54", "description": "Maestro Desktop - Beautiful Electron app for AI-assisted development", "type": "module", "main": "dist-electron/main/index.js", @@ -37,7 +37,7 @@ "directory": "packages/desktop" }, "dependencies": { - "@evalops/contracts": "^0.10.53", + "@evalops/contracts": "^0.10.54", "electron-store": "^10.0.1", "electron-updater": "^6.3.9" }, diff --git a/packages/github-agent/package.json b/packages/github-agent/package.json index 97de139fd..47f681d46 100644 --- a/packages/github-agent/package.json +++ b/packages/github-agent/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/github-agent", - "version": "0.10.53", + "version": "0.10.54", "description": "Autonomous GitHub agent - Maestro building Maestro", "type": "module", "bin": { @@ -36,8 +36,8 @@ "node": ">=20.0.0" }, "dependencies": { - "@evalops/ai": "^0.10.53", - "@evalops/contracts": "^0.10.53", + "@evalops/ai": "^0.10.54", + "@evalops/contracts": "^0.10.54", "@octokit/rest": "^21.0.0", "@octokit/webhooks": "^13.0.0", "@sinclair/typebox": "^0.34.0", diff --git a/packages/governance-mcp-server/package.json b/packages/governance-mcp-server/package.json index 36d4895fd..1aa03edce 100644 --- a/packages/governance-mcp-server/package.json +++ b/packages/governance-mcp-server/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/governance-mcp-server", - "version": "0.10.53", + "version": "0.10.54", "description": "MCP server exposing Platform governance proxy tools to any MCP-compatible agent.", "type": "module", "main": "./dist/index.js", @@ -43,7 +43,7 @@ "node": ">=20.0.0" }, "dependencies": { - "@evalops/governance": "^0.10.53", + "@evalops/governance": "^0.10.54", "@modelcontextprotocol/sdk": "^1.25.2", "zod": "^4.3.5" }, diff --git a/packages/governance/package.json b/packages/governance/package.json index 7c1298be1..0488e6b6c 100644 --- a/packages/governance/package.json +++ b/packages/governance/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/governance", - "version": "0.10.53", + "version": "0.10.54", "description": "Thin Platform governance proxy for MCP-compatible agents.", "type": "module", "main": "./dist/index.js", diff --git a/packages/memory/package.json b/packages/memory/package.json index c6b7f1c43..8641b4dc6 100644 --- a/packages/memory/package.json +++ b/packages/memory/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/memory", - "version": "0.10.53", + "version": "0.10.54", "private": true, "type": "module", "sideEffects": false, diff --git a/packages/slack-agent-ui/package.json b/packages/slack-agent-ui/package.json index d9995837a..cf947da53 100644 --- a/packages/slack-agent-ui/package.json +++ b/packages/slack-agent-ui/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/slack-agent-ui", - "version": "0.10.53", + "version": "0.10.54", "description": "Web UI for Slack Agent - Dashboard gallery, connector management, OAuth flows", "type": "module", "private": true, diff --git a/packages/slack-agent/package.json b/packages/slack-agent/package.json index 97a42202f..ea4c79a90 100644 --- a/packages/slack-agent/package.json +++ b/packages/slack-agent/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/slack-agent", - "version": "0.10.53", + "version": "0.10.54", "description": "Slack teammate agent with sandbox support for Maestro", "type": "module", "bin": { @@ -30,8 +30,8 @@ }, "dependencies": { "@daytonaio/sdk": "^0.139.0", - "@evalops/ai": "^0.10.53", - "@evalops/contracts": "^0.10.53", + "@evalops/ai": "^0.10.54", + "@evalops/contracts": "^0.10.54", "@sinclair/typebox": "^0.34.0", "@slack/socket-mode": "^2.0.0", "@slack/web-api": "^7.0.0", diff --git a/packages/tui-rs/src/app.rs b/packages/tui-rs/src/app.rs index 5647fdc04..997acf7e6 100644 --- a/packages/tui-rs/src/app.rs +++ b/packages/tui-rs/src/app.rs @@ -856,8 +856,22 @@ Always use tools when they would be helpful. Be concise and direct in your respo /// /// Exit code for the process (0 = success, non-zero = error). pub async fn run(mut self) -> Result { - // Load workspace files for @ mentions in the input. - self.load_workspace_files(); + // Paint the chrome immediately. Workspace file indexing used to run + // *before* the first frame and blocked on `rg --files --follow` of the + // entire cwd (often `$HOME`), so typing `maestro` looked hung/broken. + self.render()?; + + // Index @-mention files with a bounded, killable scan (see workspace.rs). + // Kick it off on a background thread so agent spawn is not gated on it. + let (workspace_tx, workspace_rx) = std::sync::mpsc::channel(); + std::thread::Builder::new() + .name("maestro-workspace-scan".into()) + .spawn(move || { + let cwd = std::env::current_dir().unwrap_or_default(); + let files = get_workspace_files(&cwd, 10_000); + let _ = workspace_tx.send(files); + }) + .ok(); // Spawn the agent (async operation). // This creates the channels and starts the agent task. @@ -869,7 +883,22 @@ Always use tools when they would be helpful. Be concise and direct in your respo } // Main event loop - runs until should_quit is set to true. + let mut workspace_rx = Some(workspace_rx); loop { + // Apply workspace scan results as soon as they arrive (non-blocking). + if let Some(rx) = workspace_rx.as_ref() { + match rx.try_recv() { + Ok(files) => { + self.file_search.set_files(files); + workspace_rx = None; + } + Err(std::sync::mpsc::TryRecvError::Disconnected) => { + workspace_rx = None; + } + Err(std::sync::mpsc::TryRecvError::Empty) => {} + } + } + // Render the UI to the terminal. // This is a sync operation that writes to stdout. self.render()?; diff --git a/packages/tui-rs/src/files/workspace.rs b/packages/tui-rs/src/files/workspace.rs index 828a11741..658db87a6 100644 --- a/packages/tui-rs/src/files/workspace.rs +++ b/packages/tui-rs/src/files/workspace.rs @@ -1,10 +1,21 @@ //! Workspace file discovery //! //! Lists files in the workspace using ripgrep or find. +//! +//! Scans must never block the interactive TUI for long: callers expect +//! `get_workspace_files` to return quickly even from a large home directory. +//! External tools are streamed and killed once `max_files` is reached, and a +//! hard wall-clock timeout aborts runaway walks (e.g. `rg --follow` into huge trees). use std::collections::HashSet; +use std::io::{BufRead, BufReader}; use std::path::{Path, PathBuf}; -use std::process::Command; +use std::process::{Command, Stdio}; +use std::time::{Duration, Instant}; + +/// Wall-clock budget for an external workspace scan (rg / find). +/// Interactive startup must stay responsive even when cwd is `$HOME`. +const WORKSPACE_SCAN_TIMEOUT: Duration = Duration::from_secs(2); /// A file in the workspace #[derive(Debug, Clone)] @@ -106,32 +117,76 @@ pub fn get_workspace_files(root: &Path, max_files: usize) -> Vec manual_traverse(root, max_files) } -/// Try to list files using ripgrep +/// Detach a child from the controlling TTY (grok-build / xai-tty-utils pattern). +/// +/// While the TUI owns raw mode + `/dev/tty`, any child that can open `/dev/tty` +/// (or inherits it as a controlling terminal) can corrupt the screen with +/// capability probes / pager noise. `setsid` gives the child a new session with +/// no controlling terminal; EPERM falls back to `setpgid` for group isolation. +#[cfg(unix)] +fn detach_std_command(cmd: &mut Command) { + use std::os::unix::process::CommandExt; + // SAFETY: only setsid/setpgid inside pre_exec (async-signal-safe). + unsafe { + cmd.pre_exec(|| { + // libc setsid; ignore EPERM by falling back to setpgid(0,0) + if libc::setsid() == -1 { + let err = std::io::Error::last_os_error(); + if err.raw_os_error() == Some(libc::EPERM) { + let _ = libc::setpgid(0, 0); + } + } + Ok(()) + }); + } +} + +#[cfg(not(unix))] +fn detach_std_command(_cmd: &mut Command) {} + +/// Try to list files using ripgrep. +/// +/// Streams stdout and stops (killing `rg`) once `max_files` is reached or the +/// scan exceeds [`WORKSPACE_SCAN_TIMEOUT`]. Does **not** pass `--follow`: +/// following symlinks from `$HOME` can hang interactive startup indefinitely. +/// Child is TTY-detached (see [`detach_std_command`]) so it cannot fight the TUI. fn try_ripgrep(root: &Path, max_files: usize) -> Option> { let root_canonical = root.canonicalize().unwrap_or_else(|_| root.to_path_buf()); - let output = Command::new("rg") - .args(["--files", "--hidden", "--follow"]) + let mut cmd = Command::new("rg"); + cmd.args(["--files", "--hidden"]) .args(["--glob", "!.git"]) .args(["--glob", "!node_modules"]) .args(["--glob", "!target"]) .args(["--glob", "!.next"]) .args(["--glob", "!dist"]) .args(["--glob", "!build"]) + .args(["--glob", "!.npm-global"]) + .args(["--glob", "!.cargo"]) + .args(["--glob", "!.rustup"]) + .args(["--glob", "!Library"]) .current_dir(root) - .output() - .ok()?; - - if !output.status.success() { - return None; - } - - let stdout = String::from_utf8_lossy(&output.stdout); + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + detach_std_command(&mut cmd); + let mut child = cmd.spawn().ok()?; + + let stdout = child.stdout.take()?; + let reader = BufReader::new(stdout); + let started = Instant::now(); let mut files = Vec::new(); - for line in stdout.lines() { - if files.len() >= max_files { + + for line in reader.lines() { + if files.len() >= max_files || started.elapsed() >= WORKSPACE_SCAN_TIMEOUT { break; } - let path = root.join(line); + let Ok(line) = line else { + break; + }; + if line.is_empty() { + continue; + } + let path = root.join(&line); let canonical = match path.canonicalize() { Ok(p) => p, Err(_) => continue, @@ -142,13 +197,22 @@ fn try_ripgrep(root: &Path, max_files: usize) -> Option> { files.push(WorkspaceFile::from_path(root, path)); } - Some(files) + // Stop the walk as soon as we have enough files / hit the budget. + // Ignoring errors: the child may already have exited. + let _ = child.kill(); + let _ = child.wait(); + + if files.is_empty() { + None + } else { + Some(files) + } } -/// Try to list files using find +/// Try to list files using find (streamed + timed, same budget as ripgrep). fn try_find(root: &Path, max_files: usize) -> Option> { - let output = Command::new("find") - .arg(".") + let mut cmd = Command::new("find"); + cmd.arg(".") .args(["-type", "f"]) .args(["-not", "-path", "*/.git/*"]) .args(["-not", "-path", "*/node_modules/*"]) @@ -156,29 +220,43 @@ fn try_find(root: &Path, max_files: usize) -> Option> { .args(["-not", "-path", "*/.next/*"]) .args(["-not", "-path", "*/dist/*"]) .args(["-not", "-path", "*/build/*"]) + .args(["-not", "-path", "*/.npm-global/*"]) + .args(["-not", "-path", "*/Library/*"]) .current_dir(root) - .output() - .ok()?; + .stdin(Stdio::null()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()); + detach_std_command(&mut cmd); + let mut child = cmd.spawn().ok()?; + + let stdout = child.stdout.take()?; + let reader = BufReader::new(stdout); + let started = Instant::now(); + let mut files = Vec::new(); - if !output.status.success() { - return None; + for line in reader.lines() { + if files.len() >= max_files || started.elapsed() >= WORKSPACE_SCAN_TIMEOUT { + break; + } + let Ok(line) = line else { + break; + }; + let line = line.strip_prefix("./").unwrap_or(&line); + if line.is_empty() { + continue; + } + let path = root.join(line); + files.push(WorkspaceFile::from_path(root, path)); } - let stdout = String::from_utf8_lossy(&output.stdout); - let files: Vec = stdout - .lines() - .take(max_files) - .filter_map(|line| { - let line = line.strip_prefix("./").unwrap_or(line); - if line.is_empty() { - return None; - } - let path = root.join(line); - Some(WorkspaceFile::from_path(root, path)) - }) - .collect(); + let _ = child.kill(); + let _ = child.wait(); - Some(files) + if files.is_empty() { + None + } else { + Some(files) + } } /// Manual directory traversal diff --git a/packages/vscode-extension/package.json b/packages/vscode-extension/package.json index b1fe4fa72..98a0505f7 100644 --- a/packages/vscode-extension/package.json +++ b/packages/vscode-extension/package.json @@ -3,7 +3,7 @@ "displayName": "Maestro", "description": "Use Maestro's deterministic AI assistant inside VS Code.", "publisher": "evalops", - "version": "0.10.53", + "version": "0.10.54", "license": "MIT", "type": "commonjs", "main": "./dist/extension.js", @@ -139,7 +139,7 @@ "test": "vitest --run" }, "dependencies": { - "@evalops/contracts": "^0.10.53" + "@evalops/contracts": "^0.10.54" }, "devDependencies": { "@types/node": "^24.10.7", diff --git a/packages/web/package.json b/packages/web/package.json index 91b4e5e8f..23318a2d8 100644 --- a/packages/web/package.json +++ b/packages/web/package.json @@ -1,6 +1,6 @@ { "name": "@evalops/maestro-web", - "version": "0.10.53", + "version": "0.10.54", "description": "Web UI for Maestro - Browser-based AI coding assistant interface", "type": "module", "main": "./dist/index.js", @@ -24,7 +24,7 @@ "directory": "packages/web" }, "dependencies": { - "@evalops/contracts": "^0.10.53", + "@evalops/contracts": "^0.10.54", "docx-preview": "^0.3.7", "dompurify": "^3.3.0", "exceljs": "^4.4.0", diff --git a/scripts/evals/tool-surface-smoke/core.ts b/scripts/evals/tool-surface-smoke/core.ts index 5f4fc532e..757f957a9 100644 --- a/scripts/evals/tool-surface-smoke/core.ts +++ b/scripts/evals/tool-surface-smoke/core.ts @@ -186,11 +186,13 @@ async function evaluateApiToolsCase(): Promise { async function evaluateFindCase(): Promise { const dir = await mkdtemp(join(tmpdir(), "composer-tool-find-")); try { + // Nested glob discovery is the contract under test. Avoid gitignore + // negation patterns here — fd versions disagree on !exceptions for + // non-repo temp dirs, which made release quality-gate flakes. await writeFile(join(dir, "alpha.ts"), "export const alpha = 1;\n", "utf8"); await mkdir(join(dir, "nested"), { recursive: true }); await writeFile(join(dir, "nested", "beta.ts"), "export const beta = 2;\n", "utf8"); - await writeFile(join(dir, "nested", "ignored.ts"), "export const ignored = 3;\n", "utf8"); - await writeFile(join(dir, ".gitignore"), "nested/*.ts\n!nested/beta.ts\n", "utf8"); + await writeFile(join(dir, "nested", "notes.txt"), "not a ts match\n", "utf8"); const result = await findTool.execute("tool-surface-find", { pattern: "**/*.ts", diff --git a/scripts/install-smoke-utils.js b/scripts/install-smoke-utils.js index a547ed638..3ac560ffa 100644 --- a/scripts/install-smoke-utils.js +++ b/scripts/install-smoke-utils.js @@ -3,7 +3,7 @@ import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; -import { join } from "node:path"; +import { delimiter, join } from "node:path"; export function getNpmCommand() { return process.platform === "win32" ? "npm.cmd" : "npm"; @@ -172,6 +172,52 @@ export function runInstalledCliSmoke( }); } +/** + * Build an env for clean-install native smokes that must resolve the *packaged* + * Rust binary (vendor/maestro-tui), not a developer's ambient override or PATH. + */ +export function buildNativeInstallSmokeEnv(cwd, baseEnv = process.env) { + const isolatedMaestroHome = join(cwd, ".maestro-native-smoke"); + const env = { + ...baseEnv, + MAESTRO_HOME: isolatedMaestroHome, + OPENAI_OAUTH_FILE: join(isolatedMaestroHome, "openai-oauth.json"), + }; + // MAESTRO_TUI_BIN takes precedence over vendor/ in the JS launcher. Scrub it so + // a green smoke cannot mean "my local override works" while the tarball is broken. + delete env.MAESTRO_TUI_BIN; + + // The launcher falls back to PATH after vendor/. Constrain PATH to bare system + // dirs so ambient installs (Homebrew /usr/local/bin, cargo bin, etc.) cannot + // greenlight a package missing vendor/maestro-tui. The CLI itself is invoked + // by absolute path; vendor resolution does not need a rich PATH. + const minimalPath = + process.platform === "win32" + ? [baseEnv.SystemRoot ? join(baseEnv.SystemRoot, "System32") : "C:\\Windows\\System32"].join( + delimiter, + ) + : ["/usr/bin", "/bin"].join(delimiter); + env.PATH = minimalPath; + if ("Path" in env || process.platform === "win32") { + env.Path = minimalPath; + } + return env; +} + +export function runInstalledNativeCliSmoke(cwd, { cliCommand }) { + const command = installedBinPath(cwd, cliCommand); + // --version and --help are handled by the JavaScript launcher itself. Exercise + // a native-only command so release smokes also prove the packaged Rust runtime + // can be resolved and launched from a clean install. Isolate credential paths + // so a local smoke can never read, refresh, or remove a developer's login, and + // scrub native binary overrides so only the packaged vendor payload can win. + execFileSync(command, ["openai", "status"], { + cwd, + stdio: "ignore", + env: buildNativeInstallSmokeEnv(cwd), + }); +} + function runCliSmoke(command, prefixArgs, cwd, { cliCommand, expectedVersion, label }) { const versionOutput = execFileSync(command, [...prefixArgs, "--version"], { cwd, diff --git a/scripts/smoke-packed-cli.js b/scripts/smoke-packed-cli.js index 2c1f06265..cef0dd49a 100644 --- a/scripts/smoke-packed-cli.js +++ b/scripts/smoke-packed-cli.js @@ -14,6 +14,7 @@ import { runBunRuntimeCliSmoke, runBunxCliSmoke, runInstalledCliSmoke, + runInstalledNativeCliSmoke, runInstalledPackageAudit, } from "./install-smoke-utils.js"; import { getPackageMetadata } from "./package-metadata.js"; @@ -31,7 +32,7 @@ if (!tarballArg) { const tarballPath = resolve(process.cwd(), tarballArg); const tarballSizeBytes = statSync(tarballPath).size; const maxTarballSizeBytes = Number.parseInt( - process.env.MAESTRO_MAX_PACK_SIZE_BYTES ?? `${10 * 1024 * 1024}`, + process.env.MAESTRO_MAX_PACK_SIZE_BYTES ?? `${100 * 1024 * 1024}`, 10, ); @@ -90,6 +91,9 @@ function runNpmInstallSmoke() { expectedVersion: version, label: "npm-installed packed CLI", }); + if (process.env.MAESTRO_REQUIRE_PACKAGED_TUI === "1") { + runInstalledNativeCliSmoke(tempDir, { cliCommand }); + } console.log( `Smoke-tested ${cliCommand} from ${tarballPath} with npm (${tarballSizeBytes} bytes).`, @@ -121,6 +125,9 @@ function runBunInstallSmoke() { expectedVersion: version, label: "Bun-installed packed CLI", }); + if (process.env.MAESTRO_REQUIRE_PACKAGED_TUI === "1") { + runInstalledNativeCliSmoke(tempDir, { cliCommand }); + } runBunxCliSmoke(tempDir, { cliCommand, expectedVersion: version, diff --git a/scripts/smoke-registry-install.js b/scripts/smoke-registry-install.js index 9280a9b5a..ce78ac77a 100644 --- a/scripts/smoke-registry-install.js +++ b/scripts/smoke-registry-install.js @@ -14,6 +14,7 @@ import { runBunxCliSmoke, runBunRuntimeCliSmoke, runInstalledCliSmoke, + runInstalledNativeCliSmoke, runInstalledPackageAudit, runNpxCliSmoke, } from "./install-smoke-utils.js"; @@ -227,6 +228,7 @@ async function main() { expectedVersion: version, label: "npm-installed registry CLI", }); + runInstalledNativeCliSmoke(tempDir, { cliCommand }); runNpxCliSmoke(tempDir, { cliCommand, expectedVersion: version, @@ -270,6 +272,7 @@ async function main() { expectedVersion: version, label: "Bun-installed registry CLI", }); + runInstalledNativeCliSmoke(bunTempDir, { cliCommand }); runBunxCliSmoke(bunTempDir, { cliCommand, expectedVersion: version, diff --git a/test/scripts/ci-guardrails.test.ts b/test/scripts/ci-guardrails.test.ts index bdb6da0d0..1a7b81957 100644 --- a/test/scripts/ci-guardrails.test.ts +++ b/test/scripts/ci-guardrails.test.ts @@ -1216,6 +1216,65 @@ describe("ci workflow guardrails", () => { expect(script).toContain("getBunCommand"); expect(script).toContain("runNpmInstallSmoke();"); expect(script).toContain("runBunInstallSmoke();"); + + const smokeUtils = readFileSync( + new URL("../../scripts/install-smoke-utils.js", import.meta.url), + { encoding: "utf8" }, + ); + expect(smokeUtils).toContain('["openai", "status"]'); + expect(smokeUtils).toContain("buildNativeInstallSmokeEnv"); + expect(smokeUtils).toContain("delete env.MAESTRO_TUI_BIN"); + expect(smokeUtils).toContain('["/usr/bin", "/bin"]'); + expect(smokeUtils).toContain("MAESTRO_HOME:"); + expect(script).toContain("MAESTRO_REQUIRE_PACKAGED_TUI"); + }); + + it("builds every packaged Rust TUI target on Blacksmith", () => { + const workflowPath = new URL( + "../../.github/workflows/release.yml", + import.meta.url, + ); + const workflow = readFileSync(workflowPath, { encoding: "utf8" }); + const parsedWorkflow = parse(workflow) as Workflow; + if ( + isPublicValidationWorkflow(parsedWorkflow) && + !workflow.includes("release-tui-binaries:") + ) { + // The public-owned publishing workflow lands in its downstream PR after + // the internal source change; mirror validation must permit that ordering. + return; + } + + for (const runner of [ + "blacksmith-6vcpu-macos-15", + "blacksmith-4vcpu-ubuntu-2404", + "blacksmith-4vcpu-ubuntu-2404-arm", + ]) { + expect(workflow).toContain(runner); + } + expect(workflow).toContain("release-tui-binaries:"); + expect(workflow).toContain( + "build --release --locked --bin maestro-tui --target", + ); + expect(workflow).toContain('strip -x "$dest"'); + expect(workflow).toContain('strip "$dest"'); + // Standalone bun binary smoke must resolve native maestro-tui. + expect(workflow).toContain( + 'export MAESTRO_TUI_BIN="$PWD/packages/tui-rs/target/release/maestro-tui"', + ); + expect(workflow).toContain("smoke-release-binary.mjs"); + // Isolated rust for smoke must not precede build:all (breaks rustfmt contracts). + const releaseBinariesSection = workflow.slice( + workflow.indexOf("release-binaries:"), + workflow.indexOf("\n notify:"), + ); + const buildAllAt = releaseBinariesSection.indexOf("npm run build:all"); + const setupRustAt = releaseBinariesSection.indexOf( + "./.github/actions/setup-rust", + ); + expect(buildAllAt).toBeGreaterThan(-1); + expect(setupRustAt).toBeGreaterThan(buildAllAt); + expect(releaseBinariesSection).toContain("components: rustfmt"); }); it("runs published replay E2E for npm and Bun registry installs", () => {