test(start-work): batch 46 (4 files)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -23,14 +23,14 @@ describe("start-work hook", () => {
|
||||
let omoDir: string
|
||||
|
||||
function createMockPluginInput() {
|
||||
return {
|
||||
return unsafeTestValue<Parameters<typeof createStartWorkHook>[0]>({
|
||||
directory: testDir,
|
||||
client: {
|
||||
session: {
|
||||
messages: async () => ({ data: [] }),
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof createStartWorkHook>[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<Parameters<typeof createStartWorkHook>[0]>({
|
||||
directory: testDir,
|
||||
client: {
|
||||
session: {
|
||||
@@ -207,7 +207,7 @@ You are starting a Sisyphus work session.
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof createStartWorkHook>[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<Parameters<typeof createStartWorkHook>[0]>({
|
||||
directory: testDir,
|
||||
client: {
|
||||
session: {
|
||||
@@ -262,7 +262,7 @@ You are starting a Sisyphus work session.
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof createStartWorkHook>[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<Parameters<typeof createStartWorkHook>[0]>({
|
||||
directory: testDir,
|
||||
client: {
|
||||
session: {
|
||||
@@ -313,7 +313,7 @@ You are starting a Sisyphus work session.
|
||||
}),
|
||||
},
|
||||
},
|
||||
} as Parameters<typeof createStartWorkHook>[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<typeof spyOn>
|
||||
|
||||
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 () => {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user