Skip to content
Open
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
19 changes: 19 additions & 0 deletions harness/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
Minimal Harness for revfactory/harness

Quick start

1. Run the harness:

```bash
python3 harness/harness.py --config harness/config/agents.json
```

What this does

- Loads `harness/config/agents.json` which declares the agent team and simple parameters.
- Dynamically imports example agent modules from `harness/agents/` and calls their `run(config)` function.

Next steps

- Replace or extend the example agents in `harness/agents/` with real implementations.
- Hook the harness into CI or orchestration for automated runs.
1 change: 1 addition & 0 deletions harness/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""harness package init"""
Binary file added harness/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file added harness/__pycache__/harness.cpython-312.pyc
Binary file not shown.
Binary file added harness/__pycache__/logging_setup.cpython-312.pyc
Binary file not shown.
34 changes: 34 additions & 0 deletions harness/agent_team.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Agent team design for this domain


Team overview

- **Orchestrator**: coordinates the workflow, schedules agents, and aggregates results.
- **RepoInspector**: inspects repository structure and key files (README, LICENSE, CHANGELOG, docs).
- **DependencyChecker**: finds dependency manifests (requirements.txt, package.json, pyproject.toml) and reports potential issues.
- **ChangelogAuditor**: parses `CHANGELOG.md` and release notes for recency and formatting hints.
- **DocsChecker**: validates presence of key docs (quickstart, experimental notes) and extracts TODOs.
- **ReleasePlanner**: suggests a next-release checklist based on findings.
- **LocalTester**: runs lightweight smoke checks (existence of test commands, basic lint heuristics).
- **Strategist / ContentGenerator / QA**: as previously defined to plan, generate artifacts, and validate.

Interaction patterns

- Orchestrator invokes Scout -> Auditor -> Strategist -> ContentGenerator -> QA in a pipeline for each work item.
- Agents exchange structured JSON payloads; each agent reads the previous step's output and appends `result` and `metadata`.
- The Orchestrator keeps an audit log and can re-run or branch tasks for parallel experimentation.

Data contracts

- Input: {"id": "<task-id>", "payload": {...}, "history": [...]}
- Agent output: {"id": "<task-id>", "result": {...}, "metadata": {"agent":"<name>","timestamp":"..."}}

Failure and retries

- Agents should return exit codes and error objects; Orchestrator retries transient failures with exponential backoff.
- Critical failures are surfaced to a human reviewer via the Orchestrator's notification hook.

Extensibility

- Add new agents by registering them in the config and implementing `run(config, input)`.
- Support for remote agents (HTTP/gRPC) can be added by providing an adapter layer behind the same interface.
1 change: 1 addition & 0 deletions harness/agents/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
# agents package
Binary file added harness/agents/__pycache__/__init__.cpython-312.pyc
Binary file not shown.
Binary file not shown.
Binary file not shown.
49 changes: 49 additions & 0 deletions harness/agents/example_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@

"""Example agent implementations used by the harness for demonstration.

Each callable accepts `(params, input_payload)` and returns a serializable result.
"""
import logging
from datetime import datetime

logger = logging.getLogger(__name__)


def orchestrator(params, input_payload):
logger.debug("orchestrator start")
return {"note": "orchestrator started", "timestamp": datetime.utcnow().isoformat()}


def scout(params, input_payload):
logger.debug("scout start params=%s", params)
seed = params.get("seed", "unknown")
discovered = {"repos": ["/workspaces/harness"], "seed": seed}
logger.debug("scout discovered=%s", discovered)
return {"discovered": discovered, "timestamp": datetime.utcnow().isoformat()}


def auditor(params, input_payload):
logger.debug("auditor start")
issues = []
if "harness" in input_payload.get("payload", {}).get("repo", ""):
issues.append({"type": "info", "message": "harness folder present"})
logger.debug("auditor issues=%s", issues)
return {"issues": issues, "timestamp": datetime.utcnow().isoformat()}


def strategist(params, input_payload):
logger.debug("strategist start")
plan = ["audit", "generate_content", "qa"]
return {"plan": plan, "timestamp": datetime.utcnow().isoformat()}


def content_generator(params, input_payload):
logger.debug("content_generator start")
summary = "Generated summary for task"
return {"summary": summary, "timestamp": datetime.utcnow().isoformat()}


def qa(params, input_payload):
logger.debug("qa start")
ok = True
return {"ok": ok, "notes": [], "timestamp": datetime.utcnow().isoformat()}
102 changes: 102 additions & 0 deletions harness/agents/repo_agents.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@

"""Repository-specific agent implementations for the harness.

