Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
54 changes: 53 additions & 1 deletion docs/integrations/omp.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,56 @@ Invoke them in the agent's chat using dotted notation, for example
Workflow steps use `omp --print <prompt>`; `--print` enables one-shot mode
and the prompt is positional. The adapter forwards a configured `--model` and
uses `--mode json`, not `--output-format`, for structured output.
There are no integration-specific setup options.
There are no integration-specific setup options; see Runtime Options below.

## Runtime Options

Command steps can pass per-step runtime configuration to `omp --print` (see
[Per-Step Integration Configuration](../reference/workflows.md#per-step-integration-configuration)):

| `integration_options` key | OMP flag | Value |
| ------------------------- | ------------ | ------------------------------------------------------------------------ |
| `profile` | `--profile` | Isolated OMP profile name |
| `thinking` | `--thinking` | One of `off`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`, `auto` |
| `tools` | `--tools` | Comma-separated list of tools to enable |

```yaml
- id: plan-with-omp
type: command
command: speckit.plan
integration: omp
integration_args:
- "--no-session"
integration_options:
profile: work
thinking: high
tools: "read,bash"
model: "gpt-5"
```

Option values must be non-empty strings; unknown keys and unsupported
`thinking` values fail before OMP is launched. Select the model with the step's
top-level `model` field: `integration_options.model` and a `--model` flag in
`integration_args` are both rejected.

`integration_args` is passed to OMP as raw flags, but OMP's parser lets some
tokens consume or reorder the generated options and prompt, so each value must
be one of:

- a known valueless flag such as `--no-session`, `--no-title` or `--yolo`; or
- a self-contained `--name=value` token such as `--system-prompt=...` or
`--resume=<id>`.

Bare value-taking flags (`--system-prompt`, `--resume`), the `--` terminator
and `--=` forms (which OMP normalizes to `--`), positional text, `@file`
references, short flags, and `--help`/`--version`/`--print`/`--export`/`--alias`/`--mode` fail before OMP is
launched.

Arguments are passed to `omp --print` in this order, so later sources follow
earlier ones on the command line:

1. `SPECKIT_INTEGRATION_OMP_EXTRA_ARGS`
2. `integration_args`
3. `integration_options` flags
4. `--model` from the step's `model` field
5. `--mode json` (or `--mode text` when JSON output is off), then the prompt
110 changes: 108 additions & 2 deletions src/specify_cli/integrations/omp/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
"""Oh My Pi (omp) coding agent integration."""

from __future__ import annotations

import re
from pathlib import Path

from collections.abc import Mapping, Sequence
Expand All @@ -26,6 +28,39 @@ class OmpIntegration(MarkdownIntegration):
}
multi_install_safe = True

_RUNTIME_OPTION_FLAGS = {
"profile": "--profile",
"thinking": "--thinking",
"tools": "--tools",
}
_THINKING_LEVELS = {
"off", "minimal", "low", "medium", "high", "xhigh", "max", "auto",
}

# OMP's parser (args.ts / flag-tables.ts in can1357/oh-my-pi) is not
# POSIX-strict: value-taking flags consume the *next* token, even one that
# looks like a flag, and `--=...` is normalized to the `--` terminator.
# To keep generated flags and the prompt intact, ``integration_args`` is
# limited to (a) known valueless flags and (b) self-contained
# ``--name=value`` tokens.
_VALUELESS_FLAGS = frozenset({
"--allow-home", "--continue", "--from-claude", "--from-codex",
"--no-session", "--no-tools", "--no-lsp", "--no-pty",
"--hide-thinking", "--advisor", "--wait-async", "--external-thinking",
"--prewalk", "--no-prewalk", "--plan-yolo", "--print-thoughts",
"--no-extensions", "--no-skills", "--no-rules", "--no-context-files",
"--no-title", "--no-ui", "--auto-approve", "--yolo",
})
# OMP splits ``--name=value`` at ``=`` and enables these flags regardless
# of the inline value (``--help=false`` still prints help and exits).
# ``--export``/``--alias`` replace prompt execution (the prompt becomes an
# output filename, or alias installation returns early) and ``--mode`` is
# owned by the adapter, so they are rejected in equals-form too.
_RESERVED_FLAG_NAMES = frozenset({
"--help", "--version", "--print", "--export", "--alias", "--mode",
})
_EQUALS_FLAG_RE = re.compile(r"^--[A-Za-z][A-Za-z0-9-]*=")

def build_exec_args(
self,
prompt: str,
Expand All @@ -45,9 +80,80 @@ def build_exec_args(
self.validate_runtime_config(integration_args, integration_options)
args = [self._resolve_executable(), "--print"]
self._apply_extra_args_env_var(args)
args.extend(integration_args or ())
Comment thread
kevinnguyenhoang91 marked this conversation as resolved.
Resolved
for option, value in (integration_options or {}).items():
Comment thread
kevinnguyenhoang91 marked this conversation as resolved.
Resolved
args.extend([self._RUNTIME_OPTION_FLAGS[option], value])
if model:
args.extend(["--model", model])
if output_json:
args.extend(["--mode", "json"])
args.extend(["--mode", "json" if output_json else "text"])
args.append(prompt)
return args

def validate_runtime_config(
self,
integration_args: Sequence[str] | None = None,
integration_options: Mapping[str, Any] | None = None,
) -> None:
"""Validate OMP per-step CLI flags and named options."""
if not all(
isinstance(value, str) and value.strip()
for value in integration_args or ()
):
raise ValueError("OMP 'integration_args' values must be non-empty strings.")
if "--" in (integration_args or ()):
raise ValueError(
"OMP 'integration_args' must not contain the '--' option "
"terminator; it would turn generated options and the prompt "
"into positional arguments."
)
if any(
value == "--model" or value.startswith("--model=")
for value in integration_args or ()
):
raise ValueError(
"OMP model selection must use the command-step 'model' field, "
"not a '--model' flag in 'integration_args'."
)

for value in integration_args or ():
if (
value in self._VALUELESS_FLAGS
or self._EQUALS_FLAG_RE.match(value)
and value.partition("=")[0] not in self._RESERVED_FLAG_NAMES
):
Comment on lines +119 to +123
continue
raise ValueError(
f"OMP 'integration_args' value {value!r} is not allowed. Use a "
"known valueless flag (for example '--no-session') or pass "
"flag values inline as '--name=value'; bare value-taking "
"flags, positional text, '@file' references and '--=' forms "
"can consume or reorder the generated options and prompt."
)

options = integration_options or {}
if not all(isinstance(name, str) for name in options):
raise ValueError("OMP 'integration_options' keys must be strings.")
if "model" in options:
raise ValueError(
"OMP model selection must use the command-step 'model' field, "
"not 'integration_options.model'."
)
unknown = sorted(set(options) - self._RUNTIME_OPTION_FLAGS.keys())
if unknown:
names = ", ".join(repr(name) for name in unknown)
allowed = ", ".join(sorted(self._RUNTIME_OPTION_FLAGS))
raise ValueError(
f"OMP received unknown integration option(s): {names}. "
f"Supported options: {allowed}."
)
for name, value in options.items():
if not isinstance(value, str) or not value.strip():
raise ValueError(
f"OMP integration option {name!r} must be a non-empty string."
)
thinking = options.get("thinking")
if thinking is not None and thinking not in self._THINKING_LEVELS:
allowed = ", ".join(sorted(self._THINKING_LEVELS))
raise ValueError(
f"OMP integration option 'thinking' must be one of: {allowed}."
)
Loading
Loading