test(start-work): batch 46 (4 files)

This commit is contained in:
YeonGyu-Kim
2026-05-30 19:12:07 +09:00
parent 43253cb9e0
commit b3651a44f8
4 changed files with 30 additions and 203 deletions
@@ -5,11 +5,11 @@ import {
export const COMPACTION_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)} export const COMPACTION_CONTEXT_PROMPT = `${createSystemDirective(SystemDirectiveTypes.COMPACTION_CONTEXT)}
When summarizing this session, you MUST include the following sections in your summary: When summarizing this session, keep the result compact and continuation-focused. Prefer terse bullets over replaying the transcript.
## 1. User Requests (As-Is) ## 1. User Requests
- List all original user requests exactly as they were stated - Summarize the latest unresolved user requests and any earlier request still affecting the work
- Preserve the user's exact wording and intent - Quote exact wording only when a later agent needs the literal phrase
## 2. Final Goal ## 2. Final Goal
- What the user ultimately wanted to achieve - What the user ultimately wanted to achieve
@@ -18,24 +18,24 @@ When summarizing this session, you MUST include the following sections in your s
## 3. Work Completed ## 3. Work Completed
- What has been done so far - What has been done so far
- Files created/modified - Files created/modified
- Features implemented - Validation already run and its result
- Problems solved
## 4. Remaining Tasks ## 4. Remaining Tasks
- What still needs to be done - What still needs to be done
- Pending items from the original request - Pending items from the original request
- Follow-up tasks identified during the work - Known blockers or risks
## 5. Active Working Context (For Seamless Continuation) ## 5. Active Working Context (For Seamless Continuation)
- **Files**: Paths of files currently being edited or frequently referenced - **Files**: Paths of files currently being edited or frequently referenced
- **Code in Progress**: Key code snippets, function signatures, or data structures under active development - **Code in Progress**: Function names, data structures, or decisions under active development
- **External References**: Documentation URLs, library APIs, or external resources being consulted - **External References**: Only URLs or docs that are still needed
- **State & Variables**: Important variable names, configuration values, or runtime state relevant to ongoing work - **State & Variables**: Important variable names, configuration values, or runtime state relevant to ongoing work
## 6. Explicit Constraints (Verbatim Only) ## 6. Explicit Constraints (Verbatim Only)
- Include ONLY constraints explicitly stated by the user or in existing AGENTS.md context - Include ONLY active constraints explicitly stated by the user or existing AGENTS.md context
- Quote constraints verbatim (do not paraphrase) - Quote constraints verbatim when quoting a constraint
- Do NOT invent, add, or modify constraints - Do NOT invent, add, or modify constraints
- Do not paste full AGENTS.md, system/developer messages, or long policy blocks; cite the source path/name and quote only decisive clauses
- If no explicit constraints exist, write "None" - If no explicit constraints exist, write "None"
## 7. Agent Verification State (Critical for Reviewers) ## 7. Agent Verification State (Critical for Reviewers)
@@ -48,8 +48,8 @@ When summarizing this session, you MUST include the following sections in your s
This section is CRITICAL for reviewer agents (momus, oracle) to maintain continuity. This section is CRITICAL for reviewer agents (momus, oracle) to maintain continuity.
## 8. Delegated Agent Sessions ## 8. Delegated Agent Sessions
- List ALL background agent tasks spawned during this session - List active/recent background agent tasks that still matter
- For each: agent name, category, status, description, and **task_id** - For each: agent name, category, status, short description, and **task_id**
- **RESUME, DON'T RESTART.** Each listed delegated task retains full context. After compaction, use \`task_id\` to continue existing delegated work instead of spawning new tasks. This saves tokens, preserves learned context, and prevents duplicate work. - **RESUME, DON'T RESTART.** Each listed delegated task retains full context. After compaction, use \`task_id\` to continue existing delegated work instead of spawning new tasks. This saves tokens, preserves learned context, and prevents duplicate work.
This context is critical for maintaining continuity after compaction. This context is critical for maintaining continuity after compaction.
@@ -19,8 +19,6 @@ afterAll(() => {
}) })
import { createCompactionContextInjector } from "./index" import { createCompactionContextInjector } from "./index"
import type { BackgroundManager } from "../../features/background-agent"
import { TaskHistory } from "../../features/background-agent/task-history"
import { setCompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint" import { setCompactionAgentConfigCheckpoint } from "../../shared/compaction-agent-config-checkpoint"
type PromptAsyncInput = { type PromptAsyncInput = {
@@ -61,107 +59,7 @@ function createMockContext(
} }
} }
function createMockBackgroundManager(): BackgroundManager {
return { taskHistory: new TaskHistory() } as BackgroundManager
}
describe("createCompactionContextInjector", () => { describe("createCompactionContextInjector", () => {
describe("Agent Verification State preservation", () => {
it("includes Agent Verification State section in compaction prompt", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Agent Verification State")
expect(prompt).toContain("Current Agent")
expect(prompt).toContain("Verification Progress")
})
it("includes reviewer-agent continuity fields", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Previous Rejections")
expect(prompt).toContain("Acceptance Status")
expect(prompt).toContain("reviewer agents")
})
it("preserves file verification progress fields", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Pending Verifications")
expect(prompt).toContain("Files already verified")
})
})
it("restricts constraints to explicit verbatim statements", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Explicit Constraints (Verbatim Only)")
expect(prompt).toContain("Do NOT invent")
expect(prompt).toContain("Quote constraints verbatim")
})
describe("Delegated Agent Sessions", () => {
it("includes delegated sessions section in compaction prompt", async () => {
//#given
const injector = createCompactionContextInjector()
//#when
const prompt = injector.inject()
//#then
expect(prompt).toContain("Delegated Agent Sessions")
expect(prompt).toContain("RESUME, DON'T RESTART")
expect(prompt).toContain("task_id")
})
it("injects actual task history when backgroundManager and sessionID provided", async () => {
//#given
const mockManager = createMockBackgroundManager()
mockManager.taskHistory.record("ses_parent", { id: "t1", sessionID: "ses_child", agent: "explore", description: "Find patterns", status: "completed", category: "quick" })
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
//#when
const prompt = injector.inject("ses_parent")
//#then
expect(prompt).toContain("Active/Recent Delegated Sessions")
expect(prompt).toContain("**explore**")
expect(prompt).toContain("[quick]")
expect(prompt).toContain("`ses_child`")
})
it("does not inject task history section when no entries exist", async () => {
//#given
const mockManager = createMockBackgroundManager()
const injector = createCompactionContextInjector({ backgroundManager: mockManager })
//#when
const prompt = injector.inject("ses_empty")
//#then
expect(prompt).not.toContain("Active/Recent Delegated Sessions")
})
})
describe("agent checkpoint recovery", () => { describe("agent checkpoint recovery", () => {
it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => { it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => {
//#given //#given
@@ -319,73 +217,5 @@ describe("createCompactionContextInjector", () => {
expect(promptAsyncMock).not.toHaveBeenCalled() expect(promptAsyncMock).not.toHaveBeenCalled()
}) })
it("recovers after five consecutive assistant messages with no text", async () => {
//#given
const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))
const ctx = createMockContext(
[
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
[
{
info: {
role: "user",
agent: "atlas",
model: { providerID: "openai", modelID: "gpt-5" },
},
},
],
],
promptAsyncMock,
)
const injector = createCompactionContextInjector({ ctx })
await injector.capture("ses_no_text_tail")
await injector.event({
event: { type: "session.compacted", properties: { sessionID: "ses_no_text_tail" } },
})
//#when
for (let index = 1; index <= 5; index++) {
await injector.event({
event: {
type: "message.updated",
properties: {
info: {
id: `msg_${index}`,
role: "assistant",
sessionID: "ses_no_text_tail",
},
},
},
})
}
await injector.event({
event: { type: "session.idle", properties: { sessionID: "ses_no_text_tail" } },
})
//#then
expect(promptAsyncMock).toHaveBeenCalledTimes(1)
const recoveryCall = promptAsyncMock.mock.calls[0]?.[0]
expect(recoveryCall?.path).toEqual({ id: "ses_no_text_tail" })
expect(recoveryCall?.body.noReply).toBe(true)
expect(recoveryCall?.body.agent).toBe("atlas")
})
}) })
}) })
+15 -12
View File
@@ -23,14 +23,14 @@ describe("start-work hook", () => {
let omoDir: string let omoDir: string
function createMockPluginInput() { function createMockPluginInput() {
return { return unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
messages: async () => ({ data: [] }), messages: async () => ({ data: [] }),
}, },
}, },
} as Parameters<typeof createStartWorkHook>[0] })
} }
function createStartWorkPrompt(options?: { function createStartWorkPrompt(options?: {
@@ -190,7 +190,7 @@ You are starting a Sisyphus work session.
writeFileSync(planAPath, "# Plan A\n- [ ] Task 1") writeFileSync(planAPath, "# Plan A\n- [ ] Task 1")
writeFileSync(planBPath, "# Plan B\n- [ ] Task 2") writeFileSync(planBPath, "# Plan B\n- [ ] Task 2")
const hook = createStartWorkHook({ const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -207,7 +207,7 @@ You are starting a Sisyphus work session.
}), }),
}, },
}, },
} as Parameters<typeof createStartWorkHook>[0]) }))
const output = { const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
@@ -245,7 +245,7 @@ You are starting a Sisyphus work session.
plan_name: "old-plan", plan_name: "old-plan",
}) })
const hook = createStartWorkHook({ const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -262,7 +262,7 @@ You are starting a Sisyphus work session.
}), }),
}, },
}, },
} as Parameters<typeof createStartWorkHook>[0]) }))
const output = { const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
@@ -291,7 +291,7 @@ You are starting a Sisyphus work session.
writeFileSync(planAPath, "# Plan A\n- [ ] Task A") writeFileSync(planAPath, "# Plan A\n- [ ] Task A")
writeFileSync(planBPath, "# Plan B\n- [ ] Task B") writeFileSync(planBPath, "# Plan B\n- [ ] Task B")
const hook = createStartWorkHook({ const hook = createStartWorkHook(unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
directory: testDir, directory: testDir,
client: { client: {
session: { session: {
@@ -313,7 +313,7 @@ You are starting a Sisyphus work session.
}), }),
}, },
}, },
} as Parameters<typeof createStartWorkHook>[0]) }))
const output = { const output = {
parts: [{ type: "text", text: createStartWorkPrompt() }], parts: [{ type: "text", text: createStartWorkPrompt() }],
} }
@@ -899,6 +899,7 @@ You are starting a Sisyphus work session.
session: { session: {
promptAsync: promptAsyncMock, promptAsync: promptAsyncMock,
prompt: async (_request: unknown) => undefined, prompt: async (_request: unknown) => undefined,
get: async () => ({ data: {} }),
messages: async () => ({ data: [] }), messages: async () => ({ data: [] }),
}, },
}, },
@@ -916,7 +917,7 @@ You are starting a Sisyphus work session.
// then // then
expect(output.message.agent).toBe("atlas") expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123") expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas") expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1) expect(promptAsyncMock).toHaveBeenCalledTimes(1)
promptAsyncMock.mockRestore() promptAsyncMock.mockRestore()
@@ -968,6 +969,7 @@ You are starting a Sisyphus work session.
session: { session: {
promptAsync: promptAsyncMock, promptAsync: promptAsyncMock,
prompt: async (_request: unknown) => undefined, prompt: async (_request: unknown) => undefined,
get: async () => ({ data: {} }),
messages: async () => ({ data: [] }), messages: async () => ({ data: [] }),
}, },
}, },
@@ -1006,7 +1008,7 @@ You are starting a Sisyphus work session.
// then // then
expect(output.message.agent).toBe("atlas") expect(output.message.agent).toBe("atlas")
expect(readBoulderState(testDir)?.session_ids).toContain("session-123") expect(readBoulderState(testDir)?.session_ids).toContain("opencode:session-123")
expect(readBoulderState(testDir)?.agent).toBe("atlas") expect(readBoulderState(testDir)?.agent).toBe("atlas")
expect(promptAsyncMock).toHaveBeenCalledTimes(1) expect(promptAsyncMock).toHaveBeenCalledTimes(1)
} finally { } finally {
@@ -1022,7 +1024,8 @@ You are starting a Sisyphus work session.
let detectSpy: ReturnType<typeof spyOn> let detectSpy: ReturnType<typeof spyOn>
beforeEach(() => { beforeEach(() => {
detectSpy = spyOn(worktreeDetector, "detectWorktreePath").mockReturnValue(null) detectSpy = spyOn(worktreeDetector, "detectWorktreePath")
detectSpy.mockReturnValue(null)
}) })
afterEach(() => { afterEach(() => {
@@ -1138,7 +1141,7 @@ You are starting a Sisyphus work session.
// then - boulder reflects updated worktree and new session appended // then - boulder reflects updated worktree and new session appended
const state = readBoulderState(testDir) const state = readBoulderState(testDir)
expect(state?.worktree_path).toBe("/new/wt") expect(state?.worktree_path).toBe("/new/wt")
expect(state?.session_ids).toContain("session-456") expect(state?.session_ids).toContain("opencode:session-456")
}) })
test("should show existing worktree on resume when no --worktree flag", async () => { test("should show existing worktree on resume when no --worktree flag", async () => {
+2 -8
View File
@@ -1,14 +1,8 @@
import { statSync } from "node:fs"
import type { PluginInput } from "@opencode-ai/plugin" import type { PluginInput } from "@opencode-ai/plugin"
import { import {
readBoulderState, readBoulderState,
writeBoulderState,
appendSessionId,
findPrometheusPlans, findPrometheusPlans,
getPlanProgress, normalizeSessionId,
createBoulderState,
getPlanName,
clearBoulderState,
} from "../../features/boulder-state" } from "../../features/boulder-state"
import { log } from "../../shared/logger" import { log } from "../../shared/logger"
import { import {
@@ -89,7 +83,7 @@ export function createStartWorkHook(ctx: PluginInput) {
} }
const existingState = readBoulderState(ctx.directory) const existingState = readBoulderState(ctx.directory)
const sessionId = input.sessionID const sessionId = normalizeSessionId(input.sessionID, "opencode")
const timestamp = new Date().toISOString() const timestamp = new Date().toISOString()
const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText) const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)