Skip to content

Add oh-my-pi (omp) harness adapter - #55

Open
gurnben wants to merge 1 commit into
mainfrom
omp-adapter
Open

gurnben wants to merge 1 commit into
mainfrom
omp-adapter

Conversation

@gurnben

@gurnben gurnben commented Nov 6, 2026

Copy link
Copy Markdown
Member

Summary

  • Adds an omp (oh-my-pi) harness adapter so skills that fan out review subagents (vuln-scan, triage) run at full capability on omp's task/wait subagent facility, instead of degraded as on pi
  • Headless runs use omp --mode json -p with extension/skill/rule discovery, LSP, PTY and prewalk model-switching disabled — deterministic single-model sessions with no config pickup from the surrounding filesystem (S9)
  • Claude-style Task grants map onto task+wait; full-permission mode auto-approves the coding + subagent toolset (read,bash,edit,write,glob,grep,task,wait,todo) — omp's browser/github/web/ask tools stay disabled (no interactive counterpart, no network egress for headless audits)
  • Event parser absorbs the pi-family stream differences: usage rides turn_end, tool names only on tool_execution_* events, advisor_cost_changed is bookkeeping, no agent_settled (last turn_end is the result)

Verification

  • go vet clean, full suite passes (99 tests incl. new omp_test.go command/mapping/event tests built from real captured omp 18.12.6 streams)
  • Headless subagent fan-out round trip verified (task + wait executed, correct output)
  • Full vuln-scan through the runner on a real target: journal, budget bands, artifact discovery and validation all correct

💘 Generated with Crush

omp is a pi-family harness that ships a task/wait subagent facility, so
skills that fan out review subagents (vuln-scan, triage) run at full
capability on it instead of degraded as on pi. Headless runs use
`omp --mode json -p` with extension/skill/rule discovery, LSP, PTY and
prewalk model-switching disabled for deterministic single-model sessions;
Claude-style Task grants map onto omp's task+wait tools; full-permission
mode auto-approves the coding + subagent toolset (omp's browser, github,
web and ask tools stay disabled — headless audit runs have no interactive
counterpart and no network egress). The JSON event parser handles the
pi-family stream differences: usage rides turn_end, tool names appear only
on tool_execution_* events, advisor_cost_changed is bookkeeping, and there
is no agent_settled — the last turn_end is the run result.

Verified against omp 18.12.6, including a headless subagent fan-out round
trip and a full vuln-scan through the runner (journal, budget bands,
artifact discovery).

💘 Generated with Crush

Assisted-by: Crush:rits/zai-org/glm-5-3
@gurnben
gurnben requested a review from a team as a code owner November 6, 2026 00:02
@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (7) 📘 Rule violations (0) 📜 Skill insights (0)

Grey Divider


Action required

1. Scoped shell grants allow any command 🐞 Bug ⛨ Security
Description
mapOmpTools removes the command pattern from a grant such as Bash(git clone:*) and passes
unrestricted bash to omp. A normal run of a skill with that grant can therefore invoke shell
commands outside the declared pattern without full-permission consent.
Code

internal/harness/omp.go[R57-60]

+		name := grant
+		if i := strings.IndexByte(name, '('); i > 0 {
+			name = name[:i]
+		}
Evidence
The catalog accepts scoped grants, the runner forwards them unchanged, and the new adapter strips
the scope before constructing its ordinary --tools argument. The new test explicitly expects the
widened mapping.

internal/catalog/parser.go[15-23]
internal/runner/runner.go[222-225]
internal/harness/omp.go[56-66]
internal/harness/omp.go[97-104]
internal/harness/omp_test.go[47-49]
SECURITY.md[14-16]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Scoped Bash grants become unrestricted shell access in ordinary omp runs.

## Fix Focus Areas
- internal/harness/omp.go[56-66]
- internal/harness/omp_test.go[37-50]

## Recommended Fix
Preserve and enforce the command scope if omp supports it; otherwise omit or reject the scoped Bash grant rather than mapping it to unrestricted `bash`. Test that `Bash(git clone:*)` cannot produce a plain `bash` grant.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


2. Unsupported grants enable write tools 🐞 Bug ⛨ Security
Description
mapOmpTools returns ompFullTools when a nonempty grant list contains no names it recognizes. A
skill declaring only an unsupported tool such as AskUserQuestion consequently receives bash,
edit and write in an ordinary run, rather than receiving no mapped tools.
Code

internal/harness/omp.go[R83-85]

+	if len(out) == 0 {
+		return ompFullTools
+	}
Evidence
The mapper has no AskUserQuestion case, but its empty-output fallback supplies the full set to the
non-full-permission command path. The skill checkout is not present here, so the evidence
establishes the reachable conditional path rather than a currently shipped skill that triggers it.

internal/harness/omp.go[35-45]
internal/harness/omp.go[61-86]
internal/harness/omp.go[97-104]
internal/harness/omp_test.go[43-45]
internal/runner/runner.go[222-225]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A nonempty, unsupported permission list falls back to the full omp toolset.

