code-yeongyu / code-yeongyu/lazycodex
Write start-work checkpoint handoff before stopping continuation
- Dominant language
- TypeScript
- Stars
- 3.5k
- Forks
- 216
- PR merge metrics
- No merged PRs in 30d
Description
## Problem Situation
Start-work continuation already stops auto-injection when a transcript reaches context pressure, 8,000,000 total tokens, or 3 continuation markers. That avoids making the current session worse, but it leaves no durable compact resume point. A user who runs `/renew` then has to recover from old transcript context or rediscover the active plan state.
## Reproduction Logs
Before the fix, the start-work Stop hook returned empty output when the token budget was exhausted, but did not write any handoff file. The existing tests asserted only the empty output behavior.
```text
npx vitest --run test/codex-hook.test.ts
Existing budget tests passed while no session handoff was produced.
```
## Root Cause
The budget guard in `components/start-work-continuation/src/codex-hook.ts` ran before continuation state was read. It returned early for context pressure and budget exhaustion, so the hook had no chance to serialize the active Boulder/plan/ledger state into a compact handoff.
## Verified Fix
Read active continuation state before applying the context-pressure and continuation-budget stop gates. When a gate stops auto-continuation, write a compact checkpoint to `$CODEX_HOME/session-handoffs/start-work--.md` and then still return empty output. The handoff tells the next `/renew` session to read only the handoff plus the referenced plan, Boulder state, and ledger.
```diff
diff --git a/plugins/omo/components/start-work-continuation/dist/cli.js b/plugins/omo/components/start-work-continuation/dist/cli.js
index f33460c..377da67 100755
--- a/plugins/omo/components/start-work-continuation/dist/cli.js
+++ b/plugins/omo/components/start-work-continuation/dist/cli.js
@@ -1,416 +1,518 @@
#!/usr/bin/env node
-
-// components/start-work-continuation/src/cli.ts
-import { readFileSync as readFileSync4 } from "node:fs";
-import { stdin as processStdin, stdout as processStdout } from "node:process";
-
-// components/start-work-continuation/src/boulder-reader.ts
-import { existsSync as existsSync2, readFileSync as readFileSync2 } from "node:fs";
+import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
+import { stdin, stdout } from "node:process";
import { isAbsolute, join, relative, resolve } from "node:path";
-
-// components/start-work-continuation/src/plan-checklist.ts
-import { existsSync, readFileSync } from "node:fs";
-var TODO_HEADING_PATTERN = /^##[ \t]+TODOs(?:[ \t]+#+)?[ \t]*$/i;
-var FINAL_VERIFICATION_HEADING_PATTERN = /^##[ \t]+Final Verification Wave(?:[ \t]+#+)?[ \t]*$/i;
-var SECTION_BOUNDARY_HEADING_PATTERN = /^#{1,2}(?:[ \t]+|$)/;
-var FENCE_PATTERN = /^[ \t]{0,3}(`{3,}|~{3,})(.*)$/;
-var SIMPLE_CHECKBOX_PATTERN = /^[-*][ \t]*\[[ \t]*([xX]?)[ \t]*\][ \t]+(.+)$/;
-var TODO_CHECKBOX_PATTERN = /^- \[([ xX])\] ([1-9]\d*\. .+)$/;
-var FINAL_WAVE_CHECKBOX_PATTERN = /^- \[([ xX])\] (F[1-9]\d*\. .+)$/i;
+//#region src/plan-checklist.ts
+const TODO_HEADING_PATTERN = /^##[ \t]+TODOs(?:[ \t]+#+)?[ \t]*$/i;
+const FINAL_VERIFICATION_HEADING_PATTERN = /^##[ \t]+Final Verification Wave(?:[ \t]+#+)?[ \t]*$/i;
+const SECTION_BOUNDARY_HEADING_PATTERN = /^#{1,2}(?:[ \t]+|$)/;
+const FENCE_PATTERN = /^[ \t]{0,3}(`{3,}|~{3,})(.*)$/;
+const SIMPLE_CHECKBOX_PATTERN = /^[-*][ \t]*\[[ \t]*([xX]?)[ \t]*\][ \t]+(.+)$/;
+const TODO_CHECKBOX_PATTERN = /^- \[([ xX])\] ([1-9]\d*\. .+)$/;
+const FINAL_WAVE_CHECKBOX_PATTERN = /^- \[([ xX])\] (F[1-9]\d*\. .+)$/i;
function getPlanChecklist(planPath) {
- if (!existsSync(planPath))
- return emptyChecklist();
- try {
- return parsePlanChecklist(readFileSync(planPath, "utf8"));
- } catch (error) {
- if (error instanceof Error)
- return emptyChecklist();
- throw error;
- }
+ if (!existsSync(planPath)) return emptyChecklist();
+ try {
+ return parsePlanChecklist(readFileSync(planPath, "utf8"));
+ } catch (error) {
+ if (error instanceof Error) return emptyChecklist();
+ throw error;
+ }
}
function parsePlanChecklist(markdown) {
- const lines = markdown.split(/\r?\n/);
- if (!hasStructuredSection(lines))
- return parseSimpleChecklist(lines);
- let completed = 0;
- let remaining = 0;
- let nextTaskLabel = null;
- let section = "other";
- let fence = null;
- for (const line of lines) {
- if (fence !== null) {
- if (isClosingFence(line, fence))
- fence = null;
- continue;
- }
- const openingFence = parseOpeningFence(line);
- if (openingFence !== null) {
- fence = openingFence;
- continue;
- }
- if (SECTION_BOUNDARY_HEADING_PATTERN.test(line)) {
- section = parseStructuredSectionHeading(line);
- continue;
- }
- if (section === "other")
- continue;
- const checkbox = parseStructuredCheckbox(line, section);
- if (checkbox === null)
- continue;
- if (checkbox.checked)
- completed += 1;
- else {
- remaining += 1;
- nextTaskLabel = nextTaskLabel ?? checkbox.label;
- }
- }
- return { completed, remaining, total: completed + remaining, nextTaskLabel };
+ const lines = markdown.split(/\r?\n/);
+ if (!hasStructuredSection(lines)) return parseSimpleChecklist(lines);
+ let completed = 0;
+ let remaining = 0;
+ let nextTaskLabel = null;
+ let section = "other";
+ let fence = null;
+ for (const line of lines) {
+ if (fence !== null) {
+ if (isClosingFence(line, fence)) fence = null;
+ continue;
+ }
+ const openingFence = parseOpeningFence(line);
+ if (openingFence !== null) {
+ fence = openingFence;
+ continue;
+ }
+ if (SECTION_BOUNDARY_HEADING_PATTERN.test(line)) {
+ section = parseStructuredSectionHeading(line);
+ continue;
+ }
+ if (section === "other") continue;
+ const checkbox = parseStructuredCheckbox(line, section);
+ if (checkbox === null) continue;
+ if (checkbox.checked) completed += 1;
+ else {
+ remaining += 1;
+ nextTaskLabel = nextTaskLabel ?? checkbox.label;
+ }
+ }
+ return {
+ completed,
+ remaining,
+ total: completed + remaining,
+ nextTaskLabel
+ };
}
function hasStructuredSection(lines) {
- let fence = null;
- for (const line of lines) {
- if (fence !== null) {
- if (isClosingFence(line, fence))
- fence = null;
- continue;
- }
- const openingFence = parseOpeningFence(line);
- if (openingFence !== null) {
- fence = openingFence;
- continue;
- }
- if (parseStructuredSectionHeading(line) !== "other")
- return true;
- }
- return false;
+ let fence = null;
+ for (const line of lines) {
+ if (fence !== null) {
+ if (isClosingFence(line, fence)) fence = null;
+ continue;
+ }
+ const openingFence = parseOpeningFence(line);
+ if (openingFence !== null) {
+ fence = openingFence;
+ continue;
+ }
+ if (parseStructuredSectionHeading(line) !== "other") return true;
+ }
+ return false;
}
function parseSimpleChecklist(lines) {
- let completed = 0;
- let remaining = 0;
- let nextTaskLabel = null;
- let fence = null;
- for (const line of lines) {
- if (fence !== null) {
- if (isClosingFence(line, fence))
- fence = null;
- continue;
- }
- const openingFence = parseOpeningFence(line);
- if (openingFence !== null) {
- fence = openingFence;
- continue;
- }
- const checkbox = parseSimpleTopLevelCheckbox(line);
- if (checkbox === null)
- continue;
- if (checkbox.checked)
- completed += 1;
- else {
- remaining += 1;
- nextTaskLabel = nextTaskLabel ?? checkbox.label;
- }
- }
- return { completed, remaining, total: completed + remaining, nextTaskLabel };
+ let completed = 0;
+ let remaining = 0;
+ let nextTaskLabel = null;
+ let fence = null;
+ for (const line of lines) {
+ if (fence !== null) {
+ if (isClosingFence(line, fence)) fence = null;
+ continue;
+ }
+ const openingFence = parseOpeningFence(line);
+ if (openingFence !== null) {
+ fence = openingFence;
+ continue;
+ }
+ const checkbox = parseSimpleTopLevelCheckbox(line);
+ if (checkbox === null) continue;
+ if (checkbox.checked) completed += 1;
+ else {
+ remaining += 1;
+ nextTaskLabel = nextTaskLabel ?? checkbox.label;
+ }
+ }
+ return {
+ completed,
+ remaining,
+ total: completed + remaining,
+ nextTaskLabel
+ };
}
function parseStructuredSectionHeading(line) {
- if (TODO_HEADING_PATTERN.test(line))
- return "todo";
- if (FINAL_VERIFICATION_HEADING_PATTERN.test(line))
- return "final-wave";
- return "other";
+ if (TODO_HEADING_PATTERN.test(line)) return "todo";
+ if (FINAL_VERIFICATION_HEADING_PATTERN.test(line)) return "final-wave";
+ return "other";
}
function parseStructuredCheckbox(line, section) {
- const pattern = section === "todo" ? TODO_CHECKBOX_PATTERN : FINAL_WAVE_CHECKBOX_PATTERN;
- const match = line.match(pattern);
- const marker = match?.[1];
- const label = match?.[2];
- if (marker === undefined || label === undefined)
- return null;
- return { checked: marker.toLowerCase() === "x", label };
+ const pattern = section === "todo" ? TODO_CHECKBOX_PATTERN : FINAL_WAVE_CHECKBOX_PATTERN;
+ const match = line.match(pattern);
+ const marker = match?.[1];
+ const label = match?.[2];
+ if (marker === void 0 || label === void 0) return null;
+ return {
+ checked: marker.toLowerCase() === "x",
+ label
+ };
}
function parseSimpleTopLevelCheckbox(line) {
- const match = line.match(SIMPLE_CHECKBOX_PATTERN);
- const marker = match?.[1];
- const label = match?.[2];
- if (marker === undefined || label === undefined)
- return null;
- return { checked: marker.toLowerCase() === "x", label };
+ const match = line.match(SIMPLE_CHECKBOX_PATTERN);
+ const marker = match?.[1];
+ const label = match?.[2];
+ if (marker === void 0 || label === void 0) return null;
+ return {
+ checked: marker.toLowerCase() === "x",
+ label
+ };
}
function parseOpeningFence(line) {
- const match = line.match(FENCE_PATTERN);
- const run = match?.[1];
- const info = match?.[2];
- const marker = run?.charAt(0);
- if (run === undefined || info === undefined || marker !== "`" && marker !== "~" || marker === "`" && info.includes("`"))
- return null;
- return { marker, length: run.length };
+ const match = line.match(FENCE_PATTERN);
+ const run = match?.[1];
+ const info = match?.[2];
+ const marker = run?.charAt(0);
+ if (run === void 0 || info === void 0 || marker !== "`" && marker !== "~" || marker === "`" && info.includes("`")) return null;
+ return {
+ marker,
+ length: run.length
+ };
}
function isClosingFence(line, fence) {
- const run = line.match(/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/)?.[1];
- return run?.charAt(0) === fence.marker && run.length >= fence.length;
+ const run = line.match(/^[ \t]{0,3}(`{3,}|~{3,})[ \t]*$/)?.[1];
+ return run?.charAt(0) === fence.marker && run.length >= fence.length;
}
function emptyChecklist() {
- return { completed: 0, remaining: 0, total: 0, nextTaskLabel: null };
-}
-
-// components/start-work-continuation/src/boulder-reader.ts
-var SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/;
+ return {
+ completed: 0,
+ remaining: 0,
+ total: 0,
+ nextTaskLabel: null
+ };
+}
+//#endregion
+//#region src/boulder-reader.ts
+const SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/;
function readContinuationState(cwd, sessionId) {
- const boulderPath = getBoulderFilePath(cwd);
- const boulderState = readBoulderState(boulderPath);
- if (boulderState === null)
- return null;
- const work = getWorkForSession(boulderState, normalizeSessionId(sessionId, "codex"));
- if (work === null || !isContinuableStatus(work.status))
- return null;
- const planPath = resolveBoulderPlanPathForWork(cwd, work);
- const checklist = getPlanChecklist(planPath);
- if (checklist.total === 0)
- return null;
- return {
- planName: work.planName,
- planPath,
- boulderPath,
- ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"),
- worktreePath: work.worktreePath ?? null,
- checklist
- };
+ const boulderPath = getBoulderFilePath(cwd);
+ const boulderState = readBoulderState(boulderPath);
+ if (boulderState === null) return null;
+ const work = getWorkForSession(boulderState, normalizeSessionId(sessionId, "codex"));
+ if (work === null || !isContinuableStatus(work.status)) return null;
+ const planPath = resolveBoulderPlanPathForWork(cwd, work);
+ const checklist = getPlanChecklist(planPath);
+ if (checklist.total === 0) return null;
+ return {
+ planName: work.planName,
+ planPath,
+ boulderPath,
+ ledgerPath: join(cwd, ".omo", "start-work", "ledger.jsonl"),
+ worktreePath: work.worktreePath ?? null,
+ checklist
+ };
}
function readBoulderState(path) {
- try {
- const parsed = JSON.parse(readFileSync2(path, "utf8"));
- return parseBoulderState(parsed);
- } catch (error) {
- if (error instanceof Error)
- return null;
- throw error;
- }
+ try {
+ return parseBoulderState(JSON.parse(readFileSync(path, "utf8")));
+ } catch (error) {
+ if (error instanceof Error) return null;
+ throw error;
+ }
}
function parseBoulderState(value) {
- if (!isRecord(value))
- return null;
- const works = [];
- const worksValue = value["works"];
- const hasWorksMap = isRecord(worksValue);
- if (hasWorksMap) {
- for (const workValue of Object.values(worksValue)) {
- const work = parseBoulderWork(workValue);
- if (work !== null)
- works.push(work);
- }
- }
- const mirrorWork = parseBoulderWork(value);
- if (works.length === 0 && mirrorWork === null)
- return null;
- return { works, mirrorWork, hasWorksMap };
+ if (!isRecord$1(value)) return null;
+ const works = [];
+ const worksValue = value["works"];
+ const hasWorksMap = isRecord$1(worksValue);
+ if (hasWorksMap) for (const workValue of Object.values(worksValue)) {
+ const work = parseBoulderWork(workValue);
+ if (work !== null) works.push(work);
+ }
+ const mirrorWork = parseBoulderWork(value);
+ if (works.length === 0 && mirrorWork === null) return null;
+ return {
+ works,
+ mirrorWork,
+ hasWorksMap
+ };
}
function parseBoulderWork(value) {
- if (!isRecord(value))
- return null;
- const activePlan = value["active_plan"];
- const planName = value["plan_name"];
- if (typeof activePlan !== "string")
- return null;
- const status = parseBoulderWorkStatus(value["status"]);
- const sessionIds = parseSessionIds(value["session_ids"]);
- const worktreePath = value["worktree_path"];
- const startedAt = value["started_at"];
- const updatedAt = value["updated_at"];
- return {
- activePlan,
- planName: typeof planName === "string" ? planName : activePlan,
- sessionIds,
- ...status === undefined ? {} : { status },
- ...typeof startedAt === "string" ? { startedAt } : {},
- ...typeof updatedAt === "string" ? { updatedAt } : {},
- ...typeof worktreePath === "string" ? { worktreePath } : {}
- };
+ if (!isRecord$1(value)) return null;
+ const activePlan = value["active_plan"];
+ const planName = value["plan_name"];
+ if (typeof activePlan !== "string") return null;
+ const status = parseBoulderWorkStatus(value["status"]);
+ const sessionIds = parseSessionIds(value["session_ids"]);
+ const worktreePath = value["worktree_path"];
+ const startedAt = value["started_at"];
+ const updatedAt = value["updated_at"];
+ return {
+ activePlan,
+ planName: typeof planName === "string" ? planName : activePlan,
+ sessionIds,
+ ...status === void 0 ? {} : { status },
+ ...typeof startedAt === "string" ? { startedAt } : {},
+ ...typeof updatedAt === "string" ? { updatedAt } : {},
+ ...typeof worktreePath === "string" ? { worktreePath } : {}
+ };
}
function getWorkForSession(state, normalizedSessionId) {
- let newestWork = null;
- let newestWorkMs = 0;
- for (const work of state.works) {
- if (!work.sessionIds.includes(normalizedSessionId))
- continue;
- const workMs = parseIsoToMs(work.updatedAt ?? work.startedAt) ?? 0;
- if (newestWork === null || workMs > newestWorkMs) {
- newestWork = work;
- newestWorkMs = workMs;
- }
- }
- if (newestWork !== null)
- return newestWork;
- if (state.hasWorksMap)
- return null;
- if (state.mirrorWork?.sessionIds.includes(normalizedSessionId) === true)
- return state.mirrorWork;
- return null;
+ let newestWork = null;
+ let newestWorkMs = 0;
+ for (const work of state.works) {
+ if (!work.sessionIds.includes(normalizedSessionId)) continue;
+ const workMs = parseIsoToMs(work.updatedAt ?? work.startedAt) ?? 0;
+ if (newestWork === null || workMs > newestWorkMs) {
+ newestWork = work;
+ newestWorkMs = workMs;
+ }
+ }
+ if (newestWork !== null) return newestWork;
+ if (state.hasWorksMap) return null;
+ if (state.mirrorWork?.sessionIds.includes(normalizedSessionId) === true) return state.mirrorWork;
+ return null;
}
function resolveBoulderPlanPathForWork(cwd, work) {
- const absolutePlanPath = resolveTrackedPath(cwd, work.activePlan);
- const worktreePath = work.worktreePath?.trim();
- if (worktreePath === undefined || worktreePath.length === 0)
- return absolutePlanPath;
- const relativePlanPath = relative(resolve(cwd), absolutePlanPath);
- if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
- return absolutePlanPath;
- }
- const worktreePlanPath = resolve(resolveTrackedPath(cwd, worktreePath), relativePlanPath);
- return existsSync2(worktreePlanPath) ? worktreePlanPath : absolutePlanPath;
+ const absolutePlanPath = resolveTrackedPath(cwd, work.activePlan);
+ const worktreePath = work.worktreePath?.trim();
+ if (worktreePath === void 0 || worktreePath.length === 0) return absolutePlanPath;
+ const relativePlanPath = relative(resolve(cwd), absolutePlanPath);
+ if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) return absolutePlanPath;
+ const worktreePlanPath = resolve(resolveTrackedPath(cwd, worktreePath), relativePlanPath);
+ return existsSync(worktreePlanPath) ? worktreePlanPath : absolutePlanPath;
}
function resolveTrackedPath(baseDirectory, trackedPath) {
- return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
+ return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
}
function parseBoulderWorkStatus(value) {
- if (value === "active" || value === "paused" || value === "completed" || value === "abandoned")
- return value;
- return;
+ if (value === "active" || value === "paused" || value === "completed" || value === "abandoned") return value;
}
function parseSessionIds(value) {
- if (!Array.isArray(value))
- return [];
- const sessionIds = [];
- for (const item of value) {
- if (typeof item === "string")
- sessionIds.push(normalizeSessionId(item));
- }
- return sessionIds;
+ if (!Array.isArray(value)) return [];
+ const sessionIds = [];
+ for (const item of value) if (typeof item === "string") sessionIds.push(normalizeSessionId(item));
+ return sessionIds;
}
function normalizeSessionId(sessionId, platform = "opencode") {
- if (SESSION_ID_PREFIX_PATTERN.test(sessionId))
- return sessionId;
- return `${platform}:${sessionId}`;
+ if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) return sessionId;
+ return `${platform}:${sessionId}`;
}
function parseIsoToMs(value) {
- if (value === undefined)
- return null;
- const parsed = Date.parse(value);
- return Number.isNaN(parsed) ? null : parsed;
+ if (value === void 0) return null;
+ const parsed = Date.parse(value);
+ return Number.isNaN(parsed) ? null : parsed;
}
function isContinuableStatus(status) {
- return status === "active" || status === "paused";
+ return status === "active" || status === "paused";
}
function getBoulderFilePath(cwd) {
- return join(cwd, ".omo", "boulder.json");
+ return join(cwd, ".omo", "boulder.json");
}
-function isRecord(value) {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-// components/start-work-continuation/src/directive.ts
-import { readFileSync as readFileSync3 } from "node:fs";
-var START_WORK_CONTINUATION_DIRECTIVE = readFileSync3(new URL("../directive.md", import.meta.url), "utf8");
-
-// components/start-work-continuation/src/codex-hook.ts
+function isRecord$1(value) {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+//#endregion
+//#region src/directive.ts
+const START_WORK_CONTINUATION_DIRECTIVE = readFileSync(new URL("../directive.md", import.meta.url), "utf8");
+//#endregion
+//#region src/codex-hook.ts
function runStopHook(input, fs) {
- if (!isStopInput(input))
- return "";
- if (input.stop_hook_active)
- return "";
- if (transcriptHasContextPressureMarker(input.transcript_path, fs))
- return "";
- const state = readContinuationState(input.cwd, input.session_id);
- if (state === null)
- return "";
- return JSON.stringify({
- decision: "block",
- reason: renderDirective(state, input.session_id)
- });
+ if (!isStopInput(input)) return "";
+ if (input.stop_hook_active) return "";
+ const state = readContinuationState(input.cwd, input.session_id);
+ if (state === null) return "";
+ if (transcriptHasContextPressureMarker(input.transcript_path, fs)) {
+ writeCheckpointHandoff(input, state, "context-pressure", fs);
+ return "";
+ }
+ const budgetStatus = transcriptContinuationBudgetStatus(input.transcript_path, fs);
+ if (budgetStatus.exceeded) {
+ writeCheckpointHandoff(input, state, budgetStatus.reason, fs);
+ return "";
+ }
+ return JSON.stringify({
+ decision: "block",
+ reason: renderDirective(state, input.session_id)
+ });
}
function renderDirective(state, sessionId) {
- 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 ?? "none (final gate pending)",
- WORKTREE_BLOCK: worktreeBlock,
- LEDGER_PATH: state.ledgerPath,
- SESSION_ID: sessionId
- };
- let rendered = START_WORK_CONTINUATION_DIRECTIVE;
- for (const [placeholder, value] of Object.entries(replacements)) {
- rendered = rendered.replaceAll(`{{${placeholder}}}`, value);
- }
- return rendered;
-}
-var CONTEXT_PRESSURE_MARKERS = [
- "context compacted",
- "context_length_exceeded",
- "skill descriptions were shortened",
- "context_too_large",
- "codex ran out of room in the model's context window",
- "your input exceeds the context window",
- "long threads and multiple compactions"
+ 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 ?? "none (final gate pending)",
+ WORKTREE_BLOCK: worktreeBlock,
+ LEDGER_PATH: state.ledgerPath,
+ SESSION_ID: sessionId
+ };
+ let rendered = START_WORK_CONTINUATION_DIRECTIVE;
+ for (const [placeholder, value] of Object.entries(replacements)) rendered = rendered.replaceAll(`{{${placeholder}}}`, value);
+ return rendered;
+}
+function writeCheckpointHandoff(input, state, reason, fs) {
+ if (!isCheckpointFileSystem(fs)) return;
+ const codexHome = resolveCodexHome();
+ if (codexHome === null) return;
+ const handoffDir = `${codexHome}/session-handoffs`;
+ const handoffPath = `${handoffDir}/start-work-${safePathSegment(input.session_id)}-${safePathSegment(input.turn_id)}.md`;
+ const content = renderCheckpointHandoff(input, state, reason, handoffPath);
+ try {
+ fs.mkdirSync(handoffDir, { recursive: true });
+ fs.writeFileSync(handoffPath, content, "utf8");
+ } catch (error) {
+ if (error instanceof Error) return;
+ throw error;
+ }
+}
+function renderCheckpointHandoff(input, state, reason, handoffPath) {
+ const nextTask = state.checklist.nextTaskLabel ?? "none (final gate pending)";
+ const worktreeLine = state.worktreePath === null ? "" : `- Worktree: \`${state.worktreePath}\`\n`;
+ return `${[
+ "# Codex Start-Work Checkpoint",
+ "",
+ `Reason: ${reason}`,
+ `Session: ${input.session_id}`,
+ `Turn: ${input.turn_id}`,
+ `Created by: start-work-continuation stop hook`,
+ `Handoff path: \`${handoffPath}\``,
+ "",
+ "## Resume",
+ "",
+ "- Start a fresh Codex session or use `/renew` before continuing.",
+ "- Read only this handoff plus the referenced plan, Boulder state, and ledger; do not paste the old transcript back into context.",
+ "- Continue the next incomplete task and update the ledger with fresh evidence before claiming completion.",
+ "",
+ "## Active Work",
+ "",
+ `- Plan: \`${state.planName}\``,
+ `- Plan file: \`${state.planPath}\``,
+ `- Boulder state: \`${state.boulderPath}\``,
+ `- Ledger: \`${state.ledgerPath}\``,
+ worktreeLine.trimEnd(),
+ `- Remaining top-level checkboxes: \`${state.checklist.remaining}\` of \`${state.checklist.total}\``,
+ `- Next incomplete task: \`${nextTask}\``,
+ ""
+ ].filter((line) => line.length > 0).join("\n")}\n`;
+}
+const CONTEXT_PRESSURE_MARKERS = [
+ "context compacted",
+ "context_length_exceeded",
+ "skill descriptions were shortened",
+ "context_too_large",
+ "codex ran out of room in the model's context window",
+ "your input exceeds the context window",
+ "long threads and multiple compactions"
];
+const CONTINUATION_MARKER = "";
+const DEFAULT_MAX_CONTINUATION_TOTAL_TOKENS = 8e6;
+const DEFAULT_MAX_CONTINUATION_INJECTIONS = 3;
function transcriptHasContextPressureMarker(transcriptPath, fs) {
- try {
- const transcript = fs.readFileSync(transcriptPath, "utf8").toLowerCase();
- return CONTEXT_PRESSURE_MARKERS.some((marker) => transcript.includes(marker));
- } catch (error) {
- if (error instanceof Error)
- return false;
- throw error;
- }
+ try {
+ const transcript = fs.readFileSync(transcriptPath, "utf8").toLowerCase();
+ return CONTEXT_PRESSURE_MARKERS.some((marker) => transcript.includes(marker));
+ } catch (error) {
+ if (error instanceof Error) return false;
+ throw error;
+ }
+}
+function transcriptContinuationBudgetStatus(transcriptPath, fs) {
+ try {
+ const transcript = fs.readFileSync(transcriptPath, "utf8");
+ const maxTotalTokens = readPositiveIntegerEnv("OMO_START_WORK_CONTINUATION_MAX_TOTAL_TOKENS", DEFAULT_MAX_CONTINUATION_TOTAL_TOKENS);
+ const maxInjections = readPositiveIntegerEnv("OMO_START_WORK_CONTINUATION_MAX_INJECTIONS", DEFAULT_MAX_CONTINUATION_INJECTIONS);
+ if (maxTranscriptTotalTokens(transcript) >= maxTotalTokens) return {
+ exceeded: true,
+ reason: "token-budget"
+ };
+ if (countOccurrences(transcript, CONTINUATION_MARKER) >= maxInjections) return {
+ exceeded: true,
+ reason: "injection-budget"
+ };
+ return { exceeded: false };
+ } catch (error) {
+ if (error instanceof Error) return { exceeded: false };
+ throw error;
+ }
+}
+function maxTranscriptTotalTokens(transcript) {
+ let maxTotalTokens = 0;
+ for (const line of transcript.split(/\r?\n/)) {
+ const totalTokens = readTokenCountTotal(line);
+ if (totalTokens !== null && totalTokens > maxTotalTokens) maxTotalTokens = totalTokens;
+ }
+ return maxTotalTokens;
+}
+function readTokenCountTotal(line) {
+ try {
+ const parsed = JSON.parse(line);
+ if (!isRecord(parsed) || parsed["type"] !== "event_msg") return null;
+ const payload = parsed["payload"];
+ if (!isRecord(payload) || payload["type"] !== "token_count") return null;
+ const info = payload["info"];
+ if (!isRecord(info)) return null;
+ const totalUsage = info["total_token_usage"];
+ if (!isRecord(totalUsage)) return null;
+ const totalTokens = totalUsage["total_tokens"];
+ return typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null;
+ } catch (error) {
+ if (error instanceof SyntaxError) return null;
+ throw error;
+ }
+}
+function countOccurrences(text, needle) {
+ if (needle.length === 0) return 0;
+ let count = 0;
+ let offset = 0;
+ for (;;) {
+ const index = text.indexOf(needle, offset);
+ if (index === -1) return count;
+ count += 1;
+ offset = index + needle.length;
+ }
+}
+function readPositiveIntegerEnv(name, fallback) {
+ const raw = process.env[name];
+ if (raw === void 0 || raw.trim().length === 0) return fallback;
+ const parsed = Number.parseInt(raw, 10);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+}
+function resolveCodexHome() {
+ const explicit = process.env["CODEX_HOME"]?.trim();
+ if (explicit !== void 0 && explicit.length > 0) return explicit;
+ const home = process.env["HOME"]?.trim();
+ if (home === void 0 || home.length === 0) return null;
+ return `${home}/.codex`;
+}
+function safePathSegment(value) {
+ const sanitized = value.replaceAll(/[^A-Za-z0-9._-]/g, "-");
+ return sanitized.length === 0 ? "unknown" : sanitized;
+}
+function isCheckpointFileSystem(fs) {
+ const candidate = fs;
+ return typeof candidate.mkdirSync === "function" && typeof candidate.writeFileSync === "function";
}
function isStopInput(value) {
- return isRecord2(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"]);
+ 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) {
- return value === "Stop" || value === "SubagentStop";
+ return value === "Stop" || value === "SubagentStop";
}
function optionalString(value) {
- return value === undefined || typeof value === "string";
+ return value === void 0 || typeof value === "string";
}
-function isRecord2(value) {
- return typeof value === "object" && value !== null && !Array.isArray(value);
-}
-
-// components/start-work-continuation/src/cli.ts
-var nodeFileSystem = {
- readFileSync(path, encoding) {
- return readFileSync4(path, encoding);
- }
+function isRecord(value) {
+ return typeof value === "object" && value !== null && !Array.isArray(value);
+}
+//#endregion
+//#region src/cli.ts
+const nodeFileSystem = {
+ mkdirSync(path, options) {
+ mkdirSync(path, options);
+ },
+ readFileSync(path, encoding) {
+ return readFileSync(path, encoding);
+ },
+ writeFileSync(path, data, encoding) {
+ writeFileSync(path, data, encoding);
+ }
};
-var command = process.argv[2];
-var subcommand = process.argv[3];
-if (command === "hook" && (subcommand === "stop" || subcommand === "subagent-stop")) {
- await runHookCli();
-} else {
- process.stderr.write(`Usage: omo-start-work-continuation hook
-`);
- process.exitCode = 1;
+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: omo-start-work-continuation hook \n");
+ process.exitCode = 1;
}
async function runHookCli() {
- 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);
+ const raw = await readStdin();
+ if (raw.trim().length === 0) return;
+ const output = runStopHook(parseHookInput(raw), nodeFileSystem);
+ if (output.length > 0) stdout.write(output);
}
function parseHookInput(raw) {
- try {
- const parsed = JSON.parse(raw);
- return parsed;
- } catch (error) {
- if (error instanceof SyntaxError)
- return;
- throw error;
- }
+ try {
+ return JSON.parse(raw);
+ } catch (error) {
+ if (error instanceof SyntaxError) return void 0;
+ throw error;
+ }
}
function readStdin() {
- return new Promise((resolve2) => {
- let data = "";
- processStdin.setEncoding("utf8");
- processStdin.on("data", (chunk) => {
- data += chunk;
- });
- processStdin.once("error", () => resolve2(data));
- processStdin.once("end", () => resolve2(data));
- });
-}
+ return new Promise((resolve) => {
+ let data = "";
+ stdin.setEncoding("utf8");
+ stdin.on("data", (chunk) => {
+ data += chunk;
+ });
+ stdin.once("error", () => resolve(data));
+ stdin.once("end", () => resolve(data));
+ });
+}
+//#endregion
+export {};
diff --git a/plugins/omo/components/start-work-continuation/src/cli.ts b/plugins/omo/components/start-work-continuation/src/cli.ts
index b0548fe..52e5e9a 100644
--- a/plugins/omo/components/start-work-continuation/src/cli.ts
+++ b/plugins/omo/components/start-work-continuation/src/cli.ts
@@ -1,14 +1,20 @@
#!/usr/bin/env node
-import { readFileSync } from "node:fs";
+import { mkdirSync, readFileSync, writeFileSync } 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";
+import type { CheckpointFileSystem } from "./types.js";
-const nodeFileSystem: ReadonlyFileSystem = {
+const nodeFileSystem: CheckpointFileSystem = {
+ mkdirSync(path, options) {
+ mkdirSync(path, options);
+ },
readFileSync(path, encoding) {
return readFileSync(path, encoding);
},
+ writeFileSync(path, data, encoding) {
+ writeFileSync(path, data, encoding);
+ },
};
const command = process.argv[2];
diff --git a/plugins/omo/components/start-work-continuation/src/codex-hook.ts b/plugins/omo/components/start-work-continuation/src/codex-hook.ts
index f794047..c7f61f9 100644
--- a/plugins/omo/components/start-work-continuation/src/codex-hook.ts
+++ b/plugins/omo/components/start-work-continuation/src/codex-hook.ts
@@ -1,14 +1,28 @@
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";
+import type {
+ CheckpointFileSystem,
+ 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 "";
- if (transcriptHasContextPressureMarker(input.transcript_path, fs)) return "";
const state = readContinuationState(input.cwd, input.session_id);
if (state === null) return "";
+ if (transcriptHasContextPressureMarker(input.transcript_path, fs)) {
+ writeCheckpointHandoff(input, state, "context-pressure", fs);
+ return "";
+ }
+ const budgetStatus = transcriptContinuationBudgetStatus(input.transcript_path, fs);
+ if (budgetStatus.exceeded) {
+ writeCheckpointHandoff(input, state, budgetStatus.reason, fs);
+ return "";
+ }
return JSON.stringify({
decision: "block",
reason: renderDirective(state, input.session_id),
@@ -39,6 +53,65 @@ function renderDirective(state: ContinuationState, sessionId: string): string {
return rendered;
}
+function writeCheckpointHandoff(
+ input: StopInput,
+ state: ContinuationState,
+ reason: "context-pressure" | "token-budget" | "injection-budget",
+ fs: ReadonlyFileSystem,
+): void {
+ if (!isCheckpointFileSystem(fs)) return;
+ const codexHome = resolveCodexHome();
+ if (codexHome === null) return;
+ const handoffDir = `${codexHome}/session-handoffs`;
+ const handoffPath = `${handoffDir}/start-work-${safePathSegment(input.session_id)}-${safePathSegment(input.turn_id)}.md`;
+ const content = renderCheckpointHandoff(input, state, reason, handoffPath);
+ try {
+ fs.mkdirSync(handoffDir, { recursive: true });
+ fs.writeFileSync(handoffPath, content, "utf8");
+ } catch (error) {
+ if (error instanceof Error) return;
+ throw error;
+ }
+}
+
+function renderCheckpointHandoff(
+ input: StopInput,
+ state: ContinuationState,
+ reason: "context-pressure" | "token-budget" | "injection-budget",
+ handoffPath: string,
+): string {
+ const nextTask = state.checklist.nextTaskLabel ?? "none (final gate pending)";
+ const worktreeLine = state.worktreePath === null ? "" : `- Worktree: \`${state.worktreePath}\`\n`;
+ return `${[
+ "# Codex Start-Work Checkpoint",
+ "",
+ `Reason: ${reason}`,
+ `Session: ${input.session_id}`,
+ `Turn: ${input.turn_id}`,
+ `Created by: start-work-continuation stop hook`,
+ `Handoff path: \`${handoffPath}\``,
+ "",
+ "## Resume",
+ "",
+ "- Start a fresh Codex session or use `/renew` before continuing.",
+ "- Read only this handoff plus the referenced plan, Boulder state, and ledger; do not paste the old transcript back into context.",
+ "- Continue the next incomplete task and update the ledger with fresh evidence before claiming completion.",
+ "",
+ "## Active Work",
+ "",
+ `- Plan: \`${state.planName}\``,
+ `- Plan file: \`${state.planPath}\``,
+ `- Boulder state: \`${state.boulderPath}\``,
+ `- Ledger: \`${state.ledgerPath}\``,
+ worktreeLine.trimEnd(),
+ `- Remaining top-level checkboxes: \`${state.checklist.remaining}\` of \`${state.checklist.total}\``,
+ `- Next incomplete task: \`${nextTask}\``,
+ "",
+ ]
+ .filter((line) => line.length > 0)
+ .join("\n")}\n`;
+}
+
const CONTEXT_PRESSURE_MARKERS = [
"context compacted",
"context_length_exceeded",
@@ -49,6 +122,10 @@ const CONTEXT_PRESSURE_MARKERS = [
"long threads and multiple compactions",
] as const;
+const CONTINUATION_MARKER = "";
+const DEFAULT_MAX_CONTINUATION_TOTAL_TOKENS = 8_000_000;
+const DEFAULT_MAX_CONTINUATION_INJECTIONS = 3;
+
function transcriptHasContextPressureMarker(transcriptPath: string, fs: ReadonlyFileSystem): boolean {
try {
const transcript = fs.readFileSync(transcriptPath, "utf8").toLowerCase();
@@ -59,6 +136,95 @@ function transcriptHasContextPressureMarker(transcriptPath: string, fs: Readonly
}
}
+type ContinuationBudgetStatus =
+ | { readonly exceeded: false }
+ | { readonly exceeded: true; readonly reason: "token-budget" | "injection-budget" };
+
+function transcriptContinuationBudgetStatus(transcriptPath: string, fs: ReadonlyFileSystem): ContinuationBudgetStatus {
+ try {
+ const transcript = fs.readFileSync(transcriptPath, "utf8");
+ const maxTotalTokens = readPositiveIntegerEnv(
+ "OMO_START_WORK_CONTINUATION_MAX_TOTAL_TOKENS",
+ DEFAULT_MAX_CONTINUATION_TOTAL_TOKENS,
+ );
+ const maxInjections = readPositiveIntegerEnv(
+ "OMO_START_WORK_CONTINUATION_MAX_INJECTIONS",
+ DEFAULT_MAX_CONTINUATION_INJECTIONS,
+ );
+ if (maxTranscriptTotalTokens(transcript) >= maxTotalTokens) return { exceeded: true, reason: "token-budget" };
+ if (countOccurrences(transcript, CONTINUATION_MARKER) >= maxInjections)
+ return { exceeded: true, reason: "injection-budget" };
+ return { exceeded: false };
+ } catch (error) {
+ if (error instanceof Error) return { exceeded: false };
+ throw error;
+ }
+}
+
+function maxTranscriptTotalTokens(transcript: string): number {
+ let maxTotalTokens = 0;
+ for (const line of transcript.split(/\r?\n/)) {
+ const totalTokens = readTokenCountTotal(line);
+ if (totalTokens !== null && totalTokens > maxTotalTokens) maxTotalTokens = totalTokens;
+ }
+ return maxTotalTokens;
+}
+
+function readTokenCountTotal(line: string): number | null {
+ try {
+ const parsed: unknown = JSON.parse(line);
+ if (!isRecord(parsed) || parsed["type"] !== "event_msg") return null;
+ const payload = parsed["payload"];
+ if (!isRecord(payload) || payload["type"] !== "token_count") return null;
+ const info = payload["info"];
+ if (!isRecord(info)) return null;
+ const totalUsage = info["total_token_usage"];
+ if (!isRecord(totalUsage)) return null;
+ const totalTokens = totalUsage["total_tokens"];
+ return typeof totalTokens === "number" && Number.isFinite(totalTokens) ? totalTokens : null;
+ } catch (error) {
+ if (error instanceof SyntaxError) return null;
+ throw error;
+ }
+}
+
+function countOccurrences(text: string, needle: string): number {
+ if (needle.length === 0) return 0;
+ let count = 0;
+ let offset = 0;
+ for (;;) {
+ const index = text.indexOf(needle, offset);
+ if (index === -1) return count;
+ count += 1;
+ offset = index + needle.length;
+ }
+}
+
+function readPositiveIntegerEnv(name: string, fallback: number): number {
+ const raw = process.env[name];
+ if (raw === undefined || raw.trim().length === 0) return fallback;
+ const parsed = Number.parseInt(raw, 10);
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback;
+}
+
+function resolveCodexHome(): string | null {
+ const explicit = process.env["CODEX_HOME"]?.trim();
+ if (explicit !== undefined && explicit.length > 0) return explicit;
+ const home = process.env["HOME"]?.trim();
+ if (home === undefined || home.length === 0) return null;
+ return `${home}/.codex`;
+}
+
+function safePathSegment(value: string): string {
+ const sanitized = value.replaceAll(/[^A-Za-z0-9._-]/g, "-");
+ return sanitized.length === 0 ? "unknown" : sanitized;
+}
+
+function isCheckpointFileSystem(fs: ReadonlyFileSystem): fs is CheckpointFileSystem {
+ const candidate = fs as Partial;
+ return typeof candidate.mkdirSync === "function" && typeof candidate.writeFileSync === "function";
+}
+
function isStopInput(value: unknown): value is StopInput {
return (
isRecord(value) &&
diff --git a/plugins/omo/components/start-work-continuation/src/types.ts b/plugins/omo/components/start-work-continuation/src/types.ts
index 01f74b5..6abb826 100644
--- a/plugins/omo/components/start-work-continuation/src/types.ts
+++ b/plugins/omo/components/start-work-continuation/src/types.ts
@@ -21,3 +21,8 @@ export type StopHookOutput = {
export type ReadonlyFileSystem = {
readFileSync(path: string, encoding: "utf8"): string;
};
+
+export type CheckpointFileSystem = ReadonlyFileSystem & {
+ mkdirSync(path: string, options: { readonly recursive: true }): void;
+ writeFileSync(path: string, data: string, encoding: "utf8"): void;
+};
diff --git a/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts b/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
index 14b4a24..9ef8050 100644
--- a/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
+++ b/plugins/omo/components/start-work-continuation/test/codex-hook.test.ts
@@ -4,14 +4,16 @@ import { join } from "node:path";
import { afterEach, describe, expect, it } from "vitest";
import { runStopHook } from "../src/codex-hook.js";
-import type { ReadonlyFileSystem, StopInput } from "../src/types.js";
+import type { CheckpointFileSystem, ReadonlyFileSystem, StopInput } from "../src/types.js";
const DEFAULT_WORKSPACE = "/repo";
const cleanupRoots: string[] = [];
+const originalCodexHome = process.env["CODEX_HOME"];
const SCAFFOLD_PLAN_MARKDOWN = readFileSync(new URL("./fixtures/plan-scaffold.md", import.meta.url), "utf8");
afterEach(() => {
for (const root of cleanupRoots.splice(0)) rmSync(root, { recursive: true, force: true });
+ restoreEnv("CODEX_HOME", originalCodexHome);
});
describe("start-work Stop hook", () => {
@@ -117,6 +119,67 @@ describe("start-work Stop hook", () => {
expect(output).toBe("");
});
+ it("#given transcript token budget is exhausted #when hook runs #then writes a checkpoint handoff", () => {
+ // given
+ const workspace = createWorkspace({
+ boulderJson: createBoulderJson({ sessionIds: ["codex:sess_abc"], status: "active" }),
+ planMarkdown: SCAFFOLD_PLAN_MARKDOWN,
+ });
+ const transcriptPath = "/repo/transcript.jsonl";
+ process.env["CODEX_HOME"] = "/codex";
+ const fs = createWritableMemoryFs({
+ [transcriptPath]: JSON.stringify({
+ type: "event_msg",
+ payload: {
+ type: "token_count",
+ info: {
+ total_token_usage: {
+ total_tokens: 8_000_000,
+ },
+ },
+ },
+ }),
+ });
+
+ // when
+ const output = runStopHook({ ...createStopInput(workspace), transcript_path: transcriptPath }, fs);
+
+ // then
+ expect(output).toBe("");
+ const handoff = fs.files["/codex/session-handoffs/start-work-sess_abc-turn_1.md"];
+ expect(handoff).toContain("Reason: token-budget");
+ expect(handoff).toContain("- Start a fresh Codex session or use `/renew` before continuing.");
+ expect(handoff).toContain(`- Plan file: \`${join(workspace, ".omo", "plans", "plan.md")}\``);
+ expect(handoff).toContain("- Next incomplete task: `1. Implement checklist parser parity`");
+ });
+
+ it("#given continuation was already injected repeatedly #when hook runs #then writes a checkpoint handoff", () => {
+ // given
+ const workspace = createWorkspace({
+ boulderJson: createBoulderJson({ sessionIds: ["codex:sess_abc"], status: "active" }),
+ planMarkdown: SCAFFOLD_PLAN_MARKDOWN,
+ });
+ const transcriptPath = "/repo/transcript.jsonl";
+ process.env["CODEX_HOME"] = "/codex";
+ const fs = createWritableMemoryFs({
+ [transcriptPath]: [
+ "",
+ "",
+ "",
+ "",
+ ].join("\n"),
+ });
+
+ // when
+ const output = runStopHook({ ...createStopInput(workspace), transcript_path: transcriptPath }, fs);
+
+ // then
+ expect(output).toBe("");
+ const handoff = fs.files["/codex/session-handoffs/start-work-sess_abc-turn_1.md"];
+ expect(handoff).toContain("Reason: injection-budget");
+ expect(handoff).toContain("- Read only this handoff plus the referenced plan, Boulder state, and ledger");
+ });
+
it("#given active work belongs to another harness #when hook runs #then returns empty output", () => {
// given
const workspace = createWorkspace({
@@ -256,6 +319,30 @@ function createMemoryFs(files: Record = {}): ReadonlyFileSystem
};
}
+function createWritableMemoryFs(
+ files: Record = {},
+): CheckpointFileSystem & { readonly files: Record } {
+ return {
+ files,
+ mkdirSync() {},
+ readFileSync(path, encoding) {
+ expect(encoding).toBe("utf8");
+ const value = files[path];
+ if (value === undefined) throw new Error(`Missing fixture: ${path}`);
+ return value;
+ },
+ writeFileSync(path, data, encoding) {
+ expect(encoding).toBe("utf8");
+ files[path] = data;
+ },
+ };
+}
+
+function restoreEnv(name: string, value: string | undefined): void {
+ if (value === undefined) delete process.env[name];
+ else process.env[name] = 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");
```
## Verification
- Active OMO cache: `npm run typecheck` passed.
- Active OMO cache: `npx biome check src/cli.ts src/codex-hook.ts src/types.ts test/codex-hook.test.ts` passed.
- Active OMO cache: `npx vitest --run` passed, 39 tests.
- Active OMO cache manual QA: piped a Stop hook payload with an 8,000,000-token transcript into `node dist/cli.js hook stop`; stdout was empty and `$CODEX_HOME/session-handoffs/start-work-sess_smoke-turn_smoke.md` was written with plan/Boulder/ledger/next-task resume instructions.
- Source worktree manual QA: rebuilt `plugins/omo/components/start-work-continuation/dist/cli.js` with Rolldown and repeated the same Stop-hook smoke; stdout was empty and the expected handoff file was written.
- Source worktree direct typecheck/vitest were not run because the generated distribution checkout has no local `node_modules`; active OMO cache has the matching component dependencies and passed those gates.
---
This fix was debugged, implemented, and verified with [LazyCodex](https://github.com/code-yeongyu/lazycodex).
Tag: lazycodex-generated
Contributor guide
No contributing guide indexed for this repository
Research direction
Start with components/start-work-continuation/src/codex-hook.ts and run test/codex-hook.test.ts, focusing on the existing context-pressure and continuation-budget tests. Trace how active Boulder, plan, and ledger state is read, then verify that a stopped continuation writes $CODEX_HOME/session-handoffs/start-work--.md while still returning empty output.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- node.js, typescript
- Domain
- cli, tooling
- Issue type
- Feature
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Quiet
- Clarity
- Clearly specified
- Newbie friendliness
- 65/100