Skip to content

Write start-work checkpoint handoff before stopping continuation #146

Description

@johnnyisme

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.

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-<session-id>-<turn-id>.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 --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 = "<start-work-continuation>";
+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 <stop|subagent-stop>
-`);
-  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 <stop|subagent-stop>\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 = "<start-work-continuation>";
+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<CheckpointFileSystem>;
+	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]: [
+				"<start-work-continuation>",
+				"<start-work-continuation>",
+				"<start-work-continuation>",
+				"",
+			].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<string, string> = {}): ReadonlyFileSystem
 	};
 }
 
+function createWritableMemoryFs(
+	files: Record<string, string> = {},
+): CheckpointFileSystem & { readonly files: Record<string, string> } {
+	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.
Tag: lazycodex-generated

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions