diff --git a/packages/omo-claude/plugin/components/start-work-continuation/directive.md b/packages/omo-claude/plugin/components/start-work-continuation/directive.md
new file mode 100644
index 000000000..959019d67
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/directive.md
@@ -0,0 +1,50 @@
+
+
+You are mid-flight on a Prometheus work plan. The turn just ended without finishing the plan. This is an automatic continuation — keep going. Do NOT ask the user whether to continue; the contract is auto-continue until every top-level checkbox is `- [x]`.
+
+# State
+
+- Plan: `{{PLAN_NAME}}`
+- Plan file: `{{PLAN_PATH}}`
+- Boulder state: `{{BOULDER_PATH}}`
+- Remaining top-level checkboxes: `{{REMAINING_COUNT}}` of `{{TOTAL_COUNT}}`
+- Next incomplete task: `{{NEXT_TASK_LABEL}}`
+{{WORKTREE_BLOCK}}
+- Ledger: `{{LEDGER_PATH}}`
+- Your session id in boulder.json: `codex:{{SESSION_ID}}`
+
+# What to do this turn
+
+1. Read `{{PLAN_PATH}}` AND `{{LEDGER_PATH}}` first — ground truth for what remains and what evidence has already been recorded. The plan checkbox and the ledger are the only sources of truth; do not trust your own memory of prior turns.
+2. Pick the FIRST unchecked top-level checkbox in `## TODOs` or `## Final Verification Wave`. Ignore nested checkboxes under Acceptance Criteria / Evidence / Definition of Done.
+3. Follow the `start-work` skill in full. The skill is already loaded from your earlier turn — re-read its file at `packages/omo-codex/plugin/skills/start-work/SKILL.md` if you have lost context.
+4. Decompose the checkbox into atomic sub-tasks. Dispatch them in PARALLEL via `spawn_agent` calls in this same response unless a sub-task has a NAMED blocking dependency (input from another sub-task or shared file).
+5. Every sub-task message MUST include all 6 sections and name one Manual-QA channel (HTTP call / tmux / browser use / computer use) with a captured artifact + a cleanup receipt. Tests are the floor; the channel artifact is the ceiling. Both are required.
+6. After verification of ALL sub-tasks under this checkbox: `apply_patch` the plan to change `- [ ]` → `- [x]`, re-read the plan to confirm the count decreased, append a `task-completed` line to the ledger, then continue.
+7. Do not start fresh on a sub-agent failure. Re-dispatch the same `task_name` with a fix-message: `FAILED: ` + `Diagnosis: ` + `Fix: `.
+
+# Hard constraints
+
+- No production code before a failing test exists. RED → GREEN → SURFACE.
+- No `--dry-run` as evidence. No "should work". No "tests pass" as completion proof.
+- No `as any` / `@ts-ignore` / `@ts-expect-error`. No deleting failing tests.
+- Cleanup receipt is mandatory. Leftover PIDs / `tmux` sessions / browser contexts / bound ports / containers / temp dirs = BLOCKED, not PASS.
+- The worktree path (if set in boulder.json) governs every file edit and command. Do not stray into the main repo.
+- session_ids you write to boulder.json MUST be prefixed `codex:`. Bare ids on read are legacy `opencode:`.
+
+# Stop conditions for THIS turn
+
+- A top-level checkbox flipped to `- [x]` after the 4-phase QA gate (Phase 1 read, Phase 2 automated, Phase 3 channel scenario, Phase 4 gate decision). Then the Stop hook will re-evaluate; if more checkboxes remain you will be continued again.
+- 3 same-failure cycles on one sub-task → escalate via `spawn_agent(agent_type="codex-ultrawork-reviewer", ...)` and stop dispatch.
+- Safety boundary (destructive command, secret exfiltration, production write) → stop and surface a safe substitute.
+- All top-level checkboxes `- [x]` AND (if gate triggered) `codex-ultrawork-reviewer` approved unconditionally → print the ORCHESTRATION COMPLETE block and end.
+
+# Output discipline
+
+- Surface only state changes: sub-agent dispatched, channel scenario PASS/FAIL with artifact path, checkbox marked, evidence appended to ledger.
+- Do NOT print "Should I continue?" — the Stop hook handles continuation.
+- Do NOT restate the full plan. Do NOT recap prior turns. The ledger and the plan file are the durable record.
+
+Begin now. Pick the next checkbox, dispatch the parallel sub-agents, verify, mark, continue.
+
+
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/hooks/hooks.json b/packages/omo-claude/plugin/components/start-work-continuation/hooks/hooks.json
new file mode 100644
index 000000000..1aaed5725
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/hooks/hooks.json
@@ -0,0 +1,26 @@
+{
+ "hooks": {
+ "Stop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js\" hook stop",
+ "timeout": 10
+ }
+ ]
+ }
+ ],
+ "SubagentStop": [
+ {
+ "hooks": [
+ {
+ "type": "command",
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js\" hook subagent-stop",
+ "timeout": 10
+ }
+ ]
+ }
+ ]
+ }
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/package.json b/packages/omo-claude/plugin/components/start-work-continuation/package.json
new file mode 100644
index 000000000..3ea0f40a9
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/package.json
@@ -0,0 +1,53 @@
+{
+ "name": "@code-yeongyu/codex-start-work-continuation",
+ "version": "0.1.0",
+ "description": "Codex Stop hook continuation injector for omo-codex start-work plans.",
+ "type": "module",
+ "packageManager": "npm@11.12.1",
+ "license": "MIT",
+ "homepage": "https://github.com/code-yeongyu/codex-start-work-continuation",
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/code-yeongyu/codex-start-work-continuation.git"
+ },
+ "bugs": {
+ "url": "https://github.com/code-yeongyu/codex-start-work-continuation/issues"
+ },
+ "keywords": [
+ "codex",
+ "codex-plugin",
+ "start-work",
+ "continuation",
+ "hooks",
+ "boulder"
+ ],
+ "bin": {
+ "codex-start-work-continuation": "./dist/cli.js"
+ },
+ "scripts": {
+ "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": [
+ "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"
+ }
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/boulder-reader.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/boulder-reader.ts
new file mode 100644
index 000000000..d94e1c808
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/boulder-reader.ts
@@ -0,0 +1,167 @@
+import { isAbsolute, join, resolve } from "node:path";
+
+import type { ReadonlyFileSystem } from "./types.js";
+
+const CHECKBOX_PATTERN = /^- \[[ xX]\] /;
+const UNCHECKED_PATTERN = /^- \[ \] /;
+const TODO_HEADING = "TODOs";
+const FINAL_VERIFICATION_HEADING = "Final Verification Wave";
+
+type WorkStatus = "active" | "completed" | "paused" | "abandoned";
+
+type BoulderWork = {
+ readonly activePlan: string;
+ readonly planName: string;
+ readonly status: WorkStatus;
+ readonly sessionIds: readonly string[];
+ readonly worktreePath: string | null;
+};
+
+export type PlanChecklist = {
+ readonly remaining: number;
+ readonly total: number;
+ readonly nextTaskLabel: string | null;
+};
+
+export type ContinuationState = {
+ readonly planName: string;
+ readonly planPath: string;
+ readonly boulderPath: string;
+ readonly ledgerPath: string;
+ readonly worktreePath: string | null;
+ readonly checklist: PlanChecklist;
+};
+
+export function parsePlanChecklist(markdown: string): PlanChecklist {
+ const lines = markdown.split(/\r?\n/);
+ const hasCountedSections = lines.some(hasCountedSectionHeading);
+ let remaining = 0;
+ let total = 0;
+ let nextTaskLabel: string | null = null;
+ let isCountedSection = !hasCountedSections;
+ for (const line of lines) {
+ const heading = parseLevelTwoHeading(line);
+ if (heading !== null) isCountedSection = isCountedHeading(heading);
+ if (!isCountedSection) continue;
+ if (!CHECKBOX_PATTERN.test(line)) continue;
+ total += 1;
+ if (!UNCHECKED_PATTERN.test(line)) continue;
+ remaining += 1;
+ if (nextTaskLabel === null) nextTaskLabel = line.slice("- [ ] ".length);
+ }
+ return { remaining, total, nextTaskLabel };
+}
+
+function hasCountedSectionHeading(line: string): boolean {
+ const heading = parseLevelTwoHeading(line);
+ return heading !== null && isCountedHeading(heading);
+}
+
+export function readContinuationState(
+ cwd: string,
+ sessionId: string,
+ fs: ReadonlyFileSystem,
+): ContinuationState | null {
+ const boulderPath = join(cwd, ".omo", "boulder.json");
+ const boulderText = readTextFile(fs, boulderPath);
+ if (boulderText === null) return null;
+ const parsed = parseJsonObject(boulderText);
+ if (parsed === null) return null;
+ const work = findMatchingWork(parsed, `claude:${sessionId}`);
+ if (work === null) return null;
+ const planPath = resolvePlanPath(cwd, work.activePlan);
+ const planText = readTextFile(fs, planPath);
+ if (planText === null) return null;
+ const checklist = parsePlanChecklist(planText);
+ if (checklist.remaining === 0) return null;
+ return {
+ planName: work.planName,
+ planPath,
+ boulderPath,
+ ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"),
+ worktreePath: work.worktreePath,
+ checklist,
+ };
+}
+
+function findMatchingWork(state: Record, prefixedSessionId: string): BoulderWork | null {
+ const worksValue = state["works"];
+ const candidates = isRecord(worksValue) ? Object.values(worksValue) : [state];
+ for (const candidate of candidates) {
+ const work = parseBoulderWork(candidate);
+ if (work === null) continue;
+ if (!isContinuableStatus(work.status)) continue;
+ if (work.sessionIds.includes(prefixedSessionId)) return work;
+ }
+ return null;
+}
+
+function parseBoulderWork(value: unknown): BoulderWork | null {
+ if (!isRecord(value)) return null;
+ const activePlan = value["active_plan"];
+ const planName = value["plan_name"];
+ const status = parseWorkStatus(value["status"]);
+ const sessionIds = value["session_ids"];
+ const worktreePath = value["worktree_path"];
+ if (typeof activePlan !== "string") return null;
+ if (typeof planName !== "string") return null;
+ if (status === null) return null;
+ if (!isStringArray(sessionIds)) return null;
+ return {
+ activePlan,
+ planName,
+ status,
+ sessionIds,
+ worktreePath: typeof worktreePath === "string" ? worktreePath : null,
+ };
+}
+
+function parseWorkStatus(value: unknown): WorkStatus | null {
+ if (value === "active" || value === "completed" || value === "paused" || value === "abandoned") return value;
+ return null;
+}
+
+function isContinuableStatus(status: WorkStatus): boolean {
+ return status === "active" || status === "paused";
+}
+
+function parseLevelTwoHeading(line: string): string | null {
+ if (!line.startsWith("## ")) return null;
+ if (line.startsWith("### ")) return null;
+ return line.slice("## ".length).trim();
+}
+
+function isCountedHeading(heading: string): boolean {
+ return heading === TODO_HEADING || heading === FINAL_VERIFICATION_HEADING;
+}
+
+function resolvePlanPath(cwd: string, activePlan: string): string {
+ return isAbsolute(activePlan) ? activePlan : resolve(cwd, activePlan);
+}
+
+function readTextFile(fs: ReadonlyFileSystem, path: string): string | null {
+ try {
+ return fs.readFileSync(path, "utf8");
+ } catch (error) {
+ if (error instanceof Error) return null;
+ throw error;
+ }
+}
+
+function parseJsonObject(json: string): Record | null {
+ try {
+ const parsed: unknown = JSON.parse(json);
+ return isRecord(parsed) ? parsed : null;
+ } catch (error) {
+ if (error instanceof SyntaxError) return null;
+ throw error;
+ }
+}
+
+function isStringArray(value: unknown): value is readonly string[] {
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/cli.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/cli.ts
new file mode 100644
index 000000000..09cfbaa0e
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/cli.ts
@@ -0,0 +1,52 @@
+#!/usr/bin/env node
+import { readFileSync } from "node:fs";
+import { stdin as processStdin, stdout as processStdout } from "node:process";
+
+import { runStopHook } from "./codex-hook.js";
+import type { ReadonlyFileSystem } from "./types.js";
+
+const nodeFileSystem: ReadonlyFileSystem = {
+ readFileSync(path, encoding) {
+ return readFileSync(path, encoding);
+ },
+};
+
+const command = process.argv[2];
+const subcommand = process.argv[3];
+
+if (command === "hook" && (subcommand === "stop" || subcommand === "subagent-stop")) {
+ await runHookCli();
+} else {
+ process.stderr.write("Usage: codex-start-work-continuation hook \n");
+ process.exitCode = 1;
+}
+
+async function runHookCli(): Promise {
+ const raw = await readStdin();
+ if (raw.trim().length === 0) return;
+ const parsed = parseHookInput(raw);
+ const output = runStopHook(parsed, nodeFileSystem);
+ 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 {
+ 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));
+ });
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/codex-hook.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/codex-hook.ts
new file mode 100644
index 000000000..3db4c31c1
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/codex-hook.ts
@@ -0,0 +1,66 @@
+import type { ContinuationState } from "./boulder-reader.js";
+import { readContinuationState } from "./boulder-reader.js";
+import { START_WORK_CONTINUATION_DIRECTIVE } from "./directive.js";
+import type { ReadonlyFileSystem, StopHookEventName, StopHookOutput, StopInput } from "./types.js";
+
+export function runStopHook(input: unknown, fs: ReadonlyFileSystem): string {
+ if (!isStopInput(input)) return "";
+ if (input.stop_hook_active) return "";
+ const state = readContinuationState(input.cwd, input.session_id, fs);
+ if (state === null) return "";
+ return JSON.stringify({
+ decision: "block",
+ reason: renderDirective(state, input.session_id),
+ } satisfies StopHookOutput);
+}
+
+function renderDirective(state: ContinuationState, sessionId: string): string {
+ const lineBreak = String.fromCharCode(10);
+ const worktreeBlock =
+ state.worktreePath === null
+ ? ""
+ : `${lineBreak}- Worktree: \`${state.worktreePath}\` (all edits, tests, and commands run inside this directory)`;
+ const replacements = {
+ PLAN_NAME: state.planName,
+ PLAN_PATH: state.planPath,
+ BOULDER_PATH: state.boulderPath,
+ REMAINING_COUNT: String(state.checklist.remaining),
+ TOTAL_COUNT: String(state.checklist.total),
+ NEXT_TASK_LABEL: state.checklist.nextTaskLabel ?? "",
+ WORKTREE_BLOCK: worktreeBlock,
+ LEDGER_PATH: state.ledgerPath,
+ SESSION_ID: sessionId,
+ } as const;
+ let rendered = START_WORK_CONTINUATION_DIRECTIVE;
+ for (const [placeholder, value] of Object.entries(replacements)) {
+ rendered = rendered.replaceAll(`{{${placeholder}}}`, value);
+ }
+ return rendered;
+}
+
+function isStopInput(value: unknown): value is StopInput {
+ return (
+ isRecord(value) &&
+ isStopHookEventName(value["hook_event_name"]) &&
+ typeof value["session_id"] === "string" &&
+ (typeof value["turn_id"] === "string" || value["turn_id"] === undefined) &&
+ typeof value["transcript_path"] === "string" &&
+ typeof value["cwd"] === "string" &&
+ (typeof value["model"] === "string" || value["model"] === undefined) &&
+ typeof value["permission_mode"] === "string" &&
+ typeof value["stop_hook_active"] === "boolean" &&
+ optionalString(value["last_assistant_message"])
+ );
+}
+
+function isStopHookEventName(value: unknown): value is StopHookEventName {
+ return value === "Stop" || value === "SubagentStop";
+}
+
+function optionalString(value: unknown): boolean {
+ return value === undefined || typeof value === "string";
+}
+
+function isRecord(value: unknown): value is Record {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/directive.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/directive.ts
new file mode 100644
index 000000000..12fe3af22
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/directive.ts
@@ -0,0 +1,6 @@
+import { readFileSync } from "node:fs";
+
+export const START_WORK_CONTINUATION_DIRECTIVE: string = readFileSync(
+ new URL("../directive.md", import.meta.url),
+ "utf8",
+);
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/index.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/index.ts
new file mode 100644
index 000000000..fd51acd0e
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/index.ts
@@ -0,0 +1,5 @@
+export type { ContinuationState, PlanChecklist } from "./boulder-reader.js";
+export { parsePlanChecklist, readContinuationState } from "./boulder-reader.js";
+export { runStopHook } from "./codex-hook.js";
+export { START_WORK_CONTINUATION_DIRECTIVE } from "./directive.js";
+export type { ReadonlyFileSystem, StopHookEventName, StopHookOutput, StopInput } from "./types.js";
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/src/types.ts b/packages/omo-claude/plugin/components/start-work-continuation/src/types.ts
new file mode 100644
index 000000000..01f74b59b
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/src/types.ts
@@ -0,0 +1,23 @@
+export const STOP_HOOK_EVENTS = ["Stop", "SubagentStop"] as const;
+export type StopHookEventName = (typeof STOP_HOOK_EVENTS)[number];
+
+export type StopInput = {
+ readonly hook_event_name: StopHookEventName;
+ readonly session_id: string;
+ readonly turn_id: string;
+ readonly transcript_path: string;
+ readonly cwd: string;
+ readonly model: string;
+ readonly permission_mode: string;
+ readonly stop_hook_active: boolean;
+ readonly last_assistant_message?: string;
+};
+
+export type StopHookOutput = {
+ readonly decision: "block";
+ readonly reason: string;
+};
+
+export type ReadonlyFileSystem = {
+ readFileSync(path: string, encoding: "utf8"): string;
+};
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.build.json b/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.build.json
new file mode 100644
index 000000000..5b5bbcafd
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.build.json
@@ -0,0 +1,12 @@
+{
+ "extends": "./tsconfig.json",
+ "compilerOptions": {
+ "allowImportingTsExtensions": false,
+ "declaration": true,
+ "outDir": "dist",
+ "rootDir": "src",
+ "noEmit": false
+ },
+ "include": ["src/**/*"],
+ "exclude": ["test/**/*"]
+}
diff --git a/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.json b/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.json
new file mode 100644
index 000000000..3ac7ab0e2
--- /dev/null
+++ b/packages/omo-claude/plugin/components/start-work-continuation/tsconfig.json
@@ -0,0 +1,27 @@
+{
+ "compilerOptions": {
+ "target": "ESNext",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "lib": ["ESNext"],
+ "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/**/*", "vitest.config.ts"]
+}