Files
oh-my-opencode/src/features/boulder-state/storage.ts
T
Rishi Vhavle 5b89d1d5d6 fix: track agent in boulder state to fix session continuation (fixes #927)
Add 'agent' field to BoulderState to track which agent (atlas) should
resume on session continuation. Previously, when user typed 'continue'
after interruption, Prometheus (planner) resumed instead of Sisyphus
(executor), causing all delegate_task calls to get READ-ONLY mode.

Changes:
- Add optional 'agent' field to BoulderState interface
- Update createBoulderState() to accept agent parameter
- Set agent='atlas' when /start-work creates boulder.json
- Use stored agent on boulder continuation (defaults to 'atlas')
- Add tests for new agent field functionality
2026-02-04 21:21:57 +05:30

153 lines
3.7 KiB
TypeScript

/**
* Boulder State Storage
*
* Handles reading/writing boulder.json for active plan tracking.
*/
import { existsSync, readFileSync, writeFileSync, mkdirSync, readdirSync } from "node:fs"
import { dirname, join, basename } from "node:path"
import type { BoulderState, PlanProgress } from "./types"
import { BOULDER_DIR, BOULDER_FILE, PROMETHEUS_PLANS_DIR } from "./constants"
export function getBoulderFilePath(directory: string): string {
return join(directory, BOULDER_DIR, BOULDER_FILE)
}
export function readBoulderState(directory: string): BoulderState | null {
const filePath = getBoulderFilePath(directory)
if (!existsSync(filePath)) {
return null
}
try {
const content = readFileSync(filePath, "utf-8")
return JSON.parse(content) as BoulderState
} catch {
return null
}
}
export function writeBoulderState(directory: string, state: BoulderState): boolean {
const filePath = getBoulderFilePath(directory)
try {
const dir = dirname(filePath)
if (!existsSync(dir)) {
mkdirSync(dir, { recursive: true })
}
writeFileSync(filePath, JSON.stringify(state, null, 2), "utf-8")
return true
} catch {
return false
}
}
export function appendSessionId(directory: string, sessionId: string): BoulderState | null {
const state = readBoulderState(directory)
if (!state) return null
if (!state.session_ids.includes(sessionId)) {
state.session_ids.push(sessionId)
if (writeBoulderState(directory, state)) {
return state
}
}
return state
}
export function clearBoulderState(directory: string): boolean {
const filePath = getBoulderFilePath(directory)
try {
if (existsSync(filePath)) {
const { unlinkSync } = require("node:fs")
unlinkSync(filePath)
}
return true
} catch {
return false
}
}
/**
* Find Prometheus plan files for this project.
* Prometheus stores plans at: {project}/.sisyphus/plans/{name}.md
*/
export function findPrometheusPlans(directory: string): string[] {
const plansDir = join(directory, PROMETHEUS_PLANS_DIR)
if (!existsSync(plansDir)) {
return []
}
try {
const files = readdirSync(plansDir)
return files
.filter((f) => f.endsWith(".md"))
.map((f) => join(plansDir, f))
.sort((a, b) => {
// Sort by modification time, newest first
const aStat = require("node:fs").statSync(a)
const bStat = require("node:fs").statSync(b)
return bStat.mtimeMs - aStat.mtimeMs
})
} catch {
return []
}
}
/**
* Parse a plan file and count checkbox progress.
*/
export function getPlanProgress(planPath: string): PlanProgress {
if (!existsSync(planPath)) {
return { total: 0, completed: 0, isComplete: true }
}
try {
const content = readFileSync(planPath, "utf-8")
// Match markdown checkboxes: - [ ] or - [x] or - [X]
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,
}
} catch {
return { total: 0, completed: 0, isComplete: true }
}
}
/**
* Extract plan name from file path.
*/
export function getPlanName(planPath: string): string {
return basename(planPath, ".md")
}
/**
* Create a new boulder state for a plan.
*/
export function createBoulderState(
planPath: string,
sessionId: string,
agent?: string
): BoulderState {
return {
active_plan: planPath,
started_at: new Date().toISOString(),
session_ids: [sessionId],
plan_name: getPlanName(planPath),
...(agent !== undefined ? { agent } : {}),
}
}