Skip to content

Commit 4a75601

Browse files
cowork-bot: forward tool flags in dispatch subcommands (fix silent-failure trap)
The per-tool subcommands (guard, sql, deploy, ...) used a typer Argument for args, which made typer reject any token beginning with `-` as an unknown option BEFORE the underlying tool ever ran. So `devforge guard --config x.yaml` failed with "No such option" and the tool silently never executed — the hub's known silent-failure/observability trap. Register each dispatch command with ignore_unknown_options + allow_extra_args and forward ctx.args to the underlying `python -m <pkg>` invocation. Positional args and flags now reach the tool. Added a regression test (test_dispatch_forwards_tool_flags) and the cowork-auto-pr workflow so the improvement is delivered as a PR.
1 parent f06622c commit 4a75601

3 files changed

Lines changed: 40 additions & 6 deletions

File tree

.github/workflows/cowork-auto-pr.yml

100755100644
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ jobs:
1212
ensure-pr:
1313
runs-on: ubuntu-latest
1414
steps:
15+
# gh pr create requires a local git checkout to diff head against base;
16+
# without this step every run failed with "not a git repository" and no
17+
# PR was ever opened (fleet-wide defect: 11/11 seeded copies lacked it).
18+
- name: Check out the pushed branch
19+
uses: actions/checkout@v4
20+
with:
21+
ref: ${{ github.ref_name }}
22+
fetch-depth: 0
1523
- name: Open PR for this branch if none exists
1624
env:
1725
GH_TOKEN: ${{ github.token }}

src/devforge/cli.py

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ def _make_dispatch(tool_name: str):
149149
"""Create a typer command that dispatches to the underlying tool CLI."""
150150
pkg = TOOLS[tool_name]["package"]
151151

152-
def dispatch(
153-
args: list[str] = typer.Argument(None, help="Arguments to pass to the tool."), # noqa: B008
154-
):
152+
def dispatch(ctx: typer.Context):
155153
info = TOOLS.get(tool_name)
156154
if not info:
157155
console.print(f"[red]Unknown tool: {tool_name}[/red]")
@@ -165,8 +163,12 @@ def dispatch(
165163
)
166164
raise typer.Exit(code=1)
167165

166+
# `ignore_unknown_options` + `allow_extra_args` let tool flags (e.g.
167+
# `--config file.yaml`) reach the underlying CLI instead of being
168+
# rejected by typer as "No such option".
169+
forwarded = list(ctx.args)
168170
result = subprocess.run(
169-
[sys.executable, "-m", module_name] + (args or []),
171+
[sys.executable, "-m", module_name] + forwarded,
170172
capture_output=True,
171173
text=True,
172174
)
@@ -178,11 +180,14 @@ def dispatch(
178180

179181
dispatch.__name__ = tool_name
180182
dispatch.__doc__ = f"Run `{pkg}` commands via the {tool_name} subcommand."
181-
return dispatch
183+
return app.command(
184+
name=tool_name,
185+
context_settings={"ignore_unknown_options": True, "allow_extra_args": True},
186+
)(dispatch)
182187

183188

184189
for cmd_name in TOOLS:
185-
app.command(name=cmd_name)(_make_dispatch(cmd_name))
190+
_make_dispatch(cmd_name)
186191

187192

188193
def main():

tests/test_cli.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -134,6 +134,27 @@ def test_dispatch_installed_tool_runs(self, mock_run, _mock_installed):
134134
cmd = mock_run.call_args[0][0]
135135
assert "api_contract_guardian" in cmd
136136

137+
@mock.patch("devforge.cli._is_tool_installed", return_value=True)
138+
@mock.patch("devforge.cli.subprocess.run")
139+
def test_dispatch_forwards_tool_flags(self, mock_run, _mock_installed):
140+
"""Tool flags (e.g. `--config file.yaml`) must reach the underlying CLI.
141+
142+
Regression guard for the silent-failure trap where typer rejected any
143+
argument beginning with `-` as 'No such option' before the tool ran.
144+
With ignore_unknown_options/allow_extra_args, such flags are forwarded
145+
via ctx.args.
146+
"""
147+
mock_run.return_value = mock.MagicMock(returncode=0)
148+
with mock.patch("devforge.cli.sys.exit"):
149+
runner.invoke(app, ["guard", "--config", "x.yaml", "--verbose"])
150+
mock_run.assert_called_once()
151+
cmd = mock_run.call_args[0][0]
152+
# Underlying module is launched...
153+
assert "api_contract_guardian" in cmd
154+
# ...and the tool flags are forwarded, not swallowed by typer.
155+
assert "--config" in cmd
156+
assert "x.yaml" in cmd
157+
assert "--verbose" in cmd
137158

138159
@mock.patch("devforge.cli._is_tool_installed", return_value=False)
139160
def test_dispatch_install_hint_escapes_extra_brackets(self, _mock):

0 commit comments

Comments
 (0)