Implement REIN-A-0002 ops_run claim loop and approach registry.

Add activity-core ops_run client, approach selection (FI/Binky/mail/agent),
claim-loop worker with lease heartbeat, CLI run --from-ops-run and claim-loop,
install units, and docs demoting issue-core to legacy external tickets.
T05 timer cutover remains operator after five clean cycles.
This commit is contained in:
tegwick 2026-08-03 20:04:25 +02:00
parent 9644202eb2
commit 8200a672ea
15 changed files with 1807 additions and 73 deletions

View file

@ -69,6 +69,48 @@ def _cmd_profiles(_args: argparse.Namespace) -> int:
def _cmd_poll(args: argparse.Namespace) -> int:
source = getattr(args, "source", "issue-core") or "issue-core"
if source in {"ops-run", "ops_run", "ops"}:
from rein_aharness.claim_loop import process_one, poll_peek
from rein_aharness.ops_run_client import OpsRunError
try:
if args.no_claim:
rows = poll_peek()
print(
json.dumps(
{"source": "ops-run", "queue": "empty" if not rows else "open", "items": rows},
indent=2,
)
)
return 0
result = process_one(
dry_run=bool(getattr(args, "dry_run", False)),
report_to_hub=False if getattr(args, "no_hub", False) else True,
)
except OpsRunError as exc:
print(f"ops-run error: {exc}", file=sys.stderr)
return 2
print(
json.dumps(
{
"source": "ops-run",
"claimed": result.claimed,
"empty": result.empty,
"run_id": result.run_id,
"approach": result.approach,
"ok": result.ok,
"ops_state": result.ops_state,
"reason": result.reason,
"detail": result.detail,
},
indent=2,
)
)
if result.empty:
return 0
return 0 if result.ok else 1
from rein_aharness.intake import IntakeError, IssueCoreClient, poll_next
try:
@ -78,12 +120,13 @@ def _cmd_poll(args: argparse.Namespace) -> int:
print(f"intake error: {exc}", file=sys.stderr)
return 2
if result is None:
print(json.dumps({"queue": "empty"}, indent=2))
print(json.dumps({"source": "issue-core", "queue": "empty"}, indent=2))
return 0
issue, spec = result
print(
json.dumps(
{
"source": "issue-core",
"issue_id": issue.issue_id,
"state": issue.state,
"title": issue.title,
@ -99,12 +142,62 @@ def _cmd_poll(args: argparse.Namespace) -> int:
return 0
def _cmd_claim_loop(args: argparse.Namespace) -> int:
import logging
from rein_aharness.claim_loop import run_claim_loop
logging.basicConfig(
level=logging.DEBUG if args.verbose else logging.INFO,
format="%(asctime)s %(levelname)s %(name)s: %(message)s",
)
return run_claim_loop(
once=bool(args.once),
interval_seconds=args.interval,
report_to_hub=not args.no_hub,
commit=not args.no_commit,
dry_run=bool(args.dry_run),
max_iterations=args.max_iterations,
)
def _cmd_run(args: argparse.Namespace) -> int:
from rein_aharness.intake import IntakeError, IssueCoreClient, poll_next
issue_id: str | None = None
client: IssueCoreClient | None = None
if getattr(args, "from_ops_run", False):
from rein_aharness.claim_loop import process_one
from rein_aharness.ops_run_client import OpsRunError
try:
result = process_one(
report_to_hub=not args.no_hub,
commit=not getattr(args, "no_commit", False),
)
except OpsRunError as exc:
print(f"ops-run error: {exc}", file=sys.stderr)
return 2
print(
json.dumps(
{
"source": "ops-run",
"ok": bool(result.ok) if result.claimed else True,
"empty": result.empty,
"run_id": result.run_id,
"approach": result.approach,
"ops_state": result.ops_state,
"reason": result.reason,
"detail": result.detail,
},
indent=2,
)
)
if result.empty:
return 0
return 0 if result.ok else 1
if args.from_issue_core:
try:
client = IssueCoreClient()
@ -113,14 +206,14 @@ def _cmd_run(args: argparse.Namespace) -> int:
print(f"intake error: {exc}", file=sys.stderr)
return 2
if polled is None:
print(json.dumps({"ok": True, "queue": "empty"}, indent=2))
print(json.dumps({"ok": True, "source": "issue-core", "queue": "empty"}, indent=2))
return 0
issue, spec = polled
issue_id = issue.issue_id
else:
if not args.task_file:
print(
"error: provide --task-file or --from-issue-core",
"error: provide --task-file, --from-ops-run, or --from-issue-core",
file=sys.stderr,
)
return 2
@ -160,6 +253,7 @@ def _cmd_run(args: argparse.Namespace) -> int:
json.dumps(
{
"ok": result.ok,
"source": "issue-core" if issue_id else "task-file",
"committed": result.committed,
"head_after": result.head_after,
"persona_source": result.persona_source,
@ -184,16 +278,26 @@ def main(argv: list[str] | None = None) -> int:
run = sub.add_parser(
"run",
help="Execute one task from a JSON file or next issue-core emission",
help="Execute one task: ops_run (primary), issue-core (legacy), or task file",
)
run_src = run.add_mutually_exclusive_group(required=True)
run_src.add_argument("--task-file", help="Local JSON task-spec (dev path)")
run_src.add_argument(
"--from-ops-run",
action="store_true",
help="Claim one activity-core ops_run, select approach, execute, complete/fail",
)
run_src.add_argument(
"--from-issue-core",
action="store_true",
help="Poll issue-core for one open harness-labeled task, claim, run, close",
help="Legacy: poll issue-core for one open harness-labeled task, claim, run, close",
)
run.add_argument("--no-hub", action="store_true", help="Skip hub reporting")
run.add_argument(
"--no-commit",
action="store_true",
help="With --from-ops-run: approaches that support it skip git commit",
)
run.add_argument(
"--no-metrics",
action="store_true",
@ -329,13 +433,51 @@ def main(argv: list[str] | None = None) -> int:
poll = sub.add_parser(
"poll",
help="Peek/claim next open issue-core task labeled for the harness (no execute)",
help="Peek/claim next task (default source=ops-run; issue-core is legacy)",
)
poll.add_argument(
"--source",
choices=["ops-run", "issue-core"],
default="ops-run",
help="ops-run = activity-core claim queue (primary); issue-core = legacy tickets",
)
poll.add_argument(
"--no-claim",
action="store_true",
help="List/map only; do not set in_progress",
help="List/map only; do not claim",
)
poll.add_argument(
"--dry-run",
action="store_true",
help="With ops-run: claim then fail+reopen without executing",
)
poll.add_argument("--no-hub", action="store_true", help="Skip hub on execute")
claim_loop = sub.add_parser(
"claim-loop",
help="Continuously claim ops_runs, select approach, execute, complete/fail",
)
claim_loop.add_argument(
"--once",
action="store_true",
help="Process at most one claim cycle then exit",
)
claim_loop.add_argument(
"--interval",
type=float,
default=None,
help="Seconds between empty-queue polls (default env AGENT_HARNESS_CLAIM_INTERVAL or 30)",
)
claim_loop.add_argument(
"--max-iterations",
type=int,
default=None,
help="Stop after N cycles (tests / bounded runs)",
)
claim_loop.add_argument("--dry-run", action="store_true")
claim_loop.add_argument("--no-hub", action="store_true")
claim_loop.add_argument("--no-commit", action="store_true")
claim_loop.add_argument("-v", "--verbose", action="store_true")
args = parser.parse_args(argv)
@ -348,6 +490,9 @@ def main(argv: list[str] | None = None) -> int:
if args.command == "poll":
return _cmd_poll(args)
if args.command == "claim-loop":
return _cmd_claim_loop(args)
if args.command == "run":
return _cmd_run(args)