feat(start-work): add --worktree flag support in hook
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
parent
f500fb0286
commit
f872f5e171
@ -7,9 +7,12 @@ import { createStartWorkHook } from "./index"
|
|||||||
import {
|
import {
|
||||||
writeBoulderState,
|
writeBoulderState,
|
||||||
clearBoulderState,
|
clearBoulderState,
|
||||||
|
readBoulderState,
|
||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import type { BoulderState } from "../../features/boulder-state"
|
import type { BoulderState } from "../../features/boulder-state"
|
||||||
import * as sessionState from "../../features/claude-code-session-state"
|
import * as sessionState from "../../features/claude-code-session-state"
|
||||||
|
import * as worktreeDetector from "./worktree-detector"
|
||||||
|
import * as worktreeDetector from "./worktree-detector"
|
||||||
|
|
||||||
describe("start-work hook", () => {
|
describe("start-work hook", () => {
|
||||||
let testDir: string
|
let testDir: string
|
||||||
@ -402,4 +405,152 @@ describe("start-work hook", () => {
|
|||||||
updateSpy.mockRestore()
|
updateSpy.mockRestore()
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("worktree support", () => {
|
||||||
|
let detectSpy: ReturnType<typeof spyOn>
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
detectSpy = spyOn(worktreeDetector, "detectWorktreePath").mockReturnValue(null)
|
||||||
|
})
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
detectSpy.mockRestore()
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should inject model-decides instructions when no --worktree flag", async () => {
|
||||||
|
// given - single plan, no worktree flag
|
||||||
|
const plansDir = join(testDir, ".sisyphus", "plans")
|
||||||
|
mkdirSync(plansDir, { recursive: true })
|
||||||
|
writeFileSync(join(plansDir, "my-plan.md"), "# Plan\n- [ ] Task 1")
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context></session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-123" }, output)
|
||||||
|
|
||||||
|
// then - model-decides instructions should appear
|
||||||
|
expect(output.parts[0].text).toContain("Worktree Setup Required")
|
||||||
|
expect(output.parts[0].text).toContain("git worktree list --porcelain")
|
||||||
|
expect(output.parts[0].text).toContain("git worktree add")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should inject worktree path when --worktree flag is valid", async () => {
|
||||||
|
// given - single plan + valid worktree path
|
||||||
|
const plansDir = join(testDir, ".sisyphus", "plans")
|
||||||
|
mkdirSync(plansDir, { recursive: true })
|
||||||
|
writeFileSync(join(plansDir, "my-plan.md"), "# Plan\n- [ ] Task 1")
|
||||||
|
detectSpy.mockReturnValue("/validated/worktree")
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /validated/worktree</user-request>\n</session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-123" }, output)
|
||||||
|
|
||||||
|
// then - validated path shown, no model-decides instructions
|
||||||
|
expect(output.parts[0].text).toContain("**Worktree**: /validated/worktree")
|
||||||
|
expect(output.parts[0].text).not.toContain("Worktree Setup Required")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should store worktree_path in boulder when --worktree is valid", async () => {
|
||||||
|
// given - plan + valid worktree
|
||||||
|
const plansDir = join(testDir, ".sisyphus", "plans")
|
||||||
|
mkdirSync(plansDir, { recursive: true })
|
||||||
|
writeFileSync(join(plansDir, "my-plan.md"), "# Plan\n- [ ] Task 1")
|
||||||
|
detectSpy.mockReturnValue("/valid/wt")
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /valid/wt</user-request>\n</session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-123" }, output)
|
||||||
|
|
||||||
|
// then - boulder.json has worktree_path
|
||||||
|
const state = readBoulderState(testDir)
|
||||||
|
expect(state?.worktree_path).toBe("/valid/wt")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should NOT store worktree_path when --worktree path is invalid", async () => {
|
||||||
|
// given - plan + invalid worktree path (detectWorktreePath returns null)
|
||||||
|
const plansDir = join(testDir, ".sisyphus", "plans")
|
||||||
|
mkdirSync(plansDir, { recursive: true })
|
||||||
|
writeFileSync(join(plansDir, "my-plan.md"), "# Plan\n- [ ] Task 1")
|
||||||
|
// detectSpy already returns null by default
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /nonexistent/wt</user-request>\n</session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-123" }, output)
|
||||||
|
|
||||||
|
// then - worktree_path absent, setup instructions present
|
||||||
|
const state = readBoulderState(testDir)
|
||||||
|
expect(state?.worktree_path).toBeUndefined()
|
||||||
|
expect(output.parts[0].text).toContain("needs setup")
|
||||||
|
expect(output.parts[0].text).toContain("git worktree add /nonexistent/wt")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should update boulder worktree_path on resume when new --worktree given", async () => {
|
||||||
|
// given - existing boulder with old worktree, user provides new worktree
|
||||||
|
const planPath = join(testDir, "plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1")
|
||||||
|
const existingState: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["old-session"],
|
||||||
|
plan_name: "plan",
|
||||||
|
worktree_path: "/old/wt",
|
||||||
|
}
|
||||||
|
writeBoulderState(testDir, existingState)
|
||||||
|
detectSpy.mockReturnValue("/new/wt")
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context>\n<user-request>--worktree /new/wt</user-request>\n</session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-456" }, output)
|
||||||
|
|
||||||
|
// then - boulder reflects updated worktree and new session appended
|
||||||
|
const state = readBoulderState(testDir)
|
||||||
|
expect(state?.worktree_path).toBe("/new/wt")
|
||||||
|
expect(state?.session_ids).toContain("session-456")
|
||||||
|
})
|
||||||
|
|
||||||
|
test("should show existing worktree on resume when no --worktree flag", async () => {
|
||||||
|
// given - existing boulder already has worktree_path, no flag given
|
||||||
|
const planPath = join(testDir, "plan.md")
|
||||||
|
writeFileSync(planPath, "# Plan\n- [ ] Task 1")
|
||||||
|
const existingState: BoulderState = {
|
||||||
|
active_plan: planPath,
|
||||||
|
started_at: "2026-01-01T00:00:00Z",
|
||||||
|
session_ids: ["old-session"],
|
||||||
|
plan_name: "plan",
|
||||||
|
worktree_path: "/existing/wt",
|
||||||
|
}
|
||||||
|
writeBoulderState(testDir, existingState)
|
||||||
|
|
||||||
|
const hook = createStartWorkHook(createMockPluginInput())
|
||||||
|
const output = {
|
||||||
|
parts: [{ type: "text", text: "<session-context></session-context>" }],
|
||||||
|
}
|
||||||
|
|
||||||
|
// when
|
||||||
|
await hook["chat.message"]({ sessionID: "session-789" }, output)
|
||||||
|
|
||||||
|
// then - shows existing worktree, no model-decides instructions
|
||||||
|
expect(output.parts[0].text).toContain("/existing/wt")
|
||||||
|
expect(output.parts[0].text).not.toContain("Worktree Setup Required")
|
||||||
|
})
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@ -1 +1,4 @@
|
|||||||
export { HOOK_NAME, createStartWorkHook } from "./start-work-hook"
|
export { HOOK_NAME, createStartWorkHook } from "./start-work-hook"
|
||||||
|
export { detectWorktreePath } from "./worktree-detector"
|
||||||
|
export type { ParsedUserRequest } from "./parse-user-request"
|
||||||
|
export { parseUserRequest } from "./parse-user-request"
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { statSync } from "node:fs"
|
||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import {
|
import {
|
||||||
readBoulderState,
|
readBoulderState,
|
||||||
@ -11,11 +12,11 @@ import {
|
|||||||
} from "../../features/boulder-state"
|
} from "../../features/boulder-state"
|
||||||
import { log } from "../../shared/logger"
|
import { log } from "../../shared/logger"
|
||||||
import { updateSessionAgent } from "../../features/claude-code-session-state"
|
import { updateSessionAgent } from "../../features/claude-code-session-state"
|
||||||
|
import { detectWorktreePath } from "./worktree-detector"
|
||||||
|
import { parseUserRequest } from "./parse-user-request"
|
||||||
|
|
||||||
export const HOOK_NAME = "start-work" as const
|
export const HOOK_NAME = "start-work" as const
|
||||||
|
|
||||||
const KEYWORD_PATTERN = /\b(ultrawork|ulw)\b/gi
|
|
||||||
|
|
||||||
interface StartWorkHookInput {
|
interface StartWorkHookInput {
|
||||||
sessionID: string
|
sessionID: string
|
||||||
messageID?: string
|
messageID?: string
|
||||||
@ -25,73 +26,76 @@ interface StartWorkHookOutput {
|
|||||||
parts: Array<{ type: string; text?: string }>
|
parts: Array<{ type: string; text?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractUserRequestPlanName(promptText: string): string | null {
|
|
||||||
const userRequestMatch = promptText.match(/<user-request>\s*([\s\S]*?)\s*<\/user-request>/i)
|
|
||||||
if (!userRequestMatch) return null
|
|
||||||
|
|
||||||
const rawArg = userRequestMatch[1].trim()
|
|
||||||
if (!rawArg) return null
|
|
||||||
|
|
||||||
const cleanedArg = rawArg.replace(KEYWORD_PATTERN, "").trim()
|
|
||||||
return cleanedArg || null
|
|
||||||
}
|
|
||||||
|
|
||||||
function findPlanByName(plans: string[], requestedName: string): string | null {
|
function findPlanByName(plans: string[], requestedName: string): string | null {
|
||||||
const lowerName = requestedName.toLowerCase()
|
const lowerName = requestedName.toLowerCase()
|
||||||
|
const exactMatch = plans.find((p) => getPlanName(p).toLowerCase() === lowerName)
|
||||||
const exactMatch = plans.find(p => getPlanName(p).toLowerCase() === lowerName)
|
|
||||||
if (exactMatch) return exactMatch
|
if (exactMatch) return exactMatch
|
||||||
|
const partialMatch = plans.find((p) => getPlanName(p).toLowerCase().includes(lowerName))
|
||||||
const partialMatch = plans.find(p => getPlanName(p).toLowerCase().includes(lowerName))
|
|
||||||
return partialMatch || null
|
return partialMatch || null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const MODEL_DECIDES_WORKTREE_BLOCK = `
|
||||||
|
## Worktree Setup Required
|
||||||
|
|
||||||
|
No worktree specified. Before starting work, you MUST choose or create one:
|
||||||
|
|
||||||
|
1. \`git worktree list --porcelain\` — list existing worktrees
|
||||||
|
2. Create if needed: \`git worktree add <absolute-path> <branch-or-HEAD>\`
|
||||||
|
3. Update \`.sisyphus/boulder.json\` — add \`"worktree_path": "<absolute-path>"\`
|
||||||
|
4. Work exclusively inside that worktree directory`
|
||||||
|
|
||||||
|
function resolveWorktreeContext(
|
||||||
|
explicitWorktreePath: string | null,
|
||||||
|
): { worktreePath: string | undefined; block: string } {
|
||||||
|
if (explicitWorktreePath === null) {
|
||||||
|
return { worktreePath: undefined, block: MODEL_DECIDES_WORKTREE_BLOCK }
|
||||||
|
}
|
||||||
|
|
||||||
|
const validatedPath = detectWorktreePath(explicitWorktreePath)
|
||||||
|
if (validatedPath) {
|
||||||
|
return { worktreePath: validatedPath, block: `\n**Worktree**: ${validatedPath}` }
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
worktreePath: undefined,
|
||||||
|
block: `\n**Worktree** (needs setup): \`git worktree add ${explicitWorktreePath} <branch>\`, then add \`"worktree_path"\` to boulder.json`,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function createStartWorkHook(ctx: PluginInput) {
|
export function createStartWorkHook(ctx: PluginInput) {
|
||||||
return {
|
return {
|
||||||
"chat.message": async (
|
"chat.message": async (input: StartWorkHookInput, output: StartWorkHookOutput): Promise<void> => {
|
||||||
input: StartWorkHookInput,
|
|
||||||
output: StartWorkHookOutput
|
|
||||||
): Promise<void> => {
|
|
||||||
const parts = output.parts
|
const parts = output.parts
|
||||||
const promptText = parts
|
const promptText =
|
||||||
?.filter((p) => p.type === "text" && p.text)
|
parts
|
||||||
.map((p) => p.text)
|
?.filter((p) => p.type === "text" && p.text)
|
||||||
.join("\n")
|
.map((p) => p.text)
|
||||||
.trim() || ""
|
.join("\n")
|
||||||
|
.trim() || ""
|
||||||
|
|
||||||
// Only trigger on actual command execution (contains <session-context> tag)
|
if (!promptText.includes("<session-context>")) return
|
||||||
// NOT on description text like "Start Sisyphus work session from Prometheus plan"
|
|
||||||
const isStartWorkCommand = promptText.includes("<session-context>")
|
|
||||||
|
|
||||||
if (!isStartWorkCommand) {
|
log(`[${HOOK_NAME}] Processing start-work command`, { sessionID: input.sessionID })
|
||||||
return
|
updateSessionAgent(input.sessionID, "atlas")
|
||||||
}
|
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] Processing start-work command`, {
|
|
||||||
sessionID: input.sessionID,
|
|
||||||
})
|
|
||||||
|
|
||||||
updateSessionAgent(input.sessionID, "atlas") // Always switch: fixes #1298
|
|
||||||
|
|
||||||
const existingState = readBoulderState(ctx.directory)
|
const existingState = readBoulderState(ctx.directory)
|
||||||
const sessionId = input.sessionID
|
const sessionId = input.sessionID
|
||||||
const timestamp = new Date().toISOString()
|
const timestamp = new Date().toISOString()
|
||||||
|
|
||||||
|
const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)
|
||||||
|
const { worktreePath, block: worktreeBlock } = resolveWorktreeContext(explicitWorktreePath)
|
||||||
|
|
||||||
let contextInfo = ""
|
let contextInfo = ""
|
||||||
|
|
||||||
const explicitPlanName = extractUserRequestPlanName(promptText)
|
|
||||||
|
|
||||||
if (explicitPlanName) {
|
if (explicitPlanName) {
|
||||||
log(`[${HOOK_NAME}] Explicit plan name requested: ${explicitPlanName}`, {
|
log(`[${HOOK_NAME}] Explicit plan name requested: ${explicitPlanName}`, { sessionID: input.sessionID })
|
||||||
sessionID: input.sessionID,
|
|
||||||
})
|
|
||||||
|
|
||||||
const allPlans = findPrometheusPlans(ctx.directory)
|
const allPlans = findPrometheusPlans(ctx.directory)
|
||||||
const matchedPlan = findPlanByName(allPlans, explicitPlanName)
|
const matchedPlan = findPlanByName(allPlans, explicitPlanName)
|
||||||
|
|
||||||
if (matchedPlan) {
|
if (matchedPlan) {
|
||||||
const progress = getPlanProgress(matchedPlan)
|
const progress = getPlanProgress(matchedPlan)
|
||||||
|
|
||||||
if (progress.isComplete) {
|
if (progress.isComplete) {
|
||||||
contextInfo = `
|
contextInfo = `
|
||||||
## Plan Already Complete
|
## Plan Already Complete
|
||||||
@ -99,12 +103,10 @@ export function createStartWorkHook(ctx: PluginInput) {
|
|||||||
The requested plan "${getPlanName(matchedPlan)}" has been completed.
|
The requested plan "${getPlanName(matchedPlan)}" has been completed.
|
||||||
All ${progress.total} tasks are done. Create a new plan with: /plan "your task"`
|
All ${progress.total} tasks are done. Create a new plan with: /plan "your task"`
|
||||||
} else {
|
} else {
|
||||||
if (existingState) {
|
if (existingState) clearBoulderState(ctx.directory)
|
||||||
clearBoulderState(ctx.directory)
|
const newState = createBoulderState(matchedPlan, sessionId, "atlas", worktreePath)
|
||||||
}
|
|
||||||
const newState = createBoulderState(matchedPlan, sessionId, "atlas")
|
|
||||||
writeBoulderState(ctx.directory, newState)
|
writeBoulderState(ctx.directory, newState)
|
||||||
|
|
||||||
contextInfo = `
|
contextInfo = `
|
||||||
## Auto-Selected Plan
|
## Auto-Selected Plan
|
||||||
|
|
||||||
@ -113,17 +115,20 @@ All ${progress.total} tasks are done. Create a new plan with: /plan "your task"`
|
|||||||
**Progress**: ${progress.completed}/${progress.total} tasks
|
**Progress**: ${progress.completed}/${progress.total} tasks
|
||||||
**Session ID**: ${sessionId}
|
**Session ID**: ${sessionId}
|
||||||
**Started**: ${timestamp}
|
**Started**: ${timestamp}
|
||||||
|
${worktreeBlock}
|
||||||
|
|
||||||
boulder.json has been created. Read the plan and begin execution.`
|
boulder.json has been created. Read the plan and begin execution.`
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const incompletePlans = allPlans.filter(p => !getPlanProgress(p).isComplete)
|
const incompletePlans = allPlans.filter((p) => !getPlanProgress(p).isComplete)
|
||||||
if (incompletePlans.length > 0) {
|
if (incompletePlans.length > 0) {
|
||||||
const planList = incompletePlans.map((p, i) => {
|
const planList = incompletePlans
|
||||||
const prog = getPlanProgress(p)
|
.map((p, i) => {
|
||||||
return `${i + 1}. [${getPlanName(p)}] - Progress: ${prog.completed}/${prog.total}`
|
const prog = getPlanProgress(p)
|
||||||
}).join("\n")
|
return `${i + 1}. [${getPlanName(p)}] - Progress: ${prog.completed}/${prog.total}`
|
||||||
|
})
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
contextInfo = `
|
contextInfo = `
|
||||||
## Plan Not Found
|
## Plan Not Found
|
||||||
|
|
||||||
@ -143,9 +148,25 @@ No incomplete plans available. Create a new plan with: /plan "your task"`
|
|||||||
}
|
}
|
||||||
} else if (existingState) {
|
} else if (existingState) {
|
||||||
const progress = getPlanProgress(existingState.active_plan)
|
const progress = getPlanProgress(existingState.active_plan)
|
||||||
|
|
||||||
if (!progress.isComplete) {
|
if (!progress.isComplete) {
|
||||||
appendSessionId(ctx.directory, sessionId)
|
const effectiveWorktree = worktreePath ?? existingState.worktree_path
|
||||||
|
|
||||||
|
if (worktreePath !== undefined) {
|
||||||
|
const updatedSessions = existingState.session_ids.includes(sessionId)
|
||||||
|
? existingState.session_ids
|
||||||
|
: [...existingState.session_ids, sessionId]
|
||||||
|
writeBoulderState(ctx.directory, {
|
||||||
|
...existingState,
|
||||||
|
worktree_path: worktreePath,
|
||||||
|
session_ids: updatedSessions,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
appendSessionId(ctx.directory, sessionId)
|
||||||
|
}
|
||||||
|
|
||||||
|
const worktreeDisplay = effectiveWorktree ? `\n**Worktree**: ${effectiveWorktree}` : worktreeBlock
|
||||||
|
|
||||||
contextInfo = `
|
contextInfo = `
|
||||||
## Active Work Session Found
|
## Active Work Session Found
|
||||||
|
|
||||||
@ -155,6 +176,7 @@ No incomplete plans available. Create a new plan with: /plan "your task"`
|
|||||||
**Progress**: ${progress.completed}/${progress.total} tasks completed
|
**Progress**: ${progress.completed}/${progress.total} tasks completed
|
||||||
**Sessions**: ${existingState.session_ids.length + 1} (current session appended)
|
**Sessions**: ${existingState.session_ids.length + 1} (current session appended)
|
||||||
**Started**: ${existingState.started_at}
|
**Started**: ${existingState.started_at}
|
||||||
|
${worktreeDisplay}
|
||||||
|
|
||||||
The current session (${sessionId}) has been added to session_ids.
|
The current session (${sessionId}) has been added to session_ids.
|
||||||
Read the plan file and continue from the first unchecked task.`
|
Read the plan file and continue from the first unchecked task.`
|
||||||
@ -167,13 +189,15 @@ Looking for new plans...`
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((!existingState && !explicitPlanName) || (existingState && !explicitPlanName && getPlanProgress(existingState.active_plan).isComplete)) {
|
if (
|
||||||
|
(!existingState && !explicitPlanName) ||
|
||||||
|
(existingState && !explicitPlanName && getPlanProgress(existingState.active_plan).isComplete)
|
||||||
|
) {
|
||||||
const plans = findPrometheusPlans(ctx.directory)
|
const plans = findPrometheusPlans(ctx.directory)
|
||||||
const incompletePlans = plans.filter(p => !getPlanProgress(p).isComplete)
|
const incompletePlans = plans.filter((p) => !getPlanProgress(p).isComplete)
|
||||||
|
|
||||||
if (plans.length === 0) {
|
if (plans.length === 0) {
|
||||||
contextInfo += `
|
contextInfo += `
|
||||||
|
|
||||||
## No Plans Found
|
## No Plans Found
|
||||||
|
|
||||||
No Prometheus plan files found at .sisyphus/plans/
|
No Prometheus plan files found at .sisyphus/plans/
|
||||||
@ -187,7 +211,7 @@ All ${plans.length} plan(s) are complete. Create a new plan with: /plan "your ta
|
|||||||
} else if (incompletePlans.length === 1) {
|
} else if (incompletePlans.length === 1) {
|
||||||
const planPath = incompletePlans[0]
|
const planPath = incompletePlans[0]
|
||||||
const progress = getPlanProgress(planPath)
|
const progress = getPlanProgress(planPath)
|
||||||
const newState = createBoulderState(planPath, sessionId, "atlas")
|
const newState = createBoulderState(planPath, sessionId, "atlas", worktreePath)
|
||||||
writeBoulderState(ctx.directory, newState)
|
writeBoulderState(ctx.directory, newState)
|
||||||
|
|
||||||
contextInfo += `
|
contextInfo += `
|
||||||
@ -199,15 +223,17 @@ All ${plans.length} plan(s) are complete. Create a new plan with: /plan "your ta
|
|||||||
**Progress**: ${progress.completed}/${progress.total} tasks
|
**Progress**: ${progress.completed}/${progress.total} tasks
|
||||||
**Session ID**: ${sessionId}
|
**Session ID**: ${sessionId}
|
||||||
**Started**: ${timestamp}
|
**Started**: ${timestamp}
|
||||||
|
${worktreeBlock}
|
||||||
|
|
||||||
boulder.json has been created. Read the plan and begin execution.`
|
boulder.json has been created. Read the plan and begin execution.`
|
||||||
} else {
|
} else {
|
||||||
const planList = incompletePlans.map((p, i) => {
|
const planList = incompletePlans
|
||||||
const progress = getPlanProgress(p)
|
.map((p, i) => {
|
||||||
const stat = require("node:fs").statSync(p)
|
const progress = getPlanProgress(p)
|
||||||
const modified = new Date(stat.mtimeMs).toISOString()
|
const modified = new Date(statSync(p).mtimeMs).toISOString()
|
||||||
return `${i + 1}. [${getPlanName(p)}] - Modified: ${modified} - Progress: ${progress.completed}/${progress.total}`
|
return `${i + 1}. [${getPlanName(p)}] - Modified: ${modified} - Progress: ${progress.completed}/${progress.total}`
|
||||||
}).join("\n")
|
})
|
||||||
|
.join("\n")
|
||||||
|
|
||||||
contextInfo += `
|
contextInfo += `
|
||||||
|
|
||||||
@ -220,6 +246,7 @@ Session ID: ${sessionId}
|
|||||||
${planList}
|
${planList}
|
||||||
|
|
||||||
Ask the user which plan to work on. Present the options above and wait for their response.
|
Ask the user which plan to work on. Present the options above and wait for their response.
|
||||||
|
${worktreeBlock}
|
||||||
</system-reminder>`
|
</system-reminder>`
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -229,13 +256,14 @@ Ask the user which plan to work on. Present the options above and wait for their
|
|||||||
output.parts[idx].text = output.parts[idx].text
|
output.parts[idx].text = output.parts[idx].text
|
||||||
.replace(/\$SESSION_ID/g, sessionId)
|
.replace(/\$SESSION_ID/g, sessionId)
|
||||||
.replace(/\$TIMESTAMP/g, timestamp)
|
.replace(/\$TIMESTAMP/g, timestamp)
|
||||||
|
|
||||||
output.parts[idx].text += `\n\n---\n${contextInfo}`
|
output.parts[idx].text += `\n\n---\n${contextInfo}`
|
||||||
}
|
}
|
||||||
|
|
||||||
log(`[${HOOK_NAME}] Context injected`, {
|
log(`[${HOOK_NAME}] Context injected`, {
|
||||||
sessionID: input.sessionID,
|
sessionID: input.sessionID,
|
||||||
hasExistingState: !!existingState,
|
hasExistingState: !!existingState,
|
||||||
|
worktreePath,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user