#!/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 { join as join2 } from "node:path";

// ../../boulder-state/src/constants.ts
var BOULDER_DIR = ".omo";
var BOULDER_FILE = "boulder.json";
var BOULDER_STATE_PATH = `${BOULDER_DIR}/${BOULDER_FILE}`;
var NOTEPAD_DIR = "notepads";
var NOTEPAD_BASE_PATH = `${BOULDER_DIR}/${NOTEPAD_DIR}`;
// ../../boulder-state/src/plan-checklist.ts
import { existsSync, readFileSync } from "node:fs";
var CHECKBOX_PATTERN = /^- \[[ xX]\] /;
var UNCHECKED_PATTERN = /^- \[ \] /;
var TODO_HEADING = "TODOs";
var FINAL_VERIFICATION_HEADING = "Final Verification Wave";
function getPlanChecklist(planPath) {
  if (!existsSync(planPath)) {
    return emptyChecklist();
  }
  try {
    return parsePlanChecklist(readFileSync(planPath, "utf-8"));
  } catch (error) {
    if (error instanceof Error) {
      return emptyChecklist();
    }
    throw error;
  }
}
function parsePlanChecklist(markdown) {
  const lines = markdown.split(/\r?\n/);
  const hasCountedSections = lines.some(hasCountedSectionHeading);
  let remaining = 0;
  let total = 0;
  let nextTaskLabel = null;
  let isCountedSection = !hasCountedSections;
  for (const line of lines) {
    const heading = parseLevelTwoHeading(line);
    if (heading !== null) {
      isCountedSection = isCountedHeading(heading);
    }
    if (!isCountedSection || !CHECKBOX_PATTERN.test(line)) {
      continue;
    }
    total += 1;
    if (!UNCHECKED_PATTERN.test(line)) {
      continue;
    }
    remaining += 1;
    if (nextTaskLabel === null) {
      nextTaskLabel = line.slice("- [ ] ".length);
    }
  }
  return {
    completed: total - remaining,
    remaining,
    total,
    nextTaskLabel
  };
}
function hasCountedSectionHeading(line) {
  const heading = parseLevelTwoHeading(line);
  return heading !== null && isCountedHeading(heading);
}
function parseLevelTwoHeading(line) {
  if (!line.startsWith("## ")) {
    return null;
  }
  return line.slice("## ".length).trim();
}
function isCountedHeading(heading) {
  return heading === TODO_HEADING || heading === FINAL_VERIFICATION_HEADING;
}
function emptyChecklist() {
  return { completed: 0, remaining: 0, total: 0, nextTaskLabel: null };
}
// ../../boulder-state/src/storage/path.ts
import { existsSync as existsSync2 } from "node:fs";
import { isAbsolute, join, relative, resolve } from "node:path";
function getBoulderFilePath(directory) {
  return join(directory, BOULDER_DIR, BOULDER_FILE);
}
function resolveTrackedPath(baseDirectory, trackedPath) {
  return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath);
}
function resolveBoulderPlanPath(directory, state) {
  const absolutePlanPath = resolveTrackedPath(directory, state.active_plan);
  const worktreePath = state.worktree_path?.trim();
  if (!worktreePath) {
    return absolutePlanPath;
  }
  const absoluteDirectory = resolve(directory);
  const relativePlanPath = relative(absoluteDirectory, absolutePlanPath);
  if (relativePlanPath.length === 0 || relativePlanPath.startsWith("..") || isAbsolute(relativePlanPath)) {
    return absolutePlanPath;
  }
  const absoluteWorktreePath = resolveTrackedPath(directory, worktreePath);
  const worktreePlanPath = resolve(absoluteWorktreePath, relativePlanPath);
  return existsSync2(worktreePlanPath) ? worktreePlanPath : absolutePlanPath;
}
function resolveBoulderPlanPathForWork(directory, work) {
  return resolveBoulderPlanPath(directory, work);
}
// ../../boulder-state/src/storage/shared.ts
var RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"]);
var SESSION_ID_PREFIX_PATTERN = /^(codex|opencode):/;
function normalizeSessionId(sessionId, platform = "opencode") {
  if (SESSION_ID_PREFIX_PATTERN.test(sessionId)) {
    return sessionId;
  }
  return `${platform}:${sessionId}`;
}
function parseIsoToMs(value) {
  if (!value) {
    return null;
  }
  const parsed = Date.parse(value);
  return Number.isNaN(parsed) ? null : parsed;
}
function buildWorkFromMirror(state) {
  const planName = state.plan_name ?? state.active_plan;
  const workId = `${planName}-legacy`;
  return {
    work_id: workId,
    active_plan: state.active_plan,
    plan_name: planName,
    status: state.status,
    started_at: state.started_at,
    ended_at: state.ended_at,
    elapsed_ms: state.elapsed_ms,
    updated_at: state.updated_at,
    session_ids: Array.isArray(state.session_ids) ? [...state.session_ids] : [],
    session_origins: state.session_origins,
    agent: state.agent,
    worktree_path: state.worktree_path,
    task_sessions: state.task_sessions
  };
}
function projectWorkToMirror(state, work) {
  state.active_plan = work.active_plan;
  state.plan_name = work.plan_name;
  state.status = work.status;
  state.started_at = work.started_at;
  state.ended_at = work.ended_at;
  state.elapsed_ms = work.elapsed_ms;
  state.updated_at = work.updated_at;
  state.session_ids = [...work.session_ids];
  state.session_origins = work.session_origins ? { ...work.session_origins } : {};
  state.agent = work.agent;
  state.worktree_path = work.worktree_path;
  state.task_sessions = work.task_sessions ? { ...work.task_sessions } : {};
}
function selectMirrorWork(state) {
  const works = state.works ? Object.values(state.works) : [];
  if (works.length === 0) {
    return null;
  }
  if (state.active_work_id) {
    const matched = works.find((work) => work.work_id === state.active_work_id);
    if (matched) {
      return matched;
    }
  }
  const sorted = [...works].sort((left, right) => {
    const leftMs = parseIsoToMs(left.updated_at ?? left.started_at) ?? 0;
    const rightMs = parseIsoToMs(right.updated_at ?? right.started_at) ?? 0;
    return rightMs - leftMs;
  });
  return sorted[0] ?? null;
}
// ../../boulder-state/src/storage/read-state.ts
import { existsSync as existsSync3, readFileSync as readFileSync2 } from "node:fs";
function readBoulderState(directory) {
  const filePath = getBoulderFilePath(directory);
  if (!existsSync3(filePath)) {
    return null;
  }
  try {
    const content = readFileSync2(filePath, "utf-8");
    const parsed = JSON.parse(content);
    if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
      return null;
    }
    normalizeState(parsed);
    const state = parsed;
    const mirrorWork = selectMirrorWork(state);
    if (mirrorWork) {
      state.active_work_id = mirrorWork.work_id;
      projectWorkToMirror(state, mirrorWork);
    }
    return state;
  } catch {
    return null;
  }
}
function normalizeState(state) {
  normalizeSessionFields(state);
  const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : [];
  const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins) ? state.session_origins : {};
  state.session_origins = sessionOrigins;
  if (sessionIds.length === 1) {
    const soleSessionId = sessionIds[0];
    if (typeof soleSessionId === "string" && sessionOrigins[soleSessionId] !== "appended" && sessionOrigins[soleSessionId] !== "direct") {
      sessionOrigins[soleSessionId] = "direct";
    }
  }
  if (!state.task_sessions || typeof state.task_sessions !== "object" || Array.isArray(state.task_sessions)) {
    state.task_sessions = {};
  }
  normalizeWorkSessionFields(state.works);
}
function normalizeSessionFields(target) {
  const sessionIds = Array.isArray(target.session_ids) ? target.session_ids.filter((sessionId) => typeof sessionId === "string").map((sessionId) => normalizeSessionId(sessionId)) : [];
  target.session_ids = sessionIds;
  const sessionOrigins = target.session_origins && typeof target.session_origins === "object" && !Array.isArray(target.session_origins) ? normalizeSessionOrigins(target.session_origins) : {};
  target.session_origins = sessionOrigins;
}
function normalizeSessionOrigins(sessionOrigins) {
  return Object.fromEntries(Object.entries(sessionOrigins).map(([sessionId, origin]) => [normalizeSessionId(sessionId), origin]));
}
function normalizeWorkSessionFields(works) {
  if (!works || typeof works !== "object" || Array.isArray(works)) {
    return;
  }
  for (const work of Object.values(works)) {
    if (work && typeof work === "object" && !Array.isArray(work)) {
      normalizeSessionFields(work);
    }
  }
}
function getBoulderWorks(state) {
  if (state.works && typeof state.works === "object") {
    return Object.values(state.works);
  }
  if (!state.active_plan || !state.plan_name || !state.started_at) {
    return [];
  }
  return [buildWorkFromMirror(state)];
}
function getWorkForSession(directory, sessionId) {
  const state = readBoulderState(directory);
  if (!state) {
    return null;
  }
  const normalizedSessionId = normalizeSessionId(sessionId);
  let newestWork = null;
  let newestWorkMs = 0;
  for (const work of getBoulderWorks(state)) {
    if (!work.session_ids.includes(normalizedSessionId)) {
      continue;
    }
    const workMs = parseIsoToMs(work.updated_at ?? work.started_at) ?? 0;
    if (!newestWork || workMs > newestWorkMs) {
      newestWork = work;
      newestWorkMs = workMs;
    }
  }
  if (newestWork) {
    return newestWork;
  }
  return state.session_ids.includes(normalizedSessionId) ? buildWorkFromMirror(state) : null;
}
// components/start-work-continuation/src/boulder-reader.ts
function readContinuationState(cwd, sessionId) {
  const work = getWorkForSession(cwd, normalizeSessionId(sessionId, "codex"));
  if (work === null || !isContinuableStatus(work.status))
    return null;
  const planPath = resolveBoulderPlanPathForWork(cwd, work);
  const checklist = getPlanChecklist(planPath);
  if (checklist.remaining === 0)
    return null;
  return {
    planName: work.plan_name,
    planPath,
    boulderPath: getBoulderFilePath(cwd),
    ledgerPath: join2(cwd, ".omo", "start-work", "ledger.jsonl"),
    worktreePath: work.worktree_path ?? null,
    checklist
  };
}
function isContinuableStatus(status) {
  return status === "active" || status === "paused";
}

// 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 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)
  });
}
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 ?? "",
    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"
];
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;
  }
}
function isStopInput(value) {
  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";
}
function optionalString(value) {
  return value === undefined || typeof value === "string";
}
function isRecord(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);
  }
};
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;
}
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);
}
function parseHookInput(raw) {
  try {
    const parsed = JSON.parse(raw);
    return parsed;
  } catch (error) {
    if (error instanceof SyntaxError)
      return;
    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));
  });
}
