diff --git a/CHANGELOG.md b/CHANGELOG.md
index 4cae0a0..01bf07a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,8 @@
## Unreleased
+- Stop silently dropping `role: "developer"` messages (for example OMP advisor steering notes, reminders, and nudges). `/alpha/generate` only accepts `user`, `assistant`, and `tool` roles, so developer messages are now forwarded as `user` messages with identical content in the same chronological position instead of disappearing from the request.
+
## 0.5.1 - 2026-08-11
- Add model-specific image input capabilities from the `command-code@1.15.1` catalog and forward user and tool-result images using the current Command Code wire format.
diff --git a/src/converters.ts b/src/converters.ts
index 4012fbb..6480263 100644
--- a/src/converters.ts
+++ b/src/converters.ts
@@ -190,7 +190,12 @@ export function messagesToCC(
const pairedToolCallIds = completeToolCallIds(messages)
for (const message of messages ?? []) {
- if (message.role === "user") {
+ if (message.role === "user" || message.role === "developer") {
+ // Hosts such as OMP steer the agent by injecting developer-role messages
+ // (advisor notes, reminders, nudges) mid-conversation. /alpha/generate
+ // only accepts user, assistant, and tool roles, so degrade the role to
+ // user instead of dropping the message. Content and chronological
+ // position are preserved; system-prompt hoisting would change semantics.
out.push({
role: "user",
content: userContentToCommandCode(message.content, allowImages),
diff --git a/tests/test-pure-functions.ts b/tests/test-pure-functions.ts
index 136025f..c5d235a 100644
--- a/tests/test-pure-functions.ts
+++ b/tests/test-pure-functions.ts
@@ -626,6 +626,89 @@ describe("messagesToCC()", () => {
it("handles empty conversations", () => {
assert.deepEqual(messagesToCC([]), [])
})
+
+ it("keeps developer messages instead of dropping them", () => {
+ const result = messagesToCC([
+ { role: "user", content: "start" },
+ { role: "developer", content: "mid-conversation steering note" },
+ ])
+
+ assert.deepEqual(result, [
+ { role: "user", content: "start" },
+ { role: "user", content: "mid-conversation steering note" },
+ ])
+ })
+
+ it("converts developer text parts with the same shape as user content", () => {
+ const result = messagesToCC([
+ {
+ role: "developer",
+ content: [{ type: "text", text: "reminder one" }],
+ },
+ ])
+
+ assert.deepEqual(result, [
+ {
+ role: "user",
+ content: [{ type: "text", text: "reminder one" }],
+ },
+ ])
+ })
+
+ it("preserves advisory XML verbatim in the serialized request messages", () => {
+ const advisory =
+ '\nStop and correct the benchmark.\n'
+ const serialized = JSON.stringify(messagesToCC([{ role: "developer", content: advisory }]))
+
+ assert.deepEqual(JSON.parse(serialized), [{ role: "user", content: advisory }])
+ })
+
+ it("keeps developer advisories in chronological position without hoisting", () => {
+ const advisory =
+ '\nStop and correct the benchmark.\n'
+ const result = messagesToCC([
+ { role: "user", content: "run the benchmark" },
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "running it" },
+ { type: "toolCall", id: "c1", name: "bash", arguments: { command: "bench" } },
+ ],
+ },
+ {
+ role: "toolResult",
+ toolCallId: "c1",
+ toolName: "bash",
+ content: [{ type: "text", text: "benchmark output" }],
+ },
+ { role: "developer", content: advisory },
+ { role: "user", content: "continue" },
+ ])
+
+ assert.deepEqual(result, [
+ { role: "user", content: "run the benchmark" },
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "running it" },
+ { type: "tool-call", toolCallId: "c1", toolName: "bash", input: { command: "bench" } },
+ ],
+ },
+ {
+ role: "tool",
+ content: [
+ {
+ type: "tool-result",
+ toolCallId: "c1",
+ toolName: "bash",
+ output: { type: "text", value: "benchmark output" },
+ },
+ ],
+ },
+ { role: "user", content: advisory },
+ { role: "user", content: "continue" },
+ ])
+ })
})
describe("parseStreamEventLine()", () => {
diff --git a/tests/test-stream.ts b/tests/test-stream.ts
index e30ac49..82ed325 100644
--- a/tests/test-stream.ts
+++ b/tests/test-stream.ts
@@ -495,6 +495,56 @@ describe("streamCommandCode — request serialization", () => {
assert.equal(headers["x-session-id"], undefined)
})
+ it("sends developer advisories as user messages in position, without system hoisting", async () => {
+ server.mockResponse({
+ type: "success",
+ events: [JSON.stringify({ type: "finish", finishReason: "stop" })],
+ })
+ const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() })
+ const advisory =
+ '\nStop and correct the benchmark.\n'
+ const context = makeContext({
+ messages: [
+ { role: "user", content: "run the benchmark" },
+ {
+ role: "assistant",
+ content: [
+ { type: "text", text: "running it" },
+ { type: "toolCall", id: "c1", name: "bash", arguments: { command: "bench" } },
+ ],
+ },
+ {
+ role: "toolResult",
+ toolCallId: "c1",
+ toolName: "bash",
+ content: [{ type: "text", text: "benchmark output" }],
+ },
+ { role: "developer", content: advisory },
+ { role: "user", content: "continue" },
+ ],
+ })
+
+ await collectEvents(streamCommandCode(makeModel(), context, { apiKey: "mock-key" }))
+
+ const body = server.lastRequestBody()
+ assert.deepEqual(
+ objectAt(body, ["params", "messages", "3"]),
+ { role: "user", content: advisory },
+ "developer advisory should arrive as an in-position user message with identical content",
+ )
+ assert.deepEqual(objectAt(body, ["params", "messages", "4"]), {
+ role: "user",
+ content: "continue",
+ })
+ assert.equal(objectAt(body, ["params", "messages", "5"]), undefined)
+ assert.equal(
+ objectAt(body, ["params", "system"]),
+ "You are a test assistant.",
+ "advisory must not be hoisted into the system prompt",
+ )
+ assert.doesNotMatch(String(objectAt(body, ["params", "system"])), /advisory/)
+ })
+
it("accepts the legacy OMP nested reasoning map", async () => {
server.mockResponse({
type: "success",