From ced472dedf17c47af7a1f98de2c4e364c2582df4 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Thu, 28 May 2026 16:14:42 +0900 Subject: [PATCH] feat(omo-codex): add start-work continuation component Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../start-work-continuation/.gitattributes | 13 ++ .../start-work-continuation/.gitignore | 3 + .../start-work-continuation/AGENTS.md | 43 +++++ .../start-work-continuation/CHANGELOG.md | 5 + .../start-work-continuation/LICENSE | 21 +++ .../components/start-work-continuation/NOTICE | 5 + .../start-work-continuation/README.md | 55 ++++++ .../start-work-continuation/biome.json | 48 +++++ .../start-work-continuation/directive.md | 50 ++++++ .../start-work-continuation/hooks/hooks.json | 26 +++ .../start-work-continuation/package.json | 53 ++++++ .../src/boulder-reader.ts | 167 ++++++++++++++++++ .../start-work-continuation/src/cli.ts | 52 ++++++ .../start-work-continuation/src/codex-hook.ts | 66 +++++++ .../start-work-continuation/src/directive.ts | 6 + .../start-work-continuation/src/index.ts | 5 + .../start-work-continuation/src/types.ts | 23 +++ .../test/boulder-reader.test.ts | 63 +++++++ .../start-work-continuation/test/cli.test.ts | 124 +++++++++++++ .../test/codex-hook.test.ts | 160 +++++++++++++++++ .../test/fixtures/boulder-completed.json | 19 ++ .../fixtures/boulder-mixed-platforms.json | 27 +++ .../fixtures/boulder-single-codex-work.json | 19 ++ .../test/fixtures/plan-all-done.md | 5 + .../fixtures/plan-with-nested-checkboxes.md | 11 ++ .../test/fixtures/plan-with-unchecked.md | 6 + .../tsconfig.build.json | 12 ++ .../start-work-continuation/tsconfig.json | 27 +++ .../start-work-continuation/vitest.config.ts | 10 ++ packages/omo-codex/plugin/package.json | 1 + 30 files changed, 1125 insertions(+) create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/.gitattributes create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/.gitignore create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/CHANGELOG.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/LICENSE create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/NOTICE create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/README.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/biome.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/directive.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/package.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/cli.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/directive.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/index.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/src/types.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-completed.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-mixed-platforms.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-single-codex-work.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-all-done.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-nested-checkboxes.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-unchecked.md create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/tsconfig.build.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/tsconfig.json create mode 100644 packages/omo-codex/plugin/components/start-work-continuation/vitest.config.ts diff --git a/packages/omo-codex/plugin/components/start-work-continuation/.gitattributes b/packages/omo-codex/plugin/components/start-work-continuation/.gitattributes new file mode 100644 index 000000000..9363fdb74 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/.gitattributes @@ -0,0 +1,13 @@ +# Normalize line endings: store LF in git, check out LF on every platform. +# Required so biome's --check passes on Windows (default core.autocrlf=true). +* text=auto eol=lf + +# Explicit binary types +*.png binary +*.jpg binary +*.jpeg binary +*.gif binary +*.ico binary +*.zip binary +*.tgz binary +*.gz binary diff --git a/packages/omo-codex/plugin/components/start-work-continuation/.gitignore b/packages/omo-codex/plugin/components/start-work-continuation/.gitignore new file mode 100644 index 000000000..746087d37 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/.gitignore @@ -0,0 +1,3 @@ +dist/ +node_modules/ +*.log diff --git a/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md b/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md new file mode 100644 index 000000000..7c8528b5e --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/AGENTS.md @@ -0,0 +1,43 @@ +# Repository Conventions + +Conventions for human contributors and AI agents working on this repository. + +## Stack + +- Node >=20 runtime. +- npm package manager. +- TypeScript 6 strict mode. +- Biome 2 linting and formatting. +- Vitest 4 test runner. + +## Forbidden + +- No `as any` or `as unknown`. +- No `@ts-ignore` or `@ts-expect-error`. +- No enums. +- No non-null assertions. +- No default exports. `vitest.config.ts` is exempt because the framework requires that shape. + +## File Ceiling + +- Keep each `src/` TypeScript file under 250 pure LOC. +- Split by responsibility before a file reaches the ceiling. + +## Test Discipline + +- Use Vitest with nested `describe` names in `#given`, `#when`, and `#then` form, or inline `// given`, `// when`, and `// then` comments. +- Never use Arrange-Act-Assert comments. +- Keep fixtures in `test/fixtures/`. + +## Build and Hooks + +- Build output goes to `dist/`. +- `hooks/hooks.json` registers Codex `Stop` and `SubagentStop` hooks. +- Hook commands run `node ${PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js hook stop` and `node ${PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js hook subagent-stop`. + +## Constraints + +- Never let the hook block a Codex turn because of malformed input. +- Never make a network call from the hook. +- Keep the directive in `directive.md`. Do not inline it into TypeScript files. +- The hook only continues sessions listed in `.omo/boulder.json` as `codex:`. diff --git a/packages/omo-codex/plugin/components/start-work-continuation/CHANGELOG.md b/packages/omo-codex/plugin/components/start-work-continuation/CHANGELOG.md new file mode 100644 index 000000000..72bf8e5ef --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/CHANGELOG.md @@ -0,0 +1,5 @@ +# Changelog + +## 0.1.0 - 2026-05-28 + +- Initial release: Stop and SubagentStop continuation injection. diff --git a/packages/omo-codex/plugin/components/start-work-continuation/LICENSE b/packages/omo-codex/plugin/components/start-work-continuation/LICENSE new file mode 100644 index 000000000..09aac3c3b --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Yeongyu Kim + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/packages/omo-codex/plugin/components/start-work-continuation/NOTICE b/packages/omo-codex/plugin/components/start-work-continuation/NOTICE new file mode 100644 index 000000000..5195cba89 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/NOTICE @@ -0,0 +1,5 @@ +codex-start-work-continuation +Copyright (c) 2026 Yeongyu Kim + +This product includes software released under the MIT License. +See LICENSE for the full text. diff --git a/packages/omo-codex/plugin/components/start-work-continuation/README.md b/packages/omo-codex/plugin/components/start-work-continuation/README.md new file mode 100644 index 000000000..12500f3a4 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/README.md @@ -0,0 +1,55 @@ +# codex-start-work-continuation + +Codex Stop-hook continuation injector for the omo-codex `start-work` skill. + +It reads `.omo/boulder.json` in the hook payload `cwd`, resolves the active work, inspects the active plan for incomplete top-level checkboxes, and emits Codex Stop-hook JSON when the plan still has work: + +```json +{"decision":"block","reason":""} +``` + +The `reason` is loaded from `directive.md` on every invocation and filled with current plan state. The hook returns no output when `stop_hook_active` is `true`, when no active Boulder work exists, when the work is completed, when the active work is not tied to `codex:`, or when all top-level plan checkboxes are complete. + +This pairs with the `start-work` skill at `plugin/skills/start-work/SKILL.md`. That skill writes `.omo/boulder.json` with Codex session ids prefixed as `codex:` so the hook can continue only its own active Codex session. + +## Counted plan checkboxes + +Only column-0 checkboxes under these sections are counted: + +- `## TODOs` +- `## Final Verification Wave` + +Nested checkboxes under `### Acceptance Criteria`, `### Evidence`, and `### Definition of Done` are ignored. + +## Smoke test + +```bash +TMP=$(mktemp -d) +mkdir -p "$TMP/.omo/plans" +cat > "$TMP/.omo/plans/test.md" < "$TMP/.omo/boulder.json" < + +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-codex/plugin/components/start-work-continuation/hooks/hooks.json b/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json new file mode 100644 index 000000000..2922addf3 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/hooks/hooks.json @@ -0,0 +1,26 @@ +{ + "hooks": { + "Stop": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"${PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js\" hook stop", + "timeout": 10 + } + ] + } + ], + "SubagentStop": [ + { + "hooks": [ + { + "type": "command", + "command": "node \"${PLUGIN_ROOT}/components/start-work-continuation/dist/cli.js\" hook subagent-stop", + "timeout": 10 + } + ] + } + ] + } +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/package.json b/packages/omo-codex/plugin/components/start-work-continuation/package.json new file mode 100644 index 000000000..3ea0f40a9 --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/src/boulder-reader.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/boulder-reader.ts new file mode 100644 index 000000000..6e371f0fa --- /dev/null +++ b/packages/omo-codex/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, `codex:${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-codex/plugin/components/start-work-continuation/src/cli.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/cli.ts new file mode 100644 index 000000000..09cfbaa0e --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/src/codex-hook.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/codex-hook.ts new file mode 100644 index 000000000..7717809fe --- /dev/null +++ b/packages/omo-codex/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" && + typeof value["transcript_path"] === "string" && + typeof value["cwd"] === "string" && + typeof value["model"] === "string" && + 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-codex/plugin/components/start-work-continuation/src/directive.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/directive.ts new file mode 100644 index 000000000..12fe3af22 --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/src/index.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/index.ts new file mode 100644 index 000000000..fd51acd0e --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/src/types.ts b/packages/omo-codex/plugin/components/start-work-continuation/src/types.ts new file mode 100644 index 000000000..01f74b59b --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts b/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts new file mode 100644 index 000000000..f4ba8739a --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/boulder-reader.test.ts @@ -0,0 +1,63 @@ +import { describe, expect, it } from "vitest"; + +import { parsePlanChecklist } from "../src/boulder-reader.js"; + +describe("start-work plan checklist parser", () => { + it("#given top-level completed and incomplete checkboxes #when parsed #then counts remaining and total", () => { + // given + const markdown = ["# Plan", "", "## TODOs", "- [ ] First", "- [x] Done", "- [X] Also done", "- [ ] Second"].join( + "\n", + ); + + // when + const checklist = parsePlanChecklist(markdown); + + // then + expect(checklist).toEqual({ remaining: 2, total: 4, nextTaskLabel: "First" }); + }); + + it("#given nested checkboxes #when parsed #then ignores non-column-zero items", () => { + // given + const markdown = ["## TODOs", "- [ ] Top-level", " - [ ] Nested", "\t- [ ] Tab nested", "- [x] Complete"].join( + "\n", + ); + + // when + const checklist = parsePlanChecklist(markdown); + + // then + expect(checklist).toEqual({ remaining: 1, total: 2, nextTaskLabel: "Top-level" }); + }); + + it("#given checkboxes outside counted sections #when parsed #then ignores unrelated top-level tasks", () => { + // given + const markdown = [ + "# Plan", + "- [ ] Preamble task", + "## TODOs", + "- [ ] Build hook", + "## Acceptance Criteria", + "- [ ] Acceptance item", + "## Final Verification Wave", + "- [x] Run tests", + "- [ ] Run smoke", + ].join("\n"); + + // when + const checklist = parsePlanChecklist(markdown); + + // then + expect(checklist).toEqual({ remaining: 2, total: 3, nextTaskLabel: "Build hook" }); + }); + + it("#given all top-level tasks complete #when parsed #then next task is null", () => { + // given + const markdown = ["## TODOs", "- [x] First", "- [X] Second"].join("\n"); + + // when + const checklist = parsePlanChecklist(markdown); + + // then + expect(checklist).toEqual({ remaining: 0, total: 2, nextTaskLabel: null }); + }); +}); diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts b/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts new file mode 100644 index 000000000..6d6fd65d2 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/cli.test.ts @@ -0,0 +1,124 @@ +import { spawnSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { execPath } from "node:process"; +import { afterEach, describe, expect, it } from "vitest"; + +const cleanupRoots: string[] = []; + +afterEach(() => { + for (const root of cleanupRoots.splice(0)) rmSync(root, { recursive: true, force: true }); +}); + +describe("start-work continuation CLI", () => { + it("#given valid Stop stdin #when CLI runs #then stdout contains block JSON", () => { + // given + const cwd = createWorkspace(["codex:s1"]); + const payload = JSON.stringify(makePayload(cwd, false, "Stop")); + + // when + const result = runCli("stop", payload); + + // then + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(0); + expect(result.stdout).toContain('"decision":"block"'); + }); + + it("#given valid SubagentStop stdin #when CLI runs #then stdout contains block JSON", () => { + // given + const cwd = createWorkspace(["codex:s1"]); + const payload = JSON.stringify(makePayload(cwd, false, "SubagentStop")); + + // when + const result = runCli("subagent-stop", payload); + + // then + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(0); + expect(result.stdout).toContain('"decision":"block"'); + }); + + it("#given active stop hook stdin #when CLI runs #then stdout is empty and exit is zero", () => { + // given + const cwd = createWorkspace(["codex:s1"]); + const payload = JSON.stringify(makePayload(cwd, true, "Stop")); + + // when + const result = runCli("stop", payload); + + // then + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + }); + + it("#given unrelated session stdin #when CLI runs #then stdout is empty and exit is zero", () => { + // given + const cwd = createWorkspace(["codex:other"]); + const payload = JSON.stringify(makePayload(cwd, false, "Stop")); + + // when + const result = runCli("stop", payload); + + // then + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + }); + + it("#given malformed stdin #when CLI runs #then stdout is empty and exit is zero", () => { + // given + const payload = "{not-json"; + + // when + const result = runCli("stop", payload); + + // then + if (result.error !== undefined) throw result.error; + expect(result.status).toBe(0); + expect(result.stdout).toBe(""); + }); +}); + +function runCli(subcommand: "stop" | "subagent-stop", input: string) { + return spawnSync(execPath, [join(process.cwd(), "dist", "cli.js"), "hook", subcommand], { input, encoding: "utf8" }); +} + +function createWorkspace(sessionIds: readonly string[]): string { + const root = mkdtempSync(join(tmpdir(), "codex-continuation-cli-")); + cleanupRoots.push(root); + mkdirSync(join(root, ".omo", "plans"), { recursive: true }); + writeFileSync(join(root, ".omo", "plans", "plan.md"), "## TODOs\n\n- [ ] Task one\n"); + const work = { + work_id: "w1", + active_plan: ".omo/plans/plan.md", + plan_name: "cli plan", + session_ids: sessionIds, + status: "active", + }; + writeFileSync( + join(root, ".omo", "boulder.json"), + `${JSON.stringify({ schema_version: 2, active_work_id: "w1", works: { w1: work } })}\n`, + ); + return root; +} + +function makePayload( + cwd: string, + stopHookActive: boolean, + eventName: "Stop" | "SubagentStop", +): Record { + return { + session_id: "s1", + turn_id: "t1", + transcript_path: "", + cwd, + hook_event_name: eventName, + model: "gpt-5.5", + permission_mode: "default", + stop_hook_active: stopHookActive, + last_assistant_message: "done", + }; +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts b/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts new file mode 100644 index 000000000..afa95f502 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/codex-hook.test.ts @@ -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 = {}): 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 { + return typeof value === "object" && value !== null && !Array.isArray(value); +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-completed.json b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-completed.json new file mode 100644 index 000000000..02f127018 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-completed.json @@ -0,0 +1,19 @@ +{ + "schema_version": 2, + "active_work_id": "work_1", + "works": { + "work_1": { + "work_id": "work_1", + "active_plan": "/repo/.omo/plans/plan-with-unchecked.md", + "plan_name": "completed-plan", + "status": "completed", + "started_at": "2026-05-28T00:00:00Z", + "session_ids": ["codex:sess_abc"] + } + }, + "active_plan": "/repo/.omo/plans/plan-with-unchecked.md", + "plan_name": "completed-plan", + "started_at": "2026-05-28T00:00:00Z", + "status": "completed", + "session_ids": ["codex:sess_abc"] +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-mixed-platforms.json b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-mixed-platforms.json new file mode 100644 index 000000000..095bf7b9c --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-mixed-platforms.json @@ -0,0 +1,27 @@ +{ + "schema_version": 2, + "active_work_id": "codex_work", + "works": { + "opencode_work": { + "work_id": "opencode_work", + "active_plan": "/repo/.omo/plans/opencode-plan.md", + "plan_name": "opencode-plan", + "status": "active", + "started_at": "2026-05-28T00:00:00Z", + "session_ids": ["opencode:sess_abc"] + }, + "codex_work": { + "work_id": "codex_work", + "active_plan": "/repo/.omo/plans/plan-with-unchecked.md", + "plan_name": "codex-plan", + "status": "paused", + "started_at": "2026-05-28T00:00:00Z", + "session_ids": ["codex:def"] + } + }, + "active_plan": "/repo/.omo/plans/opencode-plan.md", + "plan_name": "legacy-opencode-plan", + "started_at": "2026-05-28T00:00:00Z", + "status": "active", + "session_ids": ["opencode:sess_abc"] +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-single-codex-work.json b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-single-codex-work.json new file mode 100644 index 000000000..9c123ea40 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/boulder-single-codex-work.json @@ -0,0 +1,19 @@ +{ + "schema_version": 2, + "active_work_id": "work_1", + "works": { + "work_1": { + "work_id": "work_1", + "active_plan": "/repo/.omo/plans/plan-with-unchecked.md", + "plan_name": "launch-plan", + "status": "active", + "started_at": "2026-05-28T00:00:00Z", + "session_ids": ["codex:sess_abc"] + } + }, + "active_plan": "/repo/.omo/plans/plan-with-unchecked.md", + "plan_name": "legacy-launch-plan", + "started_at": "2026-05-28T00:00:00Z", + "status": "active", + "session_ids": ["codex:sess_legacy"] +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-all-done.md b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-all-done.md new file mode 100644 index 000000000..453ae07b4 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-all-done.md @@ -0,0 +1,5 @@ +# Launch Plan + +## TODOs +- [x] First +- [x] Second diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-nested-checkboxes.md b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-nested-checkboxes.md new file mode 100644 index 000000000..59cc4fe65 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-nested-checkboxes.md @@ -0,0 +1,11 @@ +# Launch Plan + +## TODOs +- [ ] Top-level + +### Acceptance Criteria + - [ ] Nested under acceptance criteria + - [x] Nested done + +## Final Checklist + - [ ] Nested under final checklist diff --git a/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-unchecked.md b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-unchecked.md new file mode 100644 index 000000000..12a1d7ce4 --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/test/fixtures/plan-with-unchecked.md @@ -0,0 +1,6 @@ +# Launch Plan + +## TODOs +- [ ] First +- [x] Done already +- [ ] Second diff --git a/packages/omo-codex/plugin/components/start-work-continuation/tsconfig.build.json b/packages/omo-codex/plugin/components/start-work-continuation/tsconfig.build.json new file mode 100644 index 000000000..5b5bbcafd --- /dev/null +++ b/packages/omo-codex/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-codex/plugin/components/start-work-continuation/tsconfig.json b/packages/omo-codex/plugin/components/start-work-continuation/tsconfig.json new file mode 100644 index 000000000..3ac7ab0e2 --- /dev/null +++ b/packages/omo-codex/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"] +} diff --git a/packages/omo-codex/plugin/components/start-work-continuation/vitest.config.ts b/packages/omo-codex/plugin/components/start-work-continuation/vitest.config.ts new file mode 100644 index 000000000..5453488cc --- /dev/null +++ b/packages/omo-codex/plugin/components/start-work-continuation/vitest.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + environment: "node", + pool: "threads", + isolate: true, + }, +}); diff --git a/packages/omo-codex/plugin/package.json b/packages/omo-codex/plugin/package.json index 790253e30..c9cbc8d62 100644 --- a/packages/omo-codex/plugin/package.json +++ b/packages/omo-codex/plugin/package.json @@ -10,6 +10,7 @@ "components/rules", "components/lsp", "components/telemetry", + "components/start-work-continuation", "components/ultragoal", "components/ultrawork" ],