5b89d1d5d6
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
153 lines
3.7 KiB
TypeScript
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 } : {}),
|
|
}
|
|
}
|