## Fix Focus Areas
- internal/harness/omp.go[46-86]
- internal/harness/omp.go[97-104]
- internal/harness/omp_test.go[22-26]

## Recommended Fix
Distinguish an absent grant list from a nonempty list that maps to nothing. Fail closed for the latter instead of passing `ompFullTools`, and add an unsupported-only grant test.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


3. Multi-turn audits undercount token spend 🐞 Bug ≡ Correctness
Description
ParseEvent attaches each assistant message’s usage to turn_end, while applyEvent retains only
the largest usage value seen. When an omp run has multiple assistant turns, their token counts are
not accumulated, so the recorded spend and output-token budget decision can be too low.
Code

internal/harness/omp.go[R233-237]

+		if m.Message.Usage != nil {
+			ev.TokensIn, ev.TokensOut = m.Message.Usage.Input, m.Message.Usage.Output
+		} else if m.Usage != nil {
+			ev.TokensIn, ev.TokensOut = m.Usage.Input, m.Usage.Output
+		}
Evidence
Omp’s session-statistics implementation sums usage across assistant messages, whereas the new parser
exposes an individual message’s usage and the runner maxes values under its cumulative-usage
assumption. The runner uses that value for both budget bands and spend declaration.

internal/harness/omp.go[215-238]
internal/harness/omp_test.go[104-107]
internal/runner/runner.go[228-251]
internal/runner/runner.go[408-416]
internal/runner/runner.go[452-466]
🌐 Omp computes session totals by adding each assistant message’s usage rather than taking the largest message.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Per-message omp usage is folded by a runner that expects cumulative counts.

## Fix Focus Areas
- internal/harness/omp.go[193-238]
- internal/harness/omp_test.go[95-107]
- internal/runner/runner.go[452-466]

## Recommended Fix
Normalize omp usage into cumulative run totals before forwarding it to the runner, counting each assistant message only once despite its `message_end` and `turn_end` events. Add a multi-turn regression that checks the budget and final spend totals.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


View high (1)
4. Subagent work is absent from spend totals 🐞 Bug ≡ Correctness
Description
ParseEvent ignores the result payload of tool_execution_end, including usage returned by a
blocking task call. When the advertised subagent fan-out runs, that usage never reaches the
runner’s budget or spend accounting even if the parent turn’s usage is counted correctly.
Code

internal/harness/omp.go[R190-192]

+	case "tool_execution_end":
+		return Event{Kind: EventToolResult, Tool: m.ToolName,
+			IsError: len(m.Error) > 0 && string(m.Error) != "null"}, true
Evidence
The new parser reads only the tool name and error from the end event; its captured test event
already shows a result object. Omp documents per-subagent usage in task result details and
includes task-result usage when calculating session totals, while this runner accounts only for
usage delivered in normalized events.

internal/harness/omp.go[125-152]
internal/harness/omp.go[188-192]
internal/harness/omp_test.go[81-89]
internal/runner/runner.go[228-251]
internal/runner/runner.go[408-416]
🌐 Settled task responses include details.results, whose per-agent results contain usage.
🌐 Omp adds task tool-result usage to its session totals.

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Omp task results carry subagent usage that the adapter discards.

## Fix Focus Areas
- internal/harness/omp.go[125-152]
- internal/harness/omp.go[188-192]
- internal/harness/omp_test.go[81-94]

## Recommended Fix
Read usage from settled task results and incorporate it once into the adapter’s cumulative run totals. Cover a parent turn plus a blocking task result, checking budget classification and declared spend without double-counting the parent.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗



Remediation recommended

5. Tool output disappears from the run view 🐞 Bug ◔ Observability
Description
ParseEvent emits EventToolResult without copying text from tool_execution_end.result. When an
omp tool returns content, the journal receives an empty result and the live monitor omits it
entirely, leaving only the tool invocation visible.
Code

internal/harness/omp.go[R190-192]

+	case "tool_execution_end":
+		return Event{Kind: EventToolResult, Tool: m.ToolName,
+			IsError: len(m.Error) > 0 && string(m.Error) != "null"}, true
Evidence
The new test’s captured event contains a result object, but the parser never reads it. The runner
journals the resulting empty event, and the monitor expressly returns without displaying empty
tool-result text.

internal/harness/omp.go[125-152]
internal/harness/omp.go[188-192]
internal/harness/omp_test.go[81-89]
internal/runner/runner.go[228-236]
internal/tui/monitor.go[193-201]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Omp tool-result content is discarded before journaling and display.

## Fix Focus Areas
- internal/harness/omp.go[125-152]
- internal/harness/omp.go[188-192]
- internal/harness/omp_test.go[81-94]

## Recommended Fix
Extract the tool result’s displayable text into `EventToolResult.Text`, applying the adapter’s bounded-output and sensitive-data handling conventions. Test a result with nonempty content and an error result.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


6. Stream errors can leave a successful run 🐞 Bug ☼ Reliability
Description
ParseEvent reports an omp message_update error as EventError, but the runner does not turn
that event into a run failure. If omp subsequently exits with status zero, the journal and result
retain a successful exit code despite the reported provider error.
Code

