feat(start-work): use getWorkResumeOptions for multi-work resume selection
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/// <reference types="bun-types" />
|
||||
|
||||
import { afterEach, beforeEach, describe, expect, spyOn, test } from "bun:test"
|
||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
||||
import { randomUUID } from "node:crypto"
|
||||
import { join } from "node:path"
|
||||
import { tmpdir } from "node:os"
|
||||
import { buildStartWorkContextInfo } from "./context-info-builder"
|
||||
import {
|
||||
addBoulderWork,
|
||||
createBoulderState,
|
||||
getBoulderFilePath,
|
||||
getWorkByPlanName,
|
||||
readBoulderState,
|
||||
writeBoulderState,
|
||||
} from "../../features/boulder-state"
|
||||
import * as boulderState from "../../features/boulder-state"
|
||||
|
||||
describe("buildStartWorkContextInfo", () => {
|
||||
let testDirectory = ""
|
||||
|
||||
function createPluginInput() {
|
||||
return {
|
||||
directory: testDirectory,
|
||||
} as never
|
||||
}
|
||||
|
||||
function writePlan(planName: string, content: string): string {
|
||||
const plansDirectory = join(testDirectory, ".sisyphus", "plans")
|
||||
mkdirSync(plansDirectory, { recursive: true })
|
||||
const planPath = join(plansDirectory, `${planName}.md`)
|
||||
writeFileSync(planPath, content)
|
||||
return planPath
|
||||
}
|
||||
|
||||
function readExistingState() {
|
||||
return readBoulderState(testDirectory)
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
testDirectory = join(tmpdir(), `context-info-builder-${randomUUID()}`)
|
||||
mkdirSync(testDirectory, { recursive: true })
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
if (existsSync(testDirectory)) {
|
||||
rmSync(testDirectory, { recursive: true, force: true })
|
||||
}
|
||||
})
|
||||
|
||||
test("lists multiple active works and asks agent to choose resume vs new when no explicit plan", () => {
|
||||
// given
|
||||
const clearSpy = spyOn(boulderState, "clearBoulderState")
|
||||
const planAPath = writePlan("plan-alpha", "## TODOs\n- [ ] 1. Alpha")
|
||||
const planBPath = writePlan("plan-beta", "## TODOs\n- [ ] 1. Beta")
|
||||
const initialState = createBoulderState(planAPath, "session-a", "atlas", "/tmp/worktree-a")
|
||||
writeBoulderState(testDirectory, initialState)
|
||||
addBoulderWork(testDirectory, {
|
||||
planPath: planBPath,
|
||||
sessionId: "session-b",
|
||||
agent: "atlas",
|
||||
worktreePath: "/tmp/worktree-b",
|
||||
})
|
||||
|
||||
// when
|
||||
const contextInfo = buildStartWorkContextInfo({
|
||||
ctx: createPluginInput(),
|
||||
explicitPlanName: null,
|
||||
existingState: readExistingState(),
|
||||
sessionId: "session-current",
|
||||
timestamp: "2026-05-11T00:00:00.000Z",
|
||||
activeAgent: "atlas",
|
||||
worktreePath: undefined,
|
||||
worktreeBlock: "",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(contextInfo).toContain("plan-alpha")
|
||||
expect(contextInfo).toContain("plan-beta")
|
||||
expect(contextInfo).toContain("Use the Question tool")
|
||||
expect(clearSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test("auto-resumes when exactly one active work exists and no explicit plan", () => {
|
||||
// given
|
||||
const clearSpy = spyOn(boulderState, "clearBoulderState")
|
||||
const planPath = writePlan("single-active-plan", "## TODOs\n- [ ] 1. Single task")
|
||||
const initialState = createBoulderState(planPath, "session-a", "atlas", "/tmp/worktree-single")
|
||||
writeBoulderState(testDirectory, initialState)
|
||||
|
||||
// when
|
||||
const contextInfo = buildStartWorkContextInfo({
|
||||
ctx: createPluginInput(),
|
||||
explicitPlanName: null,
|
||||
existingState: readExistingState(),
|
||||
sessionId: "session-current",
|
||||
timestamp: "2026-05-11T00:00:00.000Z",
|
||||
activeAgent: "atlas",
|
||||
worktreePath: undefined,
|
||||
worktreeBlock: "",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(contextInfo).toContain("RESUMING existing work")
|
||||
expect(contextInfo).toContain("single-active-plan")
|
||||
expect(contextInfo).not.toContain("Use the Question tool")
|
||||
expect(clearSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
|
||||
test("explicit plan selects matching work only and never clears boulder state", () => {
|
||||
// given
|
||||
const clearSpy = spyOn(boulderState, "clearBoulderState")
|
||||
const planAPath = writePlan("explicit-plan-a", "## TODOs\n- [ ] 1. A")
|
||||
const planBPath = writePlan("explicit-plan-b", "## TODOs\n- [ ] 1. B")
|
||||
const initialState = createBoulderState(planAPath, "session-a", "atlas", "/tmp/worktree-a")
|
||||
writeBoulderState(testDirectory, initialState)
|
||||
addBoulderWork(testDirectory, {
|
||||
planPath: planBPath,
|
||||
sessionId: "session-b",
|
||||
agent: "atlas",
|
||||
worktreePath: "/tmp/worktree-b",
|
||||
})
|
||||
|
||||
// when
|
||||
const contextInfo = buildStartWorkContextInfo({
|
||||
ctx: createPluginInput(),
|
||||
explicitPlanName: "explicit-plan-a",
|
||||
existingState: readExistingState(),
|
||||
sessionId: "session-current",
|
||||
timestamp: "2026-05-11T00:00:00.000Z",
|
||||
activeAgent: "atlas",
|
||||
worktreePath: "/tmp/worktree-a",
|
||||
worktreeBlock: "",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(contextInfo).toContain("explicit-plan-a")
|
||||
expect(contextInfo).not.toContain("explicit-plan-b")
|
||||
expect(clearSpy).toHaveBeenCalledTimes(0)
|
||||
|
||||
const selectedWork = getWorkByPlanName(testDirectory, "explicit-plan-a", { worktreePath: "/tmp/worktree-a" })
|
||||
const nextState = readBoulderState(testDirectory)
|
||||
expect(selectedWork).not.toBeNull()
|
||||
expect(nextState?.active_work_id).toBe(selectedWork?.work_id)
|
||||
})
|
||||
|
||||
test("falls back to auto-select latest plan when no works exist", () => {
|
||||
// given
|
||||
const clearSpy = spyOn(boulderState, "clearBoulderState")
|
||||
const coldStartPlanPath = writePlan("cold-start-plan", "## TODOs\n- [ ] 1. Cold start")
|
||||
|
||||
// when
|
||||
const contextInfo = buildStartWorkContextInfo({
|
||||
ctx: createPluginInput(),
|
||||
explicitPlanName: null,
|
||||
existingState: null,
|
||||
sessionId: "session-current",
|
||||
timestamp: "2026-05-11T00:00:00.000Z",
|
||||
activeAgent: "atlas",
|
||||
worktreePath: undefined,
|
||||
worktreeBlock: "",
|
||||
})
|
||||
|
||||
// then
|
||||
expect(contextInfo).toContain("Auto-Selected Plan")
|
||||
expect(contextInfo).toContain("cold-start-plan")
|
||||
expect(contextInfo).toContain(coldStartPlanPath)
|
||||
expect(existsSync(getBoulderFilePath(testDirectory))).toBe(true)
|
||||
expect(clearSpy).toHaveBeenCalledTimes(0)
|
||||
})
|
||||
})
|
||||
@@ -1,13 +1,17 @@
|
||||
import { statSync } from "node:fs"
|
||||
import {
|
||||
appendSessionId,
|
||||
clearBoulderState,
|
||||
addBoulderWork,
|
||||
createBoulderState,
|
||||
findPrometheusPlans,
|
||||
getActiveWorks,
|
||||
getPlanName,
|
||||
getPlanProgress,
|
||||
getWorkByPlanName,
|
||||
getWorkResumeOptions,
|
||||
readBoulderState,
|
||||
resolveBoulderPlanPath,
|
||||
selectActiveWork,
|
||||
writeBoulderState,
|
||||
} from "../../features/boulder-state"
|
||||
import { log } from "../../shared/logger"
|
||||
@@ -99,9 +103,73 @@ Ask the user which plan to work on.`
|
||||
No incomplete plans available. Create a new plan using the Prometheus agent.`
|
||||
}
|
||||
|
||||
function formatElapsedHuman(elapsedMs: number | undefined): string {
|
||||
if (typeof elapsedMs !== "number" || elapsedMs <= 0) {
|
||||
return "running"
|
||||
}
|
||||
|
||||
const totalSeconds = Math.floor(elapsedMs / 1000)
|
||||
const seconds = totalSeconds % 60
|
||||
const totalMinutes = Math.floor(totalSeconds / 60)
|
||||
const minutes = totalMinutes % 60
|
||||
const hours = Math.floor(totalMinutes / 60)
|
||||
if (hours > 0) {
|
||||
return `${hours}h ${minutes}m ${seconds}s`
|
||||
}
|
||||
if (minutes > 0) {
|
||||
return `${minutes}m ${seconds}s`
|
||||
}
|
||||
return `${seconds}s`
|
||||
}
|
||||
|
||||
function buildMultipleActiveWorksContext(params: {
|
||||
resumeOptions: ReturnType<typeof getWorkResumeOptions>
|
||||
sessionId: string
|
||||
timestamp: string
|
||||
}): string {
|
||||
const { resumeOptions, sessionId, timestamp } = params
|
||||
const optionList = resumeOptions
|
||||
.map((option, index) => `${index + 1}. ${option.plan_name} - ${option.progress.completed}/${option.progress.total} (${option.progress.total === 0 ? 0 : Math.floor((option.progress.completed / option.progress.total) * 100)}%) - elapsed: ${formatElapsedHuman(option.elapsed_ms)} - worktree: ${option.worktree_path ?? "current directory"} - sessions: ${option.session_count}`)
|
||||
.join("\n")
|
||||
|
||||
return `
|
||||
<system-reminder>
|
||||
## Multiple Active Works Found
|
||||
|
||||
Current Time: ${timestamp}
|
||||
Session ID: ${sessionId}
|
||||
|
||||
${optionList}
|
||||
|
||||
Use the Question tool to ask the user which plan to resume.
|
||||
- If the user chooses one option, run /start-work {plan-name} for that plan.
|
||||
- If the user chooses to start a new plan, proceed with cold-start auto-selection flow.
|
||||
</system-reminder>`
|
||||
}
|
||||
|
||||
function createNewWorkOrInitialize(params: {
|
||||
directory: string
|
||||
planPath: string
|
||||
sessionId: string
|
||||
activeAgent: string
|
||||
worktreePath: string | undefined
|
||||
}): void {
|
||||
const { directory, planPath, sessionId, activeAgent, worktreePath } = params
|
||||
const created = addBoulderWork(directory, {
|
||||
planPath,
|
||||
sessionId,
|
||||
agent: activeAgent,
|
||||
worktreePath,
|
||||
})
|
||||
|
||||
if (!created) {
|
||||
const initializedState = createBoulderState(planPath, sessionId, activeAgent, worktreePath)
|
||||
writeBoulderState(directory, initializedState)
|
||||
}
|
||||
}
|
||||
|
||||
function buildExplicitPlanContext(params: {
|
||||
explicitPlanName: string
|
||||
existingState: ReturnType<typeof readBoulderState>
|
||||
sessionId: string
|
||||
timestamp: string
|
||||
activeAgent: string
|
||||
@@ -109,9 +177,24 @@ function buildExplicitPlanContext(params: {
|
||||
worktreeBlock: string
|
||||
directory: string
|
||||
}): string {
|
||||
const { explicitPlanName, existingState, sessionId, timestamp, activeAgent, worktreePath, worktreeBlock, directory } = params
|
||||
const { explicitPlanName, sessionId, timestamp, activeAgent, worktreePath, worktreeBlock, directory } = params
|
||||
log(`[${HOOK_NAME}] Explicit plan name requested: ${explicitPlanName}`, { sessionID: sessionId })
|
||||
|
||||
const matchedWork = getWorkByPlanName(directory, explicitPlanName, { worktreePath })
|
||||
if (matchedWork) {
|
||||
const selectedState = selectActiveWork(directory, matchedWork.work_id)
|
||||
if (selectedState) {
|
||||
return buildExistingSessionContext({
|
||||
existingState: selectedState,
|
||||
sessionId,
|
||||
activeAgent,
|
||||
worktreePath,
|
||||
worktreeBlock,
|
||||
directory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const allPlans = findPrometheusPlans(directory)
|
||||
const matchedPlan = findPlanByName(allPlans, explicitPlanName)
|
||||
if (!matchedPlan) {
|
||||
@@ -127,9 +210,13 @@ function buildExplicitPlanContext(params: {
|
||||
All ${progress.total} tasks are done. Create a new plan using the Prometheus agent.`
|
||||
}
|
||||
|
||||
if (existingState) {
|
||||
clearBoulderState(directory)
|
||||
}
|
||||
createNewWorkOrInitialize({
|
||||
directory,
|
||||
planPath: matchedPlan,
|
||||
sessionId,
|
||||
activeAgent,
|
||||
worktreePath,
|
||||
})
|
||||
|
||||
return buildAutoSelectedPlanContext({
|
||||
planPath: matchedPlan,
|
||||
@@ -287,11 +374,48 @@ export function buildStartWorkContextInfo(params: {
|
||||
}): string {
|
||||
const { ctx, explicitPlanName, existingState, sessionId, timestamp, activeAgent, worktreePath, worktreeBlock } = params
|
||||
|
||||
const resumeOptions = getWorkResumeOptions(ctx.directory)
|
||||
.filter((option) => option.status === "active" || option.status === "paused")
|
||||
|
||||
if (!explicitPlanName && resumeOptions.length > 1) {
|
||||
return buildMultipleActiveWorksContext({
|
||||
resumeOptions,
|
||||
sessionId,
|
||||
timestamp,
|
||||
})
|
||||
}
|
||||
|
||||
if (!explicitPlanName && resumeOptions.length === 1) {
|
||||
const onlyOption = resumeOptions[0]
|
||||
const selectedState = selectActiveWork(ctx.directory, onlyOption.work_id)
|
||||
if (selectedState) {
|
||||
return buildExistingSessionContext({
|
||||
existingState: selectedState,
|
||||
sessionId,
|
||||
activeAgent,
|
||||
worktreePath,
|
||||
worktreeBlock,
|
||||
directory: ctx.directory,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (!explicitPlanName && resumeOptions.length === 0 && getActiveWorks(ctx.directory).length === 0) {
|
||||
return buildPlanDiscoveryContext({
|
||||
contextInfo: "",
|
||||
sessionId,
|
||||
timestamp,
|
||||
activeAgent,
|
||||
worktreePath,
|
||||
worktreeBlock,
|
||||
directory: ctx.directory,
|
||||
})
|
||||
}
|
||||
|
||||
let contextInfo = ""
|
||||
if (explicitPlanName) {
|
||||
contextInfo = buildExplicitPlanContext({
|
||||
explicitPlanName,
|
||||
existingState,
|
||||
sessionId,
|
||||
timestamp,
|
||||
activeAgent,
|
||||
|
||||
Reference in New Issue
Block a user