Each function receives (params, input_payload) and returns JSON-serializable findings.
"""
import logging
import os
import json
from datetime import datetime

logger = logging.getLogger(__name__)


def _read_file(path):
try:
with open(path, "r", encoding="utf-8") as f:
return f.read()
except Exception:
logger.debug("Failed to read file %s", path, exc_info=True)
return None


def repo_inspector(params, input_payload):
logger.debug("repo_inspector start")
root = os.getcwd()
files = sorted([f for f in os.listdir(root) if os.path.isfile(f)])
dirs = sorted([d for d in os.listdir(root) if os.path.isdir(d)])
important = {"README.md": os.path.exists("README.md"), "CHANGELOG.md": os.path.exists("CHANGELOG.md"), "LICENSE": os.path.exists("LICENSE")}
return {"files": files, "dirs": dirs, "important": important, "timestamp": datetime.utcnow().isoformat()}


def dependency_checker(params, input_payload):
logger.debug("dependency_checker start")
roots = {"requirements.txt": os.path.exists("requirements.txt"), "package.json": os.path.exists("package.json"), "pyproject.toml": os.path.exists("pyproject.toml")}
details = {}
if roots.get("requirements.txt"):
content = _read_file("requirements.txt") or ""
details["requirements.txt"] = content[:200]
if roots.get("package.json"):
pj_text = _read_file("package.json") or "{}"
try:
details["package.json"] = json.loads(pj_text)
except Exception:
logger.debug("package.json parse failed", exc_info=True)
details["package.json"] = {}
return {"manifests": roots, "details_preview": details, "timestamp": datetime.utcnow().isoformat()}


def changelog_auditor(params, input_payload):
logger.debug("changelog_auditor start")
text = _read_file("CHANGELOG.md")
if not text:
return {"found": False, "issues": ["CHANGELOG.md missing"], "timestamp": datetime.utcnow().isoformat()}
recent_lines = text.strip().splitlines()[:20]
return {"found": True, "recent_preview": "\n".join(recent_lines), "timestamp": datetime.utcnow().isoformat()}


def docs_checker(params, input_payload):
logger.debug("docs_checker start")
docs = {}
check_files = ["docs/quickstart.md", "docs/experimental-dependency.md", "README.md", "privacy.html"]
for p in check_files:
docs[p] = os.path.exists(p)
todos = []
if os.path.isdir("docs"):
for root, _, files in os.walk("docs"):
for f in files:
path = os.path.join(root, f)
content = _read_file(path) or ""
if "TODO" in content:
todos.append({"file": path, "snippet": content[:120]})
return {"checks": docs, "todos": todos, "timestamp": datetime.utcnow().isoformat()}


def release_planner(params, input_payload):
logger.debug("release_planner start")
history = input_payload.get("history", [])
issues = []
for h in history:
r = h.get("result", {})
if isinstance(r, dict) and r.get("issues"):
issues.extend(r.get("issues"))
plan = ["bump-version", "update-changelog", "run-tests", "publish"]
if issues:
plan.insert(0, "address-issues")
return {"plan": plan, "issues": issues, "timestamp": datetime.utcnow().isoformat()}


def local_tester(params, input_payload):
logger.debug("local_tester start")
hints = {"has_pytest": False, "has_test_script": False}
if os.path.exists("requirements.txt"):
req = _read_file("requirements.txt") or ""
hints["has_pytest"] = "pytest" in req
if os.path.exists("package.json"):
try:
pj = json.loads(_read_file("package.json") or "{}")
scripts = pj.get("scripts", {})
hints["has_test_script"] = "test" in scripts
except Exception:
logger.debug("package.json parse failed in local_tester", exc_info=True)
return {"hints": hints, "timestamp": datetime.utcnow().isoformat()}
14 changes: 14 additions & 0 deletions harness/config/agents.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"agents": [
{"name": "orchestrator", "module": "harness.agents.example_agents", "callable": "orchestrator", "params": {}},
{"name": "repo_inspector", "module": "harness.agents.repo_agents", "callable": "repo_inspector", "params": {}},
{"name": "dependency_checker", "module": "harness.agents.repo_agents", "callable": "dependency_checker", "params": {}},
{"name": "changelog_auditor", "module": "harness.agents.repo_agents", "callable": "changelog_auditor", "params": {}},
{"name": "docs_checker", "module": "harness.agents.repo_agents", "callable": "docs_checker", "params": {}},
{"name": "release_planner", "module": "harness.agents.repo_agents", "callable": "release_planner", "params": {}},
{"name": "local_tester", "module": "harness.agents.repo_agents", "callable": "local_tester", "params": {}},
{"name": "strategist", "module": "harness.agents.example_agents", "callable": "strategist", "params": {}},
{"name": "content", "module": "harness.agents.example_agents", "callable": "content_generator", "params": {}},
{"name": "qa", "module": "harness.agents.example_agents", "callable": "qa", "params": {}}
]
}
23 changes: 23 additions & 0 deletions harness/config/harness_defaults.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"logging": {
"log_file": "harness_rot.log",
"log_level": "INFO",
"log_max_bytes": 0,
"log_backup_count": 0
},
"agents_config": "harness/config/agents.json",
"agents": {
"agents": [
{"name": "orchestrator", "module": "harness.agents.example_agents", "callable": "orchestrator", "params": {}},
{"name": "repo_inspector", "module": "harness.agents.repo_agents", "callable": "repo_inspector", "params": {}},
{"name": "dependency_checker", "module": "harness.agents.repo_agents", "callable": "dependency_checker", "params": {}},
{"name": "changelog_auditor", "module": "harness.agents.repo_agents", "callable": "changelog_auditor", "params": {}},
{"name": "docs_checker", "module": "harness.agents.repo_agents", "callable": "docs_checker", "params": {}},
{"name": "release_planner", "module": "harness.agents.repo_agents", "callable": "release_planner", "params": {}},
{"name": "local_tester", "module": "harness.agents.repo_agents", "callable": "local_tester", "params": {}},
{"name": "strategist", "module": "harness.agents.example_agents", "callable": "strategist", "params": {}},
{"name": "content", "module": "harness.agents.example_agents", "callable": "content_generator", "params": {}},
{"name": "qa", "module": "harness.agents.example_agents", "callable": "qa", "params": {}}
]
}
}
115 changes: 115 additions & 0 deletions harness/harness.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
#!/usr/bin/env python3
"""Simple harness runner: loads a JSON config and executes agent callables.

