diff --git a/assets/oh-my-opencode.schema.json b/assets/oh-my-opencode.schema.json index f11fbffcf..d8e3095a3 100644 --- a/assets/oh-my-opencode.schema.json +++ b/assets/oh-my-opencode.schema.json @@ -53,7 +53,8 @@ "frontend-ui-ux", "git-master", "review-work", - "ai-slop-remover", + "remove-ai-slops", + "init-deep", "team-mode" ] } @@ -69,7 +70,6 @@ "items": { "type": "string", "enum": [ - "init-deep", "ralph-loop", "ulw-loop", "cancel-ralph", diff --git a/docs/AGENTS.md b/docs/AGENTS.md index a16a715f7..395f7dea2 100644 --- a/docs/AGENTS.md +++ b/docs/AGENTS.md @@ -21,6 +21,7 @@ | Known issues & workarounds | [docs/reference/known-issues.md](file:///Users/yeongyu/local-workspaces/omo/docs/reference/known-issues.md) | | `prompt_async_gate` deep-dive | [docs/reference/prompt-async-gate-rfc.md](file:///Users/yeongyu/local-workspaces/omo/docs/reference/prompt-async-gate-rfc.md) | | Release process | [docs/reference/release-process.md](file:///Users/yeongyu/local-workspaces/omo/docs/reference/release-process.md) | +| Claiming the lazycodex npm name | [docs/reference/lazycodex-npm-reservation.md](file:///Users/yeongyu/local-workspaces/omo/docs/reference/lazycodex-npm-reservation.md) | | Rules-injector cross-module comparison | [docs/reference/rules-injection-cross-module-comparison.md](file:///Users/yeongyu/local-workspaces/omo/docs/reference/rules-injection-cross-module-comparison.md) | | Sample configs | [docs/examples/](file:///Users/yeongyu/local-workspaces/omo/docs/examples/) (default, coding-focused, planning-focused) | | Privacy & ToS | [docs/legal/](file:///Users/yeongyu/local-workspaces/omo/docs/legal/) | diff --git a/src/hooks/compaction-context-injector/prompt.test.ts b/src/hooks/compaction-context-injector/prompt.test.ts new file mode 100644 index 000000000..449df9775 --- /dev/null +++ b/src/hooks/compaction-context-injector/prompt.test.ts @@ -0,0 +1,163 @@ +import { afterAll, describe, expect, it, mock } from "bun:test" + +mock.module("../../shared/system-directive", () => ({ + createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`, + SystemDirectiveTypes: { + TODO_CONTINUATION: "TODO CONTINUATION", + RALPH_LOOP: "RALPH LOOP", + BOULDER_CONTINUATION: "BOULDER CONTINUATION", + DELEGATION_REQUIRED: "DELEGATION REQUIRED", + SINGLE_TASK_ONLY: "SINGLE TASK ONLY", + COMPACTION_CONTEXT: "COMPACTION CONTEXT", + CONTEXT_WINDOW_MONITOR: "CONTEXT WINDOW MONITOR", + PROMETHEUS_READ_ONLY: "PROMETHEUS READ-ONLY", + }, +})) + +afterAll(() => { + mock.restore() +}) + +import type { BackgroundManager } from "../../features/background-agent" +import { TaskHistory } from "../../features/background-agent/task-history" +import { createCompactionContextInjector } from "./index" + +function createMockBackgroundManager(): BackgroundManager { + return { taskHistory: new TaskHistory() } as BackgroundManager +} + +describe("createCompactionContextInjector prompt", () => { + 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") + }) + + it("does not ask the compaction agent to replay complete history verbatim", async () => { + //#given + const injector = createCompactionContextInjector() + + //#when + const prompt = injector.inject() + + //#then + expect(prompt).not.toContain("List all original user requests exactly as they were stated") + expect(prompt).toContain("latest unresolved user requests") + expect(prompt).toContain("Do not paste full AGENTS.md") + }) + + 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") + }) + + it("keeps injected delegated history bounded for long task lists", async () => { + //#given + const mockManager = createMockBackgroundManager() + for (let i = 0; i < 100; i++) { + mockManager.taskHistory.record("ses_parent", { + id: `t${i}`, + sessionID: `ses_child_${i}`, + agent: "explore", + description: "Inspect verbose delegated task context. ".repeat(200), + status: "completed", + category: "quick", + }) + } + const injector = createCompactionContextInjector({ backgroundManager: mockManager }) + + //#when + const prompt = injector.inject("ses_parent") + + //#then + expect(prompt.length).toBeLessThanOrEqual(9_000) + expect(prompt).toContain("older delegated sessions omitted") + expect(prompt).toContain("`t99`") + expect(prompt).not.toContain("`t0`") + }) + }) +}) diff --git a/src/hooks/compaction-context-injector/tail.test.ts b/src/hooks/compaction-context-injector/tail.test.ts new file mode 100644 index 000000000..65aaf1201 --- /dev/null +++ b/src/hooks/compaction-context-injector/tail.test.ts @@ -0,0 +1,92 @@ +import { afterAll, describe, expect, it, mock } from "bun:test" + +mock.module("../../shared/system-directive", () => ({ + createSystemDirective: (type: string) => `[DIRECTIVE:${type}]`, + SystemDirectiveTypes: { + TODO_CONTINUATION: "TODO CONTINUATION", + RALPH_LOOP: "RALPH LOOP", + BOULDER_CONTINUATION: "BOULDER CONTINUATION", + DELEGATION_REQUIRED: "DELEGATION REQUIRED", + SINGLE_TASK_ONLY: "SINGLE TASK ONLY", + COMPACTION_CONTEXT: "COMPACTION CONTEXT", + CONTEXT_WINDOW_MONITOR: "CONTEXT WINDOW MONITOR", + PROMETHEUS_READ_ONLY: "PROMETHEUS READ-ONLY", + }, +})) + +afterAll(() => { + mock.restore() +}) + +import { createCompactionContextInjector } from "./index" + +type PromptAsyncInput = { + path: { id: string } + body: { + noReply?: boolean + agent?: string + } +} + +function createMockContext(promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({}))) { + let callIndex = 0 + const responses = [ + [{ 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" } } }], + ] + + return { + client: { + session: { + messages: mock(async () => { + const response = responses[Math.min(callIndex, responses.length - 1)] ?? [] + callIndex += 1 + return { data: response } + }), + promptAsync: promptAsyncMock, + }, + }, + directory: "/tmp/test", + } +} + +describe("createCompactionContextInjector tail recovery", () => { + it("recovers after five consecutive assistant messages with no text", async () => { + //#given + const promptAsyncMock = mock(async (_input: PromptAsyncInput) => ({})) + const ctx = createMockContext(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") + }) +})