From a4aad6d4a815bf90e75abeee2e2a923a6d293504 Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Thu, 8 Oct 2026 14:50:00 +0700 Subject: [PATCH 1/8] feat(omp): support integration args and options in build_exec_args - Pass per-step integration_args straight through to the omp CLI. - Map the named options profile, thinking, and tools to their flags. - Add validate_runtime_config to reject unknown options, empty values, and invalid thinking levels before the command runs. - Reject integration_options.model so model selection stays on the command-step 'model' field. --- src/specify_cli/integrations/omp/__init__.py | 52 ++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index b2088593d2..fa488cc27a 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -26,6 +26,15 @@ 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", + } + def build_exec_args( self, prompt: str, @@ -45,9 +54,52 @@ 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 ()) + for option, value in (integration_options or {}).items(): + args.extend([self._RUNTIME_OPTION_FLAGS[option], value]) if model: args.extend(["--model", model]) if output_json: args.extend(["--mode", "json"]) 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.") + + 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}." + ) From da070fed4d47f42a4a439b9517a6d219eff76660 Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Thu, 8 Oct 2026 15:21:33 +0700 Subject: [PATCH 2/8] test(omp): cover integration args and options in build_exec_args - Verify integration_args and integration_options are passed to omp and ordered after SPECKIT_INTEGRATION_OMP_EXTRA_ARGS. - Accept every documented thinking level and valid runtime configs. - Reject malformed args, the reserved model option, and invalid, unknown or non-string option keys and values. --- tests/integrations/test_integration_omp.py | 111 +++++++++++++++++++++ 1 file changed, 111 insertions(+) diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 07dd57336b..1b22f706a3 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -1,5 +1,7 @@ """Tests for OmpIntegration.""" +import pytest + from specify_cli.integrations import get_integration from .test_integration_base_markdown import MarkdownIntegrationTests @@ -34,3 +36,112 @@ def test_build_exec_args_uses_omp_json_mode(self): "json", "/speckit.specify Build auth", ] + + def test_build_exec_args_passes_integration_args_and_options(self, monkeypatch): + monkeypatch.delenv("SPECKIT_INTEGRATION_OMP_EXECUTABLE", raising=False) + monkeypatch.delenv("SPECKIT_INTEGRATION_OMP_EXTRA_ARGS", raising=False) + + args = get_integration(self.KEY).build_exec_args( + "/speckit.plan", + model="gpt-5", + integration_args=["--no-session", "--verbose"], + integration_options={ + "profile": "work", + "thinking": "high", + "tools": "read,bash", + }, + ) + + assert args == [ + "omp", + "--print", + "--no-session", + "--verbose", + "--profile", + "work", + "--thinking", + "high", + "--tools", + "read,bash", + "--model", + "gpt-5", + "--mode", + "json", + "/speckit.plan", + ] + + def test_build_exec_args_places_integration_args_after_env_extra_args( + self, monkeypatch + ): + monkeypatch.delenv("SPECKIT_INTEGRATION_OMP_EXECUTABLE", raising=False) + monkeypatch.setenv("SPECKIT_INTEGRATION_OMP_EXTRA_ARGS", "--from-env") + + args = get_integration(self.KEY).build_exec_args( + "prompt", + output_json=False, + integration_args=["--per-step"], + integration_options={"thinking": "low"}, + ) + + assert args == [ + "omp", + "--print", + "--from-env", + "--per-step", + "--thinking", + "low", + "prompt", + ] + + @pytest.mark.parametrize( + "thinking", + ["off", "minimal", "low", "medium", "high", "xhigh", "max", "auto"], + ) + def test_validate_runtime_config_accepts_thinking_levels(self, thinking): + get_integration(self.KEY).validate_runtime_config( + None, {"thinking": thinking} + ) + + @pytest.mark.parametrize( + ("integration_args", "integration_options"), + [(None, None), ([], {}), (["--no-session"], {"profile": "work"})], + ) + def test_validate_runtime_config_accepts_valid_config( + self, integration_args, integration_options + ): + get_integration(self.KEY).validate_runtime_config( + integration_args, integration_options + ) + + @pytest.mark.parametrize("integration_args", [[""], [" "], [42], ["--ok", None]]) + def test_integration_args_reject_malformed_values(self, integration_args): + with pytest.raises(ValueError, match="non-empty strings"): + get_integration(self.KEY).build_exec_args( + "prompt", + integration_args=integration_args, + ) + + def test_integration_options_model_is_rejected(self): + with pytest.raises(ValueError, match="command-step 'model' field"): + get_integration(self.KEY).build_exec_args( + "prompt", + integration_options={"model": "gpt-5"}, + ) + + @pytest.mark.parametrize( + ("options", "message"), + [ + ({1: "value"}, "keys must be strings"), + ({"unknown": "value"}, r"unknown integration option\(s\): 'unknown'"), + ({"profile": ""}, "'profile' must be a non-empty string"), + ({"tools": " "}, "'tools' must be a non-empty string"), + ({"profile": 42}, "'profile' must be a non-empty string"), + ({"thinking": "extreme"}, "'thinking' must be one of"), + ], + ) + def test_integration_options_are_validated(self, options, message): + with pytest.raises(ValueError, match=message): + get_integration(self.KEY).build_exec_args( + "prompt", + integration_options=options, + ) From 1362568bd1ac43a5d0675821553db4e8e43de520 Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Thu, 8 Oct 2026 15:30:12 +0700 Subject: [PATCH 3/8] feat(omp): reject --model in integration_args and document OMP options Model selection must go through the command-step `model` field, so a `--model` flag in `integration_args` now fails validation before OMP is launched. This matches the existing rejection of `integration_options.model` and avoids two competing model sources on the command line. - Reject `--model` and `--model=...` in `integration_args`; `--models` (Ctrl+P cycling scope) stays allowed. - Document `profile`, `thinking`, and `tools` options, `integration_args` passthrough, and argument ordering in the integrations reference. - Link the OMP options from the workflows per-step configuration section. - Add tests for rejected `--model` forms and the allowed `--models` flag. --- docs/reference/integrations.md | 41 ++++++++++++++++++++ docs/reference/workflows.md | 5 +++ src/specify_cli/integrations/omp/__init__.py | 8 ++++ tests/integrations/test_integration_omp.py | 17 ++++++++ 4 files changed, 71 insertions(+) diff --git a/docs/reference/integrations.md b/docs/reference/integrations.md index 4bbbb4704e..52d2dc892c 100644 --- a/docs/reference/integrations.md +++ b/docs/reference/integrations.md @@ -340,6 +340,47 @@ extension files intact. The core `speckit.taskstoissues` command remains available; installing the GitHub extension adds the namespaced replacement without deprecating or removing the core command. +### Oh My Pi runtime options + +The options above apply at install time. Oh My Pi (`omp`) additionally accepts +per-step runtime configuration on workflow command steps (see +[Per-Step Integration Configuration](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. `integration_args` is passed +through as raw OMP CLI flags and each value must be a non-empty string. Select +the model with the step's top-level `model` field: `integration_options.model` +and a `--model` flag in `integration_args` are both rejected. + +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`, then the prompt + ## Scaffold a New Integration ```bash diff --git a/docs/reference/workflows.md b/docs/reference/workflows.md index e60653dfe0..f2a462312e 100644 --- a/docs/reference/workflows.md +++ b/docs/reference/workflows.md @@ -821,6 +821,11 @@ or paused command is resumed, the complete dispatch configuration is re-resolved from the current inputs, so values supplied with `workflow resume --input` take effect consistently. A resume without updated inputs reproduces the same values. +Oh My Pi (`omp`) also accepts per-step configuration: `integration_args` are +passed through as raw CLI flags, and `profile`, `thinking`, and `tools` are +accepted as named integration options. See +[Oh My Pi runtime options](integrations.md#oh-my-pi-runtime-options). + ## Expressions Steps can reference inputs and previous step outputs using `{{ expression }}` syntax: diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index fa488cc27a..9518b1a5c7 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -75,6 +75,14 @@ def validate_runtime_config( for value in integration_args or () ): raise ValueError("OMP 'integration_args' values must be non-empty strings.") + 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'." + ) options = integration_options or {} if not all(isinstance(name, str) for name in options): diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 1b22f706a3..65aff0fb4b 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -121,6 +121,23 @@ def test_integration_args_reject_malformed_values(self, integration_args): integration_args=integration_args, ) + @pytest.mark.parametrize( + "integration_args", + [["--model", "gpt-5"], ["--model=gpt-5"], ["--no-session", "--model", "gpt-5"]], + ) + def test_integration_args_model_flag_is_rejected(self, integration_args): + with pytest.raises(ValueError, match="command-step 'model' field"): + get_integration(self.KEY).build_exec_args( + "prompt", + integration_args=integration_args, + ) + + def test_integration_args_allow_models_cycling_flag(self): + # `--models` only scopes Ctrl+P cycling; it does not select the model. + get_integration(self.KEY).validate_runtime_config( + ["--models", "claude-sonnet,gpt-5"], None + ) + def test_integration_options_model_is_rejected(self): with pytest.raises(ValueError, match="command-step 'model' field"): get_integration(self.KEY).build_exec_args( From 6234eeeea25bf4d39b087e96b307859129ee7866 Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Fri, 30 Oct 2026 19:50:25 +0700 Subject: [PATCH 4/8] fix(omp): reject '--' option terminator in integration_args A bare '--' passed validation and, being inserted before the generated --model, --mode json and prompt, made OMP treat them as positional messages. Reject it in validate_runtime_config and add regression tests. Assisted-by: Claude Code (model: Claude Sonnet 5.5, autonomous) Co-Authored-By: Claude Sonnet 5.5 --- src/specify_cli/integrations/omp/__init__.py | 6 ++++++ tests/integrations/test_integration_omp.py | 10 ++++++++++ 2 files changed, 16 insertions(+) diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index 9518b1a5c7..1cfbc7fa22 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -75,6 +75,12 @@ def validate_runtime_config( 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 () diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 65aff0fb4b..5e5ba2d218 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -132,6 +132,16 @@ def test_integration_args_model_flag_is_rejected(self, integration_args): integration_args=integration_args, ) + @pytest.mark.parametrize( + "integration_args", [["--"], ["--no-session", "--"], ["--", "extra"]] + ) + def test_integration_args_option_terminator_is_rejected(self, integration_args): + with pytest.raises(ValueError, match="option terminator"): + get_integration(self.KEY).build_exec_args( + "prompt", + integration_args=integration_args, + ) + def test_integration_args_allow_models_cycling_flag(self): # `--models` only scopes Ctrl+P cycling; it does not select the model. get_integration(self.KEY).validate_runtime_config( From 346e55598b63fb3bea623165f386a4405488e4ac Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Fri, 30 Oct 2026 22:24:32 +0700 Subject: [PATCH 5/8] docs: drop OMP-specific paragraph from workflows reference Integration-specific documentation lives only in docs/integrations/omp.md. Assisted-by: Claude Code (model: Claude Sonnet 5.5, autonomous) Co-Authored-By: Claude Sonnet 5.5 --- docs/reference/workflows.md | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/reference/workflows.md b/docs/reference/workflows.md index e2e6ece331..cb4bc1fa96 100644 --- a/docs/reference/workflows.md +++ b/docs/reference/workflows.md @@ -1060,11 +1060,6 @@ or paused command is resumed, the complete dispatch configuration is re-resolved from the current inputs, so values supplied with `workflow resume --input` take effect consistently. A resume without updated inputs reproduces the same values. -Oh My Pi (`omp`) also accepts per-step configuration: `integration_args` are -passed through as raw CLI flags, and `profile`, `thinking`, and `tools` are -accepted as named integration options. See -[Oh My Pi runtime options](../integrations/omp.md#runtime-options). - ### Capturing Command and Prompt Output `command` and `prompt` steps stream the integration CLI's output to the From 818ce723e821a9b3b5d1a014ed67feb548673fa0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 3 Nov 2026 02:14:17 +0000 Subject: [PATCH 6/8] fix(omp): restrict integration_args to parser-safe tokens OMP normalizes '--=' to the '--' terminator, and bare value-taking flags (--system-prompt, --resume) consume generated flags or the prompt. Accept only known valueless flags and self-contained --name=value tokens, and add regression tests that replay OMP's parsing over the generated argv. Co-Authored-By: Claude Sonnet 5.5 --- docs/integrations/omp.md | 18 ++- src/specify_cli/integrations/omp/__init__.py | 29 +++++ tests/integrations/test_integration_omp.py | 126 ++++++++++++++++++- 3 files changed, 164 insertions(+), 9 deletions(-) diff --git a/docs/integrations/omp.md b/docs/integrations/omp.md index ee4eec2ef1..43acc034ca 100644 --- a/docs/integrations/omp.md +++ b/docs/integrations/omp.md @@ -56,13 +56,23 @@ Command steps can pass per-step runtime configuration to `omp --print` (see ``` Option values must be non-empty strings; unknown keys and unsupported -`thinking` values fail before OMP is launched. `integration_args` is passed -through as raw OMP CLI flags and each value must be a non-empty string. The -`--` option terminator is rejected because it would turn the generated options -and the prompt into positional arguments. Select the model with the step's +`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=`. + +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` fail before OMP is +launched. + Arguments are passed to `omp --print` in this order, so later sources follow earlier ones on the command line: diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index 1cfbc7fa22..071cdf2c29 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -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 @@ -35,6 +37,22 @@ class OmpIntegration(MarkdownIntegration): "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", + }) + _EQUALS_FLAG_RE = re.compile(r"^--[A-Za-z][A-Za-z0-9-]*=") + def build_exec_args( self, prompt: str, @@ -90,6 +108,17 @@ def validate_runtime_config( "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): + 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.") diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 5e5ba2d218..3c0c2b1ae6 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -44,7 +44,7 @@ def test_build_exec_args_passes_integration_args_and_options(self, monkeypatch): args = get_integration(self.KEY).build_exec_args( "/speckit.plan", model="gpt-5", - integration_args=["--no-session", "--verbose"], + integration_args=["--no-session", "--yolo"], integration_options={ "profile": "work", "thinking": "high", @@ -56,7 +56,7 @@ def test_build_exec_args_passes_integration_args_and_options(self, monkeypatch): "omp", "--print", "--no-session", - "--verbose", + "--yolo", "--profile", "work", "--thinking", @@ -79,7 +79,7 @@ def test_build_exec_args_places_integration_args_after_env_extra_args( args = get_integration(self.KEY).build_exec_args( "prompt", output_json=False, - integration_args=["--per-step"], + integration_args=["--no-title"], integration_options={"thinking": "low"}, ) @@ -87,7 +87,7 @@ def test_build_exec_args_places_integration_args_after_env_extra_args( "omp", "--print", "--from-env", - "--per-step", + "--no-title", "--thinking", "low", "prompt", @@ -142,10 +142,78 @@ def test_integration_args_option_terminator_is_rejected(self, integration_args): integration_args=integration_args, ) + @pytest.mark.parametrize( + "integration_args", + [ + ["--="], + ["--=x"], + ["--system-prompt"], + ["--resume"], + ["--models", "a,b"], + ["positional"], + ["@file.md"], + ["-"], + ["-r"], + ["--help"], + ["--print"], + ], + ) + def test_integration_args_unsafe_tokens_are_rejected(self, integration_args): + with pytest.raises(ValueError, match="not allowed"): + get_integration(self.KEY).build_exec_args( + "prompt", integration_args=integration_args + ) + + @pytest.mark.parametrize( + "integration_args", + [ + ["--no-session", "--yolo"], + ["--system-prompt=be brief"], + ["--resume=abc123"], + ["--models=a,b", "--no-title"], + ], + ) + @pytest.mark.parametrize("output_json", [True, False]) + def test_integration_args_preserve_omp_parsing( + self, monkeypatch, integration_args, output_json + ): + """Replay OMP's parseArgs semantics over the generated argv. + + Mirrors args.ts/flag-tables.ts (can1357/oh-my-pi): ``--x=v`` is split + into ``--x v``, string flags consume the next token even if + flag-looking, optional-value flags consume the next non-dash token, + and ``--`` ends option parsing. + """ + monkeypatch.delenv("SPECKIT_INTEGRATION_OMP_EXECUTABLE", raising=False) + monkeypatch.delenv("SPECKIT_INTEGRATION_OMP_EXTRA_ARGS", raising=False) + argv = get_integration(self.KEY).build_exec_args( + "the prompt", + model="gpt-5", + output_json=output_json, + integration_args=integration_args, + integration_options={"thinking": "high"}, + ) + parsed = _parse_omp_args(argv[1:]) + assert parsed["messages"] == ["the prompt"] + assert parsed["model"] == "gpt-5" + assert parsed["thinking"] == "high" + assert parsed["print"] is True + assert parsed.get("mode") == ("json" if output_json else None) + + def test_replayed_parser_shows_why_bare_flags_are_unsafe(self): + # Sanity-check the model against the cases from the PR review. + assert _parse_omp_args(["--print", "--=", "--model", "m", "p"])[ + "messages" + ] == ["--model", "m", "p"] + assert _parse_omp_args(["--system-prompt", "--mode", "json", "p"])[ + "system-prompt" + ] == "--mode" + assert _parse_omp_args(["--resume", "p"])["messages"] == [] + def test_integration_args_allow_models_cycling_flag(self): # `--models` only scopes Ctrl+P cycling; it does not select the model. get_integration(self.KEY).validate_runtime_config( - ["--models", "claude-sonnet,gpt-5"], None + ["--models=claude-sonnet,gpt-5"], None ) def test_integration_options_model_is_rejected(self): @@ -172,3 +240,51 @@ def test_integration_options_are_validated(self, options, message): "prompt", integration_options=options, ) + + +_STRING_FLAGS = { + "--system-prompt", "--model", "--mode", "--thinking", "--tools", + "--models", "--profile", "--alias", "--append-system-prompt", "--cwd", +} +_OPTIONAL_FLAGS = {"--resume", "-r", "--restore", "--session"} + + +def _parse_omp_args(argv): + """Minimal replay of OMP's ``parseArgs`` for the flags this adapter emits.""" + args = list(argv) + result = {"messages": [], "print": False} + separator = False + i = 0 + while i < len(args): + arg = args[i] + if separator: + result["messages"].append(arg) + i += 1 + continue + eq_index = -1 + if arg.startswith("--") and "=" in arg: + name, _, value = arg.partition("=") + arg = name + args.insert(i + 1, value) + eq_index = i + 1 + start = i + if arg in _STRING_FLAGS: + if i + 1 < len(args): + i += 1 + result[arg[2:]] = args[i] + elif arg in _OPTIONAL_FLAGS: + nxt = args[i + 1] if i + 1 < len(args) else None + if nxt is not None and not nxt.startswith("-") and nxt != "": + i += 1 + result[arg.lstrip("-")] = args[i] + elif arg in ("--print", "-p"): + result["print"] = True + elif arg == "--": + separator = True + elif arg.startswith("@") or not arg.startswith("-") or arg == "-": + if not arg.startswith("@"): + result["messages"].append(arg) + if eq_index != -1 and i == start: + del args[eq_index] + i += 1 + return result From b236d49395c5f022b741d77af9b8ea265e15a357 Mon Sep 17 00:00:00 2001 From: kevinnguyenhoang91 Date: Tue, 3 Nov 2026 12:01:23 +0000 Subject: [PATCH 7/8] fix(omp): reject equals-form help/version/print in integration_args Co-Authored-By: Claude Sonnet 5.5 --- src/specify_cli/integrations/omp/__init__.py | 10 +++++++++- tests/integrations/test_integration_omp.py | 4 ++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index 071cdf2c29..3c024d7175 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -51,6 +51,10 @@ class OmpIntegration(MarkdownIntegration): "--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), so + # they are rejected in equals-form too. + _RESERVED_FLAG_NAMES = frozenset({"--help", "--version", "--print"}) _EQUALS_FLAG_RE = re.compile(r"^--[A-Za-z][A-Za-z0-9-]*=") def build_exec_args( @@ -109,7 +113,11 @@ def validate_runtime_config( ) for value in integration_args or (): - if value in self._VALUELESS_FLAGS or self._EQUALS_FLAG_RE.match(value): + if ( + value in self._VALUELESS_FLAGS + or self._EQUALS_FLAG_RE.match(value) + and value.partition("=")[0] not in self._RESERVED_FLAG_NAMES + ): continue raise ValueError( f"OMP 'integration_args' value {value!r} is not allowed. Use a " diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 3c0c2b1ae6..3855d3261f 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -156,6 +156,10 @@ def test_integration_args_option_terminator_is_rejected(self, integration_args): ["-r"], ["--help"], ["--print"], + ["--help=false"], + ["--version=false"], + ["--print=false"], + ["--help="], ], ) def test_integration_args_unsafe_tokens_are_rejected(self, integration_args): From 0042975f441fff5de8442fdfa4fde819a2f66655 Mon Sep 17 00:00:00 2001 From: Kha Nguyen Date: Wed, 4 Nov 2026 09:44:42 +0700 Subject: [PATCH 8/8] fix(omp): reserve --export/--alias/--mode and emit --mode text when JSON is off Co-Authored-By: Claude Sonnet 5.5 --- docs/integrations/omp.md | 4 ++-- src/specify_cli/integrations/omp/__init__.py | 13 ++++++++----- tests/integrations/test_integration_omp.py | 7 ++++++- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/integrations/omp.md b/docs/integrations/omp.md index 43acc034ca..339131732e 100644 --- a/docs/integrations/omp.md +++ b/docs/integrations/omp.md @@ -70,7 +70,7 @@ be one of: 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` fail before OMP is +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 @@ -80,4 +80,4 @@ earlier ones on the command line: 2. `integration_args` 3. `integration_options` flags 4. `--model` from the step's `model` field -5. `--mode json`, then the prompt +5. `--mode json` (or `--mode text` when JSON output is off), then the prompt diff --git a/src/specify_cli/integrations/omp/__init__.py b/src/specify_cli/integrations/omp/__init__.py index 3c024d7175..4de0e9e1ed 100644 --- a/src/specify_cli/integrations/omp/__init__.py +++ b/src/specify_cli/integrations/omp/__init__.py @@ -52,9 +52,13 @@ class OmpIntegration(MarkdownIntegration): "--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), so - # they are rejected in equals-form too. - _RESERVED_FLAG_NAMES = frozenset({"--help", "--version", "--print"}) + # 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( @@ -81,8 +85,7 @@ def build_exec_args( 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 diff --git a/tests/integrations/test_integration_omp.py b/tests/integrations/test_integration_omp.py index 3855d3261f..28e4b2f42a 100644 --- a/tests/integrations/test_integration_omp.py +++ b/tests/integrations/test_integration_omp.py @@ -90,6 +90,8 @@ def test_build_exec_args_places_integration_args_after_env_extra_args( "--no-title", "--thinking", "low", + "--mode", + "text", "prompt", ] @@ -160,6 +162,9 @@ def test_integration_args_option_terminator_is_rejected(self, integration_args): ["--version=false"], ["--print=false"], ["--help="], + ["--export=session.jsonl"], + ["--alias=omp-work"], + ["--mode=rpc"], ], ) def test_integration_args_unsafe_tokens_are_rejected(self, integration_args): @@ -202,7 +207,7 @@ def test_integration_args_preserve_omp_parsing( assert parsed["model"] == "gpt-5" assert parsed["thinking"] == "high" assert parsed["print"] is True - assert parsed.get("mode") == ("json" if output_json else None) + assert parsed.get("mode") == ("json" if output_json else "text") def test_replayed_parser_shows_why_bare_flags_are_unsafe(self): # Sanity-check the model against the cases from the PR review.