refactor(packages): extract boulder-state package

This commit is contained in:
YeonGyu-Kim
2026-05-21 01:19:16 +09:00
parent 7028c1f40a
commit f7ceb03efe
24 changed files with 1228 additions and 1177 deletions
+21
View File
@@ -0,0 +1,21 @@
{
"name": "@oh-my-opencode/boulder-state",
"version": "0.1.0",
"type": "module",
"private": true,
"description": "Pure TypeScript boulder work-tracking state machine for oh-my-opencode.",
"exports": {
".": {
"types": "./index.d.ts",
"import": "./src/index.ts"
}
},
"types": "./index.d.ts",
"scripts": {
"typecheck": "tsgo --noEmit -p tsconfig.json",
"test": "bun test src/*.test.ts"
},
"dependencies": {
"@oh-my-opencode/utils": "workspace:*"
}
}
+8
View File
@@ -0,0 +1,8 @@
export const BOULDER_DIR = ".omo"
export const BOULDER_FILE = "boulder.json"
export const BOULDER_STATE_PATH = `${BOULDER_DIR}/${BOULDER_FILE}`
export const NOTEPAD_DIR = "notepads"
export const NOTEPAD_BASE_PATH = `${BOULDER_DIR}/${NOTEPAD_DIR}`
export const PROMETHEUS_PLANS_DIR = ".omo/plans"
+42
View File
@@ -0,0 +1,42 @@
export { BOULDER_DIR, BOULDER_FILE, BOULDER_STATE_PATH, NOTEPAD_BASE_PATH, NOTEPAD_DIR, PROMETHEUS_PLANS_DIR } from "./constants"
export { readCurrentTopLevelTask } from "./top-level-task"
export {
addBoulderWork,
appendSessionId,
appendSessionIdForWork,
clearBoulderState,
completeBoulder,
createBoulderState,
endTaskTimer,
findPrometheusPlans,
generateWorkId,
getActiveWorks,
getBoulderFilePath,
getBoulderWorks,
getPlanName,
getPlanProgress,
getTaskSessionState,
getWorkById,
getWorkByPlanName,
getWorkForSession,
getWorkResumeOptions,
readBoulderState,
resolveBoulderPlanPath,
resolveBoulderPlanPathForWork,
selectActiveWork,
startTaskTimer,
upsertTaskSessionState,
upsertTaskSessionStateForWork,
writeBoulderState,
} from "./storage"
export type {
BoulderSessionOrigin,
BoulderState,
BoulderTaskStatus,
BoulderWorkResumeOption,
BoulderWorkState,
BoulderWorkStatus,
PlanProgress,
TaskSessionState,
TopLevelTaskRef,
} from "./types"
@@ -0,0 +1,15 @@
export { getBoulderFilePath, resolveBoulderPlanPath, resolveBoulderPlanPathForWork } from "./path"
export { findPrometheusPlans, getPlanName, getPlanProgress } from "./plan-progress"
export {
getActiveWorks,
getBoulderWorks,
getTaskSessionState,
getWorkById,
getWorkByPlanName,
getWorkForSession,
getWorkResumeOptions,
readBoulderState,
} from "./read-state"
export { appendSessionId, appendSessionIdForWork } from "./session"
export { endTaskTimer, startTaskTimer, upsertTaskSessionState, upsertTaskSessionStateForWork } from "./task"
export { addBoulderWork, clearBoulderState, completeBoulder, createBoulderState, generateWorkId, selectActiveWork, writeBoulderState } from "./write-state"
@@ -0,0 +1,41 @@
import { existsSync } from "node:fs"
import { isAbsolute, join, relative, resolve } from "node:path"
import { BOULDER_DIR, BOULDER_FILE } from "../constants"
import type { BoulderState, BoulderWorkState } from "../types"
export function getBoulderFilePath(directory: string): string {
return join(directory, BOULDER_DIR, BOULDER_FILE)
}
function resolveTrackedPath(baseDirectory: string, trackedPath: string): string {
return isAbsolute(trackedPath) ? resolve(trackedPath) : resolve(baseDirectory, trackedPath)
}
export function resolveBoulderPlanPath(
directory: string,
state: Pick<BoulderState, "active_plan" | "worktree_path">,
): string {
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 existsSync(worktreePlanPath) ? worktreePlanPath : absolutePlanPath
}
export function resolveBoulderPlanPathForWork(
directory: string,
work: Pick<BoulderWorkState, "active_plan" | "worktree_path">,
): string {
return resolveBoulderPlanPath(directory, work)
}
@@ -0,0 +1,105 @@
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs"
import { basename, join } from "node:path"
import { PROMETHEUS_PLANS_DIR } from "../constants"
import type { PlanProgress } from "../types"
const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
const UNCHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[\s*\]\s*(.+)$/
const CHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[[xX]\]\s*(.+)$/
const TODO_TASK_PATTERN = /^\d+\.\s+/
const FINAL_WAVE_TASK_PATTERN = /^F\d+\.\s+/i
type ProgressSection = "todo" | "final-wave" | "other"
export function findPrometheusPlans(directory: string): string[] {
const plansDir = join(directory, PROMETHEUS_PLANS_DIR)
if (!existsSync(plansDir)) {
return []
}
try {
return readdirSync(plansDir)
.filter((file) => file.endsWith(".md"))
.map((file) => join(plansDir, file))
.sort((left, right) => statSync(right).mtimeMs - statSync(left).mtimeMs)
} catch {
return []
}
}
export function getPlanName(planPath: string): string {
return basename(planPath, ".md")
}
export function getPlanProgress(planPath: string): PlanProgress {
if (!existsSync(planPath)) {
return { total: 0, completed: 0, isComplete: false }
}
try {
const content = readFileSync(planPath, "utf-8")
const lines = content.split(/\r?\n/)
const hasStructuredSections = lines.some(
(line) => TODO_HEADING_PATTERN.test(line) || FINAL_VERIFICATION_HEADING_PATTERN.test(line),
)
if (hasStructuredSections) {
return getStructuredPlanProgress(lines)
}
return getSimplePlanProgress(content)
} catch {
return { total: 0, completed: 0, isComplete: false }
}
}
function getStructuredPlanProgress(lines: string[]): PlanProgress {
let section: ProgressSection = "other"
let total = 0
let completed = 0
for (const line of lines) {
if (SECOND_LEVEL_HEADING_PATTERN.test(line)) {
section = TODO_HEADING_PATTERN.test(line)
? "todo"
: FINAL_VERIFICATION_HEADING_PATTERN.test(line)
? "final-wave"
: "other"
continue
}
if (section !== "todo" && section !== "final-wave") {
continue
}
const checkedMatch = line.match(CHECKED_CHECKBOX_PATTERN)
const uncheckedMatch = checkedMatch ? null : line.match(UNCHECKED_CHECKBOX_PATTERN)
const match = checkedMatch ?? uncheckedMatch
if (!match || match[1].length > 0) {
continue
}
const taskBody = match[2].trim()
const labelPattern = section === "todo" ? TODO_TASK_PATTERN : FINAL_WAVE_TASK_PATTERN
if (!labelPattern.test(taskBody)) {
continue
}
total += 1
if (checkedMatch) {
completed += 1
}
}
return { total, completed, isComplete: total > 0 && completed === total }
}
function getSimplePlanProgress(content: string): PlanProgress {
const uncheckedMatches = content.match(/^[-*]\s*\[\s*\]/gm) ?? []
const checkedMatches = content.match(/^[-*]\s*\[[xX]\]/gm) ?? []
const total = uncheckedMatches.length + checkedMatches.length
const completed = checkedMatches.length
return { total, completed, isComplete: total > 0 && completed === total }
}
@@ -0,0 +1,169 @@
import { existsSync, readFileSync } from "node:fs"
import type { BoulderState, BoulderWorkResumeOption, BoulderWorkState, TaskSessionState } from "../types"
import { getBoulderFilePath, resolveBoulderPlanPathForWork } from "./path"
import { getPlanProgress } from "./plan-progress"
import { buildWorkFromMirror, isValidWorkStatus, parseIsoToMs, projectWorkToMirror, selectMirrorWork } from "./shared"
export function readBoulderState(directory: string): BoulderState | null {
const filePath = getBoulderFilePath(directory)
if (!existsSync(filePath)) {
return null
}
try {
const content = readFileSync(filePath, "utf-8")
const parsed = JSON.parse(content)
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
return null
}
normalizeState(parsed)
const state = parsed as BoulderState
const mirrorWork = selectMirrorWork(state)
if (mirrorWork) {
state.active_work_id = mirrorWork.work_id
projectWorkToMirror(state, mirrorWork)
}
return state
} catch {
return null
}
}
function normalizeState(state: Record<string, unknown>): void {
const sessionIds = Array.isArray(state.session_ids) ? state.session_ids : []
state.session_ids = sessionIds
const sessionOrigins = state.session_origins && typeof state.session_origins === "object" && !Array.isArray(state.session_origins)
? (state.session_origins as Record<string, unknown>)
: {}
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 = {}
}
}
export function getBoulderWorks(state: BoulderState): BoulderWorkState[] {
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)]
}
export function getActiveWorks(directory: string): BoulderWorkState[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getBoulderWorks(state).filter((work) => work.status !== "completed" && work.status !== "abandoned")
}
export function getWorkById(directory: string, workId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
return getBoulderWorks(state).find((work) => work.work_id === workId) ?? null
}
export function getWorkByPlanName(
directory: string,
planName: string,
options?: { worktreePath?: string },
): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const worktreePath = options?.worktreePath
return getBoulderWorks(state).find((work) => {
if (work.plan_name !== planName) {
return false
}
return worktreePath ? work.worktree_path === worktreePath : true
}) ?? null
}
export function getWorkForSession(directory: string, sessionId: string): BoulderWorkState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
.filter((work) => work.session_ids.includes(sessionId))
.sort((left, right) => (parseIsoToMs(right.updated_at ?? right.started_at) ?? 0) - (parseIsoToMs(left.updated_at ?? left.started_at) ?? 0))
if (works.length > 0) {
return works[0] ?? null
}
return state.session_ids.includes(sessionId) ? buildWorkFromMirror(state) : null
}
export function getWorkResumeOptions(directory: string): BoulderWorkResumeOption[] {
const state = readBoulderState(directory)
if (!state) {
return []
}
return getBoulderWorks(state)
.filter((work) => work.status !== "completed" && work.status !== "abandoned")
.map((work) => {
const progress = getPlanProgress(resolveBoulderPlanPathForWork(directory, work))
return {
work_id: work.work_id,
plan_name: work.plan_name,
active_plan: work.active_plan,
worktree_path: work.worktree_path,
status: work.status && isValidWorkStatus(work.status) ? work.status : "active",
started_at: work.started_at,
updated_at: work.updated_at ?? work.started_at,
ended_at: work.ended_at,
elapsed_ms: work.elapsed_ms,
session_count: work.session_ids.length,
progress,
is_current_mirror: state.active_work_id === work.work_id,
}
})
}
export function getTaskSessionState(directory: string, taskKey: string): TaskSessionState | null {
const state = readBoulderState(directory)
if (state?.active_work_id) {
const work = state.works?.[state.active_work_id]
const taskSession = work?.task_sessions?.[taskKey]
if (taskSession) {
return taskSession
}
}
if (!state?.task_sessions) {
return null
}
return state.task_sessions[taskKey] ?? null
}
@@ -0,0 +1,93 @@
import type { BoulderSessionOrigin, BoulderState, BoulderWorkState } from "../types"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { nowIsoString, projectWorkToMirror } from "./shared"
import { writeBoulderState } from "./write-state"
export function appendSessionId(
directory: string,
sessionId: string,
origin: "direct" | "appended" = "direct",
): BoulderState | null {
const activeWorkId = readBoulderState(directory)?.active_work_id
if (activeWorkId) {
return appendSessionIdForWork(directory, activeWorkId, sessionId, origin)
}
const state = readBoulderState(directory)
if (!state) {
return null
}
if (!state.session_origins || typeof state.session_origins !== "object" || Array.isArray(state.session_origins)) {
state.session_origins = {}
}
if (!state.session_ids?.includes(sessionId)) {
if (!Array.isArray(state.session_ids)) {
state.session_ids = []
}
const originalSessionIds = [...state.session_ids]
const originalSessionOrigins = { ...state.session_origins }
state.session_ids.push(sessionId)
state.session_origins[sessionId] = origin
if (writeBoulderState(directory, state)) {
return state
}
state.session_ids = originalSessionIds
state.session_origins = originalSessionOrigins
return null
}
if (!state.session_origins[sessionId]) {
state.session_origins[sessionId] = origin
if (!writeBoulderState(directory, state)) {
return null
}
}
return state
}
export function appendSessionIdForWork(
directory: string,
workId: string,
sessionId: string,
origin: BoulderSessionOrigin = "direct",
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const targetWork = works.find((work) => work.work_id === workId)
if (!targetWork) {
return null
}
const updatedWork: BoulderWorkState = {
...targetWork,
session_ids: targetWork.session_ids.includes(sessionId)
? [...targetWork.session_ids]
: [...targetWork.session_ids, sessionId],
session_origins: { ...(targetWork.session_origins ?? {}), [sessionId]: origin },
updated_at: nowIsoString(),
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: {
...Object.fromEntries(works.map((work) => [work.work_id, work])),
[workId]: updatedWork,
},
}
if (state.active_work_id === workId) {
projectWorkToMirror(nextState, updatedWork)
}
return writeBoulderState(directory, nextState) ? nextState : null
}
@@ -0,0 +1,86 @@
import type { BoulderState, BoulderWorkState, BoulderWorkStatus } from "../types"
export const RESERVED_KEYS = new Set(["__proto__", "prototype", "constructor"])
export function nowIsoString(): string {
return new Date().toISOString()
}
export function parseIsoToMs(value: string | undefined): number | null {
if (!value) {
return null
}
const parsed = Date.parse(value)
return Number.isNaN(parsed) ? null : parsed
}
export function getElapsedMs(startedAt: string | undefined, endedAt: string | undefined): number | undefined {
const startedMs = parseIsoToMs(startedAt)
const endedMs = parseIsoToMs(endedAt)
if (startedMs === null || endedMs === null) {
return undefined
}
return endedMs - startedMs
}
export function isValidWorkStatus(status: unknown): status is BoulderWorkStatus {
return status === "active" || status === "completed" || status === "paused" || status === "abandoned"
}
export function buildWorkFromMirror(state: BoulderState): BoulderWorkState {
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,
}
}
export function projectWorkToMirror(state: BoulderState, work: BoulderWorkState): void {
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 } : {}
}
export function selectMirrorWork(state: BoulderState): BoulderWorkState | null {
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
}
+167
View File
@@ -0,0 +1,167 @@
import type { BoulderState, BoulderWorkState, TaskSessionState } from "../types"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { getElapsedMs, nowIsoString, projectWorkToMirror, RESERVED_KEYS } from "./shared"
import { writeBoulderState } from "./write-state"
export function upsertTaskSessionState(
directory: string,
input: {
taskKey: string
taskLabel: string
taskTitle: string
sessionId: string
agent?: string
category?: string
},
): BoulderState | null {
const stateForWork = readBoulderState(directory)
if (stateForWork?.active_work_id) {
return upsertTaskSessionStateForWork(directory, stateForWork.active_work_id, input)
}
const state = readBoulderState(directory)
if (!state || RESERVED_KEYS.has(input.taskKey)) {
return null
}
const taskSessions = state.task_sessions ?? {}
taskSessions[input.taskKey] = {
task_key: input.taskKey,
task_label: input.taskLabel,
task_title: input.taskTitle,
session_id: input.sessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
updated_at: nowIsoString(),
}
state.task_sessions = taskSessions
return writeBoulderState(directory, state) ? state : null
}
export function upsertTaskSessionStateForWork(
directory: string,
workId: string,
input: {
taskKey: string
taskLabel: string
taskTitle: string
sessionId: string
agent?: string
category?: string
},
): BoulderState | null {
if (RESERVED_KEYS.has(input.taskKey)) {
return null
}
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const targetWork = works.find((work) => work.work_id === workId)
if (!targetWork) {
return null
}
const previousTaskSession = targetWork.task_sessions?.[input.taskKey]
const nextTaskSession: TaskSessionState = {
task_key: input.taskKey,
task_label: input.taskLabel,
task_title: input.taskTitle,
session_id: input.sessionId,
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.category !== undefined ? { category: input.category } : {}),
...(previousTaskSession?.started_at !== undefined ? { started_at: previousTaskSession.started_at } : {}),
...(previousTaskSession?.ended_at !== undefined ? { ended_at: previousTaskSession.ended_at } : {}),
...(previousTaskSession?.elapsed_ms !== undefined ? { elapsed_ms: previousTaskSession.elapsed_ms } : {}),
...(previousTaskSession?.status !== undefined ? { status: previousTaskSession.status } : {}),
updated_at: nowIsoString(),
}
const nextWork: BoulderWorkState = {
...targetWork,
task_sessions: { ...(targetWork.task_sessions ?? {}), [input.taskKey]: nextTaskSession },
updated_at: nowIsoString(),
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: {
...Object.fromEntries(works.map((work) => [work.work_id, work])),
[workId]: nextWork,
},
}
if (state.active_work_id === workId) {
projectWorkToMirror(nextState, nextWork)
}
return writeBoulderState(directory, nextState) ? nextState : null
}
export function startTaskTimer(
directory: string,
workId: string,
input: {
taskKey: string
taskLabel: string
taskTitle: string
sessionId: string
agent?: string
category?: string
startedAt?: string
},
): BoulderState | null {
const nextState = upsertTaskSessionStateForWork(directory, workId, input)
if (!nextState) {
return null
}
const work = nextState.works?.[workId]
const taskSession = work?.task_sessions?.[input.taskKey]
if (!work || !taskSession) {
return null
}
const startedAt = taskSession.started_at ?? input.startedAt ?? nowIsoString()
taskSession.started_at = startedAt
taskSession.status = "running"
taskSession.updated_at = nowIsoString()
work.updated_at = nowIsoString()
return writeBoulderState(directory, nextState) ? nextState : null
}
export function endTaskTimer(
directory: string,
workId: string,
taskKey: string,
endedAt?: string,
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const work = state.works?.[workId] ?? getBoulderWorks(state).find((candidate) => candidate.work_id === workId)
if (!work?.task_sessions?.[taskKey]) {
return null
}
const taskSession = work.task_sessions[taskKey]
const endAt = endedAt ?? nowIsoString()
taskSession.ended_at = endAt
taskSession.elapsed_ms = getElapsedMs(taskSession.started_at, endAt)
taskSession.status = "completed"
taskSession.updated_at = nowIsoString()
work.updated_at = nowIsoString()
if (state.active_work_id === workId) {
projectWorkToMirror(state, work)
}
return writeBoulderState(directory, state) ? state : null
}
@@ -0,0 +1,190 @@
import { existsSync, mkdirSync, unlinkSync, writeFileSync } from "node:fs"
import { dirname } from "node:path"
import type { BoulderState, BoulderWorkState } from "../types"
import { getBoulderFilePath } from "./path"
import { getPlanName } from "./plan-progress"
import { getBoulderWorks, readBoulderState } from "./read-state"
import { getElapsedMs, nowIsoString, projectWorkToMirror } from "./shared"
export function writeBoulderState(directory: string, state: BoulderState): boolean {
const filePath = getBoulderFilePath(directory)
try {
const dir = dirname(filePath)
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
const stateToWrite: BoulderState = { ...state }
if (stateToWrite.works && stateToWrite.active_work_id) {
const activeWork = stateToWrite.works[stateToWrite.active_work_id]
if (activeWork) {
stateToWrite.works = {
...stateToWrite.works,
[stateToWrite.active_work_id]: {
...activeWork,
active_plan: stateToWrite.active_plan,
plan_name: stateToWrite.plan_name,
status: stateToWrite.status,
started_at: stateToWrite.started_at,
ended_at: stateToWrite.ended_at,
elapsed_ms: stateToWrite.elapsed_ms,
updated_at: stateToWrite.updated_at,
session_ids: [...stateToWrite.session_ids],
session_origins: stateToWrite.session_origins ? { ...stateToWrite.session_origins } : {},
agent: stateToWrite.agent,
worktree_path: stateToWrite.worktree_path,
task_sessions: stateToWrite.task_sessions ? { ...stateToWrite.task_sessions } : {},
},
}
}
}
writeFileSync(filePath, JSON.stringify(stateToWrite, null, 2), "utf-8")
return true
} catch {
return false
}
}
export function clearBoulderState(directory: string): boolean {
const filePath = getBoulderFilePath(directory)
try {
if (existsSync(filePath)) {
unlinkSync(filePath)
}
return true
} catch {
return false
}
}
export function generateWorkId(planName: string): string {
const slug = planName.trim().toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "")
const randomHex = Math.floor(Math.random() * 0xffffffff).toString(16).padStart(8, "0")
return `${slug.length > 0 ? slug : "work"}-${randomHex}`
}
export function createBoulderState(planPath: string, sessionId: string, agent?: string, worktreePath?: string): BoulderState {
const startedAt = nowIsoString()
const workId = generateWorkId(getPlanName(planPath))
const work: BoulderWorkState = {
work_id: workId,
active_plan: planPath,
plan_name: getPlanName(planPath),
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [sessionId],
session_origins: { [sessionId]: "direct" },
...(agent !== undefined ? { agent } : {}),
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
task_sessions: {},
}
return {
schema_version: 2,
active_work_id: workId,
works: { [workId]: work },
active_plan: planPath,
started_at: startedAt,
status: "active",
updated_at: startedAt,
session_ids: [sessionId],
session_origins: { [sessionId]: "direct" },
plan_name: getPlanName(planPath),
task_sessions: {},
...(agent !== undefined ? { agent } : {}),
...(worktreePath !== undefined ? { worktree_path: worktreePath } : {}),
}
}
export function selectActiveWork(directory: string, workId: string): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const works = getBoulderWorks(state)
const nextWork = works.find((work) => work.work_id === workId)
if (!nextWork) {
return null
}
const nextState: BoulderState = {
...state,
schema_version: 2,
active_work_id: workId,
works: state.works ?? Object.fromEntries(works.map((work) => [work.work_id, work])),
}
projectWorkToMirror(nextState, nextWork)
return writeBoulderState(directory, nextState) ? nextState : null
}
export function addBoulderWork(
directory: string,
input: { planPath: string; sessionId: string; agent?: string; worktreePath?: string; startedAt?: string },
): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const workId = generateWorkId(getPlanName(input.planPath))
const startedAt = input.startedAt ?? nowIsoString()
const nextWork: BoulderWorkState = {
work_id: workId,
active_plan: input.planPath,
plan_name: getPlanName(input.planPath),
status: "active",
started_at: startedAt,
updated_at: startedAt,
session_ids: [input.sessionId],
session_origins: { [input.sessionId]: "direct" },
...(input.agent !== undefined ? { agent: input.agent } : {}),
...(input.worktreePath !== undefined ? { worktree_path: input.worktreePath } : {}),
task_sessions: {},
}
const nextState: BoulderState = {
...state,
schema_version: 2,
works: { ...Object.fromEntries(getBoulderWorks(state).map((work) => [work.work_id, work])), [workId]: nextWork },
active_work_id: workId,
}
projectWorkToMirror(nextState, nextWork)
return writeBoulderState(directory, nextState) ? nextState : null
}
export function completeBoulder(directory: string, workId?: string, endedAt?: string): BoulderState | null {
const state = readBoulderState(directory)
if (!state) {
return null
}
const targetWorkId = workId ?? state.active_work_id
if (!targetWorkId) {
return null
}
const work = state.works?.[targetWorkId] ?? getBoulderWorks(state).find((candidate) => candidate.work_id === targetWorkId)
if (!work) {
return null
}
if (work.status === "completed" && work.ended_at !== undefined && work.elapsed_ms !== undefined) {
return state
}
const endAt = endedAt ?? nowIsoString()
work.ended_at = endAt
work.elapsed_ms = getElapsedMs(work.started_at, endAt)
work.status = "completed"
work.updated_at = nowIsoString()
if (state.active_work_id === targetWorkId) {
projectWorkToMirror(state, work)
}
return writeBoulderState(directory, state) ? state : null
}
@@ -0,0 +1,69 @@
import { existsSync, readFileSync } from "node:fs"
import type { TopLevelTaskRef } from "./types"
const TODO_HEADING_PATTERN = /^##\s+TODOs\b/i
const FINAL_VERIFICATION_HEADING_PATTERN = /^##\s+Final Verification Wave\b/i
const SECOND_LEVEL_HEADING_PATTERN = /^##\s+/
const UNCHECKED_CHECKBOX_PATTERN = /^(\s*)[-*]\s*\[\s*\]\s*(.+)$/
const TODO_TASK_PATTERN = /^(\d+)\.\s+(.+)$/
const FINAL_WAVE_TASK_PATTERN = /^(F\d+)\.\s+(.+)$/i
type PlanSection = "todo" | "final-wave" | "other"
function buildTaskRef(section: "todo" | "final-wave", taskLabel: string): TopLevelTaskRef | null {
const pattern = section === "todo" ? TODO_TASK_PATTERN : FINAL_WAVE_TASK_PATTERN
const match = taskLabel.match(pattern)
if (!match) {
return null
}
const rawLabel = match[1]
const title = match[2].trim()
return {
key: `${section}:${rawLabel.toLowerCase()}`,
section,
label: rawLabel,
title,
}
}
export function readCurrentTopLevelTask(planPath: string): TopLevelTaskRef | null {
if (!existsSync(planPath)) {
return null
}
try {
const content = readFileSync(planPath, "utf-8")
const lines = content.split(/\r?\n/)
let section: PlanSection = "other"
for (const line of lines) {
if (SECOND_LEVEL_HEADING_PATTERN.test(line)) {
section = TODO_HEADING_PATTERN.test(line)
? "todo"
: FINAL_VERIFICATION_HEADING_PATTERN.test(line)
? "final-wave"
: "other"
}
const uncheckedTaskMatch = line.match(UNCHECKED_CHECKBOX_PATTERN)
if (!uncheckedTaskMatch || uncheckedTaskMatch[1].length > 0) {
continue
}
if (section !== "todo" && section !== "final-wave") {
continue
}
const taskRef = buildTaskRef(section, uncheckedTaskMatch[2].trim())
if (taskRef) {
return taskRef
}
}
return null
} catch {
return null
}
}
+79
View File
@@ -0,0 +1,79 @@
export interface BoulderState {
schema_version?: 2
active_work_id?: string
works?: Record<string, BoulderWorkState>
active_plan: string
started_at: string
ended_at?: string
elapsed_ms?: number
status?: BoulderWorkStatus
updated_at?: string
session_ids: string[]
session_origins?: Record<string, "direct" | "appended">
plan_name: string
agent?: string
worktree_path?: string
task_sessions?: Record<string, TaskSessionState>
}
export type BoulderSessionOrigin = "direct" | "appended"
export type BoulderWorkStatus = "active" | "completed" | "paused" | "abandoned"
export type BoulderTaskStatus = "running" | "completed" | "cancelled"
export interface BoulderWorkState {
work_id: string
active_plan: string
plan_name: string
status?: BoulderWorkStatus
started_at: string
ended_at?: string
elapsed_ms?: number
updated_at?: string
session_ids: string[]
session_origins?: Record<string, BoulderSessionOrigin>
agent?: string
worktree_path?: string
task_sessions?: Record<string, TaskSessionState>
}
export interface PlanProgress {
total: number
completed: number
isComplete: boolean
}
export interface TaskSessionState {
task_key: string
task_label: string
task_title: string
session_id: string
agent?: string
category?: string
started_at?: string
ended_at?: string
elapsed_ms?: number
status?: BoulderTaskStatus
updated_at: string
}
export interface BoulderWorkResumeOption {
work_id: string
plan_name: string
active_plan: string
worktree_path?: string
status: BoulderWorkStatus
started_at: string
updated_at: string
ended_at?: string
elapsed_ms?: number
session_count: number
progress: PlanProgress
is_current_mirror: boolean
}
export interface TopLevelTaskRef {
key: string
section: "todo" | "final-wave"
label: string
title: string
}
+14
View File
@@ -0,0 +1,14 @@
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["ESNext"],
"types": ["bun-types"]
},
"include": ["src/**/*"]
}