internal/harness/omp.go[R170-171]

+		case "error":
+			return Event{Kind: EventError, Text: m.AssistantMessageEvent.Error, IsError: true}, true
Evidence
The new fixture establishes the error-event shape. The runner forwards that event, but applyEvent
handles only result and usage events; its final status logic otherwise depends on the process exit
or HarnessError.

internal/harness/omp.go[167-171]
internal/harness/omp_test.go[109-112]
internal/runner/runner.go[228-236]
internal/runner/runner.go[346-374]
internal/runner/runner.go[452-478]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
An omp stream-reported error is displayed but does not affect the persisted run outcome.

## Fix Focus Areas
- internal/harness/omp.go[167-171]
- internal/runner/runner.go[452-478]
- internal/harness/omp_test.go[109-112]

## Recommended Fix
Ensure a terminal omp stream error sets a failure state consumed by the runner, including when the process exits zero. Add a regression with an error event and zero process exit, while preserving legitimate recovery if omp explicitly reports it.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


7. Completed answers repeat in the live view 🐞 Bug ≡ Correctness
Description
ParseEvent emits assistant message_end content as EventText and emits the same turn’s content
again as EventResult, after already emitting any text deltas. When an omp stream contains these
events for one answer, the monitor appends the completed answer again instead of replacing the
streamed text.
Code

internal/harness/omp.go[R210-214]

+		ev := Event{Kind: EventText, Text: sb.String()}
+		if m.Message.Usage != nil {
+			ev.TokensIn, ev.TokensOut = m.Message.Usage.Input, m.Message.Usage.Output
+		}
+		return ev, true
Evidence
The parser emits unkeyed text at each stage, and the runner forwards every event. The monitor
appends both text and result events, so a complete sequence repeats content in the new adapter’s
live view.

internal/harness/omp.go[163-170]
internal/harness/omp.go[193-238]
internal/runner/runner.go[228-236]
internal/tui/monitor.go[181-187]
internal/tui/monitor.go[209-218]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Multiple omp events for one assistant answer append duplicate text to the live view.

## Fix Focus Areas
- internal/harness/omp.go[163-170]
- internal/harness/omp.go[193-238]
- internal/harness/omp_test.go[76-80]
- internal/harness/omp_test.go[95-107]

## Recommended Fix
Choose one display representation for each assistant message, or give its streaming and completion events a shared replacement key while retaining the final result for storage. Test a complete delta, message-end and turn-end sequence through the event sink.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Dismiss ↗ | View ↗


Grey Divider

Context sources
✅ Web pages:
  +12 more
Review mode: Auto: ⚖️ Balanced: New permission-granting adapter: tool-grant mapping falls back to full toolset, Bash scopes widen.

Grey Divider

Tip of the day
💡 Did you know, you can switch off images and animations for a plain-text comment

More tips ↗ | Customize Qodo ↗ | Qodo docs ↗

Grey Divider

Qodo Logo

@smith-xyz smith-xyz left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

first pass — 5 notes

Comment thread internal/harness/omp.go
if sb.Len() == 0 {
return Event{}, false
}
ev := Event{Kind: EventText, Text: sb.String()}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omp repeats the assistant's answer three times: text_delta streams it, message_end re-emits it complete, turn_end emits it again as the result — and the monitor appends all three. Drop the assistant branch here (recognize-and-ignore; pi ≥1.4 does exactly that, see the turn_end note in pi.go) — turn_end already carries content + usage.

Comment thread internal/harness/omp.go
sb.WriteString(block.Text)
}
}
ev.Text = sb.String()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

omp's usage is per-message, not cumulative (its session-stats sums assistant messages), but the runner's applyEvent maxes token counts — multi-turn runs undercount spend and can miss the refuse budget band. Keep running totals in the adapter and emit cumulative values.

Comment thread internal/harness/omp.go
}
case "tool_execution_start":
return Event{Kind: EventToolCall, Tool: m.ToolName}, true
case "tool_execution_end":

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The task tool's result payload carries per-subagent usage (omp folds details.results usage into session totals); discarding it means fan-out spend never reaches budget bands — this PR's headline capability escapes accounting. Parse the task result and fold its usage into the adapter's running totals.

Comment thread internal/harness/omp.go
add("web_search")
}
}
if len(out) == 0 {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fail-open: a skill declaring only unmappable tools (e.g. AskUserQuestion) gets the full bash/edit/write set in an ordinary run — contra SECURITY.md's fail-closed permissions. Return a minimal read-only set instead; note pi.go:63 shares this, worth fixing both.

Comment thread internal/harness/omp.go
for _, grant := range allowed {
name := grant
if i := strings.IndexByte(name, '('); i > 0 {
name = name[:i]

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Bash(git clone:*) silently widens to unrestricted bash — a scoped grant becomes full shell without full-permission consent. If omp's --tools can't express command scopes, surface the widening in Notes() (or refuse scoped grants); pi.go shares this too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants