refactor(omo-codex): port ultrawork hook to typescript

This commit is contained in:
YeonGyu-Kim
2026-05-28 15:07:51 +09:00
parent 0f0c6d0850
commit 091550b94d
21 changed files with 515 additions and 255 deletions
+1 -1
View File
@@ -17,7 +17,7 @@ Codex harness adapter for **oh-my-openagent**. Brings the OMO experience (rules
- `rules` (TypeScript) - injects `AGENTS.md` / `CLAUDE.md` / `.omo/rules/**` into context via `SessionStart`, `UserPromptSubmit`, `PostToolUse`, `PostCompact`.
- `comment-checker` (TypeScript) - runs `@code-yeongyu/comment-checker` after `apply_patch` / `edit` / `write` tool use.
- `lsp` (TypeScript + LSP MCP) - exposes LSP diagnostics, navigation, symbols, rename via MCP + post-edit hooks.
- `ultrawork` (Python) - keyword detector (`ulw` / `ultrawork`) that injects the full ultrawork directive, plus a `SessionStart` hook that syncs bundled agent TOML files into `CODEX_HOME/agents`.
- `ultrawork` (TypeScript) - keyword detector (`ulw` / `ultrawork`) that injects the full ultrawork directive; bundled agent TOML files are installed into `CODEX_HOME/agents`.
- `ultragoal` (TypeScript) - durable multi-goal orchestration backed by `.omo/ultragoal/` evidence audit.
## Install
@@ -5,13 +5,17 @@ Conventions for human contributors and AI agents working on this repository.
## Style
- Terse technical prose. No emojis in commits, issues, PR comments, or code.
- Python: PEP 484 type hints. Hook script stays in standard-library only — no pip dependencies.
- Tabs for indentation in JSON and Markdown tables. Spaces (4) for Python.
- TypeScript strict mode. No `any`, no `@ts-ignore`, no `@ts-expect-error`, no enums, no non-null assertions.
- ESM modules with `.js` suffix in runtime import paths.
- Runtime is Node only because Codex launches plugin hooks with Node.
- Tabs for indentation in JSON, TypeScript, and Markdown tables.
- Double quotes for JSON strings.
## Layout
- `hooks/ultrawork-detector.py` — pure stdlib `UserPromptSubmit` hook. Reads JSON on stdin, writes the directive to stdout when the keyword matches, exits 0 otherwise.
- `src/cli.ts` `UserPromptSubmit` hook CLI. Reads JSON on stdin, writes the directive to stdout when the keyword matches, exits 0 otherwise.
- `src/codex-hook.ts` — pure detector/hook behavior.
- `directive.md` — bundled ultrawork directive text.
- `agents/*.toml` — bundled Codex agent role files. Installed into `CODEX_HOME/agents/` by `src/cli/install-codex/link-cached-plugin-agents.ts` at install time (symlink on Unix, copy on Windows). No runtime `SessionStart` hook is involved.
- `hooks/hooks.json` — registers the prompt-detector hook only.
- `.codex-plugin/plugin.json` — Codex plugin manifest. Marketplace metadata lives here, not in `package.json`.
@@ -20,17 +24,18 @@ Conventions for human contributors and AI agents working on this repository.
- Never let the hook block a turn — exit code is always 0.
- Never make a network call from the hook.
- Keep the directive in `ULTRAWORK_DIRECTIVE` self-contained inside the Python file. The prompt hook is a single artifact a reviewer can read top-to-bottom.
- Keep the directive in `directive.md`. Do not inline it into TypeScript files.
- Keep bundled agent role prompts concise and model-specific; measure prompt length when changing them.
- When editing `ULTRAWORK_DIRECTIVE`, apply the `prompt-engineering` skill's entropy gate: every edit must reduce uncertainty per token. Re-measure character count before committing.
- When editing `directive.md`, apply the `prompt-engineering` skill's entropy gate: every edit must reduce uncertainty per token. Re-measure character count before committing.
## Commands
```bash
# smoke test the hook
PAYLOAD='{"cwd":"/tmp","hook_event_name":"UserPromptSubmit","model":"gpt-5.5","permission_mode":"default","session_id":"x","transcript_path":"","turn_id":"y","prompt":"please ultrawork"}'
echo "$PAYLOAD" | python3 hooks/ultrawork-detector.py | head -3
npm run build
echo "$PAYLOAD" | node dist/cli.js hook user-prompt-submit | head -3
# pattern boundary check (must be empty)
echo '{"hook_event_name":"UserPromptSubmit","prompt":"refactor ulw_helper.ts"}' | python3 hooks/ultrawork-detector.py | wc -c
echo '{"hook_event_name":"UserPromptSubmit","prompt":"refactor ulw_helper.ts"}' | node dist/cli.js hook user-prompt-submit | wc -c
```
@@ -2,12 +2,13 @@
## Unreleased
- Runtime hook migrated from `python3 hooks/ultrawork-detector.py` to the component-standard TypeScript build output `node dist/cli.js hook user-prompt-submit`, removing the Codex runtime dependency on Python.
- New top-level **`# Manual-QA channels`** section explicitly enumerates the four real-usage channels the agent MUST verify through: (1) HTTP call, (2) tmux, (3) Browser use, (4) Computer use — each with concrete commands and the artifact to capture. Auxiliary surfaces (CLI stdout / DB diff / parsed config dump) only count for genuinely CLI- or data-shaped criteria.
- Goal section now shouts **TESTS ALONE NEVER PROVE DONE**: a green test suite is supporting evidence, never completion proof. Every criterion needs its own real-usage scenario, built fresh and run through one of the four channels, every time.
- Bootstrap criterion item 2 and execution step 4 collapse onto the new channel table to remove triple-enumeration of the same surfaces (single source of truth, less drift).
- Execution loop step 4 (**SURFACE-AS-SCENARIO**) runs the chosen channel scenario; step 5 (**CLEANUP, PAIRED**) tears down server PIDs, `tmux` sessions, browser / Playwright contexts, containers, bound ports, temp files / dirs, QA-only env vars and records a one-line receipt. Missing receipt → criterion stays in_progress. Leftover state from QA = NOT done (Stop rule).
- Regression tests in `hooks/ultrawork-hooks.test.mjs` now pin: the four channel labels (`HTTP call`, `tmux`, `Browser use`, `Computer use`), `TESTS ALONE NEVER PROVE DONE`, `every criterion needs its own real-usage scenario`, the `# Manual-QA channels` heading, plus SURFACE-AS-SCENARIO + CLEANUP + leftover-state stop rule.
- Directive size: 11,005 chars across 232 lines.
- Regression tests in `test/codex-hook.test.ts` now pin: the four channel labels (`HTTP call`, `tmux`, `Browser use`, `Computer use`), `TESTS ALONE NEVER PROVE DONE`, `every criterion needs its own real-usage scenario`, the `# Manual-QA channels` heading, plus SURFACE-AS-SCENARIO + CLEANUP + leftover-state stop rule.
- Directive size: 10,951 chars across 231 lines.
### Pre-cleanup unreleased entries (folded above)
@@ -19,6 +20,6 @@
Initial release.
- Codex `UserPromptSubmit` hook (`hooks/ultrawork-detector.py`) that detects `ultrawork` / `ulw` (word-bounded, case-insensitive) in the user prompt and injects the ultrawork orchestration directive.
- Codex `UserPromptSubmit` hook that detects `ultrawork` / `ulw` (word-bounded, case-insensitive) in the user prompt and injects the ultrawork orchestration directive.
- Directive enforces: goal + binding success criteria with manual-QA scenarios + evidence, durable `/tmp` notepad lifecycle, obsessive atomic todos, scenario-driven execution loop, and a GPT-5.2 xhigh verification gate with no "false positive" escape hatch.
- Directive size: 5,775 chars across 143 lines.
@@ -2,7 +2,7 @@
Codex plugin that injects a compact orchestration directive (the **ultrawork** prompt) when the user prompt contains `ultrawork` or `ulw` (word-bounded, case-insensitive).
Bundled Codex agent role TOMLs in `agents/` are installed into `CODEX_HOME/agents/` by the omo-codex installer (`linkCachedPluginAgents`, in `src/cli/install-codex/link-cached-plugin-agents.ts`). Install-time linking uses symlinks on Linux / macOS and file copies on Windows. There is no Python `SessionStart` hook anymore.
Bundled Codex agent role TOMLs in `agents/` are installed into `CODEX_HOME/agents/` by the omo-codex installer (`linkCachedPluginAgents`, in `src/cli/install-codex/link-cached-plugin-agents.ts`). Install-time linking uses symlinks on Linux / macOS and file copies on Windows. There is no runtime Python hook.
## What the injected directive enforces
@@ -15,7 +15,7 @@ Bundled Codex agent role TOMLs in `agents/` are installed into `CODEX_HOME/agent
| Obsessive atomic todos | Every action — even one-line edits, `ls`, single test runs — becomes a todo. Format: `path: <action> for <criterion> — verify by <check>`. One in_progress at a time, mark completed immediately. |
| GPT-5.2 xhigh verification gate | Triggered automatically on user-requested rigor, 3+ files, 20+ turns, 30+ minutes, or refactor/migration/perf/security work. Use the bundled `codex-ultrawork-reviewer` agent role when available. Reviewer verdict is **binding** — no "false positive", no minimising, no arguing. Loop until **unconditional** approval. "Looks good but…" = REJECTION. |
The directive is currently 11,005 chars / 232 lines and follows the GPT-5.5 prompting structure (Role / Goal / Manual-QA channels / Bootstrap / Execution loop / Verification gate / Commits / Constraints / Output / Stop rules).
The directive is currently 10,951 chars / 231 lines and follows the GPT-5.5 prompting structure (Role / Goal / Manual-QA channels / Bootstrap / Execution loop / Verification gate / Commits / Constraints / Output / Stop rules).
## Install (via this marketplace)
@@ -30,7 +30,7 @@ The installer copies the plugin into `~/.codex/plugins/cache/sisyphuslabs/omo/0.
`hooks/hooks.json` registers a `UserPromptSubmit` hook running:
```
python3 ${PLUGIN_ROOT}/hooks/ultrawork-detector.py
node ${PLUGIN_ROOT}/dist/cli.js hook user-prompt-submit
```
Codex passes the prompt payload on stdin. When the pattern `\b(?:ultrawork|ulw)\b` (case-insensitive) matches, the hook writes the directive to stdout — Codex injects non-JSON stdout as `additional_context` for the next turn. Otherwise the hook writes nothing and exits 0. Malformed input also exits 0 to never block the turn.
@@ -41,7 +41,8 @@ Bundled agent role TOMLs in `agents/` ship to `CODEX_HOME/agents/` at install ti
```bash
PAYLOAD='{"cwd":"/tmp","hook_event_name":"UserPromptSubmit","model":"gpt-5.5","permission_mode":"default","session_id":"x","transcript_path":"","turn_id":"y","prompt":"please ultrawork"}'
echo "$PAYLOAD" | python3 hooks/ultrawork-detector.py | head -3
npm run build
echo "$PAYLOAD" | node dist/cli.js hook user-prompt-submit | head -3
```
Expect `<ultrawork-mode>` ... directive body.
@@ -0,0 +1,48 @@
{
"$schema": "https://biomejs.dev/schemas/2.4.15/schema.json",
"linter": {
"enabled": true,
"rules": {
"recommended": true,
"style": {
"noDefaultExport": "error",
"noEnum": "error",
"noNonNullAssertion": "error",
"useImportType": "error",
"useConst": "error",
"useNodejsImportProtocol": "off"
},
"complexity": {
"useLiteralKeys": "off"
},
"suspicious": {
"noExplicitAny": "error",
"noTsIgnore": "error",
"noControlCharactersInRegex": "off",
"noEmptyInterface": "off"
}
}
},
"formatter": {
"enabled": true,
"formatWithErrors": false,
"indentStyle": "tab",
"indentWidth": 3,
"lineWidth": 120
},
"files": {
"includes": ["src/**/*.ts", "test/**/*.ts", "vitest.config.ts", "!**/node_modules/**/*", "!**/dist/**/*"]
},
"overrides": [
{
"includes": ["vitest.config.ts"],
"linter": {
"rules": {
"style": {
"noDefaultExport": "off"
}
}
}
}
]
}
@@ -1,28 +1,4 @@
#!/usr/bin/env python3
"""Codex UserPromptSubmit hook: inject ultrawork directive on `ulw`/`ultrawork`.
Contract (required for codex hooks runtime):
stdin: JSON {cwd, hook_event_name="UserPromptSubmit", model,
permission_mode, prompt, session_id, transcript_path, turn_id}
stdout: when the user prompt matches the ultrawork keyword, the directive
text below; otherwise empty. Non-JSON stdout is treated by codex as
`additional_context` and injected into the model's turn context.
exit: 0 always (this hook never blocks the turn).
"""
from __future__ import annotations
import json
import re
import sys
from typing import cast
# `\b(?:ultrawork|ulw)\b` — word-bounded match excludes paths and identifiers.
ULTRAWORK_PATTERN = re.compile(r"\b(?:ultrawork|ulw)\b", re.IGNORECASE)
ULTRAWORK_DIRECTIVE = """<ultrawork-mode>
<ultrawork-mode>
**MANDATORY**: First user-visible line this turn MUST be exactly:
`ULTRAWORK MODE ENABLED!`
@@ -252,42 +228,4 @@ message + present for approval.
- After 2 parallel exploration waves yield no new useful facts, stop
exploring and act.
</ultrawork-mode>"""
def _load_payload() -> dict[str, object] | None:
try:
raw = sys.stdin.read()
except (OSError, ValueError):
return None
if not raw.strip():
return None
try:
parsed = cast(object, json.loads(raw))
except json.JSONDecodeError:
return None
if not isinstance(parsed, dict):
return None
values = cast(dict[object, object], parsed)
return {str(k): v for k, v in values.items()}
def _should_inject(payload: dict[str, object]) -> bool:
if payload.get("hook_event_name") != "UserPromptSubmit":
return False
prompt = payload.get("prompt")
if not isinstance(prompt, str) or not prompt:
return False
return ULTRAWORK_PATTERN.search(prompt) is not None
def main() -> None:
payload = _load_payload()
if payload is not None and _should_inject(payload):
_ = sys.stdout.write(ULTRAWORK_DIRECTIVE)
_ = sys.stdout.flush()
sys.exit(0)
if __name__ == "__main__":
main()
</ultrawork-mode>
@@ -5,7 +5,7 @@
"hooks": [
{
"type": "command",
"command": "python3 \"${PLUGIN_ROOT}/hooks/ultrawork-detector.py\"",
"command": "node \"${PLUGIN_ROOT}/dist/cli.js\" hook user-prompt-submit",
"timeout": 5
}
]
@@ -1,169 +0,0 @@
import assert from "node:assert/strict";
import { spawn } from "node:child_process";
import { readFile } from "node:fs/promises";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import test from "node:test";
const hookDir = dirname(fileURLToPath(import.meta.url));
const pluginRoot = dirname(hookDir);
const detectorPath = join(hookDir, "ultrawork-detector.py");
async function runPython(scriptPath, input, env = {}) {
return new Promise((resolve, reject) => {
const child = spawn("python3", [scriptPath], {
env: {
...process.env,
...env,
},
});
let stdout = "";
let stderr = "";
child.stdout.setEncoding("utf8");
child.stderr.setEncoding("utf8");
child.stdout.on("data", (chunk) => {
stdout += chunk;
});
child.stderr.on("data", (chunk) => {
stderr += chunk;
});
child.once("error", reject);
child.once("close", (code, signal) => {
resolve({ code, signal, stdout, stderr });
});
child.stdin.end(input);
});
}
test("#given ultrawork prompt #when detector runs #then emits directive", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ulw this change",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.signal, null);
assert.equal(result.stderr, "");
assert.match(result.stdout, /^<ultrawork-mode>/);
assert.match(result.stdout, /First user-visible line this turn MUST be exactly:/);
});
test("#given ultrawork prompt #when detector runs #then directive keeps goal budget unlimited", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork this change",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.signal, null);
assert.equal(result.stderr, "");
assert.match(result.stdout, /Goals are\s+unlimited/);
assert.match(result.stdout, /exactly `objective` and `status` fields/);
assert.doesNotMatch(result.stdout, /token[_-]?budget/i);
assert.doesNotMatch(result.stdout, /200000/i);
});
test("#given ultrawork prompt #when detector runs #then directive mandates manual-QA-as-scenario for http/tmux/computer-use", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.stderr, "");
assert.match(result.stdout, /SURFACE-AS-SCENARIO/);
assert.match(result.stdout, /MANUAL QA \u2014 YOU EXECUTE IT, NO STUBS/);
assert.match(result.stdout, /curl -i/);
assert.match(result.stdout, /tmux new-session/);
});
test("#given ultrawork prompt #when detector runs #then directive enumerates 4 manual-QA channels explicitly", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.stderr, "");
assert.match(result.stdout, /# Manual-QA channels/);
assert.match(result.stdout, /PICK ONE PER CRITERION \u2014 ACTUALLY RUN IT/);
assert.match(result.stdout, /1\. HTTP call/);
assert.match(result.stdout, /2\. tmux/);
assert.match(result.stdout, /3\. Browser use/);
assert.match(result.stdout, /4\. Computer use/);
});
test("#given ultrawork prompt #when detector runs #then directive forbids tests-alone verification", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.stderr, "");
assert.match(result.stdout, /TESTS ALONE NEVER PROVE DONE/);
assert.match(result.stdout, /Every[\s\n]+criterion needs its own real-usage scenario/);
assert.match(result.stdout, /every time/);
});
test("#given ultrawork prompt #when detector runs #then directive mandates paired cleanup with receipt and leftover-state stop rule", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.stderr, "");
assert.match(result.stdout, /CLEANUP \(PAIRED \u2014 NEVER SKIP\)/);
assert.match(result.stdout, /cleanup receipt/);
assert.match(result.stdout, /tmux kill-session/);
assert.match(result.stdout, /Leftover state from QA/);
assert.match(result.stdout, /means NOT done/);
});
test("#given identifier-like ulw #when detector runs #then does not emit directive", async () => {
const payload = JSON.stringify({
hook_event_name: "UserPromptSubmit",
prompt: "refactor ulw_helper.ts",
});
const result = await runPython(detectorPath, payload);
assert.equal(result.code, 0);
assert.equal(result.signal, null);
assert.equal(result.stdout, "");
assert.equal(result.stderr, "");
});
test("#given hook manifest #when read #then registers only the prompt detector hook", async () => {
const manifest = JSON.parse(await readFile(join(hookDir, "hooks.json"), "utf8"));
assert.match(
manifest.hooks.UserPromptSubmit[0].hooks[0].command,
/ultrawork-detector\.py/,
);
assert.equal(manifest.hooks.SessionStart, undefined);
assert.equal(pluginRoot.endsWith("components/ultrawork"), true);
});
test("#given component package #when inspected #then plugin identity is owned by aggregate root", async () => {
const pkg = JSON.parse(await readFile(join(pluginRoot, "package.json"), "utf8"));
assert.equal(pkg.files.includes(".codex-plugin"), false);
await assert.rejects(
readFile(join(pluginRoot, ".codex-plugin", "plugin.json"), "utf8"),
/code: 'ENOENT'|ENOENT/,
);
});
@@ -3,6 +3,7 @@
"version": "0.1.0",
"description": "Codex plugin that injects the ultrawork orchestration directive and syncs the ultrawork reviewer agent role.",
"type": "module",
"packageManager": "npm@11.12.1",
"license": "MIT",
"homepage": "https://github.com/code-yeongyu/codex-ultrawork",
"repository": {
@@ -20,15 +21,34 @@
"hooks",
"orchestration"
],
"bin": {
"codex-ultrawork": "./dist/cli.js"
},
"scripts": {
"test": "node --test hooks/*.test.mjs"
"build": "tsc -p tsconfig.build.json",
"test": "vitest --run",
"test:watch": "vitest",
"typecheck": "tsc --noEmit",
"lint": "biome check .",
"lint:fix": "biome check --write .",
"check": "tsc --noEmit && biome check . && npm run build"
},
"files": [
"agents",
"hooks/hooks.json",
"hooks/ultrawork-detector.py",
"dist",
"directive.md",
"hooks",
"README.md",
"LICENSE",
"NOTICE"
]
],
"devDependencies": {
"@biomejs/biome": "2.4.15",
"@types/node": "^25.7.0",
"typescript": "^6.0.3",
"vitest": "^4.1.5"
},
"engines": {
"node": ">=20.0.0"
}
}
@@ -0,0 +1,50 @@
#!/usr/bin/env node
import { stdin as processStdin, stdout as processStdout } from "node:process";
import { runUserPromptSubmitHook } from "./codex-hook.js";
const command = process.argv[2];
const subcommand = process.argv[3];
if (command === "hook" && subcommand === "user-prompt-submit") {
await runHookCli();
} else {
process.stderr.write("Usage: codex-ultrawork hook user-prompt-submit\n");
process.exitCode = 1;
}
async function runHookCli(): Promise<void> {
const raw = await readStdin();
if (raw.trim().length === 0) return;
const parsed = parseHookInput(raw);
const output = runUserPromptSubmitHook(parsed);
if (output.length > 0) {
processStdout.write(output);
}
}
function parseHookInput(raw: string): unknown | undefined {
try {
const parsed: unknown = JSON.parse(raw);
return parsed;
} catch (error) {
if (error instanceof SyntaxError) return undefined;
throw error;
}
}
function readStdin(): Promise<string> {
return new Promise((resolve) => {
let data = "";
processStdin.setEncoding("utf8");
processStdin.on("data", (chunk: string) => {
data += chunk;
});
processStdin.once("error", () => {
resolve(data);
});
processStdin.once("end", () => {
resolve(data);
});
});
}
@@ -0,0 +1,25 @@
import { ULTRAWORK_DIRECTIVE } from "./directive.js";
const ULTRAWORK_PATTERN = /\b(?:ultrawork|ulw)\b/i;
export type CodexUserPromptSubmitInput = {
readonly hook_event_name: "UserPromptSubmit";
readonly prompt: string;
};
export function runUserPromptSubmitHook(input: unknown): string {
if (!isCodexUserPromptSubmitInput(input)) return "";
return isUltraworkPrompt(input.prompt) ? ULTRAWORK_DIRECTIVE : "";
}
export function isUltraworkPrompt(prompt: string): boolean {
return ULTRAWORK_PATTERN.test(prompt);
}
function isCodexUserPromptSubmitInput(value: unknown): value is CodexUserPromptSubmitInput {
return isRecord(value) && value["hook_event_name"] === "UserPromptSubmit" && typeof value["prompt"] === "string";
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,3 @@
import { readFileSync } from "node:fs";
export const ULTRAWORK_DIRECTIVE: string = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
@@ -0,0 +1,66 @@
import { describe, expect, it } from "vitest";
import { isUltraworkPrompt, runUserPromptSubmitHook } from "../src/codex-hook.js";
describe("codex ultrawork hook", () => {
it("#given ultrawork prompt #when hook runs #then emits directive", () => {
// given
const payload = {
hook_event_name: "UserPromptSubmit",
prompt: "please ulw this change",
};
// when
const output = runUserPromptSubmitHook(payload);
// then
expect(output).toMatch(/^<ultrawork-mode>/);
expect(output).toMatch(/First user-visible line this turn MUST be exactly:/);
});
it("#given identifier-like ulw #when hook runs #then does not emit directive", () => {
// given
const payload = {
hook_event_name: "UserPromptSubmit",
prompt: "refactor ulw_helper.ts",
};
// when
const output = runUserPromptSubmitHook(payload);
// then
expect(output).toBe("");
expect(isUltraworkPrompt("ulw_helper.ts")).toBe(false);
});
it("#given malformed or empty input #when hook runs #then exits with empty output", () => {
// given
const inputs = [undefined, {}, { hook_event_name: "UserPromptSubmit", prompt: "" }] as const;
// when
const outputs = inputs.map((input) => runUserPromptSubmitHook(input));
// then
expect(outputs).toEqual(["", "", ""]);
});
it("#given directive #when inspected #then keeps manual QA and cleanup invariants", () => {
// given
const payload = {
hook_event_name: "UserPromptSubmit",
prompt: "please ultrawork",
};
// when
const output = runUserPromptSubmitHook(payload);
// then
expect(output).toMatch(/# Manual-QA channels/);
expect(output).toMatch(/TESTS ALONE NEVER PROVE DONE/);
expect(output).toMatch(/1\. HTTP call/);
expect(output).toMatch(/2\. tmux/);
expect(output).toMatch(/3\. Browser use/);
expect(output).toMatch(/4\. Computer use/);
expect(output).toMatch(/CLEANUP \(PAIRED/);
});
});
@@ -0,0 +1,78 @@
import { readFileSync } from "node:fs";
import { describe, expect, it } from "vitest";
type PackageJson = {
readonly type: string;
readonly packageManager: string;
readonly bin: Record<string, string>;
readonly files: readonly string[];
readonly scripts: Record<string, string>;
};
describe("codex ultrawork package metadata", () => {
it("#given package metadata #when inspected #then hook ships as built TypeScript", () => {
// given
const packageJson = readPackageJson("package.json");
const hooksJson = readJson("hooks/hooks.json");
const cliSource = readFileSync("src/cli.ts", "utf8");
// when
const packageFiles = packageJson.files;
const hookCommands = collectHookCommandsFromValue(hooksJson);
const pluginRoot = ["$", "{PLUGIN_ROOT}"].join("");
// then
expect(packageJson.type).toBe("module");
expect(packageJson.packageManager).toBe("npm@11.12.1");
expect(packageJson.bin["codex-ultrawork"]).toBe("./dist/cli.js");
expect(packageJson.scripts["build"]).toBe("tsc -p tsconfig.build.json");
expect(packageJson.scripts["test"]).toBe("vitest --run");
expect(packageFiles).toContain("dist");
expect(packageFiles).toContain("directive.md");
expect(packageFiles).not.toContain("hooks/ultrawork-detector.py");
expect(cliSource.startsWith("#!/usr/bin/env node")).toBe(true);
expect(hookCommands).toContain(`node "${pluginRoot}/dist/cli.js" hook user-prompt-submit`);
expect(hookCommands).not.toContainEqual(expect.stringMatching(/\bpython3?\b|ultrawork-detector\.py/));
});
});
function readJson(path: string): unknown {
return JSON.parse(readFileSync(path, "utf8"));
}
function readPackageJson(path: string): PackageJson {
const parsed = readJson(path);
if (!isPackageJson(parsed)) throw new TypeError(`Invalid package metadata: ${path}`);
return parsed;
}
function collectHookCommandsFromValue(value: unknown): readonly string[] {
if (typeof value === "string") return [];
if (Array.isArray(value)) return value.flatMap(collectHookCommandsFromValue);
if (!isRecord(value)) return [];
const ownCommand = typeof value["command"] === "string" ? [value["command"]] : [];
return [...ownCommand, ...Object.values(value).flatMap(collectHookCommandsFromValue)];
}
function isPackageJson(value: unknown): value is PackageJson {
return (
isRecord(value) &&
value["type"] === "module" &&
value["packageManager"] === "npm@11.12.1" &&
isStringRecord(value["bin"]) &&
isStringArray(value["files"]) &&
isStringRecord(value["scripts"])
);
}
function isStringArray(value: unknown): value is readonly string[] {
return Array.isArray(value) && value.every((item) => typeof item === "string");
}
function isStringRecord(value: unknown): value is Record<string, string> {
return isRecord(value) && Object.values(value).every((item) => typeof item === "string");
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
@@ -0,0 +1,12 @@
{
"extends": "./tsconfig.json",
"compilerOptions": {
"allowImportingTsExtensions": false,
"declaration": true,
"outDir": "dist",
"rootDir": "src",
"noEmit": false
},
"include": ["src/**/*"],
"exclude": ["test/**/*"]
}
@@ -0,0 +1,27 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "Node16",
"moduleResolution": "Node16",
"lib": ["ES2022"],
"strict": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"noPropertyAccessFromIndexSignature": true,
"verbatimModuleSyntax": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"esModuleInterop": true,
"allowImportingTsExtensions": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"resolveJsonModule": true,
"useDefineForClassFields": false,
"types": ["node"],
"noEmit": true
},
"include": ["src/**/*", "test/**/*"]
}
+1 -1
View File
@@ -36,7 +36,7 @@
"hooks": [
{
"type": "command",
"command": "python3 \"${PLUGIN_ROOT}/components/ultrawork/hooks/ultrawork-detector.py\"",
"command": "node \"${PLUGIN_ROOT}/components/ultrawork/dist/cli.js\" hook user-prompt-submit",
"timeout": 5
}
]
@@ -47,7 +47,7 @@ test("#given isolated components #when hooks are inspected #then commands stay i
"components/rules/dist/cli.js",
"components/telemetry/dist/cli.js",
"components/ultragoal/dist/cli.js",
"components/ultrawork/hooks/ultrawork-detector.py",
"components/ultrawork/dist/cli.js",
];
// then
@@ -0,0 +1,62 @@
import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
import { runUserPromptSubmitHook } from "../plugin/components/ultrawork/src/codex-hook"
const repoRoot = join(import.meta.dir, "..", "..", "..")
describe("omo-codex Python migration cross-platform behavior", () => {
it("handles empty inventory, malformed input, and Windows paths without Python", () => {
// given
const aggregateHooks = readJson(join(repoRoot, "packages/omo-codex/plugin/hooks/hooks.json"))
const componentHooks = readJson(join(repoRoot, "packages/omo-codex/plugin/components/ultrawork/hooks/hooks.json"))
const hookCommands = collectHookCommands([aggregateHooks, componentHooks])
// when
const outputs = [
runUserPromptSubmitHook(undefined),
runUserPromptSubmitHook({ hook_event_name: "UserPromptSubmit", prompt: "" }),
runUserPromptSubmitHook({ hook_event_name: "UserPromptSubmit", prompt: "refactor ulw_helper.ts" }),
runUserPromptSubmitHook({
cwd: "C:\\Users\\codex\\project",
hook_event_name: "UserPromptSubmit",
model: "gpt-5.5",
permission_mode: "default",
prompt: "please ulw this",
session_id: "s",
transcript_path: null,
turn_id: "t",
}),
]
// then
expect(hookCommands).not.toContainEqual(expect.stringMatching(/\bpython3?\b/i))
expect(hookCommands).toContain('node "${PLUGIN_ROOT}/components/ultrawork/dist/cli.js" hook user-prompt-submit')
expect(hookCommands).toContain('node "${PLUGIN_ROOT}/dist/cli.js" hook user-prompt-submit')
expect(outputs[0]).toBe("")
expect(outputs[1]).toBe("")
expect(outputs[2]).toBe("")
expect(outputs[3]).toStartWith("<ultrawork-mode>")
})
})
function readJson(path: string): unknown {
return JSON.parse(readFileSync(path, "utf8"))
}
function collectHookCommands(values: readonly unknown[]): readonly string[] {
return values.flatMap(collectHookCommandsFromValue)
}
function collectHookCommandsFromValue(value: unknown): readonly string[] {
if (typeof value === "string") return []
if (Array.isArray(value)) return value.flatMap(collectHookCommandsFromValue)
if (!isRecord(value)) return []
const ownCommand = typeof value["command"] === "string" ? [value["command"]] : []
return [...ownCommand, ...Object.values(value).flatMap(collectHookCommandsFromValue)]
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}
@@ -0,0 +1,54 @@
import { describe, expect, it } from "bun:test"
import { readdirSync, statSync } from "node:fs"
import { join, relative, sep } from "node:path"
const repoRoot = join(import.meta.dir, "..", "..", "..")
const packageRoot = join(repoRoot, "packages", "omo-codex")
const retainedPythonFiles = [
"packages/omo-codex/plugin/components/lsp/test/fixtures/broken.py",
"packages/omo-codex/plugin/skills/programming/scripts/go/new-project.py",
"packages/omo-codex/plugin/skills/programming/scripts/python/check-no-excuse-rules.py",
"packages/omo-codex/plugin/skills/programming/scripts/python/new-project.py",
"packages/omo-codex/plugin/skills/programming/scripts/python/new-script.py",
"packages/omo-codex/plugin/skills/programming/scripts/rust/check-no-excuse-rules.py",
"packages/omo-codex/plugin/skills/programming/scripts/rust/new-project.py",
] as const
const retainedPythonFileSet = new Set<string>(retainedPythonFiles)
describe("omo-codex Python migration inventory", () => {
it("classifies every Python file under packages/omo-codex", () => {
// given
const pythonFiles = listPythonFiles(packageRoot)
// when
const unclassified = pythonFiles.filter((path) => !retainedPythonFileSet.has(path))
// then
expect(unclassified).toEqual([])
expect(pythonFiles).toEqual([...retainedPythonFiles].sort())
})
})
function listPythonFiles(root: string): readonly string[] {
const files: string[] = []
collectPythonFiles(root, files)
return files.sort()
}
function collectPythonFiles(directory: string, files: string[]): void {
for (const entry of readdirSync(directory)) {
if (entry === "node_modules" || entry === "dist") continue
const absolutePath = join(directory, entry)
const stats = statSync(absolutePath)
if (stats.isDirectory()) {
collectPythonFiles(absolutePath, files)
continue
}
if (entry.endsWith(".py") || entry.endsWith(".pyi")) {
files.push(relative(repoRoot, absolutePath).split(sep).join("/"))
}
}
}
@@ -0,0 +1,39 @@
import { describe, expect, it } from "bun:test"
import { readFileSync } from "node:fs"
import { join } from "node:path"
const repoRoot = join(import.meta.dir, "..", "..", "..")
const ultraworkRoot = join(repoRoot, "packages/omo-codex/plugin/components/ultrawork")
describe("omo-codex Python migration regression", () => {
it("keeps package scripts and plugin packaging Python-free", () => {
// given
const componentPackage = readJson(join(ultraworkRoot, "package.json"))
const aggregateHooks = readFileSync(join(repoRoot, "packages/omo-codex/plugin/hooks/hooks.json"), "utf8")
const componentHooks = readFileSync(join(ultraworkRoot, "hooks/hooks.json"), "utf8")
const aggregateTest = readFileSync(join(repoRoot, "packages/omo-codex/plugin/test/aggregate.test.mjs"), "utf8")
// when
const packagedFiles = isRecord(componentPackage) && Array.isArray(componentPackage["files"])
? componentPackage["files"]
: []
const scripts = isRecord(componentPackage) && isRecord(componentPackage["scripts"]) ? componentPackage["scripts"] : {}
const bin = isRecord(componentPackage) && isRecord(componentPackage["bin"]) ? componentPackage["bin"] : {}
// then
expect(scripts["build"]).toBe("tsc -p tsconfig.build.json")
expect(scripts["test"]).toBe("vitest --run")
expect(bin["codex-ultrawork"]).toBe("./dist/cli.js")
expect(packagedFiles).toContain("dist")
expect(packagedFiles).not.toContain("hooks/ultrawork-detector.py")
expect(`${aggregateHooks}\n${componentHooks}\n${aggregateTest}`).not.toMatch(/\bpython3?\b|ultrawork-detector\.py/)
})
})
function readJson(path: string): unknown {
return JSON.parse(readFileSync(path, "utf8"))
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value)
}