diff --git a/src/hooks/compaction-context-injector/compaction-context-prompt.ts b/src/hooks/compaction-context-injector/compaction-context-prompt.ts index 9439ca8d4..dace21df0 100644 --- a/src/hooks/compaction-context-injector/compaction-context-prompt.ts +++ b/src/hooks/compaction-context-injector/compaction-context-prompt.ts @@ -5,11 +5,11 @@ import { 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) -- List all original user requests exactly as they were stated -- Preserve the user's exact wording and intent +## 1. User Requests +- Summarize the latest unresolved user requests and any earlier request still affecting the work +- Quote exact wording only when a later agent needs the literal phrase ## 2. Final Goal - 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 - What has been done so far - Files created/modified -- Features implemented -- Problems solved +- Validation already run and its result ## 4. Remaining Tasks - What still needs to be done - Pending items from the original request -- Follow-up tasks identified during the work +- Known blockers or risks ## 5. Active Working Context (For Seamless Continuation) - **Files**: Paths of files currently being edited or frequently referenced -- **Code in Progress**: Key code snippets, function signatures, or data structures under active development -- **External References**: Documentation URLs, library APIs, or external resources being consulted +- **Code in Progress**: Function names, data structures, or decisions under active development +- **External References**: Only URLs or docs that are still needed - **State & Variables**: Important variable names, configuration values, or runtime state relevant to ongoing work ## 6. Explicit Constraints (Verbatim Only) -- Include ONLY constraints explicitly stated by the user or in existing AGENTS.md context -- Quote constraints verbatim (do not paraphrase) +- Include ONLY active constraints explicitly stated by the user or existing AGENTS.md context +- Quote constraints verbatim when quoting a constraint - 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" ## 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. ## 8. Delegated Agent Sessions -- List ALL background agent tasks spawned during this session -- For each: agent name, category, status, description, and **task_id** +- List active/recent background agent tasks that still matter +- 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. This context is critical for maintaining continuity after compaction. diff --git a/src/hooks/compaction-context-injector/index.test.ts b/src/hooks/compaction-context-injector/index.test.ts index 635d17eae..f34182a86 100644 --- a/src/hooks/compaction-context-injector/index.test.ts +++ b/src/hooks/compaction-context-injector/index.test.ts @@ -19,8 +19,6 @@ afterAll(() => { }) 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" type PromptAsyncInput = { @@ -61,107 +59,7 @@ function createMockContext( } } -function createMockBackgroundManager(): BackgroundManager { - return { taskHistory: new TaskHistory() } as BackgroundManager -} - 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", () => { it("re-injects checkpointed agent config after compaction when latest agent is lost", async () => { //#given @@ -319,73 +217,5 @@ describe("createCompactionContextInjector", () => { 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") - }) }) }) diff --git a/src/hooks/start-work/index.test.ts b/src/hooks/start-work/index.test.ts index 7b8f8c2b8..6ca1211d1 100644 --- a/src/hooks/start-work/index.test.ts +++ b/src/hooks/start-work/index.test.ts @@ -23,14 +23,14 @@ describe("start-work hook", () => { let omoDir: string function createMockPluginInput() { - return { + return unsafeTestValue[0]>({ directory: testDir, client: { session: { messages: async () => ({ data: [] }), }, }, - } as Parameters[0] + }) } function createStartWorkPrompt(options?: { @@ -190,7 +190,7 @@ You are starting a Sisyphus work session. writeFileSync(planAPath, "# Plan A\n- [ ] Task 1") writeFileSync(planBPath, "# Plan B\n- [ ] Task 2") - const hook = createStartWorkHook({ + const hook = createStartWorkHook(unsafeTestValue[0]>({ directory: testDir, client: { session: { @@ -207,7 +207,7 @@ You are starting a Sisyphus work session. }), }, }, - } as Parameters[0]) + })) const output = { parts: [{ type: "text", text: createStartWorkPrompt() }], } @@ -245,7 +245,7 @@ You are starting a Sisyphus work session. plan_name: "old-plan", }) - const hook = createStartWorkHook({ + const hook = createStartWorkHook(unsafeTestValue[0]>({ directory: testDir, client: { session: { @@ -262,7 +262,7 @@ You are starting a Sisyphus work session. }), }, }, - } as Parameters[0]) + })) const output = { parts: [{ type: "text", text: createStartWorkPrompt() }], } @@ -291,7 +291,7 @@ You are starting a Sisyphus work session. writeFileSync(planAPath, "# Plan A\n- [ ] Task A") writeFileSync(planBPath, "# Plan B\n- [ ] Task B") - const hook = createStartWorkHook({ + const hook = createStartWorkHook(unsafeTestValue[0]>({ directory: testDir, client: { session: { @@ -313,7 +313,7 @@ You are starting a Sisyphus work session. }), }, }, - } as Parameters[0]) + })) const output = { parts: [{ type: "text", text: createStartWorkPrompt() }], } @@ -899,6 +899,7 @@ You are starting a Sisyphus work session. session: { promptAsync: promptAsyncMock, prompt: async (_request: unknown) => undefined, + get: async () => ({ data: {} }), messages: async () => ({ data: [] }), }, }, @@ -916,7 +917,7 @@ You are starting a Sisyphus work session. // then 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(promptAsyncMock).toHaveBeenCalledTimes(1) promptAsyncMock.mockRestore() @@ -968,6 +969,7 @@ You are starting a Sisyphus work session. session: { promptAsync: promptAsyncMock, prompt: async (_request: unknown) => undefined, + get: async () => ({ data: {} }), messages: async () => ({ data: [] }), }, }, @@ -1006,7 +1008,7 @@ You are starting a Sisyphus work session. // then 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(promptAsyncMock).toHaveBeenCalledTimes(1) } finally { @@ -1022,7 +1024,8 @@ You are starting a Sisyphus work session. let detectSpy: ReturnType beforeEach(() => { - detectSpy = spyOn(worktreeDetector, "detectWorktreePath").mockReturnValue(null) + detectSpy = spyOn(worktreeDetector, "detectWorktreePath") + detectSpy.mockReturnValue(null) }) afterEach(() => { @@ -1138,7 +1141,7 @@ You are starting a Sisyphus work session. // 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") + expect(state?.session_ids).toContain("opencode:session-456") }) test("should show existing worktree on resume when no --worktree flag", async () => { diff --git a/src/hooks/start-work/start-work-hook.ts b/src/hooks/start-work/start-work-hook.ts index af5c1f41f..6248d5f46 100644 --- a/src/hooks/start-work/start-work-hook.ts +++ b/src/hooks/start-work/start-work-hook.ts @@ -1,14 +1,8 @@ -import { statSync } from "node:fs" import type { PluginInput } from "@opencode-ai/plugin" import { readBoulderState, - writeBoulderState, - appendSessionId, findPrometheusPlans, - getPlanProgress, - createBoulderState, - getPlanName, - clearBoulderState, + normalizeSessionId, } from "../../features/boulder-state" import { log } from "../../shared/logger" import { @@ -89,7 +83,7 @@ export function createStartWorkHook(ctx: PluginInput) { } const existingState = readBoulderState(ctx.directory) - const sessionId = input.sessionID + const sessionId = normalizeSessionId(input.sessionID, "opencode") const timestamp = new Date().toISOString() const { planName: explicitPlanName, explicitWorktreePath } = parseUserRequest(promptText)