Skip to content
Merged
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
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,15 @@ agentic-harness check
agentic-harness report
```

To use a free-form managed workflow (kept separate from the verified `do` path):

```bash
cd /path/to/your/project
agentic-harness external-do "prepare release notes" --mode cloud
agentic-harness external-status
agentic-harness external-watch
```

`--check` is the independent completion gate. A worker saying “done” cannot
replace it. The durable report is written to
`.agentic-harness/runs/{goal-id}/report.md`.
Expand Down
156 changes: 156 additions & 0 deletions agentic_harness/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
from agentic_harness.core.local_goal_bridge import (
CommandResult,
DOC_ROOT_ENV,
EXECUTION_PROFILES,
LocalGoalBridge,
Mode3AGoalOptions,
format_command_result,
Expand Down Expand Up @@ -125,6 +126,45 @@ def build_parser() -> argparse.ArgumentParser:
help="Independent command to run before accepting done; repeat for multiple checks.",
)
easy_do.add_argument("--json", action="store_true", help="Print final goal JSON.")
external_do = sub.add_parser(
"external-do",
help="Start a free-form goal through the optional managed external backend",
)
external_do.add_argument("objective")
external_do.add_argument(
"--mode",
default="local",
help="External mode: local, guided, cloud, or experimental. Default: local.",
)
external_do.add_argument("--safe-area", action="append", default=[])
external_do.add_argument(
"--check",
"--verify",
dest="check",
action="append",
default=[],
metavar="COMMAND",
help="Verification command to carry with the goal; repeat for multiple checks.",
)
external_do.add_argument("--doc-root", default=None, help=DOC_ROOT_HELP)
external_do.add_argument(
"--execution-profile",
choices=sorted(EXECUTION_PROFILES),
default="automatic",
help="Optional managed model profile; default lets the backend choose safely.",
)
external_do.add_argument(
"--supervision",
choices=("none", "glm-5.2"),
default="none",
help="Optional advisory reviewer for a local Mode 1 goal.",
)
external_do.add_argument(
"--monitor",
action="store_true",
help="Run one external monitor pass after a successful start.",
)
external_do.add_argument("--json", action="store_true", help="Print a safe JSON receipt.")
work = sub.add_parser("work", help="Interactive no-jargon mode picker")
work.add_argument("--doc-root", default=None, help=DOC_ROOT_HELP)
gui = sub.add_parser("gui", help="Open the local browser app")
Expand Down Expand Up @@ -181,6 +221,18 @@ def build_parser() -> argparse.ArgumentParser:
)
mode3a_monitor.add_argument("--doc-root", default=None, help=DOC_ROOT_HELP)
mode3a_monitor.add_argument("--json", action="store_true")
external_status = sub.add_parser(
"external-status",
help="Show status from the optional managed external backend",
)
external_status.add_argument("--doc-root", default=None, help=DOC_ROOT_HELP)
external_status.add_argument("--json", action="store_true")
external_watch = sub.add_parser(
"external-watch",
help="Run one monitor pass on the optional managed external backend",
)
external_watch.add_argument("--doc-root", default=None, help=DOC_ROOT_HELP)
external_watch.add_argument("--json", action="store_true")
sub.add_parser("agents", help="Show supported backend tools found on PATH")
create_demo_cmd = sub.add_parser("create-demo", help="Create a runnable example project")
create_demo_cmd.add_argument("demo", choices=demo_names())
Expand Down Expand Up @@ -407,6 +459,8 @@ def main(argv: list[str] | None = None) -> int:
return run_server_from_args(args)
if args.command == "do":
return run_easy_do_command(args, project_dir)
if args.command == "external-do":
return run_external_goal_command(args)
if args.command == "check":
return run_easy_check_command(args, project_dir)
if args.command == "watch":
Expand All @@ -417,6 +471,10 @@ def main(argv: list[str] | None = None) -> int:
return run_mode3a_status(args)
if args.command == "mode3a-monitor":
return run_mode3a_monitor(args)
if args.command == "external-status":
return run_external_status_command(args)
if args.command == "external-watch":
return run_external_watch_command(args)
if args.command == "create-demo":
try:
demo_path = create_demo(args.demo, args.path, force=args.force)
Expand Down Expand Up @@ -1097,6 +1155,104 @@ def run_mode3a_monitor(args: argparse.Namespace) -> int:
return result.returncode


def _safe_external_payload(mode_key: str, result: CommandResult) -> dict[str, object]:
return {
"ok": result.returncode == 0,
"mode": mode_key,
"returncode": result.returncode,
"output": format_command_result(result),
}


def run_external_goal_command(args: argparse.Namespace) -> int:
objective = args.objective.strip()
if not objective:
print("No goal entered. Nothing started.")
return 2

bridge = LocalGoalBridge(doc_root=resolve_doc_root(args.doc_root))
if not bridge.available():
payload = {
"ok": False,
"error": "local-goal backend not found or not executable",
"path": str(bridge.local_goal),
"next": local_goal_setup_hint(),
}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(payload["error"])
print(f"Expected: {payload['path']}")
print(f"Next: {payload['next']}")
return 2

try:
mode = human_mode_by_key(args.mode)
result = bridge.start_human_goal(
mode_key=mode.key,
objective=objective,
safe_areas=tuple(args.safe_area),
checks=tuple(args.check),
execution_profile=args.execution_profile,
supervision=args.supervision,
)
except ValueError as exc:
payload = {"ok": False, "error": str(exc)}
if args.json:
print(json.dumps(payload, indent=2, sort_keys=True))
else:
print(f"Could not start external goal: {exc}")
return 2

monitor_result: CommandResult | None = None
if args.monitor and result.returncode == 0:
monitor_result = bridge.monitor(json_output=args.json)

if args.json:
payload = _safe_external_payload(mode.key, result)
if monitor_result is not None:
payload["monitor"] = _safe_external_payload(mode.key, monitor_result)
print(json.dumps(payload, indent=2, sort_keys=True))
else:
if result.returncode == 0:
print("Started external goal.")
print(f"Mode: {mode.key} — {mode.title}")
print(_friendly_queue_summary(result.stdout))
print("Check it: agentic-harness external-status")
print("Move it forward: agentic-harness external-watch")
if monitor_result is not None:
print(format_command_result(monitor_result))
else:
print("The managed external backend did not start the goal.")
print(format_command_result(result))

if monitor_result is not None:
return monitor_result.returncode
return result.returncode


def run_external_status_command(args: argparse.Namespace) -> int:
bridge = LocalGoalBridge(doc_root=resolve_doc_root(args.doc_root))
if not bridge.available():
print(f"local-goal backend not found or not executable: {bridge.local_goal}")
print(f"Next: {local_goal_setup_hint()}")
return 2
result = bridge.status(json_output=args.json)
print(format_command_result(result))
return result.returncode


def run_external_watch_command(args: argparse.Namespace) -> int:
bridge = LocalGoalBridge(doc_root=resolve_doc_root(args.doc_root))
if not bridge.available():
print(f"local-goal backend not found or not executable: {bridge.local_goal}")
print(f"Next: {local_goal_setup_hint()}")
return 2
result = bridge.monitor(json_output=args.json)
print(format_command_result(result))
return result.returncode


def run_recipe(
project_dir: Path,
recipe: Recipe,
Expand Down
128 changes: 128 additions & 0 deletions tests/test_external_goal_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from __future__ import annotations

import json
from pathlib import Path

from agentic_harness.core.local_goal_bridge import CommandResult
import agentic_harness.cli as cli


class FakeLocalGoalBridge:
def __init__(self, *, doc_root: Path | None = None) -> None:
self.doc_root = doc_root
self.local_goal = Path("/tmp/fake-local-goal")
self.status_calls = 0
self.monitor_calls = 0
self.start_kwargs: dict[str, object] | None = None

def available(self) -> bool:
return True

def start_human_goal(self, **kwargs: object) -> CommandResult:
self.start_kwargs = kwargs
return CommandResult(
("local-goal", "start"),
0,
"queued_id=goal-123\nrun_dir=/tmp/queue\n",
"",
)

def status(self, *, json_output: bool = False) -> CommandResult: # noqa: ARG002
self.status_calls += 1
return CommandResult(
("local-goal", "status"),
0,
"mode=local\nqueued_id=goal-123\n",
"",
)

def monitor(self, *, json_output: bool = False) -> CommandResult: # noqa: ARG002
self.monitor_calls += 1
return CommandResult(("local-goal", "monitor"), 0, "monitor ok\n", "")


class MissingLocalGoalBridge(FakeLocalGoalBridge):
def available(self) -> bool:
return False


def test_external_do_command_starts_managed_goal_and_outputs_receipt(monkeypatch, tmp_path, capsys) -> None:
captured: dict[str, FakeLocalGoalBridge] = {}

def bridge_factory(*_args: object, **_kwargs: object) -> FakeLocalGoalBridge:
bridge = FakeLocalGoalBridge()
captured["bridge"] = bridge
return bridge

monkeypatch.setattr(cli, "LocalGoalBridge", bridge_factory)

rc = cli.main(
[
"--project-dir",
str(tmp_path),
"external-do",
"prepare release notes",
"--mode",
"local",
"--safe-area",
"docs",
"--check",
"python -m pytest",
"--execution-profile",
"automatic",
"--supervision",
"none",
"--monitor",
"--json",
]
)

assert rc == 0
output = capsys.readouterr().out
payload = json.loads(output)
assert payload["ok"] is True
assert payload["mode"] == "local"
assert payload["returncode"] == 0
assert payload["monitor"]["returncode"] == 0

bridge = captured["bridge"]
assert bridge.start_kwargs == {
"mode_key": "local",
"objective": "prepare release notes",
"safe_areas": ("docs",),
"checks": ("python -m pytest",),
"execution_profile": "automatic",
"supervision": "none",
}


def test_external_do_command_fails_when_backend_missing(monkeypatch, tmp_path, capsys) -> None:
monkeypatch.setattr(cli, "LocalGoalBridge", MissingLocalGoalBridge)

rc = cli.main(
[
"--project-dir",
str(tmp_path),
"external-do",
"prepare release notes",
"--json",
]
)

assert rc == 2
payload = json.loads(capsys.readouterr().out)
assert payload["ok"] is False
assert payload["error"] == "local-goal backend not found or not executable"


def test_external_status_and_watch_commands(monkeypatch, tmp_path) -> None:
bridge = FakeLocalGoalBridge()
monkeypatch.setattr(cli, "LocalGoalBridge", lambda *args, **kwargs: bridge)

rc_status = cli.main(["--project-dir", str(tmp_path), "external-status"])
assert rc_status == 0
assert bridge.status_calls == 1

rc_watch = cli.main(["--project-dir", str(tmp_path), "external-watch", "--json"])
assert rc_watch == 0
assert bridge.monitor_calls == 1