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)}
|
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")
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 () => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user