From bf68ca3b0c74e6d9a2f46c4bcfab7516810b33a7 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:05:53 +0800 Subject: [PATCH 1/9] feat(ops): add omx preflight and evidence workflows Add WSL2-first preflight routing and structured completion evidence capture for team and ralph execution modes.\n\nCo-authored-by: Codex --- package.json | 2 + scripts/omx-capture-evidence.js | 335 ++++++++++++++++++++++++++++++++ scripts/omx-preflight-wsl2.js | 307 +++++++++++++++++++++++++++++ 3 files changed, 644 insertions(+) create mode 100644 scripts/omx-capture-evidence.js create mode 100644 scripts/omx-preflight-wsl2.js diff --git a/package.json b/package.json index 99934cf4..156f103a 100644 --- a/package.json +++ b/package.json @@ -42,6 +42,8 @@ "test:ui": "vitest --ui", "test:coverage": "vitest run --coverage", "coverage": "vitest run --coverage", + "omx:preflight": "node scripts/omx-preflight-wsl2.js", + "omx:evidence": "node scripts/omx-capture-evidence.js", "audit:prod": "npm audit --omit=dev --audit-level=high", "audit:all": "npm audit --audit-level=high", "audit:dev:allowlist": "node scripts/audit-dev-allowlist.js", diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js new file mode 100644 index 00000000..14ef7976 --- /dev/null +++ b/scripts/omx-capture-evidence.js @@ -0,0 +1,335 @@ +#!/usr/bin/env node + +import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { spawnSync } from "node:child_process"; + +const __filename = fileURLToPath(import.meta.url); + +function normalizePathForCompare(path) { + const resolved = resolve(path); + return process.platform === "win32" ? resolved.toLowerCase() : resolved; +} + +const isDirectRun = (() => { + if (!process.argv[1]) return false; + return normalizePathForCompare(process.argv[1]) === normalizePathForCompare(__filename); +})(); + +function resolveTool(toolName) { + if (process.platform !== "win32") return toolName; + if (toolName === "npm") return "npm.cmd"; + if (toolName === "npx") return "npx.cmd"; + return toolName; +} + +export function parseArgs(argv) { + const options = { + mode: "", + team: "", + architectTier: "", + architectRef: "", + architectNote: "", + output: "", + }; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + const value = argv[index + 1] ?? ""; + if (token === "--mode") { + if (!value) throw new Error("Missing value for --mode"); + options.mode = value; + index += 1; + continue; + } + if (token === "--team") { + if (!value) throw new Error("Missing value for --team"); + options.team = value; + index += 1; + continue; + } + if (token === "--architect-tier") { + if (!value) throw new Error("Missing value for --architect-tier"); + options.architectTier = value; + index += 1; + continue; + } + if (token === "--architect-ref") { + if (!value) throw new Error("Missing value for --architect-ref"); + options.architectRef = value; + index += 1; + continue; + } + if (token === "--architect-note") { + if (!value) throw new Error("Missing value for --architect-note"); + options.architectNote = value; + index += 1; + continue; + } + if (token === "--output") { + if (!value) throw new Error("Missing value for --output"); + options.output = value; + index += 1; + continue; + } + throw new Error(`Unknown option: ${token}`); + } + + if (options.mode !== "team" && options.mode !== "ralph") { + throw new Error("`--mode` must be `team` or `ralph`."); + } + if (options.mode === "team" && !options.team) { + throw new Error("`--team` is required when --mode team."); + } + if (!options.architectTier) { + throw new Error("`--architect-tier` is required."); + } + if (!options.architectRef) { + throw new Error("`--architect-ref` is required."); + } + + return options; +} + +export function runCommand(command, args, overrides = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + shell: false, + stdio: ["ignore", "pipe", "pipe"], + ...overrides, + }); + + return { + command: `${command} ${args.join(" ")}`.trim(), + code: typeof result.status === "number" ? result.status : 1, + stdout: typeof result.stdout === "string" ? result.stdout.trim() : "", + stderr: typeof result.stderr === "string" ? result.stderr.trim() : "", + }; +} + +function nowStamp() { + const date = new Date(); + const year = date.getFullYear(); + const month = String(date.getMonth() + 1).padStart(2, "0"); + const day = String(date.getDate()).padStart(2, "0"); + const hour = String(date.getHours()).padStart(2, "0"); + const minute = String(date.getMinutes()).padStart(2, "0"); + const second = String(date.getSeconds()).padStart(2, "0"); + const millis = String(date.getMilliseconds()).padStart(3, "0"); + return `${year}${month}${day}-${hour}${minute}${second}-${millis}`; +} + +function clampText(text, maxLength = 12000) { + if (text.length <= maxLength) return text; + return `${text.slice(0, maxLength)}\n...[truncated]`; +} + +function parseCount(text, keyAliases) { + for (const key of keyAliases) { + const patterns = [ + new RegExp(`${key}\\s*[=:]\\s*(\\d+)`, "i"), + new RegExp(`"${key}"\\s*:\\s*(\\d+)`, "i"), + ]; + for (const pattern of patterns) { + const match = text.match(pattern); + if (match) return Number(match[1]); + } + } + return null; +} + +export function parseTeamCounts(statusOutput) { + try { + const parsed = JSON.parse(statusOutput); + if (parsed && typeof parsed === "object") { + const summary = + "task_counts" in parsed && parsed.task_counts && typeof parsed.task_counts === "object" + ? parsed.task_counts + : "tasks" in parsed && parsed.tasks && typeof parsed.tasks === "object" + ? parsed.tasks + : null; + if (summary) { + const pending = "pending" in summary && typeof summary.pending === "number" ? summary.pending : null; + const inProgress = "in_progress" in summary && typeof summary.in_progress === "number" ? summary.in_progress : null; + const failed = "failed" in summary && typeof summary.failed === "number" ? summary.failed : null; + if (pending !== null && inProgress !== null && failed !== null) { + return { pending, inProgress, failed }; + } + } + } + } catch { + // ignore and fallback to regex parse + } + + const pending = parseCount(statusOutput, ["pending"]); + const inProgress = parseCount(statusOutput, ["in_progress", "in-progress", "in progress"]); + const failed = parseCount(statusOutput, ["failed"]); + if (pending === null || inProgress === null || failed === null) return null; + return { pending, inProgress, failed }; +} + +function formatOutput(result) { + const combined = [result.stdout, result.stderr].filter((value) => value.length > 0).join("\n"); + if (!combined) return "(no output)"; + return clampText(combined); +} + +function ensureRepoRoot(cwd) { + const packagePath = join(cwd, "package.json"); + if (!existsSync(packagePath)) { + throw new Error(`Expected package.json in current directory (${cwd}). Run this command from repo root.`); + } +} + +function buildOutputPath(options, cwd, runId) { + if (options.output) return options.output; + const filename = `${runId}-${options.mode}-evidence.md`; + return join(cwd, ".omx", "evidence", filename); +} + +export function runEvidence(options, deps = {}) { + const cwd = deps.cwd ?? process.cwd(); + ensureRepoRoot(cwd); + + const run = deps.runCommand ?? runCommand; + const npm = resolveTool("npm"); + const npx = resolveTool("npx"); + const omx = resolveTool("omx"); + + const metadataBranch = run("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd }); + const metadataCommit = run("git", ["rev-parse", "HEAD"], { cwd }); + + const typecheck = run(npm, ["run", "typecheck"], { cwd }); + const tests = run(npm, ["test"], { cwd }); + const build = run(npm, ["run", "build"], { cwd }); + const diagnostics = run(npx, ["tsc", "--noEmit", "--pretty", "false"], { cwd }); + + let teamStatus = null; + let teamCounts = null; + if (options.mode === "team") { + teamStatus = run(omx, ["team", "status", options.team], { cwd }); + if (teamStatus.code === 0) { + teamCounts = parseTeamCounts(`${teamStatus.stdout}\n${teamStatus.stderr}`); + } + } + + const teamStatePassed = + options.mode === "team" + ? teamStatus !== null && + teamStatus.code === 0 && + teamCounts !== null && + teamCounts.pending === 0 && + teamCounts.inProgress === 0 && + teamCounts.failed === 0 + : true; + + const architectPassed = options.architectTier.trim().length > 0 && options.architectRef.trim().length > 0; + + const gates = [ + { name: "Typecheck", passed: typecheck.code === 0, detail: "npm run typecheck" }, + { name: "Tests", passed: tests.code === 0, detail: "npm test" }, + { name: "Build", passed: build.code === 0, detail: "npm run build" }, + { name: "Diagnostics", passed: diagnostics.code === 0, detail: "npx tsc --noEmit --pretty false" }, + { + name: "Team terminal state", + passed: teamStatePassed, + detail: + options.mode === "team" + ? teamCounts + ? `pending=${teamCounts.pending}, in_progress=${teamCounts.inProgress}, failed=${teamCounts.failed}` + : "Unable to parse team status counts." + : "Not applicable (mode=ralph)", + }, + { + name: "Architect verification", + passed: architectPassed, + detail: `tier=${options.architectTier}; ref=${options.architectRef}`, + }, + ]; + + const overallPassed = + typecheck.code === 0 && + tests.code === 0 && + build.code === 0 && + diagnostics.code === 0 && + teamStatePassed && + architectPassed; + + const runId = nowStamp(); + const outputPath = buildOutputPath(options, cwd, runId); + mkdirSync(dirname(outputPath), { recursive: true }); + + const lines = []; + lines.push("# OMX Execution Evidence"); + lines.push(""); + lines.push("## Metadata"); + lines.push(`- Run ID: ${runId}`); + lines.push(`- Generated at: ${new Date().toISOString()}`); + lines.push(`- Mode: ${options.mode}`); + if (options.mode === "team") lines.push(`- Team name: ${options.team}`); + lines.push(`- Branch: ${metadataBranch.code === 0 ? metadataBranch.stdout : "unknown"}`); + lines.push(`- Commit: ${metadataCommit.code === 0 ? metadataCommit.stdout : "unknown"}`); + lines.push(""); + lines.push("## Gate Summary"); + lines.push("| Gate | Result | Detail |"); + lines.push("| --- | --- | --- |"); + for (const gate of gates) { + lines.push(`| ${gate.name} | ${gate.passed ? "PASS" : "FAIL"} | ${gate.detail.replace(/\|/g, "\\|")} |`); + } + lines.push(""); + lines.push(`## Overall Result: ${overallPassed ? "PASS" : "FAIL"}`); + lines.push(""); + lines.push("## Command Output"); + + const commandResults = [ + { name: "typecheck", result: typecheck }, + { name: "tests", result: tests }, + { name: "build", result: build }, + { name: "diagnostics", result: diagnostics }, + ]; + if (teamStatus) commandResults.push({ name: "team-status", result: teamStatus }); + + for (const item of commandResults) { + lines.push(`### ${item.name} (${item.result.code === 0 ? "PASS" : "FAIL"})`); + lines.push("```text"); + lines.push(`$ ${item.result.command}`); + lines.push(formatOutput(item.result)); + lines.push("```"); + lines.push(""); + } + + lines.push("## Architect Verification"); + lines.push("```text"); + lines.push(`tier=${options.architectTier}`); + lines.push(`ref=${options.architectRef}`); + if (options.architectNote) lines.push(`note=${options.architectNote}`); + lines.push("```"); + lines.push(""); + + writeFileSync(outputPath, lines.join("\n"), "utf8"); + return { overallPassed, outputPath }; +} + +export function main(argv = process.argv.slice(2)) { + const options = parseArgs(argv); + const result = runEvidence(options); + if (result.overallPassed) { + console.log(`Evidence captured at ${result.outputPath}`); + console.log("All gates passed."); + process.exit(0); + } + console.error(`Evidence captured at ${result.outputPath}`); + console.error("One or more gates failed."); + process.exit(1); +} + +if (isDirectRun) { + try { + main(); + } catch (error) { + console.error("Failed to capture evidence."); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} diff --git a/scripts/omx-preflight-wsl2.js b/scripts/omx-preflight-wsl2.js new file mode 100644 index 00000000..88a9e056 --- /dev/null +++ b/scripts/omx-preflight-wsl2.js @@ -0,0 +1,307 @@ +#!/usr/bin/env node + +import { existsSync, readFileSync } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, join, resolve } from "node:path"; +import { spawnSync } from "node:child_process"; + +const PLACEHOLDER_PANE_ID = "replace-with-tmux-pane-id"; + +const __filename = fileURLToPath(import.meta.url); + +function normalizePathForCompare(path) { + const resolved = resolve(path); + return process.platform === "win32" ? resolved.toLowerCase() : resolved; +} + +const isDirectRun = (() => { + if (!process.argv[1]) return false; + return normalizePathForCompare(process.argv[1]) === normalizePathForCompare(__filename); +})(); + +export function parseArgs(argv) { + const options = { + json: false, + distro: "", + }; + + for (let index = 0; index < argv.length; index += 1) { + const token = argv[index]; + if (token === "--json") { + options.json = true; + continue; + } + if (token === "--distro") { + const value = argv[index + 1] ?? ""; + if (!value) throw new Error("Missing value for --distro"); + options.distro = value; + index += 1; + continue; + } + throw new Error(`Unknown option: ${token}`); + } + + return options; +} + +export function runProcess(command, args, overrides = {}) { + const result = spawnSync(command, args, { + encoding: "utf8", + shell: false, + ...overrides, + }); + + return { + code: typeof result.status === "number" ? result.status : 1, + stdout: typeof result.stdout === "string" ? result.stdout : "", + stderr: typeof result.stderr === "string" ? result.stderr : "", + }; +} + +function addCheck(checks, status, severity, name, detail) { + checks.push({ status, severity, name, detail }); +} + +export function parseDistroList(stdout) { + return stdout + .replace(/\u0000/g, "") + .split(/\r?\n/) + .map((value) => value.trim()) + .filter((value) => value.length > 0); +} + +function getShellCommand(toolName) { + if (process.platform !== "win32") return toolName; + if (toolName === "npm") return "npm.cmd"; + if (toolName === "npx") return "npx.cmd"; + return toolName; +} + +function checkOmxOnHost(checks, runner) { + const omxHelp = runner(getShellCommand("omx"), ["--help"]); + if (omxHelp.code === 0) { + addCheck(checks, "pass", "info", "omx host runtime", "omx is available in current host runtime."); + } else { + addCheck( + checks, + "fail", + "fatal", + "omx host runtime", + "omx is required for both team mode and fallback mode. Install/enable omx first.", + ); + } +} + +function checkHookConfig(checks, cwd, fsDeps) { + const hookPath = join(cwd, ".omx", "tmux-hook.json"); + if (!fsDeps.existsSync(hookPath)) { + addCheck(checks, "warn", "info", "tmux hook config", `${hookPath} not found (optional but recommended).`); + return; + } + + let parsed; + try { + parsed = JSON.parse(fsDeps.readFileSync(hookPath, "utf8")); + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + addCheck(checks, "fail", "fixable", "tmux hook config parse", `Invalid JSON in ${hookPath}: ${message}`); + return; + } + + const target = + parsed && typeof parsed === "object" && "target" in parsed && parsed.target && typeof parsed.target === "object" + ? parsed.target + : null; + const value = target && "value" in target && typeof target.value === "string" ? target.value : ""; + if (value === PLACEHOLDER_PANE_ID) { + addCheck( + checks, + "fail", + "fixable", + "tmux hook pane target", + `Set .omx/tmux-hook.json target.value to a real pane id (for example %12), not ${PLACEHOLDER_PANE_ID}.`, + ); + return; + } + addCheck(checks, "pass", "info", "tmux hook pane target", "tmux hook target is not placeholder."); +} + +function runWindowsChecks(checks, requestedDistro, runner) { + checkOmxOnHost(checks, runner); + + const wsl = runner("wsl", ["-l", "-q"]); + if (wsl.code !== 0) { + addCheck(checks, "fail", "team_hard", "wsl availability", "WSL unavailable. Team mode requires WSL2 or Unix host."); + return { distro: "" }; + } + + const allDistros = parseDistroList(wsl.stdout); + if (allDistros.length === 0) { + addCheck(checks, "fail", "team_hard", "wsl distros", "No WSL distro found."); + return { distro: "" }; + } + + const usableDistros = allDistros.filter((name) => !/^docker-desktop(-data)?$/i.test(name)); + if (usableDistros.length === 0) { + addCheck(checks, "fail", "team_hard", "usable distro", "Only Docker Desktop distros found. Install Ubuntu or another Linux distro."); + return { distro: "" }; + } + + let selectedDistro = usableDistros[0]; + if (requestedDistro) { + if (!allDistros.includes(requestedDistro)) { + addCheck(checks, "fail", "team_hard", "requested distro", `Requested distro '${requestedDistro}' not found.`); + return { distro: "" }; + } + selectedDistro = requestedDistro; + } + addCheck(checks, "pass", "info", "selected distro", `Using WSL distro: ${selectedDistro}`); + + function runInWsl(command) { + return runner("wsl", ["-d", selectedDistro, "--", "sh", "-lc", command]); + } + + const tmux = runInWsl("command -v tmux >/dev/null 2>&1"); + if (tmux.code === 0) { + addCheck(checks, "pass", "info", "tmux in WSL", "tmux is available in selected distro."); + } else { + addCheck(checks, "fail", "team_hard", "tmux in WSL", "Install tmux in selected distro."); + } + + const omx = runInWsl("command -v omx >/dev/null 2>&1"); + if (omx.code === 0) { + addCheck(checks, "pass", "info", "omx in WSL", "omx is available in selected distro."); + } else { + addCheck(checks, "fail", "team_hard", "omx in WSL", "Install/enable omx inside selected distro."); + } + + const teamHelp = runInWsl("omx team --help >/dev/null 2>&1"); + if (teamHelp.code === 0) { + addCheck(checks, "pass", "info", "omx team in WSL", "omx team command is callable in selected distro."); + } else { + addCheck(checks, "fail", "team_hard", "omx team in WSL", "omx team --help failed in selected distro."); + } + + const tmuxSession = runInWsl("[ -n \"${TMUX:-}\" ]"); + if (tmuxSession.code === 0) { + addCheck(checks, "pass", "info", "tmux leader session", "Current WSL shell is inside tmux."); + } else { + addCheck(checks, "fail", "fixable", "tmux leader session", "Attach/start tmux in WSL before running omx team."); + } + + return { distro: selectedDistro }; +} + +function runUnixChecks(checks, runner) { + checkOmxOnHost(checks, runner); + + const tmux = runner("sh", ["-lc", "command -v tmux >/dev/null 2>&1"]); + if (tmux.code === 0) { + addCheck(checks, "pass", "info", "tmux installed", "tmux is available in current runtime."); + } else { + addCheck(checks, "fail", "team_hard", "tmux installed", "Install tmux to use team mode."); + } + + const teamHelp = runner("sh", ["-lc", "omx team --help >/dev/null 2>&1"]); + if (teamHelp.code === 0) { + addCheck(checks, "pass", "info", "omx team help", "omx team command is callable."); + } else { + addCheck(checks, "fail", "team_hard", "omx team help", "omx team --help failed in current runtime."); + } + + const tmuxSession = runner("sh", ["-lc", "[ -n \"${TMUX:-}\" ]"]); + if (tmuxSession.code === 0) { + addCheck(checks, "pass", "info", "tmux leader session", "Current shell is inside tmux."); + } else { + addCheck(checks, "fail", "fixable", "tmux leader session", "Enter a tmux session before running omx team."); + } +} + +export function decide(checks) { + const hasFatal = checks.some((entry) => entry.status === "fail" && entry.severity === "fatal"); + const hasTeamHard = checks.some((entry) => entry.status === "fail" && entry.severity === "team_hard"); + const hasFixable = checks.some((entry) => entry.status === "fail" && entry.severity === "fixable"); + + if (hasFatal) return { mode: "blocked", exitCode: 4 }; + if (hasTeamHard) return { mode: "fallback_ralph", exitCode: 3 }; + if (hasFixable) return { mode: "team_blocked", exitCode: 2 }; + return { mode: "team_ready", exitCode: 0 }; +} + +export function formatConsoleOutput(payload) { + const lines = []; + lines.push("OMX WSL2 Team Preflight"); + lines.push("======================="); + lines.push(`Decision: ${payload.mode}`); + if (payload.distro) lines.push(`Distro: ${payload.distro}`); + lines.push(""); + lines.push("Checks:"); + for (const check of payload.checks) { + let label = "PASS"; + if (check.status === "warn") label = "WARN"; + if (check.status === "fail" && check.severity === "fixable") label = "FAIL-FIX"; + if (check.status === "fail" && check.severity === "team_hard") label = "FAIL-TEAM"; + if (check.status === "fail" && check.severity === "fatal") label = "FAIL-FATAL"; + lines.push(`- [${label}] ${check.name}: ${check.detail}`); + } + lines.push(""); + if (payload.mode === "team_ready") { + lines.push("Next: run `omx team ralph 6:executor \"\"` inside tmux."); + } else if (payload.mode === "team_blocked") { + lines.push("Next: fix FAIL-FIX checks and rerun preflight."); + } else if (payload.mode === "fallback_ralph") { + lines.push("Next: run controlled fallback `omx ralph \"\"` while team prerequisites are unavailable."); + } else { + lines.push("Next: fix FAIL-FATAL prerequisites before continuing."); + } + return lines.join("\n"); +} + +export function runPreflight(options = {}, deps = {}) { + const checks = []; + const runner = deps.runProcess ?? runProcess; + const platform = deps.platform ?? process.platform; + const cwd = deps.cwd ?? process.cwd(); + const fsDeps = { + existsSync: deps.existsSync ?? existsSync, + readFileSync: deps.readFileSync ?? readFileSync, + }; + + let distro = ""; + if (platform === "win32") { + const winResult = runWindowsChecks(checks, options.distro ?? "", runner); + distro = winResult.distro; + } else { + runUnixChecks(checks, runner); + } + + checkHookConfig(checks, cwd, fsDeps); + const decision = decide(checks); + return { + mode: decision.mode, + exitCode: decision.exitCode, + distro, + checks, + }; +} + +export function main(argv = process.argv.slice(2)) { + const options = parseArgs(argv); + const result = runPreflight(options); + if (options.json) { + console.log(JSON.stringify(result, null, 2)); + } else { + console.log(formatConsoleOutput(result)); + } + process.exit(result.exitCode); +} + +if (isDirectRun) { + try { + main(); + } catch (error) { + console.error("Preflight failed."); + console.error(error instanceof Error ? error.message : String(error)); + process.exit(1); + } +} From a273e25ef21626b6c845d6136b09b9e5883af971 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:06:03 +0800 Subject: [PATCH 2/9] docs(ops): add atomic ralph-team runbook Document WSL2-first team lifecycle, deterministic fallback routing, retry policy, and evidence/cleanup gates for comprehensive operational execution.\n\nCo-authored-by: Codex --- docs/README.md | 1 + docs/development/OMX_TEAM_RALPH_PLAYBOOK.md | 281 ++++++++++++++++++++ 2 files changed, 282 insertions(+) create mode 100644 docs/development/OMX_TEAM_RALPH_PLAYBOOK.md diff --git a/docs/README.md b/docs/README.md index 9a1c586a..9d9065ce 100644 --- a/docs/README.md +++ b/docs/README.md @@ -17,6 +17,7 @@ Explore the engineering depth behind this plugin: - **[Configuration System](development/CONFIG_FLOW.md)** - How config loading and merging works - **[Config Fields Guide](development/CONFIG_FIELDS.md)** - Understanding config keys, `id`, and `name` - **[Testing Guide](development/TESTING.md)** - Test scenarios, verification procedures, integration testing +- **[OMX Team + Ralph Playbook](development/OMX_TEAM_RALPH_PLAYBOOK.md)** - WSL2-first atomic workflow, fallback routing, and completion evidence gates - **[TUI Parity Checklist](development/TUI_PARITY_CHECKLIST.md)** - Auth dashboard/UI parity requirements for future changes ## Key Architectural Decisions diff --git a/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md new file mode 100644 index 00000000..0a2df17e --- /dev/null +++ b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md @@ -0,0 +1,281 @@ +# OMX Team + Ralph Reliability Playbook (WSL2-First) + +This runbook defines the repository-standard execution flow for high-rigor work using `omx team` plus `omx ralph`. + +## Scope + +- Repository-specific workflow for `oc-chatgpt-multi-auth`. +- Primary mode: team execution on WSL2 + tmux. +- Controlled fallback: single-agent Ralph execution. +- Completion requires parity quality gates in both modes. + +## Defaults and Guardrails + +- Default team topology: `6:executor`. +- Retry policy: fail-fast with at most `2` controlled retries per run. +- No normal shutdown when tasks are non-terminal. +- Mandatory completion gates: + - terminal state (`pending=0`, `in_progress=0`, `failed=0`) for team mode + - `npm run typecheck` + - `npm test` + - `npm run build` + - `npx tsc --noEmit --pretty false` diagnostics + - architect verification (`--architect-tier` and `--architect-ref`) +- Ralph completion requires explicit state cleanup (`omx cancel`). + +## Atomic Phases + +### Phase 0 - Intake Contract + +Lock execution contract for this run: + +- target task statement +- default worker topology (`6:executor`) +- gate policy and architect verification format + +### Phase 1 - Baseline Integrity Gate + +From repo root: + +```bash +git fetch origin --prune +git rev-parse origin/main +``` + +If working on an isolated branch/worktree, confirm: + +```bash +git status --short +git branch --show-current +``` + +### Phase 2 - Mainline Deep Audit + +Audit surfaces before mutation: + +- workflow docs (`docs/development`) +- scripts contract (`scripts`) +- package scripts (`package.json`) +- `.omx/tmux-hook.json` integrity + +### Phase 3 - Isolation Provisioning + +Create isolated worktree from synced `origin/main`: + +```bash +git worktree add -b origin/main +``` + +Never implement directly on `main`. + +### Phase 4 - Deterministic Routing + +Run preflight: + +```bash +npm run omx:preflight +``` + +JSON mode: + +```bash +npm run omx:preflight -- --json +``` + +Optional distro selection: + +```bash +npm run omx:preflight -- --distro Ubuntu +``` + +#### Preflight Exit Codes + +| Exit Code | Mode | Meaning | Required Action | +| --- | --- | --- | --- | +| `0` | `team_ready` | Team prerequisites are satisfied | Continue with team mode | +| `2` | `team_blocked` | Fixable blockers (for example tmux session or hook config) | Fix blockers, rerun preflight | +| `3` | `fallback_ralph` | Team-only prerequisites failed | Execute controlled Ralph fallback | +| `4` | `blocked` | Fatal blocker for both team and fallback (for example missing `omx`) | Stop and fix fatal prerequisite | +| `1` | script error | Invocation/runtime failure | Fix command/environment | + +### Phase 5 - Ralph Execution Loop + +#### Team Path (preferred) + +Inside WSL tmux session: + +```bash +omx team ralph 6:executor "execute task: " +``` + +Capture startup evidence: + +```bash +omx team status +tmux list-panes -F '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}' +test -f ".omx/state/team//mailbox/leader-fixed.json" && echo "leader mailbox present" +``` + +Monitor until terminal: + +```bash +omx team status +``` + +Terminal gate for normal completion: + +- `pending=0` +- `in_progress=0` +- `failed=0` + +#### Controlled Fallback Path + +Use fallback only when preflight mode is `fallback_ralph`: + +```bash +omx ralph "execute task: " +``` + +### Phase 6 - Hardening and Evidence + +Capture evidence before shutdown/handoff: + +```bash +npm run omx:evidence -- --mode team --team --architect-tier standard --architect-ref "" --architect-note "" +``` + +Fallback evidence: + +```bash +npm run omx:evidence -- --mode ralph --architect-tier standard --architect-ref "" --architect-note "" +``` + +Ralph state cleanup (required for completion): + +```bash +omx cancel +``` + +### Phase 7 - Shutdown and Handoff + +For team mode, only after evidence passes: + +```bash +omx team shutdown +test ! -d ".omx/state/team/" && echo "team state cleaned" +``` + +Handoff package must include: + +- branch name and commit SHA +- gate evidence file path +- architect verification reference +- unresolved blockers (if any) + +## Fail-Fast Controlled Retry Contract + +Retry budget is `2` retries maximum for a single run. + +Retry triggers: + +- team task failures +- no-ACK startup condition +- non-reporting worker condition after triage + +Retry steps: + +1. Capture current status and error output. +2. Attempt resume: + - `omx team resume ` + - `omx team status ` +3. If unresolved, controlled restart: + - `omx team shutdown ` + - stale pane/state cleanup + - relaunch with same task +4. After second retry failure, stop and escalate as blocked. + +## Reliability Remediation + +### tmux-hook placeholder target + +If `.omx/tmux-hook.json` contains: + +```json +"value": "replace-with-tmux-pane-id" +``` + +Set a real pane id: + +```bash +tmux display-message -p '#{pane_id}' +``` + +Then validate: + +```bash +omx tmux-hook validate +omx tmux-hook status +``` + +### Stale pane and team state cleanup + +Inspect panes: + +```bash +tmux list-panes -F '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}' +``` + +Kill stale worker panes only: + +```bash +tmux kill-pane -t % +``` + +Remove stale team state: + +```bash +rm -rf ".omx/state/team/" +``` + +## Failure Matrix + +| Symptom | Detection | Action | +| --- | --- | --- | +| `tmux_hook invalid_config` | `.omx/logs/tmux-hook-*.jsonl` | fix `.omx/tmux-hook.json`, revalidate | +| `omx team` fails on tmux/WSL prerequisites | command output | use preflight routing, fallback if mode `fallback_ralph` | +| startup without ACK | missing mailbox updates | resume/triage then controlled retry | +| non-terminal task counts at completion | `omx team status` | block shutdown until terminal gate | +| architect verification missing | no `--architect-*` evidence | block completion | +| fatal preflight blocker (`blocked`) | preflight exit code `4` | stop and fix prerequisite | + +## Done Checklist + +- [ ] Preflight routing executed and mode recorded. +- [ ] Team startup evidence captured (if team mode). +- [ ] Terminal task-state gate satisfied before shutdown. +- [ ] Fresh quality gates passed (`typecheck`, `test`, `build`, diagnostics). +- [ ] Architect verification recorded with tier + reference. +- [ ] Evidence file created under `.omx/evidence/`. +- [ ] `omx cancel` executed for Ralph cleanup. +- [ ] Team shutdown + cleanup verified (team mode only). + +## Command Reference + +```bash +# Preflight +npm run omx:preflight + +# Team execution +omx team ralph 6:executor "execute task: " +omx team status +omx team resume +omx team shutdown + +# Ralph fallback +omx ralph "execute task: " +omx cancel + +# Evidence +npm run omx:evidence -- --mode team --team --architect-tier standard --architect-ref "" +npm run omx:evidence -- --mode ralph --architect-tier standard --architect-ref "" +``` From 495dd3644c3a72e82cabdf3b3ed3eaf659b67af7 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:06:13 +0800 Subject: [PATCH 3/9] test(ops): cover preflight and evidence scripts Add deterministic tests for argument parsing, routing decisions, status parsing, and evidence file generation for the new OMX workflow scripts.\n\nCo-authored-by: Codex --- test/omx-evidence.test.ts | 85 +++++++++++++++++++++++++++++ test/omx-preflight.test.ts | 106 +++++++++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 test/omx-evidence.test.ts create mode 100644 test/omx-preflight.test.ts diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts new file mode 100644 index 00000000..a7776f25 --- /dev/null +++ b/test/omx-evidence.test.ts @@ -0,0 +1,85 @@ +import { describe, it, expect } from "vitest"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("omx-capture-evidence script", () => { + it("parses required args", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + expect( + mod.parseArgs([ + "--mode", + "ralph", + "--architect-tier", + "standard", + "--architect-ref", + "architect://run/123", + ]), + ).toEqual({ + mode: "ralph", + team: "", + architectTier: "standard", + architectRef: "architect://run/123", + architectNote: "", + output: "", + }); + }); + + it("requires architect args", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + expect(() => mod.parseArgs(["--mode", "ralph"])).toThrow("`--architect-tier` is required."); + }); + + it("parses team status counts from json and text", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + expect(mod.parseTeamCounts('{"task_counts":{"pending":0,"in_progress":0,"failed":1}}')).toEqual({ + pending: 0, + inProgress: 0, + failed: 1, + }); + expect(mod.parseTeamCounts("pending=2 in_progress=1 failed=0")).toEqual({ + pending: 2, + inProgress: 1, + failed: 0, + }); + }); + + it("writes evidence markdown when gates pass in ralph mode", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + const root = await mkdtemp(join(tmpdir(), "omx-evidence-")); + await writeFile(join(root, "package.json"), '{"name":"tmp"}', "utf8"); + + try { + const outputPath = join(root, ".omx", "evidence", "result.md"); + const result = mod.runEvidence( + { + mode: "ralph", + team: "", + architectTier: "standard", + architectRef: "architect://verdict/ok", + architectNote: "approved", + output: outputPath, + }, + { + cwd: root, + runCommand: (command: string, args: string[]) => { + if (command === "git" && args[0] === "rev-parse" && args[1] === "--abbrev-ref") { + return { command: "git rev-parse --abbrev-ref HEAD", code: 0, stdout: "feature/test", stderr: "" }; + } + if (command === "git" && args[0] === "rev-parse" && args[1] === "HEAD") { + return { command: "git rev-parse HEAD", code: 0, stdout: "abc123", stderr: "" }; + } + return { command: `${command} ${args.join(" ")}`, code: 0, stdout: "ok", stderr: "" }; + }, + }, + ); + + expect(result.overallPassed).toBe(true); + const markdown = await readFile(outputPath, "utf8"); + expect(markdown).toContain("## Overall Result: PASS"); + expect(markdown).toContain("architect://verdict/ok"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); diff --git a/test/omx-preflight.test.ts b/test/omx-preflight.test.ts new file mode 100644 index 00000000..db5d7072 --- /dev/null +++ b/test/omx-preflight.test.ts @@ -0,0 +1,106 @@ +import { describe, it, expect } from "vitest"; +import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; + +describe("omx-preflight-wsl2 script", () => { + it("parses cli args", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + expect(mod.parseArgs(["--json", "--distro", "Ubuntu"])).toEqual({ + json: true, + distro: "Ubuntu", + }); + }); + + it("throws on unknown args", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + expect(() => mod.parseArgs(["--wat"])).toThrow("Unknown option"); + }); + + it("normalizes WSL distro output that contains null chars", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + const output = "d\u0000o\u0000c\u0000k\u0000e\u0000r\u0000-\u0000d\u0000e\u0000s\u0000k\u0000t\u0000o\u0000p\u0000\r\n\u0000Ubuntu\r\n"; + expect(mod.parseDistroList(output)).toEqual(["docker-desktop", "Ubuntu"]); + }); + + it("routes to blocked when omx is missing on host", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + + const result = mod.runPreflight( + { distro: "" }, + { + platform: "win32", + cwd: process.cwd(), + existsSync: () => false, + readFileSync: () => "", + runProcess: (command: string, args: string[]) => { + if (command === "omx") return { code: 1, stdout: "", stderr: "missing" }; + if (command === "wsl" && args[0] === "-l") return { code: 0, stdout: "Ubuntu\n", stderr: "" }; + return { code: 0, stdout: "", stderr: "" }; + }, + }, + ); + + expect(result.mode).toBe("blocked"); + expect(result.exitCode).toBe(4); + }); + + it("routes to fallback when team-only prerequisites fail", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + + const result = mod.runPreflight( + { distro: "" }, + { + platform: "win32", + cwd: process.cwd(), + existsSync: () => false, + readFileSync: () => "", + runProcess: (command: string, args: string[]) => { + if (command === "omx") return { code: 0, stdout: "ok", stderr: "" }; + if (command === "wsl" && args[0] === "-l") return { code: 0, stdout: "docker-desktop\n", stderr: "" }; + return { code: 0, stdout: "", stderr: "" }; + }, + }, + ); + + expect(result.mode).toBe("fallback_ralph"); + expect(result.exitCode).toBe(3); + }); + + it("detects placeholder tmux hook pane target as fixable", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + const root = await mkdtemp(join(tmpdir(), "omx-preflight-")); + const omxDir = join(root, ".omx"); + await mkdir(omxDir, { recursive: true }); + await writeFile( + join(omxDir, "tmux-hook.json"), + JSON.stringify({ + enabled: true, + target: { type: "pane", value: "replace-with-tmux-pane-id" }, + }), + "utf8", + ); + + try { + const result = mod.runPreflight( + { distro: "" }, + { + platform: "linux", + cwd: root, + runProcess: (command: string, args: string[]) => { + if (command === "sh" && args.join(" ").includes("command -v tmux")) return { code: 0, stdout: "", stderr: "" }; + if (command === "sh" && args.join(" ").includes("omx team --help")) return { code: 0, stdout: "", stderr: "" }; + if (command === "sh" && args.join(" ").includes("${TMUX:-}")) return { code: 0, stdout: "", stderr: "" }; + if (command === "omx") return { code: 0, stdout: "ok", stderr: "" }; + return { code: 0, stdout: "", stderr: "" }; + }, + }, + ); + + expect(result.mode).toBe("team_blocked"); + expect(result.checks.some((entry: { name: string; status: string }) => entry.name === "tmux hook pane target" && entry.status === "fail")).toBe(true); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); +}); From 56e2e136559410dc9fec9e27af0c2e865973ed44 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:14:37 +0800 Subject: [PATCH 4/9] fix(ops): resolve architect blockers in preflight gates Remove false-positive windows blockers, harden ralph cleanup evidence gating, and update tests/docs to match deterministic routing behavior.\n\nCo-authored-by: Codex --- docs/development/OMX_TEAM_RALPH_PLAYBOOK.md | 10 ++++- scripts/omx-capture-evidence.js | 31 +++++++++++++- scripts/omx-preflight-wsl2.js | 30 ++++++++++--- test/omx-evidence.test.ts | 47 ++++++++++++++++++++- test/omx-preflight.test.ts | 25 ++++++++++- 5 files changed, 130 insertions(+), 13 deletions(-) diff --git a/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md index 0a2df17e..bfa9715a 100644 --- a/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md +++ b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md @@ -93,7 +93,7 @@ npm run omx:preflight -- --distro Ubuntu | Exit Code | Mode | Meaning | Required Action | | --- | --- | --- | --- | | `0` | `team_ready` | Team prerequisites are satisfied | Continue with team mode | -| `2` | `team_blocked` | Fixable blockers (for example tmux session or hook config) | Fix blockers, rerun preflight | +| `2` | `team_blocked` | Fixable blockers (for example hook config) | Fix blockers, rerun preflight | | `3` | `fallback_ralph` | Team-only prerequisites failed | Execute controlled Ralph fallback | | `4` | `blocked` | Fatal blocker for both team and fallback (for example missing `omx`) | Stop and fix fatal prerequisite | | `1` | script error | Invocation/runtime failure | Fix command/environment | @@ -144,6 +144,12 @@ Capture evidence before shutdown/handoff: npm run omx:evidence -- --mode team --team --architect-tier standard --architect-ref "" --architect-note "" ``` +Ralph cleanup before fallback evidence: + +```bash +omx cancel +``` + Fallback evidence: ```bash @@ -256,7 +262,7 @@ rm -rf ".omx/state/team/" - [ ] Fresh quality gates passed (`typecheck`, `test`, `build`, diagnostics). - [ ] Architect verification recorded with tier + reference. - [ ] Evidence file created under `.omx/evidence/`. -- [ ] `omx cancel` executed for Ralph cleanup. +- [ ] Ralph cleanup state is inactive in evidence output (`omx cancel` done before final ralph evidence). - [ ] Team shutdown + cleanup verified (team mode only). ## Command Reference diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js index 14ef7976..5d88c69d 100644 --- a/scripts/omx-capture-evidence.js +++ b/scripts/omx-capture-evidence.js @@ -1,6 +1,6 @@ #!/usr/bin/env node -import { existsSync, mkdirSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; @@ -182,6 +182,25 @@ function ensureRepoRoot(cwd) { } } +function checkRalphCleanup(cwd) { + const statePath = join(cwd, ".omx", "state", "ralph-state.json"); + if (!existsSync(statePath)) { + return { passed: true, detail: "ralph state file not present (treated as cleaned)." }; + } + + try { + const parsed = JSON.parse(readFileSync(statePath, "utf8")); + const active = parsed && typeof parsed === "object" && "active" in parsed ? parsed.active : undefined; + const phase = parsed && typeof parsed === "object" && "current_phase" in parsed ? parsed.current_phase : undefined; + if (active === false) { + return { passed: true, detail: `ralph state inactive${phase ? ` (${String(phase)})` : ""}.` }; + } + return { passed: false, detail: "ralph state is still active; run `omx cancel` before final evidence capture." }; + } catch { + return { passed: false, detail: "ralph state file unreadable; fix state file or run `omx cancel`." }; + } +} + function buildOutputPath(options, cwd, runId) { if (options.output) return options.output; const filename = `${runId}-${options.mode}-evidence.md`; @@ -224,6 +243,8 @@ export function runEvidence(options, deps = {}) { teamCounts.failed === 0 : true; + const ralphCleanup = options.mode === "ralph" ? checkRalphCleanup(cwd) : { passed: true, detail: "Not applicable (mode=team)" }; + const architectPassed = options.architectTier.trim().length > 0 && options.architectRef.trim().length > 0; const gates = [ @@ -246,6 +267,11 @@ export function runEvidence(options, deps = {}) { passed: architectPassed, detail: `tier=${options.architectTier}; ref=${options.architectRef}`, }, + { + name: "Ralph cleanup state", + passed: ralphCleanup.passed, + detail: ralphCleanup.detail, + }, ]; const overallPassed = @@ -254,7 +280,8 @@ export function runEvidence(options, deps = {}) { build.code === 0 && diagnostics.code === 0 && teamStatePassed && - architectPassed; + architectPassed && + ralphCleanup.passed; const runId = nowStamp(); const outputPath = buildOutputPath(options, cwd, runId); diff --git a/scripts/omx-preflight-wsl2.js b/scripts/omx-preflight-wsl2.js index 88a9e056..1f078a9c 100644 --- a/scripts/omx-preflight-wsl2.js +++ b/scripts/omx-preflight-wsl2.js @@ -92,6 +92,21 @@ function checkOmxOnHost(checks, runner) { } } +function checkOmxOnHostAdvisory(checks, runner) { + const omxHelp = runner(getShellCommand("omx"), ["--help"]); + if (omxHelp.code === 0) { + addCheck(checks, "pass", "info", "omx host runtime", "omx is available in current host runtime."); + } else { + addCheck( + checks, + "warn", + "info", + "omx host runtime", + "omx is not available on host. Team mode can still run in WSL; fallback should run via WSL omx.", + ); + } +} + function checkHookConfig(checks, cwd, fsDeps) { const hookPath = join(cwd, ".omx", "tmux-hook.json"); if (!fsDeps.existsSync(hookPath)) { @@ -127,7 +142,7 @@ function checkHookConfig(checks, cwd, fsDeps) { } function runWindowsChecks(checks, requestedDistro, runner) { - checkOmxOnHost(checks, runner); + checkOmxOnHostAdvisory(checks, runner); const wsl = runner("wsl", ["-l", "-q"]); if (wsl.code !== 0) { @@ -182,12 +197,13 @@ function runWindowsChecks(checks, requestedDistro, runner) { addCheck(checks, "fail", "team_hard", "omx team in WSL", "omx team --help failed in selected distro."); } - const tmuxSession = runInWsl("[ -n \"${TMUX:-}\" ]"); - if (tmuxSession.code === 0) { - addCheck(checks, "pass", "info", "tmux leader session", "Current WSL shell is inside tmux."); - } else { - addCheck(checks, "fail", "fixable", "tmux leader session", "Attach/start tmux in WSL before running omx team."); - } + addCheck( + checks, + "warn", + "info", + "tmux leader session check", + "Windows preflight cannot reliably assert existing tmux attachment. Rerun preflight from inside WSL tmux session before team launch.", + ); return { distro: selectedDistro }; } diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts index a7776f25..987440c3 100644 --- a/test/omx-evidence.test.ts +++ b/test/omx-evidence.test.ts @@ -1,5 +1,5 @@ import { describe, it, expect } from "vitest"; -import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -82,4 +82,49 @@ describe("omx-capture-evidence script", () => { await rm(root, { recursive: true, force: true }); } }); + + it("fails ralph mode evidence when cleanup state is still active", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + const root = await mkdtemp(join(tmpdir(), "omx-evidence-active-")); + await writeFile(join(root, "package.json"), '{"name":"tmp"}', "utf8"); + await mkdir(join(root, ".omx", "state"), { recursive: true }); + await writeFile( + join(root, ".omx", "state", "ralph-state.json"), + JSON.stringify({ active: true, current_phase: "executing" }), + "utf8", + ); + + try { + const outputPath = join(root, ".omx", "evidence", "result-active.md"); + const result = mod.runEvidence( + { + mode: "ralph", + team: "", + architectTier: "standard", + architectRef: "architect://verdict/ok", + architectNote: "", + output: outputPath, + }, + { + cwd: root, + runCommand: (command: string, args: string[]) => { + if (command === "git" && args[0] === "rev-parse" && args[1] === "--abbrev-ref") { + return { command: "git rev-parse --abbrev-ref HEAD", code: 0, stdout: "feature/test", stderr: "" }; + } + if (command === "git" && args[0] === "rev-parse" && args[1] === "HEAD") { + return { command: "git rev-parse HEAD", code: 0, stdout: "abc123", stderr: "" }; + } + return { command: `${command} ${args.join(" ")}`, code: 0, stdout: "ok", stderr: "" }; + }, + }, + ); + + expect(result.overallPassed).toBe(false); + const markdown = await readFile(outputPath, "utf8"); + expect(markdown).toContain("Ralph cleanup state"); + expect(markdown).toContain("FAIL"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); }); diff --git a/test/omx-preflight.test.ts b/test/omx-preflight.test.ts index db5d7072..523a3546 100644 --- a/test/omx-preflight.test.ts +++ b/test/omx-preflight.test.ts @@ -23,7 +23,7 @@ describe("omx-preflight-wsl2 script", () => { expect(mod.parseDistroList(output)).toEqual(["docker-desktop", "Ubuntu"]); }); - it("routes to blocked when omx is missing on host", async () => { + it("warns on missing host omx in windows mode when WSL checks pass", async () => { const mod = await import("../scripts/omx-preflight-wsl2.js"); const result = mod.runPreflight( @@ -36,6 +36,29 @@ describe("omx-preflight-wsl2 script", () => { runProcess: (command: string, args: string[]) => { if (command === "omx") return { code: 1, stdout: "", stderr: "missing" }; if (command === "wsl" && args[0] === "-l") return { code: 0, stdout: "Ubuntu\n", stderr: "" }; + if (command === "wsl" && args[0] === "-d") return { code: 0, stdout: "", stderr: "" }; + return { code: 0, stdout: "", stderr: "" }; + }, + }, + ); + + expect(result.mode).toBe("team_ready"); + expect(result.exitCode).toBe(0); + expect(result.checks.some((entry: { name: string; status: string }) => entry.name === "omx host runtime" && entry.status === "warn")).toBe(true); + }); + + it("routes to blocked when omx is missing on unix host", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + + const result = mod.runPreflight( + { distro: "" }, + { + platform: "linux", + cwd: process.cwd(), + existsSync: () => false, + readFileSync: () => "", + runProcess: (command: string) => { + if (command === "omx") return { code: 1, stdout: "", stderr: "missing" }; return { code: 0, stdout: "", stderr: "" }; }, }, From 18232cf239810a86921bd03a3bc3a748b7e8cefe Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 08:20:45 +0800 Subject: [PATCH 5/9] fix(ops): block impossible fallback routing Add combined fatal gate for windows preflight when omx is unavailable in both host and WSL runtimes, and add regression coverage for this matrix.\n\nCo-authored-by: Codex --- docs/development/OMX_TEAM_RALPH_PLAYBOOK.md | 2 +- scripts/omx-preflight-wsl2.js | 34 +++++++++++++++------ test/omx-preflight.test.ts | 28 +++++++++++++++++ 3 files changed, 54 insertions(+), 10 deletions(-) diff --git a/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md index bfa9715a..620c7473 100644 --- a/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md +++ b/docs/development/OMX_TEAM_RALPH_PLAYBOOK.md @@ -95,7 +95,7 @@ npm run omx:preflight -- --distro Ubuntu | `0` | `team_ready` | Team prerequisites are satisfied | Continue with team mode | | `2` | `team_blocked` | Fixable blockers (for example hook config) | Fix blockers, rerun preflight | | `3` | `fallback_ralph` | Team-only prerequisites failed | Execute controlled Ralph fallback | -| `4` | `blocked` | Fatal blocker for both team and fallback (for example missing `omx`) | Stop and fix fatal prerequisite | +| `4` | `blocked` | Fatal blocker for both team and fallback (for example `omx` missing in both host and WSL runtimes) | Stop and fix fatal prerequisite | | `1` | script error | Invocation/runtime failure | Fix command/environment | ### Phase 5 - Ralph Execution Loop diff --git a/scripts/omx-preflight-wsl2.js b/scripts/omx-preflight-wsl2.js index 1f078a9c..735a284e 100644 --- a/scripts/omx-preflight-wsl2.js +++ b/scripts/omx-preflight-wsl2.js @@ -96,15 +96,16 @@ function checkOmxOnHostAdvisory(checks, runner) { const omxHelp = runner(getShellCommand("omx"), ["--help"]); if (omxHelp.code === 0) { addCheck(checks, "pass", "info", "omx host runtime", "omx is available in current host runtime."); - } else { - addCheck( - checks, - "warn", - "info", - "omx host runtime", - "omx is not available on host. Team mode can still run in WSL; fallback should run via WSL omx.", - ); + return true; } + addCheck( + checks, + "warn", + "info", + "omx host runtime", + "omx is not available on host. Team mode can still run in WSL; fallback should run via WSL omx.", + ); + return false; } function checkHookConfig(checks, cwd, fsDeps) { @@ -142,23 +143,33 @@ function checkHookConfig(checks, cwd, fsDeps) { } function runWindowsChecks(checks, requestedDistro, runner) { - checkOmxOnHostAdvisory(checks, runner); + const hostOmxAvailable = checkOmxOnHostAdvisory(checks, runner); + let wslOmxAvailable = false; const wsl = runner("wsl", ["-l", "-q"]); if (wsl.code !== 0) { addCheck(checks, "fail", "team_hard", "wsl availability", "WSL unavailable. Team mode requires WSL2 or Unix host."); + if (!hostOmxAvailable) { + addCheck(checks, "fail", "fatal", "omx runtime availability", "omx is unavailable in both host and WSL runtimes."); + } return { distro: "" }; } const allDistros = parseDistroList(wsl.stdout); if (allDistros.length === 0) { addCheck(checks, "fail", "team_hard", "wsl distros", "No WSL distro found."); + if (!hostOmxAvailable) { + addCheck(checks, "fail", "fatal", "omx runtime availability", "omx is unavailable in both host and WSL runtimes."); + } return { distro: "" }; } const usableDistros = allDistros.filter((name) => !/^docker-desktop(-data)?$/i.test(name)); if (usableDistros.length === 0) { addCheck(checks, "fail", "team_hard", "usable distro", "Only Docker Desktop distros found. Install Ubuntu or another Linux distro."); + if (!hostOmxAvailable) { + addCheck(checks, "fail", "fatal", "omx runtime availability", "omx is unavailable in both host and WSL runtimes."); + } return { distro: "" }; } @@ -185,6 +196,7 @@ function runWindowsChecks(checks, requestedDistro, runner) { const omx = runInWsl("command -v omx >/dev/null 2>&1"); if (omx.code === 0) { + wslOmxAvailable = true; addCheck(checks, "pass", "info", "omx in WSL", "omx is available in selected distro."); } else { addCheck(checks, "fail", "team_hard", "omx in WSL", "Install/enable omx inside selected distro."); @@ -205,6 +217,10 @@ function runWindowsChecks(checks, requestedDistro, runner) { "Windows preflight cannot reliably assert existing tmux attachment. Rerun preflight from inside WSL tmux session before team launch.", ); + if (!hostOmxAvailable && !wslOmxAvailable) { + addCheck(checks, "fail", "fatal", "omx runtime availability", "omx is unavailable in both host and WSL runtimes."); + } + return { distro: selectedDistro }; } diff --git a/test/omx-preflight.test.ts b/test/omx-preflight.test.ts index 523a3546..e69e49ce 100644 --- a/test/omx-preflight.test.ts +++ b/test/omx-preflight.test.ts @@ -90,6 +90,34 @@ describe("omx-preflight-wsl2 script", () => { expect(result.exitCode).toBe(3); }); + it("routes to blocked on windows when omx is missing in host and WSL", async () => { + const mod = await import("../scripts/omx-preflight-wsl2.js"); + + const result = mod.runPreflight( + { distro: "" }, + { + platform: "win32", + cwd: process.cwd(), + existsSync: () => false, + readFileSync: () => "", + runProcess: (command: string, args: string[]) => { + if (command === "omx") return { code: 1, stdout: "", stderr: "missing" }; + if (command === "wsl" && args[0] === "-l") return { code: 0, stdout: "Ubuntu\n", stderr: "" }; + if (command === "wsl" && args[0] === "-d") { + if (args.join(" ").includes("command -v omx")) return { code: 1, stdout: "", stderr: "missing" }; + if (args.join(" ").includes("command -v tmux")) return { code: 0, stdout: "", stderr: "" }; + if (args.join(" ").includes("omx team --help")) return { code: 1, stdout: "", stderr: "missing" }; + } + return { code: 0, stdout: "", stderr: "" }; + }, + }, + ); + + expect(result.mode).toBe("blocked"); + expect(result.exitCode).toBe(4); + expect(result.checks.some((entry: { name: string; severity: string }) => entry.name === "omx runtime availability" && entry.severity === "fatal")).toBe(true); + }); + it("detects placeholder tmux hook pane target as fixable", async () => { const mod = await import("../scripts/omx-preflight-wsl2.js"); const root = await mkdtemp(join(tmpdir(), "omx-preflight-")); From 2cf5ec43b0cc512d162792bc1814d81b780939c2 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:04:03 +0800 Subject: [PATCH 6/9] fix(ops): harden evidence capture for Windows locks Add retry-on-EBUSY/EPERM writes for evidence files, redact sensitive command output before persistence, and cover the retry path with a 100-write regression test. Co-authored-by: Codex --- scripts/omx-capture-evidence.js | 85 ++++++++++++++++++++++++++++- test/omx-evidence.test.ts | 94 +++++++++++++++++++++++++++++++++ 2 files changed, 177 insertions(+), 2 deletions(-) diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js index 5d88c69d..d1a95d22 100644 --- a/scripts/omx-capture-evidence.js +++ b/scripts/omx-capture-evidence.js @@ -6,6 +6,9 @@ import { fileURLToPath } from "node:url"; import { spawnSync } from "node:child_process"; const __filename = fileURLToPath(import.meta.url); +const REDACTION_PLACEHOLDER = "***REDACTED***"; +const WRITE_RETRY_ATTEMPTS = 6; +const WRITE_RETRY_BASE_DELAY_MS = 40; function normalizePathForCompare(path) { const resolved = resolve(path); @@ -125,6 +128,45 @@ function clampText(text, maxLength = 12000) { return `${text.slice(0, maxLength)}\n...[truncated]`; } +export function redactSensitiveText(text) { + let redacted = text; + const replacementRules = [ + { + pattern: /\b(Authorization\s*:\s*Bearer\s+)([^\s\r\n]+)/gi, + replace: (_match, prefix, _secret) => `${prefix}${REDACTION_PLACEHOLDER}`, + }, + { + pattern: /("(?:token|secret|password|api[_-]?key|authorization|access_token)"\s*:\s*")([^"]+)(")/gi, + replace: (_match, start, _secret, end) => `${start}${REDACTION_PLACEHOLDER}${end}`, + }, + { + pattern: /\b((?:token|secret|password|api[_-]?key|authorization|access_token)\b[^\S\r\n]*[:=][^\S\r\n]*)([^\s\r\n]+)/gi, + replace: (_match, prefix, _secret) => `${prefix}${REDACTION_PLACEHOLDER}`, + }, + { + pattern: /\b(Bearer\s+)([A-Za-z0-9._~+/=-]+)/gi, + replace: (_match, prefix, _secret) => `${prefix}${REDACTION_PLACEHOLDER}`, + }, + { + pattern: /([?&](?:token|api[_-]?key|access_token|password)=)([^&\s]+)/gi, + replace: (_match, prefix, _secret) => `${prefix}${REDACTION_PLACEHOLDER}`, + }, + { + pattern: /\bsk-[A-Za-z0-9]{20,}\b/g, + replace: REDACTION_PLACEHOLDER, + }, + { + pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, + replace: REDACTION_PLACEHOLDER, + }, + ]; + + for (const rule of replacementRules) { + redacted = redacted.replace(rule.pattern, rule.replace); + } + return redacted; +} + function parseCount(text, keyAliases) { for (const key of keyAliases) { const patterns = [ @@ -172,7 +214,43 @@ export function parseTeamCounts(statusOutput) { function formatOutput(result) { const combined = [result.stdout, result.stderr].filter((value) => value.length > 0).join("\n"); if (!combined) return "(no output)"; - return clampText(combined); + return clampText(redactSensitiveText(combined)); +} + +function getErrorCode(error) { + if (error && typeof error === "object" && "code" in error && typeof error.code === "string") { + return error.code; + } + return ""; +} + +function isRetryableWriteError(error) { + const code = getErrorCode(error); + return code === "EBUSY" || code === "EPERM"; +} + +function sleepSync(milliseconds) { + const waitMs = Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : 0; + if (waitMs === 0) return; + Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs); +} + +export function writeFileWithRetry(outputPath, content, deps = {}) { + const writeFn = deps.writeFileSyncFn ?? writeFileSync; + const sleepFn = deps.sleepSyncFn ?? sleepSync; + const maxAttempts = Number.isInteger(deps.maxAttempts) ? deps.maxAttempts : WRITE_RETRY_ATTEMPTS; + const baseDelayMs = Number.isFinite(deps.baseDelayMs) ? deps.baseDelayMs : WRITE_RETRY_BASE_DELAY_MS; + + for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { + try { + writeFn(outputPath, content, "utf8"); + return; + } catch (error) { + const isRetryable = isRetryableWriteError(error); + if (!isRetryable || attempt === maxAttempts) throw error; + sleepFn(baseDelayMs * attempt); + } + } } function ensureRepoRoot(cwd) { @@ -307,6 +385,9 @@ export function runEvidence(options, deps = {}) { lines.push(""); lines.push(`## Overall Result: ${overallPassed ? "PASS" : "FAIL"}`); lines.push(""); + lines.push("## Redaction Strategy"); + lines.push(`- Command output is sanitized before writing evidence; keys matching token/secret/password/api key patterns are replaced with ${REDACTION_PLACEHOLDER}.`); + lines.push(""); lines.push("## Command Output"); const commandResults = [ @@ -334,7 +415,7 @@ export function runEvidence(options, deps = {}) { lines.push("```"); lines.push(""); - writeFileSync(outputPath, lines.join("\n"), "utf8"); + writeFileWithRetry(outputPath, lines.join("\n")); return { overallPassed, outputPath }; } diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts index 987440c3..9d171ebf 100644 --- a/test/omx-evidence.test.ts +++ b/test/omx-evidence.test.ts @@ -1,4 +1,5 @@ import { describe, it, expect } from "vitest"; +import { writeFileSync } from "node:fs"; import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; @@ -44,6 +45,99 @@ describe("omx-capture-evidence script", () => { }); }); + it("redacts sensitive command output before writing evidence", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + const root = await mkdtemp(join(tmpdir(), "omx-evidence-redaction-")); + await writeFile(join(root, "package.json"), '{"name":"tmp"}', "utf8"); + + try { + const outputPath = join(root, ".omx", "evidence", "redacted.md"); + mod.runEvidence( + { + mode: "ralph", + team: "", + architectTier: "standard", + architectRef: "architect://verdict/ok", + architectNote: "", + output: outputPath, + }, + { + cwd: root, + runCommand: (command: string, args: string[]) => { + if (command === "git" && args[0] === "rev-parse" && args[1] === "--abbrev-ref") { + return { command: "git rev-parse --abbrev-ref HEAD", code: 0, stdout: "feature/test", stderr: "" }; + } + if (command === "git" && args[0] === "rev-parse" && args[1] === "HEAD") { + return { command: "git rev-parse HEAD", code: 0, stdout: "abc123", stderr: "" }; + } + return { + command: `${command} ${args.join(" ")}`, + code: 0, + stdout: "token=secret-value Authorization: Bearer bearer-value sk-1234567890123456789012", + stderr: "", + }; + }, + }, + ); + + const markdown = await readFile(outputPath, "utf8"); + expect(markdown).toContain("***REDACTED***"); + expect(markdown).not.toContain("secret-value"); + expect(markdown).not.toContain("bearer-value"); + expect(markdown).toContain("## Redaction Strategy"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + + it("handles 100 concurrent retry-prone writes without EBUSY throw", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + const root = await mkdtemp(join(tmpdir(), "omx-evidence-concurrency-")); + const sharedPath = join(root, "shared-evidence.md"); + const seenPayloadAttempts = new Map(); + + const makeBusyError = () => { + const error = new Error("file busy"); + Object.assign(error, { code: "EBUSY" }); + return error; + }; + + try { + const concurrencyCount = 100; + const writes = Array.from({ length: concurrencyCount }, (_value, index) => { + return new Promise((resolve, reject) => { + setTimeout(() => { + try { + mod.writeFileWithRetry(sharedPath, `write-${index}`, { + writeFileSyncFn: (path: string, content: string, encoding: BufferEncoding) => { + const attempts = seenPayloadAttempts.get(content) ?? 0; + if (attempts === 0) { + seenPayloadAttempts.set(content, 1); + throw makeBusyError(); + } + seenPayloadAttempts.set(content, attempts + 1); + writeFileSync(path, content, encoding); + }, + sleepSyncFn: () => undefined, + maxAttempts: 5, + baseDelayMs: 0, + }); + resolve(); + } catch (error) { + reject(error); + } + }, 0); + }); + }); + + await expect(Promise.all(writes)).resolves.toHaveLength(concurrencyCount); + const finalContent = await readFile(sharedPath, "utf8"); + expect(finalContent.startsWith("write-")).toBe(true); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it("writes evidence markdown when gates pass in ralph mode", async () => { const mod = await import("../scripts/omx-capture-evidence.js"); const root = await mkdtemp(join(tmpdir(), "omx-evidence-")); From 871168741e90821aacb734e25d09af96c94a8df7 Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:15:45 +0800 Subject: [PATCH 7/9] fix(ops): replace Atomics wait in retry sleep path Use a synchronous deadline-based sleep fallback in evidence write retry logic and add a regression test that exercises retry behavior with the built-in sleep implementation. Co-authored-by: Codex --- scripts/omx-capture-evidence.js | 5 ++++- test/omx-evidence.test.ts | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 1 deletion(-) diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js index d1a95d22..4b5a3dde 100644 --- a/scripts/omx-capture-evidence.js +++ b/scripts/omx-capture-evidence.js @@ -232,7 +232,10 @@ function isRetryableWriteError(error) { function sleepSync(milliseconds) { const waitMs = Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : 0; if (waitMs === 0) return; - Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, waitMs); + const deadline = Date.now() + waitMs; + while (Date.now() < deadline) { + // Busy-wait fallback keeps retry logic synchronous and avoids Atomics.wait main-thread restrictions. + } } export function writeFileWithRetry(outputPath, content, deps = {}) { diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts index 9d171ebf..796582b7 100644 --- a/test/omx-evidence.test.ts +++ b/test/omx-evidence.test.ts @@ -138,6 +138,39 @@ describe("omx-capture-evidence script", () => { } }); + it("retries EBUSY with built-in sleep implementation", async () => { + const mod = await import("../scripts/omx-capture-evidence.js"); + const root = await mkdtemp(join(tmpdir(), "omx-evidence-sleep-")); + const outputPath = join(root, "retry-output.md"); + let calls = 0; + + const makeBusyError = () => { + const error = new Error("file busy"); + Object.assign(error, { code: "EBUSY" }); + return error; + }; + + try { + expect(() => { + mod.writeFileWithRetry(outputPath, "content", { + writeFileSyncFn: (path: string, content: string, encoding: BufferEncoding) => { + calls += 1; + if (calls === 1) throw makeBusyError(); + writeFileSync(path, content, encoding); + }, + maxAttempts: 3, + baseDelayMs: 1, + }); + }).not.toThrow(); + + expect(calls).toBe(2); + const fileContent = await readFile(outputPath, "utf8"); + expect(fileContent).toBe("content"); + } finally { + await rm(root, { recursive: true, force: true }); + } + }); + it("writes evidence markdown when gates pass in ralph mode", async () => { const mod = await import("../scripts/omx-capture-evidence.js"); const root = await mkdtemp(join(tmpdir(), "omx-evidence-")); From a0d3854fc07c35673e82590182e6e6f2d95bfbbb Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:30:30 +0800 Subject: [PATCH 8/9] fix(ops): make evidence retry backoff non-busy and async Refactor evidence write retry to use non-blocking timer delays instead of CPU-intensive busy waits, and update evidence tests to await async retry and run-evidence flows. Co-authored-by: Codex --- scripts/omx-capture-evidence.js | 31 +++++++++---------- test/omx-evidence.test.ts | 53 +++++++++++++++------------------ 2 files changed, 38 insertions(+), 46 deletions(-) diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js index 4b5a3dde..dce99f85 100644 --- a/scripts/omx-capture-evidence.js +++ b/scripts/omx-capture-evidence.js @@ -229,18 +229,17 @@ function isRetryableWriteError(error) { return code === "EBUSY" || code === "EPERM"; } -function sleepSync(milliseconds) { +function sleep(milliseconds) { const waitMs = Number.isFinite(milliseconds) && milliseconds > 0 ? milliseconds : 0; - if (waitMs === 0) return; - const deadline = Date.now() + waitMs; - while (Date.now() < deadline) { - // Busy-wait fallback keeps retry logic synchronous and avoids Atomics.wait main-thread restrictions. - } + if (waitMs === 0) return Promise.resolve(); + return new Promise((resolve) => { + setTimeout(resolve, waitMs); + }); } -export function writeFileWithRetry(outputPath, content, deps = {}) { +export async function writeFileWithRetry(outputPath, content, deps = {}) { const writeFn = deps.writeFileSyncFn ?? writeFileSync; - const sleepFn = deps.sleepSyncFn ?? sleepSync; + const sleepFn = deps.sleepFn ?? sleep; const maxAttempts = Number.isInteger(deps.maxAttempts) ? deps.maxAttempts : WRITE_RETRY_ATTEMPTS; const baseDelayMs = Number.isFinite(deps.baseDelayMs) ? deps.baseDelayMs : WRITE_RETRY_BASE_DELAY_MS; @@ -251,7 +250,7 @@ export function writeFileWithRetry(outputPath, content, deps = {}) { } catch (error) { const isRetryable = isRetryableWriteError(error); if (!isRetryable || attempt === maxAttempts) throw error; - sleepFn(baseDelayMs * attempt); + await sleepFn(baseDelayMs * attempt); } } } @@ -288,7 +287,7 @@ function buildOutputPath(options, cwd, runId) { return join(cwd, ".omx", "evidence", filename); } -export function runEvidence(options, deps = {}) { +export async function runEvidence(options, deps = {}) { const cwd = deps.cwd ?? process.cwd(); ensureRepoRoot(cwd); @@ -418,13 +417,13 @@ export function runEvidence(options, deps = {}) { lines.push("```"); lines.push(""); - writeFileWithRetry(outputPath, lines.join("\n")); + await writeFileWithRetry(outputPath, lines.join("\n")); return { overallPassed, outputPath }; } -export function main(argv = process.argv.slice(2)) { +export async function main(argv = process.argv.slice(2)) { const options = parseArgs(argv); - const result = runEvidence(options); + const result = await runEvidence(options); if (result.overallPassed) { console.log(`Evidence captured at ${result.outputPath}`); console.log("All gates passed."); @@ -436,11 +435,9 @@ export function main(argv = process.argv.slice(2)) { } if (isDirectRun) { - try { - main(); - } catch (error) { + main().catch((error) => { console.error("Failed to capture evidence."); console.error(error instanceof Error ? error.message : String(error)); process.exit(1); - } + }); } diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts index 796582b7..ea97640a 100644 --- a/test/omx-evidence.test.ts +++ b/test/omx-evidence.test.ts @@ -52,7 +52,7 @@ describe("omx-capture-evidence script", () => { try { const outputPath = join(root, ".omx", "evidence", "redacted.md"); - mod.runEvidence( + await mod.runEvidence( { mode: "ralph", team: "", @@ -105,29 +105,24 @@ describe("omx-capture-evidence script", () => { try { const concurrencyCount = 100; const writes = Array.from({ length: concurrencyCount }, (_value, index) => { - return new Promise((resolve, reject) => { - setTimeout(() => { - try { - mod.writeFileWithRetry(sharedPath, `write-${index}`, { - writeFileSyncFn: (path: string, content: string, encoding: BufferEncoding) => { - const attempts = seenPayloadAttempts.get(content) ?? 0; - if (attempts === 0) { - seenPayloadAttempts.set(content, 1); - throw makeBusyError(); - } - seenPayloadAttempts.set(content, attempts + 1); - writeFileSync(path, content, encoding); - }, - sleepSyncFn: () => undefined, - maxAttempts: 5, - baseDelayMs: 0, - }); - resolve(); - } catch (error) { - reject(error); - } - }, 0); - }); + return new Promise((resolve) => { + setTimeout(resolve, 0); + }).then(() => + mod.writeFileWithRetry(sharedPath, `write-${index}`, { + writeFileSyncFn: (path: string, content: string, encoding: BufferEncoding) => { + const attempts = seenPayloadAttempts.get(content) ?? 0; + if (attempts === 0) { + seenPayloadAttempts.set(content, 1); + throw makeBusyError(); + } + seenPayloadAttempts.set(content, attempts + 1); + writeFileSync(path, content, encoding); + }, + sleepFn: async () => Promise.resolve(), + maxAttempts: 5, + baseDelayMs: 0, + }), + ); }); await expect(Promise.all(writes)).resolves.toHaveLength(concurrencyCount); @@ -151,7 +146,7 @@ describe("omx-capture-evidence script", () => { }; try { - expect(() => { + await expect( mod.writeFileWithRetry(outputPath, "content", { writeFileSyncFn: (path: string, content: string, encoding: BufferEncoding) => { calls += 1; @@ -160,8 +155,8 @@ describe("omx-capture-evidence script", () => { }, maxAttempts: 3, baseDelayMs: 1, - }); - }).not.toThrow(); + }), + ).resolves.toBeUndefined(); expect(calls).toBe(2); const fileContent = await readFile(outputPath, "utf8"); @@ -178,7 +173,7 @@ describe("omx-capture-evidence script", () => { try { const outputPath = join(root, ".omx", "evidence", "result.md"); - const result = mod.runEvidence( + const result = await mod.runEvidence( { mode: "ralph", team: "", @@ -223,7 +218,7 @@ describe("omx-capture-evidence script", () => { try { const outputPath = join(root, ".omx", "evidence", "result-active.md"); - const result = mod.runEvidence( + const result = await mod.runEvidence( { mode: "ralph", team: "", From 401a146755601a893b53365c592e40fd2d1f201c Mon Sep 17 00:00:00 2001 From: Neil Daquioag <405533+ndycode@users.noreply.github.com> Date: Sun, 1 Mar 2026 13:38:57 +0800 Subject: [PATCH 9/9] fix(ops): extend evidence redaction for AWS credentials Add explicit AWS access key and AWS secret access key redaction patterns, update the evidence redaction strategy note, and extend redaction tests to cover both AWS credential forms. Co-authored-by: Codex --- scripts/omx-capture-evidence.js | 12 +++++++++++- test/omx-evidence.test.ts | 5 ++++- 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/scripts/omx-capture-evidence.js b/scripts/omx-capture-evidence.js index dce99f85..3ec38f54 100644 --- a/scripts/omx-capture-evidence.js +++ b/scripts/omx-capture-evidence.js @@ -159,6 +159,14 @@ export function redactSensitiveText(text) { pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g, replace: REDACTION_PLACEHOLDER, }, + { + pattern: /\b(?:AKIA|ASIA)[A-Z0-9]{16}\b/g, + replace: REDACTION_PLACEHOLDER, + }, + { + pattern: /\b(AWS_SECRET_ACCESS_KEY\b[^\S\r\n]*[:=][^\S\r\n]*)([A-Za-z0-9/+=]{40})\b/gi, + replace: (_match, prefix, _secret) => `${prefix}${REDACTION_PLACEHOLDER}`, + }, ]; for (const rule of replacementRules) { @@ -388,7 +396,9 @@ export async function runEvidence(options, deps = {}) { lines.push(`## Overall Result: ${overallPassed ? "PASS" : "FAIL"}`); lines.push(""); lines.push("## Redaction Strategy"); - lines.push(`- Command output is sanitized before writing evidence; keys matching token/secret/password/api key patterns are replaced with ${REDACTION_PLACEHOLDER}.`); + lines.push( + `- Command output is sanitized before writing evidence; token/secret/password/api key patterns, GitHub/OpenAI tokens, and AWS key formats are replaced with ${REDACTION_PLACEHOLDER}.`, + ); lines.push(""); lines.push("## Command Output"); diff --git a/test/omx-evidence.test.ts b/test/omx-evidence.test.ts index ea97640a..4d16a277 100644 --- a/test/omx-evidence.test.ts +++ b/test/omx-evidence.test.ts @@ -73,7 +73,8 @@ describe("omx-capture-evidence script", () => { return { command: `${command} ${args.join(" ")}`, code: 0, - stdout: "token=secret-value Authorization: Bearer bearer-value sk-1234567890123456789012", + stdout: + "token=secret-value Authorization: Bearer bearer-value sk-1234567890123456789012 AKIA1234567890ABCDEF AWS_SECRET_ACCESS_KEY=abcdABCD0123abcdABCD0123abcdABCD0123abcd", stderr: "", }; }, @@ -84,6 +85,8 @@ describe("omx-capture-evidence script", () => { expect(markdown).toContain("***REDACTED***"); expect(markdown).not.toContain("secret-value"); expect(markdown).not.toContain("bearer-value"); + expect(markdown).not.toContain("AKIA1234567890ABCDEF"); + expect(markdown).not.toContain("abcdABCD0123abcdABCD0123abcdABCD0123abcd"); expect(markdown).toContain("## Redaction Strategy"); } finally { await rm(root, { recursive: true, force: true });