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.
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.
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");
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
/renewthen 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.
Root Cause
The budget guard in
components/start-work-continuation/src/codex-hook.tsran 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>.mdand then still return empty output. The handoff tells the next/renewsession to read only the handoff plus the referenced plan, Boulder state, and ledger.Verification
npm run typecheckpassed.npx biome check src/cli.ts src/codex-hook.ts src/types.ts test/codex-hook.test.tspassed.npx vitest --runpassed, 39 tests.node dist/cli.js hook stop; stdout was empty and$CODEX_HOME/session-handoffs/start-work-sess_smoke-turn_smoke.mdwas written with plan/Boulder/ledger/next-task resume instructions.plugins/omo/components/start-work-continuation/dist/cli.jswith Rolldown and repeated the same Stop-hook smoke; stdout was empty and the expected handoff file was written.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