feat(workspace): store runtime state under omo

This commit is contained in:
YeonGyu-Kim
2026-05-16 17:40:11 +09:00
parent 5dca1a5742
commit a86221b1a9
13 changed files with 137 additions and 137 deletions
+2 -2
View File
@@ -34,7 +34,7 @@ interface BoulderState {
| File | Purpose |
|------|---------|
| `types.ts` | `BoulderState`, `BoulderWorkState`, `TaskSessionState`, status enums |
| `storage.ts` | Atomic CRUD on `.sisyphus/boulder-state.json`. Writes via temp file + rename; file lock per work_id |
| `storage.ts` | Atomic CRUD on `.omo/boulder-state.json`. Writes via temp file + rename; file lock per work_id |
| `constants.ts` | Path resolution + schema version constant |
| `top-level-task.ts` | Helpers to identify the current top-level plan task and resolve its reusable subagent session |
| `format-duration.ts` | `formatDurationHuman(ms)` — "1h 23m 5s" formatting for boulder duration |
@@ -67,7 +67,7 @@ session.completed
## STORAGE
```
<worktree-root>/.sisyphus/boulder-state.json # gitignored; one file per worktree
<worktree-root>/.omo/boulder-state.json # gitignored; one file per worktree
```
Atomic writes: temp file → fsync (where supported) → rename. File lock prevents concurrent corruption. Schema migrations between versions handled inline in `storage.ts`.
+2 -2
View File
@@ -2,7 +2,7 @@
* Boulder State Constants
*/
export const BOULDER_DIR = ".sisyphus"
export const BOULDER_DIR = ".omo"
export const BOULDER_FILE = "boulder.json"
export const BOULDER_STATE_PATH = `${BOULDER_DIR}/${BOULDER_FILE}`
@@ -10,4 +10,4 @@ export const NOTEPAD_DIR = "notepads"
export const NOTEPAD_BASE_PATH = `${BOULDER_DIR}/${NOTEPAD_DIR}`
/** Prometheus plan directory pattern */
export const PROMETHEUS_PLANS_DIR = ".sisyphus/plans"
export const PROMETHEUS_PLANS_DIR = ".omo/plans"
+31 -31
View File
@@ -34,14 +34,14 @@ import { readCurrentTopLevelTask } from "./top-level-task"
describe("boulder-state", () => {
const TEST_DIR = join(tmpdir(), "boulder-state-test-" + Date.now())
const SISYPHUS_DIR = join(TEST_DIR, ".sisyphus")
const OMO_DIR = join(TEST_DIR, ".omo")
beforeEach(() => {
if (!existsSync(TEST_DIR)) {
mkdirSync(TEST_DIR, { recursive: true })
}
if (!existsSync(SISYPHUS_DIR)) {
mkdirSync(SISYPHUS_DIR, { recursive: true })
if (!existsSync(OMO_DIR)) {
mkdirSync(OMO_DIR, { recursive: true })
}
clearBoulderState(TEST_DIR)
})
@@ -55,7 +55,7 @@ describe("boulder-state", () => {
describe("readBoulderState", () => {
test("should preserve legacy boulder.json fields during round-trip", () => {
// given
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
const legacyRawState = {
active_plan: "/path/to/legacy-plan.md",
started_at: "2026-01-01T00:00:00.000Z",
@@ -88,7 +88,7 @@ describe("boulder-state", () => {
test("should return null for JSON null value", () => {
//#given - boulder.json containing null
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, "null")
//#when
@@ -100,7 +100,7 @@ describe("boulder-state", () => {
test("should return null for JSON primitive value", () => {
//#given - boulder.json containing a string
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, '"just a string"')
//#when
@@ -112,7 +112,7 @@ describe("boulder-state", () => {
test("should default session_ids to [] when missing from JSON", () => {
//#given - boulder.json without session_ids field
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -129,7 +129,7 @@ describe("boulder-state", () => {
test("should default session_ids to [] when not an array", () => {
//#given - boulder.json with session_ids as a string
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -147,7 +147,7 @@ describe("boulder-state", () => {
test("should default session_ids to [] for empty object", () => {
//#given - boulder.json with empty object
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({}))
//#when
@@ -160,7 +160,7 @@ describe("boulder-state", () => {
test("should backfill missing origin as direct only for a single tracked session", () => {
// given
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -177,7 +177,7 @@ describe("boulder-state", () => {
test("should keep missing origins empty when multiple sessions are tracked", () => {
// given
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -213,7 +213,7 @@ describe("boulder-state", () => {
test("should default task_sessions to empty object when missing from JSON", () => {
// given - boulder.json without task_sessions field
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/path/to/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -231,7 +231,7 @@ describe("boulder-state", () => {
})
describe("writeBoulderState", () => {
test("should write state and create .sisyphus directory if needed", () => {
test("should write state and create .omo directory if needed", () => {
// given - state to write
const state: BoulderState = {
active_plan: "/test/plan.md",
@@ -298,7 +298,7 @@ describe("boulder-state", () => {
test("should not crash when boulder.json has no session_ids field", () => {
//#given - boulder.json without session_ids
const boulderFile = join(SISYPHUS_DIR, "boulder.json")
const boulderFile = join(OMO_DIR, "boulder.json")
writeFileSync(boulderFile, JSON.stringify({
active_plan: "/plan.md",
started_at: "2026-01-01T00:00:00Z",
@@ -430,7 +430,7 @@ describe("boulder-state", () => {
test("should add second work and keep both active works", () => {
// given
const firstState = createBoulderState(
join(TEST_DIR, ".sisyphus/plans/plan-a.md"),
join(TEST_DIR, ".omo/plans/plan-a.md"),
"session-a",
"atlas",
"/worktree-a",
@@ -440,7 +440,7 @@ describe("boulder-state", () => {
// when
const updatedState = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
planPath: join(TEST_DIR, ".omo/plans/plan-b.md"),
sessionId: "session-b",
agent: "atlas",
worktreePath: "/worktree-b",
@@ -459,12 +459,12 @@ describe("boulder-state", () => {
test("should resolve work for session using updated_at tie-break", () => {
// given
const baseState = createBoulderState(
join(TEST_DIR, ".sisyphus/plans/plan-a.md"),
join(TEST_DIR, ".omo/plans/plan-a.md"),
"session-a",
)
writeBoulderState(TEST_DIR, baseState)
const stateWithSecond = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
planPath: join(TEST_DIR, ".omo/plans/plan-b.md"),
sessionId: "session-b",
})
expect(stateWithSecond).not.toBeNull()
@@ -486,10 +486,10 @@ describe("boulder-state", () => {
test("should support selecting active work and read helpers", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
const initialState = createBoulderState(join(TEST_DIR, ".omo/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const added = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
planPath: join(TEST_DIR, ".omo/plans/plan-b.md"),
sessionId: "session-b",
worktreePath: "/tmp/worktree-b",
})
@@ -516,7 +516,7 @@ describe("boulder-state", () => {
test("should upsert task session for specific work and keep first started_at", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
const initialState = createBoulderState(join(TEST_DIR, ".omo/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const workId = initialState.active_work_id!
@@ -550,7 +550,7 @@ describe("boulder-state", () => {
describe("task timer and completion helpers", () => {
test("should keep started_at stable when starting timer repeatedly", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
const initialState = createBoulderState(join(TEST_DIR, ".omo/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const workId = initialState.active_work_id!
@@ -578,7 +578,7 @@ describe("boulder-state", () => {
test("should compute elapsed_ms when ending task timer", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
const initialState = createBoulderState(join(TEST_DIR, ".omo/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const workId = initialState.active_work_id!
startTaskTimer(TEST_DIR, workId, {
@@ -601,11 +601,11 @@ describe("boulder-state", () => {
test("should complete one work and keep other work untouched", () => {
// given
const initialState = createBoulderState(join(TEST_DIR, ".sisyphus/plans/plan-a.md"), "session-a")
const initialState = createBoulderState(join(TEST_DIR, ".omo/plans/plan-a.md"), "session-a")
writeBoulderState(TEST_DIR, initialState)
const firstWorkId = initialState.active_work_id!
const withSecond = addBoulderWork(TEST_DIR, {
planPath: join(TEST_DIR, ".sisyphus/plans/plan-b.md"),
planPath: join(TEST_DIR, ".omo/plans/plan-b.md"),
sessionId: "session-b",
})
const secondWorkId = Object.keys(withSecond!.works!).find((workId) => workId !== firstWorkId)!
@@ -620,13 +620,13 @@ describe("boulder-state", () => {
Date.parse("2026-01-01T01:00:00.000Z") - Date.parse(completedState!.works![firstWorkId]!.started_at),
)
expect(completedState?.works?.[secondWorkId]?.status).not.toBe("completed")
expect(existsSync(join(SISYPHUS_DIR, "boulder.json"))).toBe(true)
expect(existsSync(join(OMO_DIR, "boulder.json"))).toBe(true)
})
test("should keep first completion timing when completeBoulder is called repeatedly", () => {
// given
const initialState = createBoulderState(
join(TEST_DIR, ".sisyphus/plans/plan-idempotent.md"),
join(TEST_DIR, ".omo/plans/plan-idempotent.md"),
"session-a",
)
writeBoulderState(TEST_DIR, initialState)
@@ -974,7 +974,7 @@ describe("boulder-state", () => {
describe("getPlanName", () => {
test("should extract plan name from path", () => {
// given
const path = "/home/user/.sisyphus/plans/project/my-feature.md"
const path = "/home/user/.omo/plans/project/my-feature.md"
// when
const name = getPlanName(path)
// then
@@ -1042,9 +1042,9 @@ describe("boulder-state", () => {
describe("resolveBoulderPlanPath", () => {
test("should prefer the mirrored worktree plan when it exists", () => {
// given
const planPath = join(TEST_DIR, ".sisyphus", "plans", "worktree-plan.md")
const planPath = join(TEST_DIR, ".omo", "plans", "worktree-plan.md")
const worktreeDir = join(tmpdir(), `boulder-state-worktree-${Date.now()}`)
const worktreePlanPath = join(worktreeDir, ".sisyphus", "plans", "worktree-plan.md")
const worktreePlanPath = join(worktreeDir, ".omo", "plans", "worktree-plan.md")
mkdirSync(dirname(planPath), { recursive: true })
mkdirSync(dirname(worktreePlanPath), { recursive: true })
writeFileSync(planPath, "# Plan\n- [ ] Main repo task\n")
@@ -1066,7 +1066,7 @@ describe("boulder-state", () => {
test("should fall back to the tracked plan when the mirrored worktree plan is missing", () => {
// given
const planPath = join(TEST_DIR, ".sisyphus", "plans", "fallback-plan.md")
const planPath = join(TEST_DIR, ".omo", "plans", "fallback-plan.md")
mkdirSync(dirname(planPath), { recursive: true })
writeFileSync(planPath, "# Plan\n- [ ] Main repo task\n")
+1 -1
View File
@@ -348,7 +348,7 @@ export function upsertTaskSessionState(
/**
* Find Prometheus plan files for this project.
* Prometheus stores plans at: {project}/.sisyphus/plans/{name}.md
* Prometheus stores plans at: {project}/.omo/plans/{name}.md
*/
export function findPrometheusPlans(directory: string): string[] {
const plansDir = join(directory, PROMETHEUS_PLANS_DIR)
@@ -11,9 +11,9 @@ export const START_WORK_TEMPLATE = `You are starting a Sisyphus work session.
## WHAT TO DO
1. **Find available plans**: Search for Prometheus-generated plan files at \`.sisyphus/plans/\`
1. **Find available plans**: Search for Prometheus-generated plan files at \`.omo/plans/\`
2. **Check for active boulder state**: Read \`.sisyphus/boulder.json\` if it exists
2. **Check for active boulder state**: Read \`.omo/boulder.json\` if it exists
3. **Decision logic**:
- If multiple active works are listed in your context:
@@ -119,10 +119,10 @@ Register these as task/todo items so progress is tracked and visible throughout
When working in a worktree (\`worktree_path\` is set in boulder.json) and ALL plan tasks are complete:
1. Commit all remaining changes in the worktree
2. **Sync .sisyphus state back**: Copy \`.sisyphus/\` from the worktree to the main repo before removal.
This is CRITICAL when \`.sisyphus/\` is gitignored - state written during worktree execution would otherwise be lost.
2. **Sync .omo state back**: Copy \`.omo/\` from the worktree to the main repo before removal.
This is CRITICAL when \`.omo/\` is gitignored - state written during worktree execution would otherwise be lost.
\`\`\`bash
cp -r <worktree-path>/.sisyphus/* <main-repo>/.sisyphus/ 2>/dev/null || true
cp -r <worktree-path>/.omo/* <main-repo>/.omo/ 2>/dev/null || true
\`\`\`
3. Switch to the main working directory (the original repo, NOT the worktree)
4. Merge the worktree branch into the current branch: \`git merge <worktree-branch>\`
@@ -1 +1 @@
export const CONTINUATION_MARKER_DIR = ".sisyphus/run-continuation"
export const CONTINUATION_MARKER_DIR = ".omo/run-continuation"