···200200201201 assert result.exit_code == 0
202202 assert "1710864123456" in result.output
203203- mock_cortex.assert_called_once_with(prompt="Fix the bug", name="coder")
203203+ mock_cortex.assert_called_once_with(
204204+ prompt="Fix the bug", name="coder", config=None
205205+ )
204206205207 def test_handoff_empty_stdin(self):
206208 """Empty stdin produces error and exit code 1."""
+155
tests/test_engage.py
···11+# SPDX-License-Identifier: AGPL-3.0-only
22+# Copyright (c) 2026 sol pbc
33+44+"""Tests for the engage CLI command."""
55+66+import importlib
77+from unittest.mock import patch
88+99+from typer.testing import CliRunner
1010+1111+runner = CliRunner()
1212+1313+1414+def _engage_app():
1515+ mod = importlib.reload(importlib.import_module("think.engage"))
1616+ return mod.engage_app
1717+1818+1919+def _invoke_engage(*args, input_text=""):
2020+ return runner.invoke(_engage_app(), [*args], input=input_text)
2121+2222+2323+def _call_app():
2424+ call_mod = importlib.reload(importlib.import_module("think.call"))
2525+ return call_mod.call_app
2626+2727+2828+class TestEngage:
2929+ def test_fire_and_forget(self):
3030+ with patch(
3131+ "think.cortex_client.cortex_request", return_value="agent-123"
3232+ ) as mock_cr:
3333+ result = _invoke_engage("coder", input_text="fix the bug\n")
3434+3535+ assert result.exit_code == 0
3636+ assert "agent-123" in result.output
3737+ mock_cr.assert_called_once_with(
3838+ prompt="fix the bug", name="coder", config=None
3939+ )
4040+4141+ def test_empty_stdin(self):
4242+ result = _invoke_engage("coder", input_text="")
4343+4444+ assert result.exit_code == 1
4545+ assert (
4646+ "no prompt" in result.output.lower()
4747+ or "no prompt" in (result.stderr or "").lower()
4848+ )
4949+5050+ def test_cortex_failure(self):
5151+ with patch("think.cortex_client.cortex_request", return_value=None):
5252+ result = _invoke_engage("coder", input_text="fix the bug\n")
5353+5454+ assert result.exit_code == 1
5555+5656+ def test_wait_success(self):
5757+ with patch(
5858+ "think.cortex_client.cortex_request", return_value="agent-123"
5959+ ), patch(
6060+ "think.cortex_client.wait_for_agents",
6161+ return_value=({"agent-123": "finish"}, []),
6262+ ), patch(
6363+ "think.cortex_client.read_agent_events",
6464+ return_value=[{"event": "finish", "result": "All fixed!"}],
6565+ ):
6666+ result = _invoke_engage("coder", "--wait", input_text="fix the bug\n")
6767+6868+ assert result.exit_code == 0
6969+ assert "All fixed!" in result.output
7070+7171+ def test_wait_error(self):
7272+ with patch(
7373+ "think.cortex_client.cortex_request", return_value="agent-123"
7474+ ), patch(
7575+ "think.cortex_client.wait_for_agents",
7676+ return_value=({"agent-123": "error"}, []),
7777+ ):
7878+ result = _invoke_engage("coder", "--wait", input_text="fix the bug\n")
7979+8080+ assert result.exit_code == 1
8181+8282+ def test_wait_timeout(self):
8383+ with patch(
8484+ "think.cortex_client.cortex_request", return_value="agent-123"
8585+ ), patch(
8686+ "think.cortex_client.wait_for_agents",
8787+ return_value=({}, ["agent-123"]),
8888+ ):
8989+ result = _invoke_engage("coder", "--wait", input_text="fix the bug\n")
9090+9191+ assert result.exit_code == 1
9292+ combined_output = result.output
9393+ if result.stderr:
9494+ combined_output += result.stderr
9595+ assert "timed out" in combined_output.lower()
9696+9797+ def test_facet_and_day(self):
9898+ with patch(
9999+ "think.cortex_client.cortex_request", return_value="agent-123"
100100+ ) as mock_cr:
101101+ result = _invoke_engage(
102102+ "coder",
103103+ "--facet",
104104+ "work",
105105+ "--day",
106106+ "20260404",
107107+ input_text="do stuff\n",
108108+ )
109109+110110+ assert result.exit_code == 0
111111+ mock_cr.assert_called_once_with(
112112+ prompt="do stuff",
113113+ name="coder",
114114+ config={"facet": "work", "day": "20260404"},
115115+ )
116116+117117+ def test_facet_only(self):
118118+ with patch(
119119+ "think.cortex_client.cortex_request", return_value="agent-123"
120120+ ) as mock_cr:
121121+ result = _invoke_engage(
122122+ "coder", "--facet", "work", input_text="do stuff\n"
123123+ )
124124+125125+ assert result.exit_code == 0
126126+ mock_cr.assert_called_once_with(
127127+ prompt="do stuff", name="coder", config={"facet": "work"}
128128+ )
129129+130130+ def test_day_only(self):
131131+ with patch(
132132+ "think.cortex_client.cortex_request", return_value="agent-123"
133133+ ) as mock_cr:
134134+ result = _invoke_engage(
135135+ "coder", "--day", "20260404", input_text="do stuff\n"
136136+ )
137137+138138+ assert result.exit_code == 0
139139+ mock_cr.assert_called_once_with(
140140+ prompt="do stuff", name="coder", config={"day": "20260404"}
141141+ )
142142+143143+144144+class TestHandoffDeprecated:
145145+ def test_handoff_still_works(self):
146146+ with patch("think.cortex_client.cortex_request", return_value="agent-123"):
147147+ result = runner.invoke(_call_app(), ["handoff", "coder"], input="fix the bug\n")
148148+149149+ assert result.exit_code == 0
150150+ assert "agent-123" in result.output
151151+152152+ def test_handoff_hidden(self):
153153+ result = runner.invoke(_call_app(), ["--help"])
154154+155155+ assert "handoff" not in result.output
+1-1
tests/test_handoff.py
···2727 result = _invoke_handoff("coder", input_text="fix the bug\n")
2828 assert result.exit_code == 0
2929 assert "agent-123" in result.output
3030- mock_cr.assert_called_once_with(prompt="fix the bug", name="coder")
3030+ mock_cr.assert_called_once_with(prompt="fix the bug", name="coder", config=None)
313132323333def _assert_handoff_empty_stdin():
+4-17
think/call.py
···111111 typer.echo(f"Navigate: {' '.join(parts)}")
112112113113114114-@call_app.command("handoff")
114114+@call_app.command("handoff", hidden=True)
115115def handoff(
116116 agent: str = typer.Argument(help="Agent name to hand off to (e.g. coder)."),
117117) -> None:
118118- """Spawn a cogitate agent with a request from stdin (fire-and-forget).
119119-120120- Reads a prompt from stdin, sends it to cortex as an agent request,
121121- prints the agent_id to stdout, and exits immediately.
122122-123123- Example::
124124-125125- echo 'Fix the matching bug' | sol call handoff coder
126126- """
118118+ """Spawn a cogitate agent with a request from stdin (fire-and-forget)."""
127119 prompt = sys.stdin.read()
128120 if not prompt.strip():
129121 typer.echo("Error: no prompt provided on stdin.", err=True)
130122 raise typer.Exit(1)
131123132132- from think.cortex_client import cortex_request
133133-134134- agent_id = cortex_request(prompt=prompt.strip(), name=agent)
135135- if agent_id is None:
136136- typer.echo("Error: failed to send cortex request.", err=True)
137137- raise typer.Exit(1)
124124+ from think.engage import _engage
138125139139- typer.echo(agent_id)
126126+ _engage(agent, prompt.strip())
140127141128142129def main() -> None:
+100
think/engage.py
···11+# SPDX-License-Identifier: AGPL-3.0-only
22+# Copyright (c) 2026 sol pbc
33+44+"""CLI for delegating work to cogitate agents.
55+66+Provides ``sol engage <name>`` as the primary agent delegation command,
77+replacing ``sol call handoff`` with richer options.
88+"""
99+1010+import sys
1111+1212+import typer
1313+1414+engage_app = typer.Typer(name="engage")
1515+1616+1717+def _engage(
1818+ name: str,
1919+ prompt: str,
2020+ *,
2121+ wait: bool = False,
2222+ facet: str | None = None,
2323+ day: str | None = None,
2424+) -> None:
2525+ config_data = {}
2626+ if facet is not None:
2727+ config_data["facet"] = facet
2828+ if day is not None:
2929+ config_data["day"] = day
3030+ config = config_data or None
3131+3232+ from think.cortex_client import cortex_request
3333+3434+ agent_id = cortex_request(prompt=prompt, name=name, config=config)
3535+ if agent_id is None:
3636+ typer.echo("Error: failed to send cortex request.", err=True)
3737+ raise typer.Exit(1)
3838+3939+ if not wait:
4040+ typer.echo(agent_id)
4141+ return
4242+4343+ from think.cortex_client import read_agent_events, wait_for_agents
4444+4545+ completed, timed_out = wait_for_agents([agent_id])
4646+ if agent_id in timed_out:
4747+ typer.echo("Error: agent timed out.", err=True)
4848+ raise typer.Exit(1)
4949+5050+ end_state = completed.get(agent_id, "error")
5151+ if end_state != "finish":
5252+ typer.echo(f"Error: agent ended with state: {end_state}", err=True)
5353+ raise typer.Exit(1)
5454+5555+ events = read_agent_events(agent_id)
5656+ result = ""
5757+ for event in reversed(events):
5858+ if event.get("event") == "finish":
5959+ result = event.get("result", "")
6060+ break
6161+6262+ typer.echo(result)
6363+6464+6565+@engage_app.command()
6666+def engage(
6767+ name: str = typer.Argument(help="Agent name to delegate to (e.g. coder)."),
6868+ wait: bool = typer.Option(
6969+ False,
7070+ "--wait",
7171+ help="Block until the agent completes and print its result.",
7272+ ),
7373+ facet: str | None = typer.Option(
7474+ None, "--facet", help="Facet context for the agent."
7575+ ),
7676+ day: str | None = typer.Option(
7777+ None, "--day", help="Day context for the agent (e.g. 20260404)."
7878+ ),
7979+) -> None:
8080+ """Delegate work to a cogitate agent.
8181+8282+ Reads a prompt from stdin, sends it to cortex as an agent request.
8383+ By default, prints the agent_id and exits immediately (fire-and-forget).
8484+8585+ Example::
8686+8787+ echo 'Fix the matching bug' | sol engage coder
8888+ echo 'Fix the matching bug' | sol engage coder --wait
8989+ """
9090+ prompt = sys.stdin.read()
9191+ if not prompt.strip():
9292+ typer.echo("Error: no prompt provided on stdin.", err=True)
9393+ raise typer.Exit(1)
9494+9595+ _engage(name, prompt.strip(), wait=wait, facet=facet, day=day)
9696+9797+9898+def main() -> None:
9999+ """Entry point for ``sol engage``."""
100100+ engage_app()