Usage: python3 harness/harness.py --config harness/config/agents.json
"""
import argparse
import importlib
import json
import os
import sys
import traceback
from datetime import datetime

# Ensure the project root is on sys.path so `import harness.agents...` works
ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
if ROOT not in sys.path:
sys.path.insert(0, ROOT)

# logging will be initialized after parsing CLI args
from harness.logging_setup import init_logging
logger = None


def load_config(path):
with open(path, "r", encoding="utf-8") as f:
return json.load(f)


def call_agent(agent_conf, payload):
module_name = agent_conf.get("module")
callable_name = agent_conf.get("callable")
params = agent_conf.get("params", {})
module = importlib.import_module(module_name)
func = getattr(module, callable_name)
start = datetime.utcnow().isoformat() + "Z"
logger.info("Calling agent %s.%s", module_name, callable_name)
try:
result = func(params, payload)
end = datetime.utcnow().isoformat() + "Z"
logger.info("Agent %s finished", agent_conf.get("name"))
return {"agent": agent_conf.get("name"), "start": start, "end": end, "result": result}
except Exception as e:
end = datetime.utcnow().isoformat() + "Z"
tb = traceback.format_exc()
logger.exception("Agent %s raised exception", agent_conf.get("name"))
return {
"agent": agent_conf.get("name"),
"start": start,
"end": end,
"error": {"message": str(e), "type": e.__class__.__name__, "traceback": tb},
}


def main():
p = argparse.ArgumentParser()
p.add_argument("--config", required=False, help="path to agents config JSON")
p.add_argument("--log-file", required=False, help="path to log file (append)")
p.add_argument("--log-level", required=False, default=None, help="logging level, e.g. DEBUG, INFO, WARNING")
p.add_argument("--log-max-bytes", required=False, type=int, default=None, help="max bytes for rotating log (0 disables rotation)")
p.add_argument("--log-backup-count", required=False, type=int, default=None, help="number of rotated backup files to keep")
p.add_argument("--defaults", required=False, help="path to defaults JSON (overrides harness/config/harness_defaults.json)")
args = p.parse_args()

# load defaults from file (bundle defaults live in harness/config)
defaults_path = args.defaults or os.path.join(os.path.dirname(__file__), "config", "harness_defaults.json")
defaults = {}
if os.path.exists(defaults_path):
try:
defaults = load_config(defaults_path)
except Exception:
# fall back to empty defaults
defaults = {}

# resolve config path: CLI > defaults > error
config_path = args.config or defaults.get("agents_config")
if not config_path:
raise SystemExit("No agents config provided (use --config or set agents_config in defaults)")

# resolve logging options: CLI values override defaults
resolved_log_file = args.log_file if args.log_file is not None else defaults.get("logging", {}).get("log_file")
resolved_log_level = (args.log_level or defaults.get("logging", {}).get("log_level", "INFO")).upper()
resolved_max_bytes = args.log_max_bytes if args.log_max_bytes not in (None, 0) else defaults.get("logging", {}).get("log_max_bytes", 0)
resolved_backup_count = args.log_backup_count if args.log_backup_count not in (None, 0) else defaults.get("logging", {}).get("log_backup_count", 0)

# initialize logging according to resolved values
try:
lvl = getattr(__import__("logging"), resolved_log_level)
except Exception:
lvl = getattr(__import__("logging"), "INFO")
global logger
logger = init_logging(level=lvl, logfile=resolved_log_file, max_bytes=resolved_max_bytes or 0, backup_count=resolved_backup_count or 0)

if config_path:
conf = load_config(config_path)
else:
# support embedding agents directly in defaults under key 'agents'
conf = defaults.get("agents") or {}
agents = conf.get("agents", [])

# initial payload
payload = {"id": "task-1", "payload": {"repo": "./"}, "history": []}

for a in agents:
print(f"Running agent: {a.get('name')}")
out = call_agent(a, payload)
payload["history"].append(out)
# optionally pass the last result as the new payload
payload["payload"][a.get("name")] = out.get("result")

print("\nRun complete. History:")
print(json.dumps(payload["history"], indent=2))


if __name__ == "__main__":
main()
31 changes: 31 additions & 0 deletions harness/logging_setup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import logging
import sys
from logging.handlers import RotatingFileHandler


def init_logging(level=logging.INFO, logfile: str | None = None, max_bytes: int = 0, backup_count: int = 0):
fmt = "%(asctime)s %(levelname)s [%(name)s] %(message)s"
logger = logging.getLogger("harness")
logger.setLevel(level)

# clear existing handlers
for h in list(logger.handlers):
logger.removeHandler(h)

# console handler
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(level)
ch.setFormatter(logging.Formatter(fmt))
logger.addHandler(ch)

# optional rotating file handler
if logfile:
if max_bytes and max_bytes > 0:
fh = RotatingFileHandler(logfile, mode="a", maxBytes=max_bytes, backupCount=backup_count, encoding="utf-8")
else:
fh = RotatingFileHandler(logfile, mode="a", maxBytes=0, backupCount=0, encoding="utf-8")
fh.setLevel(level)
fh.setFormatter(logging.Formatter(fmt))
logger.addHandler(fh)

return logger