Formalize the CLI as the first Channel extension (GLAS-WP-0003)
All checks were successful
CI Smoke / host-smoke (push) Successful in 0s
CI Smoke / container-smoke (push) Successful in 1s

channels/base.py: GatewayInvocation dataclass + Channel ABC
(parse_invocation/render_result), mirroring Rein one layer up -- a
channel turns an external invocation into what run_task_through_rein
needs, and turns its result back into whatever the invocation medium
expects.

channels/cli_channel.py: CLIChannel, a pure refactor of what cli.py did
inline -- argparse Namespace -> GatewayInvocation, result dict ->
json.dumps(..., indent=2). cli.py's run handler is now a thin
construct-parse-invoke-render wrapper.

docs/channel-contract.md documents the pattern and sketches (without
building) a second channel.

Found and fixed a real bug while live-testing this refactor:
ReinAharness's default cli_bin was still "agent-harness", stale from
before HARNESS-WP-0002-T02's rename.

4 new tests (27/27 passing), plus a real live run through
python3 -m glas_harness.cli against rein-aharness/real Claude Code
confirming identical output shape and a real commit.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
tegwick 2026-07-26 22:41:51 +02:00
parent a87b924126
commit d3130162b1
9 changed files with 251 additions and 17 deletions

View file

View file

@ -0,0 +1,38 @@
"""The channel contract — how an external invocation reaches the gateway.
See docs/channel-contract.md (GLAS-WP-0003). A Channel turns an external
invocation (CLI args today; a Slack message or HTTP request for a future
channel) into the inputs run_task_through_rein needs, and turns its
result back into whatever the invocation medium expects.
"""
from __future__ import annotations
from abc import ABC, abstractmethod
from dataclasses import dataclass
from typing import Any
@dataclass
class GatewayInvocation:
"""Everything run_task_through_rein needs, channel-agnostic."""
sandbox_profile: str
repo: str
title: str
description: str
actor: str = "agt"
project: str = "glas-harness"
report_to_hub: bool = True
class Channel(ABC):
"""Base class for concrete invocation channels (CLI, Slack, HTTP, ...)."""
@abstractmethod
def parse_invocation(self, raw: Any) -> GatewayInvocation:
"""Turn a channel-specific invocation into a GatewayInvocation."""
@abstractmethod
def render_result(self, result: dict[str, Any]) -> Any:
"""Turn a gateway result dict into whatever this channel returns."""

View file

@ -0,0 +1,31 @@
"""CLIChannel — the first Channel extension, formalizing what cli.py already did.
GLAS-WP-0003: this is a refactor, not a behavior change. cli.py's
argparse Namespace -> GatewayInvocation mapping and the
json.dumps(..., indent=2) output are exactly what cli.py did inline
before this existed.
"""
from __future__ import annotations
import argparse
import json
from typing import Any
from glas_harness.channels.base import Channel, GatewayInvocation
class CLIChannel(Channel):
def parse_invocation(self, raw: argparse.Namespace) -> GatewayInvocation:
return GatewayInvocation(
sandbox_profile=raw.sandbox_profile,
repo=raw.repo,
title=raw.title,
description=raw.description,
actor=raw.actor,
project=raw.project,
report_to_hub=not raw.no_hub,
)
def render_result(self, result: dict[str, Any]) -> str:
return json.dumps(result, indent=2)

View file

@ -1,9 +1,8 @@
"""glas-harness CLI — minimal gateway invocation (GLAS-WP-0001-T04)."""
"""glas-harness CLI — thin wrapper around CLIChannel (GLAS-WP-0003)."""
from __future__ import annotations
import argparse
import json
import sys
@ -23,18 +22,21 @@ def main(argv: list[str] | None = None) -> int:
args = parser.parse_args(argv)
if args.command == "run":
from glas_harness.channels.cli_channel import CLIChannel
from glas_harness.gateway import run_task_through_rein
channel = CLIChannel()
invocation = channel.parse_invocation(args)
result = run_task_through_rein(
sandbox_profile=args.sandbox_profile,
repo=args.repo,
title=args.title,
description=args.description,
actor=args.actor,
project=args.project,
report_to_hub=not args.no_hub,
sandbox_profile=invocation.sandbox_profile,
repo=invocation.repo,
title=invocation.title,
description=invocation.description,
actor=invocation.actor,
project=invocation.project,
report_to_hub=invocation.report_to_hub,
)
print(json.dumps(result, indent=2))
print(channel.render_result(result))
return 0 if result["tool_ok"] else 1
return 1

View file

@ -34,7 +34,7 @@ class ReinAharnessNotInstalled(RuntimeError):
class ReinAharness(Rein):
def __init__(self, cli_bin: str = "agent-harness", stream_tool_events: bool = False) -> None:
def __init__(self, cli_bin: str = "rein-aharness", stream_tool_events: bool = False) -> None:
self.cli_bin = cli_bin
self.stream_tool_events = stream_tool_events