feat(omo-codex): add start-work continuation component
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,160 @@
|
||||
import { join } from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
|
||||
import { runStopHook } from "../src/codex-hook.js";
|
||||
import type { ReadonlyFileSystem, StopInput } from "../src/types.js";
|
||||
|
||||
const WORKSPACE = "/repo";
|
||||
const BOULDER_PATH = join(WORKSPACE, ".omo", "boulder.json");
|
||||
const PLAN_PATH = join(WORKSPACE, ".omo", "plans", "plan.md");
|
||||
const LEDGER_PATH = join(WORKSPACE, ".omo", "start-work", "ledger.jsonl");
|
||||
|
||||
describe("start-work Stop hook", () => {
|
||||
it("#given stop hook is already active #when hook runs #then returns empty output", () => {
|
||||
// given
|
||||
const fs = createMemoryFs();
|
||||
const input = { ...createStopInput(), stop_hook_active: true };
|
||||
|
||||
// when
|
||||
const output = runStopHook(input, fs);
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given active codex work with remaining top-level tasks #when hook runs #then returns block JSON", () => {
|
||||
// given
|
||||
const fs = createMemoryFs({
|
||||
[BOULDER_PATH]: createBoulderJson({
|
||||
sessionIds: ["codex:sess_abc"],
|
||||
status: "active",
|
||||
worktreePath: "/tmp/worktree",
|
||||
}),
|
||||
[PLAN_PATH]: ["# Plan", "", "## TODOs", "- [ ] First", "- [x] Done", "- [ ] Second"].join("\n"),
|
||||
});
|
||||
|
||||
// when
|
||||
const output = runStopHook(createStopInput(), fs);
|
||||
|
||||
// then
|
||||
const parsed = parseBlockOutput(output);
|
||||
expect(parsed.decision).toBe("block");
|
||||
expect(parsed.reason).toContain("- Plan: `launch-plan`");
|
||||
expect(parsed.reason).toContain(`- Plan file: \`${PLAN_PATH}\``);
|
||||
expect(parsed.reason).toContain(`- Boulder state: \`${BOULDER_PATH}\``);
|
||||
expect(parsed.reason).toContain("- Remaining top-level checkboxes: `2` of `3`");
|
||||
expect(parsed.reason).toContain("- Next incomplete task: `First`");
|
||||
expect(parsed.reason).toContain("- Worktree: `/tmp/worktree`");
|
||||
expect(parsed.reason).toContain(`- Ledger: \`${LEDGER_PATH}\``);
|
||||
expect(parsed.reason).toContain("- Your session id in boulder.json: `codex:sess_abc`");
|
||||
});
|
||||
|
||||
it("#given active work belongs to another harness #when hook runs #then returns empty output", () => {
|
||||
// given
|
||||
const fs = createMemoryFs({
|
||||
[BOULDER_PATH]: createBoulderJson({ sessionIds: ["opencode:sess_abc"], status: "active" }),
|
||||
[PLAN_PATH]: "- [ ] First",
|
||||
});
|
||||
|
||||
// when
|
||||
const output = runStopHook(createStopInput(), fs);
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given bare legacy session id #when hook runs #then returns empty output", () => {
|
||||
// given
|
||||
const fs = createMemoryFs({
|
||||
[BOULDER_PATH]: createBoulderJson({ sessionIds: ["sess_abc"], status: "active" }),
|
||||
[PLAN_PATH]: "- [ ] First",
|
||||
});
|
||||
|
||||
// when
|
||||
const output = runStopHook(createStopInput(), fs);
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given completed boulder work #when hook runs #then returns empty output", () => {
|
||||
// given
|
||||
const fs = createMemoryFs({
|
||||
[BOULDER_PATH]: createBoulderJson({ sessionIds: ["codex:sess_abc"], status: "completed" }),
|
||||
[PLAN_PATH]: "- [ ] First",
|
||||
});
|
||||
|
||||
// when
|
||||
const output = runStopHook(createStopInput(), fs);
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
|
||||
it("#given malformed input #when hook runs #then returns empty output", () => {
|
||||
// given
|
||||
const fs = createMemoryFs();
|
||||
|
||||
// when
|
||||
const output = runStopHook({ hook_event_name: "Stop", session_id: 123 }, fs);
|
||||
|
||||
// then
|
||||
expect(output).toBe("");
|
||||
});
|
||||
});
|
||||
|
||||
type BoulderInput = {
|
||||
readonly sessionIds: readonly string[];
|
||||
readonly status: "active" | "completed" | "paused" | "abandoned";
|
||||
readonly worktreePath?: string;
|
||||
};
|
||||
|
||||
function createStopInput(): StopInput {
|
||||
return {
|
||||
hook_event_name: "Stop",
|
||||
session_id: "sess_abc",
|
||||
turn_id: "turn_1",
|
||||
transcript_path: "",
|
||||
cwd: WORKSPACE,
|
||||
model: "gpt-5.5",
|
||||
permission_mode: "default",
|
||||
stop_hook_active: false,
|
||||
last_assistant_message: "done",
|
||||
};
|
||||
}
|
||||
|
||||
function createBoulderJson(input: BoulderInput): string {
|
||||
const work = {
|
||||
work_id: "work_1",
|
||||
active_plan: ".omo/plans/plan.md",
|
||||
plan_name: "launch-plan",
|
||||
status: input.status,
|
||||
session_ids: input.sessionIds,
|
||||
...(input.worktreePath === undefined ? {} : { worktree_path: input.worktreePath }),
|
||||
};
|
||||
return JSON.stringify({ schema_version: 2, active_work_id: "work_1", works: { work_1: work } });
|
||||
}
|
||||
|
||||
function createMemoryFs(files: Record<string, string> = {}): ReadonlyFileSystem {
|
||||
return {
|
||||
readFileSync(path, encoding) {
|
||||
expect(encoding).toBe("utf8");
|
||||
const value = files[path];
|
||||
if (value === undefined) throw new Error(`Missing fixture: ${path}`);
|
||||
return value;
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function parseBlockOutput(output: string): { readonly decision: "block"; readonly reason: string } {
|
||||
const parsed: unknown = JSON.parse(output);
|
||||
if (!isRecord(parsed)) throw new Error("Expected object output");
|
||||
if (parsed["decision"] !== "block") throw new Error("Expected block decision");
|
||||
const reason = parsed["reason"];
|
||||
if (typeof reason !== "string") throw new Error("Expected string reason");
|
||||
return { decision: "block", reason };
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user