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 { statSync } from "node:fs"
|
||||||
import {
|
import {
|
||||||
appendSessionId,
|
appendSessionId,
|
||||||
clearBoulderState,
|
addBoulderWork,
|
||||||
createBoulderState,
|
createBoulderState,
|
||||||
findPrometheusPlans,
|
findPrometheusPlans,
|
||||||
|
getActiveWorks,
|
||||||
getPlanName,
|
getPlanName,
|
||||||
getPlanProgress,
|
getPlanProgress,
|
||||||
|
getWorkByPlanName,
|
||||||
|
getWorkResumeOptions,
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
resolveBoulderPlanPath,
|
resolveBoulderPlanPath,
|
||||||
|
selectActiveWork,
|
||||||
writeBoulderState,
|
writeBoulderState,
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
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.`
|
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: {
|
function buildExplicitPlanContext(params: {
|
||||||
explicitPlanName: string
|
explicitPlanName: string
|
||||||
existingState: ReturnType<typeof readBoulderState>
|
|
||||||
sessionId: string
|
sessionId: string
|
||||||
timestamp: string
|
timestamp: string
|
||||||
activeAgent: string
|
activeAgent: string
|
||||||
@@ -109,9 +177,24 @@ function buildExplicitPlanContext(params: {
|
|||||||
worktreeBlock: string
|
worktreeBlock: string
|
||||||
directory: string
|
directory: string
|
||||||
}): 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 })
|
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 allPlans = findPrometheusPlans(directory)
|
||||||
const matchedPlan = findPlanByName(allPlans, explicitPlanName)
|
const matchedPlan = findPlanByName(allPlans, explicitPlanName)
|
||||||
if (!matchedPlan) {
|
if (!matchedPlan) {
|
||||||
@@ -127,9 +210,13 @@ function buildExplicitPlanContext(params: {
|
|||||||
All ${progress.total} tasks are done. Create a new plan using the Prometheus agent.`
|
All ${progress.total} tasks are done. Create a new plan using the Prometheus agent.`
|
||||||
}
|
}
|
||||||
|
|
||||||
if (existingState) {
|
createNewWorkOrInitialize({
|
||||||
clearBoulderState(directory)
|
directory,
|
||||||
}
|
planPath: matchedPlan,
|
||||||
|
sessionId,
|
||||||
|
activeAgent,
|
||||||
|
worktreePath,
|
||||||
|
})
|
||||||
|
|
||||||
return buildAutoSelectedPlanContext({
|
return buildAutoSelectedPlanContext({
|
||||||
planPath: matchedPlan,
|
planPath: matchedPlan,
|
||||||
@@ -287,11 +374,48 @@ export function buildStartWorkContextInfo(params: {
|
|||||||
}): string {
|
}): string {
|
||||||
const { ctx, explicitPlanName, existingState, sessionId, timestamp, activeAgent, worktreePath, worktreeBlock } = params
|
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 = ""
|
let contextInfo = ""
|
||||||
if (explicitPlanName) {
|
if (explicitPlanName) {
|
||||||
contextInfo = buildExplicitPlanContext({
|
contextInfo = buildExplicitPlanContext({
|
||||||
explicitPlanName,
|
explicitPlanName,
|
||||||
existingState,
|
|
||||||
sessionId,
|
sessionId,
|
||||||
timestamp,
|
timestamp,
|
||||||
activeAgent,
|
activeAgent,
|
||||||
|
|||||||
Reference in New Issue
